在上一篇文章中,我们已经分析了Backbone在组件化开发上的不足,以及如何使用打包手段弥补这些不错。接下来我们来逐步通过一个例子来讲解优化的过程
1. 场景假设
假设我们需要使用Backbone编写这样一个输入框组件,它有一个input和一个label组成。当用户在input中输入文字,并且触发blur事件时,输入的文字会在label中显示。如下图所示。
我们还需要一个tab组件,tab组件上有多个tab切换按钮,点击不同的按钮能打开对应的tab页。每个tab页中可以嵌套任意组件,例如嵌套上面的输入框组件。如下图所示。
2. 项目结构
项目中包含myInput和tab两个组件。myInput组件中包含5个文件,我们挨个来看。
2.1 tpl.htm
tpl.htm是组件的模板文件,例如underscore模板文件。
2.2 model.js
model.js是Backbone中的Model实例。
2.3 view.js
view.js是Backbone中的view实例,view.js中会引用tpl.htm。
2.4 index.less
组件样式文件
2.5 index.js
组件的入口,组件入口会引入index.less、view.js、model.js。
3. 个文件解析
3.1 tpl.htm
这里的模板包含一个input元素和用于展示数据的value
<h3><%=title%></h3>
<div>
<strong><%=label%>:</strong></strong><input class="my-input" type="text" name="firstname" placeholder=<%=placeholder%> />
</div>
<div>
<strong><%=word%>:</strong><%=value%>
</div>
3.2 model.js
model文件没有什么特殊。
var model = Backbone.Model.extend({
defaults: function () {
return {
title: "title",
label: "label",
placeholder: "placeholder",
word: "word",
value: ""
};
},
});
3.3 view.js
这里的view.js需要通过underscore-template-loader加载模板文件。通过这个loader实现模板在代码打包阶段的预编译。loader的配置详见第四章。
// 通过underscore-template-loader加载模板文件
var tpl = require("./tpl.htm")
var view = Backbone.View.extend({
className: 'input-module',
template: tpl,
events: {
// 监听input元素的chang事件,并修改model中的value值。
'change .my-input': 'valuechange'
},
valuechange: function(event) {
var newValue = event.currentTarget.value;
this.model.set('value', newValue);
},
render: function() {
this.$el.html(this.template(this.model.attributes));
if (this.model.get('value') != '') {
this.$el.find('input').attr('value', this.model.get('value'));
}
return this;
}
});
3.4 index.less
index.less通过less-loader加载到js中,这也就解决了样式文件和组件分离的缺点。
.input-module {
font-family: 'Microsoft YaHei';
}
strong {
color: gray;
}
3.5 index.js
index.js通过引入model、view、模板文件、样式文件,把一个组件所有需要的资源整合了起来。并对外提供组件初始化和配置的接口。
所以外部只需要引入组件的入口文件,并进行配置,就可以加载组件的所有资源。这样就做到了组件资源的集中管理。
var model = require("./model"),
view = require("./view");
require("./index.less");
function myInput() {
}
// 对外初始化接口
myInput.prototype.init = function(title, label, placeholder, word) {
this._model = new model({
title: title,
label: label,
placeholder: placeholder,
word: word,
value: ""
});
this._view = new view({
model: this._model
});
this._view.render();
}
// 设置组件标题接口
myInput.prototype.setTitle = function(title) {
this._model.set('title', title);
}
// 获取Dom
myInput.prototype.getView = function() {
return this._view;
}
module.exports = myInput;
4 优化过程的解析
4.1 underscore-template-loader
在view.js中我们通过underscore-template-loader
实现了在代码打包阶段的模板编译,这项优化可以节省浏览器的编译模板所需要的时间,从而加快模块的加载。
loader的配置
module: {
loaders: [
{
test: /\.htm$/,
loader: "underscore-template-loader",
query: {
// 这个配置可以在最终渲染的模板中加入注释,此处使用模板的文件路径作为注释
prependFilenameComment: __dirname,
}
}
]
}
更详尽的配置可以在github中找到
4.2 less-loader
为了把模块的样式和模块js打包到同一个组件模块中,我们可以使用less-loader
。less-loader
可以在代码打包阶段把less文件转义为html内联样式,并通过js函数将内联样式插入到页面中。less-loader
的配置并不难,可以自行查看文档。这里只贴出基本的配置。
module: {
loaders: [
{
test: /\.less$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
{ loader: 'less-loader', options: { strictMath: true, noIeCompat: true } }
]
}
]
}
4.3 整合view与model
当我们引入一个组件时,我们通常希望组件提供一个入口文件,组件的使用者只要引入入口文件就可以调用组件的所有资源。然而一个Backbone组件通常由view和model构成,这使得组件的引用变成头疼的事。
所以我们可以提供一个入口文件,这个入口文件会引入组件所需要的view、model、样式文件等。入口文件对外提供init
接口对view和model进行初始化,提供setXXX
接口实现对model的修改,提供getView
接口返回组件渲染结果。这样我们就可以通过如果文件操作Backbone组件。
Demo请看上一节中的index.js
。
4.4 组件的嵌套
在Backbone中实现组件嵌套的基本思路是先渲染父组件的Dom,然后将子组件的Dom插入到父组件的节点中。因为每个组件都提供getView接口用于返回组件的Dom,所以父组件可以调用这个接口嵌入任意组件。
我们可以通过父组件的init接口,把子组件的实例对象传入父组件。父组件在构建完自身的Dom后挂在子组件。例如:
tab.prototype.add = function(title, content /* 子组件对象 */, isSelect) {
var m = new model({
title: title,
content: content,
isSelect: isSelect
});
this._collection.add(m);
}
组件嵌套中的另一个关键点是父子组件的通信。先来说父组件如何向子组件传递消息,方案父组件调用子组件的接口,例如子组件上可以提供组件销毁接口,刷新接口等。
而子组件向父组件发送消息则可以通过事件的机制,也就是说子组件的入口需要继承Backbone的Event事件系统,父组件通过监听子组件发出的消息实现通信。