background
A large number of business operations are inseparable from complex page production, such as Taobao, whether it is industry categories or different forms of shopping guide marketing, behind all need to make a large number of page support. Abstract the process of making these pages, so there is a “page matching system”.
Tianma building service is to provide this page to build capacity, capacity building however only provides implementations of the API service for service access the side still have a lot of work, especially the need to generate the page set-up and data configuration of this kind of complex front-end interactive logic, and this part of the front-end interactive logic almost convergence, so this part will be abstract logic, Separately developed the build editor to achieve a unified set of basic build interaction logic, so the build editor version 1.0 was produced.
Although the Build editor 1.0 has been able to meet most of the needs of page build, with the development of business, different scenarios have new demands for page build. Such as:
-
Add new capabilities: You want to add a review process before a page is published
-
Remove irrelevant information: In the nail scenario, you only need to upload a picture to configure the data of materials, and do not want to be interfered by the configuration of e-commerce attributes
-
I only want to reuse some of the build editor’s capabilities: custom page editing, and reuse the build editor’s publishing process
-
.
For these appeals, there are the following solutions:
-
Scheme 1: The access party will no longer use the build editor and develop a new build editor to achieve its desired functions.
-
Scheme 2: We help the business to achieve in the build editor, in the build editor through different branch statements to achieve the requirements of different scenes.
-
Scheme 3: The construction editor is divided into fine-grained components to provide basic components for access parties to use according to their needs
In scheme 1, there is repeated construction. When the building service launches new functions or upgrades capabilities, the access party needs to re-develop UI interaction to upgrade the new functions. Plan 2, although it can be implemented, but once there will be a second time, there will be more and more business for you to help implement the logic in different scenarios, and finally build the editor code is branch statements, and difficult to maintain.
If (Scenario 1){XXXX} if(Scenario 2){XXXX} if(Scenario 3){XXXX}...Copy the code
Plan three is feasible but of little value. Increases the maintenance costs of fine-grained components on the one hand, on the other hand access party need to know the build data flow state of the editor, will be fine-grained components together, increase the cost of access, then on the one hand, only part of the business of some components have the custom demand, dismantling the fine-grained components is essentially to make custom business. Therefore, it is possible to explore a solution that enables business access parties to quickly customize without disassembling existing components.
We have summarized these requirements as follows: Access parties want to use 80% of the capabilities of the Build editor, and 20% of the capabilities they want to extend and customize themselves, including executing render logic at specified locations and using data flows in the Build editor.
At the first level of abstraction, the scaffolding editor is expected to be able to give a render component node and support access to the internal data state, and can be loaded independently on demand.
So, we started for the next generation to build editor to explore and practice, expect to achieve the following two kinds of capability: (1) support (a) internal component replacement component replacement (b) block unwanted interaction logic (2) share the state of data within the business component (a) obtain the component internal state (b) modify the internal state of the component
Two ideas
Extensible architecture
The backend services connected by all access parties when using the build editor are essentially the services provided by Tianma, so it can be understood as the front-end interaction realization of the same back-end services in different business scenarios. Its essence is to set up the editor under different scenarios have different capacity, the design of the reference vscode, we let build editor for extension ability, let consumer through development extension to build rich editor’s ability, to meet the needs of different scenarios, build editor provides the core of the cast ability.
Extensions can only add capabilities, and if there is a need for customization of existing build editor capabilities, it is expected to be modified by replacing internal components. We can think of the Build editor as a container for components in which each child component is registered
{name:' component name ', Component :' component instance '}Copy the code
Components are rendered dynamically when the mapping of components in the container changes, allowing component replacement by registering components globally.
Component data state sharing
Data state management libraries like Redux can share data state across different sub-components of the same component, but sharing data state between two business components requires adding a common parent component between the two business components, and then sharing data state through data management solutions like Redux. But it is very difficult to access the data state in business component B from business component A.
It would be best if the data state of business components A and B could be maintained separately within the components, but accessed in some way.
To implement the above two ideas, we did the following implementation.
Designed and implemented
Step 1: comb the core tie-in process
First of all, we sort out the core tie-in process and determine the core capabilities needed to build the editor, that is, the capabilities of the kernel to build the editor.
Define the process of one partnership: create a page -> Select the build type -> enter the build editor -> page build -> Data release – Currently, module build is the core process
Step 2: Set up the editor level design
According to the core capabilities used in the build process, we designed the build editor into UI and Data.
-
UI:
-
Header: Convergence of page-level operations: page setting and page publishing.
-
Editor: The core editing area of a page, including page plug-ins, build page previews, module management, and data configuration
-
Data:
-
Model: Global data state management
Step 3: Model layer design
The data state between components can be shared through props. For example, when clicking the Add module button in the module list to pop up the module center, the data state needs to be passed to the module center and the module list through props in the common parent of the module list and the module center.
// ModuleCenter and module list pseudocode // ModuleCenter and module list common parent pseudocode import ModuleCenter from 'ModuleCenter'; import ModuleList from 'ModuleList'; import {useState} from 'react; function App() { const [moduleCenterVisible,setModuleCenterVisible] = useState(false); return ( <> <ModuleCenter moduleCenterVisible={moduleCenterVisible} setModuleCenterVisible={setModuleCenterVisible} /> <ModuleList setModuleCenterVisible={setModuleCenterVisible}/> </> )}Copy the code
Since the ability to replace components and share data state was not considered at the beginning of the editing design, it was difficult for me to change the way I interact by clicking on the Header to open the center of the module.
In order to make the data state of the component be called in multiple locations across regions, we need to design the Model layer, which removes the original complex dependencies and manages them in a unified way, using a global data state to manage them.
After that, one more data state is registered with the global data state manager, and the rest of the components can get the value from the global data state.
We divide the Model layer into four classes based on the type of operations:
-
Global configuration
-
Module operation
-
Page operation
-
Plug-in related
Step 4: Support internal component replacement
The core of the next generation build editor is still able to modify the internal details of business components, notably component replacement. If internal components can be replaced, then the consumer only needs to replace the component for which he/she has customization requirements, reducing the cost of developing the entire business component to developing only part of the functional component. For example, if a business doesn’t need a complex release process, just a release review, replacing the release process is the fastest way to reuse the build editor.
Original release process
New release review
We impose interface specification constraints on the replaceable components:
interface IInjectComponent { name: string; // Name of the component to be replaced, globally unique component? : React.ComponentType | string; // Replace component}Copy the code
Use a global Map to manage the mapping between name and Component, which can be either an NPM package or a CDN. Provide a component registration method, registerComponent, that modifies the component mapping
The function registerComponent (props: IInjectComponent | IInjectComponent []) {/ / replace the mapping relationship between component}Copy the code
(1) We use React. CreateElement to render local components loaded by NPM package. (2) Components loaded by remote CDN. We use @ICE/Stark-Module, which can remotely load components packaged by UMD into a micro-module. Then let the component be replaced at runtime.
At this point, the way to support internal component replacement is basically completed, and the implementation of pseudo-code is
function InjectComponent(props) { const {name,defaultComponent,... otherProps} = props; const component= getComponent(name) || defaultComponent; If (isRemote(Component)) {return <MicroModule url={component} {... otherProps}/>; }else {return react. createElement(Component,otherProps)}} RegisterComponent ({name:'publish-component', Component: PublishComponent}) / / defaultComponent used to register the default component < InjectComponent name = "publish - component" defaultComponent={PublishComponent} {... /> // registerComponent({name:'publish-component', component:'https://new-publish-component'})Copy the code
This enables both modification of local logic of business components and dynamic on-demand loading of extensions. And any child of a business component can become a replaceable component simply by wrapping it with an InjectComponent.
Step 5: Share the data state within the business component
Since extension components developed by the access side are not packaged inside the build editor, there needs to be a way to get state inside the build editor. A common idea in this case is to pre-design the data state that needs to be used by the props, and then use the data state passed in by the build editor as long as the replacement component is consistent with the props of the original component
<InjectComponent name="publish-component" props1={props1} props2={props} {... otherProps}/>Copy the code
This approach has a limitation that only the props passed in can be used. If you want to use other data states, you need to modify the build editor code to add additional props.
If the data state of the business component is mounted in the state of the global application, then the data state of the business component can be shared globally.
One idea is to have a state management library that is a singleton pattern that manages data state through namespaces that can be accessed when two components of the state management library are used in the same application.The global state management library only needs to have two methods registerModel,useModel, pseudocode representation:
Import {registerModel} from 'golbalStore'; Import {useState} from 'react'function ModleA(){useState const [state1,setState1] = UseState () return {state1,setState1}}// Export default registerModel(ModuleA,{name:' coma-modulea '}) // Register with the global singleton as name Import {useModel} from 'golbalStore'; import {useModel} from 'golbalStore'; Function ExtCom() {// Find the Model const comAModelA = useModel(' coma-modulea '); // Access data state console.log(comamodela.state1)... }Copy the code
I provide a Redux-like state management tool that registers the Model layer into a global singleton. In this way, the extension component can quickly access and modify the data state using only this singleton.
Step 6: Modify local state in a way that implements middleware like
Sometimes just want to modify the component parts of the state does not need to replace the components, for example, a building a Button copy editor from “published” changed to “page”, is just modify the copy, Button click on the logical or want to retain, component replacement needs to be implemented at this time again the Button click logic.
Import Button from 'Button'function App(){return <Button text=" Button "}/>}Copy the code
If Button’s text is passed in through props, we just need a middleware like capability to process the passed props and return the desired result.
Build the editor implementation
<Injectcomponent name="publish-button" component={button} text=" publish "/>Copy the code
Modify props inside Injectcomponent
function InjectComponent(props){ const = {name,component,... otherProps} = props; // Call middleware const newProps = FnModdileWay(otherProps)... React.createElement(component,newProps)}Copy the code
To take another complex example, consider the need to add a new publishing node. To simplify the problem, we have a List with an item inserted into it
[a,b,c] => [a,d,b,c]
Copy the code
At this point, we need to get the original List [a,b,c] and then operate on the List, add a D to get the new List [A, D,b,c], and then consume the new List.
If the nodes in the publishing process are abstracted and passed in as props, then there is a middleware function that can modify the props, this satisfies the requirements. We call such components that can operate on props extensionPoints.
First we need a middleware registration function that tells the component “the props passed to you need to be processed by middleware first” and registers the middleware function. The register function puts middleware functions into a queue.
Const register = useExtensionPoint<ComAModuleAProps>(' coma-modulea '); const register = useExtensionPoint<ComAModuleAProps>(' coma-modulea '); UseEffect (()=>{return a clean, Const clean = register((props)=>{// register return {props, State1 :newState1 // Modify the new state}}) return () => clean(); }, [])Copy the code
Let’s take the InjectComponent we mentioned above and give it a makeover
function InjectComponent(props) { const {name,... otherProps} = props; Const Component = getComponent(name); // deepClone const extProps = useExtension(name,otherprops); if(isRemote(component)) { return <MicroModule url={component} {... extProps}/>; }else { return React.createElement(component,extProps) } }Copy the code
This makes it easy to modify the props of the internal component to modify the local state. To add a new release process node, add a new node for the props that conforms to the abstract specification of the node.
A generic approach to extensible business components
The scaffolding editor has now been modified to be a business component with extensible capabilities. Moreover, this transformation method is very simple and can be quickly transplanted. All you need to do is to use a global singleton to manage the state of an application and modify the components to be transformed as follows:
<InjectComponent name=" component name "defaultComponent={defaultComponent} {... The default props} / >Copy the code
You can quickly change an existing business component into one that can be extended.
In the future
The current implementation is equivalent to combining the existing “eraser” + “pencil” into “pencil with eraser”, although it can achieve the desired effect, but there are still some problems:
1. The Build editor provides default components. Even after component replacement, the original components are packaged inside the Build editor, increasing the size of the code. In the future, expect to be able to package components on demand according to extended configuration files. 2. There is no extension ecosystem like VS Code. It is necessary to establish a unified extension development standard and extension development scaffolding, and gradually establish the extension ecology of the build editor, so as to facilitate the management and maintenance of extension. 3, rich page building ability. Page = Page structure (data) is the basic paradigm of generating a page. The interface between page and data is fixed, but the way of generating page structure is flexible. Through the way of expansion, the ability of page building can be enriched, and different ways of building can be used in different scenarios. 4. Currently, icestore is the state management adopted in the technical solution, and ICestACK/Module is the micro module replacement solution. Therefore, we still expect to integrate this solution into ICE system and open source it together.