自从3年前Backbone.js发布第一版以来,Backbone.js就成为一个流行的开源JavaScript “MV*”框架,并获得人们的青睐。尽管Backbone.js给JavaScript应用提供了框架,但是它仍然给开发者留有很多设计模式供选择,不管怎样,当开发者第一次使用Backbone.js时还会产生很多普遍的问题的。
因此,在这篇文章中,我们将介绍很多不同的设计模式供你在Backbone.js应用中使用,而且我们也会一同来看看对于开发者来说会产生很多普遍的有关性能伸缩的问题。
对象深度拷贝
JavaScript对待所有原生类型变量是传值。所以,当变量被引用时就传递了变量的值。
var helloWorld = “Hello World”; var helloWorldCopy = helloWorld;
举个例子,上面的代码将变量helloWorldCopy的值设置为变量helloWorld的值。这样, 自从它的值被复制之后,所有修改helloWorldCopy的值不会修改helloWorld的值。JavaScript对待所有非原始类型的变量时传引用,这就意味着当变量传递的时候将会传递内存地址引用。
var helloWorld = { ‘hello': ‘world' } var helloWorldCopy = helloWorld;
举个例子,上面的代码将设置helloWorldCopy为helloWorld的引用,而且,也许你会猜到任何修改helooWorldCopy的值都会直接导致helloWorld值的变化。如果你想要helloWorld的拷贝,你可以创建一个拷贝对象即可。
也许你会想到“为什么Backbone.js可以解释为所有的工作都是通过传递引用?”事实上,Backbone.js不会拷贝对象,这将意味着如果你从模型里调用.get()方法获得一个对象,任何给这个对象的修改都会直接修改原来的对象。让我们一起来看一个例子来阐明哪里会发生这样的情况。如果你有个如下的Person模型:
var Person = Backbone.Model.extend({ defaults: { 'name': 'John Doe', 'address': { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 78701 } } });
这样你就创建了一个新的person对象:
var person = new Person({ 'name': 'Phillip W' });
现在我们来对新对象的一些属性进行操作操作:
person.set('name', 'Phillip W.', { validate: true });
上面的代码成功的给person对象的name属性赋了值。现在我们在来操作person对象的地址属性。当然,在我们这样做之前先验证一下地址属性。
var Person = Backbone.Model.extend({ validate: function(attributes) { if(isNaN(attributes.address.zipCode)) return "Address ZIP code must be a number!"; }, defaults: { 'name': 'John Doe', 'address': { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 78701 } } });
现在,让我们试图给地址属性设置一个不正确的ZIP代码。
var address = person.get('address'); address.zipCode = 'Hello World'; // Raises an error since the ZIP code is invalid person.set('address', address, { validate: true }); console.log(person.get('address')); /* Prints an object with these properties. { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 'Hello World' } */
这将会怎样呢?我们的验证出现了错误!为什么属性依旧被改变了?前边我们说过,Backbone.js不会拷贝模型属性;它会返回你所请求的一切。这样,你也许会猜到,如果你需要一个对象,你将得到这个对象的引用,对这个对象的任何操作都会直接改变模型里的对象。如果你要debug,这可能将把你带入到无底的兔子黑洞。
这个问题对于新的Backbone.js使用要引起注意,甚至对于老练的JavaScript程序员有时也会没有提防。这个问题在GitHub的Backbone.js讨论组中有很激烈的讨论。正如Jeremy Ashkenas指出,执行一个深的对象引用是个很难解决的难题,一个很深的对象引用是要花费很大代价的。
幸运的,jQuery 提供了一个深度拷贝功能来实现,$.extend. 如同, Underscore.js ,一个Backbone.js的依靠,提供_.extend 方法,但是我必须避免使用它,因为它没有执行一份是个深度的复制,Lo-Dash, Underscore.js的一个分叉版本,提供了对象一个深度克隆的_.clone 方法的选项。然而,我使用 $.extend 方法的模型使用的语法规则去执行一个任意对象的深度克隆。记得通过后,结果它执行的是一个深度的克隆方法
var address = $.extend(true, {}, person.address);
我们现在快速准确的复制一个theaddressobject"htmlcode">
var Hotel = Backbone.Model.extend({ defaults: { "availableRooms": ["a"], "rooms": { "a": { "size": 1200, "bed": "queen" }, "b": { "size": 900, "bed": "twin" }, "c": { "size": 1100, "bed": "twin" } }, getRooms: function() { $.extend(true, {}, this.get("rooms")); }, getRoomsByBed: function(bed) { return _.where(this.getRooms(), { "bed": bed }); } } });
现在我们假设明天你就要发布你的代码,而你又发现端点开发者忘记告诉你rooms的数据结构改变了,由一个对象变为一个数组。你的代码现在看起来会像下面这样。
var Hotel = Backbone.Model.extend({ defaults: { "availableRooms": ["a"], "rooms": [ { "name": "a", "size": 1200, "bed": "queen" }, { "name": "b", "size": 900, "bed": "twin" }, { "name": "c", "size": 1100, "bed": "twin" } ], getRooms: function() { var rooms = $.extend(true, {}, this.get("rooms")), newRooms = {}; // transform rooms from an array back into an object _.each(rooms, function(room) { newRooms[room.name] = { "size": room.size, "bed": room.bed } }); }, getRoomsByBed: function(bed) { return _.where(this.getRooms(), { "bed": bed }); } } });
我们仅仅更新了一个函数,以便将Hotel的结构转变为这个应用的其余部分所期望的结构,同时整个应用仍然像我们所期待的一样运作。如果这里没有一个getter,我们很可能不得不为rooms更新每个访问点。理想情况下,你会希望更新所有的函数,以适应新的数据结构,但如果你在时间方面有压力急于发布的话,这个模式将可以拯救你。
离题说一句,这个模式既可以被认为是装饰模式,因为它隐藏了创建对象拷贝的复杂性,也可以认为是桥接模式,因为它可以用来将数据转换为所期望的形式。一个好的经验是对任何对象元素使用getters 和setters 。
存储数据不是通过服务器保存
尽管Backbone.js有模型和集合映射的规定去具象状态的传输(or REST-ful)的端点,你将花大量的时间去找你想要的存储数据在你的模型或者不是在服务器上的连接。另外一些关于Backbone.js的文章,例如“Backbone.js Tips: Lessons From the Trenches” 是通过SupportBee的Prateek Dayal ,这个模式还有其他的描述。让我们一起来快速的看一个小例子来帮助我们说明它可能会派上用场。假设你有一个集合。
<ul> <li><a href="#" data-id="1">One</a></li> <li><a href="#" data-id="2">Two</a></li> . . . <li><a href="#" data-id="n">n</a></li> </ul>
当使用者点击其中一个项目时,这个项目成为了被选中状态并且对于使用者作为选中项目是通过 aselectedclass 添加的是可视化的。以下这是一种方式:
var Model = Backbone.Model.extend({ defaults: { items: [ { "name": "One", "id": 1 }, { "name": "Two", "id": 2 }, { "name": "Three", "id": 3 } ] } }); var View = Backbone.View.extend({ template: _.template($('#list-template').html()), events: { "#items li a": "setSelectedItem" }, render: function() { $(this.el).html(this.template(this.model.toJSON())); }, setSelectedItem: function(event) { var selectedItem = $(event.currentTarget); // Set all of the items to not have the selected class $('#items li a').removeClass('selected'); selectedItem.addClass('selected'); return false; } }); <script id="list-template" type="template"> <ul id="items"> <% for(i = items.length - 1; i >= 0; i--) { %> <li> <a href="#" data-id="<%= item[i].id %>"><%= item[i].name %></a></li> <% } %></ul> </script>
现在我们能够很容易的判断被选中的项目,并且我们没有必要通过对象模型去判断。这种模式对于存储无用的数据是非常有用的以至于 你可能非常想要去跟踪;请记住你能够创建一个模型并且没有必要去关联于他们存储的一些无用的图像数据。
var View = Backbone.View.extend({ initialize: function(options) { // Re-render when the model changes this.model.on('change:items', this.render, this); }, template: _.template($('#list-template').html()), events: { "#items li a": "setSelectedItem" }, render: function() { $(this.el).html(this.template(this.model.toJSON())); }, setSelectedItem: function(event) { var selectedItem = $(event.currentTarget); // Set all of the items to not have the selected class $('#items li a').removeClass('selected'); selectedItem.addClass('selected'); // Store a reference to what item was selected this.selectedItemId = selectedItem.data('id')); return false; } });
现在我们可以很容易的确定哪些项已经被选中,并且我们没有必要通过这些对象模型来了解。这个模式对于存储无用的数据是非常有用的,请记住,您可以创建不一定有端点相关联的存储无关的视图数据的模型和集合。
这种模式的缺点是你存储了无用的数据在你的模型或者集合中,它们不能真正意义上的追随一个平静的架构是因为它们不会完美的去映射在web资源上;另外,这个模式会引起一些很膨胀的在你的模型中;;并且当你保存你的模型的时候如果你的端点严格的只接受JSON数据它会引起一个很大的烦恼。
你可能会问你自己,“我如何确定我是否应该讲把额外的数据放进视图或者是模型中?”。如果额外的属性你将要增加的是围绕性的呈现,例如一个容器的高度,我们应该要添加它的图形。如果这个属性跟底层的数据模型有一些关系,然后你想要将它放进这个模型中。例如,如果上面的例子更多的显露出,因为某些原因我仅仅只希望用户通过从模型返回的项目列表中选择一个特殊的项,我可能会增加这种逻辑模型。总而言之,大多数的事情,它实际上取决于这种依赖。你能够为保持你的模型而辩论并且你可以认为保持你的观点是可能的并且把尽可能多的逻辑放进你的模型中。
渲染部分视图,而不是整个视图
当你第一次开始开发Backbone.js应用时,典型的视图结构是像这样的:
var View = Backbone.View.extend({ initialize: function(options) { this.model.on('change', this.render, this); }, template: _.template($(‘#template').html()), render: function() { this.$el.html(template(this.model.toJSON()); $(‘#a', this.$el).html(this.model.get(‘a')); $(‘#b', this.$el).html(this.model.get(‘b')); } });
在这里,任何对模型的改变都会触发对视图的一个全面的重新渲染。我第一次用Backbone.js开发时,我是这个模式的实践者。但随着视图代码的增长,我迅速的意识到,这种方法不利于维护或优化,因为当模型的任何一个属性发生变化时,视图将会完全的重新渲染。
当我遇到这个问题,我迅速的用Google搜索了一下,看看别人是怎么做的,结果找到了Ian Storm Taylor的博客,“分解你的Backbone.js渲染方法”,他在其中描述了在模型中监听单独的属性变化,然后仅仅重新渲染相对于变化属性的视图部分。Taylor也描述了返回对象的引用,以便单独的渲染函数可以很容易的链接在一起。上面的例子现在现在就变得更易于维护,性能更优。因为我们仅仅更新了模型变化的属性相对应的视图部分。
var View = Backbone.View.extend({ initialize: function(options) { this.model.on('change:a', this.renderA, this); this.model.on('change:b', this.renderB, this); }, renderA: function() { $(‘#a', this.$el).html(this.model.get(‘a')); return this; }, renderB: function() { $(‘#b', this.$el).html(this.model.get(‘b')); return this; }, render: function() { this .renderA() .renderB(); } });
我应该说一下有许多插件,比如Backbone.StickIt和Backbone.ModelBinder,提供了模型属性与视图元素的键-值绑定,这会让你省去编写许多样板代码,如果你具有复杂的表单字段检验一下它们。
保持模型与视图无关
正如 Jeremy Ashkenas 在 Backbone.js的 GitHub问题 之一中所指出的,Backbone.js 并没有实施数据与视图层之间关注点的任何真正分离,除非模型未引用视图而创建。因为Backbone.js并没有执行一个关注点分离,所以你应该将其分离吗?我和许多其他的Backbone.js开发人员,如Oz Katz 和 Dayal ,都相信答案毫无疑问是yes:模型与集合,也就是数据层,应该彻底的与绑定到它们的视图无关,保持一个清晰的关注点分离。如果你没有遵循关注点分离,你的基础代码将很快变成意大利面条式的代码,而没有人喜欢意大利面条式的代码。
保持模型与视图无关将会帮助你预防意大利面条式的代码,而没有人喜欢意大利面条式的代码!
保持你的数据层彻底的与视图层无关,这将会使你创建出更具模块化,可复用与可维护的基础代码。你可以非常容易的在应用程序各个地方复用与扩展模型和集合,而不需要考虑它们所绑定的视图。遵循这个模式使对你项目不熟悉的开发者能迅速的深入到基础代码之中,因为他们会确切的知道哪里发生了渲染,哪里存在有你的应用的所有商务逻辑。
这个模式也执行了单一职责原则,规定了每个类应该具有一个单一的职责,而且它的职责应该封装与这个类之中,因为模型与集合要处理数据,而视图要处理渲染。
路由中的参数
最好的演示这个模式工作方式是举个例子。比如说你需要对搜索页面进行排序,每个搜索页面都允许用户添加两个不同的过滤类型foo和bar,每个类型代表不同的观点。
因此,你的URL结构将会呈现如下:
'search/:foo' 'search/:bar' 'search/:foo/:bar'
现在,所有的路由都用的是同一个试图和模型,这样大多数人喜欢用同一个函数search()来实现。然而,你要是检查过Backbone.js代码的话,你会发祥它里面没有排序的参数映射;这些参数只是从左至右依次传入函数。这样,为了都能统一使用一个函数,你就要停止创建不同的函数正确的来为search()匹配参数。
routes: { 'search/:foo': 'searchFoo', 'search/:bar': 'searchBar', 'search/:foo/:bar': 'search' }, search: function(foo, bar) { }, // I know this function will actually still map correctly, but for explanatory purposes, it's left in. searchFoo: function(foo) { this.search(foo, undefined); }, searchBar: function(bar) { this.search(undefined, bar); },
你也许能想象的到,这个模式可以使路由功能很快膨胀。当我第一次遇到这个问题时,我试图创建了一些用正则表达式定义的解析函数来“神奇”的去匹配参数,当然这个是可以工作的-但这也是有约束条件的。这样,我废弃了这个想法(有时,我仍然可以用Backbone插件来解决)。我进入GitHub中的一个 议题,其中Ashkenas建议应该让所有的参数都和search函数匹配。
上面的代码现在转变为下面维护性更强的样子:
routes: { 'search/:foo': 'searchFoo', 'search/:bar': 'searchBar', 'search/:foo/:bar': 'search' }, search: function(foo, bar) { }, // I know this function will actually still map correctly, but for explanatory purposes, it's left in. searchFoo: function(foo) { this.search(foo, undefined); }, searchBar: function(bar) { this.search(undefined, bar); },
这种模式可以戏剧性的减少路由的过分膨胀。然而,需要注意到它不会服务于不能区别的参数。比如,如果你有两个作为ID的参数,如模式XXXX-XXXX,你不能区分哪个ID是对哪个参数的回应。
model.fetch() 不会清除你的模型
这通常会将那些Backbone.js的新手给绊倒:model.fetch()并不能丢掉你的模型,而是扩展了你的模型的属性。因此,如果你的模型具有属性x,y和z,你获取到y和z,那么x将仍然是模型中的那个x,只有y和z会被更新。下面的例子将这个概念形象化了。
var Model = Backbone.Model.extend({ defaults: { x: 1, y: 1, z: 1 } }); var model = new Model(); /* model.attributes yields { x: 1, y: 1, z: 1 } */ model.fetch(); /* let's assume that the endpoint returns this { y: 2, z: 2, } */ /* model.attributes now yields { x: 1, y: 2, z: 2 } */
PUTs 需要一个 ID 属性
这一条也经常将Backbone.js的新手绊倒。要想在调用.save()的时候让模型发送一个HTTP PUT请求,你的模型需要有一个ID属性集。记得HTTP PUT谓词是设计来做更新的吧,所以发送一个PUT请求,你的模型需要有一个ID,这么做是有意义的。在理想的世界里,你的所有模型都具有一个名为ID的完美的ID属性,但是你从端点接收到的JSON数据可能并不总是具有完美命名的IDs。
因此,如果你需要更新一个模型,请在保存之前确认模型上有ID。Backbone.js 的0.5以及更高版本允许你用id属性来更新模型的ID属性名称,如果你的端点返回的不是名为id的IDs的话。
如果困顿于使用的是版本低于0.5的Backbone.js,我建议你修改你的模型或集合的parse函数,以便将你期望的ID属性映射到属性ID。这里有一个快速上手的例子,说明了你应怎样修改parse函数来做到这一点。我们假设你有一个cars的集合,它的IDs是carID。
parse: function(response) { _.each(response.cars, function(car, i) { // map the returned ID of carID to the correct attribute ID response.cars[i].id = response.cars[i].carID; }); return response; },
页面加载时创建模型数据
有时你会发现你的模型或者集合需要在页面加载时被初始化赋值。许多关于Backbone.js模式的文章,例如Rico Sta Cruz的 “Backbone 模式” 和 Katz的 “ 避免常见的Backbone.js陷阱” ,讨论了这种模式。这种模式实现很容易,只需在页面中内联一段脚本,通过你选择的服务端语言,将单个模型属性或者JSON形式的数据呈现出来。例如,在Rails语言中,我采用下面方法之一:
// a single attribute var model = new Model({ hello: <%= @world %> }); // or to have json var model = new Model(<%= @hello_world.to_json %>);
应用这种模式可以通过“立即的”渲染页面,改善你的搜索引擎排名,而且它也可以通过限制应用初始化HTTP请求的方式,大大缩短你的应用启动与运行所需要的时间。
处理失败的模型属性验证
很多时候,你会想知道是哪个模型属性验证失败了。例如,如果你有一个极其复杂的表单,你或许想知道哪个模型属性验证失败,这样你就可以将这个属性对应的输入字段高亮显示。不幸的是,提醒视图到底是哪个模型属性验证失败并没有直接集成于Backbone.js,但是你可以用一些不同的模式去处理这个问题。
返回一个错误对象
一个给视图提醒哪个模型属性验证失败的模式是,返回一个对象,其中包含某种标志,它详细的记录了哪个属性验证为失败,就像下面这样:
// Inside your model validate: function(attrs) { var errors = []; if(attrs.a < 0) { errors.push({ 'message': 'Form field a is messed up!', 'class': 'a' }); } if(attrs.b < 0) { errors.push({ 'message': 'Form field b is messed up!', 'class': 'b' }); } if(errors.length) { return errors; } } // Inside your view this.model.on('invalid', function(model, errors) { _.each(errors, function(error, i) { $(‘.' + error.class).addClass('error'); alert(error.message); }); });
这个模式的优点在于,你是在一个地方处理所有不合法的消息。缺点在于,如果你以不同的方式处理不合法的属性的话,你的invalid方法可能会成为一个很大的switch或者if语句。
广播自定义Error事件
我的一个朋友,Derick Bailey,推荐了一个可替代模式,就是为每个模型属性触发自定义的errors事件。这将允许你的视图能够针对单独的属性绑定到特定的error事件:
// Inside your model validate: function(attrs) { if(attrs.a < 0) { this.trigger(‘invalid:a', 'Form field a is messed up!', this); } if(attrs.b < 0) { this.trigger(‘invalid:b', 'Form field b is messed up!', this); } } // Inside your view this.model.on('invalid:a', function(error) { $(‘a').addClass('error'); alert(error); }); this.model.on('invalid:b', function(error) { $(‘b').addClass('error'); alert(error); });
这个模式的优点在于,你的视图明确的绑定到它们所绑定到的error类型,而且如果你对每一种属性error有特定的指令的话,它可以清理你的视图部分代码,使之更易于维护。这个模式的一个不好的地方在于,如果在你处理不同的属性error时并没有太多的不同的话,你的视图可能会变得极为膨胀。
这两种模式都有其利弊,你应该考虑清楚哪个模式对你的应用案例是最优的。如果你按照同样的方式处理所有失败的验证,那么第一个方法可能是最好的;如果你对每个模型属性有特定的UI变化,那么后一种方法更好。
HTTP状态代码200所触发的错误
如果你的浏览器端模型或者集合收到了无效的JSON,尽管HTTP的状态代码是200,但浏览器端依然会触发一个“错误”事件。这种事件常发生于本地模拟JSON数据造成的。那么,一个好的方法就是读取经过 JSON 验证器验证了的模拟JSON数据文件。或者从你的IDE获得相应的 插件来及时获取格式错误的JSON信息。
创建一个一般性错误显示模式
创建一个常见错误显示代码可以节省你的时间以及创建一个统一的模式来处理、可视化错误信息,而且它可以增加开发者的经验。我之前开发的每一个Backbone.js应用中我都会创建一个可以处理alert的视图:
var AlertView = Backbone.View.extend({ set: function(typeOfError, message) { var alert = $(‘.in-page-alert').length "htmlcode">var alert = new AlertView(); this.model.on('error', function(model, error) { alert.set('TYPE-OF-ERROR', error); });单页面应用中更新浏览器页面标题
这是一个比任何东西都重要的可用性问题。如果你正在开发一个单页面应用程序,谨记更新每个页面的标题。我写过一个的插件(Backbone.js Router Title Helper)来扩展 backbone.js router 的功能。它通过一个 Map 对象来控制路由,键来代表路由函数的名字,值则映射到页面的标题。
Backbone.Router = Backbone.Router.extend({ initialize: function(options){ var that = this; this.on('route', function(router, route, params) { if(that.titles) { if(that.titles[router]) document.title = that.titles[router]; else if(that.titles.default) document.title = that.titles.default; else throw 'Backbone.js Router Title Helper: No title found for route:' + router + ' and no default route specified.'; } }); } });单页面应用中的缓存对象
当我们谈论单页面应用时,另一个叫缓存对象模式你将会经常用到!下面的例子直截了当而且简单:
// Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter: parameter }); this.cached.view = this.cached.view || new View({ model: this.cached.model }); }这个模式可以加速你得应用,因为你不用重复初始化你得Backbone.js对象。然而,它会过多的消耗内存;所以,缓存对象就要在整个应用中使用。如果以前你用过Backbone.js开发过应用,也许你会问你自己,“ 我要重取数据该怎么做?”你可以每次在如下路径中触发后重取数据:
// Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter: parameter }); this.cached.view = this.cached.view || new View({ model: this.cached.model }); this.cached.model.fetch(); }当你的应用从端点(如,一个收件箱)必须检索最新数据时上面的模式就可以工作。当然,如果你要拿的数据时凭借应用的某个状态(假设这个状态是通过URL和参数来决定的),甚至是在用户上一个页面应用的状态没有改变, 你可以重取数据。一个好的解决方案去重拿数据时当应用(参数)发生变化时:
// Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter:parameter }); this.cached.model.set('parameter', parameter); this.cached.view = this.cached.view || new View({ model: this.cached.model }); } // Inside of the model initialize: function() { this.on("change:parameter", this.fetchData, this); }JSDoc函数和Backbone.js类
我是文档注释和JSDoc的超级粉丝。我用JSDoc对所有的Backbone类添加了文档注释:
var Thing = Backbone.View.extend(/** @lends Thing.prototype */{ /** @class Thing * @author Phillip Whisenhunt * @augments Backbone.View * @contructs Thing object */ initialize() {}, /** Gets data by ID from the thing. If the thing doesn't have data based on the ID, an empty string is returned. * @param {String} id The id of get data for. * @return {String} The data. */ getDataById: function(id) {} });如果你对Backbone类进行如上添加文档注释,这样你可以给所有类和函数 添加参数、返回类型以及描述文档注释了。确保保持初始化函数作为一个声明的函数,这样可以帮助我们生成JSDoc。如果你想看看JSDoc的例子工程,那就在 HomeAway Calendar Widget下载例子。同时这里也有个 Grunt.js插件, grunt-jsdoc-plugin,这个也可以作为你构建文档注释时的一部分。
联系测试驱动的开发模式
我认为如果你用Backbone.js,你应该在开发模型和集合时遵循测试驱动开发(TDD)。我第一次用Jasmine.js创建模型和集合时遵循TDD进行单元测试,但失败了。一旦写下单元测试并且失败,我会对整个模型和集合进行重写。
通过这一点,我的所有Jasmine测试都通过了,而且我有信心我的模型和集合会和我期望的一样工作。自从我遵循TDD,我的视图层非常容易写而且非常简单。当你开始用TDD时,你得速度当然会很慢;但是一但你得脑海里一直想着TDD,你的编程效率和质量会神奇般的提高。
Backbone
P70系列延期,华为新旗舰将在下月发布
3月20日消息,近期博主@数码闲聊站 透露,原定三月份发布的华为新旗舰P70系列延期发布,预计4月份上市。
而博主@定焦数码 爆料,华为的P70系列在定位上已经超过了Mate60,成为了重要的旗舰系列之一。它肩负着重返影像领域顶尖的使命。那么这次P70会带来哪些令人惊艳的创新呢?
根据目前爆料的消息来看,华为P70系列将推出三个版本,其中P70和P70 Pro采用了三角形的摄像头模组设计,而P70 Art则采用了与上一代P60 Art相似的不规则形状设计。这样的外观是否好看见仁见智,但辨识度绝对拉满。