-
vue create projectName
创建项目;默认ts转码会顺便转es6语法,此处我们让babel进行转不用ts的转;sass建议选择dart-sass。 - git status,git init,
git add remote origin http://...
给仓库起个origin别名方便提交;git push -u origin master
第一次推送指定到origin仓库的master分支,通过-u记录本次提交信息避免以后每次提交都要写仓库名和分支;git remote rename test origin
改仓库名;git remote -v
查看仓库信息; - .browserslistrc浏览器兼容处理文件,shims.vue.d.ts是ts配置文件,比如ts不认识.vue文件,需要进行声明;shims.tsx.d.ts是补充tsx的配置,如果没用到tsx不用管;Vetur插件提供了SFC单页面组件的支持;vue-class-component提供使用class语法写vue组件,vue-property-decorator在class语法上提供了一些辅助装饰器。
- 三种写法:https://class-component.vuejs.org/用类的写法,所有data钩子method是同级的;options api写法如下;还有类加装饰器,装饰器只是ES草案暂不建议使用。
- 人少用standard标准,人多用爱彼迎或google标准。vue等校验需要导入插件。github.com/typescript-eslint查看ts的eslint规则。
<script lang="ts">
import Vue from 'vue'
export default Vue.extends({
data(){},
methods: {}
})
</script>
- 全局样式可以直接在main.js里导入,变量样式可以根目录增加vue.config.js配置文件在scss预处理时全局导入。https://cli.vuejs.org/zh/可以看到vue-cli配置。新版的vue-cli将webpack很多东西集成了,直接再vue.config.js配置即可,如代理也可以统一在此配置。
module.exports = {
css: {
loaderOptions: {
scss: {
prependData: '@import "~@/styles/variables.scss"'
}
}
},
devServer: {
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
},
}
}
}
- 路由懒加载最后生成默认是序号,不方便定位维护,建议写上别名。嵌套路由公共部分不需要懒加载,子路由可以懒加载,默认子路由path可以设为''。
import VueRouter, {RouteConfig} from 'vue-router';
import Layout from '@/layouts/index.vue'
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'login'
component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '*',
name: '404'
component: () => import(/* webpackChunkName: '404' */ '@/views/error/404.vue')
},
{
path: '/',
name: Layout,
children: [
{
path: '',
name: 'home'
component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
}
]
}
]
- 接口调试可以下载postman,然后先测试接口是否正常,再进行对接;可以在api key给header加登录token。axios默认是用json发送的请求,若需要用x-www-form-unlencoded,需要加载qs库做处理。提交请求的时候加个loading,避免用户网络慢时频繁重复提交请求。elementUI实例调用方法时ts可能会报错,因为默认认为是vue实例,需要手动指定类型,可利用编辑器快捷跳转查看源码定位类型。
import {Form} from 'element-ui'
await (this.$refs.form as Form).validate()
- 封装请求方法,如果data是普通对象,则axios默认用json格式发送;如果用qs,则默认用x-www-form-urlencoded;如果是formData则默认用multipart;这样可以省去写contentType。
// services/user.ts
import request from '@/utils/request'
import qs from 'qs'
interface User {
phone: string,
password: string
}
export const login = (data: User) => {
return request({
method: 'POST',
url: '/front/user/login',
// headers: {'content-type': 'application/x-www-form-urlencoded'},
data: qs.stringify(data)
})
}
try {
this.isLoading = true
const {data} = await login(this.form)
if (data.state !== 1) {
this.$message.error(data.message)
} else {
this.$router.push(this.$route.query.redirect as string || '/') // 重定向回原来页面
this.$message.success('登录成功')
}
} catch(err) {
console.log('登录失败', err)
}
this.isLoading = false
- 将登录状态保存在vuex中,且需要做持久化即存在localStorage里,不然每次刷新登录信息就没了。获取动态src时需要require('@/assets/img/test.png')否则无法获取。退出的时候清除登录状态并跳转到登录页面即可。
export default new Vuex.Store({
state: {
user: JSON.parse(localStorage.getItem('user') || 'null') // 只接受字符串
},
mutations: {
setUser(state, payload) {
state.user = JSON.parse(payload)
localStorage.setItem('user', payload)
}
}
})
logout() {
this.$store.commit('setUser', null) // 本地存储数据也会同步清空
this.$router.push({name: 'login'})
}
- 路由中配置meta元信息可以给每个路由设置参数,比如设置该路由是否需要登录访问
meta: {requiresAuth: true}
,不设置默认也有空的meta。利用路由守卫钩子可以校验是否登录,beforeEach访问每个路由前都需要经过这个钩子。父路由需要登录则子路由也需要,反之亦然。
router.beforeEach((to, from, next) => {
// to.matched是一个数组(匹配到的路由记录,包括父子路由)
if (to.matched.some(item => item.meta.requiresAuth)) {
if (store.state.user) {
next()
} else {
next({
name: 'login' ,
query: {
redirect: to.fullPath
}
})
}
} else {
next()
}
})
- 每个接口请求需要token验证,可以在请求增加拦截器,统一增加token。刷新token方式一:拦截请求,判断token是否过期,过期刷新再请求。需要后端过期时间,有bug系统时间会被纂改但少请求。方式二是请求,如果后端返回401,则刷新token重新请求,会多个请求但没有bug。
import axios from 'axios'
import {Message} from 'element-ui'
import store from 'store'
import router from 'router'
import qs from 'qs'
const request = axios.create({
})
function redirectLogin() {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath // 登录后要重定向的路径
}
})
}
function refreshToken() {
return axios.create({ // 此处要用新的axios,不然此处接口401又会进行此处请求导致循环
method: 'POST',
url: '/font/user/refresh_token',
data: qs.stringify({
refreshtoken: store.state.user.refresh_token // 该token只能使用一次,若同时多个接口401都用同个token请求只有第一次成功
})
})
}
// 请求拦截器
request.interceptors.request.use(function(config){
const {user} = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
return config // 一定要返回config,不然请求发不出去
}, function(error){
return Promise.reject(error)
})
let isRefreshing = false
let requests = [] // 存储刷新token期间的请求,等token刷新完要全部重新请求
// 响应拦截器
request.interceptors.response.use(function(response){
// 状态码是2XX
return response
}, function(error){ // 需要返回一个promise对象
if (error.response) { // 请求响应了,但状态码超过2XX
const {status} = error.response
if (status === 401) {
// 如果连用户都没有,则直接跳转到登录
if (!store.state.user) {
redirectLogin()
}
if (!isRefreshing) {
isRefreshing = true
// 尝试获取新的token
return refreshToken()
.then(res => {
if (!res.data.success) {
throw new Error('刷新 token 失败') // 抛出错误让下面的catch处理
}
// 成功获取token之后,把新token存储,然后重新发送失败的请求
store.commit('setUser', res.data.content)
requests.forEach(cb => cb()) // 此时才触发之前挂起的then的成功回调
requests = [] // 执行完清空
// requests不包含第一次401的失败请求,因为第一次isRefreshing为false进入当前逻辑,所以要单独再发送下第一次失败请求
return request(error.config) // error.config是请求的配置信息,return的数据作为下一个then成功回调的参数
})
.catch(err => {
// 获取失败后处理:先清除用户信息,再跳转登录
store.commit('setUser', null)
redirectLogin()
return Promise.reject(error)
})
.finally(() => {
isRefreshing = false
})
}
// 如果是正在刷新,则先把请求挂起,存到缓冲队列里
return new Promise((resolve, reject) => {
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) // 抛出错误给调用者处理
})
export default request
- H5有个progress事件用于监听上传的,axios里封装了
onUploadProgress(e) { Math. floor(e.loaded / e.total * 100 }
- 富文本编辑器:ckeditor、quilljs、mediumEditor(更新少)、wangEditor、ueditor(没维护)、tinymce。在vue中尽量别操作dom,可通过ref操作,this.$refs.editor获取到实例。
<script lang="ts">
import Vue from 'vue'
import E from 'wangeditor'
export default {
name: 'TextEditor',
props: {
value: {
type: String,
default: '',
},
},
mounted() {
this.initEditor()
},
methods: {
initEditor() {
const editor = new E(this.$refs.editor as any)
// 监听必须在 create 之前,初始化设置在 create 之后
editor.config.onchange = (value: string) => {
this.$emit('input', value) // v-model的语法糖
}
editor.create()
editor.txt.html(this.value)
// 图片上传会自动触发以下方法
editor.config.customUploadImg = async function(resultFiles: any, insertImgFn: any) {
const fd = new FormData()
fd.append('file', resultFiles[0])
const {data} = await uploadCourseImage(fd)
insertImgFn(data.data.name)
}
},
},
}
</script>
- async函数返回的是一个promise,函数返回值是then回调的参数;await后面跟一个promise,promise的resolve值即await的执行结果。
::v-deep .el-tree-node__content {}
解决scoped里作用域不生效的问题。 - 视频点播一般都用第三方服务,比如阿里云点播。先请求后台,让后台去请求阿里云拿到token;拿到token之后上传视频。前端先下载上传的sdk放到public里,由于不支持import则需要在index.html里用script全局引入。script引入的这些静态资源不会被webpack打包。/* eslint-disable */忽略eslint校验,ts识别到window上没有AliyunUpload会报错,在shims-vue.d.ts里面增加声明。
declare module '*.vue' {
import vue from 'vue'
export default Vue
}
interface Window {
AliyunUpload: any
}
- dist打包生成的html需要本地开启服务器去预览,用node来实现,其中接口代理用中间件http-proxy-middleware来实现。然后通过
node app.js
启动该文件访问dist/index.html。
const express = require('express')
const app = express()
const path = require('path')
const {createProxyMiddleware} = require('http-proxy-middleware')
// 托管了dist目录,当访问/默认访问dist/index.html
app.use(express.static(path.join(__dirname, '../dist')))
app.use('/api', createProxyMiddleware({
target: 'http://test.com',
changeOrigin: true,
}))
app.listen(3000, () => {
})
- 部署注意事项:
- 项目中如果用了history模式则需要做配置,参考vue-cli。
- 如果和服务器部署在一起则不需要跨域,如果不在一起则需要代理或cors。
- 如果部署在https上,接口也必须是https。如果用了pwa也必须是https。
权限:菜单权限(哪些页面可以访问)、资源权限(哪些接口可以访问)
页面路由记住筛选数据,part3-6用户权限模块基本就是念API,nuxt.js也是念api,vue-router实现也是念源码没有思路和扩充。