webpack插件开发之非js入口文件的处理
2015.09.15
Jeremy Tang
起因
起因是网站的 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) { 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); 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 => { 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中额外添加代码导入。