TypeScript + ExpressJS 快速搭建小工具服务

2020年6月21日 · 4 years ago

TypeScript + ExpressJS 快速搭建小工具服务

平时偶尔会需要用到一些小工具来替代重复的劳动,Node.js 一直是我的首选。虽然 Javascript 是一门神奇的语言,充满各种玄妙的“艺术机关”令新手迷失其间不知所措。而写小工具的初期因为代码量小,所以各种不规范和弱类型奇技淫巧一旦用上,将来项目大了就很可怕了,各种莫名其妙的 Crash 让你不知所措。

还好大微软 2012 年推出了 TypeScript 语言,冲着原生 JS 的弱点,在保留弱类型语言优点的基础上,增加了强类型语言的特性。

不过 ts 出来这么多年我都没怎么好好用过,作为一个业余前端开发,半年天翻地覆的技术更迭让我的印象还停留在早就过时的 CoffeeScript 以及新的 ES6 上。但是这两者都没有解决弱类型语言接口无法定义,类型检查代码极其冗长的问题。所以近来我投入了 ts 的怀抱,这里分享一下我平时如何用 ts + express 快速搭建小工具服务。

一、新建 Node 项目,配置 ts

mkdir new-project
cd new-project
npm init

开发过 Node 项目的朋友都知道怎么用 npmyarn 初始化项目,这里不再赘述,接下来简述下如何配置 ts

# 全局安装 typescript 用于编译
npm i -g typescript

# 创建 ts 配置文件
touch tsconfig.json

# 创建源文件目录与编译目录
mkdir src
mkdir built

多数人应该都习惯用 npm script 跑项目命令,我比较习惯用 make

touch Makefile

把比较冗长的常用命令塞进去即可:

all:
    make build
build:
    tsc
watch:
    tsc -w src/**/*.ts src/*.ts --outDir built

接下来是 tsconfig.json 的内容:

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true,
        "outDir": "built",
        "types": ["node"]
    },
    "include": [
        "src/**/*", "config.js"
    ]
}

我常用的编辑器是 VS Code,可以在编辑器底下开两个 Console 窗口,一边开 make watch,保存即编译;一边用于调试。

二、搭配 Express 的项目结构

express, koa 和 hapi 我都试过,作为一个业余前端觉得,还是 express 更新多一点,插件生态更丰富一点,所以一直用 express 来做接入。

一般我写的小工具要嘛是作为 API Server 吐 JSON 给客户端,要嘛是兼具前端展示部分,用 pug 做模板展示页面。两者大同小异,无非是客户端逻辑放在哪里而已。

一般我把入口放在 index.ts 或者 app.ts

import express = require('express')
import bodyParser = require('body-parser')
import { log } from './Foundation/log'
import SendMessageHander from './RequestHandler/SendMessageHandler'

const port = 3009
const app = express()

// 配置 express 插件,这里用了 body-parser
app.use(bodyParser.urlencoded({extended: false}))
app.use(bodyParser.json())

// 配置路由,把请求转发到对应的 Handler 模块
app.post('/sendMessage', new SendMessageHander().response)

// Start
app.listen(port, () => {
    log.info(`http://127.0.0.1:${port}`)
    log.info(`Running at port ${port}`)
})

这样 index.ts 只需要定义好 express 的插件和 GET/POST 请求对应转发的 handler 即可。Server要新增插件或者接口就在这里定义。比如新增一个 helmet

//…
app.use(helmet())
//…

一个简单项目的目录结构大致如下:

.
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── built
│   ├── Foundation
│   │   └──  log.js
│   ├── ReqeustHandler
│   │   ├── BaseHandler.js
│   │   └── SendMessageHandler.js
├── src #同 built 
├── tsconfig.json
└── yarn.lock

三、发挥类型检查优势

js 本身没有类型检查,如果参数传递过程中变量被修改或者内部类型检查不够全面就很容易崩。ts 利用和强类型语言一样的声明,允许编译器在编译时进行类型检查。比如下面这段代码:

function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";
console.log(greeter(user));

js 对于 person 变量没有任何要求,传 string/number/object 都可以,但是在这个场景下,我们其实期望 person 是一个 string。所以常规 js 的写法需要判断一下参数类型

if (typeof(person) == "string") {
    // blablabla
}

使用 ts 我们可以在参数后面跟上参数类型声明:

function greeter(person: string) {
    return "Hello, " + person;
}

let user = "Jane User";
console.log(greeter(user));

console.log(greeter(1)); // Argument of type '1' is not assignable to parameter of type 'string'.

这样如果上层传入的参数不是就会获得编译错误。

除了参数类型,还有变量类型声明,函数返回值声明,以及接口 interface 声明等静态语言才有的特性,是不是跟 Swift 有种相似之处?

对于开发这种 API 式小工具的场景,一般我会写一个基类 BaseHandler,定义一个返回函数:

export default class BaseHandler {
    response(req: express.Request, res: express.Response, next: express.NextFunction) {}
}

然后每个 API 写一个子类,比如 SendMessageHander:

export default class SendMessageHander extends BaseHandler {
    async response(req: express.Request, res: express.Response, next: express.NextFunction) {
        res.json(new BaseResponse(RetCode.success))
    }
}

其中 BaseReponse 也是一个基类,里面的返回码 RetCode 则定义为一个 enum:

export enum RetCode {
    success = 0,
    genericError = 1,
    invalidParameter = 2,
    serverInternalError = 3
}

这样,一个小型 API 工具的框架就快速搭建好了,剩下的就是往里面增加 API,塞各种逻辑就行了。比如我做了个查询当前值班人员的小工具,配合 tg 机器人,你给机器人发 duty 命令,它就会把当前值班的人发回给你。虽然代码很简单,但是不如这个服务未来加上了更多功能,代码肯定会越来越复杂,所以早点用上合理的架构是给未来节省时间。

四、小问题

ts 的编译器可以编成兼容各种环境的代码,节省了 Babel 的一步,结合 async/await 可以写出好看又安全的代码。但是毕竟 js 原生是不支持类型声明的,自己新写的代码还好都是可控的,但是第三方库就不一定了。有些库自带 ts 支持,但是比较少,大部分流行的库都可以通过 npm i @types/express 的方式装好声明,但依然有很多库不支持。

这种情况下你只能是手动写 .d.ts,或者直接写类型为 any,其实就是一夜回到解放前了。

另外涉及其他第三方 Server 端返回的 JSON API,如果有 Sample 我们可以放到 https://app.quicktype.io/ 自动转换一下,但是通常官方 Sample 或者单次请求返回的 JSON 内容都不全,我们不得不补上一句 key?: string | null。如果是自家 Server 这些都好说,但是对方 Server 不可控,有时候反解类型会崩,只能自己手动解,比较麻烦。

不过考虑到本文所述之场景只是“用 ts + espress 写写小工具”,那么这些缺点都还是可以接受的。如果项目大了有了性能和稳定性的要求的话那再说吧,毕竟大微软连 VS Code 这样的项目都能用 ts 解决了,已经证明了 ts 在大型项目的可用性。

总之,把之前用 js 和 coffee 匆匆写出的小工具换成 ts 之后,作者不由得大呼:真香!