Vue 3 元件開發:搭建基於SpreadJS的表格編輯系統(環境搭建)

2021-04-12 12:01:46

image.png

Vue是一套用於構建使用者介面的漸進式框架,與其它大型 JS 框架不同,Vue 被設計為可以自底向上逐層應用,更易上手,還便於與第三方庫或既有專案整合,因此,Vue完全能夠為複雜的單頁應用提供驅動。

2020年09月18日,Vue.js 3.0 正式釋出,作者尤雨溪將其描述為:更快、更小、更易於維護。

Vue 3都加入了哪些新功能?

本次釋出, Vue框架本身迎來了多項更新,如Vue 此前的反應系統是使用 Object.defineProperty 的 getter 和 setter。 但是,在 Vue 3中,將使用 ES2015 Proxy 作為其觀察者機制,這樣做的好處是消除了以前存在的警告,使速度加倍,並節省了一半的記憶體開銷。

除了基於 Proxy 的觀察者機制,Vue 3的其他新特性還包括:

1. Performance(效能提升)

在Vue 2中,當某個DOM需要更新時,需要遍歷整個虛擬DOM樹才能判斷更新點。而在Vue 3中,無需此項操作,僅需通過靜態標記,對比虛擬節點上帶有patch flag的節點,即可定位更新位置。

對比Vue 2和Vue 3的效能差異,官方檔案中給出了具體資料說明:

· SSR速度提高了2~3倍

· Update效能提高1.3~2倍

2. Composition API(組合API)

Vue 2中有data、methods、mounted等儲存資料和方法的物件,我們對此應該不陌生了。比如說要實現一個輪播圖的功能,首先需要在data裡定義與此功能相關的資料,在methods裡定義該功能的方法,在mounted裡定義進入頁面自動開啟輪播的程式碼…… 有一個顯而易見的問題,就是同一個功能的程式碼卻要分散在頁面的不同地方,維護起來會相當麻煩。

為了解決上述問題,Vue 3推出了具備清晰的程式碼結構,並可消除重複邏輯的 Composition API,以及兩個全新的函數setup和ref。

Setup 函數可將屬性和方法返回到模板,在元件初始化的時候執行,其效果類似於Vue 2中的beforeCreate 和 created。如果想使用setup裡的資料,需要將值return出來,沒有從setup函數返回的內容在模板中不可用。

Ref函數的作用是建立一個參照值,主要是對String、Number、Boolean的資料響應做參照。

相對於Vue 2,Vue 3的生命週期函數也發生了變更,如下所示:

· beforeCreate -> 請使用 setup()

· created -> 請使用 setup()

· beforeMount -> onBeforeMount

· mounted -> onMounted

· beforeUpdate -> onBeforeUpdate

· updated -> onUpdated

· beforeDestroy -> onBeforeUnmount

· destroyed -> onUnmounted

· errorCaptured -> onErrorCaptured

需要注意的是,Vue 2使用生命週期函數時是直接在頁面中寫入生命週期函數,而在Vue 3則直接參照即可:

import {reactive, ref, onMounted} from 'vue'

3. Tree shaking support(按需打包模組)

有人將「Tree shaking」  稱之為「搖樹優化」,其實就是把無用的模組進行「剪枝」,剪去沒有用到的API,因此「Tree shaking」之後,打包的體積將大幅度減少。

官方將Vue 2和Vue 3進行了對比,Vue 2若只寫了Hello World,且沒有用到任何的模組API,打包後的大小約為32kb,而Vue 3 打包後僅有13.5kb。         

4. 全新的腳手架工具:Vite

Vite 是一個由原生 ESM 驅動的 Web 開發構建工具。在開發環境下基於瀏覽器原生 ES imports 開發,在生產環境下基於 Rollup 打包。

和 Webpack相比,具有以下特點:

· 快速的冷啟動,不需要等待打包

· 即時的熱模組更新

· 真正的按需編譯,不用等待整個專案編譯完成

由於完全跳過了打包這個概念,Vite的出現大大的撼動了Webpack的地位,且真正做到了伺服器隨起隨用。看來,連尤大神都難逃「真香」理論。

image.png

Vite究竟有什麼魔力?不妨讓我們通過實際搭建一款基於Vue 3 元件的表格編輯系統,親自體驗一把。

一、環境搭建

使用 Vite 初始化一個 Vue 3 專案

1. 執行程式碼:


$ npm init vite-app <project-name>

$ cd <project-name> //進入專案目錄

$ npm install //安裝專案所需依賴

$ npm run dev //啟動專案

我們來看下生成的程式碼, 因為 vite 會盡可能多地映象 vue-cli 中的預設設定, 所以,這段程式碼看上去和 vue-cli 生成的程式碼沒有太大區別。

├── index.html

├── package.json

├── public

│ └── favicon.ico

└── src

 ├── App.vue

 ├── assets

 │ └── logo.png

 ├── components

 │ └── HelloWorld.vue

 ├── index.css

 └── main.js

2. 執行下列命令:

image.png

此時如果不通過 npm run dev 來啟動專案,而是直接通過瀏覽器開啟 index.html, 會看到下面的報錯:

image.png

報錯的原因:瀏覽器的 ES module 是通過 http 請求拿到模組的,所以 vite 的一個任務就是啟動一個 web server 去代理這些模組,在 vite 裡是借用了 koa 來啟動的服務。

export function createServer(config: ServerConfig): Server {
  // ...
  const app = new Koa<State, Context>()
  const server = resolveServer(config, app.callback())
  
  // ...
  const listen = server.listen.bind(server)
  server.listen = (async (...args: any[]) => {
    if (optimizeDeps.auto !== false) {
      await require('../optimizer').optimizeDeps(config)
    }
    return listen(...args)
  }) as any
  
  return server
}

由於瀏覽器中的 ESM 是獲取不到匯入的模組內容的,需要藉助Webpack 等工具,如果我們沒有參照相對路徑的模組,而是參照 node_modules,並直接 import xxx from 'xxx',瀏覽器便無法得知你專案裡有 node_modules,只能通過相對路徑或者絕對路徑去尋找模組。

這便是vite 的實現核心:攔截瀏覽器對模組的請求並返回處理後的結果(關於vite 的實現機制,文末會深入講解)。

3. 生成專案結構:

image.png

入口 index.html 和 main.js 程式碼結構為:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

// main.js
// 只是參照的是最新的 vue3 語法,其餘相同
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

4. 進入專案目錄:cd myVue3 

5. 安裝相關模組:npm install

image.png

6. 下載模組:

image.png

7. 啟動專案:npm run dev

image.png

8. 進入地址,當我們看到這個頁面時,說明專案已經成功啟動了。

image.png

Vite 的實現機制

1. /@module/ 字首

對比工程下的 main.js 和開發環境下實際載入的 main.js,可以發現程式碼發生了變化。

工程下的 main.js:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

實際載入的 main.js:

import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app')

為了解決 import xxx from 'xxx' 報錯的問題,vite 對這種資源路徑做了統一處理,即新增一個/@module/字首。

在 src/node/server/serverPluginModuleRewrite.ts 原始碼的 koa 中介軟體裡可以看到 vite 對 import 做了一層處理,其過程如下:

· 在 koa 中介軟體裡獲取請求 body

· 通過 es-module-lexer 解析資源 ast 拿到 import 的內容

· 判斷 import 的資源是否是絕對路徑,絕對視為 npm 模組

· 返回處理後的資源路徑:"vue" => "/@modules/vue"

2. 支援 /@module/

在 /src/node/server/serverPluginModuleResolve.ts 裡可以看到大概的處理邏輯:

· 在 koa 中介軟體裡獲取請求 body

· 判斷路徑是否以 /@module/ 開頭,如果是取出包名

· 去node_module裡找到這個庫,基於 package.json 返回對應的內容

3. 檔案編譯

通過前文,我們知道了 js module 的處理過程,對於vue、css、ts等檔案,其又是如何處理的呢?

以 vue 檔案為例,在 webpack 裡使用 vue-loader 對單檔案元件進行編譯,在這裡 vite 同樣攔截了對模組的請求並執行了一個實時編譯。

通過工程下的 App.vue 和實際載入的 App.vue,便發現改變。

工程下的 App.vue:

<template>
  ![](./assets/logo.png)
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script>
import HelloWorld from './components/HelloWorld.vue';

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
};
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

實際載入的 App.vue:

import HelloWorld from '/src/components/HelloWorld.vue';

const __script = {
    name: 'App',
    components: {
        HelloWorld,
    },
};

import "/src/App.vue?type=style&index=0&t=1592811240845"
import {render as __render} from "/src/App.vue?type=template&t=1592811240845"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue"
export default __script

可見,一個 .vue 檔案被拆成了三個請求(分別對應 script、style 和template) ,瀏覽器會先收到包含 script 邏輯的 App.vue 的響應,然後解析到 template 和 style 的路徑後,再次發起 HTTP 請求來請求對應的資源,此時 Vite 對其攔截並再次處理後返回相應的內容。

// App.vue?type=style
import { updateStyle } from "/vite/hmr"
const css = "\n#app {\n  font-family: Avenir, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  text-align: center;\n  color: #2c3e50;\n  margin-top: 60px;\n}\n"
updateStyle("7ac74a55-0", css)
export default css

// App.vue?type=template
import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = /*#__PURE__*/
_createVNode("img", {
    alt: "Vue logo",
    src: "/src/assets/logo.png"
}, null, -1 /* HOISTED */
)

export function render(_ctx, _cache) {
    const _component_HelloWorld = _resolveComponent("HelloWorld")

    return (_openBlock(),
    _createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, {
        msg: "Hello Vue 3.0 + Vite"
    })], 64 /* STABLE_FRAGMENT */
    ))
}

vite對於其他的型別檔案的處理幾乎都是類似的邏輯,即根據請求的不同檔案型別,做出不同的編譯處理結果。

擴充套件閱讀

· Vue 3 元件開發實戰:搭建基於SpreadJS的表格編輯系統(元件整合篇)

· Vue 3 元件開發實戰:搭建基於SpreadJS的表格編輯系統(功能拓展篇)

· SpreadJS Vue 框架支援