轉自IMWeb社區,做者:IMWeb團隊,原文連接javascript
webpack的核心是一切皆模塊,因此它其實本質上就是個靜態模塊打包器。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖,其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。官網顯示的這幅圖很形象地描述了這個過程:css
webpack4相比於3作了不少優化,最大的改變就是支持了零配置打包,再也不強制要求必須進行繁瑣的webpack配置。 webpack4 新增了一個 mode 配置項。Mode 有兩個值:development 或者是 production,默認值是 production。webpack4 針對不一樣的mode提供了不一樣的默認配置,這對於只但願配置打包出入口,不想深刻了解其餘配置的開發人員,提供了最基礎的打包優化。固然entry,output ,mode這些配置項也都有默認值,mode默認爲production。不一樣mode的區別與默認配置能夠參考https://segmentfault.com/a/1190000013712229那麼接下來咱們來咱們從零開始一步步完成一個完整項目的配置,每部分配置除了會列出基礎配置,還會給出一些額外須要注意的事項,也是我在項目中的踩坑總結。html
先貼一下項目目錄結構:java
- src
- common 公用代碼庫
- pages
- [活動名稱]\_[h5|pc]
- index.js
- index.html
複製代碼
首先咱們看看項目的打包入口如何配置: webpack打包入口支持但入口和多入口,但入口文件只限於js文件(聽說webpack5在考慮增長HTML文件和CSS文件做爲入口)。 多入口時,給entry傳入對象便可,以下所示, 其中對象的key值則是入口的name:node
const config = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};
複製代碼
顯然,咱們的項目頁面數量是未知的,將全部頁面都枚舉在配置裏顯然是不合理的,因此能夠定義getEntry()
方法來遍歷指定文件夾獲取入口。webpack
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
配置web
output: {
publicPath: CDN.js,
filename: '[name].[chunkhash].js',
chunkFilename: '[name]_[chunkhash].min.js',
path: distDir,
},
複製代碼
一般,dev環境時,不用配置publicPath,此時靜態資源的引用路徑相對於HTML頁面。而生產環境時,把publicPath的值設爲CDN的目錄路徑就能夠了。 這裏配置有幾點須要注意的:json
這裏說了是多端多頁面項目,多端只的就是PC和H5兩端,那麼這就意味着各端的CDN資源路徑是不同的,因此publicPath值也應該不同。如何動態設置publicPath呢? webpack 提供了__webpack_public_path__
來動態設置publicPath,咱們在入口文件的最頂部進行定義便可,以下所示index.js
。segmentfault
__webpack_public_path__ = myRuntimePublicPath; // 必定要寫在最頂部
複製代碼
hash:以項目爲維度生成的hash值,項目所有文件都共用一個hash值 chunkhash: 以chunk爲維度生成的hash值,不一樣入口生成不一樣的chunkhash值 contenthash: 根據資源內容生成的hash值 通常是用chunkhash,contenthash也有使用場合,好比在mini-css-extract-plugin插件配置使用,後面會詳細講到。windows
配置好了輸入輸出後,咱們就須要來配置對模塊內容如何進行處理。webpack 只能理解 JavaScript 和 JSON 文件。loader 讓 webpack 可以去處理其餘類型的文件,並將它們轉換爲有效模塊。
須要引入babel的話,咱們就須要使用babel-loader
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()裏的資源。對於這個問題,有兩種解決辦法:
resolve-url-loader
,將 resolve-url-loader
設置於 loader 鏈中的 sass-loader 以前,就能夠重寫 url。可是這個辦法有個問題,那就是 resolve-url-loader
不識別scss文件的行內註釋語法,即// 註釋
,這個問題使得接入一些已存在的公共樣式庫時會存在問題,目前還在研究是否有其餘loader能夠解決,你們有較好的解決辦法也能夠一塊兒討論。對於圖片等其餘資源,咱們通常使用file-loader進行處理,它實現的功能很簡單:
{
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.Zeptoexports-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()時應該注意如下幾點: **
爲了合理利用瀏覽器緩存,通常會將不常變更的第三方庫以及公共代碼和業務代碼分開打包 因此通常項目的打包策略爲:
對於分包方式,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配置,這些實踐不只適用於這個項目,對於多頁面項目和普通項目也一樣適用。