说三道四技术文摘-感悟人生的经典句子
说三道四 > 文档快照

【前端框架】Backbone.js在大型单页面应用中的应用实践

HTML文档下载 WORD文档下载 PDF文档下载
本文选自《程序员》2013年3月刊,全面介绍了轻量JavaScript MVC框架Backbone.js,并分享了豌豆荚PC客户端借助Backbone.js在开发大型单页面应用上做出的大量实践。

Backbone.js是什么?

Backbone.js是一个JavaScript MVC框架,提供了良好的代码组织能力,可以方便地将应用程序解耦成可以复用的部分,为建立大型的单页面应用提供框架支持,目前的版本是0.9.10(注:现在已到1.2.1版本)。通过将应用程序分解成MVC模式中不同职责的模块,带来了以下几点好处。

  1. 降低维护成本。数据、控制器、视图的更新都是独立进行的,互相之间都是松散耦合的。
  2. 解耦数据和视图,便于直接对业务逻辑进行单元测试。
  3. 便于团队合作,负责UI开发和业务逻辑开发的工程师可以分工并行工作。对于Web前端开发来讲,负责HTML模板和CSS的界面工程师和负责业务逻辑的JavaScript工程师可以协同工作。

Backbone.js算是比较轻量的MVC框架,所谓轻量,是说它只关注一个框架应该关注的最基本的事情——如何给应用分层、如何组织各种功能的代码。至于在实际开发中需要用到的Utils或UI组件,Backbone.js则没有提供任何支持。但Backbone.js所依赖的Underscore.js是一个功能比较全面的非侵入式工具函数类库,算是在Utils方面的一个补充。

轻量并不意味着功能薄弱。首先,Backbone.js的精髓是它定义前端MVC的方式和编码哲学,并依据这些规定了如何去给代码分层,因此Backbone.js能够让前端工程在可维护性和扩展性上都得到质的提升;同时,由于其良好且易于理解的结构,各个模块之间都是松散耦合的,虽然目前官方并没有提供根据实际需求build文件的功能,但如果你愿意,完全可以自己手工删掉源码中的Bakcbone.View只使用Model和Collection;最后,Backbone.js的任何一个部分都是非常容易扩展的。因此,Backbone.js的功能实际上非常强大的。下面将介绍Backbone.js的主要组件(架构如图1所示)。


图1 架构图

Backbone.Events和Backbone.Sync

Backbone.Events和Backbone.Sync两个组件是Backbone.js异步通信、事件驱动的编程模型的基础。

Backbone.js中所有的组件都通过_.extend()的方法“继承”了Backbone.Events所提供的功能,可以维护一套自己的事件订阅和回调列表。通过Events.on(event,[callback],[context])和Events.off([event], [callback], [context])两个方法来实现对事件的订阅和取消订阅。


早期版本中的事件订阅和取消订阅是通过Events.bind()和Events.unbind()两个方法实现的,目前的版本中还保留了这两个别名方法,但不推荐使用。

Backbone.js的所有组件都有一些内置的事件,可以查阅官方文档。除了预置事件外,通过Events.trigger(event,[*args])方法也可以方便地触发自定义事件。


从0.9版开始,Backbone.Events提供了Events.listenTo(other,event,callback)和Events.stopListening([other],[event],[callback])两个新方法来通过另外一种形式实现事件的订阅和取消订阅。

与on()和of f()不同,这种方式将监听的主动权转换了。举个例子来说:有对象A和对象B,B.on('someThingHappened',A.doSomeThing)是当对象BsomeThingHappened时候知对象A去doSomeThing;而A.listenTo(B,'someThingHappend',A.doSomeThing)是对象A主动去盯着对象B,当它someThingHappend的时候去doSomeThing。

第二种方式最大的意义是变被动为主动,从而实现了IoC(Inversionofcontrol,控制反转)。监听者和被监听者之间没有了耦合,只要被监听的对象能够抛出指定的事件,就可以和监听者组合在一起,甚至不需要去关心被监听对象的类型,这对代码的复用和行为抽象有很大的帮助。在测试层面,可以轻易地把被监听对象换成mock的测试代码来模拟真实情况。

Backbone.Sync则将同服务器的通信封装了起来,当Collection和Model需要和服务器通信交换数据时,会去调用Backbone.Sync中对应的方法并发送请求,如果服务器端支持RESTfulAPI就可以将整个通信过程描述得非常优雅并易于扩展。Sync的实现可以是jQuery.ajax()的封装,也可以是其他的类库提供的异步通信工具的封装。

Backbone.Model和Backbone.Collection

Backbone.js中的Model和Collection共同构成了MVC中的M层。Model的本质就是一组以keyvalue形式保存的数据,可以通过Backbone.Model.extend()来定义自己的Model。


上面的示例代码中defaults属性定义了一组默认值,当Model初始化时,如果没有指定defualts中所定义的属性的值,就会用默认值来填充Model;initialize()方法会在Model被实例化时调用,用来进行一些初始化的操作;validate()方法会在Model的save()或set()方法被调用时执行,可以根据具体需求进行扩展。

通过Model的getters和setters可以实现对Model中属性的读写,并且当set()方法被调用时,Model会将属性变化的事件广播给所有订阅者(通常是视图),驱动视图重新渲染或其他关联的Model的数据更新。

Collection是一组Model的集合,通过Collection可以将一组数据结构相同的Model有序地组织在一起,进行批量操作和管理等。同时,Collection代理了Undersore.js中众多用来操作Collection的工具方法,例如find、filter、map等。

Model和Collection都可以通过RESTfulAPI同服务器进行数据交换。

Backbone.View

Backbone.View是基于Backbone.js开发的Web App中的核心部分,负责用户交互事件的捕捉和处理、把用户输入导向Model或Collection、渲染视图、操作DOM等。Backbone.js的内部实现依赖$变量,因此DOM操作的库可以在jQuery、Zepto或Ender等中选择。从目前的情况来看,在桌面浏览器中,Sizzle.js(jQuer y所使用的SelectorEngine)的性能还是甩开Zepto几条街的,因此面向桌面浏览器的开发还是推荐使用jQuery,移动端考虑到文件体积等因素推荐使用Zepto。

对于一个Backbone.View来讲,最重要的就是$el属性,$el是一个jQuery对象(取决于所采用的DOM操作类库),是一个视图的最外层容器。容器所采用的HTML标签可以通过tagName属性来指定,可以是ul也可以是header或其他任何标签,默认情况下是div。

容器内部的DOM渲染可以通过模板引擎来完成。Underscore.js本身提供了一个_.template()的方法,因此Backbone.js不需要额外的模板引擎支持。当然,如果有特殊的需求,例如和后端共用模板文件,也可以选用Mustache等其他的模板引擎。


这样一个View就被渲染到界面上了。上面的代码中还监听了Model的change事件,当数据发生变化时,驱动视图重新渲染。当Model的数据比较丰富时,只有一个属性变化就重新渲染整个视图显然会带来性能上的隐患,因此这里的最佳实践就是把render的过程break-down成粒度更细的片段。


值得注意的是,当一个View不再被需要时,一定要记得销毁,除了销毁DOM对象外,也要销毁所有的事件监听器。在只有Events.on()和Events.off()的年代,由于销毁View时需要逐一取消订阅所有的消息,经常由于忘记解除某个绑定导致产生被称为GhostView的垃圾对象,既无法被释放也无法被回收。这也是Events.listentTo()方法带来的另外一个好处——只要调用Events.stopListening()方法即可将此对象的所有事件监听器销毁。

所有的DOM事件也是通过$el来代理的,在Backbone.View中可以通过以下方法来方便地管理DOM事件。


Backbone.js的内部实现里,在View的构造方法中调用View. initialize( )后将继续调用View.delegateEvents()方法,这个方法将解析events属性所定义的事件和回调列表,并将全部事件代理到$el对象上。由于使用的是事件代理,某些不支持冒泡的DOM事件则必须另外监听,如滚动条事件。


Backbone.Router和Backbone.History

Router是用来在URLHash和特定的动作或视图之间做映射的。


最后一句History.start()是告诉Backbone.js开始对URLHash的变化进行监视,也可以随时通过History.stop()来停止监视。同时,如果目标平台是支持HTML5Histor yAPI的,那么在start时传入{pushstate:true}的参数,就可以去掉URL中的#字符,对SEO有一定帮助。

Backbone.js的适用场景

经常能在各种场合听到前端工程师们讨论“你们的XXX是用什么做的啊?”“为什么不用XXX啊?”这样的问题。前端的类库和框架林林总总,算在一起数量没有一千也有五百,因此在面对一个新项目时难免会产生选择恐惧症。

但说到底,技术方案都是由需求决定的,任何一个类库或框架都有其适用范围和最佳的使用场景,Backbone.js也不例外。Backbone.js的最佳使用场景是大型的单页面应用:通过RESTfulAPI同服务器通信,然后根据数据的变化来驱动视图重新渲染,整个程序建立在异步通信和事件驱动的编程模型之上。

单页面应用给用户带来的使用体验是沉浸式的、相对重型的,对于普通的Web Page和数据相对稳定、视图不需要频繁重新渲染的场景来讲,Backbone.js显然就没有用武之地了。

Backbone.js和MV*不得不说的那点事儿

四十年前,Trygve Reenskaug基于Smartalk语言设计出了MVC模式,经过几十年的发展,MVC出现了众多的衍生。而我们今日说的MVC在不加特殊说明的情况下,通常指的是在服务器端Web应用开发中大量使用的WebMVC。

对于典型MVC模式来讲,View是无法直接获得用户输入的,而Controller则是用户输入和View之间的桥梁。但在浏览器中,View层的载体是HTML,用户输入和交互行为都是基于HTML的,对事件的捕捉、输入都由浏览器代劳了,并且输入会首先进入View层,因此对于前端开发来讲,严格意义的MVC是无法实现的。

因此,包括Backbone.js在内的JavaScript MVC框架的实现并没有严格遵循MVC的定义,Controller的部分职责被转移到了View层。Backbone.js对于前端MVC的定义非常易于理解,但对于没接触过MVC模式的同学来说在初期会有一些迷惑,原因是Backbone.js核心组件的命名。Backbone.js的核心组件包括Backbone.Collection、Backbone.Model、Backbone.View和Backbone.Router,而在早期的版本中,Backbone.Router组件的名称是Backbone.Controller,这很容易让人直接将其和MVC三层中的C层联系起来。但事实上,Backbone.Controller的作用是根据URLHash来在对应的行为和事件中做路由的,其功能同MVC中的C相比要简单很多,因此在0.5前后Controller改名叫做Router了。从实体代码的角度看,View层其实是模板代码和Backbone.View中部分代码的综合体,而Backbone.View中剩余的部分才是MVC模式中Controller的概念,负责操作View以及数据在View和Model层中的流转。

对于前端开发来说,用户直接面对的一层并不是Controller而是View,用户输入也会首先进入View,因而用MVP和MVVM模式来描述架构更加合理。相比AngularJS等框架,Backbone.js的模式显然更易于理解,学习曲线比较平缓,因为它并没有引入过多的需要重新认知和理解的新概念而是尽量在靠近传统的MVC模式,对于以前接触过MVC模式开发的同学来说非常容易上手,而AngularJS中的Directive等概念还是需要一定认知成本的。

但如果从架构的角度讨论,AngularJS其实是更为纯粹并更接近严格意义上的MVC模式。为了把View的功能提升到一个应有的高度,引入了Directive的概念,通过扩展HTML标签和自定义属性来描述View,在Directive中来解析这些扩展出来的内容,理解成本和代码的复杂程度都有所提高,但View层功能则得到了质的提升。

反观Backbone.js,并没有在前端开发中真正的View的载体HTML上做太多文章,即便采用模板引擎也仅仅是把数据和HTML组合起来。但得益于其强大的扩展性,可以很容易将Knockout等Data-binding框架集成进来,从而实现MVVM的架构和分层。例如前文提到将render的过程Breakdown就完全可以用Data-binding来取代,省掉了手工更新DOM的烦琐。

Backbone.js在豌豆荚PC客户端2.0中的实践

豌豆荚PC客户端2.0的UI是完全建立在Web前端基础上的。借助Backbone.js,豌豆荚PC客户端在开发大型单页面应用中做了大量的实践。通过在客户端中捆绑一个Webkit引擎并对其进行扩展,使得跑在Webview中的前端代码跳出沙箱的限制,可以操作文件系统并调用系统API,以此来进行桌面应用的开发。这样做的好处有以下几点。

  1. 极大提高开发效率:桌面应用的开发中,UI开发效率一直很低,但借助HTML5和CSS3的新功能,Web前端可以轻易地做出精细程度和交互体验都不输桌面应用的UI,但开发效率和维护成本都极大降低。
  2. 易于跨平台:UI依赖于Webkit引擎,而Webkit引擎本身是跨平台的,因此可以很容易地移植到Web上或其他桌面平台。
  3. 快速迭代和改进:由于维护和扩展成本的下降,可以快速的将原型设计产品化并进行验证,提高产品迭代和改进的速度。

但与此同时,用Web前端技术开发桌面应用也要面临巨大的挑战。首先就是内存消耗。用户在使用浏览器和使用桌面应用时的心理预期是不一样的,即使一段有内存泄漏问题的前端代码跑在浏览器里,当出现运行缓慢时大不了一下刷新页面,但对桌面应用来讲,大多数情况下没有刷新Webview的机会,因此必须对内存实现更精细的控制。由于Webview本身依赖于GC,前端无法主动管理和回收内存,因此必须借助ChromeDeveloper tools中的Profiles等工具查找出现内存泄漏的地方从而进行改善,这也依赖于大量的经验积累。

其次是运行速度和界面响应速度。由于Webview是单线程的,但单页面应用面临的是数百倍于Web Page的业务逻辑复杂度,同时业务逻辑的执行和UI共用一个线程,如何优化执行速度也是一个很大的挑战。虽然目前WebUI的界面流畅程度无法完全达到桌面应用的水平,但依然是很有竞争力且有提高空间的。(责编:陈秋歌)

作者简介

赵望野:豌豆荚前端团队负责人。

本文选自《程序员》2013年3月刊。2000年创刊至今所有文章目录请查看程序员封面秀。欢迎订阅程序员电子版(含iPad版、Android版、PDF版)。

欢迎加入CSDN前端交流群:218126086,进行前端技术交流。  

保证连网计算机时间同步-VB资料 编写网络寻呼机 -VB资料 编制自已的电话录音小程序-VB资料 程序中如何启动默认的拨号连接-VB资料 打开浏览器并进入指定网址-VB资料 得到用户的IP地址 -VB资料 电话拨号 -VB资料 电子邮件的标准格式 (RFC 822)-VB资料 断开拨号网络的连接-VB资料 断开与 Internet 的连接 -VB资料 发送电子邮件附件-VB资料 VB获得用户网络登录名 基 于Win95 的VB5 串 口 通 信 程 序 基于Win95的VB5串口通信程序 检测运程数据传送的断线-VB资料 简单电子邮件发送程序-VB资料 VB建立拨号联接 -VB资料 将所有窗口最小化-VB资料 VB利用IE控件访问Internet VB利用IE控件设计简易浏览器 VB利用Mscomm32控件判断MODEM是否打开,或者正在工作,并且判断拨号时是否遇忙音 VB利用TAPI进行电话拨号 利用VB访问Internet 利用VB设计聊天室 利用Visual Basic实现无线通讯 VB利用Winsock控件实现局域网通信 VB如何从 Internet 上取回某一个网页的內容? VB如何检测是否已连接到Internet? VB如何利用Winsock控件编写自己的Internet程序 VB如何每天抓取 Internet 上某一个网页中的图片来更换桌面的壁纸 VB如何启动拨号网路中的连线? 关于数据库服务器和备份的问题 win2kPRO 简单问题,高分相送!!在线发分,第一个答对者,定送此分!!!!!! c程序总出现"out of memory..." bzszp(蝎子)和ATCG(ATCG),谢谢!请问可以给我你们的E-mail吗?我以后还有些问题想请教你们! 如何设置java环境 更改工作站的下载等权限 制作一个修改程序外表的东西 怎样ENABLE 注册表编辑 MFC向导生成的基本对话框程序,一按回车就会退出,怎么设置都没有用.怎么办? 小弟下个学期就学vc了,想在这个暑假里好好的去接触接触但是不知道改看那些书好!还请~~~~~~~?高分!! 请问这个错误是什么原因啊? QMAIL高手:关于分页的问题,函数vauth_getall() 制作修改程序外表的东西 初学者学习编程什么样的学校比较适合? 制作修改程序外表的东西 原本对这个世界失望了,居然还发现还有和我一样善良的人,你让我有了活下去,继续灌水事业的勇气,谢谢给我捐献可用分的好人! ******初学者问题 ,如何实现2个Edit框同步******* 请问DataGrid如何和ADO控件梆定? 在servlet中设置了一个javaBean的属性,怎样把他传递到jsp中呢? ADOStoredProc1中存的游标数据如何附给变量中???带原码) 如何提取OLE 容器的嵌入对象? 大家喜欢什么格式的电子书? chm, pdf, html, doc,.... 怎样才能向一个文件中插入一个汉字? 在Word中快速插入日期和时间 备份access数据库原代码,看看错在哪里? 如何写收取、检索POP3邮箱的页面? 代码智能感应? 请教:c++编译器的问题 承蒙看得起~~最近半个月平均没天收到两封带毒邮件,散分:( WinSock控件编程中的一个小问题 QMAIL高手:关于分页的问题,函数vauth_getall() 高手们你们好,请您帮助 大家都在网上混了这么长时间了,有谁知道新浪网的sina是什么意思?俺不知道阿!!! 请大家帮忙看一下,这个问题怎么办???? QMAIL高手:关于分页的问题,函数vauth_getall() 哪里有英文的win2000下阿? 大家帮我看看如何在更新不成功的情况下,能提示出“更新出错” 打开一个任意一个目录下grid的存档程序怎写? 有没有办法防止<Frameset>拖动? 关于boost 请教有关于RichTextBox的问题,高手进来看! 这个ASP提交数据为什么出错,代码如下,在线给分 如何在内存中对一副图进行操作呢 订了一年的杂志,至念只收到2期!!!!!!!!!!!!!!!!!!!!!!!!!!!! 如何做点击一个超链接来运行exe文件?谢谢 在线等候 char * aaa 和CString abc之间的转换问题 有人做过俄罗斯方块游戏吗? 如何用VC实现日志!高分送出 用UDP方式时怎么得到互联网IP而不是局域网IP?大家都来,见者有分 你们见过网友吗? 潮汐,火山爆发,雷电,龙卷风,是怎么一回事 随着天气变化会变颜色的树可以起什么名字? 电磁感应 超级超导Super~有一个闭合超导线圈,当一个磁铁从中穿过时,当磁铁中心通过超导圆环时候,圆环电流最大,为什么?感应电流的消失是因为电阻?如果这样的话就知道为什么了. 一物体用弹簧称测得它的重力为100N,将它放在水中弹簧称示数为20N,求它在水中受的的浮力? 天是什么颜色的 电能的具体含义是什么?运动的电子具有的能量为什么基本不考虑?超导体回路由电磁感应而产生的电流,此时能� 有人说,湿地是一个地区的“肾”,它能起到调节的作用,请你提出几条保护湿地的措施 为什么第一个核弹爆炸后,会在15——20分钟影响后续核弹,会导致后续核弹成哑弹或者流弹. 如图所示,当把一超导线圈从匀强磁场中向右方移出磁场时,由于电磁感应,在超导线圈 世界湿地保护区曾经在盘锦呆过,知道那里有个叫红海滩的观鸟湿地保护区,其实我们家乡岳阳的东洞庭湖也是观鸟湿地保护区,想知道世界上一共还有多少湿地保护区,有没有什么尺度来区分? 赛尔号什么时候下雷雨 雷伊什么时候出现 从今天开始的日子里 为什么防冻液用冰点仪测冰点在-45度上,装入矿泉水瓶中放入冰箱内一整夜后却有冰碴,冰箱内温度为-20度, 一根一端封闭的玻璃管开口向下插入水银管中,内封一定质量的气体,管内水银面低于管外,当温度不变时,将玻璃管向下压一段距离 能受力分析一下 为什么 P气体=P0+P液面差 赛尔号雷雨季节啥时到俺要打雷伊(要写几月,能写到几月几日更好,) 防冻液用手持式冰点仪测出来-34度,但在-28度的冰箱中放一夜就结冰了,为什么? 一端封闭,粗细均匀的U型玻璃管两边长度均为1米,内装水银并封入一定质量的气体.在大气压为75cmHG时,水面高度相同,空气柱长50cm,如图所示.如果在开口端接上抽气机,把开口端的气体缓慢抽成 周期为2的函数表达式是不是一定满足f(x+1)=-f(x)?不是,为什么是f(x+2)=f(x)的周期为1? 超导体做切割磁感线运动会不会产生感应电流 电阻应变片与半导体应变片的工作原理有何不同?它们各有何特点? 漠河最近哪个名人去了 在超导体内如果产生感应电流,并且没有其他省么特殊情况,是不是这个电流恒不消失?那如果一个超导线圈用线拴着,在一个幅度内摆动,并且穿过一个磁场,会产生感应电流,按理来说它会 电阻R=30Ω,Rх的电阻未知.闭合开关S,电流表的示数为1A,断开开关S,电流表的示数变为0.8A,求RхR1 R2串联,开关与R2并联,电流表串联在电路中 为什么湿地能调节气候 超导体中的感应电流是怎样的?》一块扁平的圆柱形超导体在于一块磁铁的磁感线做切割运动的时候根据右手定则应该是有一个方向的,但是假如超导体在某一个方向运动那么感应电流应该是 一道高中的函数题(高手进)下列四个函数中,在(0,+∞) 上为增函数的是A.f(x)=3-xB.f(x)=x的平方-3xC.f(x)=-(1/(x+1))D.f(x)=-|x|不要只给答案.答案我已经知道是c了, 零度以上的温度叫什么温度,零度以下的温度叫什么温度?急 防冻液少了些可以添加蒸馏水吗? 世界海洋灾难之最损失最惨重 死亡率最高 海拔多少米温度在零度以下请高人说一下大概的高度吧.如果常温25度的话. 为什么雷电天气,父母不让看电视,打手机,玩电脑 核弹爆炸原理是什么? 我们在开发海洋的同时,给海洋带来了什么样的灾害 零度以下气温能下雨么? 求几部关于核弹爆炸的电影 为什么天会打雷下雨 零度以下的天气会下雨吗还有雨和雪的形成有什么不同吗 核弹爆炸电影看着视频刚开始这么多核弹,炸了,是什么电影 下雨打雷天,为什么先看见闪电,然后在听见声音? 求描写春天山水变化的优美句子 导体应变片与半导体应变片工作原理有何不同? 下雨天为什么会先看见闪电后听到打雷每次下雨天我们都会先看见闪电后再听到打雷的请问为什么? 硫酸银,氯化亚铁,硫酸铵,磷酸钙,氩气,碳酸钠,锰酸钾,氯化铵,硝酸汞》的化学式,谁给回答一下,急 (1)设函数f(x)=x^2-x+1/2的定义域是[n,n+1],那么f(x)的值域中共有( )个整数(2)对于任意实数x,设f(x)是 4x+1,x+2,-2x+4中的三者中的最小值,则f(x)的最大值为() 正确答案是8/3这个我题目也看不懂在说 超导体有什么实际意义 一木块漂浮在水中,在木块上放砝码,使木块正好没入水中,此时砝码质量为345g(如图左).当把砝码用一根轻质、结实的绳子吊在木块下方时,如果想要使木块正好没入水中(如图右),求所需砝 核弹爆炸时我们应该怎么做 湖泊湿地对气候的影响是什么? 2008年年初冰冻雨雪灾害造成的影响是什么?涉及经济 社会 农业 等等相关因素的影响 核弹爆炸 人会怎么样?最好有图,描述,好的话我加50 人类的哪些活动对湿地环境有影响,分别造成了那些后果?人类的哪些活动对湿地环境有影响 2008年1月10日开始的低温雨雪冰冻造成我国部分地区严重灾害,其中高压输电线因结冰而损坏严重.此次灾害牵动亿万人的心.为消除高压输电线上的凌冰有人设计了这样的融冰思路:利用电流的 描写春天天气好的成语,段落 天为什么是大海的颜色其实我想问的是大海为什么是蓝色的 不能是红的 黄的 绿的吗 描写美丽山水的句子 5月2日在智利南部查纳拍到的照片中,闪电正在划破被柴滕火山喷发出的火山灰笼罩的天空.目前已有许多证据显示火山喷发有时会伴随着雷电天气,科学家怎么解释引发此现象的原因? 天是什么颜色,海是什么味道. 同样是100斤的水100度的水到入零下60度的水剩下多少度 火山爆发时伴随着雷电和暴风雨 是为什么? 土壤颜色能改变花的颜色吗.(我只知道气候温度能够改变花的颜色)急用啊、写论文啊 英语翻译it is __ -___ that the earth ____ ____the sun
备案号:鲁ICP备13029499号-2 说三道四 www.s3d4.cn 说三道四技术文摘