最近嘗試上手 Vue3+TS+Vite,對比起 Vue2 有些不適應(yīng),但還是真香~
上手前先說下 Vue3 的一些變化吧~
Vue3 的變化
Vue3 帶來的變化主要有以下幾個方面:
-
使用層面
- 對比起 Vue2 啟動速度快很多,新項(xiàng)目從 1s 升級到不到 500ms
-
vite.config.ts配置文件修改后無需重啟服務(wù)就能更新
-
代碼層面
- 函數(shù)式編程,方便組合邏輯,如mixin容易命名沖突,數(shù)據(jù)來源不清晰
- 新增
ref,reativeAPI定義變量 - 更好的
ts支持 - 組件文件中
template模板內(nèi)無需用根節(jié)點(diǎn)標(biāo)簽包著組件元素
-
底層設(shè)計(jì)
- 雙向數(shù)據(jù)綁定從
definePropertyfor in 循環(huán)變量改成proxy。defineProperty是改變原對象屬性標(biāo)簽;而proxy未改變原對象,而是產(chǎn)生新的代理對象,js 引擎更喜歡穩(wěn)定的對象 - 重新定義
vdom對比思路:- 區(qū)分動靜態(tài) dom,只對比動態(tài)數(shù)據(jù) dom,用block 標(biāo)記動態(tài)標(biāo)簽內(nèi)部的靜態(tài)標(biāo)簽
- 使用最長遞增子序列算法,找到所有不需要移動的元素
- compile 編譯優(yōu)化,把大量計(jì)算放在 node 層,最后瀏覽器只需執(zhí)行最少的代碼
- 雙向數(shù)據(jù)綁定從
底層設(shè)計(jì)層面的改變決定了 vue3 比 vue2 更快
下面介紹上手步驟~ (官網(wǎng)鏈接)
創(chuàng)建項(xiàng)目
使用 vite 命令創(chuàng)建初始項(xiàng)目
# npm 6.x
npm create vite@latest my-vue-app --template vue
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
Vite 配置
功能一致的配置大多跟 vue-cli 配置大同小異,不過多贅述
resolve
resolve.alias:當(dāng)使用文件系統(tǒng)路徑的別名時,請始終使用絕對路徑。相對路徑的別名值會原封不動地被使用,因此無法被正常解析。
/* vite.config.ts */
resolve: {
//文件系統(tǒng)路徑的別名, 絕對路徑
alias: {
"@": path.resolve(__dirname, "src"),
}
}
sass配置
安裝sass依賴和配置 vite.config.ts 預(yù)定義全局變量
npm i sass -D
/* vite.config.ts */
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "./src/assets/scss/var.scss";'
}
}
}
開啟服務(wù)配置
開啟 http 服務(wù)
/* vite.config.ts */
server:{
host: 'dev.moon.cn',
port: 3000
}
開啟 https 服務(wù)
/* vite.config.ts */
let httpsConfig = {
key: fs.readFileSync("C:/Users/ca/wps.cn/_wildcard.wps.cn+3-key.pem"),
cert: fs.readFileSync("C:/Users/ca/wps.cn/_wildcard.wps.cn+3.pem")
};
server:{
https: httpsConfig,
host: 'dev.moon.cn',
port: 443,
open: true
}
預(yù)構(gòu)建依賴優(yōu)化
默認(rèn)情況下,Vite 會抓取你的 index.html 來檢測需要預(yù)構(gòu)建的依賴項(xiàng)。如果指定了 build.rollupOptions.input,Vite 將轉(zhuǎn)而去抓取這些入口點(diǎn)。
optimizeDeps.include
默認(rèn)情況下,不在 node_modules 中的,鏈接的包不會被預(yù)構(gòu)建。使用此選項(xiàng)可強(qiáng)制預(yù)構(gòu)建鏈接的包。
/* vite.config.ts */
optimizeDeps: {
include: ['axios'],
},
optimizeDeps.exclude
在預(yù)構(gòu)建中強(qiáng)制排除的依賴項(xiàng)。
eslint 配置
vue3 和 ts 的 eslint 配置需另外自行配置,除了需配置 eslint 規(guī)則外還需調(diào)整 vite 的相關(guān)配置,感興趣的話可以看看我另一篇文章(內(nèi)附配置解析),或者直接看完整源碼,這里不做贅述。
TypeScript
TypeScript 是添加了類型系統(tǒng)的 JavaScript,適用于任何規(guī)模的項(xiàng)目,在編譯階段進(jìn)行類型檢查。
基礎(chǔ)知識可直接看中文文檔,英文比較好的小伙伴可以直接看官方文檔,這里不做贅述,這里分享一些值得說的地方
類型/接口/泛型
類型:類型(type)不是定義一個新類型,而是一個類型別名,使類型更具體化
接口:接口(interface)則是描述一個對象的形狀,對值所具有的結(jié)構(gòu)進(jìn)行類型檢查。接口的作用類似于抽象類,不同點(diǎn)在于接口中的所有方法和屬性都是沒有實(shí)值的,換句話說接口中的所有方法都是抽象方法。接口主要負(fù)責(zé)定義一個類的結(jié)構(gòu),接口可以去限制一個對象的接口,對象只有包含接口中定義的所有屬性和方法時才能匹配接口。同時,可以讓一個類去實(shí)現(xiàn)接口,實(shí)現(xiàn)接口時類中要保護(hù)接口中的所有屬性。
泛型:支持多種數(shù)據(jù)結(jié)構(gòu),有函數(shù)泛型,類泛型,接口泛型等。
你可能想問什么時候用類型,什么時候用接口?Typescript團(tuán)隊(duì)的建議是
可以使用接口就盡量使用接口,因?yàn)榻涌诟`活,更容易處理
很多時候 interface 和 type 是相同的,但有一個明顯區(qū)別在于 interface 可以重復(fù)定義,類型注解會累加,而 type 重復(fù)定義會報(bào)錯
類型聲明
類型聲明(Type Declaration) 或者類型定義(Type Definition) 文件是一個以.d.ts作為文件后綴名的TypeScript文件。文件中只包含與類型相關(guān)的代碼,不包含邏輯代碼,它們的作用旨在為開發(fā)者提供類型信息,所以它們只在開發(fā)階段起作用。
typescript編譯后會將類型信息移除,類型信息僅在開發(fā)階段起作用。
全局類型/變量聲明
先在項(xiàng)目 src 目錄中新建 global.d.ts 文件
全局類型聲明
項(xiàng)目的根目錄有 tsconfig.json 可以配置 TypeScript,include 屬性包含了需要校驗(yàn) ts 的文件。ts 默認(rèn)會將 xx.d.ts 類型文件中的類型注冊成全局的,下面舉個栗子:
// global.d.ts
type T1 = number
// 組件內(nèi)
<script lang="ts">
let num1: T1 = 1
</script>
全局變量聲明
有三種方式聲明全局變量,掛載在瀏覽器的 window 對象中
- 使用 window
global.d.ts文件
// 若想不帶window使用userId,但需重復(fù)聲明
declare let userId: string
interface Window {
userId: string
}
注:不聲明且不帶window使用開發(fā)模式不會報(bào)錯,但打包時會報(bào)錯
組件文件
window.userId = '1'
console.log(userId)
- 使用
global配合window或var,需加export,否則會打包報(bào)錯
// global.d.ts
export {}
declare global {
interface Window {
// 使用foo全局變量時得帶window,否則打包會報(bào)錯
foo: string
}
var age: number
}
// 組件內(nèi)
globalThis.age = 18
window.foo = '1'
console.log(age, window.foo)
注:加上export后其他聲明會失效,其他聲明可另起 *.d.ts 文件定義
- 使用var
// global.d.ts
declare var age: number
// 組件內(nèi)
globalThis.age = 18
console.log(age)
每種方式各有利弊,自行選擇
第三方庫聲明
第三方庫需有類型聲明,可自動生成或者自己寫,有興趣可移步。
Vue3 + TS
vue3新增了composition api的寫法,更接近react的寫法,下面介紹ts下的vue3寫法和生命周期
setup 語法糖
一個組件選項(xiàng),在組件被創(chuàng)建之前,props 被解析之后執(zhí)行。它是組合式 API 的入口。
<script setup> 是在單文件組件 (SFC) 中使用組合式 API 的編譯時語法糖。相比于普通的 <script> 語法,它具有更多優(yōu)勢:
- 更少的樣板內(nèi)容,更簡潔的代碼。
- 能夠使用純 Typescript 聲明 props 和拋出事件。
- 更好的運(yùn)行時性能 (其模板會被編譯成與其同一作用域的渲染函數(shù),沒有任何的中間代理)。
- 更好的 IDE 類型推斷性能 (減少語言服務(wù)器從代碼中抽離類型的工作)。
使用這個語法,需要將 setup attribute 添加到 <script> 代碼塊上:
<script setup lang="ts">
</script>
里面的代碼會被編譯成組件 setup() 函數(shù)的內(nèi)容。這意味著與普通的 <script> 只在組件被首次引入的時候執(zhí)行一次不同,<script setup> 中的代碼會在每次組件實(shí)例被創(chuàng)建的時候執(zhí)行。
setup 函數(shù)在生命周期方面,它是在 beforeCreate 鉤子之前調(diào)用的。
生命周期
選項(xiàng)式 API 的生命周期選項(xiàng)和組合式 API 之間的映射
-
-> 使用beforeCreatesetup() -
-> 使用createdsetup() -
beforeMount->onBeforeMount -
mounted->onMounted -
beforeUpdate->onBeforeUpdate -
updated->onUpdated -
beforeUnmount->onBeforeUnmount -
unmounted->onUnmounted -
errorCaptured->onErrorCaptured -
renderTracked->onRenderTracked -
renderTriggered->onRenderTriggered -
activated->onActivated -
deactivated->onDeactivated
TIP: 因?yàn)?
setup是圍繞beforeCreate和created生命周期鉤子運(yùn)行的,所以不需要顯式地定義它們。換句話說,在這些鉤子中編寫的任何代碼都應(yīng)該直接在setup函數(shù)中編寫。
響應(yīng)式 ref
接受一個內(nèi)部值并返回一個響應(yīng)式且可變的 ref 對象。ref 對象僅有一個 .value property,指向該內(nèi)部值。和從 setup() 函數(shù)中返回值一樣,ref 值在模板中使用的時候會自動解包。
可以在調(diào)用 ref 時傳遞一個泛型參數(shù)以覆蓋默認(rèn)推斷
import { ref } from "vue";
let str = ref<string>("test");
還可以指定復(fù)雜類型
const foo = ref<string | number>('foo') // foo 的類型:Ref<string | number>
foo.value = 123 // ok!
props/emit
- 僅限類型的 props/emit 聲明
defineProps<{ title: string }>();
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
-
props 設(shè)置默認(rèn)值
有兩種方法設(shè)置默認(rèn)值
-
使用
運(yùn)行時聲明運(yùn)行時聲明 的方式只能設(shè)置參數(shù)類型、默認(rèn)值、是否必傳、自定義驗(yàn)證。報(bào)錯為控制臺warn警告。
若想設(shè)置[ 編輯器報(bào)錯、編輯器語法提示 ]則需要使用類型聲明的方式。const props = defineProps({ modelValue: { type: Boolean, default: false }, title: { type: String, default: '彈窗提示' }, msg: { type: String, default: '彈窗信息' } }) -
使用類型聲明時的默認(rèn) props 值
僅限類型的
defineProps聲明的不足之處在于,它不能給 props 定義默認(rèn)值。需配合withDefaults編譯器宏解決:
-
interface Props {
title?: string;
msg?: string;
}
withDefaults(defineProps<Props>(), {
title: "提示",
msg: "是否跳轉(zhuǎn)到app?",
});
defineProps、withDefaults 是只在
<script setup>語法糖中才能使用的編譯器宏。他不需要導(dǎo)入且會隨著<script setup>處理過程一同被編譯掉。
v-model 雙向綁定
vue2 中的 v-model 的使用是通過傳遞 value 屬性和接收 input 事件實(shí)現(xiàn),vue3 則換成了 modelValue 屬性,接收的方法是update:modelValue。
以下彈窗例子以Page.vue為父組件,Dialog.vue為子組件,關(guān)鍵代碼如下:
/* Page.vue */
<template>
<Dialog v-model="dialogVisible"></Dialog>
<div class="bottom-btn" @click="onTap">點(diǎn)擊按鈕</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Dialog from "./Dialog.vue";
let dialogVisible = ref<boolean>(false);
function onTap() {
dialogVisible.value = true;
}
<script>
/* Dialog.vue */
<template>
<div class="dialog" v-show="modelValue">
<span class="dialog-content-btn" @click="onConfirm">確定</span>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
interface Props {
modelValue?: boolean;
}
let props = withDefaults(defineProps<Props>(), {
modelValue: false // v-model綁定的屬性值
});
// 傳遞的方法
const emit = defineEmits<{
(e: "update:modelValue", visible: boolean): boolean;
}>();
function onConfirm() {
emit("update:modelValue", false);
}
<script>
遇到的問題
做好所有配置后,主要遇到以下兩個問題
vite 打包報(bào)錯/告警
"@charset" must be the first rule in the file }@charset "UTF-8";
告警如圖:

原因:使用了scss類庫 sass,scss編譯的時候,因?yàn)楸痪幾g的文件里可能有中文導(dǎo)致
解決:在vite.config.js里面,加一個sass的配置,把charset關(guān)掉就行了
官網(wǎng)對css預(yù)處理的api
vite.config.js 中的配置
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
charset: false
}
}
}
})
去除 Typescript 全局變量的 eslint 報(bào)錯
1. 使用 var 定義全局變量
var 相關(guān)聲明下會帶下劃線,并報(bào)錯
Unexpected var, use let or const instead.
解決:在 .eslintrc 配置文件中增加規(guī)則
rules: {
// 全局變量允許使用 var
'no-var': 'off',
}
2. 使用 global 定義全局變量
global 相關(guān)聲明下會帶下劃線,并報(bào)錯
Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.
解決:在 global.d.ts 聲明文件中添加一行代碼
export {}
注:新增后會導(dǎo)致該文件中的其他變量/類型等聲明失效,其他聲明可另起 *.d.ts 文件定義
Vite 為什么更快
Vite 主要通過以下幾個方面進(jìn)行優(yōu)化:
- 啟動應(yīng)用時按需提供代碼
- 瀏覽器緩存(協(xié)商緩存和強(qiáng)緩存)進(jìn)行代碼更新
- 使用 esbuild 預(yù)構(gòu)建依賴和加快構(gòu)建速度
啟動時間和更新時間
啟動時間
以往的打包工具當(dāng)冷啟動開發(fā)服務(wù)器時,基于打包器的方式啟動必須優(yōu)先抓取并構(gòu)建你的整個應(yīng)用,然后才能提供服務(wù)。而且存在性能瓶頸——使用 JavaScript 開發(fā)的工具通常需要很長時間(甚至是幾分鐘!)才能啟動開發(fā)服務(wù)器,即使使用 HMR,文件修改后的效果也需要幾秒鐘才能在瀏覽器中反映出來。
Vite 通過在一開始將應(yīng)用中的模塊區(qū)分為 依賴 和 源碼 兩類,并只在瀏覽器請求源碼時進(jìn)行轉(zhuǎn)換并按需提供源碼,改進(jìn)了開發(fā)服務(wù)器啟動時間。而且esbuild 預(yù)構(gòu)建依賴使用的語言是go,比以 JavaScript 編寫的打包器預(yù)構(gòu)建依賴快 10-100 倍。


更新時間
在 Vite 中,HMR 是在原生 ESM 上執(zhí)行的。當(dāng)編輯一個文件時,Vite 只需要精確地使已編輯的模塊與其最近的 HMR 邊界之間的鏈?zhǔn)Щ?a target="_blank">[1](大多數(shù)時候只是模塊本身),使得無論應(yīng)用大小如何,HMR 始終能保持快速更新。
Vite 同時利用 HTTP 頭來加速整個頁面的重新加載(再次讓瀏覽器為我們做更多事情):源碼模塊的請求會根據(jù) 304 Not Modified 進(jìn)行協(xié)商緩存,而依賴模塊請求則會通過 Cache-Control: max-age=31536000,immutable 進(jìn)行強(qiáng)緩存,因此一旦被緩存它們將不需要再次請求。
預(yù)構(gòu)建依賴的前因后果
Vite 預(yù)構(gòu)建依賴原因有二:
CommonJS 和 UMD 兼容性: 開發(fā)階段中,Vite 的開發(fā)服務(wù)器將所有代碼視為原生 ES 模塊。因此,Vite 必須先將作為 CommonJS 或 UMD 發(fā)布的依賴項(xiàng)轉(zhuǎn)換為 ESM。
-
性能: Vite 通過預(yù)構(gòu)建依賴將有許多內(nèi)部模塊的 ESM 依賴關(guān)系轉(zhuǎn)換為單個模塊,從而減少瀏覽器的請求數(shù)量,提升頁面加載性能。
如
lodash-es有超過 600 個內(nèi)置模塊,當(dāng)執(zhí)行import { debounce } from 'lodash-es'時,瀏覽器同時發(fā)出 600 多個 HTTP 請求;通過預(yù)構(gòu)建lodash-es成為一個模塊,就只需要一個 HTTP 請求。
自動依賴搜尋
如果沒有找到相應(yīng)的緩存,Vite 將抓取你的源碼,并自動尋找引入的依賴項(xiàng)(即 "bare import",表示期望從 node_modules 解析),并將這些依賴項(xiàng)作為預(yù)構(gòu)建包的入口點(diǎn)。
在服務(wù)器已經(jīng)啟動之后,如果遇到一個新的依賴關(guān)系導(dǎo)入,而這個依賴關(guān)系還沒有在緩存中,Vite 將重新運(yùn)行依賴構(gòu)建進(jìn)程并重新加載頁面。
對于 monorepo 倉庫中的某個依賴成為另一個包的依賴,Vite 會自動偵測沒有從 node_modules 解析的依賴項(xiàng),并將鏈接的依賴視為源碼。它不會嘗試打包被鏈接的依賴,而是會分析被鏈接依賴的依賴列表。
緩存
文件系統(tǒng)緩存
Vite 會將預(yù)構(gòu)建的依賴緩存到 node_modules/.vite。它根據(jù)幾個源來決定是否需要重新運(yùn)行預(yù)構(gòu)建步驟:
-
package.json中的dependencies列表 - 包管理器的 lockfile,例如
package-lock.json,yarn.lock,或者pnpm-lock.yaml - 可能在
vite.config.js相關(guān)字段中配置過的
只有在上述其中一項(xiàng)發(fā)生更改時,才需要重新運(yùn)行預(yù)構(gòu)建。
如果要強(qiáng)制 Vite 重新構(gòu)建依賴,你可以用 --force 命令行選項(xiàng)啟動開發(fā)服務(wù)器,或者手動刪除 node_modules/.vite 目錄。
瀏覽器緩存
解析后的依賴請求會以 HTTP 頭 max-age=31536000,immutable 強(qiáng)緩存,以提高在開發(fā)時的頁面重載性能。一旦被緩存,這些請求將永遠(yuǎn)不會再到達(dá)開發(fā)服務(wù)器。如果安裝了不同的版本(這反映在包管理器的 lockfile 中),則附加的版本 query 會自動使它們失效。如果你想通過本地編輯來調(diào)試依賴項(xiàng),你可以:
- 通過瀏覽器調(diào)試工具的 Network 選項(xiàng)卡暫時禁用緩存;
- 重啟 Vite dev server,并添加
--force命令以重新構(gòu)建依賴; - 重新載入頁面。
為何不用 ESBuild 打包?
雖然 esbuild 快得驚人,且是一個在構(gòu)建庫方面比較出色的工具,但一些針對構(gòu)建 應(yīng)用 的重要功能仍然還在持續(xù)開發(fā)中 —— 特別是代碼分割和 CSS 處理方面。就目前來說,Rollup 在應(yīng)用打包方面更加成熟和靈活。
最后
最后附上完整代碼,如對前端自動化部署有興趣,可繼續(xù)看在本文 vue3 基礎(chǔ)上搭建的 CICD
相關(guān)文章