相关文章推荐
怕老婆的荒野  ·  JDBC获取 ...·  4 月前    · 
发呆的骆驼  ·  python - Pandas, ...·  1 年前    · 
慈祥的沙发  ·  Elasticsearch-cloud-az ...·  1 年前    · 

Vue3.x 即将来袭,使用 TypeScirpt 重构,TypeScript 将成为 vue 社区的标配,出于一名程序员的焦虑,决定现在 Vue2.6.x 踩一波坑。

vue 官方文档已经简略地对 TypeScirpt 的支持进行了介绍,我们使用 Vue Cli3 直接生成项目

❓为什么使用 Vue Cli3 构建项目

官方维护,后续升级减少兼容性问题

使用以下配置进行项目的生成:

  • Babel 对 Ts 进行转译
  • TSLint 对 TS 代码进行规范,后续会使用 prettier 对项目进行编码的统一
  • 默认安装 Vuex 和 Router , Router 使用 history 模式
  • 使用 Jest 进行单元测试
  • ╭─~/otherEWokspace
    ╰─➤  vue create ts-vuex-demo
    Vue CLI v3.6.3
    ┌───────────────────────────┐
    │  Update available: 3.9.3  │
    └───────────────────────────┘
    ? Please pick a preset: Manually select features
    ? Check the features needed for your project: Babel, TS, Router, Vuex, CSS P
    re-processors, Linter, Unit
    ? Use class-style component syntax? Yes
    ? Use Babel alongside TypeScript for auto-detected polyfills? Yes
    ? Use history mode for router? (Requires proper server setup for index fallb
    ack in production) Yes
    ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are suppor
    ted by default): Sass/SCSS (with node-sass)
    ? Pick a linter / formatter config: TSLint
    ? Pick additional lint features: (Press <space> to select, <a> to toggle all
    , <i> to invert selection)Lint on save
    ? Pick a unit testing solution: Jest
    ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In de
    dicated config files
    ? Save this as a preset for future projects? Yes
    ? Save preset as: ts-vue-demo
    复制代码

    看一下新项目的层级目录

    ╭─~/otherEWokspace/ts-vuex-demo  ‹master›
    ╰─➤  tree -L 2 -I node_modules
    ├── README.md
    ├── babel.config.js
    ├── jest.config.js
    ├── package-lock.json
    ├── package.json
    ├── postcss.config.js
    ├── public
    │   ├── favicon.ico
    │   └── index.html
    ├── src
    │   ├── App.vue
    │   ├── assets
    │   ├── components
    │   ├── main.ts
    │   ├── router.ts
    │   ├── shims-tsx.d.ts
    │   ├── shims-vue.d.ts
    │   ├── store.ts
    │   └── views
    ├── tests
    │   └── unit
    ├── tsconfig.json
    └── tslint.json
    复制代码

    tsconfig.json

    lib 、 target 、 module 进行解释

    "compilerOptions" : { "target" : "esnext" , "module" : "esnext" , "strict" : true , "jsx" : "preserve" , // 开启对 jsx 的支持 "importHelpers" : true , "moduleResolution" : "node" , "experimentalDecorators" : true , "esModuleInterop" : true , "allowSyntheticDefaultImports" : true , "sourceMap" : true , "baseUrl" : "." , "types" : [ "webpack-env" , "jest" "paths" : { "@/*" : [ "src/*" "lib" : [ "esnext" , "dom" , "dom.iterable" , "scripthost" "include" : [ "src/**/*.ts" , "src/**/*.tsx" , "src/**/*.vue" , "tests/**/*.ts" , "tests/**/*.tsx" "exclude" : [ "node_modules"
  • target --- 被 tsc 编译后生成 js 文件代码风格
  • module --- 被 tsc 编译后生成 js 文件的模块风格
  • lib --- 原 ts 文件支持的代码库
  • 我们来看一下示例:

    // index.ts
    export const Greeter = (name: string) => `Hello ${name}`;
    
  • "module": "commonjs", "target": "es5"
  • // index.js
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.Greeter = function (name) { return "Hello " + name; };
    
  • "module": "es2015", "target": "es5"
  • // index.js
    export var Greeter = function (name) { return "Hello " + name; };
    
  • "module": "es2015", "target": "es6"
  • // index.js
    export const Greeter = (name) => `Hello ${name}`;
    
  • "module": "commonjs", "target": "es6"
  • // index.js
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.Greeter = (name) => `Hello ${name}`;
    

    如果lib没有指定默认注入的库的列表。默认注入的库为:

    针对于 target:ES5:DOM,ES5,ScriptHost

    针对于 target:ES6:DOM,ES6,DOM.Iterable,ScriptHost

    tslint

    类似于 eslint ,对 ts 代码进行检测。

    vscode 需要安装 tslint 插件 ,并在 vscode 的用户配置中加入以下配置,用来在保存时自动解决 ts 的错误。

    // settings.json
     "editor.codeActionsOnSave": {
        "source.fixAll.tsLint": true
    

    ❗️ vue cli3 已经安装了tslint依赖

    使用 prettier 插件,对项目进行代码风格的统一和规范

  • npm i tslint-config-prettier -D
  • 添加 tslint.json extends 字段如下:
  • "extends": ["tslint:recommended", "tslint-config-prettier"]
    复制代码

    设置 vscode

  • 勾选 tslintIntegration ,使 prittier 支持格式化 ts 文件
  • "editor.formatOnSave": true 保存时自动格式化
  • 也可以使用 shift + option + f 进行格式化

    在根目录下添加 .prttierrc 文件 (应对 prittier 格式化 vue 文件中的 ts 文件时,没办法使用 tslint 规则进行格式化,需要对它单独处理,以免 tslint 报错)

    { "singleQuote": true }
    复制代码

    shims-vue.d.ts

    declare module "*.vue" {
      import Vue from "vue";
      export default Vue;
    复制代码

    声明所有以 .vue 结尾的文件,默认导入 vue ,默认导出 Vue,用以在项目中ts文件识别 .vue 结尾文件。

    在 main.ts 中,引入一个 vue 组件必须以 .vue 结尾。

    import Vue from 'vue';
    import App from './App.vue';
    import router from './router';
    import store from './store';
    Vue.config.productionTip = false;
    new Vue({
      router,
      store,
      render: (h) => h(App),
    }).$mount('#app');
    复制代码

    Vue class

    vue-property-decorator

    写一个 todolist 组件顺便来介绍 vue-property-decorator,为了方便页面构建,使用 element-ui

    element-ui 使用 ts 开发,默认有 .d.ts 的声明文件

    npm i element-ui
    复制代码
    // main.ts
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    Vue.use(ElementUI);
    复制代码

    /src/compenents/ 新建 todoList.vue , 代码如下:

    <template>
      <div class="todo_list">
        <el-card class="box-card">
          <div slot="header">
            <el-row :gutter="18">
              <el-col :span="18">
                <el-input
                  id='todo'
                  v-model="todo"
                  placeholder="请输入内容"
                ></el-input>
              </el-col>
              <el-col :span="2">
                <el-button
                  id="add"
                  type="primary"
                  icon="el-icon-circle-plus-outline"
                  @click="addItem"
                >add</el-button>
              </el-col>
            </el-row>
          </div>
            v-for="(item,index) in todoList"
            :key="index"
            class="text item"
            @click="removeItem(index)"
          >{{ item }}</div>
        </el-card>
        <label
          class="text"
          style="margin-top:20px"
        >{{todoLength}} records</label>
      </div>
    </template>
    <script lang="ts">
    import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
    @Component
    export default class HelloWorld extends Vue {
      public todo: string = '';
      @Prop({ default: () => [] }) private readonly todoList!: string[];
      get todoLength(): number {
        return this.todoList.length;
      @Emit()
      private addItem(): string {
        return `${this.todo}`;
      @Emit('removeItem')
      private removeItem(index: number): number {
        return index;
    </script>
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped lang="scss">
    .todo_list {
      display: flex;
      justify-content: center;
      flex-direction: column;
      align-items: center;
      .box-card {
        width: 480px;
      .text {
        font-size: 14px;
        text-align: left;
      .item {
        margin-bottom: 18px;
    </style>
    复制代码

    对 ts 代码的用法指出以下几点:

  • prop 建议写成 xxx!: type 的形式,不然要写成 xxx : type | undefined
  • @Emit 可以不传参数,emit 出去的事件名默认是修饰的函数名,但是当函数的命名规则为 camelCase 时需要注册的函数名必须是 kebab-case
  • @Emit 传参是由修饰的函数 return value
  • 改造 Home.vue 如下:

    <template>
      <div class="home">
        <todoList
          :todoList="[]"
          @add-item="addTodoList"
          @removeItem="addTodoLisItem"
      </div>
    </template>
    <script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';
    import todoList from '@/components/todoList.vue'; // @ is an alias to /src
    import { State, Getter, Action } from 'vuex-class';
    @Component({
      components: {
        todoList
    export default class Home extends Vue {
      public addTodoList(val: string) {
        console.log(val);
      private created() {
        console.log('i add life cycle funciton -- created');
      private addTodoLisItem(index: number) {
        console.log(index);
    </script>
    复制代码

    Vuex

    有关 ts 中的 vuex 的写法要从 vuex-class 说起,在 官方的 vue-property-decorator 中也推荐使用该库。

    npm i vuex-class
    复制代码

    src 文件夹中新建 store 文件夹, 在 store 新建 index.ts,todoList.ts

    // index.ts
    import Vue from 'vue';
    import Vuex from 'vuex';
    import todolist from './todoList';
    Vue.use(Vuex);
    export default new Vuex.Store({
      modules: { todolist }
    复制代码
    // todoList.ts
    import { Commit, Dispatch, GetterTree, ActionTree, MutationTree } from 'vuex';
    const ADD_TODOLIST = 'ADD_TODOLIST';
    const REMOVE_ITEM = 'REMOVE_ITEM';
    export interface RootState {
      version: string;
    interface Payload {
      [propName: string]: any;
    interface TodoListType {
      todoList: string[];
    interface Context {
      commit: Commit;
      dispatch: Dispatch;
    const dataSource: TodoListType = {
      todoList: []
    const getters: GetterTree<TodoListType, RootState> = {
      getTodoList(state: TodoListType): string[] {
        return state.todoList;
    const mutations: MutationTree<TodoListType> = {
      ADD_TODOLIST: (state: TodoListType, item: string) => {
        console.log(item);
        state.todoList.push(item);
      REMOVE_ITEM: (state: TodoListType, removeIndex: number) => {
        state.todoList = state.todoList.filter((item: string, index: number) => {
          return removeIndex !== index;
    const actions: ActionTree<TodoListType, RootState> = {
      addList: async ({ commit }: Context, item: string) => {
        await Promise.resolve(
          setTimeout(() => {
            commit(ADD_TODOLIST, item);
          }, 100)
      removeItem: async ({ commit }: Context, { index }: Payload) => {
        await Promise.resolve(
          setTimeout(() => {
            commit(REMOVE_ITEM, index);
          }, 100)
    export default {
      namespaced: true,
      state: dataSource,
      getters,
      mutations,
      actions
    复制代码

    删除原来与 main.ts 同级的 store.ts

    todoList.ts 需要注意以下几点:

  • 对于 getters 、mutations 、actions 响应的 type 可以使用 command + 左键点击 进入声明文件查看,也可以不指定 type ,但是建议写上
  • 对于 Payload 解构 tslint 报错的,可以为 Payload 添加类型声明
  • interface Payload {
      [propName: string]: any;
    
  • 代码中的 dataSource 本意为 state ,但是不能用 state 命名,tslint 会和形参 state 冲突
  • 改造 /views/Home.vue 如下:

    <template>
      <div class="home">
        <todoList
          :todoList="todoList"
          @add-item="addTodoList"
          @removeItem="removelistItem"
      </div>
    </template>
    <script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';
    import todoList from '@/components/todoList.vue'; // @ is an alias to /src
    import { State, Getter, Action } from 'vuex-class';
    const namespace = { namespace: 'todolist' };
    @Component({
      components: {
        todoList
    export default class Home extends Vue {
      // @State(state => state.todolist.todoList) private todoList!: string[];
      @State('todoList', namespace) public todoList!: string[];
      @Action('addList', namespace) private addList!: (val: string) => void;
      @Action('removeItem', namespace) private removeItem!: (
        payload: object
      ) => void;
      // @Action('todolist/removeItem') public removeItem!: (index: number) => void;
      public addTodoList(val: string) {
        console.log('val', val);
        if (val) {
          this.addList(val);
      private created() {
        console.log('i add life cycle funciton -- created');
      private removelistItem(index: number) {
        console.log(index);
        this.removeItem({ index });
    </script>
    复制代码

    有关 vuex-class 的调用有以下几点注意

  • 修饰器的所有参数声明可以使用 command + 左键点击去查看,有多中写法,以上代码使用最优雅的写法
  • @Action 中函数的声明,形参必须和方法保持一致
  • 所有的代码到此为止,使用 npm run serve 即可查看应用,保留原有 routes 文件,保持应用的健壮性。

    本项目使用 vue-test-utils 进行编写,结合多种情况,得出以下代码:

    在 src/tests/unit 新建 todoList.spec.ts

    // todoList.spec.ts
    import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
    import Vue from 'vue';
    import Vuex from 'vuex';
    import ElementUI from 'element-ui';
    import todoList from '@/components/todoList.vue';
    import home from '@/views/Home.vue';
    const localVue = createLocalVue();
    localVue.use(ElementUI);
    localVue.use(Vuex);
    describe('todoList.vue', () => {
      let actions: any;
      let store: any;
      beforeEach(() => {
        actions = { addList: jest.fn(), removeItem: jest.fn() };
        const todolist = {
          namespaced: true,
          state: { todoList: [] },
          actions
        store = new Vuex.Store({
          modules: {
            todolist
      it('renders props.msg when passed', () => {
        const father = mount(home, { store, localVue });
        // const child = shallowMount(todoList, { localVue }) as any;
        const child = father.find(todoList) as any;
        child.vm.todo = 'todo 1';
        child.find('#add').trigger('click');
        expect(child.emitted()['add-item']).toBeTruthy();
        expect(actions.addList).toHaveBeenCalled();
    
  • 使用了第三方库需要使用 createLocalVue 进行处理
  • vuex 的模拟如果有用到 modules 必须要指定 namespaced: true,
  • 通过父组件拿到 子组件 后,如果要对子组件使用 find 方法,需使用 mount 包裹,不能使用 shallowMount
  • 在命令行调用 npm run test:unit 可查看测试结果

    最终项目目录结构如下

    ╭─~/otherEWokspace/ts-vuex-demo  ‹master›
    ╰─➤   tree -L 4 -I node_modules
    ├── README.md
    ├── babel.config.js
    ├── jest.config.js
    ├── package-lock.json
    ├── package.json
    ├── postcss.config.js
    ├── public
    │   ├── favicon.ico
    │   └── index.html
    ├── src
    │   ├── App.vue
    │   ├── assets
    │   │   └── logo.png
    │   ├── components
    │   │   └── todoList.vue
    │   ├── main.ts
    │   ├── router.ts
    │   ├── shims-tsx.d.ts
    │   ├── shims-vue.d.ts
    │   ├── store
    │   │   ├── index.ts
    │   │   └── todoList.ts
    │   └── views
    │       ├── About.vue
    │       └── Home.vue
    ├── tests
    │   └── unit
    │       └── todoList.spec.ts
    ├── tsconfig.json
    └── tslint.json
    复制代码

    写在最后

  • 本文只是介绍了一个简单构建 ts-vue 应用的例子,对于框架的健壮和可扩展性有需要慢慢考虑,比如 webpack 的配置,适应测试,生产等各种环境的区分,axois 的封装,等等。
  • 对于vue + ts 的配方,文章还有很多 vue 的特性没有去兼容,比如 this.refs 的使用,比如 vue-property-decorator 其他特性的使用。
  • 由于官方文档对 ts 的介绍有限,所以以上代码肯定有不足的地方,希望大家指正。
  • github.com/EricLLLLLL/…

  • 焕然一新的 Vue 3 中文文档要来了🎉
  • vue3保姆级教程
  • 私信