0%

JSDoc 踩坑日記

前言

相信很多開發者都有過需要撰寫文件的需求

而使用 JSDoc , Swagger , YUIDoc….等自動化生產API文件的場景肯定屢見不顯

今天則是想簡單紀錄一下,在2021台北工業自動化展覽後,由於公司需求,需要產出API Document。

使用的正式JSDoc ,但長年被JavaScript養壞的語法, 以及雜亂的結構導致文件撰寫困難的血淚史…

簡介Intro

當然想記錄一些2266的東西前,自然是不免俗需要介紹一下JSDoc。

不過如果已經熟識的人可以直接跳過這段碎念了,哈哈。

詳細的參考資料可以點我

簡單的說就是在撰寫註解的同時,也是一個標記。

可以透過JSDoc或是其他API文件自動化生產工具產生API文件

不用再自己一步一步用Word慢慢寫(雖然有的公司還是如此…)


主要文本

客戶的需求

在得到公司要求API Document的當下,自然是很得意的認為,JS這麼隨意的語言。

文件肯定也能輕輕鬆鬆產出啦 (然後事實證明文件撰寫難度遠大於強型態語言QAQ)

但還好公司的專案,後端大多由我一手包辦,因此不論是重構或是參考,相較於繼承別人的遺產更方便

系統架構簡介

這個專案主要是作為雲平台IoT監控,而這樣的服務主要有四種設計模式

  • FAAS
  • IAAS
  • PAAS
  • SAAS
  • On Premises

各種設計模式都有各自的優勢,單就這次的專案來說,我採用的是FAAS/SAAS混合架構(當然主要還是以FAAS為主,以微服務設計為核心思想)

服務之間的通訊協定應用層主要有MQTT/HTTP/WebSocket這三種方案通訊,當然也因為微服務設計,需要使用大量的RestFulAPI

這也導致API文件量大大增加 (所以獨立開發資源少的情況下,還是選擇IAAS或On Premises設計模式比較不會找自己麻煩)

每一個微服務跟每一個函式都需要文件


血淚踩坑紀錄

原因

在前面之所以認為能夠簡簡單單的自動產生文件,正是因為很多時候

舉個例子

我們打個比方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//這樣寫可能很糟糕,但為了長話短說,於是簡單舉個例子,有機會再修正。或是有好的提議也歡迎一起討論)

/**
* WebSite
*/

const http = require("http");

class WebServer {
constructor ({dbQuery}) {
this.loginRouter = require("loginRouter");//假設有五十個routes
this.regisRouter = require("loginRouter");//假設有六十個routes
this.tokenRouter = require("loginRouter");//假設有二十個routes
this.pageRouter = require("loginRouter");//假設有七十個routes,而且還引用了自製包裝的外部模組
this.dbQuery = dbQuery;//引用資料庫查詢物件,以供router使用
this.app = "webserver"
/*...balbla...*/
}

get app(){
return this.app;
}
}

class SQL_Ambassador {
constructor (void) {
this.knex = require("knex");//使用了第三方模組
this.mysql = require("mysqlManager");//包裝了第三方模組的自製模組,假設有六十個Method
this.redis = require("redisManager");//包裝了第三方模組的自製模組,假設有二十個Method
this.bigQuery = require("BQManager");//包裝了第三方模組的自製模組,假設有十五個Method
/*...balbla...*/
}
}

const dbQuery = new SQL_Ambassador();
const webSite = new WebServer({dbQuery:dbQuery});

const service = http.createServer(webSite.app);
service.linsten(port||3000);

需求

這一個簡單建立webServer的例子可以看到,引用了很多模組,然後為了microservice的設計理念,將很多動作都進行包裝跟代理處理。

因此簡單算一算,為每個方法及routes寫文件,就需要兩三百個註解 (真的是累死)

甚至為了滿足客戶及公司需求,需要對某些特別的service,在解析某些請求時會需要熱插拔函式(function)或是物件(object)或是成員(member)或是方法(method)

無論如何,就是會有動態增加屬性的需求。

比如原本解析mqtt訊息的協議只有A,然後A只做一件事情,就是把這個訊息透過LINE推播到手機

然後老闆或客人心血來潮,突然想要做B,而B是會分析訊息,根據需求做某些操作

這時候就能透過上傳文件或是傳遞字串到後端,後端把這些訊息寫入到地端(local),然後再使用require把這個檔案熱騰騰的引入使用

問題點

於時API document就變得很難撰寫,JSDoc不會幫你執行程式,所以像這樣的做法似乎是無效的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* JSDOC for class
*/
class Example {
/**
* JSDOC for constructor
*/
constructor (fs=require("fs")) {
this.fs = fs;
fs.readdir ("path", ( error, files ) => {
if (error) {
console.log(error);
return error;
}
files.forEach((file) => {
/**
* @type {NodeRequire.<"__dirname/path/to/modules/*.js">} ps:這樣的作法無法達到想要的結果
* @since 動態建立方法,來源來自於file.js
*/
this[`${file}`] = require("file"); //這裡語法錯誤,為了防止不小心透漏重要資訊,址展示大致的概念
});
})
}

addHandler(pathToFile) {
this[`${pathToFile}`] = require(pathToFile); //這裡語法錯誤,為了防止不小心透漏重要資訊,但概念足夠清楚了(偷懶複製)
}

uploadFile(file,path) {
fs.writeFile( path, file, (error) => {
if (error) {
console.log(error);
return error
} else {
this.addHandler(path);
}
});
}
}

const instance = new Example();

這樣的做法,確實可以動態引用某些檔案(但其實很不建議,很可能被駭客利用,有很大的安全性問題)

不過也可以透過讓其擁有獨立的虛擬環境,隔離資源,稍微增加一點點安全性。

(當然還有更多做法,不過主題是JSDoc就不再多談)

其次是這樣的Document超級難寫啊!!!!!!

圖片

到底要怎麼抓那該死的undefined?? 到底怎麼處理可惡的T

查閱了無數文章,也上社群詢問。都沒有人擁有更好的解決辦法


解決方案

找答案的過程

嘗試了無數方案,看了不少TS官方文件,也翻閱了JSDoc官方文檔。

始終找不太到相關的解決辦法

但最後重新思路過一次,還是使用了JSDoc本身的魔法解決問題。

讓文檔能夠呈現想表達的意境

解決方案成果圖片

答案揭曉

那麼究竟是用什麼方式,讓這個文件從抓不到的 Undefined,到能夠呈現泛型引入方法的呢?

其實還是使用了namespace包裝,再使用name強制讓JSDoc紀錄我們想要讓它呈現的結果

大概長成這副德性

答案揭曉搂

心得

其實JSDoc的作者也說過,JSDoc並非實際讓你的程式跑過一次。(原話雖然不是這樣但意思相似)

所以JSDoc在獲取及產生檔案的當下,是處於靜態的。

因此若想要使用JSDoc描述一個動態的過程,需要手刻,強制宣告。

讓JSDoc乖乖聽話產生想要的文件內容

接觸JSDoc及Clean Code的時間不長,若文章有誤十分歡迎討論糾正

如果能幫到有同樣的問題的人,我也非常開心:D

祝各位開發者開發順心