预备知识
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介绍。
 
- 提示下载完但解压或打开不了?
 
- 找不到素材资源介绍文章里的示例图片?
 
- 模板不会安装或需要功能定制以及二次开发?
 
                    
    
发表评论
还没有评论,快来抢沙发吧!