先来认识下面的小玩意
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
@Watch('child')
onChildChanged (val: string, oldVal: string) {}
@Watch('person', { immediate: true, deep: true })
onPersonChanged (val: Person, oldVal: Person) {}
解析之后会变成
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
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)
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) {
} else {
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
搞的鬼,配置一下重新运行即可
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
├── filters
├── lib
├── router
├── store
├── styles
├── types
├── utils
├── views
├── App.vue
├── main.ts
├── registerServiceWorker.ts
├── tests
├── .editorconfig
├── .npmrc
├── .postcssrc.js
├── babel.config.js
├── cypress.json
├── f2eci.json
├── package.json
├── README.md
├── tsconfig.json
├── tslint.json
└── vue.config.js
主要涉及 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"
其他内容配置(自选)
"defaultSeverity": "warning",
"extends": [
"tslint:recommended"
"linterOptions": {
"exclude": [
"node_modules/**"
"rules": {
"quotemark": false,
"indent": false,
"member-access": false,
"interface-name": false,
"ordered-imports": false,
"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,
"curly": true,
"forin": false,
"import-blacklist": true,
"no-arg": true,
"no-bitwise": true,
"no-console": false,
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": true,
"no-eval": true,
"no-floating-promises": false,
"no-for-in-array": false,
"no-implicit-dependencies": false,
"no-inferred-empty-object-type": false,
"no-invalid-template-strings": true,
"no-invalid-this": true,
"no-misused-new": true,
"no-null-keyword": false,
"no-object-literal-type-assertion": false,
"no-return-await": true,
"arrow-parens": false,
"adjacent-overload-signatures": false,
"ban-comma-operator": true,
"no-any": false,
"no-empty-interface": true,
"no-internal-module": true,
"no-magic-numbers": false,
"no-namespace": [true, "allpw-declarations"],
"no-non-null-assertion": true,
"no-parameter-reassignment": true,
"no-reference": true,
"no-unnecessary-type-assertion": false,
"no-var-requires": false,
"prefer-for-of": true,
"promise-function-async": false,
"max-classes-per-file": [true, 2],
"variable-name": false,
"prefer-const": false
世界顿时清净了~~~ 有硬需要的朋友可以自行打开,前提是一定要配置好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
cnpm i js-cookie --S
cnpm install @types/js-cookie --D
import Cookies from 'js-cookie'
import { cookieExpires } from '@/config'
* @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
* 校验手机号是否正确
* @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$/)
这么讲,是否觉得抽象,接下来我们应用下这个小东西。
对于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({
scrollBehavior() {
return { x: 0, y: 0 }
routes: ConfigRouters.concat(Common)
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'
} else {
if (token) {
next()
} else {
next({
name: LOGIN_PAGE_NAME
router.afterEach(() => {
NProgress.done()
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}}
</template>
<script lang="ts" src="./${dirName}.ts"></script>
<style lang="scss">
@import './${dirName}.scss'
</style>
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() {
const scssTep = `@import "@/assets/scss/variables.scss";
.${dirName}-wrap {
width: 100%;
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 {}
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
const apiTep = `import Api from '@/utils/request'
export const getData = () => {
return Api.getData()
fs.mkdirSync(`${basePath}/views/${dirName}`)
process.chdir(`${basePath}/views/${dirName}`)
fs.writeFileSync(`${dirName}.vue`, VueTep)
fs.writeFileSync(`${dirName}.ts`, tsTep)
fs.writeFileSync(`${dirName}.scss`, scssTep)
process.chdir(`${basePath}/types/views`);
fs.writeFileSync(`${dirName}.interface.ts`, interfaceTep)
process.chdir(`${basePath}/store/module`);
fs.writeFileSync(`${dirName}.ts`, vuexTep)
process.chdir(`${basePath}/api`);
fs.writeFileSync(`${dirName}.ts`, apiTep)
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}}
</template>
<script lang="ts" src="./${dirName}.ts"></script>
<style lang="scss">
@import './${dirName}.scss'
</style>
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() {
const scssTep = `@import "@/assets/scss/variables.scss";
.${dirName}-wrap {
width: 100%;
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 {}
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
const apiTep = `import Api from '@/utils/request'
export const getData = () => {
return Api.getData()
fs.mkdirSync(`${basePath}/views/${dirName}`)
process.chdir(`${basePath}/views/${dirName}`)
fs.writeFileSync(`${dirName}.vue`, VueTep)
fs.writeFileSync(`${dirName}.ts`, tsTep)
fs.writeFileSync(`${dirName}.scss`, scssTep)
process.chdir(`${basePath}/types/views`);
fs.writeFileSync(`${dirName}.interface.ts`, interfaceTep)
process.chdir(`${basePath}/store/module`);
fs.writeFileSync(`${dirName}.ts`, vuexTep)
process.chdir(`${basePath}/api`);
fs.writeFileSync(`${dirName}.ts`, apiTep)
process.exit(0)
cnpm run tep index
cnpm run tep login
我们实现这个功能主要要借助Node
的fs和process, 感兴趣的话可以深入研究一下。
首先我们要编写我们的node
脚本,这里是一个比较简单的版本。什么验证文件夹或者文件的都没有,只是来实现我们这个想法:
四、状态管理Vuex
vuex-module-decorators
传统的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
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: {
export default store
这样,模块化状态管理的雏形就完成了。对比来看,只是语法风格的变化,其它的变化不大。ts版的状态管理最大的改变体现在各个功能功能函数上
先看一看原始的vuex配置,轻车熟路
export default new Vuex.Store({
state: {
mutations: {
actions: {
modules: {
为了显得不那么啰嗦,直接上版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'
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) {
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
这里使用的是vuex-module
与vuex-class
还是有不少区别,在下面的内容,使用的是vuex-class
。
可能有人会有疑问,为什么介绍vuex-module
,而使用vuex-class
。。。
当初构建项目时,使用的是vuex-class
,最近学习到vuex-module
,就记录下。
具体vuex-class
的使用,文章最开始就把文档贴出来了,也可参考下窝的写法。
vuex-class
vuex-class
是一个基于 Vue
、Vuex
、vue-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
import { GetterTree, MutationTree, ActionTree } from 'vuex'
interface LoginState {
[key: string]: any
const state: LoginState = {
user_id: '1',
authority: 1,
token: ''
const getters: GetterTree<LoginState, any> = {
getUserId: (state: LoginState) => state.user_id,
getToken: (state: LoginState) => state.token
const mutations: MutationTree<LoginState> = {
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
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/*
下的文件,那么就该到'使用'登场啦。。。
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'
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
@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
@State foo
@Getter bar
@Action baz
@Mutation qux
created () {
this.stateFoo
this.stateBar
this.getterFoo
this.actionFoo({ value: true })
this.mutationFoo({ value: true })
this.moduleGetterFoo
这里再次强调,vuex-class
与vuex-module
是两个不同的东西,切勿同时使用,避免人为bug。
五、Mixins
如果我们有大量的表格页面,仔细一扒拉你发现非常多的东西都是可以复用的例如分页,表格高度,加载方法, laoding
声明等一大堆的东西。下面我们来整理出来一个简单通用混入index.js
import { Provide, Vue } from 'vue-property-decorator'
import Component from 'vue-class-component'
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
用来统一管理我们的接口。
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
import { MAINHOST, ISMOCK, QAHOST, conmomPrams } from '@/config'
import requestConfig from './requestConfig'
import { getToken, removeToken } from '@/utils/common'
import { message } from 'ant-design-vue'
import router from '@/router'
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
const baseURL = process.env.NODE_ENV === 'production' ? MAINHOST : QAHOST
class HttpRequest {
public queue: any
public hide: any
public constructor() {
this.queue = {}
destroy(url: string) {
delete this.queue[url]
if (!Object.keys(this.queue).length) {
setTimeout(this.hide, 0)
interceptors(instance: any, url?: string) {
instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (!Object.keys(this.queue).length) {
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
}
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) {
case 401:
router.replace({
path: '/'
removeToken()
break
case 403:
router.replace({
path: '/'
removeToken()
break
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
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()}` },
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 搭建项目模版教程(戳窝)