详解利用Angular实现多团队模块化SPA开发框架


Posted in Javascript onNovember 27, 2017

0、前言

当一个公司有多个开发团队时,我们可能会遇到这样一些问题:

1.技术选项杂乱,大家各玩各
2.业务重复度高,各种通用api,登录注销,权限管理都需要重复实现(甚至一个团队都需要重复实现)
3.业务壁垒,业务之间的互通变得比较麻烦
4.部署方式复杂,多个域名(或IP地址)访问,给用户造成较大的记忆难度
5.多套系统,风格难以统一
6.等等...

当然,解决方式有不少。以下就来讲解下我们这边的一种解决方案。

1、思路

Angualr

Angular(注:非AngularJS) 是流行的前端 MVVM 框架之一,配合 TypeScript,非常适合用来做后台管理系统。由于我们曾今的一套 Angularjs 开发框架,我们继续选择 Angular 来进行实现,并尽可能的兼容 AngularJS 的模块。

SPA

选 SPA 还是多页?多余 Mvvm 来说,多页并不是标配。而且多页开发中,我们势必会关注更多的内容,包括通用header,footer,而不仅仅是页面的核心内容。

模块化

为什么要模块化呢?当有多个团队开发时(或者项目较大时),我们希望各个团队开发出来的东西都是 模块(不仅限于JS模块),这样可以让我们独立发布、更新、删除模块,也能让我们的关注点集中在特定模块下,提高开发效率和可维护性。

平台化

我们需要有一个运行平台(Website站点),允许在里面运行指定的模块。这样就可以实现单一入口,也容易实现通用逻辑,模块共享机制等等。

兼容 AngularJS 模块

在考虑将框架切换到 Angular 时,我们无可避免的会遇到如何兼容当前已有模块的问题。大致可选的方案如下:

1.参考 AngualrJS -> Angular 官方升级指南,一步步将模块切换为 Angular 的实现。(工作量大,需要开发团队调整很多东西)
2.iframe嵌入,会有一定的体验差异,但对开发团队来说,基本无缝升级,也不需要做什么改动。(无疑,我们选择了这套方案)

模块打包

我们需要将单个的模块打包为资源包,进行更新。这样才能做到模块独立发布,及时生效。

CSS冲突

在大型 SPA 中,CSS冲突是很大的一个问题。我们期望通过技术手段,能够根据当前使用的模块,加载和卸载CSS。

跨页面共享数据

由于涉及到iframe兼容旧有模块,我们无可避免,需要考虑跨窗口的页面共享。

公共模块

当一个团队的模块较多时,就会有一些公共的东西被抽取出来,这个过程,框架是无法知道的,所以这个时候,我们就需要考虑支持公共模块。(模块之间也有依赖关系)

3、实现

基于以上的一些思考,我们首先需要实现一个基础的平台网站,这个没什么难度,直接用 Angular 实现即可。有了这一套东西,我们的登录注销,基本的菜单权限管理,也就实现了。

在这个基础之上,我们也能实现公共服务、公共组件了(封装一系列常用的玩意)。

如何模块化?如何打包?

注意:此模块并非Angular本身的模块。 我们通过约定,在 modules/ 下的每一个目录都是一个业务模块。一个业务模块一般会包含,静态资源、CSS以及JS。根据这个思路,我们的打包策略就是:遍历 modules/ 的所有目录,对每一个目录进行单独打包(webpack多entry打包+CSS抽取),另外使用 gulp 来处理相关的静态资源(在我看来,gulp才是构建工具,webpack是打包工具,所以混合使用,物尽其用)。

一般来说,webpack 会把所有相关依赖打包在一起,A、B 模块都依赖了 @angular/core 识别会重复打包,而且框架中,也已经打包了 @angular 相关组件。这个时候,常规的打包配置就不太合适了。那该如何做呢?

考虑到 Angular 也提供了 CDN 版本,所以我们将 Angular 的组件通过文件合并,作为全局全量访问,如 ng.core、ng.common 等。

既然这样,那我们打包的时候,就可以利用 webpack 的 externals 功能,把相关依赖替换为全局变量。

externals: [{
 'rxjs': 'Rx',
 '@angular/common': 'ng.common',
 '@angular/compiler': 'ng.compiler',
 '@angular/core': 'ng.core',
 '@angular/http': 'ng.http',
 '@angular/platform-browser': 'ng.platformBrowser',
 '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic',
 '@angular/router': 'ng.router',
 '@angular/forms': 'ng.forms',
 '@angular/animations': 'ng.animations'
}

这样处理之后,我们打包后的文件,也就不会有 Angular 框架代码了。

注:这个对引入资源的方式也有一定要求,就不能直接引入内层资源了。

如何动态加载模块

打包完成之后,这个时候就要考虑平台如何加载这些模块了(发布过程就不说了,放到指定位置即可)。

什么时候决定加载模块呢?其实是访问特定路由的时候,所以我们的顶级路由,会使用Promise方法来实现,如下:

const loadModule = (moduleName) => {
 return () => {
  return ModuleLoaderService.load(moduleName);
 };
};

const dynamicRoutes = [];

modules.forEach(item => {
 dynamicRoutes.push({
  path: item.path,
  canActivate: [AuthGuard],
  canActivateChild: [AuthGuard],
  loadChildren: loadModule(item.module)
 });
});
const appRoutes: Routes = [{
 path: 'login', component: LoginComponent
}, {
 path: 'logout', component: LogoutComponent
}, {
 path: '', component: LayoutComponent, canActivate: [AuthGuard],
 children: [
  { path: '', component: HomeComponent },
  ...dynamicRoutes,
  { path: '**', component: NotFoundComponent },
 ]
}];

我们把每个模块,按照 umd 的格式进行打包。然后再需要使用该模块的时候,使用动态构建 script 来运行脚本。

load(moduleName, isDepModule = false): Promise<any> {
 let module = window['xxx'][moduleName];
 if (module) {
  return Promise.resolve(module);
 }
 return new Promise((resolve, reject) => {
  let path = `${root}${moduleName}/app.js?rnd=${Math.random()}`;
  this._loadCss(moduleName);
  this.http.get(path)
   .toPromise()
   .then(res => {
    let code = res.text();
    this._DomEval(code);
    return window['xxx'][moduleName];
   })
   .then(mod => {
    window['xxx'][moduleName] = mod;
    let AppModule = mod.AppModule;
    // route change will call useModuleStyles function.
    // this.useModuleStyles(moduleName, isDepModule);
    resolve(AppModule);
   })
   .catch(err => {
    console.error('Load module failed: ', err);
    resolve(EmptyModule);
   });
 });
}

// 取自jQuery
_DomEval(code, doc?) {
 doc = doc || document;
 let script = doc.createElement('script');
 script.text = code;
 doc.head.appendChild(script).parentNode.removeChild(script);
}

CSS的动态加载相对比较简单,代码如下:

_loadCss(moduleName: string): void {
 let cssPath = `${root}${moduleName}/app.css?rnd=${Math.random()}`;
 let link = document.createElement('link');
 link.setAttribute('rel', 'stylesheet');
 link.setAttribute('href', cssPath);
 link.setAttribute('class', `xxx-module-style ${moduleName}`);
 document.querySelector('head').appendChild(link);
}

为了能够在模块切换时卸载,还需要提供一个方法,供路由切换时使用:

useModuleStyles(moduleName: string): void {
 let xxxModuleStyles = [].slice.apply(document.querySelectorAll('.xxx-module-style'));
 let moduleDeps = this._getModuleAndDeps(moduleName);
 moduleDeps.push(moduleName);
 xxxModuleStyles.forEach(link => {
  let disabled = true;
  for (let i = moduleDeps.length - 1; i >= 0; i--) {
   if (link.className.indexOf(moduleDeps[i]) >= 0) {
    disabled = false;
    moduleDeps.splice(i, 1);
    break;
   }
  }
  link.disabled = disabled;
 });
}

公共模块依赖

为了处理模块依赖,我们可以借鉴 AMD规范 以及使用 requirejs 作为加载器。当前在我的实现里,是自定义了一套加载器,后期应该会切换到 AMD 规范上去。

如何兼容 AngularJS模块?

为了兼容 AngularJS 的模块,我们引入了 iframe, iframe会先加载一套曾今的 AngularJS 宿主,然后再这个宿主中,运行 AngularJS 模块。为了实现通信,我们需要两套平台程序中,都引入一个基于 postMessage 实现的跨窗口通信库(因为默认跨域,所以用postMessage实现),有了它之后,我们就可以很方便的两边通信了。

AOT编译

按照 Angular 官方的 Aot 编译流程即可。

多Tab页

在后台系统中,多Tab页是比较常用了。但是多Tab页,在单页中使用,会有一定的性能风险,这个依据实际的情况,进行使用。实现多Tab页的核心就是如何动态加载组件以及如何获取到要加载的组件。

多Tab页面,实际就是一个 Tabset 组件,只是在 tab-item 的实现稍显特别一些,相关动态加载的源码:

@ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef;

constructor(
 private elementRef: ElementRef,
 private renderer: Renderer2,
 private tabset: TabsetComponent,
 private resolver: ComponentFactoryResolver,
 private parentContexts: ChildrenOutletContexts
) {
}

public destroy() {
 let el = this.elementRef.nativeElement as HTMLElement;
 // tslint:disable-next-line:no-unused-expression
 el.parentNode && (el.parentNode.removeChild(el));
}

private loadComponent(component: any) {
 let context = this.parentContexts.getContext(PRIMARY_OUTLET);
 let injector = ReflectiveInjector.fromResolvedProviders([], this.dynamicComponentContainer.injector);
 const resolver = context.resolver || this.resolver;
 let factory = resolver.resolveComponentFactory(component);
 //  let componentIns = factory.create(injector);
 //  this.dynamicComponentContainer.insert(componentIns.hostView);
 this.dynamicComponentContainer.createComponent(factory);
}

注意:要考虑组件卸载方法,如 destroy()

为了获取到当前要渲染的组件,我们可以借用路由来抓取:

this.router.events.subscribe(evt => {
 if (evt instanceof NavigationEnd) {
  let pageComponent;
  let pageName;
  try {
   let nextRoute = this.route.children[0].children[0];
   pageName = this.location.path();
   pageComponent = nextRoute.component;
  } catch (e) {
   pageName = '$$notfound';
   pageComponent = NotFoundComponent;
  }
  let idx = this.pageList.length + 1;
  if (!this.pageList.find(x => x.name === pageName)) {
   this.pageList.push({
    header: `页面${idx}`,
    comp: pageComponent,
    name: pageName,
    closable: true
   });
  }
  setTimeout(() => {
   this.selectedPage = pageName;
  });
 }
});

3、总结

以上就是大概的实现思路以及部分相关的细节。其他细节就需要根据实际的情况,进行酌情处理。

该思路并不仅限于 Angular 框架,使用 Vue、React 也可以做到类似的效果。同时,这套东西也比较适合中小企业的后台平台(不一定非要多团队,一个团队按模块开发也是不错的)。

如需要了解更多细节,可以参考:ngx-modular-platform,能给个 star 就更好了。

本文github地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript DOM学习第六章 表单实例
Feb 19 Javascript
基于jQuery+JSON的省市二三级联动效果
Jun 05 Javascript
Javascript缓存API
Jun 14 Javascript
JavaScript中重名的函数与对象示例详析
Sep 28 Javascript
基于three.js编写的一个项目类示例代码
Jan 05 Javascript
Node.js中,在cmd界面,进入退出Node.js运行环境的方法
May 12 Javascript
微信小程序实现图片上传放大预览删除代码
Jun 28 Javascript
webpack4.x CommonJS模块化浅析
Nov 09 Javascript
node和vue实现商城用户地址模块
Dec 05 Javascript
浅谈express.js框架中间件(middleware)
Apr 07 Javascript
vue实现密码显示与隐藏按钮的自定义组件功能
Apr 23 Javascript
vue项目前端知识点整理【收藏】
May 13 Javascript
JavaScript实现修改伪类样式
Nov 27 #Javascript
Vue.js搭建移动端购物车界面
Jun 28 #Javascript
Vue实现购物车场景下的应用
Nov 27 #Javascript
javascript字体颜色控件的开发 JS实现字体控制
Nov 27 #Javascript
vue购物车插件编写代码
Nov 27 #Javascript
Vue.js devtool插件安装后无法使用的解决办法
Nov 27 #Javascript
微信小程序页面跳转功能之从列表的item项跳转到下一个页面的方法
Nov 27 #Javascript
You might like
memcache命令启动参数中文解释
2014/01/13 PHP
如何批量清理系统临时文件(语言:C#、 C/C++、 php 、python 、java )
2016/02/01 PHP
yii2中结合gridview如何使用modal弹窗实例代码详解
2016/06/12 PHP
Web开发者必备的12款超赞jQuery插件
2010/12/03 Javascript
jquery uaMatch源代码
2011/02/14 Javascript
javascript中的数字与字符串相加实例分析
2011/08/14 Javascript
简单实例处理url特殊符号&amp;处理(2种方法)
2013/04/02 Javascript
学习JavaScript设计模式(代理模式)
2015/12/03 Javascript
微信小程序 less文件编译成wxss文件实现办法
2016/12/05 Javascript
jQuery Easyui 下拉树组件combotree
2016/12/16 Javascript
纯js实现html转pdf的简单实例(推荐)
2017/02/16 Javascript
Vue filter介绍及其使用详解
2017/10/21 Javascript
JavaScript通过mouseover()实现图片变大效果的示例
2017/12/20 Javascript
简单的Vue异步组件实例Demo
2017/12/27 Javascript
nodejs中express入门和基础知识点学习
2018/09/13 NodeJs
mock.js模拟前后台交互
2019/07/25 Javascript
Vuex实现简单购物车
2021/01/10 Vue.js
你所不知道的Python奇技淫巧13招【实用】
2016/12/14 Python
Selenium的使用详解
2018/10/19 Python
pycharm在调试python时执行其他语句的方法
2018/11/29 Python
Python实现的微信支付方式总结【三种方式】
2019/04/13 Python
python+numpy按行求一个二维数组的最大值方法
2019/07/09 Python
在Python中append以及extend返回None的例子
2019/07/20 Python
Python3 解决读取中文文件txt编码的问题
2019/12/20 Python
Python各种扩展名区别点整理
2020/02/27 Python
什么是CSS3 HSLA色彩模式?HSLA模拟渐变色条
2016/04/26 HTML / CSS
微信浏览器左上角返回按钮拦截功能
2017/11/21 HTML / CSS
Vans澳大利亚官网:购买鞋子、服装及配件
2019/09/05 全球购物
研究生考核个人自我鉴定
2014/03/27 职场文书
给客户的感谢信
2015/01/21 职场文书
实习班主任自我评价
2015/03/11 职场文书
音乐剧猫观后感
2015/06/04 职场文书
2016全国“质量月”活动标语口号
2015/12/26 职场文书
2019各种保证书范文
2019/06/24 职场文书
2019新员工试用期转正申请书3篇
2019/08/13 职场文书
Python机器学习三大件之一numpy
2021/05/10 Python