Before, I briefly sorted out the architecture of the whole project and the main module realization ideas of the base application. This time, I mainly talk about the main module realization ideas of the application at a time

Entrance to the file

Reactdom. render is used to render the component page of the react single-page application into the corresponding DOM node. In single-SPA sub-applications, reactdom. render is not used to render the component, but the corresponding auxiliary library. For example, our child application was developed using React, so we used single-spa-React to wrap the application in the entry file, with the following code:

import React from "react"; import ReactDOM from "react-dom"; import singleSpaReact from "single-spa-react"; import App from "./App"; function domElementGetter() { let el = document.getElementById("login"); if (! el) { el = document.createElement("div"); el.id = "login"; document.body.append(el); } return el; } const reactLifecycles = singleSpaReact({ React, ReactDOM, rootComponent: App, domElementGetter, }); export function bootstrap(props) { return reactLifecycles.bootstrap(props); } export function mount(props) { return reactLifecycles.mount(props); } export function unmount(props) { return reactLifecycles.unmount(props); }Copy the code

This is a simple single-SPA sub-app package. It doesn’t add any features, but adds the single-SPA life cycle to the React single-page app. Therefore, whether the react app is newly developed or the old React code is changed to single-SPA sub-app, We just need to change the webpack entry file to this form, and other pages will follow the normal React single-page application development without any changes.

The data warehouse

The last article mentioned the solution idea of global state warehouse, here is a sub-application for their respective store export and use of global state, first is a very classic application scenario: In the login sub-application, we integrate the page logic related to login and registration, and the corresponding login state needs to be stored in the Login store. However, in other sub-applications, some pages in show require users to have user identity after login, or some behaviors, such as “like” and “comment”, also need user identity. So our requirements are:

  • loginSub-applications require integrationreduxAnd need to have the corresponding login registrationactionandreducer.
  • loginThe child application needs to put its ownstoreExport and load inThe base applicationTo integrate into a globalstore.
  • showCall is required in the child applicationloginApplication of the childstoreThe inside of thestate.

Requirement 1: Just a regular redux implementation is unnecessary. Requirement 2: configure multi-entry packaging in WebPack, package and export store.js documents, import and integrate in dock application, which was discussed in the previous article. Requirement 3: Because when we registered the single-SPA application, we passed the integrated global state repository object to each sub-application via props. It seemed like we could just get the methods and states we needed from props, but there was a problem. Since the globalEventDistributor object we passed is a very hierarchical object, we need to get the current state of the state repository through our own encapsulated getState method, and trigger the corresponding actions in each store through our encapsulated Dispatch. The React component does not update its status updates in the globalEventDistributor. How to resolve this problem? I naturally think of the Provider and Connect components in React-Redux. So we need to implement our own Provider and connect. The core logic of react-redux is to implement the React Context.

/*provider.tsx*/ import React, { useEffect, useState, createContext } from "react"; export const Store = createContext(null); const Provider = (props) => { const { store, children } = props; const [state, setState] = useState(store.getState()); useEffect(() => { store.subscribe((value) => { setState(value); }); } []); return <Store.Provider value={{ state, store }}>{children}</Store.Provider>; }; export default Provider;Copy the code

The idea of a Provider is to create a store listener, update the state in the component when the state changes, and update the component when the state changes, so that the value in the Context is always up to date

/*connect.tsx*/ import React from "react"; import { Store } from "./provider"; const connect = (mapStateToProps) => { return (WrappedComponent) => { return (props) => { console.log(props); return ( <Store.Consumer> {({ state, store }) => { return ( <WrappedComponent {... mapStateToProps(state, props)} dispatch={(args) => { store.dispatch.call(store, args); }} {... props} /> ); }} </Store.Consumer> ); }; }; }; export default connect;Copy the code

The Consumer is able to get the value of the Context and bind the value to the sub-components as props, so that the connect function is realized. Instead of implementing mapDispatchToProps, it is simply using the dispatch method. If you want to implement mapDispatchToProps, this is the window. If you want to implement mapDispatchToProps, I’m not going to implement it anymore because I don’t have any requirements. With Provider and connect implemented for single-SPA children, you can develop single-SPA children just like a normal single-page application would with Redux.

Routing control

Since we need a level 1 route to decide which subapplication to load, the routes inside the subapplication should all be level 2 or higher routes. Here is an example of the show application:

const historySelf = createHistory(); const PrivateRoute = ({ children, islogin, ... rest }) => { return ( <React.Fragment> {islogin ? ( <Route {... rest}>{children}</Route> ) : ( <Redirect to={{ pathname: "/login/choose", state: { from: window.location.pathname }, }} /> )} </React.Fragment> ); }; const App = (props) => { const { history, loginReducer } = props; return ( <React.Fragment> <BrowserRouter history={history || historySelf}> <Switch> <Route exact path="/show/post/:id"> <PostShow /> </Route> <Route exact path="/show/album/:id"> <AlbumShow /> </Route> <Route exact path="/show/video/:id"> <VideoShow /> </Route> <Route exact path="/show/videoplay/:id"> <VideoPlay /> </Route> <Route exact path="/show/user/:id"> <UserShow /> </Route> <Route exact path="/show/switch/:id"> <PostSwitch /> </Route> <PrivateRoute  path="/show/userzone" islogin={ loginReducer && loginReducer.userId } > <UserZone /> </PrivateRoute> </Switch> </BrowserRouter> </React.Fragment> ); }; const BaseApp = connect((state) => { return { ... state }; })(App); const WrapApp = ({ globalEventDistributor, history }) => { console.log(globalEventDistributor); return ( <Provider store={globalEventDistributor}> <BaseApp history={history} /> </Provider> ); }; export default WrapApp;Copy the code

The above is a brief version of the show subapplication code. You can see that all routes are secondary or higher level routes, and some routes may have to be logged in to access. They are private routes, so we implemented a PrivateRoute component that redirects to the login page if the access conditions are not met.

The processing of the iframe

Above we have seen the normal application of routing control and the application of Provider and connect components. Because my project is special in that a large number of old pages have not been redeveloped, so we have a separate iframe child application. Redirect routes that are not in the new project to the iframe child application through route control and open it through iframe. Here I did route control in the base application and the code is as follows:

const Router = () => { return ( <BrowserRouter history={history}> <Switch> <Route path="/show"/> <Route path="/login" />  <Route path="/create" /> <Route path="/iframe" /> <Redirect from="/zone/show/:id" to={`/show/user${window.location.pathname.split("/zone/show")[1]}`} /> <Redirect from="/home" to={`/show/userzone`} /> <Redirect from="/post/make" to={`/create/new`} /> <Redirect from="/youth/post/show/:id" to={`/show/switch${window.location.pathname.split("/post/show")[1]}`} /> <Redirect from="/post/show/:id" to={`/show/switch${window.location.pathname.split("/post/show")[1]}`} /> <Redirect to={`/iframe${window.location.pathname}`} /> </Switch> </BrowserRouter> ) }Copy the code

Above you can see, for some special page, or redo a new project, the routing is different from the old project, but in the APP to share the link or path by the connection, I made a special case of redirection, and then if it doesn’t match how the Route and Redirect to the current routing, will be redirected to the iframe application, So what we do in the child application is we take the href after /iframe, and we open the full href inside the iframe, and that’s all the iframe processing logic, so we do incremental development and incremental migration, we can do it in pieces, Then the undeveloped new page is temporarily introduced into the old page with iframe, so as to solve the pain point of the reconstruction of the large old project, that is, the time limit is long. In the process of development, the old project needs to be maintained daily. There are some optimizations to be made about iframe. One is that it is better to load iframe labels dynamically, that is, to load iframe asynchronously after getting the page to be loaded. Another is that sliding in IOS iframe will be invalid, which requires some special processing, the code is as follows:

<div className="App" ref={iframeRef}></div>  iframeRef.current.innerHTML = isiOS ? `<iframe scrolling="no" class="iframe" frameBorder="0" title="wraphtml" id="pageIframe" src=${iframeUrl} ></iframe>` : `<iframe scrolling="yes" class="iframe" frameBorder="0" title="wraphtml" id="pageIframe" src=${iframeUrl} ></iframe>`; Div style.App {text-align: center; max-width: 557px; overflow: auto; -webkit-overflow-scrolling:touch; width:100%; }Copy the code

summary

This is a summary of some of the corresponding points in sub-application development. The framework and development details of the base application and sub-application have been covered, and we will continue to share questions about packaging and deployment