This article will go through TypeClient Architecture to illustrate how to use AOP+IOC Ideas to deconstruct the development of front-end projects .
First statement ,AOP+IOC The understanding of ideas needs to have a certain foundation of programming architecture . at present , The scenarios in which these two ideas are used , Basically all in nodejs End , There is very little practice in the front end . I have the idea of providing a new way to deconstruct projects , Instead of overthrowing the community's huge family barrel . Just look at it , If it can give you better inspiration , Well, it's better , Welcome to exchange .
Now we will use TypeClient Of React For example, the rendering engine .
AOP
An idea of Aspect Oriented Programming . Its performance in the front end is the decorator of the front end , We can use decorators to intercept custom behavior before and after function execution .
AOP Its main function is to extract some functions unrelated to the core business logic module , These functions that are not related to business logic usually include log statistics 、 safety control 、 Exception handling . After taking out these functions , Re pass “ Dynamic weaving ” The business logic module . AOP First of all, it can keep the business logic module pure and highly cohesive , Secondly, it is easy to reuse log statistics and other functional modules .
The above is about the Internet AOP A simple explanation of . So the actual code might look like this
@Controller()
class Demo {
@Route() Page() {}
}
Copy code
But a lot of times , We're just going to put some class The function under is just an object to store data , When you are sure to run this function, take out the data to do custom processing . Can pass reflect-metadata To learn more about the role of decorators .
IOC
Angular It is difficult to be accepted at home, in large part because its concept is too large , And one of the DI(dependency inject) It's even more confusing in use . Except DI There is also the idea of dependency injection called IOC. Its representative library is inversify. It's in github Owned on 6.7K Of star Count , In the community of dependency injection , Good reputation . We can learn about the benefits of this library for project deconstruction .
Examples are as follows :
@injectable()
class Demo {
@inject(Service) private readonly service: Service;
getCount() {
return 1 + this.service.sum(2, 3);
}
}
Copy code
Of course ,Service Has been injected into inversify Of container Inside , To get through TypeClient This call .
Reorganize the front-end project runtime
In a general way , The front-end project will go through this process .
- By monitoring
hashchange
perhapspopstate
Event intercepts browser behavior . - Set the currently obtained
window.location
How data is mapped to a component . - How components render to the page .
- When browser URL When it changes again , How do we map to a component and render .
This is a common solution for the community . Of course , We're not going to talk about how to design this pattern . We're going to deconstruct this process with a new design pattern .
Re examine the server-side routing system
We're talking about the architecture of the front end , Why do you talk about the architecture of the server ?
That's because , In fact, design patterns are not limited to the back end or the front end , It should be a more general way to solve specific problems .
So maybe someone will ask , The routing system of the server is not consistent with the front end , What is the significance? ?
We use nodejs Of http Module as an example , In fact, it's a bit similar to the front end .http Modules run in a process , adopt http.createServer
In response to the data . We can argue that , The front page is equivalent to a process , We respond by listening to events in the corresponding mode to get the component rendered to the page .
Multiple servers Client Send a request to server End port processing , Why can't we use the analogy of front-end users operating the browser address bar to get the response entry through events ?
The answer is yes . We call this way virtual server
That is, virtual services based on page level .
Since we can abstract a service architecture , Of course , We can be exactly like nodejs The service-oriented solution of the project is close to , We can handle the front-end routing as follows nodejs End common way , More in line with our intentions and abstractions .
history.route('/abc/:id(d+)', (ctx) => {
const id = ctx.params.id;
return <div>{id}</div>;
// perhaps : ctx.body = <div>{id}</div>; This is more understandable
})
Copy code
Modifying routing design
If it's written in the above way , So it can also solve the basic problem , But it doesn't fit us AOP+IOC The design of the , It's rather tedious when writing , And it doesn't deconstruct the response logic .
We need to solve the following problems :
- How to parse routing string rules ?
- This rule is used to quickly match callbacks ?
There are many libraries for parsing routing rules on the server side , What is more representative is path-to-regexp, It is used in KOA In the famous architecture . Its principle is to regularize strings , Use the currently passed in path To match the corresponding rules to get the corresponding callback function to deal with . But there are some flaws in this approach , That is, regular matching is slower , When the last rule of the processing queue is matched , All the rules will be enforced , When there are too many routes, the performance is poor , This can be seen from what I wrote earlier koa-rapid-router transcend koa-router The performance of the 100 Many times . Another flaw is , It's matched in the order you write it , So it has a certain order , Developers need to pay great attention to . such as :
http.get('/:id(d+)', () => console.log(1));
http.get('/1234', () => console.log(2));
Copy code
If we visit /1234
, So it will print out 1
, Instead of 2
.
In order to solve the performance and optimize the intelligence of the matching process , We can refer to find-my-way The routing design architecture of . Please see for yourself , I don't parse . All in all , It's a string indexing algorithm , It can match the route we need quickly and intelligently . The famous fastify This architecture is used to achieve high performance .
TypeClient The routing design of
We can quickly define our route through some simple decorators , The essence is to adopt find-my-way
Routing design principles of .
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>;
}
}
// --------------------------
// stay index.ts As long as
app.setController(DemoController);
// It automatically binds the route , At the same time, the page enters the route `/api/test` When
// The text will be displayed `Hello world! 200`.
Copy code
so ,TypeClient adopt AOP The idea of defining routing is very simple .
Routing lifecycle
When you jump from one page to another , The life cycle of the previous page ends , therefore , Routing has a lifecycle . Again , We break down the entire page cycle as follows :
- beforeCreate The page starts loading
- created Page loading complete
- beforeDestroy The page is about to be destroyed
- destroyed The page has been destroyed
To show this 4 Life cycles , We according to the React Of hooks A special function useContextEffect
To deal with the side effects of the routing lifecycle . 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(' The route load is complete ');
return () => console.log(' The route is destroyed ');
})
return <div>Hello world! {status}</div>;
}
}
Copy code
In fact, it is related to useEffect
perhaps useLayoutEffect
Some similar . It's just that we focus on the life cycle of routing , and react Focus on the lifecycle of the component .
In fact, through the above props.status.value
We can guess , Routing is stateful , Namely 100
and 200
also 500
wait . We can use such data to determine what life cycle the current route is in , You can also render different effects through the skeleton screen .
Middleware design
In order to control the operation of the routing lifecycle , We designed the middleware pattern , It is used to handle the behavior before routing , For example, request data and so on . In principle, middleware adopts and KOA Consistent patterns , This can be very compatible with the community ecology .
const middleware = async (ctx, next) => {
// ctx.....
await next();
}
Copy code
adopt AOP We can easily reference this middleware , Data processing before page loading finished state .
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(' The route load is complete ');
return () => console.log(' The route is destroyed ');
})
return <div>Hello world! {status}</div>;
}
}
Copy code
Design cycle state management - ContextStore
It has to be said that this is a bright spot . Why design such a pattern ? It is mainly to solve the problem that the operation of data in the process of middleware can respond to the page in time . Because middleware implements and react Page rendering is synchronous , So we design this pattern to facilitate the periodization of data .
We used a very black tech solution to this problem :@vue/reactity
Yes , Is it .
We are react Embedded in VUE3
The latest responsive system , Let's develop fast update data , And give up dispatch
The process . Of course , This is very powerful for middleware to update data .
here I thank you very much sl1673495 Given the black technology ideas, our design can be perfectly compatible react.
We go through @State(callback)
To define ContextStore Initialization data of , adopt useContextState
perhaps useReactiveState
Track data changes and respond to React On the page .
Let's look at 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(' The route load is complete ');
return () => console.log(' The route is destroyed ');
})
return <div onClick={click}>Hello world! {status} - {count}</div>;
}
}
function createState() {
return {
count: 0,
}
}
Copy code
You can see the constant click , The data is changing . This way of operation greatly simplifies the writing of our data , At the same time, it can also be associated with vue3 Responsive ability is in line with , make up react The short board of data operation complexity .
In addition to using this black technology in cycles , In fact, it can also be used independently , For example, define... Anywhere :
// test.ts
import { reactive } from '@vue/reactity';
export const data = reactive({
count: 0,
})
Copy code
We can use... 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 code
utilize IOC Thought deconstruction project
None of the above explanations are designed IOC aspect , So the following will explain IOC Use .
Controller Service deconstruction
Let's write a Service file
import { Service } from '@typeclient/core';
@Service()
export class MathService {
sum(a: number, b: number) {
return a + b;
}
}
Copy code
And then we can do it before Controller Call directly :
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(' The route load is complete ');
return () => console.log(' The route is destroyed ');
})
return <div onClick={click}>Hello world! {status} + {count} = {value}</div>;
}
}
function createState() {
return {
count: 0,
}
}
Copy code
You can see the data changing .
Component deconstruction
We are react A new component pattern is created by the component of , call IOCComponent
. It's a way to have IOC The components of capability , We go through useComponent
Of hooks To call .
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 code
It is then invoked 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(' The route load is complete ');
return () => console.log(' The route is destroyed ');
})
return <div onClick={click}>
Hello world! {status} + {count} = {value}
<Demo a={count} b={value} />
</div>;
}
}
function createState() {
return {
count: 0,
}
}
Copy code
Middleware deconstruction
We can abandon the traditional middleware writing method , The middleware can be added and deconstructed :
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);
await next();
}
}
Copy code
by react newly added Slot Slot concept
It supports Slot Slot mode , We can go through useSlot get Provider And Consumer. It's a pattern of passing node fragments through messages .
const { Provider, Consumer } = useSlot(ctx.app);
<Provider name="foo">provider data</Provider>
<Consumer name="foo">placeholder</Consumer>
Copy code
And then write a IOCComponent Or traditional components .
// 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 code
Last in Controller On the call
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 code
The structure you can see is as follows :
<div>
<h2>title</h2>
this is foo slot
<div>child ...</div>
</div>
Copy code
Principles for deconstructing projects
We can pass the IOC Services and Middleware There are also components that deconstruct at different latitudes , Packaged as a unified npm The package is uploaded to the private warehouse for internal development .
type
- IOCComponent + IOCService
- IOCMiddleware + IOCService
- IOCMiddlewware
- IOCService
principle
- Generalization
- Internal polymerization
- Easy to expand
Following this principle can make the company's business code or components highly reusable , And through AOP It can clearly and intuitively show the charm of code or document .
Generalization
That is to guarantee the logic encapsulated 、 High degree of generality of code or component , There is no need to encapsulate for less general purpose . for instance , Unified navigation head within the company , Navigation head may be used in any project for unification , So it is very suitable to package as a component module .
Cohesion
Common components need unified data , So you can go through IOCComponent + IOCService + IOCMiddleware In the form of , In the appropriate use, just focus on importing this component . Or for example, the general navigation head . For example, the navigation header needs to drop down a list of teams , that , We can define this component like this :
One service file :
// service.ts
import { Service } from '@typeclient/core';
@Service()
export class NavService {
getTeams() {
// ... This could be ajax Requested results
return [
{
name: 'Team 1',
id: 1,
},
{
name: 'Team 2',
id: 1,
}
]
}
goTeam(id: number) {
// ...
console.log(id);
}
}
Copy 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 code
We define this module as @fe/navbar
, Export this object at the same time :
// @fe/navbar/index.ts
export * from './component';
Copy code
At random IOC Components can be called in this way
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 code
You can see that just load this component , The request data is automatically loaded , This is very different from the normal component pattern , It can be a business type component deconstruction solution . Very practical .
Easy to expand
The main thing is to keep extensibility when designing this generic code or component , for instance , Skillfully use SLOT Slot principle , We can reserve some space for slots , It is convenient for this component to be transmitted by using different location code and replace the original location content , The benefits of this need to be realized by developers themselves .
demonstration
We provided one demo To show its ability , And you can see from the code how to deconstruct the entire project . Every one of us Controller Can exist independently , Make project content migration very easy .
- frame : github.com/flowxjs/Typ…
- Project template : github.com/flowxjs/Typ…
- Simple best practices : github.com/flowxjs/Typ…
You can learn about the development mode through the above two examples .
summary
The new concept of development is not to get rid of traditional development methods and communities , And offer better ideas . Of course , The good and bad of this kind of thinking , Each has its own understanding . But I still want to state , I'm just offering a new idea today , Just look at it , Give me what I like star. Thank you very much !