主要讲解Vue(Vue.js + Vuex + TypeScript)实战中遇到的一些技术点。
在vue项目中使用ts有两种情况:
在新项目中使用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,只需要安装之后,然后引入即可。
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')
服务处理是为开发环境配置跨域处理,使得能够正常访问后台接口。
客户端配置服务端代理跨域需要在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
}
}
}
}
router.beforeEach((to,from,next)=>{}) // 在路由跳转之前会触发回调。必须调用next,否则不会路由不会变化
// 全局前置守卫:任何页面的访问都要经过这里 (路由拦截器)
// to: 要去哪里的路由信息
// from: 从哪里来的路由信息
// next: 通行的标志
router.beforeEach((to, from, next) => {
console.log('进入了路由全局守卫')
console.log('to ==>', to)
console.log('from ==>', from)
// to.matched 是一个数组(匹配到是路由记录)
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()
})
每次请求发送之前都会执行回调函数,回调函数返回config将作为最终请求下发的配置。
// 请求拦截器
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
console.log(config, '接口进来了')
// 我们就在这里通过改写config配置信息来实现业务功能的统一处理
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
// 注意:这里一定要返回config,否则请求就发不出去了
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
请求返回响应之后,如果返回的状态码是2xx,将执行第一个回调函数,即成功的回调函数,非2xx状态码将执行第二个回调函数,即失败的回调函数。
import axios from 'axios'
import store from '@/store'
import { Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'
const request = axios.create({
// 配置选项
// baseURL
// timeout
})
// 请求拦截器
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
console.log(config, '接口进来了')
// 我们就在这里通过改写config配置信息来实现业务功能的统一处理
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
// 注意:这里一定要返回config,否则请求就发不出去了
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 响应拦截器
let isRefreshing = false // 控制刷新 token 的状态
let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求
request.interceptors.response.use(
function (response) {
// 状态码为2xx都会进这里
// 如果是自定义错误状态码,错误处理就写到这里
return response
},
async function (error) {
// 超出2xx状态码都执行这里
// console.log('请求响应失败了 ==> ', error)
// 如果是使用的 HTTP 状态码,错误处理就写到这里
// console.dir(error)
if (error.response) {
// 请求收到响应了,但是状态码超出了2xx范围
// 400 401 403 404 405 500
const { status } = error.response
if (status === 400) {
Message.error('请求参数错误')
} else if (status === 401) {
// token无效(没有提供token、token是无效的、token过期了)
// 如果有refresh_token,则尝试使用refresh_token 获取新的 access_token
// 成功了 -> 把本次失败的请求重新发出去
// 失败了 -> 跳转登录页重新登录获取新的 token
// 如果没有,则直接跳转登录页
if (!store.state.user) {
redirectLogin()
return Promise.reject(error)
}
// 尝试刷新获取新的token
if (!isRefreshing) {
isRefreshing = true // 开启刷新状态
return refreshToken()
.then(res => {
if (!res.data.success) {
throw new Error('刷新 Token 失败')
}
// 无痛刷新,用户无感知(刷新token只能使用一次)
// 把刷新拿到的新的access_token更新到容器和本地存储中
// 刷新 token 成功了
store.commit('setUser', res.data.content)
// 把 requests 队列中的请求重新发出去
requests.forEach(cb => cb())
// 重置requests 数据
requests = []
// 成功了 -> 把本次失败的请求重新发出去
// console.log(error.config) // 失败请求的配置信息
return request(error.config)
})
.catch(err => {
console.log(err)
// 把当前登录用户状态清除
store.commit('setUser', null)
// 失败了 -> 跳转登录页重新登录获取新的 token
redirectLogin()
return Promise.reject(error)
})
.finally(() => {
isRefreshing = 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)
}
)
function redirectLogin () {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
}
function refreshToken () {
return axios.create()({
method: 'POST',
url: '/front/user/refresh_token',
data: qs.stringify({
refreshtoken: store.state.user.refresh_token
})
})
}
export default request
yarn build
dist目录需要启动一个HTTP服务器来访问(除非你已经将publicPath配置为一个相对的值),所以以file://协议直接打开dist/index.html是不会工作的。在本地预览生产环境构建最简单的方式就是使用一个node.js静态文件服务器,如serve:
npm install -g serve
# -s 是将其架设在 Single-Page-Application模式下
# 这个模式会处理即将提到的路由问题
serve -s dist
如果你在 history 模式下使用 Vue Router,是无法搭配简单的静态文件服务器的。例如,如果你使用 Vue Router 为 /todos/42/ 定义了一个路由,开发服务器已经配置了相应的 localhost:3000/todos/42 响应,但是一个为生产环境构建架设的简单的静态服务器会却会返回 404。
为了解决这个问题,你需要配置生产环境服务器,将任何没有匹配到静态文件的请求回退到 index.html。
如果前端静态内容和后端 API 同源,则不需要做任何跨域处理。
如果不在同一个域名商,则需要处理:
方式一: 配置服务端代理
方式二:让后台接口服务启用CORS支持
方式三:使用node.js写脚本解决代理
test-serve/app.js
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 ... ')
})
监视运行:
nodemon .\app.js
如果你的网站应用部署在HTTPS协议下,则你的接口服务也必须是HTTPS协议。
如果你使用了 PWA 插件,那么应用必须架设在 HTTPS 上,这样 Service Worker 才能被正确注册。
建议参考:https://cli.vuejs.org/zh/guide/deployment.html