模块化是一种处理繁杂系统分解作为更好的可管理模块的方式,它能够把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化将怎样影响其它部分就会变得显而易见,系统的可守护性更加简单易得。
前端研发行业(JavaScript、CSS、Template)并无为研发者们供给以一种简洁、有条理地的方式来管理模块的办法。CommonJS(致力于设计、规划并标准化 JavaScript API)的诞生开启了“ JavaScript 模块化的时代”。CommonJS 的模块提案为在服务器端的 JavaScript 模块化做出了很大的贡献,然则在浏览器下的 JavaScript 模块应用特别有限。随之而来又诞生了其它前端行业的模块化方法,像 requireJS、SeaJS 等,然而这些模块化方法并不是非常适用 ,并无从基本上处理模块化的问题。
前端模块化并不等于 JavaScript 模块化
前端研发相对其他语言来讲比较特殊,由于咱们实现一个页面功能总是必须 JavaScript、CSS 和 Template 三种语言相互组织才行,倘若一个功能仅仅仅有 JavaScript 实现了模块化,CSS 和 Template 还是处在原始状态,那咱们调用这个功能的时候并不可完全经过模块化的方式,那样这般的模块化方法并不是完整的,因此咱们真正必须的是一种能够将 JavaScript、CSS 和 Template 同期都思虑进去的模块化方法,而非仅仅 JavaScript 模块化方法。
JavaScript 模块化并不等于异步模块化
主流的 JavaScript 模块化方法都运用“异步模块定义”的方式,这种方式给研发带来了极重的不方便,所有的同步代码都必须修改为异步的方式,咱们是不是能够在前端研发中运用“ CommonJS ”的方式,研发者能够运用自然、容易理解的模块定义和调用方式,不必须关注模块是不是异步,不必须改变研发者的研发行径。
前端模块化带来的性能问题
非常多主流的模块化处理方法经过 JavaScript 运行时来支持“匿名闭包”、“依赖分析”和“模块加载”等功能,例如“依赖分析”必须在 JavaScript 运行时经过正则匹配到模块的依赖关系,而后顺着依赖链(亦便是顺着模块声明的依赖层层进入,直到无依赖为止)把所有必须加载的模块按次序一一加载完毕,当模块非常多、依赖关系繁杂的状况下会严重影响页面性能。
模块化为打包安排带来的极重不方便
传统的模块化方法更加多的思虑是怎样将代码进行拆分,然则当咱们安排上线的时候必须将静态资源进行合并(打包),这个时候会发掘困难重重,每一个文件里只能有一个模块,由于模块运用的是“匿名定义”,经过一番科研,咱们会发掘有些处理方法,没论是“ combo 插件”还是“ flush 插件”,都必须咱们修改模块化调用的代码,这没疑是雪上加霜,研发者不仅必须在本地研发关注模块化的拆分,在调用的时候还必须关注在一个请求里面加载那些模块比较合适,模块化的初衷是为了加强研发效率、降低守护成本,但咱们发掘这般的模块化方法实质上并无降低守护成本,某种程度上来讲使得全部项目更加繁杂了。
一体化的前端模块化实践方法
写到这儿,其实咱们的“前端工程之块化”才正式起始,本文面向对前端模块化研发有所实践或有所科研的朋友,接下来咱们所介绍的前端模块化处理方法, 有别于 JavaScript 模块化方法或 CSS 模块化方法,它是一种能够综合处理前端各样资源的模块化方法;它能够极重提高研发者的研发体验,并为性能优化供给良好的支持。下面让咱们来进一步来认识什么是“一体化”的模块化实践方法。
首要咱们来看一下一个 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.jsvar $ = 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}第1处代码是对非模块化资源的调用方式;第二处是用 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 等其他静态资源,必须记录与管理这些静态资源
模块依赖关系处理,模块间存在各样依赖关系,在加载模块的时候必须处理好这些依赖关系
模块加载,在模块初始化之前必须将模块的静态资源以及所依赖的模块加载并准备好
模块沙箱(模块闭包),在 JavaScript 模块中咱们必须自动对模块添加闭包用于处理功效域问题
** 运用编译工具来管理模块 **
咱们能够经过编译工具(自动化工具) 对模块进行编译处理,包含对静态资源进行预处理(对 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,而后组织资源输出最后页面。
静态资源加载框架
下面咱们会仔细讲解怎样加载模块,如下所示,
在流程起始前咱们必须准备两个数据结构:
uris = [],数组,次序存放要输出资源的 uri
has = {},hash 表,存放已收集的静态资源,防止重复加载
加载资源表(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" } } }
执行 {widget name=”A”}
```javascript urls = [ ‘/static/css/A_7defa41.css’ ];
has = { “A/A.css”: true } ```
在表中查询 id 为 A/A.tpl 的资源,取得它的资源路径 /template/A.tpl,记为 tpl_path,加载并渲染 tpl_path 所指向的模板文件,即 /template/A.tpl,并输出它的 html 内容
查看 A/A.tpl 资源的 deps 属性,发掘它依赖资源 A/A.css,在表中查询 id 为 A/A.css 的资源,取得它的资源路径为 /static/css/A7defa41.css_,存入 uris 数组 中,并在 has 表 里标记已加载 A/A.css 资源,咱们得到:
依次执行 {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
html of A
html of B
html of C
``` 上面讲的是对模板和 CSS 资源的加载,用于描述静态资源加载的流程,下面咱们再来仔细讲解下针对 JavaScript 模块的处理,想要在前端实现类似“ commonJS ”同样的模块化研发体验必须前端模块化框架和后端模块化框架一块功效来实现,
前端模块化框架,原理上大众能够选取运用 requireJS 或 SeaJS 来做为模块化支持,然则咱们并不意见这么做,咱们意见大众运用一个 mininal AMD API,例如 requireJS 的 almond 版本或其他的精简版本,requireJS 完整版有 2000 余行,而精简版模块化框架只必须 100 行代码上下就能够实现,只必须实现以下功能:
模块定义,只必须实现如下接口 define (id, factory),由于 define 闭包是工具生成,因此咱们不必须思虑匿名闭包的实现,同期亦不必须思虑“依赖前置”的支持,咱们只必须支持一种最简单直接的模块化定义就可
模块同步骤用,require (id),静态资源管理系统会保准所需的模块都已预先加载,因此呢 require 能够立即返回该模块
模块异步骤用,思虑到有些模块没需再起步时载入,因此呢咱们必须供给一个能够在运行时加载模块的接口 require.async (names, callback),names 能够是一个 id,或是数组形式的 id 列表。当所有都加载都完成时,callback 被调用,names 对应的模块实例将依次传入。
模块自执行,即 AMD 规范的提前执行,之所选取这般做的原由是思虑到 Template 模块的特殊性,通常 Template 模块都会依赖 JavaScript 模块来做初始化工作,选取模块自执行的方式咱们就不必须显式的在 Template 页面上书写 require 依赖,静态资源系统会自动加载 Template 模块的依赖,当模块并行加载结束后会一次自执行。大众可能会认为如果页面存在有些用不到的模块那都自执行岂不会浪费资源,这儿大众能够不消担心,静态资源系统投放到前端的模块都是页面初始化所必须的,不存在浪费资源的状况。
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.tplsidebar.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: {
p公斤/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",
"p公斤": "p0"
},
"B/B.tpl": {
"uri": "/template/B.tpl",
"deps": ["B/B.css"]
},
"B/B.css": {
"uri": "/static/csss/B_33c5143.css",
"p公斤": "p0"
},
"C/C.tpl": {
"uri": "/template/C.tpl",
"deps": ["C/C.css"]
},
"C/C.css": {
"uri": "/static/csss/C_ba59c31.css",
"p公斤": "p0"
},
},
"p公斤": {
"p0": {
"uri": "/static/p公斤/aio_0cb4a19.css",
"has": ["A/A.css", "B/B.css", "C/C.css"]
}
}}
大众重视到了么,表里多了一张 p公斤 表,所有被打包的资源会有一个 p公斤 属性 指向该表中的资源,而这个资源,正是咱们配置的打包策略。这般静态资源管理系统在表中查询 id 为 A/A.css 的资源,咱们发掘该资源有 p公斤 属性,显示它被备份在了一个打包文件中。
咱们运用它的 p公斤 属性值 p0 做为 key,在 p公斤 表里读取信息,取的这个包的资源路径为 /static/p公斤/aio0cb4a19.css_ 存入 uris 数组 中将 p0 包的 has 属性所声明的资源加入到 has 表,在要输出的 html 前面,咱们读取 uris 数组 的数据,生成静态资源外链,咱们得到最后的 html 结果: <html>
<link href="/static/p公斤/aio_0cb4a19.css">
<div>html of A</div>
<div>html of B</div>
<div>html of C</div></html>
静态资源管理系统能够非常灵活的适应各样性能优化场景,咱们还能够统计 {widget} 插件的调用状况,而后自动生成最优的打包配置,让网站能够自适应优化,这般工程师不消关心资源在哪,怎么来的,怎么没的,所有资源定位的事情,都交给静态资源管理系统就好了。静态资源路径都带 md5 戳,这个值只跟内容有关,静态资源服务器从此能够安心开启强缓存了!还能实现静态资源的分级发布,容易回滚!咱们还能够继续科研,例如按照国际化、皮肤,终端等信息约定一种资源路径规范,当后端适配到特定地区、特定机型的拜访时,静态资源管理系统帮你送达区别的资源给区别的用户。说到这儿,大众应该比较清楚全部“一体化”的模块化处理方法了,有人可能会问,这般做岂不是增多了后端性能开销?针对这个问题,咱们实践过的经验是,这非常值得!其实这个后端开销很少,算法非常简单直白,但他所换来的前端工程化水平加强非常大!
总结
本文是 fis 前端工程系列文案中的一部分,其实在前端研发工程管理行业还有非常多细节值得探索和挖掘,提高前端团队生产力水平并不是一句空话,它必须咱们能对前端研发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与科研。fis 团队始终致力于从架构而非经验的方向实现性能优化原则,处理前端工程师研发、调试、安排中遇到的工程问题,供给组件化框架,加强代码复用率,供给研发工具集,提高工程师的研发效率。在前端工业化研发的所有环节均有可节省的人力成本,这些成本非常可观,相信此刻非常多大型互联网机构亦都有了这般的共识。
本文只是将这个行业中很小的一部分知识的展开讨论,抛砖引玉,期盼能为业界关联行业的工作者供给有些不同样的思路。欢迎关注fis项目,对本文有任何意见或意见都能够在fis开源项目中进行反馈和讨论。
|