предисловие
Поскольку недавно я планировал создать свою собственную систему блогов для записи ежедневного обучения и повышения уровня письма, я планирую самостоятельно создавать интерфейсные и серверные проекты. Искал в интернете и не нашел подходящего (готового) проекта, поэтому решил собрать сам. В этой статье в основном описывается, как создать службу интерфейса API узла.
Краткое описание стека технологий
Нодовых фреймворков в интернете тоже достаточно много.Самые используемые фреймворки это egg,express,koa и т.д.Между фреймворками есть свои плюсы и минусы.В итоге после баланса я решил использовать более масштабируемый koa2 для сборки проекта.После изучения машинописного текста я, наконец, решил использовать стек технологий: koa+typescript+mysql+mongodb для создания проекта.
Зачем использовать узел
Самое главное, что мы не знаем никаких других языков. . .
Ближе к дому Node.js — это фреймворк, работающий на стороне сервера.Он использует движок V8 внизу, который очень быстр, а в качестве внешнего языка внутреннего интерфейса есть и другие привлекательные места:
-
Асинхронный ввод-вывод
Поскольку все узлы являются асинхронными при вводе-выводе, они могут хорошо справляться со сценариями с высокой степенью параллелизма.
-
управляемый событиями
-
один поток
-
Кроссплатформенность
Более того, самым важным моментом является то, что узел разработан на языке JavaScript, что значительно снижает стоимость обучения фронтенд-студентов.
Koa
koa — это новый фреймворк, созданный оригинальной командой Express. По сравнению с экспрессом коа меньше, выразительнее и крепче. Конечно, то, что я сказал выше, является неправдой.На самом деле, что меня действительно привлекает, так это то, что koa использует асинхронную функцию для решения проблемы адского обратного вызова в express.js через метод записи es6, а koa не поставляется с таким количеством промежуточное программное обеспечение как экспресс.Для частного проекта это, несомненно, отлично.Еще одна особенность - уникальный контроль над промежуточным программным обеспечением koa, который является моделью koa onion, о которой все говорят.
Про луковую модель, наверное, резюмировал в двух пунктах
- Сохранение и передача контекста
- Управление промежуточным ПО и реализация следующего
(картинка взята из интернета)
На приведенных выше двух картинках наглядно показан рабочий процесс луковой модели.Конечно, если реализуется конкретный принцип, то это не имеет отношения к данной статье, поэтому подробно описываться не будет.Заинтересованные студенты могут сами поискать в интернете .
Typescript
В Интернете много дискуссий и статей на тему «Зачем использовать Typescript-разработку», «Преимущества и недостатки Typescript-разработки», «Почему не использовать Typescript-разработку» и т. д. Об этом также могут рассказать заинтересованные студенты.
Этот проект использует ts в основном по следующим соображениям:
- В моем постоянном изучении ТС «то, что я узнаю на бумаге, поверхностно, и я никогда не знаю, что этот вопрос нужно практиковать». Чтобы углубить мое понимание ТС, требуется больше реальных сражений.
- Используйте все, что хотите, для своих собственных проектов
- Сила письма будет относительно высокой
- В Ts есть много вещей, которых нет в js, например, общая абстракция интерфейса и т. д.
- хорошее управление модулями
- Строго типизированный голос, личное ощущение больше подходит чем js для разработки серверных проектов
- Имеется хороший механизм подсказок об ошибках, позволяющий избежать многих низкоуровневых ошибок на этапе разработки.
- Ограниченные привычки разработки, сделайте ваш код более элегантным.
Наконец, помните, что то, что подходит вам, лучше всего
Mysql
MySQL является самой популярной системой управления реляционными базами данных.С точки зрения веб-приложений, MySQL является одним из лучших прикладных программ RDBMS (система управления реляционными базами данных: система управления реляционными базами данных).
Mongodb
Зачем использовать mysql и mongodb? На самом деле это в основном потому, что jwt используется для аутентификации личности.Поскольку промежуточное программное обеспечение не предоставляет API для обновления времени истечения срока действия и хочет реализовать функцию автоматического обновления, mongodb используется для помощи в выполнении функции автоматического обновления. . И некоторая информация об идентификаторе пользователя или информация о скрытой точке может храниться в монго.
PM2
PM2 — это инструмент управления процессами узла, который можно использовать для упрощения многих утомительных задач управления приложениями узла, таких как мониторинг производительности, автоматический перезапуск, балансировка нагрузки и т. д., и он очень прост в использовании.
Строительство проекта
Я в основном разделяю проект на: фреймворк, лог, конфигурация, маршрутизация, обработка логики запроса, моделирование данных эти модули
Ниже представлена структура каталогов проекта:
├── app 编译后项目文件
├── node_modules 依赖包
├── static 静态资源文件
├── logs 服务日志
├── src 源码
│ ├── abstract 抽象类
│ ├── config 配置项
│ ├── controller 控制器
│ ├── database 数据库模块
│ ├── middleware 中间件模块
│ ├── models 数据库表模型
│ ├── router 路由模块 - 接口
│ ├── utils 工具
│ ├── app.ts koa2入口
├── .eslintrc.js eslint 配置
├── .gitignore 忽略提交到git目录文件
├── .prettierrc 代码美化
├── ecosystem.config.js pm2 配置
├── nodemon.json nodemon 配置
├── package.json 依赖包及配置信息文件
├── tsconfig.json typescript 配置
├── README.md 描述文件
Нечего сказать, давайте следовать коду, чтобы увидеть проект
Создать коа-приложение
Как говорится: без головы никто не ходит. Так же в проекте будет голова, которая ведет проект, это запись app.ts, а потом мы совместим код, чтобы посмотреть, как она делает эту голову
import Koa, { ParameterizedContext } from 'koa'
import logger from 'koa-logger'
// 实例化koa
const app = new Koa()
app.use(logger())
// 答应一下响应信息
app.use(async (ctx, next) => {
const start = (new Date()).getDate();
let timer: number
try {
await next()
timer = (new Date()).getDate()
const ms = timer - start
console.log(`method: ${ctx.method}, url:${ctx.url} - ${ms}ms`)
} catch (e) {
timer = (new Date()).getDate()
const ms = timer - start
console.log(`method: ${ctx.method}, url:${ctx.url} - ${ms}ms`)
}
})
// 监听端口并启动
app.listen(config.PORT, () => {
console.log(`Server running on http://localhost:${config.PORT || 3000}`)
})
app.on('error', (error: Error, ctx: ParameterizedContext) => {
// 项目启动错误
ctx.body = error;
})
export default app
На данный момент мы уже можем начать простой проект
-
npm run tsc
скомпилировать ts-файл -
node app.js
Стартовый проект
локальная среда разработки
Локальная разработка использует nodemon для автоматического перезапуска, потому что node не может напрямую распознать ts, поэтому вам нужно использовать ts-node для запуска файла ts.
// nodemon.json
{
"ext": "ts",
"watch": [ // 需要监听变化的文件
"src/**/*.ts",
"config/**/*.ts",
"router/**/*.ts",
"public/**/*",
"view/**/*"
],
"exec": "ts-node --project tsconfig.json src/app.ts" // 使用ts-node来执行ts文件
}
// package.json
"scripts": {
"start": "cross-env NODE_ENV=development nodemon -x"
}
локальная отладка
Так как иногда нам нужно увидеть запрошенную информацию, то мы не можем добавить ее в кодconsole.log(日志)
Это неэффективно и неудобно, поэтому нам нужно использовать редактор, который поможет нам реализовать функцию отладки. Вот краткое описание того, как использовать vscode для отладки.
- Включить sourceMap в tsconfig.json
- Зарегистрируйте задачу отладки vsc для ts-node, измените файл launch.json проекта и добавьте новый метод запуска.
- launch.json
{
"name": "Current TS File",
"type": "node",
"request": "launch",
"args": [
"${workspaceRoot}/src/app.ts" // 入口文件
],
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register"
],
"sourceMaps": true,
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
- Точка останова кода F9
- F5 запускает отладку кода
Импорт маршрутов интерфейса
Мы создали приложение koa выше, а затем нам нужно импортировать маршруты:
// app.ts
import router from './router'
import requestMiddleware from './middleware/request'
app
.use(requestMiddleware) // 使用路由中间件处理路由,一些处理接口的公用方法
.use(router.routes())
.use(router.allowedMethods())
// router/index.ts
import { ParameterizedContext } from 'koa'
import Router from 'koa-router'
const router = new Router()
// 接口文档 - 这边使用分模块实现路由的方式
router.use(路由模块.routes())
...
router.use(路由模块.routes())
// 测试路由连接
router.get('/test-connect', async (ctx: ParameterizedContext) => {
await ctx.body = 'Hello Frivolous'
})
// 匹配其他未定义路由
router.get('*', async (ctx: ParameterizedContext) => {
await ctx.render('error')
})
export default router
Определить модель базы данных
- Используйте sequalize как промежуточное ПО для mysql
// 实例化sequelize
import { Sequelize } from 'sequelize'
const sequelizeManager = new Sequelize(db, user, pwd, Utils.mergeDefaults({
dialect: 'mysql',
host: host,
port: port,
define: {
underscored: true,
charset: 'utf8',
collate: 'utf8_general_ci',
freezeTableName: true,
timestamps: true,
},
logging: false,
}, options));
}
// 定义表结构
import { Model, ModelAttributes, DataTypes } from 'sequelize'
// 定义用户表模型中的字段属性
const UserModel: ModelAttributes = {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
unique: true,
autoIncrement: true,
comment: 'id'
},
avatar: {
type: DataTypes.INTEGER,
allowNull: true
},
nick_name: {
type: DataTypes.STRING(50),
allowNull: true
},
email: {
type: DataTypes.STRING(50),
allowNull: true
},
mobile: {
type: DataTypes.INTEGER,
allowNull: false
},
gender: {
type: DataTypes.STRING(35),
allowNull: true
},
age: {
type: DataTypes.INTEGER,
allowNull: true
},
password: {
type: DataTypes.STRING(255),
allowNull: false
}
}
// 定义表模型
sequelizeManager.define(modelName, UserModel, {
freezeTableName: true, // model对应的表名将与model名相同
tableName: modelName,
timestamps: true,
underscored: true,
paranoid: true,
charset: "utf8",
collate: "utf8_general_ci",
})
В соответствии с приведенным выше кодом мы уже определили пользовательскую таблицу, и другие таблицы также могут быть определены в соответствии с этим. Однако, в дополнение к использованию mysql, этот проект также использует mongo, давайте посмотрим, как использовать mongodb.
- Используйте mongoose в качестве промежуточного программного обеспечения для mongodb
// mongoose入口
import mongoose from 'mongoose'
const uri = `mongodb://${DB.host}:${DB.port}`
mongoose.connect('mongodb://' + DB_STR)
mongoose.connection.on('connected', () => {
log('Mongoose connection success')
})
mongoose.connection.on('error', (err: Error) => {
log('Mongoose connection error: ' + err.message)
})
mongoose.connection.on('disconnected', () => {
log('Mongoose connection disconnected')
})
export default mongoose
// 定义表模型
import mongoose from '../database/mongoose'
const { Schema } = mongoose
const AccSchema = new Schema({}, {
strict: false, // 允许传入未定义字段
timestamps: true, // 默认会带上createTime/updateTime
versionKey: false // 默认不带版本号
})
export default AccSchema
// 定义模型
mongoose.model('AccLog', AccSchema)
реализовать интерфейс
Ну, мы определили модель таблицы выше, и следующим шагом будет реализация захватывающего интерфейса. Мы реализуем его через простой интерфейс скрытой точки.Во-первых, нам нужно проанализировать логику, реализованную инструментом скрытой точки:
- Поскольку информация о скрытых точках не является реляционной, mongodb используется для хранения информации о скрытых точках.
- Поскольку это простой интерфейс записи, он должен быть более общим, то есть за исключением нескольких ключевых полей, то, что передает вызывающий абонент, сохраняется.
- Поведение точек захоронения незаметно для пользователей, поэтому информация обратной связи не предусмотрена, и если в точках захоронения есть ошибки, они будут обрабатываться внутри.
Что ж, разобравшись с функцией этой скрытой точки, давайте начнем реализовывать этот простой интерфейс:
// route.ts 定义一个addAccLog的接口
router.post('/addAccLog', AccLogController.addAccLog)
// AccLogController.ts 实现addAccLog接口
class AccLogRoute extends RequestControllerAbstract {
constructor() {
super()
}
// AccLogController.ts
public async addAccLog(ctx: ParameterizedContext): Promise<void> {
try {
const data = ctx.request.body
const store = Mongoose.model(tableName, AccSchema, tableName)
// disposeAccInsertData 方法用来处理日志信息,有些字段嵌套太要扁平化深或者去除空值冗余字段
const info = super.disposeAccInsertData(data.logInfo)
// 添加日志
const res = await store.create(info)
// 不需要反馈
// super.handleResponse('success', ctx, res)
} catch (e) {
// 错误处理 - 比如说打个点,记录埋点出错的信息,看看是什么原因导致出错的(根据实际的需求来做)
// ...
}
}
// ...
}
export default new AccLogRoute()
Говоря об этом, я должен упомянуть, что маршрутизация может ввести написание декораторов, что может уменьшить дублирование работы и повысить эффективность Заинтересованные студенты могут прочитать мой последний блог. Вот код метода написания декоратора:
@Controller('/AccLogController')
class AccLogRoute {
@post('/addAccLog')
@RequestBody({})
async addAccLog(ctx: ParameterizedContext, next: Function) {
const res = await store.create(info)
handleResponse('success', ctx, res)
}
}
Показывает ли это сравнение преимущества декораторов?
jwt-аутентификация
Используйте jsonwebtoken здесь для проверки jwt
import { sign, decode, verify } from 'jsonwebtoken'
import { ParameterizedContext } from 'koa'
import { sign, decode, verify } from 'jsonwebtoken'
import uuid from 'node-uuid'
import IController from '../interface/controller'
import config from '../config'
import rsaUtil from '../util/rsaUtil'
import cacheUtil from '../util/cacheUtil'
interface ICode {
success: string,
unknown: string,
error: string,
authorization: string,
}
interface IPayload {
iss: number | string; // 用户id
login_id: number | string; // 登录日志id
sub?: string;
aud?: string;
nbf?: string;
jti?: string;
[key: string]: any;
}
abstract class AController implements IController {
// 服务器响应状态
// code 状态码参考 https://www.cnblogs.com/zqsb/p/11212362.html
static STATE = {
success: { code: 200, message: '操作成功!' },
unknown: { code: -100, message: '未知错误!' },
error: { code: 400, message: '操作失败!' },
authorization: { code: 401, message: '身份认证失败!' },
}
/**
* @description 响应事件
* @param {keyof ICode} type
* @param {ParameterizedContext} [ctx]
* @param {*} [data]
* @param {string} [message]
* @returns {object}
*/
public handleResponse(
type: keyof ICode,
ctx?: ParameterizedContext,
data?: any,
message?: string
): object {
const res = AController.STATE[type]
const result = {
message: message || res.message,
code: res.code,
data: data || null,
}
if (ctx) ctx.body = result
return result
}
/**
* @description 注册token
* @param {IPayload} payload
* @returns {string}
*/
public jwtSign(payload: IPayload): string {
const { TOKENEXPIRESTIME, JWTSECRET, RSA_PUBLIC_KEY } = config.JWT_CONFIG
const noncestr = uuid.v1()
const iss = payload.iss
// jwt创建Token
const token = sign({
...payload,
noncestr
}, JWTSECRET, { expiresIn: TOKENEXPIRESTIME, algorithm: "HS256" })
// 加密Token
const result = rsaUtil.pubEncrypt(RSA_PUBLIC_KEY, token)
const isSave = cacheUtil.set(`${iss}`, noncestr, TOKENEXPIRESTIME * 1000)
if (!isSave) {
throw new Error('Save authorization noncestr error')
}
return `Bearer ${result}`
}
/**
* @description 验证Token有效性,中间件
*
*/
public async verifyAuthMiddleware(ctx: ParameterizedContext, next: Function): Promise<any> {
// 校验token
const { JWTSECRET, RSA_PRIVATE_KEY, IS_AUTH, IS_NONCESTR } = config.JWT_CONFIG
if (!IS_AUTH && process.env.NODE_ENV === 'development') {
await next()
} else {
// 如果header中没有身份认证字段,则认为校验失败
if (!ctx.header || !ctx.header.authorization) {
ctx.response.status = 401
return
}
// 获取token并且解析,判断token是否一致
const authorization: string = ctx.header.authorization;
const scheme = authorization.substr(0, 6)
const credentials = authorization.substring(7)
if (scheme !== 'Bearer') {
ctx.response.status = 401;
this.handleResponse('authorization', ctx, null, 'Wrong authorization prefix')
return;
}
if (!credentials) {
ctx.response.status = 401;
this.handleResponse('authorization', ctx, null, 'Request header authorization cannot be empty')
return;
}
const token = rsaUtil.priDecrypt(RSA_PRIVATE_KEY, credentials)
if (typeof token === 'object') {
ctx.response.status = 401;
this.handleResponse('authorization', ctx, null, 'authorization is not an object')
return;
}
const isAuth = verify(token, JWTSECRET)
if (!isAuth) {
ctx.response.status = 401;
this.handleResponse('authorization', ctx, null, 'authorization token expired')
return;
}
const decoded: string | { [key: string]: any } | null = decode(token)
if (typeof decoded !== 'object' || !decoded) {
ctx.response.status = 401;
this.handleResponse('authorization', ctx, null, 'authorization parsing failed')
return;
}
const noncestr = decoded.noncestr
const exp = decoded.exp
const iss = decoded.iss
const cacheNoncestr = cacheUtil.get(`${iss}`)
if (IS_NONCESTR && noncestr !== cacheNoncestr) {
ctx.response.status = 401;
this.handleResponse('authorization', ctx, null, 'authorization signature "noncestr" error')
return;
}
if (Date.now() / 1000 - exp < 60) {
const options = { ...decoded };
Reflect.deleteProperty(options, 'exp')
Reflect.deleteProperty(options, 'iat')
Reflect.deleteProperty(options, 'nbf')
const newToken = AController.prototype.jwtSign(options as IPayload)
ctx.append('token', newToken)
}
ctx.jwtData = decoded
await next()
}
}
}
export default AController
// 授权装饰器代码
public auth() {
return (target: any, name?: string, descriptor?: IDescriptor) => {
if (typeof target === 'function' && name === undefined && descriptor === undefined) {
target.prototype.baseAuthMidws = super.verifyAuthMiddleware;
} else if (typeof target === 'object' && name && descriptor) {
descriptor.value.prototype.baseAuthMidws = super.verifyAuthMiddleware;
}
}
}
Таким образом, мы завершили авторизованный модуль jwt, который также очень прост для нас в использовании, взяв в качестве примера интерфейс addAccLog.
class AccLogRoute {
@auth() // 只要➕这一行代码就可以
@post('/addAccLog')
...
}
документация по интерфейсу
Поскольку мы написали интерфейс, всегда будет вывод документов. В это время я думаю о чванство. Давайте представим чванство в наш проект.
- Вход
// swagger入口
import swaggerJSDoc from 'swagger-jsdoc'
import config from '../config'
const { OPEN_API_DOC } = config
// swagger definition
const swaggerDefinition = {
// ...
}
const createDOC = (): object => {
const options = {
swaggerDefinition: swaggerDefinition,
apis: ['./src/controller/*.ts']
}
return OPEN_API_DOC ? swaggerJSDoc(options) : null
}
export default createDOC
// 怎么
- Пример конфигурации - обязательно обратите внимание на формат здесь
@swagger Tips: 必须要声明,不然代码不会把此处生成为文档
definitions:
Login: // 接口名
required: // 必填参数
- username
- password
properties: // 可选参数
username:
type: string
password:
type: string
path:
type: string
- официальный инструмент настройки swagger
- Порекомендуйте плагин vscode — плагин для быстрой генерации комментариев
Имитация данных
Используйте макеты для создания тестовых данных
журнал
Модуль журнала изначально планировалось сделать с помощью log4.js, но позже я почувствовал, что сделанный мной модуль журнала не оправдал ожиданий, поэтому я решил временно заменить log4 на систему журналов pm2. Я не буду публиковать здесь код, связанный с log4.
развертывать
Используйте pm2 для развертывания проекта, вот файл конфигурации
Tips
- вывод журнала ошибок error_file
- out_file нормальный вывод журнала
- файл записи сценария - используйте упакованный файл js в качестве записи
// pm2.json
{
"apps": {
"name": "xxx",
"script": "./app/server.js",
"cwd": "./",
"args": "",
"interpreter_args": "",
"watch": true,
"ignore_watch": [
"node_modules",
"logs",
"app/lib"
],
"exec_mode": "fork_mode",
"instances": 1,
"max_memory_restart": 8,
"error_file": "./logs/pm2-err.log",
"out_file": "./logs/pm2-out.log",
"merge_logs": true,
"log_date_format": "YYYY-MM-DD HH:mm:ss",
"max_restarts": 30,
"autorestart": true,
"cron_restart": "",
"restart_delay": 60,
"env": {
"NODE_ENV": "production"
}
}
}
// package.json
"scripts": {
// 生产环
"prod": "pm2 start pm2.json"
}
После настройки pm2 нам нужно только настроить его в package.jsonpm2 start pm2.json
Вы можете запустить процесс pm2
заключительные замечания
Несмотря на то, что это сервер с простым интерфейсом, нужно учитывать множество моментов, и, поскольку многие плагины являются первым контактом, весь процесс реализации проекта довольно ухабистый, в основном это своего рода переход через реку, ощупывая камни. Но это и больно и радостно.Хотя сложностей много, но в процессе я также почерпнул много новых знаний.Я примерно понимаю вес простого back-end сервисного проекта, понимаю процесс работы back-end в простой проект. Напоследок закончим причитанием: стоит войти в передок, как он глубок, как море, и на новом кадре ничему не научишься. Здоровяки, хватит учиться, не успеваешь...
Адрес проекта на GitHub
Адрес складаЕсли вам это нравится, пожалуйста, дайте ему звезду