这是第 105 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:Vite 个性和局部源码解析
Vite 的个性
Vite 的次要个性就是 Bundleless。基于浏览器开始原生的反对 JavaScript 模块性能,JavaScript 模块依赖于 import
和 export
的个性,目前支流浏览器根本都反对;
想要查看具体反对的版本能够点击这里;
那这有什么劣势呢?
去掉打包步骤
打包是开发者利用打包工具将利用各个模块汇合在一起造成 bundle,以肯定规定读取模块的代码,以便在不反对模块化的浏览器里应用,并且能够缩小 http 申请的数量。但其实在本地开发过程中打包反而减少了咱们排查问题的难度,减少了响应时长,Vite 在本地开发命令中去除了打包步骤,从而缩短构建时长。
按需加载
为了缩小 bundle 大小,个别会想要按需加载,次要有两种形式:
- 应用动静引入
import()
的形式异步的加载模块,被引入模块仍然须要提前编译打包; - 应用 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 个外围的文件 & 办法;
- 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<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