背景
在一些大型前端项目内, 需要把多个应用组合起来, 避免多次跳转和减少系统的交互成本, 以前常见的方式是按同一个模板开发, 然后用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}
|
小结
到此为止实现了基础框架的加载, 统一头部和侧边菜单, 动态加载子应用和子应用页面, 以及各子应用的独立开发环境与统一打包部署的功能, 子应用的状态分离管理下期继续