preface

Some students mentioned route authentication in the last article, but I didn’t write it due to time. This article will specifically explain vue and React for this feature. I hope you can benefit a lot from reading it and it will be helpful to your project.

background

In the separate item, you want to see if the person who logged in has access to the current page. Although the server does have access to the interface, it is wasteful to request the interface every time a route is loaded. Sometimes login permissions are verified by SESSIONID.

Let’s take a look at how vue route authentication works before we officially start react authentication:

BeforeEach of VUE Route authentication

Generally, we will configure the role menu of the routing table in the back-end accordingly. When the user accesses the URL that is not within the scope of permission directly from the address bar without using the page menu, the user’s access will be intercepted and redirected to the home page.

At the beginning of VUE, it can load the corresponding routing table AddRouter according to the permission through dynamic routing. However, due to the cross permissions, the permission routing table needs to be judged and combined, which is quite troublesome. Therefore, it adopts the non-dynamic routing method in direct judgment in beforeEach

When using Vue, the framework provides a route guard function that performs some verification before entering a path. If the verification fails, it is redirected to the 404 or login page. For example, the beforeEnter function in Vue:

. router.beforeEach(async(to, from, next) => { const toPath = to.path; const fromPath = from.path; })...Copy the code

1. Route overview

// index.js import Vue from 'vue' import Router from 'vue-router' import LabelMarket from './modules/label-market' import PersonalCenter from './modules/personal-center' import SystemSetting from './modules/system-setting' import API from '@/utils/api' Vue.use(Router) const routes = [ { path: '/label', component: () => import(/* webpackChunkName: Redirect: {name: 'LabelMarket'}, redirect: [{// base public page path: 'LabelMarket', name: 'name'] 'LabelMarket', component: () => import(/* webpackChunkName: "label-market" */ '@/components/page-layout/OneColLayout.vue'), redirect: { name: 'LabelMarketIndex' }, children: }, {path: 'personal-center', name: 'PersonalCenter', redirect: '/label/personal-center/my-apply', component: () => import(/* webpackChunkName: "Personal-center" */ '@/components/page-layout/ twocollayout.vue '), children: PersonalCenter}, {// set path: 'system-setting', name: 'SystemSetting', redirect: '/label/system-setting/theme', component: () => import(/* webpackChunkName: "system-setting" */ '@/components/page-layout/TwoColLayout.vue'), children: SystemSetting }] }, { path: '*', redirect: '/label' } ] const router = new Router({ mode: 'history', routes}) // personal-center.js export default [... {// approve my path: 'my-approve', name: 'PersonalCenterMyApprove', component: () => import(/* webpackChunkName: "Personal center" */ '@/ personal Center /index.vue'), children: [{// personal center approval path: 'API ', name: 'PersonalCenterMyApproveApi', meta: { requireAuth: true, authRole: 'dataServiceAdmin' }, component: () => import(/* webpackChunkName: "personal-center" */ '@/views/personal-center/api-approve/index.vue') }, ... ] } ]Copy the code

Export default [... {path: 'API ', name: 'SystemSettingApi', meta: {requireAuth: true, authRole: 'dataServiceAdmin' }, component: () => import(/* webpackChunkName: "// views/system-setting/ API /index.vue')}, {// set path: 'theme', name: 'SystemSettingTheme', meta: { requireAuth: true, authRole: 'topicAdmin' }, component: () => import(/* webpackChunkName: "system-setting" */ '@/views/system-setting/theme/index.vue') }, ... ]Copy the code

2. Authentication and judgment

User login information request back-end interface, return menu, permission, copyright information and other public information, stored in VUEX. The permission fields used here are as follows:

_userInfo: {admin:false, // whether the super administrator dataServiceAdmin:true, // whether the data service administrator topicAdmin:false // Whether the topic administrator}Copy the code

  1. Determine whether authentication is required for the current route (requireAuth in the Meta field of the Router is true), and enable the public page directly.
  2. Determine that the role is the super administrator and release the file directly.
  3. (Special logic of this system) Determine that the jump path is subject setting but the role is not subject administrator. Continue to determine whether the role is data service administrator. Jump to the data service Settings page or redirection (‘ system Settings’ menu ‘/label/system-setting’ defaults to ‘/label/system-setting/theme’, other menus default to redirection is the basic public page, so you need to authenticate the redirection here. The permissions set by the system are either subject administrator or data service administrator, so you can do this.
  4. Check whether the required routing permission is met. If not, direct the redirect.
// index.js router.beforeEach(async (to, from, next) => { try { // get user login info const _userInfo = await API.get('/common/query/menu', {}, false) router.app.$store.dispatch('setLoginUser', _userInfo) if (_userInfo && Object.keys(_userInfo).length > 0 && to.matched.some(record => record.meta.requireAuth)) { if (_userInfo.admin) { // super admin can pass next() } else if (to.fullPath === '/label/system-setting/theme' && ! _userInfo.topicAdmin) { if (_userInfo.dataServiceAdmin) { next({ path: '/label/system-setting/api' }) } else { next({ path: '/label' }) } } else if (! (_userInfo[to.meta. AuthRole])) {next({path: '/label'})}}} Catch (e) {router-app.$message.error(' failed to get user login information! ') } next() })Copy the code

Second, the introduction of

1. Route Introduction

What does routing do?

Display different content or pages based on different URLS.

The most important feature of a single page application is that it has only one Web page. Therefore, all page jumps need to be done in javascript. When you need to display different pages according to user operations, we need to use JS control page display content according to the access path.

2. Introduction to react-Router

React Router is a routing solution designed specifically for React. It uses the HTML5 History API to manipulate the browser’s session history.

3, use,

The React Router is split into four packages: react-router, react-router-dom, React-router-native, and React-router-config. React-router provides the core routing components and functions. React-router-config is used to configure static routing (still under development), and the other two provide specific components for the runtime environment (browser and React-Native).

To build the site (which will run in a browser environment), we should install react-router-dom. Since react-router-dom already exposes the objects and methods exposed in the React-router, you only need to install and reference the React-router-dom.

4. Related components

4-1.

Use THE HTML5 History API (pushState, replaceState and the Popstate Event) to ensure that your address bar information is consistent with the interface.

Main attributes:

Basename: sets the root path

GetUserConfirmation: Function that gets user confirmation

ForceRefresh: Whether to refresh the entire page

KeyLength: Length of location.key

Children: child node (single)

4-2,

A component developed for older browsers, which is usually simple to use BrowserRouter.

4-3,

Provide declarative, accessible navigation for projects

Main attributes:

To: Can be a string representing the destination path, or an object containing four properties:

Pathname: indicates the target path

Search: The search parameter passed

Hash: Indicates the hash value of the path

State: Indicates the address status

Replace: Whether to replace the entire history stack

InnerRef: Accesses the underlying reference of the component

All a tag attributes such as className, title, and so on are supported

4-4,

The react-Router is the most important component in the react-router, and its primary responsibility is to render the specified component based on the matching path

Main attributes:

Path: indicates the path to be matched

Component: The component to be rendered

Render: the function of the render component

Children: a function of the rendering component, often the ’empty’ state presented when path does not match is the so-called default display state

4 to 5,

Redirection component

Main property: to: The path to point to

Nested component: The only render that matches the first child of the path or

React-router-config route authentication

The introduction

In previous versions of the React Router, the React Router provided a similar onEnter hook, but in version 4.0 of the React Router, this method was removed. React Router 4.0 uses a declarative component. Routing is a component. To implement the route guard function, we had to write it ourselves.

React-router-config is a small assistant that helps us configure static routes. The source code is a high order function using a map function to generate static routes

import React from "react"; import Switch from "react-router/Switch"; import Route from "react-router/Route"; const renderRoutes = (routes, extraProps = {}, switchProps = {}) => routes ? ( <Switch {... switchProps}> {routes.map((route, i) => ( <Route key={route.key || i} path={route.path} exact={route.exact} strict={route.strict} render={props => ( <route.component {... props} {... extraProps} route={route} /> )} /> ))} </Switch> ) : null; export default renderRoutes;Copy the code

//router.js assumes that this is the array of routes we set.

const routes = [
    { path: '/',
        exact: true,
        component: Home,
    },
    {
        path: '/login',
        component: Login,
    },
    {
        path: '/user',
        component: User,
    },
    {
        path: '*',
        component: NotFound
    }
]
Copy the code

//app.js so we can use this in app.js to generate static routes

import { renderRoutes } from 'react-router-config'
import routes from './router.js'
const App = () => (
   <main>
      <Switch>
         {renderRoutes(routes)}
      </Switch>
   </main>
)

export default App
Copy the code

{requiresAuth: true} for vue router.js

Then use the navigation guard

Router.beforeeach ((to, from, next) => {// determine the value of requiresAuth beforeEach route enters, and determine whether it is landed if true})Copy the code

2. Based on the idea of route authentication similar to vUE, we modified react-router-config slightly

// utils/renderRoutes.js

import React from 'react' import { Route, Redirect, Switch } from 'react-router-dom' const renderRoutes = (routes, authed, authPath = '/login', extraProps = {}, switchProps = {}) => routes ? ( <Switch {... switchProps}> {routes.map((route, i) => ( <Route key={route.key || i} path={route.path} exact={route.exact} strict={route.strict} render={(props) => { if (! route.requiresAuth || authed || route.path === authPath) { return <route.component {... props} {... extraProps} route={route} /> } return <Redirect to={{ pathname: authPath, state: { from: props.location } }} /> }} /> ))} </Switch> ) : null export default renderRoutesCopy the code

The modified source code adds two parameters, authed and authPath, and a property, route.requiresauth

Then let’s look at the key piece of code

if (! route.requiresAuth || authed || route.path === authPath) { return <route.component {... props} {... extraProps} route={route} /> } return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />Copy the code

If rout.requiresAuth = false or authed = true or rout.path === authPath (default parameter ‘/login’) then render our page, Otherwise, render the authPath page we set and record which page to jump from.

The corresponding router.js also needs to be modified slightly

const routes = [ { path: '/', exact: true, component: Home, requiresAuth: false, }, { path: '/login', component: RequiresAuth: false, {path: '/user', component: user, requiresAuth: true}, {path: '/user', component: user, requiresAuth: true, '*', component: NotFound, requiresAuth: false, } ]Copy the code

//app.js

import React from 'react' import { Switch } from 'react-router-dom' //import { renderRoutes } from 'react-router-config'  import renderRoutes from './utils/renderRoutes' import routes from './router.js' const authed = false // If you can change this value after login using redux (redux is outside the scope of this article) const authPath = '/login' // The default page returned when logged in, Const App = () => (<main> <Switch> {renderRoutes(routes, authed, authPath)} </Switch> </main> ) export default AppCopy the code

/ / landing after the return to original login page to login function () {const {from} = this. Props. The location. The state | | {the from: {the pathname: '/'}} // authed = true this.props.history.push(from.pathname) }Copy the code

At this point react-router-config ends and gives us the desired effect

3. Pay attention to ⚠️

A lot of people find that sometimes it doesn’t work as we want it to, so what do we do? Here we go

1, design the global setup to manage whether to log in

configLogin.js

import React, { Component } from 'react' import PropTypes from 'prop-types' import { withRouter } from 'react-router-dom' class App extends Component { static propTypes = { children: PropTypes.object, location: PropTypes.object, isLogin: PropTypes.bool, history: PropTypes.object }; componentDidMount () { if (! this.props.isLogin) { setTimeout(() => { this.props.history.push('/login') }, 300) } if (this.props.isLogin && this.props.location.pathname === '/login') { setTimeout(() => { this.props.history.push('/') }, 300) } } componentDidUpdate () { if (! this.props.isLogin) { setTimeout(() => { this.props.history.push('/login') }, 300) } } render () { return this.props.children } } export default withRouter(App)Copy the code

It is introduced in the main route module index.js

import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom' <Router history={ history } basename="/" getUserConfirmation={ getConfirmation(history,  'yourCallBack') } forceRefresh={ ! supportsHistory } > <App isLogin={ isLogin ? true : false }> <Switch> <Route exact path="/" render={ () => <Redirect to="/layout/dashboard" push /> } /> <Route path="/login" component={ Login } /> <Route path="/layout" component={ RootLayout } /> <Route component={ NotFound } /> </Switch> </App> </Router>Copy the code

A lot of times you can do this by listening for route changes like the getUserConfirmation hook

const getConfirmation = (message, callback) => { if (! isLogin) { message.push('/login') } else { message.push(message.location.pathname) }Copy the code

How is the React-acL-router implemented

Iv. Rights management mechanism

Reference code for this section:

  1. react-acl-router
  2. react-boilerplate-pro/src/app/init/router.js
  3. react-boilerplate-pro/src/app/config/routes.js

As a very core part of enterprise management system, permission management has always been a difficult problem for developers because the business side can not use accurate terms to describe the requirements. Let’s start with two common rights management design patterns, role-based access control and access control lists.

1. Layout and routing

Before we get into the specifics of layout component design, we need to address the more basic problem of how to integrate layout components with application routing.

The following example is an example of the react-Router sidebar menu combined with the route. I have simplified it here:

const SidebarExample = () => (
  <Router>
    <div style={{ display: "flex" }}>
      <div
        style={{
          padding: "10px",
          width: "40%",
          background: "#f0f0f0"
        }}
      >
        <ul style={{ listStyleType: "none", padding: 0 }}>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/bubblegum">Bubblegum</Link>
          </li>
          <li>
            <Link to="/shoelaces">Shoelaces</Link>
          </li>
        </ul>
      </div>

      <div style={{ flex: 1, padding: "10px" }}>
        {routes.map((route, index) => (
          <Route
            key={index}
            path={route.path}
            exact={route.exact}
            component={route.main}
          />
        ))}
      </div>
    </div>
  </Router>
);
Copy the code

The idea of a layout is abstracted into simple pseudo-code:

<Router> <BasicLayout> // with sidebar {routes.map(route => ( <Route {... route} /> ))} </BasicLayout> </Router>Copy the code

This is a very elegant solution, but one limitation is that it does not support many different layouts. Because a Router can only contain one child component, even if we wrap multiple layout components in a container component, such as:

<Router> <div> <BasicLayout> // with sidebar {routes.map(route => ( <Route {... route} /> )} </BasicLayout> <FlexLayout> // with footer {routes.map(route => ( <Route {... route} /> )} </FlexLayout> </div> </Router>Copy the code

When the route is matched to the page under FlexLayout, the sidebar in BasicLayout will also be displayed, which is obviously not what we want. Alternatively, can we pass the layout component as children directly to the lower level Route component? The code is as follows:

<Router> <div> {basicLayoutRoutes.map(route => ( <Route {... route}> <BasicLayout component={route.component} /> </Route> ))} {flexLayoutRoutes.map(route => ( <Route {... route}> <FlexLayout component={route.component} /> </Route> ))} </div> </Router>Copy the code

Here we treat the different layout components as higher-order components and wrap them accordingly around different page components, thus enabling support for a variety of different layouts. It is also important to note that the React-Router defaults to routing information such as match, location, history, and so on to the next level component of the Route. In this case, the next level component of the Route is not the actual page component but the layout component. Therefore, we need to manually pass the routing information to the page component in the layout component, or rewrite the render method of the Route as follows:

<Route render={props => ( // props contains match, location, history <BasicLayout {... props}> <PageComponent {... props} /> </BasicLayout> )} />Copy the code

Another possible problem with the Connect-React-router is that it does not synchronize the match object (which contains params and other data for the current route) to the Redux store. So we must ensure that the layout and page components receive the Match object in the routing part, otherwise it will become very cumbersome to deal with the requirements related to the current routing parameters such as the page header.

2. Header & footer

To solve the problem of combining with the application routing, the layout components are the two most important parts of the page header and footer, and the header can be divided into the application header and the page header.

The application header refers to the header of the entire application layer. It has nothing to do with the specific page. Generally, it contains application-level information and operations such as the user profile picture, notification bar, search box, and multi-language switching. A page header typically contains page titles, breadcrumb navigation, common page actions, and other content related to a specific page.

In previous projects, especially early in the project, many developers tended to make the application header a presentation component and call it directly from different pages because they did not have a complete understanding of the project itself. There are, of course, advantages to this. For example, data synchronization between the page and the layout is omitted, and each page can pass its own internal data directly to the header.

But doing so is an anti-pattern from an ideal project architecture point of view. Since the application header is actually an application level component, it becomes a page level component as described above, the pseudocode is as follows:

<App>
  <BasicLayout>
    <PageA>
      <AppHeader title="Page A" />
    </PageA>
  </BasicLayout>
  <BasicLayout>
    <PageB>
      <AppHeader title="Page B" />
    </PageB>
  </BasicLayout>
</App>
Copy the code

The same problem exists from an application data flow point of view, where the application header is supposed to pass data to different pages, not the other way around. This causes the application header to lose the chance to control when it rerender. As a purely presentable component, the header needs to be redrawn once the received props changes.

On the other hand, in addition to the general application header, there is a strict one-to-one correspondence between the page header and the page route, so can we include the configuration of the page header in the configuration of the route? When a new page is added, you only need to configure an additional route object in config/routes.js to complete the creation of the page header. The ideal pseudocode is as follows:

<App>
  <BasicLayout>                    // with app & page header already
    <PageA />
  </BasicLayout>
  <BasicLayout>
    <PageB />
  </BasicLayout>
</App>
Copy the code

1. Configuration is better than code

In past discussions of component libraries we have come to the conclusion that code is better than configuration, that where consumer customization is required, we should try to throw callbacks so that the consumer can use the code to control the customization requirements. This is because components are extremely fine-grained abstractions, and configurational usage patterns are often difficult to meet the changing needs of consumers. But in enterprise management systems, as an application-level solution, we should try to avoid having the consumer write the code for any problem that can be solved with configuration items.

Configuration items (configuration files) are naturally a centralized management mode, which greatly reduces application complexity. Take the header as an example. If we call the header component in every page file, we need to change all the code that uses the header component if the header component fails. Aside from debugs, even if it’s a simple requirement to change the title of a page, the developer needs to first find the file that corresponds to the page and make the change in its render function. These hidden costs are what we need to pay attention to when we design enterprise management system solutions, because it is such a small detail caused by itself is not complex enterprise management system in the maintenance, iteration for a period of time after the application complexity of steep increase. Ideally, a good enterprise management system solution should be one in which more than 80% of non-functional requirement changes can be addressed by modifying configuration files.

2. Configure headers

import { matchRoutes } from 'react-router-config'; // routes config const routes = [{ path: '/outlets', exact: true, permissions: ['admin', 'user'], component: Outlets, unauthorized: Unauthorized, pageTitle: 'store management ', breadcrumb: ['/ Outlets '],}, {path: '/ Outlets /:id', exact: true, permissions: ['admin', 'user'], component: OutletDetail, unauthorized: Unauthorized, pageTitle: 'stores details, breadcrumb: ['/outlets','/outlets: id],}]; // find current route object const pathname = get(state, 'router.location.pathname', ''); const { route } = head((matchRoutes(routes, pathname)));Copy the code

In this way, we can use the matchRoutes method provided by react-router-config in the generic layout component to obtain all configuration items of the route object of the current page according to the pathname of the page. This means that we can do uniform processing for all of these configuration items. This not only makes it easier to work with common logic, but it is also a constraint on the colleagues who write the page code, making the code written by different developers less personal and easier to manage the code base as a whole.

3. Page title

renderPageHeader = () => {
  const { prefixCls, route: { pageTitle }, intl } = this.props;

  if (isEmpty(pageTitle)) {
    return null;
  }

  const pageTitleStr = intl.formatMessage({ id: pageTitle });
  return (
    <div className={`${prefixCls}-pageHeader`}>
      {this.renderBreadcrumb()}
      <div className={`${prefixCls}-pageTitle`}>{pageTitleStr}</div>
    </div>
  );
}
Copy the code

Breadcrumb navigation

renderBreadcrumb = () => {
  const { route: { breadcrumb }, intl, prefixCls } = this.props;
  const breadcrumbData = generateBreadcrumb(breadcrumb);

  return (
    <Breadcrumb className={`${prefixCls}-breadcrumb`}>
      {map(breadcrumbData, (item, idx) => (
        idx === breadcrumbData.length - 1 ?
          <Breadcrumb.Item key={item.href}>
            {intl.formatMessage({ id: item.text })}
          </Breadcrumb.Item>
          :
          <Breadcrumb.Item key={item.href}>
            <Link href={item.href} to={item.href}>
              {intl.formatMessage({ id: item.text })}
            </Link>
          </Breadcrumb.Item>
      ))}
    </Breadcrumb>
  );
}
Copy the code

3. Design strategy

1. Role-based access control

Role-based access control does not directly assign system operation permissions to specific users. Instead, a role set is established between users and permissions, and permissions are assigned to roles and roles are assigned to users. In this way, centralized management of permissions and roles is implemented to avoid complex many-to-many relationships between users and permissions.

2. Access control list

In terms of roles and permissions, an ACL refers to the list of system permissions that a role has. In traditional computer science, permission generally refers to the right to add, delete, modify, or check a file system. However, in Web applications, most systems only need to do page level permission control, which is simply to determine whether the current user has the right to view the current page according to his or her role.

Let’s implement a basic version of application routing with permission management along these lines.

4, actual code

1. Route container

Before writing permission management code, we need to find a suitable container for all page routes, the Switch component in the React-Router. Unlike multiple independent routes, a route wrapped in a Switch will only render the first successful path match at a time, rather than all routes that match the path matching conditions.

<Router>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={NoMatch}/>
</Router>
Copy the code

<Router>
  <Switch>
    <Route path="/about" component={About}/>
    <Route path="/:user" component={User}/>
    <Route component={NoMatch}/>
  </Switch>
</Router>
Copy the code

For example, if the current page path is /about, < about />,
, and
will all be rendered on the current page at the same time because their paths correspond to /about. By wrapping them in a Switch, the React-Router finds the first qualified
route and then stops looking for the
component to render directly.

In enterprise management systems, since pages are usually parallel and exclusive to each other, taking advantage of the Switch feature is a great help in simplifying the page rendering logic.

It’s also worth noting that in @reach/ Router, a new book by React-router author Ryan Florence, this feature of the Switch is included by default, and @reach/router automatically matches the route that best matches the current path. This allows users not to worry about the routing order, interested friends can pay attention.

2. Rights management

Now that we have a general framework for our route, let’s add specific permission judgment logic to it.

For an application, in addition to the pages that need authentication, there must be pages that do not need authentication. Let’s first add these pages to our route, such as the login page.

<Router>
  <Switch>
    <Route path="/login" component={Login}/>
  </Switch>
</Router>
Copy the code

For routes that need authentication, we need to abstract a function to determine whether the current user has the permission. Users can have a single role, multiple roles, or a more complex authentication function based on specific requirements. Here I provide a basic version where we store user roles as strings in the background, such as admin for one user and user for another.

import isEmpty from 'lodash/isEmpty'; import isArray from 'lodash/isArray'; import isString from 'lodash/isString'; import isFunction from 'lodash/isFunction'; import indexOf from 'lodash/indexOf'; const checkPermissions = (authorities, permissions) => { if (isEmpty(permissions)) { return true; } if (isArray(authorities)) { for (let i = 0; i < authorities.length; i += 1) { if (indexOf(permissions, authorities[i]) ! == -1) { return true; } } return false; } if (isString(authorities)) { return indexOf(permissions, authorities) ! = = 1; } if (isFunction(authorities)) { return authorities(permissions); } throw new Error('[react-acl-router]: Unsupport type of authorities.'); }; export default checkPermissions;Copy the code

We mentioned the configuration file for the route above, here we add another property permissions for each route that requires authentication, which roles can access the page.

const routes = [{
  path: '/outlets',
  exact: true,
  permissions: ['admin', 'user'],
  component: Outlets,
  unauthorized: Unauthorized,
  pageTitle: 'Outlet Management',
  breadcrumb: ['/outlets'],
}, {
  path: '/outlets/:id',
  exact: true,
  permissions: ['admin'],
  component: OutletDetail,
  redirect: '/',
  pageTitle: 'Outlet Detail',
  breadcrumb: ['/outlets', '/outlets/:id'],
}];
Copy the code

In the above configuration, both admin and User can access the store list page, but only Admin can access the store details page.

In general, there are two ways to handle the situation that the user does not have the permission to view the current page. One is to directly redirect to another page (such as the home page); the other is to render a page without permission, prompting the user that the user cannot view the page because he does not have the permission to view the current page. Each page can use only one of the two attributes. Therefore, you can configure the Redirect and Unauthorized attributes in the route configuration as required. When an unauthorized page is displayed, an unauthorized page is displayed. Specific code you can refer to the example project React-acL-Router implementation, here excerpts a small core part.

renderRedirectRoute = route => ( <Route key={route.path} {... omitRouteRenderProperties(route)} render={() => <Redirect to={route.redirect} />} /> ); renderAuthorizedRoute = (route) => { const { authorizedLayout: AuthorizedLayout } = this.props; const { authorities } = this.state; const { permissions, path, component: RouteComponent, unauthorized: Unauthorized, } = route; const hasPermission = checkPermissions(authorities, permissions); if (! hasPermission && route.unauthorized) { return ( <Route key={path} {... omitRouteRenderProperties(route)} render={props => ( <AuthorizedLayout {... props}> <Unauthorized {... props} /> </AuthorizedLayout> )} /> ); } if (! hasPermission && route.redirect) { return this.renderRedirectRoute(route); } return ( <Route key={path} {... omitRouteRenderProperties(route)} render={props => ( <AuthorizedLayout {... props}> <RouteComponent {... props} /> </AuthorizedLayout> )} /> ); }Copy the code

Therefore, in the final route, we preferentially match the page path that does not need authentication, so that all users can see the page in the first time when accessing the page that does not need authentication. Finally, if all the paths are not matched, render the 404 page to tell the user that the current page path does not exist.

<Switch>
  {map(normalRoutes, route => (
    this.renderNormalRoute(route)
  ))}
  {map(authorizedRoutes, route => (
    this.renderAuthorizedRoute(route)
  ))}
  {this.renderNotFoundRoute()}
</Switch>
Copy the code

The routes that require authentication and the routes that do not require authentication are two different pages. Generally speaking, they have different page layouts. For example, the login page uses a common page layout:

In this section, you can combine different page layouts with the authentication logic so that the authentication logic and the basic layout effect can be obtained for the newly added pages only after the corresponding attributes are configured in the route configuration. This will greatly improve the productivity of developers, especially for new members of the project team. The pure configuration approach is the most user-friendly.

5. Application integration

In this case, an application route that includes basic rights management is complete. You can abstract it into an independent routing component. You only need to configure the routes that require authentication and the routes that do not require authentication.

const authorizedRoutes = [{
  path: '/outlets',
  exact: true,
  permissions: ['admin', 'user'],
  component: Outlets,
  unauthorized: Unauthorized,
  pageTitle: 'pageTitle_outlets',
  breadcrumb: ['/outlets'],
}, {
  path: '/outlets/:id',
  exact: true,
  permissions: ['admin', 'user'],
  component: OutletDetail,
  unauthorized: Unauthorized,
  pageTitle: 'pageTitle_outletDetail',
  breadcrumb: ['/outlets', '/outlets/:id'],
}, {
  path: '/exception/403',
  exact: true,
  permissions: ['god'],
  component: WorkInProgress,
  unauthorized: Unauthorized,
}];

const normalRoutes = [{
  path: '/',
  exact: true,
  redirect: '/outlets',
}, {
  path: '/login',
  exact: true,
  component: Login,
}];

const Router = props => (
  <ConnectedRouter history={props.history}>
    <MultiIntlProvider
      defaultLocale={locale}
      messageMap={messages}
    >
      // the router component
      <AclRouter
        authorities={props.user.authorities}
        authorizedRoutes={authorizedRoutes}
        authorizedLayout={BasicLayout}
        normalRoutes={normalRoutes}
        normalLayout={NormalLayout}
        notFound={NotFound}
      />
    </MultiIntlProvider>
  </ConnectedRouter>
);

const mapStateToProps = state => ({
  user: state.app.user,
});

Router.propTypes = propTypes;
export default connect(mapStateToProps)(Router);
Copy the code

In a real project, we can use the Connect component provided by React-Redux to route the application connect to the Redux Store, so that we can directly read the role information of the current user. Once the role of the login user changes, the client route can determine and respond accordingly.

6. Combined development: permission management

For page-level rights management, the logic of the rights management part is independent of the page and irrelevant to the specific content of the page. In other words, the permission management part of the code should not be part of the page, but should replace the redirect or no-permission page when the application route is created after the user permission is obtained.

In this way, the page part of the code can be completely decoupled from the permissions management logic, so that if the permissions management layer is removed, the page becomes a page that still runs independently without permissions. The common part of the permission management code can also be fine-tuned to serve more projects, depending on business requirements.

7, summary

In this paper, we start from the basic design idea of permission management, implement a set of role-based page level application permission management system, and discuss two methods of handling when no permission redirection and no permission display no permission page.

Now let’s see how the multilevel menu is implemented

5. Menu matching logic

Reference code for this section:

react-sider

In most enterprise management systems, the basic layout of the page is generally organized in the form of sidebar menu and page content. With the support of mature component libraries, it is not difficult to make a beautiful sidebar menu at the UI level, but because the menu also takes on the function of page navigation in the enterprise management system, there are two major problems. The second is how the child page of the menu item (for example, there is no corresponding menu item in the store details page when you click a store in store management) highlights the parent menu to which it belongs.

1. Multi-level menu

In order to enhance the scalability of the system, the menus in the enterprise management system generally need to provide multi-level support, and the corresponding data structure is that each menu item should have the children attribute to configure the menu item of the next level.

Const menuData = [{name: 'dashboard', icon: 'dashboard', path: 'dashboard', children: [{name: 'analysis page ', path: 'dashboard', 'analysis' children: [{name:' real-time data, path: 'realtime,}, {name:' offline data, path: 'offline'}],}],}];Copy the code

Renders the parent and submenus recursively

To support multilevel menus, the first problem to solve is how to unify the interaction of menu items at different levels.

In most cases, each menu item represents a different path to the page, and clicking on it triggers a change in the URL and a jump to the corresponding page, the path field in the above configuration.

But for a parent menu, clicking also opens or closes the corresponding submenu, which conflicts with clicking on the redirect page. In order to simplify this problem, we first unified the interaction of the menu as clicking on the parent menu (the menu item with the property of children) to open or close the sub-menu, and clicking on the sub-menu (the menu item without the property of children) to jump to the corresponding page.

First, in order to successfully render a multilevel menu, the render function of the menu needs to support recursion, that is, if the current menu item has the children attribute, it will be rendered as the parent menu and the submenus under the children field will be rendered first. This is called depth-first traversal in algorithm.

renderMenu = data => ( map(data, (item) => { if (item.children) { return ( <SubMenu key={item.path} title={ <span> <Icon type={item.icon} /> <span>{item.name}</span> </span> } > {this.renderMenu(item.children)} </SubMenu> ); } return ( <Menu.Item key={item.path}> <Link to={item.path} href={item.path}> <Icon type={item.icon} /> <span>{item.name}</span> </Link> </Menu.Item> ); }))Copy the code

This gives us a sidebar menu that supports multiple levels of expansion, with submenus corresponding to page routing. Careful friends may also have noticed that although the parent menu does not correspond to a specific route, there is still the path attribute in the configuration item, why?

2. Handle menu highlighting

In traditional enterprise management systems, configuring page paths for different pages can be a pain. Many developers only want page paths to be different. In the example above, we can configure the menu data as such.

Const menuData = [{name: 'dashboard', icon: 'dashboard', children: [{name: 'analysis ', children: [{name:' real-time data ', path: '/ realtime,}, {name:' offline data, path: '/ offline'}]}],}]; <Router> <Route path="/realtime" render={() => <div />} <Route path="/offline" render={() => <div />} </Router>Copy the code

When the user clicks on a menu item, he or she is correctly redirected to the corresponding page. The fatal flaw is that if a route like/realTime only matches the path attribute in the menu item based on the current pathname, how can it also match the “analysis page” and “dashboard”? Because if there is no match, the “Analysis page” and “dashboard” will not be highlighted. Can we directly show inheritance between menu items in the path of the page? Look at the following utility function.

import map from 'lodash/map';

const formatMenuPath = (data, parentPath = '/') => (
  map(data, (item) => {
    const result = {
      ...item,
      path: `${parentPath}${item.path}`,
    };
    if (item.children) {
      result.children = formatMenuPath(item.children, `${parentPath}${item.path}/`);
    }
    return result;
  })
);
Copy the code

The utility function takes into account the children field that might exist in the menu item, passing in the original menu data to get the complete menu data as follows.

[{name: 'dashboard', icon: 'dashboard', path: '/dashboard', // before is' children ': [{name:' analytics ', path: 'dashboard', path: '/dashboard'] '/dashboard/analysis', // before is' analysis' children: [{name: 'real-time data ', path: '/ dashboard/analysis/realtime', / / before is' realtime '}, {name: 'offline data, path: '/dashboard/analysis/offline', // before is 'offline' }], }], }];Copy the code

Routing of the current page again and then let’s do a reverse deduction, the assumption of the current page routing to/dashboard/analysis/realtime, We hope that we can match at the same time to [‘/dashboard ‘, ‘/ dashboard/analysis’,’/dashboard/analysis/realtime ‘], as follows:

import map from 'lodash/map';

const urlToList = (url) => {
  if (url) {
    const urlList = url.split('/').filter(i => i);
    return map(urlList, (urlItem, index) => `/${urlList.slice(0, index + 1).join('/')}`);
  }
  return [];
};
Copy the code

The above array represents the different levels of menu items, and matching each of these three values to the path attribute in the menu data matches all the menu items that should be highlighted on the current page at once.

Note here that while path in menu items is generally a normal string, some special routes can also be of the regular form, such as /outlets/:id. So when we match the two, we also need to introduce a path-to-regexp library to handle paths like /outlets/1 and /outlets/:id. Since the initial menu data is in a tree structure, which is not good for matching the path attribute, we also need to flaten the tree structure of the menu data before passing it into getMeunMatchKeys.

import pathToRegexp from 'path-to-regexp'; import reduce from 'lodash/reduce'; import filter from 'lodash/filter'; const getFlatMenuKeys = menuData => ( reduce(menuData, (keys, item) => { keys.push(item.path); if (item.children) { return keys.concat(getFlatMenuKeys(item.children)); } return keys; } [])); const getMeunMatchKeys = (flatMenuKeys, paths) => reduce(paths, (matchKeys, path) => ( matchKeys.concat(filter(flatMenuKeys, item => pathToRegexp(item).test(path))) ), []);Copy the code

With the help of these utility functions, highlighting multi-level menus is no longer a problem.

3, Knowledge: Memoization

In the sidebar menu, there are two important states: one is selectedKeys, which is the currently selected menu item; The other is openKeys, which is the open state of multiple multi-level menus. The meanings of the two are different, because when the selectedKeys are not changed, the openKeys will change after the user opens or closes other multi-level menus. As shown in the following two figures, the selectedKeys are the same but the openKeys are different.

In the case of selectedKeys, since it is determined by the page path (pathname), the value of selectedKeys needs to be recalculated each time the pathname changes. And since calculating selectedKeys through the pathname and the most basic menuData, menuData, is a very expensive thing to do (a lot of data formatting and computation), is there any way to optimize the process?

Memoization gives ordinary functions the ability to remember the output by checking before each call that the parameters passed in are exactly the same as those that were executed before, and if so, returning the last calculated result, much like the usual caching.

import memoize from 'memoize-one';

constructor(props) {
  super(props);

  this.fullPathMenuData = memoize(menuData => formatMenuPath(menuData));
  this.selectedKeys = memoize((pathname, fullPathMenu) => (
    getMeunMatchKeys(getFlatMenuKeys(fullPathMenu), urlToList(pathname))
  ));

  const { pathname, menuData } = props;

  this.state = {
    openKeys: this.selectedKeys(pathname, this.fullPathMenuData(menuData)),
  };
}
Copy the code

In the component constructor we can compute the current selectedKeys from the pathname and menuData of the current props and initialize the component internal state as the openKeys initializer. Since openKeys are controlled by the user, we only need to configure the corresponding callback to the Menu component for subsequent updates to the openKeys value.

import Menu from 'antd/lib/menu';

handleOpenChange = (openKeys) => {
  this.setState({
    openKeys,
  });
};

<Menu
  style={{ padding: '16px 0', width: '100%' }}
  mode="inline"
  theme="dark"
  openKeys={openKeys}
  selectedKeys={this.selectedKeys(pathname, this.fullPathMenuData(menuData))}
  onOpenChange={this.handleOpenChange}
>
  {this.renderMenu(this.fullPathMenuData(menuData))}
</Menu>
Copy the code

In this way, we can manage selectedKeys and openKeys separately. When using the sidebar component, developers only need to synchronize the current page path of the application to the pathName property in the sidebar component. The sidebar component automatically handles the corresponding menu highlighting (selectedKeys) and multi-level menu opening and closing (openKeys).

4, knowledge point: correctly distinguish prop and state

The above scenario is also a classic example of how to correctly distinguish prop from state.

SelectedKeys are determined by the pathname passed in, so we can wrap the transformation relationship between selectedKeys and pathname in a component. The user simply passes in the correct pathname and gets the corresponding selectedKeys without caring how the transformation between them is done. The pathname is the basic data required for component rendering, and the component cannot obtain it internally, so the consumer needs to pass it in via props.

On the other hand, openKeys is the internal state of the component. The initial value can be calculated from the Pathname, and the subsequent update is not related to the data outside the component, but will be completed within the component according to the user’s operation, so it is a state. All the logic associated with it can be completely encapsulated inside the component without being exposed to the consumer.

In short, a data must be internally unavailable to the component in order to be a prop, and after it becomes a prop, any data that can be derived from its value no longer needs to be another prop, otherwise it would violate React’s single data source principle. The same is true for state. If a piece of data wants to be state, it should no longer be able to be changed by values outside the component. Otherwise, the single data source principle would be violated and the component would behave unpredictably and cause unintelligible bugs.

5, combined development: application menu

Strictly speaking, the idea of applying menus in this section is not part of combinative development thinking, but more about how to write a menu component that supports infinite levels of submenus and automatically matches the current route. Components are, of course, pluggable as long as the parent that applies the component does not depend on the information provided by the component. This is also a specification that we should follow when writing components, that is, components can obtain information from the outside world and make logical judgments within components based on this. But when component thrown to the outside world information, more time should be in the form of a callback for the caller to trigger actively, and then update the external data in the form of props is passed to the component to achieve the purpose of updating components, rather than forcing need in external configuration acceptance in a callback function to directly change the component’s internal state.

In this sense, modular development and component packaging are in the same vein, with the key being strict control of internal state. No matter how many interfaces a module or component needs to expose, it should solve one or several specific problems internally. Just like a link in the production line of a factory product, after this link, the product must have some difference compared with before entering. Whether it is added some functions or labeled, the product will become more conducive to the use of downstream partners. Ideally, even if the link is removed, the upstream and downstream of the link can continue to work seamlessly together. This is what we call module or component pluggability.

Meaning of the back-end routing service

In the context of the front-end and back-end separation architecture, the front-end has gradually replaced the back-end to take over the judgment and processing of all fixed routes, but in the dynamic routing scenario, we will find that the flexibility of the front-end routing service is far from enough. After the user reaches a certain page, only the URL of the current page is the logical basis for the next step, and the routing service at the back end of the URL can return very rich data.

Common examples are the type of page. Given that the rendering logic of the marketing and interactive pages in the application is not the same, in addition to the DSL data of the page, we need to get the type of the page to render accordingly. For example, the SEO data of the page, the creation and update time of the page, etc., all of these data can help the application flexibly display the page on the front end and handle the business logic.

We can even extend it by completely abandoning the front-end routing services provided by react-Router and write our own routing dispenser, which calls different page rendering services according to different page types and forms a complete front-end application with multiple types of pages.

Vii. Combined development

To solve the problem that large, comprehensive solutions are not flexible enough in practice, can we decouple the modules and release them independently for developers to use as needed? Let’s take a look at some pseudocode for an ideal complete enterprise management system application architecture section:

const App = props => ( <Provider> // react-redux bind <ConnectedRouter> // react-router-redux bind <MultiIntlProvider> // intl support <AclRouter> // router with access control list <Route path="/login"> // route that doesn't need authentication <NormalLayout> // layout component <View /> // page content (view component) </NormalLayout> <Route path="/login"> ... // more routes that don't need authentication <Route path="/analysis"> // route that needs authentication <LoginChecker>  // hoc for user login check <BasicLayout> // layout component <SiderMenu /> // sider menu <Content> <PageHeader /> // page header <View /> // page content (view component) <PageFooter /> // page footer </Content> </BasicLayout> </LoginChecker> </Route> ... // more routes that need authentication <Route render={() => <div>404</div>} /> // 404 page </AclRouter> </MultiIntlProvider> </ConnectedRouter> </Provider> );Copy the code

In the pseudo code above, we abstract the multi-language support, rights management, login authentication based on routing, layout of foundation and the sidebar menu more independent modules, can according to need to add or delete any one module, and add or delete any one module will not unacceptable side effects on other parts of the application. This gives us a general idea of what to do next, but in practical terms, there are still huge challenges, such as how props are passed, how data is shared between modules, and how flexibly users can customize some particular logic. We need to watch, in dealing with a specific which parts should be placed on a separate module inside to deal with, what parts should be exposed the custom interface for users, how to do zero coupling between modules and modules that the user can plug any one module to meet the needs of the current project.

Eight, learning route

The explanation of development skills and concepts is directly introduced from a specific front-end application, so there may be some lack of basic knowledge for those who just get started with React. Here is a detailed learning roadmap for React developers. I hope it can provide a standardized and convenient way to learn React for those who just get started with it.

conclusion

Here react’s route authentication screening is finished. Welcome to forward, exchange, share and republish, please indicate the source. A recent related project case code is attached to give you an idea:

[react-router-config](https://github.com/leishihong/react-router-config)

At the same time, welcome friends to join the wechat group to discuss:

WeChat ID

You might be interested

1.Juejin. Cn/post / 684490…Build your front-end workflows (VUE, React, DVA) using Husky, commitlint, and Lint-PassageJuejin. Cn/post / 684490…React Project Internationalization (ANTD) Multi-language Development 3.Segmentfault.com/a/119000001…