The Night We Stood Lyn
  1. 1 The Night We Stood Lyn
  2. 2 かかってこいよ NakamuraEmi
  3. 3 Quiet Storm Lyn
  4. 4 Libertus Chen-U
  5. 5 Hypocrite Nush
  6. 6 Time Bomb Veela
  7. 7 One Last You Jen Bird
  8. 8 Last Surprise Lyn
  9. 9 Life Will Change Lyn
  10. 10 Flower Of Life 发热巫女
  11. 11 Warcry mpi
2019-07-13 20:41:33

amis源码解析——状态管理和渲染器通信

上篇文章分析了渲染器的注册和调用,那么渲染器在工作过程中免不了要处理数据持久化和相互之间通信的问题。这篇文章就谈谈amis在这方面的处理方式。

HocStoreFactory

其实状态管理的相关入口上篇在分析渲染器的注册的时候就已经提到了。关键在于如下这段代码

export function registerRenderer(config: RendererConfig) {
  // ...
  if (config.storeType && config.component) {
    config.component = HocStoreFactory({
      storeType: config.storeType,
      extendsData: config.storeExtendsData
    })(observer(config.component))
  }
  // ...
}

注册渲染器时会调用这个高阶函数,传入一个关键参数storeType用于声明该渲染器拥有的store类型,其中就聚合了相关状态的存储和处理逻辑。进入这个高阶函数看看,其核心在componentWillMount部分。

       componentWillMount() {
                const rootStore = this.context;
                this.renderChild = this.renderChild.bind(this);
                this.refFn = this.refFn.bind(this);

                const store = this.store = rootStore.addStore({
                    id: guid(),
                    path: this.props.$path,
                    storeType: renderer.storeType,
                    parentId: this.props.store ? this.props.store.id : ''
                });

                if (renderer.extendsData === false) {
                    store.initData(createObject((this.props.data as any) ? (this.props.data as any).__super : null, {
                        ...this.formatData(this.props.defaultData),
                        ...this.formatData(this.props.data)
                    }));
                } else if (this.props.scope || this.props.data && (this.props.data as any).__super) {
                    if (this.props.store && this.props.data === this.props.store.data) {
                        store.initData(createObject(this.props.store.data, {
                            ...this.formatData(this.props.defaultData)
                        }))
                    } else {
                        store.initData(createObject((this.props.data as any).__super || this.props.scope, {
                            ...this.formatData(this.props.defaultData),
                            ...this.formatData(this.props.data)
                        }))
                    }
                } else {
                    store.initData({
                        ...this.formatData(this.props.defaultData),
                        ...this.formatData(this.props.data)
                    });
                }
            }

首先获取到通过Context从根注入的根store,再通过根store的addStore 新增自身所需的store,并根据不同的条件调用initData初始化自身store的数据,然后在render时,将storeprops传入到下层,渲染器本身就能利用到这个store进行相关操作了。

那么再看看具体的store有哪些通用逻辑。先进入rootStore

const allowedStoreList = [
    ServiceStore,
    FormStore,
    ComboStore,
    CRUDStore,
    TableStore,
    ListStore,
    ModalStore
];

export const RendererStore = types
    .model('RendererStore', {
        storeType: 'RendererStore',
        stores: types.map(types.union({
            eager: false,
            dispatcher: (snapshort:SIRendererStore) => {
                for (let storeFactory of allowedStoreList) {
                    if (storeFactory.name === snapshort.storeType) {
                        return storeFactory;
                    }
                }

                return iRendererStore;
            }
        }, iRendererStore, ...allowedStoreList)),
    })
    .views(self => ({
        get fetcher() {
            return getEnv(self).fetcher
        },

        get notify() {
            return getEnv(self).notify
        },

        get isCancel():(value:any) => boolean {
            return getEnv(self).isCancel
        }
    }))
    .views(self => ({
        getStoreById(id:string) {
            return self.stores.get(id);
        }
    }))
    .actions(self => ({
        addStore(store:SIRendererStore):IIRendererStore {
            if (self.stores.has(store.id as string)) {
                return self.stores.get(store.id) as IIRendererStore;
            }
            self.stores.put(store);
            return self.stores.get(store.id) as IIRendererStore;
        },

        removeStore(store:IIRendererStore) {
            detach(store);
        }
    }));

可以看到store底层是基于mobx-state-tree的。allowedStoreList限制了可以添加的store类型,都是业务通用的渲染器类型。rootStore本身仅提供一个map,用来存储注册的store和提供解注册的方法,以及提供fetchernotify用来获取对应的环境变量,这两个参数分别管理ajax和弹窗相关的逻辑。

更细化的store其实分为三层,第一层为iRenderer

export const iRendererStore = types
    .model('iRendererStore', {
        id: types.identifier,
        path: '',
        storeType: types.string,
        hasRemoteData: types.optional(types.boolean, false),
        data: types.optional(types.frozen(), {}),
        updatedAt: 0, // 从服务端更新时刻
        pristine: types.optional(types.frozen(), {}),
        parentId: types.optional(types.string, ''),
        action: types.optional(types.frozen(), undefined),
        dialogOpen: false,
        dialogData: types.optional(types.frozen(), undefined),
        drawerOpen: false,
        drawerData: types.optional(types.frozen(), undefined),
    })
    .views((self) => {
        return {
            get parentStore():IIRendererStore | null {
                return self.parentId && getRoot(self) && (getRoot(self) as IRendererStore).storeType === 'RendererStore'
                    ? (getRoot(self) as IRendererStore).stores.get(self.parentId)
                    : null;
            }
        };
    })
    .actions((self) => {
        const dialogCallbacks = new Map();

        return {
            initData(data:object = {}) {
                self.pristine = data;
                self.data = data;
            },

            reset() {
                self.data = self.pristine;
            },

            updateData(data:object = {}, tag?: object) {
               // ...
            },

            setCurrentAction(action:object) {
                self.action = action;
            },

            openDialog(ctx: any, additonal?:object, callback?: (ret:any) => void) {
                // ...
            },

            closeDialog(result?:any) {
               // ...
            },

            openDrawer(ctx: any, additonal?:object, callback?: (ret:any) => void) {
                // ...
            },

            closeDrawer(result?:any) {
               // ...
            }
        };
    });

其中提供了最通用的部分,包括initData,DialogDrawer相关处理。更具体的store类型都是从这一层继承而来。再下一层是service

export const ServiceStore = iRendererStore
    .named('ServiceStore')
    .props({
        msg: '',
        error: false,
        fetching: false,
        saving: false,
        busying: false,
        checking: false,
        initializing: false,
        schema: types.optional(types.frozen(), null),
        schemaKey: '',
    })
    .views(self => ({
        get loading() {
            return self.fetching || self.saving || self.busying || self.initializing;
        }
    }))
    .actions(self => {
        let fetchCancel: Function | null;
        let fetchSchemaCancel: Function | null;

        function markFetching(fetching = true) {
            self.fetching = fetching;
        }

        function markSaving(saving = true) {
            self.saving = saving;
        }

        function markBusying(busying = true) {
            self.busying = busying;
        }

        function reInitData(data:object | undefined) {
            const newData = extendObject(self.pristine, data);
            self.data = self.pristine = newData;
        }

        function updateMessage(msg?:string, error: boolean = false) {
            self.msg = String(msg) || '';
            self.error = error;
        }

        function clearMessage() {
            updateMessage('');
        }

        const fetchInitData:(api:Api, data?:object, options?:fetchOptions) => Promise<any> = flow(function *getInitData(api:string, data:object, options?:fetchOptions) {
                  // ...
        });

        const fetchData:(api:Api, data?:object, options?:fetchOptions) => Promise<any> = flow(function *getInitData(api:string, data:object, options?:fetchOptions) {
                     // ...
        });

        const saveRemote:(api:Api, data?:object, options?:fetchOptions) => Promise<any> = flow(function *saveRemote(api:string, data:object, options:fetchOptions = {}) {
                    // ...
        });

        const fetchSchema:(api:Api, data?:object, options?:fetchOptions) => Promise<any> = flow(function *fetchSchema(api:string, data:object, options:fetchOptions = {}) {
                  // ...
        });

        const checkRemote:(api:Api, data?:object, options?:fetchOptions) => Promise<any> = flow(function *checkRemote(api:string, data:object, options?:fetchOptions) {
          // ...
        });

        return {
            markFetching,
            markSaving,
            markBusying,
            fetchInitData,
            fetchData,
            reInitData,
            updateMessage,
            clearMessage,
            saveRemote,
            fetchSchema,
            checkRemote
        };
    });

提供了更加细化的数据的获取和更新部分,具体的store如果需要和外界进行数据交互的能力的话,都要继承自这层。第三层就是对应到具体渲染器需求的相关逻辑了,这里就不贴细节代码了。总结一下就是:对于需要管理状态的渲染器,比如列表,表单等,通过HocStoreFactory这个高阶函数注册对应的store,并当做props传入到下层,store从通用到细化分为三层,在渲染器挂载前注册,并完成store的初始化。

Scoped

有了状态管理的能力,最通用的业务场景已经可以实现了。举个简单例子,JSON内声明列表数据接口,页面初始化时解析到该字段时调用fetcher保存到自身store,再传入到下层渲染器进一步处理并渲染。搜索时,关联JSON内相关字段到对应渲染器的store,利用store内部子程序调用重新初始化数据即可。这里就可以发现一个问题,搜索时,相关字段如何对应到指定渲染器?搜索相关的Input在渲染器内部还好,直接调用store即可,但有些情况下,搜索相关的部分来自于其他渲染器,这就引出了另外一个问题,渲染器之间如何通信?上下层的通信可以通过props和事件解决,跨层级或者同级通信的话就是Scoped这个高阶函数要处理的事了。同样,和store类似,在注册渲染器时。根据传入参数决定,是否需要这个通信能力,通过Scoped这个高阶函数提供。

    if (config.isolateScope) {
        config.component = Scoped(config.component);
    }

进入到这个函数内部。

export function HocScoped<T extends {
    $path?: string;
    env: RendererEnv;
}>(ComposedComponent: React.ComponentType<T>):React.ComponentType<T & {
    scopeRef?: (ref: any) => void
}> & {
    ComposedComponent: React.ComponentType<T>
} {
    class ScopedComponent extends React.Component< T & {
        scopeRef?: (ref: any) => void
    }> {
        static displayName = `Scoped(${ComposedComponent.displayName || ComposedComponent.name})`;
        static contextType = ScopedContext;
        static ComposedComponent = ComposedComponent;
        ref:any;

        getWrappedInstance() {
            return this.ref;
        }

        @autobind
        childRef(ref: any) {
            while (ref && ref.getWrappedInstance) {
                ref = ref.getWrappedInstance();
            }

            this.ref = ref;
        }

        scoped = createScopedTools(this.props.$path, this.context, this.props.env)

        componentWillMount() {
            const scopeRef = this.props.scopeRef;
            scopeRef && scopeRef(this.scoped);
        }

        componentWillUnmount() {
            const scopeRef = this.props.scopeRef;
            scopeRef && scopeRef(null);
        }

        render() {
            const {
                scopeRef,
                ...rest
            } = this.props;

            return (
                <ScopedContext.Provider value={this.scoped}>
                    <ComposedComponent {...rest as any /* todo */} ref={this.childRef} />
                </ScopedContext.Provider>
            )
        }
    }

    hoistNonReactStatic(ScopedComponent, ComposedComponent);
    return ScopedComponent;
};

这个高阶组件本身做的事很简单,就是向下层注入scoped对象,该对象由createdScopedTools创建,可以看到关键都在这个函数中。

function createScopedTools(path?:string, parent?:AlisIScopedContext, env?: RendererEnv):IScopedContext {
    const components:Array<ScopedComponentType> = [];

    return {
        parent,
        registerComponent(component:ScopedComponentType) {
            // 不要把自己注册在自己的 Scoped 上,自己的 Scoped 是给孩子们注册的。
            if (component.props.$path === path && parent) {
                return parent.registerComponent(component);
            }

            if (!~components.indexOf(component)) {
                components.push(component);
            }
        },

        unRegisterComponent(component:ScopedComponentType) {
            // 自己本身实际上注册在父级 Scoped 上。
            if (component.props.$path === path && parent) {
                return parent.unRegisterComponent(component);
            }

            const idx = components.indexOf(component);

            if (~idx) {
                components.splice(idx, 1);
            }
        },

        getComponentByName(name:string) {
            if (~name.indexOf('.')) {
                const paths = name.split('.');
                const len = paths.length;

                return paths.reduce((scope, name, idx) => {
                    if (scope && scope.getComponentByName) {
                        const result = scope.getComponentByName(name);
                        return result && idx < (len - 1) ? result.context : result;
                    }

                    return null;
                }, this);
            }

            const resolved = find(components, component => component.props.name === name || component.props.id === name);
            return resolved || parent && parent.getComponentByName(name);
        },

        getComponents() {
            return components.concat();
        },

        reload(target:string, ctx:any) {
            const scoped = this;

            if (target === 'window') {
                return location.reload();
            }

            let targets = typeof target === 'string' ? target.split(/\s*,\s*/) : target;
            targets.forEach(name => {
                const idx2 = name.indexOf('?');
                let query = null;

                if (~idx2) {
                    query = dataMapping(qs.parse(name.substring(idx2 + 1)), ctx);
                    name = name.substring(0, idx2);
                }

                const idx = name.indexOf('.');
                let subPath = '';

                if (~idx) {
                    subPath = name.substring(1 + idx);
                    name = name.substring(0, idx);
                }

                const component = scoped.getComponentByName(name);
                component && component.reload && component.reload(subPath, query, ctx);
            });
        },

        send(receive:string, values:object) {
            const scoped = this;
            let receives = typeof receive === 'string' ? receive.split(/\s*,\s*/) : receive;

            // todo 没找到做提示!
            receives.forEach(name => {
                const idx = name.indexOf('.');
                let subPath = '';

                if (~idx) {
                    subPath = name.substring(1 + idx);
                    name = name.substring(0, idx);
                }

                const component = scoped.getComponentByName(name);

                if (component && component.receive) {
                    component.receive(values, subPath)
                } else if (name === 'window' && env && env.updateLocation) {
                    const query =  {
                        ...location.search ? qs.parse(location.search.substring(1)) : {},
                        ...values
                    };
                    const link = location.pathname + '?' + qs.stringify(query);
                    env.updateLocation(link);
                }
            });
        }
    }
}

首先看registerComponent/unregisterComponent方法,传入参数为当前的渲染器,当某一层渲染器注册了Scoped后,下层渲染器能拿到scoped对象,并调用这对方法完成注册和解注册,并保存到components数组中,也就是说,同级的或者跨层级的渲染器,都能注册到上层有scoped的渲染器中,并能拿到该scoped。当需要通信时,调用scopedsend方法并传入数据,就会从components数组中找到对应的渲染器,并调用对应实现的receive方法接受数据,至此就完成了通信。

小结

关于amis的核心机制其实就是这两篇文章谈到的东西,还有些不错的设计都属于renderer部分,主要涉及到formcurd,由于篇幅原因就不展开了,以后有空再写。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「JavaScript」「工程化」 标签。