webpack 和 babel

一、webpack 的基本使用

1. 拆分配置和 merge

  • 通用配置
  • 开发环境配置
  • 生产环境配置

通过引用webpack-merge插件的mergesmart方法合并。配置要点如下代码:

// webpack.common.js
// 通用配置,配置开发环境和生产环境都通用的一些插件和加载器
module.exports = {
  module: {
    rules: [],
    plugins: [],
  },
};

// webpack.dev.js
// 开发环境专属配置
module.exports = {
  mode: "development",
  module: {
    rules: [],
    plugins: [
      new webpack.DefinePlugin({
        ENV: JSON.stringify("development"),
      }),
    ],
  },
};

// webpack.prod.js
// 生产环境专属配置
module.exports = {
  mode: "production",
  module: {
    rules: [],
    plugins: [
      new webpack.DefinePlugin({
        ENV: JSON.stringify("production"),
      }),
    ],
  },
};
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

2. 启动服务

// webpack.dev.js
// 开发环境专属配置
module.exports = {
    mode: 'development',
    module: {
        rules: [],
        plugins: [
            new webpack.DefinePlugin({
                ENV: JSON.stringify('development')
            })
        ]
    },

    devServer: {
        port: 8000,
        progress: true, // 显示打包进度条
        contentBase: distPath, // 根目录
        open: true, // 自动打开浏览器
        compress: true, // 启动gzip压缩

        // 设置代理,可以解决跨域问题
        proxy: {
            // 将 /api 代理到 http://localhost:3000
            '/api': 'http://localhost:3000',
            '/api2': ...,
        }
    }
}

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

3. 处理 ES6

使用babel-loader将 ES6 及以上版本的语法转换为 ES5 语法,配置如下:

(1)webpack 配置

rules: [
  {
    test: /\.js$/,
    loader: ["babel-loader"],
    include: srcPath,
    exclude: /node_modules/,
  },
];
1
2
3
4
5
6
7
8

(2)babel 配置

.babelrc文件中进行配置:

{
    "presets": ["@babel/preset-env"],
    "plugins": []
}
1
2
3
4

3. 处理样式

(1)webpack 配置

rules: [
  {
    test: /\.css$/,
    // loader 的执行顺序:从后往前
    loader: ["style-loader", "css-loader", "postcss-loader"],
  },
  {
    test: /\.less$/,
    // loader 的执行顺序:从后往前
    loader: ["style-loader", "css-loader", "less-loader"],
  },
];
1
2
3
4
5
6
7
8
9
10
11
12
  • postcss-loader ,可以处理浏览器兼容性,比如增加 css 属性前缀-webkit-
  • css-loader 解析 css
  • style-loader 将 css 以<style>标签插入 html 中
  • less-loader解析 less,输出 css

(2)postcss 配置

postcss.config.js中配置:

module.exports = {
  // 增加css属性前缀,以兼容浏览器
  plugins: [require("autoprefixer")],
};
1
2
3
4

4.处理图片

module.exports = {
  rules: [
    // dev,图片解析,解析通过 import './xxx.png'导入的文件
    {
      test: /\.(png | jpg | jpeg | gif)$/,
      loader: ["file-loader"],
    },
    // prod,生产环境根据图片大小分别处理,小于某个值时使用 base64,否则直接通过路径引用
    {
      test: /\.(png | jpg | jpeg | gif)$/,
      use: {
        loader: "url-loader",
        options: {
          // 小于 5kb 的图片用 base64 格式产出,直接在js中插入 base64 字符串
          limit: 5 * 1024,
          // 如果 大于 5kb 的图片,输出到 img 目录下,在标签中以路径形式引用
          ouputPath: "/img/",
          // 将图片放到cdn中,在标签中以 cdn 路径引用
          // publishPath: 'http://cdn.abc.com'
        },
      },
    },
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

二、webpack 的高级配置

1. 多入口配置

(1)入口配置

module.exports = {
  entry: {
    // 这里的 index 和 other 表示 chunk name,有两个 chunk
    index: path.join(srcPath, "index.js"),
    other: path.join(srcPath, "other.js"),
  },
};
1
2
3
4
5
6
7

(2)输出配置

module.exports = {
  output: {
    // 1. 输出的 bundle 的文件名,
    // 2. name 对应 entry 中的 key,即 chunk name
    // 3. contentHash:8 根据内容的8位hash,内容不改变时,hash不变,浏览器可以命中缓存,提升加载速度
    filename: "[name].[contentHash:8].js",
    // 输出路径
    path: distPath,
  },
};
1
2
3
4
5
6
7
8
9
10

(3)html-webpack-plugin 配置

module.exports = {
  plugins: [
    // 多入口 - 生成 index.html,并将 生成的 bundle 通过 <script> 标签插入到 index.html中
    new HtmlWebpackPlugin({
      template: path.join(srcPath, "index.html"),
      filename: "index.html",
      // chunks 表示该页面要引用哪些 chunk,若不写,则引用全部 chunk
      chunks: ["index"], // 只引用 index.js
    }),
    // 多入口 - 生成 other.html,并将 生成的 bundle 通过 <script> 标签插入到 other.html中
    new HtmlWebpackPlugin({
      template: path.join(srcPath, "other.html"),
      filename: "other.html",
      chunks: ["other"], // 只引用 other.js
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2. 抽离压缩 css 文件

步骤:

  • 生产环境中使用MiniCssExtractPlugin.loader代替style-loader,开发环境不变
  • plugins中增加mini-css-extract-plugin配置,css 的生产路径和文件名
  • optimization中增加terser-webpack-pluginoptimize-css-assets-webpack-plugin配置,对 css 进行压缩
module.exports = {
    rules: [
        // dev,开发环境下,不需要抽离和压缩
        {
            test: /\.css$/,
            loader: ['style-loader', 'css-loader', 'postcss-loader']
        },
        {
            test: /\.less$/,
            loader: ['style-loader', 'css-loader', 'less-loader']
        }

        // prod,生产环境下,抽离和压缩css(需要导入 mini-css-extract-plugin)
        {
            test: /\.css$/,
            loader: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
        },
        {
            test: /\.less$/,
            loader: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader', 'postcss-loader']
        }
    ],

    plugins: [
        // prod
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        })
    ],
    // prod 压缩
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
    }
}

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

3. 抽离公共代码

开发模式下不需要设置,生成环境下抽离

module.exports = {
  optimization: {
    splitChunks: {
      // 抽离的处理模式,有以下三个选择:
      // 1. initial 入口 chunk,对于异步导入的文件不处理
      // 2. async 异步 chunk,只对异步导入的文件处理
      // 3. all 全部 chunk
      chunks: "all",

      // 缓存分组
      cacheGroups: {
        // 第三方模块,一般是通过 npm 安装的第三方依赖
        vendor: {
          name: "vendor", // chunk 名称
          priority: 1, // 优先级,越大优先级越高,优先抽离
          test: /node_modules/,
          minSize: 1, // 模块的大小限制,小于这个值时,不在抽离
          minChunks: 1, // 最少复用过多少次就要分离到这个 chunk 中
        },
        // 公共模块
        common: {
          name: "common", // chunk 名称
          priority: 0, // 优先级,权限更高,优先抽离
          test: /node_modules/,
          minSize: 1, // 公共模块的大小限制,小于这个值时,不在抽离
          minChunks: 2, // 最少复用过多少次就要分离到这个 chunk 中
        },
      },
    },
  },

  plugins: [
    // 多入口 - 生成 index.html,并将 生成的 bundle 通过 <script> 标签插入到 index.html中
    new HtmlWebpackPlugin({
      template: path.join(srcPath, "index.html"),
      filename: "index.html",
      // chunks 表示该页面要引用哪些 chunk,若不写,则引用全部 chunk
      chunks: ["index", "common", "vendor"], // 只引用 index common 和 vendor
    }),
    // 多入口 - 生成 other.html,并将 生成的 bundle 通过 <script> 标签插入到 other.html中
    new HtmlWebpackPlugin({
      template: path.join(srcPath, "other.html"),
      filename: "other.html",
      chunks: ["other", "vendor"], // 只引用 other 和 vendor
    }),
  ],
};
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

4. 懒加载(异步加载)

  • 通过import()导包
  • 异步组件
  • vue-router 懒加载

通过以上三种方式使用异步加载,webpack 自动生成以数字开头的bundle,如 0.bundle.js

5. 处理 JSX

通过 babel 设置 jsx 预处理 presets

6. 处理 Vue

vue-loader

7. module chunk 和 bundle 的区别

  • modle,表示各个源文件,包括 js,less,css,png 等资源,在 webpack 中一切皆模块
  • chunk,多个 module 合成,有三个来源:entry, splitChunk的cacheGroups中的chunk异步import()导入的模块
  • bundle,最终的输出文件,一般一个 chunk 对应一个 bundle

三、webpack 的性能优化

  • 优化构建速度-开发体验和效率
  • 优化产出代码吗-产品性能

从分析问题,解决问题的角度进行阐述,不要背诵

1. 构建速度

  • 优化 babel-loader
  • IgnorePlugin
  • noParse
  • happyPack
  • ParallelUglifyPlugin
  • 自动刷新
  • 热更新
  • DllPlugin

(1)优化 babel-loader

{
    test: /\.js$/,
    use: ['babel-loader?cacheDirectory'], // 开启缓存
    include: path.resolve(__dirname, 'src'), // 明确范围
    // 排除范围,和include二选一
    // exclude: path.resolve(__dirname, 'node_modules')
}
1
2
3
4
5
6
7

(2)IgnorePlugin 避免引用无用模块

moment库引入为例,moment库包含各个语言资源,忽略掉没用到的语言

1. 在代码中引入

import moment from "moment";
// 引入中文语言包
import "moment/locale/zh-cn";
// 设置语言为中文
moment.locale("zh-cn");
1
2
3
4
5

2. 在 webpack 中配置忽略规则

module.exports = {
  plugins: [new wepack.IgnorePlugin(/\.\/locale/, /moment/)],
};
1
2
3

(3)noParse 避免重复打包

有些引用的包发布时,已经经过打包处理,无需重复打包,例如xxx.min.js的包,下面以react.min.js为例:

module.exports = {
  module: {
    // 忽略 `react.min.js` 文件的递归解析处理
    noParse: [/react\.min\.js$/],
  },
};
1
2
3
4
5
6

IgnorePluginnoParse的区别

  • IgnorePlugin 不引用,代码中没有,提升构建速度,且优化产出
  • noParse引入,但不打包

(4)happyPack 多进程打包

  • JS 是单线程,所以项目大且复杂时,打包时速度慢
  • 开启多进程打包,提高构建速度
const HappyPack = require("happypack");

module.exports = {
  module: {
    rules: [
      // 修改babel-loader配置,替换成happypack中的babel loader
      {
        test: /\.js$/,
        use: ["happypack/loader?id=babel"], // id 对应 plugins 中 HappyPack配置的id
      },
    ],
  },
  plugins: [
    new HappyPack({
      // 用唯一的 id 来代表当前的 HappPack 用来处理某一类特定的文件
      id: "babel",
      // 如何处理 js 文件,用法和 loader 配置一样
      loaders: ["babel-loader?cacheDirectory"],
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

(5)ParallelUglifyPlugin 多进程压缩 JS

  • webpack 内置 Uglify 工具压缩 JS
  • JS 是单线程,通过插件开启多进程压缩,提升压缩速度
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin");

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      uglifyJS: {
        output: {
          beautify: false, // 最紧凑的输出
          comments: false, // 删除所有注释
        },
        compress: {
          drop_console: true, // 删除 console 语句
          collapse_vars: true, // 内嵌定义了但是只用到一次的变量
          reduce_vars: true, // 提取出现多次单没有定义成变量去引用的静态值
        },
      },
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

关于开启多进程打包和压缩:

  • 项目大,打包速度慢,开启多进程能提高速度
  • 项目小,打包速度快,开启多进程会降低速度(进程开销)
  • 要按需使用

(6)热更新

  • 热更新有代码侵入,只能在开发环境下使用,开发完成后需要删除侵入代码
  • 根据项目情况使用
// 1. webpack dev中配置
const HotModuleReplacementPlugin = require("webpack/lib/HotModuleReplacementPlugin");

module.exports = {
  entry: {
    // 这里变成了数组
    index: [
      "webpack-dev-server/client?http//localhost:8080/",
      "webpack/hot/dev-server",
      path.join(srcPath, "index.js"),
    ],
  },

  plugins: [new HotModuleReplacementPlugin()],

  devServer: {
    hot: true,
  },
};

// 2. 在引用的js中侵入测试代码,css,less等无需侵入,修改后直接生效
import { sum } from "./math";

// 增加,开启热更新后的逻辑代码
if (module.hot) {
  // 更改 math 内,会触发热更新
  module.hot.accept(["./math"], () => {
    const sumRes = sum(10, 20);
  });
}
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

(7)DllPlugin 动态链接库插件的使用

  • 前端框架如 vue react,体积打,构建慢
  • 较稳定,不常升级版本
  • 同一个版本只构建一次即可,不用每次都重新构建
简单说明
  • webpack 已内置 DllPlugin 的支持
  • DllPlugin-打包出 dll 文件
  • DllReferencePlugin-使用 dll 文件
配置步骤
1. 新建一个 webpack.dll.js,使用 npm webpack 生产 dll
const path = require("path");
const DllPlugin = require("webpack/lib/DllPlugin");
const { srcPath, distPath } = require("./paths");

module.exports = {
  mode: "development",
  // JS 执行入口文件
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ["react", "react-dom"],
    output: {
      // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
      // 也就是 entry 中配置的 react 和 polyfillfile
      name: "[name].dll.js",

      // 输出的文件都放到 dist 目录下
      path: distPath,

      // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
      // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
      library: "_dll_[name]",
    },
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      // 例如 react.manifest.json 中就有 "name": "_dll_react"
      name: "_dll_[name]",

      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(distPath, "[name].manifest.json"),
    }),
  ],
};
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
2. 在 html 模板中如 index.html 中通过<script src="./react.dll.js" />方式引用
3. 在 webpack.dev.js 中使用 DllReferencePlugin 配置
// 第一,引入 DllReferencePlugin
const DllReferencePlugin = require("webpack/lib/DllReferencePlugin");

modlue.exports = {
  mode: "development",

  module: {
    rules: [
      {
        test: /\.js$/,
        loader: ["babel-loader"],
        include: srcPath,
        exclude: /node_modules/, // 第二,不要转换 node_modules
      },
    ],
  },

  plugins: [
    // 第三,告诉 webpack 使用了哪些动态链接库
    new DllReferencePlugin({
      // 描述 react 动态链接库的文件内容
      manifest: require(path.join(distPath, "react.manifest.json")),
    }),
  ],
};
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

2. webpack 优化构建速度(可用于生产环境)

  • 优化 babel-loader(缓存-开发环境,明确和忽略范围)
  • IgnorePlugin(减少产出代码体积)
  • noParse(开发,生产皆可)
  • happyPack(按项目情况使用,开发,生产均可)
  • ParallelUglifyPlugin(若使用,必须使用生产环境)

3. webpack 优化构建速度(不可用于生产环境)

  • 自动刷新
  • 热更新
  • DllPlugin

4. webpack 性能优化-产出代码

  • 体积更小
  • 合理分包,不重复加载
  • 速度更快、内存使用更少
  1. 小图片 base64 编码(减少网络请求)
  2. bundle 加 hash(命中缓存)
  3. 懒加载-import()
  4. 提取公共代码
  5. IgnorePlugin(输出的 bundle 体积减少)
  6. 使用 CDN 加速(publicPath)
  7. 使用 production
  • 默认启动代码压缩
  • Vue React 会自动删除调试代码,如 warning
  • 默认启动 Tree-Shaking
  1. Scope Hosting
  • 代码体积更小
  • 创建函数作用域更少
  • 代码可读性更好
const MudleConcatenationPlugin = require("webpack/lib/optimize/MudleConcatenationPlugin");

module.exports = {
  resolve: {
    // 针对 npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
    mainFiles: ["jsnext:main", "brower", "main"],
  },
  plugins: [new MudleConcatenationPlugin()],
};
1
2
3
4
5
6
7
8
9

四、babel

将 ES6 及以上语法或 JSX 代码转换成 ES5 语法代码,它只负责解析语法并转换,不对符合规范的 API 进行解析,所以要使用到 babel-polyfill

  • 基本配置
  • babel-polyfill
  • babel-runtime

1. 基本配置

// .babelrc 文件配置
{
    presets: ['@babel/presets-env'],
    plugins: [ ],
}
1
2
3
4
5

2. presets 和 plugins

presets 是多个 plugin 的集合

3. babel-polyfill

  1. core-js 和 regenerator
  • core-js 集成了所有 ES6 语法的 polyfill 补丁
  • regenerator:core-js 不支持 generator 函数,regenerator 弥补了 generator
  1. babel-polyfill 是 core-js 和 regenerator 的集合

  2. babel-polyfill 在 Babel 7.4 之后弃用 babel-polyfill,并推荐直接使用 core-js 和 regenerator

(1)babel-polyfill 的按需引入

babel-polyfill 中包含所有的 ES6 语法,全部引入会引入不必要的代码,需要按需引入

// .babelrc 文件配置
{
    presets: [
        '@babel/presets-env',
        {
            "useBuiltIns": "useage", // 语法的所属阶段
            "corejs": 3 // core-js版本
        }
    ],
    plugins: [ ],
}
1
2
3
4
5
6
7
8
9
10
11

(2)babel-polyfill 的问题

  • babel-polyfill 引入的代码会污染全局变量
  • 如果做一个独立的 web 系统,则无碍
  • 如果做一个第三方的 lib,则会污染全局变量,可能会和用户使用的全局变量冲突

**解决方案:**babel-runtime

4. babel-runtime

解决 babel-polyfill 全局变量冲突

// .babelrc 文件配置
{
    presets: [
        '@babel/presets-env',
        {
            "useBuiltIns": "useage", // 语法的所属阶段
            "corejs": 3 // core-js版本
        }
    ],
    plugins: [
        "@babel/plugin-transform-runtime",
        {
            "absoluteRuntime": false,
            "corejs": 3,
            "helpers": true,
            "regenerator": true,
            "useESModule": false
        }
    ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20