置的 JavaScript 解析器及其模塊系統(tǒng)實踐)
深入解讀 AcornMeteor 倉庫中內(nèi)置的 JavaScript 解析器及其模塊系統(tǒng)實踐【免費下載鏈接】meteorMeteor, the JavaScript App Platform項目地址: https://gitcode.com/gh_mirrors/me/meteor導(dǎo)讀Acorn 是一個用 JavaScript 編寫的輕量、快速的 JavaScript 解析器它把源碼解析為符合 ESTree 規(guī)范的抽象語法樹AST是 Babel、webpack 等眾多工具鏈的底層基礎(chǔ)設(shè)施。本文以 Acorn 官方 README 為核心骨架完整講解其接口、解析選項、Parser類、插件機制與命令行工具并在此基礎(chǔ)上結(jié)合當前 Meteor 倉庫中的真實源碼src/index.js、src/options.js、src/state.js 等做縱深展開同時揭示這份 vendored Acorn 在 Meteor 模塊系統(tǒng)測試中的定位以及 Meteor 構(gòu)建工具對 Acorn 的實際調(diào)用方式。讀完本文你既能系統(tǒng)掌握 Acorn 的編程接口與高級用法也能理解在 Meteor 應(yīng)用中像npm link一樣使用本地 npm 包的測試設(shè)計。一、Acorn 是什么定位與倉庫內(nèi)存在形式Acorn 是一個用 JavaScript 編寫的、小巧而快速的 JavaScript 解析器A tiny, fast JavaScript parser written in JavaScript。它本身是獨立開源的第三方項目MIT 協(xié)議但在當前 Meteor 倉庫中它以一個**測試夾具test fixture**的形式被完整收納在模塊系統(tǒng)測試應(yīng)用中源碼副本目錄tools/tests/apps/modules/imports/links/acorn/含src/完整源碼、README.md、CHANGELOG.md、LICENSE與 package.json同一測試應(yīng)用下還有一份面向現(xiàn)代瀏覽器環(huán)境的副本tools/tests/apps/modules-modern/imports/links/acorn/。倉庫中的 package.json 記錄了該副本的信息名稱為acorn版本7.1.1module字段指向src/index.js許可證為 MIT。值得注意的是 src/index.js 中導(dǎo)出的version常量寫的是7.1.0與 package.json 的7.1.1略有出入這是倉庫內(nèi)快照自身的細節(jié)可作為閱讀源碼時的一個觀察點。模塊系統(tǒng)測試應(yīng)用tools/tests/apps/modules/package.json通過acorn: file:imports/links/acorn把這份本地副本聲明為依賴從而模擬本地鏈接類似npm link的 npm 包。這一點是理解本 README 在倉庫中價值的鑰匙它既是 Acorn 的完整使用文檔也是 Meteor 模塊系統(tǒng)能力測試的素材。二、安裝與引入方式README 給出兩種獲取 Acorn 的方式方式一從 npm 安裝npm install acorn方式二克隆源碼自行構(gòu)建git clone https://github.com/acornjs/acorn.git cd acorn npm install在當前 Meteor 倉庫這種離線鏡像/測試夾具場景中還可以直接通過file:協(xié)議引用倉庫內(nèi)的本地副本例如在應(yīng)用根目錄 tools/tests/apps/modules/package.json 中聲明acorn: file:imports/links/acorn隨后即可在代碼中像使用任何 npm 包一樣引入let acorn require(acorn); console.log(acorn.parse(1 1));在 ES Module 環(huán)境下也支持具名導(dǎo)入Meteor 模塊系統(tǒng)測試包 tools/tests/apps/modules/packages/modules-test-package/common.js 中的寫法即為import { parse } from acorn;三、核心接口parse、parseExpressionAt、tokenizer、tokTypes、getLineInfo3.1 parse(input, options)parse(input, options)是庫的主入口。input是待解析的源碼字符串options可以是undefined或一個對象各字段見第四章。返回值是符合ESTree 規(guī)范的抽象語法樹AST對象。let acorn require(acorn); console.log(acorn.parse(1 1));從源碼看頂層parse只是對Parser類靜態(tài)方法的薄封裝src/index.jsexport function parse(input, options) { return Parser.parse(input, options) }而Parser.parse內(nèi)部完成構(gòu)造解析器實例 → 讀取第一個 token → 解析頂層結(jié)構(gòu)的完整流程src/state.jsstatic parse(input, options) { return new this(options, input).parse() }語法錯誤處理當遇到語法錯誤時解析器會拋出帶可讀信息的SyntaxError對象該錯誤對象帶有pos出錯處的字符串偏移量loc一個{line, column}對象指向同一出錯位置。3.2 parseExpressionAt(input, offset, options)parseExpressionAt(input, offset, options)解析字符串中單個表達式并返回其 AST。與parse不同它不會因為表達式之后還有剩余內(nèi)容而報錯非常適合解析嵌入了 JavaScript 表達式的混合語言格式如模板字符串、配置片段等。其靜態(tài)實現(xiàn)src/state.js在給定偏移處初始化解析器后直接調(diào)用parseExpression()。3.3 tokenizer(input, options)tokenizer(input, options)返回一個帶getToken方法的對象可反復(fù)調(diào)用以逐個獲取下一個 token。每個 token 是{start, end, type, value}對象當啟用locations選項時附加loc屬性啟用ranges選項時附加range屬性。當 token 的type為tokTypes.eof時應(yīng)停止調(diào)用該方法——因為此后它只會一直返回同一個 eof token。在 ES6 環(huán)境中返回值可當作任何符合協(xié)議的可迭代對象使用for (let token of acorn.tokenizer(str)) { // iterate over the tokens } // transform code to array of tokens: var tokens [...acorn.tokenizer(str)];其底層實現(xiàn)在 src/state.jsstatic tokenizer(input, options)僅構(gòu)造解析器實例并返回后續(xù) token 完全由解析器內(nèi)部的詞法狀態(tài)機驅(qū)動。解析器的構(gòu)造過程src/state.js會初始化輸入字符串、當前 token 的類型/值/起止偏移、{line, column}位置信息、上下文棧用于判斷當前位置是否允許出現(xiàn)正則字面量、模塊模式與嚴格模式標志等這正是 tokenize 與 parse 共享同一狀態(tài)機的原因。3.4 tokTypes 與 getLineInfotokTypes一個將名稱映射到 token 類型對象的對象這些類型對象正是 token 的type屬性值源碼中由 src/tokentype.js 定義并導(dǎo)出為types經(jīng) src/index.js 以tokTypes名義對外暴露。getLineInfo(input, offset)給定源碼字符串與偏移量返回{line, column}對象實現(xiàn)位于 src/locutil.js與 AST 節(jié)點loc使用的行號/列號計算邏輯一致行號從 1 開始、列號從 0 開始。四、parse 選項詳解完整參數(shù)與源碼級佐證parse的第二個參數(shù)是配置對象可包含以下字段。以下全部選項均可在 src/options.js 的defaultOptions中找到對應(yīng)實現(xiàn)與默認值。4.1 語法版本與模式選項取值/默認值說明ecmaVersion3、5、6 (2015)、7 (2016)、8 (2017)、9 (2018)、10 (2019)、11 (2020部分支持)默認 10決定對嚴格模式、保留字集合以及新語法特性的支持程度。注意Acorn 只實現(xiàn)stage 4已定稿的 ECMAScript 特性其他提案語法需通過插件實現(xiàn)。sourceTypescript或module默認script決定全局嚴格模式以及import/export聲明是否合法。注意設(shè)為module后即使ecmaVersion小于 6靜態(tài)import/export語法也是合法的。源碼層面的兩點印證getOptions會將 2015 及以上的年份寫法歸一化為小版本號src/options.jsif (options.ecmaVersion 2015) options.ecmaVersion - 2009Parser構(gòu)造器會根據(jù)ecmaVersion與sourceType選擇關(guān)鍵字表與保留字表src/state.js模塊模式下還會額外把await加入保留字集合——這正是sourceType: module時await在頂層受到更嚴格約束的實現(xiàn)基礎(chǔ)。4.2 錯誤恢復(fù)與寬松模式選項選項默認值說明onInsertedSemicolonnull回調(diào)函數(shù)。當解析器自動插入缺失的分號時被調(diào)用參數(shù)為插入點的字符偏移量若開啟了locations還會額外傳入{line, column}位置對象。onTrailingCommanull與onInsertedSemicolon類似但針對尾隨逗號。allowReservedecmaVersion 3為true更高版本為false若為false使用保留字將產(chǎn)生錯誤設(shè)為字符串never時保留字和關(guān)鍵字連作為屬性名都不允許類似 IE 舊解析器的行為。allowReturnOutsideFunctionfalse默認情況下頂層return語句會報錯設(shè)為true則接受此類代碼。allowImportExportEverywherefalse默認import/export聲明只能出現(xiàn)在程序頂層設(shè)為true后任何允許語句出現(xiàn)的位置都允許它們。allowAwaitOutsideFunctionfalse默認await只能出現(xiàn)在async函數(shù)內(nèi)設(shè)為true后允許頂層await表達式但在非async函數(shù)內(nèi)仍然不允許。allowHashBangfalse開啟后默認關(guān)閉若代碼以#!開頭如 shell 腳本首行將被視為注釋。其中allowHashBang在Parser構(gòu)造器中有直接體現(xiàn)src/state.jsif (this.pos 0 options.allowHashBang this.input.slice(0, 2) #!) this.skipLineComment(2)4.3 位置、范圍與源碼信息選項默認值說明locationsfalse為true時每個節(jié)點都附帶loc對象內(nèi)含start與end子對象各為{line, column}形式行號 1 起始、列號 0 起始。rangesfalse節(jié)點自帶的start/end屬性記錄字符偏移量直接掛在節(jié)點上而非loc對象。設(shè)為true時額外增加半標準化的range屬性為[start, end]數(shù)組。sourceFilenull當locations開啟時可傳入該選項為每個節(jié)點的loc對象添加source屬性。內(nèi)容不會被解析或處理可自由選用任何格式。directSourceFilenull類似sourceFile但會無論locations是否開啟把sourceFile屬性直接加到節(jié)點上而非loc對象中。preserveParensfalse為true時括號表達式表示為非標準的ParenthesizedExpression節(jié)點其唯一的expression屬性保存括號內(nèi)的表達式。4.4 詞法回調(diào)onToken 與 onCommentonToken默認null傳函數(shù)時每個 token 都會以與tokenizer().getToken()相同格式傳入該函數(shù)傳數(shù)組時每個 token 被 push 進數(shù)組。注意不允許在回調(diào)中再次調(diào)用解析器——這會破壞其內(nèi)部狀態(tài)。onComment默認null傳函數(shù)時每當遇到注釋函數(shù)會收到如下參數(shù)block布爾值true表示塊注釋/* */false表示行注釋text注釋內(nèi)容start注釋起始的字符偏移量end注釋結(jié)束的字符偏移量當locations開啟時追加注釋起止的{line, column}兩個位置參數(shù)。傳數(shù)組時每個注釋以 Esprima 格式的對象被 push 進數(shù)組{ type: Line | Block, value: comment text, start: Number, end: Number, // If locations option is on: loc: { start: {line: Number, column: Number} end: {line: Number, column: Number} }, // If ranges option is on: range: [Number, Number] }同樣不允許在回調(diào)中再次調(diào)用解析器。源碼層面getOptions會把數(shù)組形式的onToken/onComment自動包裝為 push 回調(diào)其中注釋對象按上述 Esprima 格式構(gòu)造src/options.jsif (isArray(options.onToken)) { let tokens options.onToken options.onToken (token) tokens.push(token) } if (isArray(options.onComment)) options.onComment pushComment(options, options.onComment)pushComment依據(jù)locations/ranges是否為真在注釋對象上追加loc與range字段。4.5 增量解析program 選項program默認null通過把解析第一個文件得到的樹作為后續(xù)解析的program選項可以把多個文件解析進同一棵 AST。后續(xù)解析會將文件的頂層形式追加到既有解析樹的Program頂層節(jié)點上。這一點在Parser.parse()中有直接體現(xiàn)src/state.jsparse() { let node this.options.program || this.startNode() this.nextToken() return this.parseTopLevel(node) }五、Parser 類與插件擴展機制Parser類src/state.js的實例包含驅(qū)動一次解析所需的全部狀態(tài)與邏輯。它有三個與頂層同名函數(shù)一一對應(yīng)的靜態(tài)方法parse、parseExpressionAt、tokenizer。使用插件擴展解析器時必須在擴展后的類上調(diào)用這些方法。擴展方式為調(diào)用靜態(tài)方法extendvar acorn require(acorn); var jsx require(acorn-jsx); var JSXParser acorn.Parser.extend(jsx()); JSXParser.parse(foo(bar/));extend接受任意數(shù)量的插件值返回一個包含各插件額外解析邏輯的新Parser類。其實現(xiàn)src/state.js非常簡單——對每個插件依次做類裝飾static extend(...plugins) { let cls this for (let i 0; i plugins.length; i) cls pluginsi return cls }Acorn 這種解析器核心 插件混入的架構(gòu)使其既能保持核心的小巧與快速又能通過extend兼容 stage 3 提案語法與 JSX 等擴展語法。六、命令行工具 bin/acornbin/acorn工具源碼見 src/bin/acorn.js可從命令行解析文件接受輸入文件與以下選項--ecma3|--ecma5|--ecma6|--ecma7|--ecma8|--ecma9|--ecma10設(shè)置要解析的 ECMAScript 版本。README 記載默認版本為 9。順帶一提解析 API 側(cè)的默認值在 src/options.js 中為ecmaVersion: 10兩者默認不同使用 CLI 時可顯式指定版本以避免歧義。--module將解析模式設(shè)為module否則為script。--locations為每個節(jié)點附加loc對象內(nèi)含{line, column}形式的start/end行號 1 起始、列號 0 起始。--allow-hash-bang若代碼以#!開頭如 shell 腳本首行視為注釋。--compactAST 輸出中不使用空白。--silent不輸出 AST僅返回退出狀態(tài)碼。--help打印用法信息并退出。工具以 JSON 數(shù)據(jù)形式輸出語法樹。閱讀 src/bin/acorn.js 的源碼可以發(fā)現(xiàn)實際實現(xiàn)的命令行參數(shù)比 README 所列還要豐富同時支持--ecma2015、--ecma2016…… 這類年份命名arg.match(/^--ecma(\d)$/)統(tǒng)一解析見 src/bin/acorn.js支持--tokenize模式僅輸出 token 列表而不輸出 AST支持從標準輸入讀取代碼當未指定文件或文件為-時src/bin/acorn.js 會累積 stdin 數(shù)據(jù)后在結(jié)束時解析輸出格式為JSON.stringify(result, null, compact ? null : 2)即--compact時無縮進否則縮進 2 空格出錯時錯誤信息會帶上文件名若輸入來自文件并以退出碼 1 結(jié)束。七、現(xiàn)有插件生態(tài)7.1 語法擴展插件acorn-jsx解析 Facebook JSX 語法擴展。7.2 ECMAScript 提案插件acorn-stage3解析大多數(shù) stage 3 提案聚合了以下子插件acorn-class-fields解析類字段提案acorn-import-meta解析import.meta提案acorn-numeric-separator解析數(shù)字分隔符提案acorn-private-methods解析私有方法、getter 與 setter 提案。這與 README 中只有 stage 4已定稿特性才被 Acorn 核心實現(xiàn)其他提案特性通過插件實現(xiàn)的原則完全呼應(yīng)。八、在 Meteor 倉庫中的實際應(yīng)用測試夾具與構(gòu)建工具這份 README 所在目錄并非孤立存在它在 Meteor 倉庫中有兩層實際用途可以作為理解文檔的落地場景。8.1 模塊系統(tǒng)測試驗證本地鏈接 npm 包的等價性在模塊測試應(yīng)用 tools/tests/apps/modules/tests.js 中有一組專門針對npm link式本地包的用例Meteor.isClient it(should support application-compiled npm linked packages, () { assert.strictEqual( require.resolve(acorn), /node_modules/acorn/src/index.js ); const { parse } require(acorn); assert.strictEqual(typeof parse, function); assert.strictEqual( parse, require(./imports/links/acorn).parse ); });該用例斷言三件事一是require.resolve(acorn)解析到/node_modules/acorn/src/index.js注意路徑中并沒有imports/links說明file:依賴被映射進 node_modules 命名空間二是require(acorn)與直接require(./imports/links/acorn)得到的是同一個 parse 函數(shù)模塊單例等價性三是由此驗證了應(yīng)用側(cè)可以編譯npm link過的包。這正是 tools/tests/apps/modules/package.json 中acorn: file:imports/links/acorn聲明的意義所在。同樣的副本與用例還存在于現(xiàn)代瀏覽器測試應(yīng)用 tools/tests/apps/modules-modern/ 中。8.2 構(gòu)建工具調(diào)用Meteor 自身用 Acorn 做快速 JS 靜態(tài)分析Meteor 的 Isobuild 構(gòu)建工具在 tools/isobuild/js-analyze.js 中直接引入acorn并使用其parse接口tools/isobuild/js-analyze.js做 JS 文件的快速靜態(tài)分析ast acorn.parse(source, { ecmaVersion: latest, sourceType: script, allowAwaitOutsideFunction: true, allowImportExportEverywhere: true, allowReturnOutsideFunction: true, allowHashBang: true, checkPrivateFields: false, });從這段調(diào)用可以直觀看到前文各選項在真實工程中的典型組合ecmaVersion: latest解析最新語法allowAwaitOutsideFunction、allowImportExportEverywhere、allowReturnOutsideFunction、allowHashBang全部開啟以放寬約束、容忍各類非常規(guī)但可運行的代碼分析結(jié)果通過 LRU 緩存AST_CACHE按行數(shù)估算體積做緩存淘汰且當 Acorn 解析失敗時降級到 Babel 解析器tools/isobuild/js-analyze.js 同文件后續(xù)分支——這印證了 Acorn小巧快速、適合作為第一道解析閘門的定位。這些選項的語義與前文第四章的文檔說明一一對應(yīng)是理解 README 中寬松模式選項價值的最佳實例。九、總結(jié)Acorn 以極小的核心實現(xiàn)了完整的 ECMAScript 詞法與語法解析通過parse、parseExpressionAt、tokenizer、tokTypes、getLineInfo五個入口和一套可逐項配置的 options 體系覆蓋了從整文件 AST到流式 token再到精確位置查詢的全部解析需求Parser.extend插件機制與 CLI 工具則讓它既可作為庫嵌入各類工具鏈也可獨立用于命令行調(diào)試。在 Meteor 倉庫中這份 README 及其源碼副本既是模塊系統(tǒng)測試應(yīng)用驗證file:本地鏈接包與npm link等價性的載體見 tools/tests/apps/modules/tests.js其parse接口也被 Isobuild 構(gòu)建工具 js-analyze.js 實際用于 JS 靜態(tài)分析——文檔中的每個選項都能在倉庫代碼里找到真實的落點?!久赓M下載鏈接】meteorMeteor, the JavaScript App Platform項目地址: https://gitcode.com/gh_mirrors/me/meteor創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考