竟然可以在react中使用vue指令,快来看看吧。

4,093 阅读8分钟

在线体验地址:体验一下

组件仓库地址:github.com/dbfu/react-…

背景

前言

实现这个框架的灵感,来源于我以前开发的一个功能。按钮权限控制,相信做过后台管理系统的人都搞过这玩意。最开始我封装了一个组件,想要控制权限的按钮用这个组件包一下就行了,然后又封装了一个公共方法,可以在代码中判断权限。但是因为这个功能是后来加的,项目已经开发了很久,按照上面两种方式改造,领导觉得很麻烦,工作量很大,他希望做到只要传一个权限代码在组件上,就能根据用户权限显示或隐藏。我想了很久,发现要实现这个功能只能重写reactcreateElement方法了,因为每个组件都是这样创建的,createElement方法发现props中有permission属性,就表示当前组件需要控制权限,然后根据权限返回null还是正常渲染。

权限组件(伪代码)

// 权限组件
function Permisssion({ permission, children }) {

  const permissions = ['create', 'remove', 'update', 'query'];
  
  if(permission && !permissions.includes(permission)) {
     return null;
  }

  return children;
}

function App() {
   return (
     <Permisssion permission="remove">
       <button>删除</button>
     </Permisssion>
   )
}

判断权限的功能方法(伪代码)

 function isPermission(permission) {
    const permissions = ['create', 'remove', 'update', 'query'];
  
    if(permission && !permissions.includes(permission)) {
       return false;
    }
    
    return true;
 }
 
 function App() {
   return (
     <div>
       {isPermission('remove') && <button>删除</button>}
     </div>
   )
 }

重写createElement

大家应该都知道jsx最后会被编译成React.createElement吧,jsx只是语法糖,所以react组件中,即使没有用到React但是还是要引入react。例如:

function App() {
  return (<div id="div">hello</div>)
}

// 编译过后
function App() {
  return React.create('div', { id: 'div' }, 'hello');
}

react 17.0之前jsx会被编译成React.createElement,17之后编译成了jsx方法,而jsx方法是从react/jsx-runtime引入的。

// 17后版本上面代码编译结果
// 这个一般编译器都内置了,所以不用手动引入。
import { jsx } form 'react/jsx-runtime' 

function App() {
  return jsx('div', { id: 'div' }, 'hello');
}

因为要兼容不同版本,所以不止要重写createElement,还要重写react/jsx-runtime中的jsx方法。这个我参考了github.com/meowtec/rea…这个仓库,他把clsx库集成到了react底层,不用去引用clsx也能处理className,很赞。大家可以去看一下clsx的用法。

 // 重写reactElement方法后,所有组件都可以很方便的控制权限,只要传当前权限代码就行了。重写createElement的代码也很简单。

 // 新createElement方法(伪代码)
 function createElement(type, props) {
   const permissions = ['create', 'remove', 'update', 'query'];
   
   if(props.permission && !permissions.includes(props.permission)) {
     return null;
   }
   
   return React.createElement(type, props);
 }


 function App() {
   return (
     <div>
       <button permission="remove">删除</button>
     </div>
   )
 }

这个功能我已经开发完一段时间了,前两天在一个前端群里看到有人说,react太难用了,写个判断还得用三目运算符,还是vue的v-if指令好用。然后我灵光一闪,我以前既然可以通过重写createElement来实现权限控制功能,为啥不能通过重写createElement来实现一些指令呢,想到就做,然后就有了现在这个库react-directive,可以在react中使用一些常用指令,同时支持自己扩展指令。

框架介绍

介绍

react-directive库实现代码主要参考了react-auto-classnames这个库的代码,实现了可以在react项目中使用vue指令,同时也可以自定义指令。

安装依赖

npm i @dbfu/react-directive

使用说明

如果使用typescript,修改tsconfig.json文件

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@dbfu/react-directive",
  }
}

如果使用webpack打包,修改babel.config.json或修改.babelrc

// .babelrc / babel.config.json
{
  "presets": [
    [
      "@babel/preset-react",
      {
        "runtime": "automatic",
        "importSource": "@dbfu/react-directive"
      }
    ]
  ]
}

如果使用vite

export default defineConfig({
  plugins: [react({
    jsxImportSource: '@dbfu/react-directive'
  })],
})

如果使用umi,先安装@babel/preset-react依赖,然后修改.umirc配置文件

 import { defineConfig } from "umi";

export default defineConfig({
  extraBabelPresets: [
    [
      "@babel/preset-react",
      {
        "runtime": "automatic",
        "importSource": "@dbfu/react-directive"
      }
    ]
  ],
});

如果使用react-create-app,先安装@babel/preset-react,然后修改package.json文件,添加babel属性。

{
"babel": {
    "presets": [
      "react-app",
      [
        "@babel/preset-react",
        {
          "runtime": "automatic",
          "importSource": "@dbfu/react-directive"
        }
      ]
    ]
  },
}

然后就可以在项目中,使用框架内置的指令

function App() {

  const model = useModel({ name: "jack" });

  return (
    <div v-if={false} v-show={false} v-model={[model, "name"]}>
  )
}

内置指令使用文档

v-if

和vue中的v-if一样,这个不仅可以对原生dom使用,还能对组件进行使用。

v-show

和vue中的v-show一样,这个只能对原生dom使用,因为会修改dom元素的style中display属性,如果组件支持style.display属性的话,也可以使用v-show。

v-model

这个指令效果和vue中的v-model差不多,但是用法有点区别,创建的对象必须用useModel这个hooks包装一下,还有v-model指令需要传数组,第一个值是对象,第二个参数是对象里面的key,支持多层级key。看下面的例子:

import useModel from '@dbfu/react-directive/useModel'

function App() {
 
 const model = useModel({ user: { name: 'tom' } });

 return (
  <div>
    <input v-model={[model, 'user.name']} />
    <div>{model?.user?.name}</div>
  </div>
 )
}

v-focus

表单元素自动获得焦点,和原生autoFocus属性不同的是,这个可以在一开始display:none的情况下,后面再显示时也能自动获取焦点,这个autoFocus就实现不了。只要组件实现了focus方法,也可以对组件进行使用。

v-copy

点击当前元素时,会把当前指令的值复制到剪切板上,只要支持onClick事件,无论组件或原生dom元素,都可以使用这个指令

v-text

和vue实现效果一样,只支持原生dom元素。

v-html

和vue实现效果一样,只支持原生dom元素。

自定义指令

在项目入口,从@dbfu/react-directive/directive引入directive,然后就可以自定义指令了。语法如下:

import { directive } from "@dbfu/react-directive/directive"

// name 指令名称
directive("name", {
  // 组件渲染之前,在createElement的时候,在这个生命周期可以处理组件props,然后组件渲染的时候,可以拿到处理后的props。注意这个方法只要组件一重新render,就会触发一次。如果返回false这个组件就不渲染了。
  // value:当前指令的值
  // props:当前组件的props
  create: (value, props) => {
     // example v-show的实现
     if(value === false) {
       if(props?.style) {
         props.style.display = 'none';
       } else {
         props.style = { display: 'none' };
       }
     } 
  },
  // dom元素和组件渲染的时候触发,正常情况只会触发一次。如果组件多次销毁和渲染,每次渲染都会触发这个方法。
  // 这个方法里面不能处理props,但是可以拿到组件引用活dom元素引用,可以去调用组件或dom元素的方法
  // ref:如果是组件则是组件引用,如果是dom元素就是dom的引用。
  // value:当前指令的值
  // props:当前组件的props
  mounted: (ref, value, props) => {
     // example v-text的实现
     if(isDOM(ref)) {
       ref.innerText = value;
     }

      // example v-html的实现
     if(isDOM(ref)) {
       ref.innerHTML = value;
     } 
  },
  // 这个方法是组件style.display由none转换为block时触发
  // 参数和mounted一样
  show: (ref, value, props) => {
     // example v-focus的实现
     ref?.focus?.();
  },
  // 和show相反
  hidden: (ref, value, props) => {
    // 暂无使用场景
  }
});

项目中遇到的问题

ref问题

mounted生命周期中使用的ref,就是给每个组件的props注入了ref,然后在ref的回调中拿到组件或dom的引用,把拿到的引用存到WeakMap中,后面ref回调再执行的时候,拿当前的ref去WeakMap中检查是否已经存在,如果存在说明已经渲染过了,就不用再执行mounted。这里有两个问题,组件每次重新渲染,ref回调都会两次执行,第一次的值为null,第二次才是组件的引用,这里只要判断如果null,不做操作就行了。另外一个问题只有组件才会出现,原生dom没有,组件重新渲染后,ref的引用会改变,所以用ref来判断是否渲染过,就不行了,这个暂时我没有想到解决方案,所以自定义指令时,mounted生命周期只能用于原生dom元素。

// 每次组件渲染ref之所以是新的引用,是因为大家在组件中给外部暴露方法或属性的时候,都是return {},每次都是新的对象。
const Demo = forwardRef((_, ref) => {

  useImperativeHandle(ref, () => {
    return {
      name: 'demo'
    }
  }, []);

  return (
    <div>demo</div>
  )
})

// 改成下面这样就行了,但是会有问题,如果想暴露方法,可能会闭包问题。
// 我看了antd的组件对外暴露属性或方法,都是上面的写法。
const Demo = forwardRef((_, ref) => {
  const export = useRef({ name: "demo" });

  useImperativeHandle(ref, () => {
    return export.current;
  }, []);

  return (
    <div>demo</div>
  )
})

WeakMap的使用

之所以用到这个是因为我需要把组件的ref当key,能把对象当key,只能用Map对象。Map的key如果垃圾回收了,值不会自动从Map中删除。我遇到的问题是,如果dom移除然后再添加进来,dom元素就会变成的新的,老的会被移除掉,然后垃圾回收,所以需要找一个key的值垃圾回收后能自动移除值的Map对象,用WeakMap是最合适的,不用担心内存泄露的问题。不了解WeakMap的同学,可以看一下阮一峰老师关于WeakMap的讲解。es6.ruanyifeng.com/#docs/set-m…

总结

通过开发这个库,我学到了:

  1. 为什么react17版本,react组件不用手动引入React。
  2. 如何重写createElement和jsx
  3. WeakMap使用场景
  4. 给react props拓展默认属性用作ts智能代码提示(主要是typescript类型体操)

组件仓库地址:github.com/dbfu/react-…