天天看點

「webpack系列」教你寫一個webpack的loader

1.本章概述

通過前面章節的内容,你對于 Webpack 的基礎知識應該有了一個總體的了解。下面我将以一個具體的執行個體來教你如何寫一個 Webpack 的 Loader。這個 Loader 本身複用性并不高,他是我在開發中遇到的一個實際問題。通過這個例子的論述,我想你對于如何寫一個 Webpack 的 Loader應該會有一個整體的把握。下面我們開始本章節的内容

2.寫一個Webpack的Loader

假如我們如下的markdown檔案,檔案主要内容為如下(完整内容點選這裡):

import { Button } from 'antd';
ReactDOM.render(
  <div>
    <Button type="primary">Primary</Button>
    <Button>Default</Button>
    <Button type="dashed">Dashed</Button>
    <Button type="danger">Danger</Button>
  </div>
, mountNode);           

此時,我們希望使用 Webpack 的 Loader來加載這個markdown檔案的内容。那麼很顯然,我們就是要寫一個相應的Loader,比如我們在webpack.config.js中添加如下的配置:

module.exports = {
  module:{
    rule:[
     {
          test: /\.md$/,
          loaders: [
            require.resolve("babel-loader"),
            require.resolve("./markdownDemoLoader.js")
          ]
        }]
  }
}           

其中markdownDemoLoader.js就是我們需要完成的 Webpack 的 Loader。在這個Loader中我們有如下的代碼:

const loaderUtils =  require('loader-utils');
const Grob = require('grob-files');
const {p2jsonml} = require('./utils/pc2jsonml');
const transformer = require('./utils/transformer');
const Smangle = require('string-mangle');
const generator = require('babel-generator').default;
const pwd = process.cwd();
const fs = require('fs');
const path = require('path');
const util = require('util');
/**
 *第一個參數是markdown的内容
 */
module.exports = function markdown2htmlPreview (content){
  //緩存該子產品
  if (this.cacheable) {
    this.cacheable();
  }
  const loaderIndex = this.loaderIndex;
  //列印this可以得到所有的資訊,這裡得到md處理檔案的loader數組中,目前loader所在的下标
  const query = loaderUtils.getOptions(this);
  const lang = query&&query.lang || 'react-demo';
  //擷取Loader的query字段
  const processedjsonml=Smangle.stringify(p2jsonml(content));
  //得到jsonml
  const astProcessed = `module.exports = ${processedjsonml}`;
  //每一個Loader導出的内容都是module.exports
  const res = transformer(astProcessed,lang);
  //将得到的jsonml内容進一步的處理
  const inputAst = res.inputAst;
  const imports = res.imports;
  for (let k = 0; k < imports.length; k++) {
    inputAst.program.body.unshift(imports[k]);
  const code = generator(inputAst, null, content).code;
  //回到ES6代碼
  const processedCode= 'const React =  require(\'react\');\n' +
        'const ReactDOM = require(\'react-dom\');\n'+
        code;
  return processedCode;
  }
}           

我們現在看看Loader編寫中常用的方法:

if (this.cacheable) {
    this.cacheable();
  }           

我們知道Loader加載的結果預設是緩存的,如果你不想緩存可以使用this.cacheable(false);去阻止緩存。一個可以緩存的Loader必須滿足一定的條件,即當輸入和子產品依賴關系沒有發生變化的情況下,輸出預設是确定的。也就是說,這個子產品除了通過this.addDependency添加的子產品依賴以外沒有任何其他的子產品依賴。

const loaderIndex = this.loaderIndex;           

這個表示目前Loader在加載特定檔案的時候所在的下标。

在上面這個Loader中,我們首先原樣傳入markdown檔案的内容,然後将它轉化為jsonml,我們看看上面的jsonml.js的内容:

const markTwain = require('mark-twain');
const path = require('path');
function p2jsonml(fileContent){
  const markdown = markTwain(fileContent);
  return markdown;
};
module.exports = {
  p2jsonml
}           

轉化為jsonml格式以後,我們将會得到如下的内容:

module.exports = { "content": [ "article", [ "h3", "1.mark-twain解析出來的無法解析成為ast" ], [ "pre", { "lang": "jsx" }, [ "code", "import { Button } from 'antd';\nReactDOM.render(\n

\n <Button type="primary" shape="circle" icon="search" />\n <Button type="primary" icon="search">Search\n <Button shape="circle" icon="search" />\n <Button icon="search">Search\n 
\n <Button type="ghost" shape="circle" icon="search" />\n <Button type="ghost" icon="search">Search\n <Button type="dashed" shape="circle" icon="search" />\n <Button type="dashed" icon="search">Search\n
,\n mountNode\n);" ] ] ], "meta": {
} }           

但是這并不是我們希望的結果,我們需要繼續如下的處理,其中目的隻有一個:将我們的ReactDOM.render中第一個參數的值放到一個獨立的函數中,函數的名字為jsonmlReactLoader:

const babylon = require('babylon');
const types = require('babel-types');
const traverse = require('babel-traverse').default;
function parser(content) {
  return babylon.parse(content, {
    sourceType: 'module',
    plugins: [
      'jsx',
      'flow',
      'asyncFunctions',
      'classConstructorCall',
      'doExpressions',
      'trailingFunctionCommas',
      'objectRestSpread',
      'decorators',
      'classProperties',
      'exportExtensions',
      'exponentiationOperator',
      'asyncGenerators',
      'functionBind',
      'functionSent',
    ],
  });
}
module.exports = function transformer(content, lang) {
  let imports = [];
  const inputAst = parser(content);
  traverse(inputAst, {
    ArrayExpression: function(path) {
      const node = path.node;
      const firstItem = node.elements[0];
      //tagName
      const secondItem = node.elements[1];
      //attributes or child element
      let renderReturn;
      if (firstItem &&
        firstItem.type === 'StringLiteral' &&
        firstItem.value === 'pre' &&
        secondItem.properties[0].value.value === lang) {
        let codeNode = node.elements[2].elements[1];
        let code = codeNode.value;
        //得到代碼的内容了,也就是demo的代碼内容
        const codeAst = parser(code);
        //繼續解析代碼内容~~~
        traverse(codeAst, {
          ImportDeclaration: function(importPath) {
            imports.push(importPath.node);
            importPath.remove();
          },
          CallExpression: function(CallPath) {
            const CallPathNode = CallPath.node;
            if (CallPathNode.callee &&
              CallPathNode.callee.object &&
              CallPathNode.callee.object.name === 'ReactDOM' &&
              CallPathNode.callee.property &&
              CallPathNode.callee.property.name === 'render') {
              //we focus on ReactDOM.render method
              renderReturn = types.returnStatement(
                 CallPathNode.arguments[0]
              );
              //we focus on first parameter of ReactDOM.render method
              CallPath.remove();
            }
          },
        });
        const astProgramBody = codeAst.program.body;
        const codeBlock = types.BlockStatement(astProgramBody);
        if (renderReturn) {
          astProgramBody.push(renderReturn);
        }
        const coceFunction = types.functionExpression(
          types.Identifier('jsonmlReactLoader'),
          [],
        );
        path.replaceWith(coceFunction);
      }
    },
  });
  return {
    imports: imports,
    inputAst: inputAst,
  };
};           

經過上面的代碼處理,你會清楚的看到我們的markdown檔案内容變成了如下的格式了:

const React =  require('react');
const ReactDOM = require('react-dom');
import { Button } from 'antd';
module.exports = {
  "content": ["article", ["h3", "1.mark-twain解析出來的無法解析成為ast"], function jsonmlReactLoader() {
    return <div>
    <Button type="primary" shape="circle" icon="search" />
    <Button type="primary" icon="search">Search</Button>
    <Button shape="circle" icon="search" />
    <Button icon="search">Search</Button>
    <br />
    <Button type="ghost" shape="circle" icon="search" />
    <Button type="ghost" icon="search">Search</Button>
    <Button type="dashed" shape="circle" icon="search" />
    <Button type="dashed" icon="search">Search</Button>
  </div>;
  }],
  "meta": {}
};           

此時的子產品依然是ES6格式與jsx混合的代碼,我們需要進一步配合babel來處理将它轉化為ES5代碼,是以我們的webpack.config.js中才會在該插件後引入babel-loader來對代碼進行進一步的打包。那麼你可能會想,就算babel打包後,得到上面這樣的代碼會有什麼用?我給你看看,在前端我是如何将這樣的代碼轉化為React類型的:

import ReactDOM from "react-dom";
import React from "react";
const content =  require('../../demos/basic.md');
const converters = [
      [
        function(node) { return typeof node === 'function'; },
        function(node, index) {
          return React.cloneElement(node(), { key: index });
        }
      ]
    ];
//(2)converters可以引入一個庫來完成
const JsonML = require('jsonml.js/lib/utils');
const toReactComponent = require('jsonml-to-react-component');
ReactDOM.render(toReactComponent(content.content,converters), document.getElementById('react-content'));           

是不是很容易了解了,我們Loader處理後的代碼,最後會被我原樣轉化為React的元件并在頁面中展示,當然這個過程必須經過jsonml-to-react-element的轉化。是以說,我們的Loader完成了markdown檔案類型到我們最後的javascript子產品的轉化。這就是 Webpack 中 Loader 的強大作用。

3.Webpack的Loader常見配置

在 Webpack 中 Loader 就是一個子產品,該子產品導出一個函數。我們的 Webpack 的 Loader 機制會調用這些函數,并将前一個函數的處理結果傳遞給下一個處理函數,而第一個函數接受到的就是檔案的原始内容,比如上面的這個例子就是 markdown 檔案的原樣内容(與通過 Nodejs 中 fs 子產品讀取的内容一緻)。在這個函數中的 this 對象會有各種有用的方法,你可以通過這些方法将 Loader 的調用形式轉化為異步的(this.async方法),或者得到該 Loader 的配置參數等等。

第一個 Loader 會被傳入檔案的原始内容,最後的一個 Loader 必須傳回一個結果,這個結果可以是 String 或者 Buffer 類型,他們代表 JavaScript 子產品的源代碼。同時,一個可選的傳回值就是 SourceMap 。如果隻要傳回一個值,那麼可以是同步模式,如果需要傳回多個值,那麼必須調用*this.callback()*方法。在異步模式下,this,async()必須調用來通知 Webpack 的 Loader 執行器等待異步傳回的結果。它傳回this.callback(),同時該 Loader 必須傳回 undefined同時調用該回調。

3.1 同步的Loader

module.exports = function(content) {
  return someSyncOperation(content);
};           

下面是同步的Loader并傳回多個值:

module.exports = function(content) {
  this.callback(null, someSyncOperation(content), sourceMaps, ast);
  //如果要傳回多個值必須通過this.callback方法
  return; 
  // always return undefined when calling callback()
  // 當調用this.callback時候必須傳回undefined
};           

3.2 異步Loader

module.exports = function(content) {
    var callback = this.async();
    //*this,async()*必須調用來通知Webpack的Loader執行器等待異步傳回的結果
    someAsyncOperation(content, function(err, result) {
        //必須傳回undefined或者回調該callback函數
        if(err) return callback(err);
        callback(null, result);
    });
};           

下面是異步的Loader并傳回多個值的情況:

module.exports = function(content) {
    var callback = this.async();
    //異步Loader
    someAsyncOperation(content, function(err, result, sourceMaps, ast) {
        if(err) return callback(err);
        callback(null, result, sourceMaps, ast);
    });
};           

3.3 "Raw" Loader

預設情況下,源檔案的内容會被轉化為UTF-8的字元串并傳給我們的 Loader 。通過設定 raw 這個标志,那麼我們的 Loader 會接受到一個 Buffer 對象。每一個Loader 都允許将他的結果以 String 或者 Buffer 的類型傳遞給下一個 Loader ,而 Webpack 可以将它在兩者之間正常轉化:

module.exports = function(content) {
    assert(content instanceof Buffer);
    return someSyncOperation(content);
    // return value can be a `Buffer` too
    // This is also allowed if loader is not "raw"
};
module.exports.raw = true;           

3.4 Pitching Loader

我們的Loader預設都是從右邊向左邊執行的,但是在很多情況下,我們可能并不關心前一個Loader的執行結果或者輸入資源。我們僅僅關系中繼資料,我們的Loader上的pitch方法就是在 Loader 被調用之前從左邊往右邊執行的。如果某一個Loader的pitch方法輸出一個結果,那麼打包過程就是逆轉,同時跳過其他的Loader,并繼續執行左側的 Loader(左側的Loader最後執行)。同時data可以在 pitch 方法和正常調用之間傳遞:

module.exports = function(content) {
    return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
    if(someCondition()) {
        // fast exit
        // 此時允許我們隻執行左側的Loader而忽略後續的Loader
        return "module.exports = require(" + JSON.stringify("-!" + remainingRequest) + ");";
    }
    data.value = 42;
};           

比如我們的style-loader就指定了該pitch方法。

4.Webpack的Loader配置

一個 Webpack 的 Loader 的上下文表示在 Loader 中的 this 對象具有的那些屬性,假如有如下的例子:

require("./loader1?xyz!loader2!./resource?rrr");           

假如我們在*/abc/file.js*這個檔案中調用了上面的 require 方法。我們分析下常用的屬性:

  • this.version
  • 這表示 Loader 的 API 的版本。目前版本是2,該參數可用于向後相容。使用 this.version 你可以指定自定義邏輯。
  • this.context
  • 表示目前子產品所在的目錄。通過這個參數你可以擷取該目錄下的其他内容。在上面的例子中就是*/abc*這個目錄。
  • this.request

已經解析後的請求字元串,比如上面的例子就是*"/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"*。即,特定的Loader 已經轉化為絕對路徑。

  • this.query
  • 如果一個 Loader 配置了 Options 對象,那麼該參數就是指向這個對象。如果該 Loader 沒有 Options 參數,但是配置了 query 字元串,那麼該參數就是查詢字元串,并以?開頭。比如開頭的例子可以通過 Options 來配置 Loader 具備的參數:
module.exports = {
  module:{
    rule:[
      { test: /\.md$/, use:[{
        loader:'babel-loader'
      },{
        loader:"./markdownDemoLoader.js",
        options:{
         //指定該Loader的Options參數
        }
      }]
    }]
  }
}           
  • this.callback

使用這個函數可以給我們的 Loader 傳回多個結果,可以在同步或者異步的情況下調用。預設的參數類型是如下格式:

this.callback(
    err: Error | null,
    content: string | Buffer,
    sourceMap?: SourceMap,
    abstractSyntaxTree?: AST
);           

第一個參數可以是 Error 對象或者null;第二個參數是一個String或者Buffer;第三個參數可選,表示可以被該子產品解析的SourceMap;第四個參數也是可選的,是一個AST抽象文法樹,該參數Webpack本身會忽略,但是在不同的Loader之間共享AST可以提升打包的速度。調用該方法後必須傳回undefined!關于抽象文法樹的内容你可以繼續閱讀這裡的内容。

  • this.async

調用該方法相當于告訴我們的 Loader 執行器我們需要調用異步的結果,傳回的内容就是this.callback。比如下面的例子:

module.exports = function(content) {
    var callback = this.async();
    //*this,async()*必須調用來通知Webpack的Loader執行器等待異步傳回的結果
    someAsyncOperation(content, function(err, result) {
        //必須傳回undefined或者回調該callback函數
        if(err) return callback(err);
        callback(null, result);
    });
};           
  • this.data
  • 表示在 Loader 的 pitch 方法和正常打包階段共享的資料。
  • this.cacheable
  • 預設情況下,每一個Loader加載的結果都是可以緩存的。你可以在調用該方法的時候傳入false顯示要求Loader不要緩存結果。一個可以緩存的Loader必須滿足一定的條件,即當輸入和依賴關系沒有發生變化的情況下,輸出必須是确定的。也就是說,該Loader除了this.addDependency指定的依賴以外,不能有其他的依賴子產品。
  • this.loaders

表示一個Loader數組,在pitch階段是可以修改的。如:

loaders = [{request: string, path: string, query: string, module: function}]           

比如下面的例子:

[
  {
    request: "/abc/loader1.js?xyz",
    path: "/abc/loader1.js",
    query: "?xyz",
    module: [Function]
  },
  {
    request: "/abc/node_modules/loader2/index.js",
    path: "/abc/node_modules/loader2/index.js",
    query: "",
    module: [Function]
  }
]           
  • this.loaderIndex
  • 表示目前Loader所在Loaders數組中的下标,比如上面的例子中loader1就是0,而loader2就是1。
  • this.resource

Loader加載的資源部分,包含query字段。如上面的例子就是:

"/abc/resource.js?rrr"           
  • this.resourcePath
  • 表示資源檔案本身,比如上面的例子就是*"/abc/resource.js"*。
  • this.resourceQuery

表示資源的query部分。比如上面的例子就是* "?rrr"*。

  • this.target
  • 表示将目前代碼打包成的檔案格式。可以是"web"或者"node"。
  • this.webpack
  • 如果目前子產品是被Webpack打包,那麼值就是true。
  • this.sourceMap
  • 表示是否應該産生sourceMap。因為産生sourceMap的花銷是很昂貴的,是以你需要确定是否有必要産生。
  • this.emitWarning
  • 産生一個警告消息。
  • this.emitError

産生一個錯誤資訊。

  • this.loadModule

比如下面的形式:

loadModule(request: string, callback: function(err, source, sourceMap, module))           

解析一個特定的加載請求成為對某一個子產品的加載,同時使用産生的資源,sourceMap,子產品執行個體(同行是NormalModule)來調用所有的Loader和回調函數。這個函數可以用于擷取其他子產品的内容并産生結果

  • this.resolve 其中使用方法如下:
resolve(context: string, request: string, callback: function(err, result: string))           

相當于通過require方法來加載一個子產品。

  • this.addDependency
  • 可以通過下面的方法來完成
addDependency(file: string)
dependency(file: string) // shortcut           

将某一個檔案作為該Loader的依賴,進而使得該檔案任何變化可以被監聽。比如html-loader使用該技術來檢視解析的html檔案中�依賴的其他含有src和src-set屬性的資源,然後為這些屬性添加url。

  • this.addContextDependency
  • 将某一個目錄作為Loader的依賴。用法如下:
addContextDependency(directory: string)           

此時,如果目錄中資源發生變化,那麼�Loader本身的�輸出将會更新,上一次加載的資源的緩存失效。

  • this.clearDependencies
  • 移除 Loader 所有的依賴。甚至自己和其它 Loader 的初始依賴。考慮使用 pitch。用法如下:
clearDependencies()           

但是,建議在pitch方法中完成這個功能。

  • emitFile

用于輸出一個檔案。這是 webpack 特有的方法。用法如下:

emitFile(name: string, content: Buffer|string, sourceMap: {...})           

你可以檢視file-loader是如何輸出一個特定的檔案的。

  • this.fs
  • 使用這個屬性可以擷取到Compilation執行個體上的inputFileSystem屬性,其實際上是一個CachedInputFileSystem。其主要屬性如下:
CachedInputFileSystem {
  fileSystem: NodeJsInputFileSystem {},
  //NodeJsInputFileSystem
  _statStorage: 
  //_statStorage屬性,儲存加載的所有的子產品資源
   Storage {
     duration: 60000,
     running: {},
     data: 
      { '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main.js.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main1.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main1.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main1.js.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/node_modules': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules': [Object],
        '/Users/qinliang.ql/Desktop/node_modules': [Object],
        '/Users/qinliang.ql/node_modules': [Object],
        '/Users/node_modules': [Object],
        '/node_modules': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk1': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk1.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk1.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk2': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk2.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk2.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/map.png': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/map.png.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/map.png.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/[email protected]@url-loader/index.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/[email protected]@url-loader/index.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/[email protected]@url-loader/index.js.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/index': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/index.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/index.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/index': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/index.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/index.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/dist/vue.runtime.common.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/dist/vue.runtime.common.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/dist/vue.runtime.common.js.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/dist/jquery.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/dist/jquery.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/dist/jquery.js.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/process/browser.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/process/browser.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/process/browser.js.json': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/webpack/buildin/global.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/webpack/buildin/global.js.js': [Object],
        '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/webpack/buildin/global.js.json': [Object] },
     levels: 
      [ [Object] ],
     count: 48,
     interval: 
      Timeout {
        _called: false,
        _idleTimeout: 530,
        _idlePrev: [Object],
        _idleNext: [Object],
        _idleStart: 685,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: 530 },
     needTickCheck: false,
     nextTick: null,
     passive: false,
     tick: [Function: bound ] },
  //_readdirStorage
  _readdirStorage: 
   Storage {
     duration: 60000,
     running: {},
     data: {},
     levels: 
      [],
     count: 0,
     interval: null,
     needTickCheck: false,
     nextTick: null,
     passive: true,
     tick: [Function: bound ] },
  //_readFileStorage
  _readFileStorage: 
   Storage {
     duration: 60000,
     running: {},
     data: 
      { 
        //已經删除
       },
     levels: 
      [ [Object]],
     count: 32,
     interval: 
      Timeout {
        _called: false,
        _idleTimeout: 530,
        _idlePrev: [Object],
        _idleNext: [Object],
        _idleStart: 678,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: 530 },
     needTickCheck: false,
     nextTick: null,
     passive: false,
     tick: [Function: bound ] },
  //_statStorage與_readdirStorage,_readFileStorage
  _readJsonStorage: 
   Storage {
     duration: 60000,
     running: {},
     data: 
      { 
        //已經删除
      },
     levels: 
      [ [Object] ],
     count: 23,
     interval: 
      Timeout {
        _called: false,
        _idleTimeout: 530,
        _idlePrev: [Object],
        _idleNext: [Object],
        _idleStart: 679,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: 530 },
     needTickCheck: false,
     nextTick: null,
     passive: false,
     tick: [Function: bound ] },
  //_readlinkStorage
  _readlinkStorage: 
   Storage {
     duration: 60000,
     running: {},
     data: 
      {
        //已經删除
      },
     levels: 
      [ [Object] ],
     count: 55,
     interval: 
      Timeout {
        _called: false,
        _idleTimeout: 530,
        _idlePrev: [Object],
        _idleNext: [Object],
        _idleStart: 684,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: 530 },
     needTickCheck: false,
     nextTick: null,
     passive: false,
     tick: [Function: bound ] },
  _stat: [Function: bound bound ],
  _statSync: [Function: bound bound ],
  _readdir: [Function: bound readdir],
  _readdirSync: [Function: bound readdirSync],
  _readFile: [Function: bound bound readFile],
  _readFileSync: [Function: bound bound ],
  _readJson: [Function: bound ],
  _readJsonSync: [Function: bound ],
  _readlink: [Function: bound bound ],
  _readlinkSync: [Function: bound bound ] }           

是以該對象其實就包含了_statStorage,_readdirStorage,_readFileStorage,_readJsonStorage,_readlinkStorage等幾個存儲相關的字段。而至于compiler.outputFileSystem你可以檢視webpack-dev-middleware是如何使用它來将�輸出資源儲存到記憶體中而不是檔案系統中的。上面這個輸出執行個體來自于這個檔案,你可以自己運作并檢視結果。

5.本章總結

本章節,我們通過一個 markdown 的 loader 的具體事例展了如何寫一個 Webpack 的 loader。同時也給出了 loader 常見的配置和用法。通過本章節的學習,你應該能夠寫一個基礎的 Webpack 的 loader。本章節的完整執行個體代碼你可以檢視Webpack 操作 AST,但是因為這個 Loader 牽涉到了如何操作我們的 AST 文法樹,如果你對于這部分内容比較陌生,那麼你可以檢視我推薦給你的這個文章。