From afc3fdaa7e0661187ddcac9c03f86b9ccdcc3ba1 Mon Sep 17 00:00:00 2001 From: bailicangdu <1264889788@qq.com> Date: Fri, 15 Oct 2021 15:37:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=8A=A8=E6=80=81mod?= =?UTF-8?q?ule=E5=88=9B=E5=BB=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-cn/advanced.md | 202 ++++++++++-------- examples/children/angular11/src/main.ts | 36 +++- .../children/react16/config/webpack.config.js | 4 - examples/children/react16/public/index.html | 3 + examples/children/react16/src/index.js | 13 +- .../react16/src/pages/inline/inline.js | 1 - .../children/react17/config/webpack.config.js | 2 +- examples/children/vue2/src/main.js | 14 +- examples/children/vue2/vue.config.js | 2 - examples/children/vue3/src/main.js | 49 ++++- .../src/pages/multiple/multiple.js | 2 - .../main-react16/src/pages/react16/react16.js | 1 - examples/main-react16/src/pages/vite/vite.js | 1 - examples/main-react16/src/pages/vue2/vue2.js | 3 - examples/main-vue2/src/pages/multiple.vue | 2 - .../main-vue3-vite/src/pages/multiple.vue | 2 - src/__tests__/micro_app.test.ts | 2 +- src/source/scripts.ts | 89 ++++---- 18 files changed, 257 insertions(+), 171 deletions(-) diff --git a/docs/zh-cn/advanced.md b/docs/zh-cn/advanced.md index 77ecbda18..f8f211b5a 100644 --- a/docs/zh-cn/advanced.md +++ b/docs/zh-cn/advanced.md @@ -147,8 +147,7 @@ microApp.start({ ## 3、内存优化 虽然我们在卸载子应用时对变量和事件进行了清除,但仍有一些变量无法回收。如果子应用渲染和卸载非常频繁,建议通过下面方式进行内存优化。 -### 将子应用修改为umd格式 -#### 步骤1:在子应用入口文件导出相应的生命周期钩子 +#### 在window上注册mount和unmount方法 @@ -156,142 +155,161 @@ microApp.start({ ```js // index.js ... -// 将渲染和卸载的操作移动到mount和unmount函数中 -// ReactDOM.render(, document.getElementById("root")) - -// window.addEventListener('unmount', function () { -// ReactDOM.unmountComponentAtNode(document.getElementById('root')) -// }) - -// 应用每次渲染时都会执行 mount 方法,在此处可以执行初始化相关操作(必传) +// 👇 将渲染操作放入 mount 函数 export function mount () { ReactDOM.render(, document.getElementById("root")) } -// 应用每次卸载时都会执行 unmount 方法,在此处可以执行卸载相关操作(必传) +// 👇 将卸载操作放入 unmount 函数 export function unmount () { - // 卸载应用 ReactDOM.unmountComponentAtNode(document.getElementById("root")); } -// 非微前端环境直接运行 -if (!window.__MICRO_APP_ENVIRONMENT__) { - mount() +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount(); } ``` -#### ** Vue ** +#### ** Vue2 ** ```js // main.js ... -// 将渲染和卸载的操作移动到mount和unmount函数中 -// const app = new Vue({ -// router, -// render: h => h(App), -// }).$mount('#app') - -// window.addEventListener('unmount', function () { -// app.$destroy() -// }) - -let app -// 应用每次渲染时都会执行 mount 方法,在此处可以执行初始化相关操作(必传) -export function mount () { - app = new Vue({ - router, - render: h => h(App), - }).$mount('#app') +let app = null +// 👇 将渲染操作放入 mount 函数 +function mount () { + app = new Vue(...).$mount('#app') } -// 应用每次卸载时都会执行 unmount 方法,在此处可以执行卸载相关操作(必传) -export function unmount () { - // 卸载应用 +// 👇 将卸载操作放入 unmount 函数 +function unmount () { app.$destroy() + app = null } -// 非微前端环境直接运行 -if (!window.__MICRO_APP_ENVIRONMENT__) { +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 mount() } ``` - -#### 步骤2:修改子应用的webpack配置 +#### ** Vue3 ** +```js +// main.js +... +let app = null +// 👇 将渲染操作放入 mount 函数 +function mount () { + app = createApp(App) + app.use(router) + app.mount('#app') +} - +// 👇 将卸载操作放入 unmount 函数 +function unmount () { + app.unmount() + app = null +} -#### ** webpack4 ** -```js -// webpack.config.js -module.exports = { - ... - output: { - library: 'micro-app-子应用的name', // 子应用的name就是中name属性的值 - libraryTarget: 'umd', - jsonpFunction: `webpackJsonp_${packageName}`, - }, +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount() } ``` -#### ** webpack5 ** +#### ** Angular ** +以`angular11`为例。 + ```js -// webpack.config.js -module.exports = { - ... - output: { - library: { - name: `micro-app-子应用的name`, // 子应用的name就是中name属性的值 - type: 'umd', - }, - }, - devServer: { - ... - // injectClient: false, 当`webpack-dev-server`版本为3.x,需设置injectClient +// main.ts +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module'; + +declare global { + interface Window { + microApp: any + __MICRO_APP_NAME__: string + __MICRO_APP_ENVIRONMENT__: string } } -``` - - -通常`library`的值固定为`micro-app-子应用的name`,但也可以自定义。 -自定义的值需要在``标签中通过`library`属性指定。 +let app = null; +// 👇 将渲染操作放入 mount 函数 +function mount () { + platformBrowserDynamic().bootstrapModule(AppModule) + .then((ngModuleRef: any) => { + app = ngModuleRef + }) + .catch(err => console.error(err)) +} - +// 👇 将卸载操作放入 unmount 函数 +function unmount () { + app?.destroy(); + app = null; +} -#### ** webpack4 ** -```js -// webpack.config.js -module.exports = { - ... - output: { - library: '自定义的library名称', 👈 - libraryTarget: 'umd', - jsonpFunction: `webpackJsonp_${packageName}`, - }, +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount(); } ``` -#### ** webpack5 ** + +#### ** Vite ** +因为vite作为子应用时关闭了沙箱,导致`__MICRO_APP_ENVIRONMENT__`和`__MICRO_APP_NAME__`两个变量失效,所以需要自行判断是否微前端环境以及手动填写应用name值。 + ```js -// webpack.config.js -module.exports = { - ... - output: { - library: { - name: `自定义的library名称`, 👈 - type: 'umd', - }, - }, +// main.js +... +let app = null +// 👇 将渲染操作放入 mount 函数 +function mount () { + app = createApp(App) + app.use(router) + app.mount('#app') +} + +// 👇 将卸载操作放入 unmount 函数 +function unmount () { + app.unmount() + app = null +} + +// 微前端环境下,注册mount和unmount方法 +if (我在微前端环境) { + // 应用的name值,即 元素的name属性值 + window[`micro-app-${应用的name值}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount() } ``` +通常注册的形式为`window['micro-app-${window.__MICRO_APP_NAME__}'] = {...}`,但也支持自定义名称,`window['自定义的名称'] = {...}` + +自定义的值需要在``标签中通过`library`属性指定。 + ```html - ``` + +在沙箱关闭时`__MICRO_APP_NAME__`变量失效(如:vite子应用),此时可以使用自定义名称的方式进行注册,也可以通过 `window['micro-app-${应用的name值}']`的方式注册。 diff --git a/examples/children/angular11/src/main.ts b/examples/children/angular11/src/main.ts index c7b673cf4..bdc10cf6e 100644 --- a/examples/children/angular11/src/main.ts +++ b/examples/children/angular11/src/main.ts @@ -8,5 +8,37 @@ if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); +declare global { + interface Window { + microApp: any + __MICRO_APP_NAME__: string + __MICRO_APP_ENVIRONMENT__: string + } +} + +let app = null; +// 将渲染操作放入 mount 函数 +function mount () { + platformBrowserDynamic().bootstrapModule(AppModule) + .then((ngModuleRef: any) => { + app = ngModuleRef + }) + .catch(err => console.error(err)) + + console.log('微应用child-angular11渲染了'); +} + +// 将卸载操作放入 unmount 函数 +function unmount () { + app?.destroy(); + app = null; + console.log('微应用child-angular11卸载了'); +} + +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount(); +} diff --git a/examples/children/react16/config/webpack.config.js b/examples/children/react16/config/webpack.config.js index c13129e2a..07f4beef2 100644 --- a/examples/children/react16/config/webpack.config.js +++ b/examples/children/react16/config/webpack.config.js @@ -227,10 +227,6 @@ module.exports = function (webpackEnv) { // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. // globalObject: 'this', // micro-app注释 - - library: `micro-app-react16`, - libraryTarget: 'umd', - jsonpFunction: `webpackJsonp_${appPackageJson.name}`, }, optimization: { minimize: isEnvProduction, diff --git a/examples/children/react16/public/index.html b/examples/children/react16/public/index.html index 19b57017d..4a40ec524 100644 --- a/examples/children/react16/public/index.html +++ b/examples/children/react16/public/index.html @@ -16,6 +16,9 @@ color: red; } + diff --git a/examples/children/react16/src/index.js b/examples/children/react16/src/index.js index e05592a2b..aa43a4059 100644 --- a/examples/children/react16/src/index.js +++ b/examples/children/react16/src/index.js @@ -58,7 +58,7 @@ window.microApp?.addGlobalDataListener(handleGlobalData); // }) -export function mount () { +function mount () { ReactDOM.render( @@ -68,15 +68,18 @@ export function mount () { console.timeEnd("react16"); } -export function unmount () { +function unmount () { console.log("微应用react16卸载了 -- 来自umd-unmount"); // 卸载应用 ReactDOM.unmountComponentAtNode(document.getElementById("root")); } -// 非微前端环境直接运行 -if (!window.__MICRO_APP_ENVIRONMENT__) { - mount() +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount(); } // document.addEventListener('click', function () { diff --git a/examples/children/react16/src/pages/inline/inline.js b/examples/children/react16/src/pages/inline/inline.js index 3b927b0b9..b66a3000f 100644 --- a/examples/children/react16/src/pages/inline/inline.js +++ b/examples/children/react16/src/pages/inline/inline.js @@ -36,7 +36,6 @@ function Vue2 () { // inline // disableScopecss // disableSandbox - library='micro-app-vue2' /> ) diff --git a/examples/children/react17/config/webpack.config.js b/examples/children/react17/config/webpack.config.js index 3e53bd871..9c0cc6e40 100644 --- a/examples/children/react17/config/webpack.config.js +++ b/examples/children/react17/config/webpack.config.js @@ -225,7 +225,7 @@ module.exports = function (webpackEnv) { // jsonpFunction: `webpackJsonp${appPackageJson.name}`, // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. - // globalObject: 'this', // micro-app 注释 + // globalObject: 'window', // micro-app 注释 }, optimization: { minimize: isEnvProduction, diff --git a/examples/children/vue2/src/main.js b/examples/children/vue2/src/main.js index 3a0558693..3275643ec 100644 --- a/examples/children/vue2/src/main.js +++ b/examples/children/vue2/src/main.js @@ -18,7 +18,7 @@ const router = new VueRouter({ routes, }) -let app +let app = null // app = new Vue({ // router, @@ -32,7 +32,7 @@ let app // app.$destroy() // }) -export function mount () { +function mount () { console.log("微应用vue2渲染了 -- 来自umd-mount"); app = new Vue({ router, @@ -41,13 +41,17 @@ export function mount () { console.timeEnd('vue2') } -export function unmount () { +function unmount () { console.log("微应用vue2卸载了 -- 来自umd-unmount"); // 卸载应用 app.$destroy() + app = null } -// 非微前端环境直接运行 -if (!window.__MICRO_APP_ENVIRONMENT__) { +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 mount() } diff --git a/examples/children/vue2/vue.config.js b/examples/children/vue2/vue.config.js index 85f4ea624..f1d6d5fb5 100644 --- a/examples/children/vue2/vue.config.js +++ b/examples/children/vue2/vue.config.js @@ -20,8 +20,6 @@ module.exports = { configureWebpack: { output: { jsonpFunction: `webpackJsonp-chile-vue2`, - library: `micro-app-vue2`, - libraryTarget: 'umd', } }, } diff --git a/examples/children/vue3/src/main.js b/examples/children/vue3/src/main.js index 729727433..12f4851d7 100644 --- a/examples/children/vue3/src/main.js +++ b/examples/children/vue3/src/main.js @@ -2,19 +2,46 @@ import './public-path' import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/lib/theme-chalk/index.css' -import routes from './router' +import router from './router' import App from './App.vue' -const app = createApp(App) -app.use(ElementPlus) -app.use(routes) -app.mount('#app') +// const app = createApp(App) +// app.use(ElementPlus) +// app.use(router) +// app.mount('#app') -console.log('微应用vue3渲染了') +// console.log('微应用vue3渲染了') -// 监听卸载 -window.addEventListener('unmount', function () { - console.log('微应用vue3卸载了') - // 卸载应用 +// // 监听卸载 +// window.addEventListener('unmount', function () { +// console.log('微应用vue3卸载了') +// // 卸载应用 +// app.unmount() +// }) + +let app = null +// 将渲染操作放入 mount 函数 +function mount () { + app = createApp(App) + app.use(ElementPlus) + app.use(router) + app.mount('#app') + + console.log('微应用child-vue3渲染了') +} + +// 将卸载操作放入 unmount 函数 +function unmount () { app.unmount() -}) + app = null + console.log('微应用child-vue3卸载了') +} + +// 微前端环境下,注册mount和unmount方法 +if (window.__MICRO_APP_ENVIRONMENT__) { + // @ts-ignore + window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } +} else { + // 非微前端环境直接渲染 + mount() +} diff --git a/examples/main-react16/src/pages/multiple/multiple.js b/examples/main-react16/src/pages/multiple/multiple.js index fce572ddc..1d9d8cbcc 100644 --- a/examples/main-react16/src/pages/multiple/multiple.js +++ b/examples/main-react16/src/pages/multiple/multiple.js @@ -47,7 +47,6 @@ function Vue3 () { // destory // inline // scopecss='false' - library='micro-app-react16' > diff --git a/examples/main-react16/src/pages/react16/react16.js b/examples/main-react16/src/pages/react16/react16.js index f26673346..0eb916b27 100644 --- a/examples/main-react16/src/pages/react16/react16.js +++ b/examples/main-react16/src/pages/react16/react16.js @@ -171,7 +171,6 @@ export default class App extends React.Component { name='modal-app1' url={this.state.url} baseRoute='/micro-app/demo/react16' - library='micro-app-react16' // disableSandbox // macro /> diff --git a/examples/main-react16/src/pages/vite/vite.js b/examples/main-react16/src/pages/vite/vite.js index bfd4e1709..c25e9d0ae 100644 --- a/examples/main-react16/src/pages/vite/vite.js +++ b/examples/main-react16/src/pages/vite/vite.js @@ -36,7 +36,6 @@ function vite () { // destory inline disableSandbox - > diff --git a/examples/main-react16/src/pages/vue2/vue2.js b/examples/main-react16/src/pages/vue2/vue2.js index 4d09aac66..f0202319b 100644 --- a/examples/main-react16/src/pages/vue2/vue2.js +++ b/examples/main-react16/src/pages/vue2/vue2.js @@ -47,6 +47,3 @@ function Vue2 () { } export default Vue2 -// 664 649 637 663 656 650 676 平均 656 -// 751 660 683 706 695 687 689 平均 695 -// 685 671 695 663 695 720 709 平均 691 diff --git a/examples/main-vue2/src/pages/multiple.vue b/examples/main-vue2/src/pages/multiple.vue index 1fb75c7b4..3768e2889 100644 --- a/examples/main-vue2/src/pages/multiple.vue +++ b/examples/main-vue2/src/pages/multiple.vue @@ -8,7 +8,6 @@ url='http://localhost:3001/micro-app/react16/' baseRoute='/multiple' :data='data' - library='micro-app-react16' > diff --git a/examples/main-vue3-vite/src/pages/multiple.vue b/examples/main-vue3-vite/src/pages/multiple.vue index bbb0b5ab2..c4ca3ad6d 100644 --- a/examples/main-vue3-vite/src/pages/multiple.vue +++ b/examples/main-vue3-vite/src/pages/multiple.vue @@ -8,7 +8,6 @@ url='http://localhost:3001/micro-app/react16/' baseRoute='/multiple' :data='data' - library='micro-app-react16' > diff --git a/src/__tests__/micro_app.test.ts b/src/__tests__/micro_app.test.ts index 5fbf4486a..669b2d64d 100644 --- a/src/__tests__/micro_app.test.ts +++ b/src/__tests__/micro_app.test.ts @@ -33,7 +33,7 @@ test('log error message if customElements is not supported in this environment', }) microApp.start() - expect(console.error).toBeCalledWith('[micro-app] customElements is not supported in this environment') + expect(console.error).toBeCalledWith('[micro-app] micro-app is not supported in this environment') window.customElements = rawcustomElements }) diff --git a/src/source/scripts.ts b/src/source/scripts.ts index 63d051748..4f9c9b5e6 100644 --- a/src/source/scripts.ts +++ b/src/source/scripts.ts @@ -198,6 +198,39 @@ export function execScripts ( } } +/** + * run code + * @param url script address + * @param code js code + * @param app app + * @param module type='module' of script + * @param isDynamic dynamically created script + * @param callback callback from execScripts for first exec + */ +export function runScript ( + url: string, + code: string, + app: AppInterface, + module: boolean, + isDynamic: boolean, + callback?: Func, +): any { + try { + code = bindScope(url, code, app) + if (app.inline) { + const scriptElement = pureCreateElement('script') + setInlinScriptContent(url, code, module, scriptElement, callback) + if (isDynamic) return scriptElement + app.container?.querySelector('micro-app-body')!.appendChild(scriptElement) + } else { + Function(code)() + if (isDynamic) return document.createComment('dynamic script extract by micro-app') + } + } catch (e) { + console.error('[micro-app from runScript]', e) + } +} + /** * Get dynamically created remote script * @param url script address @@ -232,17 +265,16 @@ export function runDynamicScript ( replaceElement = document.createComment(`dynamic script with src='${url}' extract by micro-app`) } - fetchSource(url, app.name).then((data: string) => { - info.code = data + fetchSource(url, app.name).then((code: string) => { + info.code = code app.source.scripts.set(url, info) - if (info.isGlobal) globalScripts.set(url, data) + if (info.isGlobal) globalScripts.set(url, code) try { - data = bindScope(url, data, app) + code = bindScope(url, code, app) if (app.inline) { - if (info.module) (replaceElement as HTMLScriptElement).setAttribute('type', 'module') - replaceElement.textContent = data + setInlinScriptContent(url, code, info.module, replaceElement as HTMLScriptElement) } else { - Function(data)() + Function(code)() } } catch (e) { console.error('[micro-app from runDynamicScript]', e, url) @@ -257,44 +289,29 @@ export function runDynamicScript ( } /** - * run code + * common handle for inline script * @param url script address * @param code js code - * @param app app * @param module type='module' of script - * @param isDynamic dynamically created script + * @param scriptElement target script element * @param callback callback from execScripts for first exec */ -export function runScript ( +function setInlinScriptContent ( url: string, code: string, - app: AppInterface, module: boolean, - isDynamic: boolean, + scriptElement: HTMLScriptElement, callback?: Func, -): any { - try { - code = bindScope(url, code, app) - if (app.inline) { - const script = pureCreateElement('script') - if (module) { - // module script is async, transform it to a blob for subsequent operations - const blob = new Blob([code], { type: 'text/javascript;charset=utf-8' }) - script.src = URL.createObjectURL(blob) - script.setAttribute('type', 'module') - script.setAttribute('originSrc', url) - callback && (script.onload = callback) - } else { - script.textContent = code - } - if (isDynamic) return script - app.container?.querySelector('micro-app-body')!.appendChild(script) - } else { - Function(code)() - if (isDynamic) return document.createComment('dynamic script extract by micro-app') - } - } catch (e) { - console.error('[micro-app from runScript]', e) +): void { + if (module) { + // module script is async, transform it to a blob for subsequent operations + const blob = new Blob([code], { type: 'text/javascript;charset=utf-8' }) + scriptElement.src = URL.createObjectURL(blob) + scriptElement.setAttribute('type', 'module') + scriptElement.setAttribute('originSrc', url) + callback && (scriptElement.onload = callback) + } else { + scriptElement.textContent = code } }