本文由 IMWeb 團隊成員 Ciccy 首發於 IMWeb 社區網站 imweb.io。點擊閱讀原文查看 IMWeb 社區更多精彩文章。css
webpack的核心是一切皆模塊,因此它其實本質上就是個靜態模塊打包器。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖,其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。官網顯示的這幅圖很形象地描述了這個過程:html
webpack4相比於3作了不少優化,最大的改變就是支持了零配置打包,再也不強制要求必須進行繁瑣的webpack配置。 webpack4 新增了一個 mode 配置項。Mode 有兩個值:development 或者是 production,默認值是 production。webpack4 針對不一樣的mode提供了不一樣的默認配置,這對於只但願配置打包出入口,不想深刻了解其餘配置的開發人員,提供了最基礎的打包優化。固然entry,output ,mode這些配置項也都有默認值,mode默認爲production。不一樣mode的區別與默認配置能夠參考https://segmentfault.com/a/1190000013712229node
那麼接下來咱們來咱們從零開始一步步完成一個完整項目的配置,每部分配置除了會列出基礎配置,還會給出一些額外須要注意的事項,也是我在項目中的踩坑總結。webpack
先貼一下項目目錄結構:web
- src- common 公用代碼庫- pages - [活動名稱]\_[h5|pc] - index.js - index.html
首先咱們看看項目的打包入口如何配置: webpack打包入口支持但入口和多入口,但入口文件只限於js文件(聽說webpack5在考慮增長HTML文件和CSS文件做爲入口)。json
多入口時,給entry傳入對象便可,以下所示, 其中對象的key值則是入口的name:segmentfault
const config = { entry: { pageOne: './src/pageOne/index.js', pageTwo: './src/pageTwo/index.js', pageThree: './src/pageThree/index.js' }};
顯然,咱們的項目頁面數量是未知的,將全部頁面都枚舉在配置裏顯然是不合理的,因此能夠定義 getEntry()
方法來遍歷指定文件夾獲取入口。windows
const webpack = require("webpack");
api
const glob = require("glob");
promise
function getEntry() {
const entry = {};
//讀取src目錄全部page入口
glob.sync('./src/pages/*/*/index.js')
.forEach(function (filePath) {
var name = filePath.match(/\/pages\/(.+)\/index.js/);
name = name[1];
entry[name] = filePath;
});
return entry;
};
module.exports = {
mode: 'development',
// 多入口
entry: getEntry(),
}
不管是單入口仍是多入口,都只能指定一個輸出配置。咱們看看項目的 output
配置
output: { publicPath: CDN.js, filename: '[name].[chunkhash].js', chunkFilename: '[name]_[chunkhash].min.js', path: distDir,},
filename: 輸出文件的文件
path: 輸出文件的絕對路徑
chunkFilename:非入口打包出的文件名稱
publicPath: 文件中靜態資源的引用路徑
一般,dev環境時,不用配置publicPath,此時靜態資源的引用路徑相對於HTML頁面。而生產環境時,把publicPath的值設爲CDN的目錄路徑就能夠了。 這裏配置有幾點須要注意的:
這裏說了是多端多頁面項目,多端只的就是PC和H5兩端,那麼這就意味着各端的CDN資源路徑是不同的,因此publicPath值也應該不同。如何動態設置publicPath呢?
webpack 提供了 __webpack_public_path__
來動態設置publicPath,咱們在入口文件的最頂部進行定義便可,以下所示 index.js
。
__webpack_public_path__ = myRuntimePublicPath; // 必定要寫在最頂部
hash:以項目爲維度生成的hash值,項目所有文件都共用一個hash值 chunkhash: 以chunk爲維度生成的hash值,不一樣入口生成不一樣的chunkhash值 contenthash: 根據資源內容生成的hash值 通常是用chunkhash,contenthash也有使用場合,好比在mini-css-extract-plugin插件配置使用,後面會詳細講到。
配置好了輸入輸出後,咱們就須要來配置對模塊內容如何進行處理。webpack 只能理解 JavaScript 和 JSON 文件。loader 讓 webpack 可以去處理其餘類型的文件,並將它們轉換爲有效模塊。
須要引入babel的話,咱們就須要使用babel-loader
js文件須要使用babel的話,引入 babel-loader
{ test: /\.js$/, loader: 'babel-loader', include: [path.resolve(rootDir, 'src')],},
使用babel時須要注意,Babel默認只轉換新的JavaScript句法(syntax),而不轉換新的API,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局對象,以及一些定義在全局對象上的方法(好比Object.assign)都不會轉碼,若是要使用須要引入polyfill。
引入polyfill 的方式有不少種,這裏推薦babel transformtime
+ runtime
, transform-time
的做用是將遇到須要轉化的語法時引入polyfill,而 run-time
則是提供polyfill, 這樣就能夠作到按需引入,而不是全部的都打包進去。因此babel的配置以下:
{ "presets": [ [ "env", { "browsers": ["last 5 versions", "> 5%", "Android > 4.3"] } ], "stage-2" ], "plugins": [ "transform-runtime" ]}
對於css模塊,經常使用的loader有style-loader和css-loader。 css loader
用來處理js文件中引入的css模塊(處理@import和url()), style-loader
是將 css-loader
打包好的css代碼以 <style>
標籤的形式插入到html文件中。
這個項目用到了sass和post-css,因此這裏還引入了sass-loader和postcss-loader。由於webpack對於loader的調用是從右往左的,因此配置以下:
{ // 增長對 SCSS 文件的支持 test: /\.scss|\.css/, // SCSS 文件的處理順序爲先 sass-loader 再 css-loader 再 style-loader use: [ 'style-loader', { loader: 'css-loader', // 給 css-loader 傳入配置項 options: { importLoaders: 2, }, }, 'postcss-loader', { loader: 'sass-loader', }, ],},
若是你也使用了sass-loader,有個問題可能須要注意。當你的index.scss裏@import了其餘scss文件好比a.scss時,若是a.scss裏使用了url(),且裏面的路徑是相對路徑,那麼在sass-loader 處理事後給css-loader處理時就會報錯,找不到url()裏指定的資源。這是爲何呢?
實際上,當sass-loader處理時,會將index.scss裏@import的A.scss合併進來,最後只輸出index.scss。但A.scss裏的url()原本是以A.scss寫的相對路徑,這樣合併又不對url()作處理的話,就致使了合併後沒法定位到url()裏的資源。對於這個問題,有兩種解決辦法:
1)使用 resolve-url-loader
,將 resolve-url-loader
設置於 loader 鏈中的 sass-loader 以前,就能夠重寫 url。可是這個辦法有個問題,那就是 resolve-url-loader
不識別scss文件的行內註釋語法,即 // 註釋
,這個問題使得接入一些已存在的公共樣式庫時會存在問題,目前還在研究是否有其餘loader能夠解決,你們有較好的解決辦法也能夠一塊兒討論。
2)將資源路徑改成變量來統一管理
3)經過alias設置路徑別名,從而便捷使用絕對路徑。注意在scss文件中使用alias裏定義的路徑別名時,須要帶上~前綴,不然打包時仍會被識別爲普通路徑
對於圖片等其餘資源,咱們通常使用file-loader進行處理,它實現的功能很簡單:
將要加載的文件複製到指定目錄
生成請求文件資源URL 具體配置以下:
{ test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/, loader: 'file-loader',},
儘管webpack既支持commonjs規範也支持AMD規範。可是咱們如何經過import 的方式引入AMD 模塊或者其餘不支持模塊化的庫呢?
咱們項目裏使用到了zepto,這裏就以zepto爲例,在import zepto時會報錯
Uncaught TypeError: Cannot read property 'createElement' of undefined
這就是由於zepto只使用了AMD 規範導出模塊。解決全部這類問題其實很簡單,只須要使用 script-loader
和 exports-loader
便可:
{ test: require.resolve('zepto'), use: ['exports-loader?window.Zepto','script-loader']}
script-loader
用 eval 的方法將 zepto 在引入的時候執行了一遍,此時 zepto 庫已存在於 window.Zepto
exports-loader
將傳入的 window.Zepto 以 module.exports = window.Zepto 的形式向外暴露接口,使這個模塊符合 CommonJS 規範,支持 import 這樣咱們就能夠直接 import$from'zepto'
了,其餘AMD 模塊或者其餘不支持模塊化的庫也相似。
插件機制是webpack的核心之一,插件(Plugins)是用來拓展webpack功能的,它們會在整個構建過程當中生效,執行相關的任務。咱們通常使用插件來完善咱們的構建流程,webpack有許多插件可用,這裏只挑兩個必備插件來詳細說明
前面有說過,目前webpack的打包入口只支持JS文件,因此它打包輸出的也是JS文件,那麼如何把這個JS文件引入咱們的html中去呢,手動引入沒法監測到hash值的變化,確定是不OK的。所以咱們就用到了 html-webpack-plugin
這個插件,它會將打包好的文件自動引入到指定的html中去,並將html文件輸出在指定位置。
html-webpack-plugin
使用時,一個實例操做只能一個html,因此對於多頁面項目,咱們須要創造多個實例,結合前面的getEntry方法,咱們能夠在遍歷獲得entry的時候進行實例化,獲得htmlPluginArray
const htmlPluginArray= [];
function getEntry() {
const entry = {};
glob.sync('./src/pages/*/*/index.js')
.forEach(function (filePath) {
var name = filePath.match(/\/pages\/(.+)\/index.js/);
name = name[1];
entry[name] = filePath;
// 實例化插件
+ htmlPluginArray.push(newHtmlWebpackPlugin({
+ filename: './' + name + '/index.html',
+ template: './src/pages/' + name + '/index.html',
+ }))
});
return entry;
};
// 配置plugin,此處省略其餘配置代碼
plugins: [
htmlPluginArray
],
前面使用css loader 和 style-loader對css文件進行處理後,css文件被做爲模塊也打包在了js文件中。實際生產環境,咱們固然是但願js文件和css文件分離的,因此這裏就可使用 mini-css-extract-plugin
。 具體配置以下:
module: { rules: [ { // 增長對 SCSS 文件的支持 test: /\.scss|\.css/, // SCSS 文件的處理順序爲先 sass-loader 再 css-loader 再 style-loader use: [ {+ loader: MiniCssExtractPlugin.loader,+ options: {+ publicPath: CDN.css, }, }, { loader: 'css-loader', // 給 css-loader 傳入配置項 options: { importLoaders: 2, }, }, 'postcss-loader', { loader: 'sass-loader', }, ], } ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', chunkFilename: '[name].[contenthash].css', }), ],
這裏之因此設置爲 contenthash
,是用來解決抽離css文件後,js文件變化致使的css文件hash值變化的問題
resolve配置規定了webpack如何尋找各個依賴模塊。
前面講到的alias就是在這裏配置。在資源引用時,若是資源引用路徑太深,又比較經常使用,咱們則能夠定義路徑別名,例如:
alias: { h5: path.resolve(__dirname, 'src/common/h5/'), pc: path.resolve(__dirname, 'src/common/pc/'),}
咱們就能夠直接在代碼中這樣引用了:
import Utility from 'h5/util';
webpack-dev-server
是開發時的必備利器,它能夠在本地起一個簡單的 web 服務器,當文件發生變化時,可以實時從新加載。 webpack-dev-server
的配置也很簡單:
devServer: { publicPath: '/act/', port: 8888, hot: true,},
啓動webpack-dev-server後,在目標文件夾中是看不到編譯後的文件的,實時編譯後的文件都保存到了內存當中
hot
設置爲true是啓用 webpack 的 模塊熱替換(HMR)功能,但這裏注意必需要添加插件 webpack.HotModuleReplacementPlugin
才能徹底啓用 HMR
publicPath路徑下的打包文件能夠在瀏覽器中訪問,能夠這麼理解,webpack-dev-server打包的內容是放在內存中的,這些打包後的資源對外的的根目錄就是publicPath。
默認 devServer.publicPath 是 '/',因此你的包(bundle)能夠經過 http://localhost:8888/bundle.js
訪問。當咱們要設置具體路徑時記得要以 /
開頭,如上面配置所示,設置了 publicPath:'/act/'
後bundle的訪問路徑則變成了: http://localhost:8888/act/bundle.js
注意:當這裏的publicPath和output的publicPath同時設置時,這裏的優先級更高
一般,咱們本地開發環境和生產環境會採用不一樣的配置文件,發佈上線時,咱們會對資源進行壓縮、合併等優化,但在本地開發時,爲了提升構建速度,方便調試代碼,咱們則會省去這些優化配置,與此同時,咱們更加關注模塊熱更新、localhost server等等。因此通常會爲每一個環境編寫彼此獨立的 webpack 配置,這裏項目的webpack配置文件以下,其中webpack.common.js是用來放dev和dist裏的公共配置:
這裏會用到 webpack-merge
工具進行配置的合併。 好比 webpack.common.js
內容以下:
module.exports = { module: { rules: [] }};
webpack.dev.js
的則可使用webpack-merge合併配置:
const merge = require('webpack-merge');const common = require('./webpack.common.js');module.exports = merge(common, { devtool: 'inline-source-map', devServer: { // dev 配置 }});
因此咱們能夠在package.json添加咱們的webpack啓動命令以下:
"scripts": { "dist": "cross-env NODE_ENV=production webpack --config webpack.dist.js", "dev": "webpack-dev-server --config webpack.dev.js",},
其中,
cross-env NODE_ENV=production
是用來設置node環境變量,設置環境變量的目的是由於許多庫自身會判斷當前環境,並在生產環境下作一些優化處理,而用cross-env來設置是爲了兼容windows系統。
到這裏,咱們項目已經能起來了,可是做爲一名合格的程序猿,咱們固然要探索更優實踐。webpack有哪些經常使用的優化措施呢?
webpack 提供了兩種動態加載的語法。第一種,也是推薦選擇的方式是,使用符合 ECMAScript 提案 的 import() 語法 來實現動態導入。第二種,則是 webpack 的遺留功能,使用 webpack 特定的 require.ensure。
import() 會返回一個 promise,在代碼中全部被import()的模塊,都將打成一個單獨的包,在瀏覽器運行到這一行代碼時,就會自動請求這個資源,實現動態加載。* 使用import()時應該注意如下幾點: *
1)import()時能夠經過註釋語法import(/chunkName/'qqapi').then()來定義異步加載模塊打包出來的chunkName,不然會默認以id做爲chunkName
2) 當bundle中已經以同步方式引入模塊後,import()將不會再被webpack單獨打包出js文件,能夠認爲是按需加載無效了
爲了合理利用瀏覽器緩存,通常會將不常變更的第三方庫以及公共代碼和業務代碼分開打包
因此通常項目的打包策略爲:
第三方庫打包出vendor(基本不變)
引用兩次以上的模塊打包出common (變化較少)
業務代碼 (常變)
對於分包方式,webpack 4 移除 CommonsChunkPlugin,取而代之的是optimization.splitChunks 讓咱們看看這裏怎麼配置:
splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/ name: 'vendor', chunks: 'initial', priority: 2, minChunks: 2 }, common: { test: /.js$/, name: 'common', chunks: 'initial', priority: 1, minChunks: 2 }}}
注意抽離出來的代碼要在HTML文件裏引入
因爲項目包含兩端代碼,H5\PC部分依賴是獨立的,單純的從項目層面進行公共模塊的抽離是不行的。
因此這裏得詳細設置公共庫和代碼的匹配規則。好比咱們項目PC用的JQ,H5用的zepto,就能夠配置
optimization: { splitChunks: { cacheGroups: { h5common: { test: /zepto/, name: 'h5common', chunks: 'initial', priority: 1, minChunks: 1, }, }, }, },
配置loader時,咱們能夠經過exclude設置哪些目錄下的文件不進行處理,經過include精確指定只處理哪些目錄下的文件,以此來縮小處理範圍,加快構建速度。
module: { rules: [ { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/, include: path.resolve(__dirname, 'src') } ]}
當咱們引用模塊時,若是出現import ‘zepto’這樣的依賴引入方式,webpack會默認從當前目錄往上逐層查找是否有 node_modules
,而後在 node_modules
下查找是否存在指定依賴。
爲了減小搜索範圍,咱們能夠經過設置 resolve.modules
來告訴 webpack 解析這類依賴時應該搜索的目錄
resolve: { modules: [path.resolve(rootDir, 'node_modules')],},
這篇文章以多端多頁面項目爲例,深刻講解了如何初始化項目webpack配置,這些實踐不只適用於這個項目,對於多頁面項目和普通項目也一樣適用。