当前位置: 移动技术网 > IT编程>脚本编程>AngularJs > 详解利用Angular实现多团队模块化SPA开发框架

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

2017年12月08日  | 移动技术网IT编程  | 我要评论

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 也可以做到类似的效果。同时,这套东西也比较适合中小企业的后台平台(不一定非要多团队,一个团队按模块开发也是不错的)。

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

本文github地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网