Vue3.x 即未來襲,使用 TypeScirpt 重構,TypeScript 將成爲 vue 社區的標配,出於一名程序員的焦慮,決定如今 Vue2.6.x 踩一波坑。javascript
vue 官方文檔已經簡略地對 TypeScirpt 的支持進行了介紹,咱們使用 Vue Cli3 直接生成項目css
❓爲何使用 Vue Cli3 構建項目html
官方維護,後續升級減小兼容性問題vue
使用如下配置進行項目的生成:java
╭─~/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,ScriptHost
es6針對於 target:ES6:
DOM,ES6,DOM.Iterable,ScriptHost
相似於 eslint ,對 ts 代碼進行檢測。
vscode 須要安裝 tslint 插件 ,並在 vscode 的用戶配置中加入如下配置,用來在保存時自動解決 ts 的錯誤。
// settings.json
"editor.codeActionsOnSave": {
"source.fixAll.tsLint": true
}
複製代碼
❗️ vue cli3 已經安裝了
tslint
依賴
使用 prettier 插件,對項目進行代碼風格的統一和規範
npm i tslint-config-prettier -D
"extends": ["tslint:recommended", "tslint-config-prettier"]
複製代碼
設置 vscode
tslintIntegration
,使 prittier 支持格式化 ts 文件"editor.formatOnSave": true
保存時自動格式化也可使用
shift
+option
+f
進行格式化
在根目錄下添加 .prttierrc
文件 (應對 prittier 格式化 vue 文件中的 ts 文件時,沒辦法使用 tslint 規則進行格式化,須要對它單獨處理,以避免 tslint 報錯)
{ "singleQuote": true }
複製代碼
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');
複製代碼
寫一個 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 代碼的用法指出如下幾點:
xxx!: type
的形式,否則要寫成 xxx : type | undefined
改造 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>
複製代碼
有關 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
須要注意如下幾點:
interface Payload {
[propName: string]: any;
}
複製代碼
改造 /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 的調用有如下幾點注意
全部的代碼到此爲止,使用 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
進行處理modules
必需要指定 namespaced: true,
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
複製代碼