探索「將 Vue SFC 編譯爲 ESM 」之路

最近想解決個場景,在給 ve-charts 編寫文檔的時候,想作一個代碼示例演示功能,在改動代碼後能夠直觀的看到組件的變化。以前版本中文檔是用的 docsify ,docsify 中自帶了一個 vuep。vuep 就是解決我須要的場景的。不過 vuep 版本比較老了。目前還不支持 vue3 組件。因此想獨立開發一個運行代碼示例的組件。css

ES Modules 規範

ES modules(ESM) 是 JavaScript 官方的標準化模塊系統

演進

在 ES6 以前,社區內已經有咱們熟悉的模塊加載方案 CommonJSAMD,前者用於服務器 即 Node.js,然後者藉助第三方庫實現瀏覽器加載模塊。html

在前端工程裏,應用範圍比較廣的仍是 CommonJS,從三個方面咱們能夠看出:前端

  • 咱們依賴的發佈在 NPM 上的第三方模塊,大部分都打包默認支持 CommonJS
  • 經過 Webpack 構建的前端資源是兼容 Node.js 環境的 CommonJS
  • 咱們編寫的 ESM 代碼 須要經過 Babel 轉換爲 CommonJS

趨勢

好消息是,瀏覽器已經開始原生支持模塊功能了,而且 Node.js 也在持續推動支持 ES Modules 模塊功能vue

ESM 標準化還在道路上node

客戶端與服務端的實現區別

在 Node.js 中使用 ES Modules

Node.js v13.2.0 開始,有兩種方式能夠正確解析 ESM 標準的模塊,在此之間還須要加上 --experimental-modules 纔可使用 ESM 模塊。webpack

  • 之後綴名爲 .mjs 結尾的文件
  • 之後綴名爲 .js 結尾的文件,且在 package.json 中聲明字段 typemodule
// esmA/index.mjs

export default esmA

// or

// esmB/index.js

export default esmB

// esmB/package.json

{

  "type": "module"

}
  • 之後綴名爲 .cjs 結尾的文件,將繼續解析爲 CommonJS 模塊

在瀏覽器中使用 ES Modules

現代瀏覽器已經原生支持加載 ES Modules 須要將 type="module" 放到 <script> 標籤中,聲明這個腳本是一個模塊。git

這樣就能夠在腳本中使用 importexport 語句了github

<script type="module">
  // include script here
</script>

caniuse-esm

在 Node.js 中處理依賴關係

現代前端工程開發環境中,會根據 package.json 來描述模塊之間的依賴關係,安裝模塊後,全部模塊會放在 node_modules 文件夾下。例如 package.json 中描述依賴了 lodashweb

{
  "name": "test",
  "version": "0.0.1",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

在瀏覽器中處理依賴關係

相似的,在瀏覽器中處理模塊之間的依賴關係,目前有一個新的提案 import-mapsnpm

經過聲明 <script> 標籤的屬性 typeimportmap,來定義模塊的名稱和模塊地址之間的映射關係

例如:

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
  }
}
</script>

在瀏覽器中處理依賴、使用模塊

importmap 仍然處於提案階段,目前瀏覽器兼容狀況還很緩慢,可是將來會持續兼容。咱們可使用 es-module-shims 使瀏覽器兼容。

<!-- UNPKG -->
<script async src="https://unpkg.com/es-module-shims@0.10.1/dist/es-module-shims.js"></script>

<!-- 聲明依賴 -->
<script type="importmap">
{
  "imports": {
    "app": "./src/app.js"
  }
}
</script>

<!-- 使用模塊 -->
<script type="module">
import 'app'
</script>

Vue SFC 簡介

什麼是 Vue SFC?

Vue 生態裏 SFC 是 single-file components (單文件組件) 的縮寫

經過擴展名 .vue 來描述了一個 Vue 組件

功能特性:

代碼示例:

sfc

如何編譯 Vue SFC?

Vue 工程須要藉助 vue-loader 或者 rollup-plugin-vue 來將 SFC 文件編譯轉化爲可執行的 JS

Vue 2

vue-loader 依賴:

  • @vue/component-compiler-utils
  • vue-style-loader

Vue 3

vue-loader@next 依賴:

  • @vue/compiler-core

Vite 2

@vitejs/plugin-vue 依賴:

  • @vue/compiler-sfc

@vue/compiler-sfc 的工做原理

編譯一個 Vue SFC 組件,須要分別編譯組件的 templatescriptstyle

API

+--------------------+
                                  |                    |
                                  |  script transform  |
                           +----->+                    |
                           |      +--------------------+
                           |
+--------------------+     |      +--------------------+
|                    |     |      |                    |
|  facade transform  +----------->+ template transform |
|                    |     |      |                    |
+--------------------+     |      +--------------------+
                           |
                           |      +--------------------+
                           +----->+                    |
                                  |  style transform   |
                                  |                    |
                                  +--------------------+

facade module,最終會編譯爲以下結構有 render 方法的組件僞代碼

// main script
import script from '/project/foo.vue?vue&type=script'
// template compiled to render function
import { render } from '/project/foo.vue?vue&type=template&id=xxxxxx'
// css
import '/project/foo.vue?vue&type=style&index=0&id=xxxxxx'

// attach render function to script
script.render = render

// attach additional metadata
// some of these should be dev only
script.__file = 'example.vue'
script.__scopeId = 'xxxxxx'

// additional tooling-specific HMR handling code
// using __VUE_HMR_API__ global

export default script

Vite & Vue SFC Playground

基於 @vue/compiler-sfc 構建的官方應用有 ViteVue SFC Playground,前者運行在服務端,後者運行在瀏覽器端。

Vite 的依賴

  • vite 2 經過插件 @vitejs/plugin-vue 提供 Vue 3 單文件組件支持
  • 底層依賴 @vue/compiler-sfc

Vue SFC Playground 的依賴

二者編譯 SFC 的過程之間的區別?

SFC Playground 中模塊的編譯源自 Vite 中對 SSR 的支持

Vite

SFC Playground

二者編譯 HelloWorld.vue 組件的區別?

Vite

// /components/HelloWorld.vue
import {defineComponent} from "/node_modules/.vite/vue.js?v=49d3ccd8";
const _sfc_main = defineComponent({
  name: "HelloWorld",
  props: {
    msg: {
      type: String,
      required: true
    }
  }
});

import { toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "/node_modules/.vite/vue.js?v=49d3ccd8"

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("h1", null, _toDisplayString(_ctx.msg), 1 /* TEXT */))
}

_sfc_main.render = _sfc_render
_sfc_main.__file = "/Users/xiaoyunwei/GitHub/private/slides-vite-demo/src/components/HelloWorld.vue"
export default _sfc_main

SFC Playground

// ./HelloWorld.vue
const __sfc__ = {
  name: "HelloWorld",
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}

import { toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("h1", null, _toDisplayString($props.msg), 1 /* TEXT */))
}
__sfc__.render = render
__sfc__.__file = "HelloWorld.vue"
export default __sfc__

二者編譯 App.vue 組件的區別?

Vite

// ./App.vue
import {defineComponent} from "/node_modules/.vite/vue.js?v=49d3ccd8";
import HelloWorld from "/src/components/HelloWorld.vue";
const _sfc_main = defineComponent({
  name: "App",
  components: {
    HelloWorld
  }
});

import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "/node_modules/.vite/vue.js?v=49d3ccd8"

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_HelloWorld = _resolveComponent("HelloWorld")

  return (_openBlock(), _createBlock(_component_HelloWorld, { msg: "Hello Vue 3 + TypeScript + Vite" }))
}

_sfc_main.render = _sfc_render
_sfc_main.__file = "/Users/xiaoyunwei/GitHub/private/slides-vite-demo/src/App.vue"
export default _sfc_main

SFC Playground

// ./App.vue
import HelloWorld from './HelloWorld.vue'

const __sfc__ = {
  name: 'App',
  components: {
    HelloWorld
  }
}

import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_HelloWorld = _resolveComponent("HelloWorld")

  return (_openBlock(), _createBlock(_component_HelloWorld, { msg: "Hello Vue SFC Playground" }))
}
__sfc__.render = render
__sfc__.__file = "App.vue"
export default __sfc__

能夠看出在編譯 SFC 時,底層邏輯基本是一致的。

抽象將 SFC 編譯爲 ES Modules 的能力

借鑑 Vue SFC Playground ,造了兩個輪子 🎡

感興趣能夠點擊去 GitHub 關注

vue-sfc2esm

將 Vue SFC 編譯爲 ES modules.

sfc2esm

功能

  • 💪 基於 TypeScript 編寫
  • 🌳 TreeShakable & SideEffects Free
  • 📁 虛擬文件系統 (支持編譯 .vue/.js 文件).
  • 👬 友好的錯誤提示

核心邏輯

  • vue-sfc2esm 內部實現了一個虛擬的 📁 文件系統,用來記錄文件和代碼的關係。
  • vue-sfc2esm 會基於 @vue/compiler-sfc 將 SFC 代碼編譯成 ES Modules
  • 編譯好的 ES Modules 代碼能夠直接應用於現代瀏覽器中。

編譯 App.vue 示例代碼:

<script type="module">
import { createApp as _createApp } from "vue"

if (window.__app__) {
  window.__app__.unmount()
  document.getElementById('app').innerHTML = ''
}

document.getElementById('__sfc-styles').innerHTML = window.__css__
const app = window.__app__ = _createApp(__modules__["DefaultDemo.vue"].default)
app.config.errorHandler = e => console.error(e)
app.mount('#app')
</script>

💡 使用 ES Modules 模塊前,須要提早引入 Vue

<script type="importmap">
  {
    "imports": { "vue": "https://cdn.jsdelivr.net/npm/vue@next/dist/vue.esm-browser.js" }
  }
</script>

vue-sfc-sandbox

vue-sfc-sandboxvue-sfc2esm 的上層應用,同時也基於 @vue/compiler-sfc 開發,提供實時編輯 & 預覽 SFC 的沙盒組件。

sfc-preivew

功能

🗳️ SFC 沙盒

  • 💪 基於 TypeScript 編寫
  • 🌳 TreeShakable & SideEffects Free
  • 📁 虛擬文件系統 (支持編譯 .vue/.js 文件)
  • 👬 友好的錯誤提示,基於 vue-sfc2esm
  • 🧪 將 Vue SFC 文件轉換爲 ES Modules
  • 🔌 支持外部 CDN, 好比 unpkgjsdelivr 等.
  • 🧩 加載 Import Maps.

✏️ 編輯器面板

  • 🎨 基於 codemirror 6 的代碼編輯器。
  • 🧑‍💻 對開發者友好, 內建高亮代碼, 可交互的面板呈現 REPL 沙盒環境。

👓 預覽面板

  • ⚡️ 實時編譯 SFC 文件
  • 🔍 全屏查看

將來與現狀

✨ 功能

  • 在線實時編譯 & 預覽 SFC 文件 / Vue 3 組件
  • 支持傳入外部 CDN
  • 支持傳入 Import Maps,傳入 URL 須要爲 ESM

💠 將來

  • 導出 SFC 組件
  • 支持實時編譯 React 組件
  • 編輯器智能提示

💉 痛點

  • 沒法直接使用打包成 CommonJSUMD 格式的包
  • 第三方依賴請求過多,有明顯的等待時長

🖖 破局

類似工程

相似 sfc-sandbox,基於 Vue 技術棧能夠在線提供編輯器 + 演示的工具

  • vuep - 🎡 A component for rendering Vue components with live editor and preview.
  • demosify - Create a playground to show the demos of your projects.
  • codepan - Like codepen and jsbin but works offline (Archived).

將來前端工程構建

雖然瀏覽器目前能夠加載使用 ES Modules 了,可是它仍是存在着一些上述提到的痛點中的問題的。

不過 2021 年的今天,已經涌現出了一批新的,能夠稱之爲下一代的前端構建工具,例如 esbuildsnowpackvitewmr 等等。

能夠看看這篇文章《Comparing the New Generation of Build Tools》,從工具配置、開發服務、生產構建、構建SSR等方面分析比較了前端下一代的構建工具。

future-build-tools

參考資料

移動端閱讀

關注個人技術公號,一樣也能夠找到本文。

xiaoluoboding

原文: https://transpile-vue-sfc-to-...
相關文章
相關標籤/搜索