小巧玲珑的react框架(第二弹)正式命名--aomini

1,458 阅读9分钟

前戏

曾经我以为已经过了看偶像剧的年纪,后来无意中看了泰版浪漫满屋,然后控寄不住寄己的体内的洪荒之力,继续追完了泰版的恶作剧之吻,--有颜+演技时刻在线(虽然还是偶像剧狗血套路,不过不影响我被圈粉)。我忽然发现,原来热情还在,只是现在大多国内的流量小生小花们已经挑逗不到我了。看看国内现在的电视剧,生硬的台词,僵硬的表情,像一盆盆冷水一点点浇凉我对剧的热情,有时候不禁会想,难道没人对自己的作品负责吗?哦,大家并没有进错房间,这不是在聊电视剧,咱这还是技术文,所以我就不继续吐槽置评了,我曾经下定过决心,只要一天还在出作品,就要对自己的东西负点责任。情怀,是个好东西,得留着点。回到我们的主题,咱这个小框架再不济也是个框架,得起个名字先。我被这两部泰剧圈粉主要是因为女主aom可爱又认真的戏,干脆就叫aom了,而且我希望这是个方便任意玩耍,轻量迷你的小框架,所以,我给它起个名字叫做--aomini。大家不要被我带跑偏了,我安利的不是这两个泰剧,是介绍我的aomini,😄。
先回顾上一篇我只是用最简单的代码介绍了一下aomini的思路,那样的破砖烂瓦不修整一番怎么能用呢?好了,接下来我就带着大家一起来修整一番。

激情

如果大家不记得上一篇讲的啥破砖烂瓦,可以戳这里回去看看。上一篇完全是以糊涂流水账的形式盖的破瓦房,房屋结构啥的都是一股脑叠上去的,毫无结构可言,接下来我们来将这个破房子分解一下。首先,底层结构跟上层结构得先拆开,底层结构属于libs,上层结构就是业务逻辑,也就是我们肉眼可见的那一层,入口文件,以及那几个模块文件肯定都属于业务逻辑了,至于store,Provider和connect函数肯定属于libs范畴了。拆解后文件结构如下所示,


相信使用过react-redux的同学对libs下的这三个名称应该很熟悉了,不过我们这里跟react-redux中还是有很大区别,虽然思路还是类似的,阅读过react-redux源码的同学可能会比较清楚一点,那接下来就对我们的框架的实现思路进行深入讲解一下。虽然并没有那么复杂,但是为了照顾不那么熟悉的同学,分析之前,我们先搞清楚这三个东西的具体指责是什么,其中Store主要可以说是state的大总管;Provider可以说是入口,store会从这个入口props的方式传入,通过react context 上下文的方式进行全局定义,方便业务下的其他组件获取调用,这种方式就是为了解决任意组件之间通信非相关组件需要感知的问题;connect其实是个HOC高阶组件。好了,再让我们回顾一下我们简单的框架模型,store<=>UI,对比一下react原生的框架模型是这样的state<=>UI,对比这两个模型可以得出一个简单的Store的设计思路,store管理了state,并且当UI触发Store中状态更新时,由Store来调用组件setState进行模块的UI更新。根据这么个直白的思路设计的Store对象代码如下,与之前出入不大:

let store = {
    //目标组件对象
    contexts:[],
    //state数据
    state:{
        m1Var:"111"
    },
    //注册绑定组件与状态数据,用于setState
    bindContext(_context){
        this.contexts.push(_context);
        return this.state;
    },
    unbindContext(_context){
        for(let i = 0; i < this.contexts.length; i++){
            if(this.contexts[i] === _context){
                this.contexts[i] = null;
                console.log(_context);
            }
        }
    },
    setState(bs_state,context){
        let newState = Object.assign(this.state,bs_state);
        console.log(".......",newState)
        // this.state = {};
        this.contexts.map((item)=>{
            if(!item) return;
            React.Component.prototype.setState.call(item,bs_state);
        })

    }
}

store没有做太大变化,只是完善了一下,组件装载时,调用bindContext将组件的上下文关系添加到contexts列表,当数据中心状态变化时,调用store的setState方法,该方法实质是将所有绑定的组件进行setState调用。如此,便简单实现了状态变化与UI的数据流关系。
每个组件都需要接收store对象,因此就需要将store放入顶层父组件的context中,也就是Provider这个入口组件,该组件代码如下:

class Provider extends React.Component{
    getChildContext(){
        return {
            store:this.props.store
        }
    }
    constructor(props){
        super(props);
    }
    render(){
        return React.Children.only(this.props.children);
    }
}
Provider.childContextTypes = {
    store:PropTypes.object
}

该组件与redux中一样,子组件只接受单个元素。然后就是将props中传入的store元素进行变量的全局化,当然这里暂时实现比较粗暴,咱还没有做到像redux一样过多考虑个性化扩展,但说白了redux实现核心也就是这么个玩意儿。
接下来我们再回过头来观察一下上一篇的组件代码,每一个模块都有一堆样板代码,store.bindContext(this);尤其之前只是单文件简单的合并写法,模块拆成独立文件后,store又是需要从上下文中引用的,因此样板代码至少又这些:
1,组件装载的时候

let store = context.store;
store.bindContext(this);

2,组件卸载的时候需要移除被卸载的组件绑定关系

componentWillUnmount(){
    this.store.unbindContext(this);
}

每个组件都要在逻辑中如此调用,需要封装一下,况且,业务组件最好只关注业务点,这种非业务逻辑的样板代码不该出现在业务组件中,那这种通用逻辑调用的情况下,我们的高阶组件就需要派上用场了。类似redux中的connect组件代码如下:

let connect = function(wrappedCompo){
    class ConnectHoC extends React.Component{
        constructor(props,context){
            super();
            this.store = context.store;
            this.store.bindContext(this);
        }
        render(){
            return React.createElement(wrappedCompo,{store:this.store});
        }
        componentWillUnmount(){
            this.store.unbindContext(this);
        }
    }
    ConnectHoC.contextTypes = {
        store:PropTypes.object
    }

    return ConnectHoC;
}

这个connect高阶组件接收一个实际业务组件为参数,返回一个包含该组件的新组件,并实现了store通过props的传递,实际业务组件获取store直接从props即可,也许你会问store已经存在context中,直接去取就是了,为啥还要通过props传入,还是一样的道理,那样的话业务组件会多一些获取全局变量的样板代码,并且违背了业务组件只需要关注业务的原则。好了,现在这么一来,我们的业务组件只需要关注自己的业务点以及数据在哪里获取,无需过度关心其他。
并且就跟react-redux中使用是一样的,组件都使用connect进行包装即可,伪代码如下:

class Module extends React.Component{
    constructor(){
        super();
    }
    render(){

    }
}
let ModuleHoC = connect(Module);

因此,Provider组件下的所有经过connect包装过的组件装载都会自动进行上下文绑定,并使用store集中管理数据状态,当状态改变,所有绑定的组件进行setState操作,即完成了UI的更新。

高潮

很明显,这个设计思路就是来源于react-redux的设计思想,react-redux是个好东西,只是我在实践中发觉,react本身其实已经相当好用了,只是有些地方比如状态管理部分只用原生react的话,开发过程会变得十分不优雅,既然如此,那我们或许只需要将react中不那么优雅的东西适当折叠修整一番,就能变得更好用了。react-redux中强调逻辑与视图之间完全分离,这是很好的思想,但是无论flux或者redux的设计初衷,其实都是为了更合理地管理状态而来的,react的设计原本就并未将UIrender与逻辑完全割裂,并非像vue中将template完全进行了剥离。况且,逻辑与UI的分离并非必须分到两个不同的文件中去才是分离,这样反而从编码层面上带来不少的干扰,缺少码代码的快感。
不过目前来说,aomini虽然实现了功能,但是绝对比不上react-redux在整体设计上的优雅。react-redux是基于redux的动作订阅模式,使用了监听者模式,每一个模块的设计都是根据单一职责的原则,UI中触发state的更新必须通过派发一个action,通过监听器进行监听每一个action,不过这里的监听器是弱化了的,并没有针对每一个action进行细分监听,这里大家需要注意,其实这个设计是很巧妙且又合理的,因为react是基于状态变化的,而非我们之前熟悉的命令式开发模式,一切都是围绕整体的状态,而非某个动作,因此无论action如何指示,最终只是转化成整体状态变化的读取。reducers进行状态计算,并使用connect组件的参数mapStateToProps函数作为状态选择器,进行子状态select,完美地解决了特定状态的读取,以上处理就优雅地完成了UI=>store=>action=>reducer=>UI这样一个完整闭环。aomini上有flux和react-redux的影子,但是本质上来说,到目前为止,aomini跟后两者并不是同一种东西,flux是一套完整的web架构思想,react-redux是从flux发展而来的比较完善的开发框架,而aomini是从两者之中取出的部分思想,变得更加灵活,尽管不算是一个完整的框架,不过我从实际开发中感受到,配合原本已经足够完美的react,已经够用了。

事后--结语

框架上的东西没有绝对的好与不好,杀鸡用牛刀未尝不可,只是这把刀不可太重,不然杀只鸡比宰一头牛还累,亏本买卖。一个小工程,总共业务几句代码,非得上一堆牛逼哄哄的框架,最后出来几大m的代码量,反而拖累页面效率,这不就是得不偿失。以上对aomini的核心代码设计思路介绍了一下,目前只是一些核心骨干代码,后续接下来就是需要继续完善其中的细节,代码已上传github,并实时进行更新,有兴趣的同学们就敬请关注了。希望跟大家一起交流。
需要的同学,可以戳这里签出代码:react-aomini
会继续实时更新,欢迎各位拍砖!