preface
Also, in order not to waste time, if you don’t know what the micro sidebar is, this article can be closed.
If you have a strong interest in microfronds, you can also try to learn how I implemented microfronds in this application scenario. I hope this article will enlighten you.
The development template code of “Micro front + Enterprise micro sidebar” has been uploaded to Github, clickwecom-sidebar-qiankun-tplYou can see the need for direct white piao + Star.
Let me show you the results of the project:
The framework of the project is as follows:
Does it look Low? It is necessary to do a Low out, so that you have their own play space.
Enterprise micro sidebar
According to international conventions, a quick look at what the enterprise sidebar is, noh, is the red box at the bottom:
The sidebar is essentially an H5 page, and the corresponding name and URL need to be configured in the background of enterprise wechat:
If you’re familiar with the microfront end, you’re familiar with the development pattern of displaying different pages by configuring names and urls. Because no matter which microfront-end framework they are, they register microapplications exactly the same way they configure sidebar applications:
import { registerMicroApps } from 'qiankun';
registerMicroApps(
[
{
name: 'app1'.entry: '//localhost:8080'.container: '#container'.activeRule: '/react'.props: {
name: 'kuitos',},},], {beforeLoad: (app) = > console.log('before load', app.name),
beforeMount: [(app) = > console.log('before mount', app.name)],
},
);
Copy the code
Why use a micro front end
Similarity doesn’t mean you have to be on the micro front end. However, when managing multiple applications, the following problems occur:
- All sidebar applications are hard isolated. Switching between different applications requires reloading
- Basic information is not shared. Reloading requires re-initialization of the JS-SDK and retrieval of group chat, private chat, and user identity information, which is required for every application and should not be retrieved every time
- Facilitate multi-team collaboration. For scenarios where H5 is already embedded in the sidebar
Micro front end idea
The “register multiple sidebar applications” approach mentioned earlier by registering multiple microapplications is a good management approach.
In addition, I also hope to have the following functions:
- Microapplications can get some common information from the main application, such as
userId
Such as - Microapplications can also access the main application
jsSdk
Object, directly usedjsSdk
Interact with enterprise wechat - The main application will do it automatically
User authentication
和JS-SDK
Microapplications no longer need to do common logic and automatically own the data needed by the business - In addition to automatically registering microapplications like the Router, the master application can also register microapplications manually in the specified Container
The Qiankun micro-front-end framework perfectly solves this problem.
Main application – Initialization
As you can see from the analysis above, the main application needs to do two things:
- Perform common logic: get user identity, initialize JS-SDK
- Get public data:
userId
.context
.chat
And other sidebar public and business data that need to be shared
The wecom-sidebar-react-tpl React sidebar development template I wrote earlier already implements most of the content, so I use the ready-made public logic here.
This project adds the main application configuration:
import {initGlobalState, MicroAppStateActions, registerMicroApps, start} from "qiankun";
import {JsSDK} from ".. /jsSdk";
export const subAppContainerId = 'sub-app-container';
export const subAppContainer = ` #${subAppContainerId}`;
// Initialize state
export const microAppStateActions: MicroAppStateActions = initGlobalState({});
// Get the props to be passed to the microapplication
const initPassProps = async (jsSdk: JsSDK) => {
const res = await jsSdk.invoke<{ chatId: string} > ('getCurExternalChat');
return {
jsSdk,
isChat:!!!!! res } }// Launch the main application of Qiankun
const initQiankunMainApp = async (jsSdk: JsSDK) => {
const passProps = await initPassProps(jsSdk);
// Add a state change listener
microAppStateActions.onGlobalStateChange((state, prev) = > {
console.log('[Main application]', state, prev);
});
// Register and start the micro front end
registerMicroApps([
{
name: 'react-app'.entry: '//localhost:3001'.container: subAppContainer,
activeRule: '/#/react-app'.props: passProps
},
{
name: 'sidebar-app'.entry: '//localhost:3002'.container: subAppContainer,
activeRule: '/#/sidebar-app'.props: passProps
}
]);
// Start the main application
start();
}
// Initializes the main application content
export default initQiankunMainApp;
Copy the code
The initQiankunMainApp function needs to be passed into the jsSdk, and then interacts with the enterprise wechat through the jsSdk to obtain the content of private chat and group chat, and then passes it to the micro front end through props. When the microapplication is mounted, the initial public data is available.
If the data changes, such as when login is called, globalState can be updated with the following code:
function login() {... The login logic microAppStateActions. SetGlobalState ({msg: 'New content'})}Copy the code
After the update, microapplications also need to add onGlobalStateChange to listen for data changes. However, because you are only fetching public data, globalState generally does not change very frequently.
It is easy to register the micro application after configuring the name and entry. ActiveRule I wrote /#/xxx-app because I used the Hash Router in the main application. I’ll talk about routing later.
Since we need to register the microapplication after processing the public logic, we need to write this in the entry file index.tsx:
import { ConfigProvider} from 'antd';
// Since the antD component's default text is In English, it needs to be changed to Chinese
import zhCN from 'antd/lib/locale/zh_CN';
import App from './App'
import {fetchUserId, fetchSignatures} from './api'
import config from './_config'
import {invokeResMock, mockUserId, wxResMock} from "./mock";
import {checkRedirect, createJsSdk, initSdk} from "./lib";
import 'antd/dist/antd.css';
import initQiankunMainApp from "./lib/utils/initQiankunMainApp";
export const jsSdk = createJsSdk(wxResMock, invokeResMock);
const AppWrapper = (
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
)
checkRedirect(config, fetchUserId, mockUserId) // Redirect to get code (user identity)
.then(() = > initSdk(config, fetchSignatures)) // Initialize JsSdk
.then(() = > initQiankunMainApp(jsSdk)) // Initialize the master application and register the micro-application
.then(() = > ReactDOM.render(AppWrapper, document.getElementById('root'))) // Render the main application content
Copy the code
Primary application – Routing
The main-micro architecture would have been simpler, but I wanted the main app to be used as a sidebar app, with its own style and some simple functionality, so I thought it was a reasonable requirement to have its own routing system in the main app.
I used Hash Router here because if I use Browser Router in History mode, it would be too troublesome to initialize JS-SDK every time I switch routes. See step 2 in the documentation for details.
If you want to use history mode, you can also initialize it in the route switch callback, but I always have a feeling that some strange bugs may occur
I put all the features of the previous wecom-sidebar- React-TPL project on the home page, so there is only one home page for routing:
const RouterConfig: FC = () = > {
return (
<Switch>
<Route exact path="/">
<Home/>
</Route>
</Switch>)}export default RouterConfig;
Copy the code
The first Home page is the
Microapplication – Initialization
I used create-React-app to create these two micro-applications, and then configured the micro-applications according to the “Project Practice” section of the official qiankun document. I didn’t want to go through this again, but I found some problems in the configuration process, so LET’s expand on that.
Step 1 – publicPath
Add public-path.ts file to/SRC:
const updatePublicPath = () = > {
if (window.__POWERED_BY_QIANKUN__) {
// @ts-ignore
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
}
updatePublicPath();
export default {}
Copy the code
The CRA TS template will report a tsLint error:
TS1208: 'public-path.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.
Copy the code
Error: a file must export one thing, so it can only be written as above.
Then import and execute it on the first line of the index.tsx entry file:
import './public-path';
Copy the code
Be sure to include it in the first line, because it directly determines the publicPath of your static resource. For example, if you use an image resource in a component:
import logo from './logo.svg';
function App(props: Props) {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
</div>
);
}
Copy the code
If not execute the publicPath, or run separately the react – app, the logo here path becomes/static/media/logo. 6 ce24c58. SVG.
The URL for our main application is localhost:3000, and the URL for our micro application is localhost:3001. So when embedded to the main application, image URL becomes the localhost: 3000 / static/media/logo. 6 ce24c58. SVG, but there is no the SVG, main application and resources will be submitted to the 404 error.
Public-path. ts: localhost:3001 (localhost:3001, localhost:3001, localhost:3001);
Step 2 – Basename
Add basename to the Router.
import {HashRouter} from "react-router-dom";
function App() {
return (
<HashRouter basename={window.__POWERED_BY_QIANKUN__ ? '/sidebar-app' :'/'} >
<div className={styles.app}>
<VerticalMenu />
<RouterConfig />
</div>
</HashRouter>
);
}
Copy the code
Note: If both the master application and micro application use Router, the Router type (history mode/Hash mode) must be the same, otherwise there will be many problems.
Note: When I wrote the React Router for the main application, the React Router was already in v6.x, while the main application was still running v5.x.
After adding basename, you can write path directly instead of /sidebar-app/home:
export const routes = [
{url: '/'.label: 'home'.page: Home},
{url: '/external-user'.label: 'private chat'.page: ExternalUser},
{url: '/external-chat'.label: 'chatting'.page: ExternalChat},
{url: '/action'.label: 'operation'.page: Action},
]
const RouterConfig: FC = () = > {
return (
<Routes>
{routes.map(route => (
<Route key={route.url} path={route.url} element={<route.page/>}} / >))</Routes>)}Copy the code
Routes can also be written as path, and the following routes use the routes array above:
const VerticalMenu: FC = () = > {
const location = useLocation();
return (
<Menu
style={menuStyle}
className={styles.menu}
defaultSelectedKeys={[location.pathname]}
mode="vertical"
>
{routes.map(route => (
<Menu.Item className={styles.item} key={route.url}>
<Link to={route.url}>{route.label}</Link>
</Menu.Item>
))}
</Menu>)}Copy the code
Step 3 – Expose the life cycle
Export the lifecycle callback function at the micro-application entry index.tsx:
import './public-path'
import {ConfigProvider} from "antd";
import React from 'react';
import {Provider} from "react-redux";
import ReactDOM from 'react-dom';
import App from './App';
// Since the antD component's default text is In English, it needs to be changed to Chinese
import zhCN from 'antd/lib/locale/zh_CN';
import 'antd/dist/antd.css';
import store from "./store";
const AppWrapper = (
<ConfigProvider locale={zhCN}>
<Provider store={store}>
<App />
</Provider>
</ConfigProvider>
);
// Render the application
const render = (props: any) = > {
const { container } = props;
const containerElement = container ? container.querySelector('#root') : document.querySelector('#root');
ReactDOM.render(AppWrapper, containerElement);
}
// Everywhere qiankun needs life cycle hooks
export const bootstrap = async() = > {console.log('[sidebar-app] bootstrap');
}
export const mount = async (props: any) => {
props.onGlobalStateChange((state: any) = > {
// Update the jsSdk to store
store.dispatch({ type: 'SET_JSSDK'.payload: state.jsSdk })
});
/ / update the jsSdk
store.dispatch({ type: 'SET_JSSDK'.payload: props.jsSdk })
console.log('[sidebar-app] mount', props);
render(props);
}
export const unmount = async (props: any) = > {console.log('[sidebar-app] unmount', props);
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
Copy the code
In the mount callback, we can accept the props from the main application mentioned above, extract isChat and jsSdk from the props, and set them to the Redux Store as the global state of the entire microapplication.
MapStateToProps, mapDispatchToProps, useDispatch, and useSelector are all examples of how to use redux. Get the jsSdk to call the API like the main application.
Or if you don’t want to use redux, it’s also possible to re-render the app after each state change, which I implemented in react-app:
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
// Render function
const render = (props: any) = > {
const { container, user } = props;
ReactDOM.render(<App user={user} />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
// The output function of qiankun micro application
export const bootstrap = async() = > {console.log('react-app bootstrap');
}
export const mount = async (props: any) = > {// Each time the state changes, render again, and pass the updated state and props to the App component
props.onGlobalStateChange((state: any) = > {
console.log('[react-app] onGlobalStateChange', state); render({ ... props, ... state, }) });console.log('react-app mount', props);
render(props);
}
export const unmount = async (props: any) = > {console.log('react-app: unmount', props);
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
Copy the code
Step 4 – Modify the Webpack configuration
Just install a rescripts/ CLI to replace CRA’s garbage scaffolding with rescripts:
npm i -D @rescripts/cli
Copy the code
.rescriptsrc.js is added to the root directory:
const { name } = require('./package');
module.exports = {
webpack: (config) = > {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
devServer: (_) = > {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': The '*'}; config.historyApiFallback =true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
returnconfig; }};Copy the code
Finally modify package.json:
- "start": "react-scripts start",
+ "start": "rescripts start",
- "build": "react-scripts build",
+ "build": "rescripts build",
- "test": "react-scripts test",
+ "test": "rescripts test",
- "eject": "react-scripts eject"
Copy the code
The entire microapplication configuration is done.
The master application manually loads the microapplication
In addition to registering the micro application as a route, loadMicroApp can also be called to load the micro application under the specified Container element.
For example, I manually loaded the React-app in the “home page” of the main app and passed in the user user identity object when loading:
const Home: FC = () = >{...return (
<Spin spinning={loading}>
<div>{/* Manually load microapplications */}<MicroAppComponent user={user}/>
</div>
</Spin>)}Copy the code
MicroAppComponent code:
import {FC, useEffect, useRef} from 'react';
import {loadMicroApp, MicroApp} from "qiankun";
let microAppComponent: MicroApp;
interfaceProps { user? : UserResponse }const MicroAppComponent: FC<Props> = (props) = > {
const containerRef = useRef<HTMLDivElement>(null);
// Pass props when an update is applied
useEffect(() = > {
if (microAppComponent && microAppComponent.update) {
microAppComponent.update(props).then()
}
})
useEffect(() = > {
// Initialize the microapplication
if (containerRef.current) {
microAppComponent = loadMicroApp({
name: 'react-app-nested'.entry: '//localhost:3001'.container: containerRef.current,
props,
});
}
// Unmount the micro application
return () = > {
microAppComponent.unmount().then();
}
}, [props])
return (
<div>
<h2>Micro application</h2>
<div ref={containerRef}/>
</div>)}export default MicroAppComponent;
Copy the code
conclusion
Ok, so let’s wrap things up, after all, this project took a while to do.
- The main application is developed based on wecom-sidebar- React-TPL. The pre-configuration, Mock, and initialization logic are inherited from this project
- Main application use
loadMicroApp
Manually load a microapplication - The main application in
registerMicroApps
Register microapps (sidebar apps) and clickprops
Pass in shared data andJsSdk
- Microapplications in the life cycle of exposure
mount
The parameters of theprops
To get the data passed by the master application - After receiving the master app data, the micro app can choose to put it in redux’s Store and manage it in the redux store
onGlobalStateChange
In the callback againrender
The whole app, take your pick - Both master and micro applications can have their own routes, but the route type must be the same, otherwise there will be a big surprise! Microapplications need to be added to the Router
basename
Get rid of the prefix
Finally, my own suggestion is that the main application should have its own style, welcome page, homepage, route, or write its own department’s sidebar application, and then use Qiankun to leave an entrance for hosting other departments’ sidebars.