如何写一个webpack插件(一)
前言
最近由于用着html-webpack-plugin觉得很不爽,于是乎想自己动手写一个插件。原以为像gulp插件一样半天上手一天写完,但令人郁闷的是完全找不到相关的文章。一进官方文档却是被吓傻了。首先是进入 how to write a plugin 看了一页简单的介绍。然后教程会告诉你,你需要去了解compiler和compilation这两个对象,才能更好地写webpack的插件,然后作者给了github的链接给你,让你去看源代码,我晕。不过幸好最后给了一个 plugins 的API文档,才让我开发的过程中稍微有点头绪。
how to write a plugin这个教程还是可以好好看看的,尤其是那个simple example,它会教你在compilation的emit事件或之前,将你需要生成的文件放到webpack的compilation.assets里,这样就可以借助webpack的力量帮你生成文件,而不需要自己手动去写fs.writeFileSync。
主要就是这段代码
compilation.assets['filelist.md'] = { source: function() { return filelist; }, size: function() { return filelist.length; } };
基本特性介绍
首先,定义一个函数func,用户设置的options基本就在这里处理。
其次,需要设一个func.prototype.apply函数。这个函数是提供给webpack运行时调用的。webpack会在这里注入compiler对象。
输出complier对象,你会看到这一长串的内容,初步一看,我看出了两大类(有补充的可以告诉我)。一个webpack运行时的参数,例如_plugins,这些数组里的函数应该是webpack内置的函数,用于在compiltion,this-compilation和should-emit事件触发时调用的。另一个是用户写在webpack.config.js里的参数。隐约觉得这里好多未来都可能会是webpack暴露给用户的接口,使webpack的定制化功能更强大。
Compiler { _plugins: { compilation: [ [Function], [Function], [Function], [Function] ], 'this-compilation': [ [Function: bound ] ], 'should-emit': [ [Function] ] }, outputPath: '', outputFileSystem: null, inputFileSystem: null, recordsInputPath: null, recordsOutputPath: null, records: {}, fileTimestamps: {}, contextTimestamps: {}, resolvers: { normal: Tapable { _plugins: {}, fileSystem: null }, loader: Tapable { _plugins: {}, fileSystem: null }, context: Tapable { _plugins: {}, fileSystem: null } }, parser: Parser { _plugins: { 'evaluate Literal': [Object], 'evaluate LogicalExpression': [Object], 'evaluate BinaryExpression': [Object], 'evaluate UnaryExpression': [Object], 'evaluate typeof undefined': [Object], 'evaluate Identifier': [Object], 'evaluate MemberExpression': [Object], 'evaluate CallExpression': [Object], 'evaluate CallExpression .replace': [Object], 'evaluate CallExpression .substr': [Object], 'evaluate CallExpression .substring': [Object], 'evaluate CallExpression .split': [Object], 'evaluate ConditionalExpression': [Object], 'evaluate ArrayExpression': [Object], 'expression Spinner': [Object], 'expression ScreenMod': [Object] }, options: undefined }, options: { entry: { 'index': '/Users/mac/web/src/page/index/main.js' }, output: { publicPath: '/homework/features/model/', path: '/Users/mac/web/dist', filename: 'js/[name].js', libraryTarget: 'var', sourceMapFilename: '[file].map[query]', hotUpdateChunkFilename: '[id].[hash].hot-update.js', hotUpdateMainFilename: '[hash].hot-update.json', crossOriginLoading: false, hashFunction: 'md5', hashDigest: 'hex', hashDigestLength: 20, sourcePrefix: '\t', devtoolLineToLine: false }, externals: { react: 'React' }, module: { loaders: [Object], unknownContextRequest: '.', unknownContextRecursive: true, unknownContextRegExp: /^\.\/.*$/, unknownContextCritical: true, exprContextRequest: '.', exprContextRegExp: /^\.\/.*$/, exprContextRecursive: true, exprContextCritical: true, wrappedContextRegExp: /.*/, wrappedContextRecursive: true, wrappedContextCritical: false }, resolve: { extensions: [Object], alias: [Object], fastUnsafe: [], packageAlias: 'browser', modulesDirectories: [Object], packageMains: [Object] }, plugins: [ [Object], [Object], [Object], [Object], NoErrorsPlugin {}, [Object], [Object] ], devServer: { port: 8081, contentBase: './dist' }, context: '/Users/mac/web/', watch: true, debug: false, devtool: false, cache: true, target: 'web', node: { console: false, process: true, global: true, setImmediate: true, __filename: 'mock', __dirname: 'mock' }, resolveLoader: { fastUnsafe: [], alias: {}, modulesDirectories: [Object], packageMains: [Object], extensions: [Object], moduleTemplates: [Object] }, optimize: { occurenceOrderPreferEntry: true } }, context: '/Users/mac/web/' }
除此以外,compiler还有一些如run, watch-run的方法以及compilation, normal-module-factory对象。我目前用到的,主要是compilation。其它的等下一篇有机会再说。
对比起compiler还有compiler.plugin函数。这个相当于是插件可以进行处理的webpack的运行中的一些任务点,webpack就是完成一个又一个任务而完成整个打包构建过程的。如make是最开始的起点, complie就是编译任务点,after-complie是编译完成,emit是即将准备生成文件,after-emit是生成文件之后等等,前面几个都是比较生动形象的任务点。
至于compilation,它继承于compiler,所以能拿到一切compiler的内容(所以你也会看到webpack的options),而且也有plugin函数来接入任务点。在compiler.plugin('emit')任务点输出compilation,会得到大致下面的对象数据,因为实在太长,我只保留了最重要的assets部份:
assetsCompilation { assets: { 'js/index/main.js': CachedSource { _source: [Object], _cachedSource: undefined, _cachedSize: undefined, _cachedMaps: {} } }, errors: [], warnings: [], children: [], dependencyFactories: ArrayMap { keys: [ [Object], [Function: MultiEntryDependency], [Function: SingleEntryDependency], [Function: LoaderDependency], [Object], [Function: ContextElementDependency], values: [ NullFactory {}, [Object], NullFactory {} ] }, dependencyTemplates: ArrayMap { keys: [ [Object], [Object], [Object] ], values: [ ConstDependencyTemplate {}, RequireIncludeDependencyTemplate {}, NullDependencyTemplate {}, RequireEnsureDependencyTemplate {}, ModuleDependencyTemplateAsRequireId {}, AMDRequireDependencyTemplate {}, ModuleDependencyTemplateAsRequireId {}, AMDRequireArrayDependencyTemplate {}, ContextDependencyTemplateAsRequireCall {}, AMDRequireDependencyTemplate {}, LocalModuleDependencyTemplate {}, ModuleDependencyTemplateAsId {}, ContextDependencyTemplateAsRequireCall {}, ModuleDependencyTemplateAsId {}, ContextDependencyTemplateAsId {}, RequireResolveHeaderDependencyTemplate {}, RequireHeaderDependencyTemplate {} ] }, fileTimestamps: {}, contextTimestamps: {}, name: undefined, _currentPluginApply: undefined, fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57', hash: 'f4030c2aeb811dd6c345', fileDependencies: [ '/Users/mac/web/src/js/index/main.js' ], contextDependencies: [], missingDependencies: [] }
assets部份重要是因为如果你想借助webpack帮你生成文件,你需要像官方教程how to write a plugin在assets上写上对应的文件信息。
除此以外,compilation.getStats()这个函数也相当重要,能得到生产文件以及chunkhash的一些信息,如下:
assets{ errors: [], warnings: [], version: '1.12.9', hash: '5a5c71cb2accb8970bc3', publicPath: 'xxxxxxxxxx', assetsByChunkName: { 'index/main': 'js/index/index-4c0c16.js' }, assets: [ { name: 'js/index/index-4c0c16.js', size: 453, chunks: [Object], chunkNames: [Object], emitted: undefined } ], chunks: [ { id: 0, rendered: true, initial: true, entry: true, extraAsync: false, size: 221, names: [Object], files: [Object], hash: '4c0c16e8af4d497b90ad', parents: [], origins: [Object] } ], modules: [ { id: 0, identifier: 'multi index/main', name: 'multi index/main', index: 0, index2: 1, size: 28, cacheable: true, built: true, optional: false, prefetched: false, chunks: [Object], assets: [], issuer: null, profile: undefined, failed: false, errors: 0, warnings: 0, reasons: [] }, { id: 1, identifier: '/Users/mac/web/node_modules/babel-loader/index.js?presets[]=es2015⪯sets[]=react!/Users/mac/web/src/js/main/index.js', name: './src/js/index/main.js', index: 1, index2: 0, size: 193, cacheable: true, built: true, optional: false, prefetched: false, chunks: [Object], assets: [], issuer: 'multi index/main', profile: undefined, failed: false, errors: 0, warnings: 0, reasons: [Object], source: '' // 具体文件内容} ], filteredModules: 0, children: [] }
这里的chunks数组里,是对应会生成的文件,以及md5之后的文件名和路径,里面还有文件对应的chunkhash(每个文件不同,但如果你使用ExtractTextPlugin将css文件独立出来的话,它会与require它的js入口文件共享相同的chunkhash),而assets.hash则是统一的hash,对每个文件都一样。值得关注的是chunks里的每个文件,都有source这一项目,提供给开发者直接拿到源文件内容(主要是js,如果是css且使用ExtractTextPlugin,则请自行打印出来参考)。
例子
接下来,会以最近我写的一个插件 html-res-webpack-plugin 作为引子,来介绍基本的写插件原理。插件的逻辑就写在index.js里。
首先,将用户输入的参数在定好的函数中处理,HtmlResWebpackPlugin。
然后,新增apply函数,在里面写好插件需要切入的webpack任务点。目前HtmlResWebpackPlugin插件只用到emit这个任务点,其它几个仅作为演示。
第三步,调用addFileToWebpackAsset方法,写compilation.assets,借助webpack生成html文件。
第四步,在开发模式下(isWatch = true),直接生成html,但在生产模式下(isWatch = true),插件会开始对静态资源(js,css)进行md5或者内联。
第五步,调用findAssets方法是为了通过compilation.getStats()拿到的数据,去匹配对应的静态资源,还有找到对应的哈希(是chunkhash还是hash)。
最六步,调用addAssets方法,对静态资源分别做内联或者md5文件处理。内联资源的函数是inlineRes,你会看到我使用了compilation.assets[hashFile].source() 及 compilation.assets[hashFile].children[1]._value。前者是针对于js的,后者是针对使用了ExtractTextPlugin的css资源。
最后一步,即是内联和md5完成后,再更新一下compilation.assets中对应生成html的source内容,才能正确地生成内联和md5后的内容。
后记
有兴趣可以试用一下 html-res-webpack-plugin 这个插件(为什么要写一个新的html生成插件,我在readme里写了,此处不赘述),看看有哪些用得不爽之处。目前只是第一版,还不适合用于生产环境。希望第二版的时候能适用于更多的场景,以及性能更好。到是,我也会写第二篇插件开发文章,将本文还没提到的地方一一补充完整。也欢迎大家在这里发贴,或者指出本人的谬误之处。