dddreee / blog

1 stars 0 forks source link

浅析飞冰的微前端解决方案--icestark #21

Open dddreee opened 4 years ago

dddreee commented 4 years ago

20200421 开始填坑

什么是微前端

微前端的概念源自微服务,当前的趋势是构建一个功能强大且功能强大的浏览器应用程序(又名单页应用程序),该应用程序位于微服务架构之上。随着时间的流逝,通常由独立团队开发的前端层会不断增长,并且变得越来越难以维护。为了解决这个问题,就有人提出了这个微前端的概念,将一个web应用视为由多个独立功能模块的组合,每个模块独立维护。

dddreee commented 4 years ago

TODO:一个小问题,icestark内置的 icestark-child 中的路由跟 icestark-layout 的路由并不匹配,child并没有配置路由的 basename

官方demo icestark-layout 中的主要代码如下

app.ts ```javascript import * as React from 'react'; import { createApp } from 'ice' import { ConfigProvider } from '@alifd/next'; import PageLoading from '@/components/PageLoading'; import FrameworkLayout from '@/layouts/FrameworkLayout'; const appConfig = { app: { rootId: 'icestark-container', addProvider: ({ children }) => ( {children} ), }, router: { type: 'browser', }, icestark: { type: 'framework', Layout: FrameworkLayout, getApps: async () => { const apps = [{ path: '/seller', title: '商家平台', // React app demo: https://github.com/ice-lab/icestark-child-apps/tree/master/child-seller-react-16 url: [ '//ice.alicdn.com/icestark/child-seller-react/index.js', '//ice.alicdn.com/icestark/child-seller-react/index.css' ], }, { path: '/waiter', title: '小二平台', url: [ // Vue app demo: https://github.com/ice-lab/icestark-child-apps/tree/master/child-waiter-vue-2 '//ice.alicdn.com/icestark/child-waiter-vue/app.js', '//ice.alicdn.com/icestark/child-waiter-vue/app.css' ], }, { path: '/angular', title: 'Angular', // Angular app demo: https://github.com/ice-lab/icestark-child-apps/tree/master/child-common-angular-9 entry: '//ice.alicdn.com/icestark/child-common-angular/index.html', }]; return apps; }, appRouter: { LoadingComponent: PageLoading, }, }, }; createApp(appConfig) ```

子应用demo icestark-child 主要路由代码

routes.ts ```javascript import { renderNotFound } from '@ice/stark-app'; import BasicLayout from '@/layouts/BasicLayout'; import Detail from '@/pages/Detail'; import List from '@/pages/List'; const routerConfig = [ { path: '/', component: BasicLayout, // redirect: '/list', children: [ { path: '/', exact: true, component: List, }, { path: '/list', extra: true, component: () => { return 123 } }, { path: '/detail', component: Detail, }, { component: () => { return renderNotFound(); }, }, ], }, ]; export default routerConfig; ```

猜想:在创建子应用的时候需要传入的 appConfig 中的 icestark.typechild ,是否在 createApp 中创建 history 实例时对basename 做了处理?

dddreee commented 4 years ago

第三方应用迁移

框架应用迁移(这里先用react的项目,其他的可以去官网查看),通过 @ice/starkAppRouter/AppRoute来管理注册子应用

App.jsx ```javascript import React from 'react'; import { AppRouter, AppRoute } from '@ice/stark'; import NotFound from '@/components/NotFound'; import PageLoading from '@/components/PageLoading'; import BasicLayout from '@/layouts/BasicLayout'; export default class App extends React.Component { state = { pathname: '', } handleRouteChange = (pathname) => { console.log('route change', pathname); // 如有需求,可根据 pathname 切换 layout 的形态 this.setState({ pathname, }); } render() { const { pathname } = this.state; return ( ); } } ```

AppRouter 组件

简单的画了个图 [![tQkxqs.md.png](https://s1.ax1x.com/2020/05/30/tQkxqs.md.png)](https://imgchr.com/i/tQkxqs)
dddreee commented 4 years ago

AppRoute 组件

简单画个调用链的图 [![tJiEod.md.png](https://s1.ax1x.com/2020/06/01/tJiEod.md.png)](https://imgchr.com/i/tJiEod)
dddreee commented 4 years ago

已有项目改造为子应用

  1. 应用入口适配:通过 getMountNode() 动态获取渲染节点,注册应用自身的声明周期,下面用react代码展示
    
    import ReactDOM from 'react-dom';
    import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave } from '@ice/stark-app';
    import App from './App';

if (isInIcestark()) { registerAppEnter(() => { ReactDOM.render(router(), getMountNode()); }); registerAppLeave(() => { ReactDOM.unmountComponentAtNode(getMountNode()); }); } else { ReactDOM.render(, document.getElementById('ice-container')); }


2. 定义基准路由:注册子应用时会为每个子应用分配一个 `basename` 比如官方demo的 `/seller`,子应用可以通过 `getBasename()` 获取自身的基准路由
```javascript
import React from 'react';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import { getBasename } from '@ice/stark-app';

export default () => {
  return (
    <Router basename={getBasename()}>
      <Switch>
        // ...
      </Switch>
    </Router>
  );
};
  1. 其他问题
dddreee commented 4 years ago

应用间通信

官方提供了 @ice/stark-data 来支持状态共享和事件监听,@ice/stark-data 包含了 storeevent 两个模板,store 存储全局状态,event 负责事件监听响应

源码 实现与 store 大致相同

event 源码 ```typescript import { isArray, warn } from './utils'; import { setCache, getCache } from './cache'; const eventNameSpace = 'event'; interface Hooks { emit(key: string, value: any): void; on(key: string, callback: (value: any) => void): void; off(key: string, callback?: (value: any) => void): void; has(key: string): boolean; } class Event implements Hooks { eventEmitter: object; constructor() { this.eventEmitter = {}; } emit(key: string, ...args) { const keyEmitter = this.eventEmitter[key]; if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) { warn(`event.emit: no callback is called for ${key}`); return; } keyEmitter.forEach(cb => { cb(...args); }); } on(key: string, callback: (value: any) => void) { if (typeof key !== 'string') { warn('event.on: key should be string'); return; } if (callback === undefined || typeof callback !== 'function') { warn('event.on: callback is required, should be function'); return; } if (!this.eventEmitter[key]) { this.eventEmitter[key] = []; } this.eventEmitter[key].push(callback); } off(key: string, callback?: (value: any) => void) { if (typeof key !== 'string') { warn('event.off: key should be string'); return; } if (!isArray(this.eventEmitter[key])) { warn(`event.off: ${key} has no callback`); return; } if (callback === undefined) { this.eventEmitter[key] = undefined; return; } this.eventEmitter[key] = this.eventEmitter[key].filter(cb => cb !== callback); } has(key: string) { const keyEmitter = this.eventEmitter[key]; return isArray(keyEmitter) && keyEmitter.length > 0; } } let event = getCache(eventNameSpace); if (!event) { event = new Event(); setCache(eventNameSpace, event); } export default event; ```