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书写的方式。为什么这样做,通过代码解释:
双击代码全选 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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({
// ...
});
}
);
- 按照AMD modules规范,define的书写很烦琐
- 模块之间的依赖关系会重复声明,也很容易犯错
- CommonJS-style modules到AMD modules的转换,很容易自动化
本地开发的时候,为了提高开发效率,使用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的示例:
双击代码全选 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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在父视图中进行,示例代码如下:
双击代码全选 1
2
3
<
div
class
=
"listenNetwork__creator"
>
{{view "views/user/user-badge" resource_id=user.id}}
</
div
>
添加子视图非常简单,只需要指定模块名称,并将参数传递过去,接下来就是模版引擎解析上述模版片段:
首先模版引擎会将模版的属性、传递的参数以及对应的视图类,保存到一个临时对象中去(theTemporaryObject),并以一个唯一的、自增长ID做key,接着上述模版会被替换为一段格式化的字符串:
双击代码全选 1
2
3
<div class=
"foo"
>
<view data-id=
"123"
></view>
</div>
<view>标签是占位符,data-id对应访问临时对象theTemporaryObject的key
最后模版引擎找到占位符,替换为对应子视图的内容:
双击代码全选 1
2
3
4
5
6
7
8
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]
发表回复