0%

模块打包工具webpack的使用

模块化开发的趋势越来越明显,特别是在大型前端项目中,模块化的开发让我们更好地组织和更灵活地使用代码,在Node端使用的是CommonJS规范的模块化,在浏览器端则是使用ES Modules模块规范。由于ES Modules是ES6才提出的,目前仅有少数版本的浏览器实现了模块化,如果我们使用模块化进行开发,就需要考虑浏览器兼容问题。另外,模块化的代码虽然在开发阶段对开发友好,但过多的模块并不利于浏览器的频繁加载调用。因此,为了解决模块化开发中的兼容和代码组织问题,我们一般使用模块打包工具来为我们实现模块代码打包为生产代码

目前主流的模块打包工具有:

  • webpack:打包所有的样式表资源。
  • parcel:极速零配置Web应用打包工具
  • rollup

本文主要就webpack的使用进行展开说明。

实际上,模块打包工具除了解决上面所说的模块规范兼容问题,还能优化组织其他资源的使用,**不仅限于js**,就webpack而言,理论上,我们应当把所有的资源文件都当成独立的模块去加载,去使用,而不仅仅是处理js模块代码。

快速上手

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph)*,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 *bundle

从 webpack v4.0.0 开始,可以不用引入一个配置文件。然而,webpack 仍然还是高度可配置的。在开始前你需要先理解四个核心概念

  • 入口(entry)
  • 输出(output)
  • loader
  • 插件(plugins)

无配置(默认配置)打包

为了方便测试我们先创建一个一些基本的文件进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- dist/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Webpack Demo</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>

<script src='./main.js'></script>
</body>
</html>
1
2
3
4
5
6
7
8
// src/header.js
export default (num, text, cls) => {
num = Math.max(1, Math.min(num, 6));
const header = document.createElement(`h${num}`);
header.textContent = text || `h${num}`;
cls && header.classList.add(cls);
return header;
}
1
2
3
4
5
// scr/index.js
import createHeader from './header'

const h2 = createHeader(2, 'hello webpack!', 'heading');
document.body.append(h2);

写好了测试文件,我们安装webpack即可使用:

1
2
3
npm init -y

npm install webpack webpack-cli --dev

在4.0以上的版本我们可以不使用配置文件直接运行打包指令:

1
yarn webpack

然后在dist目录就会出现一个main.js,这个js就是打包后的js代码,打开index.html文件就能看到执行的效果。

可以发现,默认配置的打包入口为scr/index.js,出口为dist/mian.js文件。

使用配置文件 - 基本的四个核心配置

虽然我们可以使用默认配置直接打包,但通常我们都会根据自己的需求去修改配置文件webpack.config.js

(当然你也可以声明为别的文件名,然后用--config **.js的打包参数来指定打包文件名)

入口entry

上面说过,webpack的打包是递归地构建模块依赖图的过程,而入口的配置,就是指定webpack应该使用哪个或哪些模块作为递归的入口模块

比如我们以src目录下的main.js作为打包入口文件:

1
2
3
module.exports = {
entry: './src/main.js'
}

或者使用多入口文件:

1
2
3
4
5
6
module.exports = {
entry: {
main: './src/main.js',
header: './src/header/index.js'
}
}

更多使用细节请参考https://www.webpackjs.com/concepts/entry-points/

出口output

出口即递归构建的最终结果,一个入口对应一个出口文件,一般用于指定打包的路径和文件名:

1
2
3
4
5
6
7
8
9
const path = require('path')

module.exports = {
entry: './src/main.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'main.bundle.js'
}
}

当通过多个入口起点(entry point)、代码拆分(code splitting)或各种插件(plugin)创建多个 bundle,应该使用以下一种替换方式,来赋予每个 bundle 一个唯一的名称……

1
2
3
4
5
6
7
8
// 使用入口名称
filename: "[name].bundle.js"
// 使用chunk id
filename: "[id].bundls.js"
// 使用唯一hash值
filename: "[name].[hash].bundle.js"
// 使用基于chunk内容的hash值
filename: "[chunkhash].bundle.js"

更多使用细节请参考https://www.webpackjs.com/configuration/output/

加载器loader

文件加载器是webpack构建的核心部分,我们在上文说过,webpack中,不只是js被当做模块来导出,其他资源同样需要会当做模块来打包处理。

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

在更高层面,在 webpack 的配置中 loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪个 loader。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');

const config = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
}
};

module.exports = config;

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:testuse。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 ‘.txt’ 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”

更多loader使用细节请参考:https://www.webpackjs.com/concepts/loaders/

插件plugins

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量插件接口功能极其强大,可以用来处理各种各样的任务。

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。

下面是一个自动创建html文件的插件:html-webpack-pugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

const config = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};

module.exports = config;

点击这里了解更多!

模式

在实际开发中,我们一般有两种环境的代码:开发环境和生产环境。生产环境的代码应该更接近源码,以便于我们调试和定位错误源,而生产环境一般需要压缩和更优的代码组织方式,webpack提供了一个mode字段进行模式的切换,并内置了一些基本的打包配置,通过选择 developmentproduction 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化:

1
2
3
module.exports = {
mode: 'production'
};

或者在命令行中使用指定:

1
yarn webpack --mode production

点击这里了解更多!

webpack打包运行原理

为了兼容各浏览器,模块化打包之后其实并未使用es module的语法,而是基本的es语法,“与模块化无关”,只是功能上实现的模块化的效果。

比如我们打包两个模块文件:main.jsheader.js,具体代码如下:

1
2
3
4
5
// main.js
import {header} from './header'

const h2 = header(2, 'hello webpack!', 'heading');
document.body.append(h2);
1
2
3
4
5
6
7
8
// header.js
export const header = (num, text, cls) => {
num = Math.max(1, Math.min(num, 6));
const header = document.createElement(`h${num}`);
header.textContent = text || `h${num}`;
cls && header.classList.add(cls);
return header;
}

打包后的bundle为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
(() => {
"use strict";
var __webpack_modules__ = ({
"./src/header.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"header": () => header
});
var header = function header(num, text, cls) {
num = Math.max(1, Math.min(num, 6));
var header = document.createElement("h".concat(num));
header.textContent = text || "h".concat(num);
cls && header.classList.add(cls);
return header;
};
}),
"./src/main.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
var _header__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/header.js");
var h2 = (0, _header__WEBPACK_IMPORTED_MODULE_0__.header)(2, 'hello webpack!', 'heading');
document.body.append(h2);
})
});
var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key]
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
Object.defineProperty(exports, '__esModule', {
value: true
});
};
})();
__webpack_require__("./src/main.js");
})();

我们发现,整个代码都使用了立即执行的匿名函数来执行,所有的模块代码也就变成了内部函数,也就无法从window环境下修改和访问模块代码,保证了模块代码的独立性。

在立即执行函数内部,代码可分为两个部分:模块代码的注册,模块代码的引用。

模块代码的注册

注册模块代码的机制很简单,使用一个对象来保存即可,在上面的打包代码中就是,__webpack_modules__这个变量,模块代码使用了文件相对路径作为模块的key,值为下面格式的接口(函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 
* @param _module 指向模块本身的引用变量
* @param _moduel_exports 模块的exports对象
* @param _require 用于加载一个模块的逻辑函数
*/
(_module, _moduel_exports, _require) => {
// 1.[模块化标记]define __esModule on exports(在exports对象做__esModule的属性标记)
_require.r(_moduel_exports)
// 2.[exports对象的定义:成员属性绑定]
_require.d(_moduel_exports, {
k1,
k2,
});
// 3.[模块的定义代码]
const k1 = null;
const k2 = null;
}

这样,我们返回模块的exports对象就可以使用到模块内部定义的成员变量了。

模块代码的引用

上面说到模块代码的注册,我们可以通过模块的key得到模块的注册函数,这样,传入exports的引用,我们也就拿到了被引用模块的成员变量,具体的逻辑是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function __webpack_require__(moduleId) {
// 尝试从缓存中读取引用模块
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// 创建一个新的模块
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};

// 根据模块id注册模块
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// 返回模块的exports对象
return module.exports;
}

这样,我们只需要在调用手动加载一下模块入口文件,即可自动触发模块之间的调用链:

1
__webpack_require__("./src/main.js");

各种资源加载器的使用·Loader

loader的运行机制

loader是webpack中最基本,也是最核心的打包机制之一,我们知道,webpack默认只提供了js的模块化方案,但是webpack打包希望我们将所有其他资源也看作是模块进行打包,而这里的其他资源,说的就是js资源外的其余资源,webpack相应提供了各种资源的模块化加载loader,我们可以按需进行安装使用。更多细节请参考官方说明文档

loader的作用有很多,但本质上就是将指定匹配的文件输出为loader处理后的数据,可以理解为文件格式(数据)转换工具。

下面我们自定义一个loader,来感受其中的变化过程。

假设我们有这样一个readme.md文件作为主页:

1
2
3
4
## about me
好想在哪见过你~
## link me
[@fongzhizhi](https://github.com/fongzhizhi)

我们需要加载.md文件作输入数据,我们可能有这样的写法:

1
2
3
import readme from './readme.md'

document.body.innerHTML = readme;

直接webpack打包命令会发现执行报错,这是因为md不是js语法,如果直接打包到js中就会产生报错。

所以我们需要使用loadermd文本转换为js代码段,这样才能正常插入。

我们定义一个markdown-loader

1
2
3
exports.default = function(source) {
return `export default ${ JSON.stringify(source) }`;
};

source接收数据来源,经过一系列转换后得到最终的模块化代码。

我们看一下最终的界面效果:

我们发现,md文本数据正常被页面加载了,现在我们借助第三方模块markdown来讲md语法解析为真正的html代码,

1
2
3
4
5
6
7
8
const toHTML = require('markdown').parse

exports.default = function(source) {

const html= toHTML(source);

return `export default ${ JSON.stringify(html) }`;
};

然后再次编译打包查看界面效果:

当然,如果我们使用多个loader进行加载,比如使用html-loader

1
2
3
4
5
6
7
{
test: /\.md$/,
use: [
'html-loader', // html-loader
'./webpack-markdown-loader', // 自定义的loader
],
}

那么,我们上面写的markdown-loader就可以直接返回处理过的source字符串交给下一个loader:

1
2
3
4
5
6
const marked = require('marked')
exports.default = function(source) {
const html= marked(source);
// return `export default ${ JSON.stringify(html) }`;
return html
};

关于如何编写一个loader,这里有更多的示例

文件相关loader

  • raw-loader 用于加载文件的原始内容(utf-8)。比如用于读取txt纯文本。

  • val-loader 将代码作为模块执行,并将其导出为 JS 代码。

  • url-loaderfile-loader 类似,但是如果文件大写小于一个设置的值,则返回data URL。

    比如某些图片资源,本身很小,我们就可以使用url-loader转换为data url到页面中。

    1
    2
    3
    4
    {
    test: /\.([png|jpg|svg|svg|webp]$)/,
    use: 'url-loader',
    }
    1
    2
    3
    4
    5
    import google from '../public/images/google.webp'

    const img = document.createElement('img')
    img.src = google
    document.body.append(img)

    然后在bundle.js中就可以看到图片被解析为了data url格式,并作为模块导入。

    当然,如果图片太大,不应该解析为base url,这样会让bundle文件提及变大,所以可以增加limit选项来控制文件的最大转换值,此外,如果转换失败,我们还需要指定fallcall的失败后的loader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    test: /\.(png|jpg|gif|webp)$/,
    use: {
    loader: 'url-loader',
    options: {
    limit: 5 * 1024, // 5k,
    fallback: require.resolve('file-loader'), // 转换失败或超过limit,则使用file-loader进行加载
    }
    },
    }
  • file-loader 将文件保存至输出文件夹中并返回(相对)URL。

    file-loader是普通引用资源的加载器,首先file-loader会将require的数据转换为url并输出文件数据到输出目录,本质上就是一次文件的拷贝。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    test: /\.(png|jpg|gif|svg)$/,
    use: {
    loader: 'file-loader',
    options: {
    name: '[contenthash].[ext]',
    outputPath: 'images',
    }
    },
    }
  • ref-loader 用于手动建立文件之间的依赖关系

JSON相关loader

  • json5-loader 加载并转换 JSON 5 文件

    webpack.config

    1
    2
    3
    4
    5
    {
    test: /\.(json|json5)$/,
    loader: 'json5-loader',
    type: 'javascript/auto',
    },

    userInfo.json

    1
    2
    3
    4
    {
    "name": "Jinx",
    "age": 24
    }

    entry.js

    1
    2
    3
    import userInfo from '../public/userInfo.json'

    document.body.innerHTML = `Hello, I'm ${userInfo.name}, I'm ${userInfo.age} years old.`
  • cson-loader 加载并转换 CSON 文件

语法转换Loader

这里我们学习使用babel-loaderts-loader

babel-loader

老生常谈的东西了,安装必要的模块:

1
npm i babel-loader @babel/core @babel/preset-env --dev

webpack.config

1
2
3
4
5
6
7
8
9
10
{
test: /\.js$/,
use: {
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
options: {
prtesets: ['@babel/preset-env']
}
}
}

ts-loader

如果你的项目使用了ts作为开发语言,那么打包时就需要解析为js语言。

1
npm ts-loader typescript --dev

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
devtool: 'source-map',
mode: 'development',
entry: {
main: './src/main.ts',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js',
},
resolve: {
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: [".ts", ".tsx", ".js"]
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
},
]
},
plugins: [
new HtmlWebpackPlugin(),
]
}

src/main.ts

1
2
3
4
5
6
7
8
9
10
11
import { Person } from "./interface";

export function sayHi(p: Person) {
return `Hello, I'm ${p.name}, I'm ${p.age} years old.`;
}

const jinx: Person = {
name: 'Jinx',
age: 24,
}
document.body.innerHTML = sayHi(jinx);

src/interface.ts

1
2
3
4
export interface Person {
name: string;
age: number;
}

为了让ts解析能正常工作,我们还需要在项目根目录新增tsconfig.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"removeComments": true,
"sourceMap": true,
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}

更多配置请参考:编译选项

模板Loader

主要为html模板和markdown模板。

html-loader

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
esModule: true,
preprocessor: (content, loaderContext) => {
return Handlebars.compile(content)({
firstname: 'Jinx',
lastname: 'Chiang',
});
}
}
},
},

temp.html

1
2
3
4
<div>
<p>{{ firstname }} {{ lastname }}</p>
<img src="../public/images/google.webp" alt="jinx" />
</div>

main.ts

1
2
3
import temp from './temp.html'

document.body.innerHTML = temp

注意,ts默认是不支持导如html文件的,所以我们需要使用模块声明语句进行声明:

html.d.ts

1
2
3
4
5

declare module "*.html" {
const value: string;
export default value;
}

如果未生效记得把改文件手动加入tsconfig.json配置文件include字段下。

html-loader还有很多别的用法,需要配合不同的配置,这里就不再逐一介绍。

样式Loader

  • style-loader 将模块的导出作为样式添加到 DOM 中
  • css-loader 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
  • less-loader 加载和转译 LESS 文件
  • sass-loader 加载和转译 SASS/SCSS 文件
  • postcss-loader 使用 PostCSS 加载和转译 CSS/SSS 文件
  • stylus-loader 加载和转译 Stylus 文件

style-loader & css-loader

如果css文件很少,或者说希望像模块一样导入css文件,我们就可以把css文件转化为style节点,添加到对应的页面中去。通常情况下,style-loader和css-loader是联合使用的。

1
npm i style-loader css-loader --dev

main.css

1
2
3
4
5
6
p {
background: #17dede;
border: 1px solid #e46a15;
color: #1c6ac0;
font-size: 18px;
}

main.ts

1
2
3
4
import temp from './temp.html'
import './style/main.css'

document.body.innerHTML = temp

用法1:生成style节点:

1
2
3
4
5
6
7
{
test: /\.css$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" }
]
}

用法2:生成link节点:

1
2
3
4
5
6
7
8
9
10
11
{
test: /\.css$/,
use: [
{ loader: "style-loader",
options: {
injectType: 'linkTag'
}
},
{ loader: "file-loader" }
]
}

less-loader

如果项目中使用了less语法进行样式编译,我们再使用less-loader作为一级loader,把处理过的less文件交给css-loader和style-loader即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// webpack.config.js
module.exports = {
...
module: {
rules: [{
test: /\.less$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "less-loader", options: {
strictMath: true,
noIeCompat: true
}
}]
}]
}
};

Linting和测试Loader

eslint

1
npm i eslint eslint-loader --dev
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader'],
},
],
},
// ...
};

框架Loader

常用插件的使用·Plugins

webpack 有着丰富的插件接口(rich plugin interface)。webpack 自身的多数功能都使用这个插件接口。这个插件接口使 webpack 变得极其灵活

除了loader,webpack另一个核心特性就是Plugins插件的使用,各种loader让我们能将引用资源模块化,而plugins可以让满足我们模块化开发之外的需求,比如动态创建html文件、清除文件、压缩代码等等。

用法

webpack插件是一个实现了apply方法的对象。appply方法会被webpack coimpiler调用,并且在整个编译生命周期都可以访问compiler对象。

比如:ConsoleLogOnBuildWebpackPlugin.js

1
2
3
4
5
6
7
8
9
10
11
const pluginName = 'ConsoleLogoOnBuildWebpackPlugin'

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
conpiler.hooks.run.tap(pluginName, compilation => {
console.log('webpack 构建开始!')
})
}
}

module.exports = ConsoleLogOnBuildWebpackPlugin

由于插件可以携带参数,所以在webpack配置中,必须向plugins属性传入一个new实例。

一般有两种用法,

使用配置文件使用插件

webpack.config.js

1
2
3
4
5
{
plugins: [
new ConsoleLogOnBuildWebpackPlugin()
]
}

使用node api的方式使用插件

some-node-script.js

1
2
3
4
5
6
7
8
9
const webpack = require('webpack') // 访问 webpack 运行时(runtime)
const configuration = require('./webpack..config.js') // 配置文件
const ConsoleLogOnBuildWebpackPlugin = reqiure('./src/ConsoleLogOnBuildWebpackPlugin.js') // 要使用的插件

const compiler = webpack(configuration) // 获取compiler实例
new ConsoleLogOnBuildWebpackPlugin().apply(compiler); // 插件实例化并绑定compiler
compiler.run((err, stats) => { // 执行
//...
})

compiler是webpack的支柱引擎 ,它通过 CLINode API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。大多数面向用户的插件首,会先在 Compiler 上注册。

从上面的使用案例,我们可以发现,webpack的插件机制就是简单的钩子机制,通过webpack的compiler钩子来选择性地在各个编译阶段执行你需要的插件功能。

自动生成html:html-webpack-plugin

HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。

使用尤其简单,你还可以在模板html文件的基础上进行扩展:

1
2
3
4
5
6
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './public/template/index.html'
})
]

自动清除目录文件:clean-webpack-plugin

如果你有清除目录的习惯,特别是常常切换打包模式而导致打包目录下文件不同的时候,自动清除构建目录也许会很方便。

1
npm i clean-webpack-plugin --dev
1
2
3
4
plugins: [
new cleanWebpackPlugin(),
new htmlWebpackPlugin(),
]

开发一个自定义插件

插件的使用是根据需求来决定的,webpack提供了跟多插件,也有很多优秀的第三方webpack插件,我们可以按需根据关键字检索。当然,如果某些插件无法满足你的特定需求,你可以自己动手编写一个插件,因为我们已经知道,一个webpack插件运行的基本原理只需要满足:

  • 使用compiler对象作为编译时的钩子。
  • 实现一个apply方法。

我们尝试开发一个js beautify的插件,

public/plugins/js-beautify-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const beautify =  require('js-beautify')

/**
* js格式化插件
*/
class JsBeautifyPlugin {
apply(compiler) {
console.log('JsBeautifyPlugin starting....')

// 挂载到钩子函数
// emit是文件输出到output目录之前的钩子(tap是同步模式)
compiler.hooks.emit.tap('JsBeautifyPlugin', compilation => {
for(const name in compilation.assets) {
// 读取资源文件
if (name.endsWith('.js')) {
let constents = compilation.assets[name].source();
// beautify
const beautifyConstents = beautify(constents, {
indent_size: 4,
space_in_empty_paren: true,
});
// 覆盖
compilation.assets[name] = {
source: () => beautifyConstents,
size: () => beautifyConstents.length,
}
}
}
});
}
};

module.exports = JsBeautifyPlugin;

配置后执行就可以看到效果了。

wabpack开发体验优化

自动编译

自动编译基本就是通过监听文件变化来重新打包编译实现。而要开启自动编译功能,只需要开启watch模式即可:

1
yarn webapck --watch

自动更新浏览器 & Webpack Dev Server

我们可以使用类似browserSync这样的工具开启服务器,但如果要实现自动监听还需要另外开启文件的监听,这样就导致了重复了监听任务,所以这里推荐使用webpack官方的webpack-dev-server插件,这样我们在开启watch模式的同时就会自动开启浏览器的自动刷新。

需要注意的是,很多时候安装运行由于版本不匹配导致报错,可以尝试使用下面的版本:

1
2
3
4
5
{
"webpack": "^5.6.0",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.11.0"
}

webpack-dev-server还有一些配置可供选择:

1
2
3
4
5
{
devServer: {
contentBase: path.join(__dirnamr, 'public')
}
}

更多配置请参考:devServer

Source Map

由于webpack将项目模块打包编译之后,代码的位置已经发生了变化,如果出现错误,就不能迅速定位到源文件。而source map就是为了解决这一问题:记录了bundle和源代码之间的转换映射关系。

而我们要开启sourceMap的相关配置就需要配置devtool项来根据个人需求来选择。

打包构建选项devtool

配置项请参考:https://www.webpackjs.com/configuration/devtool/

模块热替换(热更新)HMR

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块, 而无需完全刷新。本页面重点介绍其 实现,而 概念 页面提供了更多关于 它的工作原理以及为什么它有用的细节。

具体请参考:模块热替换。

webpack打包工作模式

参考https://webpack.docschina.org/configuration/mode/

webpack的打包优化optimization

参考https://webpack.docschina.org/configuration/optimization/#root.

参考

谢谢你请我吃糖!