基于Seajs的项目构建 
CDC miyukizhang
使用SEA.JS做模块化开发的项目 
的构建过程
不包括: 
• 为什么使用模块化开发 
• Sea.js和其他同类项目的优劣对比 
• 各种规范的优劣对比
提纲 
• Sea.js模块标识 
• Sea.js模块加载规则 
• 项目结构 
• 构建过程 
• 项目实践
提纲 
• Sea.js模块标识 
• Sea.js模块加载规则 
• 项目结构 
• 构建过程 
• 项目实践
Sea.js的模块标识 
• 模块标识:字符串,标识模块 
– seajs.use("my-module"); 
– require("my-module"); 
• 3种模块标识: 
– 顶级标识 
– 相对标识 
– 普通路径
顶级标识 
• 不以../ , ./ , / 开头的标识 
• 相对于Sea.js base路径来解析
Sea.js base 
如果sea.js 的URL是: 
http://example.com/sea_modules/sea.js 
则base 为: 
http://example.com/sea_modules/ 
如果sea.js 的URL是: 
http://example.com/sea_modules/Sea.js/1.0.0/sea.js 
则base 为: 
http://example.com/sea_modules/
Sea.js base 
注意:这里设置的../dist,是相对于当前html页面所在路径来说的,而 
不是相对于sea.js所在路径
顶级标识 
假设Sea.js base: http://example.com/sea_modules/ 
http://example.com/sea_modules/gallery/jquery/1.10.1/jquery.js
相对标识 
• 以./ 或者../ 打头的 
• 相对于当前模块的路径来解析
相对标识 
path/to/b.js,path/c.js
普通路径 
• 三种形式: 
– http,file等开头的绝对路径 
– 以/ 开头的根路径 
– 传递给seajs.use()函数的相对路径 
• 将会根据当前html页面的位置来解析
提纲 
• Sea.js模块标识 
• Sea.js模块加载规则 
• 项目结构 
• 构建过程 
• 项目实践
模块加载规则 
假设Sea.js base: http://example.com/sea_modules/ 
文件路径:http://example.com/sea_modules/app/hello/1.1.0/hello.js
模块加载规则 
• 定义模块 
• 模块分类: 
– 具名模块 
– 匿名模块
I am not ID 
具名模块 
• 具名模块:定义了ID的模块I am ID
具名模块 
• 具名模块:定义了ID的模块 
• ID ∈ 模块标识,模块标识≠ ID 
• 当一个文件里有多个define时,ID用来判断主模块
具名模块 
• ID 和路径匹配原则 
文件路径: http://example.com/sea_modules/app/hello/1.1.0/hello.js
具名模块 
• ID 和路径匹配原则
匿名模块 
• 匿名模块:没定义ID的模块 
– 无需匹配 
– 文件中只能有一个define 块
为什么需要具名函数 
当seajs.use 这个文件时,应该返回哪个模块?
为什么需要具名函数 
和文件路径匹配的ID 的模块就是这个文件的主模块
提纲 
• Sea.js模块标识 
• Sea.js模块加载规则 
• 项目结构 
• 构建过程 
• 项目实践
Grunt 
Q:Grunt为何物? 
A:一个专为JavaScript提供的构建工具。 
Q:啥是构建工具? 
A:解决源文件压缩、合并、拷贝和提取业务 
代码依赖模块等复杂工作的自动化工具
项目结构
准备Grunt插件
Html
js文件 
main.js 
hello.js 
world.js
js文件
js文件
js文件
提纲 
• Sea.js模块标识 
• Sea.js模块加载规则 
• 项目结构 
• 构建过程 
• 项目实践
构建过程 
• 为什么要构建 
– 生产环境需要文件合并和压缩 
– 代码合并需要遵循“ID 和路径匹配原则”
构建过程 
• 提取操作:用来提取模块的标识id 和依 
赖dependencies 
• 压缩操作:包括文件合并和文件压缩
transport Grunt-cmd-transport
transport
transport 
提取后hello.js:
transport 
提取后main.js:
concat Grunt-cmd-concat
concat
concat seajs_hello/sea_modules/app/hello/1.0.0/hello.js
concat seajs_hello/sea_modules/app/hello/1.0.0/main.js
uglify和clean 
grunt-contrib-uglify,grunt-contrib-clean
构建之后的目录结构
源码 
click me
提纲 
• Sea.js模块标识 
• Sea.js模块加载规则 
• 项目结构 
• 构建过程 
• 项目实践
项目实践tracker.oa.com
目录结构及说明 
tracker_proj 
trunk release tags branches 
trunk: 项目源码 
release:部署代码,构建后代码
目录结构及说明 
statistics -- 项目页 
overview.html -- 概览 
product.html -- 产品详情 
css -- 样式文件 
img -- 图片文件 
sea_modules 
application -- 特定项目特定页面的业务层js都在这里 
jquery -- base模块,spm提供的已经以CMD格式封装好的通用库都在这里 
seajs -- seajs 
node_modules -- grunt及其所需插件。注1 
grunt -- grunt 
grunt-cmd-concat -- 依赖合并 
grunt-cmd-transport -- 提取依赖并设置模块ID 
grunt-contrib-clean -- 删除临时文件 
grunt-contrib-uglify -- 压缩 
Gruntfile.js -- grunt配置文件 
package.json -- 项目配置文件,该文件内容可以在grunt中引用 
rootConfig.js -- 开发环境下使用的seajs配置文件
目录结构及说明 
overview.js 
user_charts.js 
charts.js
模块application/statistics/1.1.0/charts.js
模块application/statistics/1.1.0/user_charts.js
模块application/statistics/1.1.0/overview.js
模块rootConfig.js
模块statistics/overview.html
构建 
1. package.json 
2. Gruntfile.js 
1. transport任务 
2. concat任务 
3. uglify任务 
4. clean任务
感受 
• 文件职责明确 
• 代码清晰 
• 减少重复开发 
• 自动化部署节省开发时间
End 
Thanks

基于Seajs的项目构建

Editor's Notes

  • #3 今天我从一个小的例子helloworld开始给大家介绍一下基于Sea.js做模块化开发项目完整的构建过程。然后会结合自身的项目实践说一下我自己的感受。 整个构建过程覆盖的面会比较多,我不可能全部介绍。所以我选出我认为最重要或者坑比较多的几点来说,另求把这几个点都给大家说明白。
  • #4 Grunt和Sea.js想必大家都并不陌生,有意或无意也听到过它们的介绍和价值,可能也有使用过它们。所以这里我不会花时间介绍这些。
  • #5 我会按照下面的提纲介绍今天的内容
  • #7 模块标识是一个字符串,用来标识模块。在seajs.use, require等加载函数中,第一个参数都是模块标识。 Sea.js中有三种模块标识方式
  • #8 我们先来看一个例子 那么像这种不以../,./,/开头的标识,就是顶级标识,它将相对于seajs base路径来解析 那么seajs base又是什么?
  • #9 Sea.js base是sea.js文件本身加载的路径 当项目较大,多人合作的时候,一个js文件往往有多个版本,这个时候,我们需要用版本号管理文件。 当 sea.js 的访问路径中含有版本号时, base 不会包含 Sea.js/x.y.z 字串。 当 sea.js 有多个版本时,这样尤其会很方便。
  • #10 当然,也可以手工配置 base 路径:
  • #11 回到刚才的例子,我们需要加载jquery的这个模块,那么这个模块的文件地址到底在哪里咧? 假设…,那么我们得到这个模块对应的文件路径为…
  • #12 刚才我们介绍模块有三种标识: 顶级标识 相对标识 普通路径 现在来看看相对标识,同样先看一个例子,相对的概念大家都应该很清楚,引用文件时经常用到相对路径,这里的相对标识其实也差不多。 以./或者../打头的是相对标识,这种标识是相对于当前模块的路径来解析的
  • #13 比如上面的例子,解析得到的路径分别是 实际上,相对标识是很方便的,在项目中推荐使用相对标识来相互引用模块
  • #14 第三种模块标识就是普通路径 除了相对和顶级标识之外的标识都是普通路径。例子中展示了它的三种形式: 普通路径的解析规则,和 HTML 代码中的 <script src="..."></script> 一样,会相对当前页面解析。 实际上,我感觉普通路径里唯一有点作用的只有seajs.use(),而且作用也不是特别大,因为可以用顶级标识代替。 绝对路径和根路径用得就更少了。 所以,不太需要关注普通路径。一般顶级标识和相对标识就已经够用了
  • #16 我们了解了模块标识,下面大家讲一下模块加载过程 使用 seajs.use 或 require 进行模块的加载,举例加载 app/hello/1.1.0/hello这个模块,这是个顶级标识,所以我们根据Sea.js的base路径来解析, 假设Sea.js base为… 则该模块对应的文件为…
  • #17 现在我们根据路径解析规则,知道这个模块就在这个文件中,在这个文件中我们需要做些什么咧? 首先是定义模块,而模块又分为两种:具名和匿名,
  • #18 我们用define函数来定义模块 具名模块(即定义了 ID 的模块)。 我们用define函数来定义模块,第一个参数即模块ID,即定义了第一个参数的就是具名模块。 刚才在加载模块的时候我们见到了类似的模块标识,但是这个参数被规定为文件路径(而不是 ID),这样的设计减轻了记忆模块 ID 的负担,无论是匿名模块还是具名模块,开发者只需要知道文件放在哪儿就行了。
  • #20 具名函数中有个非常重要的概念,ID和路径匹配原则,使用 seajs.use 或 require 进行文件引用时,如果是具名模块,会把 ID 和 use或 require 的路径名进行匹配,如果一致,则正确执行模块返回结果 只有在这种情况下,我们才能正常地执行模块
  • #21 具名模块中,如果ID和路径不匹配,则返回 null。
  • #22 和具名函数相对的,没有定义… 在 CMD 的书写规范中,一个文件对应一个模块,所有的模块都是匿名模块。那么当 seajs.use 某模块时,这个模块对应的文件里的唯一的 define 方法理所当然的是这个模块的执行代码。
  • #23 在生产环境下,静态文件不可避免地需要进行合并打包,以优化请求数提高页面性能。这时,一个 js 文件可能有很多 define() 方法。
  • #24 所以我们这个时候我们需要具名模块 我们定义好每个模块的 id ,在 Sea.js 里,那个和文件路径匹配的 ID 的模块就是这个文件的主模块。 这个原则保证了我们能够自由合并模块来优化性能。
  • #25 下面提供一个使用Grunt构建Sea.js项目的完整例子 先看看项目结构
  • #26 在项目部署上线前,通常要将源文件压缩,合并,并拷贝到bch或trunk中。 在将js模块化后,又多了一个分析,提取业务代码中所依赖模块的工作。 解决这一系列繁重工作的自动化工具,称之为构建工具。 就好像一个万能工厂(grunt),只负责执行任务(Task),不关心每个任务到底都干了什么
  • #27 app下放的是html文件 sea_modules 是所有的最终的js文件 其中 app 是项目特定页面的业务层js,是构建后的js最终文件 gallery spm提供的已经以CMD格式封装好的通用库都在这里 seajs 放seajs和其插件 static 项目特定页面的业务层js源码文件
  • #28 将需要的Grunt插件写在package.json里,用npm install命令下载,装在node_modules里。注意,node_modules不需要上传到Svn或GitHub,只需要上传package.json就可以了
  • #29 这个html作为入口,引入了sea.js,后面的id=“seajsnode",加上 seajsnode 值,可以让 sea.js 直接获取到自身路径,而不需要通过其他机制去自动获取。这对性能和稳定性会有一定提升,推荐默认都加上。 之后设置了base,因为默认的是seajs_hello/seajs_modules/seajs,不方便用top-level来引用其他模块,所以改成../sea_modules,注意这里的路径相对于html页面所在路径来说的 最后因为开发态和部署态,引的js文件不一样,开发环境下,加载src下的main.js作为入口,生产环境下加载app…作为入口(使用的是顶级标识)
  • #30 该项目共三个js文件,依赖关系为
  • #31 我们从最基本的模块world开始 开发阶段,一般每个文件对应一个模块,并使用匿名的方式定义模块 world这个模块主要是对外提供 add 方法
  • #32 在hello.js中首先用require函数来获取指定world模块的接口:add,这里加载模块使用的是相对标识 然后通过exports对外提供increment函数
  • #33 最后就是main.js就是获取hello模块的接口,并使用它
  • #34 现在,已经可以正常运行了
  • #35 下面开始一步步介绍构建的详细过程
  • #36 虽然现在项目已经能够正常运行了。但是生产环境一般都需要合并和压缩js文件来提高性能,而直接压缩的话,一个js文件会有多个define模块,Sea.js无法识别用户到底想加载哪个模块,所以需要先提取module_id,变成具名模块。 而且由于前面说过的Sea.js的“ID和路径一致性”的要求,传给require()的模块标识,不仅要能找到对应的js文件,而且模块ID还要相同才可以。 因此,需要用配套的构建工具来构建,压缩后的js才能被Sea.js正常解析和加载
  • #37 CMD 模块在构建时,有两个基本操作: 经过上面的提取操作后,构建工具就可以调用任何 JS 压缩工具来进行压缩了 具体情况,后面的例子会想详细给出
  • #38 这是构建的第一步,用的是Grunt-cmd-transport插件,Gruntfile.js这部分内容如下: idleading参数,这决定了提取出来的module_id,要和我们最终的目录结构保持一致!
  • #39 执行Grunt transport以后,会把提取之后的文件放在临时目录.build里
  • #40 看下hello.js提取后的样子: 通过transport,我们提取出了模块的标识id和依赖。将匿名模块转化为了具名模块
  • #41 main.js同理,main.js依赖hello和world
  • #42 前面提取之后,接下来的动作是合并,把hello.js和world.js合并成hello.js,用到的插件是Grunt-cmd-concat。这里务必注意,合并后的名字不能改,还必须是hello.js,这也是因为前面提到的“ID与路径匹配原则”。 include参数是合并哪些依赖,可选值有self,relative,all。self只会合并自身,relative的含义是合并采用相对路径依赖的模块,all则是会合并依赖的依赖。self是默认值
  • #43 执行grunt-cmd-concat后
  • #45 我们可以发现,多个文件合并成一个之后,对外就只能暴露一个模块了(这里是hello,world只能内部引用)
  • #46 容易出错的部分已经结束了,剩下的就是压缩和清理临时目录,没什么特别可说的,用的是Grunt-contrib-uglify和Grunt-contrib-clean
  • #50 一个以js为主的项目
  • #53 首先,示例非常简单,就是展示数据分析的图表。由于这个项目中图表特别多,由于很多除了数据,都是可以重复使用的,那么我将各类图表封装成一个模块
  • #57 现在需要一个开发阶段seajs的配置文件rootConfig.js,并将其引入到页面中:
  • #58 接下来我们来编写展示的页面overview.html: 这时候运行刚刚完成的index.html,一切顺利的话,应该已经可以看到图表的效果了。
  • #59 这里我不详细介绍了,和前面helloworld的构建过程一样,我们回忆一下