【知识学习】Vue3 + Vite + Koa + TS 项目

Vite + TS + Vue3 前端工程初始化配置

使用 vite 初始化项目

项目中使用的的是 vite 4.x 的版本

使用 yarn 执行

yarn create vite

输入项目名称,选择 VueTypeScrip

image.png

按照提示执行以下命令即可

 cd dandgangshucheng
 yarn
 yarn dev

启动后的初始页面如下

image.png

解决 main.ts 报错

打开项目,发现 main.ts 有报错

image.png

回想起来以前的工程里面有一个 shims-vue.d.ts 是用来解决这个的,但是现在的工程里面没有这个文件了, 取而代之的是一个 vite-env.d.ts 的文件,但是这个文件里并没有 shims-vue.d.ts 的内容。所以我需要手动添加一下。

// vite-env.d.ts

declare module "*.vue" {
  import type { DefineComponent } from "vue";






  const vueComponent: DefineComponent<{}, {}, any>;






  export default vueComponent;
}

这样就不会报错,并且有类型提示了。

image.png

按理来说 Vite4.x 的版本不应该出现这种问题,可能是我哪里的配置出了问题,有了解的小伙伴可以在评论区指教一二,在此谢过了。

后来发现安装了 TypeScript Vue Plugin (Volar) 这个 vscode 的插件就不会报错 , 但是鼠标移到 App 上并没有出现类型提示。

image.png

自动打开浏览器

如果需要在开发环境启动后 , 自动打开浏览器 , 需要添加 --open

// package.json

  "scripts": {

    "dev": "vite --open",
  },


环境变量

使用 import.meta.env 来获取环境变量

默认的环境变量有五个

  • BASE_URL: 公共基础路径
  • DEV: 当前环境是否为开发环境
  • MODE: 应用运行的模式 , 开发环境模式(development) , 生产环境模式(production)
  • PROD: 当前环境是否为生产环境
  • SSR: 是否为服务端渲染

通过在根目录添加以下文件来自定义环境变量

  • .env.production: 表示只有在生产环境下才会被加载的文件
  • .env.development: 表示只有在开发环境下才会被加载的文件
  • .env: 表示备选环境文件 , 在任何环境下都会被加载

只有以 VITE_ 为前缀的变量才可以在程序中使用

如果需要在 TS 中获取类型提示 , 要在vite-env.d.ts文件中扩展类型

// vite-env.d.ts

/// <reference types="vite/client" />



interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  // 更多环境变量...
}

在配置文件中使用环境变量

① 首先把默认的配置文件从 对象 的形式改为 函数 的形式

这是默认的配置文件,可以看到默认导出的是一个对象,这样的弊端就是不够灵活,不可以在代码中输入 console.log ,也没有办法获取到当前的模式(MODE)。

// vite.config.ts



import { defineConfig } from "vite";


import vue from "@vitejs/plugin-vue";








export default defineConfig({
  plugins: [vue()],
})

修改成下面这样的函数形式,就可以从回调参数中获取到当前运行的模式,然后根据模式生成不同的配置对象。

// vite.config.ts



import { defineConfig } from "vite";


import vue from "@vitejs/plugin-vue";








export default defineConfig(({ mode }) => {

  console.log("?? ~ mode", mode);
  return {

    plugins: [vue()],
  };
});

运行一下程序,可以看到控制台已经输出了当前的模式 development ,获取到这个变量,后续就可以灵活的进行配置了。

image.png

② 根据模式获取到对应的环境变量文件

根据回调参数中的 mode 属性,拼接上本地文件的前缀名,就可以拿到整个环境变量文件的名称了。

// vite.config.ts



import { defineConfig } from "vite";


import vue from "@vitejs/plugin-vue";








export default defineConfig(({ mode }) => {

  const envFilePrefix: string = ".env.";
  const curEnvFileName = `${envFilePrefix}${mode}`;
  console.log("?? ~ curEnvFileName", curEnvFileName);
  return {
    plugins: [vue()],
  };
});

运行一下程序,可以看到控制台已经输出了当前模式对应的文件 .env.development ,获取到这个文件名后,就可以读取里面的环境变量了。

image.png

③ 根据文件名读取环境变量

实现这个功能,需要安装第三方库 dotenv

import { defineConfig } from "vite";

import vue from "@vitejs/plugin-vue";

import fs from "fs";

import dotenv, { DotenvParseOutput } from "dotenv";







export default defineConfig(({ mode }) => {
  // 定义文件前缀
  const envFilePrefix: string = ".env.";
  // 获取当前模式下对应的环境变量文件
  const curEnvFileName = `${envFilePrefix}${mode}`;
  // 读取环境变量文件
  const envData = fs.readFileSync(curEnvFileName);
  // 把读取到的结果解析成对象
  const envMap: DotenvParseOutput = dotenv.parse(envData);
  console.log("?? ~ envMap", envMap);
  return {
    plugins: [vue()],
  };
});

另外,需要对 dotenv 的类型进行增强,不然我们获取不到自己定义的类型

// src\types\dotenv.d.ts
import "dotenv";
declare module "dotenv" {
  export interface DotenvParseOutput {
    VITE_HOST: string;
    VITE_PORT: number;
    VITE_BASE_URL: string;
    VITE_PROXY_DOMAIN: string;
  }
}


运行程序,可以看到环境变量就以对象的形式输出了

image.png

④ 根据环境变量配置请求代理

在环境变量文件 .env.development 里面配置好相关的变量

// .env.development
VITE_HOST = '127.0.0.1'
VITE_PORT = 3000
VITE_BASE_URL = '/dang'
VITE_PROXY_DOMAIN = 'http://192.168.2.6:5003/'

修改配置文件,到这里就完成了在配置文件中使用环境变量的全部步骤,后面其它的配置就可以灵活的在环境变量文件中进行添加和修改了。

import { defineConfig } from "vite";

import vue from "@vitejs/plugin-vue";

import fs from "fs";

import dotenv, { DotenvParseOutput } from "dotenv";

export default defineConfig(({ mode, command }) => {
  // 获取当前的模式
  console.log("?? ~ 打包编译阶段还是编码阶段", command);
  console.log("?? ~ 当前在什么环境运行项目", mode);
  // 定义文件前缀
  const envFilePrefix: string = ".env.";
  // 获取当前模式下对应的环境变量文件
  const curEnvFileName = `${envFilePrefix}${mode}`;
  // 读取环境变量文件
  const envData = fs.readFileSync(curEnvFileName);
  // 把读取到的结果解析成对象
  const envMap: DotenvParseOutput = dotenv.parse(envData);
  return {
    plugins: [vue()],
    server: {
      host: envMap.VITE_HOST,
      port: envMap.VITE_PORT,
      proxy: {
        [envMap.VITE_BASE_URL]: {
          target: envMap.VITE_PROXY_DOMAIN,
        },

      },
    },
  };
});

使用 Vite 自带的 loadEnv 方法

其实 Vite 内置一个 loadEnv 方法, 也可以实现同样的功能,但是目前对 TS 的支持不太友好,返回的是一个 Record<string,string> 类型,不能获得代码的自动提示。

import { defineConfig, loadEnv } from "vite";


export default defineConfig(({ command, mode }) => {
  // 根据当前工作目录中的 `mode` 加载 .env 文件
  // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
  const env = loadEnv(mode, process.cwd(), "");
  return {

    // vite 配置
    define: {
      __APP_ENV__: env.APP_ENV,
    },
  };
});

Vite + TS + Vue3 前端项目准备工作

铃兰

1098373.jpg

动态管理图片的实现

① 新建 imgUtil 工具类

src/utils 目录下新建一个工具类 imgUtil.ts , 然后调用 Vite 内置的 glob 方法,
获取 assets 目录下的所有图片

src\utils\imgUtil.ts
export class ImgUtil {
  static loadAllImg() {
    const imgMap = import.meta.glob("../assets/**/*.png", { eager: true });
    console.log("?? ~ imgUtil ~ imgMap", imgMap);
  }
}

结果是这样的

image.png

②缓存图片

需要安装第三方库 good-storage

yarn add good-storage -S

编码实现

// src\utils\imgUtil.ts
/**
 * 动态管理图片
 */
import goodStorage from "good-storage";






export class ImgUtil {
  // 声明图片列表
  static imgList: Record<string, string> = {};




  // 缓存图片列表
  static storageImgList() {
    this.imgList = goodStorage.get("imgList") || {};
    if (this.isEmpty()) {
      this.loadAllImg();
      goodStorage.set("imgList", this.imgList);
    }
  }


  // 判断图片列表是否为空
  static isEmpty() {
    return !Object.getOwnPropertyNames(this.imgList).length;
  }

  // 获取图片
  static getImg(imgName: string) {
    return ImgUtil.imgList[imgName];
  }


  // 加载全部图片
  static loadAllImg() {
    // 获取项目中所有图片
    const imgMap: Record<string, any> = import.meta.glob("../assets/**/*.png", {
      eager: true,
    });


    // 声明绝对路径
    let absolutePath: string = "";
    // 声明图片名称
    let imgName: string = "";
    // 遍历所有图片
    for (const path in imgMap) {
      if (Object.prototype.hasOwnProperty.call(imgMap, path)) {
        // 获取图片的相对路径
        absolutePath = imgMap[path].default;
        if (absolutePath) {
          // 获取图片的名称
          imgName = absolutePath.substring(absolutePath.lastIndexOf("/") + 1);
          // 图片列表赋值 key:文件名 value:文件的相对路径
          this.imgList[imgName] = absolutePath;
        }
      }
    }
  }
}

③ 在 main.ts 中调用

// main.ts
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import { ImgUtil } from "./utils/imgUtil";






// 缓存图片
ImgUtil.storageImgList();




createApp(App).mount("#app");

④ 组件中使用

// App.vue
<script setup lang="ts">
import { ImgUtil } from './utils/imgUtil'
</script>






<template>
  <div>
  <img :src="ImgUtil.getImg('浏览器1.png')" />
  </div>
</template>

动态管理图片的优劣

好处:

  • 缓存中只需要保存一个图片名,存取图片不需要管理图片路径问题
  • 前端从缓存中存取图片,无需考虑图片路径问题
  • 图片路径修改了,无需改代码,可维护性大大提高
  • 组件显示图片更方便
  • 图片可以分类管理

坏处:

  • 需要保持图片名称唯一,增加心智负担
  • 更新图片时,不能立刻刷新缓存,增加开发成本

好处是可采用的,坏处是可优化的,还需要开动脑筋实现一个完美的解决方案,但是目前来说本人认为并没有很好用

ESLint

① 安装依赖

yarn add eslint -D

② 初始化 eslint

yarn eslint --init

image.png

命令执行过后的默认文件如下

// .eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    commonjs: true,
    es2021: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'standard-with-typescript'
  ],
  overrides: [
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },

  plugins: [
    'vue'
  ],
  rules: {
  }
}

刚初始化完成后,代码是有报错的,需要添加

  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser'
  },


③ 添加自定义规则

添加了几个个人的代码风格,最终的文件内容如下

module.exports = {
  // eslintrc.js 文件所在的目录为 root 目录
  // eslint 规则将对这个目录以及该目录下的所有文件起作用
  root: true,
  // 让 Vue3.2 中的这些全局函数能正常使用
  globals: {
    defineProps: 'readonly',
    defineExpose: 'readonly',
    defineEmits: 'readonly',
    withDefaults: 'readonly',
  },
  env: {
    browser: true,
    commonjs: true,
    es2021: true,
  },

  // 继承别人写好的规则
  extends: ['plugin:vue/vue3-essential', 'standard-with-typescript'],
  overrides: [],
  parser: 'vue-eslint-parser',
  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser',
  },
  // 插件的作用是对规则进行补充
  plugins: ['vue'],
  // 自定义的规则, 重写继承的规则
  rules: {
    // 关闭函数名后面必须有空格的验证
    'space-before-function-paren': 0,
    // 关闭强制不变的变量使用 const, 因为自动格式化 有时候会把 let 变成 const
    'perfer-const': 0,
    // 允许行尾分号
    semi: 0,
    // 允许尾后逗号
    'comma-dangle': 0,
  },
};


prittier

prettier 是按照 eslint 的规范进行格式化的工具,如果冲突则 prettier 优先级高

安装 vscodeprettier 插件 ,无需在项目中安装 prettier

image.png

然后找到设置中的 prettier 插件 ,可以进行傻瓜式配置

image.png

在可以在项目根目录下新建 .prettierrc 文件,优先级高于手动配置的内容

vscode 设置自动格式化

在项目根目录下新建 .vscode/setting.json 文件,写入以下内容,即可在保存代码的时候自动按照 eslintprettier 的规范进行代码格式化

// 需要 vscode 安装 Prettier - Code formatter 扩展
{

  // 控制编辑器是否自动格式化粘贴的内容。格式化程序必须可用,并且能针对文档中的某一范围进行格式化
  "editor.formatOnPaste": true,
  // 在保存时格式化文件。格式化程序必须可用,延迟后文件不能保存,并且编辑器不能关闭。
  "editor.formatOnSave": true,
  // 控制编辑器在键入一行后是否自动格式化该行。
  "editor.formatOnType": false,
  // 当编辑器失去焦点时,将自动保存未保存的编辑器。
  "files.autoSave": "onFocusChange",
  //在一定数量的字符后显示标尺
  "editor.rulers": [100],
  // 定义一个默认格式化程序, 该格式化程序优先于所有其他格式化程序设置。必须是提供格式化程序的扩展的标识符。
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  // 忽略单词
  "cSpell.words": ["vite"]
}

添加路径别名

tsconfig.json 添加 paths 字段 。

// tsconfig.json
{

  "compilerOptions": {
    // 设置别名
    "paths": {
      "@/*": ["src/*"]
    }
  },
}




vite.config.ts 添加 resolve.alias 字段

// vite.config.ts



resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src'),
  },
},

TS + Koa + MySql 服务端项目准备工作

泥岩

1195802.png

安装 MySql

?? 本地安装最新版 MySQL 数据库

服务端项目准备工作

① 初始化项目

yarn init -y
yarn add
@types/koa @types/koa-json @types/koa-router
koa koa-body koa-json koa-router 
nodemon ts-node typescript

② 按照第二篇的内容设置好代码规范

③ 配置程序热启动

// package.json

  "scripts": {

    "dev": "nodemon --watch src/ -e ts --exec ts-node ./src/app.ts"
  },


写第一个路由

① 启动 koa 项目

// src\app.ts
import koa from 'koa';

import json from 'koa-json';

import Router from 'koa-router';


import body from 'koa-body';







const app = new koa();
const router = new Router();




// 定义路由前缀 , 一级路径
router.prefix('/dang');

app.use(json());
app.use(body());


router.get('/test', async (ctx: koa.Context, next: koa.Next) => {
  ctx.body = '第一个请求 test';
});


app.use(router.routes());


app.listen(3002);



console.log('程序运行在3002端口');

② 路由拆分模块

新建 src\router\user.ts

// src\router\user.ts
import { Context } from 'koa';
import Router from 'koa-router';
const router = new Router();






// 定义模块前缀
router.prefix('/usermodule');



// get 请求
router.get('/findUserInfo/:username', async (ctx: Context) => {
  const { username } = ctx.params;
  ctx.body = `欢迎${username}`;
});




// post 请求
router.post('/addUser', async (ctx: Context) => {
  const { username } = ctx.request.body;
  ctx.body = `欢迎${username}`;
});





export default router;

app.js 中追加以下代码

// app.ts
import koa from 'koa';

import json from 'koa-json';

import Router from 'koa-router';


import body from 'koa-body';

import userRouter from './router/user';




const app = new koa();
const router = new Router();




// 定义路由前缀 , 一级路径
router.prefix('/dang');


app.use(json());
app.use(body());



// 追加模块路由
router.use(userRouter.routes(), userRouter.allowedMethods());
app.use(router.routes());




app.listen(3002);

console.log('程序运行在3002端口');

优化: 路由自动加载

每添加一个路由模块, 都需要在 app.js 里面导入,然后注册,这样模块多了以后 app.js 就会变得非常臃肿,并且不好维护,所以最好能利用代码自动加载路由。

新建一个文件用来动态加载路由

// src\common\RouterLoader.ts
import path from 'path';
import fs from 'fs';
import Router from 'koa-router';


import Koa from 'koa';
import json from 'koa-json';
import body from 'koa-body';
/**
 * @description: 动态加载路由
 */
class RouterLoader {
  app!: Koa;
  static routerLoader: RouterLoader = new RouterLoader();
  init(app: Koa) {
    this.app = app;
    const router = this.loadAllRouter();
    this.app.use(router.routes());
    this.listen();
  }

  /**
   * @description: 动载加载路由模块
   * @return {*}
   */
  loadAllRouter() {
    // 获取一级路由
    const rootRouter = this.getRootRouter();
    // 获取所有路由文件的绝对路径
    const filePaths = this.getAbsoluteFilePaths();
    // 加载所有的二级路由到一级路由
    filePaths.forEach((filePath) => {
      const module = require(filePath);
      if (this.isRouter(module)) {
        rootRouter.use(module.routes(), module.allowedMethods());
      }
    });

    return rootRouter;
  }

  /**
   * @description: 获取一级路由
   * @return {*}
   */
  getRootRouter() {
    const rootRouter = new Router();
    // 定义路由前缀 , 一级路径
    rootRouter.prefix('/dang');
    this.app.use(json());
    this.app.use(body());
    return rootRouter;
  }
  /**
   * @description: 判断引入的模块是否是路由模块
   */
  isRouter(data: any): data is Router {
    return data instanceof Router;
  }

  /**
   * @description: 获取目录下所有的文件名称
   * @param {string} dir 文件目录
   * @return {string[]} 包含目录下所有文件的名称的数组
   */
  getFileNames(dir: string) {
    return fs.readdirSync(dir);
  }

  /**
   * @description: 获取所有文件的绝对路径
   * @return {string[]} 包含获取所有文件的绝对路径的数组
   */
  getAbsoluteFilePaths() {
    // 获取路由文件所在目录
    const dir = path.join(process.cwd(), '/src/router');
    // 获取所有的文件名称
    const allFiles = this.getFileNames(dir);
    // 拼接所有文件的绝对路径
    const allFullFilePaths: string[] = [];
    allFiles.forEach((file) => {
      allFullFilePaths.push(`${dir}${path.sep}${file}`);
    });
    return allFullFilePaths;
  }

  listen() {
    this.app.listen(3002);
    console.log('程序运行在3002端口');
  }
}

export default RouterLoader.routerLoader;

这样重构以后 , app.js 里面就可以清理一下了, 添加路由的话也不用修改文件的内容了。

import koa from 'koa';

import routerLoader from './common/RouterLoader';




const app = new koa();
// 调用动态加载路由
routerLoader.init(app);

全局异常处理

在路由中使用 try catch 是非常普遍的一件事,但是写多个同样的代码就很不友好,所以既然 try catch 的格式是固定的,可以封装一个全局的异常处理中间件。

// src\common\GlobalException.ts
import type { Context, Next } from 'koa';



/**
 * @description: 全局异常处理中间件
 */
const globalException = async (ctx: Context, next: Next) => {
  try {
    await next();
  } catch (error) {
    const err = error as Error;
    ctx.body = `服务器错误${err.message}`;
  }
};


export default globalException;

app.js 中注册

import koa from 'koa';

import routerLoader from './common/RouterLoader';

import globalException from './common/GlobalException';






const app = new koa();






// 注册全局异常中间件
app.use(globalException);




// 调用动态加载路由
routerLoader.init(app);

全局响应处理

精简封装响应成功和响应失败

enum Code {
  SUCCESS = 200,
  SERVER_ERROR = 500,
}






class ResResult {
  static success(data: any = undefined, msg: any = '') {
    const code: Code = Code.SUCCESS;
    return { data, msg, code };
  }
  static fail(msg: any = '') {
    const code: Code = Code.SERVER_ERROR;
    return { undefined, msg, code };
  }
}



export const { success, fail } = ResResult;

ORM 框架 Sequelize 的使用

艾雅法拉

1145273.png

封装数据库配置类

这里主要学习到函数重载的知识,以及在 TS 中判断变量是否符合类型的写法。

// src\config\db.ts
interface DbConf {
  host: string;
  user: string;
  password: string;
  port: number;
  database: string;
}





interface EnvConf {
  dev: DbConf;
  prod: DbConf;
}




class Conf {
  static conf: Conf = new Conf();
  env!: keyof EnvConf;
  envConf!: EnvConf;
  constructor() {
    this.env = process.env.NODE_ENV === 'prod' ? 'prod' : 'dev';
    this.initConf();
  }
  // 初始化配置
  initConf() {
    this.envConf = {
      dev: {
        host: 'localhost',
        user: 'admin',
        password: '123456',
        port: 3306,
        database: 'dangdang',
      },
      prod: {
        host: 'www.xxx.com',
        user: 'root',
        password: '123456',
        port: 3306,
        database: 'dangdang',
      },
    };
  }
  // 函数重载实现获取 db 连接配置
  getConf(): DbConf;
  getConf(key: string): DbConf;
  getConf(key: any = ''): any {
    if (this.isDbConfKeys(key) && key.length > 0) {
      return this.envConf[this.env][key];
    } else {
      return this.envConf[this.env];
    }
  }
  // 判断是不是符合要求的类型
  isDbConfKeys(key: any): key is keyof DbConf {
    return ['host', 'user', 'password', 'database', 'port'].includes(key);
  }

}

export default Conf.conf;

认识 ORM 框架 Sequelize

① 什么是 ORM

ORM 就是为了避免直接编写 sql 语句带来的繁琐,而把关系型数据表数据直接映射为 js 对象进行查询,同时也能把 js 对象 转换为关系型数据表的数据进行增加,修改或删除

① 什么是 Sequelize

Sequelize 是一个基于 promiseNode.js ORM,支持 MySQL

③ Sequelize 主要特点

支持事务。支持一对一,一对多,多对一,多对多,关联表的映射。

Sequelize 的使用

主要学习 Sequelize 的查询语法, 以及 Daomodel 的封装。

① 连接 mysql 数据库以及 BaseDao 封装

// src\modules\BaseDao.ts

/*
 * @Author: 一尾流莺
 * @Description:连接数据库 Dao 层
 * @Date: 2023-02-21 17:14:07
 * @LastEditTime: 2023-02-22 15:26:27
 * @FilePath: \dang-be\src\modules\BaseDao.ts
 */
import dbConfig from '../config/db';
import { type Dialect } from 'sequelize';
import { Sequelize } from 'sequelize-typescript';
import path from 'path';


class BaseDao {
  static baseDao: BaseDao = new BaseDao();
  sequelize!: Sequelize;
  constructor() {
    this.initSeqConf('mysql');
  }





  initSeqConf(dialect: Dialect) {
    const { host, user, password, database, port } = dbConfig.getConfig();
    this.sequelize = new Sequelize(database, user, password, {
      host,
      port,
      dialect, // 表示是何种数据库
      define: {
        timestamps: false, // true 表示给模型加上时间戳属性 (createAt,updateAt),false 标识不带时间戳属性
        freezeTableName: true, // true 标识使用给定的表名, false 标识模型后名加s作为表名
      },
    });
  }

  addModels() {
    const modelPath = path.join(process.cwd(), '/src/modules/decorModel');
    this.sequelize.addModels([modelPath]);
  }

}


const baseDao = BaseDao.baseDao;
baseDao.addModels();
export const { sequelize } = baseDao;

② 用户模块 model 层

// src\modules\userinfo\model\index.ts
import { sequelize } from '../../BaseDao';
import { DataTypes } from 'sequelize';






class Userinfo {
  static createModel() {
    const model = sequelize.define(
      'userinfo',
      {
        userid: {
          type: DataTypes.INTEGER,
          field: 'userid', // 不写默认使用属性名
          primaryKey: true,
          autoIncrement: true,
        },
        username: {
          type: DataTypes.STRING(30),
          field: 'username',
          allowNull: false,
        },
        pwd: {
          type: DataTypes.STRING(30),
          field: 'pwd',
          allowNull: false,
        },

        address: {
          type: DataTypes.STRING(50),
          field: 'address',
          allowNull: true,
        },
        valid: {
          type: DataTypes.TINYINT,
          field: 'valid',
          allowNull: true,
        },
      },
      // {
      //   timestamps: false, // true 表示给模型加上时间戳属性 (createAt,updateAt),false 标识不带时间戳属性
      //   freezeTableName: true, // true 标识使用给定的表名, false 标识模型后名加s作为表名
      // },
    );
    // model.sync({ force: false, alter: true }).catch(() => {}); // force true 强制同步数据表
    return model;
  }
}
export const model = Userinfo.createModel();

③ 用户模块 Dao 层

// src\modules\userinfo\dao\index.ts


import { Sequelize } from 'sequelize-typescript';
import { Op } from 'sequelize';
import { model } from '../model';






export interface Userinfo {
  userid: number;
  username: string;
  psw: string;
  address: string;
  valid: number;
}

class UserDao {
  static userDao: UserDao = new UserDao();
  // 添加数据
  async addUser(userinfo: Userinfo) {
    // 不用 any 会报错, 不会改
    return await model.create(userinfo as any);
  }


  // 查询所有数据
  async findAllUser() {
    return await model.findAll({ raw: true });
  }

  // 只查询部分属性
  async findByProps() {
    return await model.findAll({
      raw: true,
      attributes: ['username', 'pwd'],
    });
  }

  // 精确查询
  async findByUsernameAndPsw(username: string, psw: string) {
    return await model.findOne({
      raw: true,
      where: {
        [Op.or]: [
          {
            username,
          },
          {
            psw,
          },
        ],
      },
    });
  }

  // 模糊查询
  async findByLike(key: string) {
    const searchKey = `%${key}%`;
    return await model.findAll({
      raw: true,
      where: {
        username: {
          [Op.like]: searchKey,
        },
      },
    });
  }


  // 模糊查询 or
  async findByUsmAndAddr() {
    return await model.findAll({
      raw: true,
      where: {
        [Op.or]: [
          {
            username: {
              [Op.like]: '%王',
            },
          },
          {
            address: '武汉',
          },
        ],
      },
    });
  }

  // 聚合查询
  async countUserInfo() {
    return await model.findAll({
      raw: true,
      group: 'address',
      attributes: ['address', [Sequelize.fn('count', Sequelize.col('valid')), '总人数']],
      where: {
        valid: 1,
      },
    });
  }

  // 分页查询
  async findUserWithPager(limit: number, offset: number) {
    return await model.findAll({
      raw: true,
      limit: 3,
      offset: 5,
    });
  }
}

export const userDao = UserDao.userDao;

④ 用户模块路由

import { type Context } from 'koa';
import Router from 'koa-router';
import { success } from '@/common/ResResult';
import { userDao, type Userinfo } from '@/modules/userinfo/dao';






const router = new Router();




// 定义模块前缀
router.prefix('/usermodule');




// 根据用户名密码单个查询用户信息
router.get('/findUserinfo/:username/:psw', async (ctx: Context) => {
  const { username } = ctx.params;
  ctx.body = success(`欢迎${username}`);
});



// 添加用户
router.post('/addUser', async (ctx: Context) => {
  const userinfo: Userinfo = ctx.request.body;
  const dbUserinfo = await userDao.addUser(userinfo);
  ctx.body = success(dbUserinfo);
});



// 查询全部用户
router.get('/findAllUser', async (ctx: Context) => {
  const allUser = await userDao.findAllUser();
  ctx.body = success(allUser);
});


// 只查看部分属性
router.get('/findByProps', async (ctx: Context) => {
  const allUser = await userDao.findByProps();
  ctx.body = success(allUser);
});

// 用户名密码精确查询
router.get('/findByUsernameAndPsw/:username/:psw', async (ctx: Context) => {
  const { username, psw } = ctx.params;
  const allUser = await userDao.findByUsernameAndPsw(username, psw);
  ctx.body = success(allUser);
});

// 模糊查询
router.get('/findByLike/:key', async (ctx: Context) => {
  const { key } = ctx.params;
  const allUser = await userDao.findByLike(key);
  ctx.body = success(allUser);
});

// 模糊查询 or
router.get('/findByUsmAndAddr', async (ctx: Context) => {
  const allUser = await userDao.findByUsmAndAddr();
  ctx.body = success(allUser);
});

// 聚合查询
router.get('/countUserInfo', async (ctx: Context) => {
  const allUser = await userDao.countUserInfo();
  ctx.body = success(allUser);
});

// 分页查询
router.get('/findUserWithPager/:pageNo/:pageSize', async (ctx: Context) => {
  const { pageNo, pageSize } = ctx.params;
  const offset = (pageNo - 1) * pageSize;
  const allUser = await userDao.findUserWithPager(offset, pageSize);
  ctx.body = success(allUser);
});
export default router;

module.exports = router;

图书分类级联查询

数据库连接池

① 为什么要用数据库连接池

当一个网站并发量过高,假设网站一天上万的访问量,后端服务器就会和数据库服务器创建上万次连接,关闭上万次连接。而数据库创建连接非常消耗时间,关闭连接也消耗时间,严重的浪费数据库的资源,并且极易造成数据库服务器内存溢出、宕机。

② 什么是连接池

在数据库连接池是负责创建,分配,释放数据库连接的对象,在项目启动时会创建一定数量的数据库连接放到连接池对象中,并允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。

Sequelize 底层: 连接池是一个由 ConnectionManager 类管理的 Pool 类的对象,通过 Pool 类对象来管理和共享多个连接对象。

③ 连接池如何工作

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出个已建立的空闲连接对象。使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立,断开都有连接池自身来管理。

④ 代码实现

// src\modules\BaseDao.ts

this.sequelize = new Sequelize(database, user, password, {
  host,
  port,
  dialect, // 表示是何种数据库
  define: {
    timestamps: false, // true 表示给模型加上时间戳属性 (createAt,updateAt),false 标识不带时间戳属性
    freezeTableName: true, // true 标识使用给定的表名, false 标识模型后名加s作为表名
  },
  // 数据库连接池
  pool: {
    // 最大连接对象的个数
    max: 10,
    // 最小连接数
    min: 5,
    // 连接池中空闲连接的最大空闲时间,单位为毫秒
    idle: 10000,
    // 表示一条sql查询在获取连接资源之前的最长等待时间,单位为毫秒
    acquire: 1000,
  },
});

在以下场景下,就需要建立 service 类

  • 当一个业务功能需要执行一个 dao 类中的多个方法才能完成时。
  • 当一个业务功能需要执行多个 dao 中的方法才能完成时
  • 当一个业务功能需要对 dao 类取出来的数据进行处理时

图书表的创建

一级分类

CREATE TABLE `dangdang` . `firstctgy` (
`firstctgyId` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) NULL,
PRIMARY KEY ( `firstctgyId` ));






insert into dangdang.firstctgy values(1,'童书'),(2,'电子书'),(3,'女装'),(4,'食品'),(5,'男装'),(6,'数码相机'),(7,'创意文具'),(8,'童装童鞋');

二级分类

二级分类需要添加外键

CREATE TABLE `dangdang` . `secondctgy`(
`secondctgyId` int NOT NULL AUTO_INCREMENT,
`secctgyName` varchar(20) NOT NULL,
`firstctgyId` int NOT NULL,
PRIMARY KEY (`secondctgyId`),
CONSTRAINT `fk_firstctgyId` FOREIGN KEY(`firstctgyId`) REFERENCES firstctgy(`firstctgyId`));







insert into dangdang.secondctgy values(1,'0-2岁',1);
insert into dangdang.secondctgy values(2,'3-6岁',1);
insert into dangdang.secondctgy values(3,'7-10岁',1);
insert into dangdang.secondctgy values(4,'11-14岁',1);
insert into dangdang.secondctgy values(5,'文艺',2);
insert into dangdang.secondctgy values(6,'人文社科',2);
insert into dangdang.secondctgy values(7,'教育',2);

三级分类

三级分类需要添加外键

CREATE TABLE `dangdang` . `thirdctgy`(
`thirdctgyId` int NOT NULL AUTO_INCREMENT,
`thirdctgyName` varchar(20) NOT NULL,
`secondctgyId` int NULL,
PRIMARY KEY (`thirdctgyId`),
CONSTRAINT `fk_secondctgyId` FOREIGN KEY(`secondctgyId`) REFERENCES secondctgy(`secondctgyId`));







/*三级分类[二级分类为0-2岁]*/
insert into thirdctgy values(1,'图画故事',1),(2,'认知',1),(3,'益智游戏',1),(4,'纸板书',1),(5,'艺术课堂',1),(6,'入园准备',1);


/*三级分类[二级分类为3-6岁]*/
insert into thirdctgy values(7,'绘画',2),(8,'科普百科',2),(9,'少儿英语',2),(10,'乐高学习',2),(11,'入学准备',2);



/*三级分类[二级分类为7-10岁]*/
insert into thirdctgy(thirdctgyName,secondctgyId) values('文学',3),('科普百科',3),('卡通动漫',3),('童话',3),('少儿英语',3);

/*三级分类[二级分类为11-14岁]*/
insert into thirdctgy(thirdctgyName,secondctgyId) values('励志',4),('地理',4),('政治',4),('趣味默',4),('少儿英语',4),('益智游戏',4),('艺术课堂',4),('游戏/手工',4),('绘画',4);




/*三级分类[二级分类为文艺]*/
insert into thirdctgy(thirdctgyName,secondctgyId) values('小说',5),('哲理文学',5),('传记',5),('青春文学',5),('动漫/幽默',5),('艺术',5),('古籍',5),('法律',5),('经济',5);



/*三级分类[二级分类为人文社科]*/
insert into thirdctgy(thirdctgyName,secondctgyId) values('宗教哲学',6),('历史',6),('传记',6),('教育',6),('社会科学',6),('艺术',6),('工具书',6),('教师用书',6),('考研',6),('公务员',6); 

内连接

select * from 表A,表B where 表A.主键id=表B.外键id
select * from 表A inner join 表B on 表A.主键id=表B.外键id

左外连接

select * from 表A left outer join 表B on 表A.主键id=表B.外键id

查询图书分类

根据一级分类 Id ,查询所有的二三级分类

async findCtgys(firstctgyId: string) {
    const sql = `select * from secondctgy sc inner join thirdctgy tc on sc.secondctgyId = tc.secondctgyId where sc.firstctgyId=${firstctgyId}`;
    const [result] = await sequelize.query(sql);
    return result;
  }

查询出来的结果跟实际前端想要的结果相差甚远,需要进行二次处理。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYvg8emH' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片