個人信息保護法,重要細節(jié)有哪些?
隨著 TypeScript
的流行,類型系統(tǒng)也逐步進入了大家的視野,類型安全相關的問題也受到了更多人的關注,那今天我就以這個角度帶大家感受 Farrow
在類型安全方面優(yōu)秀方案的設計與思考,希望對大家能有所啟發(fā)。
在本篇文章中,我將為大家?guī)硪韵碌膬?nèi)容:
-
類型安全 What & Why?
-
當前 Node.js 主流 Web 框架現(xiàn)狀
-
當下的 API 設計中的類型問題
-
Farrow 類型安全方案
-
Farrow 未來規(guī)劃
好,那我們現(xiàn)在開始。
類型安全
關于類型安全,可能很多同學已經(jīng)有所了解,也了解過 Soundness [1] 這個詞,但也該也有許多同學不甚了解。
不了解的同學,你可以暫且將它簡單的理解為:
變量的行為與它的類型相匹配,不存在運行時的類型錯誤。
在 JavaScript 中進行下面幾個操作:
-
訪問 null 的屬性
-
將 string 類型當作 number 類型做運算
-
調(diào)用對象不存在的方法
都會在運行時拋出類型錯誤。
那我們?yōu)槭裁匆非箢愋桶踩?/p>
Well typed programs cannot go wrong.—— By Robin Milner 1978 《A Theory of Type Polymorphism in Programming》 [2]
正如上面這句話說的,類型系統(tǒng)可以有效的提升程序的正確性:
-
盡可能在編譯期通過類型檢查提前捕獲可能的程序錯誤,提高代碼的健壯性
-
配合編輯器類型提示,類型檢查是比單元測試反饋更快、更早、覆蓋更全面的實時測試
-
符合類型安全準則的代碼,往往是設計更合理、質(zhì)量更高、編寫更優(yōu)雅的、表達更清晰的
類型檢查的優(yōu)勢不用多說,要讓我們的代碼達到類型安全的狀態(tài),往往需要我們對要解決的問題進行很好的建模,所以從這個角度看,類型系統(tǒng)也可以幫助我們寫出設計更合理、質(zhì)量更高的代碼。
主流框架現(xiàn)狀
之前我們在實際的項目開發(fā)中遇到過 Node.js
框架選型的問題,經(jīng)過調(diào)研,我們發(fā)現(xiàn)主流的 Node.js 框架: Express.js
、 Koa
、 EggJS
、 Hapi
、 Restify
、 Fastify
等都是用 JavaScript
實現(xiàn)的,他們充分發(fā)揮了 Javascript
的能力,但 從類型安全的視角看,當前 Web 框架的設計存在諸多問題。
API 設計類型問題
接下來,我們就以 Express 為例來看一下。
請求意外掛起(Hanging Request)
我們發(fā)現(xiàn)以 Express 這樣的中間件設計,它允許請求可以不被響應,也無法通過 TypeScript 的類型檢查得到約束和提示。
- app.use((req, res, next) => {
- // do nothing
- });
錯誤的相應內(nèi)容(Wrong Response)
同樣的,我們無法保證 header -> body 這樣正確的的響應次序
- app.use((req, res, next) => {
- // body 提前發(fā)送
- res.end('xxx');
- // header 必須在 body 之前發(fā)送,這里報錯或 warning
- res.header('Content-Type', 'text/html');
- });
也無法在編譯期約束只發(fā)送一次 body
- app.use((req, res, next) => {
- // body 提前發(fā)送
- res.end('xxx');
- // body 只能發(fā)送一次,這里報錯或 warning
- res.json({ error: true });
- });
而這些都會導致錯誤的響應內(nèi)容。
篡改對象屬性(Monkey Patching)
在 JavaScript 中,我們可以任意的修改對象的屬性,但修改 req/res 或 ctx 污染全鏈路中間件的類型。這會導致,在任意一個中間件中,你都無法知道傳到當前中間件的對象中到底有哪些屬性和方法。
- app.use((req, res, next) => {
- // what type of res.locals
- res.locals.a = 1;
- next();
- });
- app.use((req, res, next) => {
- // what type of res.locals
- console.log(res.locals.a);
- });
當然有些框架支持一些類型標注的方案,來解決類型提示上的問題,但這并沒有從根本上解決問題,從類型系統(tǒng)的角度來看,動態(tài)追加的屬性或方法,與靜態(tài)標注的類型有本質(zhì)矛盾,正確的方式是 讓靜態(tài)類型決定能否賦值屬性,而非屬性賦值決定是否包含特定類型。
無運行時驗證(No Runtime Validation)
在當前 TypeScript 官方提供的工具鏈中,TypeScript 類型在編譯后都被抹去,的確在大多數(shù)場景下,編譯階段前類型系統(tǒng)就完成了它的任務,但這也導致了一個嚴重的問題,請求的內(nèi)容是未知的,通常需要手動進行校驗。
- app.use((req, res, next) => {
- req.body; // body type is any/unknown
- req.query; // query type is any/unknown
- const body = req.body as MyBodyType; // type wll be eliminated
- });
如果請求參數(shù)比較復雜,通常需要編寫很復雜的校驗邏輯。
不友好的類型推導(Poor Type Inference)
現(xiàn)有框架在請求內(nèi)容和響應內(nèi)容的類型方面基本沒有提供比較好的類型推導方案,很多時候我們需要手動的類型校驗 + 類型轉換。
- app.get('/user/:userId', (req, res, next) => {
- req.params.userId; // no type infer
- const params = req.params as { userId: string };
- const userId = Number(params.userId); // 必須每次手動 transform
- });
問題
到現(xiàn)在,我們已經(jīng)提到了 5 條現(xiàn)有 API 設計中的類型問題:
-
Hanging Request(請求意外掛起)
-
Wrong Response(錯誤響應內(nèi)容)
-
Monkey Patching(篡改對象屬性)
-
No Runtime Validation(無運行時驗證)
-
Poor Type Inference(不友好的類型推導)
雖然這些框架不是使用 TypeScript 實現(xiàn),但它們都提供了 @types/*
的類型包,但依舊存在諸多類型上的問題,可見只靠 *.d.ts,并不能獲得充分的類型友好和類型安全特性。
我們對這些問題和現(xiàn)有的 Node.js 框架進行了系統(tǒng)性的調(diào)研和考量,發(fā)現(xiàn):
-
基于 Express/Koa 可以用打補丁的方式解決一兩種類型問題,但不能從根本上解決問題
-
Fastify 提供了基于 JSON Schema 的運行時校驗請求內(nèi)容的方案,但方案與類型系統(tǒng)不貼合
-
要充分解決系統(tǒng)性問題,則需要基于 TypeScript 做全盤的思考
-
Type-First Development 類型優(yōu)先開發(fā)
-
Type-Driven Development 類型驅動開發(fā)
為了做到這些和解決上面提到的問題,我們就需要一個新的類型安全的服務端框架。
類型安全的服務端框架設計目標
根據(jù)之前的問題,我們可以得到類型安全的服務端框架設計目標:
-
Prevent Hanging Request(阻止請求意外掛起)
-
Refuse Wrong Response(拒絕錯誤響應內(nèi)容)
-
No need to Monkey-Patching(無需篡改對象屬性)
-
Embedded Runtime-Validation(內(nèi)置運行時驗證)
-
Excellent Type Inference(出色的類型推導)
Farrow 作者:做到之前做不到,做好之前能做到。
從而就有了 Farrow 這樣一個框架,接下來我就向大家介紹一下,F(xiàn)arrow 的一些設計和它是如何做到上面所說的事情。
Farrow-Http 設計
Prevent Hanging Request & Refuse Wrong Response
首先,為了可以做到 阻止請求意外掛起 和 拒絕錯誤響應內(nèi)容,F(xiàn)arrow 重新設計了中間件,取消了響應參數(shù),通過返回值表達響應結果。
- import { Http, Response } from 'farrow-http';
- const http = Http();
- http.use((request, next) => {
- // response is return type
- return Response.text(request.pathname);
- });
這樣 TypeScript 也可以檢查函數(shù)返回值類型是否滿足約束,沒有響應或者響應錯誤類型都會類型報錯。
Prevent Wrong Response
為了進一步解決錯誤的相應內(nèi)容的問題, Farrow 設計了 Virtual Response 虛擬響應對象,Response.text 等方法構造了樸素數(shù)據(jù),類似 Virtual DOM,并未直接產(chǎn)生作用,多次使用將 merged 到一起,F(xiàn)arrow 框架內(nèi)部最終統(tǒng)一按照正確順序處理 header -> body 的次序和類型。
- import { Http, Response } from 'farrow-http';
- const http = Http();
- http.use((request, next) => {
- // response is return type
- return Response.text(request.pathname)
- .header('abc', 'efg')
- .text('changed text');
- });
No need to Monkey-Patching(Request)
為了解決 Monkey-Patching 的問題,即不再推薦和引導開發(fā)者去修改 req
請求對象,F(xiàn)arrow 設計了 Virtual Request 虛擬請求對象,所以傳入中間件的請求對象不是原生 req
對象,而是從中提取的 plain data,所以可以通過 next(newRequest) 向后傳遞新的 request 對象,無需修改原對象。
- import { Http, Response } from 'farrow-http';
- const http = Http();
- http.use((request, next) => {
- return next({
- ...request,
- pathname: '/another/pathname',
- });
- });
- http.use((request, next) => {
- request.pathname; // is equal to /another/pathname
- });
No need to Monkey-Patching(Response)
為了進一步解決 Monkey-Patching 的問題,F(xiàn)arrow 重新設計了中間件的管理機制,next 將會返回下游中間件的 response 對象,可以加以后續(xù)處理,這樣就可以做到無需修改 res/ctx.body ,immutable 比 mutable 更加類型友好, prefer immutable。
- import { Http, Response } from 'farrow-http';
- const http = Http();
- http.use((request, next) => {
- let response = await next();
- // 合并,組合,過濾,拼裝新的 response
- return Response.header('abc', 'efg').merge(response);
- });
- http.use((request, next) => {
- return Response.text('hello world!');
- });
No need to Monkey-Patching(Middleware)
雖然之前的方面解決了修改請求對象的問題,但中間件間共享變量的需求依舊沒有被解決,所以 Farrow 提供了 Context + Hooks 的方案,他們的工作機制類似 React Context 和 React Hooks,類似跨組件傳遞數(shù)據(jù)那樣,跨中間件傳遞 Context Data,這樣中間件間的共享變量就無需掛載到 req 對象上了,并且得益于 Node.js 的新特性 Async hooks,F(xiàn)arrow 能夠提供按需的、分布式的、細粒度的、關注度分離的、類型安全的 Context Passing 機制。
- import { Http, Response, createContext } from 'farrow-http';
- const http = Http();
- // 創(chuàng)建 Context
- const AuthContext = createContext<Auth | null>(null);
- // 更新 Context
- http.use(async (request, next) => {
- AuthContext.set(await getAuth(request));
- return next();
- });
- // 不管中間插入多少中間件,request/response 類型都不會污染
- // 消費 Context
- http.use((request, next) => {
- // 跨中間件訪問 context 數(shù)據(jù)
- let auth = AuthContext.get();
- return Response.text('hello world!');
- });
Embedded Runtime-Validation & Excellent Type Inference(Schema)
為了提供運行時驗證和更友好的類型推導能力,F(xiàn)arrow 設計了一套對 TypeScript 開發(fā)者非常友好的 Schema Builder,從而基于 Schema 提供了 Runtime Validation 機制,允許開發(fā)者使用 Schema Builder 去描述請求的形狀,基于這個形狀 Farrow 會自動推導出請求對象的類型,這樣就保證了在運行時請求對象對象的值將會滿足 Schema 所描述的形狀。這樣我們就同時提供了運行時校驗和友好的類型推導。
- import { Http, Response } from 'farrow-http';
- import { Int } from 'farrow-schema';
- const http = Http();
- http
- .match({
- pathname: '/user',
- method: 'post',
- body: {
- userId: Int,
- userName: String,
- userAge: Int,
- },
- })
- .use((request, next) => {
- // request.body is { userId, userName, userAge }
- console.log('userId', request.body.userId);
- console.log('userName', request.body.userName);
- console.log('userAge', request.body.userAge);
- });
Embedded Runtime-Validation & Excellent Type Inference(URL)
后來我們發(fā)現(xiàn)很多時候我們又好像并不需要這么復雜的數(shù)據(jù)結構,所以 Farrow 提供了一種更簡單的描述方式:
- import { Http, Response } from 'farrow-http';
- import { Int } from 'farrow-schema';
- const http = Http();
- http
- .get('/greet/<name:string>?<age:int>&farrow=type-safety')
- .use((request, next) => {
- // type infer for request from url
- console.log('name', request.params.name);
- console.log('age', request.query.age);
- console.log('farrow', request.query.farrow);
- });
它是基于 TypeScript 4.1 發(fā)布的 Template literal type 特性實現(xiàn)的,從 URL 中提取 TypeScript 類型,然后自動識別是 params 參數(shù)還是 query 參數(shù),自動將 String 轉換成標記的 Int 、Boolean 等 Schema 類型,基于這個我們也可以同時提供了運行時校驗和友好的類型推導。
以上 farrow-http 運用 Type-First Development 思想和 React 啟發(fā)的函數(shù)式/immutable 理念,系統(tǒng)性地提升了 Web Framework 的類型安全水平,解決了以下的問題:
-
Prevent Hanging Request(阻止請求意外掛起)√
-
Refuse Wrong Response(拒絕錯誤響應內(nèi)容)√
-
No need to Monkey-Patching(無需篡改對象屬性)√
-
Embedded Runtime-Validation(內(nèi)置運行時驗證)√
-
Excellent Type Inference(出色的類型推導)√
新的挑戰(zhàn):端到端類型同步
farrow-Http 優(yōu)化了 Service Side 的類型安全,只解決了一半問題,End-to-end typing 將是一個新的問題 Client Side 如何復用 Service Side 的類型?Client Side 類型如何跟 Service Side 保持一致和同步?所以我們重新思考: BFF 應該為前端提供什么?
-
傳統(tǒng) BFF:為前端提供 data
-
現(xiàn)代 BFF:為前端提供 data 和 type
-
后現(xiàn)代 BFF:為前端提供 data,type 和 code
為了做到這些,F(xiàn)arrow 提供了一個新的方案:farrow-api。
Farrow-API 設計
Farrow 采用了 Introspection + Codegen 的方式來實現(xiàn)為前端提供 data,type 和 code。提供了類似 GraphQL 的 Introspection 機制,支持拉取 farrow-api 的 Schema 數(shù)據(jù),然后通過 Code Generation 生成 TypeScript Type 和 HTTP Client Code。
在服務器端,描述請求和響應的形狀,然后聚合成 Farrow API
然后為該 API 實現(xiàn)請求處理函數(shù)
然后啟動 Server,在客戶端就可以生成下面的代碼
而在客戶端開發(fā)者只需要引入生成的函數(shù),然后調(diào)用
除此之外,farrow-api 還支持其他描述 API 的屬性,比如 @deprecated 標記
至此 Farrow 實現(xiàn)了服務器端的類型安全,也解決了 C/S 模型下的類型同步問題。
Farrow 藍圖和未來展望
優(yōu)勢
除了類型安全之外,F(xiàn)arrow 的設計還帶來了另外的一些優(yōu)勢,擁有 Schema 之后可以形成接口的知識庫,知識庫可以用來做很多事情,函數(shù)級別的接口監(jiān)控、測試和版本控制。
未來規(guī)劃
Farrow 目前的規(guī)劃中有兩個主要的方向:
首先是生態(tài),因為目前 Farrow 的開發(fā)團隊比較小,所以不管是一些基礎的工具庫還是文檔、最佳實踐都是缺失和不完善的,但這些內(nèi)容缺失導致很少的開發(fā)者能夠了解 Farrow 并使用它,所以這將是接下來 Farrow 團隊的比較主要的工作方向。除此之外,是基礎能力。Farrow 目前還不夠系統(tǒng),我們還沒有將它的潛力完全發(fā)揮出來,所以也會有一大部分精力投入在繼續(xù)探索它能力的邊界。
Bonus: farrow-express & farrow-koa
需要告訴大家的一個好消息是:Farrow 現(xiàn)在已經(jīng)可以通過 adapter 復用 Express/Koa 等生態(tài):
farrow-express:將 farrow-http 運行在 Express App 上 farrow-koa:將 farrow-http 運行在 Koa App 上
總結
在本篇文章中
-
我們了解了類型安全的定義及其價值
-
我們看到了當前 Node.js Web 框架中存在的類型問題
-
我們看到了 Farrow-HTTP 如何通過類型優(yōu)先和函數(shù)式的思路,系統(tǒng)性地改善類型問題
-
我們看到了 Farrow-API 如何貫通前后端類型
-
我們了解了現(xiàn)在立刻能就在 Express/Koa 等應用中使用 Farrow 的方式
-
我們了解了 Farrow 以及其它追求類型安全的框架將來要解決的問題