Repository: xufei/blog Branch: master Commit: 2e73c0bccbab Files: 24 Total size: 126.6 KB Directory structure: gitextract_16qbl5sl/ ├── README.md ├── gitpress.json ├── posts/ │ ├── 2013-01-14-前端开发技术的发展.md │ ├── 2013-07-01-从零开始编写自己的JavaScript框架(一).md │ ├── 2013-07-10-从零开始编写自己的JavaScript框架(二).md │ ├── 2013-10-01-企业软件领域前端开发的困境.md │ ├── 2013-10-22-前端MV☆框架的意义.md │ ├── 2013-11-20-Web应用的组件化开发(一).md │ ├── 2013-12-02-一些JS题目的解答.md │ ├── 2013-12-09-Web应用的组件化(二).md │ ├── 2014-01-06-影响企业应用前端开发效率的因素.md │ ├── 2014-01-20-为什么企业应用这么“钟情”于IE6.md │ ├── 2014-04-27-基于AngularJS的企业软件前端架构.md │ ├── 2014-04-27-清华故地重游.md │ ├── 2014-05-20-前端架构那些事儿.md │ ├── 2014-10-01-From HTC to Web Components.md │ ├── 2014-10-04-to-be-a-fe.md │ ├── 2014-10-21-build-single-page-application.md │ └── 2015-02-26-components-in-webapp.md └── temp/ ├── 01.md ├── 02.md ├── 03.md ├── 04.md └── 05.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ blog ==== my personal blog # 目录 ## Angular系列 #### - [AngularJS实例教程(一)——数据绑定与监控](https://github.com/xufei/blog/issues/14) #### - [AngularJS实例教程(二)——作用域与事件](https://github.com/xufei/blog/issues/18) #### - [Angular沉思录(一)数据绑定](https://github.com/xufei/blog/issues/10) #### - [Angular沉思录(二)视图模型的层次](https://github.com/xufei/blog/issues/11) #### - [Angular沉思录(三)Angular中的模块机制](https://github.com/xufei/blog/issues/17) #### - [Angular的变革](https://github.com/xufei/blog/issues/25) #### - [浴火重生的Angular](https://github.com/xufei/blog/issues/9) #### - [后Angular时代二三事](https://github.com/xufei/blog/issues/21) #### - [优化Angular应用的性能](https://github.com/xufei/blog/issues/23) #### - [[翻译]有关Angular 2.0的一切](https://github.com/xufei/blog/issues/8) #### - [[翻译]Angular的问题](https://github.com/xufei/blog/issues/15) ## Web应用 #### - [构建单页Web应用](https://github.com/xufei/blog/issues/5) #### - [Web应用组件化的权衡](https://github.com/xufei/blog/issues/22) #### - [Web应用的组件化(一)——基本思路](https://github.com/xufei/blog/issues/6) #### - [Web应用的组件化(二)——管控平台](https://github.com/xufei/blog/issues/7) #### - [从HTML Components的衰落看Web Components的危机](https://github.com/xufei/blog/issues/3) ## 随笔系列 #### - [老码农的技术理想](https://github.com/xufei/blog/issues/16) #### - [未来Web应用的前端技术选型畅想](https://github.com/xufei/blog/issues/24) #### - [2015前端组件化框架之路](https://github.com/xufei/blog/issues/19) #### - [今年搞的一些活动的意图](https://github.com/xufei/blog/issues/13) #### - [企业文化与价值观 — 给新员工的一封信](https://github.com/xufei/blog/issues/12) #### - [给一位打算从事前端,但是又有疑惑的在校大学生的回信](https://github.com/xufei/blog/issues/4) ================================================ FILE: gitpress.json ================================================ { "docs" : {"all": "posts"}, "perpage" : 5, "types" : { "\\.(md||markdown)$" : "markdown", "\\.(js||css||json)$" : "code", "\\.html?$" : "html", ".*" : "text" }, "title" : "民工的砖墙", "comment" : {"type": "duoshuo", "short_name": "xufei"}, "friends" : [ { "name" : "月影MM", "title": "十年踪迹的博客", "url" : "http://blog.silverna.org/" }, { "name" : "JerryQu的小站", "title": "屈屈的博客", "url" : "http://imququ.com" }, { "name" : "dh20156", "title": "dh20156踪迹", "url" : "http://www.w3cgroup.com/" }, { "name" : "doyoe", "title" : "CSS探索之旅", "url" : "http://blog.doyoe.com/" } ] } ================================================ FILE: posts/2013-01-14-前端开发技术的发展.md ================================================ 前端开发技术的发展 == 前端开发技术,从狭义的定义来看,是指围绕HTML、JavaScript、CSS这样一套体系的开发技术,它的运行宿主是浏览器。从广义的定义来看,包括了: - 专门为手持终端设计的类似WML这样的类HTML语言,类似WMLScript这样的类JavaScript语言。 - VML和SVG等基于XML的描述图形的语言。 - 从属于XML体系的XML,XPath,DTD等技术。 - 用于支撑后端的ASP,JSP,ASP.net,PHP,nodejs等语言或者技术。 - 被第三方程序打包的一种类似浏览器的宿主环境,比如Adobe AIR和使用HyBird方式的一些开发技术,如PhoneGap(它使用Android中的WebView等技术,让开发人员使用传统Web开发技术来开发本地应用) - Adobe Flash,Flex,Microsoft Silverlight,Java Applet,JavaFx等RIA开发技术。 本文从狭义的前端定义出发,探讨一下这方面开发技术的发展过程。 从前端开发技术的发展来看,大致可以分为以下几个阶段: #一. 刀耕火种 ##1. 静态页面 最早期的Web界面基本都是在互联网上使用,人们浏览某些内容,填写几个表单,并且提交。当时的界面以浏览为主,基本都是HTML代码,有时候穿插一些JavaScript,作为客户端校验这样的基础功能。代码的组织比较简单,而且CSS的运用也是比较少的。 最简单的是这样一个文件: 测试一

主标题

段落内容

##2. 带有简单逻辑的界面 这个界面带有一段JavaScript代码,用于拼接两个输入框中的字符串,并且弹出窗口显示。 测试二 ##3. 结合了服务端技术的混合编程 由于静态界面不能实现保存数据等功能,出现了很多服务端技术,早期的有CGI(Common Gateway Interface,多数用C语言或者Perl实现的),ASP(使用VBScript或者JScript),JSP(使用Java),PHP等等,Python和Ruby等语言也常被用于这类用途。 有了这类技术,在HTML中就可以使用表单的post功能提交数据了,比如:

First Name:

Last Name:

在这个阶段,由于客户端和服务端的职责未作明确的划分,比如生成一个字符串,可以由前端的JavaScript做,也可以由服务端语言做,所以通常在一个界面里,会有两种语言混杂在一起,用<%和%>标记的部分会在服务端执行,输出结果,甚至经常有把数据库连接的代码跟页面代码混杂在一起的情况,给维护带来较大的不便。

Hello world!

<% response.write("Hello world from server!") %>

##4.组件化的萌芽 这个时代,也逐渐出现了组件化的萌芽。比较常见的有服务端的组件化,比如把某一类服务端功能单独做成片段,然后其他需要的地方来include进来,典型的有:ASP里面数据库连接的地方,把数据源连接的部分写成conn.asp,然后其他每个需要操作数据库的asp文件包含它。 上面所说的是在服务端做的,浏览器端通常有针对JavaScript的,把某一类的Javascript代码写到单独的js文件中,界面根据需要,引用不同的js文件。针对界面的组件方式,通常利用frameset和iframe这两个标签。某一大块有独立功能的界面写到一个html文件,然后在主界面里面把它当作一个frame来载入,一般的B/S系统集成菜单的方式都是这样的。 此外,还出现了一些基于特定浏览器的客户端组件技术,比如IE浏览器的HTC(HTML Component)。这种技术最初是为了对已有的常用元素附加行为的,后来有些场合也用它来实现控件。微软ASP.net的一些版本里,使用这种技术提供了树形列表,日历,选项卡等功能。HTC的优点是允许用户自行扩展HTML标签,可以在自己的命名空间里定义元素,然后,使用HTML,JavaScript和CSS来实现它的布局、行为和观感。这种技术因为是微软的私有技术,所以逐渐变得不那么流行。 Firefox浏览器里面推出过一种叫XUL的技术,也没有流行起来。 #二. 铁器时代 这个时代的典型特征是Ajax的出现。 ##1. AJAX AJAX其实是一系列已有技术的组合,早在这个名词出现之前,这些技术的使用就已经比较广泛了,GMail因为恰当地应用了这些技术,获得了很好的用户体验。 由于Ajax的出现,规模更大,效果更好的Web程序逐渐出现,在这些程序中,JavaScript代码的数量迅速增加。出于代码组织的需要,“JavaScript框架”这个概念逐步形成,当时的主流是prototype和mootools,这两者各有千秋,提供了各自方式的面向对象组织思路。 ##2. JavaScript基础库 Prototype框架主要是为JavaScript代码提供了一种组织方式,对一些原生的JavaScript类型提供了一些扩展,比如数组、字符串,又额外提供了一些实用的数据结构,如:枚举,Hash等,除此之外,还对dom操作,事件,表单和Ajax做了一些封装。 Mootools框架的思路跟Prototype很接近,它对JavaScript类型扩展的方式别具一格,所以在这类框架中,经常被称作“最优雅的”对象扩展体系。 从这两个框架的所提供的功能来看,它们的定位是核心库,在使用的时候一般需要配合一些外围的库来完成。 jQuery与这两者有所不同,它着眼于简化DOM相关的代码。 例如: - DOM的选择 jQuery提供了一系列选择器用于选取界面元素,在其他一些框架中也有类似功能,但是一般没有它的简洁、强大。 $("*") //选取所有元素 $("#lastname") //选取id为lastname的元素 $(".intro") //选取所有class="intro"的元素 $("p") //选取所有<p>元素 $(".intro.demo") //选取所有 class="intro"且class="demo"的元素 - 链式表达式: 在jQuery中,可以使用链式表达式来连续操作dom,比如下面这个例子: 如果不使用链式表达式,可能我们需要这么写: var neat = $("p.neat"); neat.addClass("ohmy"); neat.show("slow"); 但是有了链式表达式,我们只需要这么一行代码就可以完成这些: $("p.neat").addClass("ohmy").show("slow"); 除此之外,jQuery还提供了一些动画方面的特效代码,也有大量的外围库,比如jQuery UI这样的控件库,jQuery mobile这样的移动开发库等等。 ##3. 模块代码加载方式 以上这些框架提供了代码的组织能力,但是未能提供代码的动态加载能力。动态加载JavaScript为什么重要呢?因为随着Ajax的普及,jQuery等辅助库的出现,Web上可以做很复杂的功能,因此,单页面应用程序(SPA,Single Page Application)也逐渐多了起来。 单个的界面想要做很多功能,需要写的代码是会比较多的,但是,并非所有的功能都需要在界面加载的时候就全部引入,如果能够在需要的时候才加载那些代码,就把加载的压力分担了,在这个背景下,出现了一些用于动态加载JavaScript的框架,也出现了一些定义这类可被动态加载代码的规范。 在这些框架里,知名度比较高的是RequireJS,它遵循一种称为AMD(Asynchronous Module Definition)的规范。 比如下面这段,定义了一个动态的匿名模块,它依赖math模块 define(["math"], function(math) { return { addTen : function(x) { return math.add(x, 10); } }; }); 假设上面的代码存放于adder.js中,当需要使用这个模块的时候,通过如下代码来引入adder: RequireJS除了提供异步加载方式,也可以使用同步方式加载模块代码。AMD规范除了使用在前端浏览器环境中,也可以运行于nodejs等服务端环境,nodejs的模块就是基于这套规范定义的。(修订,这里弄错了,nodejs是基于类似的CMD规范的) #三. 工业革命 这个时期,随着Web端功能的日益复杂,人们开始考虑这样一些问题: - 如何更好地模块化开发 - 业务数据如何组织 - 界面和业务数据之间通过何种方式进行交互 在这种背景下,出现了一些前端MVC、MVP、MVVM框架,我们把这些框架统称为MV*框架。这些框架的出现,都是为了解决上面这些问题,具体的实现思路各有不同,主流的有Backbone,AngularJS,Ember,Spine等等,本文主要选用Backbone和AngularJS来讲述以下场景。 ##1. 数据模型 在这些框架里,定义数据模型的方式与以往有些差异,主要在于数据的get和set更加有意义了,比如说,可以把某个实体的get和set绑定到RESTful的服务上,这样,对某个实体的读写可以更新到数据库中。另外一个特点是,它们一般都提供一个事件,用于监控数据的变化,这个机制使得数据绑定成为可能。 在一些框架中,数据模型需要在原生的JavaScript类型上做一层封装,比如Backbone的方式是这样: var Todo = Backbone.Model.extend({ // Default attributes for the todo item. defaults : function() { return { title : "empty todo...", order : Todos.nextOrder(), done : false }; }, // Ensure that each todo created has `title`. initialize : function() { if (!this.get("title")) { this.set({ "title" : this.defaults().title }); } }, // Toggle the 'done' state of this todo item. toggle : function() { this.save({ done : !this.get("done") }); } }); 上述例子中,defaults方法用于提供模型的默认值,initialize方法用于做一些初始化工作,这两个都是约定的方法,toggle是自定义的,用于保存todo的选中状态。 除了对象,Backbone也支持集合类型,集合类型在定义的时候要通过model属性指定其中的元素类型。 // The collection of todos is backed by *localStorage* instead of a remote server. var TodoList = Backbone.Collection.extend({ // Reference to this collection's model. model : Todo, // Save all of the todo items under the '"todos-backbone"' namespace. localStorage : new Backbone.LocalStorage("todos-backbone"), // Filter down the list of all todo items that are finished. done : function() { return this.filter(function(todo) { return todo.get('done'); }); }, // Filter down the list to only todo items that are still not finished. remaining : function() { return this.without.apply(this, this.done()); }, // We keep the Todos in sequential order, despite being saved by unordered //GUID in the database. This generates the next order number for new items. nextOrder : function() { if (!this.length) return 1; return this.last().get('order') + 1; }, // Todos are sorted by their original insertion order. comparator : function(todo) { return todo.get('order'); } }); 数据模型也可以包含一些方法,比如自身的校验,或者跟后端的通讯、数据的存取等等,在上面两个例子中,也都有体现。 AngularJS的模型定义方式与Backbone不同,可以不需要经过一层封装,直接使用原生的JavaScript简单数据、对象、数组,相对来说比较简便。 ##2. 控制器 在Backbone中,是没有独立的控制器的,它的一些控制的职责都放在了视图里,所以其实这是一种MVP(Model View Presentation)模式,而AngularJS有很清晰的控制器层。 还是以这个todo为例,在AngularJS中,会有一些约定的注入,比如$scope,它是控制器、模型和视图之间的桥梁。在控制器定义的时候,将$scope作为参数,然后,就可以在控制器里面为它添加模型的支持。 function TodoCtrl($scope) { $scope.todos = [{ text : 'learn angular', done : true }, { text : 'build an angular app', done : false }]; $scope.addTodo = function() { $scope.todos.push({ text : $scope.todoText, done : false }); $scope.todoText = ''; }; $scope.remaining = function() { var count = 0; angular.forEach($scope.todos, function(todo) { count += todo.done ? 0 : 1; }); return count; }; $scope.archive = function() { var oldTodos = $scope.todos; $scope.todos = []; angular.forEach(oldTodos, function(todo) { if (!todo.done) $scope.todos.push(todo); }); }; } 本例中为$scope添加了todos这个数组,addTodo,remaining和archive三个方法,然后,可以在视图中对他们进行绑定。 ##3. 视图 在这些主流的MV*框架中,一般都提供了定义视图的功能。在Backbone中,是这样定义视图的: // The DOM element for a todo item... var TodoView = Backbone.View.extend({ //... is a list tag. tagName : "li", // Cache the template function for a single item. template : _.template($('#item-template').html()), // The DOM events specific to an item. events : { "click .toggle" : "toggleDone", "dblclick .view" : "edit", "click a.destroy" : "clear", "keypress .edit" : "updateOnEnter", "blur .edit" : "close" }, // The TodoView listens for changes to its model, re-rendering. Since there's // a one-to-one correspondence between a **Todo** and a **TodoView** in this // app, we set a direct reference on the model for convenience. initialize : function() { this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); }, // Re-render the titles of the todo item. render : function() { this.$el.html(this.template(this.model.toJSON())); this.$el.toggleClass('done', this.model.get('done')); this.input = this.$('.edit'); return this; }, //...... // Remove the item, destroy the model. clear : function() { this.model.destroy(); } }); 上面这个例子是一个典型的“部件”视图,它对于界面上的已有元素没有依赖。也有那么一些视图,需要依赖于界面上的已有元素,比如下面这个,它通过el属性,指定了HTML中id为todoapp的元素,并且还在initialize方法中引用了另外一些元素,通常,需要直接放置到界面的顶层试图会采用这种方式,而“部件”视图一般由主视图来创建、布局。 // Our overall **AppView** is the top-level piece of UI. var AppView = Backbone.View.extend({ // Instead of generating a new element, bind to the existing skeleton of // the App already present in the HTML. el : $("#todoapp"), // Our template for the line of statistics at the bottom of the app. statsTemplate : _.template($('#stats-template').html()), // Delegated events for creating new items, and clearing completed ones. events : { "keypress #new-todo" : "createOnEnter", "click #clear-completed" : "clearCompleted", "click #toggle-all" : "toggleAllComplete" }, // At initialization we bind to the relevant events on the `Todos` // collection, when items are added or changed. Kick things off by // loading any preexisting todos that might be saved in *localStorage*. initialize : function() { this.input = this.$("#new-todo"); this.allCheckbox = this.$("#toggle-all")[0]; this.listenTo(Todos, 'add', this.addOne); this.listenTo(Todos, 'reset', this.addAll); this.listenTo(Todos, 'all', this.render); this.footer = this.$('footer'); this.main = $('#main'); Todos.fetch(); }, // Re-rendering the App just means refreshing the statistics -- the rest // of the app doesn't change. render : function() { var done = Todos.done().length; var remaining = Todos.remaining().length; if (Todos.length) { this.main.show(); this.footer.show(); this.footer.html(this.statsTemplate({ done : done, remaining : remaining })); } else { this.main.hide(); this.footer.hide(); } this.allCheckbox.checked = !remaining; }, //...... }); 对于AngularJS来说,基本不需要有额外的视图定义,它采用的是直接定义在HTML上的方式,比如:
{{remaining()}} of {{todos.length}} remaining archive
在这个例子中,使用ng-controller注入了一个TodoCtrl的实例,然后,在TodoCtrl的$scope中附加的那些变量和方法都可以直接访问了。注意到其中的ng-repeat部分,它遍历了todos数组,然后使用其中的单个todo对象创建了一些HTML元素,把相应的值填到里面。这种做法和ng-model一样,都创造了双向绑定,即: - 改变模型可以随时反映到界面上 - 在界面上做的操作(输入,选择等等)可以实时反映到模型里。 而且,这种绑定都会自动忽略其中可能因为空数据而引起的异常情况。 ##4. 模板 模板是这个时期一种很典型的解决方案。我们常常有这样的场景:在一个界面上重复展示类似的DOM片段,例如微博。以传统的开发方式,也可以轻松实现出来,比如: var feedsDiv = $("#feedsDiv"); for (var i = 0; i < 5; i++) { var feedDiv = $("
"); var authorDiv = $("
"); var authorLink = $("") .attr("href", "/user.html?user='" + "Test" + "'") .html("@" + "Test") .appendTo(authorDiv); authorDiv.appendTo(feedDiv); var contentDiv = $("
") .html("Hello, world!") .appendTo(feedDiv); var dateDiv = $("
") .html("发布日期:" + new Date().toString()) .appendTo(feedDiv); feedDiv.appendTo(feedsDiv); } 但是使用模板技术,这一切可以更加优雅,以常用的模板框架UnderScore为例,实现这段功能的代码为: var templateStr = '
' +'
' + '@{{creatorName}}' +'
' +'
{{content}}
' +'
{{postedDate}}
' +'
'; var template = _.template(templateStr); template({ createName : "Xufei", content: "Hello, world", postedDate: new Date().toString() }); 也可以这么定义: 除此之外,UnderScore还提供了一些很方便的集合操作,使得模板的使用更加方便。如果你打算使用BackBone框架,并且需要用到模板功能,那么UnderScore是一个很好的选择,当然,也可以选用其它的模板库,比如Mustache等等。 如果使用AngularJS,可以不需要额外的模板库,它自身就提供了类似的功能,比如上面这个例子可以改写成这样:
@{{post.creatorName}}
{{post.content}}
发布日期:{{post.postedTime | date:'medium'}}
主流的模板技术都提供了一些特定的语法,有些功能很强。值得注意的是,他们虽然与JSP之类的代码写法类似甚至相同,但原理差别很大,这些模板框架都是在浏览器端执行的,不依赖任何服务端技术,即使界面文件是.html也可以,而传统比如JSP模板是需要后端支持的,执行时间是在服务端。 ##5. 路由 通常路由是定义在后端的,但是在这类MV*框架的帮助下,路由可以由前端来解析执行。比如下面这个Backbone的路由示例: var Workspace = Backbone.Router.extend({ routes: { "help": "help", // #help "search/:query": "search", // #search/kiwis "search/:query/p:page": "search" // #search/kiwis/p7 }, help: function() { ... }, search: function(query, page) { ... } }); 在上述例子中,定义了一些路由的映射关系,那么,在实际访问的时候,如果在地址栏输入"#search/obama/p2",就会匹配到"search/:query/p:page"这条路由,然后,把"obama"和"2"当作参数,传递给search方法。 AngularJS中定义路由的方式有些区别,它使用一个$routeProvider来提供路由的存取,每一个when表达式配置一条路由信息,otherwise配置默认路由,在配置路由的时候,可以指定一个额外的控制器,用于控制这条路由对应的html界面: app.config(['$routeProvider', function($routeProvider) { $routeProvider.when('/phones', { templateUrl : 'partials/phone-list.html', controller : PhoneListCtrl }).when('/phones/:phoneId', { templateUrl : 'partials/phone-detail.html', controller : PhoneDetailCtrl }).otherwise({ redirectTo : '/phones' }); }]); 注意,在AngularJS中,路由的template并非一个完整的html文件,而是其中的一段,文件的头尾都可以不要,也可以不要那些包含的外部样式和JavaScript文件,这些在主界面中载入就可以了。 ##6. 自定义标签 用过XAML或者MXML的人一定会对其中的可扩充标签印象深刻,对于前端开发人员而言,基于标签的组件定义方式一定是优于其他任何方式的,看下面这段HTML:
即使是刚刚接触这种东西的新手,也能够理解它的意思,并且能够照着做出类似的东西,如果使用传统的面向对象语言去描述界面,效率远远没有这么高,这就是在界面开发领域,声明式编程比命令式编程适合的最重要原因。 但是,HTML的标签是有限的,如果我们需要的功能不在其中,怎么办?在开发过程中,我们可能需要一个选项卡的功能,但是,HTML里面不提供选项卡标签,所以,一般来说,会使用一些li元素和div的组合,加上一些css,来实现选项卡的效果,也有的框架使用JavaScript来完成这些功能。总的来说,这些代码都不够简洁直观。 如果能够有一种技术,能够提供类似这样的方式,该多么好呢? content 1 content 2 回忆一下,我们在章节1.4 组件化的萌芽 里面,提到过一种叫做HTC的技术,这种技术提供了类似的功能,而且使用起来也比较简便,问题是,它属于一种正在消亡的技术,于是我们的目光投向了更为现代的前端世界,AngularJS拯救了我们。 在AngularJS的首页,可以看到这么一个区块“Create Components”,在它的演示代码里,能够看到类似的一段: ... ... 那么,它是怎么做到的呢?秘密在这里: angular.module('components', []).directive('tabs', function() { return { restrict : 'E', transclude : true, scope : {}, controller : function($scope, $element) { var panes = $scope.panes = []; $scope.select = function(pane) { angular.forEach(panes, function(pane) { pane.selected = false; }); pane.selected = true; } this.addPane = function(pane) { if (panes.length == 0) $scope.select(pane); panes.push(pane); } }, template : '
' + '' + '
' + '
', replace : true }; }).directive('pane', function() { return { require : '^tabs', restrict : 'E', transclude : true, scope : { title : '@' }, link : function(scope, element, attrs, tabsCtrl) { tabsCtrl.addPane(scope); }, template : '
' + '
', replace : true }; }) 这段代码里,定义了tabs和pane两个标签,并且限定了pane标签不能脱离tabs而单独存在,tabs的controller定义了它的行为,两者的template定义了实际生成的html,通过这种方式,开发者可以扩展出自己需要的新元素,对于使用者而言,这不会增加任何额外的负担。 #四. 一些想说的话 ###关于ExtJS 注意到在本文中,并未提及这样一个比较流行的前端框架,主要是因为他自成一系,思路跟其他框架不同,所做的事情,层次介于文中的二和三之间,所以没有单独列出。 ###写作目的 在我10多年的Web开发生涯中,经历了Web相关技术的各种变革,从2003年开始,接触并使用到了HTC,VML,XMLHTTP等当时比较先进的技术,目睹了网景浏览器的衰落,IE的后来居上,Firefox和Chrome的逆袭,各类RIA技术的风起云涌,对JavaScript的模块化有过持续的思考。未来究竟是什么样子?我说不清楚,只能凭自己的一些认识,把这些年一些比较主流的发展过程总结一下,供有需要了解的朋友们作个参考,错漏在所难免,欢迎大家指教。 个人邮箱:xu.fei@outlook.com 新浪微博:http://weibo.com/sharpmaster ================================================ FILE: posts/2013-07-01-从零开始编写自己的JavaScript框架(一).md ================================================ 从零开始编写自己的JavaScript框架(一) ==== #1. 模块的定义和加载 ##1.1 模块的定义 一个框架想要能支撑较大的应用,首先要考虑怎么做模块化。有了内核和模块加载系统,外围的模块就可以一个一个增加。不同的JavaScript框架,实现模块化方式各有不同,我们来选择一种比较优雅的方式作个讲解。 先问个问题:我们做模块系统的目的是什么?如果觉得这个问题难以回答,可以从反面来考虑:假如不做模块系统,有什么样的坏处? 我们经历过比较粗放、混乱的前端开发阶段,页面里充满了全局变量,全局函数。那时候要复用js文件,就是把某些js函数放到一个文件里,然后让多个页面都来引用。 考虑到一个页面可以引用多个这样的js,这些js互相又不知道别人里面写了什么,很容易造成命名的冲突,而产生这种冲突的时候,又没有哪里能够提示出来。所以我们要有一种办法,把作用域比较好地隔开。 JavaScript这种语言比较奇怪,奇怪在哪里呢,它的现有版本里没package跟class,要是有,我们也没必要来考虑什么自己做模块化了。那它是要用什么东西来隔绝作用域呢? 在很多传统高级语言里,变量作用域的边界是大括号,在{}里面定义的变量,作用域不会传到外面去,但我们的JavaScript大人不是这样的,他的边界是function。所以我们这段代码,i仍然能打出值: for (var i=0; i<5; i++) { //do something } alert(i); 那么,我们只能选用function做变量的容器,把每个模块封装到一个function里。现在问题又来了,这个function本身的作用域是全局的,怎么办?我们想不到办法,拔剑四顾心茫然。 我们有没有什么可参照的东西呢?这时候,脑海中一群语言飘过: C语言飘过:“我不是面向对象语言哦~不需要像你这么组织哦~”,“死开!” Java飘过:“我是纯面向对象语言哦,连main都要在类中哦,编译的时候通过装箱清单指定入口哦~”,“死开!” C++飘过:“我也是纯面向对象语言哦”,等等,C++是纯面向对象的语言吗?你的main是什么???main是特例,不在任何类中! 啊,我们发现了什么,既然无法避免全局的作用域,那与其让100个function都全局,不如只让一个来全局,其他的都由它管理。 本来我们打算自己当上帝的,现在只好改行先当个工商局长。你想开店吗?先来注册,不然封杀你!于是良民们纷纷来注册。店名叫什么,从哪进货,卖什么的,一一登记在案,为了方便下面的讨论,我们连进货的过程都让工商局管理起来。 店名,指的就是这里的模块名,从哪里进货,代表它依赖什么其他模块,卖什么,表示它对外提供一些什么特性。 好了,考虑到我们的这个注册管理机构是个全局作用域,我们还得把它挂在window上作为属性,然后再用一个function隔离出来,要不然,别人也定义一个同名的,就把我们覆盖掉了。 (function() { window.thin = { define: function(name, dependencies, factory) { //register a module } }; })(); 在这个module方法内部,应当怎么去实现呢?我们的module应当有一个地方存储,但存储是要在工商局内部的,不是随便什么人都可以看到的,所以,这个存储结构也放在工商局同样的作用域里。 用什么结构去存储呢?工商局备案的时候,店名不能跟已有的重复,所以我们发现这是用map的很好场景,考虑到JavaScript语言层面没有map,我们弄个Object来存。 (function() { var moduleMap = {}; window.thin = { define: function(name, dependencies, factory) { if (!moduleMap[name]) { var module = { name: name, dependencies: dependencies, factory: factory }; moduleMap[name] = module; } return moduleMap[name]; } }; })(); 现在,模块的存储结构就搞好了。 ##1.2 模块的使用 存的部分搞好了,我们来看看怎么取。现在来了一个商家,卖木器的,他需要从一个卖钉子的那边进货,卖钉子的已经来注册过了,现在要让这个木器厂能买到钉子。现在的问题是,两个商家处于不同的作用域,也就是说,它们互相不可见,那通过什么方式,我们才能让他们产生调用关系呢? 个人解决不了的问题还是得靠政府,有困难要坚决克服,没有困难就制造困难来克服。现在困难有了,该克服了。商家说,我能不能给你我的进货名单,你帮我查一下它们在哪家店,然后告诉我?这么简单的要求当然一口答应下来,但是采用什么方式传递给你呢?这可犯难了。 我们参考AngularJS框架,写了一个类似的代码: thin.define("A", [], function() { //module A }); thin.define("B", ["A"], function(A) { //module B var a = new A(); }); 看这段代码特别在哪里呢?模块A的定义,毫无特别之处,主要看模块B。它在依赖关系里写了一个字符串的A,然后在工厂方法的形参写了一个真真切切的A类型。嗯?这个有些奇怪啊,你的A类型要怎么传递过来呢?其实是很简单的,因为我们声明了依赖项的数组,所以可以从依赖项,挨个得到对应的工厂方法,然后创建实例,传进来。 use: function(name) { var module = moduleMap[name]; if (!module.entity) { var args = []; for (var i=0; i var person = { name: "Tom" }; 如果我们给name重新赋值,person.name = "Jerry",怎么才能让界面得到变更? 从直觉来说,我们需要在name发生改变的时候,触发一个事件,或者调用某个指定的方法,然后才好着手做后面的事情,比如: var person = { name: "Tom", setName: function(newName) { this.name = newName; //do something } }; 这样我们可以在setName里面去给input赋值。推而广之,为了使得实体包含的多个属性都可以运作,可以这么做: var person = { name: "Tom", gender: 5 set: function(key, value) { this[key] = value; //do something } }; 或者合并两个方法,只判断是否传了参数: Person.prototype.name = function(value) { if (arguments.length == 0) { return this._name; } else { this._name = value; } } 这种情况下,赋值的时候就是person.name("Tom"),取值的时候就是var name = person.name()了。 有一些框架是通过这种方式来变通实现数据绑定的,对数据的写入只能通过方法调用。但这种方式很不直接,我们来想点别的办法。 在C#等一些语言里,有一种东西叫做存取器,比如说: class Person { private string name; public string Name { get { return name; } set { name = value; } } } 用的时候,person.Name = "Jerry",就会调用到set里,相当于是个方法。 这一点非常好,很符合我们的需要,那JavaScript里面有没有类似存取器的特性呢?老早以前是没有的,但现在有了,那就是Object.defineProperty,它的第三个参数就是可选的存取函数。比如说: var person = {}; // Add an accessor property to the object. Object.defineProperty(person, "name", { set: function (value) { this._name = value; //do something }, get: function () { return this._name; }, enumerable: true, configurable: true }); 赋值的时候,person.name = "Tom",取值的时候,var name = person.name,简直太美妙了。注意这里define的时候,是定义在实例上的,如果想要定义到类型里面,可以在构造器里面定义。 现在我们从数据到DOM的绑定可以解决掉了,至少我们能够在变量被更改的时候去做一些自己的事情,比如查找这个属性被绑定到哪些控件了,然后挨个对其赋值。框架怎么知道属性被绑定到哪些控件了呢?这个直接在第二部分的实现过程中讨论。 再看控件到数据的绑定,这个其实很好理解。无非就是给控件添加change之类的事件监听,在这里面把关联到的数据更新掉。到这里,我们在原理方面已经没有什么问题了,现在开始准备把它写出来。 ##2.2 数据绑定的实现 我们的框架启动之后,要先把前面所说的这种绑定关系收集起来,这种属性会分布于DOM的各个角落,一个很现实的做法是,递归遍历界面的每个DOM节点,检测该属性,于是我们代码的结构大致如下所示。 function parseElement(element) { for (var i=0; i 为了使得结构更加容易看,我们把界面的无关属性比如样式之类都去掉了,只留下不能再减少的这么一段。现在我们可以看到,在界面的顶层定义一个vm-model属性,值为实体的名称。两个输入框通过vm-value来绑定到实例属性,vm-init绑定界面的初始化方法,vm-click绑定按钮的点击事件。 好了,现在我们可以来扫描这个简单的DOM结构了。想要做这么一个绑定,首先要考虑数据从哪里来?在绑定name和code属性之前,毫无疑问,应当先实例化一个Person,我们怎么才能知道需要把Person模块实例化呢? 当扫描到一个DOM元素的时候,我们要先检测它的vm-model属性,如果有值,就取这个值来实例化,然后,把这个值一直传递下去,在扫描其他属性或者下属DOM元素的时候都带进去。这么一来,parseElement就变成一个递归了,于是它只好有两个参数,变成了这样: function parseElement(element, vm) { var model = vm; if (element.getAttribute("vm-model")) { model = bindModel(element.getAttribute("vm-model")); } for (var i=0; i Simple binding demo
或者访问这里:http://xufei.github.io/thin/demo/simple-binding.html 以刚才文章提到的内容,还不能完全解释这个例子的效果,因为没看到在哪里调用parseElement的。说来也简单,就在thin.js里面,直接写了一个thin.ready,在那边调用了这个函数,去解析了document.body,于是测试页面里面才可以只写绑定和视图模型。 我们还有一个更实际一点的例子,结合了另外一个系列里面写的简单DataGrid控件,做了一个很基础的人员管理界面:http://xufei.github.io/thin/demo/binding.html ##2.3 小结 到此为止,我们的绑定框架勉强能够运行起来了!虽然很简陋,而且要比较新的浏览器才能跑,但毕竟是跑起来了。 注意Object.defineProperty仅在Chrome等浏览器中可用,IE需要9以上才比较正常。在司徒正美的avalon框架中,巧妙使用VBScript绕过这一限制,利用vbs的property和两种语言的互通,实现了低版本IE的兼容。我们这个框架的目标不是兼容,而是为了说明原理,所以感兴趣的朋友可以去看看avalon的源码。 ================================================ FILE: posts/2013-10-01-企业软件领域前端开发的困境.md ================================================ 企业软件领域前端开发的困境 ==== 前一段时间,看到阿里几位前端大师的讨论:[阿里前端的困局与突围][1],对这个职业的发展方向有一些思考,我上次跟winter和dh一起吃饭,也简单聊到这个话题。 winter问了一个问题,如果在互联网企业跟游戏开发的企业同时进行一次针对前端开发的大裁员,对这个企业的核心价值而言,哪种影响更大? 这个问题问得很有意思,在每个行业里,前端开发的侧重点是不一样的,重要性也有所不同,简单来说可以分为3个大类:互联网、企业应用、游戏,分别侧重于:交互、架构、算法。 在这三个大类里,互联网方向的前端开发最为正统,算是根正苗红,所以在这个领域的人,对标准研究得最为透彻,对交互理解得最为深刻,目前前端方向的高手大多集中在这个领域。这个领域的人最关注的问题是兼容性,对一些细节的优化把握得炉火纯青。因为这个领域的业务特点,前端做的事情过于扁平,整个可发挥的余地不够,虽然高手众多,但就像很多龙挤在浅水里,常常有无用武之地的感叹。刚才玉伯这篇文章,讲述的就是这个领域中前端的困惑。 企业应用方向的前端开发其实很多时候并不在意他们用的是Web还是其他类似技术,比如Flex,Silverlight等,对他们来说,即使是C/S的系统,也能够发挥出很大价值。这个领域的人最关注的问题是组件化和快速业务开发。 从企业软件的方向来说,它的业务是很丰富的,对各种前端技术的应用也都很广泛,一个大型的企业应用,几乎什么特性都能用上。相对于互联网系统,它的客户相对比较专业,可以排除一些低端过时的浏览器,所以少了很多兼容的负担,在框架选型上,也可以接受很复杂的框架,比如ExtJS、AngularJS等,因为他们的业务特性,往往需要很复杂框架的支持。 用我所在的电信行业软件举例,业务复杂度非常高,一套全业务系统会有两千左右的数据库表,两千个左右的业务菜单,其中有些业务界面的复杂度非常惊人,而且经常会根据需求有较大变动,性能也有较高要求。 理论上来说,前端在这个领域可以领悟到很多事情,但这个领域有个最大的问题,盈利太低,不足以支撑很深入的研究,另一个无奈的问题是,由于历史原因,前端开发人员在这个领域并不容易受到重视,比如说资深技术人员多数是做后端开发的,认为前端很小儿科,在应届生入职筛选的时候,也会把能力较高的弄去做后端开发,剩下的留给前端。这些原因,造成了企业应用的前端领域就像一个又大又深的湖,里面小鱼小虾众多,却很少有大鱼。 我在企业软件前端开发做了很多年,经常思考其中的一些问题,在这个领域做,总是有一种寂寞的感觉。我们这种行业,为了保证交付的及时,倾向于划分业务开发和技术平台开发,业务开发人员并不在难解决的问题上花时间,遇到问题的时候向技术平台团队寻求支持,把问题转移给更专业的人,避免耽误自己的交付时间,在他们开工之前,也由技术平台团队预先搭建框架,他们直接在这个上面以固定的模式进行开发。两个团队,前者的特点是多而泛,后者的特点是少而精。 这么做,效率比较高,但带来一个问题,业务开发团队的技术水平很难提升,因为他总是忙碌赶工,很少有时间去思考很多问题的前因后果,即使你帮他解决了问题,告诉他,他也不一定有心情去关注一遍,因为他确实很忙。可能有些有激情的人会自己花点时间研究一下,但多数人很难有这样的心境。 这就造成了业务开发团队和平台开发团队的技术实力严重脱节。从另外一个角度看,技术平台团队长期专门给别人解决问题,自己却很少全职参与某个业务项目的开发,他也很难有成就感。这还不是最大的问题,最大问题是,不管从哪个团队,都很难成长出能够设计最适合这些业务的前端架构的人,这恰恰是这个领域前端开发最重要的部分。 当出现各种新技术的时候,平台团队比较容易去快速跟进,但投入通常不会很大,当取得一些进展的时候,会逐步向业务开发团队推广,但这个推广的难度是很大的,因为人数的比例会比较大,当技术从一个人向三个人推进的时候,是相对还算容易的,如果从一个向十个或者二十个人去推进,难度就大多了。而由于传统企业盈利规模的限制,没办法在每个技术方向都有较大投入,所以往往就是一两个人去折腾,他们在探索的过程中遇到问题,是很难找到能够交流的人的,如果自己解决不了问题,就会持续苦闷,非常寂寞。前端这个领域更是如此,现在客户端技术这么多,各种终端,各种浏览器,各种前端框架,每个上面投入一个人,就已经是个很大团队了,这种模式很明显就碰到瓶颈了,因为它很直接地跟人员编制产生了冲突。扩编直接对利润产生冲击,但是不扩编的话,技术平台团队的压力就会进一步加大,除了要探索新技术,还要对越来越庞大的业务开发团队作技术支持,每个人都痛不欲生。 这种困境怎么解决呢,我想了很久,无计可施,也许,是时候要效仿互联网企业的开发模式了?但是积重难返,而且传统企业招聘的门槛远比互联网企业低,人员的能力有差距,也很难有互联网企业那么蓬勃而广泛的技术研究气氛,可能就更难做下去了。 可能这个领域的出路是寻找更为简单快速的开发方式,并且把相关的外围工具也做大做强,在业务领域中,把组件也积累沉淀出来,这时候能够用更少的业务开发人员来实现同等规模的系统,把更多人力节约出来做技术探索和改进吧? [1]: https://github.com/lifesinger/lifesinger.github.com/issues/141 ================================================ FILE: posts/2013-10-22-前端MV☆框架的意义.md ================================================ 前端MV*框架的意义 ==== 经常有人质疑,在前端搞MV*有什么意义?也有人提出这样的疑问:以AngularJS,Knockout,BackBone为代表的MV*框架,它跟jQuery这样的框架有什么区别?我jQuery用得好好的,有什么必要再引入这种框架? 回答这些问题之前,先要理清一些历史,前端从什么时候开始有框架的? 早期前端都是比较简单,基本以页面为工作单元,内容以浏览型为主,也偶尔有简单的表单操作,这个时期每个界面上只有很少的JavaScript逻辑,基本不太需要框架。随着AJAX的出现,Web2.0的兴起,人们可以在页面上可以做比较复杂的事情了,然后前端框架才真正出现了,以jQuery为代表,针对界面上常见的DOM操作,远程请求,数据处理等作了封装,也有专注于处理数据的Underscore,严格来说,这些都不能算框架,而是算库。 库和框架是有一些区别的:库是一种工具,我提供了,你可以不用,即使你用了,也没影响你自己的代码结构。框架则是面向一个领域,提供一套解决方案,如果你用我,就得按照我的方式办事。按照这个定义,jQuery和Underscore都只能算是库,ExtJS和dojo算框架。 MV*框架又是为什么兴起的呢?它的出现,伴随着一些Web产品逐渐往应用方向发展,遇到了在C/S领域相同的问题:由于前端功能的增强、代码的膨胀,导致不得不做“前端的架构”这个事情了。 很多做后端开发的人对前端架构很不屑,认为前端只是很薄的一层东西,做架构干什么?什么,不但要搞架构,还要搞MVC?Java Struts的MVC中,整个前端都只能算是View而已,你还要在这个View里面划分模型和控制器等其他东西?他们中的多数对这个很不屑,但Web前端随着复杂度的增加,很多地方跟客户端已经没有本质区别了。 jQuery的思维方式是:以DOM操作为中心 MV*框架的思维方式是:以模型为中心,DOM操作只是附加 所以回到那个问题上,jQuery满足了你的业务需要,你还有什么必要引入MV*框架? 这个是要看产品类型的,如果是页面型产品,多数确实不太需要它,因为页面中的JavaScript代码,处理交互的绝对远远超过处理模型的,但是如果是应用软件类产品,这就太需要了。 长期做某个行业软件的公司,一般都会沉淀下来一些业务组件,主要体现在数据模型、业务规则和业务流程,这些组件基本都存在于后端,在前端很少有相应的组织。在以往的经验里,他们是有做MVC的,也尝试做了一些界面组件,但做法比较过时,比如说使用JSF或者GWT这样的方式。 JSF的问题是什么?它的问题并不在于界面跟逻辑混合,所谓的纵向切分组件,Polymer这种纯前端框架也是这么切分的,它问题在于组件的生成和渲染不在同一个地方。所以,逻辑代码的位置很尴尬,如果这个界面简单还好说,复杂起来就很麻烦了,就是很多明明是前端逻辑代码,却需要通过后端去生成。 GWT这种方式相对要好一些,它的问题是留给UI调节的余地太小了,比较缺乏灵活性。 这类基于某种服务端技术的组件化方式有一些局限性,比如它较大程度限制了前端的发挥,在早一些的时候,这种方式可能还不错,但是现在随着时代发展,用户对前端用户体验要求越来越高,需要我们把很大一部分精力继续放回前端来。JSF等方案的另外一个问题是绑定了某种服务端环境,很难切换到另外一种后端上,如果碰上要用Hybird方式开发,想复用一些前端逻辑,几乎毫无可能。 那么,我们看看纯前端的框架,看看都是怎么解决这些问题的。以Google为例,它推出了两个框架,Polymer和Angular,而且处于并行发展的阶段,这两者理念还有不小的差别,给不少人带来了困惑。 Polymer切分组件的方式有点类似JSF,它跟HTML5标准中的Shadow DOM和Element有很大联系,这种切分组件的方式非常直观,每个组件都是端到端的,包含UI和逻辑,直接放置到某个界面上就能用,这种方式很容易被业务开发人员接受,但里面的时序比较难处理。 比如说,有两个组件,里面各包含一个下拉框,有数据的联动关系,因为它们处在两个不同的组件里,联动的处理代码就很难写,考虑到组件的特点,要尽量隐藏自己的内部实现,所以从外部获取组件内部的某个元素要绕一层,而组件不能依赖其他外部的东西,所以到最后只有通过事件去实现,这个联动代码写好了应当放在哪里,也是个大问题。我们的例子仅仅是这么简单,就要绕这么个大圈子才能保证时序,如果场景比较复杂,非常难以控制。 如果同样的组件在某个界面被复用多次,数据的一致性也很难保证,设想一下某个界面存在两个一样的下拉框,分别处于不同组件中,两者的数据都需要分别去加载,这个过程是有浪费的,更严重的是,如果这个下拉框对应的数据有更新,很难把每个实例都更新一遍,这个处理过程是非常麻烦的。 Angular框架处理问题的方式跟它有所不同,它是水平分层,所有这些数据访问逻辑都跟UI彻底分离,所以可以很轻松地把这个逻辑代码写出来,这么一来,前面所述端到端的组件就彻底退化,变成只有界面展现了。 看看刚才碰到的两个问题,第一个,模型代码按照业务领域进行划分,获取的数据放在两个不同的数组,然后通过双向绑定跟UI产生关联,如果UI上一个下拉框选中项发生变更,只需要监控这个取值项,然后更新另一个下拉框的取值列表即可,完全不需要绕弯子。即使这两个处于不同模型中,也可以用类似后端的方式,采用事件总线等机制去完成通信。 第二个更简单了,复用的组件其实只有UI,也就是说,只有UI是多实例的,模型其实只有一份,比如说一个地区的树形结构,即使一个界面上同时有维护和使用两种功能,都可以共享同一份模型,当维护这边对数据进行了更新,就实时反馈到模型中,然后由双向绑定再把这个模型同步到界面上的使用方去,整个过程清晰可控。 从协作关系上讲,很多前端开发团队每个成员的职责不是很清晰,有了前端的MV*框架,这个状况会大有改观。MV*框架的理念是把前端按照职责分层,每一层都相对比较独立,有自己的价值,也有各自发挥的余地。 为什么多数做互联网前端开发的同学们感受不到MV*框架的重要性呢,因为在这个协作体系里,Model的这一块不够复杂,在传统软件领域,Model的部分是代码最多的,View的相对少一些,而互联网领域里,基本是相反的,所以Model这块沦为附加,如果主要在操作View和Controller,那当然jQuery这类框架比较好用了。 所以,经常看到有互联网产品的同学们讲前端MVC,但举例的时候,都比较牵强,很多时候,他们举出来的那个Model,其实都不能算真正的Model,而是在操作View的过程中一些辅助的模型,真正的Model是贯穿前后端的。 归根结底,前端MV*框架带来的是一整套工作流程的变更,后端工程师也可以编写前端的模型代码,把它跟后端彻底打通,交互工程师处理UI跟模型的互动关系,UI工作人员可以专注、无障碍地处理HTML源码,把它们以界面模版的形式提供给交互工程师。这一整套协作机制能够大大提高B/S架构系统的开发效率,如果再有外围的管控平台,生产效率将真正踏进工业化的阶段。 到这个阶段,前端开发人员的出路是什么呢?我认为有两种。拿服装行业来对比,如果你要的是普通的,就使用工业手段批量生产,使用MV*框架,做好架构和组件重用,做得快,细节不是很讲究。如果你想要更好的,有特色的,就需要名家设计,手工打造,非常精巧,高端大气上档次。所以,这也就代表着前端开发的两种发展方向。 ================================================ FILE: posts/2013-11-20-Web应用的组件化开发(一).md ================================================ Web应用的组件化开发(一) ==== 基本思路 ---- #1. 为什么要做组件化? 无论前端也好,后端也好,都是整个软件体系的一部分。软件产品也是产品,它的研发过程也必然是有其目的。绝大多数软件产品是追逐利润的,在产品目标确定的情况下,成本有两个途径来优化:减少部署成本,提高开发效率。 减少部署成本的方面,业界研究得非常多,比如近几年很流行的“去IOE”,就是很典型的,从一些费用较高的高性能产品迁移到开源的易替换的产品集群,又比如使用Linux + Mono来部署.net应用,避开Windows Server的费用。 提高开发效率这方面,业界研究得更多,主要途径有两点:加快开发速度,减少变更代价。怎样才能加快开发速度呢?如果我们的开发不是重新造轮子,而是每一次做新产品都可以利用已有的东西,那就会好很多。怎样才能减少变更代价呢?如果我们能够理清模块之间的关系,合理分层,每次变更只需要修改其中某个部分,甚至不需要修改代码,仅仅是改变配置就可以,那就更好了。 我们先不看软件行业,来看一下制造行业,比如汽车制造业,他们是怎么造汽车的呢?造汽车之前,先设计,把整个汽车分解为不同部件,比如轮子,引擎,车门,座椅等等,分别生产,最后再组装,所以它的制造过程可以较快。如果一辆汽车轮胎被扎破了,需要送去维修,维修的人也没有在每个地方都修一下,而是只把轮胎拆下来修修就好了,这个轮胎要是实在坏得厉害,就干脆换上个新的,整个过程不需要很多时间。 席德梅尔出过一款很不错的游戏,叫做《文明》(Civilization),在第三代里面,有一项科技研究成功之后,会让工人工作效率加倍,这项科技的名字就叫做:可替换部件(Replacement Parts)。所以,软件行业也应当引入可替换的部件,一般称为组件。 #2. 早期的前端怎么做组件化的? 在服务端,我们有很多组件化的途径,像J2EE的Beans就是一种。组件建造完成之后,需要引入一些机制来让它们可配置,比如说,工作流引擎,规则引擎,这些引擎用配置的方式组织最基础的组件,把它们串联为业务流程。不管使用什么技术、什么语言,服务端的组件化思路基本没有本质差别,大家是有共识的,具体会有服务、流程、规则、模型等几个层次。 早期展示层基本以静态为主,服务端把界面生成好,浏览器去拿来展示,所以这个时期,有代码控制的东西几乎全在服务端,有分层的,也有不分的。如果做了分层,大致结构就是下图这样: ![web1.0.png](https://raw.github.com/xufei/blog/master/assets/web-components/web1.0.png "Web 1.0") 这个图里,JSP(或者其他什么P,为了举例方便,本文中相关的服务端技术都用Java系的来表示)响应浏览器端的请求,把HTML生成出来,跟相关的JavaScript和CSS一起拿出去展示。注意这里的关键,浏览器端对界面的形态和相关业务逻辑基本都没有控制权,属于别人给什么就展示什么,想要什么要先提申请的尴尬局面。 这个时期的Web开发,前端的逻辑是基本可忽略的,所以前端组件化方式大同小异,无论是ASP还是JSP还是其他什么P,都可以自定义标签,把HTML代码和行间逻辑打包成一个标签,然后使用者直接放置在想要的地方,就可以了。 在这一时代,所谓的组件化,基本都是taglib这样的思路,把某一块界面包括它的业务逻辑一起打成一个端到端的组件,整个非常独立,直接一大块从界面到逻辑都有,而且逻辑基本上都是在服务端控制,大致结构如下图所示。 ![components in web1.0.png](https://raw.github.com/xufei/blog/master/assets/web-components/components1.0.png "Components in Web 1.0") #3. SPA时代,出现了新问题 自从Web2.0逐渐流行,Web前端已经不再是纯展示了,它逐渐把以前在C/S里面做的一些东西做到B/S里面来,比如说Google和微软的在线Office,这种复杂度的Web应用如果还用传统那种方式做组件化,很显然是行不通的。 我们看看之前这种组件化的方式,本质是什么?是展现层跟业务逻辑层的隔离,后端在处理业务逻辑,前端纯展现。如果现在还这么划分,就变成了前端有界面和逻辑,后端也有逻辑,这就比较乱了。我们知道,纯逻辑的分层组件化还是比较容易的,任何逻辑如果跟展现混起来,就比较麻烦了,所以我们要把分层的点往前推,推到也能把单独的展现层剥离出来。 如下图所示,因为实际上HTML、CSS、JavaScript这些都逐渐静态化,所以不再需要把它们放在应用服务器上了,我们可以把它们放在专门的高性能静态服务器上,再进一步发展,就可以是CDN(Content Delivery Network,内容分发网络)。前端跟后端的通信,基本都是通过AJAX来,也会有一些其他的比如WebSocket之类,总之尽量少刷新了。 ![web2.0.png](https://raw.github.com/xufei/blog/master/assets/web-components/web2.0.png "Web 2.0") 在这张图里面可以看到,真正的前端已经形成了,它跟应用服务器之间形成了天然的隔离,所以也能够很独立地进行一些发展演进。 现在很多Web程序在往SPA(单页面程序,Single Page Application)的方向发展,这类系统通常比较类似传统的C/S程序,交互过程比较复杂,因此它的开发过程也会遇到一些困难。 那为什么大家要做SPA呢?它有很多明显的好处,最核心的优势就是高效。这个高效体现在两个方面:一是对于用户来说,这种方式做出来的东西体验较好,类似传统桌面程序,对于那些需要频繁操作的行业用户,有很大优势。二是运行的效率较高,之前集成一些菜单功能,可能要用iframe的方式引入,但每个iframe要独立引入一些公共文件,服务器文件传输的压力较大,还要初始化自己的一套内存环境,比较浪费,互相之间也不太方便通信,一般要通过postMessage之类的方式去交互。 有了SPA之后,比如一块界面,就可以是一个HTML片段,用AJAX去加载过来处理之后放到界面上。如果有逻辑的JavaScript代码,也可以用require之类的异步加载机制去运行时加载,整体的思路是比较好的。 很多人说,就以这样的需求,用jQuery再加一个异步js加载框架,不是很足够了吗?这两个东西用得好的话,也是能够解决一些问题的,但它们处理的并不是最关键的事情。在Web体系中,展现层是很天然的,因为就是HTML和CSS,如果只从文件隔离的角度,也可以做出一种划分的方式,逻辑放在单独的js文件里,html内部尽量不写js,这就是之前比较主流的前端代码划分方式。 刚才我们提到,SPA开发的过程中会遇到一些困难,这些困难是因为复杂度大为提升,导致了一些问题,有人把这些困难归结为纯界面的复杂度,比如说,控件更复杂了之类,没有这么简单。问题在于什么呢?我打个比方:我们在电脑上开两个资源管理器窗口,浏览到同一个目录,在一个目录里把某个文件删了,你猜猜另外一个里面会不会刷新? 毫无疑问,也会刷新,但是你看看你用的Web页面,如果把整个复杂系统整合成单页的,能保证对一个数据的更新就实时反馈到所有用它的地方吗?怎么做,是不是很头疼?代码组织的复杂度大为提高,所以需要做一些架构方面的提升。 #4. 架构的变更 提到架构,我们通常会往设计模式上想。在著名的《设计模式》一书中,刚开始就讲了一种典型的处理客户端开发的场景,那就是MVC。 传统的MVC理念我们并不陌生,因为有Struts,所以在Web领域也有比较经典的MVC架构,这里面的V,就负责了整个前端的渲染,而且是服务端的渲染,也就是输出HTML。如下图所示: ![struts-mvc.png](https://raw.github.com/xufei/blog/master/assets/web-components/struts-mvc.png "Struts MVC") 在SPA时代,这已经不合适了,所以浏览器端形成了自己的MVC等层次,这里的V已经变成客户端渲染了,通常会使用一些客户端的HTML模版去实现,而模型和控制器,也相应地在浏览器端形成了。 ![struts-mvc.png](https://raw.github.com/xufei/blog/master/assets/web-components/spa.png "Single Page Application") 我们有很多这个层面的框架,比如Backbone,Knockout,Avalon,Angular等,采用了不同的设计思想,有的是MVC,有的是MVP,有的是MVVM,各有其特点。 以Angular为例,它推荐使用双向绑定去实现视图和模型的关联,这么一来,如果不同视图绑定在同一模型上,就解决了刚才所说的问题。而模型本身也通过某种机制,跟其他的逻辑模块进行协作。 这种方式就是依赖注入。依赖注入的核心理念就是通过配置来实例化所依赖的组件。使用这种模式来设计软件架构,会牺牲一些性能,在跟踪调试的便利性等方面也会有所损失,但换来的是无与伦比的松耦合和可替代性。 比如说,这些组件就可以单独测试,然后在用的时候随手引入,毫无压力。对于从事某一领域的企业来说,光这一条就足以吸引他在上面大量投入,把所有不常变动领域模型的业务代码都用此类办法维护起来,这是一种财富。 #5. MV*框架的基本原理 如果我们来设计Angular这么一个前端框架,应当如何入手呢?很显然,逻辑的控制必须使用JavaScript,一个框架,最本质的事情在于它的逻辑处理方式。 我们的界面为什么可以多姿多彩?因为有HTML和CSS,注意到这两种东西都是配置式的写法,参照后端的依赖注入,如果把这两者视为跟Spring框架中一些XML等同的配置文件,思路就豁然开朗了。 与后端不同的是,充当前端逻辑工具的JavaScript不能做入口,必须挂在HTML里才能运行,所以出现了一个怪异的状况:逻辑要先挂在配置文件(HTML)上,先由另外的容器(浏览器或者Hybird的壳)把配置文件加载起来,然后才能从某个入口开始执行逻辑。好消息是,过了这一步,逻辑层就开始大放异彩了。 从这个时候开始,框架就启动了,它要做哪些事情呢? - 初始化自身(bootstrap) - 异步加载可能尚未引入的JavaScript代码(require) - 解析定义在HTML上的规则(template parser) - 实例化模型(scope) - 创建模型和DOM的关联关系(binding, injection) 这些是主线流程,还有一些支线,比如: - 解析url的search字符串,恢复状态(route) - 加载HTML部件模板(template url) - 部件模板和模型的关联(binding) #6. 如何做组件化 ##6.1. HTML的组件化 SPA的一个典型特征就是部分加载,界面的部件化也是其中比较重要的一环。界面片段在动态请求得到之后,借助模版引擎之类的技术,经过某种转换,放置到主界面相应的地方。所以,从这个角度来看,HTML的组件化非常容易理解,那就是界面的片段化和模板化。 ##6.2. JavaScript的组件化 JavaScript这个部分有好几个发展阶段。 - 早期的共享文件,把公共功能的代码提出出来,多个页面共用 - 动态引用,消灭全局变量 - 在某些框架上进一步划分,比如Angular里面又分为provider,service,factory,controller JavaScript组件化的目标是什么呢,是清晰的职责,松耦合,便于单元测试和重复利用。这里的松耦合不仅体现在js代码之间,也体现在js跟DOM之间的关系,所以像Angular这样的框架会有directive的概念,把DOM操作限制到这类代码中,其他任何js代码不操作DOM。 ![componentsinspa.png](https://raw.github.com/xufei/blog/master/assets/web-components/componentsinspa.png "Components in SPA") 如上图所示,总的原则是先分层次,层内再作切分。这么做的话,不再存在之前那种端到端组件了,使用起来没有原先那么方便,但在另外很多方面比较好。 ##6.3. CSS的组件化 这方面,业界也有很多探索,比如LESS,SASS,Stylus等。为什么CSS也要做组件化呢?传统的CSS是一种扁平的文本结构,变更成本较高,比如说想要把结构从松散改紧凑,需要改动很多。如果把实际使用的CSS只当作输出结果,而另外有一种适合变更的方式当作中间过程,这就好多了。比如说,我们把一些东西定义成变量,每个细节元素使用这些变量,当需要整体变更的时候,只需修改这些变量然后重新生成一下就可以了。 以上,我们讨论了大致的Web前端开发的组件化思路,后续将阐述组件化之后的协作过程和管控机制。 ================================================ FILE: posts/2013-12-02-一些JS题目的解答.md ================================================ 一些JS题目的解答 ==== 在[这里](http://davidshariff.com/quiz/)看到一些测试题,我HTML和CSS比较一般,尝试把里面的JS题目都解答一下: #1. "1" + 2 + "3" + 4 - 10 - 1234 - 37 答案:1234,加法优先级等同,从左往右,数字与字符串相加,数字转换成字符串进行运算,结果等同于:"12"+"3"+4 = "123"+4 = "1234"。 #2. 4 + 3 + 2 + "1" - 10 - 4321 - 91 答案:91,优先级同上,从左往右,等同于:7+2+"1" = 9+"1" = "91"。 #3. var foo = 1; function bar() { foo = 10; return; function foo() {} } bar(); alert(foo); - 1 - 10 - Function - undefined - Error 答案:1,function的定义会提前到当前作用域之前,所以等同于: var foo = 1; function bar() { function foo() {} foo = 10; return; } bar(); alert(foo); 所以,在foo=10的时候,foo是有定义的,属于局部变量,影响不到外层的foo。 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FFunctions_and_function_scope > Unlike functions defined by function expressions or by the Function constructor, a function defined by a function declaration can be used before the function declaration itself. #4. function bar() { return foo; foo = 10; function foo() {} var foo = 11; } alert(typeof bar()); - number - function - undefined - Error 答案:function,与上题类似,等同于: function bar() { function foo() {} return foo; foo = 10; var foo = 11; } alert(typeof bar()); 在return之后声明和赋值的foo都无效,所以返回了function。 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return > A function immediately stops at the point where return is called. 补充,这个解答有问题: > @尤里卡Eureka:JS中function声明和var声明都会被提前,最终得到结果为function,是因为*名称解析顺序-Name Resolution Order*(http://t.cn/8kcIRts导致的function声明优先级大于var声明,而不是由return语句退出导致最后的结果~ #5. var x = 3; var foo = { x: 2, baz: { x: 1, bar: function() { return this.x; } } } var go = foo.baz.bar; alert(go()); alert(foo.baz.bar()); - 1,2 - 1,3 - 2,1 - 2,3 - 3,1 - 3,2 答案:3,1 this指向执行时刻的作用域,go的作用域是全局,所以相当于window,取到的就是window.x,也就是var x=3;这里定义的x。而foo.baz.bar()里面,this指向foo.baz,所以取到的是这个上面的x,也就是1。 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FOperators%2Fthis #6. var x = 4, obj = { x: 3, bar: function() { var x = 2; setTimeout(function() { var x = 1; alert(this.x); }, 1000); } }; obj.bar(); - 1 - 2 - 3 - 4 - undefined 答案:4,不管有这个setTimeout还是把这个函数立即执行,它里面这个function都是孤立的,this只能是全局的window,即使不延时,改成立即执行结果同样是4。 #7. x = 1; function bar() { this.x = 2; return x; } var foo = new bar(); alert(foo.x); - 1 - 2 - undefined 答案:2,这里主要问题是最外面x的定义,试试把x=1改成x={},结果会不同的。这是为什么呢?在把函数当作构造器使用的时候,如果手动返回了一个值,要看这个值是否简单类型,如果是,等同于不写返回,如果不是简单类型,得到的就是手动返回的值。如果,不手动写返回值,就会默认从原型创建一个对象用于返回。 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new #8. function foo(a) { alert(arguments.length); } foo(1, 2, 3); - 1 - 2 - 3 - undefined 答案3,arguments取的是实参的个数,而foo.length取的是形参个数。 参见: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments/length?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FFunctions_and_function_scope%2Farguments%2Flength > arguments.length provides the number of arguments actually passed to a function. This can be more or less than the defined parameter count (See Function.length). #9. var foo = function bar() {}; alert(typeof bar); - function - object - undefined 答案:undefined,这种情况下bar的名字从外部不可见,那是不是这个名字别人就没法知道了呢?不是,toString就可以看到它,比如说alert(foo),可以看看能打出什么。 参见: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FFunctions_and_function_scope > The function name can be used only within the function's body. Attempting to use it outside the function's body results in an error (or undefined if the function name was previously declared via a var statement). #10. var arr = []; arr[0] = 'a'; arr[1] = 'b'; arr.foo = 'c'; alert(arr.length); - 1 - 2 - 3 - undefined 答案:2,数组的原型是Object,所以可以像其他类型一样附加属性,不影响其固有性质。 #11. function foo(a) { arguments[0] = 2; alert(a); } foo(1); - 1 - 2 - undefined 答案:2,实参可以直接从arguments数组中修改。 参见: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FFunctions_and_function_scope%2Farguments > The arguments can also be set #12. function foo(){} delete foo.length; alert(typeof foo.length); - number - undefined - object - Error 答案:number,foo.length是无法删除的,它在Function原型上,重点它的configurable是false。 参见:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete > delete can't remove certain properties of predefined objects (like Object, Array, Math etc). These are described in ECMAScript 5 and later as non-configurable ================================================ FILE: posts/2013-12-09-Web应用的组件化(二).md ================================================ Web应用的组件化开发(二) ==== 管控平台 ---- 在上一篇中我们提到了组件化的大致思路,这一篇主要讲述在这么做之后,我们需要哪些外围手段去管控整个开发过程。从各种角度看,面对较大规模前端开发团队,都有必要建立这么一个开发阶段的协作平台。 在这个平台上,我们要做哪些事情呢? #1. HTML片段 我们为什么要管理HTML片段?因为有界面要用它们,当这些片段多了之后,需要有个地方来管理起来,可以检索、预览它们,还能看到大致描述。 这应该是整个环节中一个相对很简单的东西,照理说,有目录结构,然后剩下的就是单个的HTML片段文件了,这就可以解决存储和检索的问题了,但我们还要考虑更多。 已有的HTML片段,如何被使用呢?这肯定是一种类似include的方式,通过某种特殊标签(不管是前端还是后端的方式)把这些片段引用进来,这时候就有了第一个问题: 假设有界面A和界面B同时引用了片段C,在某个开发人员修改片段C内容的时候,他如何得知将会影响到界面A和B呢?一个比较勉强的方式是全项目查找,但这在很多情况下是不够的。 如果我们的HTML片段是作为独立的公共库存在的,它已经不能通过项目内查找去解决这一问题了,因为不管A还是B,只要他不处于片段C的项目空间,就无从追寻。 这时候很多人会问两个问题: 1. 跨项目的界面片段重用,意义在哪里? 如果我们的产品是针对一个小领域,它的复杂度根本不需要划分多个项目部分来协作完成。设想场景是面对很大的行业,各项目都是子产品,将来可能是其中若干个联合部署,这时候,保持其中的一致性是非常重要的。比如我们有个基本配置界面,在多个子产品中都要用,如果各自开发一个,其操作风格很可能就是不一致的,给人的印象就是不专业。所以会需要把常见的界面片段都归集起来,供业务方挑选使用。 2. 修改C,只提供说明,但是不通知A和B,不实时更新他们的版本,然后自行决定怎样升级,如何? 这会有一个问题,每次有小功能升级的时候,代码是最容易同步合并的,所以才会有“持续集成”这个概念,如果是一直伴随升级,总要比隔一个大阶段才升级好,升级成本应尽量分摊到平时,就像农妇养小猪,小猪每天长一点,每天都抱来抱去,不觉得吃力,即使长大了也还能抱得动。 现在问题就很明确了,一定要有一种方式来把这个依赖关系管理起来,很显然,已有的版本库是肯定管不了这些的,所以只能在外围做一些处理。 我们建立一个管理平台,除了管理实体文件的版本,还管它们之间的关系。具体这个关系如何收集整理,有两种方式:手动配置,代码分析。 手动配置是比较土的方式,开发人员每提交一个文件,就去这系统上手动配置它的依赖关系。代码分析的话,要在每次提交文件的时候解析文件的包含规则,找出确切的文件。这两者各有利弊,前者比较笨,但容易做,后者对代码格式的要求比较高,要考虑的情况较多。 我们的界面往往不是那么简单,HTML片段也可能有层次的,举例来说: 界面A里面包含了片段B,但是片段B自身又包含了片段C,所以这个依赖关系也是有层级的,需要在设计的时候一并考虑。 #2. JavaScript模块 JavaScript代码的管理,比HTML片段的状况好一些,因为业界很多这方面的解决方案。但它们还是没有解决当依赖项产生变更的时候反向通知的问题。 所以我们还是得像HTML片段一样,把它们的依赖关系都管理到平台里。于是,每个JavaScript模块都显式配置了自己所依赖的其他模块,通过这种单向关系,形成了一套完整的视图。 在JavaScript模块的代码实现中,我们是不提倡直接写依赖关系的。很多通用规范,比如AMD,往往建议我们这样写模块: define(['dep1', 'dep2'], function (dep1, dep2) { var moduleA = function () {}; return moduleA; }); 但我们的系统是面向行业的,比这种通用解决方案要苛刻一些。比如说,如果有一天重构代码,JavaScript模块们调整了目录或者名字,这么写的就痛苦了,他必须把所有影响到的都去调整一遍,这是要搜索替换的。况且,就像上面HTML模板的部分提到的,影响了处于其他项目中依赖它的代码,缺少合适的方式去通知他们修改。 所以我们期望的是,在每个编写的JavaScript模块中只存放具体实现,而把依赖关系放在我们的平台上管理,这样,即使当前模块作了改名之类的重构处理,处于外部项目中依赖它的那些代码也不必修改,下一次版本发布的生成过程会自动把这些事情干掉。 对应到上面的这段代码,我们需要开发人员做的只是其中的实现,也就是moduleA的那个部分,外面这些依赖的壳子,是会在发布阶段根据已配置的依赖关系自动生成的。 如果需要,JavaScript模块还可以细分,比如类似Angular里面那样,把factory,controller和directive分离出来,这会对后续有些处理提供方便。 现在我们有必要讨论一下模块的粒度了,我们这里提到的都是基本的粒度,每个JavaScript模块中存放的应该只有一个很具体东西的实现。那么,有个问题,在我们发布的时候,是不是就按照这个粒度发布出去呢? 很显然不行,如果这么做,很可能会出现复杂界面一次要用10多个HTTP请求才能加载完它所需要的所有JavaScript代码的情况,所以需要做一些合并。 那么,合并的策略是什么?在我们这个平台上,开发人员又是要怎样定义这个合并关系的呢?我们需要在模块之上定义一个更大粒度的组织方式,这个方式与模块的关系,就好比Java里面,jar文件与class的关系。如果开发人员不显式配置,也可以通过全局策略,比如按最下层目录来合并。 这个时候,在实际使用这些代码的时候,需要带两个配置信息过去,一个是要动态载入的JavaScript文件(合并之后的),二是每个JavaScript文件中包含的原始模块。 #3. 单元测试 如果JavaScript模块都已经被良好有序管理起来,就可以为它们考虑单元测试的事情了。单元测试对于提高基础单元的可靠度,是有非常重要意义的。 在我们这个平台里,可以把单元测试跟JavaScript模块关联起来,每个JavaScript模块可以挂一组单元测试代码,这些代码可以在线编写,在线运行。 单元测试的本质就是编写模拟代码来调用已有模块,考虑到我们的模块是JavaScript,所以很多思路都倾向于在浏览器端执行它们,对于单个模块的单元测试,这不是个问题。 如果要批量执行整个系统的单元测试,那就不一样了。把JavaScript代码先加载到浏览器中,然后再执行,很多时候并不需要这么复杂。我们完全可以在服务端把它们做了。 借助Node.js的能力,我们可以在服务端执行JavaScript代码,也就意味着能够把绝大多数JavaScript模块的单元测试在服务端就执行掉。当然,我们为此可能要多做不少事情,比如说,有些库需要移植一份node版的,常见的有AJAX调用等等。 注意了,能够在服务端做JavaScript单元测试是有先决条件的,代码的分层必须很良好,除了视图层,其他任何层面都不能操作DOM。所以我们这里主要测试的也正是除了视图层之外的所有JavaScript业务逻辑。至于视图层怎么办?这个真的很难解决,这世界上不是所有东西都能自动做的,只能先把可做的做了,以后再来考虑这些。 #4. 文档和示例管理 ##4.1. 文档 现在我们有HTML片段和JavaScript模块了,需要给它们多一些描述信息。简单描述显然是不够的,我们还要详细文档。 这种详细文档可以通过某种方式生成,也可以由开发人员手动编写。与传统的离线文档不同,在线的文档更实时,并且,每当一个开发人员变更了他的文档之后,不需要经过全量构建,访问者可以实时访问到他的最新版本。 熟悉GitHub的朋友们可能早已习惯这种方式,在项目库里面存在一些以md格式结尾的文本文件,使用markdown语法来编写一些说明文档。 毫无疑问,这类格式很适合在线协作,所以我们也会在平台上集成这么一种编写文档的方式,无论是针对HTML模板还是JavaScript模块,或者是其他什么类型,甚至还可以用来当博客,就像[月影](http://blog.silverna.org/ "")同学的gitpress平台,能直接从GitHub上拉取文本或者HTML文件形成博客。 文档除了以集成的形式浏览之外,应当也可以以单独链接的方式发出去,这时候用户就可以像看一个新闻网页一样去浏览。如果再进一步做下去,还可以做电子书的生成,提供打包的离线文档。 ##4.2. 示例 在编写代码文档的过程中,可能免不了要插入示例,示例有两种形态,一种是纯文本,类似gist这样,一种是可在线运行,类似jsfiddle和jsbin这样。 这两种都有各自的优点,所以可以都做,示例的存放可以与文档类似,也应当能通过一个链接独立运行。 ##4.3. 幻灯片 有时候我们看到一些在线的幻灯片,觉得效果很帅,比如[reveal.js](http://lab.hakim.se/reveal-js/ "reveal.js - The HTML Presentation Framework"),我们的开发人员有时候作代码分析或者走查的时候也不免要写一些演示,如果能把这些东西也随项目管理起来,能在线查看,会是很不错的一件事。所以我们也可以考虑给它们加个存储界面,甚至做个简易的在线编写器。 #5. 项目与目录管理 说到现在,我们似乎还遗漏了一点什么。那就是以上提到的这些东西,以什么为组织单位来存储? 考虑到我们的这个平台是要管理一整个大产品的全部前端内容的,它里面应该分了很多项目,对应到子产品上,这么一来,很自然地,项目就成了第一级组织单位。项目之下,没有悬念地,只有目录了。 对于一个项目而言,它有哪些要做的事情呢?首先要能配置其实体存储位置。前面提到的这么多代码、文档之类,最终都是要实体存储的,怎么存?我们当然可以自己搞一套,在文件系统上做起来,但是还要考虑它们的版本管理,非常麻烦,所以不如直接对接某个版本库,调用它的接口去存取文件,这里配置的就是版本库的路径。 其次,要考虑从已有项目复制,类似GitHub里面的fork功能,不过内部处理机制可以略有不同,fork的项目默认未必要有实体文件,只有当产生了修改或者新增操作的时候才创建,剩下的还引用原来的就可以了。我们这里的项目复制功能是为项目化版本而考虑的,经常出现一个产品版本支持多个客户项目的情况,所以可能会用得着这个特性。 然后,也要考虑项目的依赖关系。依赖一个项目,意思是需要用到它里面的组件,所以实质是组件的依赖。提供项目依赖这个视图,只是为了未来变更的一些考虑。 #6. 评论管理 之前提到,我们整个平台的目的是为了提高大型前端团队的协作能力,协作是离不开交流的。上述的任何功能,都应当带有交流沟通的能力。 比如说,如果开发人员A使用了其他人写的一个代码组件a,对其中一些细节有疑问,他应当可以对它进行评论。在他评论的时候,任何参与维护过这个组件的人员都能收到一个提醒,这时候他可以选择过来看看,回复这个疑问。同理,在文档、示例下也可以如此操作。 在互联网上有这类产品,用于在任意URL下挂接评论交流系统,比较有名的就是[Disqus](http://disqus.com/ "Disqus - The Web's Community of Communities"),我们可以看到很多网站下面挂着它,用于做交流评论,这样用户可以用一个账号在多个网站之间交流。国内也有同类的,比如[多说](http://duoshuo.com/ "多说 - 社会化评论系统"),能够用微博、QQ等账号登录进行交流。 从我们这个平台本身看,如果是部署在企业内部作流程提升,引入外部评论系统的可能性就比较小了。因为在企业内部用,一定是希望这个员工的账号信息跟工号挂钩,也能够跟版本服务器账号等模块作集成,权限也便于控制。 从另外一个角度讲,某个人员登录这个系统的时候,他可能收到很多消息,来自不同的代码或文档位置,挨个点过去回复也有些麻烦,我们应当给他提供一个全局视图,让他能在一个统一的界面把这些问题都答复掉,如果他需要的话,也是可以点进去到实际的位置。 #7. 用户和权限控制 从以上部分我们已经看到,这个系统是一个比较复杂的开发过程管控平台。这样的话,每个使用的人就应当可以登录,然后分配不同的权限等级。 未登录用户应当有一些东西的查看权限,但是不能发表评论。已登录的用户根据权限级别,可以控制能否创建、修改项目,创建、修改目录,代码,单元测试,文档等。 #8. 国际化字符串管理 一个跨语言区域的Web应用不可避免要跟国际化打交道,这个事情通常是在服务端做,比如通过在界面代码中嵌入类似<% =getRes(key, lan) %>这样的代码,去获取相应的字符串,替换到界面里来。 这个事情是要占用应用服务器资源的,而且国际化本身其实是一个在运行之前就已经确定的事,完全可以把这个过程放在发布阶段就做掉。比如说,我们给每种语言预先就把代码生成多份,只是部署在一起,根据需要的情况来动态加载特定的那一份。 有不少客户端的国际化方案,是把资源文件拆细,以页面为单位存储,但这其实是不太合理的。第一个原因就是在Web2.0时代,“页面”这个概念本身就已经弱化了,到了单页应用里,整个应用都只是一个页面,这个时候,资源文件以什么粒度来组织呢? 我们提到过,采用MV*框架去做Web应用的架构,有一个目标是做组件化。组件化的意图就是某个组件可以尽可能随心所欲地放在需要的地方用。如果把资源文件的粒度弄小到对应HTML片段和JavaScript模块这一级,灵活性倒是有了,带来的问题就是管理成本增大。 做一个行业应用,最重要的就是业务一致性,这包括逻辑的一致性,也包括了术语的一致性。某一个词,可能在多个资源文件中都出现,这就增加了不一致的可能性。 所以,应当有一个统一的术语管理平台,一切界面上出现的文字或者提示,都必须来自这个平台。 #9. 静态资源的管理 在发布系统的时候,除了需要发布代码,还需要发布图片等静态资源,这些东西也应当被管理起来。 静态资源在两种情况下可用:随产品发布,在本平台被引用。比如说有一个图片,在这个平台上作了管理,它可以被配置到某个项目上,在发布的时候导出。这个图片还可以被用链接的方式查看或者下载,如果本平台内部的一个文档或者示例要引用它,也是可以的。 #10. 样式与主题管理 在Web系统里,样式和主题是很重要的一环。样式的管理和发布一直是一个比较复杂的话题,早几年一般都是分块写,然后组合合并,最近这些年有LESS,SASS和Stylus这类技术,解决了编写和发布的分离问题。 我们看看发布的最大问题是什么?是不同部分的合并。为了追求灵活性,不得不把东西拆得很细,之前HTML片段和JavaScript模块的处理方式都是这样。这么做,我们就需要另外一件事:这些细小的东西,尽可能要覆盖全面。 对应到CSS里面,我们要做的是把每种在系统中可能出现的元素、类别都作为单独的规则维护起来,生成一个全局的规则列表。不同项目间,实现可以不同,但规则的名字是固定的,定制只允许修改实现,不允许修改规则。如果要新增之前没有的规则,也必须在全局规则列表里先添加,再作实现。 样式规则被管理之后,可以在界面组件上对它作关联,也可以不做。做的好处是发布的时候能只把用到的那些样式规则生成发布出去,如果能接受每次发布全量CSS,那也无所谓。 除了规则,也需要考虑一些变量的管理,在CSS中合理使用变量,会大为减轻定制化所导致的工作量。 #11. 一键发布 我们引入了这么一堆东西,其实是增加了发布的复杂度。为什么呢? 之前不管HTML、JavaScript还是CSS,都是手写出来,最多经过一个minify的工作,就发布了,整个过程很简单,两句脚本搞定。 现在可复杂了,先要分析依赖关系,然后提取文件,然后国际化字符串替换,然后合并,然后代码压缩,整个过程很折腾,不给配置管理员一个解释的话,他一定过来砍人。 我们有个原则:解决问题的过程中,如果引入了新的问题,要求负责解决原问题的人也一起解决掉。现在为了一些意图,增加了版本发布的复杂度,那也要有个办法再把这事摆平,至少不能比原来复杂。 所以我们就要把这些过程都集成到管控平台里,做一个一键发布的过程,把所有的这些操作都集成起来,配置管理员发布版本的时候只要点一下就可以把所有这些事情做掉。甚至说,这些流程还可以配置,能够加减环节。 这时候我们做到了跟之前发版本一样方便,能不能多做点什么呢? 可以把JavaScript单元测试集成到版本发布阶段。因为我们已经把JavaScript按照职责做了分层,并且把UI部分做了隔离,就可以在浏览器之外把这个单元测试做掉,平时提交代码的时候也可以做,最终在版本发布阶段再全量做一下,也是很有意义的。 代码依赖关系管理的另一个目的是什么呢?是最小化发布,既然我们都管理了文件之间的关系,那么,从根出发,显然是能够得出哪些代码文件在本项目中使用的,就可以每次从我们的全量代码库中取得确切需要的一部分来发布。这也是我们整个管控平台带来的优势。 #12. 小结 我们这一篇比较复杂,提出了一整套解决大规模前端协作的管控机制。这套理论的本质是在开发和版本发布之间加了一个环节,把Web体系中除了服务之外的一切静态资源都纳入其中,强化了现有主流的一些基于命令行的前端工程化组织模式。 相比于传统行业,比如汽车制造,我们这个环节相当于生产流水线的设计,其中一些组件的存储就类似仓储机制,发布就类似出厂过程。 这个平台本身还有不少其他的可做的东西,比如甚至可以在上面做界面的可视化定制等,这些是长远的终极目标,在后面的文章里会谈谈一些考虑。 后续文章中,我们会展望有了这个平台之后,整个前端的协作流程是怎样的。 ================================================ FILE: posts/2014-01-06-影响企业应用前端开发效率的因素.md ================================================ 影响企业应用前端开发效率的因素 ==== 原先是在知乎上回答一个[问题](http://www.zhihu.com/question/22426434/answer/21433867 "")的,整理了放这里: 我们来分析一下究竟哪些因素让企业应用的前端开发这么困扰。 先看看界面部分吧。 #1. 命令式还是声明式 毫无疑问,就写界面来说,声明式的代码编写效率远高于命令式: 我们为了使得界面代码编写更高效,毫无疑问会倾向于把这么一堆东西简化成一个Panel标签,这样就会逐步建立一套面向自己行业的标签集。 #3. 带逻辑的控件 刚才这个例子为什么简单呢,因为它只是一个普通容器,静态的,不带逻辑,所以即使你用什么静态模板也能解决问题。如果复杂一点,是一个TabNavigator,就要考虑切换的事件,再复杂一些是个树形表格,那就更麻烦了。 我们来看jQuery提供的插件方式实现TabNaviator: 从我个人的角度看,这种代码很愚蠢。蠢在何处呢?HTML这类声明式的界面描述语言,写起来本来应当直观一些的,但是被这么一搞,又往命令式的方向去了。而且两种东西混杂,声明和渲染居然分了两处,又增加了维护的成本。 难道就没有别的办法来解决这个问题吗? 我们看看其他语言和框架,比如Flex和Silverlight。 上面这段是Flex里面的TabNavigator,在这个链接底部有运行结果:TabNavigator 为什么它可以看不到逻辑的代码,但是又确实能有动作呢,因为它的实现类是mx.containers.TabNavigator,在这个代码里,可以自己手动去处理一切内部实现,但是暴露给业务开发人员的就是这么简单的标签。 我们看看在HTML和JS这个体系里用什么办法去解决。不要提JSF这类服务端技术,因为它的思路也是不好的,展示代码的生成和渲染都不在一个地方,会有很多问题。 #4. Polymer与Angular 早期IE里有HTC,也就是HTML Components,因为别的浏览器厂商不喜欢,所以快要消亡了。在W3C新的HTML规范里,有一个Web Components,参见这里:Introduction to Web Components 这个东西跟HTC的思想本出同源,它引入了Custom Elements和Shadow DOM这两个概念,也就是说,我可以自定义一个标签,然后在内部随便怎么折腾,用这个标签的人可以很方便。 很美好,是不是,但是只适用于比较新的浏览器,基于这个理念架构的框架Polymer的目标也只是支持一些比较新的浏览器。Polymer 那么怎么办呢?我们还有Angular,它也可以自定义标签,然后用directive的方式写内部实现。 这么一来,也就有些接近我们的目标了,看到现在,我们还记得目标是什么吗?是尽可能精简的面向领域的容器和控件标签集,有了这个,写界面代码才能更简单。 #5. 为什么HTML默认标签集这么小 事情结束了吗?没有呢。我们的HTML体系为什么标签集这么小?因为他要解决的是通用领域的东西,怎样才能通用呢?要的是尽可能无歧义。 怎样的东西会没有歧义?那就是它的含义尽可能少,比如说单行文本输入框,总没人对它有歧义吧,它无非就是可以设置最大最小长度,是否只读,是否禁用,最多通过某种规则来限制输入字符,最多最多,也就这些可做的了,大家都认同。 Button就不同了,一开始他是 后来大家想要各种各样的button,于是开放了 这样的标签,可以在里面写各种HTML,我记得当时很多人在中间加上下和左右两层marquee,简直玩坏了。 现在HTML里面又有了数字输入,日期时间输入这样的东西,数字的没什么疑问,就是最大最小值,步进值等等,日期时间这个就复杂了,它怎么做,都有人不满意。有人要日期排左边,有人要时间排上面,有人只要年和月,有人只要分和秒。有人要点空白表示选中,有人要双击日期表示选中,还有人想用农历、波斯历、尼泊尔历,简直没完了,还不如不做,谁要谁自己做…… 所以,面向各领域的人们,自己动手,丰衣足食吧。 #6. 界面修饰 好了,控件集的问题解决了,我们来看看界面的修饰。 你们发现没有,不管用什么非HTML的标签体系,可能写代码会很快,但是有时候要修饰界面,比如只是调整一下所有容器的边距,某些按钮的圆角之类,就会生不如死。 这时候你会发现,HTML里面的CSS真是神器,什么都能干,而且是面向切面的,只要你的HTML结构是良好的,完全不需要调整这个层面的代码。为什么其他体系的CSS没有这么强呢?比如说Flex也可以写CSS,QT也可以写CSS。 因为CSS的部分实在是太复杂了,复杂到整个浏览器里面绝大部分的代码都在处理这方面的东西,像Google的Chrome团队有1000多人,别的体系没法有这么大投入,只能看着羡慕。 上次看到一个问题,近30年来软件开发体系有哪些本质的改进?我觉得CSS真的可以入选,这是一个把结构和展现完全分离的典范,并且实现得很好。 我们的前端开发一般都是面向某个领域的,不管什么领域,CSS方向都可以有一个很独立的规划,因为它可以不影响界面的结构。所以这个方面,其实不太会对前端开发造成太多压力,压力只集中在维护CSS的人群身上。 好了,上面扯了那么多,其实到现在还在界面的层次,一直没有去谈到真正的逻辑。那么,最让我们困扰的部分是哪里呢? #7. 模块化和加载 Web前端开发有个最苦闷的事情就是选型,因为HTML这个体系很开放,提供的默认能力又不是很足够,如果要做复杂交互的东西,会需要很多额外的工作。有各种框架从各种角度来解决问题,但怎么把这些东西整合到正好符合自己的需要,是一个很花精力的事情,很多时候恨不得自己把全部轮子都造一遍。 真正的开发工作中,跨浏览器,踩各种坑应该是最烦闷的事,其他部分,如果有做好自己领域里标签的定义,或者不用标签用其他方式,应该不算特别困难。 有人说JavaScript语言本身比较松散,所以写业务逻辑比较头疼,这不算大问题。基于B/S的开发,有一个大坑是你在运行的时候要先把代码加载过来,然后才能跑。你看那些C/S软件,有这困扰吗?再看看后端程序员,谁还要关心自己的代码执行之前要做的事情? 所以后端程序员写前端代码,都情不自禁地会引入一大堆库。我们形象一点来描述一下这个过程: 嗯,大家都用jQuery,我也引入,抄了两段代码发现真不错。咦,我要个树控件,网上逛了一圈,拿了个zTree回来。再埋头苦干半个小时,缺数据表格控件,于是过了一会,jQuery UI被整体引入了。再埋头苦干,上网乱点了点,浏览器跳出个广告,一看叫做Kendo UI,看看发现不错,引进来再说,用里面的某个控件。又过了一阵,听说最近Angular很火啊,看了看例子,表单功能怎么那么强,我也要用!捣鼓捣鼓又加进去了。项目里又要用图表库,看了半天眼睛都花了,百度的ECharts不错哦,引进来。哎呀我界面怎么那么丑,人家的怎么那么清爽,查看源码,一看,Bootstrap,去官网一看,真乃神器,不用简直对不起自己。 没多久之后,这个界面已经融合了各种主流框架,代码写法五花八门,依赖了几M的JS库,更要命的是里面某些JS有冲突,某些样式也互相覆盖,快疯了。 这里有哪些问题呢? - JS代码要先加载到界面才能执行,而这么几M的代码加载过来就要好久了,然后每个框架还要把自己初始化,又耗不少时间,半分钟之后自己写的JS才开始执行,用户等得都快怀孕了。 - 不管是JS还是CSS,都应当控制基准的代码,这件事的主要意义是避免冲突,因为整个体系都比较松散,如果不加控制,就会造成冲突。即使在服务端写Java,也有类签名一致性之类的问题,所以这个部分必须要重视。 刚才这两点,第二点暂时不是我们要探讨的范围,第一点,引出的话题就是异步加载,这是一个可以展开说很多的话题,也不再说了。异步加载和缓存是面对复杂场景必做的优化措施。 但是这个里面规范就有好几种,具体实现方式就更多了。ES6的module也许可以解决这个问题。harmony:modules [ES Wiki] #8. 逻辑的分层 网站型和应用型Web程序对分层的需求是不一样的。网站型的逻辑大部分都在处理UI,而应用型可能有很多业务逻辑,这部分需要更好的组织,以便复用,或者即使我们的目标不包括复用,为了这个代码的可维护性,也需要有比较好的组织方式。 本质上这些组织方式与传统的客户端软件开发没什么不同,主要要做的无非就是UI层的隔离,或者模板化,或者别的什么方式。纯逻辑的代码大家都会写,但这个逻辑怎么跟界面产生关系,这是个问题。 有些框架通过在HTML元素上设置额外属性,然后启动的时候读取,在框架内部做一些相关的事情,比如Angular、Avalon和Knockout。有的框架在视图层中让开发人员手动去处理界面,就像未引入框架的那样,比如Backbone,两者是各有利弊的。 前面这种,一般功能是会很强大,但是它自身所做的东西必须足够多,多得帮你做掉绝大部分本来该自己做的事,你才会特别爽。所以,用这类框架来做表单型应用的时候,是会非常舒服的,因为这些需求他做框架的时候能预见,所以比如校验、联动、存取之类的都会处理掉。假如你要做一个绘图类应用,这就麻烦了,不管你是用Canvas还是SVG,它所能帮到的都不多。这时候,后面这类可能反而适合一些。 这些数据分层框架的原理是什么呢?是要做一层表单与数据的对应关系,所以他要检测数据的变动,比如一个Object,它某个值变更了,要去把对应的界面更改之类。这里面也有很多的坑,可以一步一步踩过来。。。 到现在,我大致可以回答你的问题,什么情况下前端开发会比较轻松呢? - 针对自己领域的界面标签库比较完善,或者易于扩展 - 样式容易调整,并且独立于界面元素 - 逻辑模块化,层次分明,在某种统一规范上存在大量可用库 咦,我这三点好像在说微软的WPF体系吗? ================================================ FILE: posts/2014-01-20-为什么企业应用这么“钟情”于IE6.md ================================================ 为什么企业应用这么“钟情”于IE6 ==== 企业内部的信息化,很多起步都很早,但B/S化基本都是从2000年以后开始的,之前有各种客户端,比如基本的Win32 API开发,或者Delphi体系(VCL等,C++ Builder也算在内),微软的几个封装化的体系(ATL,MFC,还有昙花一现的JFC),Java体系(AWT、Swing,SWT等),这些技术本身都比较成熟,IDE的支持也不错,但最终,很大一部分迁移到Web上了。 迁移到Web的最直接因素是部署成本,B/S架构有天然的部署优势,无需分发,这一点人所共知,但另外还有个重要因素是开发成本。很多人对这一点不相信,为什么呢,因为现在前端码农也不便宜啊,怎么就他降低开发成本了? 早期的Web界面并不花哨,所以对一些技巧的要求远不如现在高,而且只存在一个主流浏览器(IE6),更是无需更多的知识,逻辑基本都放在后端,前端要做的事情基本只有写表单和表格类的HTML,从这个角度看,门槛确实低得可以,写标签的难度远远低于用高级语言写界面。 在这种情况下,大量的表单类应用就被迁移到B/S模式了,其中有时候会出现复杂一些的需求,比如流程的建模,在对VML不熟悉的情况下,很多这种东西会被开发成一个独立的客户端,跟B/S系统连到同样的数据库,变通解决这个问题。但其实在IE6里面,VML本身已经非常可用了,所以这时候也有很多图形化的东西用VML做,比如我自己05年写的这个,抓了个录像在这里:http://xufei.github.io/assets/iom.swf 作为企业应用,另有个重要的事情就是代码的组件化复用,比如说,上面我这个录像里面的树型结构和选项卡,其实给开发人员用的时候很简单: document.getElementById("tacheTree").loadXML(xml); 这两句分别是界面中的声明跟JS代码中的调用,为什么可以这么写呢,是因为IE中有HTC(HTML Component),可以自己用HTML和JS定义标签,只需要用命名空间引入就可以。这个其实是非常有用的功能,一般的企业都必定会在此基础上有所积累,即使现在的Web Components,也没有比它高端到哪里去。 以上这些是从开发视角看的,作为一个运行平台来说,IE6已经非常足够,布局有些小问题,大家其实无所谓,用点技巧也可以摆得比较整齐。如果有大模块的集成,就来个iframe,也都工作得很好。 我们再来看看为什么很多企业软件固守IE体系。提到企业软件,大家经常有个误会,认为企业软件的前端就都是照着IE6来开发的,其实不是这样,企业软件产品是有延续性的,在已有产品上做升级的代价非常大,比如说一套很深入使用了IE only特性的系统,想要迁移到跨浏览器上,这个代价有多大?首先要构建同等复杂度的控件库,然后把功能逐个迁移,最后还要考虑一些已经被废弃了但是暂时没有替代方案的东西,整个过程是非常痛苦的。 很多时候,并不是开发人员意识不到这些问题,而是他没有解决办法。技术上的困难,经过一些努力都是可以克服的,更大的困难在于软件升级的成本。比如说,你评估了把代码修改成跨浏览器,需要50个人做一年,谁为这个过程买单?如果你站在企业决策者的角度,是会搞50个人来做一年,还是让你的2000个员工都稍微克服一下,只使用IE来访问系统? 即使你说这次升级完了就解决了现在的跨浏览器问题,如何确保以后再出个什么浏览器也能支持?如何保证再也不会伤筋动骨地修改代码?谁也说不准未来是什么样,所以,他只有到了不得不升级的时候,才会考虑做这件事。会是现在吗?不一定,因为即使很新版本的IE,也还有兼容模式,他只要还能用下去,就不太有动力去折腾。 那么,我们再看看企业软件开发商是如何处理这个问题的。毫无疑问,企业软件开发商是希望你每年都升级的,因为传统的软件都是一次买断,然后收点维护费,客户是卖一个少一个,但你想升级,那就得交钱,谁会跟钱过不去呢?从这些企业长久的发展来说,它也会让自己的产品逐步去贴近标准,老的产品就这样了,你总不能现在开发个东西还说IE only吧,那估计都不好意思拿出去卖。所以说,这类企业在开发新系统的时候,反而会采用更激进的策略,比如说我就从某个基线往上支持,IE10+,Chrome 27+之类。在江苏电信的CRM系统里,从2011年开始就是推荐客户使用Firefox来访问系统的,大家去电信营业厅办业务,可以观察一下营业员使用的浏览器,其实在很多方面,传统软件厂商也不像外界猜测的那么固步自封。 很多时候,从企业软件开发商的角度看,新的浏览器标准并未带来多少比IE6为代表的“低端浏览器”更有价值的特性,SVG取代了VML,Web Components取代了HTML Components,多了一些储存、文件之类的本地接口,还有摄像头之类偶尔用得上的东西,之前大家用Flash的Socket,现在变成WebSocket,该XMLHTTP的还是XMLHTTP。HTML5体系中提升最大的标签语义化、布局和样式等在这个领域带来的震撼并不强烈,最有价值的反倒是JS性能上的大幅提升。 ================================================ FILE: posts/2014-04-27-基于AngularJS的企业软件前端架构.md ================================================ 基于AngularJS的企业软件前端架构 ==== 这篇是我参加QCon北京2014的演讲内容: ## 提纲: ## 企业应用在软件行业中占有很大的比重,而这类软件多数现在也都采用B/S的模式开发,在这个日新月异的时代,它们的前端开发技术找到了什么改进点呢? B/S企业软件前端开发模式大体上与桌面软件类似,都是偏重量级的,在前端可能会有较多的业务逻辑,这些业务逻辑如何被合理模块化,与界面分离,以便测试,成为这个领域的一个重要挑战。另一方面,由于企业应用的界面相对规整,偏重的是数据存取,没有太多花哨的东西,所以常见的界面控件也是可枚举的,如何让开发界面的工作能更快完成,甚至由不擅长编写代码的业务设计人员来做,与界面原型的工作合二为一,能提高不少开发效率。 在AngularJS等MV*框架出现之后,给这个领域带来一些契机,架构师们能够有机会去重新规划前端的架构,甚至是开发流程,从而让整个软件的生产更为高效。 本文将探讨它给这个领域带来的变化。 ## 正文: ## ### 企业应用前端的特点 企业应用系统是一种很常见的软件系统,这类系统的特点是面向某个行业,功能较复杂,对界面的要求一般是整齐,不追求花哨。这类系统通常有C/S和B/S两个流派,其中的B/S方式因为部署和集成的便利,使用得较为普遍。 同样是在浏览器中做东西,写企业应用和网站的差别也很明显。企业应用的业务逻辑较重,前端有一定的厚重性,但是对效果并不追求很多,主要是各类控件的使用,表单的存取值等等。 企业应用产品的一些特点如下: - 独占模式。 一般用户使用互联网产品,都是片段时间使用,比如购物或者阅读,做完之后就刷新或者关闭浏览器了,而企业应用往往是工作的全部,从早上上班开始打开,到下班才关掉,一天绝大部分工作都在上面完成,比如一个呼叫中心的操作员。 - 重业务,轻视觉 企业应用对视觉的追求是比较低的,一般不会要求花哨效果,以业务操作的流畅性为第一目标。 - 界面规整,模式单一 企业应用的界面布局相对有模式可循,可以用很少的场景来穷举,界面横平竖直,比较规整,使用到的控件元素也是可穷举的,基本没有什么特效。 - 键盘操作 由于企业应用的用户都相对比较专业,在上岗之前需要经过统一培训,而且每个用户使用的频度较高,很多时候他们会用尽量快捷的方式来做操作,比如键盘,这一点在互联网产品中比较少见。所以,有时候大家为了追求好看,把系统原生的select用div来替换,在这种情况下反而增加了用户的麻烦。 - 逻辑复杂 我之前所在的行业中,业务逻辑很复杂,前端可能会需要写很多复杂的逻辑,JS代码大部分是在处理逻辑,而不是界面交互。 - 加载速度的侧重不同 互联网产品往往很重视首屏优化,但是其策略可能与企业应用不同。比如说,3个200k的模块,在网站型产品中可能优化成一个100k加三个150k的模块,但在企业应用中,很可能优化成一个400k加三个50k的模块。为什么会这样呢?因为内容型的网站讲究的优化策略是分摊,如果首次加载太慢,会很影响用户的信心,但企业应用用户的容忍度是较高的,他并不在乎刚打开的时候慢一些,因为打开了之后就要用一天,对于之后每步操作的模块加载速度倒是要求很高。另外,对于内存泄露的处理,也要求得比较高一些。整个这些策略,其实是来源于C/S系统的影响。 - 浏览器版本相对宽松 很多时候提到企业应用,大家的想法就是低端,IE6,但其实这个的原因是客户只购买软件,运维一般自己做,每年不会有很多持续的投入来改进,所以导致很多老系统不能持续升级。软件厂商其实反倒可以用更激进的策略去升级浏览器,用户对这个的接受度还是比较高的,使用系统的群体也是比互联网用户小很多的,抛弃老旧浏览器的事情也确实可以干,比如我就见过几年前某电信营业系统预装的都是Firefox。 ### 企业应用常见的前端框架 在开发B/S企业应用前端的人群中,有很大一部分群体选择了服务端的组件化方式,比如JSF之类,它的弊端是与异构服务端的第三方系统集成比较麻烦。也有不少人使用Bindows和ExtJS这样的框架,最近的KendoUI也是个不错的选择。 每种类型选一个有代表性的来说说: - HTC 在浏览器端扩展标签 早期有些团队采用的方式,一般会跟XMLHTTP等结合使用,易于使用,界面代码整洁,但已被主流浏览器抛弃。 - JSF等 在服务端生成界面 以后端为主的架构师最推崇的方式,受Struts的MVC模型影响很深,弱化了前端,使得前端蜕化为后端的一种附属。 - GWT 编译阶段生成界面 写其他语言来生成HTML和JS,一般会依赖于一种前端UI库。这种方式也比较受后端架构师喜欢,因为他们觉得写JS很头疼,宁可写Java。 - ExtJS 用JS封装界面组件,干脆就不要HTML了 这是另外一种极端,从Bindows开始,使用纯逻辑代码来描述界面,走着跟Java Swing一样的道路,也有不少人喜欢。但这种方式在没有好用的界面设计器的情况下非常痛苦。 - Flex等 脱离HTML体系,另辟蹊径 这条路其实是对Java Applet的一种延续,好处是可以不受HTML体系的制约,独立发展,所以其实这些体系在企业应用领域的成熟度远超HTML体系。 ### 曾经的企业B/S应用几件宝 有一段时间,我们几乎只有IE6,所以那个时候的前端开发人员很快乐,没有兼容的压力。那时候,我们如何构建前端应用呢? 参见[http://weibo.com/1858846672/B1fL3vuYN?mod=weibotime](http://weibo.com/1858846672/B1fL3vuYN?mod=weibotime "这条微博") - HTC 这是最好用的声明控件的方式。 - XMLHTTP 尽管还没有AJAX的概念,但我们已经可以用它做前后端分离的传输机制了。 - VML 在IE里面画矢量图,不使用插件,有其他选择吗? - XSLT 把XML数据转换成HTML,跟现在的前端模板像吗? - popup 创建右键菜单最好的方式。 [用这些技术构建的一个典型企业应用](http://xufei.github.io/assets/iom.swf "用这些技术构建的一个典型企业应用") ### 单页应用和前端分层 当时这些系统的构建方式也可以算单页应用,我们用iframe来集成菜单,每个菜单有自己独立的功能,整个主界面是始终不会刷新的。 时光飞逝,这些年,前端有了什么本质的改变,产生了翻天覆地的变化吗? 有时候我们回顾一下,却发现多数都是在增加完善一些细节,真正有颠覆性的有比如以RequireJS和SeaJS为代表的模块定义和加载库,npm这样的包管理器,grunt,gulp,百度fis这样的集成开发模式。为什么它们算是本质改进呢? 因为这些标志着前端开发从粗放的模式,逐渐变化到精确控制的形态。比如我们再也不能不管代码的依赖关系,也不能一打开界面就不分青红皂白把所有可能要用到的代码都立刻加载过来,那个时代已经过去了,从任何角度讲,现代的前端开发都在精细化,从代码的可控,到界面体验的精细优化,到整个团队甚至公司甚至互联网上的组件共享,以及前端团队协作流程的改进,这已经是一个很成规模的产业了。 我们把眼光放到2013年,在这一年里最火的前端技术莫过于NodeJS和AngularJS,前者给我们带来的是一种开发方式的改变,后者是一种典型的前端分层方案。Angular是前端MV*框架的一个流派,用过的人都会觉得很爽。它爽在什么地方呢?因为它帮我们做的事情太多了,一个双向绑定,无所不包,凡是存取值相关的操作,基本都不用自己写代码。在企业应用前端功能里,表单的存取值和校验占据了很大的比例,这些事都不用干了,那简直太好了。 如果就因为这个用Angular,那还有些早。有一些第三方代码被称为库,另外一些称为框架,Angular是框架而不是库。框架的含义是,有更强的约束性,并非作为辅助功能来提供的。 先看一下企业应用的通常形态吧,会有一个可配置的菜单,然后多半会采用MDI的形式,能打开多个业务功能,用选项卡的形式展示起来,可以随时切换操作。每个人每天常用的功能是可以穷举的,他进入系统之后,一般要用到下班才关掉。所以这种系统非常适合做成单页应用,开始的时候加载一个总体框架,每点击一个菜单,就加载这个菜单对应的功能模块,放在一个新的选项卡或者别的什么地方展示出来。 在早期做这种系统的时候,一般都会用iframe来集成菜单,这种方式很方便,但是每个菜单页都要载入共同的框架文件,初始化一个环境,数据之间也不能精确共用。 所以现在我们做企业信息系统,不再适合用iframe来集成菜单,所有菜单的业务代码,会在同一个页面的作用域中共存。这在某些方面是便利,比如数据的共享,一个选择全国城市的下拉框,在多个功能中都存在,意味着这些城市的数据我们可以只加载一次。但从另外一个角度来说,也是一种挑战,因为数据之间产生干扰的可能性大大增加了。 我们回顾一下在传统的客户端开发中是怎么做的,早在经典的《设计模式》一书中,就提到了MVC模式,这是一种典型的分层模式。长期以来,在Web开发人员心中的MVC,指的都是Struts框架的那张图,但我们单页应用中的MVC,其实更接近最原始的《设计模式》书中概念。所以我们要在前端分层,而不仅仅把整个前端都推到视图层。 做单页应用,前端不分层是很难办的,当规模扩大的时候,很难处理其中一些隐患。分层更重要的好处是能够从全盘考虑一些东西,比如说数据的共享。跨模块的数据共享是一个比较复杂的话题,搞得不好就会导致不一致的情况,如果考虑到在分层的情况下,把各种数据来源都统一维护,就好办多了。 所以,以AngularJS为代表的前端MV*框架最重要的工作就是做了这些对于分层的指导和约束性工作,在此基础上,我们可以进一步优化单页应用这类产品。 ### 前端的自定义标签体系 构建一个大型企业应用,最重要的是建立整套组件体系。一般针对某行业的软件,长期下来都会有很多固定的模式,可以提炼成组件和规则,从前端来看,体现为控件库和前端逻辑。控件库这个是老生常谈,在很多框架里都有这个概念,但各自对应的机制是不同的。 从写一个界面的角度来讲,最为便利的方式是基于标签的声明式代码,比如我们常见的HTML,还有微软的XAML,Flex中的MXML等,都很直接,设想一下在没有可视化IDE的情况用类似Java Swing和微软WinForm这样的方式编写界面,毫无疑问写XML的方式更易被接受。所以,我们可以得出初步的结论,界面的部分应该写标签。 很遗憾,HTML自带的标签是不足的,它有基本表单输入控件,但是缺乏DataGrid,Tree之类更富有表现性的控件。所以绝大多数界面库,都采用某种使用JavaScript的方式来编写这类控件,比如: ```HTML ``` ```JavaScript $(function() { $( "#tabs" ).tabs(); }); ``` 如果这样,这些复杂控件就都要通过JavaScript来创建和渲染了,这与我们刚才提到的原则是违背的。那我们寻找的是什么呢,是一种能扩展已有HTML体系的东西。在早期,IE浏览器中有HTC,可以通过引入命名空间来声明组件,现在的标准浏览器中又引入了Web Components,在Polymer这个框架中可以看到更多的细节。说到底,这类方式要做些什么事情呢? - 隔离组件的实现,让使用变得简单 - 支持自行扩展新的组件 - 作一些作用域上的隔离,比如Web Components里面,style标签上可以加作用域,表示这个样式只生效于组件内部 从另外一个角度讲,为什么我们非要这么做不可?最大好处来自哪里?对于大型项目而言,管理成本和变更成本都是需要认真考虑的。如果一个组件,需要在DOM中声明一个节点, 然后再用一个js去获取DOM,把DOM渲染出来,再填充数据的话,这个过程的管理成本是很大的,因为HTML和JS这两个部分丢了一个都会有问题,无论在什么时候,维护一个文件总是比维护多个文件要强的,我们看HTC那种方式,为什么它的使用成本很低,因为它可以把控件自身的DOM、逻辑、样式全部写在自己内部,整个一个文件被人引用就可以了。在现在这个阶段不存在这么好用的技术了,只能退而求其次。 所以,在这个点上,Angular带来的好处是可扩展的标签体系,这也就是标签的语义化。Angular的主打功能之一是指令,使用这种方式,可以很容易扩展标签或者属性。比如,业务开发人员可以直接写: ```JavaScript ``` 这样多么直观,而且可以跟原有的HTML代码一起编写,不造成任何负担。语义化的标签是快速编写界面的不二法门。 ### 业务逻辑 有了语义化标签之后,如果我们只写界面不写逻辑,那也够了,但现实往往没有这么美好,我们还要来考虑一下业务逻辑怎么办。 企业应用一般都是面向某行业的,在这个行业内部,会有一些约定俗成的业务模型和流程,这些东西如何复用,一直是一个难题。以往的做法,会把这些东西都放在服务端,用类似Java这样的语言来实现业务元素、业务规则和业务流程的管理。 这种做法所带来的一个缺点就是对界面层的忽视,因为他只把界面层当作展示,对其中可能出现的大量JavaScript逻辑感到无所适从。很多从事这一领域的架构师不认同界面层的厚度,他们认为这一层只应当是很薄的,纯展示相关的,但在这个时代,已经不存在真正轻量级的界面了。 前面提到,我们在前端作分层,把展现层跟业务逻辑层完全隔离,带来的好处就是逻辑层不存在对DOM的操作,只有纯粹的逻辑和远程调用,这么一来,这一层的东西都可以很容易做测试。对于一个大型产品来说,持续集成是很有必要的,自动化测试是持续集成中不可缺少的一环。如果不做分层,这个测试可能就比较难做,现在我们能把容易的先做掉,而且纯逻辑的代码,还可以用更快的方式来测试。 之前我们做前端的单元测试,都需要把代码加载到浏览器来执行,或者自行封装一些“无头浏览器”,也就是不打开实际的展示,模拟这个测试过程。这个过程相对来说还是有些慢,因为它还有加载的这个网络传输的过程,如果我们能在服务端做这个事情呢? 我们看到,最近很火的NodeJS,它从很多方面给了前端工程师一个机会,去更多地把控整个开发流程,在我们这个场景下,如果能把针对前端逻辑的单元测试都放在node里做,那效率就会更高。 ### 二次开发平台 我们来看看,有了这么一套分层机制,又有了界面标签库之后,该做些什么呢? 做企业软件的公司,有不少会做二次开发平台,这个平台的目标是整合一些已有的行业组件,让业务开发人员甚至是不懂技术的业务人员通过简单的拖拉、配置的形式,组合生成新的业务功能。 从界面的角度看,拖拽生成很容易,很多界面原型工具都可以做,但要如何整合数据和业务?因为你要生成的这个功能,是实实在在要拿去用,不是有个样子看就可以,所以要能跟真实数据结合起来。 但这事情谈何容易! 就比如说,界面上有一个选择所属行业的下拉框,里面数据是配置出来的,对这个数据的查询操作在后端,作为一个查询服务或者是业务对象管理起来,有些传统的方式可能是在后端作这个关联,Angular框架可以把这个事情推到前端来。相比Backbone这样的框架来说,Angular由于有双向绑定,这个过程会变得特别省事。一个界面片段想要和数据关联起来,要做的事情就是各种属性的设置,所以动态加载和动态绑定都会比较容易。 比如: partial.html ```HTML
  • {{item.name}}
``` main.html ```HTML ...
... ``` a.js ```JavaScript function CtrlA($scope) { $scope.items = [{name:"Tom"}, {name:"Jerry"}]; } ``` b.js ```JavaScript function CtrlB($scope) { $scope.items = [{name:"Donald"}, {name:"Micky"}]; } ``` 在上面的例子里,这个列表显示什么,完全取决于ng-controller="CtrlA"这句,如果我们把这句搞成配置的,就很容易把数据源换成另外一个CtrlB,甚至说,即使在同一版本上做项目化,引入另外一个包含CtrlA其他版本的js文件,也基本无需更改其他代码,这就达到了二次开发的一个目的:尽可能以配置而不是编码去新增、维护新功能。 ### 移动开发 现在的企业软件已经不能只考虑PC的浏览器了,很多客户都会有移动办公的需求。响应式设计是一种常见的解决方案,但是在企业应用领域,想要把复杂的业务功能设计成响应式界面的代价太大了,况且界面设计本身就是开发企业软件的这些公司的短板,所以我们的比较简单的办法是对PC和移动终端单独设计界面,这样就有了一个问题了,这两种界面的业务逻辑并没有差别,如果我们要维护两套代码,代价是非常大的,能有什么办法共用一些东西呢? 如果不采用分层的形式,那这个很麻烦,我们注意到两种系统的差异只在UI层,如果我们用分层的模式,可以共用UI层以外的东西。具体到Angular里面来说,比如service,factory,甚至controller都是可以共用的,只有directive和HTML模板随设备产生差异就可以了。 之前我们很少看到有基于Angular的移动端开发框架,但现在有了,比如Ionic,使用这样的框架,可以直接引用已有的业务逻辑代码,只在展示上作一些调整。这么做有很多好处,同时也对代码的架构水准有一定要求,需要把业务逻辑跟界面展示完全切割开。 这样带来的好处也是很明显的,独立的业务逻辑,因为它不依赖于界面了,所以很容易控制,做单元测试,集成测试,打桩等等,总之它是纯逻辑的东西,在后端可以用什么方式保证代码质量,在前端的业务逻辑也一样可以用,业务逻辑可以因此而清晰稳定。 对于企业应用而言,这么做可以极大程度地复用以往的业务逻辑,只在负责最终展示的代码部分作差异化。 ### 工程化 上面这些技术性的问题都解决了,剩下的都是规模带来的边际效应,这需要我们从工程化角度去考虑很多问题: - 某个JS模块被修改,如何得知会影响谁? - 某个界面片段被调整,会影响什么界面? - 如何最小化发布? - 如何一键测试、打包、压缩? - 。。。。。。 这些话题,篇幅所限,不在本文中叙述,可以查看我另外的关于Web应用组件化的文章。 ================================================ FILE: posts/2014-04-27-清华故地重游.md ================================================ 清华故地重游 ==== 2014年4月底在北京参加QCon,27号抽空回清华看了一遍,10多年没回来,很感慨,没想到正好还碰上校庆了。学校里面的路居然还记得,打车从东门进,到主楼附近下车了往里走,那些年的回忆涌上心头。 这是四教,入学时候英语分级考试就在这。第一节正式的课程:王致勇老师的《无机化学》也是在这里上。 ![四教](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu1fxhokj21kw166qti.jpg) 这是四教和五教中间的过道。有次下雪,同学提醒我小心点,我说,你见过龙王被水呛的吗?话音未落立刻摔在地上,被群嘲了。 ![四教五教中间的过道](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu1h45ayj21kw1667wh.jpg) 这是刚才的路口。有一次骑车路过,右前方的一个人急速左拐,我没刹得住车,撞在他后座,从他车上面飞了过去,手心都是血。 ![五教路口](http://ww2.sinaimg.cn/mw690/6ecbbfd0gw1efwu0qk3msj21mg17ce81.jpg) 这是9号楼,计算机系的,后面是10号楼。 ![9号楼](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu0v6l8hj21mg17cnpd.jpg) 入学的时候在这个地方,有志愿者迎接新生。 ![入学迎新](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu1ot39kj21mg17cx6p.jpg) 体育课一般在这上。 ![体育课](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu1jhoa5j21mg17cu0x.jpg) 到东面来了,左边是9号楼,右边是10号楼,在这个楼下丢过好几辆自行车。 ![9号楼10号楼中间](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu0tre1sj21mg17cu0x.jpg) 10号楼的门口,变成办公楼了。 ![10号楼](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu0x1gglj21mg17ckjl.jpg) 10号楼427,从98年住到99年,进门左手边靠窗的上铺,现在是办公室了,没进去。 ![427](http://ww2.sinaimg.cn/mw690/6ecbbfd0gw1efwu14qf6uj21mg17c1br.jpg) 宿舍斜对面的洗手间,亮亮在这里一边洗衣服一边欢快唱歌,现在他是海归副教授了,当年也有唱ws歌的一面,哈哈。 ![wc](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu13pyk9j21mg17cne6.jpg) 那时候宿舍没电话,家里打电话过来的时候,宿管喊427徐飞电话,然后一边答应说来了来了,一边飞奔下来。 ![楼梯口](http://ww2.sinaimg.cn/mw690/6ecbbfd0gw1efwu15mv34j21mg17ch7k.jpg) 这个地方以前是8食堂,在这吃饭次数很多。 ![原来的8食堂](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu0rxegtj21mg17ckjl.jpg) 那时候这里是个小店,有卖汽水的,我这种乡下孩子从来没喝过芬达和苹果汁,西瓜汁,不知道那个叫什么,只见过可乐和雪碧,后来听到站在我前面的来自深圳的赵铌同学说,才学着说要苹果汁。 ![小卖部](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu1e49v3j21mg17ckjl.jpg) 在这学排球的,学不会,被老师训,标语很震撼人心,每个人入学的时候都默默算了一下吧? ![体育系](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu1i2esaj21mg17c7wh.jpg) 那时候这里是平地,在这军训,很大一片操场。 ![东操场](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu1bret3j21mg17cb29.jpg) 10食堂,做化学实验回来一般会在这吃饭,河对面的树林里当时有练某某功的,好奇围观了一次。 ![10食堂](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu0ybl2xj21mg17ce81.jpg) 28号楼,机械系在里面,某师兄的宿舍在这。 ![28号楼](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu126j1ej21mg17ckjl.jpg) 我们材料系在这,23号楼,住306,现在要刷卡进去,没能进。 ![23号楼](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu0znzibj21mg17ce81.jpg) 23号楼背后,99年短学期有一次整个宿舍回来晚了,没喊宿管,武涛从这爬上去,然后挨个拉我们上去,那时候没有栏杆. ![23号楼背后](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu10wt2qj21mg17chdt.jpg) 这是14还是15食堂?经常跟材81的吴光麟一起在这吃。那会旁边有一个店卖饼干,3块一斤,很划算。 ![丁香园](http://ww2.sinaimg.cn/mw690/6ecbbfd0gw1efwu17ypvdj21mg17c1kx.jpg) 二校门 ![二校门](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu18z6i6j21mg17c7wh.jpg) 一教,在这里上CAD课程,学autocad14 ![一教](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu1nkp5bj21mg17ce81.jpg) 日晷和大礼堂 ![日晷](http://ww2.sinaimg.cn/mw690/6ecbbfd0gw1efwu1d0ejpj21mg17c7wh.jpg) 学堂,制图课程好像在这,也有在水利馆的 ![学堂](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu1m74a1j217c1mghdt.jpg) 同方部 ![同方部](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu1l0pwzj217c1mge81.jpg) 大礼堂西面的池塘 ![池塘](http://ww4.sinaimg.cn/mw690/6ecbbfd0gw1efwu16t420j21mg17ce81.jpg) 自清亭 ![自清亭](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu1r09ucj21mg17ce81.jpg) 朱自清雕像 ![雕像](http://ww3.sinaimg.cn/mw690/6ecbbfd0gw1efwu1pxaxkj21mg17ce81.jpg) 水木清华 ![水木清华](http://ww1.sinaimg.cn/mw690/6ecbbfd0gw1efwu1f2ygjj21mg17c4po.jpg) 荷塘月色 ![荷塘月色](http://ww2.sinaimg.cn/mw690/6ecbbfd0gw1efwu1ahyv7j21mg17chdt.jpg) 其实这一片我很少来,太文艺了。。。 下午还去亮亮的办公室坐了会,大家都跟以前有些不一样了,他送我们下楼的时候,在电梯碰到个女生,叫他李老师好,我就在犹豫要不要把李老师当年的糗事说出来,哈哈哈哈。 ================================================ FILE: posts/2014-05-20-前端架构那些事儿.md ================================================ 前端架构那些事儿 ==== 在谈前端架构之前,需要先探讨一下不同人群对前端产生的困惑。前端这个职业最近几年才逐渐被认可,之前一直是低端的代名词,所以多数高手很不屑搞这个。之前的很多项目,人们对前端这块的要求也只是能用就行,所以很少会在上面去细致、深入地建立一套完善体系。而多数产品的技术经理也会是后端出身的,往往对前端的认识还停留在Java Struts那个原始的MVC模型上,或者首先想到的就是GWT和JSF,这是从后端角度出发的一种视角。用这类思维方式做出来的产品,一般用户体验都不会很好。 另一方面,从界面层上手的人群,他对用户体验这方面会把控得比较好,但通常缺架构意识,或者说是软件工程的意识。在界面层比较复杂的情况下,很可能会有失控的趋势。对整个系统结构的认知程度通常不够深入,也缺乏设计模式等方面的知识。 开发人员会有一些困惑: - 创建项目的时候,一般没有人作前端的技术选型 - 拿到项目之后,没有直接可复制的基础版本 - 习惯于引用第三方组件 - 赶功能,需要某个组件或者特效 - 上网搜到一个合适的,加进来 - 它还依赖一些别的库 - 文件大还是次要的 - 可能会产生冲突,样式也不一致 开发经理也会有一些困惑: - 协作过程感觉有问题 - 前端人员写原始界面,包含静态界面和特效 - 开发人员接着改,加逻辑 - 发现有问题要返工了 - 在谁的代码基础上继续改?如何合并? - 2014年了,为什么还有这么多人工环节? - 能自动单元测试吗? - 能自动发布打包吗? 用户会对这些事情感到烦恼: - 长得丑 - 界面老土 - 风格不一致 - 速度慢 - 加载慢 - 渲染慢 - 执行慢 - 出错 架构的本质是什么?其实也是一种管理。通常我们所说的管理,都是指对于任务和人员的管理,而架构管的是机器和代码。比如说,机器的部署属于运维的物理架构,SOA属于服务架构,那么,前端的架构指什么呢? 长期以来,前端所处的位置是比较偏应用层,而且是很薄的一层,而架构又要求深度和广度,所以之前在前端里面做架构,好比在小水塘里游泳,稍微扑腾两下就到处碰壁。但最近这几年来,前端的范围被大大拓展了,所以这一层逐渐变得大有可为。 怎样去理解架构呢?在早期的文字MUD游戏里,有这么一句话:“你感觉哪里不对,但是又说不上来。”在我们开发和使用软件系统的过程中,或多或少会遇到这样的感觉,有这种感觉就说明架构方面可能有些问题。 在狭义的前端领域,架构要处理的很重要的事情是组件的集成。由于JavaScript本身缺乏命名空间这样的机制,多数框架都倾向于自己搞一套,所以这方面的碎片化是很严重的。如果一个公司的实力不足以自研所有用到的组件,就会一直面临这方面的问题。 比如说,在做某个功能的过程中,发现需要一个组件,时间来不及做,就到网上搜了个,加到代码里面,先运行起来再说。一不小心,等功能做完的时候,已经引入了无数种组件了,有很多代码是重叠的,可能有的还有冲突,外观也不一致。 环顾四周的大型互联网公司,基本上都有自己的前端框架,比如阿里的Kissy和Arale,腾讯的JX,百度的Tangram,360的QWrap等,为什么?因为要整合别的框架,并且在此基础上发展适合自己的组件库,代价非常大,初期没办法的时候只能凑合,长期来说,所有代码都可控的意义非常重要。 那么,是不是一套框架可以包打天下呢,这个真的很难。对于不同的产品形态,如果想要用一套框架去适应,有的会偏轻,有的又偏重,有的要兼容低端浏览器,有的又不要,很难取舍。 常见的前端产品形态包括: - 内容型Web站点 侧重渲染方面的优化,前端逻辑比重小 - 操作型B/S系统 以数据和逻辑为中心,界面较规整 - 内嵌Web的本地应用 要处理缓存和一些本地接口,包括PC客户端和移动端 另外有Web游戏,因为跟我们的企业形态关系不大,而且也比较独特,所以不包含在内。这三种产品的前端框架要处理的事情显然是不太一样的,所以可以细分成2-3种项目模板,整理出对应的种子项目,供同类产品初始化用。 最近我们经常在前端领域听说两个词:全端、全栈。 全端的意思是,原来的只做在浏览器中运行的Web程序不够,还要做各种终端,包括iOS,Android等本地应用,甚至PC桌面应用。 为什么广义的前端应当包含本地应用呢?因为现在的本地应用,基于很多考虑,都变成了混合应用,也就是说,开发这个应用的技术,既包含原生的代码,也包含了嵌入的HTML5代码。这么一来,就造成了开发本地应用的人技能要求较广,能够根据产品的场景,合理选择每个功能应当使用的技术。 现在有一些PC端的混合应用开发技术,比如node-webkit和hex,前者的典型应用是Intel® XDK,后者的典型应用是有道词典,此外,豌豆荚的PC客户端也是采用类似技术的,也有一些产品是用的qt-webkit。这类技术可以方便做跨平台,极大减少开发工作量。 所以,我们可以看到,在很多公司,开发安卓、iOS应用的人员跟Web前端的处于同一个团队中,这很大程度上就是考虑到这种情况。 全栈的意思是,除了只做在浏览器中运行的代码,还写一些服务端的代码,这个需求又是从哪里来的呢? 这个需求其实来自优化。我们要优化一个系统的前端部分,有这么一些事情可以做: - HTML结构的优化,减少DOM树的层次等等 - CSS渲染性能的优化,批量写入DOM变更之类 - 资源文件的优化,比如小图片的合并,图像格式的处理,图标字体的使用等 - JavaScript逻辑的优化,模块化,异步加载,性能优化 - 加载字节量的优化,主要是分摊的策略 - HTTP请求的优化 这里面,除了前三条,其他都可能跟后端有些关系,尤其是最后一条。但是前端的人没法去优化后端的东西,这是不同的协作环节,所以就很麻烦。 那么,如果有了全栈,这个问题可以怎么解决呢? 比如说,我们要做最原始的小文件合并,可以在服务器做一些配置,把多个合并成一个请求,比如天猫的某个url: http://g.tbcdn.cn/kissy/k/1.4.1/??dom/base-min.js,event-min.js,event/dom/base-min.js,event/base-min.js,event/dom/touch-min.js,event/dom/shake-min.js,event/dom/focusin-min.js,event/custom-min.js,cookie-min.js?t=1.js 这个就很明显是多个文件合并而成的,9个小文件的请求,合并成了一个64k的文件请求。 这种简单的事情可以在静态代理服务器上配置出来,更复杂的就比较难了,需要一定的服务端逻辑。比如说,我们有多个ajax请求,请求不同的服务,每个请求的数据量都非常少,但因为请求数很多,可能会影响加载性能,如果能把它们在服务端就合并成一个就好了。但这个优化是前端发起的,传统模式下,他的职责范围有限,优化不到服务端去,而这多个服务很可能是跨产品模块的,想要合并,放在哪个后端团队都很怪异。 这可真难办,就像老虎追猴子,猴子上了树,老虎只能在下面干瞪眼。但是如果我们能让老虎上树,这就不是个问题了。如果有这么一层NodeJS,这一层完全由前端程序员控制,他就可以在这个地方做这种合并,非常的合理。 除此之外,我们常常会用到HTML模板,但使用它的最佳位置是随着产品的场景而不同的,可能某个地方在前端更好,可能某个地方在后端好些。到底放在哪合适,只有前端开发人员才会知道,如果前端开发人员不能参与一部分后端代码的开发,优化工作也还是做不彻底。有NodeJS之后会怎样呢,因为不管前端模板还是后端模板,都是JavaScript的,可以使用同一套库,这样在做调整的时候不会有代码迁移的烦恼,直接把模板换地方即可。 现在,也有很多业务场景有实时通信的需求,目前来说最合适的方案是Socket.io,它默认使用NodeJS来当服务端,这也是NodeJS的一个重要使用场景。 这样,前端开发人员也部分参与了运行在服务端的代码,他的工作范围从原先客户端浏览器,向后拓展了一个薄层,所以就有了全栈的称呼。至于说这个称呼还继续扩展,一个前端开发人员从视觉到交互到静态HTML到JavaScript包办的情况,这个就有些过头了。 以上这些,主要解决的都是代码层面的事情。另外有一个方面,也是需要关注,但却常常不能引起重视的,那就是前端的工程化问题。 早期为什么没有这些问题?因为那时候前端很简单,复杂度不高,现在整个很复杂了,就带来了很多管理问题。比如说整个系统的前端都组件化了之后,HTML会拆分成各种模板,JavaScript会拆分成各种模块,而CSS也通过LESS或者SASS这种方式,变成了一种编译式的语言。 这时候,我们考虑一个所谓的组件,它就比较麻烦了。它可能是一个或者多个HTML模板,加上一个或者多个JavaScript模块,再包含CSS中的一部分构成的,而前两者都可能有依赖项,三个部分也都要避免与其他组件的冲突。 这些东西都需要管理,并且提供一种比较好的方案去维护。在JavaScript被模块化之后,也可以通过单元测试来控制它们的质量,并且把这个过程自动化,每次版本有变更之前,保证它们最基本的正确性。最终,需要有一种自动化的发布机制,把这几类代码提取,打包合并,压缩,发布。 这个主题展开可以讲很多,所以我们不在本次分享中涉及。在我之前的几篇文章中,也阐述过观点。 目前这方面研究最深入的是之前百度FIS团队的张云龙,他的几篇文章在[这里](https://github.com/fouber/blog "这里"),强烈推荐阅读。 后记: 这篇文章是我入职苏宁之后第一次公开分享,目标受众主要是后端出身的技术经理,目的是让这个群体能有更多的前端意识。现在公司的项目基本都有前端模块,但人员专职程度较低,水平也参差不齐。苏宁的战略口号之一是提升用户体验,从产品角度看,用户体验的提升并非是UI做几个图,搞一些花哨效果就可以了,它是一个系统工程,涉及从用户习惯调研、产品设计、前端开发、甚至后端服务等一系列环节,需要从易用度、观感、加载性能、流畅度等各方面共同提升。 这些东西都需要从全局角度作规划,从源头控制起,否则只能是头疼医头,脚痛医脚。为此,基础技术中心会逐步整合几套适合不同场景的基础前端框架,作为种子项目供今后的技术选型使用。此外,还会从前端开发的各种主题组织一些技术分享,并且逐步形成一套制度化,流程化的培训体系。 ================================================ FILE: posts/2014-10-01-From HTC to Web Components.md ================================================ 从HTML Components的衰落看Web Components的危机 ==== 搞前端时间比较长的同学都会知道一个东西,那就是HTC(HTML Components),这个东西名字很现在流行的Web Components很像,但却是不同的两个东西,它们的思路有很多相似点,但是前者已是昨日黄花,后者方兴未艾,是什么造成了它们的这种差距呢? ## HTML Components的一些特性 因为主流浏览器里面只有IE支持过HTC,所以很多人潜意识都认为它不标准,但其实它也是有标准文档的,而且到现在还有链接,注意它的时间! [http://www.w3.org/TR/NOTE-HTMLComponents](http://www.w3.org/TR/NOTE-HTMLComponents "HTML Components") 我们来看看它主要能做什么呢? 它可以以两种方式被引入到HTML页面中,一种是作为“行为”被附加到元素,使用CSS引入,一种是作为“组件”,扩展HTML的标签体系。 ### 行为 行为(Behavior)是在IE5中引入的一个概念,主要是为了做文档结构和行为的分离,把行为通过类似样式的方式隔离出去,详细介绍在这里可以看: [http://msdn.microsoft.com/en-us/library/ms531079(v=vs.85).aspx](http://msdn.microsoft.com/en-us/library/ms531079(v=vs.85).aspx) 行为里可以引入HTC文件,刚才的HTC规范里就有,我们把它摘录出来,能看得清楚一些: *engine.htc* ```HTML