实现一个页面功能总是需要 JavaScript、CSS 和 Template 三种语言相互组织,所以我们真正需要的是一种可以将 JavaScript、CSS 和 Template 同时都考虑进去的模块化方案。
前端模块化带来的性能问题
很多主流的模块化解决方案通过 JavaScript 运行时来支持“匿名闭包”、“依赖分析”和“模块加载”等功能,例如“依赖分析”需要在 JavaScript 运行时通过正则匹配到模块的依赖关系,然后顺着依赖链(也就是顺着模块声明的依赖层层进入,直到没有依赖为止)把所有需要加载的模块按顺序一一加载完毕, 当模块很多、依赖关系复杂的情况下会严重影响页面性能。
模块化为打包部署带来的极大不便
传统的模块化方案更多的考虑是如何将代码进行拆分,但是当我们部署上线的时候需要将静态资源进行合并(打包),这个时候会发现困难重重,每个文件里 只能有一个模块,因为模块使用的是“匿名定义”,经过一番研究,我们会发现一些解决方案,无论是“ combo 插件”还是“ flush 插件”,都需要我们修改模块化调用的代码,这无疑是雪上加霜,开发者不仅仅需要在本地开发关注模块化的拆分,在调用的时候还需要关注在一个请求里面加载哪 些模块比较合适,模块化的初衷是为了提高开发效率、降低维护成本,但我们发现这样的模块化方案实际上并没有降低维护成本,某种程度上来说使得整个项目更加 复杂了。
首先我们来看一下一个 web 项目是如何通过“一体化”的模块化方案来划分目录结构:
- 站点(site):一般指能独立提供服务,具有单独二级域名的产品线。如旅游产品线或者特大站点的子站点(lv.baidu.com)。
- 子系统(module):具有较清晰业务逻辑关系的功能业务集合,一般也叫系统子模块,多个子系统构成一个站点。子系统(module)包括 两类: common 子系统, 为其他业务子系统提供规范、资源复用的通用模块;业务子系统:,根据业务、URI 等将站点进行划分的子系统站点。
- 页面(page): 具有独立 URL 的输出内容,多个页面一般可组成子系统。
- 模块(widget):能独立提供功能且能够复用的模块化代码,根据复用的方式不同分为 Template 模块、JS 模块、CSS 模块三种类型。
- 静态资源(static):非模块化资源目录,包括模板页面引用的静态资源和其他静态资源(favicon,crossdomain.xml 等)。
前端模块(widget),是能独立提供功能且能够复用的模块化代码,根据复用的方式不同分为 Template 模块、JS 模块、CSS 模块三种类型,CSS 组件,一般来说,CSS 模块是最简单的模块,它只涉及 CSS 代码与 HTML 代码; JS 模块,稍为复杂,涉及 JS 代码,CSS 代码和 HTML 代码。一般,JS 组件可以封装 CSS 组件的代码; Template 模块,涉及代码最多,可以综合处理 HTML、JavaScript、CSS 等各种模块化资源,一般情况,Template 会将 JS 资源封装成私有 JS 模块、CSS 资源封装成自己的私有 CSS 模块。下面我们来一一介绍这几种模块的模块化方案。
模板模块
我们可以将任何一段可复用的模板代码放到一个 smarty 文件中,这样就可以定义一个模板模块。在 widget 目录下的 smarty 模板(本文仅以 Smarty 模板为例)即为模板模块,例如 common 子系统的 widget/nav/ 目录
├── nav.css
├── nav.js
└── nav.tpl
下 nav.tpl 内容如下:
<nav id="nav" class="navigation" role="navigation"> <ul> <%foreach $data as $doc%> <li class="active"> <a href="#section-{$doc@index}"> <i class="icon-{$doc.icon} icon-white"></i><span>{$doc.title}</span> </a> </li> <%/foreach%> </ul> </nav>
然后,我们只需要一行代码就可以调用这个包含 smarty、JS、CSS 资源的模板模块,
// 调用模块的路径为 子系统名称:模板在 widget 目录下的路劲
{widget name="common:widget/nav/nav.tpl" }
这个模板模块(nav)目录下有与模板同名的 JS、CSS 文件,在模板被执行渲染时这些资源会被自动加载。如上所示,定义 template 模块的时候,只需要将 template 所依赖的 JS 模块、CSS 模块存放在同一目录(默认 JavaScript 模块、CSS 模块与 Template 模块同名)下即可,调用者调用 Template 模块只需要写一行代码即可,不需要关注所调用的 template 模块所依赖的静态资源,模板模块会帮助我们自动处理依赖关系以及资源加载。
JavaScript 模块
上面我们介绍了一个模板模块是如何定义、调用以及处理依赖的,接下来我们来介绍一下模板模块所依赖的 JavaScript 模块是如何来处理模块交互的。我们可以将任何一段可复用的 JavaScript 代码放到一个 JS 文件中,这样就可以定义为一个 JavaScript 类型的模块,我们无须关心“ define ”闭包的问题,我们可以获得“ CommonJS ”一样的开发体验,下面是 nav.js 中的源码.
// common/widget/nav/nav.js
var $ = require('common:widget/jquery/jquery.js'); exports.init = function() { ... };
我们可以通过 require、require.async 的方式在任何一个地方(包括 html、JavaScript 模块内部)来调用我们需要的 JavaScript 类型模块,require 提供的是一种类似于后端语言的同步调用方式,调用的时候默认所需要的模块都已经加载完成,解决方案会负责完成静态资源的加载。require.async 提供的是一种异步加载方式,主要用来满足“按需加载”的场景,在 require.async 被执行的时候才去加载所需要的模块,当模块加载回来会执行相应的回调函数,语法如下:
// 模块名: 文件所在 widget 中路径
require.async(["common:widget/menu/menu.js"], function( menu ) { menu.init(); });
一般 require 用于处理页面首屏所需要的模块,require.async 用于处理首屏外的按需模块。
CSS 模块
在模板模块中以及 JS 模块中对应同名的 CSS 模块会自动与模板模块、JS 模块添加依赖关系,进行加载管理,用户不需要显示进行调用加载。那么如何在一个 CSS 模块中声明对另一个 CSS 模块的依赖关系呢,我们可以通过在注释中的@require 字段标记的依赖关系,这些分析处理对 html 的 style 标签内容同样有效,
/**
* demo.css
* @require reset.css
*/
非模块化资源
在实际开发过程中可能存在一些不适合做模块化的静态资源,那么我们依然可以通过声明依赖关系来托管给静态资源管理系统来统一管理和加载,
{require name="home:static/index/index.css" }
如果通过如上语法可以在页面声明对一个非模块化资源的依赖,在页面运行时可以自动加载相关资源。
项目实例
下面我们来看一下在一个实际项目中,如果在通过页面来调用各种类型的 widget,首先是目录结构:
├── common
│ ├── fis-conf.js
│ ├── page
│ ├── plugin
│ ├── static
│ └── widget
└── photo
├── fis-conf.js
├── output
├── page
├── static
├── test
└── widget
我们有两个子系统,一个 common 子系统(用作通用),一个业务子系统,page 目录用来存放页面,widget 目录用来存放各种类型的模块,static 用于存放非模块化的静态资源,首先我们来看一下 photo/page/index.tpl 页面的源码,
{extends file="common/page/layout/layout.tpl"}
{block name="main"}
{require name="photo:static/index/index.css"}
{require name="photo:static/index/index.js"}
<h3>demo 1</h3>
<button id="btn">Button</button> {script type="text/javascript"} // 同步调用 jquery var $ = require('common:widget/jquery/jquery.js'); $('#btn').click(function() { // 异步调用 respClick 模块 require.async(['/widget/ui/respClick/respClick.js'], function() { respClick.hello(); }); }); {/script} // 调用 renderBox 模块 {widget name="photo:widget/renderBox/renderBox.tpl"} {/block}
第一处代码是对非模块化资源的调用方式;第二处是用 require 的方式调用一个 JavaScript 模块;第三处是通过 require.async 通过异步的方式来调用一个 JavaScript 模块;最后一处是通过 widget 语法来调用一个模板模块。 respclick 模块的源码如下:
exports.hello = function() { alert('hello world'); };
renderBox 模板模块的目录结构如下:
└── widget
└── renderBox
├── renderBox.css
├── renderBox.js
├── renderBox.tpl
└── shell.jpeg
虽然 renderBox 下面包括 renderBox.js、renderBox.js、renderBox.tpl 等多种模块,我们再调用的时候只需要一行代码就可以了,并不需要关注内部的依赖,以及各种模块的初始化问题。
fis开源项目前端工程模块化: http://fex.baidu.com/blog/2014/03/fis-module/