前言
對於現代web前端工程化,webpack起到了絕大的作用,此篇文章致力於讓你學習webpack的配置,能夠從無到有的配置一套屬於自己的webpack配置,此教程從基礎配置,到優化兩個維度進行講解,其中有大量的示例,文尾部分會提供git倉庫以供程式碼下載。
配置
這裡只講解配置相關的細節,不講解原理性的知識,建議熟練使用webpack之後,學習下webpack是如何工作的,這樣可以定製loader和plugin,能夠更加高效的解決專案上的問題。
初始化工程
前端工程化最基礎的東西,就是npm包管理器,這一切都是建立在這上面的,首先執行。
npm init -y
初始化package.json
前提是你安裝了node.js,下載地址
其中 -y 指令是跳過package.json的配置,直接初始化一個package.json檔案。建議去掉-y指令,進行一次定製化的配置。
建立一個build目錄,用於存放webpack相關配置,建立一個src目錄,用於存放工程檔案。
安裝webpack
在build目錄下新建一個webpack.base.js檔案,這個檔案用於存放公共的webpack配置項,比如sass,babel什麼的,因為一個工程可能不止一個需求,所以建立一個公共的配置項方便其他需求的配置。
安裝webpack依賴,推薦使用yarn
npm i yarn -g
yarn add webpack --dev
因為是配置4.0版本,所以還需要安裝webpack-cli
yarn add webpack-cli --dev
配置完成後,在webpack.base.js進行以下配置,測試是否安裝成功
const path = require('path')
const getPathByRoot = function(targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: './static/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
}
}
複製程式碼
這是一個最簡單的配置,引入了 node 的path模組,用於路徑解析,定義了webpack入口檔案,以及出口的路徑及檔名,注意檔名後面有一個[hash],這個的作用是給打包後的檔案新增一個hash值,使用者解決瀏覽器快取的問題。
再看看入口檔案。
class Test {
constructor() {
alert(1)
}
}
new Test()
複製程式碼
很簡單的程式碼,只是為了檢視打包後的檔案。
很關鍵的一步就是需要在package.json檔案中定義打包命令,在package.json檔案的script中新增以下程式碼:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config ./build/webpack.base.js --mode=production"
}
複製程式碼
build命令的大致意思是,以production模式執行webpack使用build目錄下的webpack.base.js配置檔案。
上述檔案新增完成後,執行:
npm run build
可以看見生成了一個dist目錄,目錄下有一個static目錄,其中有一個index.js檔案,其中的程式碼如下:
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="./",r(r.s=0)}([function(e,t){new class{constructor(){alert(1)}}}]);
複製程式碼
多了一些檔案,在最後可以看到:
new class{constructor(){alert(1)}}}
複製程式碼
入口檔案的程式碼已經被打包了,但是還是之前的es6的程式碼,如果想要轉換成es5,這就要用到babel了。
loaders & plugins
webpack的loader作用是,loader通過正規表示式將匹配到的檔案中的字串,經過自己的處理後,再交給下一個loader處理,所以最終得到的檔案,是一個自己需要的檔案。
bable-loader
這裡介紹babel-loader的配置,babel的作用是將低版本瀏覽器不支援的es6 7 8 的程式碼轉換成可執行的es5程式碼,而babel-loader的作用是將匹配到的js檔案交給babel轉換器處理成es5的程式碼,再交給下一個loader,先安裝babel相關的依賴
yarn add @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader --dev
yarn add @babel/runtime
用兩條命令的原因是, @babel/runtime 是生產依賴,用於將babel不轉換的es6 函式如promise,set, map等轉換成es5的 polyfill,並且通過 @babel/plugin-transform-runtime 外掛將這些函式提取成公共的函式,以免重複轉換,增加程式碼量。
完成依賴的安裝後,我們需要配置webpack的loader,如下圖。
const path = require('path')
const getPathByRoot = function(targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: './static/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader']
}]
}
}
複製程式碼
新增了一個module屬性,其中有一個欄位是rules,值是一個陣列,表示可以處理多種格式的檔案。test是一個正規表示式,表示需要經過loader處理的檔案型別。use對應的是需要使用的loader,是一個陣列,可以新增多個loader,處理的順序是,最後一個loader先處理,第一個loader最後處理,這個後面會詳細講解,現在不懂沒關係。
然後在專案根目錄新建一個檔案,名為 .babelrc ,內容如下:
{
"presets": [
"@babel/env"
]
}
複製程式碼
關於babelrc檔案具體的講解可以看這篇文章,這裡我使用的是最新的 "@babel/env"
執行 npm run build 命令,得到如下程式碼
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="./",t(t.s=0)}([function(e,n){new function e(){!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),alert(1)}}]);
複製程式碼
在檔案的最後,可以看到 es6 的 class 語法已經轉換成了es5的程式碼,至此,babel就配置成功了。
其次一個web前端專案肯定是少不了html檔案的,所以接下來教大家處理html檔案。
html-loader 以及 html-webpack-plugin
html-loader可以解析html文件中的圖片資源,交給其他的loader處理,如url-loader或者是file-loader等。
html-webpacl-plugin的功能是將一個html作為模版,打包完成後會將打包後的資源比如js css,自動新增進html文件中。
首先進行安裝:
yarn add html-loader html-webpack-plugin --dev
再向webpack.base.config中新增以下配置:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const getPathByRoot = function(targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /.js$/,
use: ['babel-loader']
}, {
test: /.html/,
use: ['html-loader']
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
複製程式碼
然後在專案根目錄下新增一個 index.html檔案
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>learn webpack</title>
</head>
<body>
</body>
</html>
複製程式碼
此時執行打包命令,可以看見輸出的檔案下已經有了index.html檔案,並且已經引入了打包後的js檔案。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>learn webpack</title>
</head>
<body>
<script type="text/javascript" src="./static/index.js"></script></body>
</html>
複製程式碼
在瀏覽器中開啟index.html,已經可以正常執行了。
css前處理器
前處理器作為前端css高效的工具,不可不用,這一節教學sass的安裝與配置。首先安裝css sass的依賴以及style-loader。
yarn add node-sass sass-loader css-loader style-loader --dev
如果提示編譯失敗,需要安裝python2的話,就使用cnpm 安裝 node-sass
cnpm i node-sass --save-dev
其中style-loader的作用是,將處理完成的css檔案使用style標籤內聯進html文件中,之後會講解將css檔案抽離出js檔案。
安裝完成後進行webpack配置,如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const getPathByRoot = function(targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /.js$/,
use: ['babel-loader']
}, {
test: /.html$/,
use: ['html-loader']
}, {
test: /.(sc|c)ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
複製程式碼
這裡我們同時匹配了css 以及scss檔案,因為scss是css的一個超集,所以sass-loader也可以解析css檔案,減少了不必要的配置。
再看看使用的loader的順序,值得注意的是,在這個陣列中,webpack使用的loader是倒序執行的,也是就是先執行的sass-loader再執行的css-loader,其次才是style-loader,要是反過來,是會報錯的哦。
執行 build 命令得到以下程式碼:
!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="./",n(n.s=0)}([function(t,e,n){"use strict";n.r(e);n(1);new function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),alert(1)}},function(t,e,n){var r=n(2);"string"==typeof r&&(r=[[t.i,r,""]]);var o={hmr:!0,transform:void 0,insertInto:void 0};n(4)(r,o);r.locals&&(t.exports=r.locals)},function(t,e,n){(t.exports=n(3)(!1)).push([t.i,".test {\n height: 100px; }\n .test .test1 {\n width: 100px; }\n",""])},function(t,e){t.exports=function(t){var e=[];return e.toString=function(){return this.map(function(e){var n=function(t,e){var n=t[1]||"",r=t[3];if(!r)return n;if(e&&"function"==typeof btoa){var o=function(t){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(t))))+" */"}(r),i=r.sources.map(function(t){return"/*# sourceURL="+r.sourceRoot+t+" */"});return[n].concat(i).concat([o]).join("\n")}return[n].join("\n")}(e,t);return e[2]?"@media "+e[2]+"{"+n+"}":n}).join("")},e.i=function(t,n){"string"==typeof t&&(t=[[null,t,""]]);for(var r={},o=0;o<this.length;o++){var i=this[o][0];"number"==typeof i&&(r[i]=!0)}for(o=0;o<t.length;o++){var s=t[o];"number"==typeof s[0]&&r[s[0]]||(n&&!s[2]?s[2]=n:n&&(s[2]="("+s[2]+") and ("+n+")"),e.push(s))}},e}},function(t,e,n){var r={},o=function(t){var e;return function(){return void 0===e&&(e=t.apply(this,arguments)),e}}(function(){return window&&document&&document.all&&!window.atob}),i=function(t){var e={};return function(t,n){if("function"==typeof t)return t();if(void 0===e[t]){var r=function(t,e){return e?e.querySelector(t):document.querySelector(t)}.call(this,t,n);if(window.HTMLIFrameElement&&r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(t){r=null}e[t]=r}return e[t]}}(),s=null,a=0,u=[],f=n(5);function c(t,e){for(var n=0;n<t.length;n++){var o=t[n],i=r[o.id];if(i){i.refs++;for(var s=0;s<i.parts.length;s++)i.parts[s](o.parts[s]);for(;s<o.parts.length;s++)i.parts.push(b(o.parts[s],e))}else{var a=[];for(s=0;s<o.parts.length;s++)a.push(b(o.parts[s],e));r[o.id]={id:o.id,refs:1,parts:a}}}}function l(t,e){for(var n=[],r={},o=0;o<t.length;o++){var i=t[o],s=e.base?i[0]+e.base:i[0],a={css:i[1],media:i[2],sourceMap:i[3]};r[s]?r[s].parts.push(a):n.push(r[s]={id:s,parts:[a]})}return n}function p(t,e){var n=i(t.insertInto);if(!n)throw new Error("Couldn't find a style target. This probably means that the value for the 'insertInto' parameter is invalid.");var r=u[u.length-1];if("top"===t.insertAt)r?r.nextSibling?n.insertBefore(e,r.nextSibling):n.appendChild(e):n.insertBefore(e,n.firstChild),u.push(e);else if("bottom"===t.insertAt)n.appendChild(e);else{if("object"!=typeof t.insertAt||!t.insertAt.before)throw new Error("[Style Loader]\n\n Invalid value for parameter 'insertAt' ('options.insertAt') found.\n Must be 'top', 'bottom', or Object.\n (https://github.com/webpack-contrib/style-loader#insertat)\n");var o=i(t.insertAt.before,n);n.insertBefore(e,o)}}function d(t){if(null===t.parentNode)return!1;t.parentNode.removeChild(t);var e=u.indexOf(t);e>=0&&u.splice(e,1)}function h(t){var e=document.createElement("style");if(void 0===t.attrs.type&&(t.attrs.type="text/css"),void 0===t.attrs.nonce){var r=function(){0;return n.nc}();r&&(t.attrs.nonce=r)}return v(e,t.attrs),p(t,e),e}function v(t,e){Object.keys(e).forEach(function(n){t.setAttribute(n,e[n])})}function b(t,e){var n,r,o,i;if(e.transform&&t.css){if(!(i=e.transform(t.css)))return function(){};t.css=i}if(e.singleton){var u=a++;n=s||(s=h(e)),r=m.bind(null,n,u,!1),o=m.bind(null,n,u,!0)}else t.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(t){var e=document.createElement("link");return void 0===t.attrs.type&&(t.attrs.type="text/css"),t.attrs.rel="stylesheet",v(e,t.attrs),p(t,e),e}(e),r=function(t,e,n){var r=n.css,o=n.sourceMap,i=void 0===e.convertToAbsoluteUrls&&o;(e.convertToAbsoluteUrls||i)&&(r=f(r));o&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */");var s=new Blob([r],{type:"text/css"}),a=t.href;t.href=URL.createObjectURL(s),a&&URL.revokeObjectURL(a)}.bind(null,n,e),o=function(){d(n),n.href&&URL.revokeObjectURL(n.href)}):(n=h(e),r=function(t,e){var n=e.css,r=e.media;r&&t.setAttribute("media",r);if(t.styleSheet)t.styleSheet.cssText=n;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(n))}}.bind(null,n),o=function(){d(n)});return r(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;r(t=e)}else o()}}t.exports=function(t,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(e=e||{}).attrs="object"==typeof e.attrs?e.attrs:{},e.singleton||"boolean"==typeof e.singleton||(e.singleton=o()),e.insertInto||(e.insertInto="head"),e.insertAt||(e.insertAt="bottom");var n=l(t,e);return c(n,e),function(t){for(var o=[],i=0;i<n.length;i++){var s=n[i];(a=r[s.id]).refs--,o.push(a)}t&&c(l(t,e),e);for(i=0;i<o.length;i++){var a;if(0===(a=o[i]).refs){for(var u=0;u<a.parts.length;u++)a.parts[u]();delete r[a.id]}}}};var y=function(){var t=[];return function(e,n){return t[e]=n,t.filter(Boolean).join("\n")}}();function m(t,e,n,r){var o=n?"":r.css;if(t.styleSheet)t.styleSheet.cssText=y(e,o);else{var i=document.createTextNode(o),s=t.childNodes;s[e]&&t.removeChild(s[e]),s.length?t.insertBefore(i,s[e]):t.appendChild(i)}}},function(t,e){t.exports=function(t){var e="undefined"!=typeof window&&window.location;if(!e)throw new Error("fixUrls requires window.location");if(!t||"string"!=typeof t)return t;var n=e.protocol+"//"+e.host,r=n+e.pathname.replace(/\/[^\/]*$/,"/");return t.replace(/url\s*\(((?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)\)/gi,function(t,e){var o,i=e.trim().replace(/^"(.*)"$/,function(t,e){return e}).replace(/^'(.*)'$/,function(t,e){return e});return/^(#|data:|http:\/\/|https:\/\/|file:\/\/\/|\s*$)/i.test(i)?t:(o=0===i.indexOf("//")?i:0===i.indexOf("/")?n+i:r+i.replace(/^\.\//,""),"url("+JSON.stringify(o)+")")})}}]);
複製程式碼
可以看見sass檔案已經轉成了css檔案,並且內嵌進了js檔案中,但是因為js要將css插入頁面渲染,所以多了一堆的程式碼,這樣很不利於優化,所以我們接下來將css檔案抽離出js檔案。
mini-css-extract-plugin
這裡我們使用的是 mini-css-extract-plugin,首先進行安裝。
yarn add mini-css-extract-plugin --dev
安裝完成後,進行webpack配置,如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const getPathByRoot = function(targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/js/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader']
}, {
test: /\.html$/,
use: ['html-loader']
}, {
test: /\.(sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.[hash].css'
})
]
}
複製程式碼
在plugins中例項化了 MiniCssExtractPlugin 並且設定了打包後的名稱,以及在rules中修改了最終的loader。
執行打包命令後,可以看見,dist目錄下static目錄中多了一個index.css檔案,並且index.html也自動引入了這個css檔案,並且js檔案的體積也小了很多。
file-loader url-loader
對於檔案的處理,可以使用 url-loader 和 file-loader,url-loader 可以將很小的icon檔案轉換成base64編碼內聯進js檔案中,這樣可以減少http的請求次數,file-loader 可以制定一個可以讓檔案輸出的路徑便於管理,更加好用的功能是可以使用import 或者 require 圖片檔案。首先安裝依賴:
yarn add file-loader url-loader --dev
其次進行webpack的配置:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const getPathByRoot = function (targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/js/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader']
}, {
test: /\.html$/,
use: ['html-loader']
}, {
test: /.(sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: '[name].[hash].[ext]',
outputPath: 'static/images',
fallback: 'file-loader',
}
}
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.[hash].css'
})
]
}
複製程式碼
可以看見首先對於檔案,使用了url-loader,如果檔案體積小於8192b 也就是 8kb 左右,那麼url-loader會將圖片轉換為base64內聯進js檔案中,如果檔案大於設定的值,那麼就會將檔案交給file-loader處理,並且將options選項交給file-loader這個是url的使用方式。
做一個實驗,在index.html檔案中新增一個img標籤,執行打包。如果圖片大於8kb圖片會被儲存到dist/static/images目錄下。
如果檔案小於8kb,那麼檔案會被內聯到Html或者js中,這取決於是哪個型別的檔案引用了這張圖片。
到這一步,一個專案最基礎的幾個功能都已經配置完成了,但是隻是這樣怎麼行,沒法高效率的開發,接下來就要介紹webpack-dev-server的用法了。
HotModuleReplacementPlugin
熱模組載入依賴於 webpack-dev-server,首先安裝
yarn add webpack-dev-server --dev
接下來進行webpack的配置:
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const getPathByRoot = function (targetPath) {
path.resolve(__dirname, '../', targetPath)
}
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/js/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader']
}, {
test: /\.html$/,
use: ['html-loader']
}, {
test: /\.(sc|c)ss$/,
// use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
use: ['style-loader', 'css-loader', 'sass-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: '[name].[hash].[ext]',
outputPath: 'static/images',
fallback: 'file-loader',
}
}
]
}]
},
devtool: 'inline-source-map',
devServer: {
host: '0.0.0.0',
publicPath: '/',
hot: true,
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'static/css/index.[hash].css'
}),
new webpack.HotModuleReplacementPlugin()
]
}
複製程式碼
首先require了webpack模組,因為 HotModuleReplacementPlugin 是webpack內建的外掛,所以不用引入。
其次將css解析的loader中,移除了 MiniCssExtractPlugin.loader ,改成了 style-loader ,將樣式內聯進html文件,這樣做是為了熱更新,如果是用了抽離css的 MiniCssExtractPlugin.loader ,就無法熱更新了(之前配置是可以熱更新的,但這次遇到了未知的情況,所以換一個處理方式),不過這個問題,後期可以將開發以及生產的webpack分開配置來實現不同的需求。
然後新增了這些配置項
{
devtool: 'inline-source-map',
devServer: {
host: '0.0.0.0',
publicPath: '/',
hot: true,
}
}
複製程式碼
inline-source-map 的作用為將js檔案打包後同時生成source map檔案用於開發時的debugger,生產環境下建議不配置,後期也會將它抽離出公共的配置。 devServer 是用於配置開發環境下的配置項,更多的引數可以檢視官方文件,這裡不做詳細介紹。
最後配置package.json的script,用於快速啟動開發服務:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config ./build/webpack.base.js --mode=production",
"dev": "webpack-dev-server --config ./build/webpack.base.js --mode=development --color --open"
},
複製程式碼
此時執行
npm run dev
開啟控制檯提示的執行的地址
Project is running at http://localhost:8080/
開啟這個地址就能夠看到站點了,同時修改js檔案以及css檔案,可以看見頁面的同步更改。
優化
完成上面的步驟基本上已經可以進行開發和生產了,但是對於工程的優化可不能停下腳步,所以得進行以下步驟。
抽離不同的配置項
就像之前說過的,維護一個公共的配置項,非公共的配置項抽離成幾個不同的功能以適應不同的需求。這裡需要使用到webpack-merge功能,先進行依賴的安裝
yarn add webpack-merge --dev
新建三個檔案,分別是 webpack.pro.js 用於配置打包生產環境的檔案, webpack.dev.js 用於配置開發環境的檔案,utils 用於抽離公共的工具函式。檔案如下:
// utils.js
const path = require('path')
module.exports.getPathByRoot = function (targetPath) {
return path.resolve(__dirname, '../', targetPath)
}
複製程式碼
// webpack.base.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const getPathByRoot = require('./utils').getPathByRoot
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/js/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
use: ['babel-loader']
}, {
test: /\.html$/,
use: ['html-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: '[name].[hash].[ext]',
outputPath: 'static/images',
fallback: 'file-loader',
}
}
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
複製程式碼
// webpack.pro.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
module.exports = merge(webpackBaseConfig, {
module: {
rules: [{
test: /\.(sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/index.[hash].css'
})
]
})
複製程式碼
// webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
module.exports = merge(webpackBaseConfig, {
module: {
rules: [{
test: /\.(sc|c)ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}]
},
devtool: 'inline-source-map',
devServer: {
host: '0.0.0.0',
publicPath: '/',
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
複製程式碼
修改package.json的script:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config ./build/webpack.pro.js --mode=production",
"dev": "webpack-dev-server --config ./build/webpack.dev.js --mode=development --color --open"
}
複製程式碼
主要是修改了使用的配置檔案為pro 和 dev 檔案。
打包前刪除之前的檔案
每次打包如果檔案修改了,那麼就會修改檔案的hash值,所以檔案不會存在衝突的情況,所以上一次打包的檔案還會存在於dist目錄,這樣會造成打包後的檔案過大不便於管理,也有可能因為瀏覽器快取的原因,請求的是之前的檔案,所以得將之前的打包檔案刪除掉,所以這裡我們需要使用到 clean-webpack-plugin,首先安裝依賴。
yarn add clean-webpack-plugin --dev
然後修改配置項:
// webpack.pro.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
const WebpackCleanPlugin = require('clean-webpack-plugin')
const getPathByRoot = require('./utils').getPathByRoot
module.exports = merge(webpackBaseConfig, {
module: {
rules: [{
test: /\.(sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/index.[hash].css'
}),
new WebpackCleanPlugin(getPathByRoot('./dist'), {
allowExternal: true
})
]
})
複製程式碼
執行打包命令,可以看見在打包之前,刪除了dist目錄。
打包後GZIP壓縮靜態資源
gzip的壓縮,可以極大的減少http請求的size,優化站點的載入速度,需進行以下配置:
先安裝依賴 compression-webpack-plugin
yarn add compression-webpack-plugin@1.1.12 --dev
這裡指定版本號為1.1.12,因為2.0的版本在我這個環境下報錯了,所以使用1.1.12
其次修改webpack配置:
//webpack.pro.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
const WebpackCleanPlugin = require('clean-webpack-plugin')
const CompressionPlugin = require("compression-webpack-plugin")
const getPathByRoot = require('./utils').getPathByRoot
module.exports = merge(webpackBaseConfig, {
module: {
rules: [{
test: /\.(sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}]
},
plugins: [
new CompressionPlugin({
test: [/\.js$/, /\.css$/],
asset: '[path].gz',
algorithm: 'gzip'
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.[hash].css'
}),
new WebpackCleanPlugin(getPathByRoot('./dist'), {
allowExternal: true
})
]
})
複製程式碼
完成配置後就可以打包出gzip檔案了,不過還得nginx伺服器開啟gzip的支援才行。
檢視webpack打包進度
使用 progress-bar-webpack-plugin 外掛,首先安裝
yarn add progress-bar-webpack-plugin --dev
其次修改webpack配置:
// webpack.base.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const getPathByRoot = require('./utils').getPathByRoot
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/js/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
exclude: /\/node_modules/,
use: ['babel-loader']
}, {
test: /\.html$/,
use: ['html-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: '[name].[hash].[ext]',
outputPath: 'static/images',
fallback: 'file-loader',
}
}
]
}]
},
plugins: [
new ProgressBarPlugin(),
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
複製程式碼
進度條就新增了。
抽離公共依賴項
因為是模組式的開發,對於jquery vue等庫的檔案,需要多次引入,如果不提取公共依賴項,那麼就會導致每個js檔案中都會有這兩個庫的檔案。如果提取出來,將會大大減少檔案的體積,優化瀏覽器的下載速度,而且可以將公共依賴項設定成強快取,可以進一步減少http請求的開支,下面我們就新增這個功能,程式碼如下:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const getPathByRoot = require('./utils').getPathByRoot
module.exports = {
entry: getPathByRoot('src/index.js'),
output: {
filename: 'static/js/index.[hash].js',
path: getPathByRoot('dist'),
publicPath: './'
},
module: {
rules: [{
test: /\.js$/,
exclude: /\/node_modules/,
use: ['babel-loader']
}, {
test: /\.html$/,
use: ['html-loader']
}, {
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: '[name].[hash].[ext]',
outputPath: 'static/images',
fallback: 'file-loader',
}
}
]
}]
},
optimization: {
splitChunks: {
chunks: 'all',
name: 'commons',
filename: 'static/js/[name].[hash].js'
}
},
plugins: [
new ProgressBarPlugin(),
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
複製程式碼
因為在webpack4.0中提取公共檔案已經是內建功能了,所以不需要外掛。只需要在配置檔案中插入這樣的一個配置就行:
optimization: {
splitChunks: {
chunks: 'all',
name: 'commons',
filename: 'static/js/[name].[hash].js'
}
},
複製程式碼
安裝jqery 和 vue進行一次測試
yarn add jquery vue
然後在src/index.js中引入這兩個庫檔案。
import './index.scss'
import $ from 'jquery'
import Vue from 'vue'
new Vue({
el: '#app'
})
$('#app').on('click', function() {
})
複製程式碼
執行打包命令後檢視js檔案會發現多出了一個common.js檔案,開啟可以看見內容只包含了jquery 和 vue 的原始碼,開啟打包後的index.html也可以看見引入了common.js檔案,說明提取公共依賴項成功了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link href="./static/css/main.bf73755b575b70e4fc39.css" rel="stylesheet"></head>
<body>
<img src="./static/images/WechatIMG5.72047b6d3b0f1c1f1dc0a1fa9c81188f.png" alt="">
<script type="text/javascript" src="./static/js/commons.bf73755b575b70e4fc39.js"></script>
<script type="text/javascript" src="./static/js/index.bf73755b575b70e4fc39.js"></script>
</body>
</html>
複製程式碼
檢視打包後的檔案結構
使用 webpack-bundle-analyzer 外掛,可以檢視到打包後的js檔案的內部細節,可以很清楚的分析哪些細節可以優化,首先安裝依賴。
yarn add webpack-bundle-analyzer --dev
然後進行webpack配置
//webpack.pro.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base')
const WebpackCleanPlugin = require('clean-webpack-plugin')
const CompressionPlugin = require("compression-webpack-plugin")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const getPathByRoot = require('./utils').getPathByRoot
module.exports = merge(webpackBaseConfig, {
module: {
rules: [{
test: /\.(sc|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}]
},
plugins: [
new CompressionPlugin({
test: [/\.js$/, /\.css$/],
asset: '[path].gz',
algorithm: 'gzip'
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.[hash].css'
}),
new WebpackCleanPlugin(getPathByRoot('./dist'), {
allowExternal: true
}),
new BundleAnalyzerPlugin()
]
})
複製程式碼
執行打包後會開啟瀏覽器有以下介面,可以很清楚的看見js檔案的內容:
借用官方文件的動圖,可以看見更直觀的細節:
結尾
到這裡,基本的配置都已經完成了,如果你想實現更多的功能,就需要自己去探索啦。 工程地址:github.com/Richard-Cho…
例外有兩個之前配置的webpack腳手架可以看一看:
用於vue元件系統開發的webpack配置: github.com/Richard-Cho…
多頁面開發的webpack配置:github.com/Richard-Cho…