如何寫一個webpack插件(一)

原文連接css

前言

最近因爲用着html-webpack-plugin以爲很不爽,因而乎想本身動手寫一個插件。原覺得像gulp插件同樣半天上手一天寫完,但使人鬱悶的是徹底找不到相關的文章。一進官方文檔倒是被嚇傻了。首先是進入how to write a plugin看了一頁簡單的介紹。而後教程會告訴你,你須要去了解compiler和compilation這兩個對象,才能更好地寫webpack的插件,而後做者給了github的連接給你,讓你去看源代碼,我暈。不過幸虧最後給了一個plugins的API文檔,才讓我開發的過程當中稍微有點頭緒。html

how to write a plugin這個教程仍是能夠好好看看的,尤爲是那個simple example,它會教你在compilation的emit事件或以前,將你須要生成的文件放到webpack的compilation.assets裏,這樣就能夠藉助webpack的力量幫你生成文件,而不須要本身手動去寫fs.writeFileSync。node

主要就是這段代碼react

compilation.assets['filelist.md'] = {
      source: function() {
        return filelist;
      },
      size: function() {
        return filelist.length;
      }
};

基本特性介紹

首先,定義一個函數func,用戶設置的options基本就在這裏處理。webpack

其次,須要設一個func.prototype.apply函數。這個函數是提供給webpack運行時調用的。webpack會在這裏注入compiler對象。git

輸出complier對象,你會看到這一長串的內容,初步一看,我看出了兩大類(有補充的能夠告訴我)。一個webpack運行時的參數,例如_plugins,這些數組裏的函數應該是webpack內置的函數,用於在compiltion,this-compilation和should-emit事件觸發時調用的。另外一個是用戶寫在webpack.config.js裏的參數。隱約以爲這裏好多將來均可能會是webpack暴露給用戶的接口,使webpack的定製化功能更強大。github

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。其它的等下一篇有機會再說。web

對比起compiler還有compiler.plugin函數。這個至關因而插件能夠進行處理的webpack的運行中的一些任務點,webpack就是完成一個又一個任務而完成整個打包構建過程的。如make是最開始的起點, complie就是編譯任務點,after-complie是編譯完成,emit是即將準備生成文件,after-emit是生成文件以後等等,前面幾個都是比較生動形象的任務點。express

至於compilation,它繼承於compiler,因此能拿到一切compiler的內容(因此你也會看到webpack的options),並且也有plugin函數來接入任務點。在compiler.plugin('emit')任務點輸出compilation,會獲得大體下面的對象數據,由於實在太長,我只保留了最重要的assets部份:json

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。

而後,新增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裏寫了,此處不贅述),看看有哪些用得不爽之處。目前只是初版,還不適合用於生產環境。但願第二版的時候能適用於更多的場景,以及性能更好。到是,我也會寫第二篇插件開發文章,將本文還沒提到的地方一一補充完整。也歡迎你們在這裏發貼,或者指出本人的謬誤之處。

相關文章
相關標籤/搜索