背景
在一些大型前端项目内, 需要把多个应用组合起来, 避免多次跳转和减少系统的交互成本, 以前常见的方式是按同一个模板开发, 然后用iframe组合起来, 能实现较高的隔离性, 但是开发/维护/体验都不算舒适, 最近开发了一个基于react+ts实现的框架, 实现了开发/体验/运维都比较好的效果, 会逐渐将一些重点内容记录下来
框架开发
基础组件
应用容器
最先要实现的就是底层的应用容器app-container了, 有几个基本要素:
- 根据不同
url路由到不同的子应用 
- 每个子应用隔离开发并支持动态加载
 
- 子应用统一风格, 可以选择是否支持切换项目
 
- 子应用的状态管理隔离
 
- 用户基础信息由外层统一提供
 
这几个中难度比较大的是子应用的切换项目和状态管理, 按难度排序给出主要实现
子应用路由
主要原理是通过react-router结合React.Suspense实现, 首先实现root的切换组件, 然后实现动态加载app的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
   | 
 
  const RootAppContainer: React.FC = () => {   return (     <main className='container flex-auto overflow-hidden flex-column'>       <Switch>         <Route           exact           path='/'           render={(props) => {             return (               <RouteLazyComponent                 app='home'                 isApp                 project={null}                 title={CONSTANT.ALL_APPS.home.title}                 import={import(/* webpackChunkName: "home" */ `./../../home`)}                 routeProps={props}               />             );           }}         />         {_.map(CONSTANT.APPS, ({ name, title, hasProject }, app: AppOptions) => (           <Route             key={app}             path={`/${app}`}             render={(props) => {               currentApp !== app && dispatch(globalConfigDucks.changeApp(app));               return hasProject ? (                 <RootAppProjectContainer app={app} title={title} routeProps={props} />               ) : (                 <RouteLazyComponent                   app={app}                   isApp                   project={null}                   title={title}                   import={import(/* webpackChunkName: "[request]" */ `./../../../containers/${app}`)}                   routeProps={props}                 />               );             }}           />         ))}         <Route render={() => <Redirect to='/' />} />       </Switch>     </main>   ); };
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
   | 
 
  const RouteLazyComponent: React.FC<{   app: AllAppOptions;   isApp: boolean;   path?: string;   project?: Project;   title: string;   import: Promise<{ default: React.ComponentType<any> }>;   routeProps: RouteComponentProps<any, StaticContext, any>; }> = ({ app, path, isApp, title, project, import: _import, routeProps }) => {   const { t } = useTranslation();   const componentCache = useRef<{ [key: string]: React.LazyExoticComponent<any> }>({});   const key = isApp ? app : path;   const LazyComponent = (componentCache.current[key] = componentCache.current[key] || lazy(() => _import));   const App = CONSTANT.ALL_APPS[app];   useMemoCondition(     () => {       if (isApp) {         document.title = title;         return;       }       if (App.hasProject && project) {         document.title = `${app}:${title} - ${project.code} - ${App.title}`;         return;       }       document.title = `${app}:${title} - ${App.title}`;       return;     },     [title, project],     ([title, project]) => !!title || !!project,   );   const _project = useMemo(() => project, [project && project.code, project && project.name]);   return (     <Suspense fallback={<Loading loading appendToBody />}>       <LazyComponent {...routeProps} app={app} {...(_project ? { project: _project } : {})} />     </Suspense>   ); };
 
  | 
 
子应用统一菜单
由于侧边菜单和页面header部分需要保持统一风格, 实现通用的子应用加载组件, 并且支持组件内页面的动态加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | 
 
  const SubApp: React.FC<{   app: AllAppOptions,   menu: SubAppMenu,   translation: Translation,   AppIcon: React.FC,   reducers?: ReducersMapObject,   preloadedState?: any,   store?: Store<{}, Action>, }> = ({ app, menu, children, reducers, preloadedState, store, translation, AppIcon }) => {   const [menuFoldState, setMenuFoldState] = useState(false);   const match = useMatch();   return (     <div className={classNames('sub-app flex-auto overflow-hidden flex-row position-relative', app)}>       <AppLeftMenu fold={menuFoldState} setFold={setMenuFoldState} menu={menu} app={app} match={match} AppIcon={AppIcon} />       <AppMain fold={menuFoldState} menu={menu} app={app} match={match}>         {children}       </AppMain>     </div>   ); };
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
   | 
 
  const AppMain: React.FC<{   app: AllAppOptions;   fold: boolean;   menu: SubAppMenu;   match: match; }> = ({ app, fold, menu, match }) => {   return (     <>       <div className={classNames('app-main flex-column', { 'menu-fold': fold })}>         <Switch>           {menu.map((p) => {             const pc = p as SubAppMenuParentHasChildren;             const pi = p as SubAppMenuParentItem;             return _.size(pc.children) > 0 ? (               pc.children.map((c) => (                 <Route                   exact                   key={c.name}                   path={`${match.url}${c.path}`}                   render={(props) => (                     <RouteLazyComponent                       app={app}                       isApp={false}                       path={c.component_name}                       title={c.title}                       import={import(/* webpackChunkName: "[request]" */ `./../../containers/${app}/containers/${c.component_name}`)}                       routeProps={props}                     />                   )}                 />               ))             ) : (               <Route                 exact                 key={p.name}                 path={`${match.url}${pi.path}`}                 render={(props) => (                   <RouteLazyComponent                     app={app}                     isApp={false}                     path={pi.component_name}                     title={p.title}                     import={import(/* webpackChunkName: "[request]" */ `./../../containers/${app}/containers/${pi.component_name}`)}                     routeProps={props}                   />                 )}               />             );           })}           {_.size(menu) && <Route render={() => <Redirect to={`${match.url}`} />} />}         </Switch>       </div>     </>   ); };
 
  | 
 
git-submodule
子应用需要隔离开发互不影响, 最好的方式就是各自一个独立的repo, 但是完全独立后又没有一个很好的办法统一管理和开发, 于是使用submodule来实现开发和版本隔离, 基础框架不需要考虑各子应用的分支版本细节, 只有在发布的时候子应用在基础框架repo中加上对应ref的提交即可, 在gitlab-ci中加上GIT_SUBMODULE_STRATEGY: recursive, 就可以实现基础框架打包完整系统部署了
同时在子模块的ci中加上获取全局框架并打包的支持, 就可以实现子模块分离打包和部署各自的测试环境了
1 2
   |  git submodule add --name 'src/containers/${app}' ../${app}-front.git src/containers/${app}
 
  | 
 
1 2 3 4 5
   |  - git clone -b ${PARENT_BRANCH} --recurse-submodules https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/web-group/front-base - ln -s /home/web/node_modules /home/web/front-base/node_modules - rm -r /home/web/front-base/src/containers/${APPNAME} - cp -r ${CI_PROJECT_DIR}/ /home/web/front-base/src/containers/${APPNAME}
 
  | 
 
小结
到此为止实现了基础框架的加载, 统一头部和侧边菜单, 动态加载子应用和子应用页面, 以及各子应用的独立开发环境与统一打包部署的功能, 子应用的状态分离管理下期继续