写点什么

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:537127

评论

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

Nginx负载均衡配置、限流配置、Https配置详解

C++后台开发

nginx 负载均衡 HTTP 后端开发 C++开发

集世界杯+GameFi元素的MetaElfLand,为何将在世界杯期间爆发?

鳄鱼视界

华为再次入选2022年Gartner® SIEM魔力象限

科技怪授

华为云

Baklib帮助中心:自助服务指南

Baklib

客户服务 帮助中心

大厂数据开发老司机送给数据工程师的10条建议,建议先收藏再细品!

雨果

数据开发

不懂“数据服务”,聊什么“数据中台”

雨果

数据中台 数据服务

5G+云渲染,助力虚拟仿真医学实训

Finovy Cloud

云渲染

MemArts :高效解决存算分离架构中数据访问的组件

华为云开发者联盟

云计算 后端 华为云

全面焕新|详解 Grafana v9.0.x 新增功能特性

阿里巴巴云原生

阿里云 云原生 Grafana 新功能

OneFlow-ONNX v0.6.0正式发布

OneFlow

人工智能 深度学习

助力双12,Quick Stock-瓴羊交易9.9元畅享打单发货

瓴羊企业智能服务

云小课|帮您高效快速上传组件至私有依赖库

华为云开发者联盟

云计算 后端 华为云

华为云安全亮相世界互联网大会

科技怪授

华为云

Stable Diffusion半秒出图;VLIW的前世今生;YOLOv5全面解析教程 | AI系统前沿动态

OneFlow

人工智能 深度学习 VLIW

集世界杯+GameFi元素的MetaElfLand,推出世界杯专场活动

小哈区块

【论文复现】Panoptic Deeplab(全景分割PyTorch)

华为云开发者联盟

人工智能 华为云 论文复现

华为云GaussDB打造金融行业坚实数据底座,共创数字金融新未来

清欢科技

offset新探索:双管齐下,加速大数据量查询

IT科技苏辞

NFT盲盒链游DAPP系统开发搭建技术

薇電13242772558

web3

如何在 Rocky Linux 上安装 Apache Kafka?

wljslmz

Apache kafka 11月月更

大规模 Spring Cloud 微服务无损上下线探索与实践

阿里巴巴云原生

阿里云 微服务 云原生 Spring Cloud

一文详解GaussDB(DWS) 的并发管控和内存管控

华为云开发者联盟

大数据 后端 华为云

PMO推行制度落地陷入困境怎么办?

PMO实践

项目管理 PMO 项目经理

二面被RocketMQ虐后,狂刷这套实战到源码手册,再战阿里

钟奕礼

Java Java 面试 java程序员 java编程

全球6位IT负责人解读数字化转型不断失败的原因

雨果

数字化转型

2023年值得学习的云计算技术有哪些?

wljslmz

云计算 11月月更

万恶的strpos函数

J.Smile

自学php

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