likes
comments
collection
share

实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react

作者站长头像
站长
· 阅读数 18

本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react并没有esm包,所以这里并进行了一些react支持方面的改造,供学习和交流 阅读完本文,读者应该能够了解:

  • es module相关的知识
  • koa的基本使用
  • vite的核心原理
  • dev-server自动更新的原理
  • esbuild的基础用法

预备知识

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(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react

vite原理

首先我们创建一下vite项目跑一下

yarn create vite my-react-app --template react
yarn dev

可以看到:实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react浏览器发出了一个请求,请求了main.jsx实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react查看main.jsx的内容,我们可以发现,vite启动的·服务器对引入模块的路径进行了处理,对jsx写法也进行了处理,转化成了浏览器可以运行的代码继续看实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react在client中,我们看到了websocket的代码,所以可以理解为vite服务器注入客户端的websocket代码,用来获取服务器中代码的变化的通知,从而达到热更新的效果综上,我们知道了vite服务器做的几件事:

  • 读取本地代码文件
  • 解析引入模块的路径并重写
  • websocket代码注入客户端

代码实现

本文的完整代码在:github.com/yklydxtt/vi…这里我们分五步:

  1. 创建服务
  2. 读取本地静态资源
  3. 并重写模块路径
  4. 解析模块路径
  5. 处理css文件
  6. 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目录实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react我们用koa-static代理static目录下的静态资源index.html中的内容如下:实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react执行

node index.js

访问loaclhost:3001可以到我们刚刚写的index.html的内容实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react

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

看到如下页面实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react

5.处理css文件

添加一个like_button.css

// ./static.like_button.css

h1{
  color: #ff0
}

在like_button.js中引入

// like_button.js

import './like_button.css';

刷新页面会看到这样的报错:实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,reactes 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)
          `
        }
    });
}

重新启动服务实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react样式就有了like_button.css的请求body变成了如下的样子实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react

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!'
    );
...

可以看到页面有了更新,感叹号有了实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,react

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;
        }
    // ...
}

实现一个vite(react版)本文参考了vite的0.x版本的源码,由于0.x版本只支持vue和esm依赖,reactjsx代码也渲染出来了

尾声

本文是通过阅读vite源码并加上一点自己的理解写出来的,为了方便大家理解,只实现了核心功能,细节方便没有做过多说明,如果有错误希望得到指正。如果大家有收获,请给我点个赞Thanks♪(・ω・)ノ 陌小路:Vite原理分析

转载自:https://juejin.cn/post/6987737754293501988
评论
请登录