New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何写一个webpack插件(一) #1

Open
lcxfs1991 opened this Issue Jan 9, 2016 · 26 comments

Comments

Projects
None yet
@lcxfs1991
Owner

lcxfs1991 commented Jan 9, 2016

前言

最近由于用着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对象。

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就是完成一个又一个任务而完成整个打包构建过程的。如下图:
task

其它的任务点如invalid, after-plugins, after-resolvers具体可参考 compiler对象

compilation对象

至于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&presets[]=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。

function HtmlResWebpackPlugin(opt) {
    // 进行参数的处理
}

然后,新增apply函数,在里面写好插件需要切入的webpack任务点。目前HtmlResWebpackPlugin插件只用到emit这个任务点,其它几个仅作为演示。

HtmlResWebpackPlugin.prototype.apply = function(compiler, callback) {
            // some code here
    compiler.plugin("make", function(compilation, callback) {
        // some code here
        callback();  // 异步回调,跟gulp类似
    });

    compiler.plugin("emit", function(compilation, callback) {
         // 对即将生成的文件进行处理
             _this.options.basename = _this.addFileToWebpackAsset(compilation, _this.options.template, true);

        if (_this.options.favicon) {
            _this.options.faviconBaseName = _this.addFileToWebpackAsset(compilation, _this.options.favicon);
        }

        _this.findAssets(compilation);

        if (!_this.options.isWatch) {
            _this.processHashFormat();
        }

        _this.addAssets(compilation);

            // other codes
        callback();
    });
}

第三步,调用addFileToWebpackAsset方法,写compilation.assets,借助webpack生成html文件(项目里最新版本新增了添加favicon的功能,因此有2个addFileToWebpackAsset的方法)。这里的核心就是本文一开始那段给compilation.assets添加source和size的代码。

第四步,在开发模式下(isWatch = true),直接生成html,但在生产模式下(isWatch = true),插件会开始对静态资源(js,css)进行md5或者内联。项目用了_this.options.isWatch去进行判断。

第五步,调用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后的内容。这一步在addAssets方法里有一处代码如下:

compilation.assets[this.options.basename].source = function() {
    return htmlContent;
};

后记

有兴趣可以试用一下html-res-webpack-plugin这个插件(为什么要写一个新的html生成插件,我在readme里写了,此处不赘述),看看有哪些用得不爽之处。目前只是第一版,还不适合用于生产环境。希望第二版的时候能适用于更多的场景,以及性能更好。到是,我也会写第二篇插件开发文章,将本文还没提到的地方一一补充完整。也欢迎大家在这里发贴,或者指出本人的谬误之处。

@lcxfs1991 lcxfs1991 changed the title from test to 如何写一个webpack插件(一) Jan 9, 2016

@tiger02

This comment has been minimized.

tiger02 commented Jan 11, 2016

讲得不错,对新开发一个webpack插件有启发

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jan 11, 2016

:) 。第二篇得探索一下如何利用webpack的缓存机制。

@bluemsn

This comment has been minimized.

bluemsn commented Apr 19, 2016

mark

@Phlicess

This comment has been minimized.

Phlicess commented Apr 28, 2016

多谢博主分享,刚刚开始学webpack,先mark下

@8427003

This comment has been minimized.

8427003 commented May 30, 2016

博主,能概括说下,loader 和 plugin 有什么区别吗,或者他们能力范围有什么不同?

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented May 30, 2016

loader一般是用来处理文件语言的编译,例如sass -> css, babel转es6->es5。plugin的功能则强大的得多,能调用许多webpack提供的能力,如果处理构建流程中的事件,例如丑化代码、去重、code splitting都是通过plugin去处理。

@e-cloud

This comment has been minimized.

e-cloud commented Jun 5, 2016

@lcxfs1991 ,请问你了解 这个issue 怎么破?

另外,你那个插件生成的模块好像不会经过匹配loader的处理?

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jun 5, 2016

@e-cloud 暂时没有,后续加上

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jun 5, 2016

@e-cloud 可以参考一下官方的 extract text plugin

@JonneyYan

This comment has been minimized.

JonneyYan commented Jul 6, 2016

请教一个问题,我现在需要吧每个chunk上传到公司CDN静床,上传之后会返回一个没有规则但唯一的URL。请问如何load这些chunk?
webpack的output.publicPath是指定chunk的根目录,但我们的CDN静床没有目录的概念。

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jul 6, 2016

@JonneyYan 能给具体例子吗

@JonneyYan

This comment has been minimized.

JonneyYan commented Jul 6, 2016

比如说:用Code Split分出来chunk.1.js、chunk.2.js两个包,output.publicPath = 'http://cdn.a.com/',bundle.js会在app运行时异步加载http://cdn.a.com/chunk.1.js和http://cdn.a.com/chunk.2.js

但是我需要的是把chunk.1.js、chunk.2.js上传到一个网站上,这个网站会返回这两个文件的URL(http://abc.com/sajdhkjhf.js和http://abc.com/askjdhj12kj32.j,文件名没有规则),如何设置才能让bundle.js加载这两个地址的文件?

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jul 6, 2016

这样子的 话,可能你得在拿到url之后,通过插件将加载地址写进去

@fengluhome

This comment has been minimized.

fengluhome commented Jul 27, 2016

请问编写webpack 插件,如何更好的断点调试,请问有什么好的办法?

@e-cloud

This comment has been minimized.

e-cloud commented Jul 27, 2016

调试工具吗?node-inspector, webstorm, vscode
还是其他因素?

@jimole775

This comment has been minimized.

jimole775 commented Oct 21, 2016

marks,read it when needed

@Tk-archer

This comment has been minimized.

Tk-archer commented Nov 17, 2016

@lcxfs1991
你好,我最近也遇到一个比较难于处理的问题。

简单来说,在我的项目中,前端需要输出 .pug 模板文件给后端,由后端传入一些变量去动态渲染出html。
并且,前端输出的 .pug 文件需要替换好 webpack 编译输出的 js/css 文件的访问路径,同时保持整个模板文件目录的结构。

这一点我通过在emit事件中读取 .pug 文件,匹配相应的js/css文件,复制加入到assets当中,勉强解决了。

不过还有的问题就是,模板文件实际上并没有经过 webpack 去处理资源的依赖,这就导致了模板文件中存在的图片资源的依赖无法解析并交由 webpack 去内联或者添加 hash

我想请教一下,要出如何编写插件,才能在编译之前将 .pug 文件加入到 webpack 的处理流程中,图片资源的依赖关系,然后在编译之后读取出来?或者说有什么更好的处理方法?

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Nov 19, 2016

@Tk-archer 可以参考html-webpack-plugin的源码,里面有做处理。留意一下它源码里vm的作用

@Tk-archer

This comment has been minimized.

Tk-archer commented Nov 22, 2016

@lcxfs1991
感谢回复,已经解决了。

我发现其实就算是通过 html-webpack-plugin 类似的方式将模板加入 webpack 处理模块中,还是不够的,还需要一个 loader 去触发解析过程,不然的话只会源文件返回。可以试试直接用 html-webpack-plugin 通过模板文件而不添加相应的 loader 就能看得见我说的这个行为。

所以我最终的解决方案是,既然已经要遍历模板文件了,索性直接自己解析,因为实际上要解析的依赖只有 img 的依赖,还是相对单一好解析,只要吧路径解析出来,剩下的其实就是把 url-loader 的代码搬到这里处理一边,该内联的内联,该给hash的给hash。

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Nov 22, 2016

恩,html-webpack-plugin有调用loader的功能。

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jan 19, 2017

@Tk-archer 我参考了html-webpack-plugin的代码,可以提取image了,但缺点是会经过各种loader的处理,有时候可能不可控。

@henryzp

This comment has been minimized.

henryzp commented Mar 20, 2017

mark

@jschyz

This comment has been minimized.

jschyz commented Mar 20, 2017

关于webpack的 Compiler plugin的任务点是 compile -> make -> ...
你的图列有误!

详见 Compiler.js

@lkdghzh

This comment has been minimized.

lkdghzh commented Apr 13, 2017

mark

@zhaihaoran

This comment has been minimized.

zhaihaoran commented Jun 6, 2018

您好,问一下有没有什么方法或工具可以在插件代码里打断点调试?是只能通过console.log或table来查看吗?

@lcxfs1991

This comment has been minimized.

Owner

lcxfs1991 commented Jun 6, 2018

@zhaihaoran vscode给node打断点可以试下

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment