所謂打包多頁面,就是同時打包出多個 html 頁面,打包多頁面也是使用 html-webpack-plugin,只不過,在引入插件的時候是創(chuàng)建多個插件對象,因為一個html-webpack-plugin 插件對象只能打包出一個 html 頁面。如:
module.exports = { entry: { index: "./src/index.js", // 指定打包輸出的chunk名為index foo: "./src/foo.js" // 指定打包輸出的chunk名為foo }, plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", // 要打包輸出哪個文件,可以使用相對路徑 filename: "index.html", // 打包輸出后該html文件的名稱 chunks: ["index"] // 數(shù)組元素為chunk名稱,即entry屬性值為對象的時候指定的名稱,index頁面只引入index.js }), new HtmlWebpackPlugin({ template: "./src/index.html", // 要打包輸出哪個文件,可以使用相對路徑 filename: "foo.html", // 打包輸出后該html文件的名稱 chunks: ["foo"] // 數(shù)組元素為chunk名稱,即entry屬性值為對象的時候指定的名稱,foo頁面只引入foo.js }), ] }
打包多頁面時,關(guān)鍵在于 chunks 屬性的配置,因為在沒有配置 chunks 屬性的情況下,打包輸出的 index.html 和 foo.html 都會同時引入 index.js 和 foo.js。
所以必須配置 chunks 屬性,來指定打包輸出后的 html 文件中要引入的輸出模塊,數(shù)組的元素為 entry 屬性值為對象的時候指定的 chunk 名,如上配置,才能實現(xiàn),index.html 只引入 index.js,foo.html 只引入 foo.js 文件
二、配置 source-map
source-map 就是源碼映射,主要是為了方便代碼調(diào)試,因為我們打包上線后的代碼會被壓縮等處理,導(dǎo)致所有代碼都被壓縮成了一行,如果代碼中出現(xiàn)錯誤,那么瀏覽器只會提示出錯位置在第一行,這樣我們無法真正知道出錯地方在源碼中的具體位置。webpack 提供了一個 devtool 屬性來配置源碼映射。
let foo = 1; console.lg(`console對象的方法名log寫成了lg`); // 源文件第二行出錯 index.js:1 Uncaught TypeError: console.lg is not a function at Object.anonymous> (index.js:1) at o (index.js:1) at Object.anonymous> (index.js:1) at o (index.js:1) at index.js:1 at index.js:1
源碼中出錯的位置明明是第二行代碼,而瀏覽器中提示的錯誤確實在第一行,所以如果代碼很復(fù)雜的情況下,我們就無法找到出錯的具體位置
devtool 常見的有 6 種配置:
1、source-map: 這種模式會產(chǎn)生一個.map文件,出錯了會提示具體的行和列,文件里面保留了打包后的文件與原始文件之間的映射關(guān)系,打包輸出文件中會指向生成的.map文件,告訴js引擎源碼在哪里,由于源碼與.map文件分離,所以需要瀏覽器發(fā)送請求去獲取.map文件,常用于生產(chǎn)環(huán)境,如:
//# sourceMappingURL=index.js.map
2、eval: 這種模式打包速度最快,不會生成.map文件,會使用eval將模塊包裹,在末尾加入sourceURL,常用于開發(fā)環(huán)境,如:
//# sourceURL=webpack:///./src/index.js
3、eval-source-map: 每個 module 會通過 eval() 來執(zhí)行,并且生成一個 DataUrl 形式的 SourceMap (即 base64 編碼形式內(nèi)嵌到 eval 語句末尾), 但是不會生成 .map 文件,可以減少網(wǎng)絡(luò)請求*,但是打包文件會非常大*。
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ== //# sourceURL=webpack-internal:///./src/index.js
4、cheap-source-map: 加上 cheap,就只會提示到第幾行報錯,少了列信息提示,同時不會對引入的庫做映射,可以提高打包性能,但是會產(chǎn)生 .map 文件。
5、cheap-module-source-map: 和 cheap-source-map 相比,加上了 module,就會對引入的庫做映射,并且也會產(chǎn)生 .map 文件,用于生產(chǎn)環(huán)境。
6、cheap-module-eval-source-map: 常用于開發(fā)環(huán)境,使用 cheap 模式可以大幅提高 souremap 生成的效率,加上 module 同時會對引入的庫做映射,eval 提高打包構(gòu)建速度,并且不會產(chǎn)生 .map 文件減少網(wǎng)絡(luò)請求。
凡是帶 eval 的模式都不能用于生產(chǎn)環(huán)境,因為其不會產(chǎn)生 .map 文件,會導(dǎo)致打包后的文件變得非常大。通常我們并不關(guān)心列信息,所以都會使用 cheap 模式,但是我們也還是需要對第三方庫做映射,以便精準找到錯誤的位置。
三、watch 和 watchOptions 配置
webpack 可以監(jiān)聽文件變化,當它們修改后會重新編譯,如果需要開啟該功能,那么需要將 watch 設(shè)置為 true,具體監(jiān)聽配置通過 watchOptions 進行相應(yīng)的設(shè)置。
module.exports = { watch: true, watchOptions: { poll: 1000, // 每隔一秒輪詢一次文件是否發(fā)生變化 aggregateTimeout: 1000, // 當?shù)谝粋€文件更改,會在重新構(gòu)建前增加延遲。這個選項允許 webpack 將這段時間內(nèi)進行的任何其他更改都聚合到一次重新構(gòu)建里 ignored: /node_modules/ // 排除一些文件的監(jiān)聽 } }
四、三個常見小插件的使用
1、clean-webpack-plugin: 其作用就是每次打包前先先將輸出目錄中的內(nèi)容進行清空,然后再將打包輸出的文件輸出到輸出目錄中。
const {CleanWebpackPlugin} = require("clean-webpack-plugin"); module.exports = { plugins: [ new CleanWebpackPlugin() // 打包前清空輸出目錄 ] }
需要注意的是,require("clean-webpack-plugin)的結(jié)果是一個對象而不是類,這個對象中的 CleanWebpackPlugin 屬性才是一個類,我們就是用這個類去創(chuàng)建插件對象。
2、copy-webpack-plugin: 其作用就是打包的時候帶上一些 readMe.md、history.md 等等一起輸出到輸出目錄中。
module.exports = { plugins: [ new CopyWebpackPlugin([ { from: "./readMe.md", // 將項目根目錄下的readMe.md文件一起拷貝到輸出目錄中 to: "" // 屬性值為空字符串則表示是輸出目錄 } ]) ] }
3、BannerPlugin: 其作用就是在打包輸出的 js 文件的頭部添加一些文字注釋,比如版權(quán)說明等等,BannerPlugin 是 webpack 內(nèi)置的插件,如:
module.exports = { plugins: [ new webpack.BannerPlugin("Copyright © 2019") // 在js文件頭部添加版權(quán)說明 ] }
五、webpack 跨域問題
為什么 webpack 會存在跨域問題?因為 webpack 打包的是前端代碼,其最終會被部署到前端服務(wù)器上,而前后端代碼通常部署在不同的服務(wù)器上,即使是部署在同一個服務(wù)器上,所使用的端口也是不一樣的,當前端代碼通過 ajax 等手段向后端服務(wù)器獲取數(shù)據(jù)的時候,由于前后端代碼不在同一個域中,故存在跨域問題。
比如,我們通過 webpack 的 devServer 來運行部署我們的前端應(yīng)用代碼,devServer 啟動在 8080 端口上,而前端應(yīng)用代碼中會通過 ajax 請求后端數(shù)據(jù),后端服務(wù)器啟動在 3000 端口上。
// index.js const xhr = new XMLHttpRequest(); // xhr.open("get", "http://localhost:3000/api/test"); // 由于跨域問題無法直接訪問到http://localhost:3000下的資源 xhr.open("get", "/api/test"); // 本來是要訪問http://localhost:3000/api/test xhr.onreadystatechange = () => { if (xhr.readyState === 4) { console.log(xhr.responseText); } } xhr.send();
由于前端代碼是運行在瀏覽器中的,如果在前端代碼中直接通過 ajax 向http://localhost:3000/api/test 發(fā)起請求獲取數(shù)據(jù),那么由于瀏覽器同源策略的影響,會存在跨域的問題,所以必須訪問 /api/test。但是這樣訪問又會出現(xiàn) 404 問題,因為其實訪問的是 http://localhost:8080/api/test,8080 服務(wù)器上是沒有該資源的,解決辦法就是通過 devServer 配置一個代理服務(wù)器
module.exports = { devServer: { proxy: { "/api": "http://localhost:3000" // 路徑以/api開頭則代理到localhost:3000上 } } }
訪問 http://localhost:8080/api/test
就會被代理到http://localhost:3000/api/test 上,proxy 還支持路徑的重寫,如果 3000 端口服務(wù)器上并沒有 /api/test 路徑,只有 /test 路徑,那么就可以對路徑進行重寫,將 /api 替換掉
module.exports = { devServer: { proxy: { "/api": { target: "http://localhost:3000", pathRewrite: {"/api": ""} // 將/api替換掉 } } } }
訪問 http://localhost:8080/api/test
就會被代理到 http://localhost:3000/test 上
如果前端只是想 mock 一些數(shù)據(jù),并不需要真正的去訪問后臺服務(wù)器,那么我們可以通過 devServer 提供的 before 鉤子函數(shù)獲取到內(nèi)置的服務(wù)器對象進行處理請求,這個內(nèi)置的服務(wù)器對象就是 webpack 的 devServer 即 8080 端口的 server,因為是在同一個服務(wù)器中請求數(shù)據(jù)所以也不會出現(xiàn)跨域問題。
before(app) { // 此app即webpack的devServer app.get("/api/test", (req, res, next) => { res.json({name: "even"}); }) }
我們還可以不通過 webpack 提供的 devServer 來啟動 webpack,而是使用自己服務(wù)器來啟動 webapck。
const express = require("express"); const app = express(); const webpack = require("webpack"); // 引入webpack const config = require("./webpack.config.js"); // 引入配置文件 const compiler = webpack(config); // 創(chuàng)建webpack的編譯器 const middleWare = require("webpack-dev-middleware"); //引入webpack的中間件 app.use(middleWare(compiler)); // 將compiler編譯器交給中間件處理 app.get("/api/test", (req, res, next) => { res.json({name: "lhb"}); }); app.listen(3000);
通過自定義服務(wù)器啟動 webpack,這樣 webpack 中的前端代碼請求數(shù)據(jù)就和服務(wù)器的資源在同一個域中了。
六、resolve 配置
resolve 用于配置模塊的解析相關(guān)參數(shù)的,其屬性值為一個對象。
1、modules: 告訴 webpack 解析模塊時應(yīng)該搜索的目錄,即 require 或 import 模塊的時候,只寫模塊名的時候,到哪里去找,其屬性值為數(shù)組,因為可配置多個模塊搜索路徑,其搜索路徑必須為絕對路徑,比如,src 目錄下面有一個 foo.js 文件和 index.js 文件:
// index.js const foo = require("./foo"); // 必須寫全foo.js模塊的路徑 // const foo = require("foo"); // resolve.modules中配置了模塊解析路徑為.src目錄,則可用只寫foo即可搜索到foo.js模塊 console.log(foo); module.exports = { resolve: { modules: [path.resolve(__dirname, "./src/"), "node_modules"] }, }
由于 resolve.modules 中配置了 ./src 目錄作為模塊的搜索目錄,所以 index.js 中可以只寫模塊名即可搜索到 foo.js 模塊
2、alias: 用于給路徑或者文件取別名,當 import 或者 require 的模塊的路徑非常長時,我們可以給該模塊的路徑或者整個路徑名+文件名都設(shè)置成一個別名,然后直接引入別名即可找到該模塊,比如,有一個模塊位置非常深
// const foo = require("./a/b/c/foo"); // foo.js在./src/a/b/c/foo.js // const foo = require("foo"); // foo被映射成了./src/a/b/c/foo.js文件 const foo = require("bar/foo.js"); // bar被映射成了./src/a/b/c/路徑 console.log(foo); module.exports = { resolve: { alias: { "foo": path.resolve(__dirname, "./src/a/b/c/foo.js"), "bar": path.resolve(__dirname, "./src/a/b/c/") } }, }
需要注意的就是,alias 可以映射文件也可以映射路徑
3、mainFields: 我們的 package.json 中可以有多個字段,用于決定優(yōu)先使用哪個字段來導(dǎo)入模塊,比如 bootstrap 模塊中含有 js 也含有 css,其 package.json 文件中 main 字段對應(yīng)的是"dist/js/bootstrap",style 字段中對應(yīng)的是"dist/css/bootstrap.css",我們可以通過設(shè)置 mainFields 字段來改變默認引入,如:
module.exports = { resolve: { mainFields: ["style", "main"] }, }
4、extensions: 用于設(shè)置引入模塊的時候,如果沒有寫模塊后綴名,webpack 會自動添加后綴去查找,extensions 就是用于設(shè)置自動添加后綴的順序,如:
module.exports = { resolve: { extensions: ["js", "vue"] }, }
如果項目中引入了 foo 模塊,require("./foo"),其會優(yōu)先找 ./foo.js,如果沒有找到 ./foo.js 則會去找 ./foo.vue 文件
七、設(shè)置環(huán)境變量
設(shè)置環(huán)境變量需要用到 webpack 提供的一個內(nèi)置插件 DefinePlugin 插件,其作用是將一個字符串值設(shè)置為全局變量,如:
module.exports = { plugins: [ new webpack.DefinePlugin({ DEV_MODE: JSON.stringify('development') // 將'development'設(shè)置為全局變量DEV_MODE }), ] }
這樣配置之后任何一個模塊中都可以直接使用 DEV_MODE 變量了,并且其值為'development',與 ProvidePlugin 有點相似,ProvidePlugin 是將一個模塊注入到所有模塊中,實現(xiàn)模塊不需要引入即可直接使用。
八、webpack 優(yōu)化
1、noParse: 該配置是作為 module 的一個屬性值,即不解析某些模塊,所謂不解析,就是不去分析某個模塊中的依賴關(guān)系,即不去管某個文件是否 import(依賴)了某個文件,對于一些獨立的庫,比如 jquery,其根本不存在依賴關(guān)系,jquery 不會去引入其他的庫(要根據(jù)自己對某個模塊的了解去判斷是否要解析該模塊),所以我們可以讓 webpack 不去解析 jquery 的依賴關(guān)系,提高打包速度,如:
module.exports = { module: { noParse:/jquery/,//不去解析jquery中的依賴庫 } }
noParse 是 module 配置中的一個屬性,其屬性值為一個正則表達式,填入不被解析的模塊名稱。
為了更清楚的展示 noParse 的作用,假設(shè)我們在入口文件 index.js 中引入 bar.js 模塊,同時這個 bar.js 模塊中也引入了 foo.js 模塊,foo.js 不再依賴其他模塊了,那么在不使用 noParse 的情況下,webpack 打包的時候,會先去分析 index.js 模塊,發(fā)現(xiàn)其引入了 bar.js 模塊,然后接著分析 bar.js 模塊,發(fā)現(xiàn)其引入了 foo.js 模塊,接著分析 foo.js 模塊。
Entrypoint index = index.js [./src/bar.js] 55 bytes {index} [built] [./src/foo.js] 21 bytes {index} [built] [./src/index.js] 81 bytes {index} [built]
而此時如果使用了 noParse: /bar/,那么 webpack 打包的時候,會先去分析 index.js 模塊,發(fā)現(xiàn)其引入了 bar.js 模塊,但是由于 noParse 的作用,將不再繼續(xù)解析 bar.js 模塊了,即不會去分析 bar.js 中引入的 foo.js 模塊了。
Entrypoint index = index.js [./src/bar.js] 55 bytes {index} [built] [./src/index.js] 81 bytes {index} [built]
2、exclude: 在 loader 中使用 exclude 排除對某些目錄中的文件處理,即引入指定目錄下的文件時候,不使用對應(yīng)的 loader 進行處理,exclude 是 loader 配置中的一個屬性,屬性值為正則表達式,如:
module.exports = { module: { rules: [ { test: /.js$/, use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env"], plugins: ["@babel/plugin-transform-runtime"] } } ], exclude: /node_modules/ } ] } }
3、使用 IgnorePlugin 來忽略某個模塊中某些目錄中的模塊引用,比如在引入某個模塊的時候,該模塊會引入大量的語言包,而我們不會用到那么多語言包,如果都打包進項目中,那么就會影響打包速度和最終包的大小,然后再引入需要使用的語言包即可,如:
項目根目錄下有一個 time 包,其中有一個 lang 包,lang 包中包含了各種語言輸出對應(yīng)時間的 js 文件,time 包下的 index.js 會引入 lang 包下所有的 js 文件,那么當我們引入 time 模塊的時候,就會將 lang 包下的所有 js 文件都打包進去,添加如下配置:
const webpack = require("webpack"); module.exports = { plugins: [ new webpack.IgnorePlugin(/lang/, /time/) ] }
引入 time 模塊的時候,如果 time 模塊中引入了其中的 lang 模塊中的內(nèi)容,那么就忽略掉,即不引入 lang 模塊中的內(nèi)容,需要注意的是,這 /time/ 只是匹配文件夾和 time 模塊的具體目錄位置無關(guān),即只要是引入了目錄名為 time 中的內(nèi)容就會生效。
4、使用 HappyPack:由于在打包過程中有大量的文件需要交個 loader 進行處理,包括解析和轉(zhuǎn)換等操作,而由于 js 是單線程的,所以這些文件只能一個一個地處理,而 HappyPack 的工作原理就是充分發(fā)揮 CPU 的多核功能,將任務(wù)分解給多個子進程去并發(fā)執(zhí)行,子進程處理完后再將結(jié)果發(fā)送給主進程,happypack 主要起到一個任務(wù)劫持的作用,在創(chuàng)建 HappyPack 實例的時候要傳入對應(yīng)文件的 loader,即 use 部分,loader 配置中將使用經(jīng)過 HappyPack 包裝后的 loader 進行處理,如:
const HappyPack = require("happypack"); // 安裝并引入happypack模塊 module.exports = { plugins: [ new HappyPack({ // 這里對處理css文件的loader進行包裝 id: "css",// 之前的loader根據(jù)具體的id進行引入 use: ["style-loader","css-loader"], threads: 5 // 設(shè)置開啟的進程數(shù) }) ], module: { rules: [ { test: /.css$/, // 匹配以.css結(jié)尾的文件 use: ["happypack/loader?id=css"] //根據(jù)happypack實例中配置的id引入包裝后的laoder,這里的happyPack的h可以大寫也可以小寫 } ] } }
webpack 要打包的文件非常多的時候才需要使用 happypack 進行優(yōu)化,因為開啟多進程也是需要耗時間的,所以文件少的時候,使用 happypack 返回更耗時
5、抽離公共模塊: 對于多入口情況,如果某個或某些模塊,被兩個以上文件所依賴,那么可以將這個模塊單獨抽離出來,不需要將這些公共的代碼都打包進每個輸出文件中,這樣會造成代碼的重復(fù)和流量的浪費,即如果有兩個入口文件 index.js 和 other.js,它們都依賴了 foo.js,那么如果不抽離公共模塊,那么 foo.js 中的代碼都會打包進最終輸出的 index.js 和 other.js 中去,即有兩份 foo.js 了。抽離公共模塊也很簡單,直接在 optimization 中配置即可,如:
module.exports = { splitChunks: { // 分割代碼塊,即抽離公共模塊 cacheGroups: { // 緩存組 common: { // 組名為common可自定義 chunks: "initial", minSize: 0, // 文件大小為0字節(jié)以上才抽離 minChunks: 2, // 被引用過兩次才抽離 name: "common/foo", // 定義抽離出的文件的名稱 } } } }
這樣就會將公共的 foo.js 模塊抽離到 common 目錄下 foo.js 中了,但是如果我們也有多個文件依賴了第三方模塊如 jquery,如果按以上配置,那么 jquery 也會被打包進 foo.js 中,會導(dǎo)致代碼混亂,所以我們希望將 jquery 單獨抽出來,即與 foo.js 分開,我們可以復(fù)制一份以上配置,并通過設(shè)置抽離代碼權(quán)重的方式來實現(xiàn),即優(yōu)先抽離出 jquery,如:
module.exports = { splitChunks: { // 分割代碼塊,即抽離公共模塊 cacheGroups: { // 緩存組 common: { // 組名為common可自定義 chunks: "initial", minSize: 0, // 文件大小為0字節(jié)以上才抽離 minChunks: 2, // 被引用過兩次才抽離 name: "common/foo", // 定義抽離出的文件的名稱 }, verdor: { test: /node_modules/, priority: 1, // 設(shè)置打包權(quán)重,即優(yōu)先抽離第三方模塊 chunks: "initial", minSize: 0, // 文件大小為0字節(jié)以上才抽離 minChunks: 2, // 被引用過兩次才抽離 name: "common/jquery", // 定義抽離出的文件的名稱 } } } }
這樣就會在 common 目錄下同時抽離出 foo.js 和 jquery.js 了,需要注意的是,代碼的抽離必須是該模塊沒有被排除打包,即該模塊會被打包進輸出 bundle 中,如果第三方模塊已經(jīng)通過 externals 排除打包,則以上 vendor 配置無效。
6、按需加載,即在需要使用的時候才打包輸出,webpack 提供了 import() 方法,傳入要動態(tài)加載的模塊,來動態(tài)加載指定的模塊,當 webpack 遇到 import()語句的時候,不會立即去加載該模塊,而是在用到該模塊的時候,再去加載,也就是說打包的時候會一起打包出來,但是在瀏覽器中加載的時候并不會立即加載,而是等到用到的時候再去加載,比如,點擊按鈕后才會加載某個模塊,如:
const button = document.createElement("button"); button.innerText = "點我" button.addEventListener("click", () => { // 點擊按鈕后加載foo.js import("./foo").then((res) => { // import()返回的是一個Promise對象 console.log(res); }); }); document.body.appendChild(button);
從中可以看到,import() 返回的是一個 Promise 對象,其主要就是利用 JSONP 實現(xiàn)動態(tài)加載,返回的 res 結(jié)果不同的 export 方式會有不同,如果使用的 module.exports 輸出,那么返回的 res 就是 module.exports 輸出的結(jié)果;如果使用的是 ES6 模塊輸出,即 export default 輸出,那么返回的 res 結(jié)果就是 res.default,如:
// ES6模塊輸出,res結(jié)果為 {default: "foo", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
7、開啟模塊熱更新: 模塊熱更新可以做到在不刷新網(wǎng)頁的情況下,更新修改的模塊,只編譯變化的模塊,而不用全部模塊重新打包,大大提高開發(fā)效率,在未開啟熱更新的情況下,每次修改了模塊,都會重新打包。
要開啟模塊熱更新,那么只需要在 devServer 配置中添加 hot:true 即可。當然僅僅開啟模塊熱更新是不夠的,我們需要做一些類似監(jiān)聽的操作,當監(jiān)聽的模塊發(fā)生變化的時候,重新加載該模塊并執(zhí)行,如:
module.exports = { devServer: { hot: true // 開啟熱更新 } } ---------- import foo from "./foo"; console.log(foo); if (module.hot) { module.hot.accept("./foo", () => { // 監(jiān)聽到foo模塊發(fā)生變化的時候 const foo = require("./foo"); // 重新引入該模塊并執(zhí)行 console.log(foo); }); }
更多關(guān)于webpack配置車優(yōu)化的文章請點擊下面的相關(guān)文章