现在我们不再使用ts-node, 改用官方的typescript编译器,官方编译器从 .ts文件生成和打包 JavaScript 文件
setting up the project
在一个空目录**npm init**
初始化,安装typescript
npm install typescript --save-dev
TypeScript的原生tsc 编译器可以帮助我们使用命令**tsc --init**
初始化我们的项目,生成我们的tsconfig.json 文件。
(全局安装了typescript才可以直接使用tsc —init, 即使全局安装也应该安装成开发依赖)
在可执行脚本列表scripts中添加tsc命令
{
// ..
"scripts": {
"tsc": "tsc",
},
// ..
}
运行如下命令初始化tsconfig.json
npm run tsc -- --init
我们需要的配置
{
"compilerOptions": {
"target": "ES6",
"outDir": "./build/",
"module": "commonjs",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
}
}
安装express和eslint
npm install express
npm install --save-dev eslint @types/express @typescript-eslint/eslint-plugin @typescript-eslint/parser
创建.eslintrc文件, 配置如下
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"plugins": ["@typescript-eslint"],
"env": {
"browser": true,
"es6": true,
"node": true
},
"rules": {
"@typescript-eslint/semi": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
],
"no-case-declarations": "off"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
}
}
安装ts-node-dev自动重载
npm install --save-dev ts-node-dev
在scripts中定义几个脚本
{
// ...
"scripts": {
"tsc": "tsc",
"dev": "ts-node-dev index.ts",
"lint": "eslint --ext .ts ."
},
// ...
}
Let there be code
index.ts
import express from 'express'
const app = express()
app.use(express.json())
const PORT = 3000
app.get('/ping', (_req, res) => {
console.log('someone pinged here')
res.send('pong')
})
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
运行命令 **npm run tsc**
将代码转译为js, 生成build目录
新建.eslintignore文件,让ESLint忽略build目录
在scripts中添加start命令
{
"scripts": {
// ...
"start": "node build/index.js"
},
}
运行命令 **npm run start**
, 应用则在生产模式下运行
exercise 9.8 - 9.9
解决跨域问题
在后端项目中安装cors
npm install cors
在后端代码中使用cors
import express from 'express'
import cors from 'cors'
const app = express()
app.use(express.json())
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
app.use(cors())
eslint报错,需安装@types/cors
npm i --save-dev @types/cors
Implementing the functionality
项目目录结构
这次采用硬编码的数据,json文件放在data目录
[
{
"id": 1,
"date": "2017-01-01",
"weather": "rainy",
"visibility": "poor",
"comment": "Pretty scary flight, I'm glad I'm alive"
},
{
"id": 2,
"date": "2017-04-01",
"weather": "sunny",
"visibility": "good",
"comment": "Everything went better than expected, I'm learning much"
},
// ...
]
如果导入的是json文件,编辑器会报错,需要在tsconfig.json中添加如下配置
{
"compilerOptions": {
// ...
"resolveJsonModule": true
}
}
文件json有自己的值,如果赋值给指定了类型的变量时,会报错
可以使用类型断言(即as语法),给json数据指定类型
const diaries: Array<DiaryEntry> = diaryData as Array<DiaryEntry>;
node会按如下顺序对文件进行解析
["js", "json", "node", "ts", "tsx"]
如果配置了**"resolveJsonModule": true**
,
当有如下文档结构时
├── myModule.json
└── myModule.ts
想通过下列方式引入ts文件不会成功,因为json的优先级是高于ts的
import myModule from "./myModule";
所以应避免文件名重复
因为json不支持类型,改用ts文件
import { DiaryEntry } from '../src/types'
const DiaryEntries: Array<DiaryEntry> = [
{
id: 1,
date: '2017-01-01',
weather: 'rainy',
visibility: 'poor',
comment: "Pretty scary flight, I'm glad I'm alive",
},
// ...
]
export default DiaryEntries
将路由从index.ts中分离到src/routes目录中,
import express from 'express'
import diaryService from '../services/disaryService'
const router = express.Router()
router.get('/', (_req, res) => {
res.send(diaryService.getNonSensitiveEntries())
})
router.post('/', (_req, res) => {
res.send('Saving a diary!')
})
export default router
将业务逻辑分离到src/services目录中
import diaries from '../../data/diaries'
import { NonSensitiveDiaryEntry, DiaryEntry } from '../types'
const getEntries = (): Array<DiaryEntry> => {
return diaries
}
const getNonSensitiveEntries = (): NonSensitiveDiaryEntry[] => {
return diaries.map(({ id, date, weather, visibility }) => ({
id,
date,
weather,
visibility,
}))
}
const addDiary = () => {
return []
}
export default {
getEntries,
addDiary,
getNonSensitiveEntries,
}
在index.ts中使用路由
import express from 'express'
import diaryRouter from './routes/diaries'
const app = express()
app.use(express.json())
const PORT = 3000
app.get('/ping', (_req, res) => {
console.log('someone pinged here')
res.send('pong')
})
app.use('/api/diaries', diaryRouter)
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
类型声明文件为根目录的types.ts文件
export type Weather = 'sunny' | 'rainy' | 'cloudy' | 'windy' | 'stormy'
export type Visibility = 'great' | 'good' | 'ok' | 'poor'
export interface DiaryEntry {
id: number
date: string
weather: Weather
visibility: Visibility
comment?: string
}
export type NonSensitiveDiaryEntry = Omit<DiaryEntry, 'comment'>
| 分隔的为联合类型
用 ?定义可选属性, 如 comment?: string
如果从联合类型中选取一部分构成新的类型,可以使用Pick工具类型
const getNonSensitiveEntries =
(): Array<Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>> => {
// ...
}
使用嵌套的语法可能有点奇怪,可以使用alternative数组类型
const getNonSensitiveEntries =
(): Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>[] => {
// ...
}
如果只想排除某一个字段,可以使用Omit工具类型
const getNonSensitiveEntries = (): Omit<DiaryEntry, 'comment'>[] => {
// ...
}
Preventing an accidental undefined result
将后端扩展为路由 api/diaries/:id来支持获取一个特定条目
service
const findById = (id: number): DiaryEntry | undefined => {
const entry = diaries.find((d) => d.id === id)
return entry
}
export default {
getEntries,
addDiary,
getNonSensitiveEntries,
findById,
}
有可能获取不到记录,所以要加undefined
router
import express from 'express';
import diaryService from '../services/diaryService'
router.get('/:id', (req, res) => {
const diary = diaryService.findById(Number(req.params.id));
if (diary) {
res.send(diary);
} else {
res.sendStatus(404);
}
})
// ...
export default router;
Adding a new diary
通过post添加新条目
在types中声明NewDiaryEntry类型
export type NewDiaryEntry = Omit<DiaryEntry, 'id'>
router
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// ...
router.post('/', (req, res) => {
const { date, weather, visibility, comment } = req.body
const newDiaryEntry = diaryService.addDiary({
date,
weather,
visibility,
comment,
})
res.json(newDiaryEntry)
})
service
const addDiary = (entry: NewDiaryEntry): DiaryEntry => {
const newDiaryEntry = {
id: Math.max(...diaries.map((d) => d.id)) + 1,
...entry,
}
diaries.push(newDiaryEntry)
return newDiaryEntry
}
为了解析传入的数据,我们必须配置json 中间件:
import express from 'express';
import diaryRouter from './routes/diaries';
const app = express();
app.use(express.json());
const PORT = 3000;
app.use('/api/diaries', diaryRouter);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
使用vscode插件Rest Client测试, 在request目录新建一个.rest后缀的文件
使用postman测试
Proofing requests
校对请求
来自外部的数据不能完全信任,需要进行类型判断
在src目录新建utils.ts文件,外部数据设为unknown类型
import { NewDiaryEntry } from './types';
const toNewDiaryEntry = (object: unknown): NewDiaryEntry => {
const newEntry: NewDiaryEntry = {
// ...
}
return newEntry;
}
export default toNewDiaryEntry;
在route中使用这个函数
import toNewDiaryEntry from '../utils';
// ...
router.post('/', (req, res) => {
try {
const newDiaryEntry = toNewDiaryEntry(req.body);
const addedEntry = diaryService.addDiary(newDiaryEntry);
res.json(addedEntry);
} catch (e) {
res.status(400).send(e.message);
}
})
需要对unknown类型的数据进行逐一校验
为每个字段创造解析器
comment字段
const parseComment = (comment: unknown): string => {
if (!comment || !isString(comment)) {
throw new Error('Incorrect or missing comment')
}
return comment
}
isString函数是类型守卫(type guard), 它的返回类型text is string是类型谓词(type predicate), 格式是_**parameterName is Type**_
,如果函数返回true, 则可以推断参数是类型谓词中的类型
const isString = (text: unknown): text is string => {
return typeof text === 'string' || text instanceof String
}
可以看到,传入参数时comment类型为unknown, 返回时ts推断它的类型为string
为什么string的判断要用2种?typeof text === 'string' || text instanceof String
因为可以通过2种方式声明string
const a = "I'm a string primitive";
const b = new String("I'm a String Object");
typeof a; --> returns 'string'
typeof b; --> returns 'object'
a instanceof String; --> returns false
b instanceof String; --> returns true
校验日期
const isDate = (date: string): boolean => {
return Boolean(Date.parse(date))
}
const parseDate = (date: unknown): string => {
if (!date || !isString(date) || !isDate(date)) {
throw new Error('Incorrect or missing date: ' + date)
}
return date
}
校验weather
const parseWeather = (weather: unknown): Weather => {
if (!weather || !isString(weather) || !isWeather(weather)) {
throw new Error('Incorrect or missing weather: ' + weather)
}
return weather
}
const isWeather = (str: string): str is Weather => {
return ['sunny', 'rainy', 'cloudy', 'stormy'].includes(str)
}
在isWeather函数中,如果Weather类型的定义改变了,而这里没有一起更改,则会出现问题
此时,应该使用枚举类型(TypeScript enum)
枚举类型允许在程序运行时获取它的值
将Weather修改为枚举类型
export enum Weather {
Sunny = 'sunny',
Rainy = 'rainy',
Cloudy = 'cloudy',
Stormy = 'stormy',
Windy = 'windy',
}
修改isWeather函数
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isWeather = (param: any): param is Weather => {
return Object.values(Weather).includes(param);
};
Object.values返回一个数组
这里param要用any类型,string不是枚举类型
当有一组预先确定的数值预期在将来不会发生变化时,通常使用枚举。
此时data中会报错, 因为字符串不是枚举类型
使用toNewDiaryEntry函数修复这个问题
import { DiaryEntry } from "../src/types";
import toNewDiaryEntry from "../src/utils";
const data = [
{
"id": 1,
"date": "2017-01-01",
"weather": "rainy",
"visibility": "poor",
"comment": "Pretty scary flight, I'm glad I'm alive"
},
// ...
]
const diaryEntries: DiaryEntry [] = data.map(obj => {
const object = toNewDiaryEntry(obj) as DiaryEntry;
object.id = obj.id;
return object;
});
export default diaryEntries;
最后再来重新定义toNewDiaryEntry函数
const toNewDiaryEntry = (object: unknown): NewDiaryEntry => {
const newEntry: NewDiaryEntry = {
comment: parseComment(object.comment),
date: parseDate(object.date),
weather: parseWeather(object.weather),
visibility: parseVisibility(object.visibility)
};
return newEntry;
};
unknown类型是不允许任何操作的,无法访问object的属性,所以上面的代码无效
可以通过解构变量修复这个问题
type Fields = { comment : unknown, date: unknown, weather: unknown, visibility: unknown };
const toNewDiaryEntry = ({ comment, date, weather, visibility } : Fields): NewDiaryEntry => {
const newEntry: NewDiaryEntry = {
comment: parseComment(comment),
date: parseDate(date),
weather: parseWeather(weather),
visibility: parseVisibility(visibility)
};
return newEntry;
};
或者使用any类型
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const toNewDiaryEntry = (object: any): NewDiaryEntry => {
const newEntry: NewDiaryEntry = {
comment: parseComment(object.comment),
date: parseDate(object.date),
weather: parseWeather(object.weather),
visibility: parseVisibility(object.visibility)
};
return newEntry;
};
exercise 9.12 - 9.13
使用uuid
安装
npm install uuid
npm i --save-dev @types/uuid
使用
import {v1 as uuid} from 'uuid'
const id = uuid()