进阶用法
自动清理构建目录
避免构建前每次都需要手动删除 dist
- 使用 npm scripts
{
+ "scripts": {
+ "build": "rm -rf ./dist && webpack",
+ "build2": "rimraf ./dist && webpack",
+ },
+}
+
- 使用 CleanWebpackPlugin 插件。默认 删除 output 指定的输出目录
module.exports = {
+ entry: './src/index.js',
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'bundle.js',
+ },
+ plugins: [
+ new CleanWebpackPlugin()
+ ]
+};
+
PostCSS 插件 autoprefixer 自动补齐 CSS3 前缀
CSS3 属性为什么需要前缀?
- 各个浏览器实现有差别,需要进行区分
- Trident(-ms)
- Geko(-moz)
- Webkit(-webkit)
- Presto(-o)
根据 Can I Use 规则
module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.(le|c)ss$/,
+ use: [
+ 'style-loader',
+ 'css-loader',
+ 'less-loader',
+ {
+ loader: 'postcss-loader',
+ options: {
+ plugins: () => [
+ require('autoprefixer')({
+ browsers: ['last 2 version', '>1%', 'ios 7']
+ })
+ ]
+ }
+ }
+ ]
+ },
+ ]
+ }
+};
+
移动端 CSS px 自动转换 rem
移动设备浏览器分辨率不同。 早期使用 CSS 媒体查询实现响应式布局,缺点是需要写多套适配样式代码。
rem 是什么?
W3C 对 rem 的定义为 font-size of the root element(根元素的字体大小)
- rem 是相对单位
- px 是绝对单位
px2rem-loader 和 页面渲染时根元素的 font-size 值大小(可以使用lib-flexible)结合使用
module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.(le|c)ss$/,
+ use: [
+ 'style-loader',
+ 'css-loader',
+ 'less-loader',
+ {
+ loader: 'postcss-loader',
+ options: {
+ plugins: () => [
+ require('autoprefixer')({
+ browsers: ['last 2 version', '>1%', 'ios 7']
+ })
+ ]
+ }
+ },
+ {
+ loader: 'px2rem-loader',
+ options: {
+ remUnit: 16,
+ remPrecesion: 8,
+ }
+ }
+ ]
+ },
+ ]
+ }
+};
+
<!DOCTYPE html>
+<html lang="zh">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>search</title>
+ <script type="text/javascript">
+ // lib-flexible 库代码
+ </script>
+ </head>
+ <body>
+ <div id="root"></div>
+ </body>
+</html>
+
+
静态资源内联
- 代码
- 页面框架的初始化脚本
- 上报相关打点
- CSS 内联避免页面闪动
- 请求
- 减少 HTTP 网络请求数
- 小图片或字体内联(url-loader)
意义
- 工程维护 常见场景:
- 对于多页面应用(MPA)中的许多重复内容如 meta 信息等,借助 html-webpack-plugin 将 meta 信息内联进去;
- 小图片、字体等文件转换为 base64 并内联源代码中。
- 页面加载性能 减少 HTTP 网络请求数
- 页面加载体验 我们都知道浏览器解析 HTML 源码是从上到下解析,因此我们会把 CSS 放到头部,JS 放置到底部。以 SSR 场景为例,如果不将打包出来的 CSS 内联进 HTML 里面,HTML 出来的时候页面的结构已经有了,但是还需要发送一次请求去请求 css,这个时候就会出现页面闪烁,网络情况差的时候更加明显。
HTML / JS 内联引入
使用 ejs 语法结合 raw-loader 实现内联
<script>${require('raw-loader!babel-loader!./meta.html')}</script> +<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script> +
实现自定义 loader
+const fs = require('fs'); +const path = require('path'); + +const getContent = (matched, reg, resourcePath) => { + const result = matched.match(reg); + const relativePath = result && result[1]; + const absolutePath = path.join(path.dirname(resourcePath), relativePath); + return fs.readFileSync(absolutePath, 'utf-8'); +}; + +module.exports = function(content) { + const htmlReg = /<link.*?href=".*?\__inline">/gmi; + const jsReg = /<script.*?src=".*?\?__inline".*?>.*?<\/script>/gmi; + + content = content.replace(jsReg, (matched) => { + const jsContent = getContent(matched, /src="(.*)\?__inline/, this.resourcePath); + return `<script type="text/javascript">${jsContent}</script>`; + }).replace(htmlReg, (matched) => { + const htmlContent = getContent(matched, /href="(.*)\?__inline/, this.resourcePath); + return htmlContent; + }); + + return `module.exports = ${JSON.stringify(content)}`; +} + +
模板文件中使用
<link href="./meta.html?__inline" /> +<script type="text/javascript" src="../node_modules//lib-flexible/flexible.js?__inline"></script> +
CSS 内联引入
核心思路: 将页面打包过程的产生的所有 CSS 提取成一个独立的文件,然后将这个 CSS 文件内联进 HTML head 标签内,需要使用 mini-css-extract-plugin 和 html-inline-css-webpack-plugin 实现。
style-loader
module.exports = { + module: { + rules: [ + { + test: /\.(le|c)ss$/, + use: [ + { + loader: 'style-loader', + options: { + insertAt: 'top', // 样式插入到<head>中 + singleton: true, // 将所有的style标签合并成一个 + } + }, + 'css-loader', + 'less-loader', + { + loader: 'postcss-loader', + options: { + plugins: () => [ + require('autoprefixer')({ + browsers: ['last 2 version', '>1%', 'ios 7'] + }) + ] + } + }, + { + loader: 'px2rem-loader', + options: { + remUnit: 16, + remPrecesion: 8, + } + } + ] + }, + ] + } +}; +
html-inline-css-webpack-plugin 需要放在 html-webpack-plugin 后面
module.exports = { + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name]__[contenthash:8].css', + }), + new HtmlWebpackPlugin(), + new HTMLInlineCSSWebpackPlugin() + ] + }; +
图片、字体内联
使用 url-loader
module.exports = {
+ module: {
+ rules: [
+ {
+ test: /.(png|jpg|gif|jpeg)$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ name: '[name]_[hash:8].[ext]',
+ limit: 10240
+ }
+ }
+ ]
+ },
+ {
+ test: /.(woff|woff2|eot|ttf|otf)$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ name: '[name]_[hash:8][ext]',
+ limit: 10240
+ }
+ }
+ ]
+ }
+ ]
+ }
+ };
+
缺点:不能个性化设置某张图片自动编码
实现自定义 loader ,对使用?__inline
语法进行自动编码。 核心代码如下:
export default function loader(content) {
+ const options = loaderUtils.getOptions(this) || {};
+
+ validateOptions(schema, options, {
+ name: 'File Loader',
+ baseDataPath: 'options',
+ });
+
+ const hasInlineFlag = /\?__inline$/.test(this.resource);
+
+ if (hasInlineFlag) {
+ const file = this.resourcePath;
+ // Get MIME type
+ const mimetype = options.mimetype || mime.getType(file);
+
+ if (typeof content === 'string') {
+ content = Buffer.from(content);
+ }
+
+ return `module.exports = ${JSON.stringify(
+ `data:${mimetype || ''};base64,${content.toString('base64')}`
+ )}`;
+ }
+}
+
source map
通过 source map 定位到源代码
开发环境开启,线上环境关闭
线上排查问题时可将 source map 上传到错误监控系统,用于排查问题。
类型 | 描述 |
---|---|
eval | 使用 eval 包裹模块代码 |
source map | 生成.map 文件 |
cheap | 不包含列信息 |
inline | 将.map 作为 DataURI 嵌入,不单独生成.map 文件 |
module | 包含 loader 的 source map |
devtool | 首次构建 | 二次构建 | 是否适合生产环境 | 定位的代码 |
---|---|---|---|---|
(none) | +++ | +++ | yes | 最终输出的代码 |
eval | +++ | +++ | no | webpack 生成的代码(单独模块) |
cheap-eval-source-map | + | ++ | no | 经过 loader 转换后的代码(只能看到行) |
cheap-module-eval-source-map | O | ++ | no | 源代码(只能看到行) |
eval-source-map | -- | + | no | 源代码 |
cheap-source-map | + | O | yes | 经过 loader 转化后的代码(只能看到行) |
cheap-module-source-map | O | - | yes | 源代码(只能看到行) |
inline-cheap-source-map | + | O | no | 经过 loader 转换后的代码(只能看到行) |
inline-cheap-module-source-map | O | - | no | 源代码(只能看到行) |
source-map | -- | -- | yes | 源代码 |
inline-source-map | -- | -- | no | 源代码 |
hidden-source-map | -- | -- | yes | 源代码 |
提取页面公共资源
基础库分离。
使用 html-webpack-externals-plugin 例如:将 react、react-dom 基础包通过 cdn 引入,不打入 bundle 中。
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin'); + +module.exports = { + plugins: [ + new HtmlWebpackExternalsPlugin({ + externals: [ + { + module: 'react', + entry: '//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123', + global: 'React' + }, + { + module: 'react-dom', + entry: '//11.url.cn/now/lib/15.1.0/react-dom.min.js?_bid=3123', + global: 'ReactDOM' + } + ] + }) + ] +} +
利用 SplitChunksPlugin 进行公共脚本分离 Webpack 4 内置,替代 CommonsChunkPlugin 插件
- async 异步引入的库进行分离(默认)
- initial 同步引入的库进行分离
- all 所有引入的库进行分离(推荐)
module.exports = { + optimization: { + splitChunks: { + chunks: 'async', + minSize: 30000, + maxSize: 0, + minChunks: 1, + maxAsyncRequests: 5, + maxInitialRequests: 3, + automaticNameDelimiter: '~', + name: true, + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + priority: -10 + } + } + } + } +} +
- 分离基础包
module.exports = { + optimization: { + splitChunks: { + cacheGroups: { + commons: { + test: /(react|react-dom)/, + name: 'vendors', + chunks: 'all', + } + } + } + } +} +
- 提取页面公共文件
minChunks:设置最小引用次数 minSize:分离包体积的大小
module.exports = { + optimization: { + splitChunks: { + minSize: 0, + cacheGroups: { + commons: { + name: 'commons', + chunks: 'all', + minChunks: 2 + } + } + } + } +} +
优化构建时命令行的显示日志
统计信息 stats
预设值 | 可选项 | 描述 |
---|---|---|
errors-only | none | 只在发生错误时输出 |
minimal | none | 只在发生错误或有新的编译时输出 |
none | false | 没有输出 |
normal | true | 标准输出 |
verbose | none | 全部输出 |
插件优化 friendly-errors-webpack-plugin
- success
- warning
- error
module.exports = {
+ plugins: [
+ new FriendlyErrorsWebpackPlugin(),
+ ],
+ stats: 'errors-only'
+}
+
构建异常和中断处理
如何判断构建是否成功? 在 CI/CD 的 pipeline 或者发布系统需要知道当前构建状态
每次构建完成后输入 echo $? 获取错误码
webpack4 之前的版本构建失败不会抛出错误码(error code)
Node.js 中的 process.exit 规范
- 0 表示成功完成,回调函数中,err 为 null
- 非 0 表示执行失败,回调函数中,err 不为 null,err.code 为传给 exit 的数字
如何主动捕获并处理构建错误?
compiler 在每次构建结束后触发 done hook
module.exports = { + plugins: [ + // webpack 4 + function() { + this.hooks.done.tap('done', stats => { + const { errors } = stats.compilation; + if (errors && errors.length + && process.argv.indexOf('--watch') === -1) { + console.log('Build error.'); + process.exit(1); + } + }); + } + ], +} +
process.exit 主动处理构建报错
Tree Shaking 原理分析及使用
DCE(Dead Code Elimination)
死码消除(Dead code elimination)是一种编译最优化技术,它的用途是移除对程序运行结果没有任何影响的代码。
- 可以减少程序的大小;
- 可以避免程序在运行中进行不相关的运算行为,减少它运行的时间。
不会被运行到的代码(unreachable code)以及只会影响到无关程序运行结果的变量(Dead Variables),都是死码(Dead code)的范畴。
- 代码不会被执行,不可到达
- 代码执行的结果不会被用到
- 代码只会影响死变量(只写不读)
Tree Shaking 原理
一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打包到 bundle 中去,tree shaking 就是只把用到的方法打包到 bundle 中,没有使用到的方法会在 uglify 阶段被擦除掉。
webpack 默认支持,produciton 模式下默认开启,在 .babelrc 中设置 modules: false 即可
必须是 ES6 语法,不支持 CJS
利用 ES6 模块的特点
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串变量
- import binding 是 immutable 的
Scope Hoisting 原理分析和使用
现象:构建后的代码存在大量的闭包代码
- 大量函数闭包包裹代码,导致代码体积增大(模块越多越明显)
- 运行代码时创建的函数作用域变多,内存开销变大
模块转化分析
- 被 webpack 转换后的模块会带上一层包裹
import
会被转换成__webpack_require__
// 模块代码
+import { helloworld } from './helloworld';
+import '../../common';
+
+document.write(helloworld());
+
+// 模块初始化函数
+/* 0 */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _common_WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+/* harmony import */ var _helloworld_WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
+
+document.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_1__["helloworld"])());
+/***/ })
+
webpack 模块机制
- 打包出来的是一个 IIFE (匿名闭包)
- modules 是一个数组,每一项是一个模块初始化函数
__webpack_require__
用来加载模块,返回module.exports
- 通过
WEBPACK_REQUIRE_METHOD(0)
启动程序
(funciton(modules) {
+ var installedModules = {};
+
+ function __webpack_require__(moduleId) {
+ if (installedModules[moduleId])
+ return installedModules[moduleId].exports;
+ var module = installedModules[moduleId] = {
+ i: moduleId,
+ l: false,
+ exports: {}
+ };
+ modules[moduleId].call(module.exports, module, module.exports, __webpack_requrie__);
+ module.l = true;
+ return module.exports;
+ }
+ __webpack_require__(0);
+})([
+ /* 0 module */
+ (function (module, __webpack_exports__, __webpack_require__) {
+ // ...
+ }),
+ /* 1 module */
+ (function (module, __webpack_exports__, __webpack_require__) {
+ // ...
+ }),
+ /* ... */
+ /* n module */
+ (function (module, __webpack_exports__, __webpack_require__) {
+ // ...
+ }),
+]);
+
Scope Hoisting 原理分析
原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
对比:通过 scope hoisting 可以减少函数声明代码和内存开销
webpack 默认支持,produciton 模式下默认开启
必须是 ES6 语法,不支持 CJS
module.exports = {
+ plugins: [
+ new webpack.optimize.ModuleConcatenationPlugin()
+ ]
+};
+
代码分割
对于大的 Web 应用来说,将所有代码都放在一个文件中显然是不够有效的,特别当你的某些代码是在某些特殊时候才会被使用到,webpack 可以将你的代码库分割成 chunks(代码块),当代码运行到需要它们的时候才会进行加载。
适用于
- 抽离相同代码到一个共享块
- 脚本懒加载,使得初始下载的代码更小
- CommonJS: require.ensure
- ES6: 动态 import (可能需要 babel 转换)
- @babel/plugin-syntax-dynamic-import
ESLint 必要性
提前暴露一些潜在问题
Airbnb: eslint-config-airbnb / eslint-config-airbnb-base 等
制定团队的 ESLint 规范
- 不重复造轮子,基于 eslint:recommend 配置并改进
- 能够帮助发现代码错误的规则,全部开区
- 帮助保持团队的代码风格统一,而不是限制开发体验
ESLint 如何落地?
与 CI/CD 系统集成
本地开发阶段阶段增加 precommit 钩子
安装 husky
npm install --save-dev husky
增加 npm script 通过 lint-staged 增量检查修改的文件
// package.json +{ + "scripts": { + "precommit": "lint-staged" + }, + "lint-staged": { + "linters": { + "*.{js,scss}": ["eslint --fix", "git add"] + } + } +} +
和 webpack 集成
使用 eslint-loader,构建时检查 JS 规范
module.exports = { + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: [ + "babel-loader", + "eslint-loader" + ] + } + ] + } +} +
webpack 打包库和组件
打包压缩版和非压缩版本
支持 AMD / CJS / ESM 模块引入,script 直接引入
// AMD +require(['large-number'], function(largeNumebr) { + largeNumber.add('999', 1); +}); +// CJS +const largeNumber = require('large-number'); +largeNumber.add('999', 1); +// ESM +import * as largeNumber from 'large-number'; +largeNumber.add('999', 1); +// HTML script +<script src="path/to/large-number.js"></script> +
打包输出库的名称
- 未压缩版 large-number.js
- 压缩版 large-number.min.js
|-/dist
+ |-large-number.js
+ |-large-number.min.js
+|-/src
+ |-index.js
+|-index.js
+|-webpack.config.js
+|-package.json
+
暴露库
配置示例
module.exports = {
+ mode: 'none',
+ entry: {
+ 'large-number': './src/index.js',
+ 'large-number.min': './src/index.js',
+ },
+ output: {
+ filename: '[name].js',
+ libraryExport: 'default',
+ library: 'largeNumber', // 指定库的全局变量
+ libraryTarget: 'umd', // 支持库引入的方式
+ },
+ optimization: {
+ minimize: true,
+ minimizer: [
+ new TerserPlugin({
+ include: /\.min\.js$/,
+ })
+ ]
+ }
+};
+
设置入口文件
// package.json
+{
+ "version": "0.0.1",
+ "description": "description",
+ "main": "index.js",
+}
+
+// index.js
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./dist/large-number.min.js');
+} else {
+ module.exports = require('./dist/large-number.js');
+}
+