JSDoc:一個(gè)可選的 TypeScript 替代品
JavaScript[2] 一直處于近年來最常用的腳本語言之一的地位。它以在 Web 平臺(tái)上編寫腳本的便捷性而聞名。隨著語言本身的發(fā)展,它從最開始蹭 Java 熱度的“玩具”語言,變成了一種成熟的語言,還能用來構(gòu)建大的應(yīng)用了。
不幸的是,隨著深入使用,JavaScript 語言本身的缺陷也被保留出來,包括:
- 缺乏靜態(tài)類型檢查。JavaScript 是一門動(dòng)態(tài)語言,有較為寬松的限制。比如:定義的函數(shù)參數(shù)在調(diào)用時(shí)不提供也行。靜態(tài)類型語言(例如 Java)就不是這樣了,因?yàn)樗鼤?huì)在編譯時(shí)報(bào)錯(cuò),但 JavaScript 默認(rèn)不提供這方面的支持,就導(dǎo)致某些錯(cuò)誤會(huì)滲透到 Javascript 應(yīng)用程序的生產(chǎn)環(huán)境中
- 在大項(xiàng)目中很難擴(kuò)展和維護(hù)。JavaScript 沒有提供一種強(qiáng)有力的機(jī)制來管理大型代碼庫,這使得隨著時(shí)間的推移擴(kuò)展和維護(hù)項(xiàng)目變得困難。
TypeScript 出現(xiàn)
2014 年,微軟推出了 Typescript v1.0[3]。這改變了整個(gè) JavaScript 生態(tài)系統(tǒng)。
TypeScript[4] 是 JavaScript 語言的超集,它解決了上一節(jié)提到的問題以及更多其他問題。這使得它越來越受歡迎。
State of Js survey 2022[5] 展示的 TypeScript 使用率在上升。
圖片
TypeScript 雖然解決了很多問題,但也有缺點(diǎn)。
本文我們將研究 TypeScript 的一個(gè)非常好的替代方案——JSDoc,它解決了靜態(tài)類型和可擴(kuò)展問題,同時(shí)還消除了 JavaScript 生態(tài)系統(tǒng)中 TypeScript 的缺點(diǎn)。
JSDoc 是什么?
JSDoc[6] 是基于 JavaScript 語言注釋功能建立起的一套文檔系統(tǒng)??梢詭椭阍诰帉?JavaScript 代碼的同時(shí),通過使用包含 JSDoc 語法的注釋獲得文檔支持。
JSDoc 語法有多種用途,包括為變量聲明類型、指定函數(shù)參數(shù)和返回值的類型、記錄和提供函數(shù)的使用方式、避免拼寫錯(cuò)誤等。這些特性與 TypeScript 類似,可以被像 VS Code 這類現(xiàn)代代碼編輯器利用,為程序員提供構(gòu)建、使用或維護(hù)代碼的支持。
JSDoc vs Typescript
JSDoc 和 TypeScript 都解決了編寫和維護(hù)純 JavaScript 代碼的問題。然而,他們使用了不同的策略,各有優(yōu)缺點(diǎn)。
JSDoc 相對于 Typescript 的優(yōu)點(diǎn):
- 靈活并且代碼兼容:JSDoc 只是被定義的一種特殊的 JavaScript 注釋,這意味著它可以添加到任何 JavaScript 代碼庫中(無論語言版本如何),并且它不像 TypeScript 那樣與編譯器綁定
- 提供代碼注釋支持:JSDoc 不僅僅可以用于類型檢查。它可用于添加文檔說明、描述函數(shù)如何工作,并且基于此生成文檔網(wǎng)站,所有這些都為增強(qiáng)代碼的可維護(hù)性和理解提供了價(jià)值
- 無需編譯步驟:這是從 TypeScript 切換到 JSDoc 的最直接的原因之一。TypeScript 需要通過編譯器將代碼編譯成 Javascript,以便瀏覽器可以理解。而 JSDoc 不需要任何編譯步驟,因?yàn)樗举|(zhì)上只是“注釋”,這是 Javascript 本身支持的功能。與每次進(jìn)行更改時(shí)使用必要的 Typescript 構(gòu)建流程相比,這可以簡化并提高開發(fā)效率
使用 JSDoc 的缺點(diǎn)
雖然 JSDoc 比 TypeScript 有很多優(yōu)勢。但現(xiàn)狀是 Typescript 使用率不斷攀升,被大家越來越多地采用,這是有原因的。以下是 Typescript 相對于 JSDoc 的一些優(yōu)點(diǎn):
- 更強(qiáng)的靜態(tài)類型支持:TypeScript 為類型提供了強(qiáng)大的模型,并能在編譯時(shí)捕獲這些錯(cuò)誤。但 JSDoc 就不支持,這些錯(cuò)誤就直接留在當(dāng)時(shí)的代碼中,也沒有手段去要求強(qiáng)制執(zhí)行修正
- 類型推斷支持:TypeScript 可以從值推斷類型。這有助于減少顯式類型注釋并讓代碼庫更加簡潔
- 轉(zhuǎn)譯支持:TypeScript 可以通過其 polyfill 功能使用 JavaScript 語言的最新功能(甚至是更加早期的提案功能),有效地將這些最新代碼轉(zhuǎn)換成在低版本瀏覽器中也能運(yùn)行的版本
如何使用 JSDoc:基礎(chǔ)知識
JSDoc 存在很久了,因此所有現(xiàn)代編輯器中都廣泛支持它,開箱即用,無需任何安裝。
在 .js 文件中添加 JSDoc 至此,就是增加注釋,是通過添加帶有額外星號(*)的注釋來完成的。
// Normal Javascript Comment 1
/* Normal Javascript Comment 2 */
/**
JSDoc 需要使用 2 個(gè)星號
*/
接下來,我們介紹一些基本功能。
添加代碼描述
/** JSDoc 用來服務(wù)的語言 */
const language = "JavaScript"
為變量添加類型
/**
* 本篇文章的作者
* @type {string}
*/
const writerName = "Elijah"
以上注解表示變量 writerName 是字符串類型。
為對象和數(shù)組添加類型
/**
* @type {Array<string>}
*/
const colours = ['red', 'blue', 'green']
/**
* @type {Array<number[]>}
*/
const primeNumbers = [1, 2, 3, 5, 7]
以上 2 種方法都是有效的 JSDoc 注解(與 TypeScript 一樣)。
而對象類型則可以通過 @typedef 指令來創(chuàng)建。
/**
* @typeof {Object} User - A user schema
* @property {number} id
* @property {string} username
* @property {string} email
* @property {Array<number>} postLikes
* @property {string[]} friends
*/
/** @type {User} */
const person1 = {
id: 847,
username: "Elijah",
email: "elijah@user.com",
postLikes: [44, 22, 24, 39],
friends: ['fede', 'Elijah']
}
/** @type {User} */
const person2 = {
id: 424,
username: "Winston",
email: "winston@user.com",
postLike: [18, 53, 98],
friends: ['Favour', 'Jane']
}
為函數(shù)添加類型(參數(shù)、返回值和預(yù)期錯(cuò)誤類型)
/**
* Divide two numbers.
* @param {number} dividend - The number to be divided.
* @param {number} divisor - The number to divide by.
* @returns {number} The result of the division.
*/
function divideNumbers(dividend, divisor) {
return dividend/divisor;
}
@param關(guān)鍵字后面跟參數(shù)類型定義,還可以使用連字符 - 添加參數(shù)描述。
@returns 關(guān)鍵字用于定義函數(shù)返回類型。這對于大型函數(shù)特別有用,因?yàn)檫@類函數(shù)一般很難觀察它預(yù)期的返回類型。
此外,你可以使用 @throws 指令添加函數(shù)可能的拋錯(cuò)類型。
接下來,改進(jìn) divideNumbers 函數(shù),增加除數(shù)為零時(shí)的拋錯(cuò)支持。
/**
* Divide two numbers.
* @param {number} dividend - The number to be divided.
* @param {number} divisor - The number to divide by.
* @returns {number} The result of the division.
* @throws {ZeroDivisionError} Argument divisor must be non-zero
*/
function divideNumbers(dividend, divisor) {
if (divisor === 0) {
throw new DivisionByZeroError('Cannot Divide by zero')
}
return dividend/divisor;
}
你可以在 @throws 中同時(shí)指定錯(cuò)誤類型以及錯(cuò)誤描述。
/**
* Custom error for division by zero.
*/
class DivisionByZeroError extends Error {
constructor(message = "Cannot Divide By Zero") {
super(message);
this.name = "DivisionByZeroError";
}
}
由于 JavaScript 本身并不強(qiáng)制你處理錯(cuò)誤,因此這樣做一定程度上有助于改善代碼協(xié)作、便于維護(hù)。
為 class 添加類型(描述、構(gòu)造函數(shù)以及方法)
更進(jìn)一步,你還可以使用 JSDoc 為 class 提供類型支持。
/**
* A Rectangle Class
* @class
* @classdec A four-sided polygon with opposite sides of equal length and four right angles
*/
class Rectangle {
/**
* Initializing a Rectangle object.
* @param {number} length - The length of the rectangle.
* @param {number} width - The width of the rectangle.
*/
constructor(length, width) {
this.length = length;
this.width = width;
}
/**
* Calculate the area of the Rectangle
* @returns {number} The area of the rectangle.
*/
calculateArea() {
return this.length * this.width;
}
/**
* Calculate the perimeter of the rectangle.
* @returns {number} The perimeter of the rectangle.
*/
calculatePerimeter() {
return 2 * (this.length + this.width);
}
}
上面是一個(gè)簡單的矩形類,提供了 2 種方法分別用來計(jì)算其面積和周長。
@class 關(guān)鍵字用于表示這個(gè)函數(shù)需要使用 new 關(guān)鍵字調(diào)用,@classdec 用于類的描述。為類添加類型時(shí),重要的是進(jìn)一步添加類型和描述。
- 構(gòu)造函數(shù)
- 所有屬性和方法
我們使用 @params 關(guān)鍵字來提供需要傳遞到構(gòu)造函數(shù)中的參數(shù)的類型和描述。類中的方法的類型化方式與函數(shù)相同,這在上一節(jié)中已介紹過,就不再贅述。
改進(jìn)通用代碼文檔
除了向代碼添加基本類型之外,JSDoc 還有很多方法可以幫助提高可讀性性。這里有幾個(gè):
- 添加代碼作者:可以使用 @author 指令添加作者姓名和電子郵件
/**
* Possible title for this article
* @type {string}
* @author Elijah [elijah@example.com]
*/
const articleTitle = "Demystifying JSDoc"
- 用法示例:你還可以添加代碼片段,展示如何使用,這對于復(fù)雜的代碼塊特別有用
/**
* Sums of the square of two numbers a**2 + b**2
* @example <caption>How to use the sumSquares function</caption>
* // returns 13
* sumSquares(2, 3)
* @example
* // returns 41
* sumSquares(4, 5)
* // Typing the function
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {Number} Returns the sum of the squares
*/
const sumSquares = function(a, b){
return a**2 + b**2
}
我們使用 @example 指令來實(shí)現(xiàn)這一點(diǎn),也可以使用 <caption> 標(biāo)簽作為標(biāo)題。
- 版本控制:你還可以使用 @version 指令指定項(xiàng)目的版本
/**
* @version 1.0.0
* @type {number}
*/
const meaningOfLife = 42
- 有用的鏈接:通常,你可能希望向用戶提供一些跳轉(zhuǎn)鏈接,他們可以獲得有關(guān)代碼的更多知識。它可能是 GitHub 倉庫、一篇教程、一篇博客等。為此,需要兩個(gè)指令來幫助實(shí)現(xiàn):@link 和 @tutorial
/**
* How to use the link tags
* Also see the {@link https://jsdoc.app/tags-inline-link.html official docs} for more information
* @tutorial getting-started
*/
function myFunction (){
}
@link 指令將“official docs”渲染成指向某個(gè)地址的文字鏈接。而 @tutorial 指令則用于將用戶引導(dǎo)至生成文檔上的相關(guān)教程鏈接。
- 創(chuàng)建模塊:可以使用文件頂部的 @module 指令在 JSDoc 中創(chuàng)建模塊,當(dāng)前文件就成一個(gè)模塊了。模塊被分組在生成的文檔網(wǎng)站上的單獨(dú)部分中。
// jsdoc.js
/** @module firstDoc */
//The rest of the code goes here
轉(zhuǎn)換 JSDoc 文件
使用 JSDoc 的最大優(yōu)點(diǎn)之一是能夠?qū)?JSDoc 文件轉(zhuǎn)換為生成文檔網(wǎng)站——甚至是 Typescript,這樣他們就可以獲得使用 Typescript 的好處。
從 JSDoc 文件生成文檔網(wǎng)站
如上所述,你可以按照以下步驟生成更具可讀性的 GUI:
- 安裝 jsdoc
$ npm install -g jsdoc
- 對目標(biāo)文件運(yùn)行 jsdoc
$ jsdoc path/to/file.js
- 打開生成的網(wǎng)站。jsdoc CLI 會(huì)將文檔自定輸出到 out 文件夾,然后在瀏覽器中打開 out/index.html
這是默認(rèn) jsdoc 生成的模板的樣子,但你可以設(shè)置成不同的模板配置[7]。
從 JSDoc 生成 .d.ts 文件
TypeScript 中的 .d.ts 文件表示聲明文件,你可以使用以下步驟從 JSDoc 代碼生成這些文件:
- 在項(xiàng)目文件夾中安裝 tsd-jsdoc
$ npm install tsd-jsdoc
- 生成 .d.ts 文件
對于單個(gè)文件。
$jsdoc -t node_modules/tsd-jsdoc/dist -r our/jsdoc/file/path.js
對于多個(gè)文件。
$jsdoc -t node_modules/tsd-jsdoc/dist -r file1.js file2.js file3.js ...
對于整個(gè)文件夾。
$jsdoc -t node_modules/tsd-jsdoc/dist -r src
它會(huì)將文件中的所有類型合并到 單個(gè)文件 out/types.d.ts 中。
注意:這假設(shè)你已經(jīng)安裝了上一節(jié)中的 jsdoc 。如果沒有,請?jiān)谶\(yùn)行此步驟之前先安裝它。
結(jié)論
至此,我們已經(jīng)學(xué)習(xí)了使用 JSDoc 以及從 JSDoc 代碼生成類型和文檔網(wǎng)站的基礎(chǔ)知識。當(dāng) Typescript 編譯/構(gòu)建步驟對生產(chǎn)力產(chǎn)生負(fù)面影響時(shí),JSDoc 特別有用。對遺留代碼庫來說 JSDoc 也很有用。
Rich Harris(Svelte 和 SvelteKit 的創(chuàng)建者)也將整個(gè) Svelte 和 SvelteKit 倉庫從 TypeScript 改用 JSDoc[8]。另外,TypeScript 也添加了對許多 JSDoc 聲明的支持(來源[9])。
參考資料
[1]JSDoc: A Solid Alternative To TypeScript: https://blog.openreplay.com/jsdoc--a-solid-alternative-to-typescript
[2]JavaScript: https://en.wikipedia.org/wiki/JavaScript
[3]Typescript v1.0: https://devblogs.microsoft.com/typescript/announcing-typescript-1-0/
[4]TypeScript: https://www.typescriptlang.org/
[5]State of Js survey 2022: https://2022.stateofjs.com/en-US/usage/
[6]JSDoc: https://jsdoc.app/
[7]模板配置: https://jsdoc.app/about-configuring-default-template.html
[8]從 TypeScript 改用 JSDoc: https://github.com/sveltejs/kit/discussions/4429
[9]來源: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html