【詳細教程】教你如何使用Node + Express + Typescript開發一個應用
Express是nodejs開發中普遍使用的一個框架,下面要談的是如何結合Typescript去使用。
目標
我們的目標是能夠使用Typescript快速開發我們的應用程式,而最終我們的應用程式卻是編譯為原始的JavaScript程式碼,以由nodejs執行時來執行。
初始化設定
首要的是我們要建立一個目錄名為express-typescript-app
來存放我們的專案程式碼:
mkdirexpress-typescript-app
cdexpress-typescript-app
為了實現我們的目標,首先我們需要區分哪些是線上程式依賴項,哪些是開發依賴項,這樣可以確保最終編譯的程式碼都是有用的。
在這個教程中,將使用yarn命令作為程式包管理器,當然npm也是一樣可以的。
生產環境依賴
express
作為程式的主體框架,在生產環境中是必不可少的,需要安裝
yarnaddexpress
這樣當前目錄下就生成了一個package.json 檔案,裡面暫時只有一個依賴
開發環境依賴項
在開發環境中我們將要使用Typescript編寫程式碼。所以我們需要安裝typescript
。另外也需要安裝node和express的型別宣告。安裝的時候帶上- D
引數來確保它是開發依賴。
yarnadd-Dtypescript@types/express@types/node
安裝好之後,還有一點值得注意,我們並不想每次程式碼更改之後還需要手動去執行編譯才生效。這樣體驗太不好了!所以我們需要額外新增幾個依賴:
ts-node: 這個安裝包是為了不用編譯直接執行typescript程式碼,這個對本地開發太有必要了
nodemon:這個安裝包在程式程式碼變更之後自動監聽然後重啟開發服務。搭配
ts-node
模組就可以做到編寫程式碼及時生效。
因此這兩個依賴都是在開發的時候需要的,而不需編譯進生產環境的。
yarnadd-Dts-nodenodemon
配置我們的程式執行起來
配置Typescript檔案
為我們將要用的typescript設定配置檔案,建立tsconfig.json
檔案
touchtsconfig.json
現在讓我們給配置檔案新增編譯相關的配置引數:
module: "commonjs"
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
touchindex.ts
然後新增案例程式碼,在網頁中輸出“hello world”
importexpressfrom'express';
constapp=express();
constPORT=3000;
app.get('/',(req,res)=>{
res.send('Helloworld');
});
app.listen(PORT,()=>{
console.log(`ExpresswithTypescript!http://localhost:${PORT}`);
});
在終端命令列執行啟動命令 yarn run start
yarnrunstart
接下來會輸出以下內容:
[nodemon]2.0.7
[nodemon]torestartatanytime,enter`rs`
[nodemon]watchingpath(s):*.*
[nodemon]watchingextensions:ts,json
[nodemon]starting`ts-nodeindex.ts`
ExpresswithTypescript!http://localhost:3000
我們可以看到nodemon模組已經監聽到所有檔案的變更後使用ts-node index.ts
命令啟動了我們的應用。我們現在可以在瀏覽器開啟網址http://localhost:3000
,將會看到網頁中輸出我們想要的“hello world”。
“Hello World”以外的功能
我們的 “Hello World”應用算是建立好了,但是我們不僅於此,還要新增一些稍微複雜點的功能,來豐富一下應用。大致功能包括:
儲存一系列的使用者名稱和與之匹配的密碼在記憶體中
允許提交一個POST請求去建立一個新的使用者
允許提交一個POST請求讓使用者登入,並且接受因為錯誤認證返回的資訊
讓我們一個個去實現以上功能!
儲存使用者
首先,我們建立一個types.ts
檔案來定義我們用到的User
型別。後面所有型別定義都寫在這個檔案中。
touchtypes.ts
然後匯出定義的User
型別
exporttypeUser={username:string;password:string};
好了。我們將使用記憶體來儲存所有的使用者,而不是資料庫或者其它方式。根目錄下建立一個data
目錄,然後在裡面新建users.ts
檔案
mkdirdata
touchdata/users.ts
現在在users.ts檔案裡建立一個User型別的空陣列
import{User}from"../types";
constusers:User[]=[];
提交新使用者
接下來我們希望向應用提交一個新使用者。我們在這裡將要用到處理請求引數的中介軟體body-parse
yarnaddbody-parser
然後在主檔案裡匯入並使用它
importexpressfrom'express';
importbodyParserfrom'body-parser';
constapp=express();
constPORT=3000;
app.use(bodyParser.urlencoded({extended:false}));
app.get('/',(req,res)=>{
res.send('Helloworld');
});
app.listen(PORT,()=>{
console.log(`ExpresswithTypescript!http://localhost:${PORT}`);
});
最後,我們可以在users檔案裡建立POST請求處理程式。 該處理程式將執行以下操作:
校驗請求體中是否包含了使用者名稱和密碼,並且進行有效性驗證
一旦提交的使用者名稱密碼不正確返回狀態碼為
400
的錯誤資訊新增一個新使用者到users陣列中
返回一個201狀態的錯誤資訊
讓我們開始,首先,在data/users.ts
檔案中建立一個addUser
的方法
import{User}from'../types';
constusers:User[]=[];
constaddUser=(newUser:User)=>{
users.push(newUser);
};
然後回到index.ts
檔案中新增一條"/users"
的路由
importexpressfrom'express';
importbodyParserfrom'body-parser';
import{addUser}from'./data/users';
constapp=express();
constPORT=3000;
app.use(bodyParser.urlencoded({extended:false}));
app.get('/',(req,res)=>{
res.send('Helloworld');
});
app.post('/users',(req,res)=>{
const{username,password}=req.body;
if(!username?.trim()||!password?.trim()){
returnres.status(400).send('Badusernameorpassword');
}
addUser({username,password});
res.status(201).send('Usercreated');
});
app.listen(PORT,()=>{
console.log(`ExpresswithTypescript!http://localhost:${PORT}`);
});
這裡的邏輯不復雜,我們簡單解釋一下,首先請求體中要包含username
和password
兩個變數,而且使用trim()
函式去除收尾的空字元,保證它的長度大於0。如果不滿足,返回400
狀態和自定義錯誤資訊。如果驗證通過,則將使用者資訊新增到users
陣列並且返回201
狀態回來。
注意:你有沒有發現users陣列是沒有辦法知道有沒有同一個使用者被新增兩次的,我們暫且不考慮這種情況。
讓我們重新開啟一個終端(不要關掉執行程式的終端),在終端裡通過curl
命令來發出一個POST請求註冊介面
curl-d"username=foo&password=bar"-XPOSThttp://localhost:3000/users
你將會在終端的命令列中發現輸出了下面的資訊
Usercreated
然後再請求一次介面,這次password僅僅為空字串,測試一下請求失敗的情況
curl-d"username=foo&password="-XPOSThttp://localhost:3000/users
沒有讓我們失望,成功返回了一下錯誤資訊
Badusernameorpassword
登入功能
登入有點類似,我們從請求體中拿到username
和password
的值然後通過Array.find
方法去users
陣列中查詢相同的使用者名稱和密碼組合,返回200
狀態碼說明使用者登入成功,而401
狀態碼錶示使用者不被授權,登入失敗。
首先我們在data/users.ts
檔案中新增getUser方法:
import{User}from'../types';
constusers:User[]=[];
exportconstaddUser=(newUser:User)=>{
users.push(newUser);
};
exportconstgetUser=(user:User)=>{
returnusers.find(
(u)=>u.username===user.username&&u.password===user.password
);
};
這裡getUser
方法將會從users
數組裡返回與之匹配使用者或者undefined
。
接下來我們將在index.ts
裡呼叫getUser
方法
importexpressfrom'express';
importbodyParserfrom'body-parser';
import{addUser,getUser}from"./data/users';
constapp=express();
constPORT=3000;
app.use(bodyParser.urlencoded({extended:false}));
app.get('/',(req,res)=>{
res.send('Helloword');
});
app.post('/users',(req,res)=>{
const{username,password}=req.body;
if(!username?.trim()||!password?.trim()){
returnres.status(400).send('Badusernameorpassword');
}
addUser({username,password});
res.status(201).send('Usercreated');
});
app.post('/login',(req,res)=>{
const{username,password}=req.body;
constfound=getUser({username,password})
if(!found){
returnres.status(401).send('Loginfailed');
}
res.status(200).send('Success');
});
app.listen(PORT,()=>{
console.log(`ExpresswithTypescript!http://localhost:${PORT}`);
});
現在我們還是用curl命令去請求註冊介面和登入介面,登入介面請求兩次,一次成功一次失敗
curl-d"username=joe&password=hard2guess"-XPOSThttp://localhost:3000/users
#Usercreated
curl-d"username=joe&password=hard2guess"-XPOSThttp://localhost:3000/login
#Success
curl-d"username=joe&password=wrong"-XPOSThttp://localhost:3000/login
#Loginfailed
沒問題,結果都按我們預想的順利返回了
探索Express型別
您可能已經發現,講到現在,好像都是一些基礎的東西,Express裡面比較深的概念沒有涉及到,比如自定義路由,中介軟體和控制代碼等功能,我們現在就來重構它。
自定義路由型別
或許我們希望的是建立這樣一個標準的路由結構像下面這樣
constroute={
method:'post',
path:'/users',
middleware:[middleware1,middleware2],
handler:userSignup,
};
我們需要在types.ts
檔案中定義一個Route
型別。同時也需要從Express庫中匯出相關的型別:Request
,Response
和NextFunction
。Request
表示客戶端的請求資料型別,Response
是從伺服器返回值型別,NextFunction
則是next()方法的簽名,如果使用過express的中介軟體應該很熟悉。
在types.ts
檔案中,重新定義Route型別
exporttypeUser={username:string;password:string};
typeMethod=
|'get'
|'head'
|'post'
|'put'
|'delete'
|'connect'
|'options'
|'trace'
|'patch';
exporttypeRoute={
method:Method;
path:string;
middleware:any[];
handler:any;
};
如果你熟悉express中介軟體的話,你應該知道一個典型的中介軟體長這樣:
functionmiddleware(request,response,next){
//Dosomelogicwiththerequest
if(request.body.something==='foo'){
//Failedcriteria,sendforbiddenresposne
returnresponse.status(403).send('Forbidden');
}
//Succeeded,gotothenextmiddleware
next();
}
由此可知,一箇中間件需要傳入三個引數,分別是Request
,Response
和NextFunction
型別。因此如果需要我們建立一個Middleware
型別:
import{Request,Response,NextFunction}from'express';
typeMiddleware=(req:Request,res:Response,next:NextFunction)=>any;
然後express已經有了一個叫RequestHandler
型別,所以在這裡我們只需要從express匯出就好了,如果取個別名可以採用型別斷言。
import{RequestHandlerasMiddleware}from'express';
exporttypeUser={username:string;password:string};
typeMethod=
|'get'
|'head'
|'post'
|'put'
|'delete'
|'connect'
|'options'
|'trace'
|'patch';
exporttypeRoute={
method:Method;
path:string;
middleware:Middleware[];
handler:any;
};
最後我們只需要為handler
指定型別。這裡的handler
應該是程式執行的最後一步,因此我們在設計的時候就不需要傳入next
引數了,型別也就是RequestHandler
去掉第三個引數。
import{Request,Response,RequestHandlerasMiddleware}from'express';
exporttypeUser={username:string;password:string};
typeMethod=
|'get'
|'head'
|'post'
|'put'
|'delete'
|'connect'
|'options'
|'trace'
|'patch';
exporttypeHandler=(req:Request,res:Response)=>any;
exporttypeRoute={
method:Method;
path:string;
middleware:Middleware[];
handler:Handler;
};
新增一些專案結構
我們需要通過增加一些結構來把中介軟體和處理程式從index.ts檔案中移除
建立處理器
我們把一些處理方法移到handlers目錄中
mkdirhandlers
touchhandlers/user.ts
那麼在handlers/user.ts
檔案中,我們新增如下程式碼。和使用者註冊相關的處理程式碼已經被我們從index.ts
檔案中重構到這裡。重要的是我們可以確定signup
方法滿足我們定義的Handlers
型別
import{addUser}from'../data/users';
import{Handler}from'../types';
exportconstsignup:Handler=(req,res)=>{
const{username,password}=req.body;
if(!username?.trim()||!password?.trim()){
returnres.status(400).send('Badusernameorpassword');
}
addUser({username,password});
res.status(201).send('Usercreated');
};
同樣,我們把建立auth處理器新增login方法
touchhandlers/auth.ts
新增以下程式碼
import{getUser}from'../data/users';
import{Handler}from'../types';
exportconstlogin:Handler=(req,res)=>{
const{username,password}=req.body;
constfound=getUser({username,password});
if(!found){
returnres.status(401).send('Loginfailed');
}
res.status(200).send('Success');
};
最後也給我們的首頁增加一個處理器
touchhandlers/home.ts
功能很簡單,只要輸出文字
import{Handler}from'../types';
exportconsthome:Handler=(req,res)=>{
res.send('Helloworld');
};
中介軟體
現在還沒有任何的自定義中介軟體,首先建立一個middleware目錄
mkdirmiddleware
我們將新增一個列印客戶端請求路徑的中介軟體,取名requestLogger.ts
touchmiddleware/requestLogger.ts
從express庫中匯出需要定義的中介軟體型別的RequestHandler
型別
import{RequestHandlerasMiddleware}from'express';
exportconstrequestLogger:Middleware=(req,res,next)=>{
console.log(req.path);
next();
};
建立路由
既然我們已經定義了一個新的Route型別和自己的一些處理器,就可以把路由定義獨立出來一個檔案,在根目錄建立routes.ts
touchroutes.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';
exportconstroutes: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相關的程式碼。這樣做最大的好處是為所有的路由定義了型別。
importexpressfrom'express';
importbodyParserfrom'body-parser';
import{routes}from'./routes';
constapp=express();
constPORT=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(`ExpresswithTypescript!http://localhost:${PORT}`);
});
這樣看起來程式碼結構清晰多了,架構的好處就是如此。另外有了Typescript強型別的支援,保證了程式的穩定性。
完整程式碼
Github:
https://github.com/fantingsheng/express-typescript-app