spa/README.md
2025-03-29 11:46:12 +08:00

733 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[TOC]
# 前端通用框架
## 框架介绍
基于`vue@2.6`, `ant-design-vue@1.7`, `axios`, `vue-router`, `vuex`, `webpack@4.x` 的通用后台管理系统
功能:
1. 根据路由动态生成侧边导航
2. 使用`mock.js``mock-serve`进行本地mock
3. 集成`eslint``prettier`配置
4. 集成`commit插件`
5. 集成`git`钩子, 必须通过`eslit`代码检查和`commit`提交规范检查才能提交代码
6. `ant-design-vue` 组件按需引入, ant-design icon 按需引入
7. 响应错误统一处理
8. `vue-ls`本地持久化
9. 支持设置主题
10. 权限自定义指令
## not Support
1. ie11 兼容
2. h5 兼容
3. 响应式
## 快速开始
推荐使用vscode开发, 需要安装`prettier`, `eslint`, `vetur`插件
```bash
# 需要全局安装commitizen才能使用 git cz
npm install commitizen -g
# 克隆项目
git clone xxx
git cd xxx
# 安装插件
yarn
# 启动服务
yarn run dev
# 打包
yarn run build
```
## 目录结构
```json
├─.browserslistrc ------------------------ // 目标浏览器配置
├─.env.development ----------------------- // 开发环境变量配置
├─.env.production ------------------------ // 开发环境变量配置
├─.eslintignore -------------------------- // eslint忽略文件
├─.eslintrc.js --------------------------- // eslisnt配置
├─.gitignore ----------------------------- // git忽略配置
├─.prettierignore ------------------------ // prettier忽略配置
├─.prettierrc.js ------------------------- // 代码风格配置
├─.vscode -------------------------------- // vscode项目设置
└─settings.json ------------------------ // vscode配置
├─README.md
├─babel.config.js ------------------------ // js编译器设置
├─commitlint.config.js ------------------- // git commit lint
├─jsconfig.json -------------------------- // JavaScript项目的根目录配置
├─package.json --------------------------- // npm包管理
├─public --------------------------------- // 静态资源文件夹
├─color.less --------------------------- // 主题颜色配置
├─favicon.ico -------------------------- // 网站图标
└─index.html --------------------------- // 主页
├─src ------------------------------------ // 代码目录
├─App.vue ------------------------------ // 项目根组件
├─api ---------------------------------- // api目录
└─user.js ---------------------------- // 用户相关接口
├─assets ------------------------------- // 资源文件夹
├─avatar.jpg ------------------------- // 默认头像
├─background.svg --------------------- // 登录页背景
├─checkcode.png ---------------------- // 验证码默认图
├─icons ------------------------------ // 图标相关
├─index.js
├─svg ------------------------------ // svg图标存放路径
├─develop.svg
└─svgtest.svg
└─svgo.yml ------------------------- // svg图标格式化配置
├─less ------------------------------- // 样式
└─common.less ---------------------- // 公共样式
└─logo.png
├─components --------------------------- // 公共组件
├─Header.vue ------------------------- // 头部
├─SettingDrawer ---------------------- // 系统设置(主题)
├─index.vue
└─settingConfig.js ----------------- // 主题颜色生成
├─SvgIcon.vue ------------------------ // svg图标组件
├─TopTabs.vue ------------------------ // tab栏导航
├─UserPassword.vue
├─layouts ---------------------------- // 布局相关
├─AggregateLayout(可以删除).vue
├─BlankLayout.vue ------------------ // 空白布局
├─GlobalLayout.vue ----------------- // 全局部局
├─SideTabsLayout.vue --------------- // 侧边切换tab布局
└─UserLayout.vue ------------------- // 登录/注册/找回密码共用布局
└─menu
├─Contextmenu.vue ------------------ // 上下文菜单(在顶部导航鼠标右键菜单用到)
└─SideMenu.js ---------------------- // 左侧导航栏生成
├─config
└─defaultSettings.js ----------------- // 全局主题颜色/vue-ls配置
├─main.js ------------------------------ // 入口
├─mock --------------------------------- // mock
├─constant --------------------------- // 存放较大的数据
└─userPermission.js ---------------- // 权限模拟数据
├─index.js
├─mock-server.js --------------------- // mock-server本地服务
├─table.js --------------------------- // mock语法示例
├─user.js ---------------------------- // 模拟接口
└─utils.js --------------------------- // mock服务工具方法
├─router ------------------------------- // 路由
├─generateIndexRouter.js ------------- // 动态路由生成
└─index.js
├─store -------------------------------- // vuex
├─index.js
└─modules ---------------------------- // vuex模块
├─app.js --------------------------- // 全局
└─user.js -------------------------- // 用户相关
├─utils -------------------------------- // 工具类
├─lazy ------------------------------- // 按需加载
├─icons.js ------------------------- // 图标
└─lazyAntd.js ---------------------- // ant-design-vue组件
└─request.js ------------------------- // axios封装
└─views -------------------------------- // 页面
├─About.vue -------------------------- // 关于
├─Home.vue --------------------------- // 首页
├─account ---------------------------- // 账号相关
├─Center.vue ----------------------- // 个人中心页
└─settings ------------------------- // 个人设置页
├─BaseSetting.vue
├─Binding.vue
├─Custom.vue
├─Notification.vue
└─Security.vue
├─exception -------------------------- // 404/403/500等错误页
└─404.vue
├─list ------------------------------- // 多级菜单 示例
├─StandardList.vue
└─TableList.vue
├─modules ---------------------------- // 存放页面公共组件
└─online
└─desform ------------------------ // 列表页跳详情页 示例
├─DesignFormList.vue
└─DesignFormTempletList.vue
├─oneLevelMenu ----------------------- // 一级菜单示例页面
└─index.vue
├─profile ---------------------------- // 路由聚合示例
├─Advanced.vue
└─Basic.vue
├─sideTabs --------------------------- // 侧边栏tab标签 示例
├─components
├─Tab1.vue
├─Tab2.vue
├─Tab3.vue
└─Tab4.vue
└─index.vue
├─title
└─index.vue
└─user ------------------------------- // 登录/注册/找回密码 相关
├─Login.vue ------------------------ // 登录页
└─LoginSimple.vue
├─test ----------------------------------- // 测试用例
└─test.js
├─vue.config.js -------------------------- // vue-cli配置
└─yarn.lock ------------------------------ // npm包版本控制
```
### vue组件的存放路径:
1. 框架相关的组件, 存放在`@/components`
2. 布局相关的, 存在在`@/components/layouts`
3. 不相关的两个页面的公共组件, 存放在`@/viewmodules`
4. 父子关系/兄第关系的页面的公共组件, 且没有其他页面使用, 存放在他们的公共根目录下`@/views/xxx/components`
## 动态路由
### 路由菜单不要超过3层
<img src="https://i.loli.net/2021/09/18/opbsBDxKWn2vMId.png" alt="image.png" style="zoom:50%;" />
### 路由生成
<img src="https://i.loli.net/2021/09/18/1XJzZxdUqrluGDN.png" alt="image.png" style="zoom:50%;" />
### 路由配置项:
```json
{
// "hidden": true, // (默认不传)不在左侧菜单中显示
// "redirect":null, // (默认不传)重定向
// "alwaysShow": false, // (默认不传)路由聚合, 从menu菜单展示变为tab栏展示
"path":"/dashboard/analysis", // 路径
"component":"dashboard/Analysis", // 组件
"name":"dashboard-analysis", // 路由名称, 使用<keep-alive>时必填
"meta":{
"keepAlive":true, // 是否缓存
"icon":"home", // 图标
"title":"首页", // 侧边栏和面包屑中展示的名字
// "url": "http://www.baidu.com" // (默认不传)当外部链接跳转时必须传, 必须带http:// 或者httPs://
},
},
```
举例:
"系统管理"为一级菜单
"用户管理"为子菜单
```json
{
"path":"/isystem",
"component":"layouts/RouteView",
"name":"isystem",
"meta":{
"keepAlive":false,
"icon":"setting",
"title":"系统管理"
},
"children":[
{
"path":"/isystem/user",
"component":"system/UserList",
"name":"isystem-user",
"meta":{
"keepAlive":false,
"title":"用户管理"
},
},
{
"path":"/isystem/roleUserList",
"component":"system/RoleUserList",
"name":"isystem-roleUserList",
"meta":{
"keepAlive":true,
"title":"角色管理"
},
},
{
"path":"/isystem/newPermissionList",
"component":"system/NewPermissionList",
"name":"isystem-newPermissionList",
"meta":{
"keepAlive":true,
"title":"菜单管理"
}
}
}
```
### 路由聚合
<img src="https://i.loli.net/2021/09/18/zZns3Lr89ClJ2iD.png" alt="image.png" style="zoom:50%;" />
1. children中的路由不会在左侧菜单栏中显示
2. children中的路由只能有一层
3. children中的路由所对应的组件, 聚合到tab栏组件中, 通过tab栏切换
4. redirect无效, tab栏顺序为children顺序, 默认展示第一个
```json
{
path: '/account/settings',
redirect: '/account/settings/notification',
component: 'layouts/BlankLayout',
alwaysShow: true,
meta: {
title: '个人设置',
},
name: 'account-settings-Index',
children: [
{
path: '/account/settings/notification',
component: 'account/settings/Notification',
meta: {
title: '新消息通知',
},
name: 'account-settings-notification',
},
{
path: '/account/settings/base-setting',
component: 'account/settings/BaseSetting',
meta: {
title: '基本设置',
},
name: 'account-settings-base',
},
{
path: '/account/settings/binding',
component: 'account/settings/Binding',
meta: {
title: '账户绑定',
},
name: 'account-settings-binding',
},
{
path: '/account/settings/security',
component: 'account/settings/Security',
meta: {
title: '安全设置',
},
name: 'account-settings-security',
},
{
path: '/account/settings/custom',
component: 'account/settings/Custom',
meta: {
title: '个性化设置',
},
name: 'account-settings-custom',
},
],
}
```
### 列表页和详情页
1. 简单的详情页, 直接通过`抽屉`/ `模态框`渲染
2. 复杂的详情页, 通过`/id`跳转到新页面
<img src="https://i.loli.net/2021/09/18/FL21JyDI5mWwh3s.png" alt="image.png" style="zoom:50%;" />
<img src="https://i.loli.net/2021/09/18/HgJaSM5ik9vwFCR.png" alt="image.png" style="zoom:50%;" />
```json
// 路由结构 举例
{
"path": "/test",
// 加载默认子路由, 不能有name
"component":"layouts/BlankLayout",
"meta": {
"title":"测试页面",
redirectDefaultChild: true, // 子路由不在菜单栏中显示, 进入子路由显示父路由高亮
},
"children":[
{
"path": "", // 默认子路由
"name": "test-list",
"component":"test/List",
"meta": {
"title":"测试页面列表页",
// "keepAlive": true // 可以设置keepAlive使详情页回到列表页, 不重新渲染组件
},
},
{
"path": "/test/:id", // 详情页
"name": "test-detail",
"component":"test/Detail",
"meta": {
"title":"测试页面详情页"
},
}
]
}
```
### 外链url跳转
<img src="https://i.loli.net/2021/09/18/efP12VRlLObi36v.png" alt="image.png" style="zoom:50%;" />
```json
{
path: '/urllink',
component: 'layouts/BlankLayout',
name: 'baidu',
meta: {
title: 'url跳转',
url: 'https://www.baidu.com', // url必须完整, 带http://或者https://
},
}
```
## 动态权限
待补充完善
## mock
### (本地)开发模式
1. 启动`yarn run dev`时, 会同时启动`mock-serve`, 与mock相关的接口会转发到`mock-serve`
2. mock文件示例
```js
// /mock/user.js
const tokens = {
admin: {
token: 'admin-token',
},
editor: {
token: 'editor-token',
},
}
module.exports = [
// 登录接口
{
url: '/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
errcode: 1,
errmsg: 'Account and password are incorrect.',
}
}
return {
errcode: 0,
data: token,
}
},
},
// 其他接口...
]
```
```js
// /mock/index.js
// ...
const user = require('./user')
const mocks = [
...user,
]
// ...
```
### (本地)生产模式
生产模式只能通过`mock.js`模拟数据, 与`mock-serve`共同使用同一份模拟数据
相关配置:
```js
// /src/main.js
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('./mock')
mockXHR()
}
```
请求路径与mock.js中匹配的会被拦截, 但因为是重写了xhr方法, 无法在浏览器的network中看到请求, 可以通过控制台打印
==所以上线前需要在`/mock/index.js`把后端已完成的相关接口注释掉, 或者直接在`src/main.js`注释掉mock功能==
### 数据切换
1. 本地开发模式:
修改`vue.config.js`中的`mockUrls`
2. 本地生产模式
注释掉`/mock/index.js`中的对应接口
```js
const mocks = [
...user,
// ...table
]
```
全部取消:
```js
// src/main.js
// 注释掉这部分
// if (process.env.NODE_ENV === 'production') {
// const { mockXHR } = require('../mock')
// mockXHR()
// }
```
## ui组件, icon按需引入
1. ui组件在`src/utils/lazy/lazyAntd.js`中按需引入
2. ant-design-icon在`src/utils/lazy/icons.js`中按需引入
3. 其他icon:
1. 使用`Iconfont(阿里巴巴图标库)`下载svg图标, 统一放在`@/assets/icon/svg中`
2. 执行`yarn run svgo`去掉多余信息格式化图标
3. 使用`<svg-icon icon-class="xxx" />`调用
```html
<svg-icon style="font-size: 18px; color: pink" icon-class="svgtest" />
```
## 公共js , css, 工具js
* 公共js, css: 框架里只保留最小使用
* 工具js: 通用性高的工具js和框架一起维护, 需要保持工具js的单一性, 完善注释
## 体积较大的第三方js插件
例如 "excel"的导出插件, 统一放在`/src/vender`中
## 泛用组件
例如"季度选择组件", 存放到gitlab公共组件仓库中
todo:
- [ ] 打包成npm包
- [ ] git submodules
## 开发规范
### 数据处理
* 后端接口获取的数据, 只取页面需要的, 不能直接存到data中
* 当某个接口的数据有多个页面使用时, 考虑存放到vuex/localstorage中
* 退出登录时, 需要考虑清空localStorage/sessionStorage中的敏感数据, 例如用户的登录信息, 需要更新的信息
### 列表页跳详情页
优先使用抽屉, 模态框, 无法满足需求时才考虑跳转新页面
### vue单文件的可维护性
建议不超过500行, 超过的需要考虑是否可以优化
> 1.抽离组件再引入
> 2.将css通过@import方式抽离再引入
> 3.将css/js/html全部抽离只保留模块主入口文件再引入
### ui组件
优先使用ant-design-vue, 按需引用
### icon
优先使用ant-design icon , 按需引用
### ui风格
1. 滚动条/滚动范围:
以表格为例: 横向溢出时, 内容加滚动条
纵向溢出, 随页面滚动, 表头页脚不固定(优先考虑分页)
### 组件通讯
* 父 -> 子 : props
```js
//App.vue父组件
<template>
<div id="app">
<users :users="users"></users> <!-- 前者自定义名称便于子组件调用,后者要传递数据名 -->
</div>
</template>
<script>
import Users from "./components/Users"
export default {
name: 'App',
data(){
return{
users:["Henry","Bucky","Emily"]
}
},
components:{
"users":Users
}
}
//users子组件
<template>
<div class="hello">
<ul>
<li v-for="user in users">{{user}}</li> <!-- 遍历传递过来的值,然后呈现到页面 -->
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props:{
users:{ //这个就是父组件中子标签自定义名字
type:Array,
required:true
}
}
}
</script>
```
* 子 -> 父 : $emit
```js
// 子组件
<template>
<header>
<h1 @click="changeTitle">{{title}}</h1> <!-- 绑定一个点击事件 -->
</header>
</template>
<script>
export default {
name: 'app-header',
data() {
return {
title:"Vue.js Demo"
}
},
methods:{
changeTitle() {
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值”
}
}
}
</script>
// 父组件
<template>
<div id="app">
<app-header v-on:titleChanged="updateTitle" ></app-header>
<!-- 与子组件titleChanged自定义事件保持一致 -->
<!-- updateTitle($event)接受传递过来的文字 -->
<h2>{{title}}</h2>
</div>
</template>
<script>
import Header from "./components/Header"
export default {
name: 'App',
data(){
return{
title:"传递的是一个值"
}
},
methods:{
updateTitle(e){ //声明这个函数
this.title = e;
}
},
components:{
"app-header":Header,
}
}
</script>
```
* 兄弟1 -> 兄弟2 :
* 如果兄弟组件有共同的父组件(子 - 父 - 子), 通过父组件传递
* 如果链路传递超过2层, 优先考虑vuex模块( 孙 - 子 - 父 - 子)
```js
// 参考 子 -> 父 父 -> 子
```
* 链路传递超过2层, 优先考虑vuex模块
* 避免使用事件总线(待补充)
* 使用插槽向引用组件传递模板
* 普通插槽
```js
// 父组件
<MyButton>注册</MyButton>
// 自定义组件
<button>
<slot>按钮</slot>
</button>
```
* 作用域插槽
```js
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
```
## 数据持久化
### localStorage
**在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面刷新之后如果localStorage里有保存的数据取出来再替换store里的state。**
```js
let defaultCity = "上海"
try { // 用户关闭了本地存储功能此时在外层加个try...catch
if (!defaultCity){
defaultCity = JSON.parse(window.localStorage.getItem('defaultCity'))
}
}catch(e){}
export default new Vuex.Store({
state: {
city: defaultCity
},
mutations: {
changeCity(state, city) {
state.city = city
try {
window.localStorage.setItem('defaultCity', JSON.stringify(state.city));
// 数据改变的时候把数据拷贝一份保存到localStorage里面
} catch (e) {}
}
}
})
```
### sessionStorage
参考`localStorage`