12
本文基于 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

webpack 很慢

vite

参考

  1. vuejs/vite: Experimental no-bundle dev server for Vue SFCs

shoyuf
660 声望33 粉丝

Web Developer