Vue3+TS+Vite 入門指南

最近嘗試上手 Vue3+TS+Vite,對比起 Vue2 有些不適應(yīng),但還是真香~

上手前先說下 Vue3 的一些變化吧~

Vue3 的變化

Vue3 帶來的變化主要有以下幾個方面:

  • 使用層面

    • 對比起 Vue2 啟動速度快很多,新項(xiàng)目從 1s 升級到不到 500ms
    • vite.config.ts 配置文件修改后無需重啟服務(wù)就能更新
  • 代碼層面

    • 函數(shù)式編程,方便組合邏輯,如mixin容易命名沖突,數(shù)據(jù)來源不清晰
    • 新增 ref,reative API定義變量
    • 更好的 ts 支持
    • 組件文件中 template 模板內(nèi)無需用根節(jié)點(diǎn)標(biāo)簽包著組件元素
  • 底層設(shè)計(jì)

    • 雙向數(shù)據(jù)綁定從 defineProperty for in 循環(huán)變量改成 proxydefineProperty 是改變原對象屬性標(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ì)層面的改變決定了 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 配置

vue3tseslint 配置需另外自行配置,除了需配置 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 配合 windowvar,需加 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 之間的映射

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • 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 是圍繞 beforeCreatecreated 生命周期鉤子運(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),你可以:

  1. 通過瀏覽器調(diào)試工具的 Network 選項(xiàng)卡暫時禁用緩存;
  2. 重啟 Vite dev server,并添加 --force 命令以重新構(gòu)建依賴;
  3. 重新載入頁面。

為何不用 ESBuild 打包?

雖然 esbuild 快得驚人,且是一個在構(gòu)建庫方面比較出色的工具,但一些針對構(gòu)建 應(yīng)用 的重要功能仍然還在持續(xù)開發(fā)中 —— 特別是代碼分割和 CSS 處理方面。就目前來說,Rollup 在應(yīng)用打包方面更加成熟和靈活。

最后

最后附上完整代碼,如對前端自動化部署有興趣,可繼續(xù)看在本文 vue3 基礎(chǔ)上搭建的 CICD

相關(guān)文章

Monorepo實(shí)戰(zhàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容