当前位置:首页 > 技术分析 > 正文内容

Vite 的预构建原理与实践| 京东物流技术团队

ruisui883个月前 (02-03)技术分析12

Vite 预构建的核心原理

1. 兼容性与性能的双重目标

Vite 的预构建旨在解决两个主要问题:兼容性性能。对于兼容性,由于 Vite 在开发阶段将所有代码视为原生 ES 模块,因此需要将 CommonJS 或 UMD 格式的依赖转换为 ESM 格式。对于性能,Vite 通过预构建将多个内部模块的 ESM 依赖关系转换为单个模块,减少了网络请求的数量,从而提高了页面加载速度。

2. 自动依赖搜寻

Vite 通过扫描项目源码自动寻找引入的依赖项,并将这些依赖项作为预构建包的入口点。这一过程通过 esbuild 执行,因此非常快速。如果在服务器启动后遇到新的依赖关系导入,Vite 将重新运行依赖构建进程并重新加载页面。

2. 工作过程

当声明一个script标签类型为module时,如



1.当浏览器解析资源时,会往当前域名发起一个GET请求main.js文件

// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

1.请求到了main.js文件,会检测到内部含有import引入的包,又会import引用发起HTTP请求获取模块的内容文件,如App.vue、vue文件

Vite其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件,Vite启动一个koa服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多。

预构建的实现细节

1.依赖预构建的触发

当首次启动 Vite 开发服务器时,Vite 会检查是否存在预构建的依赖。如果没有找到相应的缓存,Vite 将抓取源码并自动寻找引入的依赖项。这个过程是通过 Vite 的内部插件 esbuildScanPlugin 实现的,它会遍历所有的入口文件,解析出依赖列表,并进行预构建。

2.预构建过程

预构建过程是通过 Vite 的 optimizeDeps 函数触发的。该函数首先会检查是否存在一个名为 _metadata.json 的文件,该文件记录了预构建模块的信息。如果文件存在且哈希值与当前依赖的哈希值一致,Vite 将跳过预构建过程。如果哈希值不一致或文件不存在,Vite 将执行预构建,并更新 _metadata.json 文件。

3.缓存策略

Vite 的预构建依赖会缓存在 node_modules/.vite 目录下。这个目录中的文件会根据 package.json、lockfile 以及 vite.config.js 中的配置来决定是否需要重新构建。这种缓存策略大大减少了重复构建的开销,提高了开发效率。


??

模拟实践

vite会拦截import,对于相对地址的文件,浏览器可以直接加载,但是对于像import { createApp } from 'vue'这种加载一个裸模块,vite就会通过一次预打包,将第三方模块放在node_modules/.vite,然后将裸模块地址替换成相对地址。以及加载的是vue文件浏览器无法解析,vite也是需要将vue文件转化成js文件。

所以我们第一步创建一个服务器,将裸模块替换相对地址让浏览器可以加载文件,第二步解析vue成js文件,让浏览器可以识别

1、js加载和裸模块路径重写

直接加载vue会浏览器会报错


??


??

对裸模块路径重写

const Koa = require('koa')
const fs=require('fs')
const path=require('path')

const app=new Koa();
app.use(async (ctx)=>{
    const {url}=ctx.request;
    if(url==='/'){
        //返回主页
        ctx.type='text/html'
        ctx.body=fs.readFileSync('./index.html','utf-8')
    }else if(url.endsWith('.js')){
        // js文件加载路径处理
        const p=path.join(__dirname,url);
        ctx.type='application/javascript'
        ctx.body=rewriteImport(fs.readFileSync(p,'utf-8'))
    }
})
//裸模块路径重写
//将import xxx from './xx' 替换成 import xxx from '/@moudle/xxx'
//将裸模块进行替换和重写,官方的处理方式是先使用esbuild打包后缓存在node_modules中
function rewriteImport(content){
    return content.replace(/ from ['"](.*)['"]/g,function(s1,s2){
        if(s2.startsWith('./')||s2.startsWith('/')||s2.startsWith('../')){
            return s1
        }else{
            //裸模块,需要替换
            return ` from '/@moudles/${s2}'`
        }
    })
}
app.listen(3000,()=>{
    console.log('kvite start')
})

重写后


??

但是又有新的问题,裸模块无法加载


??


2、对裸模块加载进行处理

app.use(async (ctx)=>{
    ...
    else if(url.startsWith('/@moudles/')){
        const moudleName=url.replace('/@moudles/','');
        // node_moudle中找
        const prefix=path.join(__dirname,'../node_modules',moudleName)
        //package中匹配
        const moudle=require(prefix+'/package.json').moudle
        const filePath=path.join(prefix,moudle)
        const ret=fs.readFileSync(filePath,'utf-8');
        ctx.type='application/javascript'
        ctx.body=rewriteImport(ret)
    }
    ...
})

处理后可以加载vue模块了


??

对main.js文件进行丰富




3、开始解析SFC

app.use(async (ctx)=>{
    ...
    else if(url.indexOf('.vue')>-1){
        const p=path.join(__dirname,url.split('?')[0])
        const ast=compilerSFC.parse(fs.readFileSync(p,'utf-8'))
        if(!query.type){
            //SFC请求
            //读取vue文件,解析为js文件
            //获取脚本内容
            const scriptContent=ast.descriptor.script.content;
            const script=scriptContent.replace('export defalut ','const __script=')
            ctx.type='application/javascript'
            ctx.body=`
            ${rewriteImport(script)}
            //解析tpl
            import {render as __render} from '${url}?type=template'
            __sciprt.render=__render
            export defalut __sctipt
            `
        }else if(query.type==='template'){
            const tpl=ast.descriptor.template.content;
            const render=compilerDOM.compiler(tpl,{mode:module}).code
            ctx.type='application/javascript'
            ctx.body=rewriteImport(render)
        }
    }
})

成功输出


??

完整代码

const Koa = require('koa')
const fs=require('fs')
const path=require('path')
const compilerSFC =require('vue/compiler-sfc')
const compilerDOM=require('vue/compiler-dom')

const app=new Koa();

app.use(async (ctx)=>{
    const {url}=ctx.request;
    if(url==='/'){
        ctx.type='text/html'
        ctx.body=fs.readFileSync('./index.html','utf-8')
    }else if(url.endsWith('.js')){
        // js文件加载路径处理
        const __filenameNew = fileURLToPath(import.meta.url)
        const __dirnameNew = path.dirname(__filenameNew)
        const p=path.join(__dirnameNew,url);
        ctx.type='application/javascript'
        // ctx.body=fs.readFileSync(p,'utf-8')
        ctx.body=rewriteImport(fs.readFileSync(p,'utf-8'))
    }else if(url.startsWith('/@moudles/')){
        const moudleName=url.replace('/@moudles/','');
        // node_moudle中找
        const __filenameNew = fileURLToPath(import.meta.url)
        const __dirnameNew = path.dirname(__filenameNew)
        const prefix=path.join(__dirnameNew,'../node_modules',moudleName)
        //package中匹配
        const moudle=require(prefix+'/package.json').moudle
        const filePath=path.join(prefix,moudle)
        const ret=fs.readFileSync(filePath,'utf-8');
        ctx.type='application/javascript'
        ctx.body=rewriteImport(ret)
    }else if(url.indexOf('.vue')>-1){
        const p=path.join(__dirname,url.split('?')[0])
        const ast=compilerSFC.parse(fs.readFileSync(p,'utf-8'))
        if(!query.type){
            //SFC请求
            //读取vue文件,解析为js文件
            //获取脚本内容
            const scriptContent=ast.descriptor.script.content;
            const script=scriptContent.replace('export defalut ','const __script=')
            ctx.type='application/javascript'
            ctx.body=`
            ${rewriteImport(script)}
            //解析tpl
            import {render as __render} from '${url}?type=template'
            __sciprt.render=__render
            export defalut __sctipt
            `
        }else if(query.type==='template'){
            const tpl=ast.descriptor.template.content;
            const render=compilerDOM.compiler(tpl,{mode:module}).code
            ctx.type='application/javascript'
            ctx.body=rewriteImport(render)
        }
    }
})

//裸模块重写
//将import xxx from './xx' 替换成 import xxx from '/@moudle/xxx'
//将裸模块进行替换和重写,官方的处理方式是先使用esbuild打包依赖在地址上
function rewriteImport(content){
    return content.replace(/ from ['"](.*)['"]/g,function(s1,s2){
        if(s2.startsWith('./')||s2.startsWith('/')||s2.startsWith('../')){
            return s1
        }else{
            //裸模块,需要替换
            return ` from '/@moudles/${s2}'`
        }
    })
}

app.listen(3000,()=>{
    console.log('dvite start')
})


?作者:京东物流 段欣欣

来源:京东云开发者社区

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/825.html

标签: vite.js
分享给朋友:

“Vite 的预构建原理与实践| 京东物流技术团队” 的相关文章

10分钟搞定gitlab-ci自动化部署

gitlab-ci 是持续集成工具/自动化部署工具,类似 jenkins。持续集成 是将代码集成到共享存储库并尽可能早地自动构建/测试每个更改的实践 - 通常一天几次。概述在编码完成时都会进行打包发布过程,如果每次都手动操作这一步骤就会浪费时间,效率低下。所以就有了持续集成。准备事项请提前安装以下软...

K8s里我的容器到底用了多少内存?

作者:frostchen导语 Linux下开发者习惯在物理机或者虚拟机环境下使用top和free等命令查看机器和进程的内存使用量,近年来越来越多的应用服务完成了微服务容器化改造,过去查看、监控和定位内存使用量的方法似乎时常不太奏效。如果你的应用程序刚刚迁移到K8s中,经常被诸如以下问题所困扰:容器的...

「干货」FPGA设计中深度约束技巧及调试经验总结

今天跟大家分享的内容很重要,也是我们调试FPGA经验的总结。随着FPGA对时序和性能的要求越来越高,高频率、大位宽的设计越来越多。在调试这些FPGA样机时,需要从写代码时就要小心谨慎,否则写出来的代码可能无法满足时序要求。另外,最近跟网友聊天时,有谈到公众号寿命的问题,我觉得网络交换FPGA公众号应...

《暗黑破坏神 2:重制版》PC 版 2.3 版本发布,支持英伟达 DLSS

IT之家 12 月 3 日消息,暴雪为《暗黑破坏神 2:重制版》PC 版发布了更新 2.3 版本,添加了“离线难度缩放”滑块(玩家可以在单人游戏时增加挑战和奖励的级别)、多项辅助功能和用户界面改进,以及英伟达 DLSS 支持。玩法改进:玩家现在可以在离线游戏的选项菜单中使用“游戏难度等级”,它提供与...

USB电池充电基础:应急指南

USB为便携设备供电与其串行通信功能一样,已经成为一种标准应用。如今,USB 供电已经扩展到电池充电、交流适配器及其它供电形式的应用。应用的普及带来的一个显著效果是便携设备的充电和供电可以互换插头和适配器。因此,相对于过去每种装置都采用专用适配器的架构相比,目前的解决方案允许采用多种电源进行充电。毋...

22《Vue 入门教程》VueRouter 路由嵌套

1. 前言本小节我们介绍如何嵌套使用 VueRouter。嵌套路由在日常的开发中非常常见,如何定义和使用嵌套路由是本节的重点。同学们在学完本节课程之后需要自己多尝试配置路由。2. 配置嵌套路由实际项目中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层...