preface
The microfront-end was proposed by ThoughtWorks in 2016. It applies the concept of back-end microservices to the browser side, that is, the Web application is transformed from a single single application into an aggregation of several small front-end applications.
Meituan has become a large Internet company with tens of thousands of employees. It is crucial to improve the overall efficiency, which requires many internal and external management systems to support. Because there is a lot of connectivity and interaction between these systems, we want to be able to aggregate these systems into one or several integrated systems based on users and usage scenarios.
We call this kind of single-page application aggregated by multiple micro front ends “quasi-single-page application”, and meituan HR system is based on this design. Meituan HR system is composed of more than 30 micro front-end applications, including more than 1000 pages, more than 300 navigation menu items. For users, HR system is a single page application, the whole interaction process is very smooth; For developers, each application can be developed, tested and released independently, which greatly improves the development efficiency.
Next, this paper will introduce some practices of “single page application of micro front-end construction class” in Meituan HR system. At the same time, we also share some of our thinking and experience, hoping to inspire you.
Micro front end design of HR system
Because the HR system of Meituan involves many projects, three teams are in charge of it at present. Among them, OA team is responsible for attendance, contract, process and other functions; HR team is responsible for entry, permanent, post transfer, resignation and other functions; Shanghai team is responsible for performance, recruitment and other functions. This division of teams and functions makes each system relatively independent, with independent domain name, independent UI design, and independent technology stack. However, this can lead to unclear responsibilities between development teams and poor user experience, so there is an urgent need to transform the HR system into one domain name and one presentation style.
In order to meet the requirements of the company’s business development, we made an HR portal page, linking the entrances of each subsystem together. However, we found that the significance of the HR portal was so small that users jumped twice and had no idea where they were going. Therefore, we solve the above problems by integrating the HR system into an application.
Generally speaking, there are two main ways to implement “one-page-like applications” :
- Embedded iframe
- Microfront-end merges classes for single page applications
Among them, iframe embedding is relatively easy to implement, but it brings the following problems in practice:
- Subprojects need to be revamped to provide a set of functions without navigation
- The size of the display area embedded with iframe is not easy to control and has some limitations
- The URL record is completely invalid, page refresh cannot be remembered, refresh will be returned to the home page
- The jump between iframe functions is invalid
- Iframe has limitations in style presentation, compatibility, and so on
Considering these problems, iframe embedding could not meet our business demands, so we started to build the HR system in the way of micro front-end.
In this micro front end solution, there are several problems that we must solve:
- One front-end needs to correspond to multiple backends
- Provides a set of application registration mechanism to achieve seamless integration of applications
- Integrate applications at build time and deploy applications independently
Only when the above problems are solved, can our integration be effective and real. Next, we will explain the implementation ideas of these problems in detail.
One front-end corresponds to multiple backends
The HR system finally runs a single page application online, but the application is required to be independent in the project development, so we created an entry project to integrate various applications. In our practice, this project is called a “Portal project” or “main project” and the business application is called a “sub-project”. The overall project structure is shown below:
The Portal project is a special container that does not contain any business during the development phase. In addition to providing “sub-project” registration and merge functions, it can also provide some system-level common support, such as:
- User Login Mechanism
- Menu Permission Obtaining
- Global exception handling
- Global data access
The output of “sub-project” does not need the entry HTML page, but only needs the output resource files, including JS, CSS, fonts, and IMGS.
The HR system runs a front-end service (Node Server) online, which is used to respond to user login, authentication, and resource requests. Data requests of the HR system are not transmitted through the front-end service, but forwarded to the back-end Server by Nginx. The specific interaction is shown in the figure below:
The forwarding rule limits that the data request format must be prefixed with system name +Api to ensure complete isolation of requests between different systems. The configuration example of Nginx is as follows:
server {
listen 80;
server_name xxx.xx.com;
location /project/api/ {
set $upstream_name "server.project";
proxy_pass http://$upstream_name; }... location / {set $upstream_name "web.portal";
proxy_pass http://$upstream_name; }}Copy the code
We entrusted SSO with the unified login and authentication of users. The back-end servers of all projects should access SSO to verify the login status, so as to ensure the consistency of user security authentication between business systems.
After the project structure is determined, how do applications merge? So we started to develop an application registration mechanism.
Apply registration mechanism
The Portal project provides an interface for registration, and the subprojects register and aggregate into a single page application. In the whole mechanism, the core part is the routing registration mechanism. The routing of the “sub-project” should be controlled by itself, while the navigation of the whole system is provided by the “Portal project”.
Routing registered
The control of routing consists of three parts: permission menu tree, navigation and routing tree. A component App is encapsulated in the Portal Project, and the whole page is generated according to the menu tree and routing tree. The code for mounting the route to the DOM tree is as follows:
let Router = <Router
fetchMenu = {fetchMenuHandle}
routes = {routes}
app = {App}
history = {history}
>
ReactDOM.render(Router,document.querySelector("#app"));
Copy the code
Router is encapsulated on the basis of react-router, and generates a routing tree as follows through menu and routes:
<Router>
<Route path="/" component={App}>
<Route path="/namespace/xx" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
Copy the code
The Portal project obtains the route from window.app.routes. The subproject adds its route to window.app.routes.
let app = window.app = window.app || {};
app.routes = (app.routes || []).concat([
{
code:'attendance-record'.path: '/attendance-record'.component: wrapper((a)= > async(require('./nodes/attendance-record'), 'kaoqin')),}]);Copy the code
Route merge at the same time also made the specific function reference association, and then to build time can manage all the functions and routes. How do you control the scope of a project? We require “sub-projects” to be isolated from each other, to avoid style contamination, to do independent data flow management, and we use the project scope approach to solve these problems.
Project scope control
In routing control, we mentioned window.app. We also use this global app to control the project scope. Window. app contains the following sections:
let app = window.app || {};
app = {
require:function(request){... },define:function(name,context,index){... },routes: [...]. .init:function(namespace,reducers){...}
};
Copy the code
Main functions of Window. app:
- Define project public library, mainly used to solve the JS public library management problems
- Require references its own library of definitions, used in conjunction with define
- Routes is used to store global routes. Subproject routes are added to window.app.routes to complete route registration
- Init registration entry, add the Namesapce logo for the subproject, register the reducers for the subproject management data flow
The complete registration of the sub-project is as follows:
import reducers from './redux/kaoqin-reducer';
let app = window.app = window.app || {};
app.routes = (app.routes || []).concat([
{
code:'attendance-record'.path: '/attendance-record'.component: wrapper((a)= > async(require('./nodes/attendance-record'), 'kaoqin')),
/ /... Other routing
}]);
function wrapper(loadComponent) {
let React = null;
let Component = null;
let Wrapped = props= > (
<div className="namespace-kaoqin">
<Component {. props} / >
</div>
);
return async () => {
await window.app.init('namespace-kaoqin',reducers);
React = require('react');
Component = await loadComponent();
return Wrapped;
};
}
Copy the code
Here are some things:
- Add the route to window.app
- Executed the first time a business function is invoked
window.app.init(namespace,reducers)
, register project scope and data flow reducers - Wrap a root node for a business function mount node:
Component
Mounted on theclassName
fornamespace-kaoqin
thediv
The following
This completes the registration of the “subproject,” whose outward output is an entry file and a set of resource files generated by the WebPack build.
In terms of CSS scope, webPack is used to add its own scope to all CSS of the business during the build phase, and the build configuration is as follows:
// Add namespace control in postCSS plugin
config.postcss.push(postcss.plugin('namespace', () = >css= >
css.walkRules(rule= > {
if (rule.parent && rule.parent.type === 'atrule'&& rule.parent.name ! = ='media') return;
rule.selectors = rule.selectors.map(s= > `.namespace-kaoqin ${s === 'body' ? ' ' : s}`); })));Copy the code
CSS to use postcss – loader, postcss – loader use postcss, we add postcss processing plug-in, for added called every CSS selectors. The root of the namespace – kaoqin selector, final packaging of CSS, as shown below:
.namespace-kaoqin .attendance-record {
height: 100%;
position: relative
}
.namespace-kaoqin .attendance-record .attendance-record-content {
font-size: 14px;
height: 100%;
overflow: auto;
padding: 0 20px}...Copy the code
Now that the CSS styling issue is resolved, let’s take a look at what the Portal-provided init does.
let inited = false;
let ModalContainer = null;
app.init = async function (namespace,reducers) {
if(! inited) { inited =true;
let block = await new Promise(resolve= > {
require.ensure([], function (require) {
app.define('block'.require.context('block'.true, / ^ \ \ /? ! dev)([^\/]|\/(? ! demo))+\.jsx? $/)); resolve(require('block'));
}, 'common');
});
ModalContainer = document.createElement('div');
document.body.appendChild(mtfv3ModalContainer);
let { Modal} = block;
Modal.getContainer = (a)= > ModalContainer;
}
ModalContainer.setAttribute('class'.`${namespace}`);
mountReducers(namepace,reducers)
};
Copy the code
The init method does two main things:
- Mount the reducers of the “subproject” and mount the data flow of the “subproject” on Redux
- The pop-ups of the “subproject” are all mounted on a global DIV, and the corresponding project scope is added to this DIV, along with the CSS built by the “subproject” to ensure that the pop-ups are styled correctly
We also see the use of app.define in the above code, which is mainly used to handle the control of JS common libraries, such as the component library Block, and expect the versions of each “subproject” to be uniform. Therefore, we need to solve the problem of JS common library version unification.
JS common library version unification
In order not to invade the “subproject”, we do this in a build process substitution way. The Portal project brings in the common library, redefines it, and then references it in the window.app.require way. Replace all references to the public library from require(‘react’) to window.app.require(‘ React ‘) so that the JS public library version can be controlled by the Portal project.
The code and example for define are as follows:
/** * redefines the package * @param name refers to the package name, e.g. react * @param context the resource reference is actually webpackContext (which is a method, To reference the resource file) * @param index defines the package entry file */
app.define = function (name, context, index) {
let keys = context.keys();
for (let key of keys) {
let parts = (name + key.slice(1)).split('/');
let dir = this.modules;
for (let i = 0; i < parts.length - 1; i++) {
let part = parts[i];
if(! dir.hasOwnProperty(part)) { dir[part] = {}; } dir = dir[part]; } dir[parts[parts.length -1]] = context.bind(context, key);
}
if(index ! =null) {
this.modules[name]['index.js'] = this.modules[name][index]; }};// Define app react
// Create a react repository. Bind all the.js files in the react root directory and lib directory to the newly defined react repository
app.define('react'.require.context('react'.true, /^.\/(lib\/)? [^\/]+\.js$/),'react.js');
app.define('react-dom'.require.context('react-dom'.true, /^.\/index\.js$/));
Copy the code
The “subproject” build replaces the reference with webpack externals:
/** * Public package references are handled via webpack externals */
const libs = ['react'.'react-dom'."block"];
module.exports = function (context, request, callback) {
if (libs.indexOf(request.split('/'.1) [0])! = =- 1) {
// replace window.app.require('${request}') with window.app.require('${request}');
//var ()
callback(null.`var window.app.require('${request}') `);
} else{ callback(); }};Copy the code
This completes the registration of the project, and there are other things that the “subproject” needs to do itself, such as loading the navigation of the “Portal project” into the local startup, making mock data, and so on.
Now that the project is registered, how do we release the deployment?
Post-build integration and standalone deployment
During the integration of HR systems, the development phase is “zero intrusion” to the “sub-project”, and we expect this to be the case during the release phase.
Our deployment process is roughly as follows:
Step 1: On the publisher, get the code, install the dependencies, and perform the build; Step 2: Upload the build results to the server; Step 3: Run Node index.js on the server to start the service.
After the Portal project is built, the file structure is as follows:
The file structure of the subproject is as follows:
The file structure for running online is as follows:
Upload the construction files of “subproject” to the corresponding “subproject” file directory of the server, and then integrate and merge the resource files of “subproject” to generate files in the. Dist directory for online access and use by users.
For each release, we mainly do the following three things:
- Publish the latest static resource file
- Re-generate entry-xx.js and index.html (update entry references)
- Restarting front-end Services
If the service is purely static, it can be hot deployed and the reference relationship can be dynamically updated without restarting the service. Since we did some public services in the Node service layer, we chose to restart the service, and we used the company’s base service and PM2 for hot boot.
For historical files, we need to do version control to ensure that previous access works. In addition, in order to ensure the high availability of the service, we put four machines online and deployed them in two machine rooms to improve the fault tolerance of the HR system.
conclusion
The above is the “one-page-like application” HR business system we built by using React technology stack and micro front end. Review the technical solution and the whole framework process is shown in the figure below:
On the product level, “micro-front-end single-page application” breaks the concept of independent projects, and we can freely assemble our page application according to users’ needs. For example, we can put together high-frequency functions such as attendance, leave, OA approval, and financial reimbursement on the HR portal. We can even allow users to customize their own functions, so that users really feel that we are a system.
The solution of “Micro front-end construction class single page application” is developed based on React technology stack. If the routing management mechanism and registration mechanism are removed as a common library, it can be encapsulated into a general solution of business independent based on Webpack, and it is very friendly to use.
Up to now, the HR system has been running stably for more than one year. We have summarized the following three advantages:
- Single-page apps have a better experience, loading on demand and smooth interaction
- Project microfront-end, business decoupling, guaranteed stability, easy to control the granularity of the project
- The robustness of the project is good, the project registration only increases the size of the entry file, more than 30 projects currently only 12K
Author’s brief introduction
Jia Zhao, who joined Meituan in 2014, has led the front-end construction of OA, HR, finance and other enterprise projects, independently developed the React component library Block, unified the front-end technology stack of the entire enterprise platform on the basis of Block, and committed to improving the work efficiency of the R&D team.