What are microservices? Take a look at wikipedia’s definition:
Microservices (English: Microservices is a software architecture style that combines large, complex applications in a modular manner based on Small Building Blocks that focus on a single responsibility and function. Functional blocks communicate with each other using a language-independent /Language agnostic SET of apis.
In other words, a large, complex application is broken down into several services, each of which is like a component that can be combined to build the entire application.
Imagine an application with hundreds of features and hundreds of thousands of lines of code to maintain.
- Changing just one piece of code requires redeploying the entire application. It often happens that “edit in a minute, compile in half an hour”.
- Code modules are intricate and interdependent. Changing code in one place often affects the rest of the application.
What are the benefits of using microservices to refactor the entire application?
An application is decomposed into multiple services, each of which independently serves internal functions. For example, the original application has four pages of ABCD, and now it is divided into two services. The first service has two pages of AB, and the second service has two pages of CD. The combination is the same as the original application.
When one of the application services fails, the other services can still be accessed. For example, if the first service fails, the AB page will be inaccessible, but the CD page will still be accessible.
Benefits: Different services run independently and are decoupled from each other. We can think of services as components, as described in chapter 3 of this little book, Front End Componentization. Each service can be managed independently. Modifying a service does not affect the running of the entire application, but only the functions provided by the service.
In addition, you can quickly add and remove functions during development. For example, e-commerce sites, in different holidays launched in the event page, after the event can be deleted immediately.
Difficulty: It is not easy to identify service boundaries. When an application has too many features, there is often a deep connection between multiple feature points. Therefore, it is difficult to determine which service this function belongs to.
PS: Micro front end is the application of micro service in the front end, that is, front-end micro service.
Microservice practices
Now we will use the micro-front-end framework Qiankun to build a micro-front-end application. The Qiankun framework was selected because it has the following advantages:
- Technology stack independent, any technology stack application can access.
- Style isolation, where the styles of child applications do not interfere with each other.
- The JavaScript scopes of child applications are isolated from each other.
- Resource preloading: Preloads unopened micro-application resources in idle time of the browser to speed up the opening of micro-applications.
Style isolation
The principle of style isolation is that every time a child application is switched, the CSS file corresponding to the child application is loaded. It also removes the original child application style file, thus achieving style isolation.
We can simulate this effect ourselves:
<! -- index.html -->
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<link rel="stylesheet" href="index.css">
<body>
<div>The color will not change after the style file is removed</div>
</body>
</html>
Copy the code
/* index.css */
body {
color: red;
}
Copy the code
Now let’s add some JavaScript code to remove the style file after loading it:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<link rel="stylesheet" href="index.css">
<body>
<div>The color will not change after the style file is removed</div>
<script>
setTimeout(() = > {
const link = document.querySelector('link')
link.parentNode.removeChild(link)
}, 3000)
</script>
</body>
</html>
Copy the code
Then open the page and take a look, you can find that 3 seconds later, the font style is gone.
JavaScript scope isolation
The master application records the current global status before switching over child applications, and then restores the global status after switching out child applications. Assume that the current global state is as follows:
const global = { a: 1 }
Copy the code
After entering the child application, no matter how the global state changes, it will be restored to the original global state when the child application is cut out in the future:
// global
{ a: 1 }
Copy the code
Here’s a chart to help us understand the mechanism:
Ok, now let’s create a micro front end application. The micro front-end application consists of three parts:
- Main: the main application is created using vue-CLI.
- Vue: sub-application created using vue-CLI.
- React: The React 16 version is used as a sub-application.
The corresponding directories are as follows:
-main
-vue
-react
Copy the code
Creating the primary application
We used vuE-CLI to create the main application (then performed NPM I qiankun installation) :
vue create main
Copy the code
If the main application only acts as a pedestal, it is only used to switch child applications. That can eliminate the need to install vuE-Router and Vuex.
transformApp.vue
file
The main App must provide an element that can install child apps, so we need to modify the app. vue file:
<template>
<div class="mainapp">
<! -- Title bar -->
<header class="mainapp-header">
<h1>QianKun</h1>
</header>
<div class="mainapp-main">
<! -- Sidebar -->
<ul class="mainapp-sidemenu">
<li @click="push('/vue')">Vue</li>
<li @click="push('/react')">React</li>
</ul>
<! -- Subapplication -->
<main class="subapp-container">
<h4 v-if="loading" class="subapp-loading">Loading...</h4>
<div id="subapp-viewport"></div>
</main>
</div>
</div>
</template>
<script>
export default {
name: 'App'.props: {
loading: Boolean,},methods: {
push(subapp) { history.pushState(null, subapp, subapp) }
}
}
</script>
Copy the code
You can see that the element we used to install the child app is #subapp-viewport, and there is also the ability to switch the child app:
<! -- Sidebar -->
<ul class="mainapp-sidemenu">
<li @click="push('/vue')">Vue</li>
<li @click="push('/react')">React</li>
</ul>
Copy the code
transformmain.js
RegisterMicroApps () and Start () methods are used to register the child application and start the master application:
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app'.// app name registered
entry: '//localhost:7100'.container: '#yourContainer'.activeRule: '/yourActiveRule'}, {name: 'vue app'.entry: { scripts: ['//localhost:7100/main.js']},container: '#yourContainer2'.activeRule: '/yourActiveRule2',}]); start();Copy the code
So now we need to change the main.js file:
import Vue from 'vue'
import App from './App'
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun'
let app = null
function render({ loading }) {
if(! app) { app =new Vue({
el: '#app'.data() {
return {
loading,
}
},
render(h) {
return h(App, {
props: {
loading: this.loading
}
})
}
});
} else {
app.loading = loading
}
}
/** * Step1 initialize the application (optional) */
render({ loading: true })
const loader = (loading) = > render({ loading })
/** * Step2 register child app */
registerMicroApps(
[
{
name: 'vue'.// Name of the child application
entry: '//localhost:8001'.// Sub application entry address
container: '#subapp-viewport',
loader,
activeRule: '/vue'.// Subapplications trigger routes
},
{
name: 'react'.entry: '//localhost:8002'.container: '#subapp-viewport',
loader,
activeRule: '/react'],},// Child application life cycle events
{
beforeLoad: [
app= > {
console.log('[LifeCycle] before load %c%s'.'color: green', app.name)
},
],
beforeMount: [
app= > {
console.log('[LifeCycle] before mount %c%s'.'color: green', app.name)
},
],
afterUnmount: [
app= > {
console.log('[LifeCycle] after unmount %c%s'.'color: green', app.name)
},
],
},
)
// Define the global state, which can be used in the master and child applications
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: 'qiankun',})// Listen for global state changes
onGlobalStateChange((value, prev) = > console.log('[onGlobalStateChange - master]:', value, prev))
// Set the global status
setGlobalState({
ignore: 'master'.user: {
name: 'master',}})/** * Step3 set the child application */ entered by default
setDefaultMountApp('/vue')
/** * Step4 start the application */
start()
runAfterFirstMounted(() = > {
console.log('[MainApp] first app mounted')})Copy the code
Here are a few caveats:
- The name of the child application
name
Must be subapplied underpackage.json
In the filename
The same. - Each child application has one
loader()
Method, which is designed to handle cases where the user is routed to the page directly from the child application. When entering the sub-page, check whether the main application is loaded. If not, load it and skip it. - To prevent a blank page from being displayed when switching child applications, one should be provided
loading
Configuration. - When setting the entry address of a subapplication, directly enter the access address of the subapplication.
Change access Port
The default vuE-CLI access port is generally 8080. To be consistent with the subapplications, you need to change the primary application port to 8000 (the subapplications are 8001 and 8002 respectively). Create vue.config.js file and change the access port to 8000:
module.exports = {
devServer: {
port: 8000,}}Copy the code
At this point, the main application has been transformed.
Creating a Child application
The sub-application does not need to introduce an Qiankun dependency and only needs to expose several life cycle functions:
bootstrap
Triggered when the child application starts for the first time.mount
Is triggered each time the child application is started.unmount
Triggered when the child application is switched/uninstalled.
Now change the main.js file of the child app:
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import routes from './router'
import store from './store'
Vue.config.productionTip = false
let router = null
let instance = null
function render(props = {}) {
const { container } = props
router = new VueRouter({
// Hash mode does not require the following two lines
base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'.mode: 'history',
routes,
})
instance = new Vue({
router,
store,
render: h= > h(App),
}).$mount(container ? container.querySelector('#app') : '#app')}if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
render()
}
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) = > console.log(`[onGlobalStateChange - ${props.name}] : `, value, prev),
true,
)
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
})
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped')}export async function mount(props) {
console.log('[vue] props from main framework', props)
storeTest(props)
render(props)
}
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ' '
instance = null
router = null
}
Copy the code
You can see that the bootstrap mount unmount lifecycle functions are exposed at the end of the file. In addition, when mounting a child application, you need to pay attention to whether the child application runs under the main application or independently: Container? Container. QuerySelector (‘ # app) : # ‘app’.
Configure packaging items
According to the Qiankun documentation, the following changes need to be made to the sub-application’s packaging configuration items:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`.libraryTarget: 'umd'.jsonpFunction: `webpackJsonp_${packageName}`,}};Copy the code
So now we also need to create the vue.config.js file in the subapplication directory, enter the following code:
// vue.config.js
const { name } = require('./package.json')
module.exports = {
configureWebpack: {
output: {
// Package the child application into the UMD library format
library: `${name}-[name]`.libraryTarget: 'umd'.jsonpFunction: `webpackJsonp_${name}`}},devServer: {
port: 8001.headers: {
'Access-Control-Allow-Origin': The '*'}}}Copy the code
The vue.config.js file has a few caveats:
- The primary and sub-applications run on different ports, so you need to set the cross-domain header
'Access-Control-Allow-Origin': '*'
. - Since the vUE sub-application is configured on the main application and needs to run on port 8001, it also needs to run on port 8001
devServer
Change port in.
The react subapplication is modified in the same way as VUE, so it will not be described here.
The deployment of
We will use Express to deploy the project, with no special attention other than the need to set up sub-applications across domains.
Main application server file main-server.js:
const fs = require('fs')
const express = require('express')
const app = express()
const port = 8000
app.use(express.static('main-static'))
app.get(The '*'.(req, res) = > {
fs.readFile('./main-static/index.html'.'utf-8'.(err, html) = > {
res.send(html)
})
})
app.listen(port, () = > {
console.log(`main app listening at http://localhost:${port}`)})Copy the code
Vue sub-application server file vue-server.js:
const fs = require('fs')
const express = require('express')
const app = express()
const cors = require('cors')
const port = 8001
// Set cross-domain
app.use(cors())
app.use(express.static('vue-static'))
app.get(The '*'.(req, res) = > {
fs.readFile('./vue-static/index.html'.'utf-8'.(err, html) = > {
res.send(html)
})
})
app.listen(port, () = > {
console.log(`vue app listening at http://localhost:${port}`)})Copy the code
React-server.js file for the react sub-application server:
const fs = require('fs')
const express = require('express')
const app = express()
const cors = require('cors')
const port = 8002
// Set cross-domain
app.use(cors())
app.use(express.static('react-static'))
app.get(The '*'.(req, res) = > {
fs.readFile('./react-static/index.html'.'utf-8'.(err, html) = > {
res.send(html)
})
})
app.listen(port, () = > {
console.log(`react app listening at http://localhost:${port}`)})Copy the code
In addition, you need to pack the three application files to the main-static, vue-static, and react-static directories respectively. Then run the node main-server.js, node vue-server.js, and node react-server.js commands respectively to view the deployed page. The project catalog is now as follows:
-main
-main-static // main Static file directory of the main application
-react
-react-static // react subapplication static file directory
-vue
-vue-static // Vue subapplication static file directory
-main-server.js // main Main application server
-vue-server.js // VUE child application server
-react-server.js // React child application server
Copy the code
I have uploaded the code of this micro-front-end application to Github, and suggest that the project be cloned and read together with this chapter, which will be better. Put the following DEMO running effect diagram:
summary
For the development and maintenance of large applications, using a micro front end makes it much easier. However, if it is a small application, it is recommended to build a separate project development. After all, there are additional development and maintenance costs associated with the micro front end.
The resources
- Microservices
- Probably the most complete microfront-end solution you’ve ever seen
- qiankun
Get you started with front-end engineering
- Technology selection: How to do the technology selection?
- Uniform specifications: How do you create specifications and use tools to ensure that they are strictly followed?
- Front-end componentization: What is modularization and componentization?
- Testing: How do I write unit tests and E2E (end-to-end) tests?
- Build tools: What are the build tools? What are the features and advantages?
- Automated Deployment: How to automate deployment projects with Jenkins, Github Actions?
- Front-end monitoring: explain the principle of front-end monitoring and how to use Sentry to monitor the project.
- Performance Optimization (I) : How to detect website performance? What are some useful performance tuning rules?
- Performance Optimization (2) : How to detect website performance? What are some useful performance tuning rules?
- Refactoring: Why do refactoring? What are the techniques for refactoring?
- Microservices: What are microservices? How to set up a microservice project?
- Severless: What is Severless? How to use Severless?