這幾天打算寫一個簡單的 API Mock 服務器,老生常談哈?其實我是想講 JSX, Mock 服務器只是一個幌子。javascript
我在尋找一種更簡潔、方便、同時又能夠靈活擴展的、和別人不太同樣的方式,來定義各類 Mock API。後來我發現了 JSX 在領域問題描述的優點和潛力,固然這可不是空談,咱們會實際寫一個項目來證明這個判斷。css
文章大綱html
一上來就說這麼抽象的名詞,'領域問題' 是什麼鬼?什麼是領域,Wiki 上解釋的很是好,領域就是指某一專業或事物方面範圍的涵蓋。那麼所謂領域問題就能夠理解爲,咱們須要經過程序或者其餘方式去解決的需求。前端
好比提到 API Mock 服務器,咱們須要解決的就是請求匹配和數據模擬這些問題;Nginx 解決的資源伺服和代理問題;HTML + CSS 解決的是頁面 UI 展現問題...java
咱們這裏重點關注'描述'。這些描述是提供給領域專家的‘前端‘ 或者 用戶界面(UI)。舉個例子:node
描述的形式有不少,例如配置文件、編程語言、圖形界面。 先來看看如今常見的工具是怎麼作的:ios
JSON?git
JSON 是一種很是簡單的數據表述, 沒有任何學習成本,解析也很是方便。可是它有很是多致命的缺陷,好比不支持註釋、冗餘、數據結構單一。github
YAML?正則表達式
相比 JSON 語法要簡潔不少、可讀性也比較強。做爲一個配置文件形式很是優秀
仍是其餘配置文件形式...
一般這些配置文件都是語言無關的,所以不會包含特定語言的元素。換句話說配置文件形式數據是相對靜態的, 因此靈活性、擴展性比較差。只適合簡單的配置場景。
舉個例子,這些配置文件不支持函數。咱們的 Mock 服務器可能須要經過一個函數來動態處理請求,因此配置文件在這裏並不適用。
固然你能夠經過其餘方式來取代‘函數’,例如模板、或者腳本支持
咱們須要回到編程語言自己,利用它的編程能力,實現配置文件沒法實現的更強大的功能。
不過單純使用通用類型編程語言,命令式的過程描述可能過於繁瑣。咱們最好針對具體領域問題進行簡化和抽象,給用戶提供一個友好的用戶界面,讓他們聲明式地描述他們的領域問題。咱們要儘量減小用戶對底層細節的依賴,與此同時最好能保持靈活的擴展能力。
我說的可能就是DSL(Domain-specific languages):
DSL 是一種用於描述特定應用領域的計算機語言。DSL 在計算機領域有很是普遍的應用,例如描述 Web 頁面的 HTML、數據庫查詢語言 SQL、正則表達式。 相對應的是通用類型語言(GPL, General-Purpose Language),例如 Java、C++、JavaScript。它們能夠用於描述任意的領域邏輯,它們一般是圖靈完備的。 能夠這麼認爲,雖然不嚴謹:除了通用類型語言、其餘語言都算是 DSL。
怎麼建立 DSL?
從頭開發一門新語言?No! 成本過高了
一種更優雅的方式是在通用編程語言的基礎上進行減法或者封裝抽象。固然不是全部類型語言都有這個'能力', 好比 Java、C/C++ 就不行,它們的語法太 Verbose 或者工具鏈過重了。可是 Groovy、Ruby、Scala、還有 Elixir 這些語言就能夠方便地建立出‘DSL’, 並且它們大部分是動態語言。
它們有的藉助宏、有的天生語法就很是適合做爲 DSL、有的具有很是強的動態編程能力... 這些因素促就了它們適合做爲 DSL 的母體(宿主)。
咱們一般也將這種 DSL 稱爲 Embedded DSL(嵌入式 DSL)
或者 內部 DSL
,由於它們寄生在通用類型編程語言中。而獨立的 DSL,如 JSON、HTML,稱爲外部DSL
。
內部 DSL 好處是省去了實現一門語言的複雜性(Parse->Transform->Generate)。
舉兩個很是典型的例子:
Java 開發者經常使用的 Gradle,基於 Groovy:
plugins {
id 'java-library'
}
repositories {
jcenter()
}
dependencies {
api 'org.apache.commons:commons-math3:3.6.1'
implementation 'com.google.guava:guava:27.0.1-jre'
testImplementation 'junit:junit:4.12'
}
複製代碼
還有 CocoaPods, 基於 Ruby:
source 'http://source.git'
platform :ios, '8.0'
target 'Demo' do
pod 'AFNetworking'
pod 'SDWebImage'
pod 'Masonry'
pod "Typeset"
pod 'BlocksKit'
pod 'Mantle'
pod 'IQKeyboardManager'
pod 'IQDropDownTextField'
end
複製代碼
具體的實現細節不在本文的範圍以內,仍是聊回 JavaScript。
我我的要求 DSL 應該具有這些特性:
上節提到了 Groovy、Ruby ‘適合‘ 用做 DSL 母體,並不表明必定要用它們實現,這只是說明它們天生具有的一些語言特性讓實現更加便捷,或者說外觀更加簡潔。
Google 一把 ‘JavaScript DSL‘ 匹配的有效資料不多。 若是你以爲困惑那就應該回到問題自己, 最重要的是解決領域問題,至於怎麼組織和描述則是相對次要的。因此不要去糾結 JavaScript 適不適合。
那咱們就針對 Mock Server 這個具體領域,聊一聊 JavaScript 內部 DSL 的典型組織方式:
最簡單的方式是直接基於對象或者數組進行聲明,實現簡單又保持組織性。例如 Umi Mock 還有 飛冰 Mock, 就是基於對象組織的:
export default {
// 支持值爲 Object 和 Array
'GET /api/users': { users: [1, 2] },
// GET POST 可省略
'/api/users/1': { id: 1 },
// 支持自定義函數,API 參考 express@4
'POST /api/users/create': (req, res) => {
res.end('OK')
},
// 使用 mockjs 等三方庫
'GET /api/tags': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
}),
}
複製代碼
和配置文件差很少, 實現和使用都很是簡單 ,簡單的 API Mock 場景開箱即用,對於複雜的用法和 API 協議,也能夠經過自定義函數進一步封裝。可是有時候咱們但願庫能夠承擔多一點事情。
JavaScript 做爲內部 DSL 的另一種典型的形式是鏈式調用。
其中最出名的是 JQuery, 它讓鏈式調用這種模式廣爲人知。相比囉嗦的原生 DOM 操做代碼,JQuery 確實讓人眼前一亮, 它暴露精簡的 API, 幫咱們屏蔽了許多底層 DOM 操做細節,撫平平臺差別,同時還能保持靈活性和擴展性。這纔是它真正流行的緣由,大衆喜聞樂見的都是簡單的東西。
$('.awesome')
.addClass('flash')
.draggable()
.css('color', 'red')
複製代碼
JQuery 這種 API 模式也影響到了其餘領域,好比 Iot 領域的 Ruff
:
$.ready(function(error) {
if (error) {
console.log(error)
return
}
// 點亮燈
$('#led-r').turnOn()
})
複製代碼
jest
expect(z).not.toBeNull()
expect(z).toBeDefined()
expect(value).toBeGreaterThan(3)
expect(value).toBeGreaterThanOrEqual(3.5)
複製代碼
API Mock 服務器領域也有兩個這樣的例子:
Nock:
const scope = nock('http://myapp.iriscouch.com')
.get('/users/1')
.reply(404)
.post('/users', {
username: 'pgte',
email: 'pedro.teixeira@gmail.com',
})
.reply(201, {
ok: true,
id: '123ABC',
rev: '946B7D1C',
})
.get('/users/123ABC')
.reply(200, {
_id: '123ABC',
_rev: '946B7D1C',
username: 'pgte',
email: 'pedro.teixeira@gmail.com',
})
複製代碼
還有網易雲團隊的 Srvx
get('/handle(.*)').to.handle(ctx => {
ctx.body = 'handle'
})
get('/blog(.*)').to.json({ code: 200 })
get('/code(.*)').to.send('code', 201)
get('/json(.*)').to.send({ json: true })
get('/text(.*)').to.send('haha')
get('/html(.*)').to.send('<html>haha</html>')
get('/rewrite:path(.*)').to.rewrite('/query{path}')
get('/redirect:path(.*)').to.redirect('localhost:9002/proxy{path}')
get('/api(.*)').to.proxy('http://mock.server.com/')
get('/test(.*)').to.proxy('http://mock.server.com/', {
secure: false,
})
get('/test/:id').to.proxy('http://{id}.dynamic.server.com/')
get('/query(.*)').to.handle(ctx => {
ctx.body = ctx.query
})
get('/header(.*)')
.to.header({ 'X-From': 'svrx' })
.json({ user: 'svrx' })
get('/user').to.json({ user: 'svrx' })
get('/sendFile/:path(.*)').to.sendFile('./{path}')
複製代碼
鏈式調用模式目前是主流的 JavaScript 內部 DSL 形式。並且實現也比較簡單,更重要的是它接近天然語言。
近年基於 ES6 Template Tag 特性引入‘新語言‘到 JavaScript 的庫層出不窮。
不過由於 ES6 Template Tag 本質上是字符串,因此須要解析和轉換,所以更像是外部 DSL。別忘了 Compiler as Framework! 一般咱們能夠利用 Babel 插件在編譯時提早將它們轉換爲 JavaScript 代碼。
舉幾個流行的例子:
Zebu: 這是一個專門用於解析 Template Tag 的小型編譯器, 看看它的一些內置例子:
// 範圍
range`1,3 ... (10)` // [1, 3, 5, 7, 9]
// 狀態機, 牛逼
const traffic = machine` initState: #green states: #green | #yellow | #red events: #timer onTransition: ${state => console.log(state)} #green @ #timer -> #yellow #yellow @ #timer -> #red #red @ #timer -> #green `
traffic.start() // log { type: "green" }
traffic.send({ type: 'timer' }) // log { type: "yellow" }
複製代碼
Jest 表格測試:
describe.each` a | b | expected ${1} | ${1} | ${2} ${1} | ${2} | ${3} ${2} | ${1} | ${3} `('$a + $b', ({ a, b, expected }) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected)
})
test(`returned value not be greater than ${expected}`, () => {
expect(a + b).not.toBeGreaterThan(expected)
})
test(`returned value not be less than ${expected}`, () => {
expect(a + b).not.toBeLessThan(expected)
})
})
複製代碼
除此以外還有:
Template Tag 這些方案給咱們開了不少腦洞。儘管如此,它也帶來了一些複雜性,就像開頭說的,它們是字符串,須要解析、語法檢查和轉換,且 JavaScript 自己的語言機制並無給它們帶來多少便利(如語法高亮、類型檢查)。
鋪墊了這麼多,只是前戲。上面提到這些方案,要麼過於簡單、要麼過於複雜、要麼平淡無奇。我將目光投向了 JSX,我發現它能夠知足個人大部分需求。
先來看看一下咱們的 Mock 服務器的原型設計:
import { Get, Post, mock } from 'jsxmock'
export default (
<server port="4321"> {/* 首頁 */} <Get>hello world</Get> {/* 登陸 */} <Post path="/login">login success</Post> {/* 返回 JSON */} <Get path="/json">{{ id: 1 }}</Get> {/* mockjs */} <Get path="/mockjs">{mock({ 'id|+1': 1, name: '@name' })}</Get> {/*自定義邏輯*/} <Get path="/user/:id">{(req, res) => res.send('hello')}</Get> </server>
)
複製代碼
嵌套匹配場景
export default (
<server> <Get path="/api"> {/* 匹配 /api?method=foo */} <MatchBySearch key="method" value="foo"> foo </MatchBySearch> {/* 匹配 /api?method=bar */} <MatchBySearch key="method" value="bar"> bar </MatchBySearch> <BlackHole>我會吃掉任何請求</BlackHole> </Get> </server>
)
複製代碼
有點 Verbose? 進一步封裝組件:
const MyAwesomeAPI = props => {
const { path = '/api', children } = props
return (
<Get path={path}> {Object.keys(children).map(name => ( <MatchBySearch key="method" value={name}> {children[name]} </MatchBySearch> ))} </Get>
)
}
export default (
<server> <MyAwesomeAPI>{{ foo: 'foo', bar: 'bar' }}</MyAwesomeAPI> <MyAwesomeAPI path="/api-2">{{ hello: 'foo', world: 'bar' }}</MyAwesomeAPI> </server>
)
複製代碼
看起來不錯哈?咱們看到了 JSX 做爲 DSL 的潛力,也把 React 的組件思惟搬到了 GUI 以外的領域。
你知道個人風格,篇幅較長 ☕️ 休息一會,再往下看。
若是你是 React 的開發者,JSX 應該再熟悉不過了。它不過是一個語法糖,可是它目前不是 JavaScript 標準的一部分。Babel、Typescript 都支持轉譯 JSX。
例如
const jsx = (
<div foo="bar"> <span>1</span> <span>2</span> <Custom>custom element</Custom> </div>
)
複製代碼
會轉譯爲:
const jsx = React.createElement(
'div',
{
foo: 'bar',
},
React.createElement('span', null, '1'),
React.createElement('span', null, '2'),
React.createElement(Custom, null, 'custom element')
)
複製代碼
JSX 須要一個工廠方法來建立建立'節點實例'。默認是 React.createElement
。咱們能夠經過註釋配置來提示轉譯插件。按照習慣,自定義工廠都命名爲 h
:
/* @jsx h */
/* @jsxFrag 'fragment' */
import { h } from 'somelib'
const jsx = (
<div foo="bar"> <span>1</span> <span>2</span> <>fragement</> </div>
)
複製代碼
將轉譯爲:
import { h } from 'somelib'
const jsx = h(
'div',
{
foo: 'bar',
},
h('span', null, '1'),
h('span', null, '2'),
h('fragment', null, 'fragement')
)
複製代碼
JSX 會區分兩種組件類型。小寫開頭的爲內置組件,它們以字符串的形式傳入 createElement; 大寫開頭的表示自定義組件, 做用域內必須存在該變量, 不然會報錯。
// 內置組件
;<div /> // 自定義組件 ;<Custom /> 複製代碼
export function createElement(type, props, ...children) {
const copy = { ...(props || EMPTY_OBJECT) }
copy.children = copy.children || (children.length > 1 ? children : children[0])
return {
_vnode: true,
type,
props: copy,
}
}
複製代碼
你們應該比較熟悉 koa 中間件機制。
// logger
app.use(async (ctx, next) => {
await next()
const rt = ctx.response.get('X-Response-Time')
console.log(`${ctx.method} ${ctx.url} - ${rt}`)
})
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
ctx.set('X-Response-Time', `${ms}ms`)
})
// response
app.use(async ctx => {
ctx.body = 'Hello World'
})
複製代碼
形象的說,它就是一個洋蔥模型:
中間件調用 next,就會進入下一級。 若是把函數的邊界打破。它的樣子確實像洋蔥:
✨我發現使用 JSX 能夠更直觀地表示這種洋蔥結構
因而乎,有了 <use />
這個基礎組件。它相似於 Koa 的 app.use
, 用於攔截請求,能夠進行響應, 也能夠選擇進入下一層。
① 來看看總體設計。
use 正是基於上面說的,使用 JSX 來描述中間件包裹層次的基礎組件。由於使用的是一種樹狀結構,因此要區分兄弟中間件和子中間件:
<server>
<use m={A}>
<use m={Aa} />
<use m={Ab} />
</use>
<use m={B} />
<use m={C} />
</server>
複製代碼
其中 Aa
、Ab
就是 A
的子中間件。在 A 中能夠調用相似 koa 的 next
函數,進入下級中間件。
A
、B
、C
之間就是兄弟中間件。當前繼中間件未匹配時,就會執行下一個相鄰中間件。
乍一看,這就是 koa 和 express 的結合啊!
② 再看看 Props 設計
interface UseProps {
m: (req, res, recurse: () => Promise<boolean>) => Promise<boolean>;
skip?: boolean;
}
複製代碼
m
req
、res
:Express 的請求對象和響應對象
recurse
:遞歸執行子級中間件, 相似 koa 的 next。返回一個Promise<boolean>
, 它將在下級中間件執行完成後 resolve,boolean 表示下級中間件是否匹配攔截了請求。
返回值:返回一個 Promise<boolean>
表示當前中間件是否匹配(攔截請求)。若是匹配,後續的兄弟中間件將不會被執行。
skip
:強制跳過,咱們在開發時可能會臨時跳過匹配請求,這個有點像單元測試中的 skip
③ 看一下運行實例
假設代碼爲:
const cb = name => () => {
console.log(name)
return false
}
export default (
<server>
<use
m={async (req, res, rec) => {
console.log('A')
if (req.path === '/user') await rec() // 若是匹配,則放行,讓其遞歸進入內部
console.log('end A')
return false
}}
>
<use m={cb('A-1')}>若是父級匹配,則這裏會被執行</use>
<use m={cb('A-2')}>...</use>
</use>
<use m={cb('B')} />
<use m={cb('C')} />
</server>
)
複製代碼
若是請求的是 '/',那麼打印的是 A -> end A -> B -> C
; 若是請求爲 '/user', 那麼打印的是 A -> A-1 -> A-2 -> end A -> B -> C
咱們的基礎組件和 Koa / Express 同樣,核心保持很是小而簡潔,固然它也比較低級,這樣可以保證靈活性。
這個簡單的基礎組件設計就是整個框架的‘基石’。 若是你瞭解 Koa 和 Express,這裏沒有新的東西。只是換了一種表現方式。
Ok, 有了 use
這個基礎原語, 我能夠作不少有意思的事情,使用組件化的思惟封裝出更高級的 API。
① <Log>
:打日誌
封裝一個最簡單的組件:
export const Log: Component = props => {
return (
<use m={async (req, res, rec) => { const start = Date.now() // 進入下一級 const rtn = await rec() console.log( `${req.method} ${req.path}: ${Date.now() - start}ms` ) return rtn }} > {props.children} </use>
)
}
複製代碼
用法:
<server>
<Log>
<Get>hello world</Get>
<Post path="/login">login sucess</Post>
...
</Log>
</server>
複製代碼
② <NotFound>
: 404
export const NotFound = props => {
const { children } = props
return (
<use m={async (req, res, rec) => { const found = await rec() if (!found) { // 下級未匹配 res.status(404) res.send('Not Found') } return true }} > {children} </use>
)
}
複製代碼
用法和 Log 同樣。recurse
返回 false 時,表示下級沒有匹配到請求。
③ <Catch>
: 異常處理
export const Catch: Component = props => {
return (
<use m={async (req, res, rec) => { try { return await rec() } catch (err) { res.status(500) res.send(err.message) return true } }} > {props.children} </use>
)
}
複製代碼
用法和 Log 同樣。捕獲下級中間件的異常。
④ <Match>
: 請求匹配
Match 組件也是一個很是基礎的組件,其餘高層組件都是基於它來實現。它用於匹配請求,並做出響應。先來看看 Props 設計:
export type CustomResponder =
| MiddlewareMatcher
| MockType
| boolean
| string
| number
| object
| null
| undefined
export interface MatchProps {
match?: (req: Request, res: Response) => boolean // 請求匹配
headers?: StringRecord // 默認響應報頭
code?: number | string // 默認響應碼
// children 類型則比較複雜, 能夠是原始類型、對象、Mock對象、自定義響應函數,以及下級中間件
children?: ComponentChildren | CustomResponder
}
複製代碼
Match 組件主體:
export const Match = (props: MatchProps) => {
const { match, skip, children } = props
// 對 children 進行轉換
let response = generateCustomResponder(children, props)
return (
<use skip={skip} m={async (req, res, rec) => { // 檢查是否匹配 if (match ? match(req, res) : true) { if (response) { return response(req, res, rec) } // 若是沒有響應器,則將控制權交給下級組件 return rec() } return false }} > {children} </use>
)
}
複製代碼
限於篇幅,Match 的具體細節能夠看這裏
前進,前進。 Get
、Post
、Delete
、MatchByJSON
、MatchBySearch
都是在 Match
基礎上封裝了,這裏就不展開了。
⑤ <Delay>
: 延遲響應
太興奮了,一不當心又寫得老長,我能夠去寫小冊了。Ok, 最後一個例子, 在 Mock API 會有模擬延遲響應的場景, 實現很簡單:
export const Delay = (props: DelayProps) => {
const { timeout = 3000, ...other } = props
return (
<use m={async (req, res, rec) => { await new Promise(res => setTimeout(res, timeout)) return rec() }} > <Match {...other} /> </use> ) } 複製代碼
用法:
<Get path="/delay">
{/* 延遲 5s 返回 */}
<Delay timeout={5000}>Delay Delay...</Delay>
</Get>
複製代碼
更多使用案例,請看 jsxmock 文檔)
堅持到這裏不容易,你對它的原理可能感興趣,那不妨繼續看下去。
簡單看一下實現。若是瞭解過 React 或者 Virtual-DOM 的實現原理。這一切就很好理解了。
這是打了引號的'渲染'。這只是一種習慣的稱謂,並非指它會渲染成 GUI。它用來展開整顆 JSX 樹。對於咱們來講很簡單,咱們沒有所謂的更新或者 UI 渲染相關的東西。只需遞歸這個樹、收集咱們須要的東西便可。
咱們的目的是收集到全部的中間件,以及它們的嵌套關係。咱們用 MiddlewareNode 這個樹形數據結構來存儲它們:
export type Middleware = (
req: Request,
res: Response,
// 遞歸
recurse: () => Promise<boolean>,
) => Promise<boolean>
export interface MiddlewareNode {
m: Middleware // 中間件函數
skip: boolean // 是否跳過
children: MiddlewareNode[] // 子級中間件
}
複製代碼
渲染函數:
let currentMiddlewareNode
export function render(vnode) {
// ...
// 🔴 建立根中間件
const middlewares = (currentMiddlewareNode = createMiddlewareNode())
// 🔴 掛載
const tree = mount(vnode)
// ...
}
複製代碼
掛載是一個遞歸的過程,這個過程當中,遇到自定義組件
咱們就展開,遇到 use 組件就將它們收集到 currentMiddlewareNode
中:
function mount(vnode) {
let prevMiddlewareNode
if (typeof vnode.type === 'function') {
// 🔴自定義組件展開
const rtn = vnode.type(vnode.props)
if (rtn != null) {
// 遞歸掛載自定義組件的渲染結果
mount(rtn, inst)
}
} else if (typeof vnode.type === 'string') {
// 內置組件
if (vnode.type === 'use') {
// 🔴收集中間件
const md = createMiddlewareNode(inst.props.m)
md.skip = !!inst.props.skip
currentMiddlewareNode.children.push(md)
// 保存父級中間件
prevMiddlewareNode = currentMiddlewareNode
currentMiddlewareNode = md // ⬇️推入棧,下級的中間件將加入這個列表
} else {
// ... 其餘內置組件
}
// 🔴遞歸掛載子級節點
mountChilren(inst.props.children, inst)
if (vnode.type === 'use') {
currentMiddlewareNode = prevMiddlewareNode // ⬆️彈出棧
}
}
}
// 🔴 子節點列表掛載
function mountChilren(children: any, parent: Instance) {
childrenToArray(children).forEach(mount)
}
複製代碼
如今看看怎麼運行起來。咱們實現了一個簡單的中間件機制,相對 Koa 好理解一點:
export async function runMiddlewares(req, res, current): Promise<boolean> {
const { m, skip, children } = current
if (skip) {
// 跳過, 直接返回 false
return false
}
// 調用中間件
return m(req, res, async () => {
// recurse 回調
// 🔴 若是有下級中間件,則遞歸調用子級中間件
if (children && children.length) {
for (const child of children) {
const matched = await runMiddlewares(req, res, child)
if (matched) {
// 🔴 若是其中一個兄弟中間件匹配,後續的中間件都不會被執行
return true
}
}
}
return false // 🔴 沒有下級中間件,或者沒有任何下級中間件匹配
})
}
複製代碼
很簡單哈? 就是遞歸遞歸遞歸
本文從配置文件講到 DSL,又講到了 JavaScript 內部 DSL 表達形式和能力。最後將焦點彙集在了 JSX 上面。
我經過一個實戰的案例展現了 JSX 和 React 的組件化思惟,它不只僅適用於描述用戶界面,咱們也看到 JSX 做爲一種 DSL 的潛力和靈活性。
最後總結一下優缺點。
✅ 優勢
⚠️ 缺點
靈活卻有組織性。靈活一般容易致使雜亂無章,組織性則可能意味着犧牲靈活性,二者在某種意義上面看是矛盾的。可以將二者平衡案例其實不多見,JSX 多是一個。(我好像在吹 🐂)
🎉🎉代碼已經在 Github, 目前正處於原型階段: ivan-94/jsxmock 歡迎 ⭐️ 和貢獻。
也學別人建個羣(好多讀者問過),試試水吧...