政务配置化引擎 - venom dsl 渲染引擎

@aligov/gov-venom-render

venom dsl 浏览器端渲染引擎,在线体验下:地址

快速上手

有两种接入方式:

JIT

浏览器端对 schema 数据进行编译解析及页面渲染。

对此,我们统一提供了@aligov/gov-venom-render组件,用法示例可以参考demo

他是 dsl 的浏览器端渲染引擎。具体用法说明如下:

返回一个组件

import render from '@aligov/gov-venom-render';
import ReactDOM from 'react-dom';

const dsl = `
    <div>123</div>
`; // 可以放本地,也可以从后端取回

const App = render(dsl);

ReactDOM.render(<App />, mountNode);

直接渲染进 DOM

import render from '@aligov/gov-venom-render';

const dsl = `
    <div>123</div>
`; // 可以放本地,也可以从后端取回

render(dsl, {
    container: '#container',
});

目前,render 中已默认内置了以下组件:

如果不够用,可以自己注册组件:

import React from 'react';

// 自开发组件示例
const MyComp = (props) => {
    return <div>开发者自己的组件示例 {props.name}</div>;
};

// 可以本地手动维护,也可以由后端提供,还可以在平台侧维护,平台还在开发中(手动捂脸)
const dsl = `
    <MyComp
        name='张三'
    />
`;

render(dsl, {
    components: {
        MyComp,
    },
    container: '#container',
});

组件式用法

为了进一步简化使用,我们还提供了组件式的使用方式,如:

import { Venom } from '@aligov/gov-venom-render';

const dsl = `
<>
    <State
        model={{
            methods: {

            },

            state: {
                name: '点我',
                count: 0
            },

            actions: {
                async asyncFetch(state, payload) {
                    this.methods.fetch(state);

                    this.methods.setMyName('my new name');

                    state.set({
                        name: 'hello world~'
                    });

                    setTimeout(() => {
                      this.methods.setMyName('3s change');
                    }, 3000);
                }
            }
        }}
    />

    <>
    <button onClick={() => {
        $store.dispatch('asyncFetch');
    }}>{$state.name}</button>

    <div>{$state.count}</div>

    <TextComp />
  </>
</>
`;

const TextComp = (props) => {
    return <div>text component test</div>;
};

export default (props) => {
    return (
        <Venom
            dsl={dsl}
            init={({ methods, ...others }, ...args) => {
                console.log('init args:', args);
            }}
            components={{
                TextComp,
            }}
            methods={{
                setMyName: (myName) => {
                    this.setState({ myName });
                },

                async fetch(state, payload) {
                    await new Promise((resolve, reject) => {
                        setTimeout(() => {
                            state.set('count', 100);

                            resolve();
                        }, 2000);
                    });
                },
            }}
        />
    );
};

当然,也支持直接传入 url 来完成渲染,并支持 format 做格式化:

import { Venom } from '@aligov/gov-venom-render';

export default (props) => {
    return (
        <Venom
            url='https://www.fastmock.site/mock/8b5ab209e9d13691117cba3b7baea9c4/dsl/venom/dsl'
            format={(res) => res}
            init={({ methods, ...others }, ...args) => {
                console.log('init args:', args);
            }}
            methods={{
                setMyName: (myName) => {
                    this.setState({ myName });
                },

                async fetch(state, payload) {
                    await new Promise((resolve, reject) => {
                        setTimeout(() => {
                            state.set('count', 100);

                            resolve();
                        }, 2000);
                    });
                },
            }}
        />
    );
};

model 的两种写法

我们把渲染引擎分为了 UI 跟 Model 两部分。model 部分既可以单独维护,也可以直接存放在 DSL 中。

单独维护

// model.ts
export default {
    state: {
        name: '张三',
    },
};
// index.html
<div>{$state.name}</div>
// index.ts
import model from './model';
import dsl from 'raw-loader!./index.html'; // 什么后缀都可以,这里只是示例
import render from '@aligov/gov-venom-render';

render(dsl, model, {
    container: '#container',
});

写在 DSL 中

也可以直接将 model 写在 DSL 中,示例如下:

// index.html
<>
    <State
        model={{
            state: {
                name: '张三',
            },
        }}
    />

    <div>{$state.name}</div>
</>
import dsl from 'raw-loader!./index.html'; // 什么后缀都可以,这里只是示例
import render from '@aligov/gov-venom-render';

render(dsl, {
    container: '#container',
});

Form 表单注册组件

表单方案我们使用的是集团统一中后台方案Formily,支持在入口方法中注册表单组件。

使用纯 DSL

纯 DSL 使用演示,适用于对 DSL 做统一管理的场景。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render from '@aligov/gov-venom-render';

const dsl = `
<>
    <State
        model={{
            methods: {
                init({ React, formily, components, registerComponents }) {
                    const { registerFormField, createControllerBox, connect } = formily;
                    const { Checkbox } = components;

                    registerFormField(
                        'custom-string',
                        connect()(props => <input {...props} value={props.value || ''} />)
                    );

                    const FormLayout = createControllerBox('controller-form-layout', props => {
                        return (
                            <div>
                                {props.children}
                                {props.schema['x-component-props']['attr']}
                            </div>
                        );
                    });

                    registerComponents({FormLayout});
                }
            }
        }}
    />

    <SchemaForm>
        <FormLayout attr='hello'>
            <Field type="custom-string" name="custom-string" title="Custom Field" />
        </FormLayout>
    </SchemaForm>
</>
`;

console.log('render:', render);

const App = render(dsl, {
    components: {},
});

ReactDOM.render(<App />, mountNode);

DSL 外注册组件

DSL 外注册组件使用演示,适用于在应用中部分使用 DSL,而非对 DSL 做统一管理。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render, { formily, components } from '@aligov/gov-venom-render';

const dsl = `
<SchemaForm>
    <FormLayout attr='hello'>
        <Field type="custom-string" name="custom-string" title="Custom Field" />
    </FormLayout>
</SchemaForm>
`;

const { registerFormField, createControllerBox, connect } = formily;
const { Checkbox } = components;

registerFormField(
    'custom-string',
    connect()((props) => <input {...props} value={props.value || ''} />)
);

const FormLayout = createControllerBox('controller-form-layout', (props) => {
    console.log('props:', props);

    return (
        <div>
            {props.children}
            {props.schema['x-component-props']['attr']}
        </div>
    );
});

const App = render(dsl, {
    components: {
        FormLayout,
    },
});

ReactDOM.render(<App />, mountNode);

AOT

工程侧的解析编译转换引擎,作为 webpack loader 存在,为:@aligov/gov-venom-loader

配合 build-scripts 工程构建使用,需要单独提供 build-scripts 的插件:

// build-plugin-venom.js
module.exports = async ({ onGetWebpackConfig, context }, pluginOptions = {}) => {
    onGetWebpackConfig((config) => {
        config.module
            .rule('venom')
            .test(/\.vnm$/)
            .use('@aligov/gov-venom-loader')
            .loader('@aligov/gov-venom-loader');
    });
};

然后记得在 build.json 中引入:

{
    "plugins": [
        [
            "build-plugin-fusion",
            {
                "themePackage": "@alifd/theme-design-pro"
            }
        ],
        [
            "build-plugin-moment-locales",
            {
                "locales": ["zh-cn"]
            }
        ],
        "@ali/build-plugin-ice-def",
        // 自定义插件
        [
            "./build-plugin-venom.js",
            {
                "forceBind": true
            }
        ]
    ]
}

然后就可以在项目中应用起来了,可以参考示例:gov-venom-example

TODO

[] formily 跟 alist 的注入需要统一封装实现,而不是类似现在的写死;

DEMO 列表

Simple Usage

基本用法。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render from '@aligov/gov-venom-render';

const dsl = `<>
  <State
    model={{
      state: {
        count: 0,
        editVisible: false
      },

      methods: {
        init() {
          console.log("init");
        }
      },

      actions: {
        add(state, payload) {
          const { count } = state;
          state.set('count', count + 1);
        },

        reduce(state, payload) {
          const { count } = state;
          state.set('count', count - 1);
        },

        async fetch(state, payload) {
          await new Promise((resolve, reject) => {
            setTimeout(() => {
              state.set('count', 100);

              resolve();
            }, 2000);
          });
        }
      }
    }}
  />

  <>
    <Button onClick={() => {
      $store.dispatch('add');
    }}>点击加1</Button>
    <Button onClick={() => {
      $store.dispatch('reduce');
    }}>点击减1</Button>
    <Button onClick={() => {
      $store.dispatch('fetch');
    }}>点击2s后面变100</Button>

    <div>{$state.count}</div>
  </>
</>`;

console.log('render:', render);

const App = render(dsl, {
    components: {},
});

ReactDOM.render(<App />, mountNode);

Form with dsl Usage

纯DSL使用演示,适用于对DSL做统一管理的场景。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render from '@aligov/gov-venom-render';

const dsl = `
<>
	<State
		model={{
			methods: {
				init({ React, formily, components, registerComponents }) {
                    const { registerFormField, createControllerBox, connect } = formily;
                    const { Checkbox } = components;

					registerFormField(
                        'custom-string',
                        connect()(props => <input {...props} value={props.value || ''} />)
                    );

					const FormLayout = createControllerBox('controller-form-layout', props => {
                        console.log('props:', props);

						return (
							<div>
								{props.children}
								{props.schema['x-component-props']['attr']}
							</div>
						);
                    });
                    
                    registerComponents({FormLayout});
				}
			}
		}}
	/>

	<SchemaForm>
		<FormLayout attr='hello'>
			<Field type="custom-string" name="custom-string" title="Custom Field" />
		</FormLayout>
	</SchemaForm>
</>
`;

console.log('render:', render);

const App = render(dsl, {
  components: {},
});

ReactDOM.render((
  <App />
), mountNode);

Form with js Usage

js代码中使用演示,适用于在应用中部分使用DSL,而非对DSL做统一管理。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render, { formily, components } from '@aligov/gov-venom-render';

const dsl = `
<SchemaForm>
    <FormLayout attr='hello'>
        <Field type="custom-string" name="custom-string" title="Custom Field" />
    </FormLayout>
</SchemaForm>
`;

const { registerFormField, createControllerBox, connect } = formily;
const { Checkbox } = components;

registerFormField(
    'custom-string',
    connect()(props => <input {...props} value={props.value || ''} />)
);

const FormLayout = createControllerBox('controller-form-layout', props => {
    console.log('props:', props);

    return (
        <div>
            {props.children}
            {props.schema['x-component-props']['attr']}
        </div>
    );
});

const App = render(dsl, {
  components: {
      FormLayout
  },
});

ReactDOM.render((
  <App />
), mountNode);

Search List with Alist

快速实现符合 GOV Design System 的搜索列表页面。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render from '@aligov/gov-venom-render';

const dsl = `
<>
  <State
    model={{
      state: {
        url: 'https://mocks.alibaba-inc.com/mock/alist/data'
      }
    }}
  />

  <>
    <List
      url={$state.url}
      pageSize={5}
    >
      <Filter>
        <Layout>
          <Filter.Item type="input" name="username" title="username" />
          <Filter.Item type="input" name="age" title="age" />
        </Layout>

        <Layout.ButtonGroup>
          <Search>搜索</Search>
          <Clear>重置</Clear>
        </Layout.ButtonGroup>
      </Filter>

      <Table>
        <Table.Column title="label" dataIndex="label" />
        <Table.Column title="value" dataIndex="value" />
      </Table>
      <Pagination />
    </List>
  </>
</>
`;

console.log('render:', render);

const App = render(dsl, {
    components: {},
});

ReactDOM.render(<App />, mountNode);

Form with dsl Usage

方法注入使用示例。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render from '@aligov/gov-venom-render';

const dsl = `
<>
	<State
		model={{
			methods: {
				
            },
            
            state: {
                name: 'hello',
                count: 0
            },

            actions: {
                async asyncFetch(state, payload) {
                    this.methods.fetch(state);

                    state.set({
                        name: 'hello world~'
                    });
                }
            }
		}}
	/>

	<div onClick={() => {
        $store.dispatch('asyncFetch');
    }}>{$state.name}</div>

    <div>{$state.count}</div>
</>
`;

console.log('render:', render);

const App = render(dsl, {
  components: {},
  methods: {
    async fetch(state, payload) {
        await new Promise((resolve, reject) => {
            setTimeout(() => {
                state.set('count', 100);

                resolve();
            }, 2000);
        });
    },
  }
});

ReactDOM.render((
  <App />
), mountNode);

render return Component

render()渲染完毕返回组件使用示例。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import render from '@aligov/gov-venom-render';

const dsl = `
<>
	<State
		model={{
			methods: {

            },

            state: {
                name: '点我',
                count: 0
            },

            actions: {
                async asyncFetch(state, payload) {
                    this.methods.fetch(state);

                    this.methods.setMyName('my new name');

                    state.set({
                        name: 'hello world~'
                    });

                    setTimeout(() => {
                      this.methods.setMyName('3s change');
                    }, 3000);
                }
            }
		}}
	/>

	<button onClick={() => {
      $store.dispatch('asyncFetch');
  }}>{$state.name}</button>
  <div>{$state.count}</div>
</>
`;

console.log('render:', render);

const App = render(dsl, {
  components: {},

  methods: {
    async fetch(state, payload) {
        await new Promise((resolve, reject) => {
            setTimeout(() => {
                state.set('count', 100);

                resolve();
            }, 2000);
        });
    },
  },

  init({ methods, ...others }, ...args) {
      console.log('init args:', args);
  },
});

class Comp extends Component {
    constructor(props, context) {
        super(props, context);

        this.state = {
            myName: 'test'
        };
    }

    render() {
        const { myName } = this.state;

        return <div>
            <div>dsl组件:</div>
            <App
                methods={{
                    setMyName: myName => {
                        this.setState({ myName })
                    }
                }}
                init={() => {
                  console.error('init');
                }}
            />
            <div>父组件里的变量:{myName}</div>
        </div>;
    }
}

ReactDOM.render((
  <Comp />
), mountNode);

used as Component with dsl

作为组件使用示例

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Venom } from '@aligov/gov-venom-render';

const dsl = `
<>
	<State
		model={{
			methods: {

            },

            state: {
                name: '点我',
                count: 0
            },

            actions: {
                async asyncFetch(state, payload) {
                    this.methods.fetch(state);

                    this.methods.setMyName('my new name');

                    state.set({
                        name: 'hello world~'
                    });

                    setTimeout(() => {
                      this.methods.setMyName('3s change');
                    }, 3000);
                }
            }
		}}
	/>

	<>
    <button onClick={() => {
        $store.dispatch('asyncFetch');
    }}>{$state.name}</button>

    <div>{$state.count}</div>

    <TextComp />
  </>
</>
`;

const TextComp = props => {
  return <div>text component test</div>;
};

class Comp extends Component {
  constructor(props, context) {
    super(props, context);

    this.state = {
      myName: 'test'
    };
  }

  render() {
    const { myName } = this.state;

    return <div>
      <div>dsl组件:</div>

      <Venom
        dsl={dsl}
        init={({ methods, ...others }, ...args) => {
          console.log('init args:', args);
        }}
        components={{
          TextComp
        }}
        methods={{
          setMyName: myName => {
            this.setState({ myName })
          },

          async fetch(state, payload) {
            await new Promise((resolve, reject) => {
              setTimeout(() => {
                state.set('count', 100);

                resolve();
              }, 2000);
            });
          },
        }}
      />
      <div>父组件里的变量:{myName}</div>
    </div>;
  }
}

ReactDOM.render((
  <Comp />
), mountNode);

used as Component with url

作为组件使用示例

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Venom } from '@aligov/gov-venom-render';

class Comp extends Component {
  constructor(props, context) {
    super(props, context);

    this.state = {
      myName: 'test'
    };
  }

  render() {
    const { myName } = this.state;

    return <div>
      <div>dsl组件:</div>

      <Venom
        url='https://www.fastmock.site/mock/8b5ab209e9d13691117cba3b7baea9c4/dsl/venom/dsl'
        init={({ methods, ...others }, ...args) => {
          console.log('init args:', args);
        }}
        methods={{
          setMyName: myName => {
            this.setState({ myName })
          },

          async fetch(state, payload) {
            await new Promise((resolve, reject) => {
              setTimeout(() => {
                state.set('count', 100);

                resolve();
              }, 2000);
            });
          },
        }}
      />
      <div>父组件里的变量:{myName}</div>
    </div>;
  }
}

ReactDOM.render((
  <Comp />
), mountNode);