This article will use the TypeClient architecture to illustrate how to use AOP+IOC ideas to deconstruct front-end project development.

Front-end development will theoretically drift towards IOC development

First of all, understanding the idea of AOP+IOC requires some foundation in programming architecture. Currently, the scenarios used for these two ideas are mostly on the NodeJS side, with very little front-end practice. My idea was to provide a new way of deconstructing the project, rather than overturning the community’s huge family bucket. It’s good to have a look. If it can provide you with better inspiration, it’s great. Welcome to exchange.

Here we’ll use TypeClient’s React rendering engine as an example.

AOP

An idea of section-oriented programming. It acts as a front-end decorator that intercepts custom behavior before and after a function is executed.

The main role of AOP is to isolate functions that are not related to the core business logic module, such as logging statistics, security control, exception handling, and so on. After these functions are extracted, they are incorporated into the business logic module through “dynamic weaving”. The benefits of AOP are, first, the ability to keep business logic modules pure and highly cohesive, and, second, the ease with which functional modules such as log statistics can be reused.

That’s a simple explanation of AOP on the web. So the actual code might look something like this

@Controller()
class Demo {
  @Route() Page(){}}Copy the code

Most of the time, however, we simply treat a function under a class as an object that stores data, and when we decide to run the function, we pull out the data and customize it. You can learn more about the role of decorators by using reflect-Metadata.

IOC

Angular has struggled to gain domestic acceptance in large part because it is too big a concept, and its dependency inject (DI) is even more confusing to use. In fact, there is another idea besides DI called IOC. Its representative library is Inversify. It has a 6.7K star count on Github and is very well received in the DI community. We can start with this library to see its benefits for project deconstruction.

Examples are as follows:

@injectable(a)class Demo {
  @inject(Service) private readonly service: Service;
  getCount() {
    return 1 + this.service.sum(2.3); }}Copy the code

Of course, the Service is already injected into the InVersify Container before it can be called through TypeClient.

Reorganize the front end project runtime

Typically, front-end projects go through this process.

  1. By listeninghashchangeorpopstateEvent intercepts browser behavior.
  2. Set the current obtainedwindow.locationHow data corresponds to a component.
  3. How components are rendered to pages.
  4. How do we map to a component and render it when the browser URL changes again?

This is a common solution for the community. Of course, we won’t explain how to design this pattern. We will deconstruct this process with new design patterns.

Re-examine the server routing architecture

We’re talking about front-end architecture. Why are we talking about server-side architecture?

That’s because design patterns aren’t limited to the back end or the front end; they should be a more general approach to solving specific problems.

So one might ask, what’s the point of having a routing system on the server that doesn’t match the front-end?

Take nodeJS’s HTTP module as an example, which is somewhat similar to the front end. The HTTP module runs in a process that responds to data through http.createserver’s parameter callback function. We can think of the front-end page as a process in which we respond by listening for events in the corresponding mode to get components rendered to the page.

Multiple clients send requests to a server port for processing. Why can’t the front-end user operate the browser address bar through events to get the response entry?

The answer is yes. We call this approach Virtual Server which stands for page-based virtual services.

Since we can abstract as a service architecture, of course, we can be exactly like the NodeJS servitization solution, and we can handle the front-end routing in a way that is common on the NodeJS side, more in line with our intentions and abstractions.

history.route('/abc/:id(\\d+)'.(ctx) = > {
  const id = ctx.params.id;
  return <div>{id}</div>;
  Ctx. body = 
      
{id}
; This is more understandable
}) Copy the code

Modified route design

If it is written in the above way, then it can also solve the basic problem, but it is not in line with our AOP+IOC design, the writing is still cumbersome, and there is no deconstruction of the response logic.

We need to solve the following problems:

  1. How to parse routing string rules?
  2. How can I use this rule to quickly match the corresponding callback function?

There are many libraries on the server that parse routing rules, most notably path-to-regexp, which is used in well-known architectures such as KOA. The idea is to regularize the string and use the currently passed path to match the rules to get the corresponding callback function to process it. There are some drawbacks to this approach, however, which are that the regex match speed is slow, all rules will be executed when the last rule in the processing queue is matched, and performance is poor when there are too many routes, as I wrote earlier that the KOA-Rapid-router outperforms the KOA-Router by more than 100 times. Another flaw is that it matches in the order in which you wrote it, so it has a certain orderality that developers need to be careful about. Such as:

http.get('/:id(\\d+)'.() = > console.log(1));
http.get('/ 1234'.() = > console.log(2));
Copy the code

If we access /1234, it will print 1 instead of 2.

To address performance and optimize the intelligence of the matching process, we can refer to find-My-way routing design architecture. Please look at the specific officer himself, I do not resolve. In short, it is a string indexing algorithm that can quickly and intelligently match the route we need. Fastify famously uses this architecture to achieve high performance.

TypeClient routing design

We can quickly define our routes with a few simple decorators, essentially using find-my-way route design principles.

import React from 'react';
import { Controller, Route, Context } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() = > props.status.value);
    return <div>Hello world! {status}</div>; }}// --------------------------
// In index.ts as long as
app.setController(DemoController);
// It automatically binds the route, while the page enters route '/ API /test'
// The text 'Hello world! 200 `.
Copy the code

As you can see, TypeClient is very simple to define routing through AOP concepts.

Route life cycle

When you jump from one page to another, the life cycle of the previous page ends, so routing has a life cycle. Then, we disassemble the entire page cycle as follows:

  1. The beforeCreate page starts to load
  2. The created page is loaded
  3. BeforeDestroy the page is being destroyed
  4. The Destroyed page has been destroyed

To represent these four life cycles, we created a function useContextEffect based on the React hooks to handle the side effects of the route life cycle. Such as:

import React from 'react';
import { Controller, Route, Context } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() = > props.status.value);
    useContextEffect(() = > {
      console.log('Route load completed');
      return () = > console.log('Route destroyed');
    })
    return <div>Hello world! {status}</div>; }}Copy the code

In fact, it is similar to useEffect or useEffect. It’s just that we focus on the lifecycle of the route, whereas React focuses on the lifecycle of the component.

The route is stateful, 100, 200, 500, etc. We can use this data to determine what life cycle the current route is in, and we can also use skeleton screens to render different effects.

Middleware design

In order to control the routing life cycle, we designed middleware patterns to handle pre-routing behaviors, such as requesting data and so on. The middleware is in principle koA-aligned, which makes it much more compatible with the community ecosystem.

const middleware = async (ctx, next) => {
  // ctx.....
  await next();
}
Copy the code

With AOP we can easily reference this middleware to achieve data processing before the page is loaded.

import React from 'react';
import { Controller, Route, Context, useMiddleware } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  @useMiddleware(middleware)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() = > props.status.value);
    useContextEffect(() = > {
      console.log('Route load completed');
      return () = > console.log('Route destroyed');
    })
    return <div>Hello world! {status}</div>; }}Copy the code

Design Cycle State Management – ContextStore

I have to say this is a bright spot. Why design such a pattern? The main purpose is to solve the middleware in the process of data operations can be timely response to the page. Because middleware execution is synchronized with react page rendering, we designed this pattern to facilitate data periodicity.

We took a very dark tech solution to this problem: @vue/ Reactity

Yes, that’s it.

We embedded VUE3’s latest responsive system in React, allowing us to develop fast update data without the dispatch process. Of course, this is extremely powerful for middleware to update data.

Here I would like to thank SL1673495 for giving us the idea of dark technology to make our design perfectly compatible with React.

We use @state (callback) to define ContextStore initialization data, and use useContextState or useReactiveState to track data changes and respond to the React page.

Here’s an example:

import React from 'react';
import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  @useMiddleware(middleware)
  @State(createState)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() = > props.status.value);
    const count = useReactiveState(() = > props.state.count);
    const click = useCallback(() = > ctx.state.count++, [ctx.state.count]);
    useContextEffect(() = > {
      console.log('Route load completed');
      return () = > console.log('Route destroyed');
    })
    return <div onClick={click}>Hello world! {status} - {count}</div>; }}function createState() {
  return {
    count: 0,}}Copy the code

You can see that as you click, the data changes. This mode of operation greatly simplifies the writing of data changes, and can be on a level with vue3’s responsiveness capability to make up for the complexity of react data operations.

In addition to using this hack in cycles, it can also be used independently, such as in any location:

// test.ts
import { reactive } from '@vue/reactity';

export const data = reactive({
  count: 0,})Copy the code

We can use it in any component

import React, { useCallback } from 'react';
import { useReactiveState } from '@typeclient/react-effect';
import { data } from './test';

function TestComponent() {
  const count = useReactiveState(() = > data.count);
  const onClick = useCallback(() = > data.count++, [data.count]);
  return <div onClick={onClick}>{count}</div>
}
Copy the code

Deconstruct the project using IOC thinking

None of the above has been about designing IOC, so here’s how to use IOC.

Controller service Deconstruction

Let’s start by writing a Service file

import { Service } from '@typeclient/core';

@Service()
export class MathService {
  sum(a: number, b: number) {
    returna + b; }}Copy the code

Then we can call it directly in the previous Controller:

import React from 'react';
import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
import { MathService } from './service.ts';
@Controller('/api')
export class DemoController {
  @inject(MathService) private readonly MathService: MathService;

  @Route('/test')
  @useMiddleware(middleware)
  @State(createState)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() = > props.status.value);
    const count = useReactiveState(() = > props.state.count);
    const click = useCallback(() = > ctx.state.count++, [ctx.state.count]);
    const value = this.MathService.sum(count, status);
    useContextEffect(() = > {
      console.log('Route load completed');
      return () = > console.log('Route destroyed');
    })
    return <div onClick={click}>Hello world! {status} + {count} = {value}</div>; }}function createState() {
  return {
    count: 0,}}Copy the code

You can see how the data keeps changing.

Component deconstruction

We created a new component schema for react components called IOCComponent. It is an IOC-capable component that we call through the hooks of useComponent.

import React from 'react';
import { Component, ComponentTransform } from '@typeclient/react';
import { MathService } from './service.ts';

@Component()
export class DemoComponent implements ComponentTransform {
  @inject(MathService) private readonly MathService: MathService;

  render(props: React.PropsWithoutRef<{ a: number, b: number }>) {
    const value = this.MathService.sum(props.a, props.b);
    return <div>{value}</div>}}Copy the code

It is then called in any component

import React from 'react';
import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
import { MathService } from './service.ts';
import { DemoComponent } from './component';
@Controller('/api')
export class DemoController {
  @inject(MathService) private readonly MathService: MathService;
  @inject(DemoComponent) private readonly DemoComponent: DemoComponent;

  @Route('/test')
  @useMiddleware(middleware)
  @State(createState)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() = > props.status.value);
    const count = useReactiveState(() = > props.state.count);
    const click = useCallback(() = > ctx.state.count++, [ctx.state.count]);
    const value = this.MathService.sum(count, status);
    const Demo = useComponent(this.DemoComponent);
    useContextEffect(() = > {
      console.log('Route load completed');
      return () = > console.log('Route destroyed');
    })
    return <div onClick={click}>
      Hello world! {status} + {count} = {value} 
      <Demo a={count} b={value} />
    </div>; }}function createState() {
  return {
    count: 0,}}Copy the code

Middleware deconstruction

We can completely abandon the traditional middleware writing method and use the middleware writing method that can add deconstruction:

import { Context } from '@typeclient/core';
import { Middleware, MiddlewareTransform } from '@typeclient/react';
import { MathService } from './service';

@Middleware()
export class DemoMiddleware implements MiddlewareTransform {
  @inject(MathService) private readonly MathService: MathService;

  async use(ctx: Context, next: Function) {
    ctx.a = this.MathService.sum(1.2);
    awaitnext(); }}Copy the code

Added the Slot Slot concept for React

It supports Slot Slot mode, and we can get providers and consumers through useSlot. It is a pattern of passing fragments of nodes through messages.

const { Provider, Consumer } = useSlot(ctx.app);
<Provider name="foo">provider data</Provider>
<Consumer name="foo">placeholder</Consumer>
Copy the code

Then write an IOCComponent or traditional component.

// template.tsx
import { useSlot } from '@typeclient/react';
@Component()
class uxx implements ComponentTransform {
  render(props: any) {
    const { Consumer } = useSlot(props.ctx);
    return <div>
      <h2>title</h2>
      <Consumer name="foo" />
      {props.children}
    </div>}}Copy the code

And then finally on the Controller

import { inject } from 'inversify';
import { Route, Controller } from '@typeclient/core';
import { useSlot } from '@typeclient/react';
import { uxx } from './template.tsx';
@Controller()
@Template(uxx)
class router {
  @inject(ttt) private readonly ttt: ttt;
  @Route('/test')
  test() {
    const { Provider } = useSlot(props.ctx);
    return <div>
      child ...
      <Provider name="foo">
        this is foo slot
      </Provider>
    </div>}}Copy the code

The structure you can see is as follows:

<div>
  <h2>title</h2>
  this is foo slot
  <div>child ...</div>
</div>
Copy the code

Deconstruct the principles of the project

By deconstructing IOC services and Middleware and components at different levels, we can create a unified NPM package that can be uploaded to a private repository for in-house development.

type

  1. IOCComponent + IOCService
  2. IOCMiddleware + IOCService
  3. IOCMiddlewware
  4. IOCService

The principle of

  1. generalized
  2. In the polymerization
  3. Easy extension

Following this principle can make a company’s business code or components have a high degree of reuse, and AOP can clearly and intuitively represent the charm of code as documentation.

generalized

That is, to ensure that the logic, code, or component that you encapsulate is highly generic in nature, and that there is no need to encapsulate less generic logic. For example, a unified navigation header within a company, which can be used for unification in any project, is a good candidate for encapsulation as a component module.

cohesion

General-purpose components that require uniform data can be wrapped in a form called IOCComponent + IOCService + IOCMiddleware, where appropriate, just import the component. Again, the generic navigation header. For example, if the navigation header needs to pull down a list of teams, we can define this component as follows:

A service file:

// service.ts
import { Service } from '@typeclient/core';
@Service()
export class NavService {
  getTeams() {
    / /... This could be the result of an Ajax request
    return[{name: 'Team 1'.id: 1}, {name: 'Team 2'.id: 1,}}]goTeam(id: number) {
    // ...
    console.log(id); }}Copy the code

Components:

// component.ts
import React, { useEffect, setState } from 'react';
import { Component, ComponentTransform } from '@typeclient/react';
import { NavService } from './service';

@Component()
export class NavBar implements ComponentTransform {
  @inject(NavService) private readonly NavService: NavService;
  render() {
    const [teams, setTeams] = setState<ReturnType<NavService['getTeams'] > > ([]); useEffect(() = > this.NavService.getTeams().then(data= > setTeams(data)), []);
    return <ul>
      {
        teams.map(team => <li onClick={()= > this.NavService.goTeam(team.id)}>{team.name}</li>)}</ul>}}Copy the code

We define this module as @fe/navbar and export this object:

// @fe/navbar/index.ts
export * from './component';
Copy the code

This can be done in any IOC component

import React from 'react';
import { Component, ComponentTransform, useComponent } from '@typeclient/react';
import { NavBar } from '@fe/navbar';

@Component()
export class DEMO implements ComponentTransform {
  @inject(NavBar) private readonly NavBar: NavBar;
  render() {
    const NavBar = useComponent(this.NavBar);
    return <NavBar />}}Copy the code

You can see that as soon as the component is loaded, the requested data is automatically loaded. This is very different from the normal component pattern, which can be a business component deconstruction solution. Very practical.

Easy extension

Mainly is to allow us to design the universal code or when components keep taking expansibility, for example, use opportunely SLOT SLOT principle, we can reserve some space for SLOT, convenient this component by using different location code transmission content and replace the original position, the benefits of this requires developers to experience.

demo

We provided a demo to demonstrate its capabilities, and you can see how to deconstruct the entire project from the code. Each of our controllers can exist independently, making it easy to migrate project content.

  • Frame: github.com/flowxjs/Typ…
  • Project template: github.com/flowxjs/Typ…
  • Simple best practices: github.com/flowxjs/Typ…

You can use the above two examples to understand the development pattern.

conclusion

The new development concept doesn’t mean you have to abandon traditional development methods and communities, but rather provide a better way of thinking. Of course, there are different interpretations of the good and bad aspects of this line of thinking. But I still want to make it clear that I am only providing a new idea today, and it is good for everyone to have a look. If you like it, give a star. Thank you very much!