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

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

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

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

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

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

「图解」父子组件通过 props 进行数据交互的方法

1.组件化开发,经常有这样的一个场景,就是父组件通过 Ajax 获取数据,传递给子组件,如何通过 props 进行数据交互来实现,便是本图解的重点。2.代码的结构3.具体代码 ①在父组件 data 中存放数据 ms。 ②将父组件 data 中的数据 ms 绑定到子组件中的属性 ms。 ③子组件在 p...

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

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

内存问题探微

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

博信股份新战略后再推新品 TOPPERS E2耳机售价199元

中新网6月21日电 20日,博信股份在北京正式推出新品TOPPERS主动降噪耳机E2,这是博信股份继2月战略暨新品发布会后的第二次新品亮相。价格方面,TOPPERS主动降噪耳机E2零售价199元,并于6月20日下午4点在京东商城公开销售。据介绍,TOPPERS主动降噪耳机E2采用AMS(奥地利微电子...

VUE-router

七.Vue-router1、什么是vue-routervue-router是vue.js官方路由管理器。vue的单页应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统页面切换是用超链接a标签进行切换。但vue里是用路由,因为我们用Vue做的都是单页应用,就相当于只有一个主的i...