Skip to content

Latest commit

 

History

History
520 lines (442 loc) · 21 KB

README.zh-CN.md

File metadata and controls

520 lines (442 loc) · 21 KB

English | 简体中文

一个可预测、0入侵、渐进式、高性能的增强型状态管理方案,基于引用收集依赖标记状态分发原理,power you react!

了解更多请访问官方文档https://concentjs.github.io/concent-doc.


hello-concent

review this gif

🖥在线体验

  • 快速开始:
    快速了解和上手concent的强大特性!!

Edit

  • todo app:
    下面还有一个链接使用hook&redux编写,可以点击查看并感受concent版本和hook&redux版本的差异

Edit

使用hook&redux编写 (author:Gábor Soós@blacksonic)

  • 结合了concent生态库的企业级项目模板(js):

Edit

  • 结合了concent生态库的企业级项目模板(ts):

Edit

source code see here:https://github.com/fantasticsoul/concent-guid-ts

✨特性

  • 极简的核心apirun载入模块配置启动concent,register注册组件,无需包一层Provider在根组件。
  • 0入侵成本接入,不改造代码的情况下直接接入;hello-concent
  • 贴心的模块配置,除了state,还提供reducercomputedwatchinit四项可选定义。
  • 灵活的数据消费粒度,支持跨多个模块场景,以及模块内stateKey级别的细粒度控制。
  • 渐进式构建react应用,除了setState,支持dispatchinvoke调用来让ui视图与业务逻辑彻底解耦。从class到function
  • 组件能力增强,支持实例级别computedwatch定义,支持emit&on,以及支持setup特性,让函数组件拥有定义静态api的能力。
  • 高度一致的编程体验hocrender propshook3种方式定义的组件均享有一致的api调用体验,相互切换代价为0。多种方式定义组件
  • 渲染性能出众,内置renderKeylazyDispatchdelayBroadcast等特性,保证极速的渲染效率。长列表精准渲染批处理状态提交高频输入场景状态延迟分发
  • 干净的dom层级,对于class组件,默认采用反向继承策略,让react dom树的层级结构保持简洁与干净。
  • 扩展中间件与插件,允许用户定义中间件拦截所有的数据变更提交记录,做额外处理,也可以自定义插件,接收运行时的发出的各种信号,按需增强concent自身的能力。
  • 去中心化配置模块,除了run接口一次性配置模块,还提供configure接口在任意地方动态配置模块。
  • 模块克隆,支持对已定义模块进行克隆,满足你高维度抽象的需要。
  • 完整的typescript支持,能够非常容易地书写优雅的ts代码

搭配react-router使用

请移步阅读和了解react-router-concent,暴露history对象,可以全局任意地方使用,享受编程式的导航跳转。

react-router-concent在线示例

搭配redux-dev-tool使用

请移步阅读和了解concent-plugin-redux-devtool,全流程追溯你的状态变更过程。 redux-dev-tool

搭配loading插件使用

请移步阅读和了解concent-plugin-loading,轻松控制concent应用里所有reducer函数的loading状态。

concent-plugin-loading在线示例


📦 快速开始

确保你本地机器上安装有nodejs

创建一个app

在你的电脑上,选择一个合适的目录并进入,使用create-react-app 创建一个app

$ npm i -g create-react-app
$ create-react-app cc-app

安装concent

创建好app后,进入你的app根目录,使用npm安装concent

$ cd cc-app
$ npm i --save concent

或者使用yarn安装

$ yarn add concent

App.js文件的内容全部替换为以下代码

你也可以点击这里在线编辑.

import React, { Component } from 'react';
import { register, run, useConcent } from 'concent';

// 运行concent,配置一个名为counter的模块
run({
  counter:{
    state:{count:1}
  }
})

// 定义一个属于counter模块的类组件
@register('counter')
class Counter extends Component{
  render(){
    //此时setState能够直接提交状态到store,并广播到其他同属于counter模块的实例
    const add = ()=>this.setState({count:this.state.count+1});
    return (
      <div>
        {this.state.count}
        <button onClick={add}>add</button>
      </div>
    )
  }
}

// 定义一个属于counter模块的函数组件
function FnCounter(){
  const ctx = useConcent('counter');
  const add = ()=>ctx.setState({count:ctx.state.count+1});
  return (
    <div>
      {ctx.state.count}
      <button onClick={add}>add</button>
    </div>
  )
}

export default function App() {
  return (
    <div className="App">
      <Counter />
      <FnCounter />
    </div>
  );
}

❤️依赖收集

concent使用Proxy&defineProperty in v2.3+完成了运行时的依赖收集特性,大幅缩小UI视图渲染范围,提高应用性能。

模块级别的计算依赖收集

run({
  counter:{
    state:{
      modCount: 10,
      modCountBak: 100,
      factor: 1,
    },
    computed:{
      xxx(n){
        return n.modCount + n.modCountBak;
      },// for xxx computed retKey, the depKeys is ['modCount', 'modCountBak']
      yyy(n){
        return n.modCountBak;
      },// for yyy computed retKey, the depKeys is ['modCountBak']
      zzz(n, o, f){// n means newState, o means oldState, f means fnCtx
        return f.cuVal.xxx + n.factor;
      },// for zzz computed retKey, the depKeys is ['factor', 'modCount', 'modCountBak']
    },
    watch:{
      xxx:{
        fn(n){
          console.log('---> trigger watch xxx', n.modCount);
        },// for xxx watch retKey, the depKeys is ['modCount']
        immediate: true,
      },
    }
  }
});

实例级别的计算依赖收集

const setup = ctx => {
  ctx.computed('show', (n)=>{
    return n.show + n.cool + n.good;
  });// for show retKey, the depKeys is ['show', 'cool', 'good']

  ctx.computed('show2', (n)=>{
    return n.show + '2222' + n.cool;
  });// for show2 retKey, the depKeys is ['show', 'cool', 'good']
};

渲染期间的依赖收集

import {register, useConcent} from 'concent';

const iState = ()=>({show:true});
function FnComp(){
  const {state, syncBool} = useConcent({module:'counter', state:iState, setup});
  return (
    <div>
      {/** if show is true, current ins's dependency is ['modCount']*/}
      {state.show? <span>{state.modCount}</span> : ''}
      <button onClick={syncBool('show')}>toggle</button>
    </div>
  );
}

@register({module:'counter', state:iState, setup})
class ClassComp extends React.Component{
  // state = iState(); //or write private state here
  render(){
     const {state, syncBool} = this.ctx;
    return (
      <div>
        {/** if show is true, current ins's dependency is ['modCount']*/}
        {state.show? <span>{state.modCount}</span> : ''}
        <button onClick={syncBool('show')}>toggle</button>
      </div>
  }
}

edit this demo on CodeSandbox

🔨其他高级特性实例

  • 运行concent,载入模块配置
import React, { Component, Fragment } from 'react';
import { register, run } from 'concent';

run({
  counter: {// 定义counter模块
    state: {// 【必需】,定义state
      count: 0,
      products: [],
      type: '',
    },
    reducer: {// 【可选】定义reducer,书写修改模块状态逻辑
      inc(payload=1, moduleState) {
        return { count: moduleState.count + payload };
      },
      dec(payload=1, moduleState) {
        return { count: moduleState.count - payload };
      },
      async inc2ThenDec3(payload, moduleState, actionCtx){
        await actionCtx.dispatch('inc', 2);
        await actionCtx.dispatch('dec', 3);
      }
    },
    computed:{// 【可选】定义模块computed,当对应的stateKey发生变化时触发计算函数,结果将被缓存
      count(newState, oldState){
        return newState.count * 2;
      }
    },
    watch:{// 【可选】定义模块watch,当对应的stateKey发生变化时触发watch函数,通常用于触发一些异步任务的执行
      count(newState, oldState){
        console.log(`count changed from ${oldState.count} to ${newState.count}`);
      }
    },
    init: async ()=>{// 【可选】模块状态的初始化函数,当状态需要异步的定义,且与具体挂载的组件无关时定义此项
      const state = await api.fetchState();
      return state;
    }
  }
})

更推荐将模块定义选项放置到各个文件中,然后在各自导出交给run函数配置.

|____models             # business models
| |____index.js
| |____counter
| | |____index.js
| | |____reducer.js     # change state methods(optional)
| | |____computed.js    # computed methods(optional)
| | |____watch.js       # watch methods(optional)
| | |____init.js        # async state initialization function(optional)
| | |____state.js       # module init state(required)

此时reducer文件里函数可以不需要基于字符串发起组合型调用了

export function inc(payload=1, moduleState) {
  return { count: moduleState.count + payload };
}

export function dec(payload=1, moduleState) {
  return { count: moduleState.count - payload };
}

// 组合调用其他的reducer函数完成业务逻辑
export async function inc2ThenDec3(payload, moduleState, actionCtx){
  await actionCtx.dispatch(inc, 2);
  await actionCtx.dispatch(dec, 3);
}

当然reducer文件里,你可以调用setState,是一个被promise话的句柄

export updateLoading(loading){
  return { loading }
}

export async function inc2ThenDec3(payload, moduleState, actionCtx){
  await actionCtx.dispatch(inc, 2);
  //等效于调用actionCtx.dispatch(updateLoading, true);
  await actionCtx.setState({loading: true});
  await actionCtx.dispatch(dec, 3);
  //等效于调用actionCtx.dispatch(updateLoading, false);
  await actionCtx.setState({loading: false});
  
  //最后这里你可以选择的返回一个新的片断状态,也会触发视图更新
  return { tip: 'you can return some new value in current reducer fn ot not' };
}
  • 定义 setup
    setup只会在初次渲染之前被执行一次,同样用于定义一些副作用函数,或者一些随后可以在渲染函数体内通过ctx.settings取到的方法,所以将不在产生大量的临时闭包函数,且setup可以同时传递给类组件和函数组件,意味着你可以随时地切换组件形态,优雅的复用业务逻辑。
const setup = ctx => {
  console.log('setup函数只会在组件初次渲染之前被执行一次');
  
  ctx.on('someEvent', (p1, p2)=> console.log('receive ', p1, p2));

  ctx.effect(() => {
    fetchProducts();
  }, ["type", "sex", "addr", "keyword"]);//这里只需要传key名称就可以了
  /** 原函数组件内写法:
    useEffect(() => {
      fetchProducts(type, sex, addr, keyword);
    }, [type, sex, addr, keyword]);
  */

  ctx.effect(() => {
    return () => {
      // 返回一个清理函数
      // 等价于componentWillUnmout, 这里搞清理事情
    };
  }, []);
  /** 原函数组件内写法:
    useEffect(()=>{
      return ()=>{// 返回一个清理函数
        // 等价于componentWillUnmout, 这里搞清理事情
      }
    }, []);//第二位参数传空数组,次副作用只在初次渲染完毕后执行一次
  */

  ctx.effectProps(() => {
    // 对props上的变更书写副作用,注意这里不同于ctx.effect,ctx.effect是针对state写副作用
    const curTag = ctx.props.tag;
    if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
  }, ["tag"]);//这里只需要传key名称就可以了
  /** 原函数组件内写法:
    useEffect(()=>{
      // 首次渲染时,此副作用还是会执行的,在内部巧妙的再比较一次,避免一次多余的ui更新
      // 等价于类组件里getDerivedStateFromProps里的逻辑
      if(tag !== propTag)setTag(tag);
    }, [propTag, tag]);
 */


  // 定义实例计算函数,当count值变化时会触发其计算,用户可随后在渲染函数体内通过ctx.refComputed.doubleTen获得计算结果
  ctx.computed('doubleTen', (newState, oldState)=>{
    return newState.count * 10;
  }, ['count']);
  // 大多数情况你应该首先考虑定义模块计算函数,如果你想所有实例共享计算逻辑且计算函数只被执行一次,因为对于实例计算函数来说是每个实例都会自己单独触发的


  // 如果结果key和状态key命名一样,可简写为如下格式
  ctx.computed('count', ({count})=>count*2);

  // 定义实例观察函数, 和模块计算的理由一样,你应该优先考虑定义模块模块级别的观察函数
  ctx.watch('retKey', ()=>{}, ['count']);

  const fetchProducts = () => {
    const { type, sex, addr, keyword } = ctx.state;
    api.fetchProducts({ type, sex, addr, keyword })
      .then(products => ctx.setState({ products }))
      .catch(err => alert(err.message));
  };

  const inc = () => {
    ctx.setState({ count: this.state.count + 1 });
  }
  const dec = () => {
    ctx.setState({ count: this.state.count - 1 });
  }
  // 通过dispatch触发reducer函数
  const incD = () => {
    ctx.dispatch('inc');// 也可以写为: this.ctx.moduleReducer.inc()
  }
  const decD = () => {
    ctx.dispatch('dec');// 也可以写为: this.ctx.moduleReducer.dec()
  }

  // 返回结果将收集ctx.settings里
  return {
    inc,
    dec,
    incD,
    decD,
    fetchProducts,
    //同步type值, sync能够自动提取event事件里的值
    changeType: ctx.sync('type'),
  };
};
  • 基于classrenderProps, hook3种方式注册为concent组件
@register({module:'counter', setup})
class Counter extends Component {
  constructor(props, context){
    super(props, context);
    this.state = {tag: props.tag};// 私有状态
  }
  render() {
    // 此时的state由私有状态和模块状态合并而得
    const { count, products, tag } = this.state;
    // this.state 也可以写为 this.ctx.state 
    //const { count, products, tag } = this.ctx.state;

    const {inc, dec, indD, decD, fetchProducts, changeType} = this.ctx.settings;    

    return 'your ui xml...';
  }
}

const PropsCounter = registerDumb({module:'counter', setup})(ctx=>{
  const { count, products, tag } = ctx.state;
  const {inc, dec, indD, decD, fetchProducts, changeType} = ctx.settings;    
  return 'your ui xml...';
});

function HookCounter(){
  const ctx = useConcent({module:'counter', setup});
  const { count, products, tag } = ctx.state;
  const {inc, dec, indD, decD, fetchProducts, changeType} = ctx.settings;    

  return 'your ui xml...';
}

⚖️在线比较

💻在线示例

⌨️在线git

📰图文收集


图片介绍

cc状态分发流程

cc组件渲染流程