Vue 开发者狂喜!我在 React 中完美复刻了 v-if/v-for 指令

Vue 开发者狂喜!我在 React 中完美复刻了 v-if/v-for 指令

前言

作为一名同时使用 Vue 和 React 的开发者,我深深被 Vue 的指令系统所吸引。 v-if 、 v-for 、 v-show 等指令让模板开发变得异常简洁高效。但在 React 中,我们却不得不使用略显冗长的三元表达式和 map 方法。这让我萌生了一个想法:能否在 React 中实现类似 Vue 的指令系统? 经过多次尝试,我找到了三种实现方案,其中 Babel 插件方案最为完美。下面我将详细介绍这些方案的实现思路和优劣对比。

为什么需要 React 指令?

  • 传统 React 条件渲染
jsx 复制代码
// 条件渲染
{
  isShow && <div>显示内容</div>;
}

// 列表渲染
{
  items.map((item) => <div key={item.id}>{item.name}</div>);
}
  • 理想中的写法
html 复制代码
<div r-if={isShow}>显示内容</div>
<div r-for={item in items} key={item.id}>{item.name}</div>

给人感觉就是很简洁 完美!

实现方案对比

方案一:高阶组件(不完美)

jsx 复制代码
const If = ({ condition, children }) => condition ? children : null;
const For = ({ list, children }) => list.map((item, index) => children(item, index));

<If condition={isShow}>
  <div>显示内容</div>
</If>

<For list={items}>
  {(item, index) => <div key={index}>{item}</div>}
</For>

优缺点分析:

  • ✅ 优点:遵循 React 设计理念,无需额外工具

  • ❌ 缺点:

    • 语法不够直观
    • 嵌套层级增加
    • 无法实现真正的指令效果

方案二:Babel 插件

实现思路

因为 React 的 JSX 本质上是 JavaScript 的语法糖,无法直接扩展类似 Vue 的模板指令系统,但是我们可以通过自定义 Babel 插件,在代码编译阶段将类似 r-if 的属性转换为 React 代码

  • 核心原理:

通过 Babel AST 转换,将:

jsx 复制代码
<div r-if={count > 4}>我大于4才能显示</div>

转换为:

jsx 复制代码
{
  count > 4 && <div>我大于4才能显示</div>;
}

我们都知道 React 代码的转换主要是通过 Babel 来完成的。 在之前 webpack 项目中 我们还得下载 babel-loader,用于转换 JSX 和 ES6+ 代码 我们现在用 vite 来做项目,@vitejs/plugin-react 插件已经内置了 Babel 配置,我们只需要进行相关配置即可。

接下来核心就是如何实现这个插件了 如果还不熟悉如何编写 bable 插件,可以先看一下下面的文档学习一下 扩展阅读

🔗 Babel 官方插件指南

🔗 实战:如何编写 Babel 插件 (掘金)

🔗 掘金文章 (掘金)

🔗 掘金文章 (掘金)

如果觉得内容太多学习太烦,你可以直接看我的代码,其实思路很简单

  1. 首先 bable 插件一定是一个函数,函数的参数对象 这个对象有一个 types 属性,这个属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
js 复制代码
export default function (babel) {
    //babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
     const { types: t } = babel;
    ...
    ...
}
  1. 这个函数必须返回一个具有 visitor 属性的对象,具体原因你可以看一下文档,你也可以就当做就是格式如此
js 复制代码
export default function (babel) {
  //babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
  const { types: t } = babel;
  return {
    name: "react-directives",
    visitor: {
      // 在这里编写你的访问者函数
    },
  };
}
  1. 该对象内部是对各种类型的标签(比如 JSXElement)的处理逻辑,是一个个的函数,然后我们编写函数 JSXAttribute ,目的就是转换 jsx 的 Attribute 为我们需要的语法,这个函数名称无所谓
js 复制代码
export default function (babel) {
  //babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
  const { types: t } = babel;
  return {
    name: "react-directives",
    visitor: {
      JSXElement(path) {},
    },
  };
}
  1. visitor 的每个方法都接收两个参数:path 和 state。我们这次只关注 path,path 是一个对象,它包含了当前节点的信息,比如节点的类型、属性、子节点等。
  2. 我们可以通过 path 来操作当前节点。具体 path 中有哪些值 不用过多关注,我们要获取的就是两个东西,一个是属性的名称 一个是属性的值,
  • 属性的名称 path.node.name.name ===> 'r-if'
  • 属性的值 path.node.value.expression ===> {count > 4}
  1. 查找当前 JSX 属性节点的最近的 JSX 元素父节点并且将其替换为一个新的 JSX 表达式容器节点,该节点包含一个逻辑表达式,该表达式使用逻辑与运算符将条件和原始的 JSX 元素连接起来。

    js 复制代码
    jsxElement.replaceWith(
      t.jSXExpressionContainer(
        t.logicalExpression("&&", condition, jsxElement.node)
      )
    );
  2. 最后移除原来的属性 path.remove();

实现效果
完整代码

vite.config.js

js 复制代码
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ["./babel-plugin-react-directives.js"],
      },
    }),
  ],
});

插件实现 babel-plugin-react-directives.js

js 复制代码
export default function (babel) {
  const { types: t } = babel;
  return {
    name: "react-directives",
    visitor: {
      JSXAttribute(path) {
        if (path.node.name.name?.startsWith("r-")) {
          const directive = path.node.name.name;
          const condition = path.node.value?.expression;
          if (directive === "r-if" && condition) {
            const jsxElement = path.findParent((p) => p.isJSXElement());
            jsxElement.replaceWith(
              t.jSXExpressionContainer(
                t.logicalExpression("&&", condition, jsxElement.node)
              )
            );
            // 移除原来的属性
            path.remove();
          }
        }
      },
    },
  };
}

方案三:覆写createElement

我的思路就是在运行时重写createElement,然后在createElement中处理r-if指令,但是我没有成功,有懂的大佬可以留言交流一下,不知道是不是我的方式有问题

js 复制代码
import React from "react";
const originalCreateElement = React.createElement;
const customCreateElement = function (type, props, ...children) {
  // 处理 r-if 指令
  if (props && props["r-if"] === false) {
    return null;
  }
  if (props && typeof props["r-if"] !== "undefined") {
    return props["r-if"]
      ? originalCreateElement(
          type,
          { ...props, "r-if": undefined },
          ...children
        )
      : null;
  }

  return originalCreateElement(type, props, ...children);
};
export const applyDirectives = () => {
  // 确保只应用一次
  if (!React.__directivesApplied) {
    React.__directivesApplied = true;
    React.createElement = customCreateElement; // 实际应用覆写
  }
};

然后在main.jsx 引入

js 复制代码
import { applyDirectives } from "./directives";
applyDirectives();

结语

通过Babel插件,我们成功在React中实现了类似Vue的指令系统。这不仅让代码更加简洁,也为React开发者提供了一种新的开发体验。虽然这只是一个开始,但它展示了AST操作的强大能力。

你会考虑在项目中使用这种方案吗?欢迎在评论区分享你的看法!

🔗 项目源码地址

相关推荐
顾林海4 分钟前
Flutter 图标和按钮组件
android·开发语言·前端·flutter·面试
雯0609~24 分钟前
js:循环查询数组对象中的某一项的值是否为空
开发语言·前端·javascript
bingbingyihao30 分钟前
个人博客系统
前端·javascript·vue.js
尘寰ya31 分钟前
前端面试-HTML5与CSS3
前端·面试·css3·html5
最新信息32 分钟前
PHP与HTML配合搭建网站指南
前端
前端开发张小七1 小时前
每日一练:3统计数组中相等且可以被整除的数对
前端·python
天天扭码1 小时前
一杯咖啡的时间吃透一道算法题——2.两数相加(使用链表)
前端·javascript·算法
Hello.Reader1 小时前
在 Web 中调试 Rust-Generated WebAssembly
前端·rust·wasm
NetX行者1 小时前
详解正则表达式中的?:、?= 、 ?! 、?<=、?<!
开发语言·前端·javascript·正则表达式
流云一号1 小时前
Python实现贪吃蛇三
开发语言·前端·python