一看就懂之webpack高級配置與優化

144

一看就懂之webpack基礎配置

一、打包多頁面應用

所謂打包多頁面,就是同時打包出多個html頁面,打包多頁面也是使用html-webpack-plugin,隻不過,在引入插件的時候是創建多個插件對象,因為一個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"] // 數組元素為chunk名稱,即entry屬性值為對象的時候指定的名稱,index頁面隻引入index.js
        }),
        new HtmlWebpackPlugin({
            template: "./src/index.html", // 要打包輸出哪個文件,可以使用相對路徑
            filename: "foo.html", // 打包輸出後該html文件的名稱
            chunks: ["foo"] // 數組元素為chunk名稱,即entry屬性值為對象的時候指定的名稱,foo頁面隻引入foo.js
        }),
    ]
}
打包多頁面時,關鍵在于chunks屬性的配置,因為在沒有配置chunks屬性的情況下,打包輸出的index.html和foo.html都會同時引入index.js和foo.js,所以必須配置chunks屬性,來指定打包輸出後的html文件中要引入的輸出模塊,數組的元素為entry屬性值為對象的時候指定的chunk名,如上配置,才能實現,index.html隻引入index.js,foo.html隻引入foo.js文件

二、配置source-map

source-map就是源碼映射,主要是為了方便代碼調試,因為我們打包上線後的代碼會被壓縮等處理,導緻所有代碼都被壓縮成了一行,如果代碼中出現錯誤,那麼浏覽器隻會提示出錯位置在第一行,這樣我們無法真正知道出錯地方在源碼中的具體位置。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
源碼中出錯的位置明明是第二行代碼,而浏覽器中提示的錯誤确實在第一行,所以如果代碼很複雜的情況下,我們就無法找到出錯的具體位置

devtool常見的有4種配置:
source-map: 這種模式會産生一個.map文件,出錯了會提示具體的行和列,文件裡面保留了打包後的文件與原始文件之間的映射關系,打包輸出文件中會指向生成的.map文件,告訴js引擎源碼在哪裡,由于源碼與.map文件分離,所以需要浏覽器發送請求去獲取.map文件,常用于生産環境,如:

//# sourceMappingURL=index.js.map

eval: 這種模式打包速度最快,不會生成.map文件,會使用eval将模塊包裹,在末尾加入sourceURL,常用于開發環境,如:

//# sourceURL=webpack:///./src/index.js

eval-source-map: 每個 module 會通過 eval() 來執行,并且生成一個 DataUrl 形式的 SourceMap(即base64編碼形式内嵌到eval語句末尾), 但是不會生成.map文件,可以減少網絡請求*,但是打包文件會非常大*。

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/index.js

cheap-source-map: 加上 cheap,就隻會提示到第幾行報錯,少了列信息提示,同時不會對引入的庫做映射,可以提高打包性能,但是會産生.map文件

cheap-module-source-map: 和cheap-source-map相比,加上了module,就會對引入的庫做映射,并且也會産生.map文件,用于生産環境

cheap-module-eval-source-map: 常用于開發環境,使用 cheap 模式可以大幅提高 souremap 生成的效率,加上module同時會對引入的庫做映射,eval提高打包構建速度,并且不會産生.map文件減少網絡請求。

凡是帶eval的模式都不能用于生産環境,因為其不會産生.map文件,會導緻打包後的文件變得非常大。通常我們并不關心列信息,所以都會使用cheap模式,但是我們也還是需要對第三方庫做映射,以便精準找到錯誤的位置。

三、watch和watchOptions配置

webpack 可以監聽文件變化,當它們修改後會重新編譯,如果需要開啟該功能,那麼需要将watch設置為true,具體監聽配置通過watchOptions進行相應的設置。

module.exports = {
    watch: true,
    watchOptions: {
        poll: 1000, // 每隔一秒輪詢一次文件是否發生變化
        aggregateTimeout: 1000, // 當第一個文件更改,會在重新構建前增加延遲。這個選項允許 webpack 将這段時間内進行的任何其他更改都聚合到一次重新構建裡
        ignored: /node_modules/ // 排除一些文件的監聽
    }
}

四、三個常見小插件的使用

clean-webpack-plugin: 其作用就是每次打包前先先将輸出目錄中的内容進行清空,然後再将打包輸出的文件輸出到輸出目錄中。

const {CleanWebpackPlugin} = require("clean-webpack-plugin");
module.exports = {
    plugins: [
        new CleanWebpackPlugin() // 打包前清空輸出目錄
    ]
}
需要注意的是,require("clean-webpack-plugin)的結果是一個對象而不是類,這個對象中的CleanWebpackPlugin屬性才是一個類,我們就是用這個類去創建插件對象

copy-webpack-plugin: 其作用就是打包的時候帶上一些readMe.md、history.md等等一起輸出到輸出目錄中

module.exports = {
    plugins: [
        new CopyWebpackPlugin([
            {
                from: "./readMe.md", // 将項目根目錄下的readMe.md文件一起拷貝到輸出目錄中
                to: "" // 屬性值為空字符串則表示是輸出目錄
            }
        ])
    ]
}

BannerPlugin: 其作用就是在打包輸出的js文件的頭部添加一些文字注釋,比如版權說明等等,BannerPlugin是webpack内置的插件,如:

module.exports = {
    plugins: [
        new webpack.BannerPlugin("Copyright © 2019") // 在js文件頭部添加版權說明
    ]
}

五、webpack跨域問題

為什麼webpack會存在跨域問題?因為webpack打包的是前端代碼,其最終會被部署到前端服務器上,而前後端代碼通常部署在不同的服務器上,即使是部署在同一個服務器上,所使用的端口也是不一樣的,當前端代碼通過ajax等手段向後端服務器獲取數據的時候,由于前後端代碼不在同一個域中,故存在跨域問題。比如,我們通過webpack的devServer來運行部署我們的前端應用代碼,devServer啟動在8080端口上,而前端應用代碼中會通過ajax請求後端數據,後端服務器啟動在3000端口上。
// index.js

const xhr = new XMLHttpRequest();
// xhr.open("get", "http://yeg9.caifu37931.cn:3000/api/test"); // 由于跨域問題無法直接訪問到http://1sqv.caifu37931.cn:3000下的資源
xhr.open("get", "/api/test"); // 本來是要訪問http://vmyi1r.caifu37931.cn:3000/api/test
xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        console.log(xhr.responseText);
    }
}
xhr.send();
由于前端代碼是運行在浏覽器中的,如果在前端代碼中直接通過ajax向http://ewci.caifu37931.cn:3000/api/test發起請求獲取數據,那麼由于浏覽器同源策略的影響,會存在跨域的問題,所以必須訪問/api/test,但是這樣訪問又會出現404問題,因為其實訪問的是http://7tlit.caifu37931.cn:8080/api/test,8080服務器上是沒有該資源的,解決辦法就是通過devServer配置一個代理服務器
module.exports = {
    devServer: {
        proxy: {
            "/api": "http://k9xzev1s.caifu37931.cn:3000" // 路徑以/api開頭則代理到localhost:3000上
        }
    }
}
訪問http://wgaq86.caifu37931.cn:8080/api/test就會被代理到http://ghtdj.caifu37931.cn:3000/api/test上,proxy還支持路徑的重寫,如果3000端口服務器上并沒有/api/test路徑,隻有/test路徑,那麼就可以對路徑進行重寫,将/api替換掉
module.exports = {
    devServer: {
        proxy: {
            "/api": {
                target: "http://fkx4e.caifu37931.cn:3000",
                pathRewrite: {"/api": ""} // 将/api替換掉
            }
        }
    }
}
訪問http://aiutnrd.caifu37931.cn:8080/api/test就會被代理到http://2qmjn4o.caifu37931.cn:3000/test上

如果前端隻是想mock一些數據,并不需要真正的去訪問後台服務器,那麼我們可以通過devServer提供的before鈎子函數獲取到内置的服務器對象進行處理請求,這個内置的服務器對象就是webpack的devServer即8080端口的server,因為是在同一個服務器中請求數據所以也不會出現跨域問題。

before(app) { // 此app即webpack的devServer
            app.get("/api/test", (req, res, next) => {
                res.json({name: "even"});
            })
        }

我們還可以不通過webpack提供的devServer來啟動webpack,而是使用自己服務器來啟動webapck
// server.js

const express = require("express");
const app = express();
const webpack = require("webpack"); // 引入webpack
const config = require("./webpack.config.js"); // 引入配置文件
const compiler = webpack(config); // 創建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);
通過自定義服務器啟動webpack,這樣webpack中的前端代碼請求數據就和服務器的資源在同一個域中了。

六、resolve配置

resolve用于配置模塊的解析相關參數的,其屬性值為一個對象
modules: 告訴webpack 解析模塊時應該搜索的目錄,即require或import模塊的時候,隻寫模塊名的時候,到哪裡去找,其屬性值為數組,因為可配置多個模塊搜索路徑,其搜索路徑必須為絕對路徑,比如,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模塊

alias: 用于給路徑或者文件取别名,當import或者require的模塊的路徑非常長時,我們可以給該模塊的路徑或者整個路徑名+文件名都設置成一個别名,然後直接引入别名即可找到該模塊,比如,有一個模塊位置非常深

// 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可以映射文件也可以映射路徑

mainFields: 我們的package.json中可以有多個字段,用于決定優先使用哪個字段來導入模塊,比如bootstrap模塊中含有js也含有css,其package.json文件中main字段對應的是"dist/js/bootstrap",style字段中對應的是"dist/css/bootstrap.css",我們可以通過設置mainFields字段來改變默認引入,如:

module.exports = {
    resolve: {
        mainFields: ["style", "main"]
    },
}

extensions: 用于設置引入模塊的時候,如果沒有寫模塊後綴名,webpack會自動添加後綴去查找,extensions就是用于設置自動添加後綴的順序,如:

module.exports = {
    resolve: {
        extensions: ["js", "vue"]
    },
}
如果項目中引入了foo模塊,require("./foo"),其會優先找./foo.js,如果沒有找到./foo.js則會去找./foo.vue文件

七、設置環境變量

設置環境變量需要用到webpack提供的一個内置插件DefinePlugin插件,其作用是将一個字符串值設置為全局變量,如:

module.exports = {
    plugins: [
        new webpack.DefinePlugin({
            DEV_MODE: JSON.stringify('development') // 将'development'設置為全局變量DEV_MODE
        }),
    ]
}
這樣配置之後任何一個模塊中都可以直接使用DEV_MODE變量了,并且其值為'development',與ProvidePlugin有點相似,ProvidePlugin是将一個模塊注入到所有模塊中實現模塊不需要引入即可直接使用

八、webpack優化

① noParse: 該配置是作為module的一個屬性值,即不解析某些模塊,所謂不解析,就是不去分析某個模塊中的依賴關系,即不去管某個文件是否import(依賴)了某個文件,對于一些獨立的庫,比如jquery,其根本不存在依賴關系,jquery不會去引入其他的庫(要根據自己對某個模塊的了解去判斷是否要解析該模塊),所以我們可以讓webpack不去解析jquery的依賴關系,提高打包速度,如:

module.exports = {
    module: {
        noParse:/jquery/,//不去解析jquery中的依賴庫
    }
}
noParse是module配置中的一個屬性,其屬性值為一個正則表達式,填入不被解析的模塊名稱

為了更清楚的展示noParse的作用,假設我們在入口文件index.js中引入bar.js模塊,同時這個bar.js模塊中也引入了foo.js模塊,foo.js不再依賴其他模塊了,那麼在不使用noParse的情況下,webpack打包的時候,會先去分析index.js模塊,發現其引入了bar.js模塊,然後接着分析bar.js模塊,發現其引入了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模塊,發現其引入了bar.js模塊,但是由于noParse的作用,将不再繼續解析bar.js模塊了,即不會去分析bar.js中引入的foo.js模塊了

Entrypoint index = index.js
[./src/bar.js] 55 bytes {index} [built]
[./src/index.js] 81 bytes {index} [built]

② exclude: 在loader中使用exclude排除對某些目錄中的文件處理,即引入指定目錄下的文件時候,不使用對應的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/
            }
        ]
    }
}

使用IgnorePlugin忽略某個模塊中某些目錄中的模塊引用,比如在引入某個模塊的時候,該模塊會引入大量的語言包,而我們不會用到那麼多語言包,如果都打包進項目中,那麼就會影響打包速度和最終包的大小,然後再引入需要使用的語言包即可,如:
項目根目錄下有一個time包,其中有一個lang包,lang包中包含了各種語言輸出對應時間的js文件,time
包下的index.js會引入lang包下所有的js文件,那麼當我們引入time模塊的時候,就會将lang包下的所有js文件都打包進去,添加如下配置:

const webpack = require("webpack");
module.exports = {
    plugins: [
        new webpack.IgnorePlugin(/lang/, /time/)
    ]
}
引入time模塊的時候,如果time模塊中引入了其中的lang模塊中的内容,那麼就忽略掉,即不引入lang模塊中的内容,需要注意的是,這/time/隻是匹配文件夾和time模塊的具體目錄位置無關,即隻要是引入了目錄名為time中的内容就會生效

④ 使用HappyPack:由于在打包過程中有大量的文件需要交個loader進行處理,包括解析轉換等操作,而由于js是單線程的,所以這些文件隻能一個一個地處理,而HappyPack的工作原理就是充分發揮CPU的多核功能,将任務分解給多個子進程去并發執行,子進程處理完後再将結果發送給主進程,happypack主要起到一個任務劫持的作用,在創建HappyPack實例的時候要傳入對應文件的loader,即use部分loader配置中将使用經過HappyPack包裝後的loader進行處理,如:

const HappyPack = require("happypack"); // 安裝并引入happypack模塊
module.exports = {
    plugins: [
        new HappyPack({ // 這裡對處理css文件的loader進行包裝
            id: "css",// 之前的loader根據具體的id進行引入
            use: ["style-loader","css-loader"],
            threads: 5 // 設置開啟的進程數
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/, // 匹配以.css結尾的文件
                use: ["happypack/loader?id=css"] //根據happypack實例中配置的id引入包裝後的laoder,這裡的happyPack的h可以大寫也可以小寫
            }
        ]
    }
}
webpack要打包的文件非常多的時候才需要使用happypack進行優化,因為開啟多進程也是需要耗時間的,所以文件少的時候,使用happypack返回更耗時

⑤ 抽離公共模塊: 對于多入口情況,如果某個或某些模塊,被兩個以上文件所依賴,那麼可以将這個模塊單獨抽離出來,不需要将這些公共的代碼都打包進每個輸出文件中,這樣會造成代碼的重複和流量的浪費,即如果有兩個入口文件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字節以上才抽離
                    minChunks: 2, // 被引用過兩次才抽離
                    name: "common/foo", // 定義抽離出的文件的名稱
             }
         }
     }
}
這樣就會将公共的foo.js模塊抽離到common目錄下foo.js中了,但是如果我們也有多個文件依賴了第三方模塊如jquery,如果按以上配置,那麼jquery也會被打包進foo.js中,會導緻代碼混亂,所以我們希望将jquery單獨抽出來,即與foo.js分開,我們可以複制一份以上配置,并通過設置抽離代碼權重的方式來實現,即優先抽離出jquery,如:
module.exports = {
     splitChunks: { // 分割代碼塊,即抽離公共模塊
         cacheGroups: { // 緩存組
             common: { // 組名為common可自定義
                    chunks: "initial",
                    minSize: 0, // 文件大小為0字節以上才抽離
                    minChunks: 2, // 被引用過兩次才抽離
                    name: "common/foo", // 定義抽離出的文件的名稱
             },
             verdor: {
                    test: /node_modules/,
                    priority: 1, // 設置打包權重,即優先抽離第三方模塊
                    chunks: "initial",
                    minSize: 0, // 文件大小為0字節以上才抽離
                    minChunks: 2, // 被引用過兩次才抽離
                    name: "common/jquery", // 定義抽離出的文件的名稱
                }
         }
     }
}
這樣就會在common目錄下同時抽離出foo.js和jquery.js了,需要注意的是,代碼的抽離必須是該模塊沒有被排除打包,即該模塊會被打包進輸出bundle中,如果第三方模塊已經通過externals排除打包,則以上vendor配置無效。

⑥ 按需加載,即在需要使用的時候才打包輸出,webpack提供了import()方法,傳入要動态加載的模塊,來動态加載指定的模塊,當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實現動态加載,返回的res結果不同的export方式會有不同,如果使用的module.exports輸出,那麼返回的res就是module.exports輸出的結果;如果使用的是ES6模塊輸出,即export default輸出,那麼返回的res結果就是res.default,如:

// ES6模塊輸出,res結果為

{default: "foo", __esModule: true, Symbol(Symbol.toStringTag): "Module"}

⑦ 開啟模塊熱更新: 模塊熱更新可以做到在不刷新網頁的情況下,更新修改的模塊,隻編譯變化的模塊,而不用全部模塊重新打包,大大提高開發效率,在未開啟熱更新的情況下,每次修改了模塊,都會重新打包。要開啟模塊熱更新,那麼隻需要在devServer配置中添加hot:true即可。當然僅僅開啟模塊熱更新是不夠的,我們需要做一些類似監聽的操作,當監聽的模塊發生變化的時候,重新加載該模塊并執行,如:

module.exports = {
    devServer: {
        hot: true // 開啟熱更新
    }
}

----------


import foo from "./foo";
console.log(foo);
if (module.hot) {
    module.hot.accept("./foo", () => { // 監聽到foo模塊發生變化的時候
        const foo =  require("./foo"); // 重新引入該模塊并執行
        console.log(foo);
    });
}
如果不使用module.hot.accept監聽,那麼當修改foo模塊的時候還是會刷新頁面的。

博客地址


如果覺得我的文章對你有用,請随意贊賞

你可能感興趣的

Sweets82312 · 6 天前

總結到位!收藏了!

回複

載入中...