跳至主要内容

[Express] Using TypeScript

TypeScript 的型別和屬性宣告, 確實能減少不少 JavaScript 開發時小小疏失導致的錯誤, 尤其在 API Server 時, 接收客戶端資料若沒有驗證型別, 可能會導致意外錯誤發生. 利用 TypeScript 開發 API Server 是個不錯的主意.

建立專案

和所有 Node.js 專案一樣, 使用 npm 初始化新的 package.json 不會有錯.

npm init
yarn init

npmyarn 指令依個人習慣擇一即可.

會需要填入一些專案的相關資訊:

  • package name 輸入專案名稱, 預設使用資料夾名稱
  • version 版本號, 預設 1.0.0
  • description 專案的詳細介紹, 可不填
  • entry point 專案的程式載入點, 預設 index.js
  • test command 專案測試用的指令, 可不填
  • git repository 輸入 git 遠端位址, 可不填
  • keywords 輸入專案相關的關鍵字, 可不填
  • author 輸入專案作者名稱, 可不填
  • license 授權相關, 預設 ISC

--yes

若專案不會有發布 npm 的需求, 只想快速產生 package.json 來管理套件. 可加上 --yes 快速建立預設值內容如下.

{
"name": "express-typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"keywords": [],
"author": "",
"license": "ISC"
}

TypeScript

透過 npm 進行安裝 TypeScript. 依實際需求, 通常建議全域安裝.

npm i -g typescript
yarn global add typescript

若真的沒有其他 TypeScript 專案開發需求, 以專案角度來說, TypeScript 只是開發上需要, 歸屬於開發依賴套件即可.

npm i typescript --save-dev
yarn add typescript --dev

tsconfig.json

要透過 TypeScript 指令初始化 tsconfig.json

$ tsc init
$ npx tsc init

Created a new tsconfig.json with:
target: es2016
module: commonjs
strict: true
esModuleInterop: true
skipLibCheck: true
forceConsistentCasingInFileNames: true

自從多了 npx 之後, 部分 CLI 套件指令可以直接透過 npx 來執行. npx 會自動管理與處理套件依賴的下載等, 不一定需要安裝全域套件.

專案資落夾會多一個 tsconfig.json, 這個檔案是用來設定編譯選項的. 詳細設定可參考 官方文件

tsconfig.json
{
"compilerOptions": {
"incremental": true, // 啟用增量編譯
"target": "ES2017", // 編譯成指定的 JavaScript 版本
"module": "commonjs", // 指定編譯成何種模組
"declaration": true, // 產生 '.d.ts' 檔
"sourceMap": true, // 產生 '.map' 檔
"outDir": "./dist", // 指定編譯後的檔案存放點
"rootDir": "./src", // 載入點的位置
"removeComments": true, // 移除註解
"strict": true, // 採用嚴格模式
"baseUrl": "./src", // 指定匯入檔案的基準路徑
"esModuleInterop": true, // 兼容模組
"experimentalDecorators": true, // 啟用裝飾器
"emitDecoratorMetadata": true // 提供裝飾器 metadata
},
"include": ["src/**/*.ts"], // 納入編譯範圍
"exclude": ["node_modules", "dist"] //不納入編譯範圍
}

Express

安裝 Express:

$ npm i express
$ yarn add express

yarn add v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved 1 new dependency.
info Direct dependencies
└─ express@4.18.1
info All dependencies
└─ express@4.18.1
✨ Done in 0.88s.

若指令順利執行完畢, 可看到 package.json 中的 dependencies 多了 express, 表示安裝成功. 接著要取得 Node.js 與 express 的 type 定義檔, 在開發時能夠清楚知道有哪些功能可以使用:

npm i @types/node @types/express --save-dev
yarn add @types/node @types/express -D

index.js

TypeScript .ts 原始檔需經編譯後才可執行, 為方便編譯設置, 建議放 src 資料夾下.

├── src
| └── index.ts
├── package.json
└── tsconfig.json
index.ts
import express from 'express';

const app = express();

app.get('/', (req, res, next) => {
res.send('Hello, World!!');
});

app.listen(3000, () => console.log('http server is running at port 3000.'));

ts-node

TypeScript 程式無法直接透過 Node.js 來啟動它, 必須要編譯過後才能透過 Node.js 啟動. 但每次要測試都要執行一次編譯實在太麻煩了, 所以我們安裝 ts-node 來解決這個問題:

npm i ts-node --save-dev
yarn add ts-node -D

安裝完畢後, 可更改 package.json 的配置以透過 npm 的腳本來啟動 Express 專案. 在 package.json 中的 script 欄位添加 start 項目:

"scripts": {
"dev": "ts-node ./src/index.ts"
}
$ npm run dev
$ yarn run dev

$ ts-node ./src/index.ts
http server is running at port 3000.

啟動成功後, 可以在瀏覽器中輸入 http://localhost:3000 來檢視結果.

編譯

畢竟 ts-node 並不適合用在正式環境中, 還是需要編譯成 JavaScript 再透過 Node.js 來啟動.

要如何對專案進行編譯呢? 直接在專案下 tsc 即可編譯專案. tsc 會依 tsconfig.json 的設置從 rootDir 讀取所有 .ts 檔, 編譯後輸出到 outDir 位置.

$ tsc
$ node ./dist/index.js

上述指令即可編譯與啟動 Express Server.

可以把相關命令寫入 package.json script 裡:

package.json
"scripts": {
"start": "npm run build && node ./dist/index.js",
"dev": "ts-node ./src/index.ts",
"build": "tsc"
}

每次要編譯並啟動正式環境就執行:

npm start

自動化

開發階段若每次修改程式, 需手動停止服務再啟動才可更新. 可配合 nodemon 套件來做到程式修改後自動重載的管理.

npm i nodemon --save-dev
yarn add nodemon -D

nodemon 會監看該資料夾裡所有檔案, 若檔案有更改, 則會自動重新執行指令.

package.json
"scripts": {
"start": "npm run build && node ./dist/index.js",
"dev": "nodemon --exec ts-node ./src/index.ts",
"build": "tsc"
}

如此一來, 只需要執行一次 npm run dev yarn run dev, 程式修改時就會自動重新載入.

pm2

nodemon 除了可以監看檔案變化外, 若 Node.js 程式出錯, 也會重新執行指令, 以確保服務繼續運行. 而在正式環境上, 較常用功能較強大的 pm2 來取代 nodemon 作為錯誤重啟的監控.

環境變數

開發環境和正式環境長會有不同配置檔, dotenv 這個套件可以方便管理環境變數.

npm i dotenv
yarn add dotenv

通常至少會有 development / production 兩個環境:

.
├── src
| ├── index.ts
| └── environments
| ├── development.env
| └── production.env
├── package.json
└── tsconfig.json

透過 npm 的腳本傳入參數來指定現在的環境是 development 還是 production:

package.json
"scripts": {
"start": "npm run build && NODE_ENV=production node ./dist/index.js",
"dev": "nodemon --exec NODE_ENV=development ts-node ./src/index.ts",
"build": "tsc"
}

最後調整 index.ts 的內容, 透過 NODE_ENV 參數 與 dotenv 來選定環境變數:

index.ts
import express from 'express';
import path from 'path';
import dotenv from 'dotenv';

const app = express();

dotenv.config({ path: path.resolve(__dirname, `./environments/${ process.env.NODE_ENV }.env`) });

app.get('/', (req, res, next) => {
res.send('Hello, World!!');
});

app.listen(process.env.PORT, () => console.log(`http server is running at port ${ process.env.PORT }.`));
$ yarn run dev
yarn run v1.22.19
$ nodemon --exec NODE_ENV=development ts-node ./src/index.ts
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `NODE_ENV=development ts-node ./src/index.ts`
http server is running at port 3000.

Troubleshooting

fail in prod

依上述設定在開發環境下執行沒問題, 但執行 npm start 會出現 port undefined 錯誤如下:

$ yarn start
yarn run v1.22.19
$ npm run build && NODE_ENV=production node ./dist/index.js

> taostyler@1.0.0 build
> tsc

http server is running at port undefined.

主要原因在 development.envproduction.env 都放在 sec/environment 資料夾下, 測試時執行的 ts-node ./src/index.ts 可以讀到相對路徑下的環境設定檔, 而正式環境的指令 node ./dist/index.js, 在 disc 資料夾下僅有被編譯後的 .js 檔, 並不包含其他設定檔, 靜態資源等... 程式無法讀到對應環境配置檔而出錯.

專案根目錄

解決方法之一是把設定檔和靜態資源檔挪出 src 資料夾, 如下:

├── config
│ └── env
│ ├── development.env
│ └── production.env
├── dist
├── package.json
├── src
│ └── index.ts
├── tsconfig.json
└── static

修改程式, 新增 ROOT 常態變數, 透過 ROOT 相對路徑去讀取環境配置檔或其他設定檔:

const ROOT = path.resolve(__dirname, `../`)
dotenv.config({ path: `${ ROOT }/config/env/${ process.env.NODE_ENV }.env`});
app.set('ROOT', ROOT)

copyfiles

另一個作法是把相關資源檔複製到 disc 資料夾下. 以維持和 src 資料夾下相同的結構.

然而 tsc 不支援編譯時候順便複製非 .ts 檔案到 disc 資料夾, 必須自己手動處理相關檔案的複製. 但可以修改 package.json 來簡化相關流程. 調整範例如下:

package.json
// ...
"scripts": {
"clean": "rimraf dist/",
"copy-files": "copyfiles -u 1 src/**/*.html src/**/*.css src/**/*.env src/**/*.json dist/",
"build": "yarn clean && tsc && yarn copy-files"
},
// ...

這邊用了兩個 node package 來簡化命令:

  • rimraf The UNIX command rm -rf for node.
  • copyfiles, with a very descriptive name.
npm i --save-dev rimraf copyfiles
yarn add -D rimraf copyfiles

這樣執行 npm build 時除了將 .ts 檔案編譯成 .js 之外, 也會把相關資源檔一起複製到 disc 資料夾下, 部署更新時, 直接更新 disc 資料夾下的檔案即可.

Ref: tsc: How to copy non-TypeScript files when building - Víctor Colombo

docker / packager

上述方法適合專案初期對正式環境的測試使用, 但長期更新維護的專案, 正式環境通常會有更嚴謹的規範. 可能透過 dockerfile 或其他的打包工具, 依專案實際規範而定, 暫不討論.

See Also

除了依本文從零開始建力與配置專案外, 也可以利用 typescript-express-starter 之類的套件, 或直接沿用 greenroach/express-ts-template 之類的樣板來建立專案.

文章內容步驟可能因版本迭代導致語法錯誤等, 但相關套件若有持續更新, 使用套件或樣板來建立專案, 仍是不錯的選擇.

References