Vite本地构建:手写核心原理
前言
接上篇文章,我们了解到vite
的本地构建原理主要是:启动一个 connect 服务器拦截由浏览器请求 ESM的请求。通过请求的路径找到目录下对应的文件做一下编译最终以 ESM的格式返回给浏览器。
基于这个核心思想,我们可以尝试来动手实现一下。
搭建静态服务器
基于koa
搭建一个项目:
项目结构如上,服务使用koa
搭建,bin
指定cli可执行文件的位置
#!/usr/bin/env node
// 代表该脚本使用node执行
const koa = require('koa');
const send = require('koa-send');
const App = new koa()
App.listen(3000, () => {
console.log('Server is running at http://localhost:3000');
});
这样一个服务就搭建好了,为了方便调试,我们在该工作目录下执行npm link
,这样可以将该项目链接支全局的npm,相当于全局安装了这个npm包。
接着我们在任意项目下执行my-vite
就能够启动该服务了!
处理根目录html文件
由于上面服务我们没有对任何路由进行处理,当访问http://localhost:3000
会发现什么也没有,我门首先需要将项目的模版文件index.html
返回给浏览器
const root = process.cwd(); // 获取当前工作目录
console.log('当前工作目录:', process.cwd());
// 静态文件服务区
App.use(async (ctx, next) => {
// 处理根路径,返回index.html
await send(ctx, ctx.path, { root: process.cwd() ,index: 'index.html'});
await next();
});
index.html
模版文件如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script>
window.process = { env: { NODE_ENV: 'development' } };
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
就是以ESM
的方式加载了vue
的入口文件main.ts
加完这段代码,我们在vue3
项目下执行一下my-vite
来到浏览器看一下此时的情况:
此时浏览器加载了main.ts
,该文件如下:它通过import
引入了两个模块
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
按理来说,浏览器此时应该会接着发起请求,去获取这两个模块,但现在却并没有
此时控制台有个错误:
意思就是加载模块,必须以相对路径才可以(/、./、../)
所以我们现在需要来处理这些模块的加载路径问题
处理模块加载路径
由于三方模块都是直接以模块名来加载的,所以这里我们需要将这些模块的引用路径转换成相对路径。
// 处理模块导入
const importAction = (content) => {
return content.replace(/(from\s+['"])(?!\.\/)/g, '$1/@modules/')
}
// 修改第三方模块的路径
App.use(async (ctx, next) => {
// console.log('ctx.path', ctx.type, ctx.path);
// 处理ts或者js文件
if (ctx.path.endsWith('.ts') || ctx.path.endsWith('.js')){
const content = await fileToString(ctx.body); // 获取文件内容
ctx.type = 'application/javascript'; // 设置响应类型为js
ctx.body = importAction(content); // 处理import加载路径
}
await next();
});
在这个中间件中,我们使用正则表达式将模块的引用路径替换成了/@modules
开头,这样就符合浏览器的引用规则了。
接着再到浏览器中来观察此时的情况:
此时浏览器已经可以发出另外两个请求,分别去加载vue
模块以及App.vue
组件了。
可以看到vue
模块的加载路径已经变成了/@modules
开头了,虽然现在该路径还是404
,但最起码比起之前我们又往前走一步了。
其实404
也很好理解,因为我们的服务现在压根就还没处理这类路径,所以接下来就该处理/@modules
这类path
并加载模块内容
加载第三方模块
这里我们只需要去拦截刚刚/@modules
开头的路径,并找到该路径下的模块的真正位置,最后返回给浏览器就可以了。
// 加载第三方模块
App.use(async (ctx, next) => {
if (ctx.path.startsWith('/@modules/')) {
const moduleName = ctx.path.substr(10); // 获取模块名称
const modulePath = path.join(root, 'node_modules', moduleName); // 获取模块路径
const package = require(modulePath + '/package.json'); // 获取模块的package.json
// console.log('modulePath', modulePath);
ctx.path = path.join('/node_modules', moduleName, package.module); // 重写路径
}
await next();
});
我们可以通过读取package.json
文件中的module
字段,来找到第三方模块的入口文件。
该中间件需要在处理模块加载路径的中间件之前执行
此时再来到浏览器中查看:
可以看到,此时的vue
模块已经能够重新加载了,但下面又多加载了四个模块,它们又是从哪来的呢?
可以看到vue
模块中又引入了runtime-dom
模块,并且它们的加载路径也被转成了/@modules
开头,这就是上面提到的加载模块的中间件需要在处理模块加载路径的中间件之前执行,模块加载回来之后又经过了处理加载路径的中间件,所以就相当于递归把模块的路径全都转换成相对路径了
runtime-dom
模块又引入了runtime-core
与shared
模块,而runtime-core
模块又引入了reactivity
模块,所以会看到上图中这样的一种加载顺序。
模块的加载引入都正确了,但页面还是没又任何渲染内容出现
这是因为此时的App.vue
还没经过任何编译处理,浏览器并不能直接识别并执行该文件
所以接下来的重点是需要将App.vue
文件编译成浏览器能够执行的javascript
内容(render函数)
处理Vue单文件组件
这里我们需要使用Vue
的编译模块@vue/compiler-sfc
与@vue/compiler-dom
来对vue
文件进行编译处理。
处理script
const content = await fileToString(ctx.body); // 获取文件内容
const { descriptor } = compilerSfc.parse(content); // 解析单文件组件
const compileScript = importAction(
compilerSfc.compileScript(
descriptor,
{
id: descriptor.filename
}
).content); // 编译script
处理template
const compileRender =importAction(compilerDom.compile(descriptor.template.content,
// 编译template, render函数中变量从setup中获取
{ mode: 'module',
sourceMap: true,
filename: path.basename(ctx.path),
__isScriptSetup: true, // 标记是否是setup
compatConfig: { MODE: 3 }, // 兼容vue3
}).code); // 编译template
处理style
let styles = '';
if(descriptor.styles.length){
console.log('descriptor.styles', descriptor.styles);
// 处理样式
styles = descriptor.styles.map((style,index) => {
return `
import '${ctx.path}?type=style&index=${index}';
`
}).join('\n');
} // 处理样式
这里是通过让它另外发起一次请求来对style
进行处理,这样隔离开逻辑能够更清晰
处理样式的请求
在中间件中通过拦截type
为style
的请求来进行处理
if (ctx.query.type === 'style') {
// 处理样式
const styleBlock = descriptor.styles[ctx.query.index];
console.log('styleBlock', styleBlock);
ctx.type = 'application/javascript';
ctx.body = `
const _style = (css) => {
const __style = document.createElement('style');
__style.type = 'text/css';
__style.innerHTML = css;
document.head.appendChild(__style);
}
_style(${JSON.stringify(styleBlock.content)});
export default _style;
`;
}
最后验证
总结
在深入探索了vite
的工作流程之后,你可能会发现,尽管从概念上看似简单,但vite
背后的实现却相当复杂且精妙。我们刚刚通过走一遍其核心流程,对vite
如何加载模块、解析和编译文件有了初步的认识。然而,这仅仅是冰山一角。
总的来说,vite
的工作原理虽然可以通过一个简化的示例来理解,但其真正的强大和复杂性远不止于此。如果对vite
的深入工作原理感兴趣,可以去深入阅读它的源码,在那里我们能够学习到更多知识。
Vite本地构建:手写核心原理的更多相关文章
- 手写Promise原理
我的promise能实现什么? 1:解决回调地狱,实现异步 2:可以链式调用,可以嵌套调用 3:有等待态到成功态的方法,有等待态到失败态的方法 4:可以衍生出周边的方法,如Promise.resolv ...
- 吴裕雄--天生自然python机器学习实战:K-NN算法约会网站好友喜好预测以及手写数字预测分类实验
实验设备与软件环境 硬件环境:内存ddr3 4G及以上的x86架构主机一部 系统环境:windows 软件环境:Anaconda2(64位),python3.5,jupyter 内核版本:window ...
- mnist手写数字识别——深度学习入门项目(tensorflow+keras+Sequential模型)
前言 今天记录一下深度学习的另外一个入门项目——<mnist数据集手写数字识别>,这是一个入门必备的学习案例,主要使用了tensorflow下的keras网络结构的Sequential模型 ...
- 30个类手写Spring核心原理之自定义ORM(上)(6)
本文节选自<Spring 5核心原理> 1 实现思路概述 1.1 从ResultSet说起 说到ResultSet,有Java开发经验的"小伙伴"自然最熟悉不过了,不过 ...
- 手写webpack核心原理,再也不怕面试官问我webpack原理
手写webpack核心原理 目录 手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6 ...
- 30个类手写Spring核心原理之环境准备(1)
本文节选自<Spring 5核心原理> 1 IDEA集成Lombok插件 1.1 安装插件 IntelliJ IDEA是一款非常优秀的集成开发工具,功能强大,而且插件众多.Lombok是开 ...
- 30个类手写Spring核心原理之依赖注入功能(3)
本文节选自<Spring 5核心原理> 在之前的源码分析中我们已经了解到,依赖注入(DI)的入口是getBean()方法,前面的IoC手写部分基本流程已通.先在GPApplicationC ...
- 30个类手写Spring核心原理之AOP代码织入(5)
本文节选自<Spring 5核心原理> 前面我们已经完成了Spring IoC.DI.MVC三大核心模块的功能,并保证了功能可用.接下来要完成Spring的另一个核心模块-AOP,这也是最 ...
- 30个类手写Spring核心原理之动态数据源切换(8)
本文节选自<Spring 5核心原理> 阅读本文之前,请先阅读以下内容: 30个类手写Spring核心原理之自定义ORM(上)(6) 30个类手写Spring核心原理之自定义ORM(下)( ...
- 【面试题】手写async await核心原理,再也不怕面试官问我async await原理
前言 async await 语法是 ES7出现的,是基于ES6的 promise和generator实现的 generator函数 在之前我专门讲个generator的使用与原理实现,大家没了解过的 ...
随机推荐
- liunx查看nginx 进程
ChatGPT4.0国内站点: https://www.weijiwangluo.com/talk 要查看nginx进程,可以使用以下命令: ps -ef | grep nginx 这个命令会列出当前 ...
- autojs拉人进群
/* 微信 version:8.0.1 语言:AutoJs [https://hyb1996.github.io/AutoJs-Docs/#/] @author:奔跑的前端猿 */ auto.wait ...
- 原来Stable Diffusion是这样工作的
stable diffusion是一种潜在扩散模型,可以从文本生成人工智能图像.为什么叫做潜在扩散模型呢?这是因为与在高维图像空间中操作不同,它首先将图像压缩到潜在空间中,然后再进行操作. 在这篇文章 ...
- Javascript高级程序设计第一章 | ch1 | 阅读笔记
什么是JavaScript 历史回顾 JavaScript实现 完整的JavaScript实现包括 核心 ECMAScript -> 语法.类型.关键字.保留字...(规范) 文档对象模型 DO ...
- STM32 CubeMX 学习:06-配置DMA
--- title: mcu-stm32-cube-06-配置DMA date: 2020-05-31 16:39:05 categories: tags: - stm32 - cubeMx - dm ...
- VBA-合并多个工作簿
'合并多个工作薄,并以工作薄的名字给sheet表命名(每个工作薄只有一张表) Sub test() Dim str As String Dim wb As Workbook str = Dir(&qu ...
- python跟踪脚本运行过程(类似bash shell -x)
#详细追踪 python -m trace --trace pyscript.py #显示调用了哪些函数 python -m trace --trackcalls pyscript.py
- ubuntu16.04 python2&3 pip升级后报错:sys.stderr.write(f"ERROR: {exc}")
ubuntu16.04 python2&3 pip升级后报错: sys.stderr.write(f"ERROR: {exc}") 描述 最近使用ubuntu16.04上的 ...
- Pandas中的常用函数
1. map.apply.applymap 参考:Pandas教程 | 数据处理三板斧--map.apply.applymap详解 在日常的数据处理中,经常会对一个DataFrame进行逐行.逐列和逐 ...
- Redis巡检检查 redis-check-aof
一.AOF1.AOF 是什么以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,R ...