为什么使用Vite
1.目标:沉淀一套适应中前台的解决方案(响应式,H5+组件库)。
2.构建方案:vite
- webpack文档 www.webpackjs.com/concepts/
- vite文档 www.vitejs.net/
思考: vite为什么快? vite机制存在的问题,官方如何解决这个问题?
tailwindcss 原子化css
- tailwindcss 原子化css www.tailwindcss.cn/docs/instal… 实践代码: gitee.com/carrierxia/…
(20240927 中前台解决方案学习 第三天)
- tailwindcss 体验,设计理念(原子化css),价值提现(定制化,个性化,交互性高 —— 响应式,需要主题切换定制化高的前台项目;像后台项目,就使用通用组件库)
具体类名: tailwindcss.com/docs/contai…
VSCode辅助插件
- VSCode辅助插件
(1)Prettier - Code formatter:处理代码格式
Prettier - Code formatter
prettier.io
在需要格式化的文件中右键 使用...格式化,配置默认格式化工具
在项目根目录添加.prettierrc文件
{
// 代码结尾加分号
"semi": true,
// 优先单引号
"singleQuote": true,
// 不添加尾随逗号
"trailingComa": "none"
}
保存代码时自动格式化
(2)Tailwind CSS IntelliSense
提示TailwindCSS类名
(3)Vue - Official
Vue3辅助工具
项目架构基本结构分析
项目架构基本结构分析:一套代码,实现PC端与移动端两种展示效果,响应式构建方案。
移动端结构分析:整个页面(路由)切换,一个路由出口
PC端结构分析:一级,二级路由出口
结合:APP.vue 处理一级路由出口,用于整页切换。 Main.vue 处理二级路由出口,用户局部切换
- 根据用户所在设备,构建路由表(多路由表)
- 初始化项目
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模式。
Breaking Change: Legacy JS API
- 项目基本架构如下
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配置文件
构建移动端路由表,配置@软链接
- 使用 tailwindcss 构建响应式系统,需要移动优先,先构建移动端,再构建PC端
- 判断是否是移动设备工具 MobileTerminal
- npm i @types/vue
- npm i @vueuse/core
v4-11-2.vueuse.org/guide.html - 使用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
// )
})
- 定义@软链接
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"]
}
- 构建VueRouter移动端路由表
gitee.com/carrierxia/…
搭建node服务开发接口
- 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 文件
- 下载
nodemon,监听改动并自动重启
npm i nodemon
修改 package.json,将 start 命令由 node 改为 nodemon
- 下载Docker运行数据库
docker: www.docker.com/get-started…
docker安装教程:juejin.cn/spost/74570…
设置Docker Emgine中添加中国镜像
"registry-mirrors": [
"https://docker.rainbond.cc"
]
项目根目录新建 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服务占用了,可以通过任务管理器进行关闭。
下载启动mysql
docker-compose up -d
后续可通过docker开启
端口被占用的话,可以修改配置中的端口,重新执行命令docker-compose up -d
- windows下载navicat客户端,更好地操作数据库
Navicat Premium 17安装教程:juejin.cn/spost/74571…
- 安装 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
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"
}
sequelize的migrations迁移、models模型
(20241216 中前台解决方案学习 第八天)
通过命令新建模型 article.js(单数),数据库表名Articles(首字母要大写,不大写的话将来部署到 Linux 服务器会报错;且一定要为复数形式,即后缀s,否则 nodejs 将无法查询到这个表)
sequelize model:generate --name Article --attributes title:string,content:text
运行迁移命令
sequelize db:migrate
刷新Navicat,多了表Articles,表SequelizeMeta(记录了已跑过的迁移,当再次运行 sequelize db:migrate 时,已运行过的迁移文件,就不会重复再次执行)
添加种子文件,给项目添加测试数据
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
刷新数据库,发现数据已添加(Navicat刷新没生效,可关闭数据库连接后再重新打开)
使用Sequeliz的步骤总结:
- 建模型和迁移文件 sequelize model:generate --name XXX --attributes XXX:XXX
- 根据虚修调整迁移文件 migrations/xxx.js
- 运行迁移,生成数据库表 sequelize db:migra
- 添加种子文件(可选,一些需要一下子插入大量数据的情况时推荐使用)-> 修改种子文件填充需要的数据 -> 运行种子文件,将数据填充到数据表中 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);
http://localhost:3000/articles
查询列表
/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
SELECT id, title, content, createdAt, updatedAt FROM Articles AS Article;
查询详情
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],
})
}
})
SELECT id, title, content, createdAt, updatedAt FROM Articles AS Article WHERE Article.id = '1';
新增
/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)
}
})
通过Postman请求:http://localhost:3000/articles
INSERT INTO 表名 (列1, ...) VALUES (值1, ...)
添加错误捕获处理
/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],
})
}
}
删除
/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
更新
/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
分页查询
SELECT * FROM Articles LIMIT 0, 10;
// 假如一页10条,第二页参数为 10 10,而不是 10 20
SELECT * FROM Articles LIMIT 10, 10;
http://localhost:3000/articles?pageSize=5¤tPage=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…