【详细教程】教你如何使用Node + Express + Typescript开发一个应用
Express是nodejs開發中普遍使用的一個框架,下面要談的是如何結合Typescript去使用。
目標
我們的目標是能夠使用Typescript快速開發我們的應用程序,而最終我們的應用程序卻是編譯為原始的JavaScript代碼,以由nodejs運行時來執行。
初始化設置
首要的是我們要創建一個目錄名為express-typescript-app來存放我們的項目代碼:
mkdir?express-typescript-app cd?express-typescript-app為了實現我們的目標,首先我們需要區分哪些是線上程序依賴項,哪些是開發依賴項,這樣可以確保最終編譯的代碼都是有用的。
在這個教程中,將使用yarn命令作為程序包管理器,當然npm也是一樣可以的。
生產環境依賴
express作為程序的主體框架,在生產環境中是必不可少的,需要安裝
yarn?add?express這樣當前目錄下就生成了一個package.json 文件,里面暫時只有一個依賴
開發環境依賴項
在開發環境中我們將要使用Typescript編寫代碼。所以我們需要安裝typescript。另外也需要安裝node和express的類型聲明。安裝的時候帶上- D參數來確保它是開發依賴。
yarn?add?-D?typescript?@types/express?@types/node安裝好之后,還有一點值得注意,我們并不想每次代碼更改之后還需要手動去執行編譯才生效。這樣體驗太不好了!所以我們需要額外添加幾個依賴:
-
ts-node: 這個安裝包是為了不用編譯直接運行typescript代碼,這個對本地開發太有必要了
-
nodemon:這個安裝包在程序代碼變更之后自動監聽然后重啟開發服務。搭配ts-node模塊就可以做到編寫代碼及時生效。
因此這兩個依賴都是在開發的時候需要的,而不需編譯進生產環境的。
yarn?add?-D?ts-node?nodemon配置我們的程序運行起來
配置Typescript文件
為我們將要用的typescript設置配置文件,創建tsconfig.json文件
touch?tsconfig.json現在讓我們給配置文件添加編譯相關的配置參數:
-
module: "commonjs" — 如果使用過node的都知道,這個作為編譯代碼時將被編譯到最終代碼是必不可少的。
-
esModuleInterop: true — 這個選項允許我們默認導出的時候使用*代替導出的內容。
-
target: "es6" — 不同于前端代碼,我們需要控制運行環境,得確保使用的node版本能正確識別ES6語法。
-
rootDir: "./" — 設置代碼的根目錄為當前目錄。
-
outDir: "./build" — 最終將Typescript代碼編譯成執行的Javascript代碼目錄。
-
strict: true — 允許嚴格類型檢查。
最終tsconfig.json文件內容如下:
{"compilerOptions":?{"module":?"commonjs","esModuleInterop":?true,"target":?"es6","rootDir":?"./","outDir":?"./build","strict":?true} }配置package.json腳本
目前還沒有 package.json文件的scripts項,我們需要添加幾個腳本:第一個是start啟動開發模式,另一個是 build打包線上環境代碼的命令。
啟動開發模式我們需要執行nodemon index.ts,而打包生產代碼,我們已經在tsconfig.json中給出了所有需要的信息,所以我們只需要執行tsc命令。
此刻下面是你package.json文件中所有的內容,也可能由于我們創建項目的時間不一樣,導致依賴的版本號不一樣。
{"dependencies":?{"express":?"^4.17.1"},"devDependencies":?{"@types/express":?"^4.17.11","@types/node":?"^14.14.22","nodemon":?"^2.0.7","ts-node":?"^9.1.1","typescript":?"^4.1.3"} }Git配置
如果使用git來管理代碼,還需要添加.gitignore文件來忽視node_modules目錄和build目錄
touch?.gitignore添加忽視的內容
node_modules build至此,所有的安裝過程已經結束,比單純的無Typescript版本可能稍微復雜點。
創建我們的Express應用
讓我們來正式開始創建express應用。首先創建主文件index.ts
touch?index.ts然后添加案例代碼,在網頁中輸出“hello world”
import?express?from?'express';const?app?=?express(); const?PORT?=?3000;app.get('/',?(req,?res)?=>?{res.send('Hello?world'); });app.listen(PORT,?()?=>?{console.log(`Express?with?Typescript!?http://localhost:${PORT}`); });在終端命令行執行啟動命令 yarn run start
yarn?run?start接下來會輸出以下內容:
[nodemon]?2.0.7 [nodemon]?to?restart?at?any?time,?enter?`rs` [nodemon]?watching?path(s):?*.* [nodemon]?watching?extensions:?ts,json [nodemon]?starting?`ts-node?index.ts` Express?with?Typescript!?http://localhost:3000我們可以看到nodemon模塊已經監聽到所有文件的變更后使用ts-node index.ts命令啟動了我們的應用。我們現在可以在瀏覽器打開網址http://localhost:3000,將會看到網頁中輸出我們想要的“hello world”。
“Hello World”以外的功能
我們的 “Hello World”應用算是創建好了,但是我們不僅于此,還要添加一些稍微復雜點的功能,來豐富一下應用。大致功能包括:
-
保存一系列的用戶名和與之匹配的密碼在內存中
-
允許提交一個POST請求去創建一個新的用戶
-
允許提交一個POST請求讓用戶登錄,并且接受因為錯誤認證返回的信息
讓我們一個個去實現以上功能!
保存用戶
首先,我們創建一個types.ts文件來定義我們用到的User類型。后面所有類型定義都寫在這個文件中。
touch?types.ts然后導出定義的User類型
export?type?User?=?{?username:?string;?password:?string?};好了。我們將使用內存來保存所有的用戶,而不是數據庫或者其它方式。根目錄下創建一個data目錄,然后在里面新建users.ts文件
mkdir?data touch?data/users.ts現在在users.ts文件里創建一個User類型的空數組
import?{?User?}?from?"../types";const?users:?User[]?=?[];提交新用戶
接下來我們希望向應用提交一個新用戶。我們在這里將要用到處理請求參數的中間件body-parse
yarn?add?body-parser然后在主文件里導入并使用它
import?express?from?'express'; import?bodyParser?from?'body-parser';const?app?=?express(); const?PORT?=?3000;app.use(bodyParser.urlencoded({?extended:?false?}));app.get('/',?(req,?res)?=>?{res.send('Hello?world'); });app.listen(PORT,?()?=>?{console.log(`Express?with?Typescript!?http://localhost:${PORT}`); });最后,我們可以在users文件里創建POST請求處理程序。 該處理程序將執行以下操作:
-
校驗請求體中是否包含了用戶名和密碼,并且進行有效性驗證
-
一旦提交的用戶名密碼不正確返回狀態碼為400的錯誤信息
-
添加一個新用戶到users數組中
-
返回一個201狀態的錯誤信息
讓我們開始,首先,在data/users.ts文件中創建一個addUser的方法
import?{?User?}?from?'../types';const?users:?User[]?=?[];const?addUser?=?(newUser:?User)?=>?{users.push(newUser); };然后回到index.ts文件中添加一條"/users"的路由
import?express?from?'express'; import?bodyParser?from?'body-parser'; import?{?addUser?}?from?'./data/users';const?app?=?express(); const?PORT?=?3000;app.use(bodyParser.urlencoded({?extended:?false?}));app.get('/',?(req,?res)?=>?{res.send('Hello?world'); });app.post('/users',?(req,?res)?=>?{const?{?username,?password?}?=?req.body;if?(!username?.trim()?||?!password?.trim())?{return?res.status(400).send('Bad?username?or?password');}addUser({?username,?password?});res.status(201).send('User?created'); });app.listen(PORT,?()?=>?{console.log(`Express?with?Typescript!?http://localhost:${PORT}`); });這里的邏輯不復雜,我們簡單解釋一下,首先請求體中要包含username和password兩個變量,而且使用trim()函數去除收尾的空字符,保證它的長度大于0。如果不滿足,返回400狀態和自定義錯誤信息。如果驗證通過,則將用戶信息添加到users數組并且返回201狀態回來。
注意:你有沒有發現users數組是沒有辦法知道有沒有同一個用戶被添加兩次的,我們暫且不考慮這種情況。
讓我們重新打開一個終端(不要關掉運行程序的終端),在終端里通過curl命令來發出一個POST請求注冊接口
curl?-d?"username=foo&password=bar"?-X?POST?http://localhost:3000/users你將會在終端的命令行中發現輸出了下面的信息
User?created然后再請求一次接口,這次password僅僅為空字符串,測試一下請求失敗的情況
curl?-d?"username=foo&password=?"?-X?POST?http://localhost:3000/users沒有讓我們失望,成功返回了一下錯誤信息
Bad?username?or?password登錄功能
登錄有點類似,我們從請求體中拿到username和password的值然后通過Array.find方法去users數組中查找相同的用戶名和密碼組合,返回200狀態碼說明用戶登錄成功,而401狀態碼表示用戶不被授權,登錄失敗。
首先我們在data/users.ts文件中添加getUser方法:
import?{?User?}?from?'../types';const?users:?User[]?=?[];export?const?addUser?=?(newUser:?User)?=>?{users.push(newUser); };export?const?getUser?=?(user:?User)?=>?{return?users.find((u)?=>?u.username?===?user.username?&&?u.password?===?user.password); };這里getUser方法將會從users數組里返回與之匹配用戶或者undefined。
接下來我們將在index.ts里調用getUser方法
import?express?from?'express'; import?bodyParser?from?'body-parser'; import?{?addUser,?getUser?}?from?"./data/users';const?app?=?express(); const?PORT?=?3000;app.use(bodyParser.urlencoded({?extended:?false?}));app.get('/',?(req,?res)?=>?{res.send('Hello?word'); });app.post('/users',?(req,?res)?=>?{const?{?username,?password?}?=?req.body;if?(!username?.trim()?||?!password?.trim())?{return?res.status(400).send('Bad?username?or?password');}addUser({?username,?password?});res.status(201).send('User?created'); });app.post('/login',?(req,?res)?=>?{const?{?username,?password?}?=?req.body;const?found?=?getUser({username,?password})if?(!found)?{return?res.status(401).send('Login?failed');}res.status(200).send('Success'); });app.listen(PORT,?()?=>?{console.log(`Express?with?Typescript!?http://localhost:${PORT}`); });現在我們還是用curl命令去請求注冊接口和登錄接口,登錄接口請求兩次,一次成功一次失敗
curl?-d?"username=joe&password=hard2guess"?-X?POST?http://localhost:3000/users #?User?createdcurl?-d?"username=joe&password=hard2guess"?-X?POST?http://localhost:3000/login #?Successcurl?-d?"username=joe&password=wrong"?-X?POST?http://localhost:3000/login #?Login?failed沒問題,結果都按我們預想的順利返回了
探索Express類型
您可能已經發現,講到現在,好像都是一些基礎的東西,Express里面比較深的概念沒有涉及到,比如自定義路由,中間件和句柄等功能,我們現在就來重構它。
自定義路由類型
或許我們希望的是創建這樣一個標準的路由結構像下面這樣
const?route?=?{method:?'post',path:?'/users',middleware:?[middleware1,?middleware2],handler:?userSignup, };我們需要在types.ts文件中定義一個Route類型。同時也需要從Express庫中導出相關的類型:Request,Response和NextFunction。Request表示客戶端的請求數據類型,Response是從服務器返回值類型,NextFunction則是next()方法的簽名,如果使用過express的中間件應該很熟悉。
在types.ts文件中,重新定義Route類型
export?type?User?=?{?username:?string;?password:?string?};type?Method?=|?'get'|?'head'|?'post'|?'put'|?'delete'|?'connect'|?'options'|?'trace'|?'patch';export?type?Route?=?{method:?Method;path:?string;middleware:?any[];handler:?any; };如果你熟悉express中間件的話,你應該知道一個典型的中間件長這樣:
function?middleware(request,?response,?next)?{//?Do?some?logic?with?the?requestif?(request.body.something?===?'foo')?{//?Failed?criteria,?send?forbidden?resposnereturn?response.status(403).send('Forbidden');}//?Succeeded,?go?to?the?next?middlewarenext(); }由此可知,一個中間件需要傳入三個參數,分別是Request,Response和NextFunction類型。因此如果需要我們創建一個Middleware類型:
import?{?Request,?Response,?NextFunction?}?from?'express';type?Middleware?=?(req:?Request,?res:?Response,?next:?NextFunction)?=>?any;然后express已經有了一個叫RequestHandler類型,所以在這里我們只需要從express導出就好了,如果取個別名可以采用類型斷言。
import?{?RequestHandler?as?Middleware?}?from?'express';export?type?User?=?{?username:?string;?password:?string?};type?Method?=|?'get'|?'head'|?'post'|?'put'|?'delete'|?'connect'|?'options'|?'trace'|?'patch';export?type?Route?=?{method:?Method;path:?string;middleware:?Middleware[];handler:?any; };最后我們只需要為handler指定類型。這里的handler應該是程序執行的最后一步,因此我們在設計的時候就不需要傳入next參數了,類型也就是RequestHandler去掉第三個參數。
import?{?Request,?Response,?RequestHandler?as?Middleware?}?from?'express';export?type?User?=?{?username:?string;?password:?string?};type?Method?=|?'get'|?'head'|?'post'|?'put'|?'delete'|?'connect'|?'options'|?'trace'|?'patch';export?type?Handler?=?(req:?Request,?res:?Response)?=>?any;export?type?Route?=?{method:?Method;path:?string;middleware:?Middleware[];handler:?Handler; };添加一些項目結構
我們需要通過增加一些結構來把中間件和處理程序從index.ts文件中移除
創建處理器
我們把一些處理方法移到handlers目錄中
mkdir?handlers touch?handlers/user.ts那么在handlers/user.ts文件中,我們添加如下代碼。和用戶注冊相關的處理代碼已經被我們從index.ts文件中重構到這里。重要的是我們可以確定signup方法滿足我們定義的Handlers類型
import?{?addUser?}?from?'../data/users'; import?{?Handler?}?from?'../types';export?const?signup:?Handler?=?(req,?res)?=>?{const?{?username,?password?}?=?req.body;if?(!username?.trim()?||?!password?.trim())?{return?res.status(400).send('Bad?username?or?password');}addUser({?username,?password?});res.status(201).send('User?created'); };同樣,我們把創建auth處理器添加login方法
touch?handlers/auth.ts添加以下代碼
import?{?getUser?}?from?'../data/users'; import?{?Handler?}?from?'../types';export?const?login:?Handler?=?(req,?res)?=>?{const?{?username,?password?}?=?req.body;const?found?=?getUser({?username,?password?});if?(!found)?{return?res.status(401).send('Login?failed');}res.status(200).send('Success'); };最后也給我們的首頁增加一個處理器
touch?handlers/home.ts功能很簡單,只要輸出文本
import?{?Handler?}?from?'../types';export?const?home:?Handler?=?(req,?res)?=>?{res.send('Hello?world'); };中間件
現在還沒有任何的自定義中間件,首先創建一個middleware目錄
mkdir?middleware我們將添加一個打印客戶端請求路徑的中間件,取名requestLogger.ts
touch?middleware/requestLogger.ts從express庫中導出需要定義的中間件類型的RequestHandler類型
import?{?RequestHandler?as?Middleware?}?from?'express';export?const?requestLogger:?Middleware?=?(req,?res,?next)?=>?{console.log(req.path);next(); };創建路由
既然我們已經定義了一個新的Route類型和自己的一些處理器,就可以把路由定義獨立出來一個文件,在根目錄創建routes.ts
touch?routes.ts以下是該文件的所有代碼,為了演示就只給/login添加了requestLogger中間件
import?{?login?}?from?'./handlers/auth'; import?{?home?}?from?'./handlers/home'; import?{?signup?}?from?'./handlers/user'; import?{?requestLogger?}?from?'./middleware/requestLogger'; import?{?Route?}?from?'./types';export?const?routes:?Route[]?=?[{method:?'get',path:?'/',middleware:?[],handler:?home,},{method:?'post',path:?'/users',middleware:?[],handler:?signup,},{method:?'post',path:?'/login',middleware:?[requestLogger],handler:?login,}, ];重構index.ts文件
最后也是最重要的一步就是簡化index.ts文件。我們通過一個forEach循環routes文件中聲明的路由信息來代替所有的route相關的代碼。這樣做最大的好處是為所有的路由定義了類型。
import?express?from?'express'; import?bodyParser?from?'body-parser'; import?{?routes?}?from?'./routes';const?app?=?express(); const?PORT?=?3000;app.use(bodyParser.urlencoded({?extended:?false?}));routes.forEach((route)?=>?{const?{?method,?path,?middleware,?handler?}?=?route;app[method](path,?...middleware,?handler); });app.listen(PORT,?()?=>?{console.log(`Express?with?Typescript!?http://localhost:${PORT}`); });這樣看起來代碼結構清晰多了,架構的好處就是如此。另外有了Typescript強類型的支持,保證了程序的穩定性。
完整代碼
Github:
https://github.com/fantingsheng/express-typescript-app
總結
以上是生活随笔為你收集整理的【详细教程】教你如何使用Node + Express + Typescript开发一个应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Github开源】一站搞定各种开发文档
- 下一篇: 如何在客户端终止一个已经发出的HTTP请