前言

最近在使用 Webpack 打包代码的时候,出现了一个问题,就是:HtmlWebpackPlugin 这个插件,在将其 inject 设为 true 的时候 (更多参数查看这里),它能够自动将打包好的代码插入到 HTML 文件中,但是当我需要插入多个代码文件(也就是多个 Chunk) 的时候,就会涉及到 Chunk 先后顺序的问题。

因为,比如我要插入 a.jsb.jsb.js 依赖于 a.js(也就是 b.js 执行的前提是 a.js 已经加载),我就需要保证,a.js 应该先于 b.js 插入 HTML 文件。

HtmlWebpackPlugin

其实 HtmlWebpackPlugin 已经为我们提供了三个配置项来配置插入的 Chunk:

  • chunks: 允许你添加指定的 Chunk,比如:”app” 或者 [“lib”, “app”]
  • chunksSortMode: 允许你根据一定规则对 Chunk 进行排序,允许的值有:’none’ | ‘auto’ | ‘dependency’ |’manual’ | {function}。默认是: ‘auto’
  • excludeChunks: 允许你排除指定的 Chunk,比如:”test” 或者 [“test”, “example”]

我们来粗略地讲一讲,chunksSortMode 这个配置项,他有五种可选项:’none’、’auto’、’dependency’、’manual’、{function},由于 npmjs 包括 Github 上对这几个参数的具体含义都没怎么阐述清楚,那我们就只有去看源码咯

HtmlWebpackPlugin.sortChunks

不要害怕看源码,这些开源项目的源码都写得很清晰易读的,具体的代码部分在这里 sortChunks,我顺便也贴出来:

1
HtmlWebpackPlugin.prototype.sortChunks = function (chunks, sortMode) {
2
  // Sort mode auto by default:
3
  if (typeof sortMode === 'undefined') {
4
    sortMode = 'auto';
5
  }
6
  // Custom function
7
  if (typeof sortMode === 'function') {
8
    return chunks.sort(sortMode);
9
  }
10
  // Disabled sorting:
11
  if (sortMode === 'none') {
12
    return chunkSorter.none(chunks);
13
  }
14
  // Check if the given sort mode is a valid chunkSorter sort mode
15
  if (typeof chunkSorter[sortMode] !== 'undefined') {
16
    return chunkSorter[sortMode](chunks, this.options.chunks);
17
  }
18
  throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
19
};

从源码中我们能够发现,确实如使用说明当中的一样,默认的模式(也就是没有指定)为 ‘auto’;

同时我们也能发现,当我们传入一个 Function 的时候,他会以这个函数为排序函数来对这些 Chunk 进行排序;

当输入为 none 的时候,它会用 chunkSorter.none 这个函数来排序;

最后当我们输入其他选项的时候,它会根据我们输入的值到 chunkSorter 中寻找对应的排序算法。

看来我们还需要去看看 chunkSorter 究竟是什么玩意儿,通过 sortChunks 函数所在文档的头部,我们能够看到:

1
var path = require('path');
2
var childCompiler = require('./lib/compiler.js');
3
var prettyError = require('./lib/errors.js');
4
var chunkSorter = require('./lib/chunksorter.js');
5
Promise.promisifyAll(fs);

chunkSorter 是从 ./lib/chunksorter.js 这个文件导入的,我们就再找到这个文件。

HtmlWebpackPlugin > chunksorter.js

chunksorter.js 文档看起来不少,但其实大部分都是注释,我们先看第 25 行:

1
module.exports.dependency = function (chunks) {

看来这是实现 ‘dependency’ 这个排序模式所用到的方法,如果你有兴趣你可以读一读它的实现,我们当前重点不在于此。

紧接着第 64 行 是关于 ‘id’ 排序的实现,我们需要看一下:

1
module.exports.id = function (chunks) {
2
  return chunks.sort(function orderEntryLast (a, b) {
3
    if (a.entry !== b.entry) {
4
      return b.entry ? 1 : -1;
5
    } else {
6
      return b.id - a.id;
7
    }
8
  });
9
};

关于 Array.sort 的用法你如果忘记了,可以在这里看 MDN 的说明文档;大意就是返回值小于 0 ,则将第一个参数放在第二个参数前面。

上面的源码说明,如果输入的两个 chunk 对应的入口 entry 不同(entry 是一个 Bool 值,表示这个文件是否是入口文件),则表示只有一个文件不是入口文件,然后把入口文件放在前面;还有一种情况就是两个都是或者都不是入口文件,那么我们再用它的 id 来进行排序,id 大的放前面。

那问题又来了,这个 Chunk.id 是怎么来的呢?(终于引出本文重点了)

Webpack

由于 Chunk 的实例都是 Webpack 创建好,然后传到每个插件中的,所以要了解 Chunk.id 是如何产生的,我们还要去查看 Webpack 关于 Chunk.id 的源码。

我已经找到了,在这里 applyChunkIds,足足有 46 行的代码,让我们深呼吸。

Webpack > applyChunkIds

代码我贴在下面了,你感兴趣可以全看完,我就粗略的讲一下:

1
applyChunkIds() {
2
  const unusedIds = [];
3
  let nextFreeChunkId = 0;
4
5
  function getNextFreeChunkId(usedChunkIds) {
6
    const keyChunks = Object.keys(usedChunkIds);
7
    let result = -1;
8
9
    for(let index = 0; index < keyChunks.length; index++) {
10
      const usedIdKey = keyChunks[index];
11
      const usedIdValue = usedChunkIds[usedIdKey];
12
13
      if(typeof usedIdValue !== "number") {
14
        continue;
15
      }
16
17
      result = Math.max(result, usedIdValue);
18
    }
19
20
    return result;
21
  }
22
23
  if(this.usedChunkIds) {
24
    nextFreeChunkId = getNextFreeChunkId(this.usedChunkIds) + 1;
25
    let index = nextFreeChunkId;
26
    while(index--) {
27
      if(this.usedChunkIds[index] !== index) {
28
        unusedIds.push(index);
29
      }
30
    }
31
  }
32
33
  const chunks = this.chunks;
34
  for(let indexChunk = 0; indexChunk < chunks.length; indexChunk++) {
35
    const chunk = chunks[indexChunk];
36
    if(chunk.id === null) {
37
      if(unusedIds.length > 0)
38
        chunk.id = unusedIds.pop();
39
      else
40
        chunk.id = nextFreeChunkId++;
41
    }
42
    if(!chunk.ids) {
43
      chunk.ids = [chunk.id];
44
    }
45
  }
46
}

从上面的代码我们可以看出,Chunk.id 都是递增且唯一的。那上面代码中,有一个变量我们并没有在当前文件中找到,他就是 usedChunkIds

Webpack > usedChunkIds

通过一些办法,我在这里找到了 RecordIdsPlugin。从上下文环境中,我们发现了这段代码

1
records.chunks.usedIds = {};
2
chunks.forEach(chunk => {
3
  const name = chunk.name;
4
  const blockIdents = chunk.blocks.map(getDepBlockIdent.bind(null, chunk)).filter(Boolean);
5
  if(name) records.chunks.byName[name] = chunk.id;
6
  blockIdents.forEach((blockIdent) => {
7
    records.chunks.byBlocks[blockIdent] = chunk.id;
8
  });
9
  records.chunks.usedIds[chunk.id] = chunk.id;
10
});

结论

结果是,Webpack 通过一大堆的分析树分析依赖,最终为每个入口文件确定了其依赖树的大小。

其实多个入口间的 Chunk.id 并没有一定的规律性(这里不考虑依赖中的情况),仅仅是通过判断依赖树的大小,依赖树越大的 id 越小

所以 HtmlWebpackPlugin 中,通过 chunk.id 来排序的方式存在很大的局限性,不可轻易使用。