【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

SoundCloud 前端技术团队分享开发经验

  • 2012-06-25
  • 本文字数:4479 字

    阅读完需:约 15 分钟

SoundCloud 是世界领先的基于声音分享的社交平台,每个人可以录制并上传自己的声音,同时分享给社区的好友。 SoundCloud 前端技术团队,不断通过技术的创新来提升用户体验,打造下一代单页面应用,并分享了技术实现的心得体会

下一代 SoundCloud 应用(已经在公测状态),尝试使用 HTML5 widget 实现声音播放器,未来会根据浏览器的兼容性,将老的 flash player 切换为 HTML5 widget 。前端技术实现不仅仅是 HTML5 ,构建一个坚实的底层 JavaScript 框架式是很重要的。

构建单页面应用之 JavaScript 选型

下一代 SoundCloud 应用最重要的一个特性是在不打断用户通过导航寻找其他声音的前提下,可以回放之前播放的 track(声音片段),这相当于,界面右上方总会悬浮一个迷你播放面板,每当用户想回放上一个 track,一次不刷新页面的点击就可以解决问题。这势必会鼓励用户根据当前的页面导航,不断寻求新的内容,此类行为会通过点击完成,每次点击应该保证又快又平滑。在系统层面保证又快又平滑是将下一代应用定位为单页面应用的重要原因(数据通过统一的 API 获取,前端的展现和用户点击行为,通过前端技术处理以获取更好的体验)。


图 1:悬浮按钮 [1]

在前端 JavaScript 技术框架的选型上, SoundCloud 推崇 Backbone.js ,原因除了在手机站点的实践经验外, Backbone.js 会对前端进行分层:Views(视图),Data Model(数据)以及 Collection(集合)等。剩下的业务逻辑以及组件的具体实现,会留给应用端自己处理,这就意味着应用端有非常大的灵活性。

以生成视图 Rendering Views 为例, SoundCloud 选择 Handlebars 作为页面模板库, Handlebars 与其他模版库相比有以下优势:

  • 模版内部没有具体的逻辑,便于解耦
  • 模版可以通过预编译,获取在浏览器更快的渲染性能(运行时库只有 3.3kb 大小)
  • 支持自定义 custom helpers

代码的模块化

模块化代码技术非常受用:将独立的功能,编写到独立的模块中去,并在外部显式声明模块之间的依赖关系。

SoundCloud 前端会按照 CommonJS-style modules 规范来编写代码,在浏览器执行的时候转换为 AMD modules 书写的方式。为什么这样做,通过代码解释:

// CommonJS module ////////////////
var View = require(‘lib/view’),
Sound = require(‘models/sound’),
MyView;

MyView = module.exports = View.extend({
// …
});

// Equivalent AMD module //////////
define([‘require’, ‘exports’, ‘module’, ‘lib/view’, ‘models/sound’],
function () {
var View = require(‘lib/view’),
Sound = require(‘models/sound’),
MyView;

MyView = module.exports = View.extend({
// …
});
}
);

本地开发的时候,为了提高开发效率,使用 RequireJS 分别对模块进行加载。但线上就不方便使用 RequireJS 作为模块加载器了(这会导致创建上百个 HTTP 请求),这时候更轻量级模块加载器 AlmondJS 就会派上用场(它会根据需要合并模块并打包)。

将 CSS 和 Templates 同样视为模块依赖

既然已经应用模块化的设计思想,将 CSS 以及 Templates 视为模块依赖也不足为奇了。将模版定义为模块依赖非常好理解,因为模版可以通过 Handlebars 预编译为 JavaScript 函数功能组件。而 CSS 是一个截然不同的范型:

视图会指定关联的 CSS 做显示,而且每当这个视图需要显示的时候,才会关联相应的 CSS(当然,全局 CSS 除外)。CSS 以纯 vanilla CSS 的方式书写,为了使用 RequireJS / AlmondJS 作为模块加载器加载 CSS,CSS 会被转换为功能独立的模块。这步操作需要一个构建过程:将 CSS 文本包装起来,转换为一个独立的功能,该功能返回的结果是一个 Dom 元素。

以下是将一段 CSS 代码转换为 AMD modules 的示例:

Input is plain CSS

.myView {
padding: 5px;
color: #f0f;
}
.myView__foo {
border: 1px solid #0f0;
}

Result is an AMD module

define(“views/myView.css”, […], function (…) {
var style = module.exports = document.createElement(‘style’);
style.appendChild(
document.createTextNode(’.myView { padding: 5px; color … }’);
);
})

视图也是逻辑组件

下一代 SoundCloud 应用一个很核心的理念是:将视图看做是独立的、可重用的组件。逻辑上,每个视图组件可以引用其它视图,同样被引用的视图也可以引用其它视图,以此类推。那么,整个页面就是由不同的视图组成,视图的粒度可大可小,可以小到一个按钮或者标签。

保持每个视图的独立性是很重要的,每个视图有自己的设置、数据、事件等属性,但不能改变子视图的行为和显示,甚至不能决定自己是如何被其他视图引用的。这样每个视图就是功能独立的,可以热插拔的组件。

以下一代播放按钮视图作为例子,如果想在页面某个位置放这样一个视图组件,第一步是创一个该视图的实例,并告诉它需要播放的声音 ID,至于如果播放我们无需操心。

至于创建子视图,会通过 custom Handlebars helper 在父视图中进行,示例代码如下:

{{view "views/user/user-badge" resource\_id=user.id}}

添加子视图非常简单,只需要指定模块名称,并将参数传递过去,接下来就是模版引擎解析上述模版片段:

首先模版引擎会将模版的属性、传递的参数以及对应的视图类,保存到一个临时对象中去(theTemporaryObject),并以一个唯一的、自增长 ID 做 key,接着上述模版会被替换为一段格式化的字符串:

标签是占位符,data-id 对应访问临时对象 theTemporaryObject 的 key

最后模版引擎找到占位符,替换为对应子视图的内容:

parentView.$(‘view’).each(function () {
var id = this.getAttribute(‘data-id’),
attrs = theTemporaryObject[id],
SubView = attrs.ViewClass,
subView = new SubView(attrs);
subView.render(); // repeat the process again
$(this).replaceWith(subView.el);
});

视图之间共享数据

单个页面会有许多视图,其中有许多视图会基于相同的数据,举个具体的例子 listen 页面:


图 2:声音播放面板 [2]

很明显,播放按钮、音频标题、波形图都算是页面视图,但它们都基于相同的数据——声音模型,所有人都不希望各个视图分别创建声音模型数据,必须找到一种方式在视图之间共享数据。

需要谨记的一件事是每个视图初始化的时候,很可能只有对应数据模型的 ID,而数据模型还未加载。为了解决这个问题, SoundCloud 使用一种叫实例存储的方式,具体来讲实例存储是一个 JavaScript 对象,每次通过构造函数创建爱该对象,如果构造函数参数相同,那么会返回初始的实例:

var s1 = new Sound({id: 123}),
s2 = new Sound({id: 123});
s1 === s2; // true, these are the exact same object.

看看构造函数究竟做了什么:

var store = {};

function Sound(attributes) {
var id = attributes.id;

// check if this model has already been created
if (store[id]) {
// if yes, return that
return store[id];
}
// otherwise, store this instance
store[id] = this;
}

此类方法并不是什么新花样,仅仅是工厂方法设计模式在构造函数中的应用,它完全可以写成 Sound.create({id: 123}) 的形式,但 JavaScript 给了我们这种便利,并在语义上说的通。

实例存储的方式使得视图之间共享数据变得简单,甚至视图之间不需要感知对方的存在,实例存储对象相当于一个局部的事件总线(Event Bus),负责协调 / 同步视图之间的共享状态。说的更正式一点,实例存储可以抽象为发布者 / 订阅者模式的体现,每个视图监听数据的变化,每当数据变化获得通知,并体现在页面的变化上。

而且,实例存储的方式解决了数据模型未初始化即被使用的情形,刚开始不同视图仅共享数据模型的 ID,还未加载数据。当生成第一个视图,发现数据模型还未加载数据,即刻通过 API 获取数据。数据模型会负责持续跟踪请求,因而每当其他视图请求加载数据,数据模型不会发送重复的请求。当数据从 API 获取完毕,数据模型会被更新,触发了数据变更事件,每个视图作为订阅者会被通知到。

充分使用数据

许多 API 设计的特点是,每当请求一个特定的资源,返回结果总会包含其他相关属性。就 SoundCloud 来讲,当请求一个声音的信息,返回结果总会附带创建该声音的用户:

/* api.soundcloud.com/tracks/49931 */
{
“id”: 49931,
“title”: “Hobnotropic”,

“user”: {
“id”: 1433,
“permalink”: “matas”,
“username”: “matas”,
“avatar_url”: “ http://i1.soundc …”
}
}

与其每个视图对请求结果的附带属性进行解析,不如将此类附带属性放入实例存储中,这样会带来很多好处,减少了对 API 的访问量,并加速了视图的渲染速度。

就上述示例代码,声音模型需要具备感知其附带属性(用户信息)的能力,每次视图获取 API 数据将会创建两个数据模型:声音模型和用户模型:

var sound = new Sound({id: 49931 });

sound
.fetch() // get the data
.done(function () { // and when it’s done
var user = new User({id: sound.user.id });
user.get(‘username’); // ‘matas’ – we already have the model data
});

一个很重要的概念是,不论是声音模型还是用户模型,只存在一个单一实例。并且模型本身是支持数据更新以及一致性约束的:

var sound = new Sound({id: 49931 }),
user = new User({id: 1433 });

user.get(‘username’); // undefined – we haven’t fetched anything yet.
sound
.fetch()
.done(function () {
user.get(‘username’); // ‘matas’ – the previous instance is updated
}

数据模型的资源释放

一直保持数据模型的实例是不合理的,尤其对下一代 SoundCloud 应用,用户很可能几个小时没有触发一次页面加载或者页面点击。这就意味着,浏览器端的内存资源得不到释放,所以定期清理数据模型的资源是必须的。释放资源的策略是,实例存储维护一个实例引用计数器,每当一个实例被请求一次,对应的计数器会加一。当一个实例不在被引用的时候,视图就可以释放该数据模型实例资源了。

系统会有一个定时器,每隔一段固定时间来检查实例存储中,哪些数据模型的引用计数器为零,将为零的计数器清除,同时强制浏览器垃圾回收器释放资源。引用计数器的实现如下:

var store = {},
counts = {};

function Sound(attributes) {
var id = attributes.id;
if (store[id]) {
counts[id]++;
return store[id];
}
store[id] = this;
counts[id] = 1;
}

Sound.prototype.release = function () {
counts[this.id]–;
}

定期进行垃圾回收清理工作,而不是在引用计数器会为零即刻触发的原因是,即刻触发可能导致资源浪费。比如从一个页面导航到另一个页面,有一个时间差(清理当前页面视图资源到加载新页面视图资源),实例资源的引用计数器为零,而新页面可能包含其中一个或多个实例资源,所以即刻触发释放资源的做法太浪费。

以上所讲仅仅是下一代 SoundCloud 应用的开始,后续会有更多精彩的内容,读者朋友不但可以体验全新的产品,还可以享受技术分享的圣餐。

详细内容,请关注 SoundCloud开发团队官方博客

引用

[1] http://next.soundcloud.com/

[2] http://backstage.soundcloud.com/2012/06/building-the-next-soundcloud/

2012-06-25 00:536233

评论

发布
暂无评论
发现更多内容

华为云发布《高可用网站架构云化解决方案》

爱尚科技

华为云发布《基于MetaTown构建数字资产平台》

爱尚科技

基于云原生的火山引擎边缘云应用与实践

火山引擎边缘云

分布式 云原生 边缘计算 节点 火山引擎边缘计算

译文 | A poor man's API

API7.ai 技术团队

API APISIX RESTful API

刘德华在线演唱会,火山引擎边缘云助力打造极致视频直播体验

火山引擎边缘云

云原生 边缘计算 节点 火山引擎边缘计算

【11.25-12.02】写作社区优秀技术博文回顾

InfoQ写作社区官方

热门活动

技术内幕 | 阿里云EMR StarRocks 极速数据湖分析

StarRocks

#数据库

一文读懂|2021年数据库领域精彩回顾

YMatrix 超融合数据库

腾讯云升级发布两大区块链产品,助力产业区块链数字化生态建设

科技热闻

三江学院计算机科学与工程学院举办“火焰杯”软件测试开发选拔赛颁奖仪式

霍格沃兹测试开发学社

一线大厂为什么面试必问分布式?

钟奕礼

Java 程序员 java面试 java编程

一张「有想法」的表单,玩出线上填表新花样

爱科技的水月

BSN开放联盟链“中移链”浏览器2.0正式发布!

BSN研习社

BSN 中移链

源码级解决方案一键部署,华为云Solution as Code正式上线

科技怪授

雾霾对户外LED显示屏的考验

Dylan

LED LED显示屏 户外LED显示屏

从React源码来学hooks是不是更香呢

goClient1992

React

结合RocketMQ 源码,带你了解并发编程的三大神器

华为云开发者联盟

RocketMQ 开发 华为云 12 月 PK 榜

使用 Databend 加速 Hive 查询

Databend

iOS 查找字符串出现的范围

刿刀

ios swift

三江学院计算机科学与工程学院举办“火焰杯”软件测试开发选拔赛颁奖仪式

测吧(北京)科技有限公司

软件测试 测试

WeLink&SKG,让年轻人爱上养生

i生活i科技

SEAL 0.3 正式发布:国内首个全链路软件供应链安全管理平台

SEAL安全

安全 全链路 软件供应链 SEAL

火山引擎DataTester揭秘:字节如何用A/B测试,解决增长问题的?

字节跳动数据平台

大数据 AB testing实战 12 月 PK 榜

国产开源操作系统OpenCloudOS新进展:装机量超1000万节点,合作伙伴超500 家

科技热闻

购物季订单多管理难?用WeLink轻松搞定

科技怪授

从React源码角度看useCallback,useMemo,useContext

goClient1992

React

星环科技数据中台解决方案,助力某政府机构建设新型智慧城市

星环科技

腾讯产业生态规模大、增速快、质量高,2023年将加大生态开放力度

科技热闻

WeLink助力中建西南院成功举办数字赋能培训

i生活i科技

BSN-DDC基础网络DDC SDK详细设计(七):数据解析

BSN研习社

BSN-DDC

从React源码分析看useEffect

goClient1992

React

SoundCloud前端技术团队分享开发经验_JavaScript_李湃_InfoQ精选文章