溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么用JS實(shí)現(xiàn)代碼編譯器

發(fā)布時間:2021-11-04 16:47:49 來源:億速云 閱讀:162 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“怎么用JS實(shí)現(xiàn)代碼編譯器”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

一、前言

對于前端同學(xué)來說,編譯器可能適合神奇的魔盒?,表面普通,但常常給我們驚喜。

編譯器,顧名思義,用來編譯,編譯什么呢?當(dāng)然是編譯代碼咯?。

其實(shí)我們也經(jīng)常接觸到編譯器的使用場景:

  •  React 中 JSX 轉(zhuǎn)換成 JS 代碼;

  •  通過 Babel 將 ES6 及以上規(guī)范的代碼轉(zhuǎn)換成 ES5 代碼;

  •  通過各種 Loader 將 Less / Scss 代碼轉(zhuǎn)換成瀏覽器支持的 CSS 代碼;

  •  將 TypeScript 轉(zhuǎn)換為 JavaScript 代碼。

  •  and so on...

使用場景非常之多,我的雙手都數(shù)不過來了。?

雖然現(xiàn)在社區(qū)已經(jīng)有非常多工具能為我們完成上述工作,但了解一些編譯原理是很有必要的。

二、編譯器介紹

2.1 程序運(yùn)行方式

現(xiàn)代程序主要有兩種編譯模式:靜態(tài)編譯和動態(tài)解釋。推薦一篇文章《Angular 2 JIT vs AOT》介紹得非常詳細(xì)。

靜態(tài)編譯

簡稱 「AOT」(Ahead-Of-Time)即 「提前編譯」 ,靜態(tài)編譯的程序會在執(zhí)行前,會使用指定編譯器,將全部代碼編譯成機(jī)器碼。

怎么用JS實(shí)現(xiàn)代碼編譯器

(圖片來自:https://segmentfault.com/a/1190000008739157)

在 Angular 的 AOT 編譯模式開發(fā)流程如下:

  •  使用 TypeScript 開發(fā) Angular 應(yīng)用

  •  運(yùn)行 ngc 編譯應(yīng)用程序

    •   使用 Angular Compiler 編譯模板,一般輸出 TypeScript 代碼

    •   運(yùn)行 tsc 編譯 TypeScript 代碼

  •  使用 Webpack 或 Gulp 等其他工具構(gòu)建項(xiàng)目,如代碼壓縮、合并等

  •  部署應(yīng)用

動態(tài)解釋

簡稱 「JIT」(Just-In-Time)即 「即時編譯」 ,動態(tài)解釋的程序會使用指定解釋器,一邊編譯一邊執(zhí)行程序。

怎么用JS實(shí)現(xiàn)代碼編譯器

(圖片來自:https://segmentfault.com/a/1190000008739157[1])

在 Angular 的 JIT 編譯模式開發(fā)流程如下:

  •  使用 TypeScript 開發(fā) Angular 應(yīng)用

  •  運(yùn)行 tsc 編譯 TypeScript 代碼

  •  使用 Webpack 或 Gulp 等其他工具構(gòu)建項(xiàng)目,如代碼壓縮、合并等

  •  部署應(yīng)用

AOT vs JIT

AOT 編譯流程:

怎么用JS實(shí)現(xiàn)代碼編譯器

(圖片來自:https://segmentfault.com/a/1190000008739157)

JIT 編譯流程:

怎么用JS實(shí)現(xiàn)代碼編譯器

(圖片來自:https://segmentfault.com/a/1190000008739157)

特性AOTJIT
編譯平臺(Server) 服務(wù)器(Browser) 瀏覽器
編譯時機(jī)Build (構(gòu)建階段)Runtime (運(yùn)行時)
包大小較小較大
執(zhí)行性能更好-
啟動時間更短-

除此之外 AOT 還有以下優(yōu)點(diǎn):

  •  在客戶端我們不需要導(dǎo)入體積龐大的 angular 編譯器,這樣可以減少我們 JS 腳本庫的大小。

  •  使用 AOT 編譯后的應(yīng)用,不再包含任何 HTML 片段,取而代之的是編譯生成的 TypeScript 代碼,這樣的話 TypeScript 編譯器就能提前發(fā)現(xiàn)錯誤??偠灾?,采用 AOT 編譯模式,我們的模板是類型安全的。

2.2 現(xiàn)代編譯器工作流程

摘抄維基百科中對 編譯器[2]工作流程介紹:

  ?    一個現(xiàn)代編譯器的主要工作流程如下:源代碼(source code)→ 預(yù)處理器(preprocessor)→ 編譯器(compiler)→ 匯編程序(assembler)→ 目標(biāo)代碼(object code)→ 鏈接器(linker)→ 可執(zhí)行文件(executables),最后打包好的文件就可以給電腦去判讀運(yùn)行了。    ?

怎么用JS實(shí)現(xiàn)代碼編譯器

這里更強(qiáng)調(diào)了編譯器的作用:「將原始程序作為輸入,翻譯產(chǎn)生目標(biāo)語言的等價程序」。

怎么用JS實(shí)現(xiàn)代碼編譯器

編譯器三個核心階段.png

目前絕大多數(shù)現(xiàn)代編譯器工作流程基本類似,包括三個核心階段:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2.  「解析(Parsing)」 :通過詞法分析和語法分析,將原始代碼字符串解析成「抽象語法樹(Abstract Syntax Tree)」;

  3.  「轉(zhuǎn)換(Transformation)」:對抽象語法樹進(jìn)行轉(zhuǎn)換處理操作;

  4.  「生成代碼(Code Generation)」:將轉(zhuǎn)換之后的 AST 對象生成目標(biāo)語言代碼字符串。

三、編譯器實(shí)現(xiàn)

本文將通過 「The Super Tiny Compiler[3]」 源碼解讀,學(xué)習(xí)如何實(shí)現(xiàn)一個輕量編譯器,最終「實(shí)現(xiàn)將下面原始代碼字符串(Lisp 風(fēng)格的函數(shù)調(diào)用)編譯成 JavaScript 可執(zhí)行的代碼」。

 Lisp 風(fēng)格(編譯前)JavaScript 風(fēng)格(編譯后)
2 + 2(add 2 2)add(2, 2)
4 - 2(subtract 4 2)subtract(4, 2)
2 + (4 - 2)(add 2 (subtract 4 2))add(2, subtract(4, 2))

話說 The Super Tiny Compiler 號稱「可能是有史以來最小的編譯器」,并且其作者 James Kyle 也是 Babel 活躍維護(hù)者之一。

怎么用JS實(shí)現(xiàn)代碼編譯器

讓我們開始吧~

3.1 The Super Tiny Compiler 工作流程

現(xiàn)在對照前面編譯器的三個核心階段,了解下 The Super Tiny Compiler  編譯器核心工作流程:

怎么用JS實(shí)現(xiàn)代碼編譯器

圖中詳細(xì)流程如下:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2.  執(zhí)行「入口函數(shù)」,輸入「原始代碼字符串」作為參數(shù); 

// 原始代碼字符串  (add 2 (subtract 42))

    2.  進(jìn)入「解析階段(Parsing)」,原始代碼字符串通過「詞法分析器(Tokenizer)」轉(zhuǎn)換為「詞法單元數(shù)組」,然后再通過 「詞法分析器(Parser)」將「詞法單元數(shù)組」轉(zhuǎn)換為「抽象語法樹(Abstract Syntax Tree 簡稱 AST)」,并返回;

怎么用JS實(shí)現(xiàn)代碼編譯器 

怎么用JS實(shí)現(xiàn)代碼編譯器

 3.   進(jìn)入「轉(zhuǎn)換階段(Transformation)」,將上一步生成的 「AST 對象」 導(dǎo)入「轉(zhuǎn)換器(Transformer)」,通過「轉(zhuǎn)換器」中的「遍歷器(Traverser)」,將代碼轉(zhuǎn)換為我們所需的「新的 AST 對象」;

怎么用JS實(shí)現(xiàn)代碼編譯器

  4.   進(jìn)入「代碼生成階段(Code Generation)」,將上一步返回的「新 AST 對象」通過「代碼生成器(CodeGenerator)」,轉(zhuǎn)換成 「JavaScript Code」;

怎么用JS實(shí)現(xiàn)代碼編譯器

  5.   「代碼編譯結(jié)束」,返回 「JavaScript Code」。

怎么用JS實(shí)現(xiàn)代碼編譯器

上述流程看完后可能一臉懵逼,不過沒事,請保持頭腦清醒,先有個整個流程的印象,接下來我們開始閱讀代碼:

3.2 入口方法

首先定義一個入口方法 compiler ,接收原始代碼字符串作為參數(shù),返回最終 JavaScript Code:

// 編譯器入口方法 參數(shù):原始代碼字符串 input  function compiler(input) {    let tokens = tokenizer(input);    let ast    = parser(tokens);    let newAst = transformer(ast);    let output = codeGenerator(newAst);    return output;  }

3.3 解析階段

在解析階段中,我們定義「詞法分析器方法」 tokenizer  和「語法分析器方法」 parser 然后分別實(shí)現(xiàn):

// 詞法分析器 參數(shù):原始代碼字符串 input  function tokenizer(input) {};  // 語法分析器 參數(shù):詞法單元數(shù)組tokens  function parser(tokens) {};

詞法分析器

「詞法分析器方法」 tokenizer 的主要任務(wù):遍歷整個原始代碼字符串,將原始代碼字符串轉(zhuǎn)換為「詞法單元數(shù)組(tokens)」,并返回。

在遍歷過程中,匹配每種字符并處理成「詞法單元」壓入「詞法單元數(shù)組」,如當(dāng)匹配到左括號( ( )時,將往「詞法單元數(shù)組(tokens)「壓入一個」詞法單元對象」({type: 'paren', value:'('})。

怎么用JS實(shí)現(xiàn)代碼編譯器

// 詞法分析器 參數(shù):原始代碼字符串 input  function tokenizer(input) {    let current = 0;  // 當(dāng)前解析的字符索引,作為游標(biāo)    let tokens = [];  // 初始化詞法單元數(shù)組    // 循環(huán)遍歷原始代碼字符串,讀取詞法單元數(shù)組    while (current < input.length) {      let char = input[current];      // 匹配左括號,匹配成功則壓入對象 {type: 'paren', value:'('}      if (char === '(') {        tokens.push({          type: 'paren',          value: '('        });        current++;        continue; // 自增current,完成本次循環(huán),進(jìn)入下一個循環(huán)      }      // 匹配右括號,匹配成功則壓入對象 {type: 'paren', value:')'}      if (char === ')') {        tokens.push({          type: 'paren',          value: ')'        });        current++;        continue;      }       // 匹配空白字符,匹配成功則跳過      // 使用 \s 匹配,包括空格、制表符、換頁符、換行符、垂直制表符等      let WHITESPACE = /\s/;      if (WHITESPACE.test(char)) {        current++;        continue;      }      // 匹配數(shù)字字符,使用 [0-9]:匹配      // 匹配成功則壓入{type: 'number', value: value}      // 如 (add 123 456) 中 123 和 456 為兩個數(shù)值詞法單元      let NUMBERS = /[0-9]/;      if (NUMBERS.test(char)) {        let value = '';        // 匹配連續(xù)數(shù)字,作為數(shù)值        while (NUMBERS.test(char)) {          value += char;          char = input[++current];        }        tokens.push({ type: 'number', value });        continue;      }      // 匹配形雙引號包圍的字符串      // 匹配成功則壓入 { type: 'string', value: value }      // 如 (concat "foo" "bar") 中 "foo" 和 "bar" 為兩個字符串詞法單元      if (char === '"') {        let value = '';        char = input[++current]; // 跳過左雙引號        // 獲取兩個雙引號之間所有字符        while (char !== '"') {          value += char;          char = input[++current];        }        char = input[++current];// 跳過右雙引號        tokens.push({ type: 'string', value });        continue;      }      // 匹配函數(shù)名,要求只含大小寫字母,使用 [a-z] 匹配 i 模式      // 匹配成功則壓入 { type: 'name', value: value }      // 如 (add 2 4) 中 add 為一個名稱詞法單元      let LETTERS = /[a-z]/i;      if (LETTERS.test(char)) {        let value = '';        // 獲取連續(xù)字符        while (LETTERS.test(char)) {          value += char;          char = input[++current];        }        tokens.push({ type: 'name', value });        continue;      }      // 當(dāng)遇到無法識別的字符,拋出錯誤提示,并退出      thrownewTypeError('I dont know what this character is: ' + char);    }    // 詞法分析器的最后返回詞法單元數(shù)組    return tokens;  }

語法分析器

「語法分析器方法」 parser 的主要任務(wù):將「詞法分析器」返回的「詞法單元數(shù)組」,轉(zhuǎn)換為能夠描述語法成分及其關(guān)系的中間形式(「抽象語法樹 AST」)。

怎么用JS實(shí)現(xiàn)代碼編譯器

// 語法分析器 參數(shù):詞法單元數(shù)組tokens  function parser(tokens) {    let current = 0; // 設(shè)置當(dāng)前解析的詞法單元的索引,作為游標(biāo)    // 遞歸遍歷(因?yàn)楹瘮?shù)調(diào)用允許嵌套),將詞法單元轉(zhuǎn)成 LISP 的 AST 節(jié)點(diǎn)    function walk() {      // 獲取當(dāng)前索引下的詞法單元 token      let token = tokens[current];      // 數(shù)值類型詞法單元      if (token.type === 'number') {        current++; // 自增當(dāng)前 current 值        // 生成一個 AST節(jié)點(diǎn) 'NumberLiteral',表示數(shù)值字面量        return {          type: 'NumberLiteral',          value: token.value,        };      }      // 字符串類型詞法單元      if (token.type === 'string') {        current++;        // 生成一個 AST節(jié)點(diǎn) 'StringLiteral',表示字符串字面量        return {          type: 'StringLiteral',          value: token.value,        };      }      // 函數(shù)類型詞法單元      if (token.type === 'paren' && token.value === '(') {        // 跳過左括號,獲取下一個詞法單元作為函數(shù)名        token = tokens[++current];        let node = {          type: 'CallExpression',          name: token.value,          params: []        };       // 再次自增 current 變量,獲取參數(shù)詞法單元        token = tokens[++current];        // 遍歷每個詞法單元,獲取函數(shù)參數(shù),直到出現(xiàn)右括號")"        while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) {          node.params.push(walk());          token = tokens[current];        }        current++; // 跳過右括號        return node;      }      // 無法識別的字符,拋出錯誤提示      thrownewTypeError(token.type);    }    // 初始化 AST 根節(jié)點(diǎn)    let ast = {      type: 'Program',      body: [],    };    // 循環(huán)填充 ast.body    while (current < tokens.length) {      ast.body.push(walk());    }    // 最后返回ast    return ast;  }

3.4 轉(zhuǎn)換階段

在轉(zhuǎn)換階段中,定義了轉(zhuǎn)換器 transformer 函數(shù),使用詞法分析器返回的 LISP 的 AST 對象作為參數(shù),將 AST 對象轉(zhuǎn)換成一個新的 AST 對象。

為了方便代碼組織,我們定義一個遍歷器 traverser 方法,用來處理每一個節(jié)點(diǎn)的操作。

// 遍歷器 參數(shù):ast 和 visitor  function traverser(ast, visitor) {    // 定義方法 traverseArray    // 用于遍歷 AST節(jié)點(diǎn)數(shù)組,對數(shù)組中每個元素調(diào)用 traverseNode 方法。    function traverseArray(array, parent) {      array.forEach(child => {        traverseNode(child, parent);      });    }    // 定義方法 traverseNode    // 用于處理每個 AST 節(jié)點(diǎn),接受一個 node 和它的父節(jié)點(diǎn) parent 作為參數(shù)    function traverseNode(node, parent) {      // 獲取 visitor 上對應(yīng)方法的對象      let methods = visitor[node.type];      // 獲取 visitor 的 enter 方法,處理操作當(dāng)前 node      if (methods && methods.enter) {        methods.enter(node, parent);      }      switch (node.type) {        // 根節(jié)點(diǎn)        case'Program':          traverseArray(node.body, node);          break;       // 函數(shù)調(diào)用        case'CallExpression':          traverseArray(node.params, node);          break;        // 數(shù)值和字符串,忽略        case'NumberLiteral':        case'StringLiteral':          break;        // 當(dāng)遇到無法識別的字符,拋出錯誤提示,并退出        default:          thrownewTypeError(node.type);      }      if (methods && methods.exit) {        methods.exit(node, parent);      }    }    // 首次執(zhí)行,開始遍歷    traverseNode(ast, null);  }

在看「遍歷器」 traverser 方法時,建議結(jié)合下面介紹的「轉(zhuǎn)換器」 transformer 方法閱讀:

// 轉(zhuǎn)化器,參數(shù):ast  function transformer(ast) {    // 創(chuàng)建 newAST,與之前 AST 類似,Program:作為新 AST 的根節(jié)點(diǎn)    let newAst = {      type: 'Program',      body: [],    };    // 通過 _context 維護(hù)新舊 AST,注意 _context 是一個引用,從舊的 AST 到新的 AST。    ast._context = newAst.body;    // 通過遍歷器遍歷 處理舊的 AST    traverser(ast, {      // 數(shù)值,直接原樣插入新AST,類型名稱 NumberLiteral      NumberLiteral: {        enter(node, parent) {          parent._context.push({            type: 'NumberLiteral',            value: node.value,          });        },      },      // 字符串,直接原樣插入新AST,類型名稱 StringLiteral      StringLiteral: {        enter(node, parent) {          parent._context.push({            type: 'StringLiteral',            value: node.value,          });        },      },      // 函數(shù)調(diào)用      CallExpression: {        enter(node, parent) {          // 創(chuàng)建不同的AST節(jié)點(diǎn)          let expression = {            type: 'CallExpression',            callee: {              type: 'Identifier',              name: node.name,            },            arguments: [],          };          // 函數(shù)調(diào)用有子類,建立節(jié)點(diǎn)對應(yīng)關(guān)系,供子節(jié)點(diǎn)使用          node._context = expression.arguments;          // 頂層函數(shù)調(diào)用算是語句,包裝成特殊的AST節(jié)點(diǎn)          if (parent.type !== 'CallExpression') {            expression = {              type: 'ExpressionStatement',              expression: expression,            };          }          parent._context.push(expression);        },      }    });    return newAst;  }

重要一點(diǎn),這里通過 _context 引用來「維護(hù)新舊 AST 對象」,管理方便,避免污染舊 AST 對象。

3.5 代碼生成

接下來到了最后一步,我們定義「代碼生成器」 codeGenerator 方法,通過遞歸,將新的 AST 對象代碼轉(zhuǎn)換成 JavaScript 可執(zhí)行代碼字符串。

// 代碼生成器 參數(shù):新 AST 對象  function codeGenerator(node) {    switch (node.type) {      // 遍歷 body 屬性中的節(jié)點(diǎn),且遞歸調(diào)用 codeGenerator,按行輸出結(jié)果      case'Program':        return node.body.map(codeGenerator)          .join('\n');      // 表達(dá)式,處理表達(dá)式內(nèi)容,并用分號結(jié)尾      case'ExpressionStatement':        return (          codeGenerator(node.expression) +          ';'        );      // 函數(shù)調(diào)用,添加左右括號,參數(shù)用逗號隔開      case'CallExpression':        return (          codeGenerator(node.callee) +          '(' +          node.arguments.map(codeGenerator)            .join(', ') +          ')'        );      // 標(biāo)識符,返回其 name      case'Identifier':        return node.name;      // 數(shù)值,返回其 value      case'NumberLiteral':        return node.value;      // 字符串,用雙引號包裹再輸出      case'StringLiteral':        return'"' + node.value + '"';      // 當(dāng)遇到無法識別的字符,拋出錯誤提示,并退出      default:        thrownewTypeError(node.type);    }  }

3.6 編譯器測試

截止上一步,我們完成簡易編譯器的代碼開發(fā)。接下來通過前面原始需求的代碼,測試編譯器效果如何:

const add = (a, b) => a + b;  const subtract = (a, b) => a - b;  const source = "(add 2 (subtract 4 2))";  const target = compiler(source); // "add(2, (subtract(4, 2));"  const result = eval(target); // Ok result is 4

3.7 工作流程小結(jié)

總結(jié) The Super Tiny Compiler 編譯器整個工作流程:

「1、input => tokenizer => tokens」

「2、tokens => parser => ast」

「3、ast => transformer => newAst」

「4、newAst => generator => output」

其實(shí)多數(shù)編譯器的工作流程都大致相同:

怎么用JS實(shí)現(xiàn)代碼編譯器

四、手寫 Webpack 編譯器

根據(jù)之前介紹的 The Super Tiny Compiler編譯器核心工作流程,再來手寫 Webpack 的編譯器,會讓你有種眾享絲滑的感覺~

怎么用JS實(shí)現(xiàn)代碼編譯器

話說,有些面試官喜歡問這個呢。當(dāng)然,手寫一遍能讓我們更了解 Webpack 的構(gòu)建流程,這個章節(jié)我們簡要介紹一下。

4.1 Webpack 構(gòu)建流程分析

從啟動構(gòu)建到輸出結(jié)果一系列過程:

1.  「初始化參數(shù)」

解析 Webpack 配置參數(shù),合并 Shell 傳入和 webpack.config.js 文件配置的參數(shù),形成最后的配置結(jié)果。

2.  「開始編譯」

上一步得到的參數(shù)初始化 compiler 對象,注冊所有配置的插件,插件監(jiān)聽 Webpack 構(gòu)建生命周期的事件節(jié)點(diǎn),做出相應(yīng)的反應(yīng),執(zhí)行對象的 run 方法開始執(zhí)行編譯。

3.  「確定入口」

從配置的 entry 入口,開始解析文件構(gòu)建 AST 語法樹,找出依賴,遞歸下去。

4.  「編譯模塊」

遞歸中根據(jù)「文件類型」和 「loader 配置」,調(diào)用所有配置的 loader 對文件進(jìn)行轉(zhuǎn)換,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理。

5  「完成模塊編譯并輸出」

遞歸完事后,得到每個文件結(jié)果,包含每個模塊以及他們之間的依賴關(guān)系,根據(jù) entry 配置生成代碼塊 chunk 。

6. 「輸出完成」

輸出所有的 chunk 到文件系統(tǒng)。

注意:在構(gòu)建生命周期中有一系列插件在做合適的時機(jī)做合適事情,比如 UglifyPlugin 會在 loader 轉(zhuǎn)換遞歸完對結(jié)果使用 UglifyJs 壓縮「覆蓋之前的結(jié)果」。

怎么用JS實(shí)現(xiàn)代碼編譯器

4.2 代碼實(shí)現(xiàn)

手寫 Webpack 需要實(shí)現(xiàn)以下三個核心方法:

  •  createAssets : 收集和處理文件的代碼;

  •  createGraph :根據(jù)入口文件,返回所有文件依賴圖;

  •  bundle : 根據(jù)依賴圖整個代碼并輸出;

1. createAssets

function createAssets(filename){      const content = fs.readFileSync(filename, "utf-8"); // 根據(jù)文件名讀取文件內(nèi)容      // 將讀取到的代碼內(nèi)容,轉(zhuǎn)換為 AST      const ast = parser.parse(content, {          sourceType: "module"// 指定源碼類型      })      const dependencies = []; // 用于收集文件依賴的路徑      // 通過 traverse 提供的操作 AST 的方法,獲取每個節(jié)點(diǎn)的依賴路徑      traverse(ast, {          ImportDeclaration: ({node}) => {              dependencies.push(node.source.value);          }      });      // 通過 AST 將 ES6 代碼轉(zhuǎn)換成 ES5 代碼      const { code } = babel.transformFromAstSync(ast, null, {          presets: ["@babel/preset-env"]      });      let id = moduleId++;      return {          id,          filename,          code,          dependencies      }  }

2. createGraph

function createGraph(entry) {      const mainAsset = createAssets(entry); // 獲取入口文件下的內(nèi)容      const queue = [mainAsset];      for(const asset of queue){          const dirname = path.dirname(asset.filename);          asset.mapping = {};          asset.dependencies.forEach(relativePath => {              const absolutePath = path.join(dirname, relativePath); // 轉(zhuǎn)換文件路徑為絕對路徑              const child = createAssets(absolutePath);              asset.mapping[relativePath] = child.id;              queue.push(child); // 遞歸去遍歷所有子節(jié)點(diǎn)的文件          })      }      return queue;  }

3. bunlde

function bundle(graph) {      let modules = "";      graph.forEach(item => {          modules += `              ${item.id}: [                  function (require, module, exports){                      ${item.code}                  },                  ${JSON.stringify(item.mapping)}              ],          `      })      return`          (function(modules){              function require(id){                  const [fn, mapping] = modules[id];                  function localRequire(relativePath){                      return require(mapping[relativePath]);                  }                  const module = {                      exports: {}                  }                  fn(localRequire, module, module.exports);                  return module.exports;              }              require(0);          })({${modules}})      `  }

“怎么用JS實(shí)現(xiàn)代碼編譯器”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

js
AI