基于Koa2+Typescript+MongoDB搭建简易官网后台(一)

597 阅读8分钟

Koa2+TypeScript+MongoDB 官网后台开发记录

前言

最近在给一个组织官网进行重构,考虑到用户量和开发便捷程度,于是采用了这套技术栈——Koa2+TypeScript+MongoDB,typescript 是未来,选择ts毋庸置疑,而为什么选择Koa2而不选择nest.js这种成熟框架呢?我想大概有两个原因:

  • 直接使用牛逼框架,那是有点没意思。

既然都用js了,那肯定是选择能够diy的彻底点的东西,所以我这里仅仅只是使用了Koa作为基底,连脚手架都没有使用,所以本文出现的一些东西多少会有很多偏差,不一定是最佳实践,也不一定规范。仅供参考

  • nest.js听别人说像java,我那就不用了

虽然可能他实际上并不像java Springboot,但是我是一个容易相信传言的人,所以暂时不接触啦

而数据库选择MongoDB而不选择Mysql当然也不是因为什么MongoDB快啥的,单纯只是想用一下这个数据库(之前没用过)

好的 就聊到这里,开始写代码吧~

依赖安装/项目搭建

首先 想要使用Koa进行搭建服务端,那肯定是要安装Koa的,但是除此之外也需要一些项目初始化,我本项目使用的包管理工具是yarn 所以均用yarn命令来写。

init

git init
yarn init

通过两个init 生成packag.json文件,然后运行下yarn install 就可以将项目搭起来了。

eslint/tsconfig

当然在这之前需要配置一下tsconfig以及eslint的基本工具,大概如下:

/// tsconfig.json
{
    "ts-node": {
        "files": true,
        "compilerOptions": {
            "paths": {
                "~/*": ["*"],
                "@/*": ["src/*"]
            }
        }
    },
    "compilerOptions": {
        "target": "es2016",
        "module": "commonjs",
        "allowJs": true,
        "checkJs": true,
        "declaration": true,
        "inlineSourceMap": true,
        "inlineSources": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "outDir": "./dist",
        "baseUrl": "src",
        "experimentalDecorators": true,
        "paths": {
            "~/*": ["*"],
            "@/*": ["src/*"]
        },
        "types": ["types"]
    },
    "include": ["src/**/*.ts", "src/**/*"],
    "exclude": ["node_modules"]
}
/// .eslintrc.js
module.exports = {
    env: {
        browser: true,
        es2021: true
    },
    extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
    parser: '@typescript-eslint/parser',
    parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module'
    },
    plugins: ['@typescript-eslint/eslint-plugin', 'prettier'],
    rules: {
        'no-undef': 'off',
        'no-console': 'off',
        'no-debugger': 'warn',
        '@typescript-eslint/no-explicit-any': 'off',
        'no-mutating-props': 'off',
        '@typescript-eslint/ban-types': 'off',
        'prettier/prettier': [
            'error',
            {
                printWidth: 140, //代码单行长度
                tabWidth: 2, //tab键缩进为2空格
                useTabs: true, //使用空格缩进
                singleQuote: true, //js单引号
                semi: false, //去分号
                trailingComma: 'none', //无尾逗号
                arrowParens: 'avoid', //箭头函数尽可能省略括号
                endOfLine: 'auto'
            }
        ]
    }
}

然后再安装一下一些必须的依赖,为了做到生产环境和开发环境隔离并且方便开发,这里需要使用到cross-env来做环境的处理

yarn add -D cross-env 

同时我们需要安装typescript 和eslint的依赖

yarn add -D eslint eslint-plugin-prettier typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier

安装热重载工具nodemon和ts-node用以直接调试程序

yarn add -D nodemon ts-node tsconfig-paths

好的 大概的依赖差不多装完了,然后就应该创建以下文件,如图所示:

image-20221103201443588.png

可以看到 两个.env开头的是我们的环境变量存放的文件,之后可以在这里修改全局的环境变量,而src存放源代码,dist则是输出的文件夹,这个在上面的tsconfig.json中已经写好了,所以在此不表。

运行脚本编写

这时候我们大概的文件目录搭好了 但是还没有写好package.json以及dev的命令。

在package.json下 的script加上dev的命令 写下如下命令行:

cross-env NODE_ENV=dev MODE=dev nodemon --watch src/**/* -e ts --delay 500ms --exec ts-node -r tsconfig-paths/register src/index.ts --files

image-20221103201734539.png

稍微解释一下:

  • 首先我们每次启动都使用cross-env来启动,他能够直接设置一个全局的环境变量以标识环境 例:NODE_ENV=dev MODE=dev
  • 使用nodemon 来检测src下的所有文件的更改 -e表示只检测xx后缀名的文件 这里只检测ts文件的更改 --delay 500ms 则很直白,--exec则是在监测到更改后执行什么脚本
  • 检测后这里会使用ts-node直接执行目录下的index.ts,免去了编译的过程,这里通过tsconfig-paths能够读取到tsconfig.json所设置的path的路径 并且通过--files,能够读取目录下定义的全局类型声明

到这里,我们应该在src下新建一个index.ts了 然后安装一下简单的代码依赖

yarn add koa
import Koa from 'koa'
const app = new Koa()
app.use(async (ctx)=>{
    ctx.body = "hello world"
})
app.listen(3000)
console.log(`your server is running on localhost:3000`)

到这里,我们应该就能够大概的跑起来这个项目了~,访问localhost:3000应该就能看到效果啦~

项目全局配置

之前我们配置好了.env.development和.production两个文件,那这两个文件就必然是存放一些全局的变量的文件,这里就来说说如何使用配置文件来进行全局变量的读取。

dotenv

首先介绍一个库 dotenv,这个库能够有效的,很好的帮助你管理配置文件以及全局环境的变量。

yarn add -D dotenv

安装好后 如下创建好目录(先忽略common):

image-20221103202825135.png

写下如下代码:

import dotEnv from 'dotenv'const path: NormalObject = {
    default: '.env.development',
    dev: '.env.development',
    prod: '.env.production'
}
const NODE_ENV = process.env.NODE_ENV || 'default'
​
dotEnv.config({ path: path[NODE_ENV] })
​
export default process.env

(NormalObject)是一个简单的[key:string]:string 的obj的类型,你们可以自行创建

可以看到 dotEnv帮助我们,直接将相对应路径的环境配置文件挂载到了process.env下,而这个环境配置的判断和读取正是依赖了上面所说的cross-env。通过在命令中指定NODE_ENV,我们的程序才能得以判断使用哪个配置文件。

使用

切回到index.ts,使用一下这个刚配好的环境配置文件。

import config from './config/config.default'
import Koa from 'koa'
const app = new Koa()
app.use(async (ctx)=>{
    ctx.body = "hello world"
})
app.listen(config.APP_PORT)
console.log(`your server is running on localhost:${config.APP_PORT}`)

我这里已经在.env.development下配好了APP_PORT

image-20221103203255969.png

再启动一下,看看是否生效了~

路由配置 装饰器

首先如下创建好目录

image-20221103203504767.png

这里所做的事情都是为了我们使用注解来代替router的频繁注册和use,如果你使用过java Springboot,那么就会对我这里的操作表示了然于胸。

为了能够让ts支持装饰器,需要我们在tsconfig.json中配置一个选项,当然在上面提供的tsconfig.json中我已经设置好了该选项:

"experimentalDecorators": true,

装饰器方法编写

首先我们需要安装一下koa-router

yarn add koa-router

类型定义

同时我也需要介绍一下我的类型定义和枚举定义。

在使用ts中,我会定义一些固定好的数据类型和枚举来规范我的项目代码。

image-20221103203920052.png

当然您可以自行定义,我在之后的教程中主要用到了几个枚举和类型定义如下:

export const enum REQUEST_METHOD {
    GET = 'get',
    POST = 'post',
    DELETE = 'delete',
    PUT = 'put',
    PATCH = 'patch'
}
// 枚举 enum.ts
declare global {
    interface NormalObject {
        [key: string]: string
    }
​
    interface ControllerRouter {
        url: string
        method: string
        middleware?: any
        handler?: () => void | any
        constructor?: Function | any
    }
}
export {}
// 全局类型声明 global.ts

工具方法

首先在decorator.ts中,编写如下的代码:

import { REQUEST_METHOD } from '~/types/enum'export const controllers: Array<ControllerRouter> = []
​
/**
 * Controller 注解方法 可以解析一个path 直接作用于API上
 * 使用方法:在类上直接使用即可 @Controller('/user')
 * @param path 类请求前缀
 * @returns
 */
export const Controller = (path = '') => {
    return function (target: any) {
        target.prefix = path
    }
}
​
/**
 * 用于方法上的注解
 * @param param0 ControllerRouter 对象
 * @returns
 */
export const RequestMapping = ({ url = '', method = '', middleware = [] }: ControllerRouter) => {
    return function (target: any, name: string) {
        let path = ''
        // 判断有没有定义url
        if (!url) {
            path = `/${name}` // 取方法名作为路径
        } else {
            path = url // 自己定义的url
        }
​
        const item: ControllerRouter = {
            url: path,
            method: method || REQUEST_METHOD.GET,
            middleware: middleware,
            handler: target[name],
            constructor: target.constructor
        }
​
        controllers.push(item)
    }
}

稍作解释:

  • controllers提供了一个数组,这个数组在我们定义好路由后,会全部存放在此处,并且统一在router.ts中进行注册
  • Controller是一个装饰器,主要会使用在Controller的类上
  • RequestMapping是一个装饰器,主要用在类方法中,需要写好请求的方法和url路径,如果不写会默认使用方法名作为url请求路径

来到index.ts中,我们需要导出一个方法供给koa实例使用,这个方法需求的参数则是一个koa实例以及一个koa-router实例

import { controllers } from './decorator'export default (app: any, router: any) => {
    controllers.forEach((item: any) => {
        const prefix = item.constructor.prefix
        let url = item.url
        if (prefix) url = `${prefix}${url}` // 组合真正链接
        router[item.method](url, ...item.middleware, item.handler) // 创建路由
    })
    app.use(router.routes()).use(router.allowedMethods()) // 路由装箱
}

定义router

这个时候我们再创建一个文件夹如下

image-20221103204807416.png

我们需要导出一个koa-router的实例出去给koa实例使用(这都是为了规范化项目的代码!)

// router.ts
import KoaRouter from 'koa-router'
import config from '~/config/config.default'const router = new KoaRouter()
​
router.prefix(config.MMGC_PREFIX || '/') // 定义你的api服务全局前缀export default router

app统一使用

好的 统一来到index.ts下,前期的准备都做好了,准备好装箱了。

import Koa from 'koa'
import config from './config/config.default'
import router from './router/router'
import initRoutes from './common/decorator/index'const app = new Koa()
​
initRoutes(app, router)
​
app.listen(config.APP_PORT)
​
console.log(`your server is running on ${config.HOST_NAME}:${config.APP_PORT}`)
console.log(`click to view  http://${config.HOST_NAME}:${config.APP_PORT}${config.MMGC_PREFIX}`)

好的 此时的我们拥有了使用装饰器来进行代码编写的能力了,这个时候可以编写controller来写接口了~

如果你不了解controller的概念,你可以去稍微百度一下。

测试/controller编写

创建以下文件夹:

image-20221103205528238.png

来到user.controller.ts 我们写点测试代码,并且将这个controller导出

import { Context } from 'koa'
import { REQUEST_METHOD } from '~/types/enum'
import { Controller, RequestMapping } from '~/common/decorator/decorator'@Controller('/user')
export default class UserController {
    @RequestMapping({ url: '/login', method: REQUEST_METHOD.GET })
    async login(ctx: Context) {
        const res = {
            username: '你好',
            password: '谢谢'
        }
        ctx.body = res
    }
}

来到index.ts下

import UserController from './user.controller'
export { UserController }

最后来到src/common/decorator/index.ts下

增加这一行代码

export * from '~/controller/index'

好的 目前只需要访问你所设置的路由地址应该就能够出现和我一样的结果啦~

image-20221103205939130.png

使用ApiPost/fox/postman 管理接口

首先声明

我本人只是恰巧在用apipost,并且觉得他的界面还可以,如果你喜欢可以用任意一款api管理工具来管理你写的接口。这里仅阐述我使用apipost的使用体验~

apipost 创建团队 项目

image-20221103210926244.png

进入apipost 我们会需要注册账号并需要创建一个团队,在团队中,我们就可以去创建相应的项目,这里我是创建了一个项目如图第一个

环境配置 接口管理 文档编写

image-20221103211041495.png

在界面中,我们所需要做的最多的就是配置好测试环境和新建接口文件夹并且写好接口参数和文档,这有利于我们规范的开发接口和进行调试。

image-20221103211137330.png

你可以在环境中填写好你的请求url地址,一些全局变量或者参数等。

image-20221103211252761.png

同时 写好接口文档,和参数的类型配置,能够有效的进行mock和测试

image-20221103211351685.png

好的 今天就写到这里,在之后我会更新第二篇的 敬请期待~