preface
Recently, the visual construction project that took half a year to develop was finally launched [manual flower scattering 🌹🌹🌹]. The product is not very different from the common visual editor in the market in terms of functions, but slightly different in functional details. This paper is mainly to record the development process encountered problems and solutions.
Product Demo drawing
Demand analysis
Preliminary preparation is still quite important, especially for the front-end project. If the whole project is set up and it is found that the interaction logic of a certain function is extremely difficult to realize, the probability of workload will be doubled. Ah, don’t say much, know all understand.
- Material area, currently support 5 types of components, reusable, can support expansion
- Visual drag, material area drag to preview area, preview area inside the page drag sort,
- Preview area, streaming layout, click to open component configuration, follow component location
- Real-time preview, that is, configuration changes need to be immediately reflected in the preview area
- Configuration area, a large number of calls to the business related popover function
- In the configuration area, you need to customize the verification logic and save it separately
Technology stack
The system uses the technology stack as follows
react
typescript
mobx
scss
antd
Data structure definition
The first step, of course, is to define the data structure stored in the interface page with the backend, which should not be controversial.
interface Page {
id: number / / page id
siteName: string // Page name
description: string / / description
createdAt: number // Create time
operatorName: string // Operator name
modules: [
// Page component configuration
{
id: number / / component id
name: string // Component name
type: number // Component type
configuration: JSON.stringify({ // Serialized configuration, using the content list as an example
displayRowNum: 8.subPageConfiguration: {... },title: "Heart-warming night talk.".contentType: 3.columnId: 6747.columnName: "Emotional syndrome that affects 99% of adults. Have you been shot?",}status: number // It is in the shelf state}}]Copy the code
You only need to parse the configration configuration in the Modules field in sequence, edit it, and send it back to the back end according to the original data structure. Note that many of the component configuration fields here only store index relationships, and the detailed presentation information still needs to be retrieved at runtime.
The directory structure
├ ─ ─ @ types# declaration file├ ─ ─ store# Data-related operations are centralized here├ ─ ─ constant# Constant correlation├ ─ ─ the service# Remote service├ ─ ─ commonThe related component of the call├ ─ ─ Editor# editor│ ├ ─ ─ BasicModules# Base component area│ ├ ─ ─ the Empty# empty data│ ├ ─ ─ FormContainer# configuration area│ ├ ─ ─ PreviewComponent# Preview component│ ├ ─ ─ PreviewContainer# preview area│ ├ ─ ─ UIModules# Extend the component area│ ├─ Index. TSX ├─ ModulesEditor component, take the List component as an example│ ├─ List │ ├─ ├─ TSX# Render component│ ├ ─ ─ ├ ─ ─ Form. The TSX# Form component│ └ ─ ─ index. TsCopy the code
component
Valence design is the most important part of this system, all operations are connected by component decoupling and series together
The data structure
The following is the data structure required by the runtime. We encapsulate the ration sent by the backend in data and extend some fields, such as UI state and verification attributes.
interface CmsModule {
id: number // uuid
name: string // Component name
component: any // Display the component
form: any // Form component
type: number // Component type
selected: boolean // Whether it is selected
error: boolean // Is there an erroruntouched? :boolean // Whether the state is initialized. Only newly added components have this state
data: { id? :number } & Record<string.any> // Component configration
}
Copy the code
Initialize the
Initialization is written in store. Here is an example of code that parses server-side data to generate a local model
import { BASIC_MODULE_LIST } from 'Modules'
// modules are data structures passed in from the back end
store.deserialize = (modules) = > {
this.value = modules.map((module) = > {
Filter static attributes by type
const staticInfo = BASIC_MODULE_LIST.find(
(item) = > item.type === module.type
)
constcomponent: CmsModule = { ... staticInfo,id: module.id,
type: module.type,
name: module.name,
selected: false.error: false.data: {
id: module.id, ... (() = > {
try {
return JSON.parse(module.configuration)
} catch (e) {}
})(),
},
}
return component
})
}
Copy the code
The component registration
The BASIC_MODULE_LIST in the above code is equivalent to a registered list of components, through which static properties of components are injected into the runtime. In the same way, adding a component requires only the following conditions. Of course, if you want to use remote components, that’s fine
import Search from './Search'
import SearchForm from './Search/Form'
export const BASIC_MODULE_LIST = [
{
type: 20.component: Search,
name: 'search'.form: SearchForm,
},
]
Copy the code
// The remote component can be loaded using require.js or directly
init() {
const script = document.createElement('script')
script.src = 'https://demo.umd.component.js'
script.onload = () = > {
BASIC_MODULE_LIST.push([
{
type: 31.component: window.Search,
name: 'Remote Component Sample'.form: window.Search.Form,
},
])
}
document.body.appendChild(sciprt)
}
Copy the code
Finally, how do we use component data
PreviewComponent.tsx
render() {
const Module = module.component
const Form = module.form
return <div
className={classnames(
style.preview.module.selected && style.selected.module.error && style.error
)}
onClick={this.handleSelect}>
<span className={style.component}>
{<Module {. module.data} / >}
</span>
{data.selected && <FormContainer data={... module.data} Form={module.form}>
<div className={style.title}>{module.name}</div>
</FormContainer>}
</div>
}
Copy the code
configuration
Component configuration
Let’s start with component configuration and review the requirements for enabling real-time error checking, invoking popovers related to business resources, and separate storage. Of course, the most important thing is to implement inversion of control, which means that the configuration file only describes the form rules, while the actual form needs to be created by the editor. This system uses antD Form component to create a Form, component to achieve the following interface
import { WrappedFormUtils } from 'antd/lib/form/Form'
interface ModuleFormProps {
form: WrappedFormUtils // AntD form instance, passed in by the external editorinitialValue? :any // The form default value, usually obtained from configrationlayout? : {// Layout configuration
labelCol: { span: number }
wrapperCol: { span: number}}}// Form component signature
type FormComponent =
| React.Component<ModuleFormProps>
| React.FC<ModuleFormProps>
// Sample form component
import SourceModal from '.. /common/SourceModal' // Introduce a business related resource popup
const BannerForm: React.Component<ModuleFormProps> = (props) = > {
return (
<Form.Item label="Configuration Example" {. this.props.layout} >{getFieldDecorator('title', { initialValue: this.props.initialValue? .title, // Default values rules: [{required: true, message: 'Please enter the title'}], // verify})(<SourceModal />)}
</Form.Item>)}Copy the code
Similarly, if the above components need to be called from remote, just need to inject the SourceModal as the form object, simple modification, external call is relatively easy
FormContainer.tsx
import { Form as AntForm } from 'antd'
render() {
const { data, Form } = this.props
return (
<AntForm>
<Form
form={this.props.form}
initialValue={data}
layout={... }
/>
</Form>
)
}
Copy the code
Configuration synchronization
As mentioned earlier, we created a globally unique store for unified processing of data. In principle, we need to encapsulate all data and methods to modify data in the Store in case we need to implement undo/redo stack. The following code shows how to synchronize Form field changes to the Store
FormContainer.tsx
import { Form as AntForm } from 'antd'
import store from 'store'
export default Form.create<FormProps>({
onValuesChange: (props, changedFields, allValues) = > {
store.updateComponent(this.props.data.id, allValues)
},
})(FormContainer)
Copy the code
There are a number of ways to reflect store data on the UI in React. Since the project itself uses MOBx, wrap the PreviewComponent in an observer
Error handling
Previously we only defined form error validation for individual components, so we need to monitor the error status of each component, otherwise we will only get the error status of the current component when we save the page. Currently, the render hook function of the Form component is used to synchronize the current Form state when the toggle is selected
FormContainer.tsx
import store from 'store'
// Toggle the destruction hook of the previous component's Form when the component is selected
componentWillUnmount() {
const { form, data } = this.props
form.validateFields((err, values) = > {
store.updateComponent(data.id, values)
store.changeComponentError(data.id, Boolean(err))
})
}
Copy the code
CmsModule also has a Amanda field that can let if a component has been checked (only new components can hold this field). When it holds true, the Amanda field is empty and cannot save
other
Drag and drop
React-dnd is a well-known third-party library used for drag and drop
There are several customization optimizations in the experience. First, drag from the material area on the left to the preview area has an intermediate preview state; second, the page scrolling will be automatically enabled when dragging and sorting, which will be friendly when sorting long pages.
performance
Rendering performance
Because mobx is used, accurate updates can be made even with a large amount of list data, and there will be no lag without optimization
Data acquisition
As mentioned earlier, many components only store resource index ids and only request interface data during component rendering. Imagine if 100 components are configured and 100 requests are sent simultaneously when the page is initialized. Similar to image lazy loading, component data loading can also be optimized
List/index.tsx
if (!window.IntersectionObserver) {
this.fetchData()
} else {
const observer = new IntersectionObserver(([entry]) = > {
if (entry.isIntersecting) {
this.fetchData()
observer.unobserve(this.listRef.current)
}
})
observer.observe(this.listRef.current)
}
Copy the code
interaction
A library called React-Flip-Move is recommended to quickly implement dynamic list insertion, deletion, and sorting animation, with zero configuration access and minimal code intrusion.
planning
- More material components are implemented
- Replace the component with a remote component
- undo/redo
- Lazy loading does not depend on the specific implementation of the component