利用typescript + express 开发一个nodejs服务端demo

3,530 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情

如何理解express框架文章中,我们介绍了express的基本使用,本片文章将利用typescript和express开发一个服务端的demo。

项目目录

package.json

相关配置参考ts学习心得: nodejs项目配置package.json

"scripts": {
  "dev:build": "tsc -w",
  "dev:start": "nodemon ./build/index.js",
  "dev": "tsc && concurrently npm:dev:*",
  "mock": "json-server --watch ./data/course.json  --port 3004"
}
  • 利用 tsc -w 把ts文件编译成js文件,使其能在nodejs中运行(ts是不能运行的)
  • 利用 nodemon 来监控并执行./build/index.js文件
  • 利用 concurrently 同时启动这两个命令
  • 利用 && 先后执行命令
  • 利用json-server来启动一个静态资源服务器,读取public/index.html文件

src/index.ts

import express from 'express'

const app = express()

app.get('/', (req, res) => {
    res.end('hello world')
})

app.listen('7001', () => {
  console.log('listen at 7001')
})

利用ts+express开发遇到的问题

问题一

express 库的类型定义文件 .d.ts 文件类型描述不准确。

router.post('/getData', (req: Request, res: Response) => {
  const { password, username } = req.body
  if (req.body.password === '123') {}
}

我们可以看到password是 any 类型,这是因为 @types/express对body设置的是any类型

export interface Request< ... ReqBody = any ... > {
   body: ReqBody;
}

这就不符合我们的预期了,它应该是一个字符串类型,或者是undefined(前端有可能没有传值),所以需要对类型文件进行修改,但是我们又不能在@types/express上做修改,怎么办呢?

在route.ts代码中加上RequestWithBody接口,让这个接口继承Request,并添加body属性,body里面是一个可索引的类型。这样password就是字符串或者undefined类型了。

interface RequestWithBody extends Request {
  body: {
      [key: string]: string | undefined
  }
}

router.post('/getData', (req: RequestWithBody, res: Response) => {
  const { password, username } = req.body
  if (req.body.password === '123') {}
}

问题二

当我们使用中间件修改req,但是req的类型文件没有跟着变化。

app.use((req: Request, res: Response, next: NextFunction) => {
  req.teacherName = 'dell'
  next()
})

我们可以定义一个custom.d.ts文件,在Request的接口中添加我们加入的属性,这样在使用的时候就会出现提示。custom.d.ts文件会和@types/express里面的类型文件进行合并。

declare namespace Express {
  interface Request {
    teacherName: string
  }
}

image.png

登录登出接口

解析前端传过来的数据

在index.ts加入解析请求头的中间件,我们可以安装body-parser,也可以始终express自带的函数来解析。

const app = express()

app.use(express.json())
app.use(express.urlencoded({ extended: false }))

给请求头加上额外的类型

interface RequestWithBody extends Request {
  body: {
    [key: string]: string | undefined
  }
}

封装返回的数据格式

interface Result {
  success: boolean
  errMsg?: string
  data: any
}

export const getResponseResult = (data: any, errMsg?: string): Result => {
  if (errMsg) {
    return {
      success: false,
      errMsg,
      data
    }
  }
  return {
    success: true,
    data
  }
}

cookie-session 用户的session有两种存储方式,一种是存放在客户端的cookie里面,另一种是存放在服务端的数据库里面(redis)。cookie-session采用的是第一种方式,即把用户信息加密后通过set-cookie存放在客户端的cookie里面,这种方法的优势是没必要在服务端搞个数据库,非常麻烦。

import cookieSession from 'cookie-session'
app.use(
  cookieSession({
    name: 'session',
    keys: ['xiaopen'],
    // Cookie Options
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  })
)

新建router.ts

import { Router, Request, Response, NextFunction } from 'express'
const router = Router()

router.get('/logout', (req, res, next) => {
  if (req.session) {
    req.session.isLogin = undefined
  }
  res.json(getResponseResult(true))
})

router.post(
  '/login',
  (req: RequestWithBody, res: Response, next: NextFunction) => {
    const { password } = req.body
    const isLogin = req.session?.isLogin
    if (isLogin) {
      res.end('already login')
    } else {
      if (password === '123' && req.session) {
        req.session.isLogin = true
        req.session.username = 'pengchangjun'
        req.session.userId = '1234567890'
        res.json(getResponseResult(true))
      } else {
        res.json(getResponseResult(null, 'login error'))
      }
    }
  }
)
export default router

当登录后,我们会在req.session写入一些用户信息(isLogin, username, userId),同时在响应头加入set-cookie把这些信息写入到浏览器cookie中。

当用户下次访问的时候会带上cookie,然后cookie-session就会解密cookie,把解密后的信息挂载到req.session,这样我就可以通过req.session来判断这个用户是否登录及用户信息了。

权限验证中间件

大部分的接口都需要验证用户是否登录:

router.get('/showData', (req, res) => {
  const isLogin = req.session?.isLogin
  if (isLogin) {
    const path1 = path.resolve(__dirname, '../data/course.json')
    const content = fs.readFileSync(path1, 'utf-8')
    res.json(JSON.parse(content))
  } else {
    res.end(`please login`)
  }
})

但是这样写非常麻烦,我们可以把权限验证的功能写成一个中间件:

const checkLogin = (req: Request, res: Response, next: NextFunction) => {
  const isLogin = req.session?.isLogin
  if (isLogin) {
    next()
  } else {
    res.json(getResponseResult(null, 'please login'))
  }
}

router.get('/showData', checkLogin, (req, res) => {
  const path1 = path.resolve(__dirname, '../data/course.json')
  const content = fs.readFileSync(path1, 'utf-8')
  res.json(JSON.parse(content))
})

总结

综合上面的代码,我们发现ts仅仅只是提供了类型检查的功能,感觉用处也不是很大,这是因为上面的代码没有把ts最强的优势运用进来。上面写的代码是一个过程化的代码,即面向过程编程,要想最大限度的利用ts,就必须要把面向对象类的概念应用进来。下一篇文章让我们见识下typescript的真实威力。