seajs+spm之再研究

好久没有用seajs了,之前对spm也只是一知半解,这些天再次拿起来研究.谈谈我的认识与理解.

声明:本文不适合对seajs完全不了解的同学阅读.对于想知道seajs来龙去脉以及spm相关的同学”可能”有帮助.对于我自己也是个梳理的机会.

一.seajs部分

1.seajs由来:

传统web前端的js开发,主要基于script标签的引入,一个文件一个script标签,或者对他们进行简单的压缩与合并,以减少http请求.

没错,我们以前都是这么干的,甚至现在还有很多人这么干.

随着这些年的发展,前端越来越被重视,逻辑越来越复杂,前端代码的维护变得越来越难.

2009年,nodejs诞生(nodejs是什么东西,可以自行google),其模块化的编程思想,给攻城师牛人们(包括但不限于前端攻城师)带来了很大的启发.特别是它的模块化依赖管理系统.工程师们想把这个依赖管理机制移植到浏览器端.但是浏览器端目前还没有高度统一的规范,更别说依赖管理机制了.于是工程师们绞尽脑汁,日思夜想.

不久后,requireJS诞生了,也的确火了,但它所坚持的原则规范并不被前面那些的大部分工程师们接受.

这时,我国的一位前端大牛也逐渐参与到模块化开发的研究中来.经过不断的虚心学习,借鉴,摸索,seaJS诞生了.他的作者是玉伯(–仰视,尊敬).

以上历史介绍,只是我简单的整理与理解.详细的可以参考玉伯本人的描述https://github.com/seajs/seajs/issues/588

2.seajs是什么,能做什么

它是模块加载器(也即js文件加载器),所有模块都可以通过它加载,而且很重要的是,它可以帮你很好的管理模块依赖.当你的页面上有十几二十个js文件的时候,用了seajs,你就会知道它有多么美妙.

3.怎么用

这里只讲个大概,教程随便google就一打.

所有的模块几乎(下面会讲为什么不是全部)都被define(function(require,exports,module){})这个全局函数包围,require参数用于表明被依赖模块,exports和module用于输出接口,让别的模块可以调用.对于开发者,按照它的规范写代码,就能很好的管理依赖了.

现在来讲,上面那个”几乎”.其实也是后来看aralejs(aralejs是基于seajs的组件库,其开放程度可以用包罗万象来形容,具体可自行google)上的模块才发现的(只是个别模块这样做).

对于我们后来写成的模块,seajs官网是推荐上面那种方式的,而且也没有什么问题.之前随着requireJS的推出,一些开源的类库或者框架(比如jquery,backbone等)都纷纷加入了requireJS的行列,后续的新版本默认支持AMD模块规范.这些开源代码为了兼容页面上有无requireJS的情况,并没有将所有代码都包在define函数中,而是判断页面中是否定义了define函数进行模块定义,类似于下面这样

1
2
3
if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
    define( "jquery", [], function () { return jQuery; } );
}

而arale或者说seajs官方的做法是,可能为了兼容AMD,将其改成了

1
2
3
4
5
if ( typeof define === "function" ) {
define("jquery/jquery/1.7.2/jquery-debug", [], function () { return jQuery; } );
}

可能有其他更特殊的原因吧,还有json的模块更简单,直接在最后追加

1
2
3
4
5
define("gallery/json/1.0.3/json-debug", [], function() {
return window.JSON;
});

而underscore和bockbone就是按照”标准”,在最外层包上define.不是特别清楚为什么有这么些个做法.可以再看看其他被包装过的模块的都是什么样子的.

4.seajs的原理

这个可以参考我之前的文章(差不多一年前了囧,现在才来写这篇) : http://www.cnblogs.com/webstone/articles/3043119.html

5.引出下一节

其实说到这里,seajs以及它的模块都可以在浏览器里正常运行.那么为什么还要有spm呢? 且继续往下看

二.spm部分

1.spm由来:

各个模块开发测试完毕后,为了进一步提升页面性能,我们还要对代码进行压缩,合并操作.

于是问题产生了.

为什么压缩后,被依赖的模块不加载了?

为什么合并后的模块一个都不执行了?(新版本不执行任何操作,1.3.1及以前会只执行第一个模块)

对于第一个问题,这涉及到seajs模块依赖处理的原理,对于第一节中谈到的模块组织方式define(factory),seajs寻找依赖是依靠正则判断factory.toString()中的require关键字,如下代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g;
...
function parseDependencies(code) {
var ret = []
code.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if (m2) {
ret.push(m2)
}
})
return ret
}
// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}

而代码经过压缩后,require就不见了,自然就找不到依赖了.还有就是,这种依赖分析很消耗性能.

第二个问题,涉及到模块标识问题.其实上述的模块组织方式称为”匿名模块”.多个没有名字的模块合并到一起后,各自都返回了输出接口,那么对于这个”大模块”,应该返回哪一个接口呢?所以理想情况下每个模块都需要一个模块标识,这样就可以任何合并了.

其实seajs支持两种规范:

第一种就是第一节中的那种,叫做CMD规范,即define(factory)只传入一个callback.

第二种是给define传入三个参数define(id, deps, factory),id为模块标识,reps为依赖模块数组,第三个factory同上面那个factory.

这种模块属于Modules/Transport 规范 ,参考https://github.com/seajs/seajs/issues/242

第二种方式下,seajs的工作原理略有不同.它不再对factory进行正则依赖分析,直接加载deps中的依赖模块(数组中的字符串即模块标识,要与被依赖模块的id相匹配,或者与seajs.config别名中的映射后相匹配),这样一来,依赖处理的速度更快了,同时所有模块均可以任意压缩合并.

另外,路径与模块标识的问题可以参考这篇文章: https://github.com/seajs/seajs/issues/930

所以呢,为了使CMD模块(也即匿名模块),更加便于管理,包括模块的压缩,合并,打包,部署,甚至公用下载安装,spm就应运而生了.

2.spm是什么,能做什么

spm是CMD模块管理器,就像npm,其实spm本身就是nodejs的一个模块,负责CMD模块的打包发布删除等操作.当然对外输出的模块已经不是CMD模块了,是Modules/Transport 模块.

接着要说的是spm的源,就像npm有个代码集中管理的地方,甚至svn的代码仓库也有些类似.

当然,这个源可以是官方默认的https://spmjs.org/ ,也可以是私有源,搭建方法参考:https://github.com/spmjs/spm-yuan,然后需要通过修改配置文件中的soucre值来将spm的源地址改为私有源(默认为官方)

然后我们就可以开发我们的模块了.个人认为,最终生成的模块不一定要发布到源上.具有公共特性的组件类的模块才需要放到源上,业务模块放到项目中就好.只是build出来的id会看起来比较怪异,因为必须要与加载的路径一致.

3.spm的主要功能

nodejs和spm及其插件的安装这里就不说了,也是google一大把

我们从使用的角度,按开发顺序走下来

A.如果是开发一个公用组件,将来是要上传到spm源的模块,可以这么干(这种情况,除了源配置,我们暂时使用默认的配置信息):

1)spm init

打开终端,进入你将要开发组件的那个目录,执行

1
spm init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{{newModuleName}}/ //这一层是你执行spm init前创建的
src/
{{newModuleName}}.js
{{newModuleName}}.css
examples/
index.md
tests/
{{newModuleName}}-spec.js
dist/ //这一层现在不会有,后面build完会出现,输出的文件在这一层下
README.md
HISTORY.md
package.json

然后你就可以在src下的js和css里开发组件了,这两个文件初始化了一小部分代码,在src下你可以自由创建文件夹与其他js,css甚至模板文件等,用于被依赖

2)spm build

组件开发完成后,在终端下,同样是在上一步的目录下,执行

1
spm build

然后,就生成了上一步看到的dist目录,下面有打包好的具名模块,其中如果有依赖,默认是会被打包进来的

3)接着开始测试模块

官网说,spm test以及spm totoro都是支付宝内部才能使用的插件,不推荐外部使用,我还没有深入研究过.

所以呢,我们只能暂时自己创建测试环境,不过也不太麻烦啦

4)spm publish

测试通过后,我们就可以把它发布到源上了,就像nodejs模块可以通过npm发布一样.

在终端,同样的目录下执行

1
spm login

按照提示,登陆你的源账号,如果没有账号则按照提示创建账号登陆

接着执行

1
spm publish

如果没有意外,就可以发布成功了

打开浏览器,访问你的源地址,就能看到你刚刚发布的模块了

其他同学就可以通过spm install /安装(即下载)你发布的模块了

B.如果是开发业务模块,按照我个人的观点,业务模块不需要被发布到源上,或者说不需要和公用组件放在一块

这里我想起一个误区,而且我之前就一直是这么认为的:

“源上的模块是可以被http访问到的”

走出误区:简单讲,源只是一个存储模块文件的地方,不提供模块的web服务.于是就有了”部署”这件事情.但是spm deploy也是支付宝内部插件,我也没深入研究.

通常大型项目,静态资源文件与应用服务是分离的,也即模块文件是存放在另一台服务器上的.

但是小型项目,放在一起也基本能够满足需求.

但好像没有看到有人将公用模块与业务模块分离的.经过个人简单试验,这样是行的通的,并打算应用在项目中.

之前一直在探索如何使用spm构建业务模块,但总找不到合适的资料,尤其是它要求的目录结构以及生成的id让我束手无策.后来看到有大牛使用grunt-cmd-transport构建模块,自己跟着试了一下,的确可行.但似乎有些问题,比如:依赖的css没有被打包进来.在咨询了官方的人员后,并没有给出我这些问题的答案,而是建议我用spm,并给出了一些方案.我又重新拿起spm开始尝试.终于研究出了可行的方案,相对公用组件的开发方式,只需要修改package.json中的一些配置即可,而grunt的配置实在太多,看的人眼花.再次感谢支付宝贯高(github @popomore)给我的建议

由于需要比较多的笔墨,我将在下一篇讲述如何使用spm构建业务模块.

–从cnblogs搬来,写于2014-04-02 10:44

关于IE文档模式

如果不设置DOCTYPE,IE会默认进入怪异模式

1
<input type=”hidden” value=”xxx” name=”action”/>
1
document.getElementById('action');

此句代码是可以生效的!即给document.getElementById传入name参数,查找元素

如果不设置DOCTYPE,设置了<META http-equiv=”X-UA-Compatible” content=”IE=edge” />的话,
文档模式会按照指定的变化

IE下有这种用法

1
form.elements('action1').value = 'xxxx';

其中form中有id或name为action1的input元素,即设置该input元素的值(若同时存在id和name为action1则value为undefined)

为干掉IE67举杯!!
用这个

1
<meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1” />

还有这个

1
2
3
if($.browser.msie && ($.browser.version < 8 || document.documentMode < 8)){
alert(‘get out!’)
}

seajs初探

出自大名鼎鼎玉伯之手的seajs在业界引起了广泛的反响,好评如潮,如今我也加入到了其中.

到今天在项目中使用seajs也有几个月了,不久前开始正式研究seajs源码,终于准备发这篇博客了,也是我在博客园的第一篇博客(^__^) .怪鸡冻的.那么接下来进入正题.

一.seajs初步理解(seajs内部执行流程)

1).use或data-main(用户手动)

2).rquest(动态插入script标签)
3).扫描模块中的factory.toString(),根据一个很长的正则去查找use进来的模块中rquire的所有模块(即require括号里的那个字符串,也即依赖模块)
4).将第三步中查找出来的模块再进行2、3步操作(如果有),直到所有模块(包括所有依赖模块)都加载完毕,在此期间已经做好了依赖分析
5).至此所有模块均已就绪,按照之前分析好的依赖关系,按顺序执行各模块中define方法中传入的factory函数
小结:
1).发现源码中对路径的处理花了很多笔墨,尚需研究

2).正因为动态插入script标签不会产生阻塞,即可以并行下载模块,通过依赖分析,保证了模块的顺序执行,最终提高了页面性能,当然模块少的时候显示不出其效用(甚至慢了),但是模块越多其作用越明显。

3).引用拔赤的话:seajs提供“方法和思路”,而jquery、yui等则提供工具。seajs需要“学习”,而jquery更多的则是需要“查阅”
二.再理解

当seajs加载完一个模块时,会立即扫描factory.toString(),这件事如何触发的呢,通过阅读源码发现是利用了节点的onload和onerror事件,

1
2
3
4
5
6
node.onload = node.onerror = node.onreadystatechange = function() {
if (READY_STATE_RE.test(node.readyState)) {
//...do something
callback()//扫描
}
}

其中老的webkit和firefox还有一个问题:当加载css失败,不会触发onerror事件,seajs通过设置定时器不断访问node.sheet判断加载是否完成(包括失败的情况)
三.路径研究

发现它考虑到了base标签的问题,其中有一句代码

1
2
3
baseElement?
head.insertBefore(node, baseElement) :
head.appendChild(node)

根据它的意思,应该是要避开base影响seajs本身的基础路径,但是我经过测试(不使用seajs),通过js动态在base标签前插入script标签,这个新的script的src还是会带上base标签指定的路径,后用seajs测试,发现script节点的src值是绝对路径(http开头的那种),并以此为前缀动态插入script标签引进新的模块,而绝对路径是不受base标签影响的.那么它考虑base标签是不是有些多余,这个不得而知.准备去社区问问看.

今天又针对路径的问题进行了一番测试,有这么几种使用方式:

  1. seajs的data-main
  2. seajs.use
  3. define中的require
    不过它们的参数,也就是模块路径,原理是一致的.上图,大家一看就都明白了

下面是从官网摘来的片段:

———————————————我是万恶的分割线———————————————————–

模块系统的基础路径即 base 的默认值,与 sea.js 的访问路径相关:
如果 sea.js 的访问路径是:
http://example.com/assets/sea.js

则 base 路径为:
http://example.com/assets/
除了相对和顶级标识之外的标识都是普通路径。普通路径的解析规则,和 HTML 代码中的<script src=”…”></script> 一样,会相对当前页面解析。

// 假设当前页面是 http://example.com/path/to/page/index.html

// 绝对路径是普通路径:require.resolve(‘http://cdn.com/js/a‘);
// => http://cdn.com/js/a.js

// 根路径是普通路径:require.resolve(‘/js/b’);
// => http://example.com/js/b.js

// use 中的相对路径始终是普通路径:seajs.use(‘./c’);
// => 加载的是 http://example.com/path/to/page/c.js

seajs.use(‘../d’);
// => 加载的是 http://example.com/path/to/d.js
提示:

小结:

  1. 顶级标识始终相对 base 基础路径解析。
  2. 绝对路径和根路径始终相对当前页面解析。
  3. require 和 require.async 中的相对路径相对当前模块路径来解析。
  4. seajs.use 中的相对路径始终相对当前页面来解析。

——————————————-我也是万恶的分割线—————————————————
下面是我自己的test:
101
其中,seajs可以是外网的,那么相应地base路径也是外网的,见下图
102

–从以前的cnblogs搬来,写于2013-04-27 12:07

seajs中define方法的id和dependencies参数

这篇其实算是补充上一篇。

事记:

昨天一同事(之前没用过seajs)突然要用seajs,我无意间听到,加上我最近又在研究它,便“自不量力”地跑上去跃跃欲试。

看到他已经敲出来的那些看似有点像的seajs代码,跟我之前的用法不尽相同,我就犯迷糊了。然后回来又自行研究了一下,才大概搞明白了怎么回事,也算帮同事搞定了seajs的引进。

下面进入正题:

我之前的用法是

html:

1
2
<script type=”text/javascript” src=”xx/js/sea.js” id=”seajsnode” data-main=”mymod”></script>
<!-- data-main指定入口文件 -->

mymod.js

1
2
3
4
define(function (require) {
//require('xxx')//引入别的模块
//业务代码
}

基本没有使用seajs.use方法

今天总结了一下seajs.use的使用

首先:页面上还是引入seajs

1
<script type=”text/javascript” src=”xx/js/sea.js” id=”seajsnode” ></script>

然后下面紧跟着调用seajs.use

1
2
3
4
5
6
<script>
seajs.use([ ‘seajs/jquery’,’seajs/tagcloud’, ‘seajs/imgAlter’], function ($, b, c) {
var v = $.browser.version;
//debugger;
});
</script>

第一个参数,即那个数组,是依赖的模块,各个模块的输出接口按顺序跟后面的callback形参一一对应。

依赖模块加载完毕后,执行callback。

其实上面的内容很多教程里都有。

但这里不得不提的是,如果这里的依赖模块里如果指定了id,比如

1
2
3
define(‘gallery/jquery/1.7.2/jquery’,[],function (require) {
//jquery code
}

而这个id跟模块路径又不一致,会导致无法获取输出接口。

经浏览器查看,引用依赖的jquery的路径是正确的,并且也的确加载到jquery模块了,但输出接口始终为null。

后来google到,开发阶段不推荐define的参数传入三个,只需给定后面的factory即可,发布时通过构建工具提取压缩模块,会自动加上idhe依赖数组(即第二个参数),这样seajs能够更快的定位本身这个模块和它依赖的模块。

顺便提一下,第二个参数,如果显示传入了,那么seajs就不会再通过正则去扫描factory.toString(),直接根据这个参数去加载依赖模块,如果为[]即表示无依赖。

–从cnblog搬来,写于2013-04-27 09:53