摘要:自定義規(guī)則校驗代碼業(yè)務(wù)邏輯是社區(qū)中主流的工具,提供的大量規(guī)則有效的保障了許多項目的代碼質(zhì)量。本文將介紹如何通過自定義檢查規(guī)則,校驗項目中特有的一些業(yè)務(wù)邏輯,如特殊作用域特殊使用規(guī)范性等。
自定義 eslint 規(guī)則校驗代碼業(yè)務(wù)邏輯
eslint 是 JavaScript 社區(qū)中主流的 lint 工具,提供的大量規(guī)則有效的保障了許多項目的代碼質(zhì)量。本文將介紹如何通過自定義 eslint 檢查規(guī)則,校驗項目中特有的一些業(yè)務(wù)邏輯,如 i18n、特殊作用域、特殊 API 使用規(guī)范性等。代碼靜態(tài)分析與 eslint
代碼靜態(tài)分意指是不需要實際執(zhí)行代碼就能獲取到程序中的部分信息并加以使用,lint 就是其中一種常見的實踐,通常為檢查代碼中錯誤的寫法或是不符合標(biāo)準(zhǔn)的代碼風(fēng)格。許多編程語言都自帶 lint 工具,甚至直接將其植入到編譯器中。
但這一重要的功能對于 JavaScript 來說卻是一大痛點,作為動態(tài)且弱類型的語言 JavaScript 沒有編譯階段也就無從進(jìn)行靜態(tài)分析,這導(dǎo)致程序錯誤只能在運(yùn)行時被發(fā)現(xiàn),部分錯誤非常低級例如variable is undefined。而當(dāng)程序變得更為復(fù)雜時,這類錯誤甚至難以在開發(fā)、測試階段暴露,只會在用戶實際使用的過程中遇到,造成嚴(yán)重的后果。
為了彌補(bǔ)語言天生的弱點,社區(qū)開發(fā)出了一些 lint 工具,在所謂預(yù)編譯階段完成代碼的靜態(tài)分析檢查,而 eslint 就是其中的佼佼者。現(xiàn)在社區(qū)已經(jīng)普遍接受使用 eslint 作為代碼規(guī)范工具,也延伸出了許多常用的規(guī)則與規(guī)則集。但實際上 eslint 拓展性極佳,我們還可以基于 eslint 提功的靜態(tài)分析能力對代碼進(jìn)行業(yè)務(wù)邏輯的檢查,本文將講解一些筆者所在項目中的靜態(tài)分析實踐,以說明這一方案的適用場景和優(yōu)缺點。
eslint 基本原理首先快速說明 eslint 工作的基本流程,幫助理解它將給我們提供哪些方面的能力以及如何編寫我們的自定義規(guī)則。
配置規(guī)則與插件eslint 主要依靠配置決定執(zhí)行哪些規(guī)則的校驗,例如我們可以通過配置no-extra-semi決定是否需要寫分號,這類規(guī)則中不包含具體的業(yè)務(wù)邏輯,而是對所有項目通用,因此會被集成在 eslint 的內(nèi)置規(guī)則中。
而還有一些規(guī)則也不包含業(yè)務(wù)邏輯,但只在部分項目場景中使用,如 React 相關(guān)的大量規(guī)則,那么顯然不應(yīng)該集成在內(nèi)置規(guī)則中,但也應(yīng)該自成一個集合。這種情況下 eslint 提供了另一種規(guī)則單位——插件,可以作為多個同類規(guī)則的集合被引入到配置中。
如果我們準(zhǔn)備自定義一些規(guī)則用于校驗項目中的業(yè)務(wù)邏輯,那么也應(yīng)該創(chuàng)建一套自用的插件,并將自用的規(guī)則都存放其中。推薦使用 eslint 的 yeoman generator 腳手架新建插件或規(guī)則,該腳手架能夠生成插件項目的目錄結(jié)構(gòu)、規(guī)則文件、文檔以及單元測試等模版,下文中我們將通過示例理解這些文件的的作用。
JavaScript 解析如上文所說,要實現(xiàn)靜態(tài)分析則需要自建一個預(yù)編譯階段對代碼進(jìn)行解析,eslint 也不例外。
首先我們看看大部分編譯器工作時的三個階段:
解析,將未經(jīng)處理的代碼解析成更為抽象的表達(dá)式,通常為抽象語法樹,即 AST。
轉(zhuǎn)換,通過修改解析后的代碼表達(dá)式,將其轉(zhuǎn)換為符合預(yù)期的新格式。
代碼生成,將轉(zhuǎn)換后的表達(dá)式生成為新的目標(biāo)代碼。
如果想快速的加深對編譯器工作原理的理解,推薦閱讀 the-super-tiny-compiler。
對于 eslint 而言,主要是將 JavaScript 代碼解析為 AST 之后,再在遍歷 AST 的過程中對代碼進(jìn)行各個規(guī)則的校驗。因此 eslint 也有一個解析器用于將原始代碼解析為特定的 AST,目前所使用的解析器是 eslint 基于 Acorn 開發(fā)的一個名為 Espree 的項目。而對于我們編寫自定義規(guī)則來說更關(guān)心的是解析器生成的 AST 節(jié)點的結(jié)構(gòu),在閱讀 eslint 文檔之后會了解到包括 Espree 在內(nèi)的許多編譯器項目都需要一套 JavaScript 的 AST 規(guī)范,而為了保證規(guī)范的一致性以及實效性,社區(qū)共同維護(hù)了一套規(guī)范:estree。
在接下來講解規(guī)則編寫與執(zhí)行的過程中,我們將直接引用 estree 的各種 AST 結(jié)構(gòu)。
規(guī)則的執(zhí)行eslint 中一般一個規(guī)則存放在一個文件中,以 module 的形式導(dǎo)出并掛載,其結(jié)構(gòu)如下:
module.exports = { meta: { docs: { description: "disallow unnecessary semicolons", category: "Possible Errors", recommended: true, url: "https://eslint.org/docs/rules/no-extra-semi", }, fixable: "code", schema: [], // no options }, create: function(context) { return { // callback functions }; }, };
其中meta部分主要包括規(guī)則的描述、類別、文檔地址、修復(fù)方式以及配置下 schema 等信息,對于項目中自用的規(guī)則來說可以只填寫基本的描述和類別,其余選項在有需要時再根據(jù)文檔補(bǔ)充,并不會影響規(guī)則的檢驗邏輯。
而create則需要定義一個函數(shù)用于返回一個包含了遍歷規(guī)則的對象,并且該函數(shù)會接收context對象作為參數(shù),context對象中除了包含report等報告錯誤的方法之外,還提供了許多幫助方法,可以簡化規(guī)則的編寫。下文中我們會通過幾個示例理解create函數(shù)的使用方式,但首先可以通過一段代碼建立初步的印象:
module.exports = { create: function(context) { // declare the state of the rule return { ReturnStatement: function(node) {}, "FunctionExpression:exit": function(node) {}, "ArrowFunctionExpression:exit": function(node) {}, }; }, };
在這段代碼中我們可以看到create返回的所謂“包含了遍歷規(guī)則的對象”的基本結(jié)構(gòu)。對象的 value 均為一個接收當(dāng)前 AST 節(jié)點的函數(shù),而 key 則是 eslint 的節(jié)點 selector。selector 分為兩部分,第一部分為必須聲明的 AST 節(jié)點類型,如ReturnStatement和FunctionExpression。第二部分則是可選的:exit標(biāo)示,因為在遍歷 AST 的過程中會以“從上至下”再“從下至上”的順序經(jīng)過節(jié)點兩次,selector 默認(rèn)會在下行的過程中執(zhí)行對應(yīng)的訪問函數(shù),如果需要再上行的過程中執(zhí)行,則需要添加:exit。
那么 eslint 解析出的 AST 有哪些節(jié)點類型,每種節(jié)點的數(shù)據(jù)結(jié)構(gòu)又是什么,則需要通過查看上文提到的 estree 定義文檔進(jìn)行了解。
適用場景與示例接下來我們會看到 eslint 自定義規(guī)則校驗的一些具體示例,但首先我們先要明確它的適用場景以及與一些常見代碼 QA 手段的異同。
適用場景我們可以通過以下方法判斷一個工具的質(zhì)量:
工具質(zhì)量 = 工具節(jié)省的時間 / 開發(fā)工具消耗的時間
對于靜態(tài)分析來說,要想提高“工具節(jié)省的時間”,應(yīng)該要讓檢查的規(guī)則盡量覆蓋全局性的且經(jīng)常發(fā)生的問題,如使用最為廣泛的檢查:是否使用了未定義的變量。同時還需要考慮當(dāng)問題發(fā)生后 debug 所消耗的時間,例如有的項目有 i18n 需求,而在代碼的個別地方又直接使用了中文的字符串,雖然問題很小,但是人工測試覆蓋卻很麻煩,如果能夠通過工具進(jìn)行覆蓋,那么原來用于 debug 的時間也應(yīng)該歸入“工具節(jié)省的時間”當(dāng)中。
另一方面則是對比“開發(fā)工具消耗的時間”,首先要強(qiáng)調(diào)通過靜態(tài)分析去對邏輯進(jìn)行判斷,不論是學(xué)習(xí)成本還是實際編寫成本都較高,如果一類問題可以通過編寫簡單的單元測試進(jìn)行覆蓋,那么應(yīng)該優(yōu)先考慮使用單元測試。但有的時候代碼邏輯對外部依賴較多,單元測試的開銷很大,例如我們有一段 e2e 測試的代碼,需要在目標(biāo)瀏覽器環(huán)境中執(zhí)行一段代碼,但是常規(guī)的 eslint 并不能判斷某個函數(shù)中的代碼實際執(zhí)行在另一個作用域下,部分檢查就會失效,例如瀏覽器運(yùn)行時引用的變量實際定義在本地運(yùn)行時中,eslint 無法察覺。而如果通過單元測試覆蓋,則需要實際運(yùn)行對應(yīng)的 e2e 代碼,或者 mock 其執(zhí)行環(huán)境的各種依賴,都是非常重的工作,取舍之下通過靜態(tài)分析覆蓋會事半功倍。
最后還需要考慮到使用體驗,許多編輯器都有 eslint 的集成插件,可以在編程的過程中實時檢測各個規(guī)則,在實時性方面遠(yuǎn)強(qiáng)于單元測試等 QA 手段的使用體驗。
示例 1:i18n許多項目都有國際化的需求,因此項目中的文案需要避免直接使用中文,常見的方案包括用變量代替字符串或者使用全局的翻譯函數(shù)處理字符串,例如:
// 錯誤:直接只用中文字符串 console.log("中文"); // 使用變量 const currentLocale = "cn"; const T = { str_1: { cn: "中文", }, }; console.log(T.str_1[currentLocale]); // 使用翻譯函數(shù)處理 console.log(t("中文"));
如果出現(xiàn)了直接使用中文字符串的錯誤,其實在代碼運(yùn)行過程中也不會有任何錯誤提示,只能靠 code review 和人工觀察測試來發(fā)現(xiàn)。我們嘗試自定義一條 eslint 規(guī)則解決它,此處假設(shè)項目中使用的是將所有中文內(nèi)容存放在一個變量中,其余地方直接引用變量的方法。
const SYMBOL_REGEX = /[u3002uff1buff0cuff1au201cu201duff08uff09u3001uff1fu300au300b]/; const WORD_REGEX = /[u3400-u9FBF]/; function hasChinese(value) { return WORD_REGEX.test(value) || SYMBOL_REGEX.test(value); } module.exports = { create: function(context) { return { Literal: function(node) { const { value } = node; if (hasChinese(value)) { context.report({ node, message: "{{ str }} contains Chinese, move it to T constant.", data: { str: node.value, }, }); } }, }; }, };
在這段代碼中,我們在create里遍歷所有Literal類型節(jié)點,因為我們需要檢查的對象是所有字符串。根據(jù) estree 的定義,我們會知道Literal類型階段結(jié)構(gòu)如下:
interface Literal <: Expression { type: "Literal"; value: string | boolean | null | number | RegExp; }
那么需要做的就是判斷該節(jié)點的 value 是否包含中文,在這里我們用的是正則表達(dá)式進(jìn)行判斷,當(dāng)含有中文字符或標(biāo)點時,就調(diào)用context.report方法報告一個錯誤。在應(yīng)用這條規(guī)則之后,全局所有直接使用中文字符串的代碼都會報錯,只需要對統(tǒng)一存放中文的變量T所在的代碼部分禁用這條規(guī)則,就可以避免誤判。
在筆者所在項目中我們使用的是“通過翻譯函數(shù)處理”的方式,所以規(guī)則會更為復(fù)雜一些,需要判斷當(dāng)前字符串的父節(jié)點是否為我們的翻譯函數(shù),Espree 會在每個節(jié)點上都記錄對應(yīng)的父節(jié)點信息,因此我們可以通過類似node.parent.callee.name === "t"這樣的方式進(jìn)行判斷。不過實際情況中還需要做更安全、全面的判斷,例如正確識別這樣的使用方式t("你好" + "世界"),后一個字符串的父節(jié)點是加法運(yùn)算符。
在這個示例中我們主要理解了遍歷函數(shù)的工作方式以及如何使用合理的節(jié)點類型實現(xiàn)需求,因此不再過度展開實際場景中的細(xì)節(jié)實現(xiàn)。不過相信讀者已經(jīng)可以感受到寫一條自定義規(guī)則需要非常全面的考慮代碼中的各類場景,這也是為什么 eslint 要求自定義規(guī)則要遵循 TDD 的開發(fā)方式,用足夠多的單元測試保證規(guī)則使用時符合預(yù)期,在最后我們會介紹 eslint 提供的單測框架。
示例 2:特殊作用域首先構(gòu)建一個場景用于展示這類規(guī)則:
不論是以及非常成熟的 Node.JS + selenium 體系還是較新的 headless chrome 生態(tài),這類端到端工具一般都會提供在目標(biāo)瀏覽器上執(zhí)行一段 JavaScript 的能力,例如這樣:
client.execute( function(foo, bar) { document.title = foo + bar; }, ["foo", "bar"] );
client.execute方法接收兩個參數(shù),第一個為在瀏覽器端執(zhí)行的函數(shù),第二個則是從當(dāng)前代碼傳遞給執(zhí)行函數(shù)的參數(shù),而瀏覽器端也只能使用傳遞的參數(shù)而不能直接使用當(dāng)前代碼中的變量。在這種場景下,很容易出現(xiàn)類似這樣的問題:
const foo = "foo"; const bar = "bar"; client.execute(function() { document.title = foo + bar; });
對于 eslint 來說并不知道document.title = foo + bar;將在瀏覽器端的作用域中執(zhí)行,而又發(fā)現(xiàn)有同名變量foo和bar被定義在當(dāng)前代碼中,則不會認(rèn)為這段代碼有錯誤,這種情況下我們就可以嘗試自定義規(guī)則來對這個特殊場景做檢查:
module.exports = { create: function(context) { return { "Program:exit": function() { const globalScope = context.getScope(); const stack = globalScope.childScopes.slice(); while (stack.length) { const scope = stack.pop(); stack.push.apply(stack, scope.childScopes); if (scope.block.parent.callee.property.name === "execute") { const undefs = scope.through.forEach((ref) => context.report({ node: ref.identifier, message: ""{{name}}" is not defined.", data: ref.identifier, }) ); } } }, }; }, };
以上代碼中繼續(xù)省略一些過于細(xì)節(jié)的實現(xiàn),例如判斷子作用域是否為client.execute的第一個參數(shù)以及將瀏覽器中的全局變量加入未定義變量的白名單等等,重點關(guān)注 eslint 為我們提供的一些幫助方法。
這次我們的節(jié)點選擇器為Program:exit,也就是下行完畢、開始上行完整的 AST 時執(zhí)行我們的自定義檢查,Program類型的節(jié)點對應(yīng)的是完整的源碼樹,在 eslint 中即是當(dāng)前文件。
在檢查時,首先我們使用context.getScope獲取了當(dāng)前正在遍歷的作用域,又由于我們處在Program節(jié)點中,這個作用域即為這個代碼文件中的最高作用域。之后我們構(gòu)建一個棧,通過不斷地把 childScopes 壓入棧中在讀取出來的方式,實現(xiàn)遞歸的訪問到所有的子作用域。
之后在處理每個子作用域時,都做了一個簡單的判斷(同樣是簡化過后的版本),來確定該作用域是否為我們需要獨立判斷的client.execute方法中第一個函數(shù)內(nèi)的作用域。
當(dāng)找到該函數(shù)內(nèi)的作用域之后,我們就可以使用scope對象上的各種方法進(jìn)行判斷了。事實上作用域是靜態(tài)分析中較為復(fù)雜的部分,如果完全獨立的去判斷作用域中的引用等問題相對復(fù)雜,好在 eslint 對外暴露了 scope manager interface,讓我們可以最大程度的復(fù)用封裝好的各類作用域接口。
在 scope manager interface 中可以看到scope.through方法的描述:
The array of references which could not be resolved in this scope.
正是我們需要的!所以最后只需要簡單的遍歷scope.through返回的未定義引用數(shù)組,就可以找到該作用域下所有的未定義變量。
通過這個示例,可以看出 eslint 本身已經(jīng)對許多常用需求做了高階的封裝,直接復(fù)用可以大大縮減“開發(fā)工具消耗的時間”。
示例 3:保證 API 使用規(guī)范繼續(xù)構(gòu)建一個場景:假如我們在業(yè)務(wù)中我們有一個內(nèi)部 API "Checker",用于校驗?zāi)承┎僮鳎╝ction)是否可執(zhí)行,而校驗的方式是判斷 action 對應(yīng)的規(guī)則(rule)是否全部通過,代碼如下:
const checker = new Checker({ rules: { ruleA(value) {}, ruleB(value) {}, }, actions: { action1: ["ruleA", "ruleB"], action2: ["ruleB"], }, });
在 Checker 這個 API 使用的過程中,我們需要:
所有 action 依賴的 rule 都在rules屬性中被定義。
所有定義的 rule 都被 action 使用。
由于 action 和 rule 的關(guān)聯(lián)性只靠 action value 數(shù)組中的字符串名稱與 rule key 值保持一致來維護(hù),所以第一條要求如果出了問題只能在運(yùn)行時發(fā)現(xiàn)錯誤,而第二條要求甚至不會造成任何錯誤,但在長期的迭代下可能會遺留大量無用代碼。
當(dāng)然這個場景我們很容易通過單元測試進(jìn)行覆蓋,但如果 Checker 是一個在項目各種都會分散使用的 API,那么單元測試即使有一個通用的用例,也需要開發(fā)者手動導(dǎo)出 checker 再引入到測試代碼中去,這本身就存在一定遺漏的風(fēng)險。
從開發(fā)體驗出發(fā),我們也嘗試用 eslint 的自定義規(guī)則完成這個需求,實現(xiàn)一個實時的 Checker API 使用方式校驗。
首先我們需要在靜態(tài)分析階段分辨代碼中的一個 Class 是否為 Checker Class,從而進(jìn)一步做校驗,單純從變量名稱判斷過于粗暴,容易發(fā)生誤判;而從 Class 來源分析很可能出現(xiàn)跨文件引用的情況,又過于復(fù)雜。所以我們借鑒一些編程語言中處理類似場景的做法,在需要編譯器特殊處理的地方加一些特殊的標(biāo)記幫助編譯器定位,例如這樣:
// [action-checker] const checker = new Checker({});
在構(gòu)造 checker 實例的前一行寫一個注釋// [action-checker],表明下一行開始的代碼是使用了 Checker API,在這基礎(chǔ)上,我們就可以開始編寫 eslint 規(guī)則:
const COMMENT_MARKER = "[action-checker]"; function getStartLine(node) { return node.loc.start.line; } module.exports = { create: function(context) { const sourceCode = context.getSourceCode(); const markerLines = {}; return { Program: function() { const comments = sourceCode.getAllComments(); comments.forEach((comment) => { if (comment.value.trim() === COMMENT_MARKER) { markerLines[getStartLine(comment)] = comment; } }); }, ObjectExpression: function(expressionNode) { const startLine = getStartLine(expressionNode); if (markLines[startLine - 1]) { // check actions and rules } }, }; }, };
在這個示例中,我們使用了context.getSourceCode獲取 sourceCode 對象,和上個例子中的 scope 類似,也是 eslint 封裝過后的接口,例如可以繼續(xù)通過sourceCode.getAllComments獲取代碼中的所有注釋。
為了實現(xiàn)通過注釋定位 checker 實例的目的,我們在markLines對象中存儲了帶有特殊標(biāo)記的注釋的行數(shù),獲取行數(shù)的方式則是node.loc.start.line。這里的loc也是 eslint 給各個 AST 節(jié)點增加的一個重要屬性,包含了節(jié)點對應(yīng)代碼在源代碼中的坐標(biāo)信息。
之后遍歷所有ObjectExpression類型節(jié)點,通過markLines中存儲的位置信息,確定某個ObjectExpression節(jié)點是否為我們需要校驗的 checker 對象,再根據(jù) estree 中定義的ObjectExpression結(jié)構(gòu),找到我們需要的 actions values 和 rules keys 進(jìn)行比較,此處不對細(xì)節(jié)處理做進(jìn)一步展開。
這個示例說明注釋作為靜態(tài)分析中非常重要的元素有很好的利用價值,許多項目也提供從一定格式(例如 JSDoc)的注釋中直接生成文檔的功能,也是代碼靜態(tài)分析常見的應(yīng)用,除了示例中用到的sourceCode.getAllComments可以獲取所有注釋,還提供sourceCode.getJSDocComment這樣只獲取 JSDoc 類型注釋的方法。
總而言之,基于 eslint 提供的強(qiáng)大框架,我們可以拓展出很多極大提高開發(fā)體驗和代碼質(zhì)量的用法。
雜項 借鑒社區(qū)eslint 本身提供的功能很強(qiáng)但也很多,光從文檔中不一定能找到最適用的方法,而 eslint 本身已經(jīng)有大量的 通用規(guī)則,很多時候直接從相近的規(guī)則中學(xué)習(xí)會更加有效。例如示例 2 中對作用域的判斷就是從社區(qū)的通用規(guī)則no-undef中借鑒了很多大部分思路。
TDD上文提到,靜態(tài)分析需要非常全面的考慮編譯器會遇到的各類代碼,但如果每次編寫規(guī)則都需要在一個很大的 code base 中進(jìn)行測試效率也很低。因此 eslint 提倡用測試驅(qū)動開發(fā)的方式,先寫出對規(guī)則的預(yù)期結(jié)果,再實現(xiàn)規(guī)則。
如果通過上文提到的 eslint yeoman 腳手架新建一個規(guī)則模版,會自動生成一個對應(yīng)的測試文件。以示例 1 為例,內(nèi)容如下:
const rule = require("../../../lib/rules/use-t-function"); const RuleTester = require("eslint").RuleTester; const parserOptions = { ecmaVersion: 8, sourceType: "module", ecmaFeatures: { experimentalObjectRestSpread: true, jsx: true, }, }; const ruleTester = new RuleTester({ parserOptions }); ruleTester.run("use-t-function", rule, { valid: [ { code: "fn()" }, { code: ""This is not a chinese string."" }, { code: "t("名稱:")" }, { code: "t("一" + "二" + "三")" }, ], invalid: [ { code: "名稱:", errors: [ { message: "名稱: contains Chinese, use t function to wrap it.", type: "Literal", }, ], }, ], });
核心的部分是require("eslint").RuleTester提供的單測框架 Class,傳入一些參數(shù)例如解析器配置之后就可以實例化一個 ruleTester。實際執(zhí)行時需要提供足夠的 valid 和 invalid 代碼場景,并且對 invalid 類型代碼報告的錯誤信息做斷言,當(dāng)所有測試用例通過后,就可以認(rèn)為規(guī)則的編寫符合預(yù)期了。
完整示例代碼自定義 eslint 規(guī)則在我們的實際項目中已經(jīng)有所應(yīng)用,示例中的實際完整規(guī)則代碼都存放在公網(wǎng) Github 倉庫中,如果對文中跳過的細(xì)節(jié)實現(xiàn)感興趣可以自行翻看。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/107728.html
摘要:接入分為兩部分,其一是可視化編輯器,在官網(wǎng)上我們可以獲取該編輯器的安裝包,并通過的插件管理進(jìn)行安裝。借助可視化編輯器,在整個過程中我們可以替換大部分手工編寫代碼的工作,進(jìn)行一站式操作。,有趣實用的分布式架構(gòu)頻道。本文根據(jù) SOFAChannel#5 直播分享整理,主題:給研發(fā)工程師的代碼質(zhì)量利器 —— 自動化測試框架 SOFAActs?;仡櫼曨l以及 PPT 查看地址見文末。歡迎加入直播互動釘...
摘要:看到很多團(tuán)隊和開源項目都在用代碼檢查工具,自己一直沒用過,最近加入了新團(tuán)隊有項目在用,就想著研究一下。代碼校驗工具能夠讓你在寫代碼時避免一些低級的錯誤。同時,也有友好的文檔針對每一條規(guī)則。在上文提高的所有工具當(dāng)中它對有著最好的支持。 看到很多團(tuán)隊和開源項目都在用代碼檢查工具,自己一直沒用過,最近加入了新團(tuán)隊有項目在用,就想著研究一下??吹絪itepoint上的一篇2015年的文章覺得不...
摘要:前言本文主要是有關(guān)前端方面知識按照目前的認(rèn)知進(jìn)行的收集歸類概括和整理,涵蓋前端理論與前端實踐兩方面。 前言:本文主要是有關(guān)前端方面知識按照 XX 目前的認(rèn)知進(jìn)行的收集、歸類、概括和整理,涵蓋『前端理論』與『前端實踐』兩方面。本文會告訴你前端需要了解的知識大致有什么,看上去有很多,但具體你要學(xué)什么,還是要 follow your heart & follow your BOSS。 初衷...
摘要:前言本文主要是有關(guān)前端方面知識按照目前的認(rèn)知進(jìn)行的收集歸類概括和整理,涵蓋前端理論與前端實踐兩方面。 前言:本文主要是有關(guān)前端方面知識按照 XX 目前的認(rèn)知進(jìn)行的收集、歸類、概括和整理,涵蓋『前端理論』與『前端實踐』兩方面。本文會告訴你前端需要了解的知識大致有什么,看上去有很多,但具體你要學(xué)什么,還是要 follow your heart & follow your BOSS。 初衷...
摘要:前言本文主要是有關(guān)前端方面知識按照目前的認(rèn)知進(jìn)行的收集歸類概括和整理,涵蓋前端理論與前端實踐兩方面。 前言:本文主要是有關(guān)前端方面知識按照 XX 目前的認(rèn)知進(jìn)行的收集、歸類、概括和整理,涵蓋『前端理論』與『前端實踐』兩方面。本文會告訴你前端需要了解的知識大致有什么,看上去有很多,但具體你要學(xué)什么,還是要 follow your heart & follow your BOSS。 初衷...
閱讀 1977·2021-09-23 11:21
閱讀 1749·2019-08-29 17:27
閱讀 1112·2019-08-29 17:03
閱讀 783·2019-08-29 15:07
閱讀 2001·2019-08-29 11:13
閱讀 2433·2019-08-26 12:14
閱讀 1003·2019-08-26 11:52
閱讀 1779·2019-08-23 17:09