ES modules(ESM) 是 JavaScript 官方的标准化模块系统
在 ES6 之前,社区内已经有我们熟悉的模块加载方案 CommonJS
和 AMD
,前者用于服务器 即 Node.js
,而后者借助第三方库实现浏览器加载模块。
在前端工程里,应用范围比较广的还是 CommonJS
,从三个方面我们可以看出:
NPM
上的第三方模块,大部分都打包默认支持 CommonJS
Webpack
构建的前端资源是兼容 Node.js 环境的 CommonJS
Babel
转换为 CommonJS
好消息是,浏览器已经开始原生支持模块功能了,并且 Node.js
也在持续推进支持 ES Modules 模块功能
ESM 标准化还在道路上
自 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" }
// esmA/index.mjs export default esmA // or // esmB/index.js export default esmB // esmB/package.json { "type": "module" }
.cjs
结尾的文件,将继续解析为 CommonJS
模块现代浏览器已经原生支持加载 ES Modules
需要将 type="module"
放到 <script>
标签中,声明这个脚本是一个模块。
这样就可以在脚本中使用 import
、export
语句了
<script type="module"> // include script here </script>
<script type="module"> // include script here </script>
现代前端工程开发环境中,会根据 package.json
来描述模块之间的依赖关系,安装模块后,所有模块会放在 node_modules
文件夹下。例如 package.json 中描述依赖了lodash:
{ "name": "test", "version": "0.0.1", "dependencies": { "lodash": "^4.17.21" } }
{ "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>
<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>
<!-- 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 工程需要借助 vue-loader
或者 rollup-plugin-vue
来将 SFC 文件编译转化为可执行的 JS
vue-loader 依赖的是:
@vue/component-compiler-utils
vue-style-loader
vue-loader@next 依赖的是:
@vue/compiler-core
@vitejs/plugin-vue 依赖的是:
@vue/compiler-sfc
Lower level utilities for compiling Vue Single File Components
编译一个 Vue SFC 组件,需要分别编译组件的 template
、script
和 style
+--------------------+ | | | script transform | +----->+ | | +--------------------+ | +--------------------+ | +--------------------+ | | | | | | facade transform +----------->+ template transform | | | | | | +--------------------+ | +--------------------+ | | +--------------------+ +----->+ | | style transform | | | +--------------------+
+--------------------+ | | | script transform | +----->+ | | +--------------------+ | +--------------------+ | +--------------------+ | | | | | | facade transform +----------->+ template transform | | | | | | +--------------------+ | +--------------------+ | | +--------------------+ +----->+ | | style transform | | | +--------------------+
// 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
// 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
基于 @vue/compiler-sfc
构建的官方应用有 Vite
与 Vue SFC Playground
,前者运行在服务端,后者运行在浏览器端。
- vite 2 通过插件
@vitejs/plugin-vue
提供 Vue 3 单文件组件支持- 底层依赖
@vue/compiler-sfc
@vue/compiler-sfc
- 实际上
SFC Playground
是基于 @vue/compiler-sfc/dist/compiler-sfc.esm-browser.js 编译 ES Modules 的
SFC Playground
中模块的编译源自 Vite
中对 SSR
的支持
编译 HelloWorld.vue
// /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
// /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
// ./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__
// ./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
// ./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
// ./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
// ./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__
// ./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__
Transpiled Vue SFC File to ES modules.
将 Vue SFC 编译为 ES modules.
.vue/.js
文件).💡 灵感来自 Vue SFC Playground.
核心逻辑
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>
<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>
<script type="importmap"> { "imports": { "vue": "https://cdn.jsdelivr.net/npm/vue@next/dist/vue.esm-browser.js" } } </script>
Vue SFC Sandbox built on top of @vue/compiler-sfc
, Sandbox as a Vue 3 component.
vue-sfc-sandbox
是 vue-sfc2esm
的上层应用,同时也基于 @vue/compiler-sfc
构建,提供实时编辑 & 预览 SFC 的沙盒组件。
.vue/.js
文件)windicss
,同时支持 attributify
模式<template>
和 <script>
代码编辑器,分别编辑SFC
文件 / Vue 3
组件CDN
Import Maps
,传入 URL 需要为 ESMwindicss
Import Maps
包管理React
组件2021 年的今天,已经涌现出了一批新的,可以称之为下一代的前端构建工具,例如 esbuild
、snowpack
、vite
、wmr
等等。
可以看看这篇文章《Comparing the New Generation of Build Tools》,从工具配置、开发服务、生产构建、构建SSR等方面分析比较了前端下一代的前端构建工具。
可以关注我的技术公号,不定期分享前端新知。