大家好,我是小雨小雨,致力于分享有趣的、实用的文章。

内容分为原创和翻译,如果有问题,欢迎随时评论或私信,很乐意和大家一起探讨,一起进步。

分享不易,希望能够得到大家的支持和关注。


vite出了好久了,也出了好多相关文章,我也想出,然后我就写了。

该文档对应的vite版本:2.0.0-beta.4
vite文档

整体流程

笔者认为,vite是站在巨人肩膀上的一个创新型dev构建工具,分别继承于:

其中洋葱模型如果将next()放到函数最底部的话,和rollup的插件驱动是类似的。

也就是说可插拔架构是vite的整体思想,不仅可以编写内部插件,将内部插件原子化,还可以借助npm上各种已有的插件。非常灵活。

为什么采用es module呢?

vite采用的es module进行模块导入,这是现代浏览器原生支持的,当import一个模块时,会发出一个请求,正因如此,只能在服务中使用es module。而且import的模块路径发生变化的时候,会重新发送请求,路径变化包括query。


下面我们进入整体

vite采用monorepo架构,我们要关心的代码主要两部分:

先从vite cli说起。

这里是vite的入口:

  1. const { createServer } = await import('./server')
  2. try {
  3. const server = await createServer(
  4. {
  5. root,
  6. mode: options.mode,
  7. logLevel: options.logLevel,
  8. server: cleanOptions(options) as ServerOptions
  9. },
  10. options.config
  11. )
  12. await server.listen()

简单粗暴,通过createServer创建一个服务,然后开始监听,我们直接一个瞎子摸葫芦,打开createServer看看。

  1. export async function createServer(
  2. inlineConfig: UserConfig & { mode?: string } = {},
  3. configPath?: string | false
  4. ): Promise<ViteDevServer> {
  5. // 代码太多不放了,放点方便看的,有兴趣的话可以打开代码一边看这里的注释一边看代码
  6. // 配置相关,比如记载本地配置文件、集成插件,环境变量等等
  7. // 利用connect初始化服务,connect是一个使用中间件为node提供可扩展服务的http框架,有兴趣可以去看看
  8. // 创建webSocket服务
  9. // 利用chokidar进行文件监听
  10. // vite继承rollup实现了一个迷你版的构解析构建工具
  11. // 创建一个图来维护模块之间的关系
  12. // 当文件发生变化的时候进行hmr相关操作,后续会介绍
  13. // 接入各种各样的中间件,比如接口代理的、静态服务的、解析请求资源的、重定向、处理html的等,其中最重要的就是解析请求资源的了,下面具体来扣一下这块
  14. // 调用插件中的configureServer,这一步可以将vite中所有内容暴露给用户,比如node服务app,配置,文件监听器,socket等等,很大胆,很坏,但是我好喜欢
  15. // 返回node服务,供listen
  16. }

运行完这一堆后,我们就启动了一个服务,我们发现,vite到目前为止,并没有任何关于打包的代码,那他快在哪里呢?

其实没有打包就是vite快的原因之一,而他的打包做到了真正的按需。

启动服务后,我们访问页面会发送一个个的请求,这些请求会经过中间件处理,而中间件,就会进行打包,注入等相关操作。

核心内容其实就是上面注释中写的解析请求资源这个中间件,vite中叫做transformMiddleware

  1. export function transformMiddleware(
  2. server: ViteDevServer
  3. ): Connect.NextHandleFunction {
  4. const {
  5. config: { root, logger },
  6. moduleGraph
  7. } = server
  8. return async (req, res, next) => {
  9. // 其他代码
  10. // Only apply the transform pipeline to:
  11. // - requests that initiate from ESM imports (any extension)
  12. // - CSS (even not from ESM)
  13. // - Source maps (only for resolving)
  14. if (
  15. isJSRequest(url) || // 指定的(j|t)sx?|mjs|vue这类文件,或者没有后缀
  16. isImportRequest(url) || // import来的
  17. isCSSRequest(url) || // css
  18. isHTMLProxy(url) || // html-proxy
  19. server.config.transformInclude(withoutQuery) // 命中需要解析的
  20. ) {
  21. // 移除import的query,例: (\?|$)import=xxxx
  22. url = removeImportQuery(url)
  23. // 删调idprefix,importAnalysis生成的不合法的浏览器说明符被预先解析id
  24. if (url.startsWith(VALID_ID_PREFIX)) {
  25. url = url.slice(VALID_ID_PREFIX.length)
  26. }
  27. // for CSS, we need to differentiate between normal CSS requests and
  28. // imports
  29. // 处理css链接
  30. if (isCSSRequest(url) && req.headers.accept?.includes('text/css')) {
  31. url = injectQuery(url, 'direct')
  32. }
  33. // check if we can return 304 early
  34. const ifNoneMatch = req.headers['if-none-match']
  35. // 命中浏览器缓存,利用浏览器的特性
  36. if (
  37. ifNoneMatch &&
  38. (await moduleGraph.getModuleByUrl(url))?.transformResult?.etag ===
  39. ifNoneMatch
  40. ) {
  41. res.statusCode = 304
  42. return res.end()
  43. }
  44. // 解析vue js css 等文件的关键
  45. const result = await transformRequest(url, server)
  46. if (result) {
  47. const type = isDirectCSSRequest(url) ? 'css' : 'js'
  48. const isDep =
  49. DEP_VERSION_RE.test(url) ||
  50. url.includes(`node_modules/${DEP_CACHE_DIR}`)
  51. return send(
  52. req,
  53. res,
  54. result.code,
  55. type,
  56. result.etag,
  57. // allow browser to cache npm deps!
  58. isDep ? 'max-age=31536000,immutable' : 'no-cache',
  59. result.map
  60. )
  61. }
  62. }
  63. } catch (e) {
  64. return next(e)
  65. }
  66. next()
  67. }
  68. }

其中最重要的是transformRequest,该方法进行了缓存,请求资源解析,加载,转换操作。

  1. export async function transformRequest(
  2. url: string,
  3. { config: { root }, pluginContainer, moduleGraph, watcher }: ViteDevServer
  4. ): Promise<TransformResult | null> {
  5. url = removeTimestampQuery(url)
  6. const prettyUrl = isDebug ? prettifyUrl(url, root) : ''
  7. // 检查上一次的transformResult,这个东西会在hmr中被主动移除掉
  8. const cached = (await moduleGraph.getModuleByUrl(url))?.transformResult
  9. if (cached) {
  10. isDebug && debugCache(`[memory] ${prettyUrl}`)
  11. return cached
  12. }
  13. // resolve
  14. const id = (await pluginContainer.resolveId(url))?.id || url
  15. const file = cleanUrl(id)
  16. let code = null
  17. let map: SourceDescription['map'] = null
  18. // load
  19. const loadStart = Date.now()
  20. const loadResult = await pluginContainer.load(id)
  21. // 加载失败,直接读文件
  22. if (loadResult == null) {
  23. // try fallback loading it from fs as string
  24. // if the file is a binary, there should be a plugin that already loaded it
  25. // as string
  26. try {
  27. code = await fs.readFile(file, 'utf-8')
  28. isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`)
  29. } catch (e) {
  30. if (e.code !== 'ENOENT') {
  31. throw e
  32. }
  33. }
  34. if (code) {
  35. map = (
  36. convertSourceMap.fromSource(code) ||
  37. convertSourceMap.fromMapFileSource(code, path.dirname(file))
  38. )?.toObject()
  39. }
  40. } else {
  41. isDebug && debugLoad(`${timeFrom(loadStart)} [plugin] ${prettyUrl}`)
  42. if (typeof loadResult === 'object') {
  43. code = loadResult.code
  44. map = loadResult.map
  45. } else {
  46. code = loadResult
  47. }
  48. }
  49. if (code == null) {
  50. throw new Error(`Failed to load url ${url}. Does the file exist?`)
  51. }
  52. // 将当前处理请求地址添加到维护的图中
  53. const mod = await moduleGraph.ensureEntryFromUrl(url)
  54. // 监听
  55. if (mod.file && !mod.file.startsWith(root + '/')) {
  56. watcher.add(mod.file)
  57. }
  58. // transform
  59. const transformStart = Date.now()
  60. // 所有的插件都被闭包保存了,然后调用pluginContainer上的某个钩子函数,该函数会loop插件进行具体操作
  61. const transformResult = await pluginContainer.transform(code, id, map)
  62. if (
  63. transformResult == null ||
  64. (typeof transformResult === 'object' && transformResult.code == null)
  65. ) {
  66. // no transform applied, keep code as-is
  67. isDebug &&
  68. debugTransform(
  69. timeFrom(transformStart) + chalk.dim(` [skipped] ${prettyUrl}`)
  70. )
  71. } else {
  72. isDebug && debugTransform(`${timeFrom(transformStart)} ${prettyUrl}`)
  73. if (typeof transformResult === 'object') {
  74. code = transformResult.code!
  75. map = transformResult.map
  76. } else {
  77. code = transformResult
  78. }
  79. }
  80. // 返回并缓存当前转换结果
  81. return (mod.transformResult = {
  82. code,
  83. map,
  84. etag: getEtag(code, { weak: true })
  85. } as TransformResult)
  86. }

主要涉及插件提供的三个钩子函数:

  • pluginContainer.resolveId
  • pluginContainer.load
  • pluginContainer.transform

resolveIdload将请求的url解析成对应文件中的内容供transform使用

transform会调用插件提供的transform方法对不同文件代码进行转换操作,比如vite提供的plugin-vue,就对vue进行了转换,提供的plugin-vue-jsx,就对jsx写法进行了支持。如果要支持其他框架语言,也可以自行添加。

到这里,vite的大致流程就结束了。

可能光看代码不是很直观,这边提供一个简单的例子:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>Vite App</title>
  5. </head>
  6. <body>
  7. <div id="app"></div>
  8. <script type="module" src="/src/main.js"></script>
  9. </body>
  10. </html>
  1. // main.js
  2. import { createApp } from 'vue'
  3. import App from './App.vue'
  4. createApp(App).mount('#app')
  1. // app.vue
  2. <template>
  3. <div>hello world</div>
  4. </template>

浏览器中看到的app.vue中的内容是这样的:

除了render相关函数,还有createHotContext、import.meta.hot.accept这类内容,这是和hmr相关的,下面会讲到。

hmr

hmr在我们的开发工程中也提到举足轻重的作用,那vite是怎么做的呢?

涉及部分:

  • client提供hmr上下文环境,其中包含当前文件对应的更新方法,ws通知时会调用

  • importsAnalysisimport模块的时候对模块进行图依赖更新、拼接等操作,比如针对hmr模块注入client中提供的hmr api

  • plugin-vue注入vue上下文环境,并且将client中的方法拼接到当前模块中

当我们import一个模块时,会发送一个请求,当前请求在transformMiddleware中间件处理的时候,当前请求url会被添加到图中,然后被各种插件的transform处理,其中就包括importsAnalysis插件,importsAnalysis会通过es-module-lexer解析import的export,将当前模块插入到模块图中,并且将当前importe和被引入的importedModules建立依赖关系。

  1. // importsAnalysis.ts
  2. if (!isCSSRequest(importer)) {
  3. const prunedImports = await moduleGraph.updateModuleInfo(
  4. importerModule, // 当前解析的主体
  5. importedUrls, // 被引入的文件
  6. normalizedAcceptedUrls,
  7. isSelfAccepting
  8. )
  9. if (hasHMR && prunedImports) {
  10. handlePrunedModules(prunedImports, server)
  11. }
  12. }

并且会为当前请求的文件中加入hmr api。

  1. // importsAnalysis.ts
  2. if (hasHMR) {
  3. // inject hot context
  4. str().prepend(
  5. `import { createHotContext } from "${CLIENT_PUBLIC_PATH}";` +
  6. `import.meta.hot = createHotContext(${JSON.stringify(
  7. importerModule.url
  8. )});`
  9. )
  10. }

除了importsAnalysis插件外,还有plugin-vue插件的transform,插入的是re-render方法。

  1. // /plugin-vue/src/main.ts
  2. if (devServer && !isProduction) {
  3. output.push(`_sfc_main.__hmrId = ${JSON.stringify(descriptor.id)}`)
  4. output.push(
  5. `__VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)`
  6. )
  7. // check if the template is the only thing that changed
  8. if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
  9. output.push(`export const _rerender_only = true`)
  10. }
  11. output.push(
  12. `import.meta.hot.accept(({ default: updated, _rerender_only }) => {`,
  13. ` if (_rerender_only) {`,
  14. ` __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)`,
  15. ` } else {`,
  16. ` __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)`,
  17. ` }`,
  18. `})`
  19. )
  20. }

其中__VUE_HMR_RUNTIME__为vue runtime暴露的,已经在main.js中引入过了,下面的import.meta.hot.accept则是client暴露的方法,import.meta为es module当前模块的元数据。

而client就是浏览器端hmr相关的逻辑了,也是上面插件注入的方法的依赖。

  1. // client.ts
  2. function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) {
  3. // hotModulesMap被闭包保存了
  4. // ownerPath是当importsAnalysis实例化hmr上下文的时候传入的当前模块的id地址
  5. const mod: HotModule = hotModulesMap.get(ownerPath) || {
  6. id: ownerPath,
  7. callbacks: []
  8. }
  9. mod.callbacks.push({
  10. deps,
  11. fn: callback
  12. })
  13. hotModulesMap.set(ownerPath, mod)
  14. }
  15. // 通过importsAnalysis添加在文件中
  16. // plugin-vue插件会使用该方法添加模块(mod),并且会添加一些vue相关的内容,比如:
  17. // 添加vue render方法,以供hmr调用
  18. const hot = {
  19. // 调用的时候给callback增加刷新方法
  20. accept(deps: any, callback?: any) {
  21. if (typeof deps === 'function' || !deps) {
  22. // self-accept: hot.accept(() => {})
  23. acceptDeps([ownerPath], ([mod]) => deps && deps(mod))
  24. } else if (typeof deps === 'string') {
  25. // explicit deps
  26. acceptDeps([deps], ([mod]) => callback && callback(mod))
  27. } else if (Array.isArray(deps)) {
  28. acceptDeps(deps, callback)
  29. } else {
  30. throw new Error(`invalid hot.accept() usage.`)
  31. }
  32. },
  33. // ...
  34. }

我们调用import.meta.hot.accept的时候,比如传入方法,那么会以importer模块为key将更新方法添加到一个hotModulesMap中。记录当前待更新模块。

接下来,ws会在在文件变化后发送message到浏览器端。这一步会涉及判断是否为自更新、(主要是根据accept方法主体内容判断,具体逻辑可自行查看)是否有importer等逻辑决定hmr类型。

我们以hmr类型为js-update为例子继续往下说。

主要是两个方法,一个是fetchUpdate,用来获取即将更新的模块,import模块,返回一个调用re-render的方法,一个是queueUpdate,用于执行fetchUpdate返回的方法。

进入fetchUpdate后,会判断是否更新的是当前模块,是的话添加当前模块到modulesToUpdate,不是的话将依赖的子模块添加到待更新的记录中modulesToUpdate,之后过滤出之前收集的待更新的模块,循环进行import操作,但是会在import模块的路径上加上当前时间戳,以强制触发http请求,用引入的新模块替换之前的旧模块,最后返回plugin-vue提供的re-render方法。

  1. async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  2. // 当前更新的模块
  3. const mod = hotModulesMap.get(path)
  4. if (!mod) {
  5. return
  6. }
  7. const moduleMap = new Map()
  8. // 自更新
  9. const isSelfUpdate = path === acceptedPath
  10. // make sure we only import each dep once
  11. const modulesToUpdate = new Set<string>()
  12. if (isSelfUpdate) {
  13. // self update - only update self
  14. modulesToUpdate.add(path)
  15. } else {
  16. // dep update
  17. for (const { deps } of mod.callbacks) {
  18. deps.forEach((dep) => {
  19. if (acceptedPath === dep) {
  20. modulesToUpdate.add(dep)
  21. }
  22. })
  23. }
  24. }
  25. // determine the qualified callbacks before we re-import the modules
  26. // 符合标准的更新函数才会留下来
  27. const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
  28. return deps.some((dep) => modulesToUpdate.has(dep))
  29. })
  30. // 将modulesToUpdate变成对应模块的更新函数
  31. await Promise.all(
  32. Array.from(modulesToUpdate).map(async (dep) => {
  33. const disposer = disposeMap.get(dep)
  34. if (disposer) await disposer(dataMap.get(dep))
  35. const [path, query] = dep.split(`?`)
  36. try {
  37. // 这里又会发一个请求,然后新的模块就下来了,但是dom树还没变化,下载下来的文件会有id,对应当前即将被更新的模块
  38. const newMod = await import(
  39. /* @vite-ignore */
  40. path + `?t=${timestamp}${query ? `&${query}` : ''}`
  41. )
  42. moduleMap.set(dep, newMod)
  43. } catch (e) {
  44. warnFailedFetch(e, dep)
  45. }
  46. })
  47. )
  48. // 返回函数,函数内容是plugin-vue中的accept注入的,比如vue文件就是vue的render更新方法
  49. // 这里会调用新文件中的render方法,进而在浏览器端进行模块更新操作
  50. return () => {
  51. for (const { deps, fn } of qualifiedCallbacks) {
  52. fn(deps.map((dep) => moduleMap.get(dep)))
  53. }
  54. const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
  55. console.log(`[vite] hot updated: ${loggedPath}`)
  56. }
  57. }

fetchUpdate的结果会流向queueUpdate,queueUpdate将更新任务放到微任务中,自动收集一定时间内的渲染。

  1. async function queueUpdate(p: Promise<(() => void) | undefined>) {
  2. queued.push(p)
  3. if (!pending) {
  4. pending = true
  5. await Promise.resolve()
  6. pending = false
  7. const loading = [...queued]
  8. queued = []
  9. ;(await Promise.all(loading)).forEach((fn) => fn && fn())
  10. }
  11. }

vite简版流程图

总结

vite对es module的使用让人惊艳,一下子解决了大项目build所有内容的痛点,而且与rollup完美集结合,任何rollup插件都可以在vite中使用。

当然,vite的这种思想不是首例,很早之前snowpack利用es module也是名噪一时。

vite目前主要解决的是dev环境的问题,生产环境还是需要build才能使用,vite使用esbuild进行生产环境打包,esbuild使用go开发,原生到原生,感兴趣的朋友可以去看一看,这里就不班门弄斧了。

最后感谢大家的内心阅读,如果觉得不错,可以通过关注,点赞,转发多多支持~

祝大家工作顺利,节节高升

vue-vite浅析的更多相关文章

  1. Vue.nextTick浅析

    Vue.nextTick浅析 Vue的特点之一就是响应式,但数据更新时,DOM并不会立即更新.当我们有一个业务场景,需要在DOM更新之后再执行一段代码时,可以借助nextTick实现.以下是来自官方文 ...

  2. 浅谈Jquery和常用框架Vue变化

    区别 Vue数据与视图的分离 Vue数据驱动视图 Jquery 简单示例: <!DOCTYPE html> <html lang="en"> <hea ...

  3. vue-cli3 vue2 保留 webpack 支持 vite 成功实践

    大家好! 文本是为了提升开发效率及体验实践诞生的. 项目背景: 脚手架:vue-cli3,具体为 "@vue/cli-service": "^3.4.1" 库: ...

  4. Vue 浅析与实践

    欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~ 作者:曾柏羲 导语 入职接到的第一个需求是实现一个关于K歌实体售卖的ERP系统,管理系统过去做过不少,这次打算换个姿势,基于时下正热但早已不新 ...

  5. 浅析Vue.js 中的条件渲染指令

    1 应用于单个元素 Vue.js 中的条件渲染指令可以根据表达式的值,来决定在 DOM 中是渲染还是销毁元素或组件. html: <div id="app"> < ...

  6. vue的双向绑定原理浅析与简单实现

    很久之前看过vue的一些原理,对其中的双向绑定原理也有一定程度上的了解,只是最近才在项目上使用vue,这才决定好好了解下vue的实现原理,因此这里对vue的双向绑定原理进行浅析,并做一个简单的实现. ...

  7. Vue中的nextTick()浅析

    引言 在开发过程中,我们经常遇到这样的问题:我明明已经更新了数据,为什么当我获取某个节点的数据时,却还是更新前的数据? 一,浅析 为什么会这样呢?带着这个疑问先往下看. 先看一个小的例子: <d ...

  8. Vue3: 如何以 Vite 创建,以 Vue Router, Vuex, Ant Design 开始应用

    本文代码: https://github.com/ikuokuo/start-vue3 在线演示: https://ikuokuo.github.io/start-vue3/ Vite 创建 Vue ...

  9. 如何在 Vite 中使用 Element UI + Vue 3

    在上篇文章<2021新年 Vue3.0 + Element UI 尝鲜小记>里,我们尝试使用了 Vue CLI 创建 Vue 3 + Element UI 的项目,而 Vue CLI 实际 ...

  10. Vue3教程:Vue 3 + Element Plus + Vite 2 的后台管理系统开源啦

    之前发布过一篇文章<Vue3教程:开发一个 Vue 3 + element-plus 的后台管理系统>,文中提到会开发并开源一个 Vue 3 + Element Plus 的项目供大家练手 ...

随机推荐

  1. Java设计模式(一)——单例模式

    简介 定义: 确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例. 单例类拥有一个私有构造函数,确保用户无法通过 new 来直接实例化它.类中包含一个静态私有成员变量与静态公有的工厂方法, ...

  2. Angular:使用service进行数据的持久化设置

    ①使用ng g service services/storage创建一个服务组件 ②在app.module.ts 中引入创建的服务 ③利用本地存储实现数据持久化 ④在组件中使用

  3. IDM(Internet Download Manager)—下载各类安装包(github代码、python包)、软件、视频、文档的神器,居家必备良药

    自从有了IDM (Internet Download Manager),不知迅雷.github加速器.镜像为何物.鸟枪换炮,过上了,"他娘的意大利炮"的幸福生活[CoderBaby ...

  4. 如何配置nginx达到只允许域名访问网址,禁止ip访问的效果

    需求:接入阿里云的waf对网站进行防护,但是如果直接通过IP地址访问网站即可绕过阿里云waf,于是希望禁止通过ip访问网站 修改nginx配置文件 在server段里插入如下内容即可 if ($hos ...

  5. 查询id为键的数组

    public static function getKeyValuePairs() { $sql = 'SELECT id, name FROM ' . self::tableName() . ' O ...

  6. 初级程序需要掌握的SQL(一)

    之前我也是,是一个看视频学习的小白,以前老是喜欢通宵看视频,一天10小时小时的学习量,一点效率都没有,就想写一个博客,来帮助大家回顾的SQL语句, 因为我也是初级,所以名字就叫初级程序员需要掌握的sq ...

  7. 探究虚拟dom与diff算法

    一.虚拟DOM (1)什么是虚拟DOM? vdom可以看作是一个使用javascript模拟了DOM结构的树形结构,这个树结构包含整个DOM结构的信息,如下图:   可见左边的DOM结构,不论是标签名 ...

  8. Python写一个对象,让它自己能够迭代

    仿写range()对象,对象是可迭代的: 1 #!usr/bin/env python3 2 # -*- coding=utf-8 -*- 3 4 class myRange(): 5 #初始化,也叫 ...

  9. C#中打印拼接的字符串

    实例化打印文档 //声明打印对象 PrintDocument pd = new PrintDocument(); int ilvPreviewIndex = 0; 在打印事件中设置基本属性 priva ...

  10. python初学者-从小到大排序

    x=input("x=") y=input("y=") z=input("z=") if x>y: x,y=y,x if x>z ...