模块化是一种处理复杂系统分解成为更好的可管理模块的方式,它能够把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化将如何影响其它部分就会变得显而易见,系统的可维护性更加简单易得。javascript
前端开发领域(JavaScript、CSS、Template)并无为开发者们提供以一种简洁、有条理地的方式来管理模块的方法。CommonJS(致力于设计、规划并标准化 JavaScript API)的诞生开启了“ JavaScript 模块化的时代”。CommonJS 的模块提案为在服务器端的 JavaScript 模块化作出了很大的贡献,可是在浏览器下的 JavaScript 模块应用颇有限。随之而来又诞生了其它前端领域的模块化方案,像 requireJS、SeaJS 等,然而这些模块化方案并非十分适用 ,并无从根本上解决模块化的问题。css
前端开发相对其余语言来讲比较特殊,由于咱们实现一个页面功能老是须要 JavaScript、CSS 和 Template 三种语言相互组织才行,若是一个功能仅仅只有 JavaScript 实现了模块化,CSS 和 Template 仍是处于原始状态,那咱们调用这个功能的时候并不能彻底经过模块化的方式,那么这样的模块化方案并非完整的,因此咱们真正须要的是一种能够将 JavaScript、CSS 和 Template 同时都考虑进去的模块化方案,而非仅仅 JavaScript 模块化方案。html
主流的 JavaScript 模块化方案都使用“异步模块定义”的方式,这种方式给开发带来了极大的不便,全部的同步代码都须要修改成异步的方式,咱们是否能够在前端开发中使用“ CommonJS ”的方式,开发者可使用天然、容易理解的模块定义和调用方式,不须要关注模块是否异步,不须要改变开发者的开发行为。前端
不少主流的模块化解决方案经过 JavaScript 运行时来支持“匿名闭包”、“依赖分析”和“模块加载”等功能,例如“依赖分析”须要在 JavaScript 运行时经过正则匹配到模块的依赖关系,而后顺着依赖链(也就是顺着模块声明的依赖层层进入,直到没有依赖为止)把全部须要加载的模块按顺序一一加载完毕,当模块不少、依赖关系复杂的状况下会严重影响页面性能。java
传统的模块化方案更多的考虑是如何将代码进行拆分,可是当咱们部署上线的时候须要将静态资源进行合并(打包),这个时候会发现困难重重,每一个文件里只能有一个模块,由于模块使用的是“匿名定义”,通过一番研究,咱们会发现一些解决方案,不管是“ combo 插件”仍是“ flush 插件”,都须要咱们修改模块化调用的代码,这无疑是雪上加霜,开发者不只仅须要在本地开发关注模块化的拆分,在调用的时候还须要关注在一个请求里面加载哪些模块比较合适,模块化的初衷是为了提升开发效率、下降维护成本,但咱们发现这样的模块化方案实际上并无下降维护成本,某种程度上来讲使得整个项目更加复杂了。node
写到这里,其实咱们的“前端工程之块化”才正式开始,本文面向对前端模块化开发有所实践或有所研究的同窗,接下来咱们所介绍的前端模块化解决方案, 有别于 JavaScript 模块化方案或 CSS 模块化方案,它是一种能够综合处理前端各类资源的模块化方案;它能够极大提高开发者的开发体验,并为性能优化提供良好的支持。下面让咱们来进一步来了解什么是“一体化”的模块化实践方案。jquery
首先咱们来看一下一个 web 项目是如何经过“一体化”的模块化方案来划分目录结构:web
前端模块(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/ 目录shell
├── 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 代码放到一个 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 用于处理首屏外的按需模块。
在模板模块中以及 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 等多种模块,咱们再调用的时候只须要一行代码就能够了,并不须要关注内部的依赖,以及各类模块的初始化问题。
为了实现一种天然、便捷、高性能、一体化的模块化方案,咱们须要解决如下一些问题,
** 使用编译工具来管理模块 **
咱们能够经过编译工具(自动化工具) 对模块进行编译处理,包括对静态资源进行预处理(对 JavaScript 模块添加闭包、对 CSS 进行 LESS 预处理等)、记录每一个静态资源的部署路径以及依赖关系并生成资源表(resource map)。咱们能够经过编译工具来托管全部的静态资源,这样能够帮咱们解决模块静态资源管理、模块依赖关系、模块沙箱问题。
** 使用静态资源加载框架来加载模块 **
那么如何解决模块加载问题,咱们能够经过静态资源加载框架来解决,主要包含前端模块加载框架,用于 JavaScript 模块化支持,控制资源的异步加载。后端模块化框架,用于解决 JavaScript 同步加载、CSS 和模板等模块资源的加载,静态资源加载框架能够用于对页面进行持续的自适应的前端性能优化,自动对页面的不一样状况投递不一样的资源加载方案,帮助开发者管理静态资源,抹平本地开发到部署上线的性能沟壑。 编译工具和静态资源加载框架的流程图以下:
自动化工具会扫描目录下的模块进行编译处理并输出产出文件:
静态资源,通过编译处理过的 JavaScript、CSS、Image 等文件,部署在 CDN 服务器自动添加闭包,咱们但愿工程师在开发 JavaScript 模块的时候不须要关心” define ”闭包的事情,因此采用工具自动帮工程师添加闭包支持,例如如上定义的 nav.js 模块在通过自动化工具处理后变成以下,
define('common:widget/nav/nav.js', function( require, exports, module ) { // common/widget/nav/nav.js var $ = require('common:widget/jquery/jquery.js'); exports.init = function() { ... }; });
模板文件,通过编译处理过的 smarty 文件,自动部署在模板服务器
资源表,记录每一个静态资源的部署路径以及依赖关系,用于静态资源加载框架 静态资源加载框架(SR Management System)会加载 source maps 拿到页面所须要的全部模块以及静态资源的 url,而后组织资源输出最终页面。
下面咱们会详细讲解如何加载模块,以下所示,
在流程开始前咱们须要准备两个数据结构:
加载资源表(resource map):
javascript { "res": { "A/A.tpl": { "uri": "/templates/A.tpl", "deps": ["A/A.css"] }, "A/A.css": { "uri": "/static/css/A_7defa41.css" }, "B/B.tpl": { "uri": "/templates/B.tpl", "deps": ["B/B.css"] }, "B/B.css": { "uri": "/static/css/B_33c5143.css" }, "C/C.tpl": { "uri": "/templates/C.tpl", "deps": ["C/C.css"] }, "C/C.css": { "uri": "/static/css/C_6a59c31.css" } } }
```javascript urls = [ ‘/static/css/A_7defa41.css’ ];
has = { “A/A.css”: true } ```
依次执行 {widget name=”B”}、{widget name=”c”},步骤与上述步骤 3 相同,获得,
```javascript urls = [ ‘/static/css/A_7defa41.css’, ‘/static/css/B_33c5143.css’, ‘/static/css/C_6a59c31.css’ ];
has = { “A/A.css”: true, “B/B.css”: true, “C/C.css”: true }
```
在要输出的 html 前面,咱们读取 uris 数组的数据,生成静态资源外链,咱们获得最终的 html 结果:
```html
``` 上面讲的是对模板和 CSS 资源的加载,用于描述静态资源加载的流程,下面咱们再来详细讲解下对于 JavaScript 模块的处理,要想在前端实现相似“ commonJS ”同样的模块化开发体验须要前端模块化框架和后端模块化框架一块儿做用来实现,
前端模块化框架,原理上你们能够选择使用 requireJS 或 SeaJS 来做为模块化支持,可是咱们并不建议这么作,咱们建议你们使用一个 mininal AMD API,例如 requireJS 的 almond 版本或者其余的精简版本,requireJS 完整版有 2000 余行,而精简版模块化框架只须要 100 行代码左右就能够实现,只须要实现如下功能:
Resource map 前端支持,主要用于为异步模块调用提供 uri 支持,resourceMap 为静态资源管理系统自动生成,无需人工调用,用于查询一个异步模块的真正 url,用于自动处理异步模块的 CDN、资源打包合并、强缓存问题,格式以下,
javascript require.resourceMap({ "res": { "common:widget/sidebar/sidebar.async.js": { "url": "/static/common/widget/sidebar/sidebar.async_449e169.js" } } });
处理循环引用,参照 nodeJS 处理循环引用的方式,在形成循环依赖的 require 以前把须要的东西 exports 出去,例如
```javascript // a.js console.log(‘a string’); exports.done = false; var b = require(‘./b.js’); console.log(‘in a, b.done = ‘ + b.done); exorts.done = true; console.log(‘b done’);
// b.js console.log(‘b starting’); exports.done = false;
var a = require(‘./a.js’); console.log(‘in b, a.done = ‘ + a.done); exports.done = true; console.log(‘b done’);
// main.js console.log(‘main starting’); var a = require(‘./a.js’); var b = require(‘./b.js’); console.log(‘in main. a.done = ‘ + a.done + ‘, b.done = ‘ + b.done); ```
若是在加载 a 的过程当中,有其余的代码(假设为 b)require a.js 的话,那么 b 能够从 cache 中直接取到 a 的 module,从而不会引发重复加载的死循环。但带来的代价就是在 load 过程当中,b 看到的是不完整的 a。
后端模块加载框架,主要用于处理模块的依赖并生成模块静态资源外链,下面咱们将以实例讲解静态资源管理系统是如何对 JavaScript 模块进行加载的,以下咱们有一个 sidebar 模块,目录下有以下资源
├── sidebar.async.js
├── sidebar.css
├── sidebar.js
└── sidebar.tpl
sidebar.tpl 中的内容以下,
<a id="btn-navbar" class="btn-navbar"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </a> {script} $('a.btn-navbar').click(function() { require.async('./sidebar.async.js', function( sidebar ) { sidebar.run(); }); }); {/script}
对项目编译后,自动化工具会分析模块的依赖关系,并生成 map.json,以下
"common:widget/sidebar/sidebar.tpl": { "uri": "common/widget/sidebsr/sidebar.tpl", "type": "tpl", "extras": { "async": [ "common:widget/sidebar/sidebar.async.js" ] }, "deps": [ "common:widget/sidebar/sidebar.js", "common:widget/sidebar/sidebar.css" ] }
在 sidebar 模块被调用后,静态资源管理系统经过查询 map.json 能够得知,当前 sidebar 模块同步依赖 sidebar.js、sidebar.css,异步依赖 sdebar.async.js,在要输出的 html 前面,咱们读取 uris 数组的数据,生成静态资源外链,咱们获得最终的 html
<script type="text/javascript"> require.resourceMap({ "res": { "common:widget/sidebar/sidebar.async.js": { "url": "/satic/common/widget/sidebar/sidebar.async_449e169.js" } } }); </script> <script type="text/javascript" src="/static/common/widget/sidebar/sidebar_$12cd4.js"></script>
如上可见,后端模块化框架将同步模块的 script url 统一辈子成到页面底部,将 css url 统一辈子成在 head 中,对于异步模块(require.async)注册 resourceMap 代码,框架会经过{script}标签收集到页面全部 script,统一管理并按顺序输出 script 到相应位置。
如今,当咱们想对模块进行打包,该如何处理呢,咱们首先使用一个 pack 配置项(下面是 fis 的打包配置项),对网站的静态资源进行打包,配置文件大体为,
fis.config.merge({ pack: { 'pkg/aio.css': '**.css' } });
咱们编译项目看一下产出的 map.json(resource map),有何变化,
{ "res": { "A/A.tpl": { "uri": "/template/A.tpl", "deps": ["A/A.css"] }, "A/A.css": { "uri": "/static/csss/A_7defa41.css", "pkg": "p0" }, "B/B.tpl": { "uri": "/template/B.tpl", "deps": ["B/B.css"] }, "B/B.css": { "uri": "/static/csss/B_33c5143.css", "pkg": "p0" }, "C/C.tpl": { "uri": "/template/C.tpl", "deps": ["C/C.css"] }, "C/C.css": { "uri": "/static/csss/C_ba59c31.css", "pkg": "p0" }, }, "pkg": { "p0": { "uri": "/static/pkg/aio_0cb4a19.css", "has": ["A/A.css", "B/B.css", "C/C.css"] } } }
你们注意到了么,表里多了一张 pkg 表,全部被打包的资源会有一个 pkg 属性 指向该表中的资源,而这个资源,正是咱们配置的打包策略。这样静态资源管理系统在表中查找 id 为 A/A.css 的资源,咱们发现该资源有 pkg 属性,代表它被备份在了一个打包文件中。
咱们使用它的 pkg 属性值 p0 做为 key,在 pkg 表里读取信息,取的这个包的资源路径为 /static/pkg/aio0cb4a19.css_ 存入 uris 数组 中将 p0 包的 has 属性所声明的资源加入到 has 表,在要输出的 html 前面,咱们读取 uris 数组 的数据,生成静态资源外链,咱们获得最终的 html 结果:
<html> <link href="/static/pkg/aio_0cb4a19.css"> <div>html of A</div> <div>html of B</div> <div>html of C</div> </html>
静态资源管理系统能够十分灵活的适应各类性能优化场景,咱们还能够统计 {widget} 插件的调用状况,而后自动生成最优的打包配置,让网站能够自适应优化,这样工程师不用关心资源在哪,怎么来的,怎么没的,全部资源定位的事情,都交给静态资源管理系统就行了。静态资源路径都带 md5 戳,这个值只跟内容有关,静态资源服务器今后能够放心开启强缓存了!还能实现静态资源的分级发布,轻松回滚!咱们还能够继续研究,好比根据国际化、皮肤,终端等信息约定一种资源路径规范,当后端适配到特定地区、特定机型的访问时,静态资源管理系统帮你送达不一样的资源给不一样的用户。说到这里,你们应该比较清楚整个“一体化”的模块化解决方案了,有人可能会问,这样作岂不是增长了后端性能开销?对于这个问题,咱们实践过的经验是,这很是值得!其实这个后端开销不多,算法很是简单直白,但他所换来的前端工程化水平提升很是大!
本文是 fis 前端工程系列文章中的一部分,其实在前端开发工程管理领域还有不少细节值得探索和挖掘,提高前端团队生产力水平并非一句空话,它须要咱们能对前端开发及代码运行有更深入的认识,对性能优化原则有更细致的分析与研究。fis 团队一直致力于从架构而非经验的角度实现性能优化原则,解决前端工程师开发、调试、部署中遇到的工程问题,提供组件化框架,提升代码复用率,提供开发工具集,提高工程师的开发效率。在前端工业化开发的全部环节均有可节省的人力成本,这些成本很是可观,相信如今不少大型互联网公司也都有了这样的共识。