一、微前端之实践环境变量设置、快照沙箱隔离、代理沙箱隔离、css 样式隔离、父子应用间通信和子应用间通信
- 微前端环境变量设置,如下所示:
- 在
micro
下的 sandbox
中 performScript.js
,performScriptForFunction
是执行 js
脚本,performScriptForEval
是获取到子应用的内容,代码如下:
export const performScriptForFunction = (script, appName, global) => {
window.proxy = global;
console.log(global);
const scriptText = `
return ((window) => {
${script}
return window['${appName}']
})(window.proxy)
`
return new Function(scriptText)()
}
export const performScriptForEval = (script, appName, global) => {
window.proxy = global;
const scriptText = `
((window) => {
${script}
return window['${appName}']
})(window.proxy)
`
return eval(scriptText)
- 在
micro
下的 sandbox
中 index.js
,isCheckLifeCycle
是检验是否存在生命周期,sandBox
是子应用生命周期处理, 环境变量设置。通过 new
ProxySandbox
创建代理对象,通过 window.__MICRO_WEB__
设置环境变量,通过 performScriptForEval
运行 js
文件,通过 isCheckLifeCycle(lifecycle)
判断生命周期,有则挂载到 app
上,index.js
,代码如下:
import { performScriptForEval } from './performScript'
import { ProxySandbox } from './proxySandbox'
const isCheckLifeCycle = lifecycle => lifecycle &&
lifecycle.bootstrap &&
lifecycle.mount &&
lifecycle.unmount
export const sandBox = (app, script) => {
const proxy = new ProxySandbox();
if (!app.proxy) {
app.proxy = proxy;
}
window.__MICRO_WEB__ = true;
const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy);
if (isCheckLifeCycle(lifecycle)) {
app.bootstrap = lifecycle.bootstrap
app.mount = lifecycle.mount
app.unmount = lifecycle.unmount
}
}
- 运行环境隔离,快照沙箱,如下所示:
- 快照沙箱,给当前的全局变量实现快照的方式来记录沙箱的内容,在子应用切换后,将所有的沙箱变量置为初始值。快照沙箱是不支持多实例的,一个页面中只能存在一个子应用,应用场景是比较老版本的浏览器。
- 在
micro
下的 sandbox
中 snapShotSandbox.js
,创建 SnapShotSandbox
类,把 window
挂载到代理对象,active
是沙箱激活,对整个 window
做了一层快照,通过 new Map
创建 一个沙箱快照 snapshot
,遍历全局环境。 inactive
是沙箱销毁,如果 window
里面的值和快照 snapshot
里面的值不一致,进行还原操作,snapShotSandbox.js
,代码如下:
export class SnapShotSandbox {
constructor() {
this.proxy = window;
this.active();
}
active() {
this.snapshot = new Map();
for(const key in window) {
this.snapshot[key] = window[key];
}
}
inactive () {
for (const key in window) {
if (window[key] !== this.snapshot[key]) {
window[key] = this.snapshot[key];
}
}
}
}
- 运行环境隔离,代理沙箱,如下所示:
- 代理沙箱,使用
proxy
实现沙箱的功能,进行拦截
- 在
micro
下的 sandbox
中 proxySandbox.js
,defaultValue
是子应用的沙箱容器,创建 ProxySandbox
类,active
是沙箱激活,子应用需要设置属性,属性设置在容器中,这个容器暂存子应用的所有值。通过 new Proxy
代理 window
对象,拦截它的 get
和 set
方法,进行子应用的沙箱容器处理,inactive
是沙箱销毁,proxySandbox.js
,代码如下:
let defaultValue = {};
export class ProxySandbox{
constructor() {
this.proxy = null;
this.active();
}
active() {
this.proxy = new Proxy(window, {
get(target, key) {
if (typeof target[key] === 'function') {
return target[key].bind(target);
}
return defaultValue[key] || target[key];
},
set(target, key, value) {
defaultValue[key] = value;
return true;
}
})
}
inactive () {
defaultValue = {};
}
}
css
样式隔离,如下所示:
- 对于
css
样式隔离,可以使用 css modules、shadow dom、minicss、 css-in-js
等方案
- 在
webpack.config.js
中,可以使用 mini-css-extract-plugin
这个插件。它可以将所有的 css
打包成单独的 css
文件,在渲染子应用的时候,通过 link
这种标签来引入 css
文件。对于一个子应用来说,一个文件的引入是放在子应用容器内部来引入。如果切换子应用,相当于是把子应用容器内部的东西全部清空,对于 css
文件的引入也是清空的,之后也不会存在两个自应用样式相互影响的问题。对于每一个子应用来说,都有使用了这样的插件处理,webpack.config.js
,代码如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: {
path: ['./index.js']
},
module: {
rules: [
{
test: /\.js(|x)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.(c|sc)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: {
loader: 'url-loader',
}
}
]
},
optimization: {
splitChunks: false,
minimize: false
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].css'
})
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'react15.js',
library: 'react15',
libraryTarget: 'umd',
umdNamedDefine: true,
publicPath: 'http://localhost:9002/'
},
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' },
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9002,
historyApiFallback: true,
hot: true,
}
}
- 应用间通信,父子通信,如下所示:
- 对于父子应用间通信,可以通过
props、customevent
等方式。依赖注入,主应用的显示隐藏,注入到子应用内部,通过子应用内部的方法进行调用,customevent
也是符合这样的方式
- 在
micro
下的 customevent
中 index.js
,建立 Custom
类,通过 on
进行事件监听,以 window.addEventListener
方式。通过 emit
进行事件触发,以 window.dispatchEvent
方式,index.js
,代码如下:
export class Custom {
on (name, cb) {
window.addEventListener(name, (e) => {
cb(e.detail)
})
}
emit(name, data) {
const event = new CustomEvent(name, {
detail: data
})
window.dispatchEvent(event);
}
}
- 在
micro
下的 start.js
中,引入 Custom
,通过实例化得到 Custom
实例,通过 custom.on
监听 test
事件,将 custom
挂载到 window
上,代码如下:
import { Custom } from './customevent'
const custom = new Custom();
custom.on('test', (data) => {
console.log(data);
})
window.custom = custom;
- 在子应用
vue3
项目中,在 main.js
中,在 mount
生命周期中,通过 window.custom.emit
去触发 test
事件,向主应用通信,发送消息传递值,主应用也会监听到子应用发的这个消息,代码如下:
export async function mount(app) {
setMain(app);
window.custom.emit('test1', {a: 1});
render();
}
- 应用间通信,子应用间通信,如下所示:
- 对于不同子应用间通信,也可以通过
props、customevent
等方式。如果使用 props
,子应用1 传递给父应用做转发,给子应用2。如果使用 customevent
,和上面的父子应用间通信类似
- 在
vue3
子应用项目中的 main.js
,先有监听,再有触发。在 mount
生命周期中,监听到 test1
事件后,在回调函数中触发 test2
事件,代码如下:
export async function mount(app) {
setMain(app);
window.custom.on('test1', () => {
window.custom.emit('test2', { b: 2 });
});
render();
- 在
vue2
子应用项目中的 main.js
,在 mount
生命周期中,通过 window.custom.emit
触发 test1
事件,给 vue3
子应用通信,代码如下:
export async function mount() {
window.custom.emit('test1', {a: 1});
render();
}