满帮web页面首屏秒开解决之道

2,281 阅读9分钟

背景

-w866

上图是我们的web项目在android平台的一个首屏渲染的时间平均值分布图,html加载600ms(蓝线),js资源加载400ms(橙线),公共逻辑和页面执行800ms(绿线),页面资源加载执行预估200ms。面对上面的问题,要优化,逃不过下面两种方案

  1. 优化资源加载时间
  2. cdn,缓存,离线包,资源按需打包
  3. 优化逻辑执行时间
  4. 减少代码复杂度,提前执行公共逻辑

在资源方面权衡各种维护便利性,以及加载性能后,我们项目做了离线包,预加载等一些手段后,首页的打开速度可以控制在700-1000ms左右,在我们想进一步像200ms内打开页面迈进的时候,我们想到预加载页面以及公共库,预执行公共逻辑。最终效果如下

效果

ScreenFlo

热门活动页面打开的效果图 (测试机型 小米6 普通机器)

  1. 一个是微前端方式打开(除了页面过渡,以及数据loading之外,页面基本150ms就打开)
  2. 一个是不使用微前端方式(明显能加载过程中的白屏,以及进度条,在页面缓存过后打开时间也差不多在1.2s左右)

我们是如何做的,下面分享我们的方案

技术方案

初步想法,让App提供一个webview预加载执行一个web项目,该web项目有运行一个有前端项目的所有依赖库。然后该项目分析访问的url,分析需要加载的业务页面逻辑js,然后加载js并创建渲染页面。 由于已经提前执行了公共逻辑。后续的业务页面加载执行其实非常快。

按这个想法我们需要进行web端项目和native项目进行改造

web端改造

-w942

上面是改造之前3个前端项目打包后的产物,包含html入口文件,公共的资源库,以及每个页面的js逻辑代码。如果一个公司里面按照公司的标准规范统一创建的项目,那么大多项目引入的公共资源库在每个项目里面都是一样的。 既然每个项目依赖的公共资源都一样,那我们是否可以想个办法把公共的东西当作一个项目,这个项目根据url的信息,动态加载对应url页面的js来运行,最终渲染页面,按这个想法我们调整了一下架构如下图。

-w940

可以看到我们把公共的依赖资源抽离成一个单独的项目,暂且叫做 微前端框架 。通过 static.ymm56.com/microweb 可以进行访问 。社区项目的某个页面可以通过如下链接进行访问 static.ymm56.com/microweb/#/…

可以看到我们把具体项目的路由信息放到hash后面(你完全可以定义自己的规则不使用hash)。 微前端框架通过获取hash值,分析出是social项目的D页面。然后通过动态加载D页面的逻辑js。执行并创建页面,最终渲染。

经过上面的调整,我们统一了所有前端项目的公共库,集中到了一个项目里面进行维护。这一步统一后我们就好去提前加载微前端框架项目并执行这些公共逻辑了。要知道基础框架初始化这个过程占了项目的90%时间,这个时间节省下来了,那200ms打开就不是问题了。

接下来需要app启动后预先加载一个webview。这个webview会直接加载微前端框架项目,当app被告知要打开一个h5页面。判断端该h5页面是否支持微前端,支持的话,就给预加载的这个微前端框架发一个消息告诉他加载这个页面,并让这个预加载的webview显示。

整个流程用户感知到的时间只有加载页面的js,直接执行。体验上是非常快的。

有想法了。接下来就是找客户端兄弟配合做细节完善了。走找客户端兄弟去。

客户端改造

-w1691

客户端要做的事不复杂,直接上图,主要关注的问题如下

  1. 如何区分一个链接是微前端的,还是非微前端的(可以自行通过url域名等信息定好规则) 非微前端的项目还是走老的流程直接打开页面,微前端的项目则是给预加载好的webview发消息,并显示出来。
  2. 如何判断一个微前端框架已经加载好了,因为微前端项目最终需要接受容器的消息去加载一个页面的js,如果发消息之前微前端的容器还没加载好,那么收到这个消息也是没用的,所以一定需要微前端基础框架加载好后通知客户端,容器已经准备好了。
  3. 如果加载一个微前端页面的时候,容器还没初始化好。那就直接打开该页面。不用走微前端的形式打开
  4. 可以自行做队列控制最大初始化微前端容器的数量,来提供微前端的命中率。

具体我们内部的流程如下。

app启动就初始化一个微前端容器,容器的html加载完,并且js逻辑执行完后会告诉客户端该微前端容器可用了。这个时候如果app接收到打开一个微前端页面,那么客户端会进过一系列逻辑判断后,给微前端容器发送打开的url信息,并把该微前端容器显示出来。

该微前端框架项目收到客户端的url后,解析url里面的hash,判断是打开什么项目的什么页面,然后在配置里面去加载该页面的js,加载完后执行,并渲染页面,当一个微前端容器使用后又接着初始化一个微前端容器,方便下次使用。

核心问题思考

在web项目改造过程中,我们碰到一些问题,以及思考

项目打包改造

微前端框架核心

微前端框架改造目标:

  1. 加载基础库,并适当暴露一些api到全局,方便业务调用
  2. 通过路由信息,加载业务js,执行并渲染页面

重点说一下路由信息加载业务js的逻辑。当访问一个微前端链接 static.ymm56.com/microweb/#/… 该路径会被处理成如下信息:

  1. microweb 是微前端框架项目.
  2. shop 是业务项目名称.
  3. home是项目的页面.

microweb框架会根据shop项目加载shop项目下面的 micro.json 配置信息(micro.json内容参考下面的打包页面里的截图), 然后找到home页面对应的js。对应的js如下格式

-w698

我们可以通过简单的动态创建script的方式获取js并执行。这个时候在window对象上就有该页面的对象了。

提供一个基于vue项目加载js,并执行创建vue页面的例子。详细参考注释

loadPage () {
        // todo 判断路径是否符合规则
        let data = parseUrl()
        let key = data.path
        let startTime = new Date() - 0
        // 获取项目的配置, 就是micro.json 信息
        let config = await window.App.getConfig(data.project)
        if (!config) {
          console.log(`%c 页面加载失败`, 'color: red;font-size: 12px;')
          if (!data.project) {
            this.currPage = 'empty'
            return
          }
          this.currPage = pagePrex + '404'
          return
        }
        if (config.techType != 'vue') {
          return
        }
        console.log('使用vue路由处理页面', key)

        try {
          // Vue未注册过该组件才执行。ps:如果不加if判断,keep-alive就会失效
          if (!Vue.component(data.name)) {
          // 动态创建script,执行后window全局会有该页面对象
            await loadJs(`${getProjectBaseUrl()}/${config.pages[key].js}`)
            window.App.setCurrentProject(config)
            console.log(`%c页面资源加载时间${new Date() - startTime}ms`, 'color: #43bb88;font-size: 12px;')
            let module = window[`${config.name}_${key}`].default
            // 有些页面存在项目整个生命周期相关信息
            try {
              await window.App.onLaunch()
            } catch (error) {
              console.log(error)
            }
            let APP = window[`${config.name}_${key}`].App
            if (APP && !window.App.ProjectInfo[window.App._currentProject]) {
              // 调用app启动的生命周期方法
              try {
                APP.onLaunch && await APP.onLaunch()
              } catch (error) {
                console.log(error)
              }
              window.App.ProjectInfo[window.App._currentProject] = APP
            }
            // 动态创建页面对象,并注册到全局。然后通过动态组件进行显示
            var tempModule = Vue.extend(module)
            tempModule = tempModule.extend({
              mixins: [BasePage],
              name: data.name,
              data: function () {
                return {
                  _startTime: startTime,
                  _projectName: config.name,
                }
              }
            })
            Vue.component(data.name, tempModule)
          }
          this.currPage = data.name
          window.App.setCurrentProject(config)
        } catch (error) {
          console.log(`%c ${key} 页面配置信息加载失败`, 'color: red;font-size: 12px;')
          this.currPage = pagePrex + '404'
        }
      }

业务项目改造

业务项目改造目标:

  1. 业务项目能打包每个页面的js,这是基本能力
  2. 要提供完整的开发体验,由于业务项目不再依赖基础公共库,所以这些资源都没在业务项目里面,我们需要找到一种办法,让业务项目在开发过程中可以使用微前端框架项目来运行页面。

打包页面

以前的项目,都是按照标准的vue-cli工具,整体打包。 改为微前端后,项目的打方式需要调整。 每个项目只需要打包出来每个页面为入口的js。最终打包出来的内容如下。主要包括每个页面的js,以及一个项目的描述文件micro.json

-w1075

webpack相关配置参考:

  1. 生成micro.json 核心配置参考:
// 一个生成 micro.json 配置信息的插件
function MicroCache(options) { }
MicroCache.prototype.apply = function (compiler) {
  let cache = {};
  compiler.plugin("emit", function (compilation, callback) {
    // 在生成文件中,创建一个头部字符串:
    var filelist = "";
    let data = {
      name: pkg.name,
      techType: pkg.techType,
      version: pkg.version,
      pages: {},
    };
    // 遍历所有编译过的资源文件,
    // 对于每个文件名称,都添加一行内容。
    for (var filename in compilation.assets) {
      if (/-[a-z0-9]{8}.js$/gi.test(filename)) {
        let key = filename.replace(/-[a-z0-9]{8}.js$/gi, "");
        data.pages[key] = {
          js: `${filename}`,
        };
      }
    }
    // 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
    filelist = JSON.stringify(data);
    compilation.assets[pkg.name + "/micro.json"] = {
      source: function () {
        return filelist;
      },
      size: function () {
        return filelist.length;
      },
    };
    callback();
  });
};
  1. 独立打包文件配置参考:核心是打包成umd模块。每个模块的名称按路径定义好唯一的规则,确定唯一性。 ${pkg.name}_[name] 输出的大概就是 shop_home_index .这样当对应的js加载完后。就可以通过 window['shop_home_index'] 获取到.
output: {
      path: path.resolve(__dirname, "./dist/" + pkg.name),
      publicPath: `${BasePublicPath}/`,
      library: `${pkg.name}_[name]`,
      libraryTarget: "umd",
      filename: "[name]-[hash:8].js",
    },

独立运行

微前端业务项目在开发过程中为了方便调试。所有的页面都可以独立运行。比如开发过程访问 http://localhost:8000/microweb/#/microdemo-react/home/index ,我们不会在localhost本地部署微前端框架项目,而是通过webpack的代理服务器。把 http://localhost:8000/microweb 代理到线上的微前端框架项目上,这样进行解耦。具体webpack配置参考如下:

devServer: {
      contentBase: path.join(__dirname, "dist"),
      filename: '[name]-[hash:8].js',
      compress: true,
      hot: true,
      // 这里的配置主要是代理访问微前端的基础项目,然后对热更新的文件进行特殊处理
      proxy: {
        '/microweb/*.hot-update.js': {
          target: 'http://localhost:' + PORT, // 接口的域名
          changeOrigin: true,
          pathRewrite: { '^/microweb': '' }
        },
        '/microweb/*.hot-update.json': {
          target: 'http://localhost:' + PORT, // 接口的域名
          changeOrigin: true,
          pathRewrite: { '^/microweb': '' }
        },
        // 将'localhost:8080/microweb/xxx'代理到'https://devstatic.ymm56.com/microweb/xxx'
        '/microweb': {
          target: 'http://devstatic.ymm56.com', // 接口的域名
          // target: 'http://localhost:8080', // 接口的域名
          changeOrigin: true,
          followRedirects: true,
        }
      },
      port: PORT
    },

健康大盘

为了提高微前端项目的推动和项目的可控。我们提供了一个监控大盘来整体监控微前端项目的性能,报错,打包等信息。 下图是一个简单的大盘。

-w1486

优劣分析

优点

  1. 页面打开速度快
  2. 公共库和资源统一管理
  3. 业务项目标准化,能提供集中管控

缺点

  1. 所有项目都在微前端框架上运行,对框架的稳定性要求极高
  2. 升级和迭代微前端基础项目风险高

总结

改造后的项目能独立访问,如果在app里面可以变成加速的微前端访问。对业务开发的侵入性也非常小,老的项目升级切换也是非常平滑的。