一看就懂之webpack基礎配置javascript
所謂打包多頁面,就是同時打包出多個html頁面,打包多頁面也是使用html-webpack-plugin,只不過,在引入插件的時候是建立多個插件對象,由於一個html-webpack-plugin插件對象只能打包出一個html頁面。如:css
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文件js文件能夠經過chunks屬性進行篩選,可是css則沒法篩選,css是否會被html文件所引入,徹底是看html中引入的js文件,若是引入的js中require或者import了某個css文件,那麼這個css文件就會被引入到該html文件中。html
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文件,經常使用於生產環境,如:vue
//# sourceMappingURL=index.js.map
② eval: 這種模式打包速度最快,不會生成.map文件,會使用eval將模塊包裹,在末尾加入sourceURL,經常使用於開發環境,如:java
//# sourceURL=webpack:///./src/index.js
③ eval-source-map: 每一個 module 會經過 eval() 來執行,而且生成一個 DataUrl 形式的 SourceMap(即base64編碼形式內嵌到eval語句末尾), 可是不會生成.map文件,能夠減小網絡請求*,可是打包文件會很是大*。node
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/index.js
④ cheap-source-map: 加上 cheap,就只會提示到第幾行報錯,少了列信息提示,同時不會對引入的庫作映射,能夠提升打包性能,可是會產生.map文件。jquery
③ cheap-module-source-map: 和cheap-source-map相比,加上了module,就會對引入的庫作映射,而且也會產生.map文件,用於生產環境。webpack
④ cheap-module-eval-source-map: 經常使用於開發環境,使用 cheap 模式能夠大幅提升 souremap 生成的效率,加上module同時會對引入的庫作映射,eval提升打包構建速度,而且不會產生.map文件減小網絡請求。web
凡是帶eval的模式都不能用於生產環境,由於其不會產生.map文件,會致使 打包後的文件變得很是大。一般咱們並 不關心列信息,因此都會使用cheap模式,可是咱們也仍是須要對第三方庫作映射,以便精準找到錯誤的位置。
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打包的是前端代碼,其最終會被部署到前端服務器上,而先後端代碼一般部署在不一樣的服務器上,即便是部署在同一個服務器上,所使用的端口也是不同的,當前端代碼經過ajax等手段向後端服務器獲取數據的時候,因爲先後端代碼不在同一個域中,故存在跨域問題。好比,咱們經過webpack的devServer來運行部署咱們的前端應用代碼,devServer啓動在8080端口上,而前端應用代碼中會經過ajax請求後端數據,後端服務器啓動在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發起請求獲取數據,那麼因爲 瀏覽器同源策略的影響,會存在跨域的問題,因此必須訪問/api/test,可是這樣訪問又會出現404問題,由於其實訪問的是 http://localhost:8080/api/test,8080服務器上是沒有該資源的,解決辦法就是 經過devServer配置一個代理服務器
module.exports = { devServer: { proxy: { "/api": "http://localhost:3000" // 路徑以/api開頭則代理到localhost:3000上 } } }
訪問 http://localhost:8080/api/test就會被代理到 http://localhost:3000/api/test上,proxy還支持路徑的重寫,若是3000端口服務器上並無/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一些數據,並不須要真正的去訪問後臺服務器,那麼咱們能夠經過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用於配置模塊的解析相關參數的,其屬性值爲一個對象。
① 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是將一個模塊注入到全部模塊中, 實現模塊不須要引入便可直接使用。
① 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模塊的時候仍是會刷新頁面的。