The author is | Zhang Xiaojun
Source |ERDA official account
takeaway: In fact, there are still a lot of basic things to be done in the front end area. If you don’t build a wheel for its own sake, you’re doing something worthwhile. Therefore, we decided to write the “Erda front-end sound” series of articles, in-depth analysis of our front-end exploration process in some of the landing experience, in order to help developers forge ahead in the front-end road, to discover their own wonderful as soon as possible.
Recommendations:
- The Soul Torture: How Do We Write a State Management Library for Us?
- Brief Discussion: Analysis and Practice of Front-end Routing Principle (this article)
preface
Hello, this is the ERDA Technical Team. As the front end of the Erda project, the business complexity of the Erda-UI project has been increasing continuously since the initial development to the present open source. The code files of the project have reached nearly 2000, and the internal routing configuration of the project has exceeded 500. This article will start with a brief introduction to the front-end routing principles and the basic use of React-Router. It will then focus on some of the extended functionality that the Erda-UI project has implemented on routing.
background
With the maturity of single-page applications (SPAs), routing has become the main configuration of front-end projects. We use routing to manage the composition of project pages. Each front-end framework also has its own mature routing solutions (React: React-Router, Vue: Vue-Router). In complex business systems, there is often a lot of other logic related to routing, such as permissions, breadcrumbs, and so on. We hope that this part of the logic can be integrated into the configuration of the route, which can effectively reduce the burden of development and maintenance. The Erda-UI project uses the React framework, so everything below is based on React-Router.
Routing principle
The basic principle of routing is to modify the browser link without refreshing the browser, while listening for changes in the link and finding a matching component rendering. These two conditions can be met to achieve.
Routing implementations usually take the following two forms:
- hash ( /#path )
- history ( /path )
Hash is used by default as an anchor point in the browser. In hash mode, there is always a # in the URL, which is not as beautiful as the traditional URL notation, so it is a better choice to use history mode without considering compatibility.
hash
In hash mode, the part after the # in the URL is just a client state. When this part changes, the browser itself does not refresh. The first condition is naturally met (that is, to modify the browser link without refreshing the browser). At the same time by listening for HashChange events or registering the OnHashChange callback function to listen for changes in the hash value in the URL.
window.addEventListener('hashchange', hashChangeHandler);
// or window.onhashchange = hashChangeHandler;
history
History.pushState and History.replaceState are two methods that can be used to handle the browser’s history without refreshing the page. The former isto add a new record to the page. The latter replaces the last record. Also listen for URL changes by either listening for popState events or registering the onpopstate callback function.
window.addEventListener('popState', locationChangeHandler);
// or window.onpopstate = locationChangeHandler;
However, one thing to note here is that history.pushState and history.replaceState do not automatically trigger popState. This event is triggered only when a browser action is taken, such as when the user clicks the browser’s back button. Typically, the routing library encapsulates a listening method that can be triggered by history.pushState, history.replaceState, or a user-triggered browser action on a route change. Take Listen (partially pseudocode) in React-router-DOM as an example:
function setState(nextState) { _extends(history, nextState); history.length = history.entries.length; / / routing using state management, changes in the changes, inform all listeners transitionManager. NotifyListeners (history. The location and history. The action); Function push(path, state) {//... globalHistory.pushState({ key: key, state: state }, null, href); / /... SetState ({// Manually trigger listener action: action, location: Location})} // Listeners for the popState event are setState and the Listeners for the event are notified; function handlePopState(location){ // ... setState(location) // ... } // Encapsulate listen. function listen(listener) { var unlisten = transitionManager.appendListener(listener); window.addEventListener('popState', handlePopState); // Listen for browser events. / /... }
React-Router Routing Foundation
In order to facilitate the discussion below, this chapter first introduces the basics of React-Router.
Based on library
The React-Router libraries include the following:
- The react – the router core library
- React – Router – DOM – based routing implementation, internal contains the React – Router implementation, use without referring to the React – Router
- React – Router – Native Routing Implementation Based on React Native
- React – router-Redux integration of routing and Redux, no longer maintained
- React -router-config is used to configure static routes
react-router-dom
The React -Router -DOM library also provides two routing components corresponding to the two implementations of routing: BrowserRouter and HashRouter.
- Route: The routing unit is configured with a path and corresponding rendering components, where exact represents an exact match
- Switch: controls the rendering of the first matching routing component
- Link: Linked components, equivalent to tags
- Redirect: Redirect components
use
The basic uses of routing are as follows:
import { BrowserRouter, Link, Route, Switch, Redirect } from 'react-router-dom'
function App(){
return (
<BrowserRouter>
<Link to="/home">home</Link>
<Link to="/about">About</Link>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/about" exact component={About} />
<Redirect to="/not-found" component={NotFound} />
</Switch>
</BrowserRouter>
)
}
In addition, it can also be used for nesting, where routing is reconfigured within the component. In cases where there are too many routes, you can split the Router in this way, which makes it more of a generic component and can be nested at will. The component can get a math props to get information about the parent route.
import { BrowserRouter, Link, Route, Switch, Redirect } from 'react-router-dom'
function App(){
return (
<BrowserRouter>
<Link to="/home">home</Link>
<Link to="/settings">Settings</Link>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/settings" exact component={Settings} />
</Switch>
</BrowserRouter>
)
}
const Setting = (props) => {
const matchPath = props.match.path;
return (
<div>
<Link to={`${matchPath}/a`}>a</Link>
<Link to={`${matchPath}/b`}>b</Link>
<Switch>
<Route path={`${matchPath}/a`} component={AComp} />
<Route path={`${matchPath}/b`} component={BComp} />
</Switch>
</div>
)
}
However, in addition to the large number of routes in the project, there are usually some logic that needs centralized processing. Obviously, the decentralized Route configuration method is not suitable. However, React -router-config provides us with convenient static Route configuration, whose essence is to convert a config into Route component. And in the component rendering method render, you can do some unified processing according to the business situation.
function renderRoutes(routes, extraProps, switchProps) { // ... return routes ? React.createElement(reactRouter.Switch, switchProps, routes.map(function (route, i) { return React.createElement(reactRouter.Route, { key: route.key || i, path: route.path, exact: route.exact, strict: route.strict, render: function render(props) { return route.render ? route.render(_extends({}, props, {}, extraProps, { route: route })) : React.createElement(route.component, _extends({}, props, extraProps, { route: route })); }}); })) : null; }
ERDA-UI project routing practices
The routing configuration
const routers = { path: ':orgName', mark: 'org', breadcrumbName: '{orgName}' routes: [ { path: Routes: [{path: 'projects/:projectId', breadcrumbName: 'DevOps platform ', Mark: 'workBench', Routes: [{path: 'projects/:projectId', breadcrumbName: '', Mark: 'project', AuthContainer: Projectauth, Routes: [{path: 'apps', PageTitle: 'app list ', GetComp: CB => CB (import('/xx/xx')), routes: [{path: 'apps/:appId', mark: 'application', breadcrumbName: 'application', authContainer: AppAuth, } ] }, ] } ], }, ] }
As you can see above, the React-Router fields in the configuration, with the exception of Path, do not seem to have much to do with the React-Router. These are the fields that we use to implement routing-related logic, which we will discuss below.
Routing state management: RouteInfoStore
In order to expand the routing related functions, we first need to have a route objects provide data support for us, you need to this object, because a single routing information is not enough to achieve other related logic, we need more routing information, such as routing level of link records, routing state before and after contrast, etc.
We use a RouteInfoStore object to manage the data and state associated with routing. This object can share routing state between components (similar to the store in Redux).
We update the routing data and status by listening in BrowserHistory.listen and calling the RouteInfoStore method ($_UPDATEROUTEINFO) that handles the routing changes.
BrowserHistory.listen ((loc) => {// Listen to a routing change to trigger an update to the RouterStore, similar to the patch in RedEx; $_updaterouteInfo (loc); $_updaterouteInfo (loc); $_updaterouteInfo (loc); }); // Routeore const initRouteInfo: irouteInfo = {Routes: [], // Routes: {Routes: [], Routes: [], Routes: [], Routes: [], Routes: [], Routes: [] {}, // Query: {}, // Search (? After) the parameter currenTroute: {}, // The routing configuration on the current match is routeMarks: [], // Marks Mark's routing hierarchy isIn: If () => false, if () => false, if () => false, if () => false, if () => false, if () => false, () => false, isLeaving: () => false,// Excludes information from the current route prevrouteInfo: {}, // Information from the last route};
Routing Listening extension: Mark
Usually, we need to monitor some pre-initialization operations that the route automatically performs when entering or leaving A certain range. For example, entering module A, we first need to obtain the permissions of module A, or some basic information of module A. When you leave module A, you need to clear the relevant information. In order to do this listening and initialization, we need two conditions:
- The field that marks the scope.
- When the route changes, determine whether the route leaves or enters the corresponding range.
We added the Mark field in the route configuration to mark the current route scope, similar to the ID of the route scope, which needs to be globally unique. As mentioned above in RouteInfoStore, routeMarks will record the mark collection at the routing link level, and PrevrouteInfo will record the last routing information. From this, we can add some routing range functions isIn, isEntering, isLeaving, isMatch to the RouterInfoStore.
isIn($mark) => boolean
Indicates whether the current route is in a range. Passing in a mark value is determined by whether the RouteInfoStore includes the Routemarks:
IsIn: (mark: string) => routeMarks. Includes (mark),
isEntering($mark) => boolean
The current route isIn the range. This is an ongoing judgment, unlike isIn, which indicates that the last route was not in the range and that the current route isIn the range.
// If the current mark is included and the previous route was not included, the current mark is being entered. isEntering: (mark: string) => routeMarks.includes(mark) && ! prevRouteInfo.routeMarks.includes(mark),
isLeaving($mark) => boolean
In contrast to isEntering, isLeaving means that the last route was in the range and the next route isLeaving the range.
// If the route is not included and the last route was included, the route is leaving the current mark. isLeaving: (mark: string) => ! routeMarks.includes(mark) && prevRouteInfo.routeMarks.includes(mark),
isMatch($pattern) => boolean
A regular is passed in to determine if the route matches the regular, which is usually used to directly determine the current route:
IsMatch: (pattern: string) =>!! pathToRegexp(pattern, []).exec(pathname),
Register to monitor
We provide a listening method, which allows each module to register its own routing listener function when the project is started, and in the listening function, it is convenient to use the above method to determine the range of routing.
Export const listenRoute = (CB: Function) => {// RouteInfoStore.getState (s => s); // Call the listener method on(' @RouteChange ', CB) when the route changes; }; ListenRoute ((_routeInfo) = bb0 {const {isEntering, isLeaving} = _routeInfo; If (isEntering('markA')){// initialize module A} if(isLeaving('markA')) {
Routing split: Tomark
When the number of routes is large, a piece of routing data can be nested very deep, so it is necessary to support splitting of routing configurations.
We provide a route registration method registerRouter, different modules can only register their routes, and then establish the ownership association between routes through the toMark field, the value of toMark is another route’s mark value. Within the RegisterRouter, all routes are consolidated into a complete configuration.
// RegisterRouter ({path: ':orgName', mark: 'orgName', breadcrumbName: '{orgName}'}); // RegisterRouter ({path: 'workBench', breadcrumbName: 'DevOps platform ', mark: 'workBench', toMark: 'org', // Configure the workBench route to be a child of org}); // RegisterRouter ({path: 'projects/:projectId', breadcrumbName: ", mark: 'project', toMark: Routes: [{path: 'apps', PageTitle: : 'workBench', // Container: ProjectAuth, routes: 'application list, getComp: cb = > cb (import ('/xx/xx)),,}}); // RegisterRouter ({path: 'apps/:appId', mark: 'application', toMark: 'project', // Container: appAuth,})
The routing component is loaded asynchronously: getComp
We configure the components for a single route using getComp, which introduces a component in an asynchronous method, and then we load the route component with a higher-order component that is asynchronously loaded.
// Rewrite the render map(router, route => {return {... route, render: (props) => asyncComponent(()=>route.getComp()); }}) // export const AsyncComponent = (getComponent: Function) => { return class AsyncComponent extends React.Component { static Component: any = null; state = { Component: AsyncComponent.Component }; componentDidMount() { if (! this.state.Component) { getComponent().then((Component: any) => { AsyncComponent.Component = Component; this.setState({ Component }); }); } } render() { const { Component } = this.state; If (Component) {// When the Component is loaded, render Return <Component {... this.props} />; } return null; }}; };
Breadcrumbs: breadcrumbName
In the Erda-UI business, the configuration of the route is a tree structure, and the route to the sub-module must go through the parent module route. By analyzing the routing data, we can get the hierarchical link from the root route to the current route, and the routing hierarchy link just maps the breadcrumb hierarchy.
We do this by adding the BreadCrumbName field in the routing configuration and storing the hierarchical link data of the route in the Routes of the RouteInfoStore. So the breadcrumb data can be obtained directly from the routers.
map(routes, route => {
return {
name: route.breadcrumbName,
path: route.path,
}
})
In the configuration, the breadcrumbName can be either a literal or a string template {temp}. Here is to use another store data to manage all the string template corresponding data, rendering, by matching the key value to get the corresponding display text.
Routing authentication: AuthContainer
In a project, whether the route can be accessed often needs to be judged by corresponding conditions (user permissions, whether the module is open, etc.). The authentication conditions of different routes may be different, and the authentication failure prompt may need to be personalized, or there may be a scenario where the page needs to be redirected after authentication failure. All of these require personalized authentication on the route. As in the react-router-config, we can do this by tweaking the render function of the Route component.
We do this by configuring the AuthContainer component on the route. The process consists of two steps:
- Provide a authentication component AuthComp, internal encapsulation authentication related logic and prompts.
- Get this authentication component AuthComp and override Render before rendering the route.
// AuthComp const AuthComp = (props) => { const { children } = props; const [auth, setAuth] = React.useState(undefined); UseMount (()=>{dosomeAuthCheck (). Then (()=>{setAuth(true)})}) if(auth === = undefined){return <div> </div>} return auth ? Children: <div> You are not authorized to access, please contact the administrator... </div>} // Rewrite the render map(router, route => {return {... route, render: (props) => { const AuthComp = route.AuthContainer; const Comp = route.components; return ( <AuthComp {... Props} route={route}> // add route validation block {Comp? <Comp {... props} /> : Comp } </AuthComp> ) } } })
Summary and follow-up thinking
In the Erda-UI project, we centrally manage all the routes through some of the configuration extensions above. This approach makes it easy and efficient to maintain the route itself and extend the associated business logic. In addition, you can do some more flexible things, such as analyzing the entire routing structure, generating a visual routing tree, supporting dynamic routing adjustment, and so on. Over a long period of business evolution and content refinement, we have proven the benefits of this approach.
At the same time, we are also constantly thinking about areas that can be improved, such as:
- How can routing be asynchronously cascaded between link-level modules?
For example, module A contains module B, register initA in module A, register initB in module B, control initB execution after initA completion (if initB needs to use the results returned by initA, the execution order needs to be strictly controlled).
conclusion
These are common scenarios, and the Erda project is constantly being updated and iterated to fit the needs of the business. We will always pay attention to the community and analyze our own business development, so as to do better in this area. Welcome to add our little assistant WeChat (Erda202106) to the discussion group!
- Erda GitHub: https://github.com/erda-project/erda
- Erda Cloud’s website: https://www.erda.cloud/