百度工程师手把手教你实现代码规范检测工具
01 引言
代碼規(guī)范是軟件開發(fā)領域經(jīng)久不衰的話題。在前端領域中,說到代碼規(guī)范,我們會很容易想到檢查代碼縮進、尾逗號以及分號等等,除此之外,代碼規(guī)范還包括了針對特殊場景定制化的檢查。JavaScript 代碼規(guī)范檢查工具包括 JSLint、JSHint、ESLint、FECS 等等,樣式代碼規(guī)范檢查工具主要為 StyleLint。
02 背景
san-native 是百度 APP 內部的一套動態(tài) NA 視圖框架,利用 JS 引擎驅動 NA 端渲染,使得 web 前端工程師可以十分方便的編寫原生移動應用,一套代碼多端運行。隨著百度 APP 中越來越多的業(yè)務開始接入 san-native,在此過程中,經(jīng)常遇到 h5 中的一些樣式屬性以及事件在 san-native 中不支持,不按照 san-native 中內置組件嵌套規(guī)則的代碼導致渲染結果不符合預期。比如下面一段.san 文件中的代碼存在多處錯誤會導致端上渲染不正常甚至導致 crash:
因此,為了能夠在編碼階段提前發(fā)現(xiàn)這些問題,我們需要對代碼進行一些特殊的檢測,包括樣式,事件,以及嵌套規(guī)則。為了實現(xiàn)這樣的功能,我們啟動了 san-native-linter 項目,該項目中包含了兩個相互獨立的插件:@baidu/eslint-plugin-san-native 以及 @baidu/stylelint-plugin-san-native,我們將逐一介紹其實現(xiàn)原理。
03 抽象語法數(shù)(AST)
首先我們需要了解代碼檢測的主角 —— 抽象語法樹 (Abstract Syntax Tree)。在計算機科學中,抽象語法樹簡稱 AST,它是源代碼語法結構的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結構,樹上的每個節(jié)點都表示源代碼中的一種結構。
將字符串源碼轉換成 AST 的工具稱為解析器,常見的 Javascript 解析器有 @babel/parser,espree,acorn 等,樣式解析器有 postcss,cssTree 等。AST 的生成有兩個步驟:
- 詞法分析(分詞):將整個代碼字符串分割成最小語法單元數(shù)組
- 語法分析:在分詞基礎上建立分析語法單元之間的關系
我們可以通過在線工具 [1] 查看一段代碼的 AST,比如下圖所示的 AST,圖中用到的解析器為 @babel/eslint-parsre,右側所示的對象為左側代碼對應的 AST,該 AST 的根節(jié)點 type 為 Program,其 body 中有兩個子節(jié)點,分別為 import 以及 export 對應的語法節(jié)點,其 type 分別為:ImportDeclaration 與 ExportDefaultDeclaration。每個節(jié)點中 range 表示當前節(jié)點對應的代碼在字符串源碼中的開始與結束位置,loc 為開始與結束位置的行列信息。
04eslint-plugin-san-native
介紹 eslint-plugin-san-native 插件的實現(xiàn)之前,我們會先介紹 ESlint 中的規(guī)則 (rule),ESlint 配置與復用方案以及 ESlint 的運行原理,最后介紹插件如何實現(xiàn)以及關鍵的技術點。
ESlint 中的規(guī)則(rule)
上文中已經(jīng)通過簡單的例子介紹了抽象語法樹的結構,并且在引言部分已經(jīng)簡述了 ESlint 檢測代碼的核心思想,即對 AST 進行處理從而定位不符合規(guī)定的代碼,在 ESlint 中對 AST 進行處理的實體就是這里所說的規(guī)則(rule),下面給出了一個規(guī)則的示例代碼:
module.exports = {meta: {type: "problem",docs: {...},schema: [],messages: {readonlyMember: "The members of '{{name}}' are read-only."}},create(context) {return {ImportDeclaration(node) {context.report({node: node,messageId: "readonlyMember",data: {name: 'xxx'}});}};} };在這樣的一個規(guī)則中,我們需要導出一個對象,包括 meta 屬性以及 create 方法,前者用于標記該 rule 的一些信息,后者則用于處理 AST 某個節(jié)點,并提供錯誤信息與出錯的節(jié)點。
- meta 中,通過 type 標記規(guī)則的類型,docs 包含了規(guī)則的文檔鏈接等信息,schema則表示了配置規(guī)則應該遵守的約定,messages 包含了錯誤信息。
- create 函數(shù):
1.返回的對象中具有一個名為 ImportDeclaration 的方法,我們將該方法稱之為 import 語法節(jié)點的訪問器 (visitor),即在 ESlint 對整個 AST 遍歷的過程中,訪問到 import 語法節(jié)點的時候會調用所有名稱為 ImportDeclaration 的 visitor。
2.create 函數(shù)接收的參數(shù)為 context 對象,該對象上掛載了 ESlint/ 自定義解析器為 rule 提供的方法以及用戶配置文件中的自定義配置信息,更多的屬性與方法見官方文檔。這里我們調用 context.report 將錯誤信息以及對應的語法節(jié)點提供給 ESLint。
ESlint 配置的復用方案
上文介紹了 ESlint 中的規(guī)則,在實際的工程應用中我們可以通過對規(guī)則進行定制化的配置來滿足特定的需求,但是如果每啟動一個項目,我們都需要進行相同的配置,勢必會帶來一定的時間成本。ESLint 提供了全面、靈活的配置能力,可以配置解析器、規(guī)則、環(huán)境、全局變量等等;可以快速引入另一份配置,和當前配置層疊組合為新的配置;還可以將配置好的規(guī)則集發(fā)布為 npm 包,在工程內快速應用。接下來,我們以 @ecomfe/eslint-config 為例看看如何高效的實現(xiàn)配置的復用,下圖為該代碼庫的目錄結構:
在 @ecomfe/eslint-config 中,每個 js 文件都是一份 eslint 的配置,根目錄下的入口文件 index.js 為基礎配置 (base),其他文件夾可以看作是對基礎配置的擴展,比如 san 文件夾下是關于 san 的一些規(guī)則的配置,在實際的項目中可以通過下面的方式引入:
module.exports = {extends: ['@ecomfe/eslint-config','@ecomfe/eslint-config/san', // 注意順序// 或者選擇嚴格模式// '@ecomfe/eslint-config/san/strict',], };ESlint 會將 extends 字段中的所有配置文件合并起來,每個配置文件包含如下幾個內容:
module.exports = {parser: 'xxx',parserOptions: {parser: 'yyy',sourceType: 'module'},plugins: [],env: {},rules: {'indent-legacy': 'off',} };在這樣一個配置文件中,各個字段的含義如下:
- parser:用于申明自定義 parser,該 parser 會將文件內容轉換成 AST
- parserOptions:自定義 parser 的配置項
- plugins:申明使用的 ESlint 插件,這部分會在后面 ESlint 工作原理介紹
- env:申明檢測所處的環(huán)境,該選項用于引入一組預定義的全局變量
- rules:對規(guī)則的配置
ESlint 中插件與配置文件的區(qū)別
上文中依次介紹了 ESlint 的規(guī)則的實現(xiàn)以及 ESlint 配置的復用,本節(jié)我們說明插件與配置文件之間的區(qū)別,ESlint 插件的入口文件示例代碼如下:
module.exports = {rules: {'no-style-float': require('./rules/no-style-float'),// ...},processors: {'.san': require('./processor'),// ...},configs: {always: {plugins: ['@baidu/san-native'],rules: {'@baidu/san-native/no-style-float': 'error',}},// ...} };在這個入口文件中,我們向 ESlint 提供了一個對象,該對象中包含的屬性有:
- rules:包含了該插件所有的規(guī)則的具體實現(xiàn)
- processors:這里我們定義了專門用于處理.san 文件字符串源碼以及檢查信息的 processor
- configs:包含了一些配置,可以看到與 @ecomfe/eslint-config 中的配置文件類似,具備 plugins選項,以及對 rules 的配置。
需要說明的是:為了復用上面 configs.always 的配置,我們可以在項目的.eslintrc.* 文件中 extends 選項加上如下代碼:
module.exports = {extends: ['plugin:@baidu/san-native/always'] };因此 ESlint 插件以及配置文件的區(qū)別可以總結如下:
- plugin 插件主要涉及自定義規(guī)則的具體實現(xiàn),同時還能夠提供配置
- extend 配置主要涉及規(guī)則的具體配置
ESlint 的工作原理
接下來介紹 ESlint 是如何處理各種配置文件的,以及插件與配置文件中各字段在 ESlint 中的作用。ESlint 提供了命令行的方式來檢測某個文件的代碼,比如,我們想對.san 文件進行檢查,那么可以通過下面的命令來實現(xiàn):
eslint --ext .san src/app/component/animate/index.san當我們執(zhí)行命令行的時候,ESlint 的工作原理如下圖所示:
從上圖當中我們可以看到,文件的字符串內容首先會被插件的 prepocess 處理,然后處理的結果被 parser 解析成對應的 AST,然后遍歷 AST 的同時執(zhí)行每個規(guī)則提供的方法,最后得到的檢測結果會被 postprocess 處理。因此 ESlint 插件中的 processors 屬性給開發(fā)者提供了操作字符串源碼以及處理檢測結果的能力。接下來分析 ESlint 配置中的一些字段在檢測過程中的作用:
*處理.eslintrc.配置文件
將文件內容解析成 AST
收集與執(zhí)行 rule 生成的 visitor
當解析器將字符串解析成 AST 之后,在遍歷 AST 的過程中會根據(jù)當前的節(jié)點類型執(zhí)行對應的一些列提前注冊好的 vistor。
eslint-plugin-san-native 的實現(xiàn)
經(jīng)過上述 ESlint 的工作原理的了解之后,我們開始介紹如何實現(xiàn) eslint-plugin-san-native 來解決我們的問題,以下面的單文件組件為例:
<template><div></div> </template> <script>import Test from './index.san';export default {} </script> <style lang="less">.a {} </style>有以下兩點需要考慮的地方
項目構建
構建的項目目錄結構如下
入口文件 eslint-plugin-san-native/lib/index.js 的代碼如下:
module.exports = {rules: {'no-style-float': require('./rules/no-style-float'),// ...},processors: {'.san': require('./processor'),// ...},configs: {always: {plugins: ['@baidu/san-native'],rules: {'@baidu/san-native/no-style-float': 'error',}},// ...} };下面我們分別實現(xiàn) processor 以及 rule。
processor 的實現(xiàn)
根據(jù) ESlint 工作原理可知,ESlint 在獲取到字符串源碼的時候,會先利用插件提供的 preprocess 處理字符串源碼,接著利用 parser 解析成 ast,然后將各個 ast 節(jié)點交給 rule 處理,接著處理后的檢測結果交給 postprocess 處理,最后再執(zhí)行 fix。因此,從 preprocess 到 postprocess 的過程中,處理的文件內容是不變的(ast 會被 Object.freeze 處理),因此,我們可以在 preprocess 中將.san 中的 style 獲取之后,利用 postcss 將其解析成 ast,并存儲起來供后續(xù)所有 rule 共享。
獲取 style對應的 AST
const postcss = require('postcss'); const syntaxs = {less: require('postcss-less'),sass: require('postcss-sass'),scss: require('postcss-scss') }; const processor = postcss([() => {}]); module.exports = {getAst(syntax, content, plugins) {let ast = null;try {ast = syntax ? processor.process(content, {syntax}).sync(): processor.process(content).sync();} catch (error) {}return ast;},getStyleContentInfo(text) {const lines = text.split('\n');const content = /()([\s\S]*?)<\/style>/gi.exec(text);const langMatch = /\slang\s*=\s*("[^"]*"|'[^']*')/.exec(content[1]);const lang = langMatch[1].replace(/["'\s]/gi, '');const astFn = lang ? this.getAst : this.getAst.bind(null, syntaxs[lang]);return {startLine: lines.indexOf(content[1]),ast: astFn(content[3]),startOffset: text.indexOf(content[3])};} };上面的代碼根據(jù) style中的 lang 字段,調用不同的 parser 對樣式內容進行解析,并獲取到代碼塊 style 所在的行數(shù) startLine 以及所處文件的位置 startOffset,這些數(shù)據(jù)都是用來修正樣式 ast 節(jié)點位置的,這樣 eslint 才能在輸出錯誤信息的時候找到樣式在文件中的真實位置。當然這里也可以直接利用 postcss-syntax 提供的 syntax 傳入 postcss(defaultPlugins).process 函數(shù)中,該 syntax 可以自動根據(jù)文件名稱或者代碼內容自動選擇正確的語法解析器。
processor
const {styleAstCache} = require('./utils/cache'); module.exports = {preprocess(code, filename) {// 所有.san 都會處理styleAstCache.storeAst(styleHelper.getStyleContentInfo(code));return [code];},postprocess(messages) {// 清除數(shù)據(jù)styleAstCache.storeAst(null);return messages[0];} };在各個規(guī)則中只需要引入 styleAstCache,并調用 styleAstCache.getAst 即可獲取到樣式代碼的 AST,styleAstCache 在這里只是用于存儲 ast 而已。
規(guī)則的實現(xiàn)
由于規(guī)則的實現(xiàn)依賴于自定義 parser 提供的 ast,因此我們需要先對 san-eslint-parser 的原理有一定的了解,那么我們將現(xiàn)分析其原理,然后介紹幾類規(guī)則的實現(xiàn)方案。
san-eslint-parser
自定義 parser 需要提供 parseForESLint 方法,我們這里只關注該方法返回結果中的部分屬性 (更多屬性見官方):
- ast:AST 根節(jié)點
- services:自定義 parser 為 rule 提供的服務,每條規(guī)則可以通過 context.parserServices 訪問到
ast
san-eslint-parser 會將我們.san 的文件內容利用分成三個 block,其中利用 parserOPtions.parser 指定的解析器來處理 script 部分的內容,script 中如果是 JavaScript 代碼則 parserOPtions.parser 為 @babel/eslint-parser,如果是 Typescript 代碼則為 @typescript-eslint/parser。style 部分不會處理,template 部分當作 HTML 來解析。
上圖所示為自定義 parser 生成的 AST,根節(jié)點的 type 為 Program,根節(jié)點的 body 屬性存儲了 script 代碼的 ast,根節(jié)點上的 templateBody 為 template 部分的 ast。由于 ESlint 只會遍歷根節(jié)點以及 body 上的節(jié)點,因此如果我們想為 templateBody 注冊 visitor,那么可以通過 services 來實現(xiàn)。
services
san-eslint-parser 會在 services 屬性上定義三個方法,我們只關注其中一個,簡化后的代碼如下:
let emitter = null; // 發(fā)布訂閱器 function defineTemplateBodyVisitor(templateBodyVisitor) {let scriptVisitor = {};if (!emitter) {emitter = new EventEmitter();scriptVisitor["Program"] = node => {traverseNodes(rootAST.templateBody)};}for (const selector of Object.keys(templateBodyVisitor)) {emitter.on(selector, templateBodyVisitor[selector]);}return scriptVisitor; }該方法主要完成了兩部分的工作:
因此,我們可以利用上述方法在每條 rule 中編寫相關的 visitor 來處理 templateBody 中不同 type 類型語法節(jié)點,如下代碼所示:
module.exports = {meta: {...},create(context) {return context.parserServices.defineTemplateBodyVisitor(context, {'VElement'(node) {// do something},'VText'(node) {// do something}});} };屬性規(guī)則
在 template 模板中,我們需要檢測某個標簽上的事件,內聯(lián)樣式,必選屬性三種內容,為了避免重復代碼,希望通過配置的方式實現(xiàn)規(guī)則。首先定義內置組件的描述信息,舉例來說:
{"name": "lottie-view", // 標簽名稱"events": [ // 支持的事件"click","touchstart","touchmove","touchend","touchcancel","layout","longclick","pressin","pressout","firstmeaningfulpaint","animationfinish","downloadfinish"],"attributes": { "required": [],"oneOf": [["src", "source"]], // 必須的可選屬性"content": {}},"style": {"required": [],"notsurpport": []},"nestedTag": [] // 允許的子標簽 }上面的描述信息中依次定義了標簽名稱,支持的事件,必選的屬性,不支持 / 必須的內聯(lián)樣式,以及允許的子標簽名稱。描述信息的另一個優(yōu)勢是當組件庫更新或者添加組件的時候,只需要在組件中維護這樣的信息,則可以在不發(fā)布新版本 ESlint 插件的時候應用到組件新的規(guī)則。
在上文中已經(jīng)介紹了如何在規(guī)則中通過編寫相關的 visitor 來處理 templateBody 中不同 type 類型語法節(jié)點,因此我們只需要對節(jié)點的相關數(shù)據(jù)進行一些判斷,就可以實現(xiàn)代碼檢測。判斷的邏輯這里不再介紹,只貼上一個 VElement 中需要關注的節(jié)點屬性:
上圖中是下面標簽對應的節(jié)點數(shù)據(jù),為了獲取標簽的屬性,我們可以從 startTag.attributes 中獲取,可以看到其中屬性名稱為 style 的節(jié)點數(shù)據(jù)。
<div style="background-color:#fff; flex:1">...</div>樣式規(guī)則
對于樣式規(guī)則來說,我們需要同時檢測 tempate 上的內聯(lián)樣式,也需要檢測 style塊中的樣式代碼,簡化后的規(guī)則代碼如下:
module.exports = {meta: {...},create(context) {return context.parserServices.defineTemplateBodyVisitor(context, {'VElement[name="template"]'(node) {const {ast: result, startLine, startOffset} = styleAstCache.getAst();if (result && result.root) {result.root.walkDecls(decl => {// do something});}},VAttribute(node) {const name = utils.getName(node);if (name == null) {return;}if (name === 'style' && node.value && node.value.value) {let styleValArr = inlineStyleParser(node.value.value);styleValArr.forEach(decl => {// do something});}}});} };可以看到我們對 template 對應的 AST 定義了兩個 visitor,第一個 visitor 用于獲取 VElement 并且節(jié)點名稱是 template 的語法節(jié)點,在該節(jié)點的 visitor 中,利用 postcss 提供的 API 遍歷 style對應的 ast。第二個 visitor 用于檢測 template 中每個標簽上 style 屬性中的內聯(lián)樣式。
在 vscode 中的檢測效果
在每一條規(guī)則中,當發(fā)現(xiàn)不符合規(guī)則的代碼時,我們可以通過 context.report 將對應的 ast 語法節(jié)點 / 位置信息以及錯誤信息提供給 ESlint
// 提供節(jié)點 context.report({node: node,messageId: "..." }); // 提供位置信息:loc context.report({loc: node.loc,message: "..." });這樣 ESlint 能夠通過 vscode 中的插件對錯誤代碼進行高亮,從而實現(xiàn)在編譯前提示代碼中不支持的樣式,事件,嵌套規(guī)則等等。
05stylelint-plugin-san-native
到此,我們介紹了如何開發(fā) ESlint 插件檢測.san/.js/.ts 文件中的 san 組件,下面介紹如何開發(fā) StyleLint 插件來檢測.less/.sass/.scss/.styl 文件中的樣式代碼。StyleLint 提供了類似 ESlint 的配置方式,可以在配置文件.stylelintrc.* 中 extends 多個配置文件,對單個 rule 進行配置,支持通過編寫插件實現(xiàn)自定義的規(guī)則,支持使用 processor 在開始檢測之前對源碼字符串進行修改,并在結果輸出之前對檢測結果進行修改。
StyleLint 工作原理
下圖為 StyleLint 的工作流程圖,這里的 processor.code 相當于 ESlint 中的 preprocess,而 processor.result 相當于 ESlint 中的 postprocess。StyleLint 與 ESlint 的工作原理非常相似,從整體上來說,processor.code 與 processor.result 之間的過程與 ESLint 有區(qū)別,StyleLint 中會遍歷所有 rules,然后將 AST 根節(jié)點交給每個 rule 進行遍歷,而不像 ESlint 中需要自己遍歷 AST。
StyleLint 規(guī)則
從上文 StyleLint 的工作流程分析可以知道,StyleLint 的規(guī)則接收一個 AST 根節(jié)點以及配置數(shù)據(jù),因此其規(guī)則示例代碼如下:
module.exports = function rule(primary, secondary, context) {return (root, result) => {}; };其中,primary 以及 secondary 為 rule 配置的時候填寫的配置,舉例來說:
"rules": {"block-no-empty": null, // primary 為 null"color-no-invalid-hex": true, // primary 為 true"comment-empty-line-before": [ "always", // primary 為 always{"ignore": ["stylelint-commands", "between-comments"]} // secondary] }StyleLint 插件
只需要將 rule 利用 StyleLint 提供的方法處理后即可生成一個插件,并且需要提供 ruleName 以及 messages
const stylelint = require("stylelint"); const ruleName = "plugin/xxx"; const messages = stylelint.utils.ruleMessages(ruleName, {expected: "Expected ..." }); module.exports = stylelint.createPlugin(ruleName,function (primary, secondary, context) {return function (root, result) {// ...stylelint.utils.report({/* .o. */});};} ); module.exports.ruleName = ruleName; module.exports.messages = messages;stylelint-plugin-san-native 實現(xiàn)
構建的項目目錄結構如下:
其中入口文件 index.js 的簡化代碼如下:
module.exports = [stylelint.createPlugin(...),stylelint.createPlugin(...),// ... ]同時提供了兩份配置文件分別為:always.js 以及 temporary.js,下面為 always.js 的代碼:
module.exports = {plugins: ['.'],rules: {'@baidu/stylelint-plugin-san-native/no-flex-basis': true,// ...} };在實際工程項目的.stylelintrc.js 中可以通過 extends 字段復用配置文件,比如:
module.exports = {extends: ['@baidu/stylelint-plugin-san-native/always','@baidu/stylelint-plugin-san-native/temporary'],rules: {}由于篇幅有限,我們只對一個具體的規(guī)則實現(xiàn)進行介紹,比如在 san-native 中并不支持樣式屬性 justify-content 的值被設置為 baseline,因此我們需要對該屬性的值進行檢測以及報錯處理,規(guī)則的部分關鍵代碼如下:
const {utils} = require('stylelint'); const getDeclarationValue = require('stylelint/lib/utils/getDeclarationValue'); const declarationValueIndex = require('stylelint/lib/utils/declarationValueIndex'); const valueParser = require('postcss-value-parser'); const meta = {styleName: 'justify-content',message: `Only some values of '${styleName}' are supported in sna-native`,surrpportValue: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around'] }; const ruleName = `stylelint-plugin-san-native/valid-justify-content`; const messages = utils.ruleMessages(ruleName, {expected: () => meta.message }); module.exports = function rule(primary) {return (root, result) => {const validOptions = utils.validateOptions(result, ruleName, {primary});if (!validOptions || !primary) { return; }root.walkDecls(decl => {// 將declaration語法節(jié)點上屬性鍵值對解析成ASTconst parsed = valueParser(getDeclarationValue(decl));// 遍歷每個屬性值對應的節(jié)點parsed.walk(node => {if (meta.surrpportValue.indexOf(node.value) < 0) {utils.report({// 獲取declaration語法節(jié)點中屬性值部分在與declaration語法節(jié)點開始位置的偏移量index: declarationValueIndex(decl) + node.sourceIndex,message: messages.expected(),node: decl,ruleName,result});}});});}; }; module.exports.ruleName = ruleName; module.exports.messages = messages;在對代碼進行分析之前,我們需要了解 postcss 返回的 AST 的兩個關鍵點:
1.屬性聲明與賦值會被解析成類型為 declaration 的語法節(jié)點,舉例如下:
justify-content: baseline;2.可以通過 AST 上的 walkDecls 方法獲取 AST 樹中的每個類型為 declaration 的語法節(jié)點,該方法是由 postcss 提供,更多的方法可見 postcss 官方文檔
上面代碼中 rule 函數(shù)利用 root.walkDecls 遍歷語法樹中的 declaration 語法節(jié)點,并且每個 declaration 語法節(jié)點會被傳入 root.walkDecls 接收的回調函數(shù)中,在該回調函數(shù)中如果發(fā)現(xiàn)屬性值在 san-native 中不支持,則需要通過 stylelint.utils.report 將錯誤信息,發(fā)生錯誤的節(jié)點,以及屬性值偏移量,當前規(guī)則名稱傳遞給 StyleLint,這樣 StyleLint 才能夠定位到不規(guī)范代碼的位置。同時借助編輯器插件將不符合代碼規(guī)范的代碼高亮出來,以 vscode 為例,進行如下的高亮提示:
06 總結
至此,我們介紹了如何實現(xiàn) ESlint 以及 StyleLint 的插件來檢測 san-native 項目中不符合規(guī)定的代碼,并從底層原理的角度上介紹了插件里各個字段以及方法在檢測過程中的作用,希望能對大家有所幫助。
文章看完,還不過癮?
更多精彩內容歡迎關注百度開發(fā)者中心公眾號
總結
以上是生活随笔為你收集整理的百度工程师手把手教你实现代码规范检测工具的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百度一款前端图片合成工具库MI开源啦!
- 下一篇: 工程师必知的代码重构指南