使用 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/…

相關文章
相關標籤/搜索