ts-node项目
该简单项目搭建简单node后端环境,增删改查接口,并接口测试,最后发布到docker
创建项目
mkdir tsnode
cd tsnode
npm init -y 生成默认package.json
npx tsconfig.json 快速创建node环境
安装依赖
tnpm i express mysql2 morgan http-errors http-satus-codes -S
tnpm i cross-env typescript @types/express @types/morgan @types/http-errors ts-node-dev nodemon ts-node nyc mocha @types-mocha chai @types/chai chai-http -D
- morgan HTTP request logger middleware for node.js 打印日志
- http-errors Create HTTP errors for Express, Koa, Connect, etc. with ease.
- http-satus-codes http常用状态码
- ts-node-dev 运行ts版node代码,通过--respawn监控源文件变化
- ts-node node不支持import,export,靠它来支持
- nyc 用来生成测试覆盖率文件
- mocha 测试框架
- chai 断言库
package.json
"scripts": {
"start": "cross-env PORT=8000 ts-node-dev --respawn ./src/bin/www.ts",
"dev": "cross-env PORT=8000 nodemon --exec ts-node --files ./src/bin/www.ts",
"test": "cross-env PORT=8000 TS_NODE_FILES=true mocha -r ts-node/register src/tests/*.spec.ts --exit",
"coverage": "nyc --reporter lcov npm run test"
},
其中dev,设置环境变量 9000,用nodemon启动 执行bin下面的文件
项目目录结构
app.ts
import createError from 'http-errors';
import express, { Express, Request, Response, NextFunction } from 'express';
import { NOT_FOUND, INTERNAL_SERVER_ERROR } from 'http-status-codes';
// import logger from 'morgan';
import indexRouter from './routes/index'; // 引入routers中的index.ts
import usersRouter from './routes/user'; // 引入routers中的user.ts
const app: Express = express();
// app.use(logger('dev')); // 打印请求日志 dev是格式,short,tiny
app.use(express.json()); // 解析JSON格式的请求体,实际是调的 bodyParser.json
app.use(express.urlencoded({ extended: false })); // 解析表单形式的请求 bodyParser.urlencoded
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use( (_req: Request, _res: Response, next: NextFunction) => {
next(createError(NOT_FOUND)); // 给错误处理中间件, NOT_FOUND 传递给下面的error
})
app.use((error: any, _req: Request, res: Response, _next: NextFunction) => {
res.status(error.status || INTERNAL_SERVER_ERROR);
res.json({
success: false,
error
})
})
export default app;
www.ts
// 启动服务
import app from '../app'; // 引入
import http from 'http';
const port = process.env.PORT || 8000;
const server = http.createServer(app);
server.listen(port);
server.on('error', (error) => {
console.error(error); // 监听错误
})
server.on('listening', () => {
console.log('listening on ' + port); // 监听启动
})
数据库mysql 和 sequelize 定义模型
sequelize.ts
// Sequelize 是一个基于 promise 的 Node.js ORM,(像操作js一样操作) 目前支持 Postgresql, MySQL, SQLite 和 Microsoft SQL Server.
// 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能.
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('api', 'root', 'root', { // database, username, password
host: 'localhost', // 本地用localhost,后期docker改成db
dialect: 'mysql', // 使用哪种数据库
logging: false // 是否打印日志
})
export {
sequelize
}
user.ts
import { Model, DataTypes } from 'sequelize';
import { sequelize } from './sequelize';
/**
* 每个数据库表对应一个模型Model
* Model封装了针对数据库的各种操作
*/
class User extends Model {}
User.init({
username: DataTypes.STRING,
password: DataTypes.STRING,
email: DataTypes.STRING
}, {sequelize, modelName: 'user'}); // user表,字段username,pwd,email
// 同步到数据库中,第一次可以使用
// sequelize.sync().then(() => {
// User.create({
// username: 'jeffywin',
// password: '123456',
// email: 'jeffywin@xxx.com'
// })
// }).then((result: User) => {
// console.log('result',result);
// });
export {
User
}
routers user.ts
import express, {Response, Router, Request, NextFunction} from 'express';
import createError from 'http-errors';
import { INTERNAL_SERVER_ERROR } from 'http-status-codes'; // 500错误
import { User } from '../model'; // sequlize中的模型
let router: Router = express.Router();
// 获取所有用户 GET /users localhost:8000/user
router.get('/', async(_req: Request, res: Response) => {
let users = await User.findAll(); // 找到数据库中所有用户
res.json({
success: true,
data: users
})
})
// 获取某个用户 GET /users/:id
router.get('/:id', async(req:Request, res: Response, next: NextFunction) => {
try {
let user = await User.findByPk(req.params.id); // findPrimayKey
if(user) {
res.json({
success: true,
data: user
})
}
} catch (error) {
next(createError(INTERNAL_SERVER_ERROR))
}
})
// 添加一个用户 POST /users
router.post('/', async(req: Request, res: Response, next: NextFunction) => {
try {
let user = req.body;
user = await User.create(user);
res.json({
success: true,
data: user
})
} catch (error) {
next(createError(INTERNAL_SERVER_ERROR));
}
})
// 更新一个用户 PUT /users/:id
router.put('/:id', async(req: Request, res: Response, next: NextFunction) => {
try {
let user = await User.findByPk(req.params.id);
let update = req.body; // 拿到更新的内容
user = await user.update(update);
res.json({
success: true,
data: user
})
} catch (error) {
next(createError(INTERNAL_SERVER_ERROR));
}
})
// 删除一个用户 DELETE /users/:id
router.delete('/:id', async(req: Request, res: Response, next: NextFunction) => {
try {
let id = req.params.id;
let user = await User.findByPk(id);
user = await user.destroy(); // 删除用户
res.json({
success: true,
data: user
})
} catch (error) {
next(createError(INTERNAL_SERVER_ERROR));
}
})
export default router;
测试
helper.spec.ts
import chai from 'chai';
import chaiHttp from 'chai-http'; // 插件
import {sequelize, User} from '../model';
chai.use(chaiHttp);
// 在所有单元测试之前进行
before(async () => {
await sequelize.sync(); // 让模型里的定义和数据库定义同步
})
// 在所有单元测试之前进行
after(async () => {
})
// 在每个单元测试之前进行
beforeEach(async () => {
await User.truncate(); // 清空表
})
// 在每个单元测试之后进行
afterEach(async () => {
await User.truncate();
})
index.spec.ts
import app from '../app';
import chai, {expect} from 'chai';
describe('测试用户的restful接口', () => {
it('POST /users 添加用户', async () => {
let result = await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({username: 'jeffywin', password: '12345', email: 'jeffywin@126.com'});
expect(result).to.have.status(200);
expect(result.body).to.have.property('success');
expect(result.body.success).to.equal(true);
expect(result.body).to.have.property('data');
expect(result.body.data.id).to.equal(1);
})
it('GET /users 查询用户', async () => {
// 由于数据库清空,先添加一条用户
await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({username: 'zhangsan', password: '12345', email: 'jeffywin@126.com'});
await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({username: 'lisi', password: '12345', email: 'jeffywin@126.com'});
let result = await chai.request(app)
.get('/users')
expect(result).to.have.status(200);
expect(result.body).to.have.property('success');
expect(result.body.success).to.equal(true);
expect(result.body).to.have.property('data');
expect(result.body.data).to.have.lengthOf(2);
})
it('DELETE /users/:id 删除用户', async () => {
// 删除之前先添加一个用户,再删除
let addResult = await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({username: 'lisi', password: '12345', email: 'jeffywin@126.com'});
await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({username: 'zhangsan', password: '12345', email: 'jeffywin@126.com'});
await chai.request(app)
.delete(`/users/${addResult.body.data.id}`)
let result1 = await chai.request(app)
.get('/users')
expect(result1).to.have.status(200);
expect(result1.body).to.have.property('success');
expect(result1.body.success).to.equal(true);
expect(result1.body).to.have.property('data');
expect(result1.body.data).to.have.lengthOf(1);
})
it('PUT /users/:id 更新用户', async () => {
// 先插入用户,然后查询用户,再expect
let addResult = await chai.request(app)
.post('/users')
.set('Content-Type', 'application/json')
.send({username: 'lisi', password: '12345', email: 'jeffywin@126.com'});
let result = await chai.request(app)
.put(`/users/${addResult.body.data.id}`)
.set('Content-Type', 'application/json')
.send({username: 'lisi22', password: '654321', email: 'jeffywin@126.com'});
let result2 = await chai.request(app)
.get(`/users/${result.body.data.id}`)
expect(result2).to.have.status(200);
expect(result2.body).to.have.property('success');
expect(result2.body.success).to.equal(true);
expect(result2.body).to.have.property('data');
expect(result2.body.data.username).to.equal('lisi22');
expect(result2.body.data.password).to.equal('654321');
})
})
最后 npm run coverage 查看测试覆盖率文件
docker
Docker是一个开放源代码软件项目,让应用程序部署在软件货柜下的工作可以自动化进行,借此在Linux操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制
准备工作
- 1.购买云服务器
- 2.本地能够访问云服务器,我用的是Core Shell
连上去后就是这个状态
- 3.安装docker
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
- 4.aliyun加速,不然下载包会非常慢,安装git
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://fwvjnv59.mirror.aliyuncs.com"]
}
EOF
# 重载所有修改过的配置文件
systemctl daemon-reload
systemctl restart docker
yum install git -y
- 5.本地项目新建Dockerfile文件
FROM node
COPY . /api
WORKDIR /api
RUN yarn config set registry 'https://registry.npm.taobao.org'
RUN yarn
EXPOSE 8000
CMD npm start
- 6.本地项目新建.dockerignore文件,忽略上传文件
.git
node_modules
package.lock.json
Dockerfile
.dockerignore
- 7.云服务器安装docker-compose
curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
-
8.提交本地项目至远程仓库,并在云服务器上拉取代码
-
9.vim docker-compose.yml,注意格式问题,很容易报错
version: '2'
services:
node:
build:
context: ./tsnode // 云服务器拉取的项目文件夹
dockerfile: Dockerfile
ports:
- "8000:8000"
depends_on:
- db
db:
image: mariadb // 数据库
environment:
MYSQL_ROOT_PASSWORD: "root"
MYSQL_DATABASE: "restful"
volumes:
- db:/var/lib/mysql
volumes:
db:
driver: local
- 10.启动docker-compose,三个命令
docker-compose build 打包代码
docker-compose up 运行
docker-compose up -d 后台运行