本文基于 vite@0.7.0 编写,可能与目前代码不符,如有疑问欢迎邮件或评论沟通
什么是 vite
vite 是一个基于 Vue3 单文件组件的非打包开发服务器
这和传统基于打包(例如 Webpack)的开发服务器有什么区别
vite 在开发的时候没有打包的过程,ES 模块源码直接传输给浏览器,浏览器使用自带的 <script module>
进行解析支持,通过 HTTP 请求进行每次 import,开发服务器拦截请求和对需要转换的代码进行转换。
例如:*.vue
文件会在发回浏览器之前进行编译
这样操作有许多优势:
- 开发服务器启动后不需要进行打包操作,启动会变得非常迅速
- 代码在需要的时候进行编译,所以只有代码真正在屏幕上展现的时候才进行编译。开始开发的时候再也不需要等待整个应用编译完成,这对大型应用是一个巨大的改变
- 热模块替换的性能和模块的数量之间的关系解耦,热模块替换变得非常快
导入本地 ES 模块可能会引发深层的导入链路,整个页面重新加载会比依赖打包的开发服务器略慢。然而这是一个本地开发服务器,这部分增加的时间和实际编译的时间相比应该非常小(编译的文件会被缓存在内存中)
vite 的编译本质上还是的 Node.js 中进行,从技术上讲它可以支持打包工具能支持的各种代码转换,没有什么可以阻止你将代码包用于生产,实际上,vite 提供了vite build
的脚本用于这个操作,因此不会在生产环境中遭遇到网络流爆炸的问题
当前 vite 尚处于实验性阶段,不适合用于生产环境,但希望有一天能做到这个目标
特性
模块解析
本地 ES 模块导入不支持如下的导入方式
import { createApp } from 'vue'
默认情况下将会导致一个错误,vite 在 js 文件中检测到这种情况将会将其改写为@modules/{package-name}
,在这些特殊的路径下,vite 执行以下的方式找到正确的文件
-
vue
有特殊的处理,你不需要安装这个模块,如果需要使用特殊的版本,vite 将会使用node_modules
内部的模块包 - 如果
web_modules
目录存在,将会使用它 - 如果其它方式都没有定位到模块,将会在
node_modules
中查找
热模块替换
- 对于
*.vue
文件将会得到开箱即用的替换功能 - 对于
*.js
需要提供类似于 webpack HMR 的 API
import { foo } from "./foo.js";
import { hot } from "@hmr";
foo();
hot.accept("./foo.js", ({ foo }) => {
// the callback receives the updated './foo.js' module
foo();
});
CSS 预处理器
安装模块即可在 *.vue
中使用
<style lang="scss">
/* use scss */
</style>
生产构建
执行 vite build
,当前支持 --root
和 --cdn
两个参数
API
可以使用 API 定制开发服务器,vite 支持插件形式扩展,可以定制化访问 vite 内部的 koa 实例和增加相关的中间件
下一步开发计划
- Source Map 支持
- 自动加载 postcss 配置
解析
启动一个 vite 开发服务器
-
http://localhost:3000/
首屏页面
<div id="app"></div>
<script type="module">
import { createApp } from "/@modules/vue"; // 此模块中包含相关热加载逻辑
import App from "./App.vue"; // 此文件为SFC主模板
createApp(App).mount("#app"); // 渲染模版
</script>
-
http://localhost:3000/App.vue
主模板
import { updateStyle } from "/@hmr"; // 加载更新style方法
const __script = {
data: () => ({ count: 0 })
};
updateStyle("c44b8200-0", "/App.vue?type=style&index=0");
__script.__scopeId = "data-v-c44b8200";
import { render as __render } from "/App.vue?type=template"; // 加载template模板
__script.render = __render;
__script.__hmrId = "/App.vue";
__script.__file = "/Users/shoyuf/work/vite-app/App.vue";
export default __script;
-
/@hmr
更新逻辑
console.log("[vite] connecting...");
const socket = new WebSocket(`ws://${location.host}`);
// Listen for messages
socket.addEventListener("message", ({ data }) => {
const { type, path, id, index, timestamp } = JSON.parse(data);
switch (type) {
case "connected": // 连接成功
console.log(`[vite] connected.`);
break;
case "vue-reload": // 当script改变的情况下,需要重新加载
import(`${path}?t=${timestamp}`).then(m => {
__VUE_HMR_RUNTIME__.reload(path, m.default);
console.log(`[vite] ${path} reloaded.`);
});
break;
case "vue-rerender": // 当template改变的情况下,需要重新渲染
import(`${path}?type=template&t=${timestamp}`).then(m => {
__VUE_HMR_RUNTIME__.rerender(path, m.render);
console.log(`[vite] ${path} template updated.`);
});
break;
case "vue-style-update": // 当css改变情况下更新style
updateStyle(id, `${path}?type=style&index=${index}&t=${timestamp}`);
console.log(
`[vite] ${path} style${index > 0 ? `#${index}` : ``} updated.`
);
break;
case "vue-style-remove": // css改变后移除旧的css引用
const link = document.getElementById(`vite-css-${id}`);
if (link) {
document.head.removeChild(link);
}
break;
case "js-update": // js 模块更新重新加载
const update = jsUpdateMap.get(path);
if (update) {
update(timestamp);
console.log(`[vite]: js module reloaded: `, path);
} else {
console.error(
`[vite] got js update notification but no client callback was registered. Something is wrong.`
);
}
break;
case "full-reload": // 导入链进入死胡同,需要进行页面重新加载
location.reload();
}
});
// ping server
socket.addEventListener("close", () => {
console.log(`[vite] server connection lost. polling for restart...`);
setInterval(() => {
new WebSocket(`ws://${location.host}`).addEventListener("open", () => {
location.reload();
});
}, 1000);
});
export function updateStyle(id, url) {
const linkId = `vite-css-${id}`;
let link = document.getElementById(linkId);
if (!link) {
link = document.createElement("link");
link.id = linkId;
link.setAttribute("rel", "stylesheet");
link.setAttribute("type", "text/css");
document.head.appendChild(link);
}
link.setAttribute("href", url);
}
const jsUpdateMap = new Map();
export const hot = {
accept(importer, deps, callback) {
jsUpdateMap.set(importer, timestamp => {
if (Array.isArray(deps)) {
Promise.all(deps.map(dep => import(dep + `?t=${timestamp}`))).then(
callback
);
} else {
import(deps + `?t=${timestamp}`).then(callback);
}
});
}
};
-
/App.vue?type=template
主模板 HTML 部分
import {
createVNode as _createVNode,
toDisplayString as _toDisplayString,
Fragment as _Fragment,
openBlock as _openBlock,
createBlock as _createBlock,
withScopeId as _withScopeId,
pushScopeId as _pushScopeId,
popScopeId as _popScopeId
} from "/@modules/vue";
const _withId = _withScopeId("data-v-c44b8200");
_pushScopeId("data-v-c44b8200");
const _hoisted_1 = _createVNode(
// 创建Virtual DOM
"h1",
null,
"Hello Vite + Vue 3!",
-1 /* HOISTED */
);
const _hoisted_2 = _createVNode(
"p",
null,
"Edit ./App.vue to test hot module replacement (HMR).",
-1 /* HOISTED */
);
_popScopeId();
export const render = _withId(function render(_ctx, _cache) {
// 渲染函数
return (
_openBlock(),
_createBlock(
_Fragment,
null,
[
_hoisted_1,
_hoisted_2,
_createVNode("p", null, [
_createVNode(
"span",
null,
"Count is: " + _toDisplayString(_ctx.count),
1 /* TEXT */
),
_createVNode(
"button",
{
onClick: _cache[1] || (_cache[1] = $event => _ctx.count++)
},
"increment"
)
])
],
64 /* STABLE_FRAGMENT */
)
);
});
-
/App.vue?type=style&index=0
主模板 css 部分,包括 scopedId
h1[data-v-c44b8200] {
color: #4fc08d;
}
h1[data-v-c44b8200],
p[data-v-c44b8200] {
font-family: Arial, Helvetica, sans-serif;
}
-
ws://localost:3000/
执行热替换的数据交互,与/@hmr
相联
Example:
{
path: "/App.vue",
timestamp: 1588242356511,
type: "vue-reload"
}
type 与@hmr
的相关方法一致
- vue-reload
- vue-rerender
- vue-style-update
- vue-style-remove
- js-update
- full-reload
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。