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

amis源码解析——渲染器的注册和匹配

之前谈到了完全声明式编程的思想和用处,这篇文章就从一个百度开源的完全配置化框架amis源码入手,分析一下实现的关键点。

项目结构

完全声明式的实现思路其实一句话就能概括,就是JSON解析到工作流组件的匹配到渲染。在amis这个项目中,工作流组件被声明为渲染器renders ,这一层渲染器外部对JSON提供API,内部又包裹着基础组件,所以项目结构上可以分为三部分:

  • 底层核心代码:管理渲染器的注册和匹配等,放在src/
  • 渲染器:待注册的工作流组件,JSON解析后会渲染出这一层,放在src/renderers
  • 基础组件:渲染器内部的基础组件,可以自己实现也可以借助三方组件库,放在src/components

下面就从底层核心代码开始,分析整个框架的运行机制。

JSON匹配渲染器——render

这个框架的使用入口就是该函数,传入JSON,即可得到最终的渲染器组件。进入src/factory.tsx看实现

export function render(schema, props, options, pathPrefix) {
  options = {
    ...defaultOptions,
    ...options
  }
     let store = stores[options.session || 'global'] || (stores[options.session || 'global'] = RendererStore.create({}, {
        ...options,
        fetcher: options.fetcher ? wrapFetcher(options.fetcher) : defaultOptions.fetcher,
        confirm: options.confirm ? promisify(options.confirm) : defaultOptions.confirm,
    }));

    const env = getEnv(store);
    const theme = props.theme || options.theme || 'default';
    env.theme = getTheme(theme);

    return (
        <ScopedRootRenderer
            {...props}
            schema={schema}
            pathPrefix={pathPrefix}
            rootStore={store}
            env={env}
            theme={theme}
        />
}

可以看到,关键就做了两件事,一是创建了一个rendererStore,这个store用来存储渲染器可能用到的store,二是返回了一个ScopedRootRenderer,这个就是根渲染器了。看看这个渲染器声明的地方。

export const ScopedRootRenderer = Scoped(RootRenderer)

Scoped这个高阶函数包了一层。先看看纯粹的根组件RootRenderer内容的核心部分。

export class RootRenderer extends React.Component<RootRendererProps> {
        // ...

    render() {
             // ...
        const {
            schema,
            rootStore,
            env,
            pathPrefix,
            location,
            data,
            ...rest
        } = this.props;

        const theme = env.theme;
        const query = location && location.query 
            || location && location.search && qs.parse(location.search.substring(1))
            || window.location.search && qs.parse(window.location.search.substring(1));

        const finalData = query ? createObject({
            ...(data && data.__super ? data.__super: null),
            ...query,
            query
        }, data) : data;

        return (
            <RootStoreContext.Provider value={rootStore}>
                <ThemeContext.Provider value={this.props.theme || 'default'}>
                    {renderChild(pathPrefix || '', isPlainObject(schema) ? {
                        type: 'page',
                        ...(schema as Schema)
                    } : schema, {
                        ...rest,
                        resolveDefinitions: this.resolveDefinitions,
                        location: location,
                        data: finalData,
                        env,
                        classnames: theme.classnames,
                        classPrefix: theme.classPrefix
                    }) as JSX.Element}
                </ThemeContext.Provider>
            </RootStoreContext.Provider>
        );
    }
}

作用是从外部构造一系列全局数据,通过Context注入到内部。内部实现走renderChild函数,核心参数在于schema,也就是我们传入的JSON了。看看这个函数如何解析。

export function renderChild(prefix:string, node:SchemaNode, props:renderChildProps):ReactElement {
    // ...
    if (
        exprProps 
        && (
            exprProps.hidden
            || exprProps.visible === false
            || schema.hidden
            || schema.visible === false
            || props.hidden
            || props.visible === false
        )
    ) {
        return null;
    }

    return (
        <SchemaRenderer
            {...props}
            {...exprProps}
            schema={schema}
            $path={`${prefix ? `${prefix}/` : ''}${schema && schema.type || ''}`}
        />
    );
};

这里主要是对schema的一些参数做处理,比如可见性等,不可见就直接返回null。满足条件继续通过SchemaRenderer这个组件解析。看SchemaRenderer的关键部分

class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
        // ...
    componentWillMount() {
        this.resolveRenderer(this.props);
    }
      // ...
        resolveRenderer(props:SchemaRendererProps):any {
         // ...
        this.renderer = rendererResolver(path, schema, props);
        return schema;
    }
        // ...
    render():JSX.Element | null {
        let {
            $path,
            schema,
            ...rest
        } = this.props;
                // ...
        const renderer = this.renderer as RendererConfig;
        schema = filterSchema(schema, renderer, rest);
        const {
            data: defaultData,
            ...restSchema
        } = schema;
        const Component = renderer.component;

        return (
            <Component
                {...theme.getRendererConfig(renderer.name)}
                {...restSchema}
                {...rest}
                defaultData={defaultData}
                $path={$path}
                ref={this.refFn}
                render={this.renderChild}
            />
        );
    }
}

即将挂载时,通过自身的resolveRenderer方法调用rendererResolver解析schema赋值到renderer上,渲染时直接取renderer上的component即可。可以看到rendererResolver这个方法完成了schema到具体组件的过程。

export function resolveRenderer(path:string, schema?:Schema, props?:any): null | RendererConfig {
    if (cache[path]) {
        return cache[path];
    } else if (path && path.length > 1024) {
        throw new Error('Path太长是不是死循环了?');
    }

    let renderer:null | RendererConfig = null;

    renderers.some(item => {
        let matched = false;

        if (typeof item.test === "function") {
            matched = item.test(path, schema, resolveRenderer);
        } else if (item.test instanceof RegExp) {
            matched = item.test.test(path);
        }

        if (matched) {
            renderer = item;
        }

        return matched;
    });
      // ...

    return renderer;
}

到具体解析就很简单了,有一个全局的渲染器数组,里面每个渲染器都有自己的test参数用来匹配JSON对应字段构造出的path,当能匹配上时,就取出对应渲染器,至此就完成了JSON到渲染器的实现,值得注意的是renderChild会传入到解析后的渲染器中,也就是JSON的深度解析会借助该参数完成。

那么现在我们知道了有个全局渲染器数组用来匹配,将自己的渲染器注册到该数组中需要注意什么呢?这就是下面这个核心API关注的地方了。

渲染器的注册——Renderer

具体使用上,是通过该函数装饰渲染器组件完成。看代码。

export function Renderer(config:RendererBasicConfig) {
    return function<T extends RendererComponent>(component:T):T {
        const renderer = registerRenderer({
            ...config,
            component: component
        });
        return renderer.component as T;
    }
}

一个适配装饰器的高阶函数实现,继续走registerRenderer注册并传入配置和被装饰组件。

export function registerRenderer(config:RendererConfig):RendererConfig {
  // ...

    config.weight = config.weight || 0;
    config.Renderer = config.component;
    config.name = config.name || `anonymous-${anonymousIndex++}`;

    if (~rendererNames.indexOf(config.name)) {
        throw new Error(`The renderer with name "${config.name}" has already exists, please try another name!`);
    }

    if (config.storeType && config.component) {
        config.component = HocStoreFactory({
            storeType: config.storeType,
            extendsData: config.storeExtendsData
        })(observer(config.component));
    }

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

    const idx = findIndex(renderers, item => (config.weight as number) < item.weight);
    ~idx ? renderers.splice(idx, 0, config) : renderers.push(config);
    rendererNames.push(config.name);
    return config;
}

相当简单,最终就是一个数组的push调用。但是要注意的是,根据传入参数的不同,有机会用两个高阶函数包裹,一个是HocStoreFactory,一个是又一次见面的Scoped。这两个高阶函数有什么用呢?这就涉及到渲染器的状态管理和通信了,下一篇文章再谈。

-- EOF --

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