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 --