400-888-0545
首页 > 关于我们 > 技术文摘
办公OA:O2OA前端性能优化
发布时间:

O2OA是一个基于J2EE分布式架构,集成移动办公、智能办公,支持私有化部署,自适应负载能力的,能够很大程度上节约企业软件开发成本的基于AGPL协议开放源代码的企业信息化系统需求定制开发平台解决方案。O2OA对外提供专业的开发运维等技术服务。

开源O2OA官网:http://www.o2oa.net/

下文是O2OA前端性能优化的教程干货:


O2OA是一个SPA应用,相对与传统的多页应用,前端的代码量比较大,用户进行二次开发也主要是前端的脚本,本文中,我们来讨论一下与页面展现性能相关的一些内容。(数据库和服务器的优化不在本文讨论范围内)。本文中的内容需要O2OA 5.3 及以上版本支持。

缓存

客户端的缓存设置

一般情况下,现代浏览器都默认支持http协议的缓存策略,我们需要注意IE10和IE11,在“网站数据设置”中推荐使用“自动”。

image.png

如果您将O2OA通过webview嵌入到自己的移动端APP中时,注意使用webview的默认缓存策略。(IOS:NSURLRequestUseProtocolCachePolicy;Android:LOAD_DEFAULT


Max-Age

为了浏览器可以直接从缓存中获取静态文件,可以设置静态资源响应头的Cache-Control的Max-Age值。在服务器config/node_xxx.xxx.xxx.xxx.json文件中,配置web中的cacheControlMaxAge字段,以秒为单位,配置静态资源本地缓存的时间。如:

{
  ...
    "web": {
    ...
    "cacheControlMaxAge": 86400,
    ...
  },
}

配置后需要重启服务器生效。


Etag

一些服务请求,在响应头中会有Etag,客户端收到带有Etag响应头的响应后,会把内容和Etag值缓存下来,下次发起同一个请求时,会带上请求头If-None-Match,值为上一次收到的Etag值,服务器收到请求后,会计算出Etag值,并与请求的If-None-Match值比较,如果相同,则不需要返回内容,返回304,告诉浏览器从本地缓存获取内容即可。如果两值不匹配,说明要请求的内容已经修改过,需要重新下载。

image.png

所以,我们系统在经过一系列网络设备,如负载设备和Web应用防火墙(WAF)等,请确保这些设备不对请求的Etag响应头进行修改。


静态资源缓存服务器

我们可以把web服务器部署到其他静态资源的web服务器,也可以通过nginx等配置静态资源缓存。

服务器目录下servers/webServer/下的所有文件,拷贝到其他web服务器的web目录,修改x_desktop/res/config/config.json文件中的配置即可:

{
    "center": [
    {
      "port": "20030",      //O2服务器中心服务器端口
      "host": "develop.o2oa.net"    //O2服务器中心服务器Host
    }
  ],
  "applicationServer": {
    "host": "develop.o2oa.net"  //O2应用服务器Host
  },
  "initManagerChanged": true,
  "initManagerName": "",
    ...
}

此文件主要修改两部分内容:

1、center部分,修改为要访问的O2中心服务器地址和端口;

2、applicationServer部分,修改为要访问的O2应用服务器地址,如果没有applicationServer,就添加一个。集群环境下,可配置应用服务器负载地址



不发起option请求

默认情况下O2OA服务器启用三个端口80、20020和20030,分别对应web服务、应用服务和center服务。由于使用不同端口,在浏览器请求时,会先发送option请求来判断跨域访问策略。见下图:

image.png


方法一

我们可以按以下配置,使请求同源,就不再需要发送option请求。

在服务器config/node_xxx.xxx.xxx.xxx.json文件中,配置web中的proxyApplicationEnable字段为true,将application和center中的proxyPort配置为和web端口一致(默认为80),将proxyHost配置为用户访问的域名。

{
  ...
  "center": {
    ...
    "proxyHost": "develop.o2oa.net",
    "proxyPort": 80,
    ...
  },
  "application": {
    ...
    "proxyHost": "develop.o2oa.net",
    "proxyPort": 80,",
    ...
  },
  "web": {
    ...
    proxyApplicationEnable: true,
    ...
  },
  ...
}

配置后重启服务器后,就不会有option请求发起。

image.png

方法二

我们也可以通过负载设备的http代理配置来避免发送option请求,以nginx为例,我们可以进行如下配置:

在nginx配置中,通过配置不同路径来映射到O2OA服务器的web、application和center。

    listen       80;
    server_name   develop.o2oa.net;
    location /web/ {
        proxy_pass   http://xxx.xxx.xxx.xxx:80/;
    }
    location /app/ {
        proxy_pass   http://xxx.xxx.xxx.xxx:20020/;
    }
    location /center/ {
        proxy_pass   http://xxx.xxx.xxx.xxx:20030/;
    }

然后在O2OA服务器config/portal.json文件中配置urlMapping,如下:(如果在config目录下没有portal.json文件,可以从configSample目录下拷贝一个过来

{
  ...
  "urlMapping": {
      "develop.o2oa.net:20020": "develop.o2oa.net/app",
      "develop.o2oa.net:20030": "develop.o2oa.net/center"
  },
  ...
}

重启nginx和O2OA服务器后,我们可以用 http://develop.o2oa.net/web 访问系统。此方法通过nginx将不同路径的请求代理到O2OA服务器的不同端口,浏览器只使用80端口访问,避免的跨域请求,也不会发起option请求。


获取服务地址请求

  默认情况下,访问系统页面时,会发起若干个获取服务器地址的请求,地址如:http://develop.o2oa.net:20030/x_program_center/jaxrs/distribute/assemble/source/develop.o2oa.net。

此服务会返回一组服务列表,后续的请求都会使用此列表中的host和port。这样我们就可以在集群环境下,根据服务器策略,给不同的用户分配不同的服务地址。

image.png

  假设我们有两台服务器:server1和server2,端口配置都为默认,组成了O2OA的集群,我们可以查看/x_desktop/res/config/config.json文件,服务器自动生成了center服务器地址配置:

{
  ...
  "center": [
    {
      "port": "20030",
      "host": ""
    },
    {
      "port": "20030",
      "host": "server1"
    },
    {
      "port": "20030",
      "host": "server2"
    }
  ],
  ...
}

此时,浏览器会同时发起三个获取服务地址的请求:

1、http://(用户访问系统的域名):20030/x_program_center/jaxrs/distribute/assemble/source/(用户访问系统的域名);

2、http://server1:20030/x_program_center/jaxrs/distribute/assemble/source/server1

3、http://server2:20030/x_program_center/jaxrs/distribute/assemble/source/server2

其中只要有一个服务成功返回数据,其他服务就会取消,系统就会使用返回的服务地址列表进行后续访问,返回的数据格式如下:

{
  "type": "success",
  "data": {
    "x_portal_assemble_designer": {
      "name": "门户设计",
      "host": "server1",
      "port": 20020,
      "context": "/x_portal_assemble_designer"
    },
    "x_portal_assemble_surface": {
      "name": "门户",
      "host": "server1",
      "port": 20020,
      "context": "/x_portal_assemble_surface"
    },
    "x_query_assemble_surface": {
      "name": "数据查询",
      "host": "server1",
      "port": 20020,
      "context": "/x_query_assemble_surface"
    },
    "x_file_assemble_control": {
      "name": "云文件",
      "host": "server1",
      "port": 80,
      "context": "/x_file_assemble_control"
    },
    ...
  },
  "message": "",
  "date": "2020-11-25 16:14:58",
  "spent": 0,
  "size": -1,
  "count": 0,
  "position": 0
}

其中每一个服务的host是服务器根据策略分配的。所以不同用户或这个每次访问会分配到不同的服务地址,以此平衡服务器压力。


  但大多数情况下,我们通过都会nginx配置来搭建负载均衡集群,那此时,我们的获取到的服务地址列表始终是指向nginx服务器的。此时再去动态获取服务地址列表,就显得没有必要了。

  所以我们可以手工配置所有服务地址,这样浏览器就不需要发起获取服务地址的请求了。在服务器config/web.json文件中,添加以下配置(如果在config目录下没有web.json文件,可以从configSample目录下拷贝一个过来):

{
    "configMapping": {
    //此处的server1是用户访问系统时所用的域名。
    "server1": {
        "center": {
        "host": "center服务器域名"
        "port": "20030"
      },
      //servers下配置所有系统服务的地址,
      //可以通过http://center服务器:20030/x_program_center/jaxrs/distribute/assemble/source/server1
      //这个请求来获取,返回的data中的内容
      "servers": {
        "x_portal_assemble_designer": {
          "name": "门户设计",
          "host": "app.cqmc.com",
          "port": 8085,
          "context": "/x_portal_assemble_designer"
        },
        ......
      }
    },
      
    //如果有多个域名可以访问本系统,可以配置多组。
    "server2": {
        ...
    }
  }
}

配置完成后重启服务器,再次访问系统,就不会发起获取服务地址的请求了。


脚本预加载

  在设计表单和页面时,我们经常会引用脚本库中的设计,在以前的版本中,我们会使用this.include方法引用脚本,这样会向服务器发起请求,在请求完成后再执行其他脚本。如果在页面载入的时候,需要引入的脚本过多时,必然会拖慢页面加载速度。

  举例说明:在以前的代码中,我们很多时候都是include一个脚本,运行一部分代码,再include一部分脚本,再运行一部分代码,这样就要求include的脚本同步加载,阻塞页面载入,如以下代码:

//在表单的queryLoad事件中的代码
this.include("script1");
this.script1Function();

this.include("script2");
this.script2Function();

this.include("script3");
this.script3Function();

在表单的queryLoad事件中有以上代码,并有三个脚本库script1、script2和script3,分别定义三个方法:

//script1
this.define("script1Function", function(txt){
    console.log("script1 function is run");
});
//script2
this.define("script2Function", function(txt){
    console.log("script2 function is run");
});
//script3
this.define("script3Function", function(txt){
    console.log("script3 function is run");
});

当我们展现这个表单时,通过浏览器开发工具的网络监控可以看到:

image.png

三个引入脚本是顺序加载的,阻塞了页面载入,导致页面展现速度变慢,我们可以通过以下两种方法来解决此问题。


页面配置预加载脚本

  在5.3版本开始,我们在设计表单和页面时,可以配置需要预加载的脚本,只要选择在页面载入时需要加载的脚本即可,不需要再使用this.include方法加载了。但要注意的是,如果脚本有依赖关系,必须按照一定的顺序选择脚本。

  在上面的例子中,我们只需要将要引入的脚本,选择到表单属性的“预加载脚本”中。

image.png

然后修改queryLoad代码:

//在表单的queryLoad事件中的代码
//脚本已经预加载,不需要通过this.include载入了
this.script1Function();
this.script2Function();
this.script3Function();

  再次展显此表单时,方法正常运行,但没有了引入脚本的http请求了。


异步加载

  我们也可以通过异步加载脚本,来加快页面展现速度,我们将上例中的queryLoad事件代码做如下改动:

//在表单的queryLoad事件中的代码
this.include(["script1","script2","script3"], function(){
  this.script1Function();
  this.script2Function();
  this.script3Function();
}.bind(this), true);

  通过设置this.include方法的第二个个参数为回调函数,这样第一个参数中的数组所指向的脚本会进行异步加载,加载完成后,运行回调函数。

  再次展显此表单时,我们会看到方法正常运行,通过浏览器开发工具的网络监控可以看到:

image.png

  三个请求同时运行,完成后运行脚本中的方法,不阻塞页面展现。

  但上面的方法存在一个问题,就是在queryLoad事件中异步加载脚本,不会使页面展现阻塞,此时页面加载继续执行,如果我们在页面的其他事件或组件默认值等脚本中,使用了要加载的脚本中定义的方法时,就会出现错误,所以如果有这样的情况,我们需要在queryLoad事件中异步加载脚本时,暂停页面载入,等脚本加载完成后,再继续载入页面。我们修改上面的queryLoad事件代码如下:

//在表单的queryLoad事件中的代码
//暂停页面加载,并获取要恢复页面加载的回调方法
var resolve = this.wait();
this.include(["script1","script2","script3"], function(){
  this.script1Function();
  this.script2Function();
  this.script3Function();
  
  //异步加载完成,获取回调方法
  resolve.cb(); //回调方法继续展现页面
}.bind(this), true);

异步加载的方法不但适用脚本加载,也同样适用数据字典或其他异步请求的数据获取,可以通过异步方式获取所有需要的数据后,再加载表单,这样可以有效避免同步请求阻塞页面。请看下面的例子:

//假设在表单加载时,我们会使用到若干脚本、几个数据字典、以及通过服务获取的数据
//可以让这些请求异步执行,所有数据准备好以后,再继续展现表单
//在表单的queryLoad事件中的代码

//暂停页面加载,并获取要恢复页面加载的回调方法
var resolve = this.wait();

//加载脚本的Promise
var scriptPromise = new Promise(function(resolve, reject){
  this.include(["script1","script2","script3"], function(){
    resolve();
  }.bind(this), true);
}.bind(this));

//加载通过服务器请求的数据
//此处以获取当前用户最新10条待办为例
//Actions的方法返回Promise对象
var dataPromise = this.Actions.load("x_processplatform_assemble_surface").TaskAction.V2ListPaging(1,10);

//加载第一个数据字典
//get方法第二个参数表示异步,也可以是一个回调函数,返回Promise对象
var dict1Promise = (new this.Dict("dict1")).get("path", true);

//加载第二个数据字典
var dict2Promise = (new this.Dict("dict2")).get("path",true);

//通过Promise.all方法确保所有资源加载
Promise.all([scriptPromise, dict1Promise, dict2Promise, dataPromise]).then(function(values){ 
  //获取到的数据赋值,以备后续使用
  this.dict1Data = values[1];
  this.dict2Data = values[2];
  this.taskData = values[3].data;
  resolve.cb();     //继续加载表单
}.bind(this)).catch(function(){
  //即使资源加载有错,也继续加载表单
    resolve.cb();
});

再次展显此表单时,通过浏览器开发工具的网络监控可以看到:

image.png

所有请求同时发起,减少了页面阻塞时间。


组件延时加载

表单和页面的Tab页组件和子表单组件,具有“延时加载”选项。

Tab页组件建议“延时加载”选项始终选“是”

对于子表单,首先建议尽可能不用计算子表单。然后“延时加载”选项选:“是”,然后在需要子表单展现时,使用以下代码激活:

this.form.get("subform").active(function(){
    //子表单加载完成后的回调方法
});

组件默认值异步处理

5.3版本开始支持表单组件默认值通过Promise进行异步处理,有关详细内容可查看文档:

此处为语雀文档,点击链接查看:https://www.yuque.com/go/doc/14754201

https://www.yuque.com/o2oa/rf2zrv/ws07m0


想要了解更多开源O2OA办公开发平台,欢迎随时访问官网。

官网:http://www.o2oa.net/