概述
Vite 的特性
Vite 的主要特性就是 Bundleless。基于浏览器开始原生的支持 JavaScript 模块功能,JavaScript 模块依赖于 import 和 export 的特性,目前主流浏览器基本都支持;
想要查看具体支持的版本可以点击这里;
那这有什么优势呢?
去掉打包步骤
打包是开发者利用打包工具将应用各个模块集合在一起形成 bundle,以一定规则读取模块的代码,以便在不支持模块化的浏览器里使用,并且可以减少 http 请求的数量。但其实在本地开发过程中打包反而增加了我们排查问题的难度,增加了响应时长,Vite 在本地开发命令中去除了打包步骤,从而缩短构建时长。
按需加载
为了减少 bundle 大小,一般会想要按需加载,主要有两种方式:
使用动态引入 import() 的方式异步的加载模块,被引入模块依然需要提前编译打包;
使用 tree shaking 等方式尽力的去掉未引用的模块;
而 Vite 的方式更为直接,它只在某个模块被 import 的时候动态的加载它,实现了真正的按需加载,减少了加载文件的体积,缩短了时长;
Vite开发环境主体流程
下图是 Vite 在开发环境运行时加载文件的主体流程。
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 个核心的文件&方法;
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 会更快?
使用 Go 语言
重度并行,使用 CPU
高效使用内存
Scratch 编写,减少使用三方库,避免导致性能不可控
重写导入为合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便浏览器能够正确导入它们
热更新
热更新主体流程如下:
服务端基于 watcher 监听文件改动,根据类型判断更新方式,并编译资源
客户端通过 WebSocket 监听到一些更新的消息类型
客户端收到资源信息,根据消息类型执行热更新逻辑
下面是服务端热更新的核心 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()
// 遍历插件数组,关联下面的片段
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’;nnconst app = " +
‘createApp(App);nnimport { 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’;nn” +
‘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};nnconst KaTeX parse error: Undefined control sequence: nif at position 58: …Id('back-sky');̲n̲i̲f̲(backsky) {n $backsky.innerHTML " +
"= ‘’;n
b
a
c
k
s
k
y
.
a
p
p
e
n
d
C
h
i
l
d
(
f
(
)
.
backsky.appendChild(f().
backsky.appendChild(f().el);n} else {n window.onload = " +
"function(){n document.getElementById(‘back-sky’) && " +
“document.getElementById(‘back-sky’).appendChild(f().$el);n };n}nn” +
“window.addHistoryListener = addHistoryMethod(‘historychange’);n” +
"history.pushState = addHistoryMethod(‘pushState’);nhistory.replaceState " +
“= addHistoryMethod(‘replaceState’);nn// 监听hash路由变化,不与onhashchange互相覆盖n” +
'addHashChange(()=>{n setTimeout(() => {n const KaTeX parse error: Undefined control sequence: n at position 58: …Id('back-sky');̲n̲ ̲ ̲ ̲ ̲if(backsky && " +
"$backsky.innerHTML === ‘’) {n
b
a
c
k
s
k
y
.
a
p
p
e
n
d
C
h
i
l
d
(
f
(
)
.
backsky.appendChild(f().
backsky.appendChild(f().el);n }n " +
" },0);n});nnfunction 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}nnn’,
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 进行本地服务启动和热更新都会有明显的提效,至于编译打包环节的差异点有哪些?效果如何?你们还踩过哪些坑?留言告诉我吧。
如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力????
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: http://github.crmeb.net/u/defu 不胜感激 !
来自 “开源世界 ” ,链接: https://ym.baisou.ltd/post/765.html ,如需转载,请注明出处,否则将追究法律责任
最后
以上就是搞怪柠檬为你收集整理的Vite 特性和部分源码解析的全部内容,希望文章能够帮你解决Vite 特性和部分源码解析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复