justin-zhengyi-wu / blogs

My Blogs
http://blog.justin4u.com/
4 stars 0 forks source link

Seajs 源码分析 2 #9

Open justin-zhengyi-wu opened 11 years ago

justin-zhengyi-wu commented 11 years ago

util-path.js 处理文件路径, ID, URI

// 关于文件夹(目录)的正则表达式
// [^?#] 表示不是?和#的任意其他字符
// [^?#]* 表示这样的字符有0到多个
// [^?#]*\/ 表示再加上一个斜杠字符
var DIRNAME_RE = /[^?#]*\//

// 关于"点"(代表当前路径)的正则表达式, 全局匹配
// \/\.\/ 表示字符串/./
var DOT_RE = /\/\.\//g
// 关于双点(代表上一级路径)的正则表达式, 
// 表示: 斜杠,一到多个非斜杠字符,斜杠,两点,斜杠
// 例如: /abc/../
// \/ 表示斜杠
// [^/]+ 表示一到多个的非斜杠字符
// \/\.\.\/ 表示/../
var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\//

// 从路径中提取出文件所在目录
// 如: dirname("a/b/c.js?t=123#xx/zz") 得到 "a/b/"
function dirname(path) {
    // 由于这里的正则表达式没有g, 返回数组的第一个元素, 即匹配到的文本.
    // NOTE: 使用match方法时,如果正则没有标志g, 并且找到匹配的, 返回的数组存放的第一个元素为匹配的文本, 
    // 使用[0]取出, 第二个元素为对象属性, 可以使用['index']取出. 存放的第三个元素也为一个对象属性, 
    // 可以使用['input']取出. 如果没有找到匹配的, 返回null.
    // 如果正则使用了标志g, 并且找到匹配, 返回的数组存放的是所有的匹配子串. 而且没有了index和input属性.
    return path.match(DIRNAME_RE)[0]
}

// 对一个路径进行规范化. 例如:
// 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, "/")
    // 迭代地将斜杠-非斜杠字符-斜杠-点点-斜杠 替换成斜杠, 例如:
    // a/b/c/../../d  ==>  a/b/../d  ==>  a/d
    while (path.match(DOUBLE_DOT_RE)) {
        path = path.replace(DOUBLE_DOT_RE, "/")
    }
    return path
}

// 将ID正常化, 如:
// normalize("path/to/a") 变为 "path/to/a.js"
// 注意: 使用substring要比逆向slice和正则快.
function normalize(path) {
    // 最后一个字符的index.
    var last = path.length -1
    // 最后一个字符
    var lastC = path.charAt(last)
    // 如果uri中包含有井号, 直接取井号前的字符串
    if (lastC === "#") {
        // NOTE: 使用substring(i, j)取段包括位置i,但不包括位置j.
        return path.substring(0, last)
    }
    // 如果path已经是点js结尾, 或者含有问号, 或者点css结尾, 或者以斜杠结尾.
    // 直接返回path, 否则的话, 加上点js后返回.
    return (path.substring(last - 2) === ".js" ||
        path.indexOf("?") > 0 ||
        path.substring(last - 3) === ".css" ||
        lastC === "/") ? path: path + ".js"
}

// [^/:]+ 表示一个或多个非斜杠非冒号的字符.
// \/.+ 表示斜杠加一或多个字符
// 全部的正则表达式表示以一个或多个非斜杠非冒号的字符打头, 以斜杠加一或多个字符结尾. 例如:
// abc/def/asdf/asdf
var PATHS_RE = /^([^/:]+)(\/.+)$/

// 正则表达式, 全局匹配一对大括弧包含某几个字符
// [^{]+ 表示一个或多个非左大括弧的字符
var VARS_RE = /{([^{]+)}/g

// 对ID进行别名解析
function parseAlias(id) {
    // 取出data的alias属性, 这里data是全局对象seajs的一个属性
    var alias = data.alias
    // 如果data的alias属性存在并且id是已经定义好的别名, 返回别名指向的真正东西.
    // 否则直接返回传入的id
    return alias && isString(alias[id]) ? alias[id] : id
}

// 对路径id进行解析
function parsePaths(id) {
    // 取出data的paths属性, 这里data是全局对象seajs的一个属性
    var paths = data.paths
    var m
    // 如果paths存在并且传入的路径是符合正常路径的定义, 并且匹配到的第一组, 也就是路径的
    // 首段(第一个斜杠前的东西)是字符串的话, 把匹配到的第一组(也就是路径的首段, 第一个斜杠前的字符串)
    // 与匹配到的第二组连接(余下的匹配), 返回.
    // 否则的话, 直接返回传入的路径
    if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) {
        id = paths[m[1]] + m[2]
    }
    return id
}

// 对变量表达式进行解析
// 例如: 如果seajs.data.vars.name等于Justin, 那么,
// "My name is {name}." 解析成 "My name is Justin."
function parseVars(id) {
    // 取出data的vars属性, 这里data是全局对象seajs的一个属性
    var vars = data.vars
    // 如果vars已定义并且传入的变量表达式含有左花括弧, 将大括弧所包裹的字符串(例如是"name")替换成:
    // 1) 如果vars中有关于该字符串(例如是"name")的mapping并且值是字符串, 替换成它的mapping.
    // 2) 否则的话, 替换成传入的变量表达式, 相当于没有改变
    // 否则的话, 直接返回传入的变量表达式, 也相当于没有改变
    if (vars && id.indexOf("{") > -1) {
        // NOTE: 这里的replace方法的第二个参数如果是函数并且寻找匹配成功的话, 函数的第一个参数是匹配
        // 到的字符串, 函数的后几个参数的值分以下几种情况:
        // 1) 如果replace方法的第一个参数是字符串(例如'name')或者未分组的正则表达式的话, 函数的第二个
        // 参数是字符串或正则内容(例如'name')在replace方法的调用者(caller)的匹配到的index, 第三个参数
        // 是replace方法的调用者(caller).
        // 2) 如果replace方法的第一个参数是有分组的正则表达式的话, 函数的第二个参数是匹配到的分组对应的
        // 字符串, 第三个参数是字符串(例如'name')在replace方法的调用者(caller)的匹配到的index, 第四个
        // 参数是replace方法的调用者(caller).
        // 如果匹配未成功, 函数只有一个参数, 即replace方法的调用者(caller)自身
        id = id.replace(VARS_RE, function(m, key) {
            return isString(vars[key]) ? vars[key] : m
        })
    }
    return id
}

// 对变量uri中有map关系的字符进行解析,替换
function parseMap(uri) {
    // 取出data的map属性, 这里data是全局对象seajs的一个属性
    var map = data.map
    // 定义返回的变量ret, 初始化为传入的uri
    var ret = uri
    // 如果map存在
    if (map) {
        // 迭代map
        for (var i = 0; len = map.length; i < len; i++) {
            // 定义变量rule为当前循还取到的东西
            var rule = map[i]
            // 如果变量rule为函数, 把变量uri传给函数rule并调用, 如果调用返回结果为真值, 返回值传给变量ret; 
            //     如果调用返回结果为假值, 把变量uri传给变量ret.
            // 如果变量rule不是函数, 调用变量uri的replace方法, 把变量uri中含有等于变量rule的第一个参数的
            //     字符替换成变量rule的第二个元素, 最终将replace方法的返回值传给变量ret.
            ret = isFunction(rule) ? (rule(uri) || uri) : uri.replace(rule[0], rule[1])
            // 如果变量ret不全等于变量uri, 表明ret的值已经更新了,不再是初始值, 目的达成, 可以跳出循环
            if (ret !== uri) break
        }
    }
    // 最后返回变量ret.
    return ret
}

// 正则表达式, 表示(双斜杠开头加一个字符)或者(一个冒号加斜杠)
var ABSOLUTE_RE = /^\/\/.|:\//

// 正则表达式, 表示非贪婪匹配以零到多个字符打头, 加两斜杠加非贪婪匹配零到多个字符再加斜杠, 
// 例如: ab//c/
// 用来判断根目录
var ROOT_DIR_RE = /^.*?\/\/.*?\//

// 添加路径的基地
function addBase(id, refUri) {
    // 定义变量ret为最终要返回的东西.
    var ret
    // 传入的变量id的第一个字符
    var first = id.charAt(0)
    // 如果传入的变量id是一个绝对路径, 返回值为变量id
    if (ABSOLUTE_RE.test(id)) {
        ret = id
    }
    // 如果变量id的第一个字符为点, 表明为相对路径,
    else if (first === ".") {
        // 再判断是否传入变量refUri, 如果是的话, 取得它的所在的目录, 否则的话, 取出data(这里data是
        // 全局对象seajs的一个属性)的cwd(当前的工做目录)属性, 最后拼上变量id, 作为参数传给
        // 函数realpath, 进行规范化后赋予变量ret.
        ret = realpath((refUri ? dirname(refUri) : data.cwd) + id)
    }
    // 如果变量id的第一个字符为斜杠, 表明是根路径
    else if (first === "/") {
        // 定义变量m, 赋值为是否data的当前工作目录为根目录
        var m = data.cwd.match(ROOT_DIR_RE)
        // 如果是的话, 返回值为正则匹配到的文本连接上(去除掉第一个字符(也就是斜杠)的)变量id, 
        // 否则的话, 返回值为变量id.
        ret = m ? m[0] + id.substring(1) : id
    }
    // 其它情况的话, 返回值为data(这里data是全局对象seajs的一个属性)的属性base连接上变量id.
    else {
        ret = data.base + id
    }
    // 返回返回值
    return ret
}

// 将id转化为uri
function id2Uri(id, refUri) {
    // 如果未传入id, 或传入id未空, 假值, 直接返回空字符串.
    if (!id) return ""
    // 首先查看是否为别名, 取得对应的路径
    id = parseAlias(id)
    // 其次查看是否为路径
    id = parsePaths(id)
    // 再次查看是否含有未解决(替换)的变量表达式, 如有, 解决(替换)之.
    id = parseVars(id)
    // 再将之正常化(补上文件名后缀)
    id = normalize(id)
    // 然后再添加上路径前段的基(base), 使之成为完整路径
    var uri = addBase(id, refUri)
    // 再对有map关系的字符进行解析替换
    uri = parseMap(uri)
    // 最终返回.
    return uri
}

// 变量document的引用
var doc = document
// 变量location的引用
var loc = location
// 通过页面的路径得到当前工作目录
var cwd = dirname(loc.href)
// 页面上所有的脚本引用列表
var scripts = doc.getElementsByTagName("script")

// 定义脚本加载器, 首先考虑ID名为seajsnode的HTML元素, 如果没有, 使用最后一个脚本作为加载器.
// 推荐用户给引用seajs的脚本元素加上ID名为seajsnode
var loaderScript = doc.getElementById("seajsnode") ||
    scripts[scripts.length - 1]

// 定义加载器所在目录, 如果seajs不是通过外部引用进来的, 也就是说是行内形式, 则加载器所在目录
// 为当前工作目录.
var loaderDir = dirname(getScriptAbsoluteSrc(loaderScript) || cwd)

// 一个帮助函数, 用于从传入的HTML标签节点中取出脚本的源文件绝对路径.
function getScriptAbsoluteSrc(node) {
    // 如果非IE6/7, 使用节点的src属性, 否则
    // 参考 http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx, 
    // 使用节点的getAttribute方法得到源文件的绝对路径.
    return node.hasAttribute ? node.src : node.getAttribute("src", 4)
}