前端部署流程之「插件化设计」

3,029 阅读8分钟

哈喽大家好啊。好久没更新前端发布平台实战系列文章了,之前写的专栏文章基本全栈式的实现一个前端发布平台了,简陋归简陋,能用就行。那这次,我们来聊聊整个部署流程的插件化设计,看看怎么更好的整一个发布平台...

本文主要提出一个插件化构建流程的想法, 笔者也还没有具体实现,所以本文侧重于设计的思考部分,想直接看实战、看源码的同学可以再等一等!笔者预想着等后面自己实现了插件化部署,重构了当前的部署流程的时候再会分享一篇「插件化部署实战」的文章。

不知道是不是已经有一些小伙伴顺着笔者之前的思路开工了...但,笔者告诉你没有关系,开工了就随他去吧,等以后出现问题了再重构,也不是不行, KPI 又有了。那还没开工的小伙伴可以结合笔者这篇文章的思考再考虑动工哈,不过这样以后就少了一次重构混 KPI 的机会了~

一、前要回顾

废话不多说,我们赶紧进入主题。首先先回顾一下前端发布平台是个啥,笔者掏出了之前画的一张图:

前端链路.png

没错,这基本上就是一个前端项目从 源代码 到 线上生产 的大体流程。因为是一个完整流程,涉及的步骤点比较多,大家有兴趣再细致了解即可,而我们本文需要关注的就是核心的构建流程。也就是:用户在前端界面点击构建按钮之后发生的一系列流程

那么紧接着,我们细致分析一下之前核心构建的流程有哪些:

  1. 构建前。生成、更新构建配置(job)。
  2. 构建中。调用 jenkinsopenApi,执行 job
  3. 构建完成。得到构建产物 dist 包。
  4. 部署。将 dist 包传到生产服务器。

想要详细了解的话,可以关注笔者的专栏:实战前端发布平台,打开CICD黑盒,本文就不过多展开了。

根据这个步骤,笔者撸了个图:

build.png

看到这个图,不知道大家是否想起了一个熟悉的身影——webpack,哈哈哈,有那么一丢丢相似吧。然后笔者也特地在上述图中加了些黄色的虚线箭头在每一个流程向下个流程的跨度阶段,现在没 get 到也没关系,笔者后面会在这个图的基础上再画点东西。

好了,那笔者之前实战发布平台的时候,也是按照这个步骤,通过对过程的具体实现来做了个简易的前端发布平台,那这样做会有什么问题、隐患呢?我们接着往下看。

二、遇到问题

其实针对过程的具体实现去写代码本问题不大,功能也能实现是吧。并且相信大多数开发被淹没在各种业务代码里,日常开发中也是如此针对过程编码,确实问题不大。如果真要说问题所在,那就是项目持续发展后的扩展性、维护时的心智负担的深浅程度了。

“说废话没有用,让对面也...”,好我们不说废话,直接上例子。现在我们有一个 publish 函数,他一开始就是一个纯净的函数,只负责把产物 dist 推到属于他的生产服务器。

function publish (url: string) {
  // 假设运维提供了一个 api 给我们调用去推送产物
  rsync.run(url)
}

问题1: A业务线需要在推送完成时通知消息群,但其他业务同学不需要。现在让我们来面向过程地改动代码如下

async function publish (url: string) {
  // 假设运维提供了一个 api 给我们调用去推送产物
  await rsync.run(url)
  // 如果有配置 notifyGroupChart,需要通知到聊天群(如企微)
  if (config.notifyGroupChart) {
    企微.notify()
  }
}

问题2: B业务线需要在推送前下载 dist 包到本地,但其他业务同学不需要,我们接着改造

async function publish (url: string) {
  // 下载产物
  if (config.download) {
    download()
  }
  // 假设运维提供了一个 api 给我们调用去推送产物
  await rsync.run(url)
  // 如果有配置 notifyGroupChart,需要通知到聊天群(如企微)
  if (config.notifyGroupChart) {
    企微.notify()
  }
}

问题3: C业务线需要做离线缓存,在上传完成后通知 App 去对应的地址下载 dist 包以加速 h5 加载,但其他业务同学不需要,我们接着改造

async function publish (url: string) {
  // 下载产物
  if (config.download) {
    download()
  }
  // 假设运维提供了一个 api 给我们调用去推送产物
  await rsync.run(url)
  // 如果有配置 notifyGroupChart,需要通知到聊天群(如企微)
  if (config.notifyGroupChart) {
    企微.notify()
  }
  // 离线缓存
  if (config.offlineCache) {
    appServer.cache(url)
  }
}

好了,历经了三个业务同学的业务需求后,publish 函数由一开始的纯净变得浑浊了...讲得专业一点,就是 耦合业务 了。

别看笔者的 demo 看起来好像也没多少代码,等你抛开所有注释,没有抽离函数调用而是直接写逻辑时,你会发现代码非常的臃肿。比如笔者搞点伪代码并去掉注释:

async function publish (url: string) {
  if (config.download) {
    const file = fs.createWriteStream(`logo.png`); 
    res.pipe(file); 
    file.on('finish', () => { 
      file.close(); 
      console.log(`File downloaded!`); 
    });
  }
  
  await rsync.run(url)
  
  if (config.notifyGroupChart) {
    xxx
    xxxxx
    xxxxxx
  }
  if (config.offlineCache) {
    bbb
    bbbbb
    bbbbbbbb
  }
}

这是不是就有点熟悉了呢,回归到正常业务项目的代码风格了。好了,如果是这样的话,或许你该有点蛋疼了吧...

所以,如果我们只以面向过程的方式来设计、编写项目构建的过程,我们一定无法避免项目功能扩展、迭代进化后带来的代码量增多、代码结构混乱、基础功能业务功能耦合严重的问题。这样其实是很不利于项目的发展壮大和后期维护的...当然,如果出现了,那也就说明!混 KPI 的机会来了~

三、插件化设计

上面介绍了面向过程开发 部署流程 的问题,最主要的就是:耦合业务逻辑。所以,我们要解决的问题也很明确了,就时将 业务 与 基础功能 解耦

我们无法避免做基础功能的时候要支撑各种业务需求,所以我们要满足业务功能的同时,保证整个构建的核心流程纯净、清晰。这样的好处就是保证了基础功能的日常的 bug 排查轻松;构建过程的修改、升级轻松;更小的出错成本和回归成本;其他项目成员更小的心智负担来维护发布平台。

其实插件化也不是什么难的事情。笔者认为,只要实现了可插拔的机制,就是插件化设计。比如最简单的一点就是 事件机制。平时面试被问 发布订阅观察者模式 都给问麻了吧?既然应试的时候学了这些设计模式,不如就在实战中应用起来~

笔者对上述的 publish 代码进行一个小改造:

async function publish (url: string) {
  event.emit('beforePublish')
  // 推送产物
  await rsync.run(url)
  event.emit('afterPublish')
}

改得很简单,就通过事件机制,在 执行推送前执行推送后 加了两个钩子,对外进行事件通知。那现在,如果业务需求要在这些节点加些业务逻辑,那我们可以把处理业务的代码通过 事件监听 的方式去实现,就不用一把梭到 publish 函数里面了。

业务A:

event.on('afterPublish', function () {
  xxx
  xxxxx
  ... 我管你写多少行都行
  企微.notify()
})

业务B:

event.on('beforePublish', function () {
  const file = fs.createWriteStream(`logo.png`); 
  res.pipe(file); 
  file.on('finish', () => { 
    file.close(); 
  });
})

笔者不一一列举了,大家能 get 到点就行了。那到这里,笔者就上面画的一个流程图,按照插件化的模式进行一个扩展:

build-plugin.png

根据图中的划分,很清晰的可以看到构建流程保持了原有的纯净,业务逻辑通过插件的形式去扩展构建功能,丝毫不影响前端构建的主流程。是不是就有点 webpack 那味了呢?我们可以在 webpackvite 等一些插件化机制的钩子上去扩展很多很多自定义的需求、功能,所以我感觉我们也可以把这种设计应用到发布平台的实现中。

当然啦,笔者最终也还没有具体的代码实现,目前也只是停留在设计的想法、构思阶段。毕竟我现在还在纠结要不要考虑同步异步的问题,当前的业务场景还没有说某些业务逻辑需要 同步、异步 在一些构建流程中执行的...如果说考虑上同步异步执行,可能就要设计成 compose 那种模式了...

写在最后

其实很多时候,在做前端开发的时候更多都是面向过程的,面向对象都比较少了,更别说考虑到一些设计了。所以即使初始阶段的成品没有设计,有一定的局限性,也是正常的。

笔者个人感觉,开发最核心的就是解决当下问题,尽量做到点到为止就好,无需过度设计,因为个人感觉很多大鸿图的项目不一定能到达那个需要这样设计的未来,说不定一个阶段走不下后就停了。如果可以发展起来,并且发展到后期实在不行了,那就重构呗,相信这应该是大部分项目的发展历程了。

想在文章的末尾分享一篇 antfu 的文章:关于 Yak Shaving 。有些时候确实点到为止就好。

最后,感谢你的阅读~

本文正在参加「金石计划」