看懂前端腳手架你需要這篇WEBPACK
本文轉載自網絡。轉載編輯過程中,可能有遺漏或錯誤,請以原文為準。
原文作者:二口南洋
Webpack 是當下最熱門的前端資源模塊化管理和打包工具。它可以將許多鬆散的模塊按照依賴和規則打包成符合生產環境部署的前端資源。還可以將按需加載的模塊進行代碼分隔,等到實際需要的時候再異步加載。通過loader
的轉換,任何形式的資源都可以視作模塊,比如 CommonJs 模塊、 AMD 模塊、 ES6 模塊、CSS、圖片、 JSON、Coffeescript、 LESS
等。
Webpack 官網
Webpack 中文指南
分割WEBPACK配置文件的多種方法
一
將你的配置信息寫到多個分散的文件中去,然後在執行webpack的時候利用--config參數指定要加載的配置文件,配置文件利用module
imports
導出。你可以在webpack/react-starter看到是使用這種發方法的。
//配置文件
|-- webpack-dev-server.config.js
|-- webpack-hot-dev-server.config.js
|-- webpack-production.config.js
|-- webpack.config.js
//npm 命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-server": "webpack-dev-server --config webpack-dev-server.config.js --progress --colors --port 2992 --inline",
"hot-dev-server": "webpack-dev-server --config webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline",
"build": "webpack --config webpack-production.config.js --progress --profile --colors"
},
二
調用第三方的webpack工具,使用其集成的api,方便進行webpack配置。HenrikJoreteg/hjs-webpack 這個repo
就是這麼做的。
var getConfig = require('hjs-webpack')
module.exports = getConfig({
// entry point for the app
in: 'src/app.js',
// Name or full path of output directory
// commonly named `www` or `public`. This
// is where your fully static site should
// end up for simple deployment.
out: 'public',
// This will destroy and re-create your
// `out` folder before building so you always
// get a fresh folder. Usually you want this
// but since it's destructive we make it
// false by default
clearBeforeBuild: true
})
三 Scalable webpack configurations
ones that can be reused and combined with other partial configurations
在單個配置文件中維護配置,但是區分好條件分支。調用不同的npm
命令時候設置不同的環境變量,然後在分支中匹配,返回我們需要的配置文件。
這樣做的好處可以在一個文件中管理不同npm
操作的邏輯,並且可以共用相同的配置。webpack-merge這個模塊可以起到合並配置的作用。
const parts = require('./webpack-config/parts');
switch(process.env.npm_lifecycle_event) {
case 'build':
config = merge(common,
parts.clean(PATHS.build),
parts.setupSourceMapForBuild(),
parts.setupCSS(PATHS.app),
parts.extractBundle({
name: 'vendor',
entries: ['react', 'vue', 'vuex']
}),
parts.setFreeVariable('process.env.NODE_ENV', 'production'),
parts.minify()
);
break;
default:
config = merge(common,
parts.setupSourceMapForDev(),
parts.devServer(),
parts.setupCSS(PATHS.app));
}
// minify example
exports.minify = function () {
return {
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: true
},
comments: false,
beautify: false
})
]
}
}
開發環境下的自動刷新
webpack-dev-server
webpack-dev-server
在webpack的watch基礎上開啟服務器。webpack-dev-server
是運行在內存中的開發服務器,支持高級webpack特性hot module replacement
。這對於react vue
這種組件化開發是很方便的。
使用webpack-dev-server
命令開啟服務器,配合HMR
及可以實現代碼更改瀏覽器局部刷新的能力。
hot module replacement
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running without a page reload.
當應用在運行期間hmr機製能夠修改、添加、或者移除相應的模塊,而不使整個頁麵刷新。
hmr
機製適用於單頁應用。
要實現hmr
機製,需要配合webpack-dev-server
服務器,這個服務器本身就實現了監察watch
文件改動的能力,再開啟HMR
選項,就添加了watch
模塊變化的能力。這是HMR
機製能生效的基礎。
從webpack編譯器角度
每次修改一個模塊的時候,webpack會生成兩部分,一個是manifest.json
,另一部分是關於這次模塊更新編譯完成的chunks
。manifest.json
中存著的是chunk
更改前後的hash值。
從編譯器webpack的角度來講提供了hmr
的原材料。供後續使用。
從模塊的角度
模塊發生變化時,webpack會生成之前講過的兩部分基礎文件,但是何時將變化後的模塊應用到app中去?這裏就需要在應用代碼中編寫handler去接受到模塊變化信息。但是不能在所有模塊中編寫handler吧?這裏就用到了消息冒泡機製。

如圖A.js、C.js沒有相關hmr代碼,B.js有相關hmr代碼,如果c模塊發生了變化,c模塊沒有hmr,那麼就會冒泡到a、b模塊。b模塊捕捉到了消息,hmr運行時會相應的執行一些操作,而a.js捕捉不到信息,會冒泡到entry.js,而一旦有消息冒泡的入口塊,這就代表本次hmr失敗了,hmr會降級進行整個頁麵的reload。
從HMR運行時的角度
HMR運行時是一些相關的操作api,運行時支持兩個方法: check、apply
。
check發起 HTTP 請求去獲取更新的 manifest
,以及一些更新過後的chunk。

環境變量的設置
var env = {
'process.env.NODE_ENV': '"production"'
}
new webpack.DefinePlugin(env)
注意這裏單引號間多了個雙引號 why?
以及webpack.DefinePlugin
插件的原理?
開發的時候會想寫很多隻在開發環境出現的代碼,比如接口mock
等,在build
命令後這些代碼不會存在。
這對框架或者插件、組件的開發是很有幫助的。vue,react
等都會這麼做。可以在這些框架的dev
模式提供很多有用的提示信息。
打包文件分割
為何要進行打包文件分割?
對於一個單頁應用項目來說,有分為業務代碼和第三方代碼,業務代碼會頻繁改動,而第三方代碼一般來講變動的次數較少,如果每次修改業務代碼都需要用戶將整個js文件都重新下載一遍,對於加載性能來講是不可取的,所以一般而言我們會將代碼分為業務代碼和第三方代碼分別進行打包,雖然多了一個請求的文件,增加了一些網絡開銷,但是相比於瀏覽器能將文件進行緩存而言,這些開銷是微不足道的。
我們在entry中定義了app入口,相應的業務邏輯都封裝在這個入口文件裏,如果我們想要第三方代碼獨立出來,就要再增加一個入口,我們習慣使用vendor
這個命名。
// app.js
require('vue');
require('vuex');
// webpack.config.js
entry: {
app: 'app/app.js',
vendor: ['vue', 'vuex'],
},
vendor
入口的傳參是以一個數組的形式傳遞的,這是一種非常方便的注入多個依賴的方式,並且能把多個依賴一起打包到一個chunk中。而且不用手動的創建真實存在的入口文件。
這相當於:
// vendor.js
require('vue');
require('vuex');
// app.js
require('vue');
require('vuex');
// webpack.config.js
entry: {
app: 'app/app.js',
vendor: 'app/vendor.js',
},
但是這樣做隻是聲明了一個vendor
入口而已,對於app這個入口來說,打包完成的文件還是會有vue
和vuex
依賴,而新增的入口vendor
打包完成的文件也有了vue
和vuex
兩個依賴。模塊依賴關係如下圖所示。

這裏的A可以代表
vue
依賴,最後生成的打包文件是兩個平行關係的文件,且都包含vue
的依賴。此時需要引入
CommonsChunkPlugin
插件This is a pretty complex plugin. It fundamentally allows us to extract all the common modules from different bundles and add them to the common bundle. If a common bundle does not exist, then it creates a new one.
這是個相當複雜的插件,他的基礎功能是允許我們從不同的打包文件中抽離出相同的模塊,然後將這些模塊加到公共打包文件中。如果公共打包文件不存在,則新增一個。同時這個插件也會將運行時(runtime)轉移到公共chunk打包文件中。

plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest']
})
]
這裏的name可以選擇已經存在的塊,這裏就選擇了vendor
塊,因為我們本來就是將vendor
塊當做管理第三方代碼的入口的。
而names傳入一個數組,數組裏包含兩個trunk name,表示CommonsChunkPlugin
插件會執行兩次這個方法,第一次將公共的第三方代碼抽離移到vendor
的塊中,這個過程之前也講過會將運行時runtime也轉移到vendor
塊中,第二次執行則是將運行時runtime抽離出來轉移到manifest
塊中。這步操作解決了緩存問題。
這樣處理,最後會生成3個打包文件chunk
,app.js
是業務代碼,vendor
則是公共的第三方代碼,manifest.js
則是運行時。
CHUNK TYPE 塊的類型大揭秘
webpack1.0官網介紹中的chunk類型讀起來及其拗口chunk type, 所以我這裏解讀一下。
chunk是webpack中最基本的概念之一,且chunk
常常會和entry
弄混淆。在「打包文件分割部分」我們定義了兩個入口entry point -- app
和vendor
,而通過一些配置,webpack會生成最後的一些打包文件,在這個例子中最後生成的文件有``app.js 、 vendor.js 、 manifest.js。這些文件便被稱為塊chunk。
entry & chunk 可以簡單的理解為一個入口、一個出口
在官方1.0文檔中webpack的chunk類型分為三種:
- entry chunk 入口塊
- normal chunk 普通塊
- initial chunk 初始塊
- entry chunk 入口塊
entry chunk 入口塊不能由字麵意思理解為由入口文件編譯得到的文件,由官網介紹
An entry chunk contains the runtime plus a bunch of modules
可以理解為包含runtime運行時的塊可以稱為entry chunk
,一旦原本存在運行時(runtime)的entry chunk
失去了運行時,這個塊就會轉而變成initial chunk
。
normal chunk 普通塊
A normal chunk contains no runtime. It only contains a bunch of modules.
普通塊不包含運行時runtime,隻包含一係列模塊。但是在應用運行時,普通塊可以動態的進行加載。通常會以jsonp
的包裝方式進行加載。而code splitting
主要使用的就是普通塊。
initial chunk 初始塊
An initial chunk is a normal chunk.
官方對initial chunk
的定義非常簡單,初始塊就是普通塊,跟普通塊相同的是同樣不包含運行時runtime,不同的是初始塊是計算在初始加載過程時間內的。在介紹入口塊entry chunk
的時候也介紹過,一旦入口塊失去了運行時,就會變成初始塊。這個轉變經常由CommonsChunkPlugin
插件實現。
例子解釋
還是拿「打包文件分割」的代碼做例子,
// app.js
require('vue');
require('vuex');
// webpack.config.js
entry: {
app: 'app/app.js',
vendor: ['vue', 'vuex'],
},
沒有使用CommonsChunkPlugin
插件之前,兩個entry
分別被打包成兩個chunk
,而這兩個chunk
每個都包含了運行時,此時被稱為entry chunk
入口塊。
而一旦使用了CommonsChunkPlugin
插件,運行時runtime最終被轉移到了manifest.js
文件,此時最終打包生成的三個chunkapp.js 、 vendor.js 、 manifest.js,app.js、vendor.js
失去了runtime就由入口塊變成初始塊。
code splitting
前文有講到將依賴分割開來有助於瀏覽器緩存,提高用戶加載速度,但是當業務複雜度增加,代碼量大始終是一個問題。這時候就需要normal chunk
普通塊的動態加載能力了。
It allows you to split your code into various bundles which you can then load on demand — like when a user navigates to a matching route, or on an event from the user.
code splitting 允許我們將代碼分割到可以按需加載的不同的打包文件中,當用戶導航到對應的路由上時,或者是用戶觸發一個事件時,異步加載相應的代碼。
我們需要在業務邏輯中手動添加一些分割點,標明此處事件邏輯之後進行代碼塊的異步加載。
// test
window.addEventListener('click', function () {
require.ensure(['vue', 'vuex'], function (require) {
})
})
這段代碼表明當用戶點擊時,異步請求一個js文件,這個文件中包含該有vue vuex
的依賴。

打包後會根據手動分割點的信息生成一個打包文件,就是圖中第一行0開頭的文件。這個文件也就是異步加載的文件。
下麵是之前的一個vue
項目,采用code splitting
將幾個路由抽離出來異步加載之後,文件由212kb減少到了137kb,同樣樣式文件也由58kb減少到了7kb。對於首屏渲染來說,性能是會增加不少的。


參考
- https://itsclem.com/?p=1036
- https://webpack.js.org/concepts/
- https://blog.oyyd.net/post/how_does_react_hot_loader_works
- https://webpack.github.io/docs/hot-module-replacement-with-webpack.html
- https://survivejs.com/webpack/introduction/
- https://webpack.js.org/
原文鏈接
最後更新:2017-11-03 10:34:37