Webpack 或 Browserify & Gulp:哪個更好?
已發表: 2022-03-11隨著 Web 應用程序變得越來越複雜,使您的 Web 應用程序可擴展變得至關重要。 過去編寫臨時 JavaScript 和 jQuery 就足夠了,而如今構建 Web 應用程序需要更高程度的紀律和正式的軟件開發實踐,例如:
- 單元測試以確保對代碼的修改不會破壞現有功能
- Linting 以確保一致的編碼風格沒有錯誤
- 不同於開發版本的生產版本
Web 還提供了一些自己獨特的開發挑戰。 例如,由於網頁發出大量異步請求,您的 Web 應用程序的性能可能會因必須請求數百個 JS 和 CSS 文件而顯著降低,每個文件都有自己的微小開銷(標頭、握手等)。 這個特定問題通常可以通過將文件捆綁在一起來解決,因此您只需要一個捆綁的 JS 和 CSS 文件,而不是數百個單獨的文件。
使用編譯為原生 JS 和 CSS 的語言預處理器(如 SASS 和 JSX)以及 JS 轉譯器(如 Babel)在保持 ES5 兼容性的同時從 ES6 代碼中受益也是很常見的。
這相當於大量與編寫 Web 應用程序本身的邏輯無關的任務。 這就是任務運行程序的用武之地。任務運行程序的目的是自動執行所有這些任務,以便您可以在專注於編寫應用程序的同時從增強的開發環境中受益。 配置任務運行程序後,您需要做的就是在終端中調用單個命令。
我將使用 Gulp 作為任務運行器,因為它對開發人員非常友好、易於學習且易於理解。
Gulp 簡介
Gulp 的 API 包含四個函數:
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
例如,這裡是一個使用這四個函數中的三個的示例任務:
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });
當執行my-first-task
時,所有匹配 glob 模式/public/js/**/*.js
的文件都會被縮小,然後轉移到build
文件夾。
其美妙之處在於.pipe()
鏈接。 您獲取一組輸入文件,通過一系列轉換對它們進行管道傳輸,然後返回輸出文件。 為了使事情更方便,實際的管道轉換,例如minify()
,通常由 NPM 庫完成。 因此,在實踐中,除了重命名管道中的文件之外,您需要編寫自己的轉換是非常罕見的。
理解 Gulp 的下一步是理解任務依賴的數組。
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
這裡, my-second-task
只在lint
和bundle
任務完成後運行回調函數。 這允許關注點分離:您創建一系列具有單一職責的小任務,例如將LESS
轉換為CSS
,並創建一種主任務,通過任務依賴關係數組簡單地調用所有其他任務。
最後,我們有gulp.watch
,它監視 glob 文件模式的變化,當檢測到變化時,運行一系列任務。
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })
在上面的示例中,對匹配/public/js/**/*.js
的文件的任何更改都會觸發lint
並reload
任務。 gulp.watch
的一個常見用途是在瀏覽器中觸發實時重新加載,該功能對開發非常有用,一旦您體驗過它,您將無法離開它。
就這樣,你了解了你真正需要知道的關於gulp
的一切。
Webpack 適合哪裡?
使用 CommonJS 模式時,捆綁 JavaScript 文件並不像連接它們那麼簡單。 相反,您有一個入口點(通常稱為index.js
或app.js
),文件頂部有一系列require
或import
語句:
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');
ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';
依賴項必須在app.js
中的其餘代碼之前解決,並且這些依賴項本身可能還有進一步的依賴項需要解決。 此外,您可能在應用程序的多個位置require
相同的依賴項,但您只想解決該依賴項一次。 可以想像,一旦你有一個深幾級的依賴樹,捆綁你的 JavaScript 的過程就會變得相當複雜。 這就是 Browserify 和 Webpack 等打包工具的用武之地。
為什麼開發人員使用 Webpack 而不是 Gulp?
Webpack 是一個打包器,而 Gulp 是一個任務運行器,因此您希望看到這兩個工具經常一起使用。 相反,使用 Webpack代替Gulp 的趨勢越來越明顯,尤其是在 React 社區中。 為什麼是這樣?
簡而言之,Webpack 是一個非常強大的工具,它已經可以執行您通過任務運行器完成的絕大多數任務。 例如,Webpack 已經為你的包提供了縮小和源圖的選項。 此外,Webpack 可以通過名為webpack-dev-server
的自定義服務器作為中間件運行,該服務器支持實時重載和熱重載(我們稍後會討論這些功能)。 通過使用加載器,您還可以將 ES6 添加到 ES5 轉換,以及 CSS 預處理器和後處理器。 這真的只是讓單元測試和 linting 成為 Webpack 無法獨立處理的主要任務。 鑑於我們已經將至少六個潛在的 gulp 任務減少到兩個,許多開發人員選擇直接使用 NPM 腳本,因為這避免了將 Gulp 添加到項目中的開銷(我們稍後也會討論) .
使用 Webpack 的主要缺點是配置相當困難,如果您試圖快速啟動並運行項目,這將沒有吸引力。
我們的 3 個任務運行器設置
我將建立一個具有三種不同任務運行器設置的項目。 每個設置都將執行以下任務:
- 設置開發服務器並在監視的文件更改時實時重新加載
- 以可擴展的方式捆綁我們的 JS 和 CSS 文件(包括 ES6 到 ES5 的轉換、SASS 到 CSS 的轉換和源映射)以監視文件更改
- 作為獨立任務或在監視模式下運行單元測試
- 作為獨立任務或在監視模式下運行 linting
- 提供通過終端中的單個命令執行上述所有操作的能力
- 有另一個命令用於創建具有縮小和其他優化的生產包
我們的三個設置將是:
- Gulp + Browserify
- Gulp + Webpack
- Webpack + NPM 腳本
該應用程序將使用 React 作為前端。 最初,我想使用一種與框架無關的方法,但使用 React 實際上簡化了任務運行器的職責,因為只需要一個 HTML 文件,而且 React 與 CommonJS 模式配合得很好。
我們將介紹每種設置的優缺點,以便您可以就哪種設置最適合您的項目需求做出明智的決定。
我已經設置了一個包含三個分支的 Git 存儲庫,每種方法一個分支(鏈接)。 測試每個設置都很簡單:
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)
讓我們詳細檢查每個分支中的代碼……
通用代碼
文件夾結構
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js
索引.html
一個簡單的 HTML 文件。 React 應用程序被加載到<div></div>
中,我們只使用一個捆綁的 JS 和 CSS 文件。 事實上,在我們的 Webpack 開發設置中,我們甚至不需要bundle.css
。
index.js
這充當了我們應用程序的 JS 入口點。 本質上,我們只是將 React Router 加載到前面提到的 id app
的div
中。
路由.js
該文件定義了我們的路線。 url /
、 /about
和/contact
分別映射到HomePage
、 AboutPage
和ContactPage
組件。
index.test.js
這是一系列測試原生 JavaScript 行為的單元測試。 在真正的生產質量應用程序中,您將為每個 React 組件(至少是那些操縱狀態的組件)編寫一個單元測試,以測試特定於 React 的行為。 但是,就本文而言,只需擁有一個可以在監視模式下運行的功能單元測試就足夠了。
組件/App.js
這可以被認為是我們所有頁面視圖的容器。 每個頁面都包含一個<Header/>
組件以及this.props.children
,它評估頁面視圖本身(例如 / ContactPage
如果在瀏覽器中的/contact
處)。
組件/主頁/HomePage.js
這是我們家的景色。 我選擇使用react-bootstrap
,因為 bootstrap 的網格系統非常適合創建響應式頁面。 通過正確使用引導程序,您必須為較小的視口編寫的媒體查詢數量會大大減少。
其餘組件( Header
、 AboutPage
、 ContactPage
)的結構類似( react-bootstrap
標記,無狀態操作)。
現在讓我們更多地談談樣式。
CSS 樣式方法
我對 React 組件樣式的首選方法是每個組件有一個樣式表,其樣式的範圍僅限於適用於該特定組件。 您會注意到,在我的每個 React 組件中,頂級div
都有一個與組件名稱匹配的類名。 因此,例如, HomePage.js
將其標記包裝為:
<div className="HomePage"> ... </div>
還有一個關聯的HomePage.scss
文件,其結構如下:
@import '../../styles/variables'; .HomePage { // Content here }
為什麼這種方法如此有用? 它產生了高度模塊化的 CSS,在很大程度上消除了不需要的級聯行為的問題。
假設我們有兩個 React 組件, Component1
和Component2
。 在這兩種情況下,我們都想覆蓋h2
字體大小。
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }
Component1
和Component2
的h2
字體大小是獨立的,無論組件是相鄰的,還是一個組件嵌套在另一個組件中。 理想情況下,這意味著組件的樣式是完全獨立的,這意味著無論將組件放置在標記中的哪個位置,它看起來都完全相同。 實際上,這並不總是那麼簡單,但這無疑是朝著正確方向邁出的一大步。
除了每個組件的樣式之外,我還希望有一個包含全局樣式表global.scss
的styles
文件夾,以及處理特定職責的 SASS 部分(在本例中,分別為字體和變量的_fonts.scss
和_variables.scss
)。 全局樣式表允許我們定義整個應用程序的一般外觀和感覺,而輔助部分可以根據需要由每個組件的樣式表導入。
既然已經深入探討了每個分支中的通用代碼,讓我們將注意力轉移到第一個任務運行器/捆綁器方法上。
Gulp + Browserify 設置
gulpfile.js
這產生了一個驚人的大 gulpfile,包含 22 個導入和 150 行代碼。 因此,為簡潔起見,我將只詳細回顧js
、 css
、 server
、 watch
和default
任務。
JS 捆綁包
// Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }
由於多種原因,這種方法相當難看。 一方面,任務被分成三個獨立的部分。 首先,您創建 Browserify 捆綁對象b
,傳入一些選項並定義一些事件處理程序。 然後你有 Gulp 任務本身,它必須傳遞一個命名函數作為它的回調而不是內聯它(因為b.on('update')
使用相同的回調)。 這幾乎沒有 Gulp 任務的優雅,您只需傳入gulp.src
並管道一些更改。
另一個問題是這迫使我們在瀏覽器中重新加載html
、 css
和js
的方法不同。 查看我們的 Gulp watch
任務:
gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
更改 HTML 文件時,將重新運行html
任務。
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
如果NODE_ENV
不是production
,最後一個管道調用livereload()
,這會觸發瀏覽器中的刷新。
CSS watch 使用相同的邏輯。 當一個 CSS 文件發生變化時, css
任務會重新運行, css
任務中的最後一個管道會觸發livereload()
並刷新瀏覽器。
但是, js
手錶根本不調用js
任務。 相反,Browserify 的事件處理程序b.on('update', bundle)
使用完全不同的方法(即熱模塊替換)處理重新加載。 這種方法的不一致很煩人,但不幸的是,為了進行增量構建,這是必要的。 如果我們只是在bundle
函數的末尾天真地調用livereload()
,這將在任何單個 JS 文件更改時重新構建整個JS 包。 這種方法顯然無法擴展。 您擁有的 JS 文件越多,每次重新打包所需的時間就越長。 突然間,您的 500 毫秒重新捆綁開始需要 30 秒,這確實抑制了敏捷開發。
CSS 捆綁包
gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
這裡的第一個問題是繁瑣的供應商 CSS 包含。 每當將新的供應商 CSS 文件添加到項目中時,我們必須記住更改 gulpfile 以將元素添加到gulp.src
數組,而不是將導入添加到實際源代碼中的相關位置。
另一個主要問題是每個管道中復雜的邏輯。 我不得不添加一個名為gulp-cond
的 NPM 庫,只是為了在我的管道中設置條件邏輯,最終結果不太可讀(到處都是三方括號!)。
服務器任務
gulp.task('server', () => { nodemon({ script: 'server.js' }); });
這個任務非常簡單。 它本質上是命令行調用nodemon server.js
的包裝器,它在節點環境中運行server.js
。 使用nodemon
代替node
以便對文件的任何更改都會導致它重新啟動。 默認情況下, nodemon
會在任何JS 文件更改時重新啟動正在運行的進程,這就是為什麼包含nodemon.json
文件以限制其範圍很重要的原因:
{ "watch": "server.js" }
讓我們回顧一下我們的服務器代碼。
服務器.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();
這會根據節點環境設置服務器的基本目錄和端口,並創建 express 的實例。
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));
這添加了connect-livereload
中間件(我們的實時重新加載設置所必需的)和靜態中間件(處理我們的靜態資產所必需的)。
app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });
這只是一個簡單的 API 路由。 如果您在瀏覽器中導航到localhost:3000/api/sample-route
,您將看到:
{ website: "Toptal", blogPost: true }
在真正的後端,您將擁有一個專用於 API 路由的整個文件夾、用於建立數據庫連接的單獨文件等。 包含此示例路線只是為了表明我們可以輕鬆地在我們設置的前端之上構建一個後端。

app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });
這是一條包羅萬象的路線,這意味著無論您在瀏覽器中輸入什麼 url,服務器都會返回我們唯一的index.html
頁面。 然後 React Router 負責在客戶端解析我們的路由。
app.listen(port, () => { open(`http://localhost:${port}`); });
這告訴我們的 express 實例監聽我們指定的端口,並在指定 URL 的新選項卡中打開瀏覽器。
到目前為止,我唯一不喜歡服務器設置的是:
app.use(require('connect-livereload')({port: 35729}));
鑑於我們已經在我們的 gulpfile 中使用gulp-livereload
,這使得必須使用 livereload 的兩個不同的地方。
現在,最後但並非最不重要的:
默認任務
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });
這是在終端中輸入gulp
時運行的任務。 一個奇怪的地方是需要使用runSequence
來讓任務按順序運行。 通常,一組任務是並行執行的,但這並不總是理想的行為。 例如,我們需要在html
之前運行clean
任務,以確保我們的目標文件夾在將文件移動到其中之前是空的。 當 gulp 4 發佈時,它將原生支持gulp.series
和gulp.parallel
方法,但現在我們必須在我們的設置中留下這個小怪癖。
除此之外,這實際上非常優雅。 我們的應用程序的整個創建和託管都是在一個命令中執行的,理解工作流的任何部分就像檢查運行序列中的單個任務一樣簡單。 此外,我們可以將整個序列分解成更小的塊,以便更精細地創建和託管應用程序。 例如,我們可以設置一個名為validate
的單獨任務來運行lint
和test
任務。 或者我們可以有一個運行server
和watch
的host
任務。 這種編排任務的能力非常強大,尤其是當您的應用程序擴展並需要更多自動化任務時。
開發與生產構建
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';
使用yargs
NPM 庫,我們可以為 Gulp 提供命令行標誌。 在這裡,如果--prod
被傳遞給終端中的gulp
,我將指示 gulpfile 將節點環境設置為生產環境。 然後,我們的PROD
變量用作區分 gulpfile 中的開發和生產行為的條件。 例如,我們傳遞給browserify
配置的選項之一是:
plugin: PROD ? [] : [hmr, watchify]
這告訴browserify
在生產模式下不要使用任何插件,並在其他環境中使用hmr
和watchify
插件。
這個PROD
條件非常有用,因為它使我們不必為生產和開發編寫單獨的 gulpfile,這最終會包含大量代碼重複。 相反,我們可以執行gulp --prod
之類的操作來在生產環境中運行默認任務,或者gulp html --prod
來僅在生產環境中運行html
任務。 另一方面,我們之前看到,在 Gulp 管道中亂扔諸如.pipe(cond(!PROD, livereload()))
之類的語句並不是最易讀的。 最後,您是想使用布爾變量方法還是設置兩個單獨的 gulpfile 是一個偏好問題。
現在讓我們看看當我們繼續使用 Gulp 作為我們的任務運行器但將 Browserify 替換為 Webpack 時會發生什麼。
Gulp + Webpack 設置
突然間,我們的 gulpfile 只有 99 行長,有 12 個導入,比我們之前的設置減少了很多! 如果我們檢查默認任務:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
現在我們完整的網絡應用程序設置只需要五個任務而不是九個,這是一個巨大的改進。
此外,我們消除了對livereload
的需求。 我們的watch
任務現在很簡單:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
這意味著我們的 gulp watcher 不會觸發任何類型的重新捆綁行為。 作為額外的獎勵,我們不再需要將index.html
從app
傳輸到dist
或build
。
讓我們的注意力回到任務縮減上,我們的html
、 css
、 js
和fonts
任務都被一個build
任務所取代:
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });
很簡單。 依次運行clean
和html
任務。 完成這些後,獲取我們的入口點,通過 Webpack 對其進行管道傳輸,傳入一個webpack.config.js
文件來配置它,並將生成的包發送到我們的baseDir
( dist
或build
,取決於節點環境)。
讓我們看一下 Webpack 配置文件:
webpack.config.js
這是一個非常大且令人生畏的配置文件,所以讓我們解釋一下在我們的module.exports
對像上設置的一些重要屬性。
devtool: PROD ? 'source-map' : 'eval-source-map',
這設置了 Webpack 將使用的源映射類型。 Webpack 不僅支持開箱即用的源映射,它實際上還支持多種源映射選項。 每個選項都提供了 sourcemap 細節與重建速度(重新捆綁更改所需的時間)之間的不同平衡。 這意味著我們可以在開發中使用“便宜”的 sourcemap 選項來實現快速重新加載,並在生產中使用更昂貴的 sourcemap 選項。
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]
這是我們的捆綁包入口點。 請注意,傳遞了一個數組,這意味著可以有多個入口點。 在這種情況下,我們有預期的入口點app/index.js
以及用作熱模塊重新加載設置的一部分的webpack-hot-middleware
入口點。
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },
這是編譯後的捆綁包將輸出的地方。 最令人困惑的選項是publicPath
。 它為您的捆綁包將在服務器上託管的位置設置基本 url。 因此,例如,如果您的publicPath
是/public/assets
,那麼包將出現在服務器上的/public/assets/bundle.js
下。
devServer: { contentBase: PROD ? './build' : './app' }
這告訴服務器您項目中的哪個文件夾用作服務器的根目錄。
如果您對 Webpack 如何將項目中創建的包映射到服務器上的包感到困惑,請記住以下內容:
-
path
+ 文件filename
:項目源代碼中包的確切位置 contentBase
(作為根,/
) +publicPath
: 服務器上包的位置
plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],
這些插件以某種方式增強了 Webpack 的功能。 例如, webpack.optimize.UglifyJsPlugin
負責縮小。
loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]
這些是裝載機。 本質上,它們預處理通過require()
語句加載的文件。 它們有點類似於 Gulp 管道,因為您可以將加載器鏈接在一起。
讓我們檢查一下我們的加載器對象之一:
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}
test
屬性告訴 Webpack 如果文件與提供的正則表達式模式匹配,則應用給定的加載器,在本例中/\.scss$/
。 loader
屬性對應於 loader 執行的操作。 在這裡,我們將style
、 css
、 resolve-url
和sass
加載器鏈接在一起,它們以相反的順序執行。
我必須承認,我沒有發現loader3!loader2!loader1
的語法非常優雅。 畢竟,你什麼時候需要從右到左閱讀程序中的任何內容? 儘管如此,loader 還是 webpack 的一個非常強大的特性。 事實上,我剛才提到的加載器允許我們將 SASS 文件直接導入到我們的 JavaScript 中! 例如,我們可以在入口點文件中導入我們的供應商和全局樣式表:
index.js
import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));
同樣,在我們的 Header 組件中,我們可以添加import './Header.scss'
來導入組件的關聯樣式表。 這也適用於我們所有的其他組件。
在我看來,這幾乎可以被認為是 JavaScript 開發領域的一次革命性變革。 無需擔心 CSS 捆綁、縮小或源映射,因為我們的加載器會為我們處理所有這些。 甚至熱模塊重新加載也適用於我們的 CSS 文件。 然後能夠在同一個文件中處理 JS 和 CSS 導入使得開發在概念上更簡單:更多的一致性,更少的上下文切換,更容易推理。
簡要總結一下這個特性是如何工作的:Webpack 將 CSS 內聯到我們的 JS 包中。 事實上,Webpack 也可以對圖像和字體執行此操作:
{test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}
如果圖像和字體小於 100 KB,URL 加載器會指示 Webpack 將我們的圖像和字體作為數據 url 內聯,否則將它們作為單獨的文件提供。 當然,我們也可以將截止大小配置為不同的值,例如 10 KB。
簡而言之,這就是 Webpack 配置。 我承認有相當多的設置,但使用它的好處簡直是驚人的。 儘管 Browserify 確實有插件和轉換,但它們在附加功能方面根本無法與 Webpack 加載器相比。
Webpack + NPM 腳本設置
在這個設置中,我們直接使用 npm 腳本而不是依賴 gulpfile 來自動化我們的任務。
包.json
"scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }
要運行開發和生產構建,請分別輸入npm start
和npm run start:prod
。
這肯定比我們的 gulpfile 更緊湊,因為我們已經將 99 到 150 行代碼減少到 19 個 NPM 腳本,或者如果我們排除生產腳本(其中大部分只是鏡像開發腳本並將節點環境設置為生產)則為 12 )。 缺點是這些命令與我們的 Gulp 任務對應物相比有些神秘,而且沒有那麼富有表現力。 例如,沒有辦法(至少我知道)讓單個 npm 腳本串行運行某些命令而並行運行其他命令。 它是一個或另一個。
但是,這種方法有一個巨大的優勢。 通過直接從命令行使用諸如mocha
之類的 NPM 庫,您無需為每個庫安裝等效的 Gulp 包裝器(在本例中為gulp-mocha
)。
而不是安裝 NPM
- gulp-eslint
- 大口喝摩卡
- gulp-nodemon
- 等等
我們安裝以下軟件包:
- 埃斯林特
- 摩卡
- 節點監視器
- 等等
引用 Cory House 的帖子, Why I Left Gulp and Grunt for NPM Scripts :
我是 Gulp 的忠實粉絲。 但在我的上一個項目中,我的 gulpfile 中有數百行和大約十幾個 Gulp 插件。 我正在努力使用 Gulp 集成 Webpack、Browsersync、熱重載、Mocha 等等。 為什麼? 好吧,一些插件沒有足夠的文檔來滿足我的用例。 一些插件隻公開了我需要的部分 API。 其中一個有一個奇怪的錯誤,它只能觀看少量文件。 輸出到命令行時另一個剝離顏色。
他指出了 Gulp 的三個核心問題:
- 依賴插件作者
- 令人沮喪的調試
- 雜亂無章的文檔
我傾向於同意所有這些。
1. 對插件作者的依賴
每當像eslint
這樣的庫被更新時,相關gulp-eslint
庫都需要相應的更新。 如果庫維護者失去興趣,則庫的 gulp 版本與本機版本不同步。 創建新庫時也是如此。 如果有人創建了一個庫xyz
並且它流行起來,那麼你突然需要一個相應的gulp-xyz
庫來在你的 gulp 任務中使用它。
從某種意義上說,這種方法無法擴展。 理想情況下,我們需要像 Gulp 這樣可以使用本機庫的方法。
2.令人沮喪的調試
儘管諸如gulp-plumber
之類的庫有助於極大地緩解這個問題,但gulp
中的錯誤報告確實不是很有幫助。 如果即使一個管道引發未處理的異常,您也會得到一個堆棧跟踪,該堆棧跟踪似乎與源代碼中導致問題的原因完全無關。 在某些情況下,這會使調試成為一場噩夢。 如果錯誤足夠神秘或具有誤導性,那麼在 Google 或 Stack Overflow 上進行的任何搜索都無法真正為您提供幫助。
3. 文檔脫節
我經常發現小型gulp
庫的文檔往往非常有限。 我懷疑這是因為作者通常使圖書館主要是為了他或她自己的使用。 此外,必須同時查看 Gulp 插件和本機庫本身的文檔是很常見的,這意味著需要大量的上下文切換和兩倍的閱讀量。
結論
在我看來,Webpack 比 Browserify 更可取,NPM 腳本比 Gulp 更可取,這對我來說似乎很清楚,儘管每個選項都有其優點和缺點。 Gulp 肯定比 NPM 腳本更具表現力和使用方便,但你要為所有額外的抽象付出代價。
並非每種組合都適合您的應用程序,但如果您想避免大量的開發依賴項和令人沮喪的調試體驗,帶有 NPM 腳本的 Webpack 是您的最佳選擇。 我希望您會發現這篇文章有助於為您的下一個項目選擇正確的工具。
- 保持控制:Webpack 和 React 指南,Pt。 1
- Gulp Under the Hood:構建基於流的任務自動化工具