最近想解决个场景,在给 ve-charts
编写文档的时候,想做一个代码示例演示性能,在改变代码后能够直观的看到组件的变动。之前版本中文档是用的 docsify
,docsify 中自带了一个 vuep
。vuep 就是解决我须要的场景的。不过 vuep 版本比拟老了。目前还不反对 vue3 组件。所以想独立开发一个运行代码示例的组件。
ES Modules 标准
ES modules(ESM)是 JavaScript 官网的标准化模块零碎
演进
在 ES6 之前,社区内曾经有咱们相熟的模块加载计划 CommonJS
和 AMD
,前者用于服务器 即 Node.js
,而后者借助第三方库实现浏览器加载模块。
在前端工程里,利用范畴比拟广的还是 CommonJS
,从三个方面咱们能够看出:
- 咱们依赖的公布在
NPM
上的第三方模块,大部分都打包默认反对CommonJS
- 通过
Webpack
构建的前端资源是兼容Node.js
环境的CommonJS
- 咱们编写的
ESM
代码 须要通过Babel
转换为CommonJS
趋势
好消息是,浏览器曾经开始原生反对模块性能了,并且 Node.js
也在继续推动反对 ES Modules 模块性能
ESM 标准化还在路线上
客户端与服务端的实现区别
在 Node.js 中应用 ES Modules
自 Node.js v13.2.0
开始,有两种形式能够正确解析 ESM
规范的模块,在此之间还须要加上 --experimental-modules
才能够应用 ESM 模块。
- 以后缀名为
.mjs
结尾的文件 - 以后缀名为
.js
结尾的文件,且在package.json
中申明字段type
为module
// 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>
标签中,申明这个脚本是一个模块。
这样就能够在脚本中应用 import
、export
语句了
<script type="module">
// include script here
</script>
在 Node.js 中解决依赖关系
古代前端工程开发环境中,会依据 package.json
来形容模块之间的依赖关系,装置模块后,所有模块会放在 node_modules
文件夹下。例如 package.json 中形容依赖了 lodash
:
{
"name": "test",
"version": "0.0.1",
"dependencies": {"lodash": "^4.17.21"}
}
在浏览器中解决依赖关系
相似的,在浏览器中解决模块之间的依赖关系,目前有一个新的提案 import-maps
通过申明 <script>
标签的属性 type
为 importmap
,来定义模块的名称和模块地址之间的映射关系
例如:
<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 组件
性能个性:
- 📝 残缺语法高亮
- 📦 CommonJS 模块
- 🎨 组件作用域的 CSS
代码示例:
如何编译 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 组件,须要别离编译组件的 template
、script
和 style
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
构建的官网利用有 Vite
与 Vue SFC Playground
,前者运行在服务端,后者运行在浏览器端。
Vite 的依赖
- vite 2 通过插件
@vitejs/plugin-vue
提供 Vue 3 单文件组件反对 - 底层依赖
@vue/compiler-sfc
Vue SFC Playground 的依赖
@vue/compiler-sfc
- 实际上
SFC Playground
是基于 @vue/compiler-sfc/dist/compiler-sfc.esm-browser.js 编译 ES Modules 的
两者编译 SFC 的过程之间的区别?
SFC Playground
中模块的编译源自 Vite
中对 SSR
的反对
Vite
- 1. check all import statements and record id -> importName map
- 2. check all export statements and define exports
- 3. convert references to import bindings & import.meta references
SFC Playground
- 0. instantiate module
- 1. check all import statements and record id -> importName map
- 2. check all export statements and define exports
- 3. convert references to import bindings
- 4. convert dynamic imports
- append CSS injection code
两者编译 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,造了两个轮子 🎡
- vue-sfc2esm: https://github.com/xiaoluobod…
- vue-sfc-sandbox: https://github.com/xiaoluobod…
感兴趣能够点击去 GitHub
关注
vue-sfc2esm
将 Vue SFC 编译为 ES modules.
性能
- 💪 基于 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-sandbox
是vue-sfc2esm
的下层利用,同时也基于@vue/compiler-sfc
开发,提供实时编辑 & 预览 SFC 的沙盒组件。
性能
🗳️ SFC 沙盒
- 💪 基于 TypeScript 编写
- 🌳 TreeShakable & SideEffects Free
- 📁 虚构文件系统 (反对编译
.vue/.js
文件) - 👬 敌对的谬误提醒,基于 vue-sfc2esm
- 🧪 将 Vue SFC 文件转换为 ES Modules
- 🔌 反对内部 CDN, 比方 unpkg、jsdelivr 等.
- 🧩 加载 Import Maps.
✏️ 编辑器面板
- 🎨 基于 codemirror 6 的代码编辑器。
- 🧑💻 对开发者敌对, 内建高亮代码, 可交互的面板出现 REPL 沙盒环境。
👓 预览面板
- ⚡️ 实时编译 SFC 文件
- 🔍 全屏查看
将来与现状
✨ 性能
- 在线实时编译 & 预览
SFC
文件 /Vue 3
组件 - 反对传入内部
CDN
- 反对传入
Import Maps
,传入 URL 须要为 ESM
💠 将来
- 导出 SFC 组件
- 反对实时编译
React
组件 - 编辑器智能提醒
💉 痛点
- 无奈间接应用打包成
CommonJS
或UMD
格局的包 - 第三方依赖申请过多,有显著的期待时长
🖖 破局
-
CommonJS
ToES Modules
计划- https://jspm.org/
- http://esm.sh/
- https://www.jsdelivr.com/esm
- https://www.skypack.dev/
- Vite 2 的 依赖预构建 计划
类似工程
相似 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 年的明天,曾经涌现出了一批新的,能够称之为下一代的前端构建工具,例如 esbuild
、snowpack
、vite
、wmr
等等。
能够看看这篇文章《Comparing the New Generation of Build Tools》,从工具配置、开发服务、生产构建、构建 SSR 等方面剖析比拟了前端下一代的构建工具。
参考资料
- JavaScript modules 模块: https://developer.mozilla.org…
- ES modules: A cartoon deep-dive: https://hacks.mozilla.org/201…
- import-maps: https://github.com/WICG/impor…
- es-module-shims: https://github.com/guybedford…
- Vue 3 Template Explorer: https://vue-next-template-exp…
- Vue SFC Playground: https://sfc.vuejs.org/
- 《Comparing the New Generation of Build Tools》: https://css-tricks.com/compar…
挪动端浏览
关注我的技术公号,同样也能够找到本文。
原文:https://transpile-vue-sfc-to-…