轉自IMWeb社區,作者:IMWeb團隊 原文鏈接

webpack的核心是一切皆模塊,所以它其實本質上就是個靜態模塊打包器。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖,其中包含應用程序需要的每個模塊,然後將所有這些模塊打包成一個或多個 bundle。官網顯示的這幅圖很形象地描述了這個過程:

webpack4相比於3做了很多優化,最大的改變就是支持了零配置打包,不再強制要求必須進行繁瑣的webpack配置。 webpack4 新增了一個 mode 配置項。Mode 有兩個值:development 或者是 production,默認值是 production。webpack4 針對不同的mode提供了不同的默認配置,這對於只希望配置打包出入口,不想深入了解其他配置的開發人員,提供了最基礎的打包優化。當然entry,output ,mode這些配置項也都有默認值,mode默認為production。不同mode的區別與默認配置可以參考segmentfault.com/a/1190

那麼接下來我們來我們從零開始一步步完成一個完整項目的配置,每部分配置除了會列出基礎配置,還會給出一些額外需要注意的事項,也是我在項目中的踩坑總結。

先貼一下項目目錄結構:

- src
- common 公用代碼庫
- pages
- [活動名稱]\_[h5|pc]
- index.js
- index.html

一、多頁面入口配置

首先我們看看項目的打包入口如何配置: webpack打包入口支持但入口和多入口,但入口文件只限於js文件(據說webpack5在考慮增加HTML文件和CSS文件作為入口)。 多入口時,給entry傳入對象即可,如下所示, 其中對象的key值則是入口的name:

const config = {
entry: {
pageOne: ./src/pageOne/index.js,
pageTwo: ./src/pageTwo/index.js,
pageThree: ./src/pageThree/index.js
}
};

顯然,我們的項目頁面數量是未知的,將所有頁面都枚舉在配置里顯然是不合理的,所以可以定義getEntry()方法來遍歷指定文件夾獲取入口。

const webpack = require("webpack");
const glob = require("glob");

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的目錄路徑就可以了。 這裡配置有幾點需要注意的:

1、動態publicPath

這裡說了是多端多頁面項目,多端只的就是PC和H5兩端,那麼這就意味著各端的CDN資源路徑是不一樣的,所以publicPath值也應該不一樣。如何動態設置publicPath呢? webpack 提供了__webpack_public_path__來動態設置publicPath,我們在入口文件的最頂部進行定義即可,如下所示index.js

__webpack_public_path__ = myRuntimePublicPath; // 一定要寫在最頂部

2、hash值的區別

hash:以項目為維度生成的hash值,項目全部文件都共用一個hash值 chunkhash: 以chunk為維度生成的hash值,不同入口生成不同的chunkhash值 contenthash: 根據資源內容生成的hash值 一般是用chunkhash,contenthash也有使用場合,比如在mini-css-extract-plugin插件配置使用,後面會詳細講到。

三、loader配置

配置好了輸入輸出後,我們就需要來配置對模塊內容如何進行處理。webpack 只能理解 JavaScript 和 JSON 文件。loader 讓 webpack 能夠去處理其他類型的文件,並將它們轉換為有效模塊。

1、js 模塊

需要引入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"
]
}

2、css 模塊

對於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里定義的路徑別名時,需要帶上~前綴,否則打包時仍會被識別為普通路徑

3、圖片、字體等資源

對於圖片等其他資源,我們一般使用file-loader進行處理,它實現的功能很簡單:

  • 將要載入的文件複製到指定目錄
  • 生成請求文件資源URL 具體配置如下:

{
test: /.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,
loader: file-loader,
},

4、import AMD 模塊

儘管webpack既支持commonjs規範也支持AMD規範。但是我們如何通過import 的方式引入AMD 模塊或者其他不支持模塊化的庫呢? 我們項目里使用到了zepto,這裡就以zepto為例,在import zepto時會報錯

Uncaught TypeError: Cannot read property createElement of undefined

這就是因為zepto只使用了AMD 規範導出模塊。解決所有這類問題其實很簡單,只需要使用script-loaderexports-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 模塊或者其他不支持模塊化的庫也類似。

四、plugin 配置

插件機制是webpack的核心之一,插件(Plugins)是用來拓展webpack功能的,它們會在整個構建過程中生效,執行相關的任務。我們一般使用插件來完善我們的構建流程,webpack有許多插件可用,這裡只挑兩個必備插件來詳細說明

1、html-webpack-plugin

前面有說過,目前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
],

2、mini-css-extract-plugin

前面使用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值變化的問題

五、其他配置

1、resolve

resolve配置規定了webpack如何尋找各個依賴模塊。 前面講到的alias就是在這裡配置。在資源引用時,如果資源引用路徑太深,又比較常用,我們則可以定義路徑別名,例如:

alias: {
h5: path.resolve(__dirname, src/common/h5/),
pc: path.resolve(__dirname, src/common/pc/),
}

我們就可以直接在代碼中這樣引用了:

import Utility from h5/util;

2、webpack dev server

webpack-dev-server 是開發時的必備利器,它可以在本地起一個簡單的 web 伺服器,當文件發生變化時,能夠實時重新載入。webpack-dev-server的配置也很簡單:

devServer: {
publicPath: /act/,
port: 8888,
hot: true,
},

啟動webpack-dev-server後,在目標文件夾中是看不到編譯後的文件的,實時編譯後的文件都保存到了內存當中

1) HMR

hot設置為true是啟用 webpack 的 模塊熱替換(HMR)功能,但這裡注意必須要添加插件webpack.HotModuleReplacementPlugin 才能完全啟用 HMR

2) publicPath

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同時設置時,這裡的優先順序更高

3、配置分離

通常,我們本地開發環境和生產環境會採用不同的配置文件,發布上線時,我們會對資源進行壓縮、合併等優化,但在本地開發時,為了提高構建速度,方便調試代碼,我們則會省去這些優化配置,與此同時,我們更加關注模塊熱更新、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有哪些常用的優化措施呢?

1、按需載入

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文件,可以認為是按需載入無效了

2、抽離公共模塊

1)一般項目

為了合理利用瀏覽器緩存,一般會將不常變動的第三方庫以及公共代碼和業務代碼分開打包 所以一般項目的打包策略為:

  • 第三方庫打包出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文件里引入

2)多端項目

由於項目包含兩端代碼,H5PC部分依賴是獨立的,單純的從項目層面進行公共模塊的抽離是不行的。 所以這裡得詳細設置公共庫和代碼的匹配規則。比如我們項目PC用的JQ,H5用的zepto,就可以配置

optimization: {
splitChunks: {
cacheGroups: {
h5common: {
test: /zepto/,
name: h5common,
chunks: initial,
priority: 1,
minChunks: 1,
},
},
},
},

3、優化loader配置

配置loader時,我們可以通過exclude設置哪些目錄下的文件不進行處理,通過include精確指定只處理哪些目錄下的文件,以此來縮小處理範圍,加快構建速度。

module: {
rules: [
{
test: /.js$/,
use: babel-loader,
exclude: /node_modules/,
include: path.resolve(__dirname, src)
}
]
}

4、限制路徑解析範圍

當我們引用模塊時,如果出現import 『zepto』這樣的依賴引入方式,webpack會默認從當前目錄往上逐層查找是否有node_modules,然後在node_modules下查找是否存在指定依賴。 為了減少搜索範圍,我們可以通過設置resolve.modules來告訴 webpack 解析這類依賴時應該搜索的目錄

resolve: {
modules: [path.resolve(rootDir, node_modules)],
},

總結

這篇文章以多端多頁面項目為例,深入講解了如何初始化項目webpack配置,這些實踐不僅適用於這個項目,對於多頁面項目和普通項目也同樣適用。

推薦閱讀:

相关文章