18

Typescript+Vue大型项目构建+详细讲解

 4 years ago
source link: https://juejin.im/post/5e427f75f265da575e37a51d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在开撸之前,先把文档网址贴一贴

Typescript 中文手册

vue-property-decorator

vuex-class

vue-class-component

先来认识下面的小玩意

vue-property-decorator

这里单页面组件的书写采用的是 vue-property-decorator 库,该库完全依赖于 vue-class-component ,也是 vue 官方推荐的库。

单页面组件中,在 @Component({}) 里面写 propsdata 等调用起来极其不方便,而 vue-property-decorator里面包含了 8 个装饰符则解决了此类问题,他们分别为:

  • @Emit 指定事件emit,可以使用此修饰符,也可以直接使用 this.$emit()
  • @Inject 指定依赖注入
  • @Mixins mixin 注入
  • @Model 指定 model
  • @Prop 指定 Prop
  • @Provide 指定 Provide
  • @Watch 指定 Watch
  • @Component export from vue-class-component
import {
  Component, Prop, Watch, Vue
} from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
  dataA: string = 'test'
  count = 0
  
  @Prop({ default: 0 }) private propA!: number
  @Prop({ default: () => [10, 20, 30, 50] }) private propB!: number[]
  @Prop({ default: 'total, sizes, prev, pager, next, jumper' }) private propC!: string
  @Prop({ default: true }) private propD!: boolean,
  @prop([String, Boolean]) propE: string | boolean;
  
  @Emit('reset')
  resetCount() {
    this.count = 0
  }
  @Emit()
  returnValue() {
    return 10
  }
  @Emit()
  onInputChange(e) {
    return e.target.value
  }
  
  // watcher
  @Watch('child')
  onChildChanged (val: string, oldVal: string) {}
  @Watch('person', { immediate: true, deep: true })
  onPersonChanged (val: Person, oldVal: Person) {}

  // 其他修饰符详情见上面的 github 地址,这里就不一一做说明了
}
复制代码

解析之后会变成

export default {
  data () {
    return {
      dataA: 'test'
    }
  },
  props: {
    propA: {
	  type: Number
    },
    propB: {
      type: Array,
      default: [10, 20, 30, 50]
    },
    propC: {
      type: String,
      default: 'total, sizes, prev, pager, next, jumper'
    },
    propD: {
      type: String,
      default: 'total, sizes, prev, pager, next, jumper'
    },
    propE: {
      type: [String, Boolean]
  },
    
  watch: {
    'child': {
      // handler:其值是一个回调函数。即监听到变化时应该执行的函数。
      handler: 'onChildChanged',
      // immediate:其值是true或false;immediate:true代表如果在 wacth
      // 里声明了之后,就会立即先去执行里面的handler方法,如果为
      // false就跟我们以前的效果一样,不会在绑定的时候就执行
      immediate: false,
      // deep:其值是true或false;确认是否深入监听。deep的意思就是深入观
      // 察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器(
      // 受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue 
      // 不能检测到对象属性的添加或删除)
      deep: false
    },
    'person': {
      handler: 'onPersonChanged',
      immediate: true,
      deep: true
    }
  },
  methods: {
     resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    
    returnValue() {
      this.$emit('return-value', 10)
    },
    
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    }
 
    onChildChanged (val, oldVal) {},
    onPersonChanged (val, oldVal) {}
  }
}
复制代码

这里有两个常用修饰符!``?,!和可选参数?是相对的, !表示强制解析(也就是告诉typescript编译器,我这里一定有值),你写?的时候再调用,typescript会提示可能为undefined

@Emit

@Emit装饰器的函数会在运行之后触发等同于其函数名(驼峰式会转为横杠式写法)的事件, 并将其函数传递给$emit

  • @Emit()不传参数,那么它触发的事件名就是它所修饰的函数名.
  • @Emit(name: string),里面传递一个字符串,该字符串为要触发的事件名

@Watch

watch 是一个对象,对象就有键,有值。

  • 第一个handler:其值是一个回调函数。即监听到变化时应该执行的函数。
  • 第二个是deep:其值是truefalse;确认是否深入监听。deep的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器(受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue不能检测到对象属性的添加或删除)
  • 第三个是immediate:其值是truefalseimmediate:true代表如果在 wacth 里声明了之后,就会立即先去执行里面的handler方法,如果为 false就跟我们以前的效果一样,不会在绑定的时候就执行

@Watch使用非常简单,接受第一个参数为要监听的属性名, 第二个属性为可选对象。@Watch所装饰的函数即监听到属性变化之后应该执行的函数。 @Watch装饰的函数的函数名并非如上onStateChanged严格命名,它是多元化的,你可以随心所欲的命名,当然,能按照规范化的命名会使你的代码阅读性更好。

@Minxins

// myMixin.ts
import { Vue, Component } from 'vue-property-decorator';
declare module 'vue/types/vue' {
    interface Vue {
        mixinValue: string;
    }
}
@Component
export default class myMixins extends Vue {
    mixinValue: string = 'Hello World!!!'
}
复制代码
import { Vue, Component, Prop } from 'vue-property-decorator';
import MyMixin from './myMixin.js'

@Component({
    mixins: [MyMixin]
})
export default class extends Vue{
    created(){
        console.log(mixinValue) // => Hello World!!!
    }
}
复制代码

mixin另一写法,在下面会有出现。

@Model

@Model装饰器允许我们在一个组件上自定义v-model,接收两个参数:

  • event: string 事件名。
  • options: Constructor | Constructor[] | PropOptions 与@Prop的第一个参数一致。
import { Vue, Component, Model } from 'vue-property-decorator'

@Component
export default class MyInput extends Vue {
  @Model('change', { type: String, default: 'Hello world!!!' }) readonly value!: string
}
复制代码
<template>
  <input
    type="text"
    :value="value"
    @change="$emit('change', $event.target.value)"
  />
</template>

export default {
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    value: {
      type: String,
      default: 'Hello world!!!'
    }
  }
}
复制代码

@Provide @Inject

@Provide 声明一个值 , 在其他地方用@Inject 接收,在实战项目中用得不多,一般用于不依赖于任何第三方状态管理库(如vuex)的组件编写

@Ref(refKey?: string)

@Ref装饰器接收一个可选参数,用来指向元素或子组件的引用信息。如果没有提供这个参数,会使用装饰器后面的属性名充当参数

import { Vue, Component, Ref } from 'vue-property-decorator'
import { Form } from 'element-ui'

@Componentexport default class MyComponent extends Vue {
  @Ref() readonly loginForm!: Form
  @Ref('changePasswordForm') readonly passwordForm!: Form

  public handleLogin() {
    this.loginForm.validate(valide => {
      if (valide) {
        // login...
      } else {
        // error tips
      }
    })
  }
}
复制代码
export default {
  computed: {
    loginForm: {
      cache: false,
      get() {
        return this.$refs.loginForm
      }
    },
    passwordForm: {
      cache: false,
      get() {
        return this.$refs.changePasswordForm
      }
    }
  }
}
复制代码
? Please pick a preset:(使用上下箭头)
 ◯ default (babel, eslint)        //默认配置
❯◉ Manually select features       //手动选择
复制代码
? Check the features needed for your project:
 ◉ Babel                                    // javascript转译器
 ◉ TypeScript                               // 使用 TypeScript 书写源码
 ◯ Progressive Web App (PWA) Support        // 渐进式WEB应用
 ◉ Router                                   // 使用vue-router
 ◉ Vuex                                     // 使用vuex
 ◉ CSS Pre-processors                       // 使用css预处理器
❯◉ Linter / Formatter                       // 代码规范标准
 ◯ Unit Testing                             // 单元测试
 ◯ E2E Testing                              // e2e测试
复制代码

是否使用class风格的组件语法: 使用前:home = new Vue()创建vue实例 使用后:class home extends Vue{}

? Use class-style component syntax? (Y/n) Y

// 使用Babel与TypeScript一起用于自动检测的填充
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Y

// 路由
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y

// 预处理器
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
❯◉ Sass/SCSS (with dart-sass)    // 保存后编译
 ◯ Sass/SCSS (with node-sass)    // 实时编译 
 ◯ Less
 ◯ Stylus

// 代码格式化检测
? Pick a linter / formatter config: (Use arrow keys)
 ◯ ESLint with error prevention only     // 只进行报错提醒
 ◯ ESLint + Airbnb config                // 不严谨模式
 ◯ ESLint + Standard config              // 正常模式
 ◯ ESLint + Prettier                     // 严格模式
❯◉ TSLint(deprecated)                    // typescript格式验证工具

// 代码检查方式
? Pick additional lint features: (Press <space> to select, <a>
to toggle all, <i> to invert selection)
❯◉ Lint on save             // 保存检查
 ◯ Lint and fix on commit   // commit时fix

// 文件配置
? Where do you prefer placing config for Babel, ESLint, etc.? (
Use arrow keys)
  In dedicated config files // 配置在独立的文件中
❯ In package.json
  
// 保存上述配置,保存后下一次可直接根据上述配置生成项目
? Save this as a preset for future projects? (y/N) N

// 创建成功
🎉  Successfully created project vue-typescript-admin-demo.
复制代码

yarn run serve运行项目之后会报一堆莫名的错误,这都是 tslint.json 搞的鬼,配置一下重新运行即可

// tsconfig.json
Error: Calls to 'console.log' are not allowed.

Error: 去除行尾必加';'

Error: 禁止自动检测末尾行必须使用逗号,always总是检测,never从不检测,ignore忽略检测

"rules": {
    "no-console": false,
    "semicolon": [
        false,
        "always"
    ],
    "trailing-comma": [true, {
        "singleline": "never",
        "multiline": {
            "objects": "ignore",
            "arrays": "ignore",
            "functions": "never",
            "typeLiterals": "ignore"
        }
    }]
}

复制代码

至此,整个项目算是正常运行起来了。But... 这还是传统的Vue项目,我们要开发的是Vue+ts实战项目,所以需要改造一番,详细的目录结构,等改造完之后再附上吧。

这是改造后的目录结构

├── public                          // 静态页面
├── scripts                         // 相关脚本配置
├── src                             // 主目录
    ├── assets                      // 静态资源
    ├── api                         // axios封装
    ├── filters                     // 过滤
    ├── lib                         // 全局插件
    ├── router                      // 路由配置
    ├── store                       // vuex 配置
    ├── styles                      // 样式
    ├── types                       // 全局注入
    ├── utils                       // 工具方法(全局方法等)
    ├── views                       // 页面
    ├── App.vue                     // 页面主入口
    ├── main.ts                     // 脚本主入口
    ├── registerServiceWorker.ts    // PWA 配置
├── tests                           // 测试用例
├── .editorconfig                   // 编辑相关配置
├── .npmrc                          // npm 源配置
├── .postcssrc.js                   // postcss 配置
├── babel.config.js                 // preset 记录
├── cypress.json                    // e2e plugins
├── f2eci.json                      // 部署相关配置
├── package.json                    // 依赖
├── README.md                       // 项目 readme
├── tsconfig.json                   // ts 配置
├── tslint.json                     // tslint 配置
└── vue.config.js                   // webpack 配置

复制代码

主要涉及 shims-tsx.d.tsshims-vue.d.ts 两个文件

  • shims-tsx.d.ts,允许你以 .tsx 结尾的文件,在 Vue 项目中编写 jsx代码
  • shims-vue.d.ts 主要用于 TypeScript 识别 .vue文件, ts 默认并不支持导入 .vue文件,这个文件告诉 ts 导入.vue 文件都按 VueConstructor<Vue> 处理。

tslint添加如下配置

// tslint.json
// 不检测隐式类型
{
  "defaultSeverity": "none", // 值为warn时为警告
	"rules": {
		...
  }
  "arrow-parens": [
	false,
	"as-needed"
  ]
}
复制代码

其他内容配置(自选)

// tslint.json
{
  "defaultSeverity": "warning",
  "extends": [
    "tslint:recommended"
  ],
  "linterOptions": {
    "exclude": [
      "node_modules/**"
    ]
  },
  "rules": {
    "quotemark": false, // 字符串文字需要单引号或双引号。
    "indent": false, // 使用制表符或空格强制缩进。
    "member-access": false, // 需要类成员的显式可见性声明。
    "interface-name": false, // 接口名要求大写开头
    "ordered-imports": false, // 要求将import语句按字母顺序排列并进行分组。
    "object-literal-sort-keys": false, // 检查对象文字中键的排序。
    "no-consecutive-blank-lines": false, // 不允许连续出现一个或多个空行。
    "no-shadowed-variable": false, // 不允许隐藏变量声明。
    "no-trailing-whitespace": false, // 不允许在行尾添加尾随空格。
    "semicolon": false, // 是否分号结尾
    "trailing-comma": false, // 是否强象添加逗号
    "eofline": false, // 是否末尾另起一行
    "prefer-conditional-expression": false, // for (... in ...)语句必须用if语句过滤
    "curly": true, //for if do while 要有括号
    "forin": false, //用for in 必须用if进行过滤
    "import-blacklist": true, //允许使用import require导入具体的模块
    "no-arg": true, //不允许使用 argument.callee
    "no-bitwise": true, //不允许使用按位运算符
    "no-console": false, //不能使用console
    "no-construct": true, //不允许使用 String/Number/Boolean的构造函数
    "no-debugger": true, //不允许使用debugger
    "no-duplicate-super": true, //构造函数两次用super会发出警告
    "no-empty": true, //不允许空的块
    "no-eval": true, //不允许使用eval
    "no-floating-promises": false, //必须正确处理promise的返回函数
    "no-for-in-array": false, //不允许使用for in 遍历数组
    "no-implicit-dependencies": false, //不允许在项目的package.json中导入未列为依赖项的模块
    "no-inferred-empty-object-type": false, //不允许在函数和构造函数中使用{}的类型推断
    "no-invalid-template-strings": true, //警告在非模板字符中使用${
    "no-invalid-this": true, //不允许在非class中使用 this关键字
    "no-misused-new": true, //禁止定义构造函数或new class
    "no-null-keyword": false, //不允许使用null关键字
    "no-object-literal-type-assertion": false, //禁止object出现在类型断言表达式中
    "no-return-await": true, //不允许return await
    "arrow-parens": false, //箭头函数定义的参数需要括号
    "adjacent-overload-signatures": false, //  Enforces function overloads to be consecutive.
    "ban-comma-operator": true, //禁止逗号运算符。
    "no-any": false, //不需使用any类型
    "no-empty-interface": true, //禁止空接口 {}
    "no-internal-module": true, //不允许内部模块
    "no-magic-numbers": false, //不允许在变量赋值之外使用常量数值。当没有指定允许值列表时,默认允许-1,0和1
    "no-namespace": [true, "allpw-declarations"], //不允许使用内部modules和命名空间
    "no-non-null-assertion": true, //不允许使用!后缀操作符的非空断言。
    "no-parameter-reassignment": true, //不允许重新分配参数
    "no-reference": true, // 禁止使用/// <reference path=> 导入 ,使用import代替
    "no-unnecessary-type-assertion": false, //如果类型断言没有改变表达式的类型就发出警告
    "no-var-requires": false, //不允许使用var module = require("module"),用 import foo = require('foo')导入
    "prefer-for-of": true, //建议使用for(..of)
    "promise-function-async": false, //要求异步函数返回promise
    "max-classes-per-file": [true, 2], // 一个脚本最多几个申明类
    "variable-name": false,
    "prefer-const": false // 提示可以用const的地方
  }
}
复制代码

世界顿时清净了~~~ 有硬需要的朋友可以自行打开,前提是一定要配置好tslint规则,否则还是有点痛苦不堪的,毕竟warn多了看着难受。告辞

./src/config/index.ts

/** 
 * 线上环境
 */
export const ONLINEHOST: string = 'https://xxx.com'

/** 
 * 测试环境
 */
export const QAHOST: string = 'http://xxx.com'

/** 
 * 线上mock
 */
export const MOCKHOST: string = 'http://xxx.com'

/** 
 * 是否mock
 */
export const ISMOCK: boolean = true

/**
 * 当前的host  ONLINEHOST | QAHOST | MOCKHOST
 */
export const MAINHOST: string = ONLINEHOST

/**
 * 请求的公共参数
 */
export const conmomPrams: any = {}

/**
 * @description token在Cookie中存储的天数,默认1天
 */
export const cookieExpires: number = 1
复制代码

./src/utils/common.ts

// 下载js-cookie
cnpm i js-cookie --S
cnpm install @types/js-cookie --D
复制代码

import Cookies from 'js-cookie'
import { cookieExpires } from '@/config' // cookie保存的天数

/**
 * @Author: asheng
 * @msg: 存取token
 * @param {string} token
 */
export const TOKEN_KEY: string = 'token'
export const setToken = (token: string) => {
  Cookies.set(TOKEN_KEY, token, { expires: cookieExpires || 1 })
}
export const getToken = () => {
  const token = Cookies.get(TOKEN_KEY)
  if (token) {
    return token
  } else {
    return false
  }
}

/**
 * @param {String} url
 * @description 从URL中解析参数
 */
export const getParams = (url: string) => {
  const keyValueArr = url.split('?')[1].split('&')
  let paramObj: any = {}
  keyValueArr.forEach(item => {
    const keyValue = item.split('=')
    paramObj[keyValue[0]] = keyValue[1]
  })
  return paramObj
}

/**
 * 判断一个对象是否存在key,如果传入第二个参数key,则是判断这个obj对象是否存在key这个属性
 * 如果没有传入key这个参数,则判断obj对象是否有键值对
 */
export const hasKey = (obj: any, key: string | number) => {
  if (key) {
    return key in obj
  } else {
    const keysArr = Object.keys(obj)
    return keysArr.length
  }
}

/**
 * @msg: 获取系统当前时间
 * @param {string} fmt 时间格式 具体看代码
 * @return: string
 */
export const getDate = (fmt: any) => {
  let time = ''
  const date = new Date()
  const o: any = {
    "M+": date.getMonth() + 1, // 月份 
    "d+": date.getDate(), // 日 
    "H+": date.getHours(), // 小时 
    "m+": date.getMinutes(), // 分 
    "s+": date.getSeconds(), // 秒 
    "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 
    "S": date.getMilliseconds() // 毫秒 
  }
  if (/(y+)/.test(fmt)) {
    time = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length))
  }
  for (const k in o) {
    if (new RegExp("(" + k + ")").test(fmt)) {
      time = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)))
    }
  }
  return time
}

/**
 * @msg: 获取系统当前时间
 * @param {string} date 时间
 * @param {string} fmt 时间格式
 * @return: string
 */
export const formatDate = (date: any, fmt: string) => {
  let time = ''
  const o: any = {
    "M+": date.getMonth() + 1, // 月份 
    "d+": date.getDate(), // 日 
    "H+": date.getHours(), // 小时 
    "m+": date.getMinutes(), // 分 
    "s+": date.getSeconds(), // 秒 
    "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 
    "S": date.getMilliseconds() // 毫秒 
  }
  if (/(y+)/.test(fmt)) {
    time = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length))
  }
  for (const k in o) {
    if (new RegExp("(" + k + ")").test(fmt)) {
      time = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)))
    }
  }
  return time
}

// copy in the 'fx-fuli' utils
/**
 * 校验手机号是否正确
 * @param phone 手机号
 */

export const verifyPhone = (phone: string | number) => {
  const reg = /^1[34578][0-9]{9}$/
  const _phone = phone.toString().trim()
  let toastStr = _phone === '' ? '手机号不能为空~' : !reg.test(_phone) && '请输入正确手机号~'
  return {
    errMsg: toastStr,
    done: !toastStr,
    value: _phone
  }
}

export const verifyStr = (str: string | number, text: string) => {
  const _str = str.toString().trim()
  const toastStr = _str.length ? false : `请填写${text}~`
  return {
    errMsg: toastStr,
    done: !toastStr,
    value: _str
  }
}

// 截取字符串
export const sliceStr = (str: any, sliceLen: number) => {
  if (!str) { return '' }
  let realLength = 0
  const len = str.length
  let charCode = -1
  for (let i = 0; i < len; i++) {
    charCode = str.charCodeAt(i)
    if (charCode >= 0 && charCode <= 128) {
      realLength += 1
    } else {
      realLength += 2
    }
    if (realLength > sliceLen) {
      return `${str.slice(0, i)}...`
    }
  }

  return str
}


/**
 * JSON 克隆
 * @param {Object | Json} jsonObj json对象
 * @return {Object | Json} 新的json对象
 */
export function objClone(jsonObj: any) {
  let buf: any
  if (jsonObj instanceof Array) {
    buf = []
    let i = jsonObj.length
    while (i--) {
      buf[i] = objClone(jsonObj[i])
    }
    return buf
  } else if (jsonObj instanceof Object) {
    buf = {}
    for (let k in jsonObj) {
      buf[k] = objClone(jsonObj[k])
    }
    return buf
  } else {
    return jsonObj
  }
}
复制代码

一、巧用Webpack

Webpack是实现我们前端项目工程化的基础,但其实她的用处远不仅仅如此,我们可以通过Webpack来帮我们做一些自动化的事情。首先我们要了解require.context()这个API

require.context()

您可以使用require.context()函数创建自己的上下文。 它允许您传入一个目录进行搜索,一个标志指示是否应该搜索子目录,还有一个正则表达式来匹配文件。

其实是Webpack通过解析 require()的调用,提取出来如下这些信息:

Directory: ./template
Regular expression: /^.*\.ejs$/
复制代码

然后来创建我们自己的上下文,什么意思呢,就是我们可以通过这个方法筛选出来我们需要的文件并且读取

/**
* @param directory 要搜索的文件夹目录不能是变量,否则在编译阶段无法定位目录
* @param useSubdirectories  是否搜索子目录
* @param regExp 匹配文件的正则表达式
* @return function 返回一个具有 resolve, keys, id 三个属性的方法
          resolve() 它返回请求被解析后得到的模块 id
          keys() 它返回一个数组,由所有符合上下文模块处理的请求组成。 
          id 是上下文模块里面所包含的模块 id. 它可能在你使用 module.hot.accept 的时候被用到
*/
require.context('.', useSubdirectories = false, regExp = /\.js$/)
// (创建了)一个包含了 demo 文件夹(不包含子目录)下面的、所有文件名以 `js` 结尾的、能被 require 请求到的文件的上下文。

复制代码

这么讲,是否觉得抽象,接下来我们应用下这个小东西。

对于Vue中的路由,大家都很熟悉,类似于声明式的配置文件,其实已经很简洁了。现在我们来让他更简洁

router                           // 路由文件夹
  |__index.ts                    // 路由组织器:用来初始化路由等等
  |__common.ts                   // 通用路由:声明通用路由
  |__modules                     // 业务逻辑模块:所以的业务逻辑模块
        |__index.ts              // 自动化处理文件:自动引入路由的核心文件
        |__home.ts               // 业务模块home:业务模块
复制代码

modules

modules文件夹中存放着我们所有的业务逻辑模块,至于业务逻辑模块怎么分,我相信大家自然有自己的一套标准。我们通过上面提到的require.context()接下来编写自动化的核心部分index.js。

const files: any = require.context('.', false, /\.ts/)

let configRouters: Array<any> = []

files.keys().forEach((key) => {
  if (key === './index.ts') {
    return
  }
  configRouters = configRouters.concat(files(key).default)
})

export default configRouters
复制代码

common

common路由处理 我们的项目中有一大堆的公共路由需要处理比如404阿,503阿等等路由我们都在common.ts中进行处理。

export default [
  {
    path: '/',
    name: 'Login',
    // redirect: '/Login',
    component: Login
  },
  {
    path: '*',
    name: 'Lost',
    component: () => import('@/views/404.vue')
  }
]

复制代码

路由初始化 这是我们的最后一步了,用来初始化我们的项目路由

import Vue from 'vue'
import Router from 'vue-router'
import ConfigRouters from './modules'
import Common from './common'
// 由于是网站开发,这个是进度条,具体开百度了解一下
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/common'

Vue.use(Router)

const router = new Router({
  // mode: "history",
  // base: process.env.BASE_URL,
  scrollBehavior() {
    return { x: 0, y: 0 }
  },
  routes: ConfigRouters.concat(Common)
})

// 登陆页面路由 name
const LOGIN_PAGE_NAME = 'Login'

// 跳转之前
router.beforeEach((to, from, next) => {
  NProgress.start()
  const token = getToken()
  if (!token && to.name !== LOGIN_PAGE_NAME) {
    // 未登录且要跳转的页面不是登录页
    next({
      name: LOGIN_PAGE_NAME // 跳转到登录页
    })
  } else if (!token && to.name === LOGIN_PAGE_NAME) {
    // 未登陆且要跳转的页面是登录页
    next() // 跳转
  } else if (token && to.name === LOGIN_PAGE_NAME) {
    // 已登录且要跳转的页面是登录页
    next({
      name: 'Home' // 跳转到 index 页
    })
  } else {
    if (token) {
      next() // 跳转
    } else {
      next({
        name: LOGIN_PAGE_NAME
      })
    }
  }
})

router.afterEach(() => {
  NProgress.done() // finish progress bar
})
export default router
复制代码

三、充分利用Nodejs

放着node这么好得东西不用真是有点浪费,那么我们来看看node能为我们增加效率做出什么贡献。

有这么一个场景,我们每次创建模块的时候都要新建一个vue文件和对应的router配置,而且新页面的大部分东西都还差不多,还得去复制粘贴别得页面。这想想就有点low。那既然有了node我们可不可以通过node来做这写乱七八糟得事情? 下面来把我们的想法付诸于显示。

./scripts/template.js
const fs = require('fs')
const path = require('path')
const basePath = path.resolve(__dirname, '../src')

const dirName = process.argv[2]
const capPirName = dirName.substring(0, 1).toUpperCase() + dirName.substring(1)
if (!dirName) {
    console.log('文件夹名称不能为空!')
    console.log('示例:npm run tep ${capPirName}')
    process.exit(0)
}

/**
 * @msg: vue页面模版
 */
const VueTep = `<template>
  <div class="${dirName}-wrap">
    {{data.pageName}}
  </div>
</template>

<script lang="ts" src="./${dirName}.ts"></script>

<style lang="scss">
  @import './${dirName}.scss'
</style>

`

// ts 模版
const tsTep = `import { Component, Vue } from "vue-property-decorator"
import { Getter, Action } from "vuex-class"
import { ${capPirName}Data } from '@/types/views/${dirName}.interface'
// import {  } from "@/components" // 组件

@Component({})
export default class About extends Vue {
  // Getter
  // @Getter ${dirName}.author
    
  // Action
  // @Action GET_DATA_ASYN

  // data
  data: ${capPirName}Data = {
    pageName: '${dirName}'
  }

  created() {
    //
  }
    
  activated() {
    //
  }

  mounted() {
    //
  }

  // 初始化函数
  init() {
    //
  }
    
}
`

// scss 模版
const scssTep = `@import "@/assets/scss/variables.scss";

.${dirName}-wrap {
  width: 100%;
}
`

// interface 模版
const interfaceTep = `// ${dirName}.Data 参数类型
export interface ${capPirName}Data {
  pageName: string
}

// VUEX ${dirName}.State 参数类型
export interface ${capPirName}State {
  data?: any
}

// GET_DATA_ASYN 接口参数类型
// export interface DataOptions {}

`

// vuex 模版
const vuexTep = `import { ${capPirName}State } from '@/types/views/${dirName}.interface'
import { GetterTree, MutationTree, ActionTree } from 'vuex'
import * as ${capPirName}Api from '@/api/${dirName}'

const state: ${capPirName}State = {
  ${dirName}: {
   author: undefined
  }
}

// 强制使用getter获取state
const getters: GetterTree<${capPirName}State, any> = {
  author: (state: ${capPirName}State) => state.${dirName}.author
}

// 更改state
const mutations: MutationTree<${capPirName}State> = {
  // 更新state都用该方法
  UPDATE_STATE(state: ${capPirName}State, data: ${capPirName}State) {
    for (const key in data) {
      if (!data.hasOwnProperty(key)) { return }
      state[key] = data[key]
    }
  }
}

const actions: ActionTree<${capPirName}State, any> = {
  UPDATE_STATE_ASYN({ commit, state: ${capPirName}State }, data: ${capPirName}State) {
    commit('UPDATE_STATE', data)
  },
  // GET_DATA_ASYN({ commit, state: LoginState }) {
  //   ${capPirName}.getData()
  // }
}

export default {
  state,
  getters,
  mutations,
  actions
}

`

// api 接口模版
const apiTep = `import Api from '@/utils/request'

export const getData = () => {
  return Api.getData()
}

`

fs.mkdirSync(`${basePath}/views/${dirName}`) // mkdir

process.chdir(`${basePath}/views/${dirName}`) // cd views
fs.writeFileSync(`${dirName}.vue`, VueTep) // vue 
fs.writeFileSync(`${dirName}.ts`, tsTep) // ts
fs.writeFileSync(`${dirName}.scss`, scssTep) // scss

process.chdir(`${basePath}/types/views`); // cd types
fs.writeFileSync(`${dirName}.interface.ts`, interfaceTep) // interface

process.chdir(`${basePath}/store/module`); // cd store
fs.writeFileSync(`${dirName}.ts`, vuexTep) // vuex

process.chdir(`${basePath}/api`); // cd api
fs.writeFileSync(`${dirName}.ts`, apiTep) // api

process.exit(0)
复制代码
./scripts/component.js
const fs = require('fs')
const path = require('path')
const basePath = path.resolve(__dirname, '../src')

const dirName = process.argv[2]
const capPirName = dirName.substring(0, 1).toUpperCase() + dirName.substring(1)
if (!dirName) {
    console.log('文件夹名称不能为空!')
    console.log('示例:npm run tep ${capPirName}')
    process.exit(0)
}

/**
 * @msg: vue页面模版
 */
const VueTep = `<template>
  <div class="${dirName}-wrap">
    {{data.pageName}}
  </div>
</template>

<script lang="ts" src="./${dirName}.ts"></script>

<style lang="scss">
  @import './${dirName}.scss'
</style>

`

// ts 模版
const tsTep = `import { Component, Vue } from "vue-property-decorator"
import { Getter, Action } from "vuex-class"
import { ${capPirName}Data } from '@/types/views/${dirName}.interface'
// import {  } from "@/components" // 组件

@Component({})
export default class About extends Vue {
  // Getter
  // @Getter ${dirName}.author
    
  // Action
  // @Action GET_DATA_ASYN

  // data
  data: ${capPirName}Data = {
    pageName: '${dirName}'
  }

  created() {
    //
  }
    
  activated() {
    //
  }

  mounted() {
    //
  }

  // 初始化函数
  init() {
    //
  }
    
}
`

// scss 模版
const scssTep = `@import "@/assets/scss/variables.scss";

.${dirName}-wrap {
  width: 100%;
}
`

// interface 模版
const interfaceTep = `// ${dirName}.Data 参数类型
export interface ${capPirName}Data {
  pageName: string
}

// VUEX ${dirName}.State 参数类型
export interface ${capPirName}State {
  data?: any
}

// GET_DATA_ASYN 接口参数类型
// export interface DataOptions {}

`

// vuex 模版
const vuexTep = `import { ${capPirName}State } from '@/types/views/${dirName}.interface'
import { GetterTree, MutationTree, ActionTree } from 'vuex'
import * as ${capPirName}Api from '@/api/${dirName}'

const state: ${capPirName}State = {
  ${dirName}: {
   author: undefined
  }
}

// 强制使用getter获取state
const getters: GetterTree<${capPirName}State, any> = {
  author: (state: ${capPirName}State) => state.${dirName}.author
}

// 更改state
const mutations: MutationTree<${capPirName}State> = {
  // 更新state都用该方法
  UPDATE_STATE(state: ${capPirName}State, data: ${capPirName}State) {
    for (const key in data) {
      if (!data.hasOwnProperty(key)) { return }
      state[key] = data[key]
    }
  }
}

const actions: ActionTree<${capPirName}State, any> = {
  UPDATE_STATE_ASYN({ commit, state: ${capPirName}State }, data: ${capPirName}State) {
    commit('UPDATE_STATE', data)
  },
  // GET_DATA_ASYN({ commit, state: LoginState }) {
  //   ${capPirName}.getData()
  // }
}

export default {
  state,
  getters,
  mutations,
  actions
}

`

// api 接口模版
const apiTep = `import Api from '@/utils/request'

export const getData = () => {
  return Api.getData()
}

`

fs.mkdirSync(`${basePath}/views/${dirName}`) // mkdir

process.chdir(`${basePath}/views/${dirName}`) // cd views
fs.writeFileSync(`${dirName}.vue`, VueTep) // vue 
fs.writeFileSync(`${dirName}.ts`, tsTep) // ts
fs.writeFileSync(`${dirName}.scss`, scssTep) // scss

process.chdir(`${basePath}/types/views`); // cd types
fs.writeFileSync(`${dirName}.interface.ts`, interfaceTep) // interface

process.chdir(`${basePath}/store/module`); // cd store
fs.writeFileSync(`${dirName}.ts`, vuexTep) // vuex

process.chdir(`${basePath}/api`); // cd api
fs.writeFileSync(`${dirName}.ts`, apiTep) // api

process.exit(0)
复制代码
cnpm run tep index
cnpm run tep login
复制代码

我们实现这个功能主要要借助Nodefsprocess, 感兴趣的话可以深入研究一下。

首先我们要编写我们的node脚本,这里是一个比较简单的版本。什么验证文件夹或者文件的都没有,只是来实现我们这个想法:

四、状态管理Vuex

vuex-module-decorators

传统的vuexvue+ts的项目里面是行不通的,vue 2.0版本对ts的兼容性本身并不是特别友好,所以要达到状态管理的效果,这里要额外引用一个类库vuex-module-decorators,它是基于vue-class-component 所做的拓展,它提供了一系列的装饰器,让vue+ts结合的项目达到状态管理的作用。

先来看看要完成的模块化管理的目录结构

.
├─ src/        
│   ├─ store/
│   ├─── modules/
│   │ 		├─ app.ts 
│   │ 		├─ user.ts
│   ├─── index.ts   
复制代码
import Vue from 'vue'
import Vuex from 'vuex'
import { IAppState } from './modules/app'
import { IUserState } from './modules/user'

Vue.use(Vuex)

export interface IRootState {
    app: IAppState
    user: IUserState
}

// Declare empty store first, dynamically register all modules later.
export default new Vuex.Store<IRootState>({})
复制代码
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    user
  }
})

export default store
复制代码

这样,模块化状态管理的雏形就完成了。对比来看,只是语法风格的变化,其它的变化不大。ts版的状态管理最大的改变体现在各个功能功能函数上

先看一看原始的vuex配置,轻车熟路

export default new Vuex.Store({
    state: {
    },
    mutations: {
    },
    actions: {
    },
    modules: {
    }
});
复制代码

为了显得不那么啰嗦,直接上版ts版的状态管理吧,可以有个直观的对比

// user.ts
import { VuexModule, Module, Action, Mutation, getModule } from 'vuex-module-decorators'
import store from '@/store'

export interface IUserState {
    id_token: string
}

@Module({ dynamic: true, store, name: 'user' })
class User extends VuexModule implements IUserState {
    public id_token = ''
    
    @Mutation
    private SET_TOKEN(token: string) {
        this.id_token = token
    }
    
    @Action
    public async Login(params: any) {
        this.SET_TOKEN(`token!!!`)
    }
}

export const UserModule = getModule(User)
复制代码

Module

定义一个modules,直接使用装饰器@Module 注意:原始的vuex同样有一个名为Module的类,但它不是一个装饰器,所以别用混淆了

@Module({ dynamic: true, store, name: 'user' })
复制代码

从上面可以看到,我们定义modules不单单用了装饰器,还带了参数值,这个是表明是通过命名空间的形式来使用module,如上,这里的namespaced值即为user

详细vuex命名空间的说明,可以参考vuex命名空间

除了namespaced,我们看到还有另外一个参数值store,它即为主入口页对应的整个vuex模块的store

import store from '@/store'
复制代码

如果去掉它的话,浏览器会报以下错误

1

state

这里所有的state属性因为加了tslint都会添加上public修饰,其它的用法都是相似的

Getters

原始的getters计算函数,在这里对应的即使get方法,即

@Module
export default class UserModule extends VuexModule {
  countsNum = 2020
  
  get calculatCount() {
    return countsNum / 2
  }
}
复制代码
export default {
  state: {
    countsNum: 2
  },
  getters: {
    calculatCount: (state) => state.countsNum / 2
  }
}
复制代码

Mutations

@Mutation
private SET_TOKEN(token: string) {
    this.token = token
}

@Mutation
...
复制代码
mutations: {
    SET_TOKEN: (state, token) => {
        state.token = token
    },
    ...
}
复制代码

说明: 两者的区别其实就是语法糖,原始的Mutation同步方法都是定义在mutations内,而ts版的每一个Mutation都要加上装饰器@Mutation修饰

注意: 一旦使用@Mutation装饰某一函数后, 函数内的this上下文即指向当前的state,所以想引用state的值,可以直接this.token访问即可。

Muation函数不可为async函数, 也不能使用箭头函数来定义, 因为在代码需要在运行重新绑定执行的上下文

Action

@Action
public async Login(userInfo: { username: string, password: string}) {
    ...
    this.SET_TOKEN(data.accessToken)
}
复制代码
actions: {
    async Login({ commit }, data) {
        ...
        commit('SET_TOKEN', data.accessToken)
    }
}
复制代码

说明: 异步函数Action和同步函数Mutation使用方法大同小异,区别就是一个是同步,一个是异步,只要做好区分即可

注意

  • 如果需要在action函数中运行耗时很长的任务/函数, 建议将该任务定义为异步函数*(async methods)*
  • 千万不要使用箭头函数=>来定义action函数, 因为在运行时需要动态绑定this上下文

vuex+ts版的配置搭建成功,接下来我们把它运用到项目中来吧,这里抽一个登陆页面的模块做介绍

import {
  VuexModule,
  Module,
  Action,
  Mutation,
  getModule
} from 'vuex-module-decorators'
import { login } from '@/api/users' //调用api方法
import store from '@/store'

//声明user模块的state变量类型
//export interface 只是对一个东西的声明(不能具体的操作)
//export class 导出一个类 类里面可有参数 ,函数,方法(干一些具体的事情)
export interface IUserState {
  id_token: string
}

@Module({ dynamic: true, store, name: 'user' })
class User extends VuexModule implements IUserState {
  public id_token = ''

  @Mutation
  private SET_TOKEN(token: string) {
    //同步存储id_token变量
    this.id_token = token
  }

  @Action
  public async Login(params: any) {
    let { mobilePhone, password } = params
    const { data } = await login({ mobilePhone, password })
    this.SET_TOKEN(`Bearer ${data.id_token}`)
  }
}

export const UserModule = getModule(User)
复制代码

login页面中调用

import { UserModule } from '@/store/modules/user'

await UserModule.Login({
  ...this.loginForm,
  router: this.$router
})
复制代码

把路由对象作为参数传过去是为了根据不同的响应状态做判断,当请求成功后,可以直接应用传过来的路由对象参数跳转页面。

router.push('/')
复制代码

注意: 这一步操作其实是调用了vuexAction操作,即原始的this.$store.commit('action'),但是在vuex+ts项目中,调用异步函数Action,不需要再用this.$store.commit('action')这种方法,引用模块后,直接调用里面的Action方法就好了,同样的,同步的Mutation也是这样调用。这些都要归功于vuex-module-decorators类库的封装 好了,调用Action后粗发Mutation同步操作,保存好token令牌,因为登录之后所有的请求都要把token值放在header头中发起请求 除了vuex状态管理,在项目中可能我们还会结合工具类js-cookie一起使用,管理各种变量的值,具体用法跟原始版没有什么区别,最主要的是安装类库的过程中,还得安装一个开发ts编译版

yarn add js-cookie // dependencies yarn add @types/js-cookie --dev // devDependencies(必装)
复制代码

这里使用的是vuex-modulevuex-class还是有不少区别,在下面的内容,使用的是vuex-class

可能有人会有疑问,为什么介绍vuex-module,而使用vuex-class。。。 当初构建项目时,使用的是vuex-class,最近学习到vuex-module,就记录下。

具体vuex-class的使用,文章最开始就把文档贴出来了,也可参考下窝的写法。

vuex-class

vuex-class 是一个基于 VueVuexvue-class-component 的库,和 vue-property-decorator 一样,它也提供了4 个修饰符以及 namespace,解决了 vuex.vue 文件中使用上的不便的问题。

  • @State
  • @Getter
  • @Mutation
  • @Action
  • namespace

到了这里,就不再说明State,Getter...

先来看看要完成的模块化管理的目录结构

.
├─ src/        
│   ├─ store/
│   ├─── modules/
│   │ 	├─ user.ts
│   │ 	├─ index.ts
│   ├─── index.ts   
复制代码
// ./store/modules/user.ts
import { GetterTree, MutationTree, ActionTree } from 'vuex'
interface LoginState {
  [key: string]: any
}

const state: LoginState = {
  user_id: '1', // 用户id
  authority: 1, // 开户权限
  token: ''
}

// 强制使用getter获取state
const getters: GetterTree<LoginState, any> = {
  getUserId: (state: LoginState) => state.user_id,
  getToken: (state: LoginState) => state.token
}

// 更改state
const mutations: MutationTree<LoginState> = {
  // 更新state都用该方法
  UPDATE_STATE(state: LoginState, data: LoginState) {
    Object.keys(data).forEach((item) => {
      state[item] = data[item]
    })
  }
}

const actions: ActionTree<LoginState, any> = {
  UPDATE_STATE_ASYN({ commit, state: LoginState }, data: LoginState) {
    commit('UPDATE_STATE', data)
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

复制代码
// ./store/modules/index.ts
import { ModuleTree } from 'vuex'
const files: any = require.context('.', false, /\.ts$/)

// 这里为了方便演示,重复写了一个接口。
// 个人可放置到 * 目录下,来同一管理接口
interface LoginState {
  [key: string]: any
}

let modules: ModuleTree<any> = {}

files.keys().forEach((key) => {
  if (key === './index.ts') {
    return
  }
  modules[key.replace(/(\.\/|\.ts)/g, '')] = files(key).default
})

export default modules

复制代码

整理好./store/modules/*下的文件,那么就该到'使用'登场啦。。。

// ./store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules'

Vue.use(Vuex)
export default new Vuex.Store({
  modules
})
复制代码

我们在login.vue下使用康康

...
import { State, Action, namespace } from 'vuex-class'

// 在上面 ./store/modules/user.ts 并使用了命名空间,这里直接使用就行啦
const usreModel = namespace('user')

@Component({})
export default class Login extends Vue {
  // 窝是这样使用
  @usreModel.State((state) => state.user_id) user_id
  @usreModel.State((state) => state.authority) authority
  // 调用user/actions中的UPDATE_STATE_ASYN
  @usreModel.Action('UPDATE_STATE_ASYN') UPDATE_STATE_ASYN
  ...
}
复制代码

(窝是结束符)...

在来康康官网vuex-class(参考)

import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}
复制代码

这里再次强调,vuex-classvuex-module是两个不同的东西,切勿同时使用,避免人为bug。

五、Mixins

如果我们有大量的表格页面,仔细一扒拉你发现非常多的东西都是可以复用的例如分页,表格高度,加载方法, laoding声明等一大堆的东西。下面我们来整理出来一个简单通用混入index.js

import { Provide, Vue } from 'vue-property-decorator'
import Component from 'vue-class-component'
// 这里使用的是vuex-class,与上面的vuex-module不同,请注意
import { namespace } from 'vuex-class'
import moment from 'moment'

const usreModel = namespace('user')

@Component
export default class MyMixin extends Vue {
  @Provide() public loading: boolean = false
  @Provide() public form: any
  @Provide() public data: Array<any> = []
  @Provide() public pagination: any = {
    defaultPageSize: 6,
    showQuickJumper: true,
    hideOnSinglePage: false
  }

  @usreModel.State(state => state.user_id) user_id
  @usreModel.State(state => state.authority) authority
  
  formatDate(value, format = 'YYYY-MM-DD HH:mm') {
    if (value) {
      return moment(value).format(format)
    }
  }
}
复制代码

mixins使用

import Component, { mixins } from 'vue-class-component'
import { Vue, Provide } from 'vue-property-decorator'
import MyMixin from '@/mixins'

@Component
export default class Home extends mixins(MyMixin) {
  @Provide() private columns: Object = Columns
  @Provide() private search: string = ''
}
复制代码

这样就可以正常使用lodingform等数据方法等

注意:全局mixins一定要慎用,如果不是必须要用的话我还是不建议使用。

六、axios的封装

vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promisehttp库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。

npm install axios; // 安装axios
复制代码

一般我会在项目的src目录中,新建一个api文件夹,然后在里面新建一个api.ts和一个requestConfig.ts文件。api.ts文件用来封装我们的axiosrequestConfig.ts用来统一管理我们的接口。

// src/api/api.ts
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
// config文件夹往后会出现,这里就不说明了
import { MAINHOST, ISMOCK, QAHOST, conmomPrams } from '@/config' 
// 接口
import requestConfig from './requestConfig'
// 获取存储在 cookies 的 token
import { getToken, removeToken } from '@/utils/common'
// 这里我使用了 antd ,大家根据自己的UI来使用
import { message } from 'ant-design-vue'
// 路由
import router from '@/router'
// 下面两个是加解密文件,因为用的是http,为了安全考虑,使用到这两个。(可忽略)
import apiEncrypt from '@/utils/apiEncrypt'
import apiDecrypt from '@/utils/apiDecrypt'

declare type Methods = 'GET' | 'OPTIONS' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT'

declare interface Datas {
  method?: Methods
  [key: string]: any
}

// 根据环境,切换请求不同的url
const baseURL = process.env.NODE_ENV === 'production' ? MAINHOST : QAHOST//QAHOST

class HttpRequest {
  public queue: any // 请求的url集合
  public hide: any
  public constructor() {
    this.queue = {}
  }
  destroy(url: string) {
    delete this.queue[url]
    if (!Object.keys(this.queue).length) {
      // 关闭loding
      setTimeout(this.hide, 0)
    }
  }
  interceptors(instance: any, url?: string) {
    // 请求拦截
    instance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        // 添加全局的loading...
        if (!Object.keys(this.queue).length) {
          // show loading
          this.hide = message.loading('加载中..', 0)
        }
        if (url) {
          this.queue[url] = true
        }
        return config
      },
      (error: any) => {
        console.error(error)
      }
    )
    // 响应拦截
    instance.interceptors.response.use(
      (res: AxiosResponse) => {
        if (url) {
          this.destroy(url)
        }
        let { data, status } = res
        data = apiDecrypt(data)
        if (status === 200 && ISMOCK) {
          return data.result
        } // 如果是mock数据,直接返回
        if (status === 200 && data && data.code === 200) {
          return data.result
        } // 请求成功
        res.data = data
        return requestFail(res) // 失败回调
      },
      (error: any) => {
        if (url) {
          this.destroy(url)
        }
        message.error('服务器出错')
        console.error(error)
      }
    )
  }
  async request(options: AxiosRequestConfig) {
    const instance = axios.create()
    await this.interceptors(instance, options.url)
    return instance(options)
  }
}

// 请求失败
const requestFail = (res: AxiosResponse) => {
  let errStr = '网络繁忙!'

  if (res.data.code) {
    switch (res.data.code) {
      // 401: 未登录
      // 未登录则跳转登录页面,并携带当前页面的路径
      // 在登录成功后返回当前页面,这一步需要在登录页操作。
      case 401:
        router.replace({
          path: '/'
        })
        removeToken()
        break
      // 403 token过期
      // 登录过期对用户进行提示
      // 清除本地token和清空vuex中token对象
      // 跳转登录页面
      case 403:
        // 清除token
        // store.commit('loginSuccess', null);
        // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
        router.replace({
          path: '/'
        })
        removeToken()
        // localStorage.removeItem('token')
        break
      // 404请求不存在
      case 404:
        ...      
        break
    }
  }
  console.error({
    code: res.data.errcode || res.data.code,
    msg: res.data.errMsg || errStr
  })

  if (typeof res.data.errMsg === 'object') {
    res.data.errMsg = '服务器错误'
  }
  message.error(res.data.errMsg || errStr)
  return null
}

// 合并axios参数
const conbineOptions = (_opts: any, data: Datas, method: Methods): AxiosRequestConfig => {
  let opts = _opts
  if (typeof opts === 'string') {
    opts = { url: opts }
  }
  const _data = { ...conmomPrams, ...opts.data, ...data }
  const options = {
    method: opts.method || data.method || method || 'GET',
    url: opts.url,
    headers: { Authorization: `Bearer${getToken()}` },// 这个需要与后端配合,让后端去除掉Bearer,加上这个是为了(安全考虑)
    baseURL,
    timeout: 10000
  }
  const c = apiEncrypt(_data) // 加密数据
  return options.method !== 'GET' ? Object.assign(options, { data: c }) : Object.assign(options, { params: _data })
}

const HTTP = new HttpRequest()

/**
 * 抛出整个项目的api方法
 */
const Api = (() => {
  const apiObj: any = {}
  const requestList: any = requestConfig
  const fun = (opts: AxiosRequestConfig | string) => {
    return async (data = {}, method: Methods = 'POST') => {
      const newOpts = conbineOptions(opts, data, method)
      const res = await HTTP.request(newOpts)
      return res
    }
  }
  Object.keys(requestConfig).forEach((key) => {
    apiObj[key] = fun(requestList[key])
  })

  return apiObj
})()

export default Api as any

复制代码
src/api/requestConfig
export default {
  getData: '/mock/5e23f600df5e86413d7f1486/example/upload', // 随机数据 来自 easy mock
}
复制代码
  • 如果定义了 .d.ts 文件,请重新启动服务让你的服务能够识别你定义的模块,并重启 vscode 让编辑器也能够识别(真的恶心)
  • 设置好你的 tsconfig ,比如记得把 strictPropertyInitialization 设为 false,不然你定义一个变量就必须给它一个初始值。
  • 千万管理好你的路由层级,不然到时连正则都拯救不了你 业务层面千万做好类型检测或者枚举定义,这样不仅便利了开发,还能在出了问题的时候迅速定位
  • 跨模块使用 vuex,请直接使用 rootGetters
  • 如果你需要改造某组件库主题,请单开一个文件进行集中管理,别一个组件分一个文件去改动,不然编译起来速度堪忧
  • 能够复用团队其他人开发好的东西,尽量别去开发第二遍,不然到时浪费的可能就不是单纯的开发时间,还有 code review的时间

vue 文件中 TS 上下文顺序

  • @Prop
  • @State
  • @Getter
  • @Action
  • @Mutation
  • @Watch

生命周期钩子

  • beforeCreate(按照生命周期钩子从上到下)
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • activated
  • deactivated
  • beforeDestroy
  • destroyed
  • errorCaptured(最后一个生命周期钩子)
  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

computed

methods


分享不易,喜欢的话一定别忘了点💖!!!

只关注不点💖的都是耍流氓,只收藏也不点💖的也一样是耍流氓。

结束👍👍👍。


axios封装(戳窝)

加速vue项目开发速度(戳窝)

TypeScript + 大型项目实战(戳窝)

Typescript+Vue大型后台管理系统实战(戳窝)

vue-cli3.0 搭建项目模版教程(戳窝)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK