ts的大勢所趨,你還在吭哧吭中徘徊在vue+js大門口嗎?來吧,是時候表演真正的技術了~~~javascript
從vue開始火熱起來到如今,已經基本上前端開發小夥伴入門的技能了。相信這麼久時間過去以後,你們也早已習慣vue的開發模式了。那麼,你和別人比比的時候,難道不想有些許亮點嗎?雖然目前vue2+對ts的支持沒有像react、ng等支持的更友好,可是隨着社區相關工具鏈的完善,其生產項目使用vue+ts也是徹底ok的。css
相信不少小夥伴也早已磨刀霍霍、躍躍欲試了。那麼在vue實際生產項目該如何規範、系統的使用vue+ts的開發模式呢?本文將系統的講述如何在vue2+中開發typescript+class+註解
風格的項目。圖文詳情、系統介紹、專一於排坑,一塊兒在生產項目中大膽使用vue+ts吧。奧利給~~~html
# 終端運行
vue create vue-ts-demo
複製代碼
能夠看到,命令運行後,讓咱們選擇項目相關的配置參數,這裏咱們選擇手動選擇(Manually select fetures)前端
這裏如上圖,最重要的是勾選typescript,除了pwa以外,咱們仍是都選擇配置吧。固然了你也能夠根據你的需求而定,可是ts必需要勾選,筆記是ts的項目嘛~~~vue
這裏選擇class風格,回車就好。若是你不喜歡class風格,也能夠選擇否,可是咱們建議ts開發時選擇class風格,優雅(裝逼…),以下圖,最終class風格的代碼會是這樣:java
僅演示部分截圖,後續會詳細講解後面就是一些基本的配置選擇了,像css預處理器、代碼規範、配置文件位置、router mode等等,你們根據團隊規範或者我的的風格進行選擇吧,這個沒什麼要特別說明了。最後等待項目初始化完成便可。node
# 進入項目文件夾
cd vue-ts-demo
# 運行項目
npm run serve
複製代碼
cli4+項目在初始化完成以後,其實依賴是已經安裝好了,是不須要再npm i
的了react
項目啓動後,應該就能夠看到很熟悉的頁面了webpack
從圖上能夠看到,基礎目錄基本上和js版的vue項目沒太大區別。額外須要注意的是,能夠看到根目錄新增了tsconfig.json
做爲ts的配置文件,還有其餘的配置都獨立成了配置文件的形式,這是由於我勾選的是配置文件獨立存在。你初始化項目時也能夠選擇都放在browserList配置中。git
另外,須要注意的是,之前全部的.js
文件都變成了.ts
文件。
<template>
<div class="hello">
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
// 聲明一個props
@Prop() private msg!: string;
// 聲明一個變量
private count: number = 0
// 聲明一個方法
private addCount() {
this.count++
}
}
</script>
<style scoped lang="less">
</style>
複製代碼
能夠看到,vue組件的三大件仍是那三大件,template、script、style。可是須要注意的是script標籤須要加lang="ts"
的標識,來指明當前是ts的語法。
另外,組件的基本使用方式,變成了註解+class風格的。從vue-property-decorator
中引入了咱們須要的內容,最後導出一個繼承後的類,這個類也能夠是匿名的(若是你想偷個小懶的話):
@Component
export default class extends Vue {
// ...
}
複製代碼
import {
Component, Vue, Prop, Watch, Emit,
} from 'vue-property-decorator';
@Component
export default class Header extends Vue {
// data內的屬性直接做爲實例屬性書寫
// 默認都是public公有屬性
nums = 0;
// 也能夠定義只讀
readonly msg2 = 'this is readonly data'
// 定義私有屬性
private msg3 = 'this is a private data'
// 定義一個私有、只讀數據
private readonly msg4 = 'this is a private and readonly data'
// @Prop定義props
// 花括號內定義prop的參數,例如default、type、required等
@Prop({ default: 'this is title' }) title!: string;
@Prop({ default: true }) fixed: boolean | undefined;
// 經過getter書寫計算屬性
get wrapClass() {
return ['head', {
'is-fixed': this.fixed,
}];
}
// 觀察屬性
// 能夠經過第二個參數,設置immediate和deep
@Watch('nums', { immediate: true, deep: true })
handleNumsChange(n: number, o: number) {
console.log(n, o);
}
// 定義emit事件,參數字符串表示分發的事件名
// 若是沒有,則使用方法名做爲分發事件名,會自動轉連字符寫法
// 注意,這樣寫法貌似沒法派發多個參數,
// 能夠經過原有的this.$emit寫法派發
@Emit('add')
emitAdd(n: number) {
return n;
}
@Emit('reduce')
emitReduce() {
return (this.nums, 123, 321);
}
// 全部方法名,直接做爲類的方法書寫
private handleClickAdd() {
this.nums += 1;
this.emitAdd(this.nums);
}
private handleClickReduce() {
this.nums -= 1;
this.emitReduce();
}
}
複製代碼
上面組件,在註釋裏面詳細說明了ts組件的基本經常使用語法。有了這些語法,基本頁面書寫和數據渲染應該沒問題了。關於class組件的用法,項目實際上是使用了vue-class-component和vue-property-decorator兩個庫。更多的基本語法,小夥伴仍是須要翻閱文檔的。
這裏要說明一下這兩個庫的關係。vue-class-component
是官方維護的一個讓vue支持class的風格的庫,而vue-property-decorator
也是基於vue-class-component
的,可是在它基礎之上支持了註解的語法,從而讓咱們的代碼語法更加的簡潔和易於複用。從咱們的項目也能夠看到,其實最終使用的是vue-property-decorator
的語法的。
不少時候,若是咱們像快速的打開一個庫的文檔地址或者源碼地址時(又懶得去查),能夠經過npm的命令快速作到,只須要終端運行:
# 查閱文檔
npm docs 庫名稱
# 例如查詢vue-class-component的文檔地址
npm docs vue-class-component
# 查看源碼地址
npm repo 庫名稱
複製代碼
那麼這是怎麼作到的呢?
其實這些圖在開發時是會在其package.json文件中指定的相關參數的。對npm感興趣的小夥伴,能夠繼續翻閱npm文檔,學習更多的npm知識。
說完了vue單文件的基本用法以後,咱們要來聊聊路由了。在ts模式下,路由仍是有些東西須要說明的,否則你在項目裏有些坑,會讓你丈二和尚摸不着頭腦的~~
如圖所示,基本的路由使用方式,項目初始化的時候已經配置好了,只不過在原來的基礎之上增長了ts的類型。
這裏須要注意一下,base的設置是process.env.BASE_URL
。這個是哪裏來的呢?其實讀取的配置文件中的信息。cli4+初始化的項目是支持配置文件的。能夠在根目錄下根據環境不一樣新建幾個配置文件:
注意是.env
開頭,後面是環境類型,例如這裏簡單演示了本地開發環境,線上dev環境和線上生產環境的配置文件。在配置文件中,咱們能夠根據不一樣環境進行不一樣的配置,例如:
例如上面的路由base的配置,也大多時候會在這裏配置不一樣的環境的api地址。可能小夥伴這裏會有個小疑問,爲何配置了local環境還配置了一個dev環境,若是你本地開發出現跨域,然後端須要你來處理的時候,你跨域在這裏單獨配置一個本地環境處理api,而後作一些代理的事情,而後線上dev的時候不去處理跨域的問題。仍是根據你的需求。
這裏再補充一下,若是前端處理跨域該怎麼處理吧。在根目錄下新建一個vue.config.js
文件,這是cli4中自定義webpack腳本配置的方法,能夠翻閱cli文檔,有詳細的說明。這裏看下具體怎麼配置:
// 以我上面爲例
module.exports = {
devServer: {
proxy: {
'/hermes/api/v1/': {
// 須要代理到的域名地址
// 即,當遇到'/hermes/api/v1/'時,將請求代理到'http:xxxx.com'
target: 'http:xxxx.com',
changeOrigin: true
},
'/bms/api/v1/': {
target: ''http:xxxx.com',
changeOrigin: true
}
}
},
}
複製代碼
解釋一下,上面經過設置proxy,來設置當遇到'/hermes/api/v1/'的請求地址時,則將請求代理到其'http:xxxx.com'域名,後面同理。
那這時候,須要給你們看一下api怎麼寫了:
// 簡單演示一下,由於api是的單獨管理處理的
// 都在src/api/文件夾下
// 例如這是一個account.ts模塊
// 引入咱們的環境變量中的域名前綴
// 本地開發中,確定就是咱們上面設置的域名前綴
// 這樣的話,便會進行代理服務了,從而處理跨域問題
const { VUE_APP_API_KB } = process.env;
export interface ILogin {
account: string;
password: string;
}
// 登陸
export const login = (data: ILogin): Promise<object> => {
data.password = md5(data.password);
return request({
url: `${VUE_APP_API_KB}login`,
method: 'post',
data,
});
};
複製代碼
想使用路由鉤子,很重要的一點,是要先註冊,否則是沒法使用的。那麼如何註冊呢?咱們在router文件夾下新建一個個class-component-hooks.ts
文件:
import Component from 'vue-class-component';
// or
// import { Component } from 'vue-property-decorator';
// Register the router hooks with their names
Component.registerHooks([
'beforeRouteEnter',
'beforeRouteLeave',
'beforeRouteUpdate',
]);
複製代碼
而後在mian.ts
文件中,最頂部進行引入,保證在全部組件引入以前執行。這點很重要!!!
// main.ts頂部導入
import './router/class-component-hooks';
複製代碼
話題仍是回到咱們的路由上面。關於路由,有一點須要額外注意的就是路由守衛的使用,這點是要注意的,否則報錯。
首先,咱們看下頁面內的路由守衛的參數問題:
// 引入註解插件暴露出的相關內容
import { Vue, Component } from 'vue-property-decorator';
// 引入路由中的Route對象
import { Route } from 'vue-router';
@Component({})
export default class App extends Vue {
// 參數類型的定義,使用Route,
// next是函數,因此能夠直接使用Function對象,
// 或者是next: () => void
beforeRouteEnter(to: Route, from: Route, next: Function) {
next();
}
}
複製代碼
能夠看到,咱們在使用相似beforeRouteEnter
等路由鉤子時,須要定義參數的類型,這裏咱們從vue-router
中引入了Route
類型做爲from
和to
的參數類型。由於ts函數參數是須要定義類型的。而next參數,若是求簡單的話,能夠直接寫個Function
類型。
說到vuex,但是vue項目的重點之一了。相信你們對於js版本的項目如何使用vuex已是如魚得水了。這裏介紹一下ts版本中如何使用vuex。
在這裏,我介紹的是vuex-module-decorators,包括個人項目中也是使用的該庫。除此以外也有一個vuex-class,這兩個都是能夠在ts中輔助咱們更好的使用vuex的。可是我爲何選擇和推薦vuex-module-decorators
,後面我會進行說明。好了,下面先看下這個庫該如何使用吧:
# 安裝vuex-module-decorators
# 支持註解的方式開發store中的內容
cnpm i vuex-module-decorators -S
複製代碼
我先看下咱們以前默認的store是什麼內容:
能夠看到基本上和正常咱們的項目初始化的store結構並沒有二異,無非是index.js變成了index.ts。
下面咱們先基本的組織一下文件結構,項目中基本上必定是會按模塊劃分的,除非項目真的很小。以下圖,應該是咱們比較熟悉的代碼組織結構:
正常來講,咱們會在modules
文件夾中劃分功能模塊,index.ts
做爲出口,constant.ts
做爲常量文件;mutation-types.ts
做爲mutations
的類型管理文件。這裏暫時先不說store.ts,由於這是咱們等下改造後的文件結構;
接下來,咱們就挨個仔細說說每一個文件該如何書寫和組織。
// store/index.ts
export { default as AccountModule } from './modules/account';
export { default as DownloadModule } from './modules/download';
export { default as DownlableModule } from './modules/kbAnalyse';
// ....
複製代碼
如上所示,是咱們的index.ts文件,咱們在這個文件中作了什麼事情呢?其實就是把咱們的moudles文件夾下的模塊導入進來,而後再統一暴露出去。若是不清楚語法爲什麼這樣寫的,能夠點擊這裏瞭解一下ES6導入導出的複合寫法。
那咱們爲何要這麼作呢?爲何要統一導入導出呢?實則是爲了使用方便,下面咱們看下是如何在頁面中使用store中的數據和方法的。
咱們知道,在正常模式下的vue組件使用store一般是以下這樣的:
// 組件中
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('analyse', [
'someGetterName'
])
},
methods: {
...mapActions('analyse', [
'customResetState'
]),
}
}
複製代碼
下面咱們看下在引入了vuex-module-decorators
以後是如何使用vuex中數據和方法的:
// class風格的組件中
import { Component, Vue, Ref } from 'vue-property-decorator';
// 首先引入咱們的store中的模塊
import { AccountModule } from '@/store/index';
@Component({
name: 'Login',
})
export default class extends Vue {
// 省略其餘代碼
/**
* 發送登陸的request請求
*/
private async submitLogin() {
if (this.isLoading) return;
try {
this.isLoading = true;
const params = clone(this.formData);
// 這裏直接經過該模塊調用store中的login這個action
await AccountModule.login(params);
const { redirect } = this.$route.query;
this.$router.replace(redirect ? decodeURIComponent(redirect) : '/');
} catch (error) {
this.$Notice.error({ title: error });
} finally {
this.isLoading = false;
}
}
// 省略其餘代碼
}
複製代碼
上述演示了一個action的基本調用,其餘state、getter等都同樣,直接調用模塊裏面對應的內容便可。如此,還算是蠻方便的。更有ts的類型推導的加持,很是方便。
上面介紹了store/index.ts以及組件中如何使用,如今咱們看下如何定義模塊。說到開發模塊,其實咱們看到,咱們只是在index.ts中對模塊進行了導入導出,其實咱們仍是沒有實例化咱們的store實例的。由於原本是在index中實例化的,咱們把index.ts用來導入導出了,因此實例化的部分沒了。
那麼先說說爲何要在index.ts中進行導入導出,實際上是爲了在組件人引入時store模塊時方便,根據node.js的文件查找規則,咱們只須要寫到'@/store'便可。說完了這個,該來看咱們store/store.ts了:
// store/store.ts
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({});
複製代碼
能夠看到咱們的store.ts,實際上是被咱們用來實例化store了。注意,這裏咱們沒有簡化演示,確確實實咱們實例化store時什麼都沒有,由於咱們想作的是動態註冊store模塊,store是支持這個功能的。下面咱們看下如何動態註冊store模塊。
咱們來看下咱們書寫store模塊的基本的一個格式:
import store from '@/store/store';
import {
VuexModule, Module, Mutation, Action, getModule,
} from 'vuex-module-decorators';
@Module({
dynamic: true, namespaced: true, name: 'account', store,
})
class Account extends VuexModule {
// state演示,用戶token
public token = getToken() || ''
// getter演示,平臺code數組
public get platformGetter(): string[] {
return this.userConfig.platform;
}
// mutation演示
@Mutation
private [types.SET_TOKEN](token: string) {
this.token = token;
setToken(token);
}
// action演示
// 用戶登陸
@Action({ commit: types.SET_TOKEN, rawError: true })
public async login(params: accountApi.ILogin) {
const data = await accountApi.login(params);
return data;
}
}
複製代碼
經過如上演示,咱們要注意幾個小點:
1、咱們須要定義一個繼承VuexModule的類來做爲導出的module
2、咱們經過實例屬性來屬性state對象,經過類的get屬性來屬性vex的getter屬性,經過@Mutation、@Action來開發對於的mutation和action
3、咱們在@Module({})
中動態註冊模塊,須要咱們手動傳入store
和經過dynamic: true
指定是動態的;經過namespaced
開啓命名空間,name
來指定模塊名稱;
4、注意一下mutation的名稱寫法,和vue中實際上是同樣的,不過可能還有一些小夥伴不知道[types.SET_TOKEN]() {}
究竟是什麼意思,這實際上是es6的屬性名錶達式,就是把表達式做爲屬性的key,不清楚的小夥伴能夠點擊屬性名錶達式補補課。
5、action這裏,有2點須要注意一下。首先是,若是在action參數中指定來commit是誰,那麼能夠在action方法內直接return數據,這樣其實也是會調用上面的commit方法的
@Action({ commit: types.SET_TOKEN})
public async login() {
const data = await accountApi.login();
return data;
}
// 等同於
@Action()
public async login() {
const data = await accountApi.login();
// 注意,必定是context對象上調用commit方法
this.context.commit(types.SET_TOKEN, data)
}
複製代碼
正常調用commit都是經過this.context來傳遞的,只不過若是隻要一個就能夠簡寫而已;若是是須要多個commit,那隻能是老老實實commit了。
關於action,還有一點須要注意的是錯誤的捕獲,以下所示:
@Action({ commit: types.SET_TOKEN, rawError: true })
複製代碼
這裏若是不配置rawError: true會使得你的主動拋錯會有問題。這個必定要注意。
6、最後一點,至於則麼取得其餘模塊的state、getter、action等,直接打印一些相關的參數和載體對象就知道了,暫很少說了。
這一塊沒什麼好說的,和日常項目同樣,把全部的mutation名稱集中管理起來。可是若是名稱愈來愈多,其實也是很差管理的,命名容易衝突,若是真的不少,建議仍是分模塊吧。
export const SET_TOKEN = 'SET_TOKEN';
export const RESET_TOKEN = 'RESET_TOKEN';
export const SET_USER_INFO = 'SET_USER_INFO';
export const SET_USER_CONFIG = 'SET_USER_CONFIG';
export const LOGOUT = 'LOGOUT';
複製代碼
咱們會在store中定義state,在引入了ts以後,咱們其實更好的能夠要求咱們的module的類實現咱們的定義的好的類型:
// store/module/account.ts
// 省略其餘...
interface IState {
token: string | undefined;
userInfo: IUserInfo;
userConfig: IConfig;
}
@Module({
dynamic: true, namespaced: true, name: 'account', store,
})
class Account extends VuexModule implements IState {
// 用戶token
public token = getToken() || ''
// 用戶信息
public userInfo: IUserInfo = getDefaultUserInfo()
// 客戶配置
public userConfig: IConfig = getUserConfig()
}
// ...省略其餘
複製代碼
能夠看到,咱們要求咱們的Account在繼承VuexModule後,必須實現咱們定義的IState這個類型,即咱們的類必需要有 IState中所含有的類型,不然ts就會給我報錯。同時,這些實例的類型也必須得是咱們以前定義好的類型。這樣就會使得代碼更嚴謹和更利於多人合做,這也正體現了ts優點所在。
前面也提到了說要對比一下兩個庫,爲何選擇vuex-module-decorators。
vuex-module-decorators是在定義模塊時經過註解(裝飾器)的語法來作的,在使用的時候經過直接引入對應的module讓,而後直接使用module上的屬性或方法。
vuex-class是在定義模塊時,和普通項目基本同樣,只不過是須要對變量增長類型而已。可是使用的時候是經過註解(裝飾器的語法),以下圖:
能夠看到,在定義模塊時的語法,vuex-module-decorators是更優雅一些的,而在使用時,vuex-class是更勝一籌的。
二者結合也是徹底能夠的,筆者也曾試驗過,確實能夠。那這樣豈不是完美來?那不由要問了,爲什麼還要放棄vuex-class呢?究其緣由,是由於vuex-class在使用時,部分地方會失去ts的類型支持,而只能指定爲any。這使得咱們在某種程度上失去引入ts的意義,故而筆者更多的選後的另一個。具體的例子,如今也不想演示了,也或許是有更好的處理方式,而筆者不曾發現吧,有清楚的小夥伴也歡迎指出。
# 若是eslint-plugin-import已安裝,則不須要重複安裝
cnpm install eslint-plugin-import eslint-import-resolver-alias --save-dev
複製代碼
// ...省略其餘
settings: {
'import/resolver': {
alias: {
map: [
['@', './src'],
],
extensions: ['.ts', '.js', '.jsx', '.json']
}
}
}
// ...省略其餘
複製代碼
eslint-import-resolver-alias文檔
// 若是須要在Vue.prototype上掛載一個方法,例如:
// 僅演示用法
const noticeDefaultConfig = {
duration: 2,
};
Vue.prototype.$success = (msg = '', config = {}) => {
Vue.prototype.$Notice.success({
title: msg,
...noticeDefaultConfig,
...config,
});
};
// 這時候若是直接使用,
// 則會在編譯階段報錯$success不存在,
// 因此須要處理,給vue加強類型
// src/shims-vue.d.ts
import Vue from 'vue'
import VueRouter, { Route } from 'vue-router'
// 原有的
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
// 增長的擴展
// 加強擴展vue的類型
declare module 'vue/types/vue' {
interface Vue {
$router: VueRouter
$route: Route
$success: Function
}
}
複製代碼
// 社區維護了@types/xxx的庫
// 目前主流的js庫,@types/上基本上都有維護其ts版本
// 因此直接上npm去搜索,例如lodash
@types/lodash
// 若是有的話則直接安裝使用,
// 注意安裝在本地開發依賴就好
cnpm i @types/lodash -D
複製代碼
對於@types沒有支持到的庫該怎麼辦?
最後說下interrace,隨着interface好type聲明的愈來愈多,把interface按模塊集中管理起來或許會更好。
說到components文件夾,也就是咱們的公共組件,這塊我更傾向於使用index.ts文件對全部的組件統一進行導入導出。這樣在引入組件的時候,就沒必要關注組件實際的位置了,只須要經過從components/index進行導入。來看下components文件夾目錄:
對於組件文件的命名,你們能夠參考官網文檔的建議,最終仍是以團隊規範爲主。好比咱們這裏基礎組件所有Base開頭,對於一個組件在一個模塊中只會使用一次的組件,以The開頭。所有采用大駝峯命名,包括組件調用也是。這個具體的看規範吧。再看下index.ts文件如何組織:
export { default as BaseButton } from './BaseButton/index.vue';
export { default as BaseSideContainer } from './BaseSideContainer/index.vue';
export { default as BaseNofify } from './baseNotify/index.vue';
// ...省略其餘
複製代碼
再來看下導入時:
import { BasePopoverConfirm } from '@/components';
複製代碼
最大的好處仍是在外部不須要care組件的組件文件夾結構和組件位置關係。components內組件的組織結構發生變化,是徹底不會影響到外部的引入路徑的。
但願小夥伴們再看完本篇文章,尚未使用vue+ts開發的小夥伴們能早日在實際項目中開發使用。我相信掌握了這些,進行vue+ts開發實際項目應該是基本OK的。但願本文對想使用ts的小夥伴有那麼一丟丟幫助,記得收藏點贊哦!!!
百尺竿頭、日進一步 我是大家的老朋友愣錘,若是以爲喜歡,歡迎點贊收藏!!!