最近想解决个场景,在给 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.mjsexport default esmA// or// esmB/index.jsexport 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 scriptimport script from '/project/foo.vue?vue&type=script'// template compiled to render functionimport { render } from '/project/foo.vue?vue&type=template&id=xxxxxx'// cssimport '/project/foo.vue?vue&type=style&index=0&id=xxxxxx'// attach render function to scriptscript.render = render// attach additional metadata// some of these should be dev onlyscript.__file = 'example.vue'script.__scopeId = 'xxxxxx'// additional tooling-specific HMR handling code// using __VUE_HMR_API__ globalexport 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.vueimport {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.vueconst __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.vueimport {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.vueimport 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-...