# 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
{{ count }}
```
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 中每种数据类型属性的类型定义有待复习!