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

作者:宜信技术学院 刘琳