前言: 眾所周知奔浅,Vue
很優(yōu)秀,TypeScript
也很優(yōu)秀蚕脏,但是Vue
+ TypeScript
就會出現(xiàn)各種奇奇怪怪的問題具钥。本文就將介紹我在「CNode 社區(qū)」這個項目開發(fā)的過程中遇到一些問題和解決辦法。希望對你在Vue
中使用TypeScript
有所幫助驯用。
項目源碼及預(yù)覽地址
- 線上預(yù)覽地址
- 項目源碼 歡迎star脸秽,歡迎pr。
項目簡介
仿CNode社區(qū)蝴乔,使用Vue
+ TypeScript
+ TSX
等相關(guān)技術(shù)棧實現(xiàn)了原社區(qū)的看帖记餐、訪問用戶信息、查看回復(fù)列表薇正、查看用戶信息片酝、博客列表頁分頁查看等功能。
后端接口調(diào)用的是CNode 官方提供的api铝穷。
本項目中的所有組件都使用了Vue的渲染函數(shù)render
以及 TSX
钠怯。
項目安裝及啟動
yarn install
yarn serve
技術(shù)棧
Vue @2.6.11
TypeScript
TSX
SCSS
Vue + TypeScript 和 Vue的常規(guī)寫法有什么不同
起手式
- 首先我們要把
<script>
標(biāo)簽的lang
屬性改為ts,即<script lang="ts">
- 要在Vue項目中引入
vue-property-decorator
晦炊,后續(xù)很多操作都需要引用這個庫里面的屬性(包括Vue
,Component
等)稳衬。
shims-tsx.d.ts
薄疚、shims-vue.d.ts
的作用
如果用vue-cli 直接生成一個「Vue + TS」的項目街夭,我們會發(fā)現(xiàn)在 src 目錄下出現(xiàn)了這兩個文件,那么它們的作用是什么呢埃碱?
-
shims-vue.d.ts
shims-vue.d.ts 這個文件砚殿,主要用于 TypeScript 識別.vue 文件瓮具,Ts 默認并不支持導(dǎo)入 vue 文件,這個文件告訴 ts 導(dǎo)入.vue 文件都按VueConstructor<Vue>處理叹阔,因此導(dǎo)入 vue 文件必須寫.vue 后綴耳幢,但是這樣同樣的也會造成睛藻,就算你寫的導(dǎo)入的 .vue 文件的路徑就算是錯的店印,靜態(tài)檢測也不會檢測到錯誤按摘,如果你把鼠標(biāo)放上面你會看到錯誤的路徑就是指向這個文件,因為你定義了這個模塊是所有 .vue 后綴的導(dǎo)入都會指向到這個文件兰珍,但是如果你的路徑是對的,ts 能讀出正確的 module唠摹。
-
shims-tsx.d.ts
shims-tsx.d.ts 文件跃闹,這個文件主要是方便你使用在 ts 中使用 jsx 語法的毛好,如果不使用 jsx 語法,可以無視這個肌访,但是強烈建議使用 jsx 語法,畢竟模板是沒法獲得靜態(tài)類型提示的吼驶,當(dāng)然惩激,如果你境界高的話蟹演,直接用 vue render function风钻。
基于class的組件
- TypeScript 版本
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator' @Component export default class HelloWorld extends Vue { } </script>
- JavaScript 版本
<script> export default { name: 'HelloWorld' } </script>
引入組件 import component
- TypeScript 版本
<template> <div class="main"> <project /> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import Project from '@/components/Project.vue' @Component({ components: { project } }) export default class HelloWorld extends Vue { } </script>
- JavaScript 版本
<template> <div class="main"> <project /> </div> </template> <script> import Project from '@/components/Project.vue' export default { name: 'HelloWorld', components: { project } }) </script>
Data 數(shù)據(jù)
- TypeScript 版本
@Component export default class HelloWorld extends Vue { private msg: string = "welcome to my app" private list: Array<object> = [ { name: 'Preetish', age: '26' }, { name: 'John', age: '30' } ] }
- JavaScript 版本
export default { data() { return { msg: "welcome to my app", list: [ { name: 'Preetish', age: '26' }, { name: 'John', age: '30' } ] } }
Computed 計算屬性
- TypeScript 版本
export default class HelloWorld extends Vue { get fullName(): string { return this.first+ ' '+ this.last } set fullName(newValue: string) { let names = newValue.split(' ') this.first = names[0] this.last = names[names.length - 1] } }
- JavaScript 版本
computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } }
Methods 方法
在TS里面寫methods,就像寫class
中的方法一樣骡技,有一個可選的修飾符布朦。
- TypeScript 版本
export default class HelloWorld extends Vue { public clickMe(): void { console.log('clicked') console.log(this.addNum(4, 2)) } public addNum(num1: number, num2: number): number { return num1 + num2 } }
- JavaScript 版本
export default { methods: { clickMe() { console.log('clicked') console.log(this.addNum(4, 2)) } addNum(num1, num2) { return num1 + num2 } } }
生命周期鉤子
生命周期鉤子的寫法和上一條寫methods
是一樣的澄惊。Vue組件具有八個生命周期掛鉤唆途,包括created
,mounted
等亭敢,并且每個掛鉤使用相同的TypeScript語法滚婉。這些被聲明為普通類方法。由于生命周期掛鉤是自動調(diào)用的帅刀,因此它們既不帶參數(shù)也不返回任何數(shù)據(jù)让腹。因此,我們不需要訪問修飾符扣溺,鍵入?yún)?shù)或返回類型骇窍。
- TypeScript 版本
export default class HelloWorld extends Vue { mounted() { //do something } beforeUpdate() { // do something } }
- JavaScript 版本
export default { mounted() { //do something } beforeUpdate() { // do something } }
Props
我們可以在Vue的組件里面使用@Prop
裝飾器來替代 props
,在Vue中锥余,我們能給props提供額外的屬性腹纳,比如required
, default
, type
。如果用TypeScript,我們首先需要從vue-property-decorator
引入Prop
裝飾器嘲恍。我們甚至可以用TS提供的readonly
來避免在代碼中不小心修改了props
足画。
(備注:TypeScript中的賦值斷言。!:
表示一定存在佃牛, ?:
表示可能不存在淹辞。)
- TypeScript 版本
import { Component, Prop, Vue } from 'vue-property-decorator' @Component export default class HelloWorld extends Vue { @Prop() readonly msg!: string @Prop({default: 'John doe'}) readonly name: string @Prop({required: true}) readonly age: number @Prop(String) readonly address: string @Prop({required: false, type: String, default: 'Developer'}) readonly job: string }
- JavaScript 版本
export default { props: { msg, name: { default: 'John doe' }, age: { required: true, }, address: { type: String }, job: { required: false, type: string, default: 'Developer' } } }
Ref
在Vue中我們經(jīng)常會使用this.$refs.xxx
來調(diào)用某個組件中的方法,但是在使用TS的時候俘侠,有所不同:
<Loading ref="loading" />
export default class Article extends Mixins(LoadingMixin) {
$refs!: {
loading: Loading;
};
}
在$refs
里面聲明之后象缀,TS就可以識別到 ref 屬性了,調(diào)用方式和JS一樣:this.$refs.loading.showLoading();
Watch
要想用watch
偵聽器的話爷速,在TS中就要使用@Watch
裝飾器(同樣從vue-property-decorator
引入)央星。
- TypeScript 版本
我們還可以給@Watch('name') nameChanged(newVal: string) { this.name = newVal }
watch
添加immediate
和deep
屬性:@Watch('name') nameChanged(newVal: string) { this.name = newVal }
- JavaScript 版本
watch: { person: { handler: 'projectChanged', immediate: true, deep: true } } methods: { projectChanged(newVal, oldVal) { // do something } }
Emit
這里同樣要從vue-property-decorator
引入裝飾器@Emit
-
TypeScript 版本
@Emit() addToCount(n: number) { this.count += n } @Emit('resetData') resetCount() { this.count = 0 } @Emit('getCount') getCount(){ return this.count }
在上面這個例子中,
addToCount
方法回自動轉(zhuǎn)換成kebab-case命名惫东,即中劃線命名等曼,這和Vue的 emit 工作方式十分類似。
而resetCount
方法則不會自動轉(zhuǎn)換成中劃線命名凿蒜,因為我們給@Emit
傳入了一個參數(shù)resetCount
作為方法名。
getCount
這個方法可以向父組件傳遞參數(shù)州泊,就像在JS中寫成this.$emit("getCount", this.count)
一樣遥皂。 -
JavaScript 版本
<some-component add-to-count="someMethod" /> <some-component reset-data="someMethod" /> //Javascript Equivalent methods: { addToCount(n) { this.count += n this.$emit('add-to-count', n) }, resetCount() { this.count = 0 this.$emit('resetData') } }
Mixin
想要在Vue+TypeScript中使用mixin,首先我們先創(chuàng)建一個mixin文件:
import { Component, Vue } from 'vue-property-decorator'
@Component
class ProjectMixin extends Vue {
public projName: string = 'My project'
public setProjectName(newVal: string): void {
this.projName = newVal
}
}
export default ProjectMixin
想要使用上面代碼中的mixin,我們需要從vue-property-decorator
中引入 Mixins
以及 包含上述代碼的mixins 文件,具體寫法如下震糖,主要不同就是組件不繼承自Vue
,而是繼承自Mixins
:
<template>
<div class="project-detail">
{{ projectDetail }}
</div>
</template>
<script lang="ts">
import { Component, Vue, Mixins } from 'vue-property-decorator'
import ProjectMixin from '@/mixins/ProjectMixin'
@Component
export default class Project extends Mixins(ProjectMixin) {
get projectDetail(): string {
return this.projName + ' ' + 'Preetish HS'
}
}
</script>
Vuex
Vuex是大多數(shù)Vue.js應(yīng)用程序中使用的官方狀態(tài)管理庫。最好將store
分為 namespaced modules糊余,即帶命名空間的模塊。我們將演示如何在TypeScript中編寫Vuex。
- 首先贤惯,我們要安裝兩個流行的第三方庫:
npm install vuex-module-decorators -D npm install vuex-class -D
- 在
store
文件夾下烟很,創(chuàng)建一個module
文件夾用來放置不同的模塊文件。比如創(chuàng)建一個擁有用戶狀態(tài)的文件user.ts
:
在// store/modules/user.ts import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators' @Module({ namespaced: true, name: 'test' }) class User extends VuexModule { public name: string = '' @Mutation public setName(newName: string): void { this.name = newName } @Action public updateName(newName: string): void { this.context.commit('setName', newName) } } export default User
vuex-module-decorators
庫中提供了Module
,Mutation
和Action
裝飾器林说,對于Actions
,在Mutations
和context
中饵撑,我們不需要將狀態(tài)作為我們的第一個參數(shù)语卤,這個第三方庫庫會處理這些骂倘。這些方法已經(jīng)自動注入了荧库。 - 在store文件夾下,我們需要創(chuàng)建一個
index.ts
來初始化vuex
以及注冊這個module
:import Vue from 'vue' import Vuex from 'vuex' import User from '@/store/modules/user' Vue.use(Vuex) const store = new Vuex.Store({ modules: { User } }) export default store
- 在組件中使用 Vuex
要使用Vuex屎勘,我們可以利用第三方庫vuex-class
。該庫提供裝飾器使得在我們的Vue組件中綁定State
概漱,Getter
瓤摧,Mutation
和Action
。
由于我們正在使用命名空間的Vuex模塊这揣,因此我們首先從vuex-class
引入namespace
皆辽,然后傳遞模塊名稱以訪問該模塊。<template> <div class="details"> <div class="username">User: {{ nameUpperCase }}</div> <input :value="name" @keydown="updateName($event.target.value)" /> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import { namespace } from 'vuex-class' const user = namespace('user') @Component export default class User extends Vue { @user.State public name!: string @user.Getter public nameUpperCase!: string @user.Action public updateName!: (newName: string) => void } </script>
Axios 封裝
在Vue的項目中九秀,我們使用 axios 來發(fā)送 AJAX 請求,我在項目里寫了 axios 的統(tǒng)一攔截器征字,這里的攔截器寫法和 JS 沒有任何區(qū)別都弹,但是在使用該攔截器發(fā)送請求的方法會有一些不同之處,具體代碼可以參考項目中的api請求代碼 匙姜。下面我貼一段代碼簡單介紹一下:
export function getTopicLists(
params?: TopicListParams
): Promise<Array<TopicListEntity>> {
return request.get("topics", {
params
});
}
使用TypeScript畅厢,最重要的就是類型,所以在上述代碼中氮昧,傳進來的參數(shù)規(guī)定類型為TopicListParams
框杜,而函數(shù)返回的參數(shù)是Promise<Array<TopicListEntity>>
,這樣我們在調(diào)用getTopicLists
的時候袖肥,就可以寫成這樣:
// 使用await
const response = await getTopicLists(); // response 即返回的Array<TopicListEntity>
// 或使用promise.then
await getTopicLists({
limit: 40,
page
}).then(response => {
// response 即返回的Array<TopicListEntity>
})
});
另外:一般來說后端傳給前端的響應(yīng)體咪辱,我們應(yīng)該添加一個interface
類型來接收,就上面代碼中的TopicListEntity
椎组,如果后端傳過來的響應(yīng)數(shù)據(jù)很多油狂,手寫interface
就很麻煩,所以給大家推薦一個工具寸癌,可以根據(jù) json 自動生成 TypeScript 實體類型:json to ts专筷。
在Vue中寫TSX有哪些需要注意的地方
v-html
使用domPropsInnerHTML
來替代v-html
<main
domPropsInnerHTML={this.topicDetail.content}
class="markdown-body"
>
loading????
</main>
v-if
使用三元操作符來替代v-if
{this.preFlag ? <button class="pageBtn">......</button> : ""}
v-for
使用map
遍歷替代v-for
{this.pageBtnList.map(page => {
return (
<button
onClick={this.changePageHandler.bind(this, page)}
class={[{ currentPage: page === this.currentPage }, "pageBtn"]}
>
{page}
</button>
);
})}
render
注意:在render函數(shù)中的組件名一定用kebab-case命名
protected render() {
return (
<footer>
<hello-word />
<p>
© 2020 Designed By Enoch Qin
<a target="_blank">
源碼鏈接 GitHub >>
</a>
</p>
</footer>
);
}
onClick事件傳值(TSX)
使用template
的時候,如果用v-on
綁定事件蒸苇,想要傳參的話磷蛹,可以直接這么寫:
<button @click="clickHandle(params)">click me</button>
但是在TSX中,如果直接這么寫溪烤,就相當(dāng)于立即執(zhí)行了clickHandle函數(shù):
render(){
// 這樣寫是不行的O夷簟鸟辅!
return <button onClick={this.clickHandler(params)}>click me</button>
}
因此,我們不得不使用bind()
來綁定參數(shù)的形式傳參:
render(){
return <button onClick={this.clickHandler.bind(this, params)}>click me</button>
}
開發(fā)過程中遇到的問題及解決
Router history模式
原CNode社區(qū)的url是沒有#的history模式莺葫,但是這需要后端支持匪凉,所以本項目中使用了hash模式。
- Vue Router 默認模式是hash模式捺檬,頁面url長這樣: localhost:9090/#/payIn
如果改成history模式再层,url就變成了(沒有了#) localhost:9090/payIn - vue-router 默認 hash 模式 —— 使用 URL 的 hash 來模擬一個完整的 URL,于是當(dāng) URL 改變時堡纬,頁面不會重新加載聂受。
如果不想要很丑的 hash,我們可以用路由的 history 模式烤镐,這種模式充分利用 history.pushState API 來完成 URL 跳轉(zhuǎn)而無須重新加載頁面蛋济。const router = new VueRouter({ mode: 'history', routes: [...] })
- 當(dāng)你使用 history 模式時,URL 就像正常的 url炮叶,例如 http://yoursite.com/user/id碗旅,也好看!
不過這種模式要玩好镜悉,還需要后臺配置支持祟辟。因為我們的應(yīng)用是個單頁客戶端應(yīng)用,如果后臺沒有正確的配置侣肄,當(dāng)用戶在瀏覽器直接訪問 http://oursite.com/user/id 就會返回 404旧困,這就不好看了。
所以呢稼锅,你要在服務(wù)端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態(tài)資源吼具,則應(yīng)該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面矩距。
publicPath 部署應(yīng)用包時的基本URL
- 默認情況下【 / 】馍悟,Vue CLI 會假設(shè)你的應(yīng)用是被部署在一個域名的根路徑上,例如 https://www.my-app.com/剩晴。
- 如果應(yīng)用被部署在一個子路徑上锣咒,你就需要用這個選項指定這個子路徑。例如赞弥,如果你的應(yīng)用被部署在 https://www.my-app.com/my-app/毅整,則設(shè)置 publicPath 為 【/my-app/】。
- 這個值也可以被設(shè)置為空字符串【 (‘')】 或是相對路徑【 ('./‘)】绽左,這樣所有的資源都會被鏈接為相對路徑悼嫉,這樣打出來的包可以被部署在任意路徑,也可以用在類似 Cordova hybrid 應(yīng)用的文件系統(tǒng)中拼窥。
<base> 標(biāo)簽
在項目最開始開發(fā)的時候戏蔑,出現(xiàn)了子頁面無法刷新(刷新就會報錯:Uncaught SyntaxError: Unexpected token '<‘
)蹋凝,并且子頁面用到的圖片資源找不到的問題。通過stack overflow的這個問題的答案总棵,使用<base>
標(biāo)簽成功解決了這個問題鳍寂。
<base>
標(biāo)簽是用來指定一個HTML頁中所有的相對路徑的根路徑,在/public/index.html
中添加標(biāo)簽<base href="./" />
情龄,設(shè)置 href為相對路徑迄汛,在本地調(diào)試和打包上線的時候,資源就都不會出問題啦骤视。
Axios withCredentials
在本項目中鞍爱,后端調(diào)用的是 cnode 提供的后端接口,所有接口的都設(shè)置了Access-Control-Allow-Origin: *
专酗,用來放置跨域睹逃。但是如果我們將axios 的 withCredentials(表示跨域請求時是否需要使用憑證)設(shè)置成true,會包CORS跨域錯誤:
原因是:Access-Control-Allow-Origin
不可以為 *
祷肯,因為 *
會和 Access-Control-Allow-Credentials:true
產(chǎn)生沖突沉填,需配置指定的地址。
因此在項目中躬柬,withCredentials
設(shè)置成false
即可拜轨。
Github-markdown-css
在項目中使用到了github-markdown-css
這個庫用于展示markdown的樣式抽减。用法如下:
- 在
main.ts
引入import "github-markdown-css"
- 在
App.vue
中添加如下樣式:.markdown-body { box-sizing: border-box; min-width: 200px; max-width: 1400px; margin: 0 auto; padding: 45px; } @media (max-width: 767px) { .markdown-body { padding: 15px; } }
- 在包含markdown內(nèi)容的父標(biāo)簽添加class:
markdown-body
總結(jié)
Now you have all the basic information you need to create a Vue.js application completely in TypeScript using a few official and third-party libraries to fully leverage the typing and custom decorator features. Vue 3.0 will have better support for TypeScript out of the box, and the whole Vue.js code was rewritten in TypeScript to improve maintainability.
Using TypeScript might seem a bit overwhelming at first, but when you get used to it, you’ll have far fewer bugs in your code and smooth code collaboration between other developers who work on the same code base. (摘自How to write a Vue.js app completely in TypeScript)
翻譯:現(xiàn)在允青,您知道了在創(chuàng)建Vue.js + TypeScript應(yīng)用程序的過程中,如何使用幾個官方庫和第三方庫所需的所有基本信息卵沉,以充分利用類型和自定義裝飾器颠锉。已經(jīng)發(fā)布了公測版本的Vue 3.0開箱即用將更好地支持TypeScript,并且整個Vue.js的項目代碼都使用TypeScript進行了重寫史汗,以提高可維護性琼掠。
剛開始使用TypeScript似乎有點讓人不知所措,但是當(dāng)您習(xí)慣了它之后停撞,您的代碼中的錯誤將大大減少瓷蛙,并且,在同一個項目中可以和其他開發(fā)者更好的協(xié)同工作戈毒。
本文參考資料:
??https://blog.logrocket.com/how-to-write-a-vue-js-app-completely-in-typescript/
??https://zhuanlan.zhihu.com/p/99343202
??TypeScript 支持 — Vue.js
??TypeScript 官網(wǎng)
??https://segmentfault.com/a/1190000016837020
(完)