前言

接上篇文章,我们了解到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-coreshared模块,而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进行处理,这样隔离开逻辑能够更清晰

处理样式的请求

在中间件中通过拦截typestyle的请求来进行处理

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本地构建:手写核心原理的更多相关文章

  1. 手写Promise原理

    我的promise能实现什么? 1:解决回调地狱,实现异步 2:可以链式调用,可以嵌套调用 3:有等待态到成功态的方法,有等待态到失败态的方法 4:可以衍生出周边的方法,如Promise.resolv ...

  2. 吴裕雄--天生自然python机器学习实战:K-NN算法约会网站好友喜好预测以及手写数字预测分类实验

    实验设备与软件环境 硬件环境:内存ddr3 4G及以上的x86架构主机一部 系统环境:windows 软件环境:Anaconda2(64位),python3.5,jupyter 内核版本:window ...

  3. mnist手写数字识别——深度学习入门项目(tensorflow+keras+Sequential模型)

    前言 今天记录一下深度学习的另外一个入门项目——<mnist数据集手写数字识别>,这是一个入门必备的学习案例,主要使用了tensorflow下的keras网络结构的Sequential模型 ...

  4. 30个类手写Spring核心原理之自定义ORM(上)(6)

    本文节选自<Spring 5核心原理> 1 实现思路概述 1.1 从ResultSet说起 说到ResultSet,有Java开发经验的"小伙伴"自然最熟悉不过了,不过 ...

  5. 手写webpack核心原理,再也不怕面试官问我webpack原理

    手写webpack核心原理 目录 手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6 ...

  6. 30个类手写Spring核心原理之环境准备(1)

    本文节选自<Spring 5核心原理> 1 IDEA集成Lombok插件 1.1 安装插件 IntelliJ IDEA是一款非常优秀的集成开发工具,功能强大,而且插件众多.Lombok是开 ...

  7. 30个类手写Spring核心原理之依赖注入功能(3)

    本文节选自<Spring 5核心原理> 在之前的源码分析中我们已经了解到,依赖注入(DI)的入口是getBean()方法,前面的IoC手写部分基本流程已通.先在GPApplicationC ...

  8. 30个类手写Spring核心原理之AOP代码织入(5)

    本文节选自<Spring 5核心原理> 前面我们已经完成了Spring IoC.DI.MVC三大核心模块的功能,并保证了功能可用.接下来要完成Spring的另一个核心模块-AOP,这也是最 ...

  9. 30个类手写Spring核心原理之动态数据源切换(8)

    本文节选自<Spring 5核心原理> 阅读本文之前,请先阅读以下内容: 30个类手写Spring核心原理之自定义ORM(上)(6) 30个类手写Spring核心原理之自定义ORM(下)( ...

  10. 【面试题】手写async await核心原理,再也不怕面试官问我async await原理

    前言 async await 语法是 ES7出现的,是基于ES6的 promise和generator实现的 generator函数 在之前我专门讲个generator的使用与原理实现,大家没了解过的 ...

随机推荐

  1. 8.13考试总结(NOIP模拟38)[a·b·c]

    重要的不是你做了多少事,而是你放了多少心思进去. T1 a 解题思路 总结一下,是双指针运用不够熟练(zxb笑了笑). 其实这个题是可以用树状数组卡过的(众所周知我是一个正直的人),但是一定是要打正解 ...

  2. 面试官:说说Netty对象池的实现原理?

    Netty 作为一个高性能的网络通讯框架,它内置了很多恰夺天工的设计,目的都是为了将网络通讯的性能做到极致,其中「对象池技术」也是实现这一目标的重要技术. 1.什么是对象池技术? 对象池技术是一种重用 ...

  3. windows隐藏文件如何查看

    1.组织 2.查看 3.显示隐藏文件

  4. 未来5年,只有这种产品团队才能开启上帝视角【玩转IPD】

    一家企业如何在波涛汹涌的市场浪潮中站稳脚跟?一个团队如何快速识别风险发现机遇,成为行业的标杆?市场瞬息万变,如何准确地响应市场动向,紧跟用户需求?这些问题,已成为企业发展乃至生存的重要保障.尽管市场和 ...

  5. kettle从入门到精通 第二十四课 kettle 部署生产常用命令

    一.设置KETTLE_HOME环境变量 假设kettle软件目录为/xxx/data-integration vi ~/.bash_profile export KETTLE_HOME=/xxx/da ...

  6. babel 基础概念 & 从零到一写一个 babel 插件

    babel 基础概念 简单来说,做语法转换兼容的, 复杂一点的说,babel可以将我们写的 ES6+ 的Javascript语法转换为向后兼容的语法,以便能够在旧版本的浏览器或者其他环境运行. bab ...

  7. Linux使用docker搭建maven私有仓库

    引言 在实际开发工作中,通常需要搭建maven私有仓库,今天就教大家如何搭建一套maven的私有仓库 Nexus介绍 Nexus 是Maven仓库管理器,如果你使用Maven,你可以从Maven中央仓 ...

  8. 18.9k star!一个高性能的嵌入式分析型数据库,主要用于数据分析和数据处理任务。

    大家好,今天给大家分享的是一个开源的面向列的关系数据库管理系统(RDBMS). DuckDB是一个嵌入式的分析型数据库,它提供了高性能的数据分析和数据处理能力.DuckDB的设计目标是为数据科学家.分 ...

  9. .Net Core5.0中Autofac依赖注入整合多层,项目中可直接用

    一.配置Autofac替换内置DI 1.安装Nuget包:Autofac,Autofac.Extensions.DependencyInjection 2.Program.cs中CreateHostB ...

  10. jqurey基础知识和常用事件方法

    样式文件不需要<style>标签 引用style文件的方法 <link href="main.css" rel="stylesheet" st ...