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

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

ruisui884个月前 (02-03)技术分析20

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 的预构建原理与实践| 京东物流技术团队” 的相关文章

高校水电远程抄表收费管理系统都有哪些技术优势?

学校后勤是一个庞大的管理体系,学生宿舍用电管理是其中重要的一个环节,宿舍内漏电、超负荷用电、拖欠电费和浪费电现象一直是困扰学校后勤管理的普遍问题。而其中,学生宿舍安全用电更是学校后期管理的重中之重。为加强对学生宿舍用电管理,保障学生的财产及生命安全,现建设一套用电的控制系统。亿玛推出的高校水电远程抄...

Lindroid开源应用:在安卓手机 / 平板上安装 Linux发行版

IT之家 6 月 19 日消息,Erfan Abdi 本月发布了 Lindroid 开源应用程序,让用户可以在安卓手机上安装 GNU / Linux 发行版,在完全支持手机硬件的情况下可以运行 Linux 应用程序。Lindroid 开源应用程序就是将 Linux 放入容器中,使用 Halium 等...

代码管理-9-gitlab的使用和设置

gitlab使用1、外观设置完成后保存,返回登录页面查看关于注册,有些公司是不允许打开的,,有些人数非常多的公司就需要打开注册的功能,让人员自己注册,我们来给他特定的权限就可以,毕竟人非常多的时候还由我们来给她们注册就非常不现实了,工作量会很大2、自动注册3、组&用户&项目创建组设置组名称、描述等创...

快速掌握 Git:程序员必会的版本控制技巧

在现代软件开发中,版本控制系统(VCS)是开发人员不可或缺的工具。无论是个人项目,还是多人协作的团队开发,良好的版本控制都能确保代码管理的高效性与稳定性。而在版本控制系统中,Git 凭借其分布式、灵活性和高效性,成为了最流行的工具之一。几乎所有的开发团队都在使用 Git 来管理代码版本、协作开发和追...

内存问题探微

这篇文章是我在公司 TechDay 上分享的内容的文字实录版,本来不想写这么一篇冗长的文章,因为有不少的同学问是否能写一篇相关的文字版,本来没有的也就有了。说起来这是我第二次在 TechDay 上做的分享,四年前第一届 TechDay 不知天高地厚,上去讲了一个《MySQL 最佳实践》,现在想起来那...

vue 异步更新那点事儿 #web前端

异步更新那点事儿。wue & vueuse官方团队成员。看一下群友投稿的问题。什么问题?就是它这边有一个组件,这个组件里面有个userID,然后这个userID通过props传给了子组件,子组件是userinfo,它里面是用来渲染用户信息的。渲染用户信息的同时,userinfo这个组件又暴露...