前言:这一章主要对util-path.js部分的源码进行解析。path部分主要介绍seajs中有关路径处理、路径转换等功能的实现,通过本章的学习,我们将会了解到Seajs是如何实现一个模块的最终url路径的。
本章大部分的代码都和正则表达式有关,像许多JavaScript库一样,路径匹配的实现大都是通过正则表达式去匹配的。
分析:
因为本章大部分的内容都与正则表达式有关,所以这里先来介绍一下正则表达式。有关正则表达式的内容,《正则表达式30分钟入门教程》这篇文章介绍的很详细,有兴趣的小伙伴可以移步阅读一下。
正则表达式
不同语言对正则表达式的实现各有不同,JavaScript正则表达式的情况比较复杂,因为同样的JavaScript代码在不同的浏览器上实现的结果可能不同。所以,这里我们介绍的仅仅是”通用的”,特殊情况会标明。
1.首先需要明确的是,JavaScript的正则表达式对象是RegExp。最常见的生成方法是首尾两个斜线分隔符/之间,用正则文字给出表达式。比如/\d+/。也可以显示生成一个RegExp。例如:new RegExp(/\d+/);
2. javascript正则简介:
- 字符组:在JavaScript的正则表达式中,中文字符问题比较复杂,建议使用Unicode编码环境,这样可以避免正则表达式将多字节字符拆成”字节”来对待,可以避免错误匹配。
- 字符组简介法: JavaScript正则表达式都采用ASCII匹配规则,即便是在Unicode环境下仍然如此。即\d ==> [0-9], \w ==> [0-9a-zA-Z]
- 单词边界:\b匹配成功只有一种情况:一边保证\w匹配成功,另一边保证\w匹配不成功。
- 行起始/结束位置: 用来定位字符串或行起始位置的只有^,用来定位字符串或行结束位置的只有$.
- 匹配模式: JavaScript对匹配模式的支持十分有限:常见的匹配模式只支持 “不区分大小写模式(i)”和多行模式(m)。
- 捕获分组的引用:要在正则表达式内部引用分组捕获,应当使用\num记法,其中num为对应捕获分组的编号。如果要在替换的replacement字符串中引用捕获分组,则应当使用$num记法。
3.正则API简介:
- RegExp.exec(string):这个方法用来在字符串中寻找匹配,如果成功,则返回表示匹配信息的数组,否则返回null。在默认情况下,他返回第一次匹配的结果。
- RegExp.test(string):这个方法用来测试正则表达式能否在字符串中找到匹配文本,返回布尔值。
- string.match(RegExp):这个方法类似于RegExp.exec(string),只是调换了RegExp和string的位置。两者的区别在于:无论是否指定全局模式,RegExp.exec()总是返回单次的匹配结果,而string.match()在指定了全局匹配的情况下会返回一个字符串数组,其中包含各次成功匹配的文本。
- string.search(RegExp):这个方法用来在寻找某个正则表达式在字符串中第一次匹配成功的位置。如果不成功,则返回-1。这个方法只能找到第一次匹配的位置,即使设定了全局模式,结果也不会有任何变化。
- string.replace(RegExp,replacement):这个方法用来进行正则表达式替换,将RegExp能匹配的文本替换为replacement。默认情况下,他只进行一次替换,如果设定了全局模式,则所有能匹配的文本都会被替换。
- string.replace(RegExp,function):这个方法可以理解为是方法5的一个”变种”,将第二个参数从字符串变成了函数,这个函数的接收参数是一个字符串,返回参数也是一个字符串。这样一来,文本处理就更加灵活了。
- string.split(RegExp):这个方法使用一个正则表达式切分字符串,正则表达式是否使用了全局模式对结果没有影响。
上面对JavaScript正则表达式做了一些简单介绍,下面我们来看一下Seajs 中path功能模块的源码。
util-path.js源码分析:
要想了解Seajs是如何解析模块的uri的,首先需要了解Seajs.config
中alias,path,vars和map这四个配置参数的作用以及用法。
Seajs.config可以对Seajs进行配置,让模块编写,开发调试更方便。
seajs.config({
// 别名配置
alias: {
'es5-safe': 'gallery/es5-safe/0.9.3/es5-safe',
'json': 'gallery/json/1.0.2/json',
'jquery': 'jquery/jquery/1.10.1/jquery'
},
// 路径配置
paths: {
'gallery': 'https://a.alipayobjects.com/gallery'
},
// 变量配置
vars: {
'locale': 'zh-cn'
},
// 映射配置
map: [
['http://example.com/js/app/', 'http://localhost/js/app/']
],
...
});
支持以下配置选项:
alias Object
当模块标识很长时,可以使用 alias 来简化。
seajs.config({
alias: {
'jquery': 'jquery/jquery/1.10.1/jquery',
'app/biz': 'http://path/to/app/biz.js',
}
});
define(function(require, exports, module) {
var $ = require('jquery');
//=> 加载的是 http://path/to/base/jquery/jquery/1.10.1/jquery.js
var biz = require('app/biz');
//=> 加载的是 http://path/to/app/biz.js
});
使用 alias,可以让文件的真实路径与调用标识分开,有利于统一维护。
paths Object
当目录比较深,或需要跨目录调用模块时,可以使用 paths 来简化书写。
seajs.config({
paths: {
'gallery': 'https://a.alipayobjects.com/gallery',
'app': 'path/to/app',
}
});
define(function(require, exports, module) {
var underscore = require('gallery/underscore');
//=> 加载的是 https://a.alipayobjects.com/gallery/underscore.js
var biz = require('app/biz');
//=> 加载的是 path/to/app/biz.js
});
paths 配置可以结合 alias 配置一起使用,让模块引用非常方便。
vars Object
有些场景下,模块路径在运行时才能确定,这时可以使用 vars 变量来配置。
seajs.config({
vars: {
'locale': 'zh-cn'
}
});
define(function(require, exports, module) {
var lang = require('./i18n/{locale}.js');
//=> 加载的是 path/to/i18n/zh-cn.js
});
vars 配置的是模块标识中的变量值,在模块标识中用 {key} 来表示变量。
map Array
该配置可对模块路径进行映射修改,可用于路径转换、在线调试等。
seajs.config({
map: [
[ '.js', '-debug.js' ]
]
});
define(function(require, exports, module) {
var a = require('./a');
//=> 加载的是 path/to/a-debug.js
});
通过以上Seajs.config进行的四项配置,最终都会影响到引擎解析一个模块最终的uri路径。
下面开始逐步介绍seajs引擎解析模块最终uri路径的过程,嫌麻烦的朋友可以直接跳到最后面看这一功能的源码,都有注释。
1.首先,我们先来看一下方法dirname该方法主要用于提取目录的一部分。正则表达式DIRNAME_RE
的作用是匹配以除了?#之外的其他所有字符开头并以”/”结尾的字符串。
// Extract the directory portion of a path 提取目录的一部分
// dirname("a/b/c.js?t=123#xx/zz") ==> "a/b/"
// ref: http://jsperf.com/regex-vs-split/2
function dirname(path) {
return path.match(DIRNAME_RE)[0]
}
2.方法realpath用于返回一个规范化的path。其中正则表达式DOT_RE,DOUBLE_DOT_RE,MULTI_SLASH_RE的作用如下所示:
//全局匹配/./
var DOT_RE = /\/\.\//g
//匹配 /目录名/../
var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\//
//全局匹配双斜杠
var MULTI_SLASH_RE = /([^:/])\/+\//g
下面是realpath的方法:
// Canonicalize a path 规范化转换一个path
// realpath("http://test.com/a//./b/../c") ==> "http://test.com/a/c"
function realpath(path) {
// /a/b/./c/./d ==> /a/b/c/d
path = path.replace(DOT_RE, "/")
/*
@author wh1100717
a//b/c ==> a/b/c
a///b/c ==> a/b/c
DOUBLE_DOT_RE matches a/b/c//../d path correctly only if replace // with / first
*/
path = path.replace(MULTI_SLASH_RE, "$1/")
// a/b/c/../../d ==> a/b/../d ==> a/d
//path.match==>在全局模式下返回一个匹配的字符串数组
while (path.match(DOUBLE_DOT_RE)) {
path = path.replace(DOUBLE_DOT_RE, "/")
}
return path
}
3.方法normalize用于标准化id,即用来解决模块文件后缀名的问题。
除非在路径中出现问号(”?”)或者以斜杠(“/”)结尾,SeaJS 在解析模块标识时, 都会自动添加 JS 扩展名(”.js”)。如果不想自动添加扩展名,最简单的方法是, 在路径末尾加上井号(”#”)。
// Normalize an id 标准化id
// normalize("path/to/a") ==> "path/to/a.js"
// NOTICE: substring is faster than negative slice and RegExp
function normalize(path) {
var last = path.length - 1
var lastC = path.charCodeAt(last)
// If the uri ends with `#`, just return it without '#'
if (lastC === 35 /* "#" */) {
return path.substring(0, last)
}
return (path.substring(last - 2) === ".js" ||
path.indexOf("?") > 0 ||
lastC === 47 /* "/" */) ? path : path + ".js"
}
4.下面是针对Seajs.config中四项配置对应的源码,上面已经介绍的比较详细了,这里就不再赘述了。
//不能以"/"和":"开头,并且结尾必须是"/"后面跟着若干字符(数量至少一个)
// /^([^/:]+)(\/.+)$/.test("dd/") ==> false
// /^([^/:]+)(\/.+)$/.test("dd/dd/.s") ==> true
// /^([^/:]+)(\/.+)$/.test("s:/ss/dd") ==>false
// /^([^/:]+)(\/.+)$/.test("s/ss/dd:") ==>true
var PATHS_RE = /^([^/:]+)(\/.+)$/
//匹配变量,匹配{}对象
//TODO
var VARS_RE = /{([^{]+)}/g
//解析seajs.config({})中的alias配置
function parseAlias(id) {
var alias = data.alias
return alias && isString(alias[id]) ? alias[id] : id
}
//根据设置的data.paths转化id
function parsePaths(id) {
var paths = data.paths
var m
if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) {
id = paths[m[1]] + m[2]
}
return id
}
//根据设置的vars设置id
//vars 有些场景下,模块路径在运行时才能确定,这时可以使用vars变量来配置
//vars 的详细用法参考 https://github.com/seajs/seajs/issues/262
function parseVars(id) {
var vars = data.vars
if (vars && id.indexOf("{") > -1) {
id = id.replace(VARS_RE, function(m, key) {
return isString(vars[key]) ? vars[key] : m
})
}
return id
}
//根据在map中设置的规则转换uri
//配置项map的用法
/*seajs.config({
map: [
[ '.js', '-debug.js' ]
]
});
define(function(require, exports, module) {
var a = require('./a');
//=> 加载的是 path/to/a-debug.js
});*/
function parseMap(uri) {
var map = data.map
var ret = uri
if (map) {
for (var i = 0, len = map.length; i < len; i++) {
var rule = map[i]
ret = isFunction(rule) ?
(rule(uri) || uri) :
uri.replace(rule[0], rule[1])
// Only apply the first matched rule
if (ret !== uri) break
}
}
return ret
}
5.方法addBase将seajs默认的或者自定义base路径添加进来,通过realpath方法返回真实路径,代码如下:
//匹配包含":/"或者"//{char}"开头的字符 {char}代表任意一个字符
var ABSOLUTE_RE = /^\/\/.|:\//
//根目录匹配
//ROOT_DIR_RE.exec("http://www.baidu.com/zhidao/answer/") ==>http://www.baidu.com/
var ROOT_DIR_RE = /^.*?\/\/.*?\//
function addBase(id, refUri) {
var ret
var first = id.charCodeAt(0)
// Absolute
if (ABSOLUTE_RE.test(id)) {
ret = id
}
// Relative 第一个char是"." 判断refuri是否传入
else if (first === 46 /* "." */) {
ret = (refUri ? dirname(refUri) : data.cwd) + id
}
// Root 第一个char是"/" 根路径
else if (first === 47 /* "/" */) {
var m = data.cwd.match(ROOT_DIR_RE)
ret = m ? m[0] + id.substring(1) : id
}
// Top-level
else {
ret = data.base + id
}
// Add default protocol when uri begins with "//"
if (ret.indexOf("//") === 0) {
ret = location.protocol + ret
}
return realpath(ret)
}
6.方法id2uri通过id来匹配正确的脚本uri地址,最后将其赋给seajs.resolve,暴露给开发者。开发者通过该方法对传入的字符串参数进行路径解析。
//通过id来匹配正确的脚本uri地址
function id2Uri(id, refUri) {
if (!id) return ""
id = parseAlias(id)
id = parsePaths(id)
id = parseAlias(id)
id = parseVars(id)
id = parseAlias(id)
id = normalize(id)
id = parseAlias(id)
var uri = addBase(id, refUri)
uri = parseAlias(uri)
uri = parseMap(uri)
return uri
}
// For Developers
//会利用模块系统的内部机制对传入的字符串参数进行路径解析。
seajs.resolve = id2Uri
至此,seajs路径解析功能已基本分析完毕,下面附上本章全部代码注释:
//匹配文件夹目录(匹配开头除了?#字母之外,以"/"结尾的任意字符串)
var DIRNAME_RE = /[^?#]*\//
//全局匹配/./
var DOT_RE = /\/\.\//g
//匹配 /目录名/../
var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\//
//全局匹配双斜杠
var MULTI_SLASH_RE = /([^:/])\/+\//g
// Extract the directory portion of a path 提取目录的一部分
// dirname("a/b/c.js?t=123#xx/zz") ==> "a/b/"
// ref: http://jsperf.com/regex-vs-split/2
function dirname(path) {
return path.match(DIRNAME_RE)[0]
}
// Canonicalize a path 规范化转换一个path
// realpath("http://test.com/a//./b/../c") ==> "http://test.com/a/c"
function realpath(path) {
// /a/b/./c/./d ==> /a/b/c/d
path = path.replace(DOT_RE, "/")
/*
@author wh1100717
a//b/c ==> a/b/c
a///b/c ==> a/b/c
DOUBLE_DOT_RE matches a/b/c//../d path correctly only if replace // with / first
*/
path = path.replace(MULTI_SLASH_RE, "$1/")
// a/b/c/../../d ==> a/b/../d ==> a/d
//path.match==>在全局模式下返回一个匹配的字符串数组
while (path.match(DOUBLE_DOT_RE)) {
path = path.replace(DOUBLE_DOT_RE, "/")
}
return path
}
// Normalize an id 标准化一个id
// normalize("path/to/a") ==> "path/to/a.js"
// NOTICE: substring is faster than negative slice and RegExp
function normalize(path) {
var last = path.length - 1
var lastC = path.charCodeAt(last)
// If the uri ends with `#`, just return it without '#'
if (lastC === 35 /* "#" */) {
return path.substring(0, last)
}
return (path.substring(last - 2) === ".js" ||
path.indexOf("?") > 0 ||
lastC === 47 /* "/" */) ? path : path + ".js"
}
//不能以"/"和":"开头,并且结尾必须是"/"后面跟着若干字符(数量至少一个)
// /^([^/:]+)(\/.+)$/.test("dd/") ==> false
// /^([^/:]+)(\/.+)$/.test("dd/dd/.s") ==> true
// /^([^/:]+)(\/.+)$/.test("s:/ss/dd") ==>false
// /^([^/:]+)(\/.+)$/.test("s/ss/dd:") ==>true
var PATHS_RE = /^([^/:]+)(\/.+)$/
//匹配变量,匹配{}对象
//TODO
var VARS_RE = /{([^{]+)}/g
//解析seajs.config({})中的alias配置
function parseAlias(id) {
var alias = data.alias
return alias && isString(alias[id]) ? alias[id] : id
}
//根据设置的data.paths转化id
function parsePaths(id) {
var paths = data.paths
var m
if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) {
id = paths[m[1]] + m[2]
}
return id
}
//根据设置的vars设置id
//vars 有些场景下,模块路径在运行时才能确定,这时可以使用vars变量来配置
//vars 的详细用法参考 https://github.com/seajs/seajs/issues/262
function parseVars(id) {
var vars = data.vars
if (vars && id.indexOf("{") > -1) {
id = id.replace(VARS_RE, function(m, key) {
return isString(vars[key]) ? vars[key] : m
})
}
return id
}
//根据在map中设置的规则转换uri
//配置项map的用法
/*seajs.config({
map: [
[ '.js', '-debug.js' ]
]
});
define(function(require, exports, module) {
var a = require('./a');
//=> 加载的是 path/to/a-debug.js
});*/
function parseMap(uri) {
var map = data.map
var ret = uri
if (map) {
for (var i = 0, len = map.length; i < len; i++) {
var rule = map[i]
ret = isFunction(rule) ?
(rule(uri) || uri) :
uri.replace(rule[0], rule[1])
// Only apply the first matched rule
if (ret !== uri) break
}
}
return ret
}
//匹配包含":/"或者"//{char}"开头的字符 {char}代表任意一个字符
var ABSOLUTE_RE = /^\/\/.|:\//
//根目录匹配
//ROOT_DIR_RE.exec("http://www.baidu.com/zhidao/answer/") ==>http://www.baidu.com/
var ROOT_DIR_RE = /^.*?\/\/.*?\//
function addBase(id, refUri) {
var ret
var first = id.charCodeAt(0)
// Absolute
if (ABSOLUTE_RE.test(id)) {
ret = id
}
// Relative 第一个char是"." 判断refuri是否传入
else if (first === 46 /* "." */) {
ret = (refUri ? dirname(refUri) : data.cwd) + id
}
// Root 第一个char是"/" 根路径
else if (first === 47 /* "/" */) {
var m = data.cwd.match(ROOT_DIR_RE)
ret = m ? m[0] + id.substring(1) : id
}
// Top-level
else {
ret = data.base + id
}
// Add default protocol when uri begins with "//"
if (ret.indexOf("//") === 0) {
ret = location.protocol + ret
}
return realpath(ret)
}
//通过id来匹配正确的脚本uri地址
function id2Uri(id, refUri) {
if (!id) return ""
id = parseAlias(id)
id = parsePaths(id)
id = parseAlias(id)
id = parseVars(id)
id = parseAlias(id)
id = normalize(id)
id = parseAlias(id)
var uri = addBase(id, refUri)
uri = parseAlias(uri)
uri = parseMap(uri)
return uri
}
// For Developers
//会利用模块系统的内部机制对传入的字符串参数进行路径解析。
seajs.resolve = id2Uri