Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID 和路径匹配原则 #930

Closed
afc163 opened this issue Sep 4, 2013 · 45 comments
Closed

ID 和路径匹配原则 #930

afc163 opened this issue Sep 4, 2013 · 45 comments
Milestone

Comments

@afc163
Copy link
Member

afc163 commented Sep 4, 2013

ID 和路径匹配原则

首先声明这是个万人坑,不要怨恨苦恼,不要垂头丧气,一定要相信希望就在前方,未来一片光明!

经常收到 seajs.use 某具名模块时发现其引用为 null 的问题,或是移动了文件位置导致引用为 null 或者 object is not function 的问题。比如这个 #954 ,这个 #888 ,这个 #879 ,这个 #739 ,这个 #696 ,还有这个 seajs/examples#12

这些问题都指向 Sea.js 的一个基本约定原则:ID 和路径匹配原则

这是什么

所谓 ID 和路径匹配原则 是指,使用 seajs.userequire 进行引用的文件,如果是具名模块(即定义了 ID 的模块),会把 ID 和 seajs.use 的路径名进行匹配,如果一致,则正确执行模块返回结果。反之,则返回 null。例如:

seajs.use('lib/jquery', function($) {
    // use $
});

或者在模块中 require :

define(function(require, exports, module) {
    var $ = require('lib/jquery');
    // use $
});

当 jQuery 文件是下面的情况时,上述的变量 $ 能拿到正确的返回结果。

// 文件路径是 lib/jquery.js
// ID 和实际路径匹配了(.js 后缀会自动补上)
define('lib/jquery', function(require, exports, module) {
    // jquery code
});

下面的代码则返回 null:

// 文件路径是 lib/jquery.js
// 但是 ID 是 lib/jquery.min.js
// ID 和路径不匹配
define('lib/jquery.min', function(require, exports, module) {
    // jquery code
});

而匿名模块始终能正确返回结果:

// lib/jquery.js
// 匿名模块,不需要进行匹配
// 但是文件中只能有一个 define 块
define(function(require, exports, module) {
    // jquery code
});

注意这里用于匹配的 ID 都是经过 alias 和 path 解析并且补完后缀之后的。

为什么要有这个原则

回答这个问题前,请先阅读这篇文章:#426

首先,Sea.js 的模块启动接口秉承的是路径即 ID 的设计原则。seajs.use 的方法的第一个参数被规定为文件路径(而不是 ID),这样的设计减轻了记忆模块 ID 的负担,无论是匿名模块还是具名模块,开发者只需要知道文件放在哪儿就行了。

进一步的,之所以有这个 ID 和路径匹配原则,是因为在 CMD 的书写规范中,一个文件对应一个模块,所有的模块都是匿名模块(即 define(factory) 的形式)。那么当 seajs.use 某模块时,这个模块对应的文件里的唯一的 define 方法理所当然的是这个模块的执行代码,这时可以正确返回结果。

但是在生产环境下,静态文件不可避免地需要进行合并打包或者进行 combo,以优化请求数提高页面性能。这时,一个 js 文件可能有很多 define() 方法。

define(funtion(require, exports, module) {
    // module a
});

define(funtion(require, exports, module) {
    // module b
});

define(funtion(require, exports, module) {
    // module c
});

那么请问,当 seajs.use 这个文件时,应该返回哪个模块?

所以这时候 ID 就派上了用场,我们可以这样写:

// path/a.js

define('path/a', funtion(require, exports, module) {
    // module a
});

define('path/b', funtion(require, exports, module) {
    // module b
});

define('path/c', funtion(require, exports, module) {
    // module c
});

我们定义好每个模块的 id ,在 Sea.js 里,那个和文件路径匹配的 ID 的模块就是这个文件的主模块。此时:

seajs.use('path/a', function(a) {
    // got a, not b or c
});

这个原则保证了我们能够自由合并模块来优化性能,seajs-combospm-build 的构建机制都是基于此原则。

在 RequireJS 中,也有类似的原则:http://requirejs.org/docs/errors.html#mismatch

更深一步

可能有人要问为啥一定要把 ID 定为文件路径,Sea.js 不是可以自定义 ID 吗,像下面这样:

define('module-id', funtion(require, exports, module) {
    // module id
});

// 然后就可以
seajs.use('module-id', function(Module) {
    // Module
});

上面的代码当然可以运行。但是有一点,任何一个模块的运行都涉及到两个步骤:模块定义模块执行,上面的代码两个步骤都包括在内。而使用了 Sea.js ,我们不希望用户去手动写 script 标签引用模块。希望只需要 seajs.use 模块的文件路径即可(入口唯一):

seajs.use('path/to/module', function(Module) {
    // Module
});

Sea.js 会自动插入 script 标签,完成定义步骤,然后执行模块,拿到模块的输出。所以当一个文件里有多个 define 时,只能用 ID 是否匹配 use 中的路径来判断是否主模块。

当然可以回避掉这个原则,你只需要自己负责模块的定义部分,再自己 seajs.use 之前定义好的模块 ID 就行。

<!-- 各种模块的定义 define define define -->
<script src="http://example.com/modules.js"></script>

<script>
// 这时 use 的第一个参数就可以不必是文件路径了,因为已经有定义好的模块 ID 了
seajs.use('jquery', function($) {
    // $
});
</script>

或者通过 alias 来帮助 ID 匹配上最终的路径,这样就和 RequireJS 的方案基本一致了。

  // lib/jquery-1.7.2.js 的内容如下
define('$', funtion(require, exports, module) {
  // jQuery
});

这样就不需要自己去引用上面的文件,可以直接通过 seajs.use 调用。

seajs.config({
  alias: {
    $: 'lib/jquery-1.7.2.js'
  }
});

seajs.use('$', function() {
  // Got $ !
});

使用 spm-build 和 grunt 进行打包

我们推荐使用配套的构建工具来打包模块。

在 spm-build 中,所有的匿名模块通过标准的 transport 流程,会打包成具有实际 ID 的具名模块,而主模块(在 package.json 中指定输出的文件)的 ID 和实际路径是匹配的,符合ID 和路径匹配原则

如果没有使用官方工具,你需要在自己的打包和部署过程中保证这个原则。

历史

实际上在版本 1.3.1 之前,有一个特性叫做 firstModuleInPackage,即当一个文件里有多个 define 时,默认将第一个 define 里的模块作为主模块进行返回。由于各种原因我们去掉了这个特性,可以参见:#438

小结

ID 和路径匹配原则 是 Sea.js 实现中的一个约定,这个约定帮助我们减少了对 ID 的记忆负担,同时增加了构建的复杂度。

同样的,这也是一把双刃剑,目前还没有『完美』的处理方案,都会在某些地方存在取舍和权衡。如果这方面你有好的想法,欢迎与我们交流。

PS:对这个问题,也可以看看这篇《为什么 SeaJS 模块的合并这么麻烦》的解释。

@afc163
Copy link
Member Author

afc163 commented Sep 4, 2013

有任何问题,欢迎留言交流。
注意:已解决的问题,会在整理后删除掉。

@lifesinger
Copy link
Member

@afc163 倾情整理文档。大赞。

@edokeh
Copy link
Contributor

edokeh commented Sep 4, 2013

这篇文章就应该放到这里 http://seajs.org/docs/#docs
掉坑里的人实在太多,不是从 1.3 或者更早时期过来的人很难知道这么一回事

@nuintun
Copy link

nuintun commented Sep 4, 2013

@afc163 赞,好文章~

@chaosforfun
Copy link

例子中这样得到模块a

seajs.use('path/a', function(a) {
    // got a, not b or c
});

那么怎么得到模块b或者c呢?

@afc163
Copy link
Member Author

afc163 commented Sep 5, 2013

一般来说,你不应该去拿 b 和 c ,因为 b 和 c 是作为 a 模块的子模块存在的,他们的功能是由 a 来封装或暴露的。

但是如果一定要拿,可以用 seajs.require 来获得:

seajs.use('path/a', function(a) {
    var b = seajs.require('path/b');
    var c = seajs.require('path/c');
});

@chaosforfun
Copy link

@afc163 好的,谢谢

@yessky
Copy link

yessky commented Sep 6, 2013

关于id和路径, 不知道问什么没有采用/开头的绝对路径作为id?

  1. 在本地(file协议)环境下,/ 指向磁盘第一级
  2. 在线上,/指向网站的根目录
  3. 在node环境中,/ 指向磁盘第一级目录
  • 线上环境下,涉及到id与路径转换的相关函数调用减少或者开销减少
  • id清晰明了,通过id能够马上找到模块所在路径(如果模块在网站并没有按理想的结构存放,那么打包出来的模块是否还存在着'./' '../', '/../'这些奇怪的id呢?)

@lizzie lizzie mentioned this issue Sep 6, 2013
@lifesinger
Copy link
Member

@yessky 在线上时,js 经常存放在 cdn 上,与网站并不在一个根目录,还存在跨域等情况。用 / 设计 id 会是噩梦。

@yessky
Copy link

yessky commented Sep 8, 2013

错放在cdn上那就会配置别名或者直接使用请求地址,这个噩梦体现在?

lifesinger notifications@github.com编写:

@yessky 在线上时,js 经常存放在 cdn 上,与网站并不在一个根目录,还存在跨域等情况。用 / 设计 id 会是噩梦。


Reply to this email directly or view it on GitHub.

@hhm1999
Copy link

hhm1999 commented Oct 8, 2013

有时我们在更新迭代的时候会帮js加上一个版本号,比如
seajs.use("http://a.tbcdn.cn/mw/app/index/h5/js/indexCache.js?v=420114553_70524");
但是有了这个ID 和路径匹配原则我直接在html上加版本号后就会出现错误。这个怎么破。
我是用grunt的

@cssoul
Copy link

cssoul commented Oct 8, 2013

@huanghm 可以使用这种方式加版本号
seajs.config({
... ...
map: [
[ /^(./mw/app/index/h5/js/..(?:js))(?:.*)$/i, '$1?v=0.0.1']
]
});

@lifesinger
Copy link
Member

把 jquery.js 直接放在 x-modules 目录下。或者把 jquery.js 里面的 id 修改成 jquery/jquery

On Thu, Nov 7, 2013 at 10:23 AM, Reborn notifications@github.com wrote:

麻烦帮忙看看---获取不倒jquery
[image: qq20131107102024]https://f.cloud.github.com/assets/5562776/1488708/3ec5f91e-4753-11e3-8d53-49563e25b4e7.png
目录结构
[image: qq20131107102201]https://f.cloud.github.com/assets/5562776/1488716/7329e3dc-4753-11e3-9cf6-c473d95cbab7.png


Reply to this email directly or view it on GitHubhttps://github.com//issues/930#issuecomment-27933220
.

王保平 / 玉伯(射雕)
送人玫瑰手有余香

@younth
Copy link

younth commented Dec 11, 2014

看了还是有些不明吧啊。define 的 id 要和路径匹配,但是用grunt来transport如何自动加上路径啊

@erguotou520
Copy link

麻烦close掉
问题是因为我下载的jquery的判断语句是require.amd,不过话说我从spm上下载的为什么会这样呢?不应该有这种问题吧?

看了这么多说明和解释,我自己用的时候还是不对。。。
我想直接使用spm管理依赖包,所以我的path里面有个

'lib': 'http://localhost:8080/spm_modules',

然后我使用了别名

alias: {
    'lib/es5': 'lib/es5-shim/4.0.5/es5-shim',
    'lib/json3': 'lib/json3/3.3.2/lib/json3',
    'lib/jquery': 'lib/jquery/2.1.3/jquery',
    'app/index': 'app/index/index'
  }

但是我在index.js中使用

var $ = require('lib/jquery');

还是返回null
我试过将下载的jquery.js和jquery.min.js里面define的id都去掉,或者都改为lib/jquery都是不行的,都是返回null

@nuoku
Copy link

nuoku commented Jul 21, 2015

真是巨坑啊!

@mov-78
Copy link

mov-78 commented Dec 6, 2015

所以说,只要合并后文件中的「主模块/入口模块」遵循「ID 与路径匹配原则」即可,其余的具名模块未必需要遵循「ID 与路径匹配原则」?

@supermars01
Copy link

define(function () {
//zepto源码
return $;
});
模块化zepto 这么写没问题吧?

不过为什么都引用jquery 不用zepto啊?

@anglezhang
Copy link

如何用require 实现类似URL 参数后加上 "?ran=" + Math.random();的功能,求大神解答。

@mov-78
Copy link

mov-78 commented Jan 6, 2016

@anglezhang #264

@Mrlilili
Copy link

Mrlilili commented May 26, 2016

image
这是什么原因? console.log 是NULL

seajs 2.2.1 jquery 1.9.1
----更新----
我用cmdize这个工具吧jquery打包成cmd模块后

define(function(require){
    var $ = require('jquery');
    console.log($);//object
})

如图:
image

define(function(require){
    require('jquery');
    console.log($);//function
})

如图:
image

@hoozi
Copy link

hoozi commented May 26, 2016

@Mrlilili jq 1.9.1是amd规范

@Mrlilili
Copy link

@hoozi 没变成cmd模块之前也会返回null,是什么原因?

@lumia2046
Copy link

收藏记录一下

@summerhll
Copy link

summerhll commented Nov 21, 2022 via email

@gt333
Copy link

gt333 commented Nov 21, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests