# e-vue-boss **Repository Path**: ymcdhr/e-vue-boss ## Basic Information - **Project Name**: e-vue-boss - **Description**: vue 项目实例:vue2.0 + vuex + vue-router + typescript + element ui - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-07-17 - **Last Updated**: 2021-08-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 介绍项目 Vue + TypeScript + VueX + ElementUI 实现的课程管理系统 # 使用项目 ## 安装项目 ``` npm install ``` ### 开发环境:编译、热更新 ``` npm run lint npm run serve ``` ### 生产环境:编译、打包 ``` npm run build npm install -g serve serve -s dist ``` ### 配置代理 1. 使用 nodejs + express 代理请求 - 示例代码: ``` const express = require('express') const app = express() const path = require('path') const { createProxyMiddleware } = require('http-proxy-middleware') // 托管了 dist 目录,当访问 / 的时候,默认会返回托管目录中的 index.html 文件 app.use(express.static(path.join(__dirname, '../dist'))) app.use('/boss', createProxyMiddleware({ target: 'http://eduboss.lagou.com', changeOrigin: true })) app.use('/front', createProxyMiddleware({ target: 'http://edufront.lagou.com', changeOrigin: true })) app.listen(3000, () => { console.log('running...') }) ``` - 启动 nodejs ``` npm run preview ``` 2. 使用 nginx 配置代理 ``` server { listen 3001; server_name localhost; location / { root /home/project/e-vue-boss/dist/; index index.html index.htm; } location /boss { proxy_pass http://eduboss.lagou.com/boss; proxy_set_header Host $host:$server_port; } location /front { proxy_pass http://edufront.lagou.com/front; proxy_set_header Host $host:$server_port; } } ``` ### 注意事项 - .vuerc 为该项目创建的配置文件,可用该配置创建一个常用的 vue 项目; - 关于history模式的服务器配置:https://router.vuejs.org/zh/guide/essentials/history-mode.html; - 关于https协议,如果网站部署在https协议下,接口也必须是https协议; - 关于PWA,如果网站使用了PWA插件,也必须部署在https协议下; # Vue 中使用 TypesSript 官方文档:https://cn.vuejs.org/v2/guide/typescript.html #### TS 相关配置 1. TS 相关依赖包 ```js // dependencies "vue-class-component": "^7.2.3", => 提供使用 Class 语法写 Vue 组件 "vue-property-decorator": "^9.1.2", => 在 Class 语法基础之上提供一些辅助装饰器 // devDependencies "@typescript-eslint/eslint-plugin": "^4.18.0", => ESLint 校验 TS 代码 "@typescript-eslint/parser": "^4.18.0", => 将 TS 转换为 AST 供 ESLint 使用 "@vue/cli-plugin-typescript": "~4.5.0", => 使用 TS + ts-loader + fork-ts-checker-webpack-plugin进行更快的类型检查 "@vue/eslint-config-typescript": "^7.0.0", => 兼容 ESLint 的 TS 校验规则 "typescript": "~4.1.5", => TS 编译器,TS 类型校验,转换为 JS ``` 2. TS 配置文件:tsconfig.json 3. shims-vue.d.ts 让 TS 识别 .vue 文件 4. shims-tsx.d.ts 给 JSX\TSX 添加一些类型声明相关的补充说明 #### TS 的方式定义组件 1. 使用Options Api定义组件:如果需要开发工具和编译过程中会提示类型错误,需要以下写法 ```js ``` 2. 使用装饰器来定义组件(不建议使用,装饰器还没正式发布) - 写法更类似于定义一个 class - 完整写法参考官方文档:https://class-component.vuejs.org/ ```js ``` 3. 关于装饰器 - 作用就是扩展类的属性或者功能;版本未定,不在生产环境使用; - 相关资料:https://es6.ruanyifeng.com/#docs/decorator 4. 简化 Props 写法 - vue-class-component 中定义 Props 比较麻烦 - vue-property-decorator 简化写法,具体参考:https://www.npmjs.com/package/vue-property-decorator ```js import { Vue, Component, Prop } from 'vue-property-decorator' @Component export default class YourComponent extends Vue { @Prop(Number) readonly propA: number | undefined @Prop({ default: 'default value' }) readonly propB!: string @Prop([String, Boolean]) readonly propC: string | boolean | undefined } ``` # 项目中的代码格式规范 #### 有哪些常用的编码规范 1. 适合小型团队,Standardjs:https://standardjs.com/ 2. 适合大型团队,Airbnb:https://github.com/airbnb/javascript/tree/master/ 3. 适合大型团队,Google:https://google.github.io/styleguide/jsguide.html #### 如何约束代码规范 1. JSLint 2. JSHint 3. ESLint(最常用) #### Vue + ESLint 配置文件 1. 基础配置 ```js module.exports = { root: true, env: { node: true }, // 使用插件的编码校验规则 extends: [ 'plugin:vue/essential',// Vue官方插件:eslint-plugin-vue,用于校验template、script、指令等校验 '@vue/standard', // Standard 校验规则:@vue/eslint-config-standard '@vue/typescript/recommended'// Vue官方插件:@vue/eslint-config-typescript ], parserOptions: { ecmaVersion: 2020 }, // 自定义编码校验规则 rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' } } ``` 2. 自定义配置 - ESLint 官方文档:https://cn.eslint.org/docs/user-guide/getting-started - 其余规则:https://cn.eslint.org/docs/rules/ - 简单示例: - (1)"off" or 0 - 关闭规则 - (2)"warn" or 1 - 将规则视为一个警告(不会影响退出码) - (3)"error" or 2 - 将规则视为一个错误 (退出码为1) ``` { "rules": { "semi": ["error", "always"], "quotes": ["error", "double"] } } ``` # 项目中样式文件处理 #### 样式文件结构 1. styles - index.scss:全局样式 - mixin.scss: - reset.scss: - variables.scss:变量 2. index.css,自定义了Element UI的一些样式 ``` @import './variables.scss'; // globals html { font-family: $font-family; -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // better Font Rendering -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { margin: 0; background-color: $body-bg; } // custom element theme $--color-primary: $primary-color; $--color-success: $success-color; $--color-warning: $warning-color; $--color-danger: $danger-color; $--color-info: $info-color; // change font path, required $--font-path: '~element-ui/lib/theme-chalk/fonts'; // import element default theme @import '~element-ui/packages/theme-chalk/src/index'; // node_modules/element-ui/packages/theme-chalk/src/common/var.scss // overrides // .el-menu-item, .el-submenu__title { // height: 50px; // line-height: 50px; // } .el-pagination { color: #868e96; } // components .status { display: inline-block; cursor: pointer; width: .875rem; height: .875rem; vertical-align: middle; border-radius: 50%; &-primary { background: $--color-primary; } &-success { background: $--color-success; } &-warning { background: $--color-warning; } &-danger { background: $--color-danger; } &-info { background: $--color-info; } } ``` 3. variables.scss,共享全局变量;例如:在 App.vue 中使用变量 - 方法一:直接使用 ``` ``` - 方法二:注入到全局,在项目根目录创建vue.config.js:https://cli.vuejs.org/zh/guide/css.html#%E5%90%91%E9%A2%84%E5%A4%84%E7%90%86%E5%99%A8-loader-%E4%BC%A0%E9%80%92%E9%80%89%E9%A1%B9 ``` // vue.config.js,新版本使用:prependData module.exports = { css: { loaderOptions: { // 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效 // 因为 `scss` 语法在内部也是由 sass-loader 处理的 // 但是在配置 `prependData` 选项的时候 // `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号 // 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置 scss: { prependData: `@import "~@/styles/variables.scss";` } } } } ``` # 项目中接口统一规划 #### 配置 vue 客户端接口跨域,类似 Webpack-Dev-Server 1. 官方文档:https://cli.vuejs.org/zh/config/#devserver-proxy 2. 配置示例: ``` // vue.config.js module.exports = { css: { // ... }, // 配置本地代理 devServer: { proxy: { '/boss': { target: 'http://eduboss.lagou.com', changeOrigin: true // 把请求头中的 host 配置为 target }, '/front': { target: 'http://edufront.lagou.com', changeOrigin: true } } } } ``` #### 封装请求模块 1. 安装 axios 2. 将请求封装到 src/utils/request.js #### 请求拦截处理 #### 请求错误处理 # 登录状态规划 #### 登录状态存储与共享 1. 获取登录信息 2. 共享登录状态 3. 持久化登录状态 4. 设置默认用户头像 ``` ``` #### 登录状态控制页面权限 1. 路由前置守卫:使用路由导航钩子拦截页面,以控制页面权限;https://router.vuejs.org/zh/guide/advanced/navigation-guards.html 2. 在路由上添加是否需要登录才能访问的参数; ``` { path: '/role', name: 'role', meta: { requiresAuth: true }, component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue') }, ``` 3. 在路由钩子拦截时判断,如果是需要登录访问时再判断是否存在登录信息。 ``` // 全局前置守卫:任何页面的访问都要经过这里 // to:要去哪里的路由信息 // from:从哪里来的路由信息 // next:通行的标志,拦截后续处理 router.beforeEach((to, from, next) => { // to.matched 是一个数组(匹配到是路由记录) // 匹配 rules 里面的 meta.requiresAuth,表示需要验证登录信息 if (to.matched.some(record => record.meta.requiresAuth)) { if (!store.state.user) { // 跳转到登录页面 next({ name: 'login', query: { // 通过 url 传递查询字符串参数 redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面 } }) } else { next() // 允许通过 } } else { next() // 允许通过 } // // 路由守卫中一定要调用 next,否则页码无法展示 // next() // if (to.path !== '/login') { // // 校验登录状态 // } }) ``` #### token 过期之后的处理 1. token 过期时间比较短,主要是考虑到安全的问题 2. 处理办法,在请求的时候进行拦截: - 方法一:请求前进行拦截,根据本地时间和服务端返回的有效时间判断是否过期(本地时间可能被篡改,还需要服务端返回过期时间)。 - 方法二:响应后进行拦截,如果过期就刷新token再进行一次请求(推荐,浪费一点儿流量但是更准确)。 3. 场景错误场景处理,实际以跟后端的约定为准: - 请求收到响应,状态码为2xx - 请求收到响应,状态码超过2xx - 请求未收到响应,超时或无响应 - 请求失败 4. 无痛刷新流程(用户感知不到,需要考虑多个请求同时发出的场景) - 没有token => 跳转到登录页 - 已有token,token过期 => 刷新token - 如果 ```js // 请求拦截器,添加 token 到请求头中 request.interceptors.request.use(function (config) { // 我们就在这里通过改写 config 配置信息来实现业务功能的统一处理 const { user } = store.state if (user && user.access_token) { config.headers.Authorization = user.access_token } // 注意:这里一定要返回 config,否则请求就发不出去了 return config }, function (error) { // Do something with request error return Promise.reject(error) }) // 响应拦截器,处理 token 过期的场景 let isRfreshing = false // 控制刷新 token 的状态 let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求 request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里 // console.log('请求响应成功了 => ', response) // 如果是自定义错误状态码,错误处理就写到这里 return response }, async function (error) { // 超出 2xx 状态码都都执行这里 // console.log('请求响应失败了 => ', error) // 如果是使用的 HTTP 状态码,错误处理就写到这里 // console.dir(error) if (error.response) { // 请求发出去收到响应了,但是状态码超出了 2xx 范围 const { status } = error.response if (status === 400) { Message.error('请求参数错误') } else if (status === 401) { // token 无效(没有提供 token、token 是无效的、token 过期了) // 如果有 refresh_token 则尝试使用 refresh_token 获取新的 access_token if (!store.state.user) { redirectLogin() return Promise.reject(error) } // 刷新 token if (!isRfreshing) { isRfreshing = true // 开启刷新状态 // 尝试刷新获取新的 token return refreshToken().then(res => { if (!res.data.success) { throw new Error('刷新 Token 失败') } // 刷新 token 成功了 store.commit('setUser', res.data.content) // 把 requests 队列中的请求重新发出去 requests.forEach(cb => cb()) // 重置 requests 数组 requests = [] return request(error.config) }).catch(err => { console.log(err) store.commit('setUser', null) redirectLogin() return Promise.reject(error) }).finally(() => { isRfreshing = false // 重置刷新状态 }) } // 刷新状态下,把请求挂起放到 requests 数组中 return new Promise(resolve => { requests.push(() => { resolve(request(error.config)) }) }) } else if (status === 403) { Message.error('没有权限,请联系管理员') } else if (status === 404) { Message.error('请求资源不存在') } else if (status >= 500) { Message.error('服务端错误,请联系管理员') } } else if (error.request) { // 请求发出去没有收到响应 Message.error('请求超时,请刷新重试') } else { // 在设置请求时发生了一些事情,触发了一个错误 Message.error(`请求失败:${error.message}`) } // 把请求失败的错误对象继续抛出,扔给上一个调用者 return Promise.reject(error) }) ``` # 上传图片(elementui 结合自定义上传功能): #### 1. 上传使用的组件 ``` ``` #### 2. 上传前检查图片格式和大小:通过 before-upload 的验证才会执行 http-request ``` beforeAvatarUpload (file: any) { const isJPG = file.type === 'image/jpeg' const isLt2M = file.size / 1024 / 1024 < this.limit if (!isJPG) { this.$message.error('上传头像图片只能是 JPG 格式!') } if (!isLt2M) { this.$message.error(`上传头像图片大小不能超过 ${this.limit}MB!`) } return isJPG && isLt2M }, ``` #### 3. 上传过程中展示进度 ``` const { data } = await uploadCourseImage(fd, e => { this.percentage = Math.floor(e.loaded / e.total * 100) }) ``` #### 4. 上传接口,展示进度的方法 ``` export const uploadCourseImage = (data: any, onUploadProgress: (progressEvent: ProgressEvent) => void) => { // 该接口要求的请求数据类型是:multipart/form-data // 所以需要提交 FormData 数据对象 return request({ method: 'POST', url: '/boss/course/upload', data, // HTML5 新增的上传响应事件:progress onUploadProgress }) } ``` #### 5. 上传成功预览图片 ``` async handleUpload (options: any) { try { this.isUploading = true const fd = new FormData() fd.append('file', options.file) const { data } = await uploadCourseImage(fd, e => { this.percentage = Math.floor(e.loaded / e.total * 100) }) if (data.code === '000000') { this.isUploading = false this.percentage = 0 this.$emit('input', data.data.name) } else { this.$message.error('上传失败') } } catch (err) { console.log(err) } this.isUploading = false this.percentage = 0 } ``` # 上传视频 #### 使用场景 1. 通常小公司使用阿里云的视频上传点播服务会比较划算 #### 官方文档 - [阿里云客户端上传](https://help.aliyun.com/document_detail/55398.html?spm=a2c4g.11186623.6.680.65916b85qBUlcT) - [JavaScript上传SDK](https://help.aliyun.com/document_detail/52204.htm?spm=a2c4g.11186623.2.20.7112363b1tYpwJ#multiTask10699) #### 使用示例 1. 引入阿里云SDK 2. 获取上传凭证(图片、视频) 3. 上传视频封面图片 4. 上传视频 5. 请求视频转码 # 比较稳定的编辑器组件 1. ckeditor5:https://ckeditor.com/ckeditor-5/ 2. quill 3. medium-editor:功能强大,更新慢 4. wangEditor: 5. ueditor:百度,不再维护了 6. tinymce # 遗留问题: 1. @click.native ``` 登出 ``` 2. 路由配置:如何将路由路径参数映射到组件的 props 参数中 - 使用场景:使用路由参数传递数据给组件,可以不用再使用 this.$route.param 来获取,直接使用 props - 具体用法: ``` // 路由 { path: '/role/:roleId/alloc-menu', name: 'alloc-menu', component: () => import(/* webpackChunkName: 'alloc-menu' */ '@/views/role/alloc-menu.vue'), props: true // 将路由路径参数映射到组件的 props 数据中 }, // 链接 @click="$router.push({ name: 'alloc-menu', params: { roleId: scope.row.id } })" // 组件拿到数据 props: { roleId: { type: [String, Number], required: true } }, ``` 3. TS 中每种数据类型属性的类型定义有待复习!