preface
The author of this article fully liver more than a week, many times consider the revision of content, and strive to help readers build a micro front frame, understand the principle. If you think it’s good, give it a thumbs up.
Micro front-end is a popular technology architecture at present, many readers secretly asked me the principle of it. In order to explain how this works, I will walk you through implementing a micro front-end framework from scratch, which contains the following features:
- How to perform route hijacking
- How to render a child application
- How to implement JS sandbox and style isolation
- Enhance the experiential function
In addition, in the process of implementation, the author will also talk about what technical solutions can be used to achieve the micro front end and do the above functions when there are ways to achieve.
Here is the final output warehouse address for this article: Toy-Micro.
Micro front-end implementation scheme
There are a lot of micro front-end implementation schemes, such as:
- Qiankun, own IMPLEMENTATION of JS and style isolation
- Icestark, iframe scheme, browser native isolation, but there are some problems
- Emp, Webpack 5 Module Federation solution
- Solutions like WebComponent
But many of the scenarios that implementations solve fall into two categories:
- Single instance: Only one sub-application exists on the page, usually using Qiankun
- Multiple instances: The current page has multiple child applications that can be isolated using browser native isolation schemes such as iframe or WebComponent
Of course, this doesn’t mean that single instances can only be used with Qiankun. Browser-native isolation schemes are possible, as long as you accept their limitations:
The most important feature of iframe is that it provides a browser native hard isolation scheme, no matter the style isolation, JS isolation and other problems can be solved perfectly. However, its biggest problem is that its isolation can not be broken, leading to the application context can not be shared, resulting in the development experience, product experience problems.
The above excerpt is from Why Not Iframe.
The implementation scheme in this paper is the same as that in Qiankun, but the functions and principles involved are universal, which is also needed in another implementation scheme.
Pre – work
Before we start, we need to set up the development environment. Here, you can choose the main/sub-application stack. For example, the main application uses React and the sub-application uses Vue. Each application uses the corresponding scaffolding tool to initialize the project, so we don’t need to initialize the project. Remember that if you are in a React project, you need to execute YARN eject again.
I recommend you to directly use the example folder in the author’s warehouse, the configuration is configured, we just need to feel at ease with the author step by step to do the micro front end on the line. In this example, the main application is React and the sub-application is Vue. The resulting directory structure is roughly as follows:
The body of the
Before reading the text, I assume that you have already used the micro-front-end framework and understood the concepts involved, such as that the main application is responsible for the overall layout and the configuration and registration of sub-applications. If you have not already used it, we recommend that you briefly read the documentation for the use of any micro-front-end framework.
Application of registration
After having the master application, we need to register the information of the sub-application in the master application, which contains the following pieces:
- Name: subapplication noun
- Entry: Entry of sub-application resources
- Container: node for the master application to render child applications
- ActiveRule: Render the child application under which routes
In fact, this information is similar to how we register routes in a project. Entry can be used as a component to render, Container can be used as a node to render routes, and activeRule can be used as a rule to match routes.
Let’s first implement the function to register the child application:
// src/types.ts
export interface IAppInfo {
name: string;
entry: string;
container: string;
activeRule: string;
}
// src/start.ts
export const registerMicroApps = (appList: IAppInfo[]) = > {
setAppList(appList);
};
// src/appList/index.ts
let appList: IAppInfo[] = [];
export const setAppList = (list: IAppInfo[]) = > {
appList = list;
};
export const getAppList = () = > {
return appList;
};
Copy the code
The above implementation is as simple as saving the appList passed in by the user.
Routing hijacked
Once we have the list of child applications, we need to start the micro front end to render the appropriate child applications, that is, we need to determine the route to render the appropriate application. But before we go any further, we need to consider a question: how do we listen for route changes to determine which subapplications to render?
For non-SPA (single page application) architectures, this is not a problem at all, as we just need to determine the current URL and render the application when launching the micro front end; However, in the SPA architecture, route changes do not trigger page refreshes, so we need a way to know if we need to switch subapplications or do nothing.
If you know the Router library principles, you should immediately come up with a solution. If you don’t know, you can read my previous articles for yourself.
In order to take care of readers who do not understand, the author here first briefly talk about the principle of routing.
Currently, single-page applications use routing in two ways:
- Hash mode, which is carried in the URL
#
- Histroy mode, which is the common URL format
Here are two illustrations to show what events and apis are involved in each mode:
From the above figure, we can see that route changes involve two events:
popstate
hashchange
So these are two events that we definitely need to monitor. In addition, calls to pushState and replaceState also cause route changes but do not trigger events, so we need to override these functions.
Now that we know what events to listen for and what functions to override, let’s implement the code:
// src/route/index.ts
// Keep the old method
const originalPush = window.history.pushState;
const originalReplace = window.history.replaceState;
export const hijackRoute = () = > {
// Override the method
window.history.pushState = (. args) = > {
// Call the old method
originalPush.apply(window.history, args);
// The URL change logic is actually how to handle child applications
// ...
};
window.history.replaceState = (. args) = > {
originalReplace.apply(window.history, args);
// URL change logic
// ...
};
// Listen for events that trigger the URL change logic
window.addEventListener("hashchange".() = > {});
window.addEventListener("popstate".() = > {});
/ / rewrite
window.addEventListener = hijackEventListener(window.addEventListener);
window.removeEventListener = hijackEventListener(window.removeEventListener);
};
const capturedListeners: Record<EventType, Function[] > = {hashchange: [].popstate: [],};const hasListeners = (name: EventType, fn: Function) = > {
return capturedListeners[name].filter((listener) = > listener === fn).length;
};
const hijackEventListener = (func: Function) :any= > {
return function (name: string, fn: Function) {
// Save the callback function if the following events occur
if (name === "hashchange" || name === "popstate") {
if(! hasListeners(name, fn)) { capturedListeners[name].push(fn);return;
} else {
capturedListeners[name] = capturedListeners[name].filter(
(listener) = > listener !== fn
);
}
}
return func.apply(window.arguments);
};
};
// Used after subsequent rendering of the child application to execute the previously saved callback function
export function callCapturedListeners() {
if (historyEvent) {
Object.keys(capturedListeners).forEach((eventName) = > {
const listeners = capturedListeners[eventName as EventType]
if (listeners.length) {
listeners.forEach((listener) = > {
// @ts-ignore
listener.call(this, historyEvent)
})
}
})
historyEvent = null}}Copy the code
The above code looks at many lines, but what it actually does is very simple, divided into the following steps:
- rewrite
pushState
As well asreplaceState
Method, in which the original method is called, how to execute the logic of the child application - Listening to the
hashchange
及popstate
Event, how does the event execute the logic to handle the child application - Override the listener/remove event function if the listener is applied
hashchange
及popstate
The event stores the callback function for later use
Application life cycle
Now that we have implemented route hijacking, we need to consider how to implement the logic to handle the child application, that is, how to handle the child application loading resources and mount and unload the child application. If you look at this, this looks like a component. Components also need to handle these things and expose the lifecycle for the user to do what they want.
Therefore, for a sub-application, we also need to implement a life cycle, since the sub-application has a life cycle, the main application must also have a life cycle, and must be corresponding to the sub-application life cycle.
So at this point we can sort out the life cycle of the master/child application.
For the main application, there are three life cycles:
beforeLoad
: Before mounting the child applicationmounted
: After mounting the child applicationunmounted
: Uninstalls sub-applications
Of course, it’s perfectly fine if you want to increase the life cycle, but I’ve only implemented three for simplicity.
For sub-applications, generic is also divided into the following three life cycles:
bootstrap
: Triggers the first application loading. It is used to configure the global information of sub-applicationsmount
: Triggered when an application is mounted, often used to render child applicationsunmount
: Triggered when an application is uninstalled. It is used to destroy child applications
Next we implement the register main application lifecycle function:
// src/types.ts
export interfaceILifeCycle { beforeLoad? : LifeCycle | LifeCycle[]; mounted? : LifeCycle | LifeCycle[]; unmounted? : LifeCycle | LifeCycle[]; }// src/start.ts
// Rewrite the previous one
export const registerMicroApps = (appList: IAppInfo[], lifeCycle? : ILifeCycle) = > {
setAppList(appList);
lifeCycle && setLifeCycle(lifeCycle);
};
// src/lifeCycle/index.ts
let lifeCycle: ILifeCycle = {};
export const setLifeCycle = (list: ILifeCycle) = > {
lifeCycle = list;
};
Copy the code
Since it is the life cycle of the main application, we register the child application along with it.
Then the life cycle of the child application:
// src/enums.ts
// Set the status of the child application
export enum AppStatus {
NOT_LOADED = "NOT_LOADED",
LOADING = "LOADING",
LOADED = "LOADED",
BOOTSTRAPPING = "BOOTSTRAPPING",
NOT_MOUNTED = "NOT_MOUNTED",
MOUNTING = "MOUNTING",
MOUNTED = "MOUNTED",
UNMOUNTING = "UNMOUNTING",}// src/lifeCycle/index.ts
export const runBeforeLoad = async (app: IInternalAppInfo) => {
app.status = AppStatus.LOADING;
await runLifeCycle("beforeLoad", app);
app = awaitLoad sub-application resources; app.status = AppStatus.LOADED; };export const runBoostrap = async (app: IInternalAppInfo) => {
if(app.status ! == AppStatus.LOADED) {return app;
}
app.status = AppStatus.BOOTSTRAPPING;
awaitapp.bootstrap? .(app); app.status = AppStatus.NOT_MOUNTED; };export const runMounted = async (app: IInternalAppInfo) => {
app.status = AppStatus.MOUNTING;
awaitapp.mount? .(app); app.status = AppStatus.MOUNTED;await runLifeCycle("mounted", app);
};
export const runUnmounted = async (app: IInternalAppInfo) => {
app.status = AppStatus.UNMOUNTING;
awaitapp.unmount? .(app); app.status = AppStatus.NOT_MOUNTED;await runLifeCycle("unmounted", app);
};
const runLifeCycle = async (name: keyof ILifeCycle, app: IAppInfo) => {
const fn = lifeCycle[name];
if (fn instanceof Array) {
await Promise.all(fn.map((item) = > item(app)));
} else {
await fn?.(app);
}
};
Copy the code
The above code looks a lot, the actual implementation is very simple, to sum up is:
- Set the sub-application state for logical determination and optimization. For example, when an application is in a state of not
NOT_LOADED
Each application starts withNOT_LOADED
The next time you render the application, you don’t need to reload the resource - If you need to deal with logic, for example
beforeLoad
We need to load the child application resources - To execute the life cycle of the primary and child applications, pay attention to the execution sequence. For details, see the life cycle execution sequence of the parent and child components
Perfect route hijacking
With the application lifecycle implemented, we can now refine the “how to handle child applications” logic that was left out of the previous route-hijacking.
This logic is actually quite simple after we complete the life cycle and can be divided into the following steps:
- Determines whether the current URL is the same as the previous URL. If so, continue
- Use of course URL to match the corresponding sub-application, this is divided into several situations:
- When you start the microfront for the first time, you only need to render successfully matched child applications
- If no subapplication is switched, no subapplication needs to be processed
- Switch sub-applications. At this time, it is necessary to find out the previously rendered sub-applications for unloading, and then render the successfully matched sub-applications
- Save the current URL for the next first step
Now that we’ve figured out the steps, let’s implement it:
let lastUrl: string | null = null
export const reroute = (url: string) = > {
if(url ! == lastUrl) {const{actives, unmounts} = Matches a route and searches for a qualified subapplication// Execute the lifecycle
Promise.all(
unmounts
.map(async (app) => {
await runUnmounted(app)
})
.concat(
actives.map(async (app) => {
await runBeforeLoad(app)
await runBoostrap(app)
await runMounted(app)
})
)
).then(() = > {
// Executes a function not used in the route hijacking section
callCapturedListeners()
})
}
lastUrl = url || location.href
}
Copy the code
The above body of code is executing the lifecycle functions sequentially, but the function to match the route is not implemented because we need to consider some issues first.
You must have used routing in your daily project development, so you should know that the principle of routing matching is mainly composed of two parts:
- Nested relations
- Path to the grammar
Nesting means that if my current route is set to /vue, then something like /vue or /vue/ XXX will match the route unless we set excart (exact match).
Path syntax: path syntax: path syntax
<Route path="/hello/:name"> // Matches /hello/ Michael and /hello/ Ryan<Route path="/hello(/:name)"> /hello/ Michael and /hello/ Ryan <Route path="/files/*.*"> // match /files/hello. JPG and /files/path/to/helloCopy the code
So it seems that route matching is still quite troublesome to implement, so is there an easy way to implement this function? The answer is yes, we can read the Route library source code to find that they use path-to-regexp library inside, interested readers can read the library documentation, I have covered, we will only look at the use of one API.
With this solution in place, let’s quickly implement the following routing matching functions:
export const getAppListStatus = () = > {
// List of applications to render
const actives: IInternalAppInfo[] = []
// List of applications to be uninstalled
const unmounts: IInternalAppInfo[] = []
// Get the list of registered child applications
const list = getAppList() as IInternalAppInfo[]
list.forEach((app) = > {
// Match the route
const isActive = match(app.activeRule, { end: false })(location.pathname)
// Check the application status
switch (app.status) {
case AppStatus.NOT_LOADED:
case AppStatus.LOADING:
case AppStatus.LOADED:
case AppStatus.BOOTSTRAPPING:
case AppStatus.NOT_MOUNTED:
isActive && actives.push(app)
break
caseAppStatus.MOUNTED: ! isActive && unmounts.push(app)break}})return { actives, unmounts }
}
Copy the code
Don’t forget to call reroute to complete the route-hijacking function. The full code can be read here.
Perfect life cycle
Earlier in the implementation lifecycle, we still had an important step “loading the child application resources” to be completed, which we will take care of in this section.
To load resources, we must first need a resource entry, just like the NPM package we used, each package must have an entry file. Back to the registerMicroApps function, we initially passed the entry parameter to the function, which is the resource entry for the child application.
Resource entry is actually divided into two schemes:
- JS Entry
- HTML Entry
Both schemes are literal; the former loads all static resources through JS, while the latter loads all static resources through HTML.
JS Entry is one approach used in single-SPA. But it’s a bit restrictive, requiring the user to bundle all the files together, and unless your project isn’t performance sensitive, you can probably pass it.
HTML Entry is much better, because all websites use HTML as their Entry file. In this solution, we basically don’t need to change the packaging method, it’s almost non-invasive to the user development, we just need to find the static resources in THE HTML to load and run to render the child application, so we chose this solution.
Let’s start implementing this section.
Load resources
First we need to fetch the HTML content, here we just call the native fetch to fetch it.
// src/utils
export const fetchResource = async (url: string) = > {return await fetch(url).then(async (res) => await res.text())
}
// src/loader/index.ts
export const loadHTML = async (app: IInternalAppInfo) => {
const { container, entry } = app
const htmlFile = await fetchResource(entry)
return app
}
Copy the code
In my repository example, after we switch the route to /vue, we can print out the contents of the loaded HTML file.
<! DOCTYPEhtml>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<link rel="icon" href="/favicon.ico">
<title>sub</title>
<link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"></head>
<body>
<noscript>
<strong>We're sorry but sub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<! -- built files will be auto injected -->
<script type="text/javascript" src="/js/chunk-vendors.js"></script>
<script type="text/javascript" src="/js/app.js"></script></body>
</html>
Copy the code
We can see several static resource urls with relative paths in this file, which we then need to load. However, it is important to note that these resources can only be correctly loaded under their own BaseURL. If they are loaded under the main application’s BaseURL, a 404 error must be reported.
One more thing to note is that since we are loading the child app’s resources at the URL of the main app, this can trigger cross-domain restrictions. Therefore, it is important to pay attention to cross-domain processing in both development and production environments.
For example, if the development environment subapplication is Vue, the way to handle cross-domain:
// vue.config.js
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': The '*',,}}}Copy the code
Next, we need to process the paths of these resources first, concatenate the relative paths into the correct absolute paths, and then fetch.
// src/utils
export function getCompletionURL(src: string | null, baseURI: string) {
if(! src)return src
// If the URL is already at the beginning of the protocol, return it directly
if (/^(https|http)/.test(src)) return src
// Concatenate urls using native methods
return new URL(src, getCompletionBaseURL(baseURI)).toString()
}
// Get the complete BaseURL
// Because the user may enter // XXX or https://xxx in the application entry format
export function getCompletionBaseURL(url: string) {
return url.startsWith('/ /')?`${location.protocol}${url}` : url
}
Copy the code
I don’t need to describe the function of the above code, the annotation is very detailed, next we need to find the resources in the HTML file and fetch.
To find the resource, we need to parse the HTML content:
// src/loader/parse.ts
export const parseHTML = (parent: HTMLElement, app: IInternalAppInfo) = > {
const children = Array.from(parent.children) as HTMLElement[]
children.length && children.forEach((item) = > parseHTML(item, app))
for (const dom of children) {
if (/^(link)$/i.test(dom.tagName)) {
/ / processing link
} else if (/^(script)$/i.test(dom.tagName)) {
/ / processing script
} else if (/^(img)$/i.test(dom.tagName) && dom.hasAttribute('src')) {
// Process the image, after all, the image resource must also use the relative path 404
dom.setAttribute(
'src',
getCompletionURL(dom.getAttribute('src')!, app.entry)!
)
}
}
return{}}Copy the code
Parsing content is easy, we recursively look for elements, link, script, img elements to find out and do the corresponding processing.
First let’s see how we handle link:
// src/loader/parse.ts
// Complete parseHTML logic
if (/^(link)$/i.test(dom.tagName)) {
const data = parseLink(dom, parent, app)
data && links.push(data)
}
const parseLink = (link: HTMLElement, parent: HTMLElement, app: IInternalAppInfo) = > {
const rel = link.getAttribute('rel')
const href = link.getAttribute('href')
let comment: Comment | null
// Determine whether to obtain CSS resources
if (rel === 'stylesheet' && href) {
comment = document.createComment(`link replaced by micro`)
// @ts-ignore
comment && parent.replaceChild(comment, script)
return getCompletionURL(href, app.entry)
} else if (href) {
link.setAttribute('href', getCompletionURL(href, app.entry)!) }}Copy the code
When dealing with link tags, we only need to deal with CSS resources, other resources of preload/prefetch simply replace href.
// src/loader/parse.ts
// Complete parseHTML logic
if (/^(link)$/i.test(dom.tagName)) {
const data = parseScript(dom, parent, app)
data.text && inlineScript.push(data.text)
data.url && scripts.push(data.url)
}
const parseScript = (script: HTMLElement, parent: HTMLElement, app: IInternalAppInfo) = > {
let comment: Comment | null
const src = script.getAttribute('src')
// A SRC file is a JS file. A SRC file is an inline script
if (src) {
comment = document.createComment('script replaced by micro')}else if (script.innerHTML) {
comment = document.createComment('inline script replaced by micro')}// @ts-ignore
comment && parent.replaceChild(comment, script)
return { url: getCompletionURL(src, app.entry), text: script.innerHTML }
}
Copy the code
When dealing with script tags, we need to distinguish between JS files and in-line code, which also requires fecth to fetch the content once.
Then we return all parsed scripts, links, inlineScript in parseHTML.
Next we load the CSS and then the JS file in order:
// src/loader/index.ts
export const loadHTML = async (app: IInternalAppInfo) => {
const { container, entry } = app
const fakeContainer = document.createElement('div')
fakeContainer.innerHTML = htmlFile
const { scripts, links, inlineScript } = parseHTML(fakeContainer, app)
await Promise.all(links.map((link) = > fetchResource(link)))
const jsCode = (
await Promise.all(scripts.map((script) = > fetchResource(script)))
).concat(inlineScript)
return app
}
Copy the code
Above we have realized from loading HTML files to parsing files to find all static resources to loading CSS and JS files. But in fact our implementation is still a little rough, although the core content is implemented, but there are still some details that are not considered.
Therefore, we can also consider directly using the three-party library to realize the process of loading and parsing files. Here, we choose the import-HTml-entry library, which does the same internal things as our core, but deals with a lot of details.
If you want to use the library directly, you can modify loadHTML to look like this:
export const loadHTML = async (app: IInternalAppInfo) => {
const { container, entry } = app
// template: processed HTML content
// getExternalStyleSheets: fetch CSS file
// getExternalScripts: fetch JS file
const { template, getExternalScripts, getExternalStyleSheets } =
await importEntry(entry)
const dom = document.querySelector(container)
if(! dom) {throw new Error(Container does not exist)}// Mount HTML to the microfront-end container
dom.innerHTML = template
// Load the file
await getExternalStyleSheets()
const jsCode = await getExternalScripts()
return app
}
Copy the code
Run JS
Once we have all the JS content, it’s time to run the JS. Once we’ve done this, we can see the child application rendered on the page.
In this section, we’ll start with the easy part, which is how to run JS.
There are roughly two ways we want to execute a JS string:
eval(js string)
new Function(js string)()
Here we choose the second way to achieve:
const runJS = (value: string, app: IInternalAppInfo) = > {
const code = `
${value}
return window['${app.name}']
`
return new Function(code).call(window.window)}Copy the code
I don’t know if you remember when we registered our children we gave each child a name property, which is actually very important, and we’ll use it in the future. In addition, when setting the name of the child app, you also need to change the packaging configuration slightly, and set one of the options to the same content.
For example, if we set name: Vue to one of the sub-applications whose technology stack is Vue, then we also need to set the following in the packaging configuration:
// vue.config.js
module.exports = {
configureWebpack: {
output: {
// Same as name
library: `vue`}},}Copy the code
After configuration, we can access the contents of the application’s JS entry file export via window.vue:
As you can see in the figure above, these exported functions are the life cycle of the child application, and we need to take these functions and call them.
Finally, we call runJS in loadHTML and we’re done:
export const loadHTML = async (app: IInternalAppInfo) => {
const { container, entry } = app
const { template, getExternalScripts, getExternalStyleSheets } =
await importEntry(entry)
const dom = document.querySelector(container)
if(! dom) {throw new Error(Container does not exist)
}
dom.innerHTML = template
await getExternalStyleSheets()
const jsCode = await getExternalScripts()
jsCode.forEach((script) = > {
const lifeCycle = runJS(script, app)
if (lifeCycle) {
app.bootstrap = lifeCycle.bootstrap
app.mount = lifeCycle.mount
app.unmount = lifeCycle.unmount
}
})
return app
}
Copy the code
After completing the above steps, we should see the child application render normally!
However, this is not the end of the step, let us consider the question: ** child application changes the global variable how? ** All of our current applications can get and change the content on the Window, so global variable conflicts between applications can cause problems, so we need to fix this next.
JS sandbox
If we want to prevent the child application from directly modifying the properties of the window and also want to access the contents of the Window, then we have to make a fake window for the child application, that is, to implement a JS sandbox.
There are many ways to implement a sandbox, such as:
- The snapshot
- Proxy
In fact, this scheme is very simple to implement. In other words, it is easy to record all the contents of the current Window before mounting the child application, and then let the child application play randomly until the unmount child application restore the window before mounting. This solution is easy to implement, but the only drawback is that the performance is slow. Readers who are interested can directly see the implementation of Qiankun without Posting the code here.
Let’s talk about Proxy again, which is also the solution we choose. Many readers should have known how to use it, after all, Vue3 responsive principle is said to be rotten. If you don’t already know it, you can read the MDN documentation for yourself.
export class ProxySandbox {
proxy: any
running = false
constructor() {
// Create a false window
const fakeWindow = Object.create(null)
const proxy = new Proxy(fakeWindow, {
set: (target: any, p: string, value: any) = > {
// If the current sandbox is running, set the value directly to fakeWindow
if (this.running) {
target[p] = value
}
return true
},
get(target: any.p: string) :any {
// Prevent users from skipping classes
switch (p) {
case 'window':
case 'self':
case 'globalThis':
return proxy
}
// If the property does not exist on fakeWindow, but exists on window
// Take the value from window
if(!window.hasOwnProperty.call(target, p) &&
window.hasOwnProperty(p)
) {
// @ts-ignore
const value = window[p]
if (typeof value === 'function') return value.bind(window)
return value
}
return target[p]
},
has() {
return true}})this.proxy = proxy
}
// Activate the sandbox
active() {
this.running = true
}
// Deactivate the sandbox
inactive() {
this.running = false}}Copy the code
The above code is just a first version of the sandbox, the core idea is to create a fakeWindow, if the user set the value of the fakeWindow, so that it does not affect the global variable. If the user takes the value, it determines whether the property exists on fakeWindow or Window.
Of course, we still need to improve the actual use of the sandbox, but also need to deal with some details, here we recommend you directly read the source code of Qiankun, the code is not much, nothing more than a lot of processing boundary cases.
Note also that both snapshots and Proxy sandboxes are needed, but the former is a degraded version of the latter. After all, not all browsers support Proxy.
Finally, we need to modify the runJS code to use the sandbox:
const runJS = (value: string, app: IInternalAppInfo) = > {
if(! app.proxy) { app.proxy =new ProxySandbox()
// Hang the sandbox on the global properties
// @ts-ignore
window.__CURRENT_PROXY__ = app.proxy.proxy
}
// Activate the sandbox
app.proxy.active()
// Call JS with sandbox instead of global environment
const code = `
return (window => {
${value}
return window['${app.name}']
})(window.__CURRENT_PROXY__)
`
return new Function(code)()
}
Copy the code
At this point, we have actually completed the core functions of the entire micro front end. Because the text expression is difficult to coherent context of all the steps to improve the function, so if you are not on the reading article, or recommended to see the source of the author’s warehouse.
Now we’re going to do some improvements.
Improved function
prefetch
Our current approach is to match a child application and then load the child application, which is not efficient. We prefer that the user load up all the other child apps while browsing the current one, so that the user doesn’t have to wait to switch apps.
There is not much code to implement, and we can use our previous import-html-entry to do it immediately:
// src/start.ts
export const start = () = > {
const list = getAppList()
if(! list.length) {throw new Error('Please register the application first')
}
hijackRoute()
reroute(window.location.href)
// Prefetch is required only for children whose state is NOT_LOADED
list.forEach((app) = > {
if ((app as IInternalAppInfo).status === AppStatus.NOT_LOADED) {
prefetch(app as IInternalAppInfo)
}
})
}
// src/utils.ts
export const prefetch = async (app: IInternalAppInfo) => {
requestIdleCallback(async() = > {const { getExternalScripts, getExternalStyleSheets } = await importEntry(
app.entry
)
requestIdleCallback(getExternalStyleSheets)
requestIdleCallback(getExternalScripts)
})
}
Copy the code
There is little else to say about the code above, except to talk about the requestIdleCallback function.
* * * * window. RequestIdleCallback () method will be called function in browser free time line. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.
We use this function to perform prefetch when the browser is idle. This function is also useful in React, except that it implements a polyfill version internally. Since there are some issues with the API (50ms at best) that haven’t been resolved, but in our scenario that won’t be a problem, you can use it directly.
Resource caching mechanism
When we load resources once, users certainly do not want to load resources again when they enter the application next time, so we need to implement the caching mechanism of resources.
Since we used import-html-entry in the previous section, we have built-in caching. If you want to implement it yourself, you can refer to the internal implementation.
In simple terms, it is to make an object cache under each request of the file content, the next request to determine whether there is value in the object, if there is a direct use of the line.
Global communication and status
This part is not implemented in my code, but I can provide some ideas if you are interested in doing it yourself.
Global communication and state are actually completely an implementation of the publish-subscribe model, which should not be a problem as long as you write events by hand.
You can also read the global state implementation of Qiankun, which is only 100 lines of code.
The last
Here is the end of the article, the whole article nearly ten thousand words, read down may be many readers will have some doubts, you can choose to read a few times or combined with the author of the source code to read.
In addition, you can also ask questions in the communication area, and I will answer them in my spare time.
Author: yck
Warehouse: making
Public number: the front end is really fun
Special statement: the original is not easy, without authorization shall not be reproduced or copied, if you need to reproduce can contact the author authorized