当前位置:主页 > 查看内容

Vite 特性和部分源码解析

发布时间:2021-07-21 00:00| 位朋友查看

简介:这是第 105 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客: Vite 特性和部分源码解析 Vite 的特性 Vite 的主要特性就是 Bundleless。基于浏览器开始原生的支持 JavaScript 模块功能 ,JavaScript 模块依赖于 impo……

这是第 105 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:Vite 特性和部分源码解析

Vite 的特性

Vite 的主要特性就是 Bundleless。基于浏览器开始原生的支持 JavaScript 模块功能,JavaScript 模块依赖于 importexport 的特性,目前主流浏览器基本都支持;

想要查看具体支持的版本可以点击这里

那这有什么优势呢?

去掉打包步骤

打包是开发者利用打包工具将应用各个模块集合在一起形成 bundle,以一定规则读取模块的代码,以便在不支持模块化的浏览器里使用,并且可以减少 http 请求的数量。但其实在本地开发过程中打包反而增加了我们排查问题的难度,增加了响应时长,Vite 在本地开发命令中去除了打包步骤,从而缩短构建时长。

按需加载

为了减少 bundle 大小,一般会想要按需加载,主要有两种方式:

  1. 使用动态引入 import() 的方式异步的加载模块,被引入模块依然需要提前编译打包;
  2. 使用 tree shaking 等方式尽力的去掉未引用的模块;

而 Vite 的方式更为直接,它只在某个模块被 import 的时候动态的加载它,实现了真正的按需加载,减少了加载文件的体积,缩短了时长;

Vite开发环境主体流程

下图是 Vite 在开发环境运行时加载文件的主体流程。

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6922f9b694324cb193acdbb482babadb~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:60%;" />

Vite 部分源码解析

总体目录结构

|-CHANGELOG.md
|-LICENSE.md
|-README.md
|-bin
|? |-openChrome.applescript
|? |-vite.js
|-client.d.ts
|-package.json
|-rollup.config.js #打包配置文件
|-scripts
|? |-patchTypes.js
|-src
|? |-client #客户端
|? |? |-client.ts
|? |? |-env.ts
|? |? |-overlay.ts
|? |? |-tsconfig.json
|? |-node #服务端
|? |? |-build.ts
|? |? |-cli.ts #命令入口文件
|? |? |-config.ts
|? |? |-constants.ts #常量
|? |? |-importGlob.ts
|? |? |-index.ts
|? |? |-logger.ts
|? |? |-optimizer
|? |? |? |-esbuildDepPlugin.ts
|? |? |? |-index.ts
|? |? |? |-registerMissing.ts
|? |? |? |-scan.ts
|? |? |-plugin.ts #rollup 插件
|? |? |-plugins   #插件相关文件
|? |? |? |-asset.ts
|? |? |? |-clientInjections.ts
|? |? |? |-css.ts
|? |? |? |-esbuild.ts
|? |? |? |-html.ts
|? |? |? |-index.ts 
|? |? |? |-...
|? |? |-preview.ts
|? |? |-server
|? |? |? |-hmr.ts #热更新
|? |? |? |-http.ts
|? |? |? |-index.ts
|? |? |? |-middlewares #中间件
|? |? |? |? |-...
|? |? |? |-moduleGraph.ts #模块间关系组装(树形)
|? |? |? |-openBrowser.ts #打开浏览器
|? |? |? |-pluginContainer.ts
|? |? |? |-send.ts
|? |? |? |-sourcemap.ts
|? |? |? |-transformRequest.ts
|? |? |? |-ws.ts
|? |? |-ssr
|? |? |? |-__tests__
|? |? |? |? |-ssrTransform.spec.ts
|? |? |? |-ssrExternal.ts
|? |? |? |-ssrManifestPlugin.ts
|? |? |? |-ssrModuleLoader.ts
|? |? |? |-ssrStacktrace.ts
|? |? |? |-ssrTransform.ts
|? |? |-tsconfig.json
|? |? |-utils.ts
|-tsconfig.base.json
|-types
|? |-...                  

server 核心方法

从入口文件 cli.ts,可以看到三个命令对应了 3 个核心的文件&方法;

  1. dev 命令

文件路径:./server/index.ts;

主要方法:createServer;

主要功能:项目的本地开发命令,基于 httpServer 启动服务,Vite 通过对请求路径的劫持获取资源的内容返回给浏览器,服务端将文件路径进行了重写。例如:

项目源码如下:

import { createApp } from 'vue';
import App from './index.vue';

经服务端重写后,node_modules 文件夹下的三方包代码路径也会被拼接完整。

import __vite__cjsImport0_vue from "/node_modules/.vite/vue.js?v=ed69bae0"; 
const createApp = __vite__cjsImport0_vue["createApp"];
import App from '/src/pages/back-sky/index.vue';

2.build 命令
文件路径:./build.ts ;

主要方法:build;

主要功能:使用 rollup 打包编译

3.optimize 命令

文件路径:./optimizer/index.ts;

主要方法:optimizeDeps;

主要功能:主要针对第三方包,Vite 在执行 runOptimize 的时候中会使用 rollup 对三方包重新编译,将编译成符合 esm 模块规范的新的包放入 node_modules 下的 .vite 中,然后配合 resolver 对三方包的导入进行处理:使用编译后的包内容代替原来包的内容,这样就解决了 Vite 中不能使用 cjs 包的问题。

下面是 .vite 文件夹中的 _metadata.json 文件,它在预编译的过程中生成,罗列了所有被预编译完成的文件及其路径。例如:

{
 ?"hash": "31d458ff",
 ?"browserHash": "ed69bae0",
 ?"optimized": {
 ? ?"element-plus/lib/utils/dom": {
 ? ? ?"file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus_lib_utils_dom.js",
 ? ? ?"src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/utils/dom.js",
 ? ? ?"needsInterop": true
 ?  },
 ? ?"element-plus": {
 ? ? ?"file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/element-plus.js",
 ? ? ?"src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/index.esm.js",
 ? ? ?"needsInterop": false
 ?  },
 ? ?"vue": {
 ? ? ?"file": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js",
 ? ? ?"src": "/Users/zcy/Documents/workspace/back-sky-front/node_modules/vue/dist/vue.runtime.esm-bundler.js",
 ? ? ?"needsInterop": true
 ?  },
 ? ?......
 ?  }
  }
}

模块解析

预构建是用来提升页面重载速度,它将 CommonJS、UMD 等转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包程序都要快得多。

为什么 ESbuild 会更快?

  1. 使用 Go 语言
  2. 重度并行,使用 CPU
  3. 高效使用内存
  4. Scratch 编写,减少使用三方库,避免导致性能不可控

重写导入为合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便浏览器能够正确导入它们

热更新

热更新主体流程如下:

  1. 服务端基于 watcher 监听文件改动,根据类型判断更新方式,并编译资源
  2. 客户端通过 WebSocket 监听到一些更新的消息类型
  3. 客户端收到资源信息,根据消息类型执行热更新逻辑

下面是服务端热更新的核心 hmr.ts 中的部分判断逻辑;

如果配置文件或者环境文件发生修改时,会触发服务重启,才能让配置生效。

if (file === config.configFile || file.endsWith('.env')) {
? // auto restart server 配置&环境文件修改则自动重启服务
? debugHmr(`[config change] ${chalk.dim(shortFile)}`)
? config.logger.info(
? ? chalk.green('config or .env file changed, restarting server...'),
? ? { clear: true, timestamp: true }
? )
? await restartServer(server)
? return
}

html 文件更新时,将会触发页面的重新加载。

if (file.endsWith('.html')) { // html 文件更新
? config.logger.info(chalk.green(`page reload `) +         chalk.dim(shortFile), {
? ? clear: true,
? ? timestamp: true
? })
? ws.send({
? ? type: 'full-reload',
? ? path: config.server.middlewareMode
? ? ? '*'
? ? : '/' + normalizePath(path.relative(config.root, file))
? })
} else {
? // loaded but not in the module graph, probably not js
? debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`)
}

Vue 等文件更新时,都会进入 updateModules 方法,正常情况下只会触发 update,实现热更新,热替换;

function updateModules(
? file: string,
? modules: ModuleNode[],
? timestamp: number,
? { config, ws }: ViteDevServer
) {
? const updates: Update[] = []
? const invalidatedModules = new Set<ModuleNode>()
    // 遍历插件数组,关联下面的片段
? for (const mod of modules) {
? ? const boundaries = new Set<{
? ? ? boundary: ModuleNode
? ? ? acceptedVia: ModuleNode
? ? }>()
? ? // 设置时间戳
? ? invalidate(mod, timestamp, invalidatedModules)
? ? // 查找引用模块,判断是否需要重载页面
? ? const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
    // 找不到引用者则会发起刷新
? ? if (hasDeadEnd) {
? ? ? config.logger.info(chalk.green(`page reload `) + chalk.dim(file), {
? ? ? ? clear: true,
? ? ? ? timestamp: true
? ? ? })
? ? ? ws.send({
? ? ? ? type: 'full-reload'
? ? ? })
? ? ? return
? ? }
? ? updates.push(
? ? ? ...[...boundaries].map(({ boundary, acceptedVia }) => ({
? ? ? ? type: `${boundary.type}-update` as Update['type'],
? ? ? ? timestamp,
? ? ? ? path: boundary.url,
? ? ? ? acceptedPath: acceptedVia.url
? ? ? }))
? ? )
? }
  // 日志输出
? config.logger.info(
? ? updates
? ? ? .map(({ path }) => chalk.green(`hmr update `) + chalk.dim(path))
? ? ? .join('\n'),
? ? { clear: true, timestamp: true }
? )
? // 向客户端发送消息,进行热更新操作
? ws.send({
? ? type: 'update',
? ? updates
? })
}

上面代码中的 modules 是热更新时需要执行的各个插件

for (const plugin of config.plugins) {
? if (plugin.handleHotUpdate) {
? ? const filteredModules = await plugin.handleHotUpdate(hmrContext)
? ? if (filteredModules) {
? ? ? hmrContext.modules = filteredModules
? ? }
? }
}

Vite 会把模块的依赖关系组合成 moduleGraph,它的结构类似树形,热更新中判断哪些文件需要更新也会依赖 moduleGraph;它的文件内容大致如下:

// moduleGraph 返回的 ModuleNode 大致结构
?ModuleNode {
? id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
? file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.js',
? importers: Set {},
? importedModules: Set {
? ? ModuleNode {
? ? ? id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js?v=32cfd30c',
? ? ? file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/.vite/vue.js',
? ? ? ......
? ? ? lastHMRTimestamp: 0,
? ? ? url: '/node_modules/.vite/vue.js?v=32cfd30c',
? ? ? type: 'js'
? ? },
? ? ModuleNode {
? ? ? id: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
? ? ? file: '/Users/zcy/Documents/workspace/back-sky-front/src/pages/back-sky/index.vue',
? ? ? ......
? ? ? url: '/src/pages/back-sky/index.vue',
? ? ? type: 'js'
? ? },
? ? ModuleNode {
? ? ? id: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
? ? ? file: '/Users/zcy/Documents/workspace/back-sky-front/node_modules/element-plus/lib/theme-chalk/index.css',
? ? ? importers: [Set],
? ? ? importedModules: Set {},
? ? ? acceptedHmrDeps: Set {},
? ? ? isSelfAccepting: true,
? ? ? transformResult: [Object],
? ? ? ssrTransformResult: null,
? ? ? ssrModule: null,
? ? ? lastHMRTimestamp: 0,
? ? ? url: '/node_modules/element-plus/lib/theme-chalk/index.css',
? ? ? type: 'js'
? ? },
? ? ......
? },
? acceptedHmrDeps: Set {},
? isSelfAccepting: false,
? transformResult: {
? ? code: 'import __vite__cjsImport0_vue from ' +
? ? ? '"/node_modules/.vite/vue.js?v=32cfd30c"; const createApp = ' +
? ? ? '__vite__cjsImport0_vue["createApp"];\nimport App from ' +
? ? ? "'/src/pages/back-sky/index.vue';\nimport " +
? ? ? "'/node_modules/element-plus/lib/theme-chalk/index.css';\n\nconst app = " +
? ? ? 'createApp(App);\n\nimport { addHistoryMethod } from ' +
? ? ? "'/src/pages/back-sky/api/index.js';\nimport {\n? ElButton,\n? ElDropdown,\n? " +
? ? ? 'ElDropdownMenu,\n? ElDropdownItem,\n? ElMenu,\n? ElSubmenu,\n? ElMenuItem,\n? ' +
? ? ? 'ElMenuItemGroup,\n? ElPopover,\n? ElDialog,\n? ElRow,\n? ElInput,\n? ' +
? ? ? "ElLoading,\n} from '/node_modules/.vite/element-plus.js?v=32cfd30c';\n\n" +
? ? ? 'app.use(ElButton);\napp.use(ElLoading);\napp.use(ElDropdown);\n' +
? ? ? 'app.use(ElDropdownMenu);\napp.use(ElDropdownItem);\napp.use(ElMenu);\n' +
? ? ? 'app.use(ElSubmenu);\napp.use(ElMenuItem);\napp.use(ElMenuItemGroup);\n' +
? ? ? 'app.use(ElPopover);\napp.use(ElDialog);\napp.use(ElRow);\napp.use(ElInput);\n' +
? ? ? "\nconst f = ()=>{\n? return app.mount('#app');\n};\n\nconst $backsky = " +
? ? ? "document.getElementById('back-sky');\nif($backsky) {\n? $backsky.innerHTML " +
? ? ? "= '';\n? $backsky.appendChild(f().$el);\n} else {\n? window.onload = " +
? ? ? "function(){\n? ? document.getElementById('back-sky') && " +
? ? ? "document.getElementById('back-sky').appendChild(f().$el);\n? };\n}\n\n" +
? ? ? "window.addHistoryListener = addHistoryMethod('historychange');\n" +
? ? ? "history.pushState =? addHistoryMethod('pushState');\nhistory.replaceState " +
? ? ? "=? addHistoryMethod('replaceState');\n\n// 监听hash路由变化,不与onhashchange互相覆盖\n" +
? ? ? 'addHashChange(()=>{\n? setTimeout(() => {\n? ? const $backsky = ' +
? ? ? "document.getElementById('back-sky');\n? ? if($backsky && " +
? ? ? "$backsky.innerHTML === '') {\n? ? ? $backsky.appendChild(f().$el);\n? ? }\n " +
? ? ? " },0);\n});\n\nfunction addHashChange(callback) {\n? if('onhashchange' in " +
? ? ? 'window === false){//浏览器不支持\n? ? return false;\n? }\n? ' +
? ? ? 'if(window.addEventListener) {\n? ? ' +
? ? ? "window.addEventListener('hashchange',function(e) {\n? ? ? callback && " +
? ? ? 'callback(e);\n? ? },false);\n? }else if(window.attachEvent) {//IE 8 及更早 IE ' +
? ? ? "版本浏览器\n? ? window.attachEvent('onhashchange',function(e) {\n? ? ? callback " +
? ? ? '&& callback(e);\n? ? });\n? }\n? ' +
? ? ? "window.addHistoryListener('history',function(e){\n? ? callback && " +
? ? ? 'callback(e);\n? });\n}\n\n\n',
? ? map: null,
? ? etag: 'W/"846-Qa424gJKl3YCqHDWXXsM1mFHRqg"'
? },
? ssrTransformResult: null,
? ssrModule: null,
? lastHMRTimestamp: 0,
? url: '/src/pages/back-sky/index.js',
? type: 'js'
}

原有项目切换

最后我们来看下如何使用 Vite 去打包一个旧的 Vue 项目;

首先我们需要升级 Vue3

npm install vue@next

并为项目添加 vite 配置文件,在根目录下创建 vite.config.js,并为它添加一些基础的配置。

// vite.config.js
// vite2.1.5
const path = require('path');
import vue from '@vitejs/plugin-vue';

export default {
 ?// 配置选项
 ?resolve: {
 ? ?alias: {
 ? ? ?'@utils': path.resolve(__dirname, './src/utils')
 ?  },
  },
 ?plugins: [vue()],
};

引用的第三方组件库可能也会需要升级,例如:升 element-ui 至 element-plus

npm install element-plus

Vue3 在 import 时,需使用 createApp 方法进行初始化

import { createApp } from 'vue';
import App from './index.vue';
const app = createApp(App);
import {
 ?ElInput,
 ?ElLoading,
} from 'element-plus';

app.use(ElButton);
app.use(ElLoading);
......

到这里就可以将项目运行起来了。
注意:Vite 官方不允许省略 .vue 后缀,否则就会报错;

[plugin:vite:import-analysis] Failed to resolve import "./todoList" from "src/pages/back-sky/components/header/index.vue". Does the file exist?
/components/header/index.vue:2:23
1 ?| ?
2 ?| ?import todoList from './todoList';
import todoList from './todoList.vue';

最后我们来对比一下该项目两种构建方式时间的对比;

Webpack 冷启动,耗时 7513ms:

? ?wdm?: Hash: 1ad1dd54289cfad8ecbe
Version: webpack 4.46.0
Time: 7513ms
Built at: 2021-05-24 13:59:35

相同项目 Vite 冷启动,耗时 924ms:

> vite
Pre-bundling dependencies:
? vue
? element-plus
? @zcy/zcy-request
? element-plus/lib/utils/dom
(this will be run only when your dependencies or config have changed)
? vite v2.3.3 dev server running at:
? > Local: http://localhost:3000/
? > Network: use `--host` to expose
? ready in 924ms.

二次启动(预编译的依赖已存在),耗时 407ms;

> vite
? vite v2.3.3 dev server running at:
? > Local: http://localhost:3000/
? > Network: use `--host` to expose
? ready in 407ms.

总结

使用 Vite 进行本地服务启动和热更新都会有明显的提效,至于编译打包环节的差异点有哪些?效果如何?你们还踩过哪些坑?留言告诉我吧。

推荐阅读:

What are CJS, AMD, UMD, and ESM in Javascript?

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com


本文转自网络,版权归原作者所有,原文链接:https://segmentfault.com/a/1190000040375262
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐