<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          淺析eslint原理

          共 30493字,需瀏覽 61分鐘

           ·

          2022-06-24 19:06

              

          在前端開發(fā)過程中,eslint規(guī)范已經(jīng)成為必不可少的一環(huán),我們需要eslint來保證代碼規(guī)范,相對統(tǒng)一同學們的代碼風格,不然就會出現(xiàn)所有同學都隨意引入自己偏好的風格或者規(guī)范,讓所有人一起分擔引入規(guī)范的代價。

          同時,有些lint規(guī)則可以避免bug的產(chǎn)生,在提高代碼可讀性的前提下,減少問題數(shù)量,將問題更多的暴露在開發(fā)階段。

          一、eslint的規(guī)則

          說起eslint,第一想到的就是eslints里面的每條規(guī)則,我們通過以下簡單的配置就可以來控制規(guī)則的開啟及關閉。其中:0 1 2 分別對應 'off' 'warn' 'error';如果是個數(shù)組,第二個參數(shù)可以自定義配置。

          {
            "rules": {
              "arrow-body-style" : 0, // 0 1 2
              "quotes" : [ "error" , "single" ]
            }
          }

          其中rules的每一個key就是對應的一條規(guī)則,透過使用去思考,eslint如何去實現(xiàn)的這條規(guī)則呢???

          eslint的核心rules

          eslint 的核心就是 rules,理解一個 rule 的結構對于理解 eslint 的原理和創(chuàng)建自定義規(guī)則非常重要。

          我們看一下自定義eslint 規(guī)則[1] 再結合目前已有的某條規(guī)則來分析

          看一下最簡單的一條規(guī)則 no-with

          module.exports = {
              meta: {  // 包含規(guī)則的元數(shù)據(jù)
              // 指示規(guī)則的類型,值為 "problem""suggestion" 或 "layout"
                  type"suggestion"
                  
                  docs: { // 對 ESLint 核心規(guī)則來說是必需的
                      description: "disallow `with` statements", // 提供規(guī)則的簡短描述在規(guī)則首頁展示
                      // category (string) 指定規(guī)則在規(guī)則首頁處于的分類
                      recommended: true, // 配置文件中的 "extends""eslint:recommended"屬性是否啟用該規(guī)則
                      url: "https://eslint.org/docs/rules/no-with" // 指定可以訪問完整文檔的 url
                  },
                  
                  // fixable 如果沒有 fixable 屬性,即使規(guī)則實現(xiàn)了 fix 功能,ESLint 也不會進行修復。如果規(guī)則不是可修復的,就省略 fixable 屬性。

                  schema: [], // 指定該選項 這樣的 ESLint 可以避免無效的規(guī)則配置
                  
                  // deprecated (boolean) 表明規(guī)則是已被棄用。如果規(guī)則尚未被棄用,你可以省略 deprecated 屬性。

                  messages: {
                      unexpectedWith: "Unexpected use of 'with' statement."
                  }
              },
          // create (function) 返回一個對象,其中包含了 ESLint 在遍歷 js 代碼的抽象語法樹 AST (ESTree 定義的 AST) 時,用來訪問節(jié)點的方法。
              create(context) {
          // 如果一個 key 是個節(jié)點類型或 selector,在 向下 遍歷樹時,ESLint 調用 visitor 函數(shù)
          // 如果一個 key 是個節(jié)點類型或 selector,并帶有 :exit,在 向上 遍歷樹時,ESLint 調用 visitor 函數(shù)
          // 如果一個 key 是個事件名字,ESLint 為代碼路徑分析調用 handler 函數(shù)
          // selector 類型可以到 estree 查找
                  return {
                      // 入?yún)楣?jié)點node
                      WithStatement(node) {
                          
                          context.report({ node, messageId: "unexpectedWith" });
                      }
                  };

              }
          };

          有兩部分組成:meta create;

          meta:(對象)包含規(guī)則的元數(shù)據(jù),包括 規(guī)則的類型,文檔,是否推薦規(guī)則,是否可修復等信息;

          creat:(函數(shù))返回一個對象其中包含了 ESLint 在遍歷 JavaScript 代碼的抽象語法樹 AST (ESTree[2] 定義的 AST) 時,用來訪問節(jié)點的方法,入?yún)樵摴?jié)點。

          • 如果一個 key 是個節(jié)點類型或 selector[3],在 向下 遍歷樹時,ESLint 調用 visitor 函數(shù)
          • 如果一個 key 是個節(jié)點類型或 selector[4],并帶有 :exit,在 向上 遍歷樹時,ESLint 調用 visitor 函數(shù)
          • 如果一個 key 是個事件名字,ESLint 為代碼路徑分析[5]調用 handler 函數(shù)

          二、eslint 命令的執(zhí)行

          在package.json里配置bin

          "bin": {
            "eslint""bin/eslint.js" // 告訴 npm 你的命令是什么
          }

          然后創(chuàng)建對應的文件

          #!/usr/bin/env node
          console.log("console.log output")

          這就是eslint命令行的入口

          (async function main() {
              // 監(jiān)聽異常處理
              process.on("uncaughtException", onFatalError);
              process.on("unhandledRejection", onFatalError);

              // 如果參數(shù)有 --init 就執(zhí)行初始化
              if (process.argv.includes("--init")) {
                  await require("../lib/init/config-initializer").initializeConfig();
                  return;
              }

              // 否則就執(zhí)行 檢查代碼的代碼
              process.exitCode = await require("../lib/cli").execute(
                  process.argv,
                  process.argv.includes("--stdin") ? await readStdin() : null
              );
          }()).catch(onFatalError);

          代碼檢查的函數(shù)是 cli.execute() ****從lib中引入的cli對象。

          三、eslint 執(zhí)行的調用棧

          execute() 函數(shù)

          這是 eslint 的主要代碼執(zhí)行邏輯,主要流程如下:

          1. 解析命令行參數(shù),校驗參數(shù)正確與否及打印相關信息;
          2. 初始化 根據(jù)配置實例一個engine對象CLIEngine 實例;
          3. engine.executeOnFiles 讀取源代碼進行檢查,返回報錯信息和修復結果。
          execute(args, text) {
                  if (Array.isArray(args)) {
                      debug("CLI args: %o", args.slice(2));
                  }
                  let currentOptions;
                  try {
                  // 先校驗參數(shù) 如果輸入 --halp 提示 --help,并通過options的配置給默認值
                      currentOptions = options.parse(args);
                  } catch (error) {
                      log.error(error.message);
                      return 2;
                  }

                  const files = currentOptions._;
                  const useStdin = typeof text === "string";

                  // ---省略很多---參數(shù)校驗及輸出
                  // ...
                  // 根據(jù)配置實例一個engine對象
                  const engine = new CLIEngine(translateOptions(currentOptions));
                  // report 就是最后的結果
                  const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files);
                  // ...
                  // ---省略很多---參數(shù)校驗及輸出
                 
                  return 0;
              }

          可以看到eslint就是在執(zhí)行 engine.executeOnFiles(files) 之后獲得檢查的結果

          executeOnFiles (files) 函數(shù)

          可以看到eslint就是在執(zhí)行 engine.executeOnFiles(files) 之后獲得檢查的結果;該函數(shù)主要作用是對一組文件和目錄名稱執(zhí)行當前配置。

          簡單看一下 executeOnFile s ()

          該函數(shù)輸入文件目錄,返回lint之后的結果

          主要執(zhí)行邏輯如下:

          1. fileEnumerator 類,迭代所有的文件路徑及信息;
          2. 檢查是否忽略的文件,lint緩存 等等一堆操作;
          3. 調用 verifyText() 函數(shù)執(zhí)行檢查
          4. 儲存lint之后的結果
          /**
           * Executes the current configuration on an array of file and directory names.
           * @param {string[]} patterns An array of file and directory names.
           * @returns {LintReport} The results for all files that were linted.
           */
          executeOnFiles(patterns) {

              // .....
                  // fileEnumerator 類,迭代所有的文件路徑及信息
                  for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
                  
                  // ....... 檢查是否忽略的文件,緩存 等等一堆操作
                  
                      // Do lint.
                      const result = verifyText({
                          text: fs.readFileSync(filePath, "utf8"),
                          filePath,
                          config,
                          cwd,
                          fix,
                          allowInlineConfig,
                          reportUnusedDisableDirectives,
                          extensionRegExp: fileEnumerator.extensionRegExp,
                          linter
                      });

                      results.push(result);

                      /*
                       * Store the lint result in the LintResultCache.
                       * NOTE: The LintResultCache will remove the file source and any
                       * other properties that are difficult to serialize, and will
                       * hydrate those properties back in on future lint runs.
                       */
                      if (lintResultCache) {
                          lintResultCache.setCachedLintResults(filePath, config, result);
                      }
                  }
          }

          verifyText() 函數(shù)

          其實就是調用了 linter.verifyAndFix() 函數(shù)

          verifyAndFix() 函數(shù)

          這個函數(shù)是核心函數(shù),顧名思義verify & fix

          代碼核心處理邏輯是通過一個 do while 循環(huán)控制;以下兩個條件會打斷循環(huán)

          1. 沒有更多可以被fix的代碼了
          2. 循環(huán)超過十次
          3. 其中 verify 函數(shù)對源代碼文件進行代碼檢查,從規(guī)則維度返回檢查結果數(shù)組
          4. applyFixes 函數(shù)拿到上一步的返回,去fix代碼
          5. 如果設置了可以fix,那么使用fix之后的結果 代替原本的text
          /**
                   * This loop continues until one of the following is true:
                   *
                   * 1. No more fixes have been applied. 
                   * 2. Ten passes have been made.
                   * That means anytime a fix is successfully applied, there will be another pass.
                   * Essentially, guaranteeing a minimum of two passes.
                   */
                  do {
                      passNumber++; // 初始值0
                      // 這個函數(shù)就是 verify  在 verify 過程中會把代碼轉換成ast
                      debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
                      messages = this.verify(currentText, config, options);
                      // 這個函數(shù)就是 fix 
                      debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
                      fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);

                      /*
                       * 如果有 syntax errors 就 break.
                       * 'fixedResult.output' is a empty string.
                       */
                      if (messages.length === 1 && messages[0].fatal) {
                          break;
                      }

                      // keep track if any fixes were ever applied - important for return value
                      fixed = fixed || fixedResult.fixed;

                      // 使用fix之后的結果 代替原本的text
                      currentText = fixedResult.output;

                  } while (
                      fixedResult.fixed &&
                      passNumber < MAX_AUTOFIX_PASSES // 10
                  );

          在verify過程中,會調用 parse 函數(shù),把代碼轉換成AST

          // 默認的ast解析是espree
          const espree = require("espree");
           
          let parserName = DEFAULT_PARSER_NAME; // 'espree'
          let parser = espree;
          • parse函數(shù)會返回兩種結果

            • {success: false, error: Problem} 解析AST成功
            • {success: true, sourceCode: SourceCode} 解析AST失敗

          最終會調用 runRules() 函數(shù)

          這個函數(shù)是代碼檢查和修復的核心方法,會對代碼進行規(guī)則校驗。

          1. 創(chuàng)建一個 eventEmitter 實例。是eslint自己實現(xiàn)的很簡單的一個事件觸發(fā)類 on監(jiān)聽 emit觸發(fā);
          2. 遞歸遍歷 AST,深度優(yōu)先搜索,把節(jié)點添加到 nodeQueue。一個node放入兩次,類似于A->B->C->...->C->B->A;
          3. 遍歷 rules,調用 rule.create()(rules中提到的meta和create函數(shù)) 拿到事件(selector)映射表,添加事件監(jiān)聽。
          4. 包裝一個 ruleContext 對象,會通過參數(shù),傳給 rule.create(),其中包含 report() 函數(shù),每個rule的 handler 都會執(zhí)行這個函數(shù),拋出問題;
          5. 調用 rule.create(ruleContext), 遍歷其返回的對象,添加事件監(jiān)聽;(如果需要lint計時,則調用process.hrtime()計時);
          6. 遍歷 nodeQueue,觸發(fā)當前節(jié)點事件的回調,調用 NodeEventGenerator 實例里面的函數(shù),觸發(fā) emitter.emit()。
           // 1. 創(chuàng)建一個 eventEmitter 實例。是eslint自己實現(xiàn)的很簡單的一個事件觸發(fā)類 on監(jiān)聽 emit觸發(fā)
          const emitter = createEmitter();

          // 2. 遞歸遍歷 AST,把節(jié)點添加到 nodeQueue。一個node放入兩次 A->B->C->...->C->B->A
          Traverser.traverse(sourceCode.ast, {
                  enter(node, parent) {
                      node.parent = parent;
                      nodeQueue.push({ isEntering: true, node });
                  },
                  leave(node) {
                      nodeQueue.push({ isEntering: false, node });
                  },
                  visitorKeys: sourceCode.visitorKeys
              });
              
           // 3. 遍歷 rules,調用 rule.create() 拿到事件(selector)映射表,添加事件監(jiān)聽。
           // (這里的 configuredRules 是我們在 .eslintrc.json 設置的 rules)
           Object.keys(configuredRules).forEach(ruleId => {
                  const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
                  
                  // 通過ruleId拿到每個規(guī)則對應的一個對象,里面有兩部分 meta & create 見 【編寫rule】
                  const rule = ruleMapper(ruleId);
                
                  // ....

                  const messageIds = rule.meta && rule.meta.messages;
                  let reportTranslator = null;
                  // 這個對象比較重要,會傳給 每個規(guī)則里的 rule.create函數(shù)
                  const ruleContext = Object.freeze(
                      Object.assign(
                          Object.create(sharedTraversalContext),
                          {
                              id: ruleId,
                              options: getRuleOptions(configuredRules[ruleId]),
                              // 每個rule的 handler 都會執(zhí)行這個函數(shù),拋出問題
                              report(...args) {
                                  if (reportTranslator === null) {
                                      reportTranslator = createReportTranslator({
                                          ruleId,
                                          severity,
                                          sourceCode,
                                          messageIds,
                                          disableFixes
                                      });
                                  }
                                  const problem = reportTranslator(...args);
                                  // 省略一堆錯誤校驗
                                  // ....
                                  // 省略一堆錯誤校驗
                                  
                                  // lint的結果
                                  lintingProblems.push(problem);
                              }
                          }
                      )
                  );
                  // 包裝了一下,其實就是 執(zhí)行 rule.create(ruleContext);
                  // rule.create(ruleContext) 會返回一個對象,key就是事件名稱
                  const ruleListeners = createRuleListeners(rule, ruleContext);

                  /**
                   * 在錯誤信息中加入ruleId
                   * @param {Function} ruleListener 監(jiān)聽到每個node,然后對應的方法rule.create(ruleContext)返回的對象中對應key的value
                   * @returns {Function} ruleListener wrapped in error handler
                   */
                  function addRuleErrorHandler(ruleListener) {
                      return function ruleErrorHandler(...listenerArgs) {
                          try {
                              return ruleListener(...listenerArgs);
                          } catch (e) {
                              e.ruleId = ruleId;
                              throw e;
                          }
                      };
                  }

                  // 遍歷 rule.create(ruleContext) 返回的對象,添加事件監(jiān)聽
                  Object.keys(ruleListeners).forEach(selector => {
                      const ruleListener = timing.enabled
                          ? timing.time(ruleId, ruleListeners[selector]) // 調用process.hrtime()計時
                          : ruleListeners[selector];
                  // 對每一個 selector 進行監(jiān)聽,添加 callback
                      emitter.on(
                          selector,
                          addRuleErrorHandler(ruleListener)
                      );
                  });
              });
             
            // 只有頂層node類型是Program才進行代碼路徑分析
            const eventGenerator = nodeQueue[0].node.type === "Program"
                ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
                : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
            
            // 4. 遍歷 nodeQueue,觸發(fā)當前節(jié)點事件的回調。
            // 這個 nodeQueue 是前面push進所有的node,分為 入口 和 離開
              nodeQueue.forEach(traversalInfo => {
                  currentNode = traversalInfo.node;
                  try {
                      if (traversalInfo.isEntering) {
                          // 調用 NodeEventGenerator 實例里面的函數(shù)
                          // 在這里觸發(fā) emitter.emit()
                          eventGenerator.enterNode(currentNode);
                      } else {
                          eventGenerator.leaveNode(currentNode);
                      }
                  } catch (err) {
                      err.currentNode = currentNode;
                      throw err;
                  }
              });
           // lint的結果
           return lintingProblems;

          執(zhí)行節(jié)點匹配 NodeEventGenerator

          在該類里面,會根據(jù)前面 nodeQueque 分別調用 進入節(jié)點和離開節(jié)點,來區(qū)分不同的調用時機。

          // 進入節(jié)點 把這個node的父節(jié)點push進去
              enterNode(node) {
                  if (node.parent) {
                      this.currentAncestry.unshift(node.parent);
                  }
                  this.applySelectors(node, false);
              }
              // 離開節(jié)點
              leaveNode(node) {
                  this.applySelectors(node, true);
                  this.currentAncestry.shift();
              }
              // 進入還是離開 都執(zhí)行的這個函數(shù)
              // 調用這個函數(shù),如果節(jié)點匹配,那么就觸發(fā)事件
              applySelector(node, selector) {
                  if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
                      // 觸發(fā)事件,執(zhí)行 handler
                      this.emitter.emit(selector.rawSelector, node);
                  }
              }

          四、總體運行機制

          概括來說就是,ESLint 會遍歷前面說到的 AST,然后在遍歷到「不同的節(jié)點」或者「特定的時機」的時候,觸發(fā)相應的處理函數(shù),然后在函數(shù)中,可以拋出錯誤,給出提示。

          Tips: espree需要更換解析器

          問題:espree無法識別 TypeScript 的一些語法,所以在我們項目中的 .eslintrc.json 里才要配置

          {
           "parser"'@typescript-eslint/parser'
          }

          給eslint指定解析器,替代掉默認的解析器。

          eslint 中涉及到規(guī)則的校驗源碼調用棧大致就如上分析,但其實eslint遠不止這些,還有很多可以值得學習的點,如:迭代文件路徑、fix修復文本、報告錯誤及自定義格式等等,歡迎感興趣的同學一起討論交流,也歡迎同學批評指正~

          參考資料

          https://zhuanlan.zhihu.com/p/53680918

          https://juejin.cn/post/7054741990558138376

          https://www.teqng.com/2022/03/14/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-eslint-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/#ESLint_shi_ru_he_gong_zuo_de

          參考資料

          [1]

          我們看一下自定義eslint 規(guī)則: https://eslint.bootcss.com/docs/developer-guide/working-with-rules

          [2]

          ESTree: https://github.com/estree/estree

          [3]

          selector: https://eslint.bootcss.com/docs/developer-guide/selectors

          [4]

          selector: https://eslint.bootcss.com/docs/developer-guide/selectors

          [5]

          代碼路徑分析: https://eslint.bootcss.com/docs/developer-guide/code-path-analysis


          ?? 謝謝支持

          以上便是本次分享的全部內容,希望對你有所幫助^_^

          喜歡的話別忘了 分享、點贊、收藏 三連哦~。

          歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~

          我們來自字節(jié)跳動,是旗下大力教育前端部門,負責字節(jié)跳動教育全線產(chǎn)品前端開發(fā)工作。

          我們圍繞產(chǎn)品品質提升、開發(fā)效率、創(chuàng)意與前沿技術等方向沉淀與傳播專業(yè)知識及案例,為業(yè)界貢獻經(jīng)驗價值。包括但不限于性能監(jiān)控、組件庫、多端技術、Serverless、可視化搭建、音視頻、人工智能、產(chǎn)品設計與營銷等內容。

          歡迎感興趣的同學在評論區(qū)或使用內推碼內推到作者部門拍磚哦 ??

          字節(jié)跳動校/社招投遞鏈接: https://jobs.bytedance.com/campus/position?referral_code=BA6TQ9U

          內推碼:BA6TQ9U

          往期推薦



          零基礎理解 ESLint 核心原理


          自定義 ESLint 規(guī)則,讓代碼持續(xù)美麗

          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品无码久久 | 亚洲毛片A片 | 久久无人区无码 | 国产成人精品网站 | 亚洲大几吧色色91视频 |