Vue.js + Vuex + TypeScript 实战项目开发与项目优化

技术栈: Vue.js + Vuex + TypeScript

Todo List:

  • 使用Vue CLI初始化项目
  • 调整初始目录结构
    • 删除初始化的默认文件(对于我们项目是多余的组件components或者视图views)
    • 新增调整我们项目所需要的目录结构
  • 使用TypeScript开发Vue项目
    • 直接使用vue CLI创建项目时选择使用TypeScript
    • 已有项目,使用@vue/cli添加Vue官方配置的TypeScript适配插件
    vue add @vue/typescript
    
  • 编辑器选择 VS Code,对typescript提供较好的支持,开箱即用。如果开发了SFC单文件组件,可以选择安装Vetur插件。当然选择使用WebStorm等其他编辑器也可以的。
  • typescript配置介绍
    • dependencies
      • “vue-class-component”: “^7.0.2”,
      • “vue-property-decorator”: “^8.1.0”,
    • devDependencies
      • “@vue/cli-plugin-eslint”: “^3.0.1”,
      • “@vue/cli-plugin-typescript”: “^3.0.1”,
      • “@vue/eslint-config-typescript”: “^4.0.0”,
      • “typescript”: “^3.4.3”,
    • typescript配置文件:tsconfig.json
  • shims for Typescript: 对代码进行适配处理
    • src/shims-tsx.d.ts 作用是为JSX代码补充类型声明
    • src/shims-vue.d.ts 作用是识别*.vue文件返回值为Vue实例
  • 使用TypeScript开发Vue项目
    • 使用OptionsAPI定义Vue组件
      <script lang="ts">
      import Vue from 'vue'
      export default Vue.extend({
              
              
      	data() {
              
              
      		return {
              
              
      			a: 1,
      			b: '2',
      			c: {
              
              },
      			d: []
      		}
      	},
      	methods:{
              
              
      		test() {
              
              
      			this.c() // error tips
      		}
      	}
      })
      </script>
      
      • 作用:1、编辑器给的类型提示; 2、TypeScript编译期间的类型验证。
    • 使用ClassAPIs定义组件vue-class-component
      import Vue from 'vue'
      import Component from 'vue-class-component'
      import home from "../views/home.vue";//导入组件
      
      @Component({
              
              
        components: {
              
               home },
        props: {
              
              
          propMessage: String
        }
      })
      export default class App extends Vue {
              
              
        // 初始 data
        msg = 123
      
        // use prop values for initial data
        helloMsg = 'Hello, ' + this.propMessage
      
        // 生命钩子lifecycle hook
        mounted () {
              
              
          this.greet()
        }
      
        // 计算属性computed
        get computedMsg () {
              
              
          return 'computed ' + this.msg
        }
      
        // 方法method5
        greet () {
              
              
          alert('greeting: ' + this.msg)
        }
      }
      
    • @component装饰器:用于扩展类class,扩展其属性或功能。
  • 使用VuePropertyDecorator创建Vue组件
  • 总结创建组件的三种方式:
    • OptionsAPI
    • Class APIs: @component({…}) `vue-class-component``
    • Class APIs + Property Decorator `vue-property-decorator``
    • 项目开发建议:No Class APIs, Just Use Options API。
      • 也就是不推荐在生产环境中使用Decorator语法。
      • Class Decorator 语法仅仅是一种写法而已,最终还是要转换为普通的组件数据结构的。
      • 使用 Options API最好是使用export default Vue.extend({...}),而不是export default {...}
  • 约定使用对代码格式规范并遵循规范进行编码
    • 格式良好的代码更有利于:
      • 更好的多人协作
      • 更好的可阅读性
      • 更好的可维护性
    • 没有绝对的标准,大多采用大厂优秀的编码规范选择性采用对自己团队习惯写法:standard/airbnb/google等,仅供参考而已。
  • 如何约束代码规范:采用工具来自动强制执行
    • JSLint
    • JSHint
    • ESLint
  • Linter + Format: 项目中选择 ESList + Standard Config
    'extends': [
        'plugin:vue/essential',
        '@vue/standard',
        '@vue/typescript'
    ],
    
  • 自定义代码格式规范
    rules: {
          
          
        'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
    },
    
    • 错误级别
      • off(关闭:0)
      • warn(警告:1)
      • error(错误:2,错误会中断退出)
    • 参数选项(可选)
      • always
      • never
    • 例子:"semi": ["error", "always"] 规则不生效可以尝试清楚校验结果缓存.cache
  • 导入并使用Element组件库
    npm i element-ui -S
    
    这个ui库是比较适合做中后台的管理系统的。
    • 完整引入

      import Vue from 'vue';
      import ElementUI from 'element-ui';
      import 'element-ui/lib/theme-chalk/index.css';
      import App from './App.vue';
      
      Vue.use(ElementUI);
      
      new Vue({
              
              
        el: '#app',
        render: h => h(App)
      });
      
    • 按需引入

      • 需要借助插件,借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

      • 安装 babel-plugin-component:npm install babel-plugin-component -D

      • 将 .babelrc 修改为:

        {
                  
                  
          "presets": [["es2015", {
                  
                   "modules": false }]],
          "plugins": [
            [
              "component",
              {
                  
                  
                "libraryName": "element-ui",
                "styleLibraryName": "theme-chalk"
              }
            ]
          ]
        }
        
      • 如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

      import Vue from 'vue';
      import {
              
               Button, Select } from 'element-ui';
      import App from './App.vue';
      
      Vue.component(Button.name, Button);
      Vue.component(Select.name, Select);
      /* 或写为
       * Vue.use(Button)
       * Vue.use(Select)
       */
      
      new Vue({
              
              
        el: '#app',
        render: h => h(App)
      });
      
    • 完整组件列表和引入方式(完整组件列表以 components.json 为准)

    • 需要注意的是,样式文件需要单独引入。import 'element-ui/lib/theme-chalk/index.css';

    • 全局配置
      引入 Element 时,可以传入一个全局配置对象。该对象目前支持 size 与 zIndex 字段。size 用于改变组件的默认尺寸,zIndex 设置弹框的初始 z-index(默认值:2000)。按照引入 Element 的方式,具体操作如下:

      • 完整引入 Element:
      import Vue from 'vue';
      import Element from 'element-ui';
      Vue.use(Element, {
              
               size: 'small', zIndex: 3000 });
      
      • 按需引入 Element:
      import Vue from 'vue';
      import {
              
               Button } from 'element-ui';
      
      Vue.prototype.$ELEMENT = {
              
               size: 'small', zIndex: 3000 };
      Vue.use(Button);
      
  • 样式处理
    • 共享全局样式变量 styles/variables.scss
    •   <style lang="scss" scoped>
        @import "~@/styles/variables.scss";
        .text {
                
                
        	color: $success-color;
        }
        </style>
      
      每次用到的时候都要import,比较麻烦,重复的动作也容易出错。所以有一种更好的办法,把它注入到全局。 也就是在webpack配置与处理器Loader传递选项。可以使用vue.config.js中的css.loaderOptions选项,例如:可以这样向所有sass/less样式传入共享的全局变量。
      module.exports = {
              
              
      	css: {
              
              
      		loaderOptions: {
              
              
      			sass: {
              
              
      				prependData: `@import "~@/styles/variables.scss"`
      			},
      			scss: {
              
              
      				prependData: `@import "~@/styles/variables.scss"`
      			},
      			less: {
              
              
      				globalVars: {
              
              
      					primary: '#fff'
      				}
      			}
      		}
      	}
      }
      
  • 接口处理
    • 配置接口代理
    • 在vue.config.js配置文件中配置devServer.proxy选项。
      devServer: {
              
              
        proxy: {
              
              
          '/boss': {
              
              
              target: 'http://eduboss.lagounews.com',
              // ws: true,
              changeOrigin: true // 把请求头中的host转换成target值
          }
        }
      }
      
    • 封装请求模块
      • 安装使用axios库: npm i axios
      • 创建src/utils/request.ts
      •   import axios from 'axios'
        
          const Request = axios.create({
                  
                  
            // 配置选项
            // baseURL
            // timeout
          })
          
          // 请求拦截器
          
          // 响应拦截器
          
          export default Request
        
        

  • 初始化路由页面相关组件
    这⾥先把这⼏个主要的⻚⾯配置出来,其它⻚⾯在随后的开发过程中配置。
    • / ⾸⻚
    • /login ⽤户登录
    • /role ⻆⾊管理
    • /menu 菜单管理
    • /resource 资源管理
    • /course 课程管理
    • /user ⽤户管理
    • /advert ⼴告管理
    • /advert-space ⼴告位管理
    // router.ts
    import Vue from 'vue'
    import Router from 'vue-router'
    import Home from './views/Home.vue'
    
    Vue.use(Router)
    
    // 路由配置规则
    export default new Router({
          
          
      routes: [
        {
          
          
          name: 'home',
          path: '/',
          component: () => import(/* webpackChunkName: 'home' */'@/views/home/index.vue')
        },
        {
          
          
          name: 'login',
          path: '/login',
          component: () => import(/* webpackChunkName: 'login' */'@/views/login/index.vue')
        },
        {
          
          
          name: 'user',
          path: '/user',
          component: () => import(/* webpackChunkName: 'user' */'@/views/user/index.vue')
        },
        {
          
          
          name: 'advert',
          path: '/advert',
          component: () => import(/* webpackChunkName: 'advert' */'@/views/advert/index.vue')
        },
        {
          
          
          name: 'advert-space',
          path: '/advert-space',
          component: () => import(/* webpackChunkName: 'advert-space' */'@/views/advert-space/index.vue')
        },
        {
          
          
          name: 'course',
          path: '/course',
          component: () => import(/* webpackChunkName: 'course' */'@/views/course/index.vue')
        },
        {
          
          
          name: 'menu',
          path: '/menu',
          component: () => import(/* webpackChunkName: 'menu' */'@/views/menu/index.vue')
        },
        {
          
          
          name: 'resource',
          path: '/resource',
          component: () => import(/* webpackChunkName: 'resource' */'@/views/resource/index.vue')
        },
        {
          
          
          name: 'role',
          path: '/role',
          component: () => import(/* webpackChunkName: 'role' */'@/views/role/index.vue')
        },
        {
          
          
          name: '404',
          path: '*', // 配置not found fallback,建议配置到最后一个的位置,避免因为版本问题,可能*之后配置路由找不到
          component: () => import(/* webpackChunkName: '404' */'@/views/error-page/404.vue')
        }
      ]
    })
    

  • 具体页面布局规划
    • 有些页面没有布局的, 例如登录页面或者404错误页面
    • 大部分页面时具有相同的布局的,不同的部分是具体的路由相对应的的视图内容。
    // 添加了页面布局过后的路由配置
    import Vue from 'vue'
    import Router from 'vue-router'
    
    Vue.use(Router)
    
    // 路由配置规则
    export default new Router({
          
          
      routes: [
        {
          
          
          name: 'login',
          path: '/login',
          component: () => import(/* webpackChunkName: 'login' */'@/views/login/index.vue')
        },
        {
          
          
          path: '/',
          component: () => import(/* webpackChunkName: 'layout' */'@/layout/index.vue'),
          children: [
            {
          
          
              name: 'home',
              path: '', // 默认子路由
              component: () => import(/* webpackChunkName: 'home' */'@/views/home/index.vue')
            },
            {
          
          
              name: 'user',
              path: '/user',
              component: () => import(/* webpackChunkName: 'user' */'@/views/user/index.vue')
            },
            {
          
          
              name: 'advert',
              path: '/advert',
              component: () => import(/* webpackChunkName: 'advert' */'@/views/advert/index.vue')
            },
            {
          
          
              name: 'advert-space',
              path: '/advert-space',
              component: () => import(/* webpackChunkName: 'advert-space' */'@/views/advert-space/index.vue')
            },
            {
          
          
              name: 'course',
              path: '/course',
              component: () => import(/* webpackChunkName: 'course' */'@/views/course/index.vue')
            },
            {
          
          
              name: 'menu',
              path: '/menu',
              component: () => import(/* webpackChunkName: 'menu' */'@/views/menu/index.vue')
            },
            {
          
          
              name: 'resource',
              path: '/resource',
              component: () => import(/* webpackChunkName: 'resource' */'@/views/resource/index.vue')
            },
            {
          
          
              name: 'role',
              path: '/role',
              component: () => import(/* webpackChunkName: 'role' */'@/views/role/index.vue')
            }
          ]
        },
        {
          
          
          name: '404',
          path: '*', // 配置not found fallback,建议配置到最后一个的位置,避免因为版本问题,可能*之后配置路由找不到
          component: () => import(/* webpackChunkName: '404' */'@/views/error-page/404.vue')
        }
      ]
    })
    

  • 布局-Container布局容器,结合element-UI
    • 布局侧边栏aside
    • 布局头部header
  • 请求登录
    • 1、表单验证
      • el-form
        • model
        • rules
      • el-form-item
        • prop
    • 2、验证通过提交表单,否则提示验证错误信息,提交请求时需要注意请求发送数据的格式。
    • 3、处理请求结果,成功则记录登录用户信息并跳转,否则提示登录错误信息。
  • 封装请求方式
    • 把应用所需要用到的接口分门别类放到不同的模块services模块中进行管理
    • user.ts 保存用户相关的api请求封装
    • resource.ts 保存资源相关的api请求封装
    • 等等 便于代码的调试查看及后续维护工作
    • 关于axios请求的Content-Type的设置(自动设置的):
      • 如果data 是普通对象,则Content-Type是 application/json
      • 如果data 是 qs.stringify(data) 转换后的urlencoded值,例如:name=anb&pass=dddd, 则Content-Type是 application/x-www-form-urlencoded
      • 如果data 是FormData对象,则Content-Type是 multipart/form-data
  • 保存状态到Vuex容器,做页面访问权限控制
    • 登录成功记录登录状态,而且这个状态Vue应用中需要可以全局访问(存放放到vuex store容器中),也就是在Vue的模块或者组件中可以访问到这个状态。
    • 然后在需要认证(登录后才有权限查看)的页面的时候判断有没有这个个登录的状态(这里就需要使用到路由拦截器)
      • 路由守卫route guard(router.beforeEach), next必须被调用。如果路由比较多,有些需要有些不需要守卫,那么在配置路由的时候我们就可以给路由配置meta元信息,再在守卫函数中进行获取meta元素数据进行判断处理该如何处理,页面是否允许跳转。
    • 为防止页面刷新时数据丢失,需要在记录登录状态到Vuex store的时候使用localStorage或者cookie把数据持久化。注意持久化的时候只能保存字符串类型。
  • 在页面或组件中获取用户登录信息用于显示或者退出登录清除等操作
    • 封装获取当前登录用户信息接口
    • 在组件中获取使用
  • 考虑到多数api需要提供token 作为authorization,使用请求拦截器统一设置Token
  • 处理用户退出登录
    • 清除用登录状态
    • 跳转到登录页

  • Token 过期的问题考虑及处理方式
    • token的过期时间一般是后端设置的
    • token过去再去请求接口可能会收到401响应
    • 可以使用响应拦截器进行错误处理,想办法避免用户频繁重新登录。
      • access_token: 获取需要授权的接口数据
      • expires_in: access_token的过期时间
      • refresh_token: 刷新获取新的access_token
    • access_token过期时,可以使用refresh_token去重新获取新的access_token,调用刷新token接口。
    • 为什么:access_token需要比较短的过期时间,出去安全考虑,降低账户信息泄露的风险。
  • access_token过期的处理
    • 方法一、在请求发起前拦截每个请求,判断token是否已经过期,若已过期,则将请求挂起,先刷新token后再用新token继续请求。能节省请求,但是需要expires_in字段支持,当是两个机器时间不一致的时候就会出现问题。
    • 方法二、不在请求发起前拦截,而是在拦截返回后的数据。先发起请求,接口返回过期后,先刷新token,再进行一次重试。它会消耗一次请求,知道过期后再刷新并重试。
    • 具体的情况根据后端接口的支持而定。以上两种情况处理的优缺点是互补的,方法一有校验失败的风险(本地时间被篡改时,也就是两个机器时间不同步,加上发起请求建立连接存在一定的时间差,容易造成校验失败)。方法二比较简单粗暴,等知道服务器明确告知token过期之后再来刷新token并重试一次,只会消耗多一个请求。这里推荐使用方式二进行token过期处理。
  • 设置请求响应拦截器
    • 由于每一个需要授权的接口都有可能返回401,token过期。所以最好的方式是通过请求响应拦截器来进行统一的处理。
    • 如果后端自定义了错误状态码,全部返回2xx,则在成功响应拦截处理函数中做判断处理。
    • 若果是使用的HTTP状态码,则在错误响应处理函数中判断处理。
    • 具体情况是后端的接口返回结果进行判断处理即可。
  • token过期情况处理响应拦截处理
    • 请求收到啊响应,且响应状态码超出 2XX 范围
    • 请求发出去了,但是没有收到响应 如请求超时或者网路断开等情况
    • 在给请求设置的config配置时触发了错误

  • 菜单管理-添加菜单CURD
  • 菜单管理-添加菜单-处理上级菜单
  • 菜单管理-展示菜单列表
  • 展示资源列表
  • 资源管理-资源列表分页处理
  • 资源管理-列表数据筛选
  • 角色权限管理
  • 角色管理-添加角色/编辑角色 - dialog
  • 角色管理-分配菜单
  • 角色管理-分配资源
  • 用户管理
  • 用户管理-分配角色
  • [ ]

猜你喜欢

转载自blog.csdn.net/u011024243/article/details/126198938