Vue 开发者狂喜!我在 React 中完美复刻了 v-ifv-for 指令
(??金石瓜分计划回归,速戳下图了解详情??)
前言?作为一名同时使用 Vue 和 React 的开发者,我深深被 Vue 的指令系统所吸引。 v-if 、 v-for 、 v-show 等指令让模板开发变得异常简洁高效。但在 React 中,我们却不得不使用略显冗长的三元表达式和 map 方法。这让我萌生了一个想法:能否在 React 中实现类似 Vue 的指令系统? 经过多次尝试,我找到了三种实现方案,其中 Babel 插件方案最为完美。下面我将详细介绍这些方案的实现思路和优劣对比。
?为什么需要 React 指令?传统 React 条件渲染// 条件渲染
{
isShow div显示内容/div
}
// 列表渲染
{
items.map((item) =div key={item.id}{item.name}/div
}
理想中的写法div r-if={isShow}显示内容/div
div r-for={item in items} key={item.id}{item.name}/div
给人感觉就是很简洁 完美!
实现方案对比方案一:高阶组件(不完美)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 转换,将:
div r-if={count 4}我大于4才能显示/div
转换为:
{
count 4 div我大于4才能显示/div
}
我们都知道 React 代码的转换主要是通过 Babel 来完成的。 在之前 webpack 项目中 我们还得下载 babel-loader,用于转换 JSX 和 ES6+ 代码 我们现在用 vite 来做项目,@vitejs/plugin-react 插件已经内置了 Babel 配置,我们只需要进行相关配置即可。
接下来核心就是如何实现这个插件了 如果还不熟悉如何编写 bable 插件,可以先看一下下面的文档学习一下「扩展阅读」:
??Babel 官方插件指南[1]
??实战:如何编写 Babel 插件[2](掘金)
??掘金文章[3](掘金)
??掘金文章[4](掘金)
如果觉得内容太多学习太烦,你可以直接看我的代码,其实思路很简单
首先 bable 插件一定是一个函数,函数的参数对象 这个对象有一个 types 属性,这个属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树export default function (babel) {
//babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
const { types: t } = babel;
...
...
}
这个函数必须返回一个具有 visitor 属性的对象,具体原因你可以看一下文档,你也可以就当做就是格式如此export default function (babel) {
//babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
const { types: t } = babel;
return {
name: "react-directives",
visitor: {
// 在这里编写你的访问者函数
},
}
该对象内部是对各种类型的标签(比如 JSXElement)的处理逻辑,是一个个的函数, 然后我们编写函数 JSXAttribute ,目的就是转换 jsx 的 Attribute 为我们需要的语法, 这个函数名称无所谓export default function (babel) {
//babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
const { types: t } = babel;
return {
name: "react-directives",
visitor: {
JSXElement(path) {},
},
}
visitor 的每个方法都接收两个参数:path 和 state。我们这次只关注 path,path 是一个对象,它包含了当前节点的信息,比如节点的类型、属性、子节点等。我们可以通过 path 来操作当前节点。具体 path 中有哪些值 不用过多关注, 我们要获取的就是两个东西, 一个是属性的名称 一个是属性的值,属性的名称 path.node.name.name === 'r-if'属性的值 path.node.value.expression === {count 4}查找当前 JSX 属性节点的最近的 JSX 元素父节点并且将其替换为一个新的 JSX 表达式容器节点,该节点包含一个逻辑表达式,该表达式使用逻辑与运算符将条件和原始的 JSX 元素连接起来。
jsxElement.replaceWith(
t.jSXExpressionContainer(
t.logicalExpression("&&", condition, jsxElement.node)
)
);
最后移除原来的属性 path.remove();
实现效果完整代码vite.config.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
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 指令,但是我没有成功,有懂的大佬可以留言交流一下,不知道是不是我的方式有问题
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 引入
import { applyDirectives } from "./directives";
applyDirectives();
结语通过 Babel 插件,我们成功在 React 中实现了类似 Vue 的指令系统。这不仅让代码更加简洁,也为 React 开发者提供了一种新的开发体验。虽然这只是一个开始,但它展示了 AST 操作的强大能力。
你会考虑在项目中使用这种方案吗?欢迎在评论区分享你的看法!
??项目源码地址[5]
关注更多AI编程资讯请去AI Coding专区:https://juejin.cn/aicoding
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线