Typescript+Vue大型项目构建+详细讲解
https://juejin.im/post/5e427f75f265da575e37a51d
写在前面
在开撸之前,先把文档网址贴一贴
先来认识下面的小玩意
vue-property-decorator
这里单页面组件的书写采用的是 vue-property-decorator
库,该库完全依赖于 vue-class-component
,也是 vue
官方推荐的库。
单页面组件中,在 @Component({})
里面写 props
、data
等调用起来极其不方便,而 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: 'onChildChanged', immediate: false, 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
:其值是true
或false
;确认是否深入监听。deep
的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器(受现代JavaScript
的限制 (以及废弃Object.observe
),Vue
不能检测到对象属性的添加或删除) - 第三个是
immediate
:其值是true
或false
;immediate: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 } } } } 复制代码
vuex-class
vuex-class
是一个基于 Vue
、Vuex
、vue-class-component
的库,和 vue-property-decorator
一样,它也提供了4 个修饰符以及 namespace
,解决了 vuex
在 .vue
文件中使用上的不便的问题。
- @State
- @Getter
- @Mutation
- @Action
- namespace
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'] } } 复制代码
搭建环境
创建项目
? 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.ts
和 shims-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 复制代码
我们实现这个功能主要要借助Node
的fs和process, 感兴趣的话可以深入研究一下。
首先我们要编写我们的node
脚本,这里是一个比较简单的版本。什么验证文件夹或者文件的都没有,只是来实现我们这个想法:
四、状态管理Vuex
传统的vuex
在vue+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' 复制代码
如果去掉它的话,浏览器会报以下错误
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('/') 复制代码
注意: 这一步操作其实是调用了vuex
的Action
操作,即原始的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-module
与vuex-class
还是有不少区别,在下面的内容,使用的是vuex-class
。
可能有人会有疑问,为什么介绍vuex-module
,而使用vuex-class
。。。 当初构建项目时,使用的是vuex-class
,最近学习到vuex-module
,就记录下。
具体vuex-class
的使用,文章最开始就把文档贴出来了,可进去了解一下。
五、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 = '' } 复制代码
这样就可以正常使用loding
、form
等数据方法等
注意:全局mixins一定要慎用,如果不是必须要用的话我还是不建议使用。
六、axios的封装
在vue
项目中,和后台交互获取数据这块,我们通常使用的是axios
库,它是基于promise
的http
库,可运行在浏览器端和node.js
中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json
、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource
的维护,直接推荐我们使用axios
库。如果还对axios
不了解的,可以移步axios文档。
安装
npm install axios; // 安装axios 复制代码
引入
一般我会在项目的src
目录中,新建一个api
文件夹,然后在里面新建一个api.ts
和一个requestConfig.ts
文件。api.ts
文件用来封装我们的axios
,requestConfig.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.queueaxios封装(戳窝)" target=_blank> 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 复制代码[/code]src/api/requestConfig export default { getData: '/mock/5e23f600df5e86413d7f1486/example/upload', // 随机数据 来自 easy mock } 复制代码一些建议
- 如果定义了
.d.ts
文件,请重新启动服务让你的服务能够识别你定义的模块,并重启vscode
让编辑器也能够识别(真的恶心)- 设置好你的
tsconfig
,比如记得把strictPropertyInitialization
设为false
,不然你定义一个变量就必须给它一个初始值。- 千万管理好你的路由层级,不然到时连正则都拯救不了你 业务层面千万做好类型检测或者枚举定义,这样不仅便利了开发,还能在出了问题的时候迅速定位
- 跨模块使用
vuex
,请直接使用rootGetters
- 如果你需要改造某组件库主题,请单开一个文件进行集中管理,别一个组件分一个文件去改动,不然编译起来速度堪忧
- 能够复用团队其他人开发好的东西,尽量别去开发第二遍,不然到时浪费的可能就不是单纯的开发时间,还有
code review
的时间vue 文件中 TS 上下文顺序
- data
- @Prop
- @State
- @Getter
- @Action
- @Mutation
- @Watch
生命周期钩子
- beforeCreate(按照生命周期钩子从上到下)
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- activated
- deactivated
- beforeDestroy
- destroyed
- errorCaptured(最后一个生命周期钩子)
路由钩子
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
computed
methods
分享不易,喜欢的话一定别忘了点💖!!!
只关注不点💖的都是耍流氓,只收藏也不点💖的也一样是耍流氓。
结束👍👍👍。
参考
[url=https://juejin.im/post/5b55c118f265da0f6f1aa354]axios封装(戳窝)
[url=https://juejin.im/post/5c106485e51d450e657571a6]加速vue项目开发速度(戳窝)
- webpack构建vue项目的详细教程(配置篇)
- webpack+vue+vueRouter模块化构建完整项目实例详细步骤-入门篇
- 打通前后端全栈开发node+vue进阶【课程学习系统项目实战详细讲解】(3):用户添加/修改/删除 vue表格组件 vue分页组件
- webpack+vue+vueRouter模块化构建完整项目实例超详细步骤(附截图、代码、入门篇)
- webpack+vue+vueRouter模块化构建完整项目实例详细步骤-入门篇
- 1. webpack+vue+vueRouter模块化构建完整项目实例详细步骤-入门篇
- 构建大型 Vue.js 项目的10条建议(小结)
- vue-cli3单页构建大型项目方案
- webpack+vue+vueRouter模块化构建完整项目实例超详细步骤(附截图、代码、入门篇)
- Vue 爬坑之路(十)—— Vue2.5 + Typescript 构建项目
- 5分钟为vue-cli2构建项目添加typescript
- 构建大型 Vue.js 项目的10条建议
- vue+typescript构建项目
- vue+typescript构建项目相关配置
- Vue全家桶学习 四、大型项目如何构建Vuex及实战应用
- 5分钟为vue-cli2构建项目添加typescript
- vue构建项目全过程
- vue项目构建
- vue-cli 初始化大型项目
- vue.js 2.*项目环境搭建、运行、打包发布的详细步骤