本文将会带领大家一步一步搭建自己的typescript全栈脚手架。
项目基本结构为
├── client // 纯前端代码
├── server // nodejs代码
├── .gitignore // git忽略文件名单
├── .prettierrc // 格式化插件prettier配置文件
├── README.md // 项目介绍
搭建client层typescript开发环境
近几年,前端市场百花齐放,各类脚手架层出不穷,比较有代表性的有facebook的 create-react-app、vue家族的 vue-cli, 还有国内厂家自研的 umi 等, 本文选择了基于react+antd+dva
的脚手架 antd-admin
1. 初始化项目
进入项目根目录, 执行
git clone https://github.com/zuiidea/antd-admin.git client
如果下载很慢, 也可以选择直接下载压缩文件, 然后解压到client
文件夹下面
2. 安装依赖
推荐使用yarn, yarn与npm功能类似, 但是更快。
cd client
yarn
3. 启动服务
yarn start
用浏览器打开http://localhost:7000
将会看到以下界面
client层脚手架搭建完成, 接下来完成server层脚手架
搭建server层typescript开发环境
1. 初始化项目
进入server
文件夹, 执行
cd ../server
yarn init
将会生成package.json
2. 安装依赖
yarn add typescript ts-node-dev express
yarn add -D @types/express
一般情况下,在nodejs环境下, 如果想运行一个ts文件, 首先要将ts文件编译成js
# 监听文件变化并编译ts文件
tsc -w --locale zh-CN
然后运行编译后的文件
nodemon dist/app.js
现在, 使用ts-node
, 可以直接运行ts文件, 不用再生成中间文件
ts-node src/app.ts
如果想在开发环境, 自动监听文件变化, 并重启服务, 可以使用ts-node-dev
ts-node-dev --respawn src/app.ts
3. 创建typescript配置文件
在根目录下新建tsconfig.json
文件, typescript
编译器会读取这个文件。内容如下
{
"compilerOptions": {
"checkJs": true, // 检查数据类型
"target": "ES2015", // Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
"module": "commonjs", // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
"lib": [
"es2015"
], // Specify library files to be included in the compilation.
"allowJs": true, // Allow javascript files to be compiled.
"jsx": "react", // Specify JSX code generation: 'preserve', 'react-native', or 'react'.
"resolveJsonModule": true,
"sourceMap": true, // Generates corresponding '.map' file.
"outDir": "dist", // Redirect output structure to the directory.
"strict": true, // Enable all strict type-checking options.
"noImplicitAny": true, // Raise error on expressions and declarations with an implied 'any' type.
"strictNullChecks": true, // Enable strict null checks.
"strictFunctionTypes": true, // Enable strict checking of function types.
"strictPropertyInitialization": true, // Enable strict checking of property initialization in classes.
"moduleResolution": "node", // Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).
"baseUrl": "./src", // Base directory to resolve non-absolute module names.
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"paths": {
"*": [
"node_modules//",
"./typings//"
],
"~/src/*": [
"*"
]
},
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
"esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, // Enables experimental support for ES7 decorators.
"emitDecoratorMetadata": true // Enables experimental support for emitting type metadata for decorators.
},
"include": [
"./src"
],
"exclude": [
"node_modules",
"./src/public"
]
}
tsconfig.json配置可参考TypeScript中文手册
4. 创建入口文件
入口文件路径为src/app.ts
。 接下来, 让我们完成一个简单的例子 Hello world example
import express from 'express'
const app = express()
const port = 3000
app.get('/', (req, res) => res.send("Hello World!"))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
5. 启动服务
在package.json
的scripts
中添加启动命令
{
"start": "ts-node-dev --respawn src/app.ts"
}
在命令行输入
yarn start
用浏览器打开http://localhost:3000/
, 可以看到Hello World!
6. 渲染html
选择 ejs 作为渲染引擎
yarn add ejs
yarn add -D @types/ejs
修改app.ts
import express from "express"
import path from "path"
const app = express()
const port = 3000
// 设置存放模板引擎目录
app.set("views", path.join(__dirname, "./public"))
// 设置模板引擎为ejs
app.set("view engine", "ejs")
app.get("/", (req, res) =>
// 渲染模版/public/index.ejs
res.render("index", { content: "Hello Typescript!" })
)
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
创建src/public
文件夹, 在该文件夹下创建一个index.ejs
文件, 内容如下
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<meta name="referrer" content="origin">
<title>typescript + node </title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<%=content%>
</div>
</body>
</html>
在浏览器刷新页面, 可以看到页面内容为Hello Typescript!
7. 路由最佳实践
路由最佳实践参考 routing-controllers
安装依赖
yarn add routing-controllers reflect-metadata body-parser cookie-parser multer lodash moment module-alias cors class-transformer class-validator
yarn add -D @types/body-parser @types/multer @types/cookie-parser @types/lodash @types/cors
修改tsconfig.json
的compilerOptions
配置
{
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
修改文件目录结构为
src
├── config // 配置文件
│ ├── app.ts
│ └── env.ts
├── controller // 控制器
│ ├── api
│ │ └── user.ts
│ ├── base.ts
│ └── index.ts
├── library
│ └── utils
│ └── network.ts
├── middleware // 中间件
├── model // 模块
│ ├── user.ts
│ └── validator.ts
├── public // 静态资源
│ └── index.ejs
└── app.ts // 项目入口
src/config/**
变量配置
src/config/env.ts
环境定义文件
let env: "dev" | "test" | "prod" = "test"
switch (process.env.NODE_ENV) {
case "dev":
case "test":
case "prod":
env = process.env.NODE_ENV
break
default:
env = "test"
}
export default env
src/config/app.ts
项目配置文件
import env from "~/src/config/env"
const dev = {
// 服务启动端口
port: 7001,
}
const test = {
port: 9575,
}
const prod = {
port: 6666,
}
const config = {
dev,
test,
prod,
}
export default config[env]
src/controller/**
控制器
src/controller/base.ts
控制器基类, 封装公共方法, 供其子类使用
/**
* 项目接口格式约定
* {
* "status": 0,
* "message": "success",
* "data": {}
* }
*
* 1. status 请求成功状态码
* 1. 0 => 响应正常
* 2. 1 => 响应异常
* 3. 408 => 未登录
* 4. 403 => 无权限
* 5. 404 => 找不到页面
* 6. 500 => 服务器错误
*
* 2. message 请求状态描述
* 1. 为空 => 略过该逻辑
* 2. 存在内容 => 前端打印响应内容
*
* 3. data 请求内容
*/
// 目前的路由最佳实践
// 文档地址 => https://github.com/typestack/routing-controllers
class BaseController {
protected showResult(data = {}, message = "操作成功", status = 0) {
return {
data,
message,
status,
}
}
protected showError(message = "操作失败", data = {}, status = 1) {
return this.showResult(data, message, status)
}
}
export default BaseController
src/controller/index.ts
控制器入口文件
约定所有的控制器, 通过这个文件导出
const ControllerList: Function[] = []
export default ControllerList
src/library/**
工具类
src/library/utils/network.ts
封装了网络相关的方法
import os from "os"
class Tool {
/**
* 获取本机ip via https://stackoverflow.com/a/8440736
*/
static getLocalIpList() {
let networkInterfaceList = os.networkInterfaces()
let localIpList = ["127.0.0.1"]
for (let networkInterface of Object.keys(networkInterfaceList)) {
for (let interfaceInfo of networkInterfaceList[
networkInterface
] as os.NetworkInterfaceInfo[]) {
if (
interfaceInfo.family !== "IPv4" ||
interfaceInfo.internal !== false
) {
// skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
continue
}
let ip = interfaceInfo.address
localIpList.push(ip)
}
}
return localIpList
}
}
export default Tool
src/model/**
模块
将通用模块, 抽取到这个文件夹下面,以功能作为类名。例如
user.ts
封装user相关的方法validator.ts
封装验证器相关方法
src/public/**
静态资源
静态资源文件夹
9. 开始开发
接下来, 写一个user相关的增、删、改、查接口.
user模块
为了简化示例, 用本地变量代替数据库的功能
新建src/model/user.ts
文件
import _ from "lodash"
type IUser = {
id: number
name: string
sex: string
}
export default class MUser {
// 用户列表
static list: IUser[] = [
{
id: 1234,
name: "丹妮莉丝·坦格利安",
sex: "女",
},
{
id: 1235,
name: "珊莎·史塔克",
sex: "女",
},
{
id: 1235,
name: "艾莉亚·史塔克",
sex: "女",
},
]
/**
* 获取用户详情
*/
static async getDetail(id: string) {
return this.list.find((v) => v.id === +id)
}
/**
* 获取用户列表
*/
static async getList() {
return this.list
}
/**
* 修改用户信息
*/
static async update(id: string, options: Partial<Omit<IUser, "id">>) {
this.list = this.list.map((v) => {
if (v.id === +id) {
Object.assign(v, options)
}
return v
})
}
/**
* 删除用户信息
*/
static async delete(id: string) {
this.list = this.list.filter((v) => v.id !== +id)
}
}
user控制器
新建src/controller/api/user.ts
文件
import { Request, Response } from "express"
import { JsonController, Req, Res, Get, Post } from "routing-controllers"
import BaseController from "~/src/controller/base"
import MUser from "~/src/model/user"
import MValidator from "~/src/model/validator"
@JsonController()
class Controller extends BaseController {
@Get("/api/user/info")
async info(@Req() request: Request, @Res() response: Response) {
const { id } = request.query
MValidator.checkRequiredFields(["id"], request.query)
const res = await MUser.getDetail(id as string)
return this.showResult(res)
}
@Get("/api/user/list")
async list(@Req() request: Request, @Res() response: Response) {
const res = await MUser.getList()
return this.showResult(res)
}
@Post("/api/user/update")
async update(@Req() request: Request, @Res() response: Response) {
const { id, name } = request.body
MValidator.checkRequiredFields(["id", "name"], request.body)
await MUser.update(id, { name })
return this.showResult({ id, name }, `更新成功`)
}
@Post("/api/user/delete")
async delete(@Req() request: Request, @Res() response: Response) {
const { id } = request.body
MValidator.checkRequiredFields(["id"], request.body)
await MUser.delete(id)
return this.showResult({ id }, `删除成功`)
}
}
export default Controller
上面的代码用到了装饰器语法
JsonController
让接口返回值转换为JSON, 并且设置响应头Content-Type: application/json
Get
匹配method为get
请求Post
匹配method为post
请求Req
express request 对象Res
express response 对象
使用控制器
在src/controller/index.ts
引入user.ts
import ApiUser from "~/src/controller/api/user"
const ControllerList: Function[] = [ApiUser]
export default ControllerList
修改src/app.ts
// 路径别名
require("module-alias").addAlias("~/src", __dirname + "/")
// this shim is required
import "reflect-metadata"
import express from "express"
import { useExpressServer } from "routing-controllers"
import cookieParser from "cookie-parser"
import bodyParser from "body-parser"
import path from "path"
import _ from "lodash"
import moment from "moment"
import chalk from "chalk"
import ControllerList from "~/src/controller/index"
import NetworkUtil from "~/src/library/utils/network"
import appConfig from "~/src/config/app"
let server
const startup = () => {
// 需要先创建express实例, 注册完中间层之后再将路由交给routing-controllers
// 否则后续的中间层代码都拿不到cookie/body, 导致程序异常
const app = express()
// 设置body-parser
app.use(bodyParser.urlencoded({ extended: false }))
// 解析json请求
app.use(bodyParser.json())
// 设置cookie-parse
app.use(cookieParser())
// 设置存放模板引擎目录
app.set("views", path.join(__dirname, "./public"))
// 设置模板引擎为ejs
app.set("view engine", "ejs")
/* 添加静态路径 */
app.use("/public", express.static(path.join(__dirname, "public")))
app.use(
"/favicon.ico",
express.static(path.resolve(__dirname, "./public/favicon.ico"))
)
useExpressServer(app, {
// 支持跨域
cors: {
origin: function (origin: any, callback: any) {
callback(null, origin)
},
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
credentials: false,
allowedHeaders: [
"Origin",
"X-Requested-With",
"X-Custom-Session",
"X-Hetu-Token",
"Content-Type",
"Accept",
],
},
controllers: ControllerList,
middlewares: [],
defaultErrorHandler: false,
})
let serverStartAtYmdHis = moment().format("YYYY-MM-DD HH:mm:ss")
console.log(chalk.magenta(`服务启动于=>${serverStartAtYmdHis}`))
let ipList = NetworkUtil.getLocalIpList()
server = app.listen(appConfig.port, function () {
console.log(`listening on port ${appConfig.port}`)
console.log(`↓↓↓↓↓点击任意链接打开调试界面↓↓↓↓↓`)
console.log(``)
for (let ip of ipList) {
let uri = `http://${ip}:${appConfig.port}/api/user/info`
console.log(chalk.green(`${uri}`))
}
console.log(``)
})
}
startup()
重启服务
yarn start
使用Postman测试上面的增、删、改、查的接口
- Get
http://127.0.0.1:7001/api/user/info?id=1234
- Get
http://127.0.0.1:7001/api/user/list
- Post
http://127.0.0.1:7001/api/user/update
- Post
http://127.0.0.1:7001/api/user/delete
参考
- TypeScript-Node-Starter 微软typescript+node脚手架
- nest github热门脚手架