NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

评论

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

那些关于DIP器件不得不说的坑

华秋PCB

插件 DIP 元器件 PCB PCB设计

与全球开发者创新共赢,全球首个“开发者村” 正式落成

Geek_2d6073

Apache Flink ML 2.2.0 发布公告

阿里云大数据AI技术

大数据 算法 企业号 4 月 PK 榜

本铯智能科技是家怎样的共享电动车厂家?

共享电单车厂家

共享电动车厂家 共享电单车厂商 本铯智能科技 本铯智能电动车厂家

运维报表有哪些内容?有什么用?

行云管家

运维 报表 IT运维 容器化部署

YARN 远程代码执行(RCE)安全漏洞问题分析与解决方案

明哥的IT随笔

大数据 YARN 数据安全 RCE

ChatGPT 会在三年内终结编程吗?| 社区征文

神木鼎

三周年征文

和面试官聊1小时Java并发,多亏GitHub上这份笔记

做梦都在改BUG

Java 并发编程

亿级日活业务稳如磐石,华为云CodeArts PerfTest发布

华为云开发者联盟

云计算 后端 华为云 华为云开发者联盟 企业号 4 月 PK 榜

【FAQ】关于JavaScript版本的华为地图服务Map的点击事件与Marker的点击事件存在冲突的解决方案

HMS Core

HMS Core

从源码全面解析LinkedBlockingQueue的来龙去脉

做梦都在改BUG

Spring Boot 整合 Redis 基于 Stream 消息队列 实现异步秒杀下单

做梦都在改BUG

ByteHouse云数仓版查询性能优化和MySQL生态完善

NineData

数据库 架构 字节跳动 Clickhouse bytehouse

eBPF的发展演进---从石器时代到成为神(二)

统信软件

Linux Kenel 内核 Linux内核

麻了,不要再动不动就BeanUtil.copyProperties!

做梦都在改BUG

HashData认证云原生数据仓库管理工程师培训报名开启!

酷克数据HashData

阿里限量的性能调优+微服务+高并发设计,真的太香了!

做梦都在改BUG

Java 微服务架构 系统设计 性能调优 亿级并发

聊聊 IP packet 的 TTL 与 tcp segment 的 MSL

明哥的IT随笔

TCP/IP TTL MSL

堡垒机英文是什么?有哪些品牌?

行云管家

网络安全 堡垒机

Linux常用命令

追赶者

进程 SSH Liunx 端口占用

阿里技术官神作!大厂亿级流量性能调优学习手册,堪称保姆级教学

做梦都在改BUG

Java 性能优化 性能调优

合合信息新推出反光消除技术,助力手写文字识别更精准

合合技术团队

人工智能 文字识别 扫描全能王 反光去除

读书笔记丨远程服务调用和RESTful,如何分析和抉择?

华为云开发者联盟

开发 华为云 华为云开发者联盟 企业号 4 月 PK 榜 远程服务调用

机器学习分布式框架Ray

AIWeker

Python 分布式 python小知识 三周年连更

裸辞底气!GitHub飙升“java面试笔记2023” 了解下八股文天花板

Java你猿哥

Java 面试 Spring Boot ssm 八股文

逆天!腾讯大神纯手撸“架构师速成手册”Github狂获4.5kstar

Java你猿哥

Java 架构 ssm 架构设计 架构师

聊聊 Zookeeper 的 4lw 与信息安全

明哥的IT随笔

zookeeper 数据安全

阿里新一代微服务,内部大佬手抄的笔记+脑图不容错过,全是精华

做梦都在改BUG

Java 架构 微服务 Spring Cloud Aliababa

Apifox WebSocket 调试功能你会用了吗?

Apifox

程序员 接口 websocket API API 调试

蚂蚁安全科技 Nydus 镜像加速实践

SOFAStack

开源 镜像 镜像安全 OCI Nydus

区块链基础设施 NFTScan 新增支持 Aptos 网络

NFT Research

区块链+ NFT

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