文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

concent渐进式重构react应用使用详解

2022-11-13 19:25

关注

正文

传统的redux项目里,我们写在reducer里的状态一定是要打通到store的,我们一开始就要规划好state、reducer等定义,有没有什么方法,既能够快速享受ui与逻辑分离的福利,又不需要照本宣科的从条条框框开始呢?本文从普通的react写法开始,当你一个收到一个需求后,脑海里有了组件大致的接口定义,然后丝滑般的接入到concent世界里,感受渐进式的快感以及全新api的独有魅力吧!

需求来了

上周天气其实不是很好,记得下了好几场雨,不过北京总部大厦的隔音太好了,以致于都没有感受到外面的风雨飘摇,在工位上正在思索着整理下现有代码时,接到一个普通的需求,大致是要实现一个弹窗。

这是一个非常普通的需求,我相信不少码神看完后,脑海里已经把代码雏形大致写完了吧,嘿嘿,但是还请耐性看完本篇文章,来看看在concent的加持下,你的react应用将如何变得更加灵活与美妙,正如我们的slogan:

concent, power your react

准备工作

产品同学期望快速见到一般效果原型,而我希望原型是可以持续重构和迭代的基础代码,当然要认真对待了,不能为了交差而乱写一版,所以要快速整理需求并开始准备工作了。

因为项目大量基于antd来书写UI,听完需求后,脑海里冒出了一个穿梭框模样的组件,但因为右侧是一个可拖拽列表,查阅了下没有类似的组件,那就自己实现一个吧,初步整理下,大概列出了以下思路。

UI 实现

因为注册为concent组件后天生拥有了emit&on的能力,而且不需要手动offconcent在实例销毁前自动就帮你解除其事件监听,所以我们可以注册完成后,很方便的监听openColumnConf事件了。

我们先抛弃各种store和reducer定义,快速的基于class撸出一个原型,利用register接口将普通组件注册为concent组件,伪代码如下

import { register } from 'concent';
class ColumnConfModal extends React.Component {
  state = {
    selectedColumnKeys: [],
    selectableColumnKeys: [],
    visible: false,
  };
  componentDidMount(){
    this.ctx.on('openColumnConf', ()=>{
      this.setState({visible:true});
    });
  }
  moveToSelectedList = ()=>{
    //code here
  }
  moveToSelectableList = ()=>{
    //code here
  }
  saveSelectedList = ()=>{
    //code here
  }
  handleDragEnd = ()=>{
    //code here
  }
  render(){
    const {selectedColumnKeys, selectableColumnKeys, visible} = this.state;
    return (
      <Modal title="设置显示字段" visible={state._visible} onCancel={settings.closeModal}>
        <Head />
        <Card title="可选字段">
          <List dataSource={selectableColumnKeys} render={item=>{
            //...code here
          }}/>
        </Card>
        <Card title="已选字段">
          <DraggableList dataSource={selectedColumnKeys} onDragEnd={this.handleDragEnd}/>
        </Card>
      </Modal>
    );
  }
}
// es6装饰器还处于实验阶段,这里就直接包裹类了
// 等同于在class上@register( )来装饰类
export default register( )(ColumnConfModal)

可以发现,这个类的内部和传统的react类写法并无区别,唯一的区别是concent会为每一个实例注入一个上下文对象ctx来暴露concentreact带来的新特性api。

消灭生命周期函数

因为事件的监听只需要执行一次,所以例子中我们在componentDidMount里完成了事件openColumnConf的监听注册。

根据需求,显然的我们还要在这里书写获取表格列定义元数据和获取用户的个性化列定义数据的业务逻辑

  componentDidMount() {
    this.ctx.on('openColumnConf', () => {
      this.setState({ visible: true });
    });
    const tableId = this.props.tid;
    tableService.getColumnMeta(`/getMeta/${tableId}`, (columns) => {
      userService.getUserColumns(`/getUserColumns/${tableId}`, (userColumns) => {
        //根据columns userColumns 计算selectedList selectableList
      });
    });
  }

所有的concent实例可以定义setup钩子函数,该函数只会在初次渲染前调用一次。

现在让我们来用setup代替掉此生命周期

  //class 里定义的setup加$$前缀
  $$setup(ctx){
    //这里定义on监听,在组件挂载完毕后开始真正监听on事件
    ctx.on('openColumnConf', () => {
      this.setState({ visible: true });
    });
    //标记依赖列表为空数组,在组件初次渲染只执行一次
    //模拟componentDidMount
    ctx.effect(()=>{
      //service call balabala.....
    }, []);
  }

如果已熟悉hook的同学,看到setup里的effectapi语法是不是和useEffect有点像?

effectuseEffect的执行时机是一样的,即每次组件渲染完毕之后,但是effect只需要在setup调用一次,相当于是静态的,更具有性能提升空间,假设我们加一个需求,每次vibible变为false时,上报后端一个操作日志,就可以写为

    //依赖列表填入key的名称,表示当这个key的值发生变化时,触发副作用
    ctx.effect( ctx=>{
      if(!ctx.state.visible){
        //当前最新的visible已是false,上报
      }
    }, ['visible']);

关于effect就点到为止,说得太多扯不完了,我们继续回到本文的组件上。

提升状态到store

我们希望组件的状态变更可以被记录下来,方便观察数据变化,so,我们先定义一个store的子模块,名为ColumnConf

定义其sate为

// code in ColumnConfModal/model/state.js
export function getInitialState() {
  return {
    selectedColumnKeys: [],
    selectableColumnKeys: [],
	visible: false,
  };
}
export default getInitialState();

然后利用concentconfigure接口载入此配置

// code in ColumnConfModal/model/index.js
import { configure } from 'concent';
import state from './state';
// 配置模块ColumnConf
configure('ColumnConf', {
  state,
});

注意这里,让model跟着组件定义走,方便我们维护model里的业务逻辑。

整个store已经被concent挂载到了window.sss下,为了方便查看store,当当当当,你可以打开console,直接查看store各个模块当前的最新数据。

然后我们把class注册为'配置模ColumnConf的组件,现在class里的state声明可以直接被我们干掉了。

import './model';//引用一下model文件,触发model配置到concent
@register('ColumnConf')
class ColumnConfModal extends React.Component {
  // state = {
  //   selectedColumnKeys: [],
  //   selectableColumnKeys: [],
  //   visible: false,
  // };
  render(){
    const {selectedColumnKeys, selectableColumnKeys, visible} = this.state;
  }
}

大家可能注意到了,这样暴力的注释掉,render里的代码会不会出问题?放心吧,不会的,concent组件的state和store是天生打通的,同样的setState也是和store打通的,我们先来安装一个插件concent-plugin-redux-devtool

import ReduxDevToolPlugin from 'concent-plugin-redux-devtool';
import { run } from 'concent';
// storeConfig配置略,详情可参考concent官网
run(storeConfig, {
	plugins: [ ReduxDevToolPlugin ]
});

注意哦,concent驱动ui渲染的原理和redux完全不一样的,核心逻辑部分也不是在redux之上做包装,和redux一点关系都没有的^_^,这里只是桥接了redux-dev-tool插件,来辅助做状态变更记录的,小伙伴们千万不要误会,没有reduxconcent一样能够正常运作,但是由于concent提供完善的插件机制,为啥不利用社区现有的优秀资源呢,重复造无意义的轮子很辛苦滴(⊙﹏⊙)b......

现在让我们打开chrome的redux插件看看效果吧。

上图里是含有大量的ccApi/setState,是因为还有不少逻辑没有抽离到reducerdispatch export function moveToSelectedList() { } export function moveToSelectableList() { } export async function initSelectedList(tableId, moduleState, ctx) { //这里可以不用基于字符串 ctx.dispatch('setLoading', true) 去调用了,虽然这样写也是有效的 await ctx.dispatch(setLoading, true); const columnMeta = await tableService..getColumnMeta(`/getMeta/${tableId}`); const userColumsn = await userService.getUserColumns(`/getUserColumns/${tableId}`); //计算 selectedColumnKeys selectableColumnKeys 略 //仅返回需要设置到模块的片断state就可以了 return { loading: false, selectedColumnKeys, selectableColumnKeys }; } export async function saveSelectedList(tableId, moduleState, ctx) { } export function handleDragEnd() { }

利用concentconfigure接口把reducer也配置进去

// code in ColumnConfModal/model/index.js
import { configure } from 'concent';
import * as reducer from 'reducer';
import state from './state';
// 配置模块ColumnConf
configure('ColumnConf', {
  state,
  reducer,
});

还记得上面的setup吗,setup可以返回一个对象,返回结果将收集在settiings里,现在我们稍作修改,然后来看看class吧,世界是不是清静多了呢?

import { register } from 'concent';
class ColumnConfModal extends React.Component {
  $$setup(ctx) {
    //这里定义on监听,在组件挂载完毕后开始真正监听on事件
    ctx.on('openColumnConf', () => {
      this.setState({ visible: true });
    });
    //标记依赖列表为空数组,在组件初次渲染只执行一次
    //模拟componentDidMount
    ctx.effect(() => {
      ctx.dispatch('initSelectedList', this.props.tid);
    }, []);
    return {
      moveToSelectedList: (payload) => {
        ctx.dispatch('moveToSelectedList', payload);
      },
      moveToSelectableList: (payload) => {
        ctx.dispatch('moveToSelectableList', payload);
      },
      saveSelectedList: (payload) => {
        ctx.dispatch('saveSelectedList', payload);
      },
      handleDragEnd: (payload) => {
        ctx.dispatch('handleDragEnd', payload);
      }
    }
  }
  render() {
    //从settings里取出这些方法
    const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = this.ctx.settings;
  }
}

爱class,爱hook,让两者和谐共处

react社区轰轰烈烈推动了Hook,让大家逐步用Hook组件代替class组件,但是本质上Hook逃离了this,精简了dom渲染层级,但是也带来了组件存在期间大量的临时匿名闭包重复创建。

来看看concent怎么解决这个问题的吧,上面已提到setup支持返回结果,将被收集在settiings里,现在让稍微的调整下代码,将class组件吧变身为Hook组件吧。

import { useConcent } from 'concent';
const setup = (ctx) => {
  //这里定义on监听,在组件挂载完毕后开始真正监听on事件
  ctx.on('openColumnConf', (tid) => {
    ctx.setState({ visible: true, tid });
  });
  //标记依赖列表为空数组,在组件初次渲染只执行一次
  //模拟componentDidMount
  ctx.effect(() => {
    ctx.dispatch('initSelectedList', ctx.state.tid);
  }, []);
  return {
    moveToSelectedList: (payload) => {
      ctx.dispatch('moveToSelectedList', payload);
    },
    moveToSelectableList: (payload) => {
      ctx.dispatch('moveToSelectableList', payload);
    },
    saveSelectedList: (payload) => {
      ctx.dispatch('saveSelectedList', payload);
    },
    handleDragEnd: (payload) => {
      ctx.dispatch('handleDragEnd', payload);
    }
  }
}
const iState = { _myPrivKey: 'myPrivate state', tid:null };
export function ColumnConfModal() {
  const ctx = useConcent({ module: 'ColumnConf', setup, state: iState });
  const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = ctx.settings;
  const { selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey } = ctx.state;
  // return your ui
}

在这里要感谢尤雨溪老师的这篇Vue Function-based API RFC,给了我很大的灵感,现在你可以看到所以的方法的都在setup里定义完成,当你的组件很多的时候,给gc减小的压力是显而易见的。

由于两者的写法高度一致,从classHook是不是非常的自然呢?我们其实不需要争论该用谁更好了,按照你的个人喜好就可以,就算某天你看class不顺眼了,在concent的代码风格下,重构的代价几乎为0。

使用组件

上面我们定义了一个on事件openColumnConf,那么我们在其他页面里引用组件ColumnConfModal时,当然需要触发这个事件打开其弹窗了。

import { emit } from 'concent';
class Foo extends React.Component {
  openColumnConfModal = () => {
    //如果这个类是一个concent组件
    this.ctx.emit('openColumnConfModal', 3);
    //如果不是则可以调用顶层api emit
    emit('openColumnConfModal', 3);
  }
  render() {
    return (
      <div>
        <button onClick={this.openColumnConfModal}>配置可见字段</button>
        <Table />
          <ColumnConfModal />
      </div>
    );
  }
}

上述写法里,如果有其他很多页面都需要引入ColumnConfModal,都需要写一个openColumnConfModal,我们可以把这个打开逻辑抽象到modalService里,专门用来打开各种弹窗,而避免在业务见到openColumnConfModal这个常量字符串

//code in service/modal.js
import { emit } from 'concent';
export function openColumnConfModal(tid) {
  emit('openColumnConfModal', tid);
}

现在可以这样使用组件来触发事件调用了

import * as modalService from 'service/modal';
class Foo extends React.Component {
  openColumnConfModal = () => {
    modalService.openColumnConfModal(6);
  }
  render() {
    return (
      <div>
        <button onClick={this.openColumnConfModal}>配置可见字段</button>
        <Table />
        <ColumnConfModal />
      </div>
    );
  }
}

结语

以上代码在任何一个阶段都是有效的,想要了解渐进式重构的在线demo可以点这里

由于本篇主题主要是介绍渐进式重构组件,所以其他特性诸如synccomputed$watch、高性能杀手锏renderKey等等内容就不在这里展开讲解了,更多关于concent重构react的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯