webpack(4)從0到1,一行一行帶你擼

子物發表於2018-10-09

前言

對於現代web前端工程化,webpack起到了絕大的作用,此篇文章致力於讓你學習webpack的配置,能夠從無到有的配置一套屬於自己的webpack配置,此教程從基礎配置,到優化兩個維度進行講解,其中有大量的示例,文尾部分會提供git倉庫以供程式碼下載。

配置

這裡只講解配置相關的細節,不講解原理性的知識,建議熟練使用webpack之後,學習下webpack是如何工作的,這樣可以定製loader和plugin,能夠更加高效的解決專案上的問題。

初始化工程

前端工程化最基礎的東西,就是npm包管理器,這一切都是建立在這上面的,首先執行。

npm init -y

初始化package.json

前提是你安裝了node.js,下載地址

nodejs.org/en/

其中 -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"

www.cnblogs.com/tugenhua070…

執行 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,已經可以正常執行了。

wx20181008-171737

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檔案的體積也小了很多。

a1

file-loader url-loader

對於檔案的處理,可以使用 url-loaderfile-loaderurl-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目錄下。

a3

如果檔案小於8kb,那麼檔案會被內聯到Html或者js中,這取決於是哪個型別的檔案引用了這張圖片。

a2

到這一步,一個專案最基礎的幾個功能都已經配置完成了,但是隻是這樣怎麼行,沒法高效率的開發,接下來就要介紹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>
複製程式碼

a4

檢視打包後的檔案結構

使用 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檔案的內容:

wx20181009-172315

借用官方文件的動圖,可以看見更直觀的細節:

gif

結尾

到這裡,基本的配置都已經完成了,如果你想實現更多的功能,就需要自己去探索啦。 工程地址:github.com/Richard-Cho…

例外有兩個之前配置的webpack腳手架可以看一看:

用於vue元件系統開發的webpack配置: github.com/Richard-Cho…

多頁面開發的webpack配置:github.com/Richard-Cho…

相關文章