.39-淺析webpack源碼之parser.parse

来源:https://www.cnblogs.com/QH-Jimmy/archive/2018/04/04/8515179.html
-Advertisement-
Play Games

因為換了個工作,所以博客停了一段時間。 這是上個月留下來的坑,webpack的源碼已經不太想看了,又臭又長,噁心的要死,想去看node的源碼……總之先補完這個 上一節完成了babel-loader對JS文件字元串的轉換,最後返回後進入如下代碼: 在看這個parse方法之前,需要過一下參數,首先是這個 ...


  因為換了個工作,所以博客停了一段時間。

  這是上個月留下來的坑,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);
                });
            }
        });
    });
}

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.效果 2.源碼 ...
  • 直接使用 Vue 構建前端單頁面應用,頁面源碼時只有簡單的幾行 html,這並不利於網站的 SEO,這時候就需要服務端渲染 2016 年 10 月 25 日,zeit.co 背後的團隊對外發佈了一個 React 的服務端渲染應用框架 Next.js 幾小時後,一個基於 Vue.js 的服務端渲染應用 ...
  • 1.效果 2.源碼 ...
  • 本文內容: 什麼是CSS CSS寫在哪 樣式(CSS怎麼寫) 盒子模型 尺寸樣式 字體 文本 邊框 背景 圖片 display樣式 邊距 浮動佈局 定位佈局 首發日期:2018-04-04 什麼是css: css全名是層疊樣式表(Cascading Style Sheets) CSS的作用:給htm... ...
  • 由於公司還有用jquery的項目,沒法直接用antd之類的高質量組件,其他的jquery插件樣式又太古老,於是自己動手寫一個吧。 先上成果,樣式交互基本按照antd,原生js開發,不依賴jquery. " " 思路: 根據title屬性生成樹節點,再根據children屬性遞歸生成其子節點 在創建樹 ...
  • 項目中用到的一個功能是要通過點擊地址來實現打開地圖app實現地址導航。 如下圖: 實現思路就是在H5頁面內通過點擊marker圖標然後進行當前位置與頁面上地址的路程規劃與導航。 由於項目中用到的是高德地圖,所以這裡用到的是調起高德地圖APP來實現該功能。 首先肯定要去高德開放平臺去申請KEY,拿到這 ...
  • css2.0文檔查閱下載 網址:http://soft.hao123.com/soft/appid/9517.html <html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content=" ...
  • 錯誤: Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’ 原因: html代碼中出現類似這樣的<input type=“text” [(ngModel)]=“username”>語句,其中使用了[(ngModel)] ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...