盒子
盒子
文章目录
  1. 起因
  2. 想法
  3. 做法
    1. 添加入口
    2. 删除对应的chunk.files
  4. 使用

webpack插件开发之非js入口文件的处理

起因

起因是网站的 favicon.ico 这个文件,我并不想直接放到网站静态资源目录里,我想把这个文件同其他静态图片一道经过webpack处理,能够被压缩和添加hash名输出。
其实如果只是达到这个目的,事情也很容易解决,直接在js中导入即可。

import 'assets/img/favicon.ico';

但如果仅仅为了导入这样的一个图片而添加这样的一行代码,没有必要,我想通过构建的流程去解决这个问题。在我看来,img 文件通常是依附于html和css文件中。

<header>
<a class="logo"></a>
<img src="../../assets/img/avatar.jpg" alt="avatar" title="avatar"/>
<header/>
header .logo {
background-image: url('../../assets/img/logo.png');
}

以上代码,借助于webpack的loader很容易能理解这里将会自动导入并输出对应的图片。
但有一些静态资源,比如前面提到的 favicon.ico 这个东西,它一般是放在入口 index.html 的这个位置。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
...
<link rel="apple-touch-icon" href="assets/img/favicon.ico"/>
<link rel="icon" type="image/x-icon" href="assets/img/favicon.ico"/>
...
<body>
</body>
</html>

想法

这里,让我萌生了将这些静态资源作为webpack的入口的想法,于是,有了以下的webpack配置

entry: {
main: Path.join(ENV.srcPath, 'entry-client.ts'),
favicon: Path.join(ENV.srcPath, 'assets/img/favicon.ico')
}

webpack编译输出如下

{
"favicon.js": "favicon.js",
"main.css": "main.css",
"main.js": "main.js",
"runtime.js": "runtime.js",
"assets/img/favicon.ico": "assets/img/favicon.ico"
}

怀疑是设置了entry的key的关系,webpack生成了一个 favicon.js,

...
eval("module.exports = __webpack_require__.p + \"assets/img/favicon.ico\";//# sourceURL=[module]\n//# sourceMappin...
...

这个js文件是多余的,webpack的entry支持数组的形式,于是将key去掉,结果如我所料,多余的js文件不再产生。看来是的确是这个key的关系。

做法

实际应用的入口配置大部分都会设置key的,所以按照数组的形式去配置入口无法适应通用的场景。所以我还是想从webpack入手,写一个插件去解决。

添加入口

...
apply(compiler) {
// amend entry
compiler.options.entry = AmendEntry(compiler.options.entry, this.options);
}

webpack支持在plugin的apply阶段改变entry,所以这也为该插件的做法带来了可能性。这一步,主要是将指定的options.assets集合添加至entry中,且在此之前,为每一个asset都生成一个对应文件名的hash值作为entry的key。并将其存入externalEntry变量。

// 定义入口集合
let externalEntry = [];
function AmendEntry(entry, options) {
if (typeof entry === 'function') {
return (...args) =>
Promise.resolve(entry(...args)).then(AmendEntry.bind(this));
}

const seen = new Set();

if (options.assets) {
options.assets.forEach(v => {
externalEntry[HashChunk(v, seen)] = v;
});
}

if (typeof entry === 'string') {
return _.merge(
{},
{
main: entry
},
externalEntry
);
}
if (Array.isArray(entry)) {
return _.merge(
{},
{
main: entry
},
externalEntry
);
}
if (typeof entry === 'object') {
return _.merge({}, entry, externalEntry);
}

throw new Error('sswp> Cannot parse webpack `entry` option');
}

function HashChunk(str, seen) {
let len = 4;
let hash_ = Hash.sha1(str);
// 选择 4 位长度,并校验碰撞
while (seen.has(hash_.substr(0, len))) {
len++;
}
hash_ = hash_.substr(0, len);
seen.add(hash_);
return hash_;
}

删除对应的chunk.files

监听emit事件,从compilation.chunks中筛选出与externalEntry中同名属性的chunk,将其files中的 .js 或 .js.map 后缀的文件删除。

...
apply(compiler) {
const RE_JS_MAP = /\.js(|\.map)$/i;
compiler.plugin('emit', function(compilation, callback) {
try {
compilation.chunks.filter(chunk => {
return Object.prototype.hasOwnProperty.call(externalEntry, chunk.name);
}).forEach(chunk => {
// 删除 .js 或 .js.map 后缀的文件
chunk.files = chunk.files.filter(file => {
if (RE_JS_MAP.test(file)) {
delete compilation.assets[file];
return false;
}
return true;
});
});
callback();
} catch (e) {
console.warn(e);
compilation.errors.push(e);
callback && callback();
}
})
}

该 Plugin 定义如下

module.exports = class ExternalEntryPlugin {
constructor(options) {
this.options = options;
}

apply(compiler) {
...
}
}

使用

new ExternalEntryPlugin({
assets: [Path.join(ENV.srcPath, 'assets/img/favicon.ico')]
}),

实际使用中,可以将大部分的assets资源扔到这里,而无需再js中额外添加代码导入。