Why do you want a visual page building system
-
Unify the style and interaction of each micro application page in the micro front end architecture
Our company supply chain saas system and multiple independent deployment, technology stack is not unified system, the style of these systems, interaction, through the page visual building system generated pages use the same set of bottom component library, which can meet the style, interaction, and the style of the face after and interactive changes supports batch modification
-
Shorten regular page development time
Our company’s supply chain is a toB saas system, there is considerable similar page, repeated development page easy to kill the enthusiasm of developers, organize page in common, through visual structures, system development and to reduce the pages degrees, the complex logic allows developers to concentrate on the development of the page
The whole visual scaffolding system is divided into three parts, which are configuration page (setting), view page (View) and JSON Schema. The configuration page generates the JSON Schema, and the view page consumes the JSON Schema
Writing in the front
- Use Codemirror to implement code for editing custom behavior on a visual interface
- Enter the interface address only
/
At the beginning of the relative path, the view page determines the environment of the interface at run time - Cool-path is used to realize the value by field path and modify the value by field path
- Use new Function to convert json Schema strings into objects or functions on the view page
The types of pages that can be created are: Lists, details, and forms. The details and form pages are designed in little different ways, and the list pages are designed in much different ways than the other two
function
List of pp.
Define button operation, define search items (single row search box event selector drop-down box cascade selector batch input search), dynamic access to drop-down box and cascade selector alternative data, list sorting, table row multiple selection, customize the operation of table row, customize the display content of table column
Details page forms page
Form linkage, table data format validation, one-column layout, multi-column layout, table paging, custom text display content
List page design
The analysis of our company’s list page layout has a uniform pattern. The list consists of an action button in the upper right corner, a header, breadcrumbs in the upper left corner, a filter area right above, a table in the middle, and a pager right below. The table in the middle is mandatory, and the rest is optional. As shown in the figure below:
Since the list page has a unified layout mode, in the configuration page of the list, I divided the list page into several independent areas for configuration, as shown in the figure below:
The data entered in the basic configuration area is not displayed in the list view page. The data entered in this area is only for the convenience of searching for the list configuration data
Global configuration
As a result of the list page is a dynamic page, the page most of the data from the backend interface provided by the developer, each interface corresponding to the multiple environments, each interface in our company at least the development environment, test environment, generated the three environment, so in the list of configuration page can’t write the domain name of the interface to death, Only the relative path of the interface can be filled in where the interface address needs to be filled in. In addition, the visual construction of the system on this page needs to generate pages for multiple independently deployed systems. Therefore, the system to which the back-end interface belongs should be selected in the global configuration area, as shown in the figure below:
The list view page obtains the system identifier of the interface from json Schema, and then dynamically generates the domain name of the interface according to the running environment of the view page
Not all list pages have buttons, filterStatus, and search boxes. These three areas can be configured as required
Button configuration
When configuring buttons, you must select the operation type of the button. Currently, the operation types are upload, Export, and Customize. Different operation types require different configuration items. In this example, different list pages need to perform different subsequent operations after export, so the configurer can customize the callback function after export. In order to reduce the cost of the configurer to remember the parameter order, the codemirror code editor can only write the contents of the function body. The configuration page wraps the contents of the code editor in a function before saving the JSON schema to the server, simplifying the code as follows:
if(button.type === 'upload') {
button.callback = 'function (vm,content) {'+ toSwitch(button.callback) +'} '
} else {
button.callback = 'function (vm) {'+ toSwitch(button.callback) +'} '
}
Copy the code
When editing the contents of the function body, we need to take out the function body in the function, and simplify the code as follows:
const toSwitch = (func) = > {
const matchResult = func.toLocaleString().match(/ (? :\/\*[\s\S]*? \ | \ / \ \ * /. *? \r? \n|[^{])+\{([\s\S]*)\}$/)
const body = (matchResult||[])[1] | |' '
return body.trim();
}
button.callback = toSwitch(button.callback)
Copy the code
Because different interfaces need to pass different forms of parameters, you can customize the parameters of the assembly interface in all the places where the interface address is filled. The view page generates the behavior of interface parameters when rendering the page. You can modify this default behavior in the Custom Assembly Interface parameter editor
The filterStatus configuration is relatively simple and will be skipped here
Search Area Configuration
The searchBox area can be configured with the following search boxes: single-line input box, drop – down box, cascading selector, time selector, and time range selector
Different search boxes require different configuration items. For the time range selector, some list interfaces require the start time and end time to be placed in the same array, while others require the start time and end time to be placed in different fields, so the field name of the search box has the function of deconstructing. The format of the field name can be param1,param2. When the view page parses the JSON Schema, the parameters of the search box are assigned to the reconstructed parameters. The simplified code is as follows:
function separateParam (originalArr,key){
const keyArr = key.replace(/ ^ \ [/.' ').replace($/ / \].' ').split(', ');
const result = {};
keyArr.forEach((key,index) = > {
result[key] = originalArr[index]
});
return result;
}
Copy the code
In some lists you may need to set default values for the search box, which can be either fixed static data or dynamically generated data when the view page is run. If the default value input box contains return, the default value is assumed to be dynamically generated from the function, and the configuration page wraps the input from the code editor into the function before saving the JSON Schema to the server
The view page assigns the default value to the search box as follows:
function getDefaultValue(searchConfig) {
return isFunction(searchConfig.default) ? searchConfig.default(vm) : searchConfig.default;
}
Copy the code
Drop-down boxes and cascading selectors require drop-down options that can be retrieved from the interface or configured with static data
The table area
The configuration of table is the most complicated place in the configuration of the list page, and the table is also the main content in the list view. The complexity lies in that the number of columns is not fixed, and the display form of each column is not fixed. The configuration area is as follows:
Because the nesting level of data to be displayed in each column of the table is not fixed, the table header field supports value by path. For example, the header field could be order.id, which uses cool-path to do this
Table Supports multiple selection, Operation, and text columns. If a column is an action column, you must customize how the action column is presented. If a column is text, the default value is based on the header field and the text content is displayed on the interface. You can change the default value to customize the display content. Custom display content is implemented using Vue’s render function, simplifying the code as follows:
<template v-if="col.render"> <v-render :render-func="col.render" :row="scope.row" :index="scope.$index" :col="col" /> </template> // The v-render component defines the following components:{vRender:{render(createElement) {this.renderFunc is the return function written in the list configuration interface this.renderFunc(createElement,this.row,vm.$parent,this.col,this.index,this.oldRowData) }, props:{ renderFunc:{ type:Function, required: true }, row:{ type:Object, default(){return {}} }, index:{ type:Number, default:0 }, col:{ type:Object, default() { return {} } } }, data(){ return { oldRowData:deepClone(this.row) } } } }Copy the code
Since all the data to be displayed in the table is obtained from the interface provided by the back-end, the system built on this page in our company has to serve multiple independent systems with different back-end interface specifications of these systems, so the data to be displayed in the table can be assembled on the configuration page according to the value returned by the interface. Assembling table data is similar to assembling interface parameters. You write the function in the code edit box, and then the function must have a return value. The view page will use the return value as either the interface parameter or the table data
Detail page/form page design
The details page has the same design idea as the form page, except that the components displayed on the page are different. In the text below, the details page is referred to as the details page. There are two types of components in the details page, the layout component and the base component. The base component can only be placed in the layout component, and the layout components cannot be nested within each other
Here I create the details page at the behavior latitude and divide the rows into one to three columns, each of which can hold any number of base components. Select the base component or the layout component to configure this component, using the configuration details page as building blocks
Obtaining page data
Because the dynamic page is created and requires the back-end interface, you need to select the back-end system to which the interface belongs and fill in only the relative path of the interface when creating the details page, which is the same as that of the configuration list page
For all the detail pages, they need to display the detail data on the interface, which is referred to as the detail page data for the time being. In our company’s business system, it is common to get page data from the interface through the detail ID or other parameters
In the visual page construction system, there are two ways to obtain page data, respectively:
- Fill in the address of the interface that gets the page data. This is the easiest way to automate most of the work for the view page.
- It is flexible to configure page custom functions to get page data, and support promises and synchronously executed functions here
The first method is introduced. The interface is as follows:
In the interface address input box, you can enter something like /basic/someApi/detail? PoId = 202004130000121&Type&Code =333, the view page will use poId, type and code as the interface parameters when it gets the JSON schema to generate the view, and the view page will take these parameters from the browser address bar first. If the browser does not have a parameter, the program uses the value given in the JSON Schema. For example, the query string in the browser address bar is? Po_id =99&type=2, view page request /basic/someApi/detail interface, pass the parameter: {po_id:99,type:2,code:333}. This way the interface returns the current page data in the Content field
The code for obtaining the interface parameters based on the value in the interface address input box and query in the browser address bar is as follows:
/** * get the interface parameter * from query@param params
* @param query
* @returns {{[p: string]: *}} * /
export function getParams(params, query) {
constresult = { ... params };Object.keys(result).forEach(key= > {
// Replace the value in params with the parameter value in the browser query
if(query[key]){
result[key] = query[key]
}
});
return result
}
Copy the code
In this way, you only need to write the body of the function, and it must have a return value. The interface looks like this:
This approach supports promises and functions that execute synchronously. If the function returns a Promise, the view page will treat the value of Promise resolve as page data, and if the function is executed synchronously, the view page will treat the return value of the synchronized function as page data
Combining these two ways to view the page to get the page data code is as follows:
/** * Get page data *@param PageConfig Page configuration *@param Vue instance of the VM details page *@returns {Promise<any | never>}* /
export function fetchPageData({pageConfig,vm}){
return new Promise((resolve, reject) = > {
// Get the page data from the interface
if(pageConfig.url) {
const paramsFromUrl = getParamsFromUrl(pageConfig.url)
// Get the full interface address
const fullUrl = getFullUrl(pageConfig.belong,paramsFromUrl.origin)
request(fullUrl, getParams(paramsFromUrl.params,vm.$route.query)).then(res= > {
resolve(res.content)
})
}
// Get the page data from a custom function
else if(pageConfig.getPageData ){
if(typeof pageConfig.getPageData === 'function') {
const result = pageConfig.getPageData.call(vm,vm)
resolve(result);
} else {
resolve(pageConfig.getPageData)
}
} else {
resolve({})
}
}).then((content) = > {
return content
})
}
Copy the code
Configuration parameters of the component
Here is the configuration of an input box component:
{
"title": "User name"."path":"user.name"."key":"userName"."type":"string"."visible":true."x-linkages": []."x-component":"dm-input"."x-component-props": {"type":"text"."size":"small"."placeholder":"Please enter your user name."
},
"x-props": {"style": {"margin":"7px 5px"."color":"# 333333"}},"editable":true."triggerType":"submit"."events": {},"x-rules": {"format":""."required":false."pattern":""."max":"5"."min":"2"}}Copy the code
Components can be configured with the following fields:
The property name | describe | type |
---|---|---|
title | The field | string |
path | Values of the path | string |
key | Interface Field name | string |
description | The field | string |
default | UI component field default value | any |
editable | Editable or not | boolean |
type | Field value type | string,object,array,number,boolean |
enum | Enumeration data | array,object,function |
url | Gets the interface address for enumeration data or UI component data | string |
items | Configuration fields for the child components of a component | array |
triggerType | Field verification time | string |
visible | Whether the field is visible | boolean |
events | UI component events | Object |
x-props | Field extension properties | object |
x-component | UI component name for the field | string |
x-component-props | Field properties of the UI component | object |
x-linkages | Field linkage | array |
x-rules | Rules of the field | object |
X-props Data attribute
The property name | describe | type |
---|---|---|
style | The style of the UI component for the field | object |
className | ClassName of the UI component for the field | string |
label | Enumeration label value path for UI components of the field | string |
value | Field UI component enumeration value value path | string |
buttonType | The operation type of the button | string |
render | Customize the display content of the component | function |
buttonSubmitUrl | The interface address of the submit button | string |
paging | Whether the list is paging | boolean |
X-rules data attribute
The property name | describe | type |
---|---|---|
format | Field value type | string |
required | If required | boolean |
pattern | regular | RegExp,string |
max | The maximum length | number |
min | Minimum length | number |
len | The length of the | number |
maximum | The largest number | number |
minimum | The minimum value | number |
message | Error writing | string |
Optional values of format: URL, email, mobile phone number, amount, and number
X-linkages Field
The property name | describe | type | An optional value |
---|---|---|---|
type | Linkage type | String | linkage:hidden,linkage:disabled,linkage:value |
subscribe | Linkage subscriber | Function | – |
The following examples are text components, drop-down box components, and button components
Text component
The file component is used to display the corresponding value of a field in the details page. Its configuration interface is as follows:
In this case, the value path of the text component is mandatory. The view page will fetch the display content of the text component from the page data according to the value path. The value path also supports adding a filter after the path, which is the same function as the filter in Vue. Value path For example:
Create_at | formatDate (' datetime) : from the page data create_at value in the field, and then use the formatDate formatting create_at field corresponding valueCopy the code
The simplified code is as follows:
. Vue components
computed:{
// Use computed properties to get what the text component wants to display
textContent(){
const p = this.fieldSchema.path.split('|')
// If the value path is specified
if(formatPathStr(p[0]) {const filters = p.slice(1)
// Cool-path = cool-path
const path = new Path(p[0]);
// Select value from page data
let value = path.getIn(this.pageVm.pageData)
/ / filter
if (filters && filters.length) {
value = filters.reduce((a, b) = > {
return this.evalFilter(b, a, this)
}, value)
}
return value || '-'
} else {
return this.fieldSchema.default ||'-'}}},methods: {evalFilter(filterStr,val){
const parms = filterStr.match(/^([_$0-9A-Za-z]+)\(([^()]+)\)$/) | | [' ', filterStr]
const fn = parms[1]
let args = [val]
try {
args = args.concat(eval(` [${parms[2]}] `))}catch (e) {
console.error(e)
this.$message.error(this.fieldSchema.title+'Error in concatenating parameters while executing filter')}// Get the method corresponding to the filter based on the filter name
const filterFn = this.$options.filters && this.$options.filters[fn]
if (typeof filterFn == 'function') {
return filterFn.apply(this, args)
}
return val
}
}
Copy the code
As you can see from the above visualization of the configuration file component, we can also configure the enumeration data of the text component. This enumeration data mainly takes into account that some fields in the page data returned by the interface are numbers or English words, but we need to display the Chinese meanings of these fields on the interface. Enumeration data can be obtained from the interface and can be written to death in the configuration page. The method of obtaining enumeration data is similar to the method of obtaining page data described above and will not be described here
Not custom text components display content can satisfy most usage scenarios, this way has a limitation: a text components can only display the value of a field, in some cases may need to merge multiple fields in a text components shown in the interface, in this case I used the Vue rendering function display content from the definition of text components. The custom rendering function looks like this:
return h('div',[
h('span',pageData.user.name),
h('span',pageData.uesr.age)
])
Copy the code
The render function of the file component is executed on the view page when rendering the view. The simplified code is as follows:
<template> <! --do something--> <v-render :renderFunc="fieldSchema['x-props'].render" /> </template> <script> // do something components:{ vRender:{ render(createElement) { const parentVm = this.$parent; return this.renderFunc(createElement,parentVm,parentVm.pageVm,parentVm.pageVm.pageData) }, props:{ renderFunc:{ type:Function, required: true }, } } } </script>Copy the code
In the visual creation detail page, in addition to the text component, the columns in the table component also support write render functions
Drop-down box components
The configuration page for the components in the drop-down list box is as follows:
The drop-down box component has three areas for configuration. This section mainly describes the display configuration and association configuration of the drop-down box. First, the display configuration is introduced, and then the association configuration is introduced
The drop-down box is a form component that can display and modify data. I store the value of the form component (that is, the value corresponding to the component value attribute) in VUEX. For the detail page, the form component needs to display its initial value, which is located in the page data. In order for the form component to get the value it wants to display in VUex, IN the form component created hook function, I save the form component’s value in the page data to VUex. After that, the value and modified value of the form component are operated on the data in VUEX. The simplified code is as follows:
<template> <dm-select v-model="value" v-bind="fieldSchema['x-component-props']" :class="fieldSchema['x-props'].className" > <! -... Some options--> </dm-select> </template> <script> export default {computed:{value:{get() {// Value from Vuex formData return new Path(this.fieldSchema.key).getIn(this.formData) }, Set (value) {/ / to save the form fields to Vuex formData enclosing saveFormData ({name: enclosing fieldSchema. The key, value: the value})}},}, created(){ this.setFieldInitValue() }, Methods :{setFieldInitValue(){let initValue = new Path(this.fieldSchema.path).getin (this.pageData) / / to save form components of the initial value to Vuex formData enclosing saveFormData ({name: enclosing fieldSchema. The key, value: initValue})}}} < / script >Copy the code
In addition to displaying the selected value, the drop-down component also needs alternative data, which can be obtained from the interface or written to the configuration, to return a Promise, to return a value computed synchronously, or to fill in a URL. The alternative data acquisition method of the drop-down box is similar to the page data acquisition method described above and will not be repeated
Form Linkage Configuration
Form linkage means that the state of this form component is affected by the values of other form components. Currently, the supported types of linkage are: Hidden, Disabled, and component value linkage. The linkage subscriber is used to observe changes in values in formData and to influence the state of the component based on the linkage type of the form component. A linkage subscriber is a function that uses the linkage subscriber to compute the value of a computed property in the view page, so any change in the value accessed in the linkage subscriber will recalvaluate the computed property, affecting the state of the component. The simplified code looks like this:
<template> <dm-select v-model="value" :disabled="disabled" :hidden="hidden" > <! -... some options--> </dm-select> </template> <script> export default { computed:{ disabled(){ if(this.linkages['linkage:disabled']) { return this.linkages['linkage:disabled'](this.pageVm,this.pageVm.pageData,this.formData) } else { return false } }, hidden(){ if(this.linkages['linkage:hidden']) { return this.linkages['linkage:hidden'](this.pageVm,this.pageVm.pageData,this.formData) } else { return false } }, Value :{get() {return new Path(this.fieldSchema.key).getin (this.formdata)}, Set (value) {/ / the form of the component is stored into the Vuex formData enclosing saveFormData ({name: enclosing fieldSchema. The key, value: the value})}}, valueOfLinkage(){ if(this.linkages['linkage:value']) { return this.linkages['linkage:value'](this.pageVm,this.pageVm.pageData,this.formData) } else { return '' } } }, watch:{ valueOfLinkage(val){ this.value = val } } } </script>Copy the code
The value linkage of a form component is a little more complicated than hiding and disabling the linkage, because the linkage subscriber can change the value of the form component, and the form component itself can change its value. The value of the form component is subject to the last change
To disable linkage, its linkage subscriber can fill in the following:
if(formData.status + ' '= = ='2') {
return true
} else {
return false
}
Copy the code
The linkage subscriber above means that the form component is disabled when formData.status in vuex is equal to 2
For hidden linkage, the following can be filled out in its linkage subscriber:
if(formData.username.length > 3) {
return true
} else {
return false
}
Copy the code
The above linkage subscriber means that the form component is hidden when the length of the vuex formData.username is longer than 3
For value linkage, the following can be filled in its linkage subscriber:
if(formData.id) {
return 3
} else {
return ' '
}
Copy the code
The linkage subscriber above indicates that when formData.id in vuex is truly, the value of this form component is set to 3
Button component
The button component configuration page is as follows:
Button component is a special component, compared with other components its operation behavior is not fixed and the influence range is relatively wide. According to business requirements, there are three types of operations: submit (that is, submit the form data to the server), reset (that is, reset the value of the form component to the initial state), and custom (that is, customize the button click event handler). Only two types, commit and custom, are covered below
Commit operation
Usually, before submitting the form data to the server, we need to verify the form data. Only when all the data meets the requirements can the form data be submitted to the server; otherwise, the error language will be displayed on the interface. To meet this requirement, we need to access all form data and data validation rules of form components in the handler of button submission event. Since the form data is saved in Vuex, and the JSON schema storing the data validation rules is shared globally in the view page, So it’s easy to get the data you want in the commit event handler. Note that if a form component’s data fails validation, its error message is displayed where the form component is located, which means that the error message is consumed at a different location from where the error message is generated
I’ve collected methods to manipulate error messages into a separate module. The simplified code is as follows:
/** * Form error collector **/
import Vue from 'vue'
export const errorCollector = new Vue({
data(){
return {
errorObj: {}}}.methods: {clearError(){
this.errorObj = {}
},
delError(name){
const errorObj = {
... this.errorObj
}
delete errorObj[name]
this.errorObj = errorObj
},
setError(name,value){
this.errorObj = {
... this.errorObj,
[name]: value
}
},
initFieldError(name){
this.errorObj = {
... this.errorObj,
[name]: ' '}}}})Copy the code
The error information collector is a Vue instance. Introduce the error information collector into each form component as a data property of the component, and the error information as a calculation property of the component. In this way, whenever the data in the error information collector changes, the interface will be updated.
<template> <! -- do something--> <div>{{ errorMsg }}</div> </template> <script> export default { data(){ return { errorCollector:errorCollector } }, computed:{ errorMsg(){ return this.errorCollector.errorObj[this.fieldSchema.key] } } } </script>Copy the code
Custom operations
The custom action is actually a button click event handler defined in the JSON Schema, which is simpler to implement in the view page
How to use
In the development environment, the JSON Schema is stored in the database. To use the JSON Schema to generate pages in the test and production environments, you need to download the JSON Schema to a specific folder in the project. When you access the view page in a browser, The json schema of the page is read from the downloaded static file based on the page ID, and the view page renders the page
Read the configuration code from the static file:
import("@static/jsons/tables/table_string_"+id+".json").then(fileContent= > {
console.log('Configuration data:',fileContent)
})
Copy the code
The JSON Schema stored in the JSON file is a string, but an object is needed when rendering the interface on the view page, and some fields of the object must be functions. To convert the string to the desired format, I use new Function(‘return ‘+ strConfig)() to do this, simplifying the code as follows:
function parseStrConfig(jsonSchema) {
return new Function('return ' + jsonSchema)();
}
Copy the code
Existing shortcomings
- The produced page cannot be set up to run independently of the page. For the generated pages to be used in other systems, the page building system must be introduced in the corresponding system using iframe or single-SPA micro-front-end technology
- The JSON schema of the page is not independent of the page architecture system. Every time a page is created, the JSON schema of the page needs to be downloaded to the page visual construction system, which leads to the frequent release of the page visual construction system. However, the business functions of the page visual construction system are relatively stable
Write in the last
Follow my official wechat account