Vue实战总结

主要讲解Vue(Vue.js + Vuex + TypeScript)实战中遇到的一些技术点。

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')

服务处理

服务处理是为开发环境配置跨域处理,使得能够正常访问后台接口。
客户端配置服务端代理跨域需要在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,否则不会路由不会变化

// 全局前置守卫:任何页面的访问都要经过这里 (路由拦截器)
// 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()
})

axios请求拦截

每次请求发送之前都会执行回调函数,回调函数返回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)
  }
)

axios响应拦截

请求返回响应之后,如果返回的状态码是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.pushState 的路由

如果你在 history 模式下使用 Vue Router,是无法搭配简单的静态文件服务器的。例如,如果你使用 Vue Router 为 /todos/42/ 定义了一个路由,开发服务器已经配置了相应的 localhost:3000/todos/42 响应,但是一个为生产环境构建架设的简单的静态服务器会却会返回 404。

为了解决这个问题,你需要配置生产环境服务器,将任何没有匹配到静态文件的请求回退到 index.html。

关于接口跨域问题

如果前端静态内容和后端 API 同源,则不需要做任何跨域处理。
如果不在同一个域名商,则需要处理:

  • 方式一: 配置服务端代理

    • Nginx
    • Apache
    • tomcat
    • IIS
  • 方式二:让后台接口服务启用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协议下,则你的接口服务也必须是HTTPS协议。

关于PWA

如果你使用了 PWA 插件,那么应用必须架设在 HTTPS 上,这样 Service Worker 才能被正确注册。

部署

建议参考:https://cli.vuejs.org/zh/guide/deployment.html

你可能感兴趣的