如何开始一个模块化可扩展的Web App
虽然从没有认为自己是一个前端开发者,但不知不觉中也积累下了一些前端开发的经验。正巧之前碰到一道面试题,于是就顺便梳理了一下自己关于 Web App 的一些思路并整理为本文。
对于很多简单的网站或 Web 应用来说,引入 jQuery 以及一些插件,在当前页面内写入简单逻辑已经可以满足大部分需要。但是如果一旦多人开发,应用的复杂程度上升,就会有很多问题开始暴露出来:
- 数据源一般都与页面分离,那么 App 启动一般都需要等待数据源读入。
- UI 交互复杂时,需要将逻辑通过面向对象抽象后才能更好的复用。
- 功能间一般都存在依赖关系,需要引入支持依赖关系的模块加载器。那么如何解决这些问题,就以一个简单的订餐 App 为例,从零开始一个 模块化可扩展 Web App。
这个简单的 App 基于 HTML5 Boilerplate、requireJS、jQuery Mobile、Underscore.js,后端逻辑用 jStorage 模拟实现。完成后的成品 在此。所有代码可以在 github 查看。下文将逐一介绍实现的思路与方法。
从选择一个好模板开始
开始一个 Web 项目,HTML 的书写总是重中之重,一个好的 HTML 能从根源上规避大量潜在问题,所以 Web App 应该全部应用一个标准化的高质量 HTML 模板,而不是将所有页面交由开发人员自由发挥。
这里推荐使用 HTML5 Boilerplate 项目作为 App 的默认模板以及文件路径规范,无论是网站或者富 UI 的 App,都可以采用这个模板作为起步。
可以使用
git clone git://github.com/h5bp/html5-boilerplate.git |
或者直接下载 HTML5 Boilerplate 项目代码。HTML5 Boilerplate 的文件结构如下,
├── css |
|
之所以要这样写
- 可以使用 class 作为全局条件区分低版本的 IE 浏览器并进行调整,这显然要优于使用 CSS Hack。
- 可以避免 IE6 条件注释引起的高版本 IE 文件阻塞问题,原文的解决方法是在前面加一个空白的条件注释,但是这里显然将原本无用空白的条件注释变得有意义了。
- 仍然可以通过 HTML 验证。
- 与 Modernizr 等特征检测类库使用相同的 class,更具备通用性。
no-js
标签是需要与 Modernizr 等类库配合使用的,如果你不想在项目中引入 Modernizr,需要在 Head 部分加入一行使 no-js
标签变为 js
,代码来自 Avoiding the FOUC:
<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script> |
通过上面的条件注释,就可以在 CSS 中针对不同情况分别处理
.lt-ie7 {} /* IE6等版本时 */ |
Meta 标签的书写顺序
为了让浏览器识别正确的编码,meta charset 标签应该先于 title 标签出现。
meta X-UA-Compatible
标签可以指定 IE8 以上版本浏览器以最高级模式渲染文档,同时如果已经安装 Google Chrome Frame 则直接使用 Chrome Frame 渲染。而指定渲染模式的 meta X-UA-Compatible 标签同样需要优先出现
<meta charset="utf-8"/> |
设置移动设备显示窗口宽度
<meta name="viewport" content="width=device-width"/> |
这是移动设备专属的标签,具体设置需要根据项目实际情况调整。
使用 Modernizr 做浏览器差异检测
Modernizr 常做前端的应该都不陌生。引入 Modernizr 后,html 标签的 no-js 将会被自动替换为 js,同时 Modernizr 会向 html 标签添加代表版本检测结果的 class。
对于低版本浏览器的向上兼容需要根据项目实际需求处理,Modernizr 也非常周到的给出的 绝大多数 HTML5 功能的兼容方法。
CSS 篇
CSS 重置及增强功能
HTML5 Boilerplate 选择 Normalize.css 重置 CSS。如果项目计划引入 Twitter Bootstrap、YUI 3 这些前端框架的话则可以移除,因为这些框架已经内置了 Normalize.css。
同时 HTML5 Boilerplate 又引入了一个 main.css,内置了一些基本的排版样式以及打印样式。
使用 LESS 或 Sass 生成 CSS
在复杂应用中,如果还手写 CSS 的话将是一件痛苦的事情,大量的 class 前缀,复用样式需要来回 copy 等等。为了更好的扩展性,这里建议在项目中引入 LESS 或 Sass。这代表着:
- 支持变量与简单运算
- 支持 CSS 片段复用
- class/id 样式嵌套
等一些更像是编程语言的特性。这对于提高开发效率是效果非常明显的。
以 LESS 为例,简单介绍一下 LESS 在 Windows 下如何应用到这个项目中:
1. 下载 Nodejs 并安装,nodejs 会自动将自己加入系统路径。
2. 在 cmd 运行
npm install -g less |
3. 然后就可以通过 lessc 指令将 less 源文件编译为 css
lessc avnpc.less avnpc.css |
如果不使用 nodeJs 作为后端,最好在写 LESS 时采用 watch 模式,每次保存自动编译为 css。这里需要安装一个辅助模块 recess:
npm install -g recess |
然后运行 watch
recess avnpc.less:avnpc.css --watch |
Avascript 篇
使用 requireJS 按需加载
模块加载器的概念可能稍微接触过前端开发的童鞋都不会陌生,通过模块加载器可以有效的解决这些问题:
- JS 文件的依赖关系。
- 通过异步加载优化 script 标签引起的阻塞问题
- 可以简单的以文件为单位将功能模块化并实现复用
主流的 JS 模块加载器有 requireJS,SeaJS 等,加载器之间可能会因为遵循的规范不同有微妙的差别,从纯用户的角度出发,之所以选 requireJS 而不是 SeaJS 主要是因为:
- 功能实现上两者相差无几,没有明显的性能差异或重大问题。
- 文档丰富程度上,requireJS 远远好于 SeaJS,就拿最简单的加载 jQuery 和 jQuery 插件这回事,虽然两者的实现方法相差无几,但 requireJS 就有可以直接拿来用的 Demo,SeaJS 还要读文档自己慢慢折腾。一些问题的解决上,requireJS 为关键词也更容易找到答案。
requireJS 加载 jQuery + jQuery 插件
可能对于一般 Web App 来说,引入 jQuery 及相关插件的概率是最大的,requireJS 也亲切的给出了相应的解决方案及动态加载 jQuery 及插件的文档及实例代码。
在最新的 jQuery1.9.X 中,jQuery 已经在最后直接将自己注册为一个 AMD 模块,即是说可以直接被 requireJS 作为模块加载。如果是加载旧版的 jQuery 有两种方法:
- 让 jQuery 先于 requireJS 加载
- 对 jQuery 代码稍做一点处理,在 jQuery 代码包裹一句:
define(["jquery"], function($) { |
requireJS 的示例中,直接将 requireJS 与 jQuery 合并为一个文件,如果是采用 jQuery 作为核心库的话推荐这种做法。
同样对于 jQuery 插件来说也有两种方法
1. 在插件外包裹代码
define(["jquery"], function($){ |
2. 在使用 reuqireJS 代码加载前注册插件(比如在 main.js)中
requirejs.config({ |
requireJS 加载第三方类库
在实例的 App 中还用到了 jQuery 以外的第三方类库,如果类库不是一个标准的 AMD 模块而又不想更改这些类库的代码,同样需要提前进行定义:
require.config({ |
CSS 文件的模块化处理
在 requireJS 中,模块的概念仅限于 JS 文件,如果需要加载图片、JSON 等非 JS 文件,requireJS 实现了一系列 加载插件。
但是遗憾的是 requireJS 官方没有对 CSS 进行模块化处理,而我们在实际项目中却往往能遇到一些场景,比如一个轮播的图片展示栏,比如高级编辑器等等。几乎所有的富 UI 组件都会由 JS 与 CSS 两部分构成,而 CSS 之间也存在着模块的概念以及依赖关系。
为了更好的与 requireJS 整合,这里采用 require-css 来解决 CSS 的模块化与依赖问题。
require-css 是一个 requireJS 插件,下载后将 css.js 与 normalize.js 放于 main.js 同级即可默认被加载,比如在我们的项目中需要加载 jQuery Mobile 的 css 文件,那么可以直接这样调用:
require(['jquery', 'css!../css/jquery.mobile-1.3.0.min.css'], function($) { |
不过由于这个 CSS 本质上是属于 jQuery Mobile 模块的一部分,更好的做法是将这个 CSS 文件的定义放在 jQuery Mobile 的依赖关系中,最终我们的 requireJS 定义部分为:
require.config({ |
在使用模块时,只需要:
require(['jquery', 'underscore', 'jquerymobile', 'jstorage'], function($, _) { |
jQuery Mobile 的 CSS 文件就会被自动加载,这样 CSS 与 JS 就被整合为一个模块了。同理其他有复杂依赖关系的模块也可以做类似处理,requireJS 会解决依赖关系的逻辑。
数据源的加载与等待
Web App 一般都会动态加载后端的数据,数据格式一般可以是 JSON、JSONP 也可以直接是一个 JS 变量。这里以 JS 变量为例
var restaurants = [ |
载入这段数据:
$.getScript('data/restaurants.json', function(e){ |
单一的数据源确实很简单,但是往往一个应用中会有多个数据源,比如在这个实例 App 中 UI 就需要载入用户信息、餐厅信息、订餐信息三种数据后才能工作。如果仅仅靠多层嵌套回调函数的话,可能代码的耦合就非常重了。
为了解决多个数据加载的问题,我习惯的解决方法是构造一个 dataReady 事件响应机制。
var foodOrder = { |
用法为
foodOrder.dataReady(function(){ |
dataReady 内的 alert 将会在所有数据载入完毕后开始执行。
这段处理的逻辑并不复杂,将所有要执行的方法通过 dataReady 暂存起来,等待数据全部加载完毕后再执行,更加复杂的场景此方法仍然通用。
使用 JS 模板引擎
数据载入后,最终都会以某种形式显示在页面上。简单情况,我们可能会这样做:
$('body').append('<div>' + data.name + '</div>'); |
如果页面逻辑一旦复杂,比如需要有 if 判断或者多层循环时,这种连接字符串的方式就相形见绌了,而这也就催生出了 JS 模板引擎。
主流的 JS 模板引擎有 underscore.js,Jade,EJS 等等,可以 横向对比一下这些 JS 模板引擎的优缺点。
对于相对简单的页面逻辑(只需要支持 if 和 for/each)来说,我更倾向选用轻巧的 underscore.js
或者 JavaScript Templates。
在当前例子中,使用 underscore.js 生成列表就非常简单了,页面模板为:
在当前例子中,使用 underscore.js 生成列表就非常简单了,页面模板为:
<ul data-role="listview" data-inset="true"> |
调用引擎
$("#tmpl-restaurants").replaceWith( |
面向对象与模块化
通过上面这些工具的组合,我们有了模块的概念,有了模板引擎,有数据的加载。最终还是要通过 javascript 将这一切组织在一起并加入应用所需要的逻辑。为了能最大限度的复用代码,用面向对象的方式去组织内容是比较好的选择。
JavaScript 虽然原生并不支持面向对象,但是依然可以通过很多方式模拟出面向对象的特性。例子中采用了我个人比较喜欢的一种方式是:
var foodOrder = function(ui, options){ |
将页面的 UI 元素以及配置项目抽象出来,在实际构造对象时则可以通过入口参数复写,可以分离整个项目的逻辑与 UI,使处理的方式更加灵活。
The Why·Liam·Blog by WhyLiam is licensed under a Creative Commons BY-NC-ND 4.0 International License.
由WhyLiam创作并维护的Why·Liam·Blog采用创作共用保留署名-非商业-禁止演绎4.0国际许可证。
本文首发于Why·Liam·Blog (https://blog.naaln.com),版权所有,侵权必究。
本文永久链接:https://blog.naaln.com/2014/08/how-to-start-a-modular-and-scalable-web-app/