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 文法樹,如果你對于這部分内容比較陌生,那麼你可以檢視我推薦給你的這個文章。