TypeScript是国内外前端技术圈被评为2020年最受欢迎的技术之一,如果你还没开始学,是不是就out了呢?今天我们就开始TypeScript+Vue项目开发的探索,带你体验完全不一样的Vue开发方式。话不多说,让我们开始吧......

1、初始化项目及环境搭建

1.1、全局安装vue脚手架

npm install -g @vue/cli

目前默认安装的是vue/cli的最新4.0版本,可使用如下命令查看:

vue --version

1.2、使用vue@cli 4.0初始化项目

vue create ts-vue-music
  • 选择配置模式(自动/手动)

typescript和vue结合 typescript开发vue_Vue

默认是自动,我们选择手动模式

  • 选择集成配置项

使用上下方向键选择,空格键选中/取消

? Check the features needed for your project:
 (*) Babel 是否开启babel编译
 (*) TypeScript 是否集成TS
 ( ) Progressive Web App (PWA) Support  是否支持PWA
 (*) Router  是否集成vue-router
 (*) Vuex  是否集成vuex
 (*) CSS Pre-processors  是否使用css预处理
 (*) Linter / Formatter  是否规范代码类型
 ( ) Unit Testing  是否使用单元测试
 ( ) E2E Testing   是否使用E2E测试
  • 继续选择配置项
//是否使用class风格的组件语法
? Use class-style component syntax? Yes  
//是否使用babel做转义
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes  
//是否使用路由history模式
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes  
//选择css预处理类型,我们选择node-sass
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)  
//选择代码规范校验工具,我们选择ESLint + Prettier
? Pick a linter / formatter config: Prettier  
//选择保存时校验
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save  
// 选择 Babel, ESLint, etc.等配置的保存位置,我们选package.json 文件
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json 
// 选择是否保存这些配置到以后项目中
? Save this as a preset for future projects? Yes

选择完毕后就开始拉取配置,生成初始化项目文件。

  • 启动项目
cd ts-vue-todolist // 进入项目根目录
npm run serve // 运行项目

项目启动后,在浏览器输入对应的地址就可以看到界面了。

1.3、改造项目结构

使用脚手架初始化后会默认生成一个项目结构目录,但我们可以根据自己的项目需求进行改造。

  • 调整项目结构目录
|—— public              入口html文件
|—— src                 源文件目录
  |—— apis              请求api
  |—— assets            静态资源
  |—— components        公共组件
  |—— directives        自定义指令
  |—— filters           过滤器
  |—— mixins            mixin混入
  |—— router            vue-router路由
  |—— store             vuex状态管理
  |—— styles            样式
  |—— types             类型声明
  |—— utils             工具方法
  |—— views             页面组件
  |—— App.vue           入口页面
  |—— main.ts           入口文件
  |—— shims-tsx.d.ts    tsx声明文件
  |—— shims-vue.d.ts    vue声明文件
|—— .gitignore          git忽略文件配置
|—— babel.config.js     babel配置
|—— package.json        依赖配置
|—— README.md           项目readme文件
|—— tsconfig.json       ts配置
|—— vue.config.js       webpack配置
  • 新增vue.config.js配置文件

vue-cli脚手架默认生成的项目是零webpack配置的,但是零配置功能比较弱,@vue-cli支持自定义webpack配置,在根目录新建vue.config.js文件,这个文件会被@vue/cli-service 自动加载。常用配置如下:

const path = require("path");
const sourceMap = process.env.NODE_ENV === "development";

module.exports = {
  // 基本路径
  publicPath: "./",
  // 输出文件目录
  outputDir: "dist",
  // eslint-loader 是否在保存的时候检查
  lintOnSave: false,
  // webpack配置
  // 参考 https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
  chainWebpack: () => {},
  configureWebpack: config => {
    if (process.env.NODE_ENV === "production") {
      // 为生产环境修改配置
      config.mode = "production";
    } else {
      // 为开发环境修改配置
      config.mode = "development";
    }

    Object.assign(config, {
      // 开发生产共同配置
      resolve: {
        extensions: [".js", ".vue", ".json", ".ts", ".tsx"],
        alias: {
          vue$: "vue/dist/vue.js",
          "@": path.resolve(__dirname, "./src"),
          "@c": path.resolve(__dirname, "./src/components")
        }
      }
    });
  },
  // 生产环境是否生成 sourceMap 文件
  productionSourceMap: sourceMap,
  // css相关配置
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: true,
    // 开启 CSS source maps?
    sourceMap: false,
    // css预设器配置项
    loaderOptions: {},
    // 设置为 false 后你就可以去掉文件名中的 .module 并将所有的 *.(css|scss|sass|less|styl(us)?)
    requireModuleExtension: false
  },
  // use thread-loader for babel & TS in production build
  // enabled by default if the machine has more than 1 cores
  parallel: require("os").cpus().length > 1,
  // PWA 插件相关配置
  // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
  pwa: {},
  // webpack-dev-server 相关配置
  devServer: {
    open: true,// 启动后自动打开浏览器
    host: "localhost",
    port: 9002, ,
    https: false,
    hotOnly: false,
    proxy: {
      // 设置代理
      // proxy all requests starting with /api to jsonplaceholder
      "/api": {
        target: "http://localhost:3000/",
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          "^/api": ""
        }
      }
    },
    before: app => {}
  },
  // 第三方插件配置
  pluginOptions: {
    // ...
  }
};

至此,一套完整的Vue+TypeScript的开发环境就搭建完成了。接下来就可以愉快的进行项目开发了。

2、快速上手项目开发

2.1、让TS识别Vue

  • .vue后缀文件导入
    TypeScript开发环境默认是只能识别*.ts*.tsx文件的,因此在遇到导入*.vue文件时,会无法识别,因此在导入vue文件时需要加上后缀.vue,如下:
import Component from 'components/component.vue'
  • 声明文件
    VueCli脚手架生成的项目目录下默认会有一个shims-vue.d.ts文件,内容如下:
declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

该声明文件是告诉TS以.vue为后缀的文件交给Vue模块处理,文件中vue是指Vue的实例。

2.2、用类的方式编写组件

vue-class-component

vue-class-componentVue的官方一个基于类组件的一个库,它可以让我们以类的方式开发Vue组件,它提供的Component装饰器为类添加注释,从而以直观和标准的类语法定义组件数据和方法。以下为Vue类组件的定义方式:

<template>
  <input v-model="name" @click='handleInputChange'>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'
import childComponent from '@/components/childComponent.vue'
// 注册第三方库钩子函数,以Vue-Router为例,实际开发可以单独抽成一个文件,在main.ts中引入
Component.registerHooks([
  'beforeRouteEnter'
])
// 定义类组件,需要使用@Component装饰器装饰
@Component({
  // components、props、watch及其他options
  // 其他options可以查看:
  components: {
    childComponent
  },
  props: {},
  watch: {}
})
export default class myComponent extends Vue {
  // data
  private firstName: string = 'Jim'
  private lastName: string = 'Green'
  // lifecycle hook
  created() {}
  // 路由组件内守卫钩子函数
  beforeRouteEnter(to, from, next) {
    console.log('beforeRouteEnter')
    next()
  }
  // computed
  get name() { // 取值
    return this.firstName + ' ' + this.lastName
  }
  set name(value) { // 存值
    const splitted = value.split(' ')
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ''
  }
  // method
  private handleInputChange():void {

  }
}
</script>

vue-property-decorator

vue-property-decorator是第三方基于vue-class-component扩展而成的一个库,它完全依赖vue-class-component,除了具备vue-class-component的能力以外,还另外扩展了10个装饰器,分别为:@Prop@PropSync@Model@Watch@Provide@Inject@ProvideReactive@InjectReactive@Emit@Ref。以下详细的解析下常用的几个:

  • @Prop

@Prop装饰器主要用来进行父组件向子组件传递数据

import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  @Prop(Number) readonly propA: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
}

等同于

export default {
  props: {
    propA: {
      type: Number
    },
    propB: {
      default: 'default value'
    },
    propC: {
      type: [String, Boolean]
    }
  }
}
  • @Emit

@Emit装饰器是用来子组件通过派发事件向父组件传递数据

import { Vue, Component, Emit } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  count = 0
  @Emit()
  addToCount(n: number) {
    this.count += n
  }

  @Emit('reset')
  resetCount() {
    this.count = 0
  }

  @Emit()
  returnValue() {
    return 10
  }

  @Emit()
  onInputChange(e) {
    return e.target.value
  }

  @Emit()
  promise() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}

等同于

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    returnValue() {
      this.$emit('return-value', 10)
    },
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })

      promise.then(value => {
        this.$emit('promise', value)
      })
    }
  }
}
  • @Watch

@Watch装饰器是用于属性监听,接受一个参数作为监听属性,当属性值发生变化时,会触发执行被装饰的函数。

import { Vue, Component, Watch } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  @Watch('child')
  onChildChanged(val: string, oldVal: string) {}

  @Watch('person', { immediate: true, deep: true })
  onPersonChanged1(val: Person, oldVal: Person) {}

  @Watch('person')
  onPersonChanged2(val: Person, oldVal: Person) {}
}

等同于

export default {
  watch: {
    child: [
      {
        handler: 'onChildChanged',
        immediate: false,
        deep: false
      }
    ],
    person: [
      {
        handler: 'onPersonChanged1',
        immediate: true,
        deep: true
      },
      {
        handler: 'onPersonChanged2',
        immediate: false,
        deep: false
      }
    ]
  },
  methods: {
    onChildChanged(val, oldVal) {},
    onPersonChanged1(val, oldVal) {},
    onPersonChanged2(val, oldVal) {}
  }
}
  • @Ref

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

import { Vue, Component, Ref } from 'vue-property-decorator'
import AnotherComponent from '@/path/to/another-component.vue'
@Component
export default class YourComponent extends Vue {
  // 获取整个组件
  @Ref() readonly anotherComponent!: AnotherComponent
  // 获取具体某个元素
  @Ref('aButton') readonly button!: HTMLButtonElement
}

等同于

export default {
  computed() {
    anotherComponent: {
      cache: false,
      get() {
        return this.$refs.anotherComponent as AnotherComponent
      }
    },
    button: {
      cache: false,
      get() {
        return this.$refs.aButton as HTMLButtonElement
      }
    }
  }
}

还有其他装饰器的具体用法可参见官方仓库:https://github.com/kaorun343/vue-property-decorator

  • 简单示例:
// TodoList.vue
<template>
  <div class="todo-list">
    <div class="nav">
      <a-input placeholder="please input todo" v-model="inputValue" />
      <a-button type="primary" @click="addTodoItem">添加</a-button>
    </div>
    <ul class="list">
      <todo-item
      v-for="(item,index) in todoList" 
      :key="index"
      :item='item'
      :itemIndex='index'
      @on-delete='handleDelete'
      >{{item.text}}
      </todo-item>
    </ul>
  </div>
</template>
<script lang='ts'>
  import { Vue, Component,Watch } from 'vue-property-decorator'
  import TodoItem from '@/components/todoListItem'
  @Component({
  name: 'todoList',
  components: {
    TodoItem
  }
  })
  export default class TodoList extends Vue {
  // data
  public inputValue:string = ''
  public todoList:any = []
  // methods
  private addTodoItem():void {
    if (!this.inputValue.trim().length){
      return
    }
    let item = {
      text: this.inputValue
    }
    this.todoList.push(item)
    this.inputValue = ''
  }
  private handleDelete(index:number) {
    this.todoList.splice(index,1)
  }
  // watch
  @Watch('inputValue')
  onChangeInputValue(val: string, oldVal: string) {}
  }
</script>

// TodoItem.vue
<template>
  <div class="list-item">
    <li class="todo-item">
      <p class="text">{{item.text}}</p>
      <a-icon type="delete" @click.native="deleteItem"/>
    </li>
  </div>
</template>
<script lang='ts'>
  import { Component, Vue, Prop,Emit } from 'vue-property-decorator';
  interface ItemContent {
  text: string
  }
  @Component({
  name: 'TodoListItem'
  })
  export default class TodoListItem extends Vue {
  @Prop({default: {} }) public item!: ItemContent
  @Prop() public itemIndex!: number
  @Emit('on-delete')
  private deleteItem() {
    return this.itemIndex
  }
}
</script>

3、使用JSX进行组件开发

Vue除了模板语法外,还支持JSX语法,这里也尝试用JSX语法定义类组件。

import { Component, Vue, Prop, Emit } from 'vue-property-decorator';
import './todoListItem.scss'
interface ItemContent {
  text: string
}
@Component
export default class TodoItem extends Vue {
  @Prop({default: {} }) public item!: ItemContent
  @Prop() public itemIndex!: number

  // jsx
  protected render() {
    return (
    <li class='todo-item'>
      <p>{this.item.text}</p>
      <a-icon type="delete" nativeOn-click={this.onDelete}/>
    </li>
    )
  }
  @Emit('on-delete')
  private deleteItem() {
    return this.itemIndex
  }
}

4、引入Vuex进行状态管理

Vuex作为Vue的状态管理工具,使得公共数据的管理更加便捷,在类组件的开发中,我们可以使用基于vuexvue-class-componentvuex-class库。

  • 安装
npm install --save vuex-class
  • 基本使用
    vuex-class提供了四个装饰器,让我们可以通过类的方式使用vuex,分别为:
    @State,@Getter,@Mutation,@Action
import Vue from 'vue'
import Component from 'vue-class-component'
import {State,Getter,Action,Mutation} from 'vuex-class'
@Component
export class myComponent extends Vue {
// @State state中的foo 映射到组件的stateFoo
@State('foo') private stateFoo!:string 
// @Getter  getter中的foo 映射到组件的getterFoo
@Getter('foo') private getterFoo!: string
// @Mutation 修改state数据的方式,Mutationz中的mutationFoo方法映射到组件的mutationFoo方式
// 如果带参数使用时:this.mutationFoo({key:value}),内部执行了store.commit('mutationFoo', { key: value })
// 如果定义了类型,可以使用定义的,这里any避免ts警告
@Mutation('mutationFoo') private mutationFoo!: any
// @Action 映射action中的方法,action中的actionFoo方法映射到组件的actionFoo
// 如果带参数:this.actionFoo({key:value}),内部执行了store.dispatch('foo', { value: true })
// 如果定义了类型,可以使用定义的,这里any避免ts警告
@Action('actionFoo') private actionFoo!:any
}

其他更多用法,可以参考官方仓库:https://github.com/ktsn/vuex-class

说在最后

TS相较于JS真正强大之处在于类型校验,一定程度上减少了JS饱受诟病的弱类型导致的潜在bug。本文在vue开发之中加入TS,并以类组件的方式进行开发,并不是vue项目开发融入TS所必须的,您即使使用之前的组件开发方式一样可以使用TS,只是类组件的开发方式能够以更加扁平化的方式编写组件。本文中弱化了真正的TS的类型校验,而这部分才是我们真正值得深入和探索的。