banner
NEWS LETTER

Webpack

Scroll down

webpack

webpack术语

==bundle==
bundlewebpack打包的最终产物,webpack从入口文件开始构建所以依赖模块,得到这么一个或多个bundle文件。可以被浏览器直接加载使用

==chunk==
chunkwebpack内部处理模块时的一个中间表示,是代码块的集合。bundlechunk组成。webpack中的chunk有以下几种类型:

  • Entry Chunk:基于entry配置项
  • Normal Chunk:通常是由webpack的代码拆分功能生成的chunk,比如使用import动态导入的模块
  • Initial Chunk:包含初始加载需要的模块,是entry chunk本身或者从中拆分出来
  • Async Chunk:包含异步加载模块,通常是import()生成的
  • Vendor Chunk:包含来自node_modules的模块,通常是第三方库

==tree shaking==
通过分析导入代码的使用情况,决定实际使用到的那些依赖性,删除未使用的部分

webpack 做了什么

打包工具,一切静态资源皆可打包。webpack会分析项目结构,找到js模块以及一些其他浏览器不能直接运行的拓展语言比如typescript,将其打包为合适的格式供浏览器使用
webpack运行在nodejs

webpack的作用

webpack就像生产线,源文件经过系列处理流程后转换成输出结果,webpack通过Tapable来组织这条生产线,webpack在运行过程中会广播事件。

一次完整的webpack内部执行流程

  1. 将命令行参数和webpack配置文件合并解析,得参数对象,传给webpack执行得到compiler对象。
  2. 使用compiler对象的run方法开始编译,每次run编译都会生成一个compilation对象。
  3. 触发compiler对象的make方法,分析入口文件,调用compilationbuildModule方法创建主模块对象
  4. 生成入口文件Ast(抽象语法树),通过Ast分析和递归加载依赖模块,根据文件类型和loader配置对模块进行处理,生成模块依赖图
  5. 所有模块分析完成,执行compilationseal方法对每个chunk进行整理、优化、封装。
  6. 最后执行compileremitAsset方法把生成的文件输出到output目录中。
    image.png

loader

webpack使用loader来处理文件。webpack只能理解JavaScript和JSON文件。比如说,将lessless-loader 转换为css,将ts用ts-loader转换为js等等。

loader是一个导出为functionnode模块,它描述了webpack如何处理非js模块,将匹配到的文件一次转换,可以链式传递转换。配置loader的方式有:

  1. webpack.config.js配置文件中配置loader
  2. 命令行参数方式:webpack –module-bind ‘txt=raw-loader’
  3. 内联使用:
    1
    2
    3
    import xxx from 'raw-loader!./file.txt'
    // or
    require('style-loader!css-loader?minimize~./main.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
/* 示例,使用*标记的属性表示必传配置项 */
rules: [
{
*test: '/\.css', // 文件匹配,多用正则
*use: ['style-loader', 'css-loader'],// loader的执行顺序为从后往前
exclude: /node_modules/, // 不包括的范围
include: ['xxx'], // 包括的范围
options: {}
},
{
test: '/\.(mp4|webm|wav)(\?.*)?$/',
use: [
{
loader: 'url-loader',
options: {
limit: 10240, //kb
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash.8].[ext]'
}
}
},
}
],
include: ['assets/'],
exclude: //
},
]

==常用loader:==

  1. babel-loader
  2. css相关:css/style/less/postcss-loader
  3. File-loader:将文件处理后,移动到输出目录中
  4. url-loader:一般与file-loader搭配使用。如果文件小于限制的大小,返回base64编码,否则使用file-loader将文件移动到输出目录中
  5. css-loader:支持css modules,使用方法为在loader配置中写为css-loader?module

plugin

webpack plugin是一个具有 apply 属性的js对象。pluginloader更强大,它是webpack的核心功能,在webpack运行过程中,webpack会广播事件,plugin只需要监听它关心的事件,就能加入这条生产线。plugin通过钩子可以涉及整个构建流程,可以做一些构建范围内的事情。从代码来看,webpack编译代码过程中,会触发一系列tapable钩子事件,plugin做的事就是找到这些钩子,往上面挂上自己的任务,即注册事件,这样注册的事件就会随着钩子的触发而执行

webpack内置的plugin有:

  1. uglifyJsPlugin,用于压缩和混淆代码;
  2. commonChunkPlugin,用于将第三方库和业务代码分开打包,提高打包效率。通过plugin可以访问compilercomplilation过程,通过钩子可拦截webpack的执行。

下面列举一些有用的webpack plugin

  • ==htmlWebpackPlugin==
    会生成默认的index.html文件,会替换原来的html文件
  • ==WebpackManifestPlugin==
    webpack使用manifest追踪所有 模块 -> 输出之间的映射,此插件可以将manifest数据提取为json文件

配置示例:

1
2
3
4
5
6
// webpack打包分析插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
...
plugins: [
new BundleAnalyzerPlugin();
]

==loader比较 plugin==
loader是转换器,非js模块 => js模块(常见)、js => js(较少,比如babel-loader/typescript-loader)、非js => 非js(少见)
plugin负责在特定的生命周期执行任务,能力更广泛一些

module

webpack中一切都是modulewebpack支持的模块包括,ES2015模块,使用import加载;commonJs模块,使用require加载;AMD模块,使用require加载,

webpack可配置项

入口文件

常见的入口配置:
分离app和第三方库,这样的配置可以将第三方库单独打包成chunk,浏览器可以独立缓存他们,这种做法适合在webpack<4的版本中使用,在webpack>=4的版本,建议使用optimization.splitChunks将vendor和app应用程序分开,而不要单独设置entry

1
2
3
4
entry: {
main: './src/main.js',
vendor: ['vue', 'vue-router', 'xxx']
}

多页面应用程序。在多页面应用程序中,server 会拉取一个新的 HTML 文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事,例如使用 optimization.splitChunks为页面间共享的应用程序代码创建 bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益。

1
2
3
4
5
entry: {
pageOne: './src/page1.js',
pageTwo: './src/page2.js',
pageThree: './src/page3.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
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
// webpack.config.js
const path = require('path');
const plugin = require('');
module.exports = {
# 入口文件
entry: {
// key是入口文件名,value是入口文件路径
index: './src/index.js';
// 当配置多个入口文件时,会有对应的多个输出文件
},
devtools: 'inline-source-map', // 开启js source map功能
# 输出文件
output: {
filename: '[name].js', // name代表用内置的name变量替换,
chunkFilename: '', // 配置无入口的出口输出时的文件名
path: path.resolve(__dirname, 'dist'), // 绝对路径
publicPath: 'https://cdn.example.com/public/xxx', // 需要放到cdn上的一些异步加载的数据
crossOriginLoading: '', // 用于配置jsonp插入的<script>标签的crossorigin值,可选anonymous(默认)、use-credentials(带上cookies)
libraryTarget: '', // 枚举类型,可选'var', 'commonjs', 'commonjs2', 'this', 'window', 'global'
libraryExport: '', // 配置哪些子模块需要导出,在libraryTarget设置为'commonjs'或者'commonjs2'时有效
clean: true, // 配置为true将在每次构建前清理dist文件
},
/* module */
module: {
rules: [
{
test: /\.css|scss$/,
include: '',
exclude: '',
use: ['babel-loader?cacheDirectory', 'style-loader', 'css-loader', 'scss-loader'],
// 以上几项的值均可以是数组,数组项之间是‘或’的关系
}
]
},
noParse: [
// 不用处理和解析的模块
/xxx.js/, // 正则匹配
]
/*resolve */
resolve: {
alias: {
components: './src/components/',
'react$': '/',
},
extensions: ['.js', '.jsx'], // 导入语句未带文件后缀时,根据这里的后缀列表去尝试匹配
modules: ['./src/components', 'node_modules'], // 去这些目录下寻找第三方模块,默认只有node_modules,配置后可直接使用import 'button'
enforceExtension: false, // true时强制所有导入语句必须带上文件后缀
enforceModuleExtension: false, // true时强制所有第三方模块导入语句必须带上文件后缀
}
# 插件,重点在这些plugin的配置项
plugin: [
new plugin({
name: ''
})
]
}

webpack 模块加载原理

从一个简单的例子开始(webpack版本5),入口文件是index.js,在index.js中引入了三个模块

1
2
3
4
5
6
// index.js
import "./default.css";
import module from './test';
import test2 from './test2';

console.log(module.name);
1
2
3
4
/* default.css */
body {
background-color: aqua;
}
1
2
3
4
// test.js
module.exports = {
name: 'kafka',
}
1
2
3
4
5
// test2.js
export default function f2() {
return 'Alice';
}
export function ff() {}

打包出来的文件为:

fold
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
(() => {
// webpackBootstrap
var __webpack_modules__ = {
"./src/default.css": (module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval(
`__webpack_require__.r(__webpack_exports__);\n/* harmony export */
__webpack_require__.d(__webpack_exports__, {
/* harmony export */
"default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */
});
/* harmony import */
var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ../node_modules/css-loader/dist/runtime/noSourceMaps.js */
"./node_modules/css-loader/dist/runtime/noSourceMaps.js"
);
/* harmony import */
var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__
);
/* harmony import */
var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
/*! ../node_modules/css-loader/dist/runtime/api.js */
"./node_modules/css-loader/dist/runtime/api.js"
);
/* harmony import */
var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports

var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
// Module
___CSS_LOADER_EXPORT___.push([module.id, "body {\\n background-color: aqua;\\n}", ""]);
// Exports\n/* harmony default export */
const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
//# sourceURL=webpack://my-webpack/./src/default.css?`
);
},

"./node_modules/css-loader/dist/runtime/api.js": (module) => {
"use strict";
eval(
'\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\nmodule.exports = function (cssWithMappingToString) {\n var list = []; // return the list of modules as css string\n\n list.toString = function toString() {\n return this.map(function (item) {\n var content = "";\n var needLayer = typeof item[5] !== "undefined";\n\n if (item[4]) {\n content += "@supports (".concat(item[4], ") {");\n }\n\n if (item[2]) {\n content += "@media ".concat(item[2], " {");\n }\n\n if (needLayer) {\n content += "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {");\n }\n\n content += cssWithMappingToString(item);\n\n if (needLayer) {\n content += "}";\n }\n\n if (item[2]) {\n content += "}";\n }\n\n if (item[4]) {\n content += "}";\n }\n\n return content;\n }).join("");\n }; // import a list of modules into the list\n\n\n list.i = function i(modules, media, dedupe, supports, layer) {\n if (typeof modules === "string") {\n modules = [[null, modules, undefined]];\n }\n\n var alreadyImportedModules = {};\n\n if (dedupe) {\n for (var k = 0; k < this.length; k++) {\n var id = this[k][0];\n\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n\n for (var _k = 0; _k < modules.length; _k++) {\n var item = [].concat(modules[_k]);\n\n if (dedupe && alreadyImportedModules[item[0]]) {\n continue;\n }\n\n if (typeof layer !== "undefined") {\n if (typeof item[5] === "undefined") {\n item[5] = layer;\n } else {\n item[1] = "@layer".concat(item[5].length > 0 ? " ".concat(item[5]) : "", " {").concat(item[1], "}");\n item[5] = layer;\n }\n }\n\n if (media) {\n if (!item[2]) {\n item[2] = media;\n } else {\n item[1] = "@media ".concat(item[2], " {").concat(item[1], "}");\n item[2] = media;\n }\n }\n\n if (supports) {\n if (!item[4]) {\n item[4] = "".concat(supports);\n } else {\n item[1] = "@supports (".concat(item[4], ") {").concat(item[1], "}");\n item[4] = supports;\n }\n }\n\n list.push(item);\n }\n };\n\n return list;\n};\n\n//# sourceURL=webpack://my-webpack/./node_modules/css-loader/dist/runtime/api.js?'
);
},

"./node_modules/css-loader/dist/runtime/noSourceMaps.js": (module) => {
"use strict";
eval(
"\n\nmodule.exports = function (i) {\n return i[1];\n};\n\n//# sourceURL=webpack://my-webpack/./node_modules/css-loader/dist/runtime/noSourceMaps.js?"
);
},

"./src/index.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
eval(
'__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _default_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./default.css */ "./src/default.css");\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./test */ "./src/test.js");\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_test__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./test2 */ "./src/test2.js");\n\n\n\n\nconsole.log((_test__WEBPACK_IMPORTED_MODULE_1___default().name));\n\n//# sourceURL=webpack://my-webpack/./src/index.js?'
);
},

"./src/test.js": (module) => {
eval(
"module.exports = {\n name: 'kafka'\n}\n\n//# sourceURL=webpack://my-webpack/./src/test.js?"
);
},

"./src/test2.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
eval(
"__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ f2)\n/* harmony export */ });\nfunction f2() {\n return 'Alice';\n}\n\n//# sourceURL=webpack://my-webpack/./src/test2.js?"
);
},
};
// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
id: moduleId,
// no module.loaded needed
exports: {},
});

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

/* webpack/runtime/compat get default export */
(() => {
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = (module) => {
var getter =
module && module.__esModule ? () => module["default"] : () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();

/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony 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/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();

可以看到打包后出来的文件是一个立即执行函数,这个立即执行函数内做的事情有:

  1. __webpack_modules__对象包含了所有打包好的模块,以文件路径为key,文件内容为value
  2. 定义一个模块缓存对象__webpack_module_cache__
  3. 定义一个模块加载函数 __webpack_require__
  4. ??
  5. 使用__webpack_require__加载入口模块,赋值给__webpack_exports__

__webpack_require__函数

函数的内容如下:

fold
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
id: moduleId,
// no module.loaded needed
exports: {},
});

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}
  1. 首先判断要加载的模块是否在缓存中,如果是直接返回
  2. 否则新建一个模块module,并放入模块缓存中,modulekeymoduleId,内容是{ id: moduleId, exports: {} },加入缓存时exports的值固定为空对象,moduleId就是文件的路径名
  3. 执行模块函数,执行时传入了三个参数,分别是modulemodule.exports__webpack_require__函数
  4. 返回模块的exports对象

__webpack_modules__中入口函数打包后的内容(处理了下,eval作为参数的代码应该用引号包裹成字符串格式):

1
2
3
4
"./src/index.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval('文件内容在下一个👇🏻');
}
1
2
3
4
5
6
7
8
9
// eval('')
__webpack_require__.r(__webpack_exports__);\n
var _default_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/default.css");
var _test__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/test.js");
var _test__WEBPACK_IMPORTED_MODULE_1___default = __webpack_require__.n(_test__WEBPACK_IMPORTED_MODULE_1__);
var _test2__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("./src/test2.js");
console.log((_test__WEBPACK_IMPORTED_MODULE_1___default().name));
(0,_test2__WEBPACK_IMPORTED_MODULE_2__["default"])();
//# sourceURL=webpack://my-webpack/./src/index.js?

__webpack_require__.r

对第二个参数__webpack_exports__的处理用的是__webpack_require__.r函数,看下这个函数是如何定义的:

1
2
3
4
5
6
7
8
9
// define __esModule on exports
__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__.r的作用是给__webpack_exports__增加一个__esModule = true属性,表示这个模块是es6模块。在遇到import导入或者export导出时,就会使用这个函数。其作用是为了处理混合使用CommonJSES6 module的情况(上面的代码就是这种情况,在test.js中用module.exports导出模块,在index.js中用import导入模块)

__webpack_require__.d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// define getter functions for harmony 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_exports__增加属性

可以看到在打包后的test2模块用到了__webpack_require__.d函数.根据test2模块内容导出的内容为__webpack_exports__增加属性,对于exprot default function f2的导出方式,赋值default = f2。而对于export function f2,赋值[ff] = ff

1
2
3
4
5
6
7
8
9
10
11
12
13
"./src/test2.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval(''
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => (f2),
"ff": () => (ff)
});
function f2() {return \'Alice\';}
function ff() {}
//# sourceURL=webpack://my-webpack/./src/test2.js?
'');
}

_webpack_require__.n

1
2
3
4
5
6
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = (module) => {
var getter = module && module.__esModule ? () => module["default"] : () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};

判断该模块是否是es6模块,是则返回module[‘default’],否则返回module,返回之前为返回值增加getter属性{a: getter}

搞懂webpack hmr 热更新

热更新(HMRHot ModuleReplacement),指的是当对代码做了修改并保存好后,webpack会自动对代码进行重新打包,将改动的模块发送到浏览器端,替换对应的旧模块,实现局部更新页面。

在webpack.config.js内配置启用hmr:

1
2
3
4
5
// ....
devServer: {
hot: true,
}
// ....

HMR原理

…todo

webpack分包

权衡拆包和并包,是webpack分包的重点
chunks: 表示从哪些chunks里抽取代码,有三个值:

  1. initial:初始块,分开打包异步/非异步模块
  2. async:按需加载块, 类似initial,但是不会把同步引入的模块提取到vendors中
  3. all:全部块,无视异步/非异步,如果有异步,统一为异步,也就是提取成一个块,而不是放到入口文件打包内容中

sourcemap

sourcemap的作用是标识打包前后bundle和源码的对应关系

webpack常用包

webpack-node-externals

配置在externals选项,externals选项用于排除某些import的包打包到bundle中,webpack-node-externals能够排除node_modules中的所有模块,node模块将被保留为require('module'),插件用法如下:

1
npm i webpack-node-externals --save-dev
1
2
3
4
5
6
7
8
9
// # webpack.config.js
const nodeExternals = require('webpack-node-externals')
module.exports = {
//...
target: 'node', // 忽略内置模块比如path, fs
// webpack5中替换target: 'node'如下:
externalsPresets: {node: true},
externals: [nodeExternals]
}

postcss-px2rem-include

项目中写移动端适配时用到了这个包,它是在postcss-px2rem基础上加了一个include选项。使用的时候,在postcss.config.js中的plugins选项增加:

1
2
3
4
require('postcss-px2rem-include')({
remUnit: 37.5,
include: /download-index-m|download-album-m/i
})

用正则去匹配文件名,命中的文件,对其字符串格式的css转换为rem格式并进行替换,调用的是Px2rem的generateRem方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = postcss.plugin(
'postcss-px2rem-include',
function (options) {
return function (css, result) {
if(options.include && css.source.input.file.match(options.include) !== null) {
var oldCssText = css.toString();
var px2remIns = new Px2rem(options);
var newCssText = px2remIns.generateRem(oldCssText);
result.root = postcss.parse(newCssText)
} else {
result.root = css;
}
}
});

下面进到px2rem看一下它是如何转换px为rem的。进入px2rem/lib/px2rem.js文件,可以在Px2rem的原型上找到generateRem方法,入参是string类型的cssText,转换为css ast后处理,主要函数是processRules,这里有一个css ast的概念,css抽象语法树,它和javascript的ast是一样的,用树的形式表示代码的语法结构。例如,下面是一段css代码及其对应的css ast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
}
@keyframes rotate {
from {
transform: rotate(0);
}
to {
tranfrom: rotate(180deg);
}
}

#main {
width: 100px;
height: 100px;
border: 1px solid black;
}

ul li {
padding: 5px;
}

对应的ast为
css ast example
借助上面的ast作为参考,看下面这段processRules代码:

fold title:function_processRules
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
function processRules(rules, noDealPx) { // FIXME: keyframes do not support `force px` comment
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];

if (rule.type === 'media') {
processRules(rule.rules); // recursive invocation while dealing with media queries
continue;
} else if (rule.type === 'keyframes') {
processRules(rule.keyframes, true); // recursive invocation while dealing with keyframes
continue;
} else if (rule.type !== 'rule' && rule.type !== 'keyframe') {
continue;
}
if (!noDealPx) {
// generate 3 new rules which has [data-dpr]
var newRules = [];
for (var dpr = 1; dpr <= 3; dpr++) {
var newRule = {};
newRule.type = rule.type;
newRule.selectors = rule.selectors.map(function (sel) {
return '[data-dpr="' + dpr + '"] ' + sel;
});
newRule.declarations = [];
newRules.push(newRule);
}
}
var declarations = rule.declarations;
for (var j = 0; j < declarations.length; j++) {
var declaration = declarations[j];
// need transform: declaration && has 'px'
if (declaration.type === 'declaration' && pxRegExp.test(declaration.value)) {
var nextDeclaration = rule.declarations[j + 1];
if (nextDeclaration && nextDeclaration.type === 'comment') { // next next declaration is comment
if (nextDeclaration.comment.trim() === config.forcePxComment) { // force px
// do not transform `0px`
if (declaration.value === '0px') {
declaration.value = '0';
declarations.splice(j + 1, 1); // delete corresponding comment
continue;
}

if (!noDealPx) {
// generate 3 new declarations and put them in the new rules which has [data-dpr]
for (var dpr = 1; dpr <= 3; dpr++) {
var newDeclaration = {};
extend(true, newDeclaration, declaration);
newDeclaration.value = self._getCalcValue('px', newDeclaration.value, dpr);
newRules[dpr - 1].declarations.push(newDeclaration);
}
declarations.splice(j, 2); // delete this rule and corresponding comment
j--;
} else { // FIXME: keyframes do not support `force px` comment
declaration.value = self._getCalcValue('rem', declaration.value); // common transform
declarations.splice(j + 1, 1); // delete corresponding comment
}
} else if (nextDeclaration.comment.trim() === config.keepComment) { // no transform
declarations.splice(j + 1, 1); // delete corresponding comment
} else {
declaration.value = self._getCalcValue('rem', declaration.value); // common transform
}
} else {
declaration.value = self._getCalcValue('rem', declaration.value); // common transform
}
}
}
// if the origin rule has no declarations, delete it
if (!rules[i].declarations.length) {
rules.splice(i, 1);
i--;
}
if (!noDealPx) {
// add the new rules which contain declarations that are forced to use px
if (newRules[0].declarations.length) {
rules.splice(i + 1, 0, newRules[0], newRules[1], newRules[2]);
i += 3; // skip the added new rules
}
}
}
}

除了提供px -> rem这个转换,px2rem还有一个生成x1, x2, x3版本以适配设备dpr的样式,接下来看看这个怎么做的

fold title:适配dpr_x1_x2_x3
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
Px2rem.prototype.generateThree = function (cssText, dpr) {
dpr = dpr || 2;
var self = this;
var config = self.config;
var astObj = css.parse(cssText);

function processRules(rules) {
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule.type === 'media') {
// 媒体查询类型,递归处理,从上图的棕色框可以看出,media里面嵌套有type=='rule'类型节点
processRules(rule.rules);
continue;
} else if (rule.type === 'keyframes') {
// 动画,递归处理,也是嵌套
processRules(rule.keyframes);
continue;
} else if (rule.type !== 'rule' && rule.type !== 'keyframe') {
continue;
}
// 处理type==='rule'和'keyframe'类型的节点
// 以上图的width:100px 这条属性来说,declarations是一个数组
var declarations = rule.declarations;
for (var j = 0; j < declarations.length; j++) {
var declaration = declarations[j];
// need transform: declaration && has 'px'
if (declaration.type === 'declaration' && pxRegExp.test(declaration.value)) {
var nextDeclaration = rule.declarations[j + 1];
if (nextDeclaration && nextDeclaration.type==='comment') {
// 下一条是一个注释
if(nextDeclaration.comment.trim()===config.keepComment) {
// no transform
declarations.splice(j + 1, 1);
continue;
} else if (nextDeclaration.comment.trim() === config.forcePxComment) {
// force px
// 删除注释
declarations.splice(j + 1, 1);
}
}
// 替换 px 为对应的 rem
declaration.value = self._getCalcValue('px', declaration.value, dpr);
}
}
}
}
processRules(astObj.stylesheet.rules);
return css.stringify(astObj);
};

==HtmlWebpackPlugin==
是webpack打包的常用插件,用于生成和修改HTML文件,能够根据模板生成html文件,并自动将打包得到的js/css等资源注入html文件中

==miniCssExtractPlugin==
用于将css从js文件中提取出来,生成独立的css文件,经过css-loader处理过后的css文件内联在js文件中,用此插件将其提取为css文件,减小js文件大小,可提高性能

webpack-bundle-analyzer

一个plugin,将bundle内容展示为一个可视化交互式树状图

postcss

可以看做是一个平台,插件在上面跑,它提供了AST解析器,postcss+插件能帮助我们处理css

html-webpack-plugin

uglyfyjs-webpack-plugin

webpack VS rollup

一般来说,webpack更合适与大一点的业务,能够提供兼容性保障,webpack在打包时会注入很多自己的代码,将commonJS和esm都实现为自己的require,见上面的模块化
rollup则更适合用于一些工具SDK的打包,打包出来的内容比较纯净,并且ESM更容易进行tree-shaking

bundle VS bundleless

image.png

实际场景问题记录

项目打包占用内存过多,超出了node v8引擎默认的最大内存,打包失败问题

一次有效的解决方式:修改最大使用内存量
两种方式:一种是在启动脚本内加--max-old-space-size=4096
另一种是通过修改变量NODE_OPTIONS全局设置node v8引擎最大内存使用量 export NODE_OPTIONS=--max-old-space-size=4096

参考资料

  1. 带你入门前端工程-构建工具-webpack
其他文章
cover
Vue
  • 25/10/20
  • 10:00