# 代码分割

安装一个 lodash 插件

yarn add lodash
1

在 index.js 使用 lodash

import _ from "lodash"
console.log(_.join([1, 2, 3], "***"));
1
2

执行打包,发现我们的 app.js 有 1.39 MiB

很明显,我们什么都没有写,仅仅只有几行代码,下次如果更新,我们的 app.js 会再次打包也是这么大,这肯定不如我们所愿.

# 拆分 js

新建 lodash.js 并移除 index.js lodash 的引入

import _ from "lodash";
window._ = _;
1
2

修改 webpack 配置 entry

entry: {
    lodash: './src/lodash.js',
    app: './src/index.js',
}
1
2
3
4

再次执行打包,发现 dist 目录下生成 app.js 和 lodash.js, 这是我们的 app.js 只有 31.8kb, lodash.js 有 1.39m

lodash 作为一个库文件肯定是不会经常变更的。对于我们的业务代码 app.js 来说,如果下次更新了,用户只需要拉去最新的 app.js 代码即可, lodash 则根据浏览器的强缓存 而不需要再次请求服务器,大大的提升了性能

# 使用 splitChunks 自动拆分

上面的代码分割是我们自己手动进行的,其实 webpack splitChunks 会自动进行,下面我们来进行配置下.

删除 lodash.js,index.js 还是正常引入 lodash,修改下 webpack 配置

entry:'./src/index.js',
...
optimization: {
    splitChunks: {
        chunks: 'all'
    }
},
1
2
3
4
5
6
7

再次执行打包,发现 webpack 打包出来了一个 vendor-main.js 自动帮我们进行了代码分割

# SplitChunksPlugin

默认配置如下:

splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • async 只会对异步才进行代码分割,对同步无效,需要设置为 all
  • cacheGroups 缓存组 抽取公共模块那个地方的
  • priority 优先级权重
cacheGroups: {
    vendor: {
        name: "vendor",
        test: /[\\/]node_modules[\\/]/,
        chunks: "all",
        priority: 10 // 优先级
    },
    common: {
        name: "common",
        test: /[\\/]src[\\/]/,
        minSize: 1024,
        chunks: "all",
        priority: 5
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如上配置了两个缓存组 vendor 和 common

  • vendor: 抽取来自 node_modules 文件夹下的第三方代码,优先级权重为 10
  • common: 抽取来自 src 文件夹下的代码,优先级权重为 5
  • vendor 比 common 的优先级权重高,所以先提取 vendor,再提取 common

# chunks 为什么默认是 async

我们现在需要做这样一个需求,点击 body 创建一个 div

import _ from "lodash";
function getComponent() {
  var element = document.createElement("div");
  element.innerHTML = _.join([1, 2, 3], "***");
  return element;
}
document.addEventListener("click", () => {
  const component = getComponent();
  document.body.appendChild(component);
});
1
2
3
4
5
6
7
8
9
10

讨论这个问题之前,我们首先应该检查一下 页面代码的 Coverage, 在 Chorem 中 找到 Coverage 进行页面录制,发现页面代码使用率

很明显 getComponent 里面的代码在首次页面渲染得时候根本用不上,因此代码使用率不高,我们懒加载再来测试一下

document.addEventListener("click", () => {
  import(/* webpackChunkName: "click" */ "./click.js").then(
    ({ default: func }) => {
      const component = func();
      document.body.appendChild(component);
    },
  );
});
1
2
3
4
5
6
7
8

click.js

import _ from "lodash";
function getComponent() {
  var element = document.createElement("div");
  element.innerHTML = _.join([1, 2, 3], "***");
  return element;
}

export default getComponent;
1
2
3
4
5
6
7
8

执行打包 发现报错,提示不识别 /_ webpackChunkName: "click" _/ 这样得写法,安装插件

npm install --save-dev @babel/plugin-syntax-dynamic-import
1

配置 .babelrc

"plugins": ["@babel/plugin-syntax-dynamic-import"]
1

打开 Network 我们发现 首次只加载了 main.js 执行点击后分别加载了 vendors.js 和 click.js

webpack 希望我们是使用异步懒加载来进行编程,因此 chunks 默认 也就是 async. 对于 webpack 来说 同步提取意义也不是很大

# 利用 prefetch 进行优化

上面我们虽然对代码进行了分割懒加载,我们在点击得时候才会去加载对应得 js 文件,那么这个时候就会给人一种反应 很慢得感觉。 那能不能在进入首页后,在浏览器的空闲时间提前下好我们页面需要的 js 文件呢

webpack 给我们提供了 prefetch ,会在浏览器空闲时,下载相应文件,我们再次修改下代码

import(/* webpackPrefetch: true */ './click.js')
1

# 对 CSS 进行代码分割

安装插件

npm install --save-dev mini-css-extract-plugin
1

配置 webpack.prod.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                MiniCssExtractPlugin.loader,
                'css-loader'
            ]
        }
      ]
}
plugins: [new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[id].css',
    })],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在 index.js 中引入 index.css 和 index1.css 执行 prod 打包。发现 会生成一个 main.css

# Production 压缩

webpack5 可能会内置 CSS 压缩器,webpack4 需要自己使用压缩器,可以使用 optimize-css-assets-webpack-plugin 插件

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

optimization: {
    minimizer: [
        new OptimizeCSSAssetsPlugin({}),
    ],
},
1
2
3
4
5
6
7

再次打包 发现 css 文件被压缩合并了

# 把 CSS 提取到一个文件

可以利用 splitChunks.cacheGroups 将 css 提取到一个 CSS 中

optimization: {
...
    splitChunks: {
        cacheGroups: {
            styles: {
                name: 'styles',
                test: /\.css$/,
                chunks: 'all',
                enforce: true,
            },
        },
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 根据 entry 提取 CSS

可以根据 webpack 的 entry name 来提取 CSS,这对动态引入路由,并且动态加载对于的 css 这种情况十分有用.

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry: {
    foo: path.resolve(__dirname, "src/foo"),
    bar: path.resolve(__dirname, "src/bar"),
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        fooStyles: {
          name: "foo",
          test: (m, c, entry = "foo") =>
            m.constructor.name === "CssModule" && recursiveIssuer(m) === entry,
          chunks: "all",
          enforce: true,
        },
        barStyles: {
          name: "bar",
          test: (m, c, entry = "bar") =>
            m.constructor.name === "CssModule" && recursiveIssuer(m) === entry,
          chunks: "all",
          enforce: true,
        },
      },
    },
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
};
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

# shimming

shimming 垫片的使用

//现在有两个JS文件:

//一个index.js,内容如下
import _ from 'lodash';
import $ from 'jquery';
import {ui} from './jquery-ui';
ui();

/**其他业务代码*/

//一个我们模仿的第三方模块jqurey-ui.js文件:
import $ from 'jquery';
export function ui() {
    $('body').css('background', 'red');
}
两个文件的关系:jqurey-ui.js是一个第三方模块,jqurey-ui.js模块依赖于jQuery,
但是这个模块中并没有导入jQuery;我们在index.js中导入了这个模块,
并且在index.js中我们还引入了lodash模块和jquery模块;
此时,我们去执行导入的UI函数可以运行吗?
答案是不可以执行,会报错,这是因为前段模块化存在变量隔离的原因,
在index.js中导入的jQuery并不能在jquery-ui.js中生效;
也就是本模块导入的变量、类、函数,只在本模块生效,不会对导入模块生效;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个时候我们可以用到 webpack.ProvidePlugin

const webpack = require("webpack");
plugins: [
  new webpack.ProvidePlugin({
    _: "lodash",
  }),
];
1
2
3
4
5
6

# 使用 imports-loader 实现垫片

现在我们有这样一个场景,想让所有的 this 都指向 windows 这个时候可以使用 imports-loader

npm install imports-loader --save-dev
1

webpack 配置

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: [
        { loader: "babel-loader" },
        {
        loader: "imports-loader? ",
        options: "this=>window"
        }
      ],
 }
1
2
3
4
5
6
7
8
9
10
11