在线体验地址:体验一下
组件仓库地址:github.com/dbfu/react-…
背景
前言
实现这个框架的灵感,来源于我以前开发的一个功能。按钮权限控制,相信做过后台管理系统的人都搞过这玩意。最开始我封装了一个组件,想要控制权限的按钮用这个组件包一下就行了,然后又封装了一个公共方法,可以在代码中判断权限。但是因为这个功能是后来加的,项目已经开发了很久,按照上面两种方式改造,领导觉得很麻烦,工作量很大,他希望做到只要传一个权限代码在组件上,就能根据用户权限显示或隐藏。我想了很久,发现要实现这个功能只能重写react
的createElement
方法了,因为每个组件都是这样创建的,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…
总结
通过开发这个库,我学到了:
- 为什么react17版本,react组件不用手动引入React。
- 如何重写createElement和jsx
- WeakMap使用场景
- 给react props拓展默认属性用作ts智能代码提示(主要是typescript类型体操)
组件仓库地址:github.com/dbfu/react-…