webpack 從入門到工程實(shí)踐

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ì)討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容