Why use a micro front end
Now with the continuous development of the front-end, enterprise project volume is more and more big, more and more pages, the project becomes swollen and maintenance is also very difficult, sometimes we just change the project simple style, need the whole project set up to repackage, created a lot of trouble to the developer, is also a waste of time. In order to integrate the old project into the new project, the reconstruction needs to be carried out constantly, resulting in high human cost.
The micro front-end architecture has the following core values:
- Stack independent The main framework does not restrict access to the application stack, sub-applications have full autonomy
- Independent development, independent deployment of the sub-application repository independent, the front and back end can be independently developed, after the deployment of the main framework automatically complete synchronization update
- Independent run time state is isolated between each child application and run time state is not shared
The implementation principle of single-SPA
Firstly, the micro-front-end route is registered, and single-SPA is used as the micro-front-end loader and the single entrance of the project to accept the access of all page urls. According to the matching relationship between the page URL and the micro-front-end, the corresponding micro-front-end module is selected to load, and then the micro-front-end module responds to the URL. That is, the micro-front-end module route to find the corresponding components, rendering page content.
The function and benefit of sysyem.js
The purpose of system.js is to dynamically load modules on demand. If all our sub-projects use VUE,vuex, vuE-Router, it would be wasteful to package each project once. System. js can be used with the externals attribute of webpack to configure these modules as external links, and then load them on demand. Of course, you can also import all these common JS with script tags. We just need to expose the app.js of the subproject to it.
What is a Lerna
As front-end projects get larger and larger, common code is often broken up and maintained as separate NPM packages. But then dependency management between packages becomes a headache. To solve this problem, we can manage different NPM package projects in the same project. Such a project development strategy is also known as monorePO. Lerna is such a tool for you to do this better. Lerna is a tool that uses Git and NPM to handle multi-package dependency management, which automatically helps us manage version dependencies between various module packages. There are many public libraries that use Lerna as their dependency management tools, such as Babel, create-react-app, react-Router, jest, etc.
- Resolve dependencies between packages.
- Git repository detects changes and synchronizes them automatically.
- Generated based on the associated Git commit
CHANGELOG
.
You will also need to install Lerna globally:
npm install -g lerna
Copy the code
Built based on vUE micro front end project
1. Initialize the project
mkdir lerna-project & cd lerna-project`
lerna init
Copy the code
After the command is executed successfully, this directory structure is generated under the directory.
├─ readme.md ├─ Lerna. json # ├─ Package. json ├─ Package ├─Copy the code
2.Set up yarn workspaces mode
The default is NPM, and each subpackage has its own node_modules, so that only the top layer has one node_modules
{
"packages": [
"packages/*"]."useWorkspaces": true."npmClient": "yarn"."version": "0.0.0"
}
Copy the code
Also set package.json to true to prevent the root directory from being published to NPM:
{
"private": true."workspaces": [
"packages/*"]}Copy the code
Configure lerna.json in the root directory to use the YARN client and use workspaces
yarn config set workspaces-experimental true
Copy the code
3. Register sub-applications
Step 1: Create a child application using vue-CLI
#Go to the Packages directory
cd packages
#Create an├─ public ├─ SRC │ ├─ main.js │ ├── assets │ ├── ├─ vue ├─ vue.config.js ├─ Package. json ├─ Readme.md.├ ─ lesson.txtCopy the code
Step 2: Use vuE-CLI-plugin-single-SPA to quickly generate spa projects
#It will automatically modify main.js to add singleSpaVue and generate set-public-path.js
vue add single-spa
Copy the code
The generated main.js file
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
// el: '#app', // no mount point is mounted under body by default
render: (h) = > h(App),
router,
store: window.rootStore,
},
});
export const bootstrap = [
vueLifecycles.bootstrap
];
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
Copy the code
Step 3: Set the environment variable.env
#The application name
VUE_APP_NAME=app1
#Apply the root path. The default value is:'/'This value must be specified if you want to publish to a subdirectory
VUE_APP_BASE_URL=/
#Port. It is better to set a fixed port for sub-project development to avoid frequent modification of configuration files and set a fixed special port to avoid port conflicts as far as possible.
port=8081
Copy the code
Step 4: Set vue.config.js to modify the WebPack configuration
const isProduction = process.env.NODE_ENV === 'production'
const appName = process.env.VUE_APP_NAME
const port = process.env.port
const baseUrl = process.env.VUE_APP_BASE_URL
module.exports = {
// Prevent loading problems in development environments
publicPath: isProduction ? `${baseUrl}${appName}/ ` : `http://localhost:${port}/ `.// CSS is not packaged as a separate file in any environment. This is to ensure minimal introduction (only JS)
css: {
extract: false
},
productionSourceMap: false.outputDir: path.resolve(dirname, `.. /.. /dist/${appName}`), // package to root dist
chainWebpack: config= > {
config.devServer.set('inline'.false)
config.devServer.set('hot'.true)
// Make sure that the package is a JS file for the main application to load
config.output.library(appName).libraryTarget('umd')
config.externals(['vue'.'vue-router'.'vuex']) // Do not register
if (process.env.NODE_ENV === 'production') {
// Wrap the target file with a hash string to disable browser caching
config.output.filename('js/index.[hash:8].js')}}}Copy the code
4. Create a main project
Step 1: Add the main project Package
#Go to the Packages directory
cd packages
#Create a packge directory and go to the root-html-file directory
mkdir root-html-file && cd root-html-file
#Initialize a package
npm init -y
Copy the code
Step 2: Create the main project index.html
The main application mainly plays the role of route distribution and resource loading
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Vue-Microfrontends</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="importmap-type" content="systemjs-importmap">
<! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
<script type="systemjs-importmap" src="importmap.json"></script>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
<link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js" as="script" crossorigin="anonymous" />
<! -- SystemJS package -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
<! Parsing subpackages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
<! -- Parse package default -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
<! -- SystemJS package -->
</head>
<body>
<div id="root"></div>
</body>
</html>
Copy the code
Step 3: Edit the importMapjson file and configure the file for the child application
{
"imports": {
"navbar": "http://localhost:8888/js/app.js"."app1": "http://localhost:8081/js/app.js"."app2": "http://localhost:8082/js/app.js"."single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"."vue": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"."vue-router": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js"."vuex": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.min.js"}}Copy the code
Then systemJS can directly import, see systemJS
Step 4: Register the app
singleSpa.registerApplication(
'navbar'.// systemjs-webpack-interop, to match the application name
() = > System.import('navbar'), // Resource path
location= > location.hash.startsWith('#/navbar') // The resource is active
)
// Register child applications
singleSpa.registerApplication(
'app1'.// systemjs-webpack-interop, to match the name of the child application
() = > System.import('app1'), // Resource path
location= > location.hash.startsWith('#/app1') // The resource is active
)
singleSpa.registerApplication(
'app2'.// systemjs-webpack-interop, to match the name of the child application
() = > System.import('app2'), // Resource path
location= > location.hash.startsWith('#/app2') // The resource is active
)
/ / start singleSpa
singleSpa.start();
Copy the code
Step 5: Project development
The basic directory structure of the project is as follows:
.├ ─ readme.md ├─ Lerna. json # ├─ Node_modules ├─ Package. json ├─ ├─ App1 # ├ ─ ─ app2 # 2 │ ├ ─ ─ # navbar main application │ └ ─ ─ root - HTML - # file entry └ ─ ─ yarn. The lockCopy the code
As shown in the figure above, all applications are stored in the Packages directory. Root-html-file is the entry project, navbar is the resident main application, and the corresponding services must be started in the development process. Others are sub-applications to be developed.
Project optimization
Extract subapplication resource configurations
Extract all child applications from the main application into a common app.config.json file configuration
{
"apps": [{"name": "navbar".// Application name
"main": "http://localhost:8888/js/app.js".// The entry to the application
"path": "/".// Whether it is a resident application
"base": true.// Whether to use history mode
"hash": true // Whether to use hash mode
},
{
"name": "app1"."main": "http://localhost:8081/js/app.js"."path": "/app1"."base": false."hash": true
},
{
"name": "app2"."main": "http://localhost:8082/js/app.js"."path": "/app2"."base": false."hash": true}}]Copy the code
The child application is registered in the entry file of the main application
try {
// Read the application configuration and register the application
const config = await System.import(`/app.config.json`)
const { apps } = config.default
apps && apps.forEach((app) = > registerApp(singleSpa, app))
singleSpa.start()
} catch (e) {
throw new Error('Failed to load application configuration')}/** * Register application ** /
function registerApp (spa, app) {
const activityFunc = app.hash ? hashPrefix(app) : pathPrefix(app)
spa.registerApplication(
app.name,
() = > System.import(app.main),
app.base ? (() = > true) : activityFunc,
)
}
/** * Hash matching mode *@param App configuration */
function hashPrefix (app) {
return function (location) {
if(! app.path)return true
if (Array.isArray(app.path)) {
if (app.path.some(path= > location.hash.startsWith(` #${path}`))) {
return true}}else if (location.hash.startsWith(` #${app.path}`)) {
return true
}
return false}}/** * Common path matching mode *@param App configuration */
function pathPrefix (app) {
return function (location) {
if(! app.path)return true
if (Array.isArray(app.path)) {
if (app.path.some(path= > location.pathname.startsWith(path))) {
return true}}else if (location.pathname.startsWith(app.path)) {
return true
}
return false}}Copy the code
All subprojects share one using VUEX
The main project index. HTML registers the vuex plug-in, the Window object store, and the sub-project load starts with registerModule injection of the sub-application’s module and its own Vue instance
// in the js of the main application
Vue.use(Vuex)
window.rootStore = new Vuex.Store() // globally register a unique VUEX for sub-applications to share
// Main.js of the child application
export const bootstrap = [
() = > {
return new Promise(async (resolve, reject) => {
// Register the current app store
window.rootStore.registerModule(VUE_APP_NAME, store)
resolve()
})
},
vueLifecycles.bootstrap
];
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
Copy the code
Style isolation
We use a plugin for postCSS: postCSs-selector -namespace. It will add a class name prefix to all CSS in your project. This enables namespace isolation. NPM install postcss-selector-namespace –save -d postcss.config.js
// postcss.config.js
module.exports = {
plugins: {
// postcss-selector-namespace: Adds a uniform prefix to all CSS, and then adds a namespace to the parent project
'postcss-selector-namespace': {
namespace(css) {
// Element-UI styles do not require namespaces
if (css.includes('element-variables.scss')) return ' ';
return '.app1' // Returns the name of the class to be added}}}},Copy the code
The parent project then adds the namespace
// Add the corresponding subsystem's class namespace to the body when switching subsystems
window.addEventListener('single-spa:app-change'.() = > {
const app = singleSpa.getMountedApps().pop();
const isApp = /^app-\w+$/.test(app);
if (app) document.body.className = app;
});
Copy the code
Generate app.config.json and importMapjson using the manifest
Stats-webpack-plugin generates a manifest.json file that contains the public_path bundle list chunk list file size dependencies for each package. You can use this information to generate the app.config.json path and importMapjson for the child application.
npm install stats-webpack-plugin --save -d
Copy the code
Used in vue.config.js:
{
configureWebpack: {
plugins: [
new StatsPlugin('manifest.json', {
chunkModules: false.entrypoints: true.source: false.chunks: false.modules: false.assets: false.children: false.exclude: [/node_modules/]}),]}}Copy the code
Finally, the script generate-app.js generates the json path and importMapjson for the corresponding child application
const path = require('path')
const fs = require('fs')
const root = process.cwd()
console.log(The current working directory is:${root}`);
const dir = readDir(root)
const jsons = readManifests(dir)
generateFile(jsons)
console.log('Configuration file generated successfully')
function readDir(root) {
const manifests = []
const files = fs.readdirSync(root)
files.forEach(i= > {
const filePath = path.resolve(root, '. ', i)
const stat = fs.statSync(filePath);
const is_direc = stat.isDirectory();
if (is_direc) {
manifests.push(filePath)
}
})
return manifests
}
function readManifests(files) {
const jsons = {}
files.forEach(i= > {
const manifest = path.resolve(i, './manifest.json')
if (fs.existsSync(manifest)) {
const { publicPath, entrypoints: { app: { assets } } } = require(manifest)
const name = publicPath.slice(1, -1)
jsons[name] = `${publicPath}${assets}`}})return jsons
}
function generateFile(jsons) {
const { apps } = require('./app.config.json')
const { imports } = require('./importmap.json')
Object.keys(jsons).forEach(key= > {
imports[key] = jsons[key]
})
apps.forEach(i= > {
const { name } = i
if (jsons[name]) {
i.main = jsons[name]
}
})
fs.writeFileSync('./importmap.json'.JSON.stringify(
{
imports
}
))
fs.writeFileSync('./app.config.json'.JSON.stringify(
{
apps
}
))
}
Copy the code
The application package
The build command is executed in the root directory. All build commands in packages will be executed. This will generate dist in the root directory.
lerna run build
Copy the code
The resulting directory structure is as follows
. ├ ─ ─ dist │ ├ ─ ─ app1 / │ ├ ─ ─ app2 / ├ ─ ─ navbar / │ ├ ─ ─ app. Config. The json │ ├ ─ ─ importmap. Json │ ├ ─ ─ the main, js │ ├ ─ ─ The generate - app. Js │ └ ─ ─ index. The HTMLCopy the code
Finally, run the following command to generate generate-app.js and regenerate the importmap.json and app.config.json files with hash resource paths: importmap.json
cd dist && node generate-app.js
Copy the code
The full demo file address in the article, give a star if you find it useful
Reference documentation
- Lerna manages best practices for front-end modules
- Lerna and YARN implement monorepo
- Implementing a Single-SPA front-end Microservice from 0 (middle)
- Single-spa + Vue Cli Micro front-end Landing Guide + Video (project isolation remote loading, automatic introduction)
- Single-spa Microfront-end landing (including NGINx deployment)
- Probably the most complete microfront-end solution you’ve ever seen
- coexisting-vue-microfrontends