{"action":"created","issue":{"url":"https://api.github.com/repos/lcxfs1991/blog/issues/1","repository_url":"https://api.github.com/repos/lcxfs1991/blog","labels_url":"https://api.github.com/repos/lcxfs1991/blog/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/lcxfs1991/blog/issues/1/comments","events_url":"https://api.github.com/repos/lcxfs1991/blog/issues/1/events","html_url":"https://github.com/lcxfs1991/blog/issues/1","id":125764628,"number":1,"title":"如何写一个webpack插件（一）","user":{"login":"lcxfs1991","id":3348398,"avatar_url":"https://avatars.githubusercontent.com/u/3348398?v=3","gravatar_id":"","url":"https://api.github.com/users/lcxfs1991","html_url":"https://github.com/lcxfs1991","followers_url":"https://api.github.com/users/lcxfs1991/followers","following_url":"https://api.github.com/users/lcxfs1991/following{/other_user}","gists_url":"https://api.github.com/users/lcxfs1991/gists{/gist_id}","starred_url":"https://api.github.com/users/lcxfs1991/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lcxfs1991/subscriptions","organizations_url":"https://api.github.com/users/lcxfs1991/orgs","repos_url":"https://api.github.com/users/lcxfs1991/repos","events_url":"https://api.github.com/users/lcxfs1991/events{/privacy}","received_events_url":"https://api.github.com/users/lcxfs1991/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":14,"created_at":"2016-01-09T15:23:34Z","updated_at":"2016-07-27T10:31:02Z","closed_at":null,"body":"## 前言\r\n\r\n最近由于用着html-webpack-plugin觉得很不爽，于是乎想自己动手写一个插件。原以为像gulp插件一样半天上手一天写完，但令人郁闷的是完全找不到相关的文章。一进官方文档却是被吓傻了。首先是进入[how to write a plugin](https://webpack.github.io/docs/how-to-write-a-plugin.html)看了一页简单的介绍。然后教程会告诉你，你需要去了解compiler和compilation这两个对象，才能更好地写webpack的插件，然后作者给了github的链接给你，让你去看源代码，我晕。不过幸好最后给了一个[plugins](https://webpack.github.io/docs/plugins.html#the-compiler-instance)的API文档，才让我开发的过程中稍微有点头绪。\r\n\r\nhow to write a plugin这个教程还是可以好好看看的，尤其是那个simple example，它会教你在compilation的emit事件或之前，将你需要生成的文件放到webpack的compilation.assets里，这样就可以借助webpack的力量帮你生成文件，而不需要自己手动去写fs.writeFileSync。\r\n\r\n主要就是这段代码\r\n```\r\ncompilation.assets['filelist.md'] = {\r\n      source: function() {\r\n        return filelist;\r\n      },\r\n      size: function() {\r\n        return filelist.length;\r\n      }\r\n};\r\n```\r\n## 基本特性介绍\r\n\r\n首先，定义一个函数func，用户设置的options基本就在这里处理。\r\n\r\n其次，需要设一个func.prototype.apply函数。这个函数是提供给webpack运行时调用的。webpack会在这里注入compiler对象。\r\n\r\n### compiler对象\r\n输出complier对象，你会看到这一长串的内容（如下面代码），初步一看，我看出了两大类（有补充的可以告诉我）。一个webpack运行时的参数，例如_plugins，这些数组里的函数应该是webpack内置的函数，用于在compiltion，this-compilation和should-emit事件触发时调用的。另一个是用户写在webpack.config.js里的参数。隐约觉得这里好多未来都可能会是webpack暴露给用户的接口，使webpack的定制化功能更强大。\r\n\r\n```\r\nCompiler {\r\n  _plugins:\r\n   { compilation: [ [Function], [Function], [Function], [Function] ],\r\n     'this-compilation': [ [Function: bound ] ],\r\n     'should-emit': [ [Function] ] },\r\n  outputPath: '',\r\n  outputFileSystem: null,\r\n  inputFileSystem: null,\r\n  recordsInputPath: null,\r\n  recordsOutputPath: null,\r\n  records: {},\r\n  fileTimestamps: {},\r\n  contextTimestamps: {},\r\n  resolvers:\r\n   { normal: Tapable { _plugins: {}, fileSystem: null },\r\n     loader: Tapable { _plugins: {}, fileSystem: null },\r\n     context: Tapable { _plugins: {}, fileSystem: null } },\r\n  parser:\r\n   Parser {\r\n     _plugins:\r\n      { 'evaluate Literal': [Object],\r\n        'evaluate LogicalExpression': [Object],\r\n        'evaluate BinaryExpression': [Object],\r\n        'evaluate UnaryExpression': [Object],\r\n        'evaluate typeof undefined': [Object],\r\n        'evaluate Identifier': [Object],\r\n        'evaluate MemberExpression': [Object],\r\n        'evaluate CallExpression': [Object],\r\n        'evaluate CallExpression .replace': [Object],\r\n        'evaluate CallExpression .substr': [Object],\r\n        'evaluate CallExpression .substring': [Object],\r\n        'evaluate CallExpression .split': [Object],\r\n        'evaluate ConditionalExpression': [Object],\r\n        'evaluate ArrayExpression': [Object],\r\n        'expression Spinner': [Object],\r\n        'expression ScreenMod': [Object] },\r\n     options: undefined },\r\n  options:\r\n   { entry:\r\n      { \r\n        'index': '/Users/mac/web/src/page/index/main.js' },\r\n     output:\r\n      { publicPath: '/homework/features/model/',\r\n        path: '/Users/mac/web/dist',\r\n        filename: 'js/[name].js',\r\n        libraryTarget: 'var',\r\n        sourceMapFilename: '[file].map[query]',\r\n        hotUpdateChunkFilename: '[id].[hash].hot-update.js',\r\n        hotUpdateMainFilename: '[hash].hot-update.json',\r\n        crossOriginLoading: false,\r\n        hashFunction: 'md5',\r\n        hashDigest: 'hex',\r\n        hashDigestLength: 20,\r\n        sourcePrefix: '\\t',\r\n        devtoolLineToLine: false },\r\n     externals: { react: 'React' },\r\n     module:\r\n      { loaders: [Object],\r\n        unknownContextRequest: '.',\r\n        unknownContextRecursive: true,\r\n        unknownContextRegExp: /^\\.\\/.*$/,\r\n        unknownContextCritical: true,\r\n        exprContextRequest: '.',\r\n        exprContextRegExp: /^\\.\\/.*$/,\r\n        exprContextRecursive: true,\r\n        exprContextCritical: true,\r\n        wrappedContextRegExp: /.*/,\r\n        wrappedContextRecursive: true,\r\n        wrappedContextCritical: false },\r\n     resolve:\r\n      { extensions: [Object],\r\n        alias: [Object],\r\n        fastUnsafe: [],\r\n        packageAlias: 'browser',\r\n        modulesDirectories: [Object],\r\n        packageMains: [Object] },\r\n     plugins:\r\n      [ [Object],\r\n        [Object],\r\n        [Object],\r\n        [Object],\r\n        NoErrorsPlugin {},\r\n        [Object],\r\n        [Object] ],\r\n     devServer: { port: 8081, contentBase: './dist' },\r\n     context: '/Users/mac/web/',\r\n     watch: true,\r\n     debug: false,\r\n     devtool: false,\r\n     cache: true,\r\n     target: 'web',\r\n     node:\r\n      { console: false,\r\n        process: true,\r\n        global: true,\r\n        setImmediate: true,\r\n        __filename: 'mock',\r\n        __dirname: 'mock' },\r\n     resolveLoader:\r\n      { fastUnsafe: [],\r\n        alias: {},\r\n        modulesDirectories: [Object],\r\n        packageMains: [Object],\r\n        extensions: [Object],\r\n        moduleTemplates: [Object] },\r\n     optimize: { occurenceOrderPreferEntry: true } },\r\n  context: '/Users/mac/web/' }\r\n```\r\n\r\n除此以外，compiler还有一些如run, watch-run的方法以及compilation, normal-module-factory对象。我目前用到的，主要是compilation。其它的等下一篇有机会再说。\r\n\r\ncompiler还有compiler.plugin函数。这个相当于是插件可以进行处理的webpack的运行中的一些任务点，webpack就是完成一个又一个任务而完成整个打包构建过程的。如下图：\r\n![task](https://cloud.githubusercontent.com/assets/3348398/13768093/f46acd18-eaac-11e5-8895-a20a48e0972c.png)\r\n\r\n其它的任务点如invalid, after-plugins, after-resolvers具体可参考 [compiler对象](https://webpack.github.io/docs/plugins.html#the-compiler-instance)。\r\n\r\n\r\n### compilation对象\r\n至于compilation，它继承于compiler，所以能拿到一切compiler的内容（所以你也会看到webpack的options），而且也有plugin函数来接入任务点。在compiler.plugin('emit')任务点输出compilation，会得到大致下面的对象数据，因为实在太长，我只保留了最重要的assets部份，如下\r\n\r\n```\r\nassetsCompilation {\r\n  assets:\r\n   { 'js/index/main.js':\r\n      CachedSource {\r\n        _source: [Object],\r\n        _cachedSource: undefined,\r\n        _cachedSize: undefined,\r\n        _cachedMaps: {} } },\r\n  errors: [],\r\n  warnings: [],\r\n  children: [],\r\n  dependencyFactories:\r\n   ArrayMap {\r\n     keys:\r\n      [ [Object],\r\n        [Function: MultiEntryDependency],\r\n        [Function: SingleEntryDependency],\r\n        [Function: LoaderDependency],\r\n        [Object],\r\n        [Function: ContextElementDependency],\r\n     values:\r\n      [ NullFactory {},\r\n        [Object],\r\n        NullFactory {} ] },\r\n  dependencyTemplates:\r\n   ArrayMap {\r\n     keys:\r\n      [ [Object],\r\n        [Object],\r\n        [Object] ],\r\n     values:\r\n      [ ConstDependencyTemplate {},\r\n        RequireIncludeDependencyTemplate {},\r\n        NullDependencyTemplate {},\r\n        RequireEnsureDependencyTemplate {},\r\n        ModuleDependencyTemplateAsRequireId {},\r\n        AMDRequireDependencyTemplate {},\r\n        ModuleDependencyTemplateAsRequireId {},\r\n        AMDRequireArrayDependencyTemplate {},\r\n        ContextDependencyTemplateAsRequireCall {},\r\n        AMDRequireDependencyTemplate {},\r\n        LocalModuleDependencyTemplate {},\r\n        ModuleDependencyTemplateAsId {},\r\n        ContextDependencyTemplateAsRequireCall {},\r\n        ModuleDependencyTemplateAsId {},\r\n        ContextDependencyTemplateAsId {},\r\n        RequireResolveHeaderDependencyTemplate {},\r\n        RequireHeaderDependencyTemplate {} ] },\r\n  fileTimestamps: {},\r\n  contextTimestamps: {},\r\n  name: undefined,\r\n  _currentPluginApply: undefined,\r\n  fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57',\r\n  hash: 'f4030c2aeb811dd6c345',\r\n  fileDependencies: [ '/Users/mac/web/src/js/index/main.js' ],\r\n  contextDependencies: [],\r\n  missingDependencies: [] }\r\n```\r\nassets部份重要是因为如果你想借助webpack帮你生成文件，你需要像官方教程how to write a plugin在assets上写上对应的文件信息。\r\n\r\n除此以外，compilation.getStats()这个函数也相当重要，能得到生产文件以及chunkhash的一些信息，如下：\r\n```\r\nassets{ errors: [],\r\n  warnings: [],\r\n  version: '1.12.9',\r\n  hash: '5a5c71cb2accb8970bc3',\r\n  publicPath: 'xxxxxxxxxx',\r\n  assetsByChunkName: { 'index/main': 'js/index/index-4c0c16.js' },\r\n  assets:\r\n   [ { name: 'js/index/index-4c0c16.js',\r\n       size: 453,\r\n       chunks: [Object],\r\n       chunkNames: [Object],\r\n       emitted: undefined } ],\r\n  chunks:\r\n   [ { id: 0,\r\n       rendered: true,\r\n       initial: true,\r\n       entry: true,\r\n       extraAsync: false,\r\n       size: 221,\r\n       names: [Object],\r\n       files: [Object],\r\n       hash: '4c0c16e8af4d497b90ad',\r\n       parents: [],\r\n       origins: [Object] } ],\r\n  modules:\r\n   [ { id: 0,\r\n       identifier: 'multi index/main',\r\n       name: 'multi index/main',\r\n       index: 0,\r\n       index2: 1,\r\n       size: 28,\r\n       cacheable: true,\r\n       built: true,\r\n       optional: false,\r\n       prefetched: false,\r\n       chunks: [Object],\r\n       assets: [],\r\n       issuer: null,\r\n       profile: undefined,\r\n       failed: false,\r\n       errors: 0,\r\n       warnings: 0,\r\n       reasons: [] },\r\n     { id: 1,\r\n       identifier: '/Users/mac/web/node_modules/babel-loader/index.js?presets[]=es2015&presets[]=react!/Users/mac/web/src/js/main/index.js',\r\n       name: './src/js/index/main.js',\r\n       index: 1,\r\n       index2: 0,\r\n       size: 193,\r\n       cacheable: true,\r\n       built: true,\r\n       optional: false,\r\n       prefetched: false,\r\n       chunks: [Object],\r\n       assets: [],\r\n       issuer: 'multi index/main',\r\n       profile: undefined,\r\n       failed: false,\r\n       errors: 0,\r\n       warnings: 0,\r\n       reasons: [Object],\r\n       source: ''  // 具体文件内容}\r\n ],\r\n  filteredModules: 0,\r\n  children: [] }\r\n```\r\n这里的chunks数组里，是对应会生成的文件，以及md5之后的文件名和路径，里面还有文件对应的chunkhash（每个文件不同，但如果你使用ExtractTextPlugin将css文件独立出来的话，它会与require它的js入口文件共享相同的chunkhash），而assets.hash则是统一的hash，对每个文件都一样。值得关注的是chunks里的每个文件，都有source这一项目，提供给开发者直接拿到源文件内容（主要是js，如果是css且使用ExtractTextPlugin，则请自行打印出来参考）。\r\n\r\n\r\n## 例子\r\n接下来，会以最近我写的一个插件[html-res-webpack-plugin](https://github.com/lcxfs1991/html-res-webpack-plugin/)作为引子，来介绍基本的写插件原理。插件的逻辑就写在index.js里。\r\n\r\n首先，将用户输入的参数在定好的函数中处理，HtmlResWebpackPlugin。\r\n```\r\nfunction HtmlResWebpackPlugin(opt) {\r\n    // 进行参数的处理\r\n}\r\n```\r\n\r\n然后，新增apply函数，在里面写好插件需要切入的webpack任务点。目前HtmlResWebpackPlugin插件只用到emit这个任务点，其它几个仅作为演示。\r\n```\r\nHtmlResWebpackPlugin.prototype.apply = function(compiler, callback) {\r\n            // some code here\r\n    compiler.plugin(\"make\", function(compilation, callback) {\r\n\t    // some code here\r\n\t    callback();  // 异步回调，跟gulp类似\r\n    });\r\n\r\n    compiler.plugin(\"emit\", function(compilation, callback) {\r\n\t     // 对即将生成的文件进行处理\r\n             _this.options.basename = _this.addFileToWebpackAsset(compilation, _this.options.template, true);\r\n\r\n\t    if (_this.options.favicon) {\r\n\t    \t_this.options.faviconBaseName = _this.addFileToWebpackAsset(compilation, _this.options.favicon);\r\n\t    }\r\n\r\n\t    _this.findAssets(compilation);\r\n\r\n\t    if (!_this.options.isWatch) {\r\n\t    \t_this.processHashFormat();\r\n\t    }\r\n\r\n\t    _this.addAssets(compilation);\r\n\r\n            // other codes\r\n\t    callback();\r\n\t});\r\n}\r\n```\r\n\r\n第三步，调用addFileToWebpackAsset方法，写compilation.assets，借助webpack生成html文件（项目里最新版本新增了添加favicon的功能，因此有2个addFileToWebpackAsset的方法）。这里的核心就是本文一开始那段给compilation.assets添加source和size的代码。\r\n\r\n第四步，在开发模式下(isWatch = true)，直接生成html，但在生产模式下(isWatch = true），插件会开始对静态资源（js，css）进行md5或者内联。项目用了```_this.options.isWatch```去进行判断。\r\n\r\n第五步，调用findAssets方法是为了通过compilation.getStats()拿到的数据，去匹配对应的静态资源，还有找到对应的哈希(是chunkhash还是hash)。\r\n\r\n最六步，调用addAssets方法，对静态资源分别做内联或者md5文件处理。内联资源的函数是inlineRes,你会看到我使用了compilation.assets[hashFile].source() 及 compilation.assets[hashFile].children[1]._value。前者是针对于js的，后者是针对使用了ExtractTextPlugin的css资源。\r\n\r\n最后一步，即是内联和md5完成后，再更新一下compilation.assets中对应生成html的source内容，才能正确地生成内联和md5后的内容。这一步在addAssets方法里有一处代码如下：\r\n```\r\ncompilation.assets[this.options.basename].source = function() {\r\n    return htmlContent;\r\n};\r\n```\r\n\r\n## 后记\r\n有兴趣可以试用一下[html-res-webpack-plugin](https://github.com/lcxfs1991/html-res-webpack-plugin/)这个插件（为什么要写一个新的html生成插件，我在readme里写了，此处不赘述），看看有哪些用得不爽之处。目前只是第一版，还不适合用于生产环境。希望第二版的时候能适用于更多的场景，以及性能更好。到是，我也会写第二篇插件开发文章，将本文还没提到的地方一一补充完整。也欢迎大家在这里发贴，或者指出本人的谬误之处。"},"comment":{"url":"https://api.github.com/repos/lcxfs1991/blog/issues/comments/235548365","html_url":"https://github.com/lcxfs1991/blog/issues/1#issuecomment-235548365","issue_url":"https://api.github.com/repos/lcxfs1991/blog/issues/1","id":235548365,"user":{"login":"fengluhome","id":4446509,"avatar_url":"https://avatars.githubusercontent.com/u/4446509?v=3","gravatar_id":"","url":"https://api.github.com/users/fengluhome","html_url":"https://github.com/fengluhome","followers_url":"https://api.github.com/users/fengluhome/followers","following_url":"https://api.github.com/users/fengluhome/following{/other_user}","gists_url":"https://api.github.com/users/fengluhome/gists{/gist_id}","starred_url":"https://api.github.com/users/fengluhome/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fengluhome/subscriptions","organizations_url":"https://api.github.com/users/fengluhome/orgs","repos_url":"https://api.github.com/users/fengluhome/repos","events_url":"https://api.github.com/users/fengluhome/events{/privacy}","received_events_url":"https://api.github.com/users/fengluhome/received_events","type":"User","site_admin":false},"created_at":"2016-07-27T10:31:02Z","updated_at":"2016-07-27T10:31:02Z","body":"请问编写webpack 插件，如何更好的断点调试，请问有什么好的办法？"}}
{"actor":{"display_login":"fengluhome"}}