webpack

一、模块化开发

Commonjs 规范

  • Commonjs 是以同步模式加载模块
  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数载入模块

ES Module

ESM 是 ES6 引入的 JS 标准模块化规范,它的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系和输入和输出的变量。

功能

模块功能主要由两个命令构成:exportimport
export 命令用于规定模块的对外接口,
import 命令用于输入其他模块提供的功能。

特性

通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 js 代码了

  • 自动采用严格模式,忽略‘use strict’
  • 每个 ES Module 模块都是单独的私有作用域
  • ES Module 是通过 CORS 去请求外部 js 模块的,服务器要支持 CORS
  • ES Module 的 script 标签会延迟执行脚本(页面渲染完在执行脚本)

export 导出

  • 导出
    • 导出成员使用 export 修饰成员声明的方式,如:
1
2
export var name = 'yan;
export var age = 18'
  • export 可以单独使用,如:
1
2
3
var name = "yan";
var age = 18;
export { name, age };
  • 使用 as 给导出的成员重命名,导入时就要使用重命名后的变量名,如:
1
2
var name = "yan";
export { name as firstName };
  • 如果重命名为 default 那这个成员就会做为当前模块默认导出的成员
  • 在导入这个成员就必须要给这个成员重命名,default 是一个关键词不能作为一个变量使用
  • 导入
    • 导入使用 import 导入,如:import { name } from './module.js',name 为导出的变量名
    • 导入默认导出传成员可以不使用花括号,变量名可以根据需要随便取,如:import name from './module.js'
    • 如果导入的变量名为 default,需要使用 as 重命名,如:import { default as firstName } from './module.js'

导入导出的注意事项

  • export { name, age } 该写法不是对象字面量的,如果要对象字面量的写法应该是:export default { name, age },这样花括号会被理解为对象
  • import {} from './module.js',花括号不是解构
  • 导出成员时,导出的是这个成员的引用,导出的变量是只读的(在导入的文件中无法改变变量的值),如,在导出成员的文件中在一秒后改变变量的值,在导入变量的文件中两秒后输出变量值,输出的结果是改变后的值
1
2
3
4
5
6
7
8
9
10
export var a = 1;
setTimeout(() => {
a = 2;
}, 1000);

import { a } from "./module.js";
// a = 22 // 报错,Assignment to constant variable
setTimeout(() => {
console.log(a); // 2
}, 2000);

import 导入用法

  • import 必须使用完整的路径不能省略扩展名,即使是引入 index 也要完整的路径不能省略 index
  • import 可以使用完整的 url 加载模块 import { name } from 'https://xxx.com/xxx.js'
  • 相对路径,在网页引用资源是可以省略‘./’的,但是 import 不可以省略,如果省略了就是以字母开头 ES Module 会以为在加载第三方模块,可以使用绝对路径和完整的 url
  • 如果想要执行某个模块而不用提取模块成员,花括号为空即可,这是就只会执行这个模块,不会提出成员,如:import {} from './module.js',简写:import './module.js'
  • 如果模块导出的成员特别多,而导入的都用到,可以使用星号‘_’,把这个模块的成员全部导入,在通过 as 将导入的成员全部放到一个对象中,导出的成员都会作为这个对象的属性,如:import _ as mod from './module.js'
  • 动态导入模块,不可以通过判断使用if (true){import {name} from './module.js'},如果需要动态导入,可以使用 import 函数import('./module.js'),import 函数返回的是一个 promise
  • 如果模块同时导出默认成员和命名成员,在导入时可以给默认成员重命名import { name, default as age } from './module.js',也可以import age(默认成员), { name } from './module.js'

导出导入成员

  • import 可以配合 export 使用,效果就是将导入的结果直接作为当前模块的导出成员,那么在当前作用域就不能访问这些成员,如:export { name, age } from './module',利用这种特性可以把散落的模块组织在一起,如,一个 index 把零散的模块组织一起,然后再导出
1
2
3
4
5
6
7
8
9
10
// button.js
var Button = "Button Component";
export default Button;
// avatar.js
export var Avatar = "Avatar Component";
// index.mjs
export { default as Button } from "./button.js";
export { Avatar } from "./avatar.js";
// app.js
import { Button, Avatar } from "./index.mjs";

ES Modules 浏览器环境 Polyfill

  • 在不支持 ES Module 的浏览器中,可以使用 browser-es-module-loader 第三方库,在执行的时候解析
  • 但是在支持 ES Module 的浏览器中,这样会执行两次,可以添加 script 标签的新属性 nomodule
  • nomodule:在不支持 ES Module 的浏览器工作
1
2
3
4
5
6
7
8
9
10
11
12
<script
nomodule
src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"
></script>
<script
nomodule
src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"
></script>
<script
nomodule
src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"
></script>

ES Modules in Node.js - 支持情况

  • 在 node 中使用 ES Module 文件后缀名为‘mjs’
  • 执行 node --experimental-modules 文件,experimental-modules:启用 ES Module 实验特性
  • 可以通过 import 导入原生的模块和第三方模块import fs from 'fs'; import _ form 'loadsh'
  • import { camelCase } from 'lodash',不支持,因为第三方模块都是导出默认成员;内置模块兼容了 ES Module 的提取成员方式,所以import { writeFileSync } from 'fs'支持

ES Modules in Node.js - 与 CommonJS 交互

  • ES Module 中可以导入 Commonjs 模块
  • Commonjs 中不能导入 ES Module 模块
  • Commonjs 始终只会导出一个默认成员
  • 注意 import 不是解构导出对象

ES Modules in Node.js - 与 CommonJS 的差异

  • ES Module 中没有 Commonjs 中的那些模块全局成员(require/module/exports/**filename/**dirname)
  • Commonjs 中 require/module/exports 在 ES Module 中使用 import 和 export 替代,**filename 和 **dirname 可以通过import.meta.url 可以拿到当前工作文件的文件 url 地址,可以使用原生的 url 模块的fileURLToPath()方法可以把文件 url 转换为路径
1
2
3
4
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

ES Modules in Node.js - 新版本进一步支持

  • 在 package.json 文件添加 type 字段值为 module,此时文件扩展名可以用 js,而不需要 mjs
  • 此时想要使用 Commonjs 规范,那么文件后缀名要改为 cjs

ES Modules in Node.js - Babel 兼容方案

  • 安装 babel 相关依赖 yarn add @babel/node @babel/core @babel/preset-env babel 工作依赖插件,preset 是一个插件集合
  • 执行 yarn babel-node index.js(想要转换的文件) --presets=@babel/preset-env
  • 或者添加一个.babelrc 文件
  • babel 的核心就是 preset-env,用于将 ES6 转换成 ES5 代码。
1
2
3
{
"presets": ["@babel/preset-env"]
}

二、Webpack 打包

模块打包工具的由来

  • ES Module 存在环境兼容问题
  • 模块文件过多,网络请求频繁
  • 所有的前端资源都需要模块化

Webpack 配置文件

  • webpack 默认以src/index.js 为入口文件
  • 在项目的根目录添加webpack.config.js文件,可以自定义相应的配置,该文件是运行在 nodejs 环境中
  • 该文件导出一个对象,通过导出对象的属性就可以完成相应的配置选项
  • entry 属性:webpack 打包入口文件的路径,如果是相对路径‘./’不能省略
  • output 属性:webpack 打包输出的位置,值要求是一个对象,通过对象的 filename 属性指定输出的文件名,path 属性指定输出的目录(绝对路径)

Webpack 工作模式

  • 在 webpack.config.js 文件对象中添加 mode 属性
  • mode 默认值是 production,mode 可选值有 production、development、none
  • production:自动启动优化,会把代码压缩加密
  • development:会自动优化打包速度,添加一些调式过程中的辅助
  • none:运行最原始的打包,不做额外的处理

loader

webpack 的核心就是 loader。

Webpack 资源模块加载

  • webpack 内容默认只会处理 js 文件
  • 处理其他类型的文件要安装使用对角的 loader(加载器)
  • loader 的输入和输出都是字符串.
  • 使用 loader,在webpack.config.js导出的对象中的 module 属性,添加一个 rules 数组
  • rules 数组就是针对加载其他资源模块的规则配置
  • 每个规则对象要设置两个属性,
    • test:正则表达式,匹配打包过程中遇到的文件;
    • use:匹配到的文件使用的 loader,use 的值可以是单个 loader 字符串也可以是一个数组 loader,如果是一个数组,会从右往左执行
  • css-loader 的作用就是将 css 文件转换为一个 js 模块;具体实现就是将 css 代码 push 到一个数组中
  • style-loader 的作用就是将 css-loader 转换后的结果通过 style 标签追加到页面上
  • loader 是 webpack 的核心特性
  • 借助不同的 loader 就可以加载任何类型的资源

file-loader 文件资源加载器

  • 安装依赖 yarn add file-loader
  • 在配置文件中添加 rules { test: /.png$/, use: 'file-loader' }
  • webpack 默认所有打包的结果放在网站根目录;为项目中的所有资源指定一个基础路径,在 output 中添加 publicPath 属性,默认值为空表示网站的根目录
  • 把 publicPath 值修改为 ‘dist/‘ 注意“/”不能省略,打包后是一个变量名(publicPath 值)拼接资源名(资源名前面没有“/”)所有“/”不能省略

publicPath是整个项目的静态资源的基础路径,一般在生产模式下使用.

webpack-dev-server中的publicPath是打包后代码存放的路径.这里的文件访问的静态资源路径还是output中的publicPath.所以二者要保持一致.

假设 output.publicPath 设置成了 './dist/',那么打包后的 JS 引用路径为 ./dist/main.js。这里会存在一个问题,相对路径在本地能正常访问到。但是如果把静态资源托管到 CDN 上,访问路径显然不能使用相对路径的。如果设置成 '/dist/',则打包后的访问路径是 localhost:8080/dist/main.js,此时本地无法访问。
一般解决方法就是利用 webpack.DefinePlugin 来定义一个全局变量(process.env.NODE_ENV)区分开发、生产环境来设定不同的值,或者是采用两份不同的配置文件来进行打包。
一般来说,output.publicPath 应该以 ‘/‘ 结尾,而其他 loader 或插件的配置不要以 ‘/‘ 开头。

url-loader URL 加载器

优点: 不用再发起 http 请求

  • Data URLS 格式:data(协议):[mediatype(媒体类型和编码)][;base64],
  • 安装插件 yarn add url-loader --dev
  • 在配置文件中添加 rules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
module: {
use: [
{
test: /.png$/,
use: {
loader: "url-loader",
option: {
limit: 10 * 1024, // 对于小于10kb的png图片使用url-loader打包;
// 而大于10kb的会使用file-loader打包
},
},
},
],
},
};

loader 中做大小限制还是需要安装 file-loader,不然无法解析.

  • 打包后,图片将是以 base64 的格式显示

最佳实践

  • 小文件使用 Data URLs,减少请求次数
  • 大文件单独提取存放,提高加载速度

Webpack 常用加载器 loader 分类

  • 编译转换类:会把加载的资源转为 js 代码,如css-loader
  • 文件操作类:会把加载的资源拷贝到输出的目录,导出文件访问路径,如file-loader
  • 代码检查类:目的是统一代码风格,如eslint-loader

Webpack 与 ES6

wepack 并不能转换 es6 中的其他特性,只能处理模块相关的import,export

  • 安装babel-loader @babel/core @babel/preset-env
  • 在配置文件中 module rules 添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
};

Webpack 加载资源的方式

  • 遵循 ES Modules 标准的 import 声明
  • 遵循 Commonjs 标准的 require 函数
  • 遵循 AMD 标准的 define 函数和 require 函数
  • Loader 加载器的非 JavaScript 也会触发资源加载
    • 样式代码中的@import 指令和 url 函数
    • HTML 代码中图片标签的 src 属性

如果用require导致 esm 模块代码,需要用导入值的default属性导入.不过,除非必要,不要混用.

1
const heading = require("./heading.js").default;

Webpack 核心工作原理

  • webpack 会根据配置找到打包入口文件,然后顺着入口文件的代码
  • 根据代码中的 import 或者 require 解析推断这个文件依赖的资源模块
  • 然后分别解析每个资源模块对应的依赖,形成整个项目中所有用到文件之间的依赖关系的依赖树
  • webpack 递归依赖树,找到每个节点对于的资源文件
  • 根据配置文件中的 rules 属性找到模块对应的加载器,找到对于的资源放在打包结果中,实现整个项目的打包

Webpack 开发一个 Loader

loader 都需要导出一个函数,这个函数就是对加载资源的处理.

  • loader 负责资源文件从输入到输出的转换
  • 对于同一个资源可以依次使用多个 loader (css-loader -> style-loader)
  • loader 模块导出一个函数;该函数输入的是加载到的资源文件内容;函数体是处理内容的过程;输出的是经过处理过的资源文件内容结果,结果必须是 javascript 代码
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
// main.js
import about from "./about.md";

console.log(about);

// markdown-loader.js
const marked = require("marked");

module.exports = (source) => {
// source接受输入
// console.log(source);
// return 'hello';

// 方法一
const html = marked.parse(source);
// return `module.exports = ${JSON.stringify(html)}`

// 方法二 返回 html 字符串交给下一个 loader处理 (html-loader)
return html;
};

// webpack.config.js
module.exports = {
mode: "none",
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
publicPath: "dist/",
},
module: {
rules: [
{
test: /.md$/,
use: ["html-loader", "./markdown-loader"],
},
],
},
};

plugin

loader 实现资源加载,plugin 解决除了资源加载,其他的自动化工作.
比如,打包前清除 dist 目录(为了清除上一次代码),文件压缩,拷贝静态文件到输出目录.
也就实现了大部分的前端工程化的工作.

plugin 插件机制介绍

  • 增强 webpack 自动化能力
  • loader 专注实现资源模块加载
  • plugin 解决其他自动化工作
    • 清除 dist 目录
    • 拷贝静态文件至输出目录
    • 压缩输出代码

plugin 之 自动清除输出目录插件

  • 安装插件 yarn add clean-webpack-plugin --dev
  • 配置文件中导入插件
  • 在配置对象中 plugins 属性,该属性是专门配置插件的地方,属性值是一个数组
  • 数组里就是导入的类型创建的实例.
1
2
3
4
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [new CleanWebpackPlugin()],
};

plugin 之 自动生成 HTML 插件

  • 安装插件 yarn add html-webpack-plugin --dev
  • 在配置文件中导入插件
  • 在配置对象中 plugins 属性,该属性是专门配置插件的地方,属性值是一个数组
  • 同时输出多个页面文件,可以再通过 HtmlWebpackPlugin 生成多个实例,通过 filename 指定文件名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
// 用于生成 index.html
new HtmlWebpackPlugin({
title: "Webpack Plugin Sample", // html文件的title
meta: {
viewport: "width=device-width", // 设置meta的viewport
},
template: "./src/index.html", // 模版,写法见最下方
}),
// 输出多个文件
new HtmlWebpackPlugin({
filename: "about.html",
}),
],
};

// index.html
// ....
// <h1><%= htmlWebpackPlugin.options.title %></h1>

plugin 之 复制静态文件

  • 安装插件 yarn add copy-webpack-plugin --dev

有些文件不需要打包,比如网站图标 icon,可以使用copy-webpack-plugin直接复制.

建议上线前再使用此插件,否则频繁打包会影响性能,平时开发可以使用的devserver中的contentBase属性设置静态资源路径
1
2
3
new CopyWebpackPlugin([
'public'// 将public下所有文件拷贝到输出目录
])

plugin 之 开发一个插件

原理: 钩子机制,通过在生命周期的钩子中挂载函数实现的.

  • 相比于 loader,plugin 拥有更宽的能力范围

  • plugin 通过钩子机制实现

  • 插件必须是一个函数或者是一个包含 apply 方法的对象

  • 通过在生命周期的钩子中挂载函数实现扩展

  • 一般就是定义一个类,在类中定义 apply 方法,使用的时候就是通过这个类去构建实例.

    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
    class MyPlugin {
    // 目标 删除bundle.js中的注释
    apply(compiler) {
    // 会自动执行
    console.log("my plugin 启动");
    // https://webpack.js.org/api/compiler-hooks/
    // emit钩子:即将输出文件时执行
    // 参数一:插件名 参数二、挂载到钩子上的函数
    compiler.hooks.emit.tap("MyPlugin", (compilation) => {
    // compilation => 可以理解为此次打包的上下文
    for (const name in compilation.assets) {
    // assets:资源文件
    // console.log(name); // 文件名
    // console.log(compilation.assets[name].source());//文件内容
    if (name.endsWith(".js")) {
    // 判断是js文件
    const contents = compilation.assets[name].source(); // 文件内容
    const withoutComments = contents.replace(/\/\*\*+\*\//g, ""); // 全局替换注释
    compilation.assets[name] = {
    // 覆盖原有内容
    source: () => withoutComments, // 返回新的内容
    size: () => withoutComments.length, // 返回内容的大小 webpack内部规定必须
    };
    }
    }
    });
    }
    }

    webpack-dev-server

    集成了自动编译,自动刷新浏览器功能

    1
    2
    3
    4
    5
    # 安装
    pnpm add webpack-dev-server -D
    # 运行
    pnpm webpack-dev-server
    # 如果加--open会自动打开浏览器

    Webpack Dev Server 静态资源访问

    • Dev Server 默认只会 serve 打包输出文件
    • 只要是 webpack 打包输出的文件都可以直接被访问
    1
    2
    3
    4
    devServer: {
    // 额外开发服务指定静态资源路径,可以是字符串或数组
    contentBase: "./public";
    }

    Webpack Dev Server 代理 API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    module.exports = {
    devServer: {
    proxy: {
    // 前缀
    "/api": {
    // http://localhost:8080/api/users => https://api.github.com/api/users
    target: "https://api.github.com",
    // 路径重写
    pathRewrite: {
    "^/api": "", // 去除 /api
    },
    // 不能使用 localhost:8080 作为请求github的主机名
    changeOrigin: true,
    },
    },
    },
    };

    Source Map

    Source Map 介绍

    • 调试和报错都是基于运行代码
    • source map 映射转换后代码和源代码关系,通过 source map 逆向解析
    • 解决源代码与运行代码不一致所产生的调试问题

    Webpack 配置 Source Map

    在配置项添加 devtool: 'source-map'

    Webpack eval 模式的 Source Map

    • eval 是否使用 eval 执行模块代码,带 eval,用 eval 执行模块代码,只能定位文件,不能定位具体位置.
    • eval-source-map 可以定位具体位置
    • cheap-eval-source-map 可以定位行,不能定位列.
    • cheap-source map 是否包含行信息
    • module 是否能够得到 Loader 处理之前的源代码
    • 带 module 的模式下解析的出来的原代码是没有经过 loader 加工,手写的源代码
    • 不带 module 是 loader 加工过后的代码
    • inline-source-map:source map 是因 dataURL 嵌入代码,其他的 source map 是以物理文件存在
    • hidden-source-map:开发工具看不到 source map 效果 在开发第三方包比较有用

    Webpack 选择 Source Map 模式

    • 开发模式
      • cheap-module-eval-source-map:代码每行不会超过 80 个字符;代码进过 loader 转换后差异较大需要 module 模式(loader 处理前的源代码);首次打包速度慢无所谓,重写打包相对较快
    • 生成模式
      • nonenosources-source-map:Source Map 会暴露源代码

    热更新 HMR

    Webpack HMR 体验(热更新)

    • 应用运行过程中实时替换某个模块,应用运行状态不受影响
    • HMR 集成在webpack-dev-server
    • 开启 HMR
      • css 样式(经过 loader 处理的)可以热更新但是 js 还是会刷新浏览器,webpack 中的 HMR 不可以开箱即用
      • webpack 中的 HMR 需要手动处理模块热替换逻辑
    1
    2
    3
    4
    5
    6
    7
    8
    const webpack = require("webpack");
    module.exports = {
    target: "web",
    devServer: {
    hot: true,
    },
    plugins: [new webpack.HotModuleReplacementPlugin()],
    };

    Webpack 使用 HMR API

    Webpack 处理 JS 模块热替换

    js 热更新:保存旧数据,代码发生变化后把数据回填

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let lastEditor = editor;
    module.hot.accept("./editor", () => {
    // console.log('editor HMR')
    // console.log(createEditor)
    const value = lastEditor.innerHTML;
    document.removeChild(editor);
    const newEditor = createEditor();
    newEditor.innerHTML = value;
    document.body.appendChild(newEditor);
    lastEditor = newEditor;
    });

    // 图片热更新
    module.hot.accept("./better.png", () => {
    img.src = background;
    console.log(background);
    });

    HMR 注意事项

    • 如果没有开启热更新,但是 js 代码做了热更新处理,先做判断 if (module.hot){// 热更新逻辑}
    • hot 会刷新浏览器,报错信息会被刷走;hotOnly不会刷新浏览器,可以把错误信息输出
    • 打包后会自动去除 js 热更新逻辑代码

    Webpack 生产环境优化

    • mode:none、production、development
    • 为不同的工作环境创建不同的配置

    Webpack 不同环境下的配置

    • 配置文件根据环境不同导出不同的配置
      • webpack 可以导出一个函数,函数返回配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 参数一:cli传递的环境名参数;参数二:运行cli传递的所有参数
    module.exports = (env, argv) => {
    const config = {
    //...
    };
    if (env === "production") {
    // ...
    }
    };
    // 执行 yarn webpack --env production
    • 一个环境对应一个配置文件
      • webpack.common.js 存放公共配置
      • webpack.dev.js 开发配置,把公共配置和开发配置合并(使用 loadsh merge)
      • webpack.prod.js 生产环境,把公共配置和生产配置合并(使用 loadsh merge)
      • 使用webpack-merge插件替换 loadsh 工具.
      • 在没有默认配置文件的情况就要执行 pnpm webpack --config webpack.dev.js
      • 或者在 scripts 中添加命令 "build": "webpack --config webpack.dev.js"
    1
    2
    3
    4
    5
    const common = require('./webpack.common')
    const merge = require('webpack-merge')
    module.exports = merge(common, {
    ...
    }

    Webpack DefinePlugin

    • DefinePlugin 为代码注入全局成员
    • 接收的是一个对象,每一个键值都会注入到代码中
    • 文件代码中直接使用键,打包后把注入的值 直接替换到代码中,definePlugin 传递的字符串应该是代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const webpack = require("webpack");
    module.exports = {
    plugins: [
    new webpack.DefinePlugin({
    // 想要输出的是字符串要个引号,否则输出的不是字符串
    // 输出值可以使用JSON.stringify('https://api.github.com')
    API_BASE_URL: '"https://api.github.com"',
    }),
    ],
    };
    // ==
    console.log(API_BASE_URL);

    Tree Shaking

    tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)
    • 在生成模式下自动开启,对于冗余、未引用代码不会打包
    • 不是指某个配置选项,是一组功能搭配使用后的优化效果
    • 只支持 ESM,不支持 CommonJS.(ESM 是静态引入,cms 是动态引入)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // webpack 使用 Tree Shaking
    module.exports = {
    mode: "none",
    entry: "./src/index.js",
    output: {
    filename: "bundle.js",
    },
    // 集中配置webpack内部优化功能
    optimization: {
    usedExports: true, // 只到处外部使用了的成员 (相当于标记没使用的代码)
    minimize: true, // 开启webpack代码压缩 会把外部没使用的代码去除
    },
    };

    Webpack 合并模块—concatenateModules

    • 尽可能的将所有模块合并输出到一个函数中
    • 提升运行效率,减少代码体积
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    mode: "none",
    entry: "./src/index.js",
    output: {
    filename: "bundle.js",
    },
    // 集中配置webpack内部优化功能
    optimization: {
    concatenateModules: true, // 打包后把所有的模块放在同一个函数中;
    },
    };

    Webpack Tree Shaking 与 Babel

    • babel 和 tree shaking 同时使用会导致 tree shaking 失效
      • tree shaking 前提是 ES Modules,由 webpack 打包的代码必须使用 ESM
      • babel-loader 转换代码时可能会把 ES Modules 转为 CommonJS
        新版的 babel-loader 不会导致 tree shaking 失效,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    module.exports = {
    module: {
    rules: [
    {
    test: /\.js$/,
    use: {
    loader: "babel-loader",
    options: {
    presets: [
    // modules:默认值是auto 根据环境判断是否开启ESM,可以设为false
    ["@babel/preset-env", { modules: "commonjs" }], // 强制使用babel ESM,把ESM转为commonjs
    ],
    },
    },
    },
    ],
    },
    };

    sideEffects

    • 允许通过配置方式标识代码是否有副作用(模块执行时除了导出成员之外所做的事情)
    • 一般用于开发 npm 包时 标记是否有副作用
    • 生产模式自动开启 会检查当前代码所属的 package.json 有没有 sideEffects 标识,以此来判断这个模块是否有副作用
    • 如果这个模块没有副作用,那些没有用到的模块不会被打包
    • package.json 添加 sideEffects: false 表示 package.json 所影响的项目当中所有的代码都没有副作用
    1
    2
    3
    optimization: {
    sideEffects: true;
    }

    Webpack sideEffects 注意

    • 使用 sideEffects 前提确保代码真的没有副作用,否则 webpack 再打报时会误删有副作用的代码
    • 如在 Number 原型添加方法然后载入、载入 css 也是
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // extend.js
    // 为Number的原型添加一个扩展方法
    // 副作用代码 在package.json声明没有副作用,在打包时会被去除
    Number.prototype.pad = function(size) {
    let result = this + ''
    while(result.length < size) {
    result = '0' + result
    }
    return result
    }

    // index.js
    import './extend.js'
    console.log(8.pad(3))
    • 解决:在 package.json 关闭副作用(设置为 false)或者标识哪些文件有副作用
    1
    2
    3
    4
    5
    // package.json
    "sideEffects": [ // 标识有副作用的文件
    './src/extends.js',
    '*.css'
    ]

    代码分割

    • 所有代码最终都被打包到一起,打包文件体积过大
    • 分包,按需加载
    • 多入口打包
    • 动态导入

    Webpack 多入口打包

    • 多页应用程序,一个页面对应一个打包入口,公共部分单独提取
    • 多个入口可以把 entry 定义为对象,一个属性就是一个入口,键就是入口名,值就是入口对应的文件路径
    • 多入口,输出也要多个,可以给 filename 的值添加 [name]占位 outpur:{filename: '[name].bundle.js'} 这样 [name]就会被替换为入口名称
    • plugins 中的 HtmlWebpackPlugin 插件会自动输入注入所有打包结果的 html,所以此时入口文件会注入所有的打包结果
    • 可以给 HtmlWebpackPlugin 插件配置注入的打包文件 new HtmlWebpackPlugin({chunks:['index']}), new HtmlWebpackPlugin({chunks:['album']})
    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
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    module.exports = {
    mode: "none",
    entry: {
    index: "./src/index.js",
    album: "./src/album.js",
    },
    output: {
    filename: "[name].bundle.js",
    },
    module: {
    rules: [
    {
    test: /\.css$/,
    use: ["style-loader", "css-loader"],
    },
    ],
    },
    plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
    title: "Multi Entry",
    template: "./src/index.html",
    filename: "index.html",
    chunks: ["index"],
    }),
    new HtmlWebpackPlugin({
    title: "Multi Entry",
    template: "./src/album.html",
    filename: "album.html",
    chunks: ["album"],
    }),
    ],
    };

    Webpack 提取公共模块

    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
    module.exports = {
    // ...
    optimization: {
    splitChunks: {
    // 选择对哪些文件进行拆分,默认是async,即只对动态导入的文件进行拆分
    chunks: "all",
    // 提取chunk的最小体积
    minSize: 20000,
    // 要提取的chunk最少被引用次数
    minChunks: 1,
    // 对要提取的trunk进行分组
    cacheGroups: {
    // 匹配node_modules中的三方库,将其打包成一个trunk
    defaultVendors: {
    test: /[\\/]node_modules[\\/]/,
    name: "vendors",
    priority: -10,
    },
    default: {
    // 将至少被两个trunk引入的模块提取出来打包成单独trunk
    minChunks: 2,
    name: "default",
    priority: -20,
    },
    },
    },
    },
    // ...
    };

    Webpack 动态导入

    • 需要用到某个模块时,再加载这个模块
    • 动态导入的模块会被自动分包
    • 一般我们都是在文件头直接 import 文件的,这会造成资源浪费
    • 可以使用 ES Modules 的 import()在需要导入的地方导入模块,该方法返回的是 promise 可以拿到导出的对象
    1
    2
    3
    4
    5
    if (true) {
    import("./posts").then(({ default: posts }) => {
    mainElement.append(posts());
    });
    }

    Webpack 魔法注释

    • 在 import()方法中添加特定的注释 import(/* webpackChunkName: 'posts' */ './posts') 这样在打包后文件名就会带上这个 webpackChunkName 值
    • 如果 webpackChunkName 相同,打包后会打包在一起

    Webpack MiniCssExtractPlugin—提取 css 到单独文件

    • 安装插件 yarn add mini-css-extract-plugin --dev

    注意: 如果 css 小于 150k,就不要用这个了,不然单独生成一个文件,就多次请求,效果可能不如style-loader.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");

    module.exports = {
    module: {
    rules: [
    {
    test: /\.css$/,
    use: [
    // 'style-loader', // 将样式通过 style 标签注入
    MiniCssExtractPlugin.loader, // 通过 link 标签注入
    "css-loader",
    ],
    },
    ],
    },
    plugins: [new MiniCssExtractPlugin()],
    };

    Webpack OptimizeCssAssetsWebpackPlugin— 压缩 css

    • 安装插件 yarn add optimize-css-assets-webpack-plugin --dev
    • 如果配置在 plugins 中这个插件在任何情况都会正常工作
    • 配置在 minimizer 中 只有 minimizer 开启(生产环境自动开启)是才工作
    • webpack 建议压缩创建配置在 minimizer 中,以便通过 minimizer 选项统一控制
    • 当 minimizer 使用一个数组,webpack 认为我们要自定义所使用的压缩器插件,webpack 内部的 js 压缩器就会被覆盖,如果不配置 js 压缩器,这是打包,js 代码不会被压缩
    • 手动把 js 压缩器添加回来,安装插件 yarn add terser-webpack-plugin --dev
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
    const TerserWebpackPlugin = require("terser-webpack-plugin");

    module.exports = {
    optimization: {
    minimizer: [
    new TerserWebpackPlugin(), //压缩js
    new OptimizeCssAssetsWebpackPlugin(), // 压缩css
    ],
    },
    };

    Webpack 输出文件名 Hash

    • 在部署时服务器开启静态文件缓存,如果缓存时间过长,程序发生更改,得不到更新
    • 生产环境下,文件名使用 Hash,文件名发生变化 对客户端而言全新的文件名就是全新的请求,就不会有缓存问题
    • [hash]【占位符】:整个项目级别的,项目中有任何地方改动,这次打包的 hash 值都会发生变化
    • [chunkhash]【占位符】:chuankhash 级别,打包过程中只要是同一路的打包 chunkhash 都是相同的
    • [contenthask]【占位符】:文件级别 hash,根据文件输出内容生成 hash 值(不同的文件就有不同的 hash 值)
    • 指定 hash 值长度[chunkhash:8]【占位符】
    1
    2
    3
    4
    5
    module.exports = {
    output: {
    filename: "[name]-[contenthash:8].bundle.js",
    },
    };

    Polyfill

    可翻译为垫片,

    引入方法

    1. 入口文件引入
    1
    2
    3
    4
    5
    # npm安装
    pnpm add core-js regenerator-runtime
    # 在入口文件顶部导入
    import 'core-js/stable'
    import 'regenerator-runtime/runtime'
    1. webpack 引入
    1
    2
    3
    moduls.exports = {
    entry: ["core-js/stable", "./src/index.js"],
    };

    预设配置

    target

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //babel.config.js
    module.exports = {
    presets: [
    [
    "@babel/preset-env",
    {
    target: {
    chrome: 30,
    },
    },
    ],
    ],
    };

    配合browserslistrc

    这个文件是目标环境配置表,也就是说通过配置,可以让我们的代码运行在哪些指定的浏览器或 node 环境,比如下边这种配置的意思就是,市场份额大于 1%的浏览器,并且不考虑 ie10 以下版本的浏览器,并且不是已’死亡’(指 24 个月没有官方的更新)的浏览器.

    1
    2
    3
    4
    // .browserslistrc 文件
    > 1%
    not ie <= 10
    not dead

    上面预设配置第一个参数 targets 里边就可以填写.browserslistrc 中的内容,效果是一样的,

    useBuiltIns

    useBuiltIns有三个值.默认是false.其他是entryusage.
    entry: 需要在入口或者 webpack 配置文件中引入core-js.
    Babel 会根据我们.browserslistrc文件中的环境配置(这里是 chrome 60 版本),来补齐 chrome 60 版本所有不支持的新增 ES6API,引入 chrome60 版本所有的 polyfill,不管你的入口文件中有没有用到相应的 API。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    npm install core-js -S
    npm install regenerator-runtime -S (根据@babel/core版本判断是否需要安装)
    // babel.config.js
    module.exports = {
    presets: [["@babel/preset-env", {
    useBuiltIns: 'entry',
    corejs: 3
    }]],
    plugins: []
    }
    // before.js,在第一行引入'core-js/stable'
    import 'core-js/stable'
    let promise = Promise.resolve('success')
    //.browserslintrc文件中,我们配置一个chrome浏览器的低版本
    chrome 60

    usage: 就把上面 entry 改了就行,作用就是类似于按需加载,只把用的 Promsie 进行 polyfill 补齐.

    corejs

    就是 corejs 的版本,推荐 3.

    modules

    可以配置成”amd” | “umd” | “systemjs” | “commonjs” | “cjs” | “auto” | false,默认为 auto,这个参数的含义是,是否将 ES6 模块化语法转译成其他的语法,设置为 false 的话,转译的结果就还是 ES6 模块化语法,就是 import,export default 这种,不会使用 require(CommonJs 语法)这种进行导入导出,如果使用 webpack 或 rollup 等打包工具,推荐将 modules 设置为 false,这样的话就有利于打包工具进行静态分析,从而做 tree shaking 等优化操作

    范例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    module.exports = {
    presets: [
    [
    "@babel/preset-env",
    {
    // targets不做配置,默认使用我们 .browserslistrc文件中的配置
    // targets: {},
    // 对所用语法进行polyfill
    useBuiltIns: "usage",
    corejs: {
    // 使用最新版本的core-js进行polyfill
    version: "^3.25.2",
    // 开启草案中的polyfill转译
    proposals: true,
    },
    // 使用es6模块进行
    modules: false,
    },
    ],
    ],
    plugins: ["@babel/plugin-transform-runtime"],
    };

    Postcss

    用来处理 css 样式兼容性的工具,用于适配不同的浏览器.

    1
    pnpm add postcss-cli postcss-loader autoprefixer

    autoprefixer

    自动适配浏览器,在browserslistrc文件中写上要适配的浏览器种类

    1
    2
    3
    4
    5
    6
    7
    // .browserslistrc
    // 使用率大于0.01%
    >0.01%
    // 往上数两个版本
    last 2 versions
    // 还没停更的
    not dead

    可以将配置写在单独的postcss.config.js中,或者 webpack 配置中

    1
    2
    3
    4
    // postcss.config.js
    module.exports = {
    plugins: [require("autoprefixer")],
    };

    postcss-preset-env

    在一些陈旧的项目中可能会看到 autoprefixer 这个插件,现在我们可以直接使用 postcss-preset-env 这个插件,自动添加前缀的功能已经包含在了其中

    在项目根目录下新增 postcss.config.js 配置文件:

    1
    2
    3
    module.exports = {
    plugins: ["postcss-preset-env"],
    };

    postcss-loader

    在 webpack 中的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    moudule.exports = {
    //...,
    modules: {
    rules: [
    {
    test: /.css$/,
    use: ["style-loader", "css-loader", "postcss-loader"],
    },
    ],
    },
    };

    静态资源

    在 webpack5 之前,加载图片、字体等资源需要我们使用 url-loader、 file-loader 等来处理
    从 webpack5 开始,我们可以使用内置的资源模块类型 Asset Modules type,来替代这些 loader 的工作
    资源模块类型 Asset Modules type 分为四种:

    • asset/resource 发送一个单独的文件并导出 URL,之前通过使用 file-loader 实现
    • asset/inline 导出一个资源的 data URI,之前通过使用 url-loader 实现
    • asset/source 导出资源的源代码,之前通过使用 raw-loader 实现
    • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择,之前通过使用 url-loader 实现,并且可以配置资源体积限制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    module.epxorts = {
    module: {
    rules: [
    {
    test: /\.(png|svg|jpe?g|gif)$/,
    type: "asset",
    generator: {
    filename: "images/[name]-[hash][ext]",
    },
    },
    ],
    },
    };

    文件缓存

    webpack5 之前我们常用 cache-loader 、hard-source-webpack-plugin 做文件缓存,加速二次构建, webpack5 现在内置了这种能力,默认会把编译的结果缓存到内存中,通过配置还可以缓存到文件系统中
    修改 webpack.base.js:

    1
    2
    3
    4
    5
    6
    7
    module.exports = {
    // ...
    cache: {
    type: "filesystem",
    },
    // ...
    };

    多个 webpack 配置文件

    可以根据开发环境配置多个 webpack 配置文件,将共配置抽离出来。
    一般配置如下:
    基础:webpack.base.config.js
    开发:webpack.dev.config.js
    生产:webpack.prod.config.js

    webpack-merge

    使用该插件进行合并操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 基础配置在base文件下
    // 其他文件使用webpack.merge进行合并处理
    const { merge } = require("webpack-merge");
    // 公共配置
    const baseConfig = require("./webapck.base");

    const devConfig = {
    mode: "development",
    devtool: "cheap-module-eval-source-map",
    devServer: {},
    //...
    };

    // 合并两个配置
    module.exports = merge(baseConfig, devConfig);