最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 实现一个vite(react版)

    正文概述 掘金(小郭郭yu)   2021-07-23   581

    预备知识

    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原理

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

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

    可以看到:
    实现一个vite(react版)
    浏览器发出了一个请求,请求了main.jsx
    实现一个vite(react版)
    实现一个vite(react版)
    查看main.jsx的内容,我们可以发现,vite启动的·服务器对引入模块的路径进行了处理,对jsx写法也进行了处理,转化成了浏览器可以运行的代码
    继续看
    实现一个vite(react版)
    实现一个vite(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版)
    我们用koa-static代理static目录下的静态资源
    index.html中的内容如下:
    实现一个vite(react版)
    执行

    node index.js
    

    访问loaclhost:3001
    可以到我们刚刚写的index.html的内容
    实现一个vite(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版)

    5.处理css文件

    添加一个like_button.css

    // ./static.like_button.css
    
    h1{
      color: #ff0
    }
    

    在like_button.js中引入

    // like_button.js
    
    import './like_button.css';
    

    刷新页面会看到这样的报错:
    实现一个vite(react版)
    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)
              `
            }
        });
    }
    
    

    重新启动服务
    实现一个vite(react版)
    样式就有了
    like_button.css的请求body变成了如下的样子
    实现一个vite(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版)

    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版)
    jsx代码也渲染出来了

    尾声

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

    陌小路:Vite原理分析


    起源地下载网 » 实现一个vite(react版)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元