1. 程式人生 > 實用技巧 >搭建node服務(三):使用TypeScript

搭建node服務(三):使用TypeScript

JavaScript 是一門動態弱型別語言,對變數的型別非常寬容。JavaScript使用靈活,開發速度快,但是由於型別思維的缺失,一點小的修改都有可能導致意想不到的錯誤,使用TypeScript可以很好的解決這種問題。TypeScript是JavaScript的一個超集,擴充套件了 JavaScript 的語法,增加了靜態型別、類、模組、介面和型別註解等功能,可以編譯成純JavaScript。本文將介紹如何在node服務中使用TypeScript。

一、 安裝依賴

npm install typescript --save
npm install ts-node --save
npm install nodemon --save

或者

yarn add typescript
yarn add ts-node
yarn add nodemon

另外,還需要安裝依賴模組的型別庫:

npm install @types/koa --save
npm install @types/koa-router --save
…

或者

yarn add @types/koa
yarn add @types/koa-router
…

二、 tsconfig.json

當使用tsc命令進行編譯時,如果未指定ts檔案,編譯器會從當前目錄開始去查詢tsconfig.json檔案,並根據tsconfig.json的配置進行編譯。

1. 指定檔案

可以通過files屬性來指定需要編譯的檔案,如下所示:

{
  "files": [
    "src/server.ts"
  ]
}

另外也可以通過使用"include"和"exclude"屬性來指定,採用類似glob檔案匹配模式,如下所示:

{
  "include": [
   "src/**/*"
  ],
  "exclude": [
   "node_modules",
   "**/*.spec.ts"
  ]
}

支援的萬用字元:

    • 匹配0或多個字元(不包括目錄分隔符)
  1. ? 匹配一個任意字元(不包括目錄分隔符)
  2. **/ 遞迴匹配任意子目錄

2. 常用配置

compilerOptions 屬性用於配置編譯選項,與tsc命令的選項一致,常用的配置如下所示:

{
  "compilerOptions": {
    // 指定編譯為ECMAScript的哪個版本。預設為"ES3"
    "target": "ES6",
    // 編譯為哪種模組系統。如果target為"ES3"或者"ES5",預設為"CommonJS",否則預設為"ES6"
    "module": "CommonJS",
    // 模組解析策略,"Classic" 或者 "Node"。如果module為"AMD"、"System"或者"ES6",預設為"Classic",否則預設為"Node"
    "moduleResolution": "Node",
    // 是否支援使用import cjs from 'cjs'的方式引入commonjs包
    "esModuleInterop": true,
    // 編譯過程中需要引入的庫。target為"ES5"時,預設引入["DOM","ES5","ScriptHost"];target為"ES6"時,預設引入["DOM","ES6","DOM.Iterable","ScriptHost"]
    "lib": ["ES6"],
    // 編譯生成的js檔案所輸出的根目錄,預設輸出到ts檔案所在的目錄
    "outDir": "dist",
    // 生成相應的.map檔案
    "sourceMap": true
  },
  "include": [
   "src/**/*"
  ],
  "exclude": [
   "node_modules",
   "**/*.spec.ts"
  ]
}

1) target

target是編譯目標,可以指定編譯為ECMAScript的哪個版本,預設為"ES3"。ECMAScript的版本有:"ES3" 、"ES5"、 "ES6" 或者 "ES2015"、 "ES2016"、 "ES2017"、"ES2018"、"ES2019"、 "ES2020"、"ESNext"。

2) module

module指定編譯為哪種模組系統,如果target為"ES3"或者"ES5",預設為"CommonJS",否則預設為"ES6"。可選用的模組系統有:"None"、 "CommonJS"、 "AMD",、"System"、 "UMD"、"ES6"或者"ES2015"、"ESNext"。

3) moduleResolution

moduleResolution指定模組解析策略,模組解析策略有:"Classic"、"Node",如果module為"AMD"、"System"或者"ES6",預設為"Classic",否則預設為"Node"。

示例1:

在/root/src/moduleA.ts中以import { b } from "./moduleB" 方式相對引用一個模組。
Classic解析策略,查詢過程:

/root/src/moduleB.ts
/root/src/moduleB.d.ts

Node解析策略,查詢過程:

/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (如果指定了"types"屬性)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts

示例2:

在/root/src/moduleA.ts中以import { b } from "moduleB" 方式非相對引用一個模組。
Classic解析策略,查詢過程:

/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts

Node解析策略,查詢過程:

/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (如果指定了"types"屬性)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts

/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (如果指定了"types"屬性)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts

/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json (如果指定了"types"屬性)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts

4) esModuleInterop

esModuleInterop為true時,表示支援使用import d from 'cjs'的方式引入commonjs包。當commonjs模組轉化為esm時,會增加 __importStar 和 __importDefault 方法來處理轉化問題。

示例:

cjs為commonjs模組,程式碼如下:

module.exports = { name: 'cjs' };

另外一個模組以esm方式引用了cjs模組,程式碼如下:

import cjsDefault from 'cjs';
import * as cjsStar from 'cjs';

console.log('cjsDefault =', cjsDefault);
console.log('cjsStar =', cjsStar);

輸出結果為:

cjsDefault = { name: 'cjs' }
cjsStar = { name: 'cjs', default: { name: 'cjs' } }

編譯後生成的程式碼如下:

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });

const cjs_1 = __importDefault(require("cjs"));
const cjsStar = __importStar(require("cjs"));

console.log('cjsDefault =', cjs_1.default);
console.log('cjsStar =', cjsStar);

5) lib

lib指定編譯過程中需要引入的庫。target為"ES5"時,預設引入["DOM","ES5","ScriptHost"];target為"ES6"時,預設引入["DOM","ES6","DOM.Iterable","ScriptHost"]。由於本示例TypeScript是用於服務端的,不需要使用DOM和ScriptHost,所以lib設為["ES6"]。

6) outDir

輸出目錄,編譯生成的js檔案所輸出的根目錄,預設輸出到ts檔案所在的目錄。

7) sourceMap

是否生成source map檔案,通過使用source map 可以在錯誤資訊中可以顯示原始碼位置。
要想根據source map 顯示錯誤資訊原始碼位置,還需要在入口檔案引入source-map-support 模組,如下:

import 'source-map-support/register';

三、 指令碼命令

入口檔案為src/server.ts,package.json中的scripts配置如下:

  • package.json
{
  "scripts": {
    "dev": "nodemon --watch src -e ts,tsx --exec ts-node src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  },
  …
}
  1. 執行 npm run dev 命令可以啟動開發環境,當src下的檔案被修改後會自動重新啟動服務。
  2. 執行 npm run build 命令會進行編譯,由於tsconfig.json中 outDir 指定輸出目錄為dist,編譯後的js檔案將出輸出到dist目錄。
  3. 執行 npm run start 命令可以啟動應用,啟動前需要執行 npm run build 進行編譯。

四、 自定義型別

TypeScript 會自動從 node_modules/@types 目錄獲取模組的型別定義,引用的模組都需要安裝對應型別庫,如:

npm install @types/koa --save

安裝後,會在node_modules/@types 目錄下找到koa 資料夾,該資料夾下有koa相關的型別定義檔案。當引用koa模組時會自動引入node_modules/ 和 node_modules/@types下的 koa 包。如果某個模組沒有型別庫或者對某個模組進行了擴充套件需要修改型別定義,這時需要引入自定義的型別。

示例:給koa增加bodyparser中介軟體

1. 設定typeRoots

  • tsconfig.json
{
  "compilerOptions": {
…
   // 型別宣告檔案所在目錄
   "typeRoots": ["./node_modules/@types", "./src/types"],
},
"include": [
   "src/**/*"
  ],
  "exclude": [
   "node_modules",
   "**/*.spec.ts"
 ]
}

src/types是存放自定義型別的目錄,本示例中src/types目錄已被include包含,如果自定義的型別目錄未被include包含還需要在include中新增該目錄。

2. 編寫型別定義檔案

  • src/types/koa/index.d.ts
import * as Koa from "koa";

declare module "koa" {
    interface Request {
        body?: object;
        rawBody: string;
    }
}

這裡給koa的request物件增加body和rawBody兩個屬性,分別用於存放請求體的json物件和原始字串。

3. 編寫 jsonBodyParser 外掛

  • src/middleware/jsonBodyParser.ts
import Koa from "koa";

function getRawBody(ctx: Koa.Context): Promise<string> {
  return new Promise((resolve, reject) => {
      try {
        let postData: string = '';
        ctx.req.addListener('data', (data) => {
          postData += data;
        });
        ctx.req.on('end', () => {
          resolve(postData);
        });
      } catch (e) {
        console.error('獲取body內容失敗', e);
        reject(e);
      }
  })
}

export default function jsonBodyParser (): Koa.Middleware {
    return async(ctx: Koa.Context, next: Koa.Next) => {
      const rawBody: string = await getRawBody(ctx);
      const request: Koa.Request = ctx.request;
      request.rawBody = rawBody;
      if (rawBody) {
        try {
          request.body = JSON.parse(rawBody);
        } catch (e) {
          request.body = {};
        }
      }
      await next();   
    };
}

jsonBodyParser()會返回一個koa中介軟體,這個中介軟體將獲取請求體的內容,將原始內容字串賦值到ctx.request.rawBody,將請求體內容json物件賦值到ctx.request.body。由於src/types/koa/index.d.ts自定義型別已經擴充套件了Koa.Request的這兩個屬性,執行npm run build命令,使用 tsc 進行編譯,可以編譯成功。但是當執行 npm run dev 時,會提示編譯錯誤,那是因為ts-node預設不會根據配置中的files、include 和 exclude 載入所有ts檔案,而是從入口檔案開始根據引用和依賴載入檔案。最簡單的解決辦法就是在 ts-node 命令後增加 --files 引數,表示按配置的files、include 和 exclude載入ts檔案,如下:

  • package.json
{
  "scripts": {
    "dev": " nodemon --watch src -e ts,tsx --exec ts-node --files src/server.ts",
  }
}

五、 說明

本文介紹瞭如何在node服務中使用TypeScript,具體的TypeScript語法規則網上有很多相關的資料,這裡就不再介紹了。本文相關的程式碼已提交到GitHub以供參考,
專案地址:https://github.com/liulinsp/node-server-typescript-demo

作者:宜信技術學院 劉琳