Skip to content

Instantly share code, notes, and snippets.

@janryWang
Created April 18, 2021 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janryWang/4f7c5126507d7c5bbb980d66cf478519 to your computer and use it in GitHub Desktop.
Save janryWang/4f7c5126507d7c5bbb980d66cf478519 to your computer and use it in GitHub Desktop.

The most professional form solution you have ever seen---Decrypt Formily2.0

Formily2.0 official website: https://v2.formilyjs.org/

Source address: https://github.com/alibaba/formily/tree/formily_next

Iteration plan: The official version will be released at the end of 2021, the beta/rc version will continue to iterate for half a year, and the beta version has been released.

Introduction

If you are a friend who has never used Formily, you can go to Formily introduction to learn how Formily solves form problems step by step. If you are a friend who has used Formily, you must understand Formily’s past positioning, that is, a form solution for complex scenarios. However, the positioning of Formily 2.0 has changed a lot. In one sentence, it is a professional solution for enterprise-level forms. What is a profession? Professional is to have:

  • Industry-leading ideas
  • Rich usage scenarios
  • Extreme detail optimization
  • Complete documentation surrounding

Now Formily 2.0 can confidently say to you: "I am professional enough!", where does professionalism reflect professionalism? Let's start with decryption.

Speaking of decryption, it is natural to analyze Formily 2.0 step by step to truly understand its core value. Here, we mainly use the following questions as the starting point:

  • Why upgrade?
  • What problems have been solved?
  • What are the highlights?
  • What is the follow-up plan?

Why upgrade?

The main reason for the upgrade of Formily 2.0 is that compared with 1.x, there are the following problems:

  • Performance is still not good enough
  • The dependency is too complicated, the volume is too large, and the stability is not enough
  • The bag structure design is not elegant enough and intuitive
  • The internal design is not complete and flexible, resulting in a huge cost of answering questions
  • Because the amount of Q&A is too large, my hair volume is getting less and less

All the problems listed above, Formily 2.0 has all been solved, but unfortunately it brings the cost of Break Change and re-learning by the way. So, how are these solved?

How we solved?

Performance Problem

Problem Disassembly:

  • The initial rendering of ArrayTable in big data scenes is stuck
  • The initial rendering of a large number of fields is stuck

The essential problem of the above two problems is actually because Formily will block the calculation of the field state when the field is first rendered. For example, some linkages not only need to control the display and hide of field B when the value of field A changes, but also need to control the display and hiding of field B when rendering for the first time, otherwise it will be difficult to meet business needs.

What if we change the blocking calculation of the first rendering to calculate after the field is mounted? There will be a flashing effect with a poor experience. At first the field is displayed and then hidden. This experience is definitely unacceptable. Therefore, the blocking calculation for the first rendering is a must, but the performance problem caused by the large amount of calculation here.

So how to solve the problem of calculation?

Let's first analyze the main reason for the large amount of calculation:

  • Because it needs to support external control, it supports controlled rendering, but the kernel is in an uncontrolled state. To change from controlled to uncontrolled, you need to monitor props changes. There will be a lot of dirty checks.
  • The setFieldState that initializes the linkage will traverse all the fields to find the target field. If there are many linkage rules, then the number of initial search fields will be very large, that is, there will be a large number of repeated search fields.
  • ArrayTable data volume is too large, resulting in a large number of fields.

The first point is very easy to solve. We only need to give up the controlled mode. What should we replace? With reference to Vue, we adopt the Reactive mode and respond to external changes based on a solution similar to Mobx. This is to solve the performance problem and reduce the side effects of dirty checks on props. (such as props passing an anonymous function, whether this function should be (It is actually up to the user to make the component re-render)

The second point is not so good to optimize in setFieldState, because looking for fields is to traverse to find, can only think of other ways? Support passive linkage mode, design a responder model specifically for passive linkage with the help of a Mobx-like solution (@formily/reactive) .That is to say, only when a field is rendered, will the response linkage be triggered, which solves the problem of too many search fields.

The third point is very difficult to solve, because the number of fields is there, and the cardinality is large. For example, a data with 10 horizontal columns and 100 vertical rows can easily have 1,000 fields. Even if Reactive is used, many Observable objects will be created, so in the end, we can only compromise and turn the ArrayTable into a paging display, so that even 100,000 rows of data can be rendered easily. Specific examples move to ArrayTable

Summary

The performance optimization journey of Formily 2.0 can be said to be almost at its limit at present. Of course, there is still room for optimization. It depends on inspiration.

Dependency Problem

Problem Disassembly:

StyledComponents Dependency Problem

  • Cause the overall package volume to increase
  • Unable to override the style, a large number of random class names
  • Various Break Changes in large versions affect the stability of Formily and are prone to repeated packaging. At the same time, once repeated packaging occurs, the style will have problems
  • Unable to customize the theme, because the style variables of its own component library (less/scss) cannot be reused

Therefore, Formily 2.0 decisively removes the styled-components dependency, and the style system of the follow component library itself, such as antd, uses less instead, and fusion uses scss instead.

Immerjs Dependency Problem

  • Break Change of a large version affects the stability of Formily and is prone to repeated packaging
  • The model with Immutable as the core idea is always not suitable for the goal of Observable Form like Formily

Why is Immutable thinking not suitable for Formily? Because Formily doesn’t want to do dirty checks anymore. 1.x has iterated many versions based on immerjs. The first version has a lot of dirty checks. It has been optimized to this day, and some dirty checks are inevitable because it does not have the same dependency tracking mechanism as Mobx.

So Formily2.0 decisively abandoned the immer dependency, and internally implemented a reactive domain model based on @formily/reactive.

Rxjs Dependency Problem

  • Cause the overall package volume to increase
  • There are various major versions of Break Change in Rxjs, one is that it will affect the stability of Formily, and the other is that it is prone to repeated packaging.
  • There are not many APIs used in actual use

So in Formily2.0, rxjs is directly removed. Will it cause the user's writing logic to become more complicated?

Because rxjs requires users to turn all states into Observable event flow mode, so that it can maximize its value.

What can be brought is that the mental pressure is too great, and in actual scenarios, our state is almost all modeled by object entities, rather than by event streams.

Therefore, the two mental models keep switching for users, the learning cost is too high, and the benefits are not large.

On the contrary, the Reactive model, although it is also Observable, but it uses the hijacking agent to automatically convert the data into Observable. In other words, it digests a large number of Observable calculations internally, and exposes more of the actual business logic that users can easily understand. Therefore, this model is very friendly to business modeling, which also proves that the introduction of Reactive in Formily 2.0 can not only solve performance problems, but also help users to do domain modeling more conveniently.

CoolPath Dependency Problem

  • Independent maintenance is inconvenient to read the source code, and the document review experience is not good
  • Independent maintenance and targeted debugging of inconvenient form scenarios

So in Formily2.0, CoolPath was directly moved to the main Formily warehouse and named @formily/path

Summary

After the entire dependency is governed, the overall volume of Formily 2.0 has been reduced a lot, and the controllability and stability will also be greatly improved.

Package Design Problem

Problem Disassembly:

  • The package does not have a module export standard. All APIs are exported in the @formily/antd package. There are conflicts in the naming of the internal package APIs. At the same time, the positioning of @formily/antd and @formily/antd-components has never been mentioned clearly. If the React component is used as the classification standard, they should all be together. If the core package + extended component package is used as the classification standard, it should be disassembled.
  • @formily/antd supports the esm module standard, but other underlying libraries do not, which is very unfriendly to users who use snowpack/vite.
  • There is no separate json schema protocol part as a unified layer, which makes it difficult to extend the Vue part.
  • The positioning of @formily/react-shared-components is very tasteless, in fact, it is only for component reuse between antd/fusion, but if you want to expand the ecology of other components, it is difficult to reuse.

The first point: unified convergence component package to @formily/antd, its positioning is a pure component library, the core API is derived from @formily/react and @formily/core, no longer in a package, so there is 2 benefits:

  • The positioning of the component package is clearer, and the documentation will be easier to write, and the user will also be able to locate the documentation.
  • Completely solve the problem of naming conflicts between different packages, because all APIs will not be exported from one package.

The second point: when all packages are built, the esm module and umd module are printed at the same time, including all module standards in the industry.

The third point: separate the @formily/json-schema package to facilitate the use of other UI bridge libraries.

Fourth point: Remove @formily/react-shared-components, a certain amount of redundancy can reduce the complexity of the system.

Summary

It is necessary to ensure that the positioning and responsibilities of each package are determined, otherwise it will be difficult to explain clearly whether it is writing documents or answering questions.

Q&A Cost Problem

Problem Disassembly:

The imperfection of the Schema protocol has led to a sharp increase in the number of questions answered.

  • Why type is object, sometimes it is VirtualField, sometimes it is ordinary field? In fact, a mapping is made internally, but it is very obscure.
  • Because of the historical baggage, x-props can pass both component properties and FormItem properties, but there is a problem. There is a conflict between FormItem properties and component properties, such as addonAfter.Who should it be passed to? This kind of ambiguity is very costly to understand the user.
  • Why does setFieldState modify the component data source need to set state.props.enum, it is difficult to understand.
  • x-linkages cannot adapt to more complex linkage requirements, such as calculator requirements.

The first point, Formily2.0 defines a new Schema Type called Void, which means that as long as the type of a field is void, it will automatically become a virtual field, so the way to display and declare a virtual field is very clear. Unlike 1.x, you have to guess whether there is a VirtualField registered on the front end.

The second point, Formily2.0 defines x-decorator/x-decorator-props to describe the wrapper and wrapper properties. Users can register the wrapper more conveniently, which fundamentally solves the problem of ambiguous semantics of x-props and the problem of property name conflicts.

The third point: Formily 2.0 directly maintains the data source state on the model layer, which is called dataSource. We finally don’t need to understand state.props.enum.

Fourth point: As mentioned earlier, the Formily2.0 kernel is designed based on @formily/reactive similar to Mobx, so we also define a concept of a responder at the protocol layer, called x-reactions, which supports active linkage Mode, also supports passive (dependency tracking) linkage mode. The ability of the agreement to describe the linkage is directly powerful by an order of magnitude.

Imperfect model design has led to a sharp increase in Q&A

  • The default behavior of automatically deleting values when fields are uninstalled makes it very difficult for users to understand. This behavior brings many unexpected hidden problems to users.
  • The default value and value combination strategy is not perfect, and there are always some unpredictable problems.
  • There is still no fundamental solution to the problem of array state transposition. For example, in the nested ArrayList scenario, 1.x cannot achieve automatic destruction and automatic transposition of child field states during the process of moving up and down. At the same time, the 1.x array transposition will also pollute the original array data, and will inject symbols into the array elements, which makes users very confused.
  • Active linkage writing is very troublesome to solve the needs of calculators.
  • The partial state cannot be maintained inside Effects, making some scenes extremely complicated and troublesome to implement.

The first point: The default behavior of discarding the field to uninstall the automatically deleted value, if you want to delete the value, always follow the user's behavior. Only when the user manually controls the field to hide (display === "none"), or manually set the value to be empty, the value will be deleted

The second point: always subject to user behavior, if a field has not been manipulated by the user, the merge is subject to the order of assignment.

The third point: Define the special field model ArrayField, and move the state transposition logic to the ArrayField method, so that the original array data will not be polluted At the same time, the transposition algorithm has been optimized to a large extent to ensure the absolute correctness of the transposition process. The specific transposition algorithm will be discussed later.

The third and fourth points: After the introduction of @formily/reactive, everything will be solved. For the fourth point, I will talk about it later.

The custom component extension mechanism is not elegantly designed, resulting in a sharp increase in the amount of Q&A

  • The global registration component mechanism can easily lead to the situation where the component cannot be found due to repeated packaging.
  • Custom components want to consume the form model and the current field model state. Use useForm and useField cannot be consumed because they are used to create models, not to use models.
  • Why does the reading state of a component depend on props.disabled? That is how to realize the disabled state?

The first point: abandon the global registration component mechanism and change to the factory-style registration (createSchemaField). First, you can get stronger type hints. Second, the controllability is stronger, even if it is repeated packaging, there will be no problems.

The second point: the responsibility of useForm/useField is to use the contextual form model and field model. Create a form model or a field model without using React Hook, which makes the API more clear and easy to understand.

The third point: the readPretty mode is added to the field model layer, which represents the reading state. The disabled state is determined to be disabled, and the meaning of the reading state is no longer expressed. To achieve the reading state, you can directly consume the readPretty attribute of the field model. After this modification, custom components will become more flexible and more complete

The imperfect documentation system has led to a sharp increase in the number of questions answered

  • Searching is not supported, and it is difficult for users to quickly locate the document location.
  • The documents of each package are not given, and they are all put together. It is not convenient for users to think about Formily from the top down, which makes it difficult for users to quickly locate the document location.
  • The documentation is severely missing, and many APIs have to guess how to use it.

First of all, this problem is to use the excellent documentation tool dumi in the community. Basically, it can solve the problems related to the documentation. And then it is necessary to disassemble the independent documents of each package, and operate with the idea that each package is an independent product, so that it is convenient for users to find documents and developers to maintain documents.

Summary

To solve the problem of the cost of answering questions, the core is still to be based on the user. As long as the user feels uncomfortable, it must be that the design is not elegant enough, and not flexible enough.

What are the important features?

At present, I mainly talk about the upgrade highlights of Formily 2.0 compared to 1.x. If you want to understand the overall capabilities of Formily, you can move to Formily introduction

Independent Reactive Solution

@formily/reactive , which has also been mentioned many times before. Here is a formal introduction. Its core idea is the reference Mobx, but it solves some problems that Mobx has not solved in the complex domain model:

  • mobx does not support dependency collection inside actions.
  • The observable function of mobx does not support filtering special objects such as react node, moment, and immutable.
  • mobx's observable function will automatically turn the function into an action.
  • The batch mode of mobx does not support partial batch.
  • The observer of mobx-react-lite does not support React concurrent rendering.
  • Deep observe monitor object changes cannot get the value path.

These problems should be understood by deep Mobx users. In short, the @formily/reactive wheel was created specifically to solve complex domain models like Formily, it can completely replace Mobx. At the same time, its volume is much smaller than Mobx, and because of the Formily endorsement, completeness, performance, and guaranteed stability. Of course, this also proves that as long as it is a means to make Formily better, what about making wheels?

A more elegant way of development--TS smart tips

Because Formily 2.0 is already written in Typescript, all code means that you can get an excellent smart prompt experience when using any API. The following are two very common use cases.

Pure JSX Development Mode

Regardless of whether it is the component attribute or the decorator attribute, the smart prompt for the second parameter will always follow the component passed in by the first parameter, so that the user does not need to guess which attribute API the component has.

Schema Markup Development Mode

It is worth noting that the type hints of Schema Markup are implemented with the help of Template Literal Types of Typescript 4.2.

Support Vue2/Vue3

I am very happy to tell you that Formily finally supports Vue, and it is compatible with Vue2 and Vue3 at the same time. Thank you Song Sichen again.

Specific document address @formily/vue

At the same time, the Vue-related Formily extension component ecology is also under further construction. It is also with the help of the community power. The community power is really awesome!

Effects Local State

What is the partial status of Effects, we can first look at the Select asynchronous search case.

From this case, we will find that in the Effect Hook function useAsyncDataSource, @formily/reactive can be used directly to define the state, and then it can be consumed in onFieldReact. In other words, our Effect Hook is completely self-contained, which means that we can abstract a lot of complex form logic based on Effect Hook in a very high cohesive way. The writing method is very similar to Vue3's setup. Of course, this is not an intentional imitation. It is a coincidence after the introduction of the Reactive concept. It also proves the feasibility and completeness of the effects model is very high.

Effects Context

In the effects function, if we abstract a lot of fine-grained Effect Hooks, we need to pass layer by layer to read the top-level context data in hooks, which is obviously very inefficient. So Formily2.0 provides createEffectContext to help users quickly obtain context data, which is similar to React Context. For specific documents, please refer to createEffectContext.

Smart Grid Layout

Why do you need a smart grid layout? Why not use the default grid layout of the component library?

Take Ant Design's Grid as an example. It has several problems:

  • The writing method is too cumbersome, each time you need to nest Row/Col, the amount of code is huge.
  • Unable to quickly realize the equally divided layout.
  • Unable to control the maximum or minimum width of each column for elastic expansion.

Therefore, FormGrid smart grid layout component is provided in Formily2.0, which is implemented based on css grid. Of course, it also has some js calculations inside. Its core highlights are:

  • Only need to introduce a FormGrid component to quickly realize the grid layout, which only takes effect for the html elements under the FormGrid component.
  • Only need to introduce a FormGrid component to quickly realize the grid layout, which only takes effect for the html elements under the FormGrid component.
  • Cross-column scenarios have automatic correction capabilities, because the entire layout system is fully responsive and elastic.
  • It supports setting the maximum and minimum width of each column. This ability is very useful in form scenarios, because it is necessary to prevent the form controls from becoming extremely narrow or extremely wide in ultra-narrow screen or ultra-wide screen scenes.

Document Address

Responsive Concurrent Rendering

Garbage Collection Problem

Because React needs to support concurrent mode, Function Component is very unfriendly to Reactive solutions such as Mobx. For example, in StrictMode, each update of the component will trigger the function to execute twice, and useMemo will also trigger twice. But useEffect/useLayoutEffect will only be executed once, while the Reactive solution strongly relies on useMemo. Because it is necessary to create a persistent Reactive Tracker object to bind the current component, according to the previous very confusing execution logic, it will cause Reactive Tracker to fail to reclaim memory correctly.

Refer to Mobx's solution, @formily/reactive also uses FinalizationRegistry API to automatically reclaim memory. The specific logic is to create an object reference in Function Component, and then pass it to FinalizationRegistry to monitor when the reference is destroyed. If the reference is destroyed, the Tracker created by the first useMemo will be recycled.

It should be noted that the useEffect will be triggered when the Function Component is executed for the second time, and the listener function created by the second rendering FinalizationRegistry needs to be released in useEffect (to prevent the Tracker from being destroyed by mistake). The Tracker created by the second useMemo can only be destroyed when the component is unmount.

Source Address

Parent-child component concurrent response problem

Mainly to solve the following error reporting problems

https://gist.github.com/4000c671473beac3d10dc2e8df673a33

This problem is very difficult to solve, even Mobx has not solved it, so how can this problem be triggered?

If the parent and child components both rely on the same Reactive data, and the child component modifies the Reactive data during the first rendering, the error will be triggered.

So how does @formily/reactive-react solve it?

The author took a whole month to think of a solution, the core of the solution is: globally maintain an update queue, and a rendering counter. The component wrapped by the observer triggers the counter to increase by 1 each time it is rendered, and it triggers to decrease by 1 when the useLayoutEffect is executed. The count value needs to be checked when the component is triggered to update. If it is 0, it means that no other components are being rendered globally. If it is greater than 0, it means that other components are being rendered and will be updated to the team. The count value is also checked when useLayoutEffect is executed. If it is 0, it means that the whole is in a stable state, and the update queue is executed in batches (while executing and deleting queue elements).

Note that there is actually a pitfall in using useLayoutEffect directly here, because the number of executions of useLayoutEffect is unstable. As mentioned in the memory reclamation problem, how to solve this problem? Here the author used a very tricky method, create a timer at the same time that the counter is incremented by 1 to perform the counter decrement by 1 and execute the update queue. The timer is cleared in useLayoutEffect, because useLayoutEffect is executed synchronously. If under normal circumstances, useLayoutEffect is executed before the timer is executed, clearing the timer will not trigger the counter recalculation, but if the useLayoutEffect is not triggered correctly, such as StrictMode, and if the component calls the invalid setState scenario, then it will go. The logic of the timer is the bottom line.

Source Address

Array Transpose Algorithm

The array transposition algorithm in 1.x, it can be said that there is no algorithm, it is very incomplete, and insecure. At the same time, it is difficult to troubleshoot problems, mainly because:

  • It marks each element by polluting the original data of the array, and then judges whether the array has been moved, deleted, or added based on the mark of each update.
  • Deleting nodes based on a very unreliable timer. There are no other good ideas except that.

In Formily 2.0, the first problem is that by directly defining the ArrayField operation method, it is possible to clearly know the actual operation of the user without polluting the original data of the array.

The second question, here comes the focus:

First of all, Formily2.0 abandons the past FormGraph data structure, and directly stores all field models in the fields attribute of the form model, which is a pure object structure:

https://gist.github.com/6658c002178d9d7f666d767cf8db91fb

Because we know the specific operation type of the array, isn't our state transposed to change the address of field A into the address of field B? It's that simple, so we do a full traversal every time we operate, find the field model to be replaced, and replace the address. The model does not need to be changed at all. Of course, there are many details in it, but the overall idea is like this. In short, this kind of thinking fundamentally solves Formily's pain points in array state transposition.

Source Address

What other details are optimized?

The kernel is connected to FormilyDevtools by default

In 1.x, the communication with the chrome plug-in has always been done at the @formily/react layer. If users want to support vue, they have to implement a separate communication. Obviously, this is not elegant enough. So Formily2.0 built the plug-in communication logic into the kernel, so that no matter which framework is connected, you can get an excellent chrome debugging experience.

More precise calibration timing control

In 1.x, the verification timing parameter triggerType is linked to the field attribute, that is, it can only control the verification trigger timing of a field in batches. But in most scenarios, we actually set the trigger timing for a single verification rule. For some verification rules, we still want to use the default (onChange trigger). Therefore, Formily 2.0 implements this feature.

Specific Document ReferenceFieldValidator

The verification result supports a successful state

In 1.x, the verification result supports the warning type, that is, does not block submission, but the FormItem will have a warning style. But in some scenarios, we may also need to go through the verification rule logic for the successful state. Therefore, Formily 2.0 supports the success type, and FormItem also has a success style.

Specific Document ReferenceFieldValidator

FormPath supports relative path query

In 1.x, relative paths can only be used in x-linkages. In this 2.0 upgrade, we directly support relative path calculations to FormPath, that is, we don’t need to use cumbersome transforms. Moreover, each field model has a query method, which can quickly query relative fields, such as:

https://gist.github.com/e24a42f7a55f81c7e0e943752b961dd4

Specific Document Reference FormPath

Summary

So much has been said before, in fact, the core of the biggest change in Formily 2.0 is the introduction of Reacitve responsive programming mode, which makes all problems easily solved. Of course, the highlights and detailed optimization are more than just those mentioned, there are still many things you need to dig and experience! Seeing this, do you want to give it a try?

Q/A

Q: Why does the beta version last for half a year?

Answer: The beta version that has been released is already a usable version, and the single test coverage has exceeded 90%. Such a long iteration time is mainly used to collect user feedback. If the design is not good, it can be corrected in time to prevent the official version from giving The user buries the pit.

Question: The follow-up plan?

Answer: The next step is mainly the 2.0 form designer, which is currently in the development stage, and a beta version is expected to be released by the end of the year.

Question: Will 1.x continue to be maintained after the official version is released?

Answer: Maintenance will continue, but no new features will be added, only bugfixes will be made, and maintenance is expected to continue until the end of 2022.

Question: What is the browser compatibility of Formily2.0?

Answer: Not compatible with IE

Question: Does @formily/antd support antd3.0?

Answer: Not supported.

Question: Now that I have Vue, why do I need to provide @formily/vue?

Answer: Vue is a UI framework. The problem it solves is a wider range of UI problems. Although its reactive ability is outstanding in form scenarios, at least it is more convenient than native React to write forms, but if it is in more complex form scenarios , We still need to do a lot of abstraction and encapsulation. So @formily/vue is to help you do these abstract encapsulation things, and really let you develop super-complex form applications efficiently and conveniently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment