前言

接上篇文章,我们了解到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. 在 Inno Setup iss 打包过程 中检测 .NET 6 / .net 5 / .NET Core 运行环境是否存在或已安装

    为了将 .NET 5 / .NET Core 应用程序部署到客户机,我们可以编写 Inno Setup 代码来判断客户机是否安装了必要的运行环境..NET 官方仓库 中提供了一个名为 NetCoreC ...

  2. .net core 转 excel datatable list<t> 互转 xlsx

    using System; using System.Collections; using System.Collections.Generic; using System.ComponentMode ...

  3. 逆向WeChat(四)

    本篇在博客园地址https://www.cnblogs.com/bbqzsl/p/18209439 mars 先回顾一下,在上两篇我对wechat如何使用chrome::base框架的分析中存有错漏. ...

  4. 关于 ajax在前端提示SyntaxError: Unexpected end of JSON input

    前几日,在开发微信公众号上的网页时候,前端采用h5+jquery开发,后端采用ASP.net的ashx接收前端的参数,restful采用的是java开发,由于在ASP.ENT的 webconfig中增 ...

  5. vue自定义指令 - directive

    https://cn.vuejs.org/v2/guide/custom-directive.html 除了核心功能默认内置的指令,Vue也允许注册自定义指令.有的情况下,对普通 DOM 元素进行底层 ...

  6. Win11系统下的MindSpore环境搭建

    技术背景 笔者尝试过不少编程环境搭建的方案,例如常见的Ubuntu.Deepin.CentOS,也用过很多人力荐的Manjaro,这些发行版在需要办公的条件下,一般都需要结合Windows双系统使用. ...

  7. 阻塞外挂 TCP 端口 让外挂服务器增加无用处理 反攻击 是4个IP 苹果 安卓 pc 域名

    using namespace std;#include<stdlib.h>#pragma comment(lib,"WS2_32.lib") #include < ...

  8. C#.NET FRAMEWORK ASP.NET MVC 获取客户端IP

    C#.NET FRAMEWORK ASP.NET MVC 获取客户端IP 工具类: using System; namespace CommonUtils { public static class ...

  9. 判断一个数n是不是快乐数

    引言 题目:编写一个算法来判断一个数n是不是快乐数 来源:网友分享的面试算法题 题目描述 [快乐数定义] 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和. 然后重复这个过程直到这个数变为 ...

  10. ISO pod 使用

    pod 安装 相关依赖包 新建podfile 文件 pod init 编辑podfile文件添加第三方库 // pod '第三方依赖库名', '版本号' pod 'SDWebImageSwiftUI' ...