{"id":17640926,"url":"https://github.com/beace/custom-plugin","last_synced_at":"2026-05-04T03:31:32.358Z","repository":{"id":98535722,"uuid":"117928471","full_name":"Beace/custom-plugin","owner":"Beace","description":"webpack custom plugin","archived":false,"fork":false,"pushed_at":"2018-06-23T16:21:49.000Z","size":6311,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-05T07:31:18.696Z","etag":null,"topics":["webpack-plugin","webpack3"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Beace.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-18T03:43:29.000Z","updated_at":"2018-06-23T16:21:50.000Z","dependencies_parsed_at":"2023-05-29T09:15:27.327Z","dependency_job_id":null,"html_url":"https://github.com/Beace/custom-plugin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Beace%2Fcustom-plugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Beace%2Fcustom-plugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Beace%2Fcustom-plugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Beace%2Fcustom-plugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Beace","download_url":"https://codeload.github.com/Beace/custom-plugin/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246279871,"owners_count":20752027,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["webpack-plugin","webpack3"],"created_at":"2024-10-23T06:24:46.913Z","updated_at":"2026-05-04T03:31:27.336Z","avatar_url":"https://github.com/Beace.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 引子\n想要了解webpack plugin如何编写，首先要了解其应用场景和作用。\n\n可以先浏览这三篇文章\n\n\u003e [how-to-write-a-plugin](https://github.com/webpack/docs/wiki/how-to-write-a-plugin)\n\n\u003e [compiler API](https://webpack.js.org/api/compiler/)\n\n\u003e [plugins API](https://github.com/webpack/docs/wiki/plugins)\n\n\n除此之外，在这里我和`webpack loader`进行了简单的对比。\n\n\n\u003c!--more--\u003e\n### plugin \u0026 loader\n\n#### plugin\n\n顾名思义，webpack plugin是作为webpack的一个插件机制存在，将webpack提供的处理方法暴露给第三方（开发者）来开发。在整个项目架构中，往往起宏观上的作用。例如`HtmlWebpackPlugin`，修改一些文件，inject一些用户的资源，这些资源往往是经过loader处理过的资源，比如`jsx`文件，`css`文件。\n\n#### loader\n而loader用于对开发者源代码的转换，功能而言，跟webpack本身并没有强耦合的关系。例如，强大的babel-loader可以使用浏览器暂不支持的`JavaScript`语法（糖），`css-loader`和`styles-loader`用来处理你的`css`。\n\n总之，Loader的这些工作不需要开发者去干涉，只需相应配置全权交个loader去处理。而plugin往往需要用户先预备好已经有的资源，再去对资源进行宏观上的操作，并不会在内容细节上处理。\n\n## 场景的明确\n我们需要明确一些plugin场景来进行实际开发的模拟。比如，抽离公共模块（CommonsChunkPlugin），控制模块的输出方式，或者输出内容（这里可能体现比较直观的是UglifyJsPlugin），复制一些为经过webpack处理的静态文件（copyWebpackPlugin）。\n\n## Compiler and Compilation\n在了解生命周期之前，必须要了解`Compiler and Compilation`两个概念，我通常会翻译成`编译器`和`编译集合`。\n\n#### Compiler(编译器)\n翻译为`编译器`，是因为往往编译器在开发者的眼中是整个源代码所处的编译环境（预设环境），是一个静态场景。webpack通过Compiler提供了webpack配置内容的所有配置项和插件相关的调用函数，在这里，你可以随意获得你想要的某个配置，并且根据相应的配置书写相应的plugin代码逻辑。下面展示了compiler中用到的一些生命周期和有关webpack配置的代码。\n\n\n```js\n_plugins: { 'before-run': [ [Function] ], done: [ [Function] ] },\noptions:\n   { entry: './index.js',\n     output:\n      { path: '/Users/beace/Documents/beace/github/webpack/custom-plugins/first-plugin',\n        filename: 'bundle.js',\n        chunkFilename: '[id].bundle.js',\n        library: '',\n        hotUpdateFunction: 'webpackHotUpdate',\n        jsonpFunction: 'webpackJsonp',\n        libraryTarget: 'var',\n        sourceMapFilename: '[file].map[query]',\n        hotUpdateChunkFilename: '[id].[hash].hot-update.js',\n        hotUpdateMainFilename: '[hash].hot-update.json',\n        crossOriginLoading: false,\n        chunkLoadTimeout: 120000,\n        hashFunction: 'md5',\n        hashDigest: 'hex',\n        hashDigestLength: 20,\n        devtoolLineToLine: false,\n        strictModuleExceptionHandling: false },\n     plugins: [ HelloWorldPlugin {}, MyPlugin {} ],\n     context: '/Users/beace/Documents/beace/github/webpack/custom-plugins/first-plugin',\n     devtool: false,\n     cache: true,\n     target: 'web',\n     module:\n      { unknownContextRequest: '.',\n        unknownContextRegExp: false,\n        unknownContextRecursive: true,\n        unknownContextCritical: true,\n        exprContextRequest: '.',\n        exprContextRegExp: false,\n        exprContextRecursive: true,\n        exprContextCritical: true,\n        wrappedContextRegExp: /.*/,\n        wrappedContextRecursive: true,\n        wrappedContextCritical: false,\n        strictExportPresence: false,\n        strictThisContextOnImports: false,\n        unsafeCache: true },\n     node:\n      { console: false,\n        process: true,\n        global: true,\n        Buffer: true,\n        setImmediate: true,\n        __filename: 'mock',\n        __dirname: 'mock' },\n     performance: { maxAssetSize: 250000, maxEntrypointSize: 250000, hints: false },\n     resolve:\n      { unsafeCache: true,\n        modules: [Array],\n        extensions: [Array],\n        mainFiles: [Array],\n        aliasFields: [Array],\n        mainFields: [Array],\n        cacheWithContext: false },\n     resolveLoader:\n      { unsafeCache: true,\n        mainFields: [Array],\n        extensions: [Array],\n        mainFiles: [Array],\n        cacheWithContext: false } },\n  context: '/Users/beace/Documents/beace/github/webpack/custom-plugins/first-plugin',\n}\n```\n\n#### Compilation(编译集合)\n\nCompilation虽然继承自Compiler，但是对于本身作用来讲，因为他包含了chunks，modules，cache，assets，是动态的资源集合。动态的原因是，在某个编译阶段，产生的编译资源是不相同的。\n\n\u003e 编译会显示有关模块资源，编译资源，更改的文件以及监视的依赖项当前状态的信息。编译还提供了许多插件可以选择执行自定义操作的回调点。\n\n每一个版本执行的编辑逻辑（开发者），决定了上述特点。下面选取了部分关于chunks和assets中的内容\n\n```js\nchunks:\n   [ Chunk {\n       id: 0,\n       ids: [Array],\n       debugId: 1000,\n       name: 'main',\n       _modules: [SortableSet],\n       entrypoints: [Array],\n       chunks: [],\n       parents: [],\n       blocks: [],\n       origins: [Array],\n       files: [Array],\n       rendered: true,\n       entryModule: [NormalModule],\n       hash: 'bfe5f97a4642c50a5286f6a28486186a',\n       renderedHash: 'bfe5f97a4642c50a5286' } ],\n\n{ 'bundle.js':\n   CachedSource {\n     _source: ConcatSource { children: [Array] },\n     _cachedSource: undefined,\n     _cachedSize: undefined,\n     _cachedMaps: {},\n     node: [Function],\n     listMap: [Function] } }\n```\n\n## 生命周期\n\n#### 简历一个简单的项目\n\n通过以下简单的配置，我将一个`index.js`简单的进行webpack打包，输出`bundle.js`。并在根目录下创建`my-plugin.js`文件，作为即将开发的插件。代码如下。\n\n```js\nconst path = require('path');\nconst webpack = require('webpack');\nconst MyPlugin = require('./my-plugin');\n\nmodule.exports = {\n  entry: './index.js',\n  output: {\n    path: path.resolve(__dirname),\n    filename: 'bundle.js',\n  },\n  plugins: [\n    new MyPlugin({ options: true }),\n  ]\n}\n```\n\n根据webpack的要求，插件必须要在其原型上创建apply对象。\n\n\u003e 因为当webpack命令执行时，插件将被创建，而webpack将通过调用apply来安装插件，并将引用传递给webpack编译对象。\n\n反观表现，通过创建apply对象以及apply的参数，可以调用webpack底层的方法。在my-plugin.js中写入\n\n```js\nfunction MyPlugin(options) {}\n\nMyPlugin.prototype.apply = function(compiler) {}\n```\n\n#### 从执行顺序看生命周期\n\n如果非常粗暴的将plugin的几个关键的生命周期输出出来，执行顺序是将会是这样的\n\n```js\n\t// 1\n  compiler.plugin(\"compile\", function(params) {\n    console.log(\"The compile is starting to compile...\", params);\n  });\n  // 2\n  compiler.plugin(\"compilation\", function(compilation, params) {\n    console.log(\"The compile is starting a new compilation...\");\n    // 4\n    compilation.plugin(\"optimize\", function() {\n      console.log(\"The compilation is starting to optimize file...\");\n    });\n  });\n  // 3\n  compiler.plugin(\"make\", function(compiler, callback){\n    console.log(\"the compile is making file...\");\n    callback();\n  });\n  // 5\n  compiler.plugin(\"after-compile\", function(compilation) {\n    console.log(\"The compile has aleardy compiled\");\n  });\n\t// 6\n\tcompiler.plugin(\"emit\", function(compilation, callback) {\n    console.log(\"The compilation is going to emit files...\");\n    callback();\n  });\n\t// 7\n\tcompiler.plugin('after-emit', function(compilation) {\n    console.log('The compliation has aleardy emitted');\n  })\n```\n\n\n代码的注释，代表了执行的顺序，可以看下命令行中的输出\n\n![webpack](https://images-manager.oss-cn-shanghai.aliyuncs.com/2018/webpack/webpack-1.png)\n\n从上述代码的执行顺序来看，plugin的生命周期如下：\n\n1. `Compile` 开始进入编译环境，开始编译\n2. `Compilation` 即将产生第一个版本\n3. `make`任务开始\n4. `optimize`作为`Compilation`的回调方法，优化编译，在`Compilation`回调函数中可以为每一个新的编译绑定回调。\n5. `after-compile`编译完成\n6. `emit`准备生成文件，开始释放生成的资源，**最后一次添加资源到资源集合的机会**\n7. `after-emit`文件生成之后，编译器释放资源\n\n\n#### 从源码中看生命周期\n\n咦，好像漏了两条，当编译完成时，可以看到命令行里面并没有文件的输出，回去查看项目中的代码，也并没有`bundle.js`文件。6、7步到底执行了么？\n\n答案当然是没有执行。因为没有看到资源释放的结果。\n\n让我们在源码中一探究竟。找到Compile所在的源码。\n\n```js\ncompile(callback) {\n\t\tconst params = this.newCompilationParams();\n\t\tthis.applyPluginsAsync(\"before-compile\", params, err =\u003e {\n\t\t\tif(err) return callback(err);\n\t\t\t// 1\n\t\t\tthis.applyPlugins(\"compile\", params);\n\t\t\t// 2\n\t\t\tconst compilation = this.newCompilation(params);\n\t\t\t// 3\n\t\t\tthis.applyPluginsParallel(\"make\", compilation, err =\u003e {\n\t\t\t\tif(err) return callback(err);\n\n\t\t\t\tcompilation.finish();\n\t\t\t\t// 4\n\t\t\t\tcompilation.seal(err =\u003e {\n\t\t\t\t\tif(err) return callback(err);\n\t\t\t\t\t// 5\n\t\t\t\t\tthis.applyPluginsAsync(\"after-compile\", compilation, err =\u003e {\n\t\t\t\t\t\tif(err) return callback(err);\n\n\t\t\t\t\t\treturn callback(null, compilation);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t}\n```\n\n\n很明显，当编译完成时，webpack Seal 资源完毕后直接将callback return，所以当我们在调用`after-compile\t`没有进行任何处理，阻止了接下来的return。将my-plugin.js中的代码注释掉`after-compile`这一步骤或者添加新的参数callback并执行。\n\n```js\n// my-plugin.js\ncompiler.plugin(\"after-compile\", function(compilation, callback) {\n    console.log(\"The compile has aleardy compiled\");\n    callback();\n  });\n```\n\n这时再运行webpack，命令行中可以看到输出了`The compilation is going to emit files`,并且输出了`bundle.js`。\n\n![webpack](https://images-manager.oss-cn-shanghai.aliyuncs.com/2018/webpack/webpack-2.png)\n\n## 编写自己的插件\n\n上面截图可以看到，Hash上面的一行输出`All compilers have done.`,其实这也是在webpack plugin的生命周期的范围，`done`是所有工作结束后，会执行的最后一个步骤。并且，当webpack plugin watch到某个过程出错的时候，也会执行`done`。如以下源代码，可以看到每次执行错误之后，都会走`done`\t流程。\n\n\n```js\nthis.compiler.applyPluginsAsync(\"watch-run\", this, err =\u003e {\n\t\t\tif(err) return this._done(err);\n\t\t\tconst onCompiled = (err, compilation) =\u003e {\n\t\t\t\tif(err) return this._done(err);\n\t\t\t\tif(this.invalid) return this._done();\n\n\t\t\t\tif(this.compiler.applyPluginsBailResult(\"should-emit\", compilation) === false) {\n\t\t\t\t\treturn this._done(null, compilation);\n\t\t\t\t}\n\n\t\t\t\tthis.compiler.emitAssets(compilation, err =\u003e {\n\t\t\t\t\tif(err) return this._done(err);\n\t\t\t\t\tif(this.invalid) return this._done();\n\n\t\t\t\t\tthis.compiler.emitRecords(err =\u003e {\n\t\t\t\t\t\tif(err) return this._done(err);\n\n\t\t\t\t\t\tif(compilation.applyPluginsBailResult(\"need-additional-pass\")) {\n\t\t\t\t\t\t\tcompilation.needAdditionalPass = true;\n\n\t\t\t\t\t\t\tconst stats = new Stats(compilation);\n\t\t\t\t\t\t\tstats.startTime = this.startTime;\n\t\t\t\t\t\t\tstats.endTime = Date.now();\n\t\t\t\t\t\t\tthis.compiler.applyPlugins(\"done\", stats);\n\n\t\t\t\t\t\t\tthis.compiler.applyPluginsAsync(\"additional-pass\", err =\u003e {\n\t\t\t\t\t\t\t\tif(err) return this._done(err);\n\t\t\t\t\t\t\t\tthis.compiler.compile(onCompiled);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn this._done(null, compilation);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t};\n\t\t\tthis.compiler.compile(onCompiled);\n\t\t});\n```\n\n\n因此，为了简单而言，我们此次编写的插件也是基于`done`来进行。\n\n\n#### 编写plugin\n\n接下来将要编写一个在生成bundle.js文件之后，在第一行添加时间注释，在最后一行添加自己姓名注释，并重新输出`bundle.js`。\n\n\n```js\ncompiler.plugin(\"done\", function(stats) {\n    console.log('All compilers have done.');\n    const fileData = fs.readFileSync(path.join(path.resolve(__dirname), 'bundle.js'), {encoding: 'utf-8'});\n    console.log(fileData);\n    const prefix = '/*2018*/';\n    const author = '/* ——By Beace Lee */';\n    const finalFileData = `${prefix}\\n${fileData}\\n${author}`;\n    fs.writeFileSync(\n      path.join(path.resolve(__dirname), 'bundle.js'),\n      finalFileData\n    );\n  })\n```\n\n\n通过以上代码可以看出，在`done`这个步骤中，通过读取`emit`的`bundle.js`文件（因为这个时候资源已经释放，可以直接使用资源），**以utf-8的格式读取**，读取完毕后在整个字符串的前后添加两行注释并换行，再写到最终文件里。\n\n```js\n\n// bundle.js\n/*2018*/\n...\n/* 0 */\n/***/ (function(module, exports) {\n...\nconsole.log('this is a entry js file');\n...\n/***/ })\n/* ——By Beace Lee */\n```\n\n## 总结\n\n此种方式，其实是调用了node的fs的API去实现，看起来除了生命周期之外，并没有和webpack plugin有什么太大关系，我们其实是操作了文件，当有大量文件存在的时候，该插件显得捉襟见肘。\n\n除此之外，前面说过可以操作compiler的assets集合， 暂时写到这里，下回再聊。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeace%2Fcustom-plugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeace%2Fcustom-plugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeace%2Fcustom-plugin/lists"}