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

目录
  • 正文
  • 需求来了
  • 准备工作
  • UI 实现
  • 消灭生命周期函数
  • 提升状态到store
  • 解耦业务逻辑与UI
  • 爱class,爱hook,让两者和谐共处
  • 使用组件
  • 结语

正文

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

需求来了

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

  • 左侧有一个可选字段列表,点击任意一个字段,就会进入右侧。
  • 右侧有一个已选字段列表,该列表可以上下拖拽决定字段顺序决定表格里的列字段显示顺序,同时也可以删除,将其恢复到可选择列表。
  • 点击保存,将用户的字段配置存储到后端,用户下次再次使用查看该表格时,使用已配置的显示字段来展示。

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

concent, power your react

准备工作

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

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

  • 组件命名为ColumnConfModal,基于antdModal, Card实现布局,antdList来实现左侧的选择列表,基于react-beautiful-dnd的可拖拽api来实现右侧的拖拽列表。

  • 因为这个弹窗组件在不同页面被不同的table使用,传入的列定义数据是不一样的,所以我们使用事件的方式,来触发打开弹窗并传递表格id,打开弹窗后获取该表格的所有字段定义,以及用户针对表哥的已选择字段数据,这样把表格元数据的初始化工作收敛在ColumnConfModal内部。
  • 基于表格左右两侧的交互,大致定义一下内部接口 1 moveToSelectedList(移入到已选择列表 ) 2 moveToSelectableList(移入到可选择列表) 3 saveSelectedList(保存用户的已选择列表) 4 handleDragEnd(处理已选择列表顺序调整完成时) 5 其他略.....

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/***模样的type就是dispatch调用了,后面我们会提到。

这样看状态变迁是不是要比window.sss好多了,因为sss只能看当前最新的状态。

这里既然提到了redux-dev-tool,我们就顺道简单了解下,concent提交的数据长什么样子吧

上图里可以看到5个字段,renderKey是用于提高性能用的,可以先不作了解,这里我们就说说其他四个,module表示修改的数据所属的模块名,committedState表示提交的状态,sharedState表示共享到store的状态,ccUniqueKey表示触发数据修改的实例id。

为什么要区分committedStatesharedState呢?因为setState调用时允许提交自己的私有key的(即没有在模块里声明的key),所以committedState是整个状态都要再次派发给调用者,而sharedState是同步到store后,派发给同属于module值的其他cc组件实例的。

这里就借用官网一张图示意下:

所以我们可以在组件里声明其他非模块的key,然后在this.state里获取到了

@register('ColumnConf')
class ColumnConfModal extends React.Component {
   state = {
		_myPrivKey:'i am a private field value, not for store',
   };
  render(){
  	//这里同时取到了模块的数据和私有的数据
    const {selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey} = this.state;
  }
}

解耦业务逻辑与UI

虽然代码能够正常工作,状态也接入了store,但是我们发现class已经变得臃肿不堪了,利用setState怼固然快和方便,但是后期维护和迭代的代价就会慢慢越来越大,让我们把业务抽到reduder

export function setLoading(loading) {
  return { loading };
};
/** 移入到已选择列表 */
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的资料请关注我们其它相关文章!

(0)

相关推荐

  • React元素与组件的区别示例详解

    目录 从问题出发 元素与组件 元素 组件 问题如何解决 自定义内容 第一种实现方式 第二种实现方式 第三种实现方式 从问题出发 我被问过这样一个问题: 想要实现一个 useTitle 方法,具体使用示例如下: function Header() { const [Title, changeTitle] = useTitle(); return ( <div onClick={() => changeTitle('new title')}> <Title /> </div

  • React Refs 的使用forwardRef 源码示例解析

    目录 三种使用方式 1. String Refs 2. 回调 Refs 3. createRef 两种使用目的 Refs 转发 createRef 源码 forwardRef 源码 三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs class App extends React.Component { constructor(props) { su

  • React Context 变迁及背后实现原理详解

    目录 Context 老的 Context API 基础示例 context 中断问题 解决方案 新的 Context API 基础示例 模拟实现 createContext 源码 Context 本篇我们讲 Context,Context 可以实现跨组件传递数据,大部分的时候并无需要,但有的时候,比如用户设置 了 UI 主题.地区偏好,如果从顶层一层层往下传反而有些麻烦,不如直接借助 Context 实现数据传递. 老的 Context API 基础示例 在讲最新的 API 前,我们先回顾下老

  • React竞态条件Race Condition实例详解

    目录 竞态条件 React 与竞态条件 效果演示 问题复现 布尔值解决 useRequest 解决 Suspense 竞态条件 Race Condition,中文译为竞态条件,旨在描述一个系统或者进程的输出,依赖于不受控制事件的出现顺序或者出现时机. 举个简单的例子: if (x == 5) // The "Check" { y = x * 2; // The "Act" // 如果其他的线程在 "if (x == 5)" and "y

  • react app rewrited替代品craco使用示例

    目录 1. 不使用custom-cra的原因 2. craco基本使用 3. 使用craco修改antd主题 4. 别名 5. babel扩展 6. 分包 7. 配置代理 8. 最后 1. 不使用custom-cra的原因 custom-cra,react-app-rewired 与 craco 都是用来无 eject 重写 CRA 配置 custom-cra上次更新在两年前,有些配置跟不上新的版本,例如使用webpack5配置less会出错, 虽说目前有了解决方案引入新包customize-c

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

    目录 正文 需求来了 准备工作 UI 实现 消灭生命周期函数 提升状态到store 解耦业务逻辑与UI 爱class,爱hook,让两者和谐共处 使用组件 结语 正文 传统的redux项目里,我们写在reducer里的状态一定是要打通到store的,我们一开始就要规划好state.reducer等定义,有没有什么方法,既能够快速享受ui与逻辑分离的福利,又不需要照本宣科的从条条框框开始呢?本文从普通的react写法开始,当你一个收到一个需求后,脑海里有了组件大致的接口定义,然后丝滑般的接入到co

  • IOS React Native FlexBox详解及实例

    IOS React Native FlexBox详解及资料整理, # 前言 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML 学习 本人接触 React Native 时间并不是特别长,所以对其中的内容和性质了解可能会有所偏差,在学习中如果有错会及时修改内容,也欢迎万能的朋友们批评指出,谢谢 文章第一版出自简书,如果出现图片或页面显示问题,烦请转至 简书 查看 也希望喜欢的朋友可以点赞,谢谢 什么是 FlexBox 布局 在 html 中,界面的搭建都是采用 C

  • React合成事件详解

    react合成事件指的是react用js模拟了一个Dom事件流.(fiber树模拟Dom树结构) 合成事件的事件流在fiber树中发生捕获和冒泡. 从点击输入框开始 当你点击input输入框,react在根节点(注1)监听到focus事件(注2)(注3). 如何从原生事件找到对应的虚拟Dom? 此时,react得到的信息只有原生事件对象(nativeEvent).react通过nativeEvent对应的Dom(eventTarget),沿着Dom树向上找到距离该eventTarget最近的被r

  • 从零开始最小实现react服务器渲染详解

    前言 最近在写 koa 的时候想到,如果我部分代码提供api,部分代码支持ssr,那我应该如何写呢?(不想拆成 2个服务的情况下) 而且最近写的项目里面也用过一些服务端渲染,如nuxt,自己也搭过next的项目,确实开发体验都非常友好,但是友好归友好,具体又是如何实现的呢,诸位有没有考虑过? 本着求真务实的折腾态度,选了react作为研究对象(主要是vue写的有点多,恶心了),那下面就简单就以最小成本写一个react的服务端渲染 demo 用到的技术栈 react 16 + webpack3 +

  • React事件绑定详解

    目录 类组件事件绑定 函数组件事件绑定 总结 React事件绑定和原生DOM事件绑定相似 语法:on+事件名={事件处理程序} 例如:onClick={()=>{}} 注意:React事件采用驼峰命名法 类组件事件绑定 import React from 'react'; import ReactDOM from 'react-dom'; class App extends React.Component { handleClick() { console.log(111); } render(

  • React国际化react-i18next详解

    简介 react-i18next 是基于 i18next 的一款强大的国际化框架,可以用于 react 和 react-native 应用,是目前非常主流的国际化解决方案. i18next 有着以下优点: 基于i18next不仅限于react,学一次就可以用在其它地方 提供多种组件在hoc.hook和class的情况下进行国际化操作 适合服务端的渲染 历史悠久,始于2011年比大多数的前端框架都要年长 因为历史悠久所以更成熟,目前还没有i18next解决不了的国际化问题 有许多插件的支持,比如可

  • 初识React及React开发依赖详解

    目录 初识React React介绍 React特点 React的依赖介绍 React的开发依赖 Babel和React的关系 React的依赖引入 初识React React介绍 React是什么呢? 相信每个做开发的人对它都或多或少有一些印象; 这里我们来看一下官方对它的解释:用于构建用户界面的 JavaScript 库; 目前对于前端开发来说,几乎很少直接使用原生的JavaScript来开发应用程序,而是选择一个JavaScript库(框架). 在过去的很长时间内,jQuery是被使用最多

  • React之Hooks详解

    目录 什么是钩子(hooks) 类组件 函数组件 为什么创造Hooks 总结 什么是钩子(hooks) 消息处理的一种方法, 用来监视指定程序 函数组件中需要处理副作用,可以用钩子把外部代码"钩"进来 常用钩子:useState, useEffect, useContext, useReducer Hooks一律使用use前缀命名:useXXX 类组件 函数组件 一类特殊的函数,为你的函数式组件注入特殊的功能 为什么创造Hooks 有些类组件冗长且复杂,难以复用 结局方案:无状态组件与

  • React Hook用法示例详解(6个常见hook)

    1.useState:让函数式组件拥有状态 用法示例: // 计数器 import { useState } from 'react' const Test = () => { const [count, setCount] = useState(0); return ( <> <h1>点击了{count}次</h1> <button onClick={() => setCount(count + 1)}>+1</button> &l

  • 使用ES6语法重构React代码详解

    使用ES6语法重构React组件 在Airbnb React/JSX Style Guide中,推荐使用ES6语法来编写react组件.下面总结一下使用ES6 class语法创建组件和以前使用React.createClass方法来创建组件的不同. 创建组件 ES6 class创建的组件语法更加简明,也更符合javascript.内部的方法不需要使用function关键字. React.createClass import React from 'react'; const MyComponen

随机推荐