本模块主要讲解Vue实战中遇到的一些技术点,通过一个课程管理项目的实操来讲解在实战中如何使用Vue进行开发。
Vue项目中使用TypeScript
在vue项目中使用ts有两种情况,一、在新项目中使用TypeScript;二、在已有Vue项目中使用TypeScript。
在新项目中使用TypeScript
使用@vue/cli工具创建是选择使用ts即可。创建完的项目中会有两个ts文件。
其中shims-ts.d.ts文件是jsx语法的类型补充
/**
* Jsx 类型声明补充
*/
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}
shims-vue.d.ts则是.vue文件的类型声明
/**
* import xx from 'xxx.vue'
* ts 无法识别.vue文件
* 通过这个声明.vue模块都是Vue
*/
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
在已有项目中使用TypeScript
只要使用@vue/cli来安装ts插件即可。
vue add @vue/typescript
TypeScript方式定义vue组件
TypeScript定义vue组件有两种方式:
-
使用Vue.component或Vue.extend定义组件,即OptionApi的方式定义组件。
import Vue from 'vue' export default Vue.extend({ name: 'App' })
-
使用vue-class-component装饰器。装饰器语法尚未定案,并不稳定,不推荐在生成中使用。
import Vue from 'vue' import Component from 'vue-class-component' @Component({ name: 'App' // 选项参数 }) export default class App extends Vue { // 初始数据可以直接声明为实例的 property message: string = 'Hello!' // 组件方法也可以直接声明为实例的方法 onClick (): void { window.alert(this.message) } }
在Vue项目中使用elementUI
要在Vue项目中使用elementUI,只需要安装之后,然后引入即可。
elementUI的引入有两种情况,完整引入和按需引入。
完整引入
引入整个Element。在main.ts中添加以下代码
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
按需引入
需要借助 babel-plugin-component插件。安装插件之后,修改babel配置如下:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
在main.ts中注册要使用的组件
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)
});
项目中样式处理
在src/styles中创建下列四个样式文件
然后在main.ts中引入全局样式文件index.scss
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
// 引入全局样式,在全局样式中引入了element的样式
import './styles/index.scss'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
共享全局样式变量
在组件中要使用全局的样式变量,需要在样式文件中引入全局的样式变量文件,之后才能使用
<style lang="scss" scoped>
@import './style/variable.scss';
.text {
color: $success-color;
}
</style>
但是这样需要在使用的组件里都需要引入样式文件,比较麻烦。设置共享全局样式变量来避免这种麻烦。
根目录下创建vue.config.js,添加下列代码。会为每个组件的style注入prependData中的内容。
module.exports = {
css: {
loaderOptions: {
// 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
// 因为 `scss` 语法在内部也是由 sass-loader 处理的
// 但是在配置 `prependData` 选项的时候
// `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
// 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
scss: {
prependData: '@import "~@/styles/variables.scss";'
}
}
}
}
这样在组件中就可以直接使用全局样式变量,不用在组件中手动引入文件。
<style lang="scss" scoped>
.text {
color: $success-color;
}
</style>
接口处理
接口处理是为开发环境配置跨域处理,使得能够正常访问后台接口。
客户端配置服务端代理跨域需要在vue.config.js中配置devServer的代理。会拦截proxy中包含属性字符串的请求,将其替换成target。如下配置之后http://localhost:8099/boss开头的请求将转发到http://eduboss.lagou.com下。
module.exports = {
devServer: {
proxy: {
'/boss': {
target: 'http://eduboss.lagou.com ',
// ws: true, // websocket协议
changeOrigin: true // 是否修改请求头中的host
},
'/front': {
target: 'http://edufront.lagou.com',
changeOrigin: true // 是否修改请求头中的host
}
}
}
}
vue-router 路由拦截
router.beforeEach((to,from,next)=>{}) // 在路由跳转之前会触发回调。必须调用next,否则不会路由不会变化
// 路由拦截,再这里判断是否已经登录,没有登录则重定向到登录页面,并记录当前地址
router.beforeEach((to, from, next) => {
if (to.matched.some(item => item.meta.auth)) { // 判断目标地址是否需要登录,meta是自定义数据对象,在定义路由时传入的
if (!store.state.user) { // 未登录,重定向到登录页,并记录页面地址
next({
name: 'login',
query: {
redirect: to.fullPath
}
})
} else {
next()
}
} else {
next()
}
})
axios请求拦截
每次请求发送之前都会执行回调函数,回调函数返回config将作为最终请求下发的配置。
// 请求拦截器
request.interceptors.request.use(config => {
unResponseNum++
// 设置头部Authorization
if (store.state.user && store.state.user.access_token) {
config.headers.Authorization = store.state.user.access_token
}
return config
})
axios响应拦截
请求返回响应之后,如果返回的状态码是2xx,将执行第一个回调函数,即成功的回调函数,非2xx状态码将执行第二个回调函数,即失败的回调函数。
request.interceptors.response.use(res => {}, err => {})
token过期处理
token过期将会返回401状态码。需要根据情况判断是否是刷新token还是重新登录。整体流程如下:
- 首先需要判断是否是token过期还是没有登录过
- 如果是token过期,才去根据记录的刷新token属性去刷新token
- 刷新token过程中,可能有请求返回401,需要记录返回401的请求,待刷新token之后,使用新的token重新下发这些请求。
- 有一种特殊请求,有些401请求,在token刷新之后才返回,需要通过判断是否所有请求都返回了,所有请求返回之后再去重新下发请求。
import axios from 'axios'
import store from '@/store/index'
import { Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'
const request = axios.create({
// 配置选项
})
function redirectLogin () {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
needReRequest = [] // 清空需要重发的请求
}
function refleshToken () {
return axios.create()({
method: 'POST',
url: '/front/user/refresh_token',
data: qs.stringify({ refreshtoken: store.state.user.refresh_token })
})
}
// 未返回的请求的个数
let unResponseNum = 0
// 请求拦截器
request.interceptors.request.use(config => {
unResponseNum++
// 设置头部Authorization
if (store.state.user && store.state.user.access_token) {
config.headers.Authorization = store.state.user.access_token
}
return config
})
let isRefleshToken = false // 是否正在刷新token
let needReRequest: any[] = [] // 刷新token期间的401请求
function doReRequest () {
if (unResponseNum === 0) {
needReRequest.forEach(cb => cb())
needReRequest = [] // 清空请求缓存
}
}
// 响应拦截器
request.interceptors.response.use(res => {
unResponseNum--
doReRequest()
return new Promise((resolve, reject) => { // 2xx的成功响应会执行这里
if (res.data.state === 1) {
resolve(res)
} else {
Message.error(res.data.message)
reject(res)
}
})
}, error => { // 失败响应会执行这里
unResponseNum--
if (error.response) { // 请求有非2xx响应
const { status } = error.response
// 根据状态吗判断
if (status === 400) { // 请求参数错误
Message.error('请求参数错误')
} else if (status === 401) { // token 无效,可能没有传递token, token过期
// 判断是否是token过期
if (!store.state.user) { // 没有记录过token,跳转到登录页
redirectLogin()
return Promise.reject(error)
}
if (!isRefleshToken) { // 没有刷新token
isRefleshToken = true
// 已经有token,刷新token
return refleshToken().then(response => { // 请求成功,刷新store中的user信息,重新请求401状态的取数
const { data } = response
if (data.state === 1) {
// 重新记录token
store.commit('setUser', data.content)
// 重新发送刷新token期间的401请求
doReRequest()
// 重新请求
return request(error.config)
} else {
throw new Error(data)
}
}).catch(() => { // 刷新token失败,重新登录
// 清空保存的用户信息
store.commit('setUser', null)
if (unResponseNum === 0) { // 只有当所有请求都返回了时才跳转到登录
redirectLogin()
}
return Promise.reject(error)
}).finally(() => {
isRefleshToken = false
})
}
// 正在刷新token,要返回一个挂起的请求,使用promise挂起请求并保存
return new Promise(resolve => {
needReRequest.push(() => {
resolve(request(error.config))
})
doReRequest()
})
} 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}`)
}
doReRequest()
return Promise.reject(error)
})
export default request