预备知识
es modules
vite通过新版本浏览器支持的es modules来加载依赖
你需要把 type="module" 放到 script标签中, 来声明这个脚本是一个模块
<script type="module">
// index.js可以通过export导出模块,也可以在其中继续使用import加载其他依赖
import App from './index.js'
</script>
遇到import时,会自动发送一个http请求,来获取对应模块的内容,相应类型content-type=text/javascript
基本架构
vite原理
首先我们创建一下vite项目跑一下
yarn create vite my-react-app --template react
yarn dev
可以看到:
浏览器发出了一个请求,请求了main.jsx
查看main.jsx的内容,我们可以发现,vite启动的·服务器对引入模块的路径进行了处理,对jsx写法也进行了处理,转化成了浏览器可以运行的代码
继续看
在client中,我们看到了websocket的代码,所以可以理解为vite服务器注入客户端的websocket代码,用来获取服务器中代码的变化的通知,从而达到热更新的效果
综上,我们知道了vite服务器做的几件事:
- 读取本地代码文件
- 解析引入模块的路径并重写
- websocket代码注入客户端
代码实现
本文的完整代码在:github.com/yklydxtt/vi…
这里我们分五步:
- 创建服务
- 读取本地静态资源
- 并重写模块路径
- 解析模块路径
- 处理css文件
- websocket代码注入客户端
1.创建服务
创建index.js
// index.js
const Koa = require('koa');
const serveStaticPlugin = require('./plugins/server/serveStaticPlugin');
const rewriteModulePlugin=require('./plugins/server/rewriteModulePlugin');
const moduleResolvePlugin=require('./plugins/server/moduleResolvePlugin');
function createServer() {
const app = new Koa();
const root = process.cwd();
const context = {
app,
root
}
const resolvePlugins = [
// 重写模块路径
rewriteModulePlugin,
// 解析模块内容
moduleResolvePlugin,
// 配置静态资源服务
serveStaticPlugin,
]
resolvePlugins.forEach(f => f(context));
return app;
}
module.exports = createServer;
createServer().listen(3001);
这里我们使用koa创建了一个服务,
还注册了三个插件,分别用来配置静态资源,解析模块内容,重写模块里import其他模块路径
我们来分别实现这三个插件的功能
2.配置静态资源,读取本地代码
const KoaStatic = require('koa-static');
const path = require('path');
module.exports = function(context) {
const { app, root } = context;
app.use(KoaStatic(root));
app.use(KoaStatic(path.join(root,'static')));
}
我们创建一个static目录
我们用koa-static代理static目录下的静态资源
index.html中的内容如下:
执行
node index.js
访问loaclhost:3001
可以到我们刚刚写的index.html的内容
3.重写模块路径
我们来实现rewriteModulePlugin.js,作用是重写import后的路径
把这样的路径
import React,{ReactDOM } from 'es-react'
改为
import React,{ReactDOM } from '/__module/es-react'
// plugins/server/rewriteModulePlugin.js
const {readBody,rewriteImports}=require('./utils');
module.exports=function({app,root}){
app.use(async (ctx,next)=>{
await next();
if (ctx.url === '/index.html') {
// 修改script标签中的路径
const html = await readBody(ctx.body)
ctx.body = html.replace(
/(<script\b[^>]*>)([\s\S]*?)<\/script>/gm,
(_, openTag, script) => {
return `${openTag}${rewriteImports(script)}</script>`
}
)
}
if(ctx.body&&ctx.response.is('js')){
// 修改js中的路径
const content=await readBody(ctx.body);
ctx.body=rewriteImports(content,ctx.path);
}
});
}
实现一下rewriteImports函数和readBody
const path = require('path');
const { parse } = require('es-module-lexer');
const {Readable} =require('stream');
const resolve=require('resolve-from');
const MagicString = require('magic-string');
async function readBody(stream){
if(stream instanceof Readable){
return new Promise((resolve,reject)=>{
let res='';
stream.on('data',(data)=>res+=data);
stream.on('end',()=>resolve(res));
stream.on('error',(e)=>reject(e));
})
}else{
return stream.toString();
}
}
function rewriteImports(source,modulePath){
const imports=parse(source)[0];
const magicString=new MagicString(source);
imports.forEach(item=>{
const {s,e}=item;
let id = source.substring(s,e);
const reg = /^[^\/\.]/;
const moduleReg=/^\/__module\//;
if(moduleReg.test(modulePath)){
// 如果有/__module/前缀,就不用加了
// 处理node_modules包中的js
if(modulePath.endsWith('.js')){
id=`${path.dirname(modulePath)}/${id}`
}else{
id=`${modulePath}/${id}`;
}
magicString.overwrite(s,e,id);
return;
}
if(reg.test(id)){
// 对于前面没有/__module/前缀的node_modules模块的import,加上前缀
id=`/__module/${id}`;
magicString.overwrite(s,e,id);
}
});
return magicString.toString();
}
4.读取node_modules模块内容
我们来实现moduleResolvePlugin
因为我们只代理了static目录下的文件,所以需要读取node_modules的文件,就需要处理一下
主要功能是解析到/__module前缀,就去node_modules读取模块内容
// ./plugins/server/moduleResolvePlugin.js
const { createReadStream } = require('fs');
const { Readable } = require('stream');
const { rewriteImports, resolveModule } = require('./utils');
module.exports = function ({ app, root }) {
app.use(async (ctx, next) => {
// koa的洋葱模型
await next();
// 读取node_modules中的文件内容
const moduleReg = /^\/__module\//;
if (moduleReg.test(ctx.path)) {
const id = ctx.path.replace(moduleReg, '');
ctx.type = 'js';
const modulePath = resolveModule(root, id);
if (id.endsWith('.js')) {
ctx.body = createReadStream(modulePath);
return;
} else {
ctx.body = createReadStream(modulePath);
return;
}
}
});
}
获取node模块的路径:
// ./plugins/server/utils.js
const path = require('path');
const { parse } = require('es-module-lexer');
const {Readable} =require('stream');
const resolve=require('resolve-from'); // 这个包的功能类似require,返回值是require的路径
const MagicString = require('magic-string');
// 返回node_modules依赖的绝对路径
function resolveModule(root,moduleName){
let modulePath;
if(moduleName.endsWith('.js')){
modulePath=path.join(path.dirname(resolve(root,moduleName)),path.basename(moduleName));
return modulePath;
}
const userModulePkg=resolve(root,`${moduleName}/package.json`);
modulePath=path.join(path.dirname(userModulePkg),'index.js');
return modulePath;
}
至此,基本功能完成
在static下添加代码:
// static/add.js
// 因为react没有esm格式的包,所以这里用es-react代替react
import React,{ReactDOM } from 'es-react'
import LikeButton from './like_button.js';
const e = React.createElement;
const domContainer=document.getElementById("like_button_container");
ReactDOM.render(e(LikeButton), domContainer);
export default function add(a, b) {
return a + b;
}
// static/like_button.js
import React from 'es-react'
const e = React.createElement;
export default class LikeButton extends React.Component {
constructor(props) {
super(props);
this.state = { liked: false };
}
render() {
if (this.state.liked) {
return 'You liked this.';
}
// 因为没有用babel解析,所以这里没有用jsx,使用createElement的写法
return e(
'button',
{ onClick: () => this.setState({ liked: true }) },
'Like'
);
}
}
试着执行
node index.js
看到如下页面
5.处理css文件
添加一个like_button.css
// ./static.like_button.css
h1{
color: #ff0
}
在like_button.js中引入
// like_button.js
import './like_button.css';
刷新页面会看到这样的报错:
es modules并不支持css,所以需要将css文件转为js.或者转为在link标签中引入
在rewriteModulePlugin.js中添加处理css的判断
const {readBody,rewriteImports}=require('./utils');
module.exports=function({app,root}){
app.use(async (ctx,next)=>{
await next();
if (ctx.url === '/index.html') {
const html = await readBody(ctx.body)
ctx.body = html.replace(
/(<script\b[^>]*>)([\s\S]*?)<\/script>/gm,
(_, openTag, script) => {
return `${openTag}${rewriteImports(script)}</script>`
}
)
}
if(ctx.body&&ctx.response.is('js')){
const content=await readBody(ctx.body);
ctx.body=rewriteImports(content,ctx.path);
}
// 处理css
if(ctx.type==='text/css'){
ctx.type='js';
const code=await readBody(ctx.body);
ctx.body=`
const style=document.createElement('style');
style.type='text/css';
style.innerHTML=${JSON.stringify(code)};
document.head.appendChild(style)
`
}
});
}
重新启动服务
样式就有了
like_button.css的请求body变成了如下的样子
6.实现热更新
热更新借助websocket来实现
客户端代码
// ./plugins/client/hrmClient.js
const socket = new WebSocket(`ws://${location.host}`)
socket.addEventListener('message',({data})=>{
const {type}=JSON.parse(data);
switch(type){
case 'update':
location.reload();
break;
}
})
服务端添加一个中间件hmrWatcherPlugin.js
作用是将hrmClient.js的内容发送给客户端,并监听代码的变化,如果有变化,就通过ws发消息给客户端
// ./plugins/server/hmrWatcherPlugin.js
const fs = require('fs');
const path = require('path');
const chokidar =require('chokidar');
module.exports = function ({ app,root }) {
const hmrClientCode = fs.readFileSync(path.resolve(__dirname, '../client/hmrClient.js'))
app.use(async (ctx, next) => {
await next();
将hrmClient.js的内容发送给客户端
if (ctx.url === '/__hmrClient') {
ctx.type = 'js';
ctx.body = hmrClientCode;
}
if(ctx.ws){
// 监听本地代码的变化
const ws=await ctx.ws();
const watcher = chokidar.watch(root, {
ignored: [/node_modules/]
});
watcher.on('change',async ()=>{
ws.send(JSON.stringify({ type: 'update' }));
})
}
})
}
对rewriteModulePlugin.js中对index.html的处理进行修改
// plugins/server/rewriteModulePlugin.js
...
app.use(async (ctx,next)=>{
await next();
if (ctx.url === '/') {
const html = await readBody(ctx.body);
ctx.body = html.replace(
/(<script\b[^>]*>)([\s\S]*?)<\/script>/gm,
(_, openTag, script) => {
// 添加对websock代码的请求
return `${openTag}import "/__hmrClient"\n${rewriteImports(script)}</script>`
}
)
}
...
添加完成后重启服务
对like_button.js进行修改,给button加一个感叹号,保存
...
return e(
'button',
{ onClick: () => this.setState({ liked: true }) },
'Like!'
);
...
可以看到页面有了更新,感叹号有了
7.对jsx代码处理
vite中是通过esbuild来处理的
在对rewriteImports进行改造,使用esbuild把jsx转化成React.createElement的形式
// plugins/server/utils.js
function rewriteImports(source,modulePath){
// ...
const code=esbuild.transformSync(source, {
loader: 'jsx',
}).code;
const imports=parse(code)[0];
const magicString=new MagicString(code);
imports.forEach(item=>{
const {s,e}=item;
let id = code.substring(s,e);
const reg = /^[^\/\.]/;
const moduleReg=/^\/__module\//;
if(moduleReg.test(modulePath)){
if(modulePath.endsWith('.js')){
id=`${path.dirname(modulePath)}/${id}`
}else{
id=`${modulePath}/${id}`;
}
magicString.overwrite(s,e,id);
return;
}
// ...
}
jsx代码也渲染出来了
尾声
本文是通过阅读vite源码并加上一点自己的理解写出来的,为了方便大家理解,只实现了核心功能,细节方便没有做过多说明,如果有错误希望得到指正。
如果大家有收获,请给我点个赞Thanks♪(・ω・)ノ
陌小路:Vite原理分析
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!