1. 程式人生 > 其它 >【詳細教程】教你如何使用Node + Express + Typescript開發一個應用

【詳細教程】教你如何使用Node + Express + Typescript開發一個應用

技術標籤:中介軟體pythonjava資料庫go

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"

    — 如果使用過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

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}`);
});

這裡的邏輯不復雜,我們簡單解釋一下,首先請求體中要包含usernamepassword兩個變數,而且使用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

登入功能

登入有點類似,我們從請求體中拿到usernamepassword的值然後通過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庫中匯出相關的型別:RequestResponseNextFunctionRequest表示客戶端的請求資料型別,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();
}

由此可知,一箇中間件需要傳入三個引數,分別是RequestResponseNextFunction型別。因此如果需要我們建立一個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