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

20 KiB
Raw Permalink Blame History

[TOC]

前端通用框架

框架介绍

基于vue@2.6, ant-design-vue@1.7, axios, vue-router, vuex, webpack@4.x 的通用后台管理系统

功能:

  1. 根据路由动态生成侧边导航
  2. 使用mock.jsmock-serve进行本地mock
  3. 集成eslintprettier配置
  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插件

# 需要全局安装commitizen才能使用 git cz
npm install commitizen -g
# 克隆项目
git clone xxx
git cd xxx
# 安装插件
yarn
# 启动服务
yarn run dev
# 打包
yarn run build

目录结构

├─.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层

image.png

路由生成

image.png

路由配置项:

{
  // "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://
  },
},

举例:

"系统管理"为一级菜单

"用户管理"为子菜单

{
    "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":"菜单管理"
        }
      }
}

路由聚合

image.png
  1. children中的路由不会在左侧菜单栏中显示
  2. children中的路由只能有一层
  3. children中的路由所对应的组件, 聚合到tab栏组件中, 通过tab栏切换
  4. redirect无效, tab栏顺序为children顺序, 默认展示第一个
{
  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跳转到新页面
image.png image.png
// 路由结构 举例
{
  "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跳转

image.png
{
  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文件示例

// /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,
      }
    },
  },

  // 其他接口...
]
// /mock/index.js
// ...
const user = require('./user')
const mocks = [
  ...user,
]
// ...

(本地)生产模式

生产模式只能通过mock.js模拟数据, 与mock-serve共同使用同一份模拟数据

相关配置:

// /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中的对应接口

    const mocks = [
      ...user,
      // ...table
    ]
    

    全部取消:

    // 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" />调用

<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

    //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

    // 子组件
    <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模块( 孙 - 子 - 父 - 子)
    // 参考 子 -> 父 父 -> 子
    
  • 链路传递超过2层, 优先考虑vuex模块

  • 避免使用事件总线(待补充)

  • 使用插槽向引用组件传递模板

    • 普通插槽

      // 父组件
      <MyButton>注册</MyButton>
      
      // 自定义组件
      <button>
          <slot>按钮</slot>
      </button>
      
    • 作用域插槽

      <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。

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