现在我们不再使用ts-node, 改用官方的typescript编译器,官方编译器从 .ts文件生成和打包 JavaScript 文件

setting up the project

在一个空目录**npm init**初始化,安装typescript

  1. npm install typescript --save-dev

TypeScript的原生tsc 编译器可以帮助我们使用命令**tsc --init** 初始化我们的项目,生成我们的tsconfig.json 文件。
(全局安装了typescript才可以直接使用tsc —init, 即使全局安装也应该安装成开发依赖)
在可执行脚本列表scripts中添加tsc命令

  1. {
  2. // ..
  3. "scripts": {
  4. "tsc": "tsc",
  5. },
  6. // ..
  7. }

运行如下命令初始化tsconfig.json

  1. npm run tsc -- --init

我们需要的配置

  1. {
  2. "compilerOptions": {
  3. "target": "ES6",
  4. "outDir": "./build/",
  5. "module": "commonjs",
  6. "strict": true,
  7. "noUnusedLocals": true,
  8. "noUnusedParameters": true,
  9. "noImplicitReturns": true,
  10. "noFallthroughCasesInSwitch": true,
  11. "esModuleInterop": true
  12. }
  13. }

安装express和eslint

  1. npm install express
  2. npm install --save-dev eslint @types/express @typescript-eslint/eslint-plugin @typescript-eslint/parser

创建.eslintrc文件, 配置如下

  1. {
  2. "extends": [
  3. "eslint:recommended",
  4. "plugin:@typescript-eslint/recommended",
  5. "plugin:@typescript-eslint/recommended-requiring-type-checking"
  6. ],
  7. "plugins": ["@typescript-eslint"],
  8. "env": {
  9. "browser": true,
  10. "es6": true,
  11. "node": true
  12. },
  13. "rules": {
  14. "@typescript-eslint/semi": ["error"],
  15. "@typescript-eslint/explicit-function-return-type": "off",
  16. "@typescript-eslint/explicit-module-boundary-types": "off",
  17. "@typescript-eslint/restrict-template-expressions": "off",
  18. "@typescript-eslint/restrict-plus-operands": "off",
  19. "@typescript-eslint/no-unsafe-member-access": "off",
  20. "@typescript-eslint/no-unused-vars": [
  21. "error",
  22. { "argsIgnorePattern": "^_" }
  23. ],
  24. "no-case-declarations": "off"
  25. },
  26. "parser": "@typescript-eslint/parser",
  27. "parserOptions": {
  28. "project": "./tsconfig.json"
  29. }
  30. }

安装ts-node-dev自动重载

  1. npm install --save-dev ts-node-dev

在scripts中定义几个脚本

  1. {
  2. // ...
  3. "scripts": {
  4. "tsc": "tsc",
  5. "dev": "ts-node-dev index.ts",
  6. "lint": "eslint --ext .ts ."
  7. },
  8. // ...
  9. }

Let there be code

index.ts

  1. import express from 'express'
  2. const app = express()
  3. app.use(express.json())
  4. const PORT = 3000
  5. app.get('/ping', (_req, res) => {
  6. console.log('someone pinged here')
  7. res.send('pong')
  8. })
  9. app.listen(PORT, () => {
  10. console.log(`Server running on port ${PORT}`)
  11. })

运行命令 **npm run tsc** 将代码转译为js, 生成build目录
新建.eslintignore文件,让ESLint忽略build目录
在scripts中添加start命令

  1. {
  2. "scripts": {
  3. // ...
  4. "start": "node build/index.js"
  5. },
  6. }

运行命令 **npm run start**, 应用则在生产模式下运行

exercise 9.8 - 9.9

解决跨域问题
在后端项目中安装cors

  1. npm install cors

在后端代码中使用cors

  1. import express from 'express'
  2. import cors from 'cors'
  3. const app = express()
  4. app.use(express.json())
  5. // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  6. app.use(cors())

eslint报错,需安装@types/cors

  1. npm i --save-dev @types/cors

Implementing the functionality

项目目录结构
image.png
这次采用硬编码的数据,json文件放在data目录

  1. [
  2. {
  3. "id": 1,
  4. "date": "2017-01-01",
  5. "weather": "rainy",
  6. "visibility": "poor",
  7. "comment": "Pretty scary flight, I'm glad I'm alive"
  8. },
  9. {
  10. "id": 2,
  11. "date": "2017-04-01",
  12. "weather": "sunny",
  13. "visibility": "good",
  14. "comment": "Everything went better than expected, I'm learning much"
  15. },
  16. // ...
  17. ]

image.png
如果导入的是json文件,编辑器会报错,需要在tsconfig.json中添加如下配置

  1. {
  2. "compilerOptions": {
  3. // ...
  4. "resolveJsonModule": true
  5. }
  6. }

image.png
文件json有自己的值,如果赋值给指定了类型的变量时,会报错
可以使用类型断言(即as语法),给json数据指定类型

  1. const diaries: Array<DiaryEntry> = diaryData as Array<DiaryEntry>;

node会按如下顺序对文件进行解析

  1. ["js", "json", "node", "ts", "tsx"]

如果配置了**"resolveJsonModule": true**,
当有如下文档结构时

  1. ├── myModule.json
  2. └── myModule.ts

想通过下列方式引入ts文件不会成功,因为json的优先级是高于ts的

  1. import myModule from "./myModule";

所以应避免文件名重复

因为json不支持类型,改用ts文件

  1. import { DiaryEntry } from '../src/types'
  2. const DiaryEntries: Array<DiaryEntry> = [
  3. {
  4. id: 1,
  5. date: '2017-01-01',
  6. weather: 'rainy',
  7. visibility: 'poor',
  8. comment: "Pretty scary flight, I'm glad I'm alive",
  9. },
  10. // ...
  11. ]
  12. export default DiaryEntries

将路由从index.ts中分离到src/routes目录中,

  1. import express from 'express'
  2. import diaryService from '../services/disaryService'
  3. const router = express.Router()
  4. router.get('/', (_req, res) => {
  5. res.send(diaryService.getNonSensitiveEntries())
  6. })
  7. router.post('/', (_req, res) => {
  8. res.send('Saving a diary!')
  9. })
  10. export default router

将业务逻辑分离到src/services目录中

  1. import diaries from '../../data/diaries'
  2. import { NonSensitiveDiaryEntry, DiaryEntry } from '../types'
  3. const getEntries = (): Array<DiaryEntry> => {
  4. return diaries
  5. }
  6. const getNonSensitiveEntries = (): NonSensitiveDiaryEntry[] => {
  7. return diaries.map(({ id, date, weather, visibility }) => ({
  8. id,
  9. date,
  10. weather,
  11. visibility,
  12. }))
  13. }
  14. const addDiary = () => {
  15. return []
  16. }
  17. export default {
  18. getEntries,
  19. addDiary,
  20. getNonSensitiveEntries,
  21. }

在index.ts中使用路由

  1. import express from 'express'
  2. import diaryRouter from './routes/diaries'
  3. const app = express()
  4. app.use(express.json())
  5. const PORT = 3000
  6. app.get('/ping', (_req, res) => {
  7. console.log('someone pinged here')
  8. res.send('pong')
  9. })
  10. app.use('/api/diaries', diaryRouter)
  11. app.listen(PORT, () => {
  12. console.log(`Server running on port ${PORT}`)
  13. })

类型声明文件为根目录的types.ts文件

  1. export type Weather = 'sunny' | 'rainy' | 'cloudy' | 'windy' | 'stormy'
  2. export type Visibility = 'great' | 'good' | 'ok' | 'poor'
  3. export interface DiaryEntry {
  4. id: number
  5. date: string
  6. weather: Weather
  7. visibility: Visibility
  8. comment?: string
  9. }
  10. export type NonSensitiveDiaryEntry = Omit<DiaryEntry, 'comment'>

| 分隔的为联合类型
用 ?定义可选属性, 如 comment?: string
如果从联合类型中选取一部分构成新的类型,可以使用Pick工具类型

  1. const getNonSensitiveEntries =
  2. (): Array<Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>> => {
  3. // ...
  4. }

使用嵌套的语法可能有点奇怪,可以使用alternative数组类型

  1. const getNonSensitiveEntries =
  2. (): Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>[] => {
  3. // ...
  4. }

如果只想排除某一个字段,可以使用Omit工具类型

  1. const getNonSensitiveEntries = (): Omit<DiaryEntry, 'comment'>[] => {
  2. // ...
  3. }

Preventing an accidental undefined result

将后端扩展为路由 api/diaries/:id来支持获取一个特定条目
service

  1. const findById = (id: number): DiaryEntry | undefined => {
  2. const entry = diaries.find((d) => d.id === id)
  3. return entry
  4. }
  5. export default {
  6. getEntries,
  7. addDiary,
  8. getNonSensitiveEntries,
  9. findById,
  10. }

有可能获取不到记录,所以要加undefined
image.png
router

  1. import express from 'express';
  2. import diaryService from '../services/diaryService'
  3. router.get('/:id', (req, res) => {
  4. const diary = diaryService.findById(Number(req.params.id));
  5. if (diary) {
  6. res.send(diary);
  7. } else {
  8. res.sendStatus(404);
  9. }
  10. })
  11. // ...
  12. export default router;

Adding a new diary

通过post添加新条目
在types中声明NewDiaryEntry类型

  1. export type NewDiaryEntry = Omit<DiaryEntry, 'id'>

router

  1. /* eslint-disable @typescript-eslint/no-unsafe-assignment */
  2. // ...
  3. router.post('/', (req, res) => {
  4. const { date, weather, visibility, comment } = req.body
  5. const newDiaryEntry = diaryService.addDiary({
  6. date,
  7. weather,
  8. visibility,
  9. comment,
  10. })
  11. res.json(newDiaryEntry)
  12. })

service

  1. const addDiary = (entry: NewDiaryEntry): DiaryEntry => {
  2. const newDiaryEntry = {
  3. id: Math.max(...diaries.map((d) => d.id)) + 1,
  4. ...entry,
  5. }
  6. diaries.push(newDiaryEntry)
  7. return newDiaryEntry
  8. }

为了解析传入的数据,我们必须配置json 中间件:

  1. import express from 'express';
  2. import diaryRouter from './routes/diaries';
  3. const app = express();
  4. app.use(express.json());
  5. const PORT = 3000;
  6. app.use('/api/diaries', diaryRouter);
  7. app.listen(PORT, () => {
  8. console.log(`Server running on port ${PORT}`);
  9. });

使用vscode插件Rest Client测试, 在request目录新建一个.rest后缀的文件
image.png
使用postman测试
image.png

Proofing requests

校对请求
来自外部的数据不能完全信任,需要进行类型判断
在src目录新建utils.ts文件,外部数据设为unknown类型

  1. import { NewDiaryEntry } from './types';
  2. const toNewDiaryEntry = (object: unknown): NewDiaryEntry => {
  3. const newEntry: NewDiaryEntry = {
  4. // ...
  5. }
  6. return newEntry;
  7. }
  8. export default toNewDiaryEntry;

在route中使用这个函数

  1. import toNewDiaryEntry from '../utils';
  2. // ...
  3. router.post('/', (req, res) => {
  4. try {
  5. const newDiaryEntry = toNewDiaryEntry(req.body);
  6. const addedEntry = diaryService.addDiary(newDiaryEntry);
  7. res.json(addedEntry);
  8. } catch (e) {
  9. res.status(400).send(e.message);
  10. }
  11. })

需要对unknown类型的数据进行逐一校验
为每个字段创造解析器
comment字段

  1. const parseComment = (comment: unknown): string => {
  2. if (!comment || !isString(comment)) {
  3. throw new Error('Incorrect or missing comment')
  4. }
  5. return comment
  6. }

isString函数是类型守卫(type guard), 它的返回类型text is string是类型谓词(type predicate), 格式是_**parameterName is Type**_,如果函数返回true, 则可以推断参数是类型谓词中的类型

  1. const isString = (text: unknown): text is string => {
  2. return typeof text === 'string' || text instanceof String
  3. }

可以看到,传入参数时comment类型为unknown, 返回时ts推断它的类型为string
image.png
为什么string的判断要用2种?typeof text === 'string' || text instanceof String
因为可以通过2种方式声明string

  1. const a = "I'm a string primitive";
  2. const b = new String("I'm a String Object");
  3. typeof a; --> returns 'string'
  4. typeof b; --> returns 'object'
  5. a instanceof String; --> returns false
  6. b instanceof String; --> returns true

校验日期

  1. const isDate = (date: string): boolean => {
  2. return Boolean(Date.parse(date))
  3. }
  4. const parseDate = (date: unknown): string => {
  5. if (!date || !isString(date) || !isDate(date)) {
  6. throw new Error('Incorrect or missing date: ' + date)
  7. }
  8. return date
  9. }

校验weather

  1. const parseWeather = (weather: unknown): Weather => {
  2. if (!weather || !isString(weather) || !isWeather(weather)) {
  3. throw new Error('Incorrect or missing weather: ' + weather)
  4. }
  5. return weather
  6. }
  7. const isWeather = (str: string): str is Weather => {
  8. return ['sunny', 'rainy', 'cloudy', 'stormy'].includes(str)
  9. }

在isWeather函数中,如果Weather类型的定义改变了,而这里没有一起更改,则会出现问题
此时,应该使用枚举类型(TypeScript enum)
枚举类型允许在程序运行时获取它的值
将Weather修改为枚举类型

  1. export enum Weather {
  2. Sunny = 'sunny',
  3. Rainy = 'rainy',
  4. Cloudy = 'cloudy',
  5. Stormy = 'stormy',
  6. Windy = 'windy',
  7. }

修改isWeather函数

  1. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  2. const isWeather = (param: any): param is Weather => {
  3. return Object.values(Weather).includes(param);
  4. };

Object.values返回一个数组
这里param要用any类型,string不是枚举类型
当有一组预先确定的数值预期在将来不会发生变化时,通常使用枚举。
此时data中会报错, 因为字符串不是枚举类型
image.png
使用toNewDiaryEntry函数修复这个问题

  1. import { DiaryEntry } from "../src/types";
  2. import toNewDiaryEntry from "../src/utils";
  3. const data = [
  4. {
  5. "id": 1,
  6. "date": "2017-01-01",
  7. "weather": "rainy",
  8. "visibility": "poor",
  9. "comment": "Pretty scary flight, I'm glad I'm alive"
  10. },
  11. // ...
  12. ]
  13. const diaryEntries: DiaryEntry [] = data.map(obj => {
  14. const object = toNewDiaryEntry(obj) as DiaryEntry;
  15. object.id = obj.id;
  16. return object;
  17. });
  18. export default diaryEntries;

最后再来重新定义toNewDiaryEntry函数

  1. const toNewDiaryEntry = (object: unknown): NewDiaryEntry => {
  2. const newEntry: NewDiaryEntry = {
  3. comment: parseComment(object.comment),
  4. date: parseDate(object.date),
  5. weather: parseWeather(object.weather),
  6. visibility: parseVisibility(object.visibility)
  7. };
  8. return newEntry;
  9. };

unknown类型是不允许任何操作的,无法访问object的属性,所以上面的代码无效
可以通过解构变量修复这个问题

  1. type Fields = { comment : unknown, date: unknown, weather: unknown, visibility: unknown };
  2. const toNewDiaryEntry = ({ comment, date, weather, visibility } : Fields): NewDiaryEntry => {
  3. const newEntry: NewDiaryEntry = {
  4. comment: parseComment(comment),
  5. date: parseDate(date),
  6. weather: parseWeather(weather),
  7. visibility: parseVisibility(visibility)
  8. };
  9. return newEntry;
  10. };

或者使用any类型

  1. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  2. const toNewDiaryEntry = (object: any): NewDiaryEntry => {
  3. const newEntry: NewDiaryEntry = {
  4. comment: parseComment(object.comment),
  5. date: parseDate(object.date),
  6. weather: parseWeather(object.weather),
  7. visibility: parseVisibility(object.visibility)
  8. };
  9. return newEntry;
  10. };

此时再做request测试,就能对不合格数据进行校验了
image.png

exercise 9.12 - 9.13

使用uuid
安装

  1. npm install uuid
  2. npm i --save-dev @types/uuid

使用

  1. import {v1 as uuid} from 'uuid'
  2. const id = uuid()