写点什么

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

  • 2024-05-17
    北京
  • 本文字数:4119 字

    阅读完需:约 14 分钟

Vite 预构建的核心原理

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

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

2. 自动依赖搜寻

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

2. 工作过程

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


<script type="module" src="/src/main.js"></script>
复制代码


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


// main.jsimport { createApp } from 'vue'import App from './App.vue'createApp(App).mount('#app')
复制代码


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


Vite其核心原理是利用浏览器现在已经支持ES6import,碰见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 文件进行丰富


<template>  <div>    {{ title }}  </div></template><script>import { reactive } from "@vue/composition-api";export default {  setup() {    const state = reactive({      title: "hello,kvite!!!",    });  },};</script>
复制代码

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')})
复制代码


作者:京东物流 段欣欣


来源:京东云开发者社区

发布于: 刚刚阅读数: 5
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Vite 的预构建原理与实践| 京东物流技术团队_京东科技开发者_InfoQ写作社区