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.

  1. Material area, currently support 5 types of components, reusable, can support expansion
  2. Visual drag, material area drag to preview area, preview area inside the page drag sort,
  3. Preview area, streaming layout, click to open component configuration, follow component location
  4. Real-time preview, that is, configuration changes need to be immediately reflected in the preview area
  5. Configuration area, a large number of calls to the business related popover function
  6. 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

  1. More material components are implemented
  2. Replace the component with a remote component
  3. undo/redo
  4. Lazy loading does not depend on the specific implementation of the component