.39-淺析webpack原始碼之parser.parse

書生小龍發表於2018-04-04

  因為換了個工作,所以部落格停了一段時間。

  這是上個月留下來的坑,webpack的原始碼已經不太想看了,又臭又長,噁心的要死,想去看node的原始碼……總之先補完這個

  上一節完成了babel-loader對JS檔案字串的轉換,最後返回後進入如下程式碼:

// NormalModule.js
build(options, compilation, resolver, fs, callback) {
    // ...

    return this.doBuild(options, compilation, resolver, fs, (err) => {
        // ...

        // 進入這裡
        try {
            this.parser.parse(this._source.source(), {
                current: this,
                module: this,
                compilation: compilation,
                options: options
            });
        } catch (e) {
            const source = this._source.source();
            const error = new ModuleParseError(this, source, e);
            this.markModuleAsErrored(error);
            return callback();
        }
        return callback();
    });
}

  在看這個parse方法之前,需要過一下引數,首先是這個source方法。

  這個_source並不是轉換後的字串,而是進行一層封裝的物件,source是其原型方法,原始碼如下:

class OriginalSource extends Source {
    // value => babel-loader轉換後的字串
    // name => `D:\workspace\node_modules\babel-loader\lib\index.js!D:\workspace\doc\input.js`
    constructor(value, name) {
        super();
        this._value = value;
        this._name = name;
    }

    source() {
        return this._value;
    }

    //...
}

  有病啊!還是返回了轉換後的字串。

parser.parse

  這個parser的parse方法有兩部分,如下:

parse(source, initialState) {
    let ast;
    const comments = [];
    // 這一部分負責解析原始碼字串
    for (let i = 0, len = POSSIBLE_AST_OPTIONS.length; i < len; i++) {
        // ...
    }
    // 這裡再次進行嘗試
    if (!ast) {
        // ...
    }
    if (!ast || typeof ast !== "object")
        throw new Error("Source couldn`t be parsed");

    // code...
    // 這裡傳入parse後的ast觸發parse的事件流
    if (this.applyPluginsBailResult("program", ast, comments) === undefined) {
        this.prewalkStatements(ast.body);
        this.walkStatements(ast.body);
    }
    // code...
    return state;
}

  先不用管這裡POSSIBLE_AST_OPTIONS是啥,總之這裡做了兩件事情

1、對返回的字串再做一次parse

2、將得到的ast作為引數觸發program事件流

 

  一個一個來,首先是parse程式碼塊,如下:

/*
    const POSSIBLE_AST_OPTIONS = [
    {
        ranges: true,
        locations: true,
        ecmaVersion: ECMA_VERSION,
        sourceType: "module",
        plugins: {
            dynamicImport: true
        }
    }, 
    {
        ranges: true,
        locations: true,
        ecmaVersion: ECMA_VERSION,
        sourceType: "script",
        plugins: {
            dynamicImport: true
        }
    }
];
*/
// 抽象語法樹
let ast;
// 註釋陣列
const comments = [];
for (let i = 0, len = POSSIBLE_AST_OPTIONS.length; i < len; i++) {
    if (!ast) {
        try {
            comments.length = 0;
            POSSIBLE_AST_OPTIONS[i].onComment = comments;
            // 傳入JS字串與本地預設配置引數
            ast = acorn.parse(source, POSSIBLE_AST_OPTIONS[i]);
        } catch (e) {
            // ignore the error
        }
    }
}

  這裡引入了別的模組acorn來解析字串,負責將JS字串解析成抽象語法樹。

  這裡不關心解析的過程,假設解析完成,簡單看一下一個JS檔案原始碼與解析後的樹結構:

原始碼

const t = require(`./module.js`);
t.fn();

抽象語法樹

{
    "type": "Program",
    "start": 0,
    "end": 41,
    "body": [{
        "type": "VariableDeclaration",
        "start": 0,
        "end": 33,
        "declarations": [{
            "type": "VariableDeclarator",
            "start": 6,
            "end": 32,
            "id": { "type": "Identifier", "start": 6, "end": 7, "name": "t" },
            "init": {
                "type": "CallExpression",
                "start": 10,
                "end": 32,
                "callee": {
                    "type": "Identifier",
                    "start": 10,
                    "end": 17,
                    "name": "require"
                },
                "arguments": [{
                    "type": "Literal",
                    "start": 18,
                    "end": 31,
                    "value": "./module.js",
                    "raw": "`./module.js`"
                }]
            }
        }],
        "kind": "const"
    }, {
        "type": "ExpressionStatement",
        "start": 34,
        "end": 41,
        "expression": {
            "type": "CallExpression",
            "start": 34,
            "end": 40,
            "callee": {
                "type": "MemberExpression",
                "start": 34,
                "end": 38,
                "object": { "type": "Identifier", "start": 34, "end": 35, "name": "t" },
                "property": { "type": "Identifier", "start": 36, "end": 38, "name": "fn" },
                "computed": false
            },
            "arguments": []
        }
    }],
    "sourceType": "script"
}

  這裡涉及到一個抽象語法樹的規則,詳情可見https://github.com/estree/estree

  接下來會呼叫Parser上的program事件流,定義地點如下:

// WebpackOptionsApply.js
compiler.apply(
    // 1
    new HarmonyModulesPlugin(options.module),
    // 2
    new UseStrictPlugin(),
);

  地方不好找,總之一個一個過:

HarmonyModulesPlugin

// HarmonyModulesPlugin.js => HarmonyDetectionParserPlugin.js
parser.plugin("program", (ast) => {
    // 這裡對Import/Export的表示式進行檢索
    const isHarmony = ast.body.some(statement => {
        return /^(Import|Export).*Declaration$/.test(statement.type);
    });
    if(isHarmony) {
        const module = parser.state.module;
        const dep = new HarmonyCompatibilityDependency(module);
        dep.loc = {
            start: {
                line: -1,
                column: 0
            },
            end: {
                line: -1,
                column: 0
            },
            index: -2
        };
        // 如果存在就對該模組進行特殊標記處理
        module.addDependency(dep);
        module.meta.harmonyModule = true;
        module.strict = true;
        module.exportsArgument = "__webpack_exports__";
    }
});

  這裡的正則可以參考https://github.com/estree/estree/blob/master/es2015.md的Modules部分說明,簡單講就是檢索JS中是否出現過Import * from *、Export default *等等。

  如果存在會對該模組進行標記。

UseStrictPlugin

// UseStrictPlugin.js
parser.plugin("program", (ast) => {
    const firstNode = ast.body[0];
    // 檢測頭部是否有`use strict`字串
    if(firstNode &&
        firstNode.type === "ExpressionStatement" &&
        firstNode.expression.type === "Literal" &&
        firstNode.expression.value === "use strict") {
        // Remove "use strict" expression. It will be added later by the renderer again.
        // This is necessary in order to not break the strict mode when webpack prepends code.
        // @see https://github.com/webpack/webpack/issues/1970
        const dep = new ConstDependency("", firstNode.range);
        dep.loc = firstNode.loc;
        parserInstance.state.current.addDependency(dep);
        parserInstance.state.module.strict = true;
    }
});

  這個就比較簡單了,判斷JS是否是嚴格模式,然後做個標記。

  事件流走完,parse方法也就呼叫完畢,接下來呼叫build方法的callback,一路回到了Compilation類的buildModule方法。

// Compilation.js
buildModule(module, optional, origin, dependencies, thisCallback) {
    this.applyPlugins1("build-module", module);
    if(module.building) return module.building.push(thisCallback);
    const building = module.building = [thisCallback];

    function callback(err) {
        module.building = undefined;
        building.forEach(cb => cb(err));
    }
    module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, (error) => {
        // 處理錯誤與警告
        const errors = module.errors;
        for(let indexError = 0; indexError < errors.length; indexError++) {
            // ...
        }
        const warnings = module.warnings;
        for(let indexWarning = 0; indexWarning < warnings.length; indexWarning++) {
            // ...
        }
        module.dependencies.sort(Dependency.compare);
        // 事件流不存在
        if(error) {
            this.applyPlugins2("failed-module", module, error);
            return callback(error);
        }
        this.applyPlugins1("succeed-module", module);
        return callback();
    });

  這裡對模組解析後的警告與錯誤進行處理,根據是否有錯誤走兩個不同的事件流,然後觸發callback。

  這裡神神祕祕的搞個callback函式,還forEach,往上面一看,傻了吧唧的就是強行給外部callback引數弄成陣列,實際上就是呼叫了thisCallback。

  

  現在總算摸清了callback的套路,就跟this一樣,誰呼叫就找誰,於是這個callback回到了Compilation的_addModuleChain函式的尾部:

// Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
    // ...

    this.semaphore.acquire(() => {
        moduleFactory.create({
            // ...
        }, (err, module) => {
            // ...
            // 從這裡出來
            this.buildModule(module, false, null, null, (err) => {
                if(err) {
                    this.semaphore.release();
                    return errorAndCallback(err);
                }
                // 這屬性就是個計時器
                // 計算從讀取模組內容到構建完模組的時間
                if(this.profile) {
                    const afterBuilding = Date.now();
                    module.profile.building = afterBuilding - afterFactory;
                }

                moduleReady.call(this);
            });

            function moduleReady() {
                this.semaphore.release();
                // 跳入下一個階段
                this.processModuleDependencies(module, err => {
                    if(err) {
                        return callback(err);
                    }

                    return callback(null, module);
                });
            }
        });
    });
}

  至此,模組的構建基本完成,先到這裡吧……

相關文章