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