基于gitlab CI 搭建前端页面预览服务

1,502 阅读8分钟

前端在粗放开发模式下的痛点

前端业务在近几年迎来一个很好的发展,但关于前端的基础设施并没有跟上前端业务的迅速扩展。业务扩张之后,我们不能再像小作坊一样进行粗放的开发:开发前如何快速规范的初始化项目?开发中如何保证多人高效的合作开发?开发完成后如何保证正确快速的上线?上线后如何管理诸多业务稳定的运行?围绕这些问题,笔者列举一些相关的基础设施:完整的构建打包流程/服务(统一的脚手架、上线服务等)、完整的测试环境、前端错误日志管理系统(收集、统计、报警)、前端资源离线化管理、前端资源增量下载服务以及针对Node应用的日志(完整调用链)、性能和错误监控平台等等。

其中,针对前端业务在上线前,我们一直有这样的一个痛点:基于现有项目在继续开发时,本地开发完成后,需要启动本地服务,预览给PM查看检查,但有时PM或者团队其他人员想看下效果,而自己又不方便操作电脑,就总是需要协调时间。如果自己开发完成后,可以直接上线到一个测试环境,将链接丢到群里,会非常方便别人随时预览效果。

所以,本文的目标是: 针对前后端分离的前端项目,git push 之后,能够直接推到测试环境,可以在线预览效果。

gitlab CI 简介

从v 7.1.2 之后,gitlab支持通过配置.gitlab-ci.yaml文件支持CI/CD。
具体配置可参考文档: docs.gitlab.com.cn/ee/ci/yaml/

为了使项目能够执行yaml文件中配置的task,还需要我们首先部署安装相应的环境: install runner && register runner, 参考:docs.gitlab.com/runner/#usi…。runner的执行方式有很多种, 目前最流行的就是作为一个docker容器,其内部集成了gitlab的一些基础环境, 注册阶段就是将其与gitlab主任务做关联(runner通常不跟gitlab服务器部署在同一台服务器),而yaml中配置的任务,就是在runner中具体执行, 然后将结果发送回gitlab服务器。

最后项目需要在setttings中开启enable shared runner或者specific runner.

基于Node搭建前端业务的预览服务

使用Node搭建服务,托管静态资源,以及代理请求的转发。

基本流程

  • git push
  • runner中执行yaml中的task
    • 资源构建
      针对测试环境打包: npm run build -e test
    • 上传资源到node 服务器。
      将该服务抽离为npm 包, 执行festaging-scripts命令,上传的资源有两类:
      • 构建出的静态资源
      • 必要的请求代理配置(默认读取根目录下的.festaging.config.js, 下文会解释为何需要这个)

基本流程比较好理解,但囿于公司现有基础设施的限制,一些问题变得复杂一些:

  • node服务部署是基于公司现有的容器管理方案,支持动态扩容或者销毁。node服务集群没有固定的IP,需要首先获取所有实例ip地址,然后上传静态资源;

  • 公司load balance服务是统一管理,nginx 配置不支持泛域名解析。所以针对不同项目,不能共用二级域名,如(aa.xxx.com, bb.xxx.com),只能共用一个域名,如festaging.xxx.com。但是为了区分不同的项目,我们需要增加路径信息, 如festaging.xxx.com/aa/branch1/,这样会带来两个问题:

    • 接口代理增加难度: 倘若支持泛域名解析,针对每一个项目的请求,就可以根据域名中的信息进行相应的代理(每个项目会配置其后端接口访问地址的实际域名): aa.xxx.com/api/ -> aa.config.origin/api/ ;但现在每个项目请求的接口地址仍然会是/api/**,node端如何区分是哪个项目发出的请求,进而对其进行正确转发?
    • 多路由项目的支持:node中 配置好静态资源的路径之后,浏览器输入festaging.xxx.com/aa/branch1/能够访问到该项目的主页,但是点击按钮,切换路由之后,网址就会变为festaging.xxx.com/tab2等形式,并且在浏览器中只能通过festaging.xxx.com/tab2访问到该路径,而不是festaging.xxx.com/aa/branch1/tab2, 不能保证同一个项目在url上的统一。

静态资源的上传

上面说到,需要首先获取部署了node服务的所有实例地址,然后进行上传, 如何上传呢?

  • scp: 这可能是机器间最普遍的传输方式了, 但首次连接需要ssh 认证,需要明文写密码到脚本中,而部署了node服务的的容器连接密码我们并不知道。
  • 基于http的网络服务传输:操作简单,但需要node服务提供上传接口

使用后者作为解决方案:

  • 在runner中执行的script负责: 构建-> zip -> post到node服务(这个功能抽离为npm包, yaml配置文件的script中只要执行该npm对应的命令)
  • node服务提供接口: 接受post的zip包, 解压, 移动到指定位置。

PS: koa 的async, await与操作文件时的stream配合总觉得有点tricky: 需要将stream的操作形式转为promise, 如:

function pipe(from, to, options) {
    return new Promise((resolve, reject) => {
        from.pipe(to, options)
        from.on('error', reject)
        from.on('end', resolve)
    })
}

async function processZipFiles(input, output) {
    const reader = fs.createReadStream(input);
    const upStream = fs.createWriteStream(output);
    await pipe(input, output);
}

接口代理的处理

每个项目都需要指定其真实后端的请求域名,这样才能够对项目中的请求进行转发。初次之外,还需要支持将某些接口代理到其他指定地址,如webpack dev server所支持的那样。

所以我们支持两种方式,

  • 可以指定proxy target为一个json文件,其内容格式为
    {
      proxyApi: {
        '/api/xx': 'https://www.baidu.com',
        'default': 'https://v.qq.com'
      }
    }
    

使用这种方式,还可以继续支持以后添加除了proxyApi的配置,为以后业务的扩展提供了余量。

  • 执行script 时, 配置参数 --target "https://www.baidu.com''
如何在接口请求中注入项目的相关信息?

因为所有项目公用一个域名,紧靠路径来区分不同项目,但是接口请求时却都是域名+接口进行拼接,所以我们需要针对不同项目,在其接口中添加关于项目信息的前缀:针对测试环境,在打包时,将其请求接口地址由/api/xxx改为/project1/branch1/api/xxx。但是实际修改文件中的每个地址是不现实的,我们无法准确识别哪些地方是需要添加前缀的。而前端进行网络请求的方式就两种XMLHttpRequest和fetch, 所有我们只要在html文件最前面对其方法进行改写即可。

function buildUrl(prefix) {}

var originXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
  return originXHROpen.call(this, method, buildUrl(url, '${prefix}'), async, user, password);
};

if (window.fetch) {
  var originFetch = window.fetch;
  window.fetch = function () {
    var input = arguments[0];
    
    if (typeof input === 'string') {
      arguments[0] = buildUrl(input, '${prefix}');
    }
    
    return originFetch.apply(this, arguments);
  };
}
该配置如何发送到node服务?
  • 作为文件发送,node端也从该文件夹下的所有文件读取转发配置:当该文件夹下内容变化时,重启node服务。
  • 作为参数发送,node 端收到请求后,修改内存中的变量: node端维护一个proxy config对象,收到请求后修改其内容,无需node服务重启, config 格式如下:
    {
      project1: {
        `branch1`: {
          proxyAPI: {
            '/api/xx': 'https://www.baidu.com',
            'default': 'https://v.qq.com'
          }
         }
      }
    }
    

后者显然为更优方案。

静态资源、proxy config实例化

上文提到静态资源是直接发送到每台实例,proxy config也是发送到每个node实例,然后直接修改内存中的config。倘若node服务重启,docker容器新建,这些东西不就全部丢失了吗?所以需要对其进行静态化存储,当node重启服务时,从此读取初始值。

  • 静态资源的实例化: 当做文件, 压缩成${project}_${branch}.zip存储到公司统一的文件存储服务上。
  • proxy config的实例化: 因为config其实就是个对象,所以将其存到mongodb中。

多路由业务的支持

因为团队现在统一使用react技术栈,所以对于多路由的支持就围绕react-router-dom进行。通常会使用的路由组件是BrowserRouter或者StaticRouter, 而其
basename参数可以用来对url地址添加前缀,这跟上文中我们需要的项目相关信息完全符合,所以我们可以通过修改其basename参数实现对多路由的支持。

  • fork一个react-router-dom仓库, 对其basename进行修改,然后针对测试环境构建时,添加alias,将react-router-dom resolve为我们修改后的仓库: 优点是修改足够简单,缺点也很明显:我们需要同步更新fork的仓库,以及可能对低版本支持不足。
  • 在webpack构建之后,添加插件, 解析ast,检查如果使用了BrowserRouterStaticRouter,然后修改basename的值,返回新的code。

选用方案2。 基于webpack4 提供的parser api 来解析被webpack处理过的每个module, 类似 useStrictPlugin.js实现, 只是在得到ast后再利用babel的‘traverse‘和'generate'包生成修改了basename的方法。

【参考】

gitlab CI官方介绍
当谈到 GitLab CI 的时候,我们该聊些什么(上篇)