使用 Vue Cli3 + TypeScript + Vuex + Jest 构建 todoList

前言

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

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

建立项目

❓为何使用 Vue Cli3 构建项目html

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

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

  • 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
复制代码

看一下新项目的层级目录node

╭─~/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 进行解释webpack

{
  "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 文件支持的代码库

咱们来看一下示例:git

// 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,ScriptHostes6

  • 针对于 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>
      <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 文件,保持应用的健壮性。

Jest

本项目使用 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
复制代码

写在最后

  1. 本文只是介绍了一个简单构建 ts-vue 应用的例子,对于框架的健壮和可扩展性有须要慢慢考虑,好比 webpack 的配置,适应测试,生产等各类环境的区分,axois 的封装,等等。
  2. 对于vue + ts 的配方,文章还有不少 vue 的特性没有去兼容,好比 this.refs 的使用,好比 vue-property-decorator 其余特性的使用。
  3. 因为官方文档对 ts 的介绍有限,因此以上代码确定有不足的地方,但愿你们指正。

源码

github.com/EricLLLLLL/…

相关文章
相关标签/搜索