2

通过 webpack 打包文件,并根据不同环境独立出相应的配置文件,这是我们都会的。但,我们不能满足于能打包出文件就可以了,还应该思考怎么打包出更优的文件。比如,我们都知道浏览器有强缓存与协商缓存,如果能够让不变的文件得到有效缓存,变动的文件得到及时更新,这样就能在性能上有所提升。

建议:官网也有类似的说明,建议去官网阅读,官网提供了中文切换选项。中文网上的文档还没及时更新,有些写法变了。

如果有说的不对的地方,烦请指出,谢谢,我也是尝试总结写这样的问题。

分析问题

假如,我们有一下 webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
    })
  ],
};

有这样的 index.js

import _ from 'lodash';

const component = function() {
  let el = document.createElement('div');
  el.innerHTML = _.join(['webpack', '基础配置'], ' ');
  el.onclick = print;
  return el;
}

const element = component();
document.body.appendChild(element);

执行 npm run build 打包出来后,有如下文件:

dist
├── app.bundle.js  // 72.2 KiB
└── index.html // 206 bytes

注意上面的 app.bundle.js 中包含了 lodash,如果 app.bundle.js 更新了,每次重新加载的时候,都需要重新加载这个不变的 lodash,这样显然是浪费请求资源的。因此,这里就需要做两件事
第一,分离打包文件。第二,解决缓存问题

分离打包文件

常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

之前的教程是通过 webpack.optimize.CommonsChunkPlugin 去分离打包文件的,在 webpack4.x 开始,官方移除了 commonchunk 插件,改用了 optimization 属性进行更加灵活的配置。想要了解 这两者之间的差别,可以参考webpack4:连奏中的进化

下面,我们来配置如何分离打包文件:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
  // ...
  entry: {
    common: ['lodash'],
    app: './src/index.js',
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: "common",
          chunks: "initial",
          minChunks: 2
        },
      },
    },
  },
  plugins: [
    new CleanWebpackPlugin(), // 在后续的过程性会多次构建,添加这个插件
    // ...
  ],
  // ...
}

查看目录:

dist
├── app.bundle.js //  1.69 KiB
├── common.bundle.js // 71.1 KiB 
└── index.html // 246 bytes

可以看到,包含在 common.bundle.js 中的 lodash 已经 从 app.bundle.js 中分离出来了,从他们的大小,跟之前的对比,也能证明这点。

现在,解决了 分离打包 问题,但是 缓存 的问题,还没解决。从上面打包出来的目录,我们注意到,每次打包出来的 文件名都一样 比如 app.bundle.js。这样,浏览器可能会将这个文件缓存下来,当这个文件更新时,执行构建,它生成的依然是同样名字的文件,浏览器可能就会人会它没更新,就会使用之前缓存的文件。

这里说明一点,关于上面说的 它没更新,我的理解是:假如有效期是无限,可能会出现这种情况,跟服务端 no-cache配置有关。这里讨论的方案是,不管你后端配没配置,有改动后,我生成一个新文件,这样就能保证更新。

所以下面,我们来配置如何每次构建生成新文件

[name].[hash].bundle.js

这里利用 output 中的 filename 字段,将文件重命名,就能实现每次构建生成不一样的文件名:

module.exports = {
  entry: { /*...*/ },
  output: {
    filename: '[name].[hash].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  optimization: { /*...*/ },
  plugins: [ /*...*/ ],
};

打包后的文件如下:

dist
├── app.d58e77cdc316b3ce1854.bundle.js // 1.69 KiB
├── common.d58e77cdc316b3ce1854.bundle.js // 71.1 KiB 
└── index.html // 288 bytes

貌似,解决了这个问题。是,它是生成了hash,但是如果,我们修改 index.js 中的内容,再次执行构建:

dist
├── app.26935d7e055caac6c663.bundle.js // 1.69 KiB
├── common.26935d7e055caac6c663.bundle.js // 71.1 KiB 
└── index.html // 288 bytes

观察打包后的文件,发现,我们只改了 index.js,打包后,app.jscommo.js 的文件名都变了。这不是我们想要的,我们只需要有变动的 app.js 文件名变化,没有变动的 common.js 文件名不变化,这样就能利用缓存。

[name].[chunkhash].bundle.js

我们将配置稍微改下,将 [hash] 改成 [chunkhash] 根据,文件内容生成一串 hash。(注意,开发环境下,一般不要用 chunkhash 因为这会增加编译时间。而且,这个时候 HMR 将不起作用)

module.exports = {
  // ...
  output: {
    filename: '[name].[chunkhash].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  // ...
}

打包:

dist
├── app.6f86a1c3055fcbb2f569.bundle.js
├── common.ed707d7fd801788845a1.bundle.js
└── index.html

这个时候,还看不出效果,修改 index.js,打包:

dist
├── app.afc86616f6e622a167b5.bundle.js
├── common.ed707d7fd801788845a1.bundle.js
└── index.html

观察,发现,app.js 文件名变了,common.js 文件名没变,看起来,好像管用。

情况一

如果,我们将入口出的文件顺序颠倒一下:

module.exports = {
  entry: {
    app: './src/index.js',
    common: ['lodash'],
  },
}

再次打包:

dist
├── app.3c8b22be8868b0feb022.bundle.js
├── common.7310dc2ae1c70f3154f4.bundle.js
└── index.html

发现,两个文件名都变了。卒!

情况二

如果,在情况一的基础上(只要确保 app 入口,在 common 入口 前面,效果一样),在 index.js 中,引入 print.js

// index.js
// ...
import print from './print';
// ...

// print.js
import jquery from 'jquery';
export default function printMe() {
  console.log(jquery.name);
}

打包:

dist
├── app.0d03e2f9e94a947297c1.bundle.js
├── common.147b8278aca7b0bd1b4a.bundle.js
└── index.html

发现,两个文件名都变了。卒!

manifest

上面,两种情况主要是由于 manifest 的原因。

我的理解是,在webpack 构建过程中,通过入口去分析依赖,在内部生成一个类似 manifest 的文件,这个文件记录了模块与文件的映射关系。

情况一,当修改入口顺序,生成的 manifest 表,会变化,而这个 manifest 会打包到文件中去,因此,前面两个文件名会变;

本来分析依赖是 common.js -> index.js 现在变成 index.js -> common.js

情况二,入口顺序不变,处于入口前面的文件中,如果添加依赖,或者删除依赖,也会使 manifest 发生变化,因此,两个文件名会变。

本来分析依赖是 index.js -> common.js 现在变成 index.js -> print.js -> common.js

那怎么办呢?有办法,将 manifest 文件提出来就可以了。在 webpack4.x 之前可能是通过 webpack.optimize.CommonsChunkPlugin 提取,前面说过,这个插件已经被干掉了。webpack4.x 是通过如下方法提取:

module.export = {
  // ...
  optimization: {
    // runtimeChunk: 'single',
    runtimeChunk: {
      name: 'manifest'
    }
    // ...
  }
  // ...
}

tip:

{
  runtimeChunk: 'single' // 这个写法 相当于下面。所以 你生成的 runtime 文件就是 manifest

  runtimeChunk: {
    name: 'runtime'
  }
}

这个时候,你去打包,再将 情况一情况二 做一遍,发现还是会变。怎么回事?

moduleIds

提取了 manifest 文件,依然文件名都会变,是因为 manifest 是个映射表(我的立即),形式是个 key value,[moduleIds] -> 'file'

默认情况下,是以递增的数字作为 moduleIds 比如 [1] -> index.js [2] -> common.js

我们虽然提取出 manifest,但是顺序变了后,数字 [1] 对应的那个文件也变了,因此,我们需要让 moduleIds文件 都固定下来。·

webpack4.x 之前是通过,NamedModulesPlugin 或者 HashedModuleIdsPlugin 插件将 id 固定下来。

webpack4.x 可以通过 optimization.moduleIds 设置,可以设置为 named hash 也可以达到同样的效果。

module.export = {
  // ...
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    moduleIds: 'named',
    // ...
  }
  // ...
}

这个时候,操作情况一,情况二,应该是符合我们预期的。

完整代码

webpack.config.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
    common: ['lodash'],
  },
  output: {
    filename: '[name].[chunkhash].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  optimization: {
    // runtimeChunk: 'single',
    runtimeChunk: {
      name: 'manifest'
    },
    moduleIds: 'named',
    splitChunks: {
      // chunks: 'all',
      cacheGroups: {
        commons: {
          name: "common",
          chunks: "initial",
          minChunks: 2
        },
      },
    },
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
    }),
  ],
};

index.js

import _ from 'lodash';
import print from './print';

const component = function() {
  let el = document.createElement('div');
  el.innerHTML = _.join(['webpack', '基础配置11'], ' ');
  el.onclick = print;
  return el;
}

const element = component();
document.body.appendChild(element);

print.js

import jquery from 'jquery';
export default function printMe() {
  console.log(jquery.name);
}

参考链接

webpack进阶——缓存与独立打包


zhangjinpei
103 声望6 粉丝

做一枚精致的前端er