GitChat技術(shù)雜談
前言
本文較長,為了節(jié)省你的閱讀時間,在文前列寫作思路如下:
什么是 webpack,它要解決的是什么問題?
對webpack的主要配置項(xiàng)進(jìn)行分析,雖然不會涉及太多細(xì)節(jié),但是期待在本節(jié)能讓我們知曉如果我們有什么需求,我們該從哪些配置項(xiàng)著手修改?
分析 create-react-app 的基礎(chǔ)配置文件。
分享一些自己工作中對webpack的實(shí)踐。
本文的初衷是和你一起理清 webpack 的使用邏輯,以便能更加容易的編寫及拓展自己項(xiàng)目所需的配置文件。
不過也得提前說明本文可能并不是一篇好的可以跟著操作的教程(想跟著一步步做的童鞋可以看官方示例(
https://webpack.js.org/guides/)和 webpack 入門,看這篇就夠了(
http://www.itdecent.cn/p/42e11515c10f)。
換個角度看待 webpack
近年來,前端技術(shù)蓬勃發(fā)展,我們想在 js 更方便的實(shí)現(xiàn) html , 社區(qū)就出現(xiàn)了jsx,我們覺得原生的css不夠好用,社區(qū)就提出了scss,less,針對前端項(xiàng)目越來越強(qiáng)的模塊化開發(fā)需求,社區(qū)出現(xiàn)了AMD, CommonJS , ES2015 import 等等方案。
遺憾的是,這些方案大多并不直接被瀏覽器支持,往往伴隨這些方案而生的還有另外一些,讓這些新技術(shù)應(yīng)用于瀏覽器的方案,我們用 babel 來轉(zhuǎn)換下一代的 js,轉(zhuǎn)換 jsx;我們用各種工具轉(zhuǎn)換 scss,less為css;
我們發(fā)現(xiàn)項(xiàng)目越來越復(fù)雜,代碼體積越來越大,又要開始尋找各種優(yōu)化,壓縮,分割方案。前端工程化這個過程,真是讓我們大費(fèi)精力。我們也大多是在尋找前端模塊化解決方案的過程中知曉了webpack。
的確,webpack的流行得益于野性生長的前端,其本質(zhì)是一種前端模塊化打包解決方案,但是更重要的是它又是一個可以融合運(yùn)用各種前端新技術(shù)的平臺,明白webpack的使用哲學(xué)后,只需要簡單的配置,我們就可以隨心所欲的在webpack項(xiàng)目中使用jsx/ts 使用babel/postcss等平臺提供的眾多其它功能,只需通過一條命令由源碼構(gòu)建最終可用文件。
可以不夸張的說webpack為前端的工程化開發(fā)提供了一套相對容易和完整的解決方案。一些知名的腳手架工具,也大多基于webpack(比如create-react-app)。
webpack好難!我第一次復(fù)制別人的配置文件到我的項(xiàng)目中,發(fā)現(xiàn)以自己僅有的JS知識完全看不懂時,也有這種感覺。
后來發(fā)現(xiàn)有這種感覺其實(shí)是因?yàn)樽约嚎创齱ebpack的角度錯了,對大多數(shù)前端開發(fā)者而言,以往我們接觸的各種庫,要么類似jQuery,通過$符在前端項(xiàng)目中直接運(yùn)行,所做的事情只在前端生效,要么類似express.js,在node.js項(xiàng)目中直接require后就可以使用,所做的事情只在后端生效。
webpack的不同之處就在于,雖然我們的配置文件位于前端項(xiàng)目中,但實(shí)際上它卻運(yùn)行于node.js,之后的處理結(jié)果又供前端使用(也可能供node使用)。所以學(xué)習(xí)之前,我們轉(zhuǎn)變一下思維,從node.js的角度來看webpack,很多事情就會簡單起來。
我們對下圖一定不陌生,假設(shè)現(xiàn)在我們手中有一系列相互關(guān)聯(lián)的文件js,jsx,css,less,jpg,我們一步步的看看為了把它們轉(zhuǎn)換為項(xiàng)目最終需要的,瀏覽器可識別的文件,webpack都做了什么。
顯示大圖
對 webpack 主要配置項(xiàng)的分析
如果不去考究細(xì)節(jié),我們大可把webpack簡化理解為一個函數(shù),配置文件則是其參數(shù),傳入合理的參數(shù)后,運(yùn)行函數(shù)就能得到我們想要的結(jié)果。
webpack也只是一個打包工具,它可不是什么智能ai,我們該從哪兒輸入文件,我們想把輸出結(jié)果放哪里,輸出結(jié)果應(yīng)該長什么樣,它都不知道。而我們目前和webpack函數(shù)交互的唯一方法就是通過參數(shù),這就涉及到webpack配置對象中兩個重要概念entry和output了,因此,我們的配置對象至少具備以下結(jié)構(gòu):
// 第一階段{
entry:{},
output:{}
}
入口配置 entry
理想狀態(tài)是,我們把所有自己編寫的文件都交給webpack,讓它找明里面的關(guān)系,進(jìn)過一定處理后,給出最終我們想要的結(jié)果。
遺憾的是,webpack也不會機(jī)械學(xué)習(xí),我們手頭的一堆文件之間的關(guān)系是自己確定的,一般我們的項(xiàng)目都會存在一個或幾個主文件,其它的所有的文件(模塊)都直接或間接的鏈接到了這些文件。我們在entry項(xiàng)中需要填寫的就是這些主文件的信息。
不過我們也不要嫌棄webpack笨,通過我們給的主文件路徑,通過分析它能構(gòu)建最合適的依賴關(guān)系,這意味著只有用過的代碼才會被打包,比如我們在一個文件中寫了五個模塊,但是實(shí)際只用了其中一個,打包后的代碼只會包含引用過的模塊。
webpack中很多地方的配置都有多種寫法,這也是其讓人疑惑的地方之一,很遺憾,我們的第一個配置對象entry就是如此。
entry可以是三種值:
字符串:如entry:'./src/index.js',字符串也可以是函數(shù)的返回值,如entry: () => './demo',單一入口占位符[name]值為 main(關(guān)于占位符,稍后詳述);
數(shù)組形式,如[react,react-dom],可以把數(shù)組中的多個文件打包轉(zhuǎn)換為一個chunk;
對象形式,如果我們需要配置的是多頁應(yīng)用,或者我們要抽離出指定的模塊做為公共代碼,就需要采用這種形式了,屬性名是占位符[name]的值,屬性值可以是上面的字符串和數(shù)組,如下:
// 值得注意的是入口文件有幾個就會生成幾個獨(dú)立的依賴圖譜。entry:{
main:'./src/index.js',
second:'./src/index2.js',
vendor: ['react','react-dom']
}
好吧,千辛萬苦,我們在一堆各種類型的文件中找到了入口文件,這里我們假設(shè)為./src/index.js,此時我們的配置對象如下:
// 第二階段{
entry:{
main:'./src/index.js'
},
output:{}
}
webpack依據(jù)入口文件來構(gòu)建依賴體系,每個入口文件在打包完成后都具備其獨(dú)立的依賴圖譜,在此我們暫時稱這些由主入口配置生成的文件為主js文件。
輸出配置 output
output 配置項(xiàng)作用于打包文件的輸出階段,其作用在于告知 webpack 以何種方式輸出打包文件,關(guān)于output,webpack提供了眾多的可配置選項(xiàng),我們簡單介紹下最常用的選項(xiàng)。
output 基本配置項(xiàng)
我們都另存過文件,當(dāng)我們另存一個文件時,我們需要確定另存的文件名和另存的路徑,webpack 將打包后的結(jié)果導(dǎo)出的過程就類似于此,此過程由 output 配置項(xiàng)控制,其最基本配置包括filename和path兩項(xiàng)。這兩項(xiàng)用以決定上述主js文件的存儲行為。
不過我們程序的首頁往往不需用到某個主js文件的所有代碼,實(shí)際開發(fā)中,我們常常使用一定方法對代碼進(jìn)行分割,方便按需加載,提升體驗(yàn)。這類不具備獨(dú)立依賴的文件,我們稱之為chunkfile。chunkfile的命名,在output中對應(yīng)chunkFilename項(xiàng);
此外output的publicPath項(xiàng),用于控制打包文件的相對或者絕對引用路徑,配置不當(dāng)往往造成在運(yùn)行時找不到文件。
我們補(bǔ)充配置對象中output的配置,如下:
// 第三階段{
entry:{
main:'./src/index.js'
},
output:{
path: path.join(__dirname,'./dist'),
name:'js/bundle-[name]-[hash].js',
chunkFilename:'js/[name].chunk.js',
publicPath:'/dist/'
}
}
上述代碼中用到了占位符[name],我們對占位符做統(tǒng)一解釋:
webpack中常見的占位符有多種,常見的如下:
[name]:代表打包后文件的名稱,在entry或代碼中(之后會看到)確定;
[id]:webpack給塊分配的內(nèi)部chunk id,如果你沒有隱藏,你能在打包后的命令行中看到;
[hash]:每次構(gòu)建過程中,生成的唯一 hash 值;
[chunkhash]: 依據(jù)于打包生成文件內(nèi)容的 hash 值,內(nèi)容不變,值不變;
[ext]: 資源擴(kuò)展名,如js,jsx,png等等;
output 其它配置
output配置項(xiàng)生效于保存這個過程,除了上面的基本配置,如果你想對這個階段的打包文件進(jìn)行更改,都可在此配置項(xiàng)中進(jìn)行相關(guān)設(shè)置。
比如output提供了眾多關(guān)于hash的屬性,讓我們對[hash]占位符的值有更加精細(xì)的控制,如生成方式,使用的算法,預(yù)設(shè)的長度等等;如chunkLoadTimeout屬性則允許我們設(shè)置chunk文件的請求超時時間。
工具都是依賴于需求來使用的,如果你此階段有別的需求,可點(diǎn)擊更多配置尋找解決方案。
我們已經(jīng)知道了webpack中基本的輸入和輸出配置,但是webpack對各模塊的處理過程,目前為止,對我們還是一個謎??紤]到webpack執(zhí)行于node.js環(huán)境,其本身只能理解js文件,而我們輸入的卻是一大堆不同格式的文件,毫無疑問,要做的第一件事情是對各類模塊進(jìn)行處理,這就涉及到webpack中第三個重要配置對象了—-module。
對模塊的處理:module 的配置
使用webpack時,我們常常聽說,對webpack而言,所有的文件都是模塊,前文中我也常?;煊媚K和文件,不過本質(zhì)上模塊和文件還是不同的,webpack里,文件可以當(dāng)做模塊,而模塊卻不一定是一個獨(dú)立的文件。我們先看看webpack內(nèi)置支持的模塊類型:
ES2015 import(webpack2開始內(nèi)置支持)。
CommonJS require。
AMD define 和 require 語句。
css/less/sass 中的@ import。
樣式中的 url(...) 和 html 文件中的 。
我們知道 webpack 只能處理 js 文件,我們的瀏覽器也可能不支持一些最新的 js 語法,基于此,我們需要對傳入的模塊進(jìn)行一定的預(yù)處理,這就涉及到 webpack 的又一核心概念 —- loader,使用loader,webpack允許我們打包任何JS之外的靜態(tài)資源。
loader 的作用和基本用法
webpack中,loader的配置主要在module.rules中進(jìn)行,module.rules是一個數(shù)組,我們可以把每一項(xiàng)看做一個Rule,每個Rule主要做了以下兩件事:
識別文件類型,以確定具體處理該數(shù)據(jù)的 loader,(Rule.test屬性)。
使用相關(guān)loader對文件進(jìn)行相應(yīng)的操作轉(zhuǎn)換,(Rule.use屬性)。
還記得前面我們說過,我們手頭的文件類型有 js,jsx,css,less,jpg 嗎?我們看看在 webpack 中該如何處理和轉(zhuǎn)換它們。
注:以下 loader 使用前需通過 npm/cnpm/yarn 安裝:
module: {
rules: [{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader",
options: {
presets: ["es2015", "react"]
}
},
exclude: /node_modules/
}, {
test: /\.css$/,
use: ["style-loader", "css-loader"]
}, {
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"]
}]
},
這就是 webpack 中 loader 的基本用法了,在 module.rules 數(shù)組中進(jìn)行配置即可,module.rules 是一個數(shù)組,里面每一項(xiàng)(一個Rule)表示以一定的規(guī)則匹配和處理某種或某幾種類型的文件。具體說來:
Rule.test:表示匹配規(guī)則,它是一個正則表達(dá)式。
Rule.use:表示針對匹配的文件將使用的處理loader,其值可以是字符串,數(shù)組和對象,當(dāng)是對象形式時,我們可以使用options等命令進(jìn)行進(jìn)一步的配置。
Rule中的其它一些規(guī)則也大多圍繞匹配條件和應(yīng)用結(jié)果展開,如Rule.exclude和Rule.include表示應(yīng)該匹配或不應(yīng)該匹配某資源;Rule.oneOf表示對該資源只應(yīng)用第一個匹配的loader;Rule.enforce則用于指定loader的種類。
loader 可以做什么
webpack 的強(qiáng)大之處在于,可以輕松在其中應(yīng)用其它平臺提供的功能,比如說 babel,postcss 本身都是獨(dú)立的平臺。在webpack 中只需要添加 babel-loader 和 postcss-loader 就可以使用。
這兩個平臺本身也提供眾多的配置項(xiàng),默認(rèn)分別可在 .babelrc 和 postcss.config.js 中完成,webpack 并不影響這些配置文件的使用。
不過需要說明的可能很多童鞋是在學(xué)習(xí)webpack時才接觸這兩個平臺,導(dǎo)致在這兩個平臺上遇到的問題誤以為是webpack的問題。
除了上述的轉(zhuǎn)換編譯,通過 loader,webpack 還允許我們實(shí)現(xiàn)以下功能:
轉(zhuǎn)換編譯:
script-loader/babel-loader/ts-loader/coffee-loader等。
處理樣式:
style-loader/css-loader/less-loader/sass-loader/postcss-loader等。
處理文件:
raw-loader/url-loader/file-loader/等。
處理數(shù)據(jù):csv-loader/xml-loader等。
處理模板語言:
html-loader/pug-loader/jade-loader/markdown-loader等。
清理和測試:
mocha-loader/eslint-loader等。
關(guān)于各個loader更詳細(xì)的介紹,可點(diǎn)擊loaders查看。
module.noParse
關(guān)于 module,另一個常用的配置項(xiàng)為module.noParse,通過它,我們在構(gòu)建過程中可以忽略大型的 library 以提高構(gòu)建效率。
我們來整理一下此階段,我們的配置對象代碼,如下:
// 第四階段{
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, './dist'),
name: 'js/bundle-[name].js',
chunkFilename: 'js/[name].chunk.js',
publicPath: '/dist/'
}, module: {
rules: [{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader",
options: {
presets: ["es2015", "react"]
}
},
exclude: /node_modules/
}, {
test: /\.css$/,
use: ["style-loader", "css-loader"]
}, {
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"]
}]
}
}
進(jìn)過這一階段的處理,我們的代碼其實(shí)已經(jīng)可以輸出使用了。不過這樣的輸出可能還不能讓人滿意,我們想要抽離公共代碼;我們想統(tǒng)一修改所有代碼中的某些值;我們還想對代碼進(jìn)行壓縮,去除所有的console… , 總之這一階段的代碼還是存在很大的改進(jìn)空間的,這就是plugin的用武之地了。
plugins 的配置
webpack 稱plugins為其backbone,一切l(wèi)oader不能做的處理都可由plugins來做。此評價足見其重要性。
鑒于插件如此重要,webpack內(nèi)置了眾多的常用的plugins,無需額外安裝就可直接使用。我們先看看plugins的基本配置方法,然后再分類介紹一下常用的plugins。
plugins 的使用方法
plugins是一個數(shù)組,數(shù)組中的每一項(xiàng)都是某一個plugin的實(shí)例,plugins數(shù)組甚至可以存在一個插件的多個實(shí)例。
下面代碼中,分別展示了webpack內(nèi)置插件和第三方插件的使用方法:
// 第三方插件需要在安裝后引入const CleanWebpackPlugin = require("clean-webpack-plugin");
{
...
plugins:[ new webpack.DefinePlugin({ "process.env": {
NODE_ENV: JSON.stringify("production")
}
}), new CleanWebpackPlugin(["js"], {
root: __dirname + "/stu/",
verbose: true,
dry: false
})
]
}
一種插件其實(shí)就是一種函數(shù),通過傳入不同的參數(shù),插件可按我們的需求實(shí)現(xiàn)不同的功能。不過插件數(shù)量眾多,我們甚至還可以自己來寫插件,每個插件還有自己特定的配置規(guī)則,這也是webpack讓人覺得難學(xué)的地方之一,不過好在作為一個工具,對于我們大多數(shù)人最需要掌握的plugins并不是那么多,其它的待真的有相關(guān)需求再邊查邊學(xué)也不遲,webpack的插件列表可參看這里(
https://webpack.js.org/plugins/)。
常用 plugins 的介紹
plugins功能眾多,但是大多數(shù)plugin的功能主要集中在兩方面:
對前一階段打包后的代碼進(jìn)行處理,如添加替換一些內(nèi)容,分割代碼為多塊,添加一些全局設(shè)置等。
輔助輸出,如自動生成帶有鏈接的index.html,對生成文件存儲文件夾做一定的清理等。
對代碼進(jìn)行處理
BannerPlugin:給代碼添加版權(quán)信息,如在plugins數(shù)組中添加new BannerPlugin(‘GitChat’)后能在打包生成的所有文件前添加注釋GitChat詳見(
https://webpack.js.org/plugins/banner-plugin/)。
CommonsChunkPlugin,用于抽離代碼,具有多種用途 詳情查看CommonsChunkPlugin(
https://webpack.js.org/plugins/commons-chunk-plugin)。
抽離不同文件的共享代碼,減少chunk間的重復(fù)代碼,有效利用緩存。
抽離可能整個項(xiàng)目都在使用的第三方模塊,比如 react react-dom。
將多個子 chunk 中的共用代碼打包進(jìn)父 chunk 或使用異步加載的單獨(dú)chunk。
抽離 Manifest 這類每次打包都會變化的內(nèi)容,減輕打包時候的壓力,提升構(gòu)建速度。
CompressionWebpackPlugin:使用配置的算法(如gzip)壓縮打包生成的文件,詳見(
https://webpack.js.org/plugins/compression-webpack-plugin)。
DefinePlugin:創(chuàng)建一個在編譯時可配置的全局常量,如果你自定義了一個全局變量PRODUCTION,可在此設(shè)置其值來區(qū)分開發(fā)還是生產(chǎn)環(huán)境詳見(
https://webpack.js.org/plugins/define-plugin/)。
EnvironmentPlugin:實(shí)際上是DefinePlugin插件中對process.env進(jìn)行設(shè)置的簡寫形式,如new webpack.EnvironmentPlugin(['NODE_ENV', 'DEBUG'])將設(shè)置process.env.NODE_ENV='DEBUG',EnvironmentPlugin(
https://webpack.js.org/plugins/environment-plugin/)。
ExtractTextWebpackPlugin:抽離css文件為單獨(dú)的css文件,詳見(
https://webpack.js.org/plugins/extract-text-webpack-plugin)。
ProvidePlugin:全局自動加載模塊,如添加new webpack.ProvidePlugin({$: 'jquery', jQuery: 'jquery'})后,則全局不用在導(dǎo)入jquery就可以直接使用$,ProvidePlugin(
https://webpack.js.org/plugins/provide-plugin/)。
UglifyjsWebpackPlugin:使用前需要先安裝,基于UglifyJS壓縮代碼,支持其所有配置 UglifyjsWebpackPlugin(
https://webpack.js.org/plugins/uglifyjs-webpack-plugin/)。
輔助輸出打包后的代碼
HtmlWebpackPlugin:使用前需要先安裝,為你自動生成一個html文件,該文件將自動依據(jù)entry的配置引入依賴,如果你的文件名中添加了[hash]等占位符,這將非常有用, 詳見(
https://webpack.js.org/plugins/html-webpack-plugin/)。
CleanWebpackPlugin:使用前需要先安裝,此插件允許你在配置以后,每次打包時,清空所配置的文件夾,如果你每次打包的文件名不同,這將非常有用 GitHub - clean-webpack-plugin(
https://github.com/johnagan/clean-webpack-plugin)。
通過上述對不同插件的描述,你一定大致明白了,插件可以做什么,之后在開發(fā)的過程中,如果你遇到的什么需要在此階段解決的問題,大可搜索看看是否有相關(guān)的插件,推薦查閱awesome-webpack(
https://github.com/webpack-contrib/awesome-webpack#webpack-plugins)。
學(xué)習(xí)了插件以后,現(xiàn)在我們的配置對象是如下這樣:
// 第5階段{
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, './dist'),
name: 'js/bundle-[name].js',
chunkFilename: 'js/[name].chunk.js',
publicPath: '/dist/'
}, module: {
rules: [{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader",
options: {
presets: ["es2015", "react"]
}
},
exclude: /node_modules/
}, {
test: /\.css$/,
use: ["style-loader", "css-loader"]
}, {
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"]
}]
},
plugins: [ new webpack
.optimize
.CommonsChunkPlugin({
name: 'vendor',
filename: "js/[name]-[chunkhash].js"
}), new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}), new webpack.ProvidePlugin({
Promise: "exports-loader?global.Promise!es6-promise",
fetch: "exports-loader?self.fetch!whatwg-fetch"
}), new HtmlWebpackPlugin({
filename: "index.html",
template: "app/index.html",
inject: "body"
}), new CleanWebpackPlugin(["js"], {
root: __dirname + "/stu/",
verbose: true,
dry: false
}), new webpack.DefinePlugin({ "process.env": {
NODE_ENV: JSON.stringify("production")
}
})
]
}
至此,從輸入entry->處理loaders/plugins->輸出output,我們講解了 webpack 的核心功能,不過webpack還提供其它的一些配置項(xiàng),這些配置項(xiàng)大多從兩方面起作用,輔助開發(fā)、對構(gòu)建過程中的一些細(xì)節(jié)做調(diào)整。對這些屬性,下面只做簡單的介紹。
其它的一些配置
輔助開發(fā)的相關(guān)屬性
devtool:
打包后的代碼和原始的代碼往往存在較大的差異,此選項(xiàng)控制是否生成,以及如何生成 source map,用以幫助你進(jìn)行調(diào)試,詳情可查看 Devtool(
https://webpack.js.org/configuration/devtool/)。
devServer:
通過配置devServer選項(xiàng),你可以開啟一個本地服務(wù)器,webpack為此本地服務(wù)器提供了非常多的配置選項(xiàng),查看 dev-server (
https://webpack.js.org/configuration/dev-server/),你會發(fā)現(xiàn)通過合適的配置,你可以擁有所有本地服務(wù)器可提供的功能。
watch:
啟用 Watch 模式后,webpack 將持續(xù)監(jiān)聽任何已解析文件的更改,重新構(gòu)建文件,Watch 模式默認(rèn)關(guān)閉,在開發(fā)時候如果開啟會很方便。
watchOptions:
一組用來定制 Watch 模式的選項(xiàng): 詳見 watch(
https://webpack.js.org/configuration/watch/)。
performance:
本配置讓你設(shè)置打包后命令行中該如何展示性能提示,比如是否開啟提示,資源如果超過某個大小時該警告還是報錯,詳見 performance (
https://webpack.js.org/configuration/performance/)。
stats:
本選項(xiàng)讓你配置打包過程中輸出的內(nèi)容,如沒有輸出none,標(biāo)準(zhǔn)輸出normal,全部輸出verbose,只輸出錯誤errors-only等等。
精細(xì)配置相關(guān)屬性
content:設(shè)置基礎(chǔ)路徑,默認(rèn)使用當(dāng)前目錄。
resolve:
確定模塊如何被解析,webpack已經(jīng)提供了合理的默認(rèn)值,不過通過你的自定義配置,可以對模塊解析實(shí)現(xiàn)更加精細(xì)的控制,如對某些常用模塊可以通過設(shè)置別名以更容易引用,也可在此處設(shè)置可被忽略的后綴名,詳見 resolve(
https://webpack.js.org/configuration/resolve/)。
target:
告知 webpack 需要打包的代碼執(zhí)行的環(huán)境,針對 node 和 web 打包過程會有所不同,詳見Target(
https://webpack.js.org/configuration/target/)。
externals:
讓打包生成的代碼中不添加某依賴項(xiàng),而讓這些依賴項(xiàng)直接從用戶環(huán)境中獲取,在進(jìn)行庫的開發(fā)時非常有用。
node:
是一個對象,其中每個屬性都是 Node.js 全局變量或模塊的名稱,每一項(xiàng)的設(shè)置值都可以是(true/mock/empty/false)中的一種,以確定這些node中的對象在其它環(huán)境中是否可用。
此外webpack還具備其它一些用的比較少的配置對象,詳見 Other Options(
https://webpack.js.org/configuration/other-options/)。
至此,我們了解了webpack常用的配置項(xiàng)及其意義。為了檢測我們的學(xué)習(xí)成果,我們一起分析一個中等項(xiàng)目中的webpack配置文件。配置文件來自于create-react-app,使用create-react-app新建項(xiàng)目后,執(zhí)行npm run eject可看到多個配置文件,這里我們選擇webpack.dev.js。
分析 create-react-app 中 webpack 的配置
const path = require('path');const webpack = require('webpack');const HtmlWebpackPlugin = require('html-webpack-plugin');const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');const eslintFormatter = require('react-dev-utils/eslintFormatter');const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');module.exports = {
devtool: 'cheap-module-source-map',
entry: [ require.resolve('react-dev-utils/webpackHotDevClient'), require.resolve('./polyfills'), require.resolve('react-error-overlay'), 'src/index.js'
],
output: {
path: '/build/',
pathinfo: true,
filename: 'static/js/bundle.js',
chunkFilename: 'static/js/[name].chunk.js',
publicPath: '',
devtoolModuleFilenameTemplate: info =>
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
},
resolve: {
modules: ['node_modules'],
extensions: ['.web.js', '.js', '.json', '.web.jsx', '.jsx'],
alias: { 'react-native': 'react-native-web',
},
plugins: [ new ModuleScopePlugin('/src'),
],
}, module: {
strictExportPresence: true,
rules: [{
test: /\.(js|jsx)$/,
enforce: 'pre',
use: [{
options: {
formatter: eslintFormatter,
},
loader: require.resolve('eslint-loader'),
}, ],
include: 'src',
}, {
exclude: [/\.html$/,/\.(js|jsx)$/,/\.css$/,/\.json$/,/\.bmp$/,/\.gif$/,/\.jpe?g$/,/\.png$/],
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
}, {
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
}, {
test: /\.(js|jsx)$/,
include: 'src',
loader: require.resolve('babel-loader'),
options: {
cacheDirectory: true,
},
}, {
test: /\.css$/,
use: [ require.resolve('style-loader'), {
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
}, {
loader: require.resolve('postcss-loader'),
options: {
...
},
},
],
}, ],
},
plugins: [ new InterpolateHtmlPlugin({
NODE_ENV:'development',
PUBLIC_URL:''
}), new HtmlWebpackPlugin({
inject: true,
template: 'public/index.html',
}), new webpack.NamedModulesPlugin(), new webpack.DefinePlugin({ 'process.env':{
NODE_ENV:"development",
PUBLIC_URL:'" "'
}
}), new webpack.HotModuleReplacementPlugin(), new CaseSensitivePathsPlugin(), new WatchMissingNodeModulesPlugin(paths.appNodeModules), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
},
performance: {
hints: false,
},
};
對可能和你看到的webpack.config.dev.js有所不同的說明:
npm run reject 之前,對create-react-app的一些設(shè)置會影響這里看到的配置文件。
原始的 webpack.config.dev.js 中,部分值由外部函數(shù)生成,相關(guān)值,在上述代碼中直接改為了確定的結(jié)果,如env.raw在上述代碼中被替換為:
{
NODE_ENV:'development',
PUBLIC_URL:''
}
create-react-app在開發(fā)環(huán)境并不生成真實(shí)的文件到硬盤,上述代碼中的部分路徑可能有誤,見諒。
推薦在看下面的分析前,花三分鐘看看上述文件,如果都能看得懂,那么恭喜你,你已經(jīng)明白webpack的運(yùn)作方式了,快去自己的項(xiàng)目中實(shí)踐吧,如果還有疑惑,也不要緊,我們一起來分析。
webpack.config.dev.js 執(zhí)行于 node 環(huán)境
首先,我們應(yīng)該明確webpack.config.dev.js執(zhí)行于node環(huán)境,目的在于返回webpack需要的配置對象,因此其中可以使用node提供的一些特殊變量和語法,比如__dirname,又如引入模塊時采用CommonJS模式。
此文件的開頭,首先通過require語句引入了path,webpack和一系列webpack插件,除了HtmlWebpackPlugin在前文中我們見過,其它的我們都未曾見過,其實(shí)這些大多是create-react-app針對webpack已有的插件改進(jìn)或新開發(fā)的插件,所以不熟悉也正常,隨后我們將一個個的弄清楚它們是干嘛的。
對 module.exports 的分析
devtool
此處的配置值為cheap-module-source-map,代表不帶列映射的 SourceMap,將加載的 Source Map 簡化為每行單獨(dú)映射。
entry
此處的entry是一個數(shù)組,代表著四項(xiàng)的代碼都會添加到打包結(jié)果之中。
webpackHotDevClient可以被看做具有更好體驗(yàn)的WebpackDevServer。
./ployfill.js用以在瀏覽器中支持
promise/fetch/object-assign。
react-error-overlay在開發(fā)環(huán)境中使用,強(qiáng)制顯示錯誤頁面。
./src/index.js則是我們的app的主入口。
output
在實(shí)際使用create-react-app的過程中,我們并看不見開發(fā)環(huán)境的打包結(jié)果,因此此處的說明僅供參考。
path指定,打包后文件存放的位置為/build/。
pathinfo為true,在打包文件后,在其中所包含引用模塊的信息,這在開發(fā)環(huán)境中有利于調(diào)試。
filename指定了打包的名字和基本的引用路徑static/js/bundle.js。
chunkFilename:指定了非入口文件的名稱static/js/[name].chunk.js。
publicPath:指定服務(wù)器讀取時的路徑,此處設(shè)置為。
devtoolModuleFilenameTemplate:這里是一個函數(shù),指定了map位于磁盤的位置。
resolve
modules:指定了模塊的搜索的位置,這里設(shè)置為node_modules。
extensions:指明在引用模塊時哪些后綴名可以忽略,這里忽略的文件名包括.js/.jsx/.web.js/.web.jsx等。
alias:創(chuàng)建 import 或 require 的別名,使得部分模塊的引用變得簡單,安裝上文的設(shè)置,現(xiàn)在我們可以直接引用react-native和react-native-web了。
plugins:此處使用了 ModuleScopePlugin 的實(shí)例,用以限制自己編寫的模塊只能從src目錄中引入。
modules
strictExportPresence:這里設(shè)置為 true,表明文件中如果缺少 exports 時會直接報錯而不是警告。
rules:
Rule1:對 js/jsx 文件前置使用 eslintFormatter,設(shè)置 formatter 格式為 eslintFormatter。
Rule2:對exclude中的眾多文件類型不使用file-loader,并設(shè)置其它文件打包后的名稱按'static/media/[name].[hash:8].[ext]'格式設(shè)置。
Rule3: 對js/jsx文件調(diào)用babel-loader處理轉(zhuǎn)換。
Rule4: 對css文件,按順序調(diào)用style-loader,css-loader,postcss-loader進(jìn)行處理。
plugins
這里的一些插件,有的可能我們還比較陌生,我們一一介紹。
InterpolateHtmlPlugin:和HtmlWebpackPlugin串行使用,允許在index.html中添加變量。
HtmlWebpackPlugin:自動生成帶有入口文件引用的index.html。
NamedModulesPlugin:當(dāng)開啟 HMR 的時候使用該插件會顯示模塊的相對路徑,建議用于開發(fā)環(huán)境。
DefinePlugin:這里我們設(shè)置了process.env.NODE_ENV的值為development。
HotModuleReplacementPlugin:啟用模塊熱替換。
CaseSensitivePathsPlugin:如果路徑有誤則直接報錯。
WatchMissingNodeModulesPlugin:此插件允許你安裝庫后自動重新構(gòu)建打包文件。
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/):忽略所匹配的moment.js。
node
設(shè)置node的dgram/fs/let/tls模塊的的值,如果在其它環(huán)境中使用時值為empty。
performance
hints: false:不提示測試環(huán)境的打包結(jié)果。
上文一直討論的是,webpack各設(shè)置項(xiàng)的基本意義,目的在于讓你在有相關(guān)需求時,能知道該從哪一項(xiàng)下手查詢。不過看到這里,如果你之前從未上手操作過webpack可能依舊不知道該如何使用,下面我分析一下,我在自己的項(xiàng)目中是如何使用的。
一些工程實(shí)踐建議
官方文檔的 guides (
https://doc.webpack-china.org/guides/) 部分已經(jīng)就如何實(shí)踐提出了較多的建議,建議閱讀以下內(nèi)容前先行閱讀。
結(jié)合 npm 使用
webpack在安裝后有多種調(diào)用方法。
在命令行中直接傳入?yún)?shù)使用(這個實(shí)際我用的比較少)。
自定義 webpack.config.js文件,在其中完成配置,然后在命令行中執(zhí)行webpack --config webpack.config.js來使用,配置文件可以是任何其它名稱(如果是webpack.config.js,我們直接使用webpack命令)。
結(jié)合npm使用,在package.json文件中的scripts對象中添加相關(guān)命令使用,之后通過npm run使用,如下:
"scripts": { "build:prod": "webpack --progress --colors --watch --config webpack.prod.js", "build:dev": "webpack --progress --colors --watch --config webpack.dev.js"}
上面我們分別構(gòu)建了webpack.prod.js和webpack.dev.js來分別生成開發(fā)環(huán)境和生產(chǎn)環(huán)境的代碼,在命令行中執(zhí)行npm run build:prod和npm run build:dev即可生成對應(yīng)代碼。
為生產(chǎn)環(huán)境指定合理的緩存
關(guān)于緩存,官方文檔中有一節(jié)講解的非常詳細(xì),請參見 緩存(
https://doc.webpack-china.org/guides/caching)。
合理分割代碼
webpack提供了三種分割代碼的方法,分別是通過entry,通過CommonsChunkPlugin插件和通過動態(tài)import(在webpack1.x中時也常常使用require.ensure來依據(jù)路由分割代碼)。
entry的配置常用于多頁應(yīng)用,CommonsChunkPlugin的使用前文已做簡要敘述,下面簡單敘述下代碼分割原則及我實(shí)際工作中是如何使用動態(tài)import來分割代碼的。
分割原則
目前工作中主要依據(jù)兩個原則來分隔代碼:
前端路由:依據(jù)路由對應(yīng)的頁面進(jìn)行分割,這種分割之后的體驗(yàn)類似于小程序中每次打開新頁加載對應(yīng)頁面的js文件。
針對邏輯交互比較復(fù)雜的頁面,如果某個較復(fù)雜的組件需被某操作觸發(fā)后才呈現(xiàn),也會把該組件分割出來。
分割方法
我們知道動態(tài)import返回值其實(shí)是一個Promise,基于此,對應(yīng)于我用的React,我常采用以下函數(shù)輔助加載。
// lib.js 定義懶加載函數(shù)module.exports.withLazyLoading = function withLazyLoading(getComponent,Spinner = null) { return class LazyLoadingWrapper extends React.Component {
constructor(props) {
super(props); this.state = ({
Component: null,
})
}
componentWillMount() { const {onLoadingStart, onLoadingEnd, onError} = this.props;
onLoadingStart();
getComponent()
.then(esModule => { this.setState({Component: esModule.default})
})
.catch(err => {
onError(err, this.props)
})
}
render() { const {Component} = this.state; if (!Component) return Spinner; return
}
}
};
對代碼的分割方法如下:
// 在需要的地方調(diào)用懶加載函數(shù)import {withLazyLoading} from "lib";// import {Loading} from 'Loadings';
export default withLazyLoading(
() => { return import (/* webpackChunkName: "ConCard" */ "../../containers/ConCard.js")
}, Loading());
簡要的說明一下上述代碼的意義,懶加載函數(shù)withLazyLoading接受動態(tài)import的組件和一個加載動畫作為參數(shù),動態(tài)import的組件加載成功前顯示加載動畫組件,成功后顯示import的組件,通過自定義各種各樣的Spinner加載動畫,我們可以實(shí)現(xiàn)優(yōu)雅的js文件加載過程。
觀察打包后文件的結(jié)構(gòu),合理進(jìn)行優(yōu)化
使用webpack --json > stats.json命令可以生成一個包含依賴關(guān)系的json文件。webpack提供了多種可視化工具幫我們分析這個文件,我最喜歡的工具插件是BundleAnalyzerPlugin,可通過下述方法引入該插件:
new BundleAnalyzerPlugin({
analyzerMode: 'static'})
添加此插件,再次構(gòu)建完成時,瀏覽器中將自動打開一個類似下面這樣的網(wǎng)頁:
這樣我們可以輕易分析我們的代碼分割是否合理,比如:
分割后文件過大的主要原因是在于引入了那些模塊。
分析大多后的多文件中存不存在對某些比較大的模塊的重復(fù)引用,方便我們進(jìn)一步修正自己的配置文件。
上圖是我之前項(xiàng)目中的一張截圖,第一次見到這張圖時還是給了我很多后期優(yōu)化的思路的,引用chat.js的同時引入了moment.js,而實(shí)際上該頁面只有一張圖表,這讓我考慮另尋圖表解決方案,lodash,velocity在最初的項(xiàng)目中使用過,后逐步去除,屬于遺留代碼,現(xiàn)在還存在說明在局部可能還是用到了,這都是之后編碼的改進(jìn)方向。
后記
總覺得技術(shù)類的文章也是該有生命力的,花了好久寫完本文,回頭看發(fā)現(xiàn)有的內(nèi)容還是沒有表達(dá)或交待清楚。所以有任何建議,請隨意提出,我們在Chat中繼續(xù)討論,我也將對本文做長期持續(xù)的修改。
針對webpack3.5.5官網(wǎng)文檔,使用mindNode制作了一個思維導(dǎo)圖的草稿,此思維導(dǎo)圖還需完善,之后將持續(xù)修改,在此處(
https://github.com/zhangwang1990/blogs/tree/master/sources/mindMaps)可查看,該思維導(dǎo)圖示例如下。
另外,關(guān)于webpack1和webapck2的區(qū)別,官方文檔中有一部分做了詳細(xì)的講解(
https://webpack.js.org/guides/migrating/),所以本文中不做贅述,看完以后如果還有疑問,之后我們再詳細(xì)討論。