学习笔记 - 前中台项目跟着做(持续更新)

186 阅读10分钟

为什么使用Vite

1.目标:沉淀一套适应中前台的解决方案(响应式,H5+组件库)。

2.构建方案:vite
image.png

思考: vite为什么快? vite机制存在的问题,官方如何解决这个问题?

tailwindcss 原子化css

  1. tailwindcss 原子化css www.tailwindcss.cn/docs/instal… 实践代码: gitee.com/carrierxia/…

(20240927 中前台解决方案学习 第三天)

  1. tailwindcss 体验,设计理念(原子化css),价值提现(定制化,个性化,交互性高 —— 响应式,需要主题切换定制化高的前台项目;像后台项目,就使用通用组件库)

具体类名: tailwindcss.com/docs/contai…

gitee.com/carrierxia/…

VSCode辅助插件

  1. VSCode辅助插件

(1)Prettier - Code formatter:处理代码格式
Prettier - Code formatter
prettier.io
image.png
在需要格式化的文件中右键 使用...格式化,配置默认格式化工具
image.png
在项目根目录添加.prettierrc文件

{
  // 代码结尾加分号
  "semi": true,
  // 优先单引号
  "singleQuote": true,
  // 不添加尾随逗号
  "trailingComa": "none"
}

保存代码时自动格式化 image.png
(2)Tailwind CSS IntelliSense
提示TailwindCSS类名
(3)Vue - Official
Vue3辅助工具

项目架构基本结构分析

项目架构基本结构分析:一套代码,实现PC端与移动端两种展示效果,响应式构建方案。
移动端结构分析:整个页面(路由)切换,一个路由出口
PC端结构分析:一级,二级路由出口
结合:APP.vue 处理一级路由出口,用于整页切换。 Main.vue 处理二级路由出口,用户局部切换

  1. 根据用户所在设备,构建路由表(多路由表)
  2. 初始化项目
npm init vite@latest carrier-web
npm i
npm i -D autoprefixer postcss sass tailwindcss
npm i --save vuex vue-router
export default {
  content: ['index.html', "./src/**/*.{ts,js,vue}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
import { createApp } from 'vue'
import './style.css'
import "./styles/index.scss"
import App from './App.vue'

createApp(App).mount('#app')
@tailwind base;
@tailwind components;
@tailwind utilities;

npm run dev运行项目出现sass警告(Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.),这是由于当前使用的sass版本处于Dart实现版本,可以降低sass版本,或在vite.config中配置 css.preprocessorOptions,切换jsapi模式。

image.png

Breaking Change: Legacy JS API image.png

css.preprocessorOptions image.png

  1. 项目基本架构如下
carrier-web  // 项目名称
| - src
|   | - apis // 请求接口
|   | - assets // 静态资源
|      | - icons // svg icon 图标
|      | - images // 图片
|   | - components // 通用业务组件(一个组件在多个页面中使用)
|   | - constants // 常量
|   | - directives // 自定义指令
|   | - libs // 通用组件库
|   | - router // 路由
|      | - index.ts // 路由处理中心
|      | - modules // 路由模块
|         | - mobile-routes.ts // 移动端路由
|         | - pc-routes.ts // PC端路由
|   | - stores // 全局状态
|   | - styles // 全局样式
|      | - index.scss // 通用样式
|   | - types // ts类型声明
|   | - utils // 工具模块
|   | - vendor // 外部供应资源
|   | - views // 页面组件(对应路由表,以页面的形式表示)
|   | - APP.vue // 项目根文件,一级路由出口
|   | - main.ts // 入口文件
|   | - permission.ts // 权限控制中心
| - tailwind.config.ts // tailwindCss配置文件
| - vite.config.ts // vite配置文件

构建移动端路由表,配置@软链接

  1. 使用 tailwindcss 构建响应式系统,需要移动优先,先构建移动端,再构建PC端
  2. 判断是否是移动设备工具 MobileTerminal
  3. npm i @types/vue
  4. npm i @vueuse/core
    v4-11-2.vueuse.org/guide.html
  5. 使用vueuse的useWindowSize,得到响应式宽高
import { computed } from 'vue'
import { TERMINAL } from '@constants'
import { useWindowSize } from '@vueuse/core'

const { width } = useWindowSize()

export const isMobileTerminal = computed(() => {
  // 根据宽度
  //   return document.documentElement.clientWidth < TERMINAL.PC_DEVICE_WIDTH
  return width.value < TERMINAL.PC_DEVICE_WIDTH
  // 根据用户代理
  //   return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
  //     navigator.userAgent
  //   )
})
  1. 定义@软链接

webpack中可以直接使用@表示src路径,但是vite默认不支持,需要配置alias别名以指向具体路径 vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import autoprefixer from 'autoprefixer'
import tailwindcss from "tailwindcss"
import { join } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  // 软链接
  resolve: {
    alias: {
      '@': join(__dirname, '/src'),
      '@constants': join(__dirname, '/src/constants'),
      '@utils': join(__dirname, '/src/utils')
    }
  },
  css: {
    postcss: {
      plugins: [
        autoprefixer,
        tailwindcss
      ]
    },
    preprocessorOptions: {
      scss: {
        api: 'modern-compiler', // 或 "modern","legacy"
      },
    },
  }
})

tsconfig.node.json

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@constants": ["src/constants/index"],
      "@utils": ["src/utils/index"],
      "@apis": ["src/apis/index"],
    },
  },
  "include": ["vite.config.ts", "src/*.vue"]
}

tsconfig.app.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "preserve",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@constants": ["src/constants/index"],
      "@utils": ["src/utils/index"],
      "@apis": ["src/apis/index"],
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

  1. 构建VueRouter移动端路由表
    gitee.com/carrierxia/…

搭建node服务开发接口

  1. node搭建接口,vite设置代理服务器
    node开发接口参考博客:blog.csdn.net/owo_ovo/art…
  • 使用express-generator快速生成目录
npm i -g express-generator@4
express --no-view carrier-web-server
cd carrier-web-server
npm i
npm start

访问 http://localhost:3000 ,为index.html 仅使用 node 搭建接口,routes/index.js 中返回格式修改为 json ,删掉 public/index.html 文件

image.png

  • 下载nodemon,监听改动并自动重启
npm i nodemon

修改 package.json,将 start 命令由 node 改为 nodemon
image.png

"registry-mirrors": [
  "https://docker.rainbond.cc"
]

image.png

项目根目录新建 docker-compose.yml,复制以下mysql配置 (查看端口占用情况:netstat -aon | findstr 3306)

services:
  mysql:
    image: mysql:8.3.0
    command:
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_ROOT_PASSWORD=123456
      - MYSQL_LOWER_CASE_TABLE_NAMES=0
    ports:
      - "3306:3306"
    volumes:
      - ./data/mysql:/var/lib/mysql

端口3306一般是被本地已安装的mysql服务占用了,可以通过任务管理器进行关闭。

image.png

下载启动mysql

docker-compose up -d

image.png

image.png

blog.csdn.net/zxrhhm/arti… image.png

后续可通过docker开启 image.png

端口被占用的话,可以修改配置中的端口,重新执行命令docker-compose up -d

  • windows下载navicat客户端,更好地操作数据库

Navicat Premium 17安装教程:juejin.cn/spost/74571…

image.png

image.png

  • 安装 Sequelize 和数据库驱动
    Sequelize 是一个基于 promise 的 Node.js ORM(对象关系映射)库,支持多种数据库,包括 MySQL、PostgreSQL、SQLite 和 Microsoft SQL Server。它允许开发者使用 JavaScript(或 TypeScript)对象和方法来操作数据库,而不需要编写复杂的 SQL 语句,从而提高了代码的可读性和安全性‌

全局安装sequelize-cli,项目安装sequelize mysql2

npm i -g sequelize-cli
npm i sequelize mysql2
sequelize init

image.png

config/config.json:sequelize 需要的连接到数据库的配置文件。
migrations:迁移,用于处理 新增表、修改字段、删除表 等操作,而不用直接在客户端中点点点直接操作数据库。
models/index.js:模型文件,使用 sequelize 增删改查时,每个文件对应数据库中的一张表。
seeders:存放需要添加到数据表的测试数据。

  • password:保持与 docker-compose.yml 中一致
  • database:修改数据库名,保持与 Navicat 客户端中一致
  • timezone:时区设置为东八区

development 开发环境
test 测试环境
production 生产环境

  "development": {
    "username": "root",
    "password": "123456",
    "database": "carrier_web_development",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+08:00"
  }

image.png

image.png

gitee.com/carrierxia/…

sequelize的migrations迁移、models模型

(20241216 中前台解决方案学习 第八天)
通过命令新建模型 article.js(单数),数据库表名Articles(首字母要大写,不大写的话将来部署到 Linux 服务器会报错;且一定要为复数形式,即后缀s,否则 nodejs 将无法查询到这个表)

sequelize model:generate --name Article --attributes title:string,content:text

image.png

image.png

运行迁移命令

sequelize db:migrate

刷新Navicat,多了表Articles,表SequelizeMeta(记录了已跑过的迁移,当再次运行 sequelize db:migrate 时,已运行过的迁移文件,就不会重复再次执行)
image.png

添加种子文件,给项目添加测试数据

sequelize seed:generate --name article 

up中进行填充数据 down中进行删除数据

'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    /**
     * Add seed commands here.
     *
     * Example:
     * await queryInterface.bulkInsert('People', [{
     *   name: 'John Doe',
     *   isBetaMember: false
     * }], {});
     * 
    */

    const articles = []

    for (let i = 1; i <= 10; i++) {
      const article = {
        title: `标题 ${i}`,
        content: `内容 ${i}`,
        createdAt: new Date(),
        updatedAt: new Date(),
      }

      articles.push(article)
    }

    await queryInterface.bulkInsert("Articles", articles, {})
  },

  async down(queryInterface, Sequelize) {
    /**
     * Add commands to revert seed here.
     *
     * Example:
     * await queryInterface.bulkDelete('People', null, {});
     */
    await queryInterface.bulkDelete('Articles', null, {});
  }
};

运行种子文件

sequelize db:seed --seed xxx-article

image.png

刷新数据库,发现数据已添加(Navicat刷新没生效,可关闭数据库连接后再重新打开)

image.png

使用Sequeliz的步骤总结:

  1. 建模型和迁移文件 sequelize model:generate --name XXX --attributes XXX:XXX
  2. 根据虚修调整迁移文件 migrations/xxx.js
  3. 运行迁移,生成数据库表 sequelize db:migra
  4. 添加种子文件(可选,一些需要一下子插入大量数据的情况时推荐使用)-> 修改种子文件填充需要的数据 -> 运行种子文件,将数据填充到数据表中 sequelize db:seed --seed xxx-xxx模型名

使用Sequeliz开发接口

新增路由

/routes/articles.js

var express = require("express")
var router = express.Router()

router.get("/", function (req, res, next) {
    res.json({ message: "articles api" })
})

module.exports = router

/app.js

var articlesRouter = require("./routes/articles");
app.use('/articles', articlesRouter);

image.png

http://localhost:3000/articles image.png

查询列表

/routes/articles.js

var express = require("express")
var router = express.Router()
const { Article } = require("../models")

router.get("/", async function (req, res, next) {
    try {
        
        // 定义查询条件
        // const condition = {
        //     order: [["id", "DESC"]],
        // }
    
        // 查询数据
        // const articles = await Article.findAll(condition)
        const articles = await Article.findAll()

        // 返回查询结果
        res.json({
            status: true,
            message: "查询文章列表成功",
            data: { articles },
        })
    } catch (err) {
        res.status(500).json({
            status: false,
            message: "查询文章列表失败",
            errors: [err.message],
        })
    }
})

module.exports = router

http://localhost:3000/articles image.png

SELECT id, title, content, createdAt, updatedAt FROM Articles AS Article; image.png

查询详情

http://localhost:3000/articles/1

// 查询文章详情
router.get("/:id", async function (req, res, next) {
    try {
        // 获取参数 - 文章ID
        const { id } = req.params

        // 查询数据
        const article = await Article.findByPk(id)

        // 返回查询结果
        if (article) {
            res.json({
                status: true,
                message: "查询文章详情成功",
                data: { article },
            })
        } else {
            return res.status(404).json({
                status: false,
                message: "文章不存在",
            })
        }
    } catch (err) {
        res.status(500).json({
            status: false,
            message: "查询文章详情失败",
            errors: [err.message],
        })
    }
})

image.png

SELECT id, title, content, createdAt, updatedAt FROM Articles AS Article WHERE Article.id = '1'; image.png

新增

/models/article.js

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class Article extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
    }
  }
  Article.init({
    title: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        notNull: {
          msg: '标题必须存在。'
        },
        notEmpty: {
          msg: '标题不能为空。'
        },
        len: {
          args: [2, 45],
          msg: '标题长度需要在2 ~ 45个字符之间。'
        }
      }
    },
    content: DataTypes.TEXT
  }, {
    sequelize,
    modelName: 'Article',
  });
  return Article;
};

/routes/articles.js

// 新增
// 白名单过滤 - 获取请求参数
function getBody(req) {
    return {
        title: req.body.title,
        content: req.body.content,
    }
}

router.post("/", async function (req, res, next) {
    const body = getBody(req)
    try {
        // 创建数据
        const article = await Article.create(body)

        // 返回创建结果
        res.status(201).json({
            status: true,
            message: "创建文章成功",
            data: article,
        })
    } catch (err) {
        res.json(err)
    }
})

image.png

通过Postman请求:http://localhost:3000/articles

image.png

image.png

INSERT INTO 表名 (列1, ...) VALUES (值1, ...) image.png

添加错误捕获处理
/routes/articles.js

catch (err) {
        if (err.name === "SequelizeValidationError") {
            const errors = err.errors.map((error) => error.message)
            res.status(400).json({
                status: false,
                message: "创建文章失败",
                errors,
            })
        } else {
            res.status(500).json({
                status: false,
                message: "创建文章失败",
                errors: [err.message],
            })
        }
    }

image.png

image.png

删除

/routes/articles.js

// 删除文章
router.delete("/:id", async function (req, res, next) {
    try {
        // 获取参数 - 文章ID
        const { id } = req.params

        // 查询数据
        const article = await Article.findByPk(id)

        if (article) {
            // 删除数据
            await Article.destroy({ where: { id } })
            // 返回删除结果
            res.json({
                status: true,
                message: "删除文章成功",
            })
        } else {
            return res.status(404).json({
                status: false,
                message: "文章不存在",
            })
        }
    } catch (err) {
        res.status(500).json({
            status: false,
            message: "删除文章失败",
            errors: [err.message],
        })
    }
})

http://localhost:3000/articles/11

image.png

更新

/routes/articles.js

SELECT id, title, content, createdAt, updatedAt FROM Articles AS Article WHERE Article.id = '1';
UPDATE Articles SET title=?,updatedAt=? WHERE id = ?

// 更新文章
router.put("/:id", async function (req, res, next) {
    const body = getBody(req)

    try {
        // 获取参数 - 文章ID
        const { id } = req.params

        // 查询数据
        const article = await Article.findByPk(id)

        if (article) {
            // 更新数据
            await article.update(body)
            // 返回更新结果
            res.json({
                status: true,
                message: "更新文章成功",
            })
        } else {
            return res.status(404).json({
                status: false,
                message: "文章不存在",
            })
        }
    } catch (err) {
        res.status(500).json({
            status: false,
            message: "更新文章失败",
            errors: [err.message],
        })
    }
})

模糊查询

/routes/articles.js

const { Op } = require('sequelize');

// 查询文章列表
router.get("/", async function (req, res, next) {
    try {

        const { query } = req

        // 定义查询条件
        const condition = {
            order: [["id", "DESC"]],
        }

        // 模糊查询
        if (query.title) {
            condition.where = {
                title: {
                    [Op.like]: `%${query.title}%`,
                },
            }
        }

        // 查询数据
        const articles = await Article.findAll(condition)
        // const articles = await Article.findAll()

        // 返回查询结果
        res.json({
            status: true,
            message: "查询文章列表成功",
            data: { articles },
        })
    } catch (err) {
        res.status(500).json({
            status: false,
            message: "查询文章列表失败",
            errors: [err.message],
        })
    }
})

select * from Articles where title like '%标题 10%'
http://localhost:3000/articles?title=20241220 image.png

分页查询

SELECT * FROM Articles LIMIT 0, 10;
// 假如一页10条,第二页参数为 10 10,而不是 10 20
SELECT * FROM Articles LIMIT 10, 10;

http://localhost:3000/articles?pageSize=5&currentPage=2

SELECT count(*) AS count FROM Articles AS Article
SELECT id, title, content, createdAt, updatedAt FROM Articles AS Article ORDER BY Article.id DESC LIMIT 5, 5;

// 查询文章列表
router.get("/", async function (req, res, next) {
    try {
        const { query } = req

        // 当前页码
        const currentPage = Math.abs(Number(query.currentPage)) || 1
        // 每页显示条数
        const pageSize = Math.abs(Number(query.pageSize)) || 10
        // 计算offset
        const offset = (currentPage - 1) * pageSize

        // 定义查询条件
        const condition = {
            order: [["id", "DESC"]],
            limit: pageSize,
            offset,
        }

        // 模糊查询
        if (query.title) {
            condition.where = {
                title: {
                    [Op.like]: `%${query.title}%`,
                },
            }
        }

        // 查询数据
        // const articles = await Article.findAll(condition)
        // const articles = await Article.findAll()

        // count 为数据总数,rows 为当前查询到的数据
        const { count, rows } = await Article.findAndCountAll(condition)

        // 返回查询结果
        res.json({
            status: true,
            message: "查询文章列表成功",
            data: {
                articles: rows,
                pagination: { total: count, currentPage, pageSize },
            },
        })
    } catch (err) {
        res.status(500).json({
            status: false,
            message: "查询文章列表失败",
            errors: [err.message],
        })
    }
})

封装响应工具函数

gitee.com/carrierxia/… /utils/response.js

/**
 * 自定义 404 错误类
 */
class NotFoundError extends Error {
    constructor(message) {
        super(message)
        this.name = "NotFoundError"
    }
}

/**
 * 请求成功
 * @param res
 * @param message
 * @param data
 * @param code
 */
function success(res, message, data = {}, code = 200) {
    res.status(code).json({
        status: true,
        message,
        data,
    })
}

/**
 * 请求失败
 * @param res
 * @param error
 */
function failure(res, error) {
    if (error.name === "SequelizeValidationError") {
        const errors = error.errors.map((e) => e.message)
        return res.status(400).json({
            status: false,
            message: "请求参数错误",
            errors,
        })
    }

    if (error.name === "NotFoundError") {
        return res.status(404).json({
            status: false,
            message: "资源不存在",
            errors: [error.message],
        })
    }

    res.status(500).json({
        status: false,
        message: "服务器错误",
        errors: [error.message],
    })
}

module.exports = {
    NotFoundError,
    success,
    failure,
}

Vite处理环境变量,调用接口配置

vite处理环境变量,使用anywhere运行打包后的dist目录
www.npmjs.com/package/any…