今天是12月31号,算上掘金推送的时差,就提早跨年了,祝你们元旦快乐,新的一年,我不祝你一路顺风,我祝你乘风破浪。javascript
技术和架构方案不一样,技术能够凭空出现忽然爆火没有征兆。但方案或架构必定是为了解决某个问题而出现的,实践以前,请务必先要去搞清楚它是否能够解决当前问题,再者调研是否适合团队,考虑工程价值与产品价值,请不要盲目追求。css
原文地址html
熟悉它的人更喜欢称它为前端微服务。前端
“微前端是一种架构风格,其中众多独立交付的前端应用组合成一个大型总体。”vue
在传统模式开发中,例如阿里云、腾讯云的控制台。维护一个大型的中后台而且快速迭代是一件很困难的事情,由于它们广泛都有下面几个问题。java
有没有一种方案可以解决这些问题?node
借鉴服务端微服务的设计思想,前端微服务化就出现了。它虽然解决不了所有,但能尽小减轻负担和风险。它的实现更像是将整个项目变成一个“组件”,平台能够自由的组装这些组件。简而言之,单一的单体应用转变为多个小型前端应用聚合为一的应用。react
微服务化以前 jquery
模块复杂度可控,团队独立自治webpack
每一个模块(微服务)由一个开发团队彻底掌控,易于管理和维护,快速整合业务。虽然可能会让各个团队的工做越发分裂,可是只要控制在合理水平上仍是利大于弊的。
独立开发与部署,子仓库独立
就像微服务同样,每一个模块都具有独立运行的能力,这也表明能够独立部署。经过逐渐缩减每次部署的覆盖面下降风险。
更具扩展能力,增量升级
年份陈旧的大型前端应用的技术栈掌握的技术人员大多不在岗位上,到了重写整个前端应用的时候一次性重写整个应用风险太大,可以以增量式的风格来重写、升级、迭代,一点点换掉老的应用,同时在不受单体架构拖累的前提下为客户提供新功能。并且理论上来讲能够支持大型单页应用无限拓展。虽然不具有SPA应用自然的优点,可是也摆脱了强耦合的应用技术栈。
技术栈无关,创新自主
主框架不限制接入应用的技术栈,若是咱们想尝试新的技术或者是基于性能上有更好的实现,彻底具有自主权。
现有的微前端方案有:
答案很明显:准备祖传的项目。
单个团队没有理由采用微前端,还有须要快速开发的应用或者粒度较小的小型应用也不适用。
但也面临一些问题和挑战。
如何在一个页面里渲染多种技术栈。
Single-SPA 它能够帮助咱们在同一个页面使用多种框架((React、Vue、AngularJS、svelte、Ember等多个框架)。而且每一个独立模块的代码能够作到按需加载、独立运行,其工做机制是命中到prefix时激活相应入口应用。
使用 registerApplication
注册应用,签名
appName: string
应用程序名称
applicationOrLoadingFn: () => <Function | Promise>
必须是一个加载函数,要么返回已加载的应用,要么返回一个Promise。
activityFn: (location) => boolean
必须是纯函数。这个函数使用 window.location 做为第一个参数,当应用处于激活状态时返回状态对应的值。
customProps?: Object = {}
props 将在每一个生命周期方法期间传递给应用。
复制代码
最后经过 singleSpa.start()
启动。
import * as singleSpa from 'single-spa';
const appName = 'reactapp';
// 加载 React 应用入口文件
const loadingFunction = () => import('./react/app.js');
// 当前路由为/reactapp时为true
const activityFunction = location => location.pathname.startsWith('/reactapp');
// 注册应用
singleSpa.registerApplication(appName, loadingFunction, activityFunction ,{ token: 'xxx'});
// 启动single-spa
singleSpa.start();
复制代码
single-spa内置了四个生命周期 Hook,分别是bootstrap
, mount
, unmount
, unload
,每一个生命周期必须返回 Promise 或者是 asyncFunction.
// app1.js
let domEl;
const gen = () => Promise.resolve();
export function bootstrap(props) {
return gen().then(() => {
// 首次安装时会被调用一次,也就是路由命中的时候
domEl = document.createElement('div');
domEl.id = 'app1';
document.body.appendChild(domEl);
console.log('app1 is bootstrapped!')
});
}
export function unload(props) {
return gen().then(() => {
// 卸载注册应用,能够理解为删除,只有主动调用 unloadApplication 才会触发,相对应的是bootstrap
console.log('app1 is unloaded!');
});
}
export function mount(props) {
return gen().then(() => {
// mounted Component
domEl.textContent = 'App1.js mounted'
console.log('app1 is mounted!')
});
}
export function unmount(props) {
return gen().then(() => {
// unmounted Component
domEl.textContent = '';
console.log('app1 is unmounted!')
})
}
复制代码
这样一个简单的应用就完成了。光说不练假把式,从无到有写一个支持react
, angular
, vue
, svelte
的demo。
先定义HTML结构
<div class="micro-container">
<div class="navbar">
<ul>
<a onclick="singleSpaNavigate('/react')">
<li>React App</li>
</a>
<a onclick="singleSpaNavigate('/vue')">
<li>Vue App</li>
</a>
<a onclick="singleSpaNavigate('/svelte')">
<li>Svelte App</li>
</a>
<a onclick="singleSpaNavigate('/angular')">
<li>Angular App</li>
</a>
</ul>
</div>
<div id="container">
<div id="react-app"></div>
<div id="vue-app"></div>
<div id="angular-app"></div>
<div id="svelte-app"></div>
</div>
</div>
复制代码
上面的div.micro-container
称为容器应用。每一个页面除了包含一个容器应用外,还有可能包含多个micro-frontend
。singleSpaNavigate
方法是single-spa
内置的导航Api,能够在已注册的application之间执行 url Navigation ,并且无需处理 event.preventDefault
pushState
方法等。而后再定义 entry,如下是伪结构。
├─.babelrc
├─assets
│ └─styles
├─index.html
├─package.json
├─src
│ ├─angular
│ │ ├─app.js
│ │ ├─root.component.ts
│ │ ├─components
│ │ └─routes
│ ├─baseApplication
│ │ └─index.js // register Application
│ ├─react
│ │ ├─app.js
│ │ ├─components
│ │ ├─root.component.js
│ │ └─routes
│ ├─svelte
│ │ ├─app.js
│ │ ├─components
│ │ ├─root.component.svelte
│ │ └─routes
│ └─vue
│ ├─app.js
│ ├─components
│ ├─root.component.vue
│ └─routes
├─tsconfig.json
└─webpack.config.js
复制代码
// src/baseApplication/index.js
import * as singleSpa from 'single-spa';
singleSpa.registerApplication('react', () => import ('../react/app.js'), pathPrefix('/react'));
singleSpa.registerApplication('vue', () => import ('../vue/app.js'), pathPrefix('/vue'));
singleSpa.registerApplication('angular', () => import ('../angular/app.js'), pathPrefix('/angular'));
singleSpa.registerApplication('svelte', () => import ('../svelte/app.js'), pathPrefix('/svelte'));
singleSpa.start();
function pathPrefix(prefix) {
return function(location) {
return location.pathname.startsWith(`${prefix}`);
}
}
复制代码
以React 和 Vue 为例,当应用被import后,抛出的 boostrap
和 mount
及 unmount
会被执行,
// src/vue/app.js
import Vue from 'vue/dist/vue.min.js';
import singleSpaVue from 'single-spa-vue';
import router from './router';
import Loading from './Loading';
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
router,
el:'#vue-app',
template: ` <div id="vue-app"> <router-view></router-view> </div> `,
loadRootComponent: Loading
},
});
export const bootstrap = (props) => {
console.log('vue-app is bootstrap')
return vueLifecycles.bootstrap(props);
}
export const mount = (props) => {
console.log('vue-app is Mounted')
return vueLifecycles.mount(props);
}
export const unmount = (props) => {
console.log('vue-app is unMounted')
return vueLifecycles.unmount(props);
}
复制代码
bootstrap
和 mount
这些钩子就不凑字数了,自行补上...
// src/react/app.js
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import Root from '@React/root.component.js';
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
domElementGetter: () => document.getElementById('react-app') // 节点getter
});
// ...other
复制代码
// src/svelte/app.js
import singleSpaSvelte from 'single-spa-svelte';
import AppComponent from './root.component.svelte';
const svelteLifecycles = singleSpaSvelte({
component: AppComponent,
domElementGetter: () => document.getElementById('svelte-app'),
});
// ...other
复制代码
single-spa已经提供了大部分主流框架的对接工具库,内部对其作了适应工做,将 entry baseApplication
和 common-dependencies
注入到 html,若是只须要单一版本的话则把它放在公共依赖。
// webpack.config.js
entry: {
'baseApplication': 'src/baseApplication/index.js',
'common-dependencies': [
'core-js/client/shim.min.js',
'@angular/common',
'@angular/compiler',
'@angular/core',
'@angular/platform-browser-dynamic',
'@angular/router',
'reflect-metadata',
'react',
'react-dom',
'react-router',
'react-router-dom',
"vue",
"vue-router",
"svelte",
"svelte-routing"
],
},
plugins:[
new HTMLWebpackPlugin({
template: path.resolve('index.html'),
inject: true,
chunksSortMode: 'none'
}),
]
复制代码
源码已经上传到github
借助 single-spa
提供的 Events 钩子,能够实现子应用的 LiftCycle Hooks。从而在子应用 boostrap与 unmount 进行全局变量冻结之类的事情避免变量污染。
window.addEventListener('single-spa:before-routing-event',evt => {
'route Event事件发生以前(hashchange,popstate或triggerAppChange以后都会触发)';
});
'single-spa:routing-event' => 'route Event事件后触发'
'single-spa:app-change' => 'app change'
'single-spa:no-app-change' => '与app-change相反,app nochange 时触发'
'single-spa:before-first-mount' => '挂载第一个应用前'
'single-spa:first-mount' => '挂载第一个应用后'
复制代码
建立一个 setDefaultMountedApp
方法,其功能为指定默认挂载的 App。
function setDefaultMountedApp(path) {
window.addEventListener(`single-spa:no-app-change`, () => {
const activedApps = getMountedApps()
if (activedApps.length === 0) {
navigateToUrl(path)
}
}, {
once: true
})
}
复制代码
这里因为使用了single-spa从而避免了刷新页面形成的子应用404问题。咱们成功从应用分发路由到路由分发应用,彷佛是达到想要的效果,以前的问题真的解决了吗?
打包结果
目前加载的是 /react
,却依赖了整个公共依赖包,随着业务复杂,项目中组件库与其余库迅速发生“滚雪球效应”,依赖包体积的增大表明 FCP(First Contentful Paint) 也随之变长,即使在一个页面内实现渲染了多种技术栈,其根本意义仍是属于大型总体应用、解耦性差、不能独立部署,未对各应用进行隔离,一旦某个应用崩溃仍然会引起总体应用崩溃。因此问题仍是存在着,只是以另外一种形式体现,
这种方式被称为构建时集成,它一般会生成一个可部署的 Javascript 包,虽然咱们能够从各类应用中删除重复依赖。但这意味着咱们修改 app 的任何功能时都必须从新编译和发布全部微前端。这种齐步走的发布流程在微服务里已经够让咱们好受了,因此强烈建议不要用它来实现微前端架构。好不容易实现了解耦和独立,别在发布阶段又绕回去。
问题回到本质上,咱们的目的就将应用分离解耦,集成部署的同时也支持独立运行、独立部署,咱们得在运行时中也集成微前端。
除了使用原生JavaScript,运行时集成一般三种方式实现:
<iframe id="micro-frontend-container"></iframe>
<script type="text/javascript"> const microFrontendsByRoute = { '/': 'https://browse.example.com/index.html', '/order-food': 'https://order.example.com/index.html', '/user-profile': 'https://profile.example.com/index.html', }; const iframe = document.getElementById('micro-frontend-container'); iframe.src = microFrontendsByRoute[window.location.pathname]; </script>
复制代码
简单、粗暴,天生自带沙盒,适用于三方业务引入。
SEO差;页面响应速度慢;灵活性差;路由深层链接复杂;使用postMessage
进行消息通讯侵入性太强;双滚动条;iframe内部的DOM获取页面高度;遮罩没法覆盖外部;刷新回到iframe首页等问题。这种先甜后苦后人背锅的事情咱们可作不来,强烈不推荐。
web Component 由四个部分组成,
这里有个简单的Demo
目前React、Preact、Vue、Angular 对 Web component 都有支持,例如
class SearchBar extends HTMLElement {
constructor() {
super();
this.mountPoint = document.createElement('div');
this.attachShadow({ mode: 'open' }).appendChild(this.mountPoint); // 指定 open 模式
}
connectedCallback() {
const initialValue = this.getAttribute('initialValue') || '';
ReactDOM.render(<input value={initialValue} placeholder="Search..." />, this.mountPoint);
}
disconnectedCallback() {
console.log(`${this.nodeName} is Remove`);
}
}
customElements.define('search-bar', SearchBar);
// index.html
<search-bar defaultValue="field" />
复制代码
connectedCallback
在被插入到DOM时执行,其时机至关于 React.componentDidMount。与之对应的是disconnectedCallback
——React.componentWillUnMount.
这样咱们就能够经过建立 app 自定义应用组件,根据路由动态插入。
<script src="https://base.xxx.com/bundle.js"></script>
<script src="https://order.xxx.com/bundle.js"></script>
<script src="https://profile.xxx.com/bundle.js"></script>
<div id="root-contariner"></div>
<script type="text/javascript"> const webComponentsByRoute = { '/': 'base-dashboard', '/order-food': 'order-food', '/user-profile': 'user-profile', }; const webComponentType = webComponentsByRoute[window.location.pathname]; const root = document.getElementById('root-contariner'); const webComponent = document.createElement(webComponentType); root.appendChild(webComponent); </script>
复制代码
知足全部需求
SystemJs 是一个模块加载器,支持AMD、CommonJS、ES6等各类格式的JS模块动态加载。搭配 single-spa 再好不过。
首先将各个子应用抽离出来,概览结构以下:
├─cra-ts-app
│ ├─config
│ ├─images.d.ts
│ ├─package.json
│ ├─public
│ ├─scripts
│ ├─src
│ │ ├─index.css
│ │ ├─index.tsx
│ │ └─registerServiceWorker.ts
│ ├─tsconfig.json
│ ├─tsconfig.prod.json
│ ├─tsconfig.test.json
│ ├─tslint.json
├─nav // 导航栏
│ ├─package.json
│ ├─src
│ │ ├─app.js
│ │ └─root.component.js
│ └─webpack.config.js
├─package.json
├─portal // 入口
│ ├─index.html
│ ├─index.js
│ ├─package.json
│ └─webpack.config.js
├─react
│ ├─package.json
│ ├─src
│ │ ├─app.js
│ │ ├─main.js
│ │ ├─root.component.js
│ │ ├─routes
│ └─webpack.config.js
├─rts
│ ├─build
│ ├─package.json
│ ├─postcss.config.js
│ ├─public
│ ├─src
│ │ ├─app.tsx
│ │ ├─index.tsx
│ │ └─views
│ ├─tsconfig.json
│ └─types
├─svelte
│ ├─package.json
│ ├─src
│ │ ├─app.js
│ │ ├─root.component.svelte
│ │ └─routes
│ └─webpack.config.js
├─vts
│ ├─babel.config.js
│ ├─package.json
│ ├─public
│ ├─src
│ │ ├─App.vue
│ │ ├─assets
│ │ ├─components
│ │ ├─main.ts
│ │ ├─registerServiceWorker.ts
│ │ ├─router
│ │ ├─shims-tsx.d.ts
│ │ ├─shims-vue.d.ts
│ │ ├─store
│ │ └─views
│ ├─tsconfig.json
└─vue
├─package.json
├─src
│ ├─app.js
│ ├─app.vue
│ ├─components
│ ├─main.js
│ ├─root.component.vue
│ ├─router.js
│ ├─routes
└─webpack.config.js
复制代码
最终八个技术栈或版本各不相同的子应用,每一个子应用能够单独做为一个仓库存在并管理,portal
做为一个入口项目,用于整合和注册各应用,Portal
也是一个主项目,给它的定位是资源加载框架, Nav
做为导航路由,其余的应用做为子应用。
框架应用的本质是一个中心化部件,越简单也就越稳定,因此不要在Portal
中作任何UI及业务逻辑。能够在 Portal
来作一些系统级公共支持,e.g. 登陆验证、权限管理、鉴权、性能监控、错误调用栈上报等。
portal 主应用代码以下:
import { getMountedApps, registerApplication, start, navigateToUrl, getAppNames } from 'single-spa';
import SystemJS from 'systemjs/dist/system' // 0.20.24 DEV!!!
const apps = [
{ name: 'nav', url: true, entry: '//localhost:5005/app.js', customProps: {} },
{ name: 'react', url: '/react', entry: '//localhost:5001/app.js', customProps: {} },
{ name: 'vue', url: '/vue', entry: '//localhost:5002/app.js', customProps: {} },
{ name: 'svelte', url: '/svelte', entry: '//localhost:5003/app.js', customProps: {} },
{ name: 'react-ts', url: '/rts', entry: '//localhost:5006/app.js', customProps: {} },
{ name: 'cra-ts', url: '/crats', entry: '//localhost:5007/app.js', customProps: {} },
{ name: 'vts', url: '/vts', entry: '//localhost:5008/vts/index.js', customProps: {} },
]
/** * RegisterApp * @returns */
async function registerAllApps() {
await Promise.all(apps.map(registerApp))
await setDefaultMountedApp('/react');
start();
}
registerAllApps();
/** * set default App * @param {*} path default app path */
function setDefaultMountedApp(path) {
window.addEventListener(`single-spa:no-app-change`, (evt) => {
const activedApps = getMountedApps()
if (activedApps.length === 0 && evt.target.location.pathname === '/') {
navigateToUrl(path)
}
}, {
once: true
})
}
/** * register App * @param {*} name App Name * @param {*} url visit Url * @param {*} entry entry file * @param {*} customProps custom Props */
function registerApp({ name, url, entry, customProps = {} }) {
// 能够经过customProps来传递store与用户权限之类
return registerApplication(name, () => SystemJS.import(entry), pathPrefix(url), customProps);
}
复制代码
改动不大,上文说到过独立运行、独立部署,这套方案目前仍是不彻底的,想搭建一个符合要求的微前端架构,经过动态获取各子应用的入口写入 Portal
主应用中,以及路由、打包后的公共依赖抽离等等。
美团使用的方案就是相似 用微前端的方式搭建类单页应用
我理想中微前端的单个子应用应该还具有单独做为一个项目产品上线,因此须要将入口文件分离,single-spa
子应用入口 与 普通应用分离,方式有不少,好比双入口文件处理,或者双打包配置,可是这种不只麻烦容易出错并且比我想象中的还要复杂,不只仅是方案上的问题,试想一下,某个子应用拿出来单步部署,而登陆及鉴权系统在 Portal
其某个子应用中,难道又要将两个项目合并成一个新的微前端?想一想也就以为本身搞笑。
除此以外,这套方案存在一些问题,e.g.
@vue/cli
路由动态import Component,返回的实际上是一个html。后来借鉴了 qiankun 针对这几个问题则使用 HTML Entry 的方式。即以 {entry:'//localhost:5001/index.html'}
的形式引入;它能够很轻松的解决上述大部分问题。
function render({ appContent, loading }) {
ReactDOM.render(<Framework loading={loading} content={appContent} />, document.getElementById('container')); } render({ loading: true }); function genActiveRule(routerPrefix) => location => location.pathname.startsWith(routerPrefix); const appGroup = [ { name: 'react app', entry: '//localhost:7100', render, activeRule: genActiveRule('/react') }, { name: 'react15 app', entry: '//localhost:7101', render, activeRule: genActiveRule('/react15') }, { name: 'vue app', entry: '//localhost:7102', render, activeRule: genActiveRule('/vue') }, ] // 注册应用集 registerMicroApps(appGroup); 复制代码
registerMicroApps 大概实现以下
let microApps: RegistrableApp[] = [];
export function registerMicroApps<T extends object = {}>(
apps: Array<RegistrableApp<T>>,
lifeCycles: LifeCycles<T> = {},
opts: RegisterMicroAppsOpts = {},
) {
const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = lifeCycles;
const { fetch } = opts;
microApps = [...microApps, ...apps];
let prevAppUnmountedDeferred: Deferred<void>;
apps.forEach(app => {
const { name, entry, render, activeRule, props = {} } = app;
registerApplication(
name,
async ({ name: appName }) => {
await frameworkStartedDefer.promise;
// 获取入口 html 模板及脚本加载器 及 资源Domain
const { template: appContent, execScripts, assetPublicPath } = await importEntry(entry, { fetch });
// 卸载完后再加载
if (await validateSingularMode(singularMode, app)) {
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
render({ appContent, loading: true });
let jsSandbox: Window = window;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
if (useJsSandbox) {
const sandbox = genSandbox(appName, assetPublicPath);
jsSandbox = sandbox.sandbox;
mountSandbox = sandbox.mount;
unmountSandbox = sandbox.unmount;
}
await execHooksChain(toArray(beforeLoad), app);
// eval
let { bootstrap: bootstrapApp, mount, unmount } = await execScripts(jsSandbox);
// ...other
return {
bootstrap: [bootstrapApp],
mount: [
async () => {
if ((await validateSingularMode(singularMode, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
async () => execHooksChain(toArray(beforeMount), app),
async () => render({ appContent, loading: true }),
mountSandbox,
mount,
async () => render({ appContent, loading: false }),
async () => execHooksChain(toArray(afterMount), app),
async () => {
if (await validateSingularMode(singularMode, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
],
unmount: [
async () => execHooksChain(toArray(beforeUnmount), app),
unmount,
unmountSandbox,
async () => execHooksChain(toArray(afterUnmount), app),
async () => render({ appContent: '', loading: false }),
async () => {
if ((await validateSingularMode(singularMode, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
},
activeRule,
props,
);
});
}
复制代码
从HTML模板中提取出全部脚本样式等资源,样式直接写入html template,在沙盒部分的处理上,qiankun 利用 Proxy 劫持了对 window
的操做,使其做用到一个空字典上,在 bootstrap 及 mount 生命周期以前分别get全局状态打下快照,并使用 Map
记录下来,避免污染了全局对象,这样在沙盒 unmount
的时候也不须要手动去销毁,至于怎样将脚本默认 window 指向这个空字典也很简单,经过eval
将 window 指向 window.proxy 也就是空字典。
geval(`;(function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy);`);
复制代码
关于css隔离,因为重写了html Entry,以前的内嵌样式也天然不复存在了。其实还有一种隔离 css 的方式,与 BEM 相同,经过 postcss 去设置子应用内 class 前缀,同时支持第三方库,至于css-module就不说了,兼容性问题,好比我司还有jquery项目,这你让谁给我转去?/手动滑稽
而后剩下的就是 Lifecycle 内部的处理了。
function execHooksChain<T extends object>(hooks: Array<Lifecycle<T>>, app: RegistrableApp<T>): Promise<any> {
if (hooks.length) {
return hooks.reduce((chain, hook) => chain.then(() => hook(app)), Promise.resolve());
}
return Promise.resolve();
}
复制代码
若是采用JS Entry的方式会浪费更多时间与精力去优化。最终采用了HTML Entry的方式,简直像极了HTMLless。
这种彻底将项目独立出去的方案虽然能避免不少问题,可是也存在一个性能优化上的问题——公共依赖,若是十个子应用都是用同一技术栈,那么在打包时即便依赖抽离子应用之间也毫无关系,这其实并无一个好的解决方案,像React、React-DOM、Svelte、Vue之类占据大部分体积的包应该创建一个公共依赖池,把他们挂载在同一CDN下外链加载并经过extenals引入。
e.g.
A子应用
React@16.10.1
+ B子应用React@16.10.2
=> A+BReact@16.10.2
因为修订号保持向下兼容,修复问题但不影响特性,只要次版本号相同,修订号保持向上兼容则功能相同,利用CDN缓存尽最大程度的避免重复依赖的资源加载。
最后就是跨应用通讯了,大部分人习惯Redux之类全局状态管理库的存在,可是为了下降耦合度,咱们应该避免去应用间通讯,若是必要的话,Custom Events 能够作到,但必定要把握好这个度。另外一种方式就是以Portal
主应用 bridge 向下传递数据和回调。
可能有人以为我前面扯了一大堆到头来所有推翻感情浪费时间,“知其然而不知其因此然”,总不能知道什么是好的就直接拿来用都不知道好在哪吧?适合本身的方案才是最好的,可是没有实践,怎会知道合不合适?