帶你走進webpack世界,成為webpack頭號玩家。
最近朋友圈被《頭號玩家》刷爆了,斯皮爾伯格一個資深電影導演,把對過去經典的致敬,對未來的憧憬濃縮在這一部電影中,可以說讓觀眾燃了起來。
觀望整個前端開發,不斷的演化,發展迅速。前端開發從最開始切頁面, 前端自動化構建工具日新月異,從最初的Grunt,Gulp到現在前端項目可以說標配的webpack。
我們先來致敬經典:
1. 什麼是webpack?
可以看做一個模塊化打包機,分析項目結構,處理模塊化依賴,轉換成為瀏覽器可運行的代碼。
- 代碼轉換: TypeScript 編譯成 JavaScript、SCSS,LESS 編譯成 CSS.
- 文件優化:壓縮 JavaScript、CSS、HTML 代碼,壓縮合併圖片。
- 代碼分割:提取多個頁面的公共代碼、提取首屏不需要執行部分的代碼讓其非同步載入。
- 模塊合併:在採用模塊化的項目里會有很多個模塊和文件,需要構建功能把模塊分類合併成一個文件。
- 自動刷新:監聽本地源代碼的變化,自動重新構建、刷新瀏覽器。
構建把一系列前端代碼自動化去處理複雜的流程,解放生產力。
2. 進入webpack世界
初始化項目
npm install webpack webpack-cli -D
webpack4抽離出了webpack-cli,所以我們需要下載2個依賴。
Webpack 啟動後會從Entry里配置的Module開始遞歸解析 Entry 依賴的所有 Module。 每找到一個 Module, 就會根據配置的Loader去找出對應的轉換規則,對 Module 進行轉換後,再解析出當前 Module 依賴的 Module。 這些模塊會以 Entry 為單位進行分組,一個 Entry 和其所有依賴的 Module 被分到一個組也就是一個 Chunk。最後 Webpack 會把所有 Chunk 轉換成文件輸出。 在整個流程中 Webpack 會在恰當的時機執行 Plugin 里定義的邏輯。
webpack需要在項目根目錄下創建一個webpack.config.js來導出webpack的配置,配置多樣化,可以自行定製,下面講講最基礎的配置。
module.exports = { entry: ./src/index.js, output: { path: path.join(__dirname, ./dist), filename: main.js, }}
entry
代表入口,webpack會找到該文件進行解析output
代表輸入文件配置path
把最終輸出的文件放在哪裡filename
輸出文件的名字
有時候我們的項目並不是spa,需要生成多個js html,那麼我們就需要配置多入口。
module.exports = { entry: { pageA: ./src/pageA.js, pageB: ./src/pageB.js }, output: { path: path.join(__dirname, ./dist), filename: [name].[hash:8].js, },}
entry
配置一個對象,key值就是chunk
: 代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。看看filename[name]
: 這個name指的就是chunk的名字,我們配置的key值pageA
pageB
,這樣打包出來的文件名是不同的,再來看看[hash]
,這個是給輸出文件一個hash值,避免緩存,那麼:8
是取前8位。
這裡有人會有疑問了,項目是多頁面的,應該有pageA.html``pageA.js``pageA.css
, 那麼我應該生成多個html,這個只是做了JS的入口區分,我不想每一個頁面都去複製粘貼一個html,並且html是大部分重複的,可能不同頁面只需要修改title
,下面來看看這個問題怎麼解決:
需要引入一個webpack的plugin
:
npm install html-webpack-plugin -D
該插件可以給每一個chunk生成html,指定一個template
,可以接收參數,在模板裡面使用,下面來看看如何使用:
const HtmlWebpackPlugin = require(html-webpack-plugin);module.exports = { entry: { pageA: ./src/pageA.js, pageB: ./src/pageB.js }, output: { path: path.join(__dirname, ./dist), filename: [name].[hash:8].js, }, plugins: [ new HtmlWebpackPlugin({ template: ./src/templet.html, filename: pageA.html, title: pageA, chunks: [pageA], hash: true, minify: { removeAttributeQuotes: true } }), new HtmlWebpackPlugin({ template: ./src/templet.html, filename: pageB.html, title: pageB, chunks: [pageB], hash: true, minify: { removeAttributeQuotes: true } }), ]}
在webpack中,插件的引入順序沒有規定,這個在後面在繼續詳說。
template
: html模板的路徑地址filename
: 生成的文件名title
: 傳入的參數chunks
: 需要引入的chunkhash
: 在引入JS裡面加入hash值 比如:<script src=index.js?2f373be992fc073e2ef5></script>
removeAttributeQuotes
: 去掉引號,減少文件大小<script src=index.js></script>
- 具體文檔
這樣在dist目錄下就生成了pageA.html和pageB.html並且通過配置chunks,讓pageA.html里加上了script標籤去引入pageA.js。那麼現在還剩下css沒有導入,css需要藉助loader去做,所以現在要下載幾個依賴,以scss為例,less同理
npm install css-loader style-loader sass-loader node-sass -D
css-loader
: 支持css中的importstyle-loader
: 把css寫入style內嵌標籤sass-loader
: scss轉換為cssnode-sass
: scss轉換依賴
來看看如何配置loader
module.exports = { module: { rules: [ { test: /.scss$/, use: [style-loader, css-loader, sass-loader], exclude: /node_modules/ } ] }}
test
: 一個正則表達式,匹配文件名use
: 一個數組,裡面放需要執行的loader,倒序執行,從右至左。exclude
: 取消匹配node_modules裡面的文件
如果想把css作為一個單獨的文件,需要用到一個插件來做(webpack4.0.0以上版本需要next版本):
npm i extract-text-webpack-plugin@next -Dconst ExtractTextPlugin = require(extract-text-webpack-plugin);module.exports = { entry: ./src/index.js, output: { path: path.join(__dirname, ./dist), filename: main.js, }, module: { rules: [ { test: /.scss$/, use: ExtractTextPlugin.extract({ // style-loader 把css直接寫入html中style標籤 fallback: style-loader, // css-loader css中import支持 // loader執行順序 從右往左執行 use: [css-loader, sass-loader] }), exclude: /node_modules/ } ] }, plugins: [ new ExtractTextPlugin([name].[contenthash:8].css), ]}
- 需要在plugins里加入插件
name
: chunk名字contenthash:8
: 根據內容生成hash值取前8位 - 修改loader配置下的
use
:fallback
: 兼容方案
這樣就實現了js,html,css的打包,那麼再來看看一些常用的loader:
babel-loader
: 用babel轉換代碼url-loader
: 依賴於file-loader
,把圖片轉換成base64嵌入html,如果超出一定閾值則交給file-loader
rules: [ // 處理js { test: /.js?$/, exclude: /node_modules/, use: [babel-loader] }, // 處理圖片 { test: /.(png|jpg|gif|ttf|eot|woff(2)?)(?[=a-z0-9]+)?$/, use: [{ loader: url-loader, options: { query: { // 閾值 單位byte limit: 8192, name: images/[name]_[hash:7].[ext], } } }] }, ]
babel的配置建議在根目錄下新建一個.babelrc文件
{ "presets": [ "env", "stage-0", "react" ], "plugins": [ "transform-runtime", "transform-decorators-legacy", "add-module-exports" ]}
presets
: 預設, 一個預設包含多個插件 起到方便作用 不用引用多個插件env
: 只轉換新的句法,例如const let => ..等 不轉換 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise、Object.assign。stage-0
: es7提案轉碼規則 有 0 1 2 3 階段 0包含 1 2 3裡面的所有react
: 轉換react jsx語法plugins
: 插件 可以自己開發插件 轉換代碼(依賴於ast抽象語法數)transform-runtime
: 轉換新語法,自動引入polyfill插件,另外可以避免污染全局變數transform-decorators-legacy
: 支持裝飾器add-module-exports
: 轉譯export default {}; 添加上module.exports = exports.default 支持commonjs
因為我們在文件名中加入hash值,打包多次後dist目錄變得非常多文件,沒有刪除或覆蓋,這裡可以引入一個插件,在打包前自動刪除dist目錄,保證dist目錄下是當前打包後的文件:
plugins: [ new CleanWebpackPlugin( // 需要刪除的文件夾或文件 [path.join(__dirname, ./dist/*.*)], { // root目錄 root: path.join(__dirname, ./) } ),]
指定extension之後可以不用在require或是import的時候加文件擴展名,會依次嘗試添加擴展名進行匹配:
resolve: { extensions: [.js, .jsx, .scss, .json],},
3. 優化實戰 高級裝備
天下武功唯快不破,優化方案千千萬萬,各取所需吧。
提出公共的JS文件
webpack4中廢棄了webpack.optimize.CommonsChunkPlugin插件,用新的配置項替代
module.exports = { entry: ./src/index.js, output: { path: path.join(__dirname, ./dist), filename: main.js, }, optimization: { splitChunks: { cacheGroups: { commons: { chunks: initial, minChunks: 2, maxInitialRequests: 5, minSize: 2, name: common } } } },}
把多次import的文件打包成一個單獨的common.js
HappyPack
在webpack運行在node中打包的時候是單線程去一件一件事情的做,HappyPack可以開啟多個子進程去並發執行,子進程處理完後把結果交給主進程
npm i happypack -D
需要改造一下loader配置,此loader用子進程去處理
const HappyPack = require(happypack);module.exports = { entry: ./src/index.js, output: { path: path.join(__dirname, ./dist), filename: main.js, }, module: { rules: [ { test: /.jsx?$/, exclude: /node_modules/, use: happypack/loader?id=babel, }, ] }, plugins: [ new HappyPack({ id: babel, threads: 4, loaders: [babel-loader] }), ]}
id
: id值,與loader配置項對應threads
: 配置多少個子進程loaders
: 用什麼loader處理- 具體文檔
作用域提升
如果你的項目是用ES2015的模塊語法,並且webpack3+,那麼建議啟用這一插件,把所有的模塊放到一個函數里,減少了函數聲明,文件體積變小,函數作用域變少。
module.exports = { entry: ./src/index.js, output: { path: path.join(__dirname, ./dist), filename: main.js, }, plugins: [ new webpack.optimize.ModuleConcatenationPlugin(), ]}
提取第三方庫
方便長期緩存第三方的庫,新建一個入口,把第三方庫作為一個chunk,生成vendor.js
module.exports = { entry: { main: ./src/index.js, vendor: [react, react-dom], },}
DLL動態鏈接
第三庫不是經常更新,打包的時候希望分開打包,來提升打包速度。打包dll需要新建一個webpack配置文件,在打包dll的時候,webpack做一個索引,寫在manifest文件中。然後打包項目文件時只需要讀取manifest文件。
webpack.vendor.js
const webpack = require(webpack);const path = require(path);module.exports = { entry: { vendor: [react, react-dom], }, output: { path: path.join(__dirname, ./dist), filename: dll/[name]_dll.js, library: _dll_[name], }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, ./dist/dll, manifest.json), name: _dll_[name], }), ]};
path
: manifest文件的輸出路徑
name
: dll暴露的對象名,要跟output.library保持一致context
: 解析包路徑的上下文,這個要跟接下來配置的dll user一致webpack.config.js
module.exports = { entry: { main: ./src/index.js, vendor: [react, react-dom], }, plugins: [ new webpack.DllReferencePlugin({ manifest: path.join(__dirname, ./dist/dll, manifest.json) }) ]}
html
<script src="vendor_dll.js"></script>
4. 線上和線下
在生成環境和開發環境其實我們的配置是存在相同點,和不同點的,為了處理這個問題,會創建3個文件:
webpack.base.js
: 共同的配置webpack.dev.js
: 在開發環境下的配置webpack.prod.js
: 在生產環境的配置
通過webpack-merge去做配置的合併,比如:
開發環境
const path = require(path);const webpack = require(webpack);const merge = require(webpack-merge);const base = require(./webpack.base);const dev = { devServer: { contentBase: path.join(__dirname, ../dist), port: 8080, host: localhost, overlay: true, compress: true, open:true, hot: true, inline: true, progress: true, }, devtool: inline-source-map, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), ]}module.exports = merge(base, dev);
開發環境中我們可以啟動一個devServer靜態文件伺服器,預覽我們的項目,引入base配置文件,用merge去合併配置。
contentBase
: 靜態文件地址port
: 埠號host
: 主機overlay
: 如果出錯,則在瀏覽器中顯示出錯誤compress
: 伺服器返回瀏覽器的時候是否啟動gzip壓縮open
: 打包完成自動打開瀏覽器hot
: 模塊熱替換 需要webpack.HotModuleReplacementPlugin
插件inline
: 實時構建progress
: 顯示打包進度devtool
: 生成代碼映射,查看編譯前代碼,利於找bugwebpack.NamedModulesPlugin
: 顯示模塊的相對路徑
生產環境
再來看看生成環境最重要的代碼壓縮,混淆:
const path = require(path);const merge = require(webpack-merge);const WebpackParallelUglifyPlugin = require(webpack-parallel-uglify-plugin);const base = require(./webpack.base);const prod = { plugins: [ // 文檔: https://github.com/gdborton/webpack-parallel-uglify-plugin new WebpackParallelUglifyPlugin( { uglifyJS: { mangle: false, output: { beautify: false, comments: false }, compress: { warnings: false, drop_console: true, collapse_vars: true, reduce_vars: true } } } ), ]}module.exports = merge(base, prod);
webpack-parallel-uglify-plugin
可以並行壓縮代碼,提升打包效率
uglifyJS配置:
mangle
: 是否混淆代碼output.beautify
: 代碼壓縮成一行 true為不壓縮 false壓縮output.comments
: 去掉注釋compress.warnings
: 在刪除沒用到代碼時 不輸出警告compress.drop_console
: 刪除consolecompress.collapse_vars
: 把定義一次的變數,直接使用,取消定義變數compress.reduce_vars
: 合併多次用到的值,定義成變數- 具體文檔
5. 成為頭號玩家
想要成為頭號玩家,玩轉配置可不行,當然還要做一些loader和plugin的開發,去為項目做一些優化,解決痛點。
loader
loader是一個模塊導出函數,在正則匹配成功的時候調用,webpack把文件數組傳入進來,在this上下文可以訪問loader API
this.context
: 當前處理文件的所在目錄,假如當前 Loader 處理的文件是 /src/main.js,則 this.context 就等於 /src。this.resource
: 當前處理文件的完整請求路徑,包括 querystring,例如 /src/main.js?name=1。this.resourcePath
: 當前處理文件的路徑,例如 /src/main.js。this.resourceQuery
: 當前處理文件的 querystring。this.target
: 等於 Webpack 配置中的 Targetthis.loadModule
: 但 Loader 在處理一個文件時,如果依賴其它文件的處理結果才能得出當前文件的結果時, 就可以通過 - - - this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去獲得 request 對應文件的處理結果。this.resolve
: 像 require 語句一樣獲得指定文件的完整路徑,使用方法為 resolve(context: string, request: string, callback: function(err, result: string))。this.addDependency
: 給當前處理文件添加其依賴的文件,以便再其依賴的文件發生變化時,會重新調用 Loader 處理該文件。使用方法為 addDependency(file: string)。this.addContextDependency
: 和 addDependency 類似,但 addContextDependency 是把整個目錄加入到當前正在處理文件的依賴中。使用方法為 addContextDependency(directory: string)。this.clearDependencies
: 清除當前正在處理文件的所有依賴,使用方法為 clearDependencies()。this.emitFile
: 輸出一個文件,使用方法為 emitFile(name: string, content: Buffer|string, sourceMap: {…})。this.async
: 返回一個回調函數,用於非同步執行。
下面來看看less-loader
和style-loader
如何實現:
let less = require(less);module.exports = function (source) { const callback = this.async(); less.render(source, (err, result) => { callback(err, result.css); });}module.exports = function (source) { let script = (` let style = document.createElement("style"); style.innerText = ${JSON.stringify(source)}; document.head.appendChild(style); `); return script;}
plugin
webpack整個構建流程有許多鉤子,開發者可以在指定的階段加入自己的行為到webpack構建流程中。插件由以下構成:
- 一個 JavaScript 命名函數。
- 在插件函數的 prototype 上定義一個 apply 方法。
- 指定一個綁定到 webpack 自身的事件鉤子。
- 處理 webpack 內部實例的特定數據。
- 功能完成後調用 webpack 提供的回調。
整個webpack流程由compiler和compilation構成,compiler只會創建一次,compilation如果開起了watch文件變化,那麼會多次生成compilation. 那麼這2個類下面生成了需要事件鉤子
compiler hooks 文檔 compilation hooks 文檔
寫一個小插件,生成所有打包的文件列表(webpack4不推薦使用compiler.plugin來註冊插件,webpack5將不支持):
class FileListPlugin{ constructor(options) { this.options = options; } apply(compiler) { compiler.hooks.emit.tap(FileListPlugin,function (compilation) { let fileList = filelist:
; for (let filename in compilation.assets) { fileList += (- +filename+
); } compilation.assets[filelist.md]={ source() { return fileList; }, size() { return fileList.length } } }); }}module.exports = FileListPlugin;
6. 最後
都讀在這裡了,還不點個贊嗎。
感謝你閱讀我的文章
推薦閱讀:
※《Oli-Zhao的前端一萬小時》之:離不開的Git和GitHub(1)——版本控制、Git、GitHub初認識
※十年web前端開發工程師告訴你怎樣零基礎入門
※html5全部標籤
※免費直播 | 2018,你最需要的前端學習指南&求職指南!飢人谷
※「乾貨」從菜鳥到大神,前端學習書籍推薦