RequireJS路径详解(深入理解)
0. 前言
由于官方文档说明甚少,导致RequireJS的路径解析逻辑就像一个谜,本文希望能帮你解开它神秘的面纱。本文将深入讲解RequireJS的路径解析原理,如果你对RequireJS路径解析的一些基本概念还不清楚,请先参考:让人迷惑的路径解析
如果有描述不对的地方,希望能帮我指出,及时修改以免以讹传讹,谢谢。
参考版本: require-2.1.11.js
官网: http://requirejs.org/docs/api.html#jsfiles
如何查看RequireJS请求文件的路径?
可以在Chrome的Network
中查看,也可以故意将文件名拼错就能在报错日志中看到实际请求路径。
在RequireJS中,设置baseURl的方式有如下三种
- 用requirejs.config显示指定baseUrl;
- 如果指定了Entry Point(data-main)文件,则baseUrl为Entry Point所在目录;
- 如果上述均未指定,则baseUrl为运行RequireJS的HTML文件所在目录。
按照官方描述如果具备以下三种特性之一,则module ID会被当做普通路径
处理。
- 应用的module ID以.js结尾;
- 以“/”开始(
操作系统根目录/
); - 包含url协议:如"http:"、"https"。
原文如下
If a module ID has one of the following characteristics, the ID will not be passed through the "baseUrl + paths" configuration, and just be treated like a regular URL that is relative to the document:
1. Ends in ".js".
2. Starts with a "/".
3. Contains an URL protocol, like "http:" or "https:".
1. "主目录"的概念
指调用RequireJS的html文件所在的目录,RequireJS中并没有“主目录”的概念,本文引入该名称只是为了方面说明。
结论1.1:在RequireJS中,baseUrl的定义是“相对于主目录”的。
---后续例子的目录结构---
www/
html/
index-html.html
js/
lib/
hello.js
app.js
require-2.1.11.src.js
index.html
例子1
以index.html为例,Entry Point为app.js(www/js/app.js)。在app.js中,baseUrl被定义为js/lib
。这里的js/lib是相对于“主目录”(www/
)而言的,即baseUrl实际指向www/js/lib
。
// ----index.html----
requireJS
requireJS.
// ----app.js----
requirejs.config ({
baseUrl: 'js/lib',
});
require(['hello'], function(hello) {
hello.hello("RquireJS");
});
例子2
index-html.html位于www/html/目录下(主目录为www/html/
),Entry Point同为app.js。则app.js中的baseUrl指向主目录
+baseUrl
= www/html/js/lib
。
// ----index-html.html----
requireJS html
requireJS. in html
结论1.2:按照普通路径处理时候,引用路径是相对于主目录的。
前言
在RequireJS中,设置baseURl的方式有如下三种
- 用requirejs.config显示指定;
- 如果指定了Entry Point(data-main),则baseUrl为data-main所指的js的目录;
- 如果上述均为指定,则baseUrl为运行RequireJS的HTML文件所在目录。
但是,按照官方描述如果具备以下三种特性之一,则module ID会被当做普通路径处理。
- module ID以.js结尾;
- 以“/”开始(
操作系统根目录/
); - 包含url协议:如"http:"、"https"。
原文如下
If a module ID has one of the following characteristics, the ID will not be passed through the "baseUrl + paths" configuration, and just be treated like a regular URL that is relative to the document:
1. Ends in ".js".
2. Starts with a "/".
3. Contains an URL protocol, like "http:" or "https:".
例子1
和前文的例子一样,只是把app.js中require语句中的module ID由hello
改为hello.js
。按照上述描述,文件名已.js
结尾不会解析baseUrl。根据结论2
按照普通路径进行解析时,路径是相对于“主目录的”,所以引用文件的路径为www/hello.js
。
补充:将module ID改为./hello.js
和hello.js
引用的是同一个路径。
//index.html
requireJS
requireJS.
// app.js
requirejs.config ({
baseUrl: 'js/lib',
});
require(['hello.js'], function(hello) {
hello.hello("RquireJS");
});
例子2
把index.html换成index-html.html(位于www/html/
目录),引用相同的app.js文件。所以引用文件的路径变成了www/html/hello.js
//index-html.html
requireJS html
requireJS. in html
2.路径的级联处理
当A模块使用(require语句)了B模块,B模块的定义中指定了对C模块的依赖(define语句),则B模块中的define语句
如何根据A模块中的require语句
查找C的路径的过程称作路径的级联处理(非官方命名,只是为了便于说明)。
在定义模块时会明确指出该模块依赖了哪些模块。按理说,一个模块依赖了哪些模块、从哪里获取这些模块应该模块的编写者明确指定,模块的使用者无需关心。然而,在RquireJS中并非如此:在使用模块
时,指定该模块的路径的方式可能会影响到被使用的模块如何去查找它自身依赖的模块。——只是是我们使用RequireJS时应该特别注意的地方,个人觉得也是RquireJS应该改进的地方。
---后续例子的目录结构---
www/
js/
lib/
log.js
hello.js
app.js
require.js
index.html
例如,app.js引用了hello模块, hello模块依赖了log.js。下面列举了3种引用hello模块的方式,每种方式均能正确地找到hello.js。但是hello模块查找log.js的路径各不相同,分别是: www/js/js/lib/log.js
、www/js/lib/log.js
、www/js/log.js
。
// ---hello.js---
define(['./log'], function(log) {
var hello = function(msg) {
log.log('www/js/lib/hello.js: ' + msg);
};
return {
hello: hello
};
});
// [引用hello.js的三种方式]
// ---方式1: 使用普通路径(不使用baseUrl)---
require(['js/lib/hello.js'], function(hello) {
hello.hello("RquireJS");
});
// ---方式2: 使用baseUrl---
requirejs.config ({
baseUrl: 'js/lib',
});
require(['hello'], function(hello) {
hello.hello("RquireJS");
});
// ---方式3: 使用paths定义---
requirejs.config ({
baseUrl: 'js',
paths: {
"hello": "lib/hello"
}
});
require(['hello'], function(hello) {
hello.hello("RquireJS");
});
为什么改变app.js中引用hello模块的方式会影响到hello模块查找log.js的路径?如何才能使得hello模块查找log.js的路径不受app.js的影响?请看后文。
结论2.1:在一个模块的定义内寻找依赖时候会首先会进行./
替换
在定义模块时(define语句)声明的依赖中如果使用了./
,在路径解析时./
会被替换成使用该模块(require语句)时的路径前缀(路径中最后一个"/"前的所有部分),如果没有前缀则不进行替换。
- 如果写成:require(['js/lib/hello.js'],则路径的前缀为"js/lib/";
- 如果写成:require['hello.js'],则表示没有路径前缀。
例子2.1
// ---app.js---
require(['js/lib/hello.js'], function(hello) {
hello.hello("RquireJS");
});
// ---hello.js---
define(['./log.js'], function(log) {
var hello = function(msg) {
log.log('www/js/lib/hello.js: ' + msg);
};
return {
hello: hello
};
});
为了降低问题的复杂度先以普通路径(module ID包含.js后缀名,避免baseUrl的影响)为例进行说明。
- 本例中app.js中require语句的路径前缀为
js/lib/
,则在hello.js中./log.js
会被替换为js/lib/log.js
; - 根据前文描述,以.js结尾则不进行baseUrl处理,即最终的路径为
“主目录”/js/lib/log.js
,即:www/js/lib/log.js
附加实验
如果将./log.js
改为log.js
,则不进行./
替换,最终指向www/log.js
。
baseUrl处理
例子2.2
// hello.js
define(['./log'], function(log) {
var hello = function(msg) {
log.log('www/js/lib/hello.js: ' + msg);
};
return {
hello: hello
};
});
在级联处理中,仍然会进行baseUrl处理。将上述例子中hello.js内log.js改成log,则产生log.js的路径的步骤如下:
- 进行
./
替换,./log
->js/lib/log
- 拼接baseUrl,得到
js/js/lib/log
(没有显示指定baseUrl,则baseUrl为data-main指定的js文件所在目录,即js
) - 拼接“主目录”,得到
www/js/js/lib/log
paths替换
例子2.3
// app.js
requirejs.config ({
baseUrl: "./",
paths: {
"lib": "js/lib"
}
});
require(['lib/hello'], function(hello) {
hello.hello("RquireJS");
});
在级联处理中,仍然会做paths替换。app.js中定义了pathlib
(指向js/lib
)。在hello模块中log.js的路径解析步骤如下:
- 进行
./
替换(./
->lib
),得到lib/log
- 进行paths替换(
lib
->js/lib
),得到js/lib/log
- 进行baseUrl拼接,得到
./js/lib/log
- 进行“主目录”拼接,得到
www/./js/lib/log
。最终指向www/js/lib/log.js
(非级联处理中./
表示当前目录,可以省略)
附加实验
- 如果将
./log
改为./log.js
,则不会进行paths处理和baseUrl替换,则会指向www/lib/log.js
文件。 - 如果将
./log
改为log
,则不会进行./
替换,最终指向www/log.js
3. 路径解析逻辑
结论3.1:RequireJS中路径的处理流程如下图所示
综上所述,RequireJS中路径解析过程如上图所示。在级联处理中会首先进行./
替换操作;然后再针对不以.js结尾的进行paths替换和baseUrl拼接;最后拼接上“主目录”。
前文app.js的3种写法对应的log.js的路径解析过程如下
方式1
- 进行'./'替换,
./log
->js/lib/log
- 拼接baseUrl(baseUrl为data-main指定的app.js所在目录),得到
js/js/lib/log
- 拼接“主目录”,得到
www/js/js/lib/log
,最终指向www/js/js/lib/log.js
方式2
- 路径前缀为空,不进行'./'替换;
- 拼接baseUrl,得到
js/lib/log
- 拼接“主目录”,得到
www/js/lib/log
,最终指向www/js/lib/log.js
方式3
- 进行'./'替换,
./log
->lib/log
- 进行paths替换,得到
js/lib/log
- 拼接baseUrl(baseUrl为data-main指定的app.js所在目录),得到
js/js/lib/log
- 拼接“主目录”,得到
www/js/js/lib/log
,最终指向www/js/js/lib/log.js
4. 思考
为什么下述两种方式引用的log.js不同?
// ---方式3
requirejs.config ({
baseUrl: './',
paths: {
"hello": "js/lib/hello"
}
});
require(['hello'], function(hello) {
hello.hello("RquireJS");
});
// ---方式4
requirejs.config ({
baseUrl: "./",
paths: {
"lib": "js/lib"
}
});
require(['lib/hello'], function(hello) {
hello.hello("RquireJS");
});
回答让人迷惑的路径解析博主的问题。
下述两种写法引用的log.js分别为www/log.js
和www/js/lib/log.js
,路径解析步骤如下:
方式3
- 没有前缀,不进行
./
替换; -
./log
进行baseUrl处理,得到././log
; - 进行”主目录拼接“,得到
www././log
,最终指向www/log.js
方式4
- 进行
./
替换,./log
->lib/log
; - 进行paths替换,得到
js/lib/log
; - 进行baseUrl处理,得到
./js/lib/log
; - 进行”主目录“拼接,得到
www./js/lib/log
,最终指向www/js/lib/log.js
。
方式3和方式4关键的区别在于前者没有路径前缀,看一下方式4的./替换和paths替换流程:
1. ./
替换前
name: ./log
2. ./
替换后
name: lib/log
3. paths替换前
name: lib/log
4. paths替换后
name: js/lib/log
最后两句还进行了baseUrl拼接和后缀名拼接。
对于方式4而言,在查找`./log`的时候RequireJS先将require(['lib/hello'])中的`lib/hello`路径最后一部分去掉,将得到的路径(`lib`)拼接到`./`前,就得到了`lib/log`,然后再对lib/进行paths展开操作。对于方式3而言,由于得到的路径为空所以就没有后续的paths展开过程了。
问题的关键在于RequireJS先进行了`./`替换,再进行paths展开。这里个人认为是RequireJS的错误,将`./`替换和paths替换顺序反过来即可,应当先展开paths,将`hello`展开成`js/lib/hello`,然后再将前缀`js/lib`拼接到`./log`的前面,得到`js/lib/log`。
——目前认为是RequireJS的bug,不知道是不是RequireJS故意为之。
说明,其实并没有”主目录“的概念,只是为了便于说明。因为JS代码最终被加载到HTML中运行,所以HTML所在的目录即为JS运行时的目录,因此JS中指定的路径均是相对于该目录而言的。
4. 如何避免路径产生歧义
影响hello模块查找log.js主要在于几点:
-
./
替换 - paths替换
- baseUrl
- 主目录
未完待续...