Служба блога развития узла

Node.js

предисловие

​ Поскольку недавно я планировал создать свою собственную систему блогов для записи ежедневного обучения и повышения уровня письма, я планирую самостоятельно создавать интерфейсные и серверные проекты. Искал в интернете и не нашел подходящего (готового) проекта, поэтому решил собрать сам. В этой статье в основном описывается, как создать службу интерфейса API узла.

Краткое описание стека технологий

Нодовых фреймворков в интернете тоже достаточно много.Самые используемые фреймворки это egg,express,koa и т.д.Между фреймворками есть свои плюсы и минусы.В итоге после баланса я решил использовать более масштабируемый koa2 для сборки проекта.После изучения машинописного текста я, наконец, решил использовать стек технологий: koa+typescript+mysql+mongodb для создания проекта.

Зачем использовать узел

Самое главное, что мы не знаем никаких других языков. . .

无奈表情包

Ближе к дому Node.js — это фреймворк, работающий на стороне сервера.Он использует движок V8 внизу, который очень быстр, а в качестве внешнего языка внутреннего интерфейса есть и другие привлекательные места:

  1. Асинхронный ввод-вывод

    Поскольку все узлы являются асинхронными при вводе-выводе, они могут хорошо справляться со сценариями с высокой степенью параллелизма.

  2. управляемый событиями

  3. один поток

  4. Кроссплатформенность

Более того, самым важным моментом является то, что узел разработан на языке JavaScript, что значительно снижает стоимость обучения фронтенд-студентов.

Koa

koa — это новый фреймворк, созданный оригинальной командой Express. По сравнению с экспрессом коа меньше, выразительнее и крепче. Конечно, то, что я сказал выше, является неправдой.На самом деле, что меня действительно привлекает, так это то, что koa использует асинхронную функцию для решения проблемы адского обратного вызова в express.js через метод записи es6, а koa не поставляется с таким количеством промежуточное программное обеспечение как экспресс.Для частного проекта это, несомненно, отлично.Еще одна особенность - уникальный контроль над промежуточным программным обеспечением koa, который является моделью koa onion, о которой все говорят.

Про луковую модель, наверное, резюмировал в двух пунктах

  1. Сохранение и передача контекста
  2. Управление промежуточным ПО и реализация следующего

clipboard.png

(картинка взята из интернета)

img

На приведенных выше двух картинках наглядно показан рабочий процесс луковой модели.Конечно, если реализуется конкретный принцип, то это не имеет отношения к данной статье, поэтому подробно описываться не будет.Заинтересованные студенты могут сами поискать в интернете .

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

На данный момент мы уже можем начать простой проект

  1. npm run tscскомпилировать ts-файл
  2. node app.jsСтартовый проект

Далее в браузере вводhttp://localhost:3000, чтобы просмотреть журнал доступа в консоли. Конечно, этого шага недостаточно, потому что отладка всегда сопровождает наш процесс разработки, поэтому нам нужна более удобная среда разработки.

локальная среда разработки

Локальная разработка использует 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

Определить модель базы данных

  1. Используйте 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.

  1. Используйте 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)

реализовать интерфейс

Ну, мы определили модель таблицы выше, и следующим шагом будет реализация захватывающего интерфейса. Мы реализуем его через простой интерфейс скрытой точки.Во-первых, нам нужно проанализировать логику, реализованную инструментом скрытой точки:

  1. Поскольку информация о скрытых точках не является реляционной, mongodb используется для хранения информации о скрытых точках.
  2. Поскольку это простой интерфейс записи, он должен быть более общим, то есть за исключением нескольких ключевых полей, то, что передает вызывающий абонент, сохраняется.
  3. Поведение точек захоронения незаметно для пользователей, поэтому информация обратной связи не предусмотрена, и если в точках захоронения есть ошибки, они будут обрабатываться внутри.

Что ж, разобравшись с функцией этой скрытой точки, давайте начнем реализовывать этот простой интерфейс:

// 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

Имитация данных

Используйте макеты для создания тестовых данных

журнал

Модуль журнала изначально планировалось сделать с помощью 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

Адрес складаЕсли вам это нравится, пожалуйста, дайте ему звезду

онлайн демо