关注 前端瓶子君,回复“加群”

加入我们一起学习,天天进步

作者:康东扬

https://zhuanlan.zhihu.com/p/91386560

本文是《滨江前端技术沙龙》的分享主题《网易云音乐前端模块动态下发系统》,主要介绍了团队的一个内部系统,这是我们在实际生产中遇到痛点时的一个解决方案。

当前的前端代码部署流程

前端工程的开发流程大概如下图:

  • 本地进行业务开发、打包构建配置(webpack);

  • 线上构建机进行代码打包构建;

  • 部署系统将新的代码部署到线上服务器;

流程比较中规中矩,和现在的大部分团队应该是同一个模式,前端代码经过重新构建打包后,文件名中的 hash 发生了变化,从而让客户端不受缓存影响,可以获取到最新的代码。

寻找痛点

前端是一种技术问题较少、工程问题较多的软件开发领域。

单从一个应用的迭代维护来看,上面的流程并没有什么不妥,但是在云音乐,除了主站应用外,还有各业务线应用和各种 H5 应用。这些全部加起来可能有上百个。

现在,我们做个简单的数学:假设一次上线从审批到构建打包再到部署上线所需要花费的最短时间为 30 分钟,那 100 个应用都上线所要花费的时间:

100 * 30 = 3000 分钟 = 50 小时 = 2.08 天

然而其中很多时候并不值得花这么多时间来上线,例如:更改配置中心的某个配置、针对 Web APM 的 SDK 的升级、修改帮助手册文案等。

如果有个办法可以进行快速上线,而不是全部走严格的上线流程,那么日积月累的节省的成本将是一个可观的收益。

新模式:直接下发代码

面对痛点,我们尝试了一种新的模式:让某个脚本拥有动态的特性,开发人员通过界面操作可以对这份脚本的内容进行更改,而从直接更新线上代码。

我们看看下面的两张图片,它们是利用下发系统接入错误上发平台 sentry 的步骤图:

  • 界面上针对某个应用填写一些 sentry 平台所需要的表单,这些值用于 sentry 的 SDK 初始化;

  • 平台上点击发布代码按钮直接更新代码。

在之后如果有版本、配置的更新,直接在系统上重新配置并下发即可。

该模式操作起来简单明了,最重要的是,代码更新的时间大大缩减,可以缩短至 5 分钟,甚至实时更新。

5分钟是目前我们的一个实践值,这个时间是自己决定的,具体细节后面解释。现在我们就来为下发 web 前端代码这个魔术进行揭密。

踏出勇敢的一步:脚本 API 化

浏览器在向服务端请求一个脚本的时候,浏览器拿到的只是一份文本而已,只是,这时服务端会在 response 里面告诉浏览器,这是个脚本,所以浏览器就会把返回结果当作脚本进行编译执行。

根据这一点,我们可以在 script 标签引用一个脚本的时候,直接填写 API 的 url:

<script src="/api/script/<This is an appId>"></script>

然后,在后端的接口路由中定义如下:

// 这是 koa | eggjs 的写法
router.get('/api/script/:appid', controller.script.get);

最后,在 controller 中组织脚本内容并返回:

module.exports = class extends Egg.Controller {async get() {const { ctx } = this;// 获取到应用的IDconst appid = ctx.params.appid;// 不要缓存ctx.set('cache-control', 'max-age=0');ctx.code = 200;// 脚本内容自由组织ctx.body = 'console.log("Hello Code Puzzle");';// 告诉浏览器这是个脚本ctx.type = 'application/javascript';}
}

这里有几个点需要理解:

ctx.set('cache-control', 'max-age=0'):为了浏览器每次访问都拿到最新的代码,需要告诉浏览器不要对返回结果进行缓存;

ctx.body = 'console.log("Hello Code Puzzle");':这个是你组织代码的地方,实际业务中,代码不会这么简单,后面也会简单讲解下我们的做法;

ctx.type = 'application/javascript':有了这个,浏览器才会把返回结果当作是脚本,而不是普通的字符串。

原理很简单,实现起来也不难,但是实际生产中的情况往往没这么简单,这里就有个被我们忽视的问题:稳定性问题。

上述的方法在把脚本当作一个 API 的同时,也直接将服务暴露在用户面前,在面对大流量访问或者恶意攻击的时候,你的服务就会显得弱小无助,甚至直接崩溃。

所以,我们需要一个稳定性方案。

稳定性方案

为了不让服务直面客户端,我们可以在服务前面设立一道防线,现在常见的做法有:

  • API:接入 Gateway 网关;

  • 静态资源:放入 CDN。

这里虽然我们将脚本 API 化,但是其本质还是返回脚本,访问是不需要登录或者权限的,所以这里采取 CDN 更合适,架构如下:

CDN 的两大特性为缓存和回源,利用回源机制,我们可以在脚本服务面前嵌入 CDN,主流程主要分为 3 大部分:

客户端访问脚本:脚本地址不再是 API,而是 CDN 域名的一个脚本地址,当访问 CDN 上的脚本时,命中缓存则直接返回给客户端,如果没有命中缓存,则触发回源;

回源机制:事先配置 CDN 回源到某个服务,我们就可以在这个服务中去组织脚本内容,和上述的 controller 代码一致;

脚本内容获取:根据用户在平台的操作,组织代码,并将脚本内容存入 MySQL 数据库中。为了 CDN 回源时提升读写速度,我们会将脚本内容备份在 Redis 中,MySQL 相当于一个 fallback。

在这套方案中,需要注意的一个点是,客户端访问的脚本地址仍然是一个不变的地址,所以返回脚本不能是永久缓存,需要设置一个较短的时间。

这就是上述所说的 5 分钟的由来,5 分钟是一个可接受的实践值。

接入了 CDN 后,我们也就直接地享受了 CDN 的好处:

  • 根据缓存和回源特性,拦截住了大部分流量、缓解了源站的压力;

  • 因为 CDN 服务在全国各地都有节点,可以有效解决跨地域访问问题,降低访问延时;

  • 利用 CDN 强大的计算能力,可以拦截恶意攻击、降低“广播风暴”的影响。

管理组织下发的代码

界面操作 = 操作 schema

在界面上操作配置一个模块的时候,在背后相当于操作一份 schema,如下图:

在应用引入动态脚本

最终所有模块的 schema 会整合到一个 SDK 中,这个 SDK 就可以根据不同的模块数据进行特定的逻辑处理,SDK 的代码结构如下:

(function(modules) {modules.forEach((module = {}) => {const { name, vars } = module;// 逻辑});
})([{ name: 'sentry', vars: [ ... ] },{ name: 'wapm', vars: [ ... ] }
])

代码中,所有的应用 SDK 的函数主体都是一样的,只有传入的 modules 数据根据用户操作而发生更改。

最后在应用中引入这个 SDK 脚本即可:

<script src="https://cdn.net/appid.js"></script>

不同的处理类型

在 SDK 中,我们会根据 vars 中的不同处理类型进行不同的操作,例如:插入脚本、插入 HTML、定义变量等,可以根据不同的业务需求进行定制。

插入脚本:

schema 为:

{"fn": "script","url": "https://music.163.com/hello.js","async": true,"onload": "console.log('script onloaded')"
}

SDK 对应的处理函数为:

function execScript(params) {const oElem = document.createElement('script');oElem.src = params.url;oElem.async = params.async;document.body.appendChild(oElem);if (param.onload) {oElem.onload = function() {const oScript = document.createElement('script');oScript.innerHTML = params.onload;document.body.appendChild(oScript);}}
}

当然,其中还省略了 onload 函数中的一些用户填写的变量的替换,这样就可以在启动脚本中初始化一些公共技术模块的 SDK。

定义变量:

schema 为:

{"fn": "var","name": "key","value": "2bc5b9f43eaf407fa80f4309082a44eb"
}

SDK 对应的处理函数为:

function execVar(params) {variables[params.name] = params.value;
}
插入 HTML
{"fn": "html","selector": "body","content": "<div>container</div>"
}

SDK 对应的处理函数为:

function execHtml(params) {const container = document.querySelector(params.selector) || document.body;const oElem = document.createElement('div');oElem.innerHTML = params.content;container.appendChild(oElem);
}

通过对不同的行为进行抽象,我们就可以根据一份 schema 来完成各式各样的操作,从而达到我们的目标。

系统优化

到这里,我们已经做到了代码的动态下发,但是针对当前的设计,我们仍有优化空间。

层级配置

现在我们已经可以不用每次更改都走发布上线流程了,但是当我们的通用模块发生更改,例如 sentry 的域名发生变更,那么对于每个应用我们都需要在下发系统中进行配置操作,100 个应用就有100 次操作,而这些操作都是一模一样的,是冗余的。

针对这种情况,我们增加了层级配置的能力:

在系统中,增加了项目这个概念,可以把它当作是文件夹,项目下有着许多的应用,当我们需要配置公共配置的时候,只需要在项目级别进行操作,那么系统就会自动将配置合并到应用中并完成代码下发操作,合并配置时可以想成是:

Object.assign({}, projectConfig, appConfig);

连接部署系统,整合公技模块

即使有了这套下发体系,但是我们在使用不同的公技平台的时候仍然存在一些不方便,在云音乐公共技术平台主要有:

  • sentry:错误上发平台

  • wapm:前端监控平台

  • 鹰眼:代码检测平台

  • 前端的部署系统

  • 下发系统

每种公技平台都是相互独立的,所以在使用上也是有着不同的方法,这也就导致了开发者在创建一个新应用的时候会出现:

  • 在部署系统创建一个应用

  • 在 sentry 平台创建一个应用,获取密钥

  • 在 wamp 平台创建一个应用,获取密钥

  • 在下发系统创建一个应用并配置 sentry、wapm 等模块

对于这种繁琐、重复的操作我们经过抱持着零容忍的态度,而在云音乐,部署系统可以说是一切应用的集合点,所以我们可以利用它来做一些事情:

  • 在用户创建应用的时候自动在各个公技平台创建相同的应用;

  • 部署系统将各公技平台的初始化配置发送给下发系统;

  • 部署系统自动注入动态脚本。

总结

前端模块动态下发系统是云音乐应用数量达到一定程度后寻求的一种提升效率的解决方案,在未来可能会往开源方向前进,如果你有什么好的点子,也欢迎一起共建。

The End

1. ❤️玩得开心,不断学习,并始终保持编程。????

2. ????点击原文,查看更多精彩文章!????

3. 如有任何问题或更独特的见解,欢迎联系瓶子君!(关注公众号,回复 123 即可)????????

网易云音乐前端模块动态下发系统相关推荐

  1. Python网易云音乐爬虫大数据分析可视化系统——大屏数据可视化开发之路

    介绍 现在比较流行的大数据数据可视化都是大屏,有钱的人会使用阿里云全家桶的DataV或者商业化的大屏解决方案,但是在国内还是小公司比较多,本人50年大数据开发经验,精通数据可视化,曾经处理过百万亿级别 ...

  2. 网易云音乐前端实习二面

    (base:杭州,时长40min,一面居然给我过了) 1.position所有属性 2.absolute相对于哪些元素定位 3.fixed和absolute区别 4.sticky有了解吗? 5.浏览器 ...

  3. 2019届网易云音乐前端实习生电面心得

    电面进行了38分钟,虽然总的来说回答的并不是很好,但是也初步了解了一线技术岗对前端岗位的需求和要求,收获很大. 之前学习前端的心一直飘来飘去,没有静下来学习基础知识和深入理解原理,只停留在对工具框架的 ...

  4. Markdown插入网易云音乐播放模块

    核心代码: <iframe frameborder="no" border="0" marginwidth="0" marginhei ...

  5. 网易云音乐的前端基础设施是如何优雅地走向统一的

    点击上方 前端Q,关注公众号 回复加群,加入前端Q技术交流群 作者|葛星 编辑|黎安 网易云音乐(以下简称"云音乐")的前端团队大概在 4 年前初具规模,4 年多的快速发展过程当中 ...

  6. 网易云音乐前副总裁教你如何做产品

    本文为PMCAFF专栏作者Jinkey出品 前言 一直以来都不建议入行的人看产品方面的书籍,而是自己多时间思考分析,多参与项目事件.太多理论没用项目经历支撑是理解不了的,很多人会讲粤语的人,大都未必清 ...

  7. 网易云音乐前副总裁教你如何做产品 —《幕后产品》读书笔记

    原文链接 jinkey.ai/post/produc- 前言 一直以来都不建议入行的人看产品方面的书籍,而是自己多时间思考分析,多参与项目事件.太多理论没用项目经历支撑是理解不了的,很多人会讲粤语的人 ...

  8. Day08_vant实现_网易云音乐案例

    Day08_vant实现_网易云音乐案例 文章目录 Day08_vant实现_网易云音乐案例 知识点自测 铺垫(自学) 本地接口项目部署 今日学习目标 1. 案例-网易云音乐 1.0 网易云音乐-本地 ...

  9. vue2中vant实现网易云音乐案例-附带所有源码

    vue2中vant实现网易云音乐案例-附带所有源码 前言 学习笔记以及源码下载gitee: https://gitee.com/xingyueqianduan/vantmsicdemo 下载下来的内容 ...

最新文章

  1. java.lang.String_自己写的java.lang.String可以让jvm加载到吗?
  2. Quartz.net 开源job调度框架(二)----定点执行
  3. Android ---- Context
  4. LNMPA(LNMP0.7)安装出现502 Bad Gateway解决方法
  5. Hadoop优势,生态圈
  6. mysql 设置 0、1 用什么数据类型_MySQL数据库教程-数据表字段约束
  7. Java同步—线程池的创建和使用
  8. Ubuntu18.04 安装nextcloud
  9. 前端—每天5道面试题(2)
  10. 【poj 2891】Strange Way to Express Integers(数论--拓展欧几里德 求解同余方程组 模版题)...
  11. web安全day2:NTFS安全权限
  12. Zabbix3 ——Server端的安装配置小结
  13. RoboWare Studio入门教程(一)
  14. Fast is better than free: Revisiting adversarial training
  15. Android植入Wooboo广告教程
  16. DASCTFNepCTF 部分writeup
  17. 语义计算_语义多态性如何在量子计算中起作用
  18. Tomcat调优指南
  19. note pro 国际版_改装Redmi Note 8 Pro —一次冒险
  20. SpringBoot配置全局异常捕获

热门文章

  1. 《神经网络与深度学习》邱希鹏 学习笔记 (1)
  2. 如何打开CMD以及简单Dos命令
  3. 什么是RTK?GPS导航和RTK的基本原理有什么不同?
  4. 【数据分析】- 游戏业务常用指标
  5. Excel学习日记:L1-excel入门
  6. 浏览器不能下载response输出的excel
  7. 网络安全系列-三十一: 网络攻防之红队快速入门
  8. 基金的估值原来这么简单,一文看懂
  9. 静态博客网页中的网易云音乐播放器
  10. 历史名酒元氏益成永——宋曹贡酒