引言
Web 前端开发中,开发人员经常需要处理一些常规问题,如:
- 在页面中引用多个相互存在依赖关系的 Javascript 文件
- 在页面中引用 CSS 文件
- 浏览器事件绑定
- 表单的数据填充、数据打包提交、数据校验和格式化
- 页面初始化逻辑
采用传统的命令式编程范式来处理这些问题时,开发人员不得不反复地通过编写代码调用相关 API 来完成这些常规任务。事实上,开发人员的主要精力应该集中在业务逻辑实现上,而非在这些常规任务上过多消耗时间。声明式编程范式可以帮助开发人员以最小的工作量去快速搞定这些常规任务,从而能够将更多的精力放在业务逻辑的实现上。
decj 是一款以声明式编程范式为基础的 Javascript 开源框架。本文将介绍如何使用 decj 框架以近乎零编码的高效率方式去搞定这些常规任务。本期将介绍 decj 的声明式 Javascript 文件动态按需加载和声明式跨浏览器事件绑定。
decj 框架简介
decj 的优势及主要特性
decj 使得开发人员能够进行模块化的声明式编程,其目标在于简化 Web 应用开发中常规问题的处理,使得开发人员能够将更多的精力放在业务逻辑处理上。简单来说,decj 的优势在于:
- 声明式编程:使得开发人员能够集中精力在写必须由其编写的代码,提升开发效率。
- decj 所解决的问题是几乎每个 Web 应用中都要面对的普遍的问题:如事件绑定、表单数据校验、格式化、表单提交等。
- 代码即文档(Code is document):采用 decj 开发的应用,其代码某种程度上就是文档。
decj 的优势也就是声明式编程的优势。通过声明式编程,decj 使得开发者面对日常工作中经常要处理的问题时能够集中精力在“真正”需要其处理的问题上。
比如,但某个页面上的一个按钮被单击时,一段业务处理逻辑需要被执行。显然,这段被调用的业务逻辑才是开发者真正要集中精力处理的问题,因为业务逻辑是怎么样的只有人才能知道,而任何框架/库是无法得知并为开发人员代劳的。相反,采用命令式编程的框架 / 库,在处理此类问题时,开发人员往往得首先分心去处理一些非业务逻辑的问题,比如,如何让这个按钮响应单击事件。比如,若使用 jQuery 来实现,开发人员需要在代码中的恰当位置/时机(如
window 的 onload 事件被触发后)调用 jQuery 的 bind 方法,才能使按钮被单击后执行一段业务逻辑。如清单 1 代码所示:
清单 1. 命令式编程:使用 jQuery 处理事件绑定
$(document).ready(function(){// 加载页面完毕后执行该函数 $('#aButton').bind('click',function(){ // 在此处编写或者调用业务逻辑实现代码 }); });
而采用 decj,开发人员无须关心事件处理 API 以及在何处、何时机调用这些 API。可以更加关注业务逻辑。如清单 2 代码所示:
清单 2. 声明式编程:使用 decj 处理事件绑定
events:{ "click@aButton":function(){// 按钮 aButton 被单击时执行该函数 // 直接在此处编写或者调用业务逻辑实现代码 } }
可以看出,清单 2 的代码中并没有关于事件 API 的调用,开发人员因此可以不必关注这些 API 以及何时在何地方调用它们。开发人员可以重点关注如何响应相关事件,以实现业务逻辑。另外,decj 的应用代码可以充当文档。例如,如清单 2 的代码所示,对于页面上的某个元素的某个事件,是采用那个事件处理器响应的这个信息一目了然,这有助于问题定位,因为定位问题的人可以快速确认他需要的信息。
decj 支持以下几个主要特性:
- 声明式 Javascript 文件按需动态加载
- 声明式跨浏览器的事件绑定
- 声明式 CSS 文件按需动态加载
- 声明式 HTML 表单增强:表单内容自动填充、表单数据自动提交、表单重置增强、表单数据校验、数据格式化
- 声明式页面 / 模块初始化
- 声明式国际化(I18N)支持:支持多语言和按需加载资源文件
- HTML 代码 /CSS 文件 /Javascript 文件 / 资源文件并行加载
下面我们详细介绍 decj 框架的两大基础---声明式编程和 Javascript 模块化编程。若读者已经熟悉这两基础可以跳过这两部分内容。然后,我们将从 Web 前端开发中的日常任务入手,探讨这些日常任务的常规实现方法的弊端以及如何利用 decj 的声明式编程去克服或绕过这些弊端。
声明式编程
多数通用编程语言 (如 Java 语言) 都采用命令式编程范式(Imperative)。这种方式下的编程,我们不仅要告诉机器它要做什么,还要告诉它如何去完成。而声明式(Declarative)编程是一种只需要告诉机器要完成什么,而无需说明如何去完成的一种编程范式。声明式编程往往能够减少开发人员的工作量,使代码更加简洁和富有表现力。
声明式编程的一个常见例子是数据库查询语言 SQL(Structured Query Language)。如下一个 SQL 查询语句,它仅仅说明了要查询什么样的数据,而无需说明如何去查询这些数据:
清单 3. 声明式编程的例子—SQL 语句
SELECT license FROM frameworks WHERE name=’decj’ ; -- 查询名为 decj 的框架的许可证信息
Javascript 语言所支持的 JSON(JavaScript Object Notation) 语法非常适合于作为 Javascript 声明式编程的语法基础。比如,假设某个 Javascript UI(User Interface)库提供了一个名为 createDialog 的函数用于创建基于 HTML 的网页对话框。该函数接受一个参数,用于配置所要创建的对话框的一些属性和行为。那么,在调用 createDialog 函数就可以使用 JSON 语法来声明所要创建的对话框的属性。如清单 4 所示:
清单 4. JSON 作为 Javascript 声明式编程的基础
createDialog({// 创建一个标题为“成功”,宽 100 像素,高 200 像素的模态对话框 width:"100px", height:"200px", modal:true, title:" 成功 " });
Javascript 模块化编程
声明式编程是 decj 框架的核心,而模块化编程是其基础。decj 框架并不“再造车轮(Re-invent the wheel)”,它默认采用遵循 AMD(Asynchronous Module Definition)规范的 Javascript 库 RequireJS 来实现模块化编程。
AMD 规范定义了一个名为 define 的函数,通过该函数开发人员可以定义一个 Javascript 模块。在 AMD 规范中,一个 Javascript 模块可以是任何的 Javascript 对象,如函数、数组和普通 Javascript 对象。define 函数的签名如下:
define(id?,dependencies?,factory);
该函数的各个参数含义如下:
id:可选字符串,表示所要定义的模块的唯一标识(ID)。
dependencies:可选数组,表示当前模块所依赖的其它各个模块的唯一标识。
factory:模块工厂。通常是一个匿名函数,负责返回所要定义的 Javascript 模块。该函数接受若干个参数,每个参数都与 dependencies 参数中指定的 ID 对应的 Javascript 模块对象一一对应。
如下代码定义了一个 Javascript 模块,该模块是一个普通的 Javascript 对象,如清单 5 所示:
清单 5. 基于 AMD 规范的 Javascript 模块化编程
define(['jquery'],function(jquery){ // 该模块工厂依赖于 jQuery 库,故声明 ID 为 jquery 的模块为其依赖模块 // 返回一个模块对象。该对象包含一个名为 fieldValue 的方法 return { fieldValue:function(fieldName){ // 返回当前页面中名为 fieldName 的表单控件的值 return jquery("[name='+fieldName+']").val(); } //... }; });
AMD 规范定义了另一名为 require 的函数用于加载指定的 Javascript 模块。不过,由于 decj 框架的声明式编程的特性,应用开发者一般无需使用该函数了。因此,本文不详细介绍该函数了。
开始使用 decj
首先,从 github 下载 decj 框架,并将其部署到你的 Web 服务器上。
然后,编写 Web 应用的主页。并在主页中添加一个 script 标签,使该标签的 src 属性和 data-main 属性分别引用 RequireJS 和 decj 框架的 Javascript 文件。如清单 6 所示:
清单 6. 开始使用 decj 框架
<html> <head> <title>decj startup</title> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <script> function decjApp(){ return { initialModule:'../module/initModule' // 定义初始模块的 ID }; } </script> <script data-main="../js/lib/decj.js" src="../js/lib/requirejs.js"></script> </head> <body> 开始使用 decj! </body> </html>
模块化编程是 decj 的基础。一个 Web 应用可以包含多个模块,因此基于 decj 的 Web 应用需要一段应用启动代码。这段代码会定义 Web 应用的初始模块(即第一个会被加载的模块)的 ID。
应用启动代码是一个名为 decjApp 的函数,该函数返回一个普通的 Javascript 对象。该对象的 initialModule 属性用于指定当前应用第一个要加载的模块(初始模块),如清单 6 所示。decjApp 函数返回值的其它属性还可以用来定义 decj 的其它属性和行为。
最后,编写 decjApp 函数指定的初始模块对应的代码,这样一个基本的 decj Web 应用就可以使用了。清单 7 显示了一个示例模块代码。
清单 7. 一个 decj 示例模块
define(['jquery','decj'],function(jQuery,decj){ alert('decj comes into play!'); return {// 返回模块对象 }; });
声明式 Javascript 文件动态按需加载
前文讲到,模块化编程是 decj 的基础。采用基于 AMD 规范的 Javascript 模块化编程的好处不仅仅是降低各个模块的耦合度,一定程度上也可以提高应用的性能。因为各个 Javascript 模块及其所依赖的其它模块只有当其代码确实要被调用的时候才会由模块加载器去动态加载。
decj 默认采用 RequireJS 作为其模块加载器,因此基于 decj 框架开发的应用中的各个 Javascript 文件在运行的时候是被动态按需加载的,而不是页面一加载时就把所有可能用的 Javascript 都一起加载。事实上,开发人员也可以选择使用其它符合 AMD 规范的模块加载器。
声明式跨浏览器事件绑定
客户端编程中,事件绑定是一个几乎每天都要处理的一个问题。比如,要使页面上一个 ID 为 chkShowLog 的 checkbox 响应单击事件。当该 checkbox 被单击时,ID 为 log 的元素会在被显示和隐藏之间来回切换。采用传统的编程范式来实现这样简单的一个功能,即便在采用 jQuery 这样能够使我们编写简练代码的 Javascript 库的情况下,开发人员也不得不编写代码来调用浏览器事件处理的相关 API,如清单 8 所示:
清单 8. 使用 jQuery 实现事件绑定
$(document).ready(function(){// 在页面加载完毕后执行调用该函数 $('#chkShowLog').bind('click',function(ele){//ID 为 chkShowLog 的被单击后执行该函数 $('#log').toggle();// 显示或隐藏 ID 为 log 的元素 }); });
如果不采用任何 Javascript 库或框架,直接使用 Javascript 来实现清单 8 代码的功能,并且还要兼容不同浏览器的话,那么需要编写的代码就更加多和繁琐了。
decj 支持声明式的事件绑定。应用代码只需要在模块定义中声明哪个元素(事件目标元素)的哪个事件使用哪个监听器来处理,而无需调用任何与事件绑定有关的浏览器或者框架的 API。例如,清单 8 中的代码使用 decj 可以改写为清单 9 所示的代码:
清单 9. 使用 decj 的声明式事件绑定
define(["decj"],function(decj){ return { //… events:{ "click@#chkShowLog":function(){ // 使该函数响应 ID 为 chkShowLog 的元素的 onclick 事件 $("#log").toggle(); } } //… }; });
上面的的代码乍一看似乎比清单 8 中的代码要长。但事实上,清单 9 中的代码,除了下面清单 10 中的代码片段,其余的都是定义一个模块所必须的代码,而不属于事件绑定本身。并且,清单 9 中的代码没有任何 API 调用,开发人员只需要通过代码告诉框架本身无法知道的信息(即我们要采用哪个函数响应哪个元素的哪个事件)。
清单 10. 使用 decj 的声明式事件绑定
events:{ "click@#chkShowLog":function(){// 使该函数响应 ID 为 chkShowLog 的元素的 onclick 事件 $("#log").toggle(); } }
decj 的声明式事件绑定是跨浏览器兼容的。开发人员只需在模块定义中声明 events 属性。该属性是一个普通的 Javascript 对象。在该对象中可以声明多个事件绑定。每个事件绑定遵从如下格式:
“事件名 @目标元素对应的 CSS 选择器”: 事件监听器函数
例如,要使名为 switchLang 的函数响应 name 属性为 lang 的元素的 onchange 事件,只需在模块定义的 events 属性中声明:
“change@[name=lang]”:switchLang
事件绑定声明中“@“后面的 CSS 选择器遵从 jQuery 所支持的各个 CSS 选择器(见: http://api.jquery.com/category/selectors )。
小结
本期介绍了 decj 的优势及主要特性,并详细介绍了 decj 的“声明式 Javascript 文件动态加载”和”声明式跨浏览器事件绑定“这两个特性如何解决 Web 前端开发中以下常规问题:
- 在页面中引用多个相互存在依赖关系的 Javascript 文件
- 浏览器事件绑定
下一期将介绍 decj 的以下特性:
- 声明式表单功能增强(表单自动填充、打包、数据校验和格式化)
- 声明式页面 / 模块初始化
感谢崔康对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论