seajs主要解决的问题包括:命名冲突、文件依赖、异步加载和模块化等问题,具体怎么实现的呢?通过阅读源码一探究竟。源码地址:https://github.com/seajs/seajs
seajs-debug.js
/**
* Sea.js 3.0.0
*/
(function(global, undefined) {
//多次加载seajs.js保证只有一个seajs.js文件有效
if (global.seajs) {
return
}
var seajs = global.seajs = {
// The current version of Sea.js being used
version: "3.0.0"
} ;
//seajs的配置信息
var data = seajs.data = {}
/**
* 类型判断信息
*/
function isType(type) {
return function(obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
}
var isObject = isType("Object")
var isString = isType("String")
var isArray = Array.isArray || isType("Array")
var isFunction = isType("Function")
var _cid = 0
function cid() {
return _cid++
}
/**
* util-events.js - The minimal events support
*/
var events = data.events = {}
/**
* 注册事件
* @param name
* @param callback
* @returns {{version: string}}
*/
seajs.on = function(name, callback) {
var list = events[name] || (events[name] = [])
list.push(callback)
return seajs
}
// Remove event. If `callback` is undefined, remove all callbacks for the
// event. If `event` and `callback` are both undefined, remove all callbacks
// for all events
seajs.off = function(name, callback) {
// Remove *all* events
//如果name和callback都为空,则清除所有的事件
if (!(name || callback)) {
events = data.events = {};
return seajs
}
var list = events[name] ;
if (list) {
//如果回调函数为空,则删除所有的时间监听,否则移除指定的监听事件
if (callback) {
for (var i = list.length - 1; i >= 0; i--) {
if (list[i] === callback) {
list.splice(i, 1)
}
}
}else {
delete events[name]
}
}
return seajs
}
// Emit event, firing all bound callbacks. Callbacks receive the same
// arguments as `emit` does, apart from the event name
//触发指定的事件监听
var emit = seajs.emit = function(name, data) {
var list = events[name] ;
if (list) {
// Copy callback lists to prevent modification
list = list.slice()
// Execute event callbacks, use index because it's the faster.
for(var i = 0, len = list.length; i < len; i++) {
list[i](data)
}
}
return seajs
}
/**
* util-path.js - The utilities for operating path such as id, uri
*/
//^在方括号中使用,表示不接受字符集合。 只匹配所有的路径信息,最后一个标识符为“/”
var DIRNAME_RE = /[^?#]*\// ;
//替换/./为/
var DOT_RE = /\/\.\//g ;
//替换“/路径名/../”为单个“/”
var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\// ;
//替换多个“//”为一个“/”
var MULTI_SLASH_RE = /([^:/])\/+\//g ;
/**
* 导出请求中的路径,不包括具体的文件名称
* dirname("a/b/c.js?t=123#xx/zz") ==> "a/b/"
* ref: http://jsperf.com/regex-vs-split/2
* @param path
* @returns {*}
*/
function dirname(path) {
return path.match(DIRNAME_RE)[0]
}
/**
* 对路径进行格式化,对路径中特殊转义字符进行了转换,例如:realpath("http://test.com/a//./b/../c") ==> "http://test.com/a/c"
* @param path
* @returns {XML|string|*}
*/
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
while (path.match(DOUBLE_DOT_RE)) {
path = path.replace(DOUBLE_DOT_RE, "/")
}
return path
}
/**
* url后缀名称自动添加“.js”
* @param path
* @returns {string}
*/
function normalize(path) {
var last = path.length - 1
var lastC = path.charCodeAt(last)
if (lastC === 35 /* "#" */) {
return path.substring(0, last)
}
//此处只是针对js文件做了处理。在一般的请求后缀后添加“.js”
return (path.substring(last - 2) === ".js" || path.indexOf("?") > 0 || lastC === 47 /* "/" */) ?
path : path + ".js"
}
//只获取路径中第一个“/”之前的文字
var PATHS_RE = /^([^/:]+)(\/.+)$/ ;
//匹配大括号内的内容
var VARS_RE = /{([^{]+)}/g ;
/**
* 检查是否有配置别名,如果未配置别名,则需要使用原有id
* @param id
* @returns {*}
*/
function parseAlias(id) {
var alias = data.alias ;
return alias && isString(alias[id]) ? alias[id] : id ;
}
/**
* 根据id解析路径,id的组成部分使用“/”进行分隔
* @param id
* @returns {*}
*/
function parsePaths(id) {
var paths = data.paths ;
var m ;
//paths是一个{}对象,解析id的组成部分,使用path中指定的变量替换路径名称
//解析路径中第一个“/”之前的字符串是否有指定path,如果有指定path,则用指定的path替换当前路径中的字符串
if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) {
id = paths[m[1]] + m[2]
}
return id
}
/**
* 使用正则表达式解析字符串中的变量。变量使用“{}”包含
* @param id
* @returns {*}
*/
function parseVars(id) {
//通过配置参数中传递
var vars = data.vars ;
if (vars && id.indexOf("{") > -1) {
//function参数,第一个为所有匹配的文本,第二个为指定的子匹配文本
id = id.replace(VARS_RE, function(m, key) {
return isString(vars[key]) ? vars[key] : m ;
})
}
return id
}
/**
* 通过map对请求的url进行转换,其中map为一个数组。
* 如果数组元素为一个函数,则直接调用函数对uri进行转换,否则将当前元素看做为一个数组,使用第二个元素替换第一个元素
* @param uri
* @returns {*}
*/
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] ;
//针对uri进行映射转换
ret = isFunction(rule) ?
(rule(uri) || uri) :
uri.replace(rule[0], rule[1]) ;
//只应用一次uri的映射转换
if (ret !== uri) {
break ;
}
}
}
return ret
}
//匹配“//.”或“:/”
var ABSOLUTE_RE = /^\/\/.|:\// ;
//根目录正则表达式,匹配当前请求的根请求路径,基本规则为:“协议://域名/”。此处使用了正则表达式的非贪婪匹配
var ROOT_DIR_RE = /^.*?\/\/.*?\// ;
/**
* 添加请求路径信息。此处对应seajs中文件路径加载方式
* @param id
* @param refUri
* @returns {XML|string|*}
*/
function addBase(id, refUri) {
var ret ;
var first = id.charCodeAt(0)
if (ABSOLUTE_RE.test(id)) {
//如果是绝对路径
ret = id
}else if (first === 46 /* "." */) {
//如果是相对路径,则获取当前请求的地址路径,然后加上当前的模块的id
ret = (refUri ? dirname(refUri) : data.cwd) + id
}else if (first === 47 /* "/" */) {
//如果是根目录开始,则获取请求地址的根目录,然后加上当前资源的id
var m = data.cwd.match(ROOT_DIR_RE)
ret = m ? m[0] + id.substring(1) : id
}else {
//如果是别名字段等,则默认使用seajs配置的js加载根目录
ret = data.base + id
}
//如果请求地址是以“//”开头,则默认使用当前请求的默认协议
if (ret.indexOf("//") === 0) {
ret = location.protocol + ret
}
//格式化路径
return realpath(ret)
}
/**
* 将id解析为请求的url
* @param id
* @param refUri
* @returns {*}
*/
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
}
/**
* 格式化id为路径
* @type {id2Uri}
*/
seajs.resolve = id2Uri;
// Check environment
var isWebWorker = typeof window === 'undefined' && typeof importScripts !== 'undefined' && isFunction(importScripts) ;
//部分浏览器打开的地址为:about:xxx and blob:xxx,忽略这些地址
var IGNORE_LOCATION_RE = /^(about|blob):/;
var loaderDir;
// Sea.js's full path
var loaderPath;
//获取当前的工作目录,在web浏览器使用场景下,就是当前请求的地址,在路径“/”之前的字符
var cwd = (!location.href || IGNORE_LOCATION_RE.test(location.href)) ? '' : dirname(location.href);
if (isWebWorker) {
// Web worker doesn't create DOM object when loading scripts
// Get sea.js's path by stack trace.
var stack;
try {
var up = new Error();
throw up;
} catch (e) {
// IE won't set Error.stack until thrown
stack = e.stack.split('\n');
}
// First line is 'Error'
stack.shift();
var m;
// Try match `url:row:col` from stack trace line. Known formats:
// Chrome: ' at http://localhost:8000/script/sea-worker-debug.js:294:25'
// FireFox: '@http://localhost:8000/script/sea-worker-debug.js:1082:1'
// IE11: ' at Anonymous function (http://localhost:8000/script/sea-worker-debug.js:295:5)'
// Don't care about older browsers since web worker is an HTML5 feature
var TRACE_RE = /.*?((?:http|https|file)(?::\/{2}[\w]+)(?:[\/|\.]?)(?:[^\s"]*)).*?/i
// Try match `url` (Note: in IE there will be a tailing ')')
var URL_RE = /(.*?):\d+:\d+\)?$/;
// Find url of from stack trace.
// Cannot simply read the first one because sometimes we will get:
// Error
// at Error (native) <- Here's your problem
// at http://localhost:8000/_site/dist/sea.js:2:4334 <- What we want
// at http://localhost:8000/_site/dist/sea.js:2:8386
// at http://localhost:8000/_site/tests/specs/web-worker/worker.js:3:1
while (stack.length > 0) {
var top = stack.shift();
m = TRACE_RE.exec(top);
if (m != null) {
break;
}
}
var url;
if (m != null) {
// Remove line number and column number
// No need to check, can't be wrong at this point
var url = URL_RE.exec(m[1])[1];
}
// Set
loaderPath = url
// Set loaderDir
loaderDir = dirname(url || cwd);
// This happens with inline worker.
// When entrance script's location.href is a blob url,
// cwd will not be available.
// Fall back to loaderDir.
if (cwd === '') {
cwd = loaderDir;
}
}else {
var doc = document ;
var scripts = doc.scripts ;
//当前js文件加载后运行时的script脚本命令行
var loaderScript = doc.getElementById("seajsnode") ||
scripts[scripts.length - 1] ;
function getScriptAbsoluteSrc(node) {
return node.hasAttribute ? // non-IE6/7
node.src :
// see http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
node.getAttribute("src", 4)
}
loaderPath = getScriptAbsoluteSrc(loaderScript) ;
// When `sea.js` is inline, set loaderDir to current working directory
loaderDir = dirname(loaderPath || cwd)
//console.log("loaderPath="+loaderPath+" loaderDir="+loaderDir)
}
/**
* util-request.js - The utilities for requesting script and style files
* ref: tests/research/load-js-css/test.html
*/
if (isWebWorker) {
function requestFromWebWorker(url, callback, charset) {
// Load with importScripts
var error;
try {
importScripts(url);
} catch (e) {
error = e;
}
callback(error);
}
// For Developers
seajs.request = requestFromWebWorker;
}
else {
var doc = document ;
var head = doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement ;
var baseElement = head.getElementsByTagName("base")[0] ;
var currentlyAddingScript ;
function request(url, callback, charset) {
var node = doc.createElement("script") ;
if (charset) {
var cs = isFunction(charset) ? charset(url) : charset ;
if (cs) {
node.charset = cs
}
}
addOnload(node, callback, url) ;
node.async = true ;
node.src = url ;
// For some cache cases in IE 6-8, the script executes IMMEDIATELY after
// the end of the insert execution, so use `currentlyAddingScript` to
// hold current node, for deriving url in `define` call
currentlyAddingScript = node
// ref: #185 & http://dev.jquery.com/ticket/2709
baseElement ?
head.insertBefore(node, baseElement) :
head.appendChild(node) ;
currentlyAddingScript = null
}
function addOnload(node, callback, url) {
var supportOnload = "onload" in node ;
if (supportOnload) {
node.onload = onload ;
node.onerror = function() {
emit("error", { uri: url, node: node }) ;
onload(true) ;
}
}else {
node.onreadystatechange = function() {
if (/loaded|complete/.test(node.readyState)) {
onload() ;
}
}
}
function onload(error) {
// Ensure only run once and handle memory leak in IE
node.onload = node.onerror = node.onreadystatechange = null ;
// Remove the script to reduce memory leak
if (!data.debug) {
head.removeChild(node) ;
}
// Dereference the node
node = null ;
callback(error) ;
}
}
//For Developers,默认的request方法
seajs.request = request
}
var interactiveScript ;
function getCurrentScript() {
if (currentlyAddingScript) {
return currentlyAddingScript ;
}
/**
* In non-IE browsers, the onload event is sufficient, it always fires
immediately after the script is executed.
* In IE, if the script is in the cache, it actually executes *during*
the DOM insertion of the script tag, so you can keep track of which
script is being requested in case define() is called during the DOM
insertion.
* In IE, if the script is not in the cache, when define() is called you
can iterate through the script tags and the currently executing one will
have a script.readyState == "interactive"
**/
if (interactiveScript && interactiveScript.readyState === "interactive") {
return interactiveScript ;
}
var scripts = head.getElementsByTagName("script") ;
for (var i = scripts.length - 1; i >= 0; i--) {
var script = scripts[i] ;
if (script.readyState === "interactive") {
interactiveScript = script ;
return interactiveScript ;
}
}
}
/**
* 解析文件中的require依赖。
* 主要解析方式为:
* 1、提取第一个字符,如果是字母,则定位require开头的文本位置。
* 2、解析出require内容,通过使用“'”或“"”
* 通过正则解析require开头的字符,获取requrie内部的模块
**/
function parseDependencies(s) {
if(s.indexOf('require') == -1) {
return []
}
var index = 0, peek, length = s.length, isReg = 1, modName = 0, parentheseState = 0,
parentheseStack = [], res = [] ;
while(index < length) {
readch() ;
if(isBlank()) {
}else if(isQuote()) {
dealQuote()
isReg = 1
} else if(peek == '/') {
readch()
if(peek == '/') {
index = s.indexOf('\n', index)
if(index == -1) {
index = s.length
}
}else if(peek == '*') {
index = s.indexOf('*/', index)
if(index == -1) {
index = length
}else {
index += 2
}
}else if(isReg) {
dealReg()
isReg = 0
}else {
index--
isReg = 1
}
}
else if(isWord()) {
dealWord()
}
else if(isNumber()) {
dealNumber()
}
else if(peek == '(') {
parentheseStack.push(parentheseState)
isReg = 1
}
else if(peek == ')') {
isReg = parentheseStack.pop()
}
else {
isReg = peek != ']'
modName = 0
}
}
return res
function readch() {
peek = s.charAt(index++)
}
function isBlank() {
return /\s/.test(peek)
}
function isQuote() {
return peek == '"' || peek == "'"
}
//查找下一个引用字符的位置,并把当前被引用的内容加入到依赖列表中
function dealQuote() {
var start = index ;
var c = peek ;
//查找后续出现的引用字符
var end = s.indexOf(c, start)
if(end == -1) {
index = length
}else if(s.charAt(end - 1) != '\\') {
index = end + 1
}else {
while(index < length) {
readch()
if(peek == '\\') {
index++
}
else if(peek == c) {
break
}
}
}
//判断是否已经解析了require开头的字符
if(modName) {
//单独存储依赖信息
res.push(s.slice(start, index - 1))
modName = 0
}
}
function dealReg() {
index--
while(index < length) {
readch()
if(peek == '\\') {
index++
}
else if(peek == '/') {
break
}
else if(peek == '[') {
while(index < length) {
readch()
if(peek == '\\') {
index++
}
else if(peek == ']') {
break
}
}
}
}
}
function isWord() {
return /[a-z_$]/i.test(peek)
}
function dealWord() {
var s2 = s.slice(index - 1)
var r = /^[\w$]+/.exec(s2)[0];
//判断是否为特殊的符号
parentheseState = {
'if': 1,
'for': 1,
'while': 1,
'with': 1
}[r]
isReg = {
'break': 1,
'case': 1,
'continue': 1,
'debugger': 1,
'delete': 1,
'do': 1,
'else': 1,
'false': 1,
'if': 1,
'in': 1,
'instanceof': 1,
'return': 1,
'typeof': 1,
'void': 1
}[r];
//判断是否是require开头的字符。此处正则表达式用到了“反向向匹配”,其中“\1”指向前面“()”中的内容。
modName = /^require\s*\(\s*(['"]).+?\1\s*\)/.test(s2) ;
//如果符合以上格式,则只获取require('字符串
if(modName) {
r = /^require\s*\(\s*['"]/.exec(s2)[0]
index += r.length - 2
}else {
index += /^[\w$]+(?:\s*\.\s*[\w$]+)*/.exec(s2)[0].length - 1
}
}
function isNumber() {
return /\d/.test(peek)
|| peek == '.' && /\d/.test(s.charAt(index))
}
function dealNumber() {
var s2 = s.slice(index - 1)
var r
if(peek == '.') {
r = /^\.\d+(?:E[+-]?\d*)?\s*/i.exec(s2)[0]
}
else if(/^0x[\da-f]*/i.test(s2)) {
r = /^0x[\da-f]*\s*/i.exec(s2)[0]
}
else {
r = /^\d+\.?\d*(?:E[+-]?\d*)?\s*/i.exec(s2)[0]
}
index += r.length - 1
isReg = 0
}
}
//========================module相关操作========================
//模块缓存,存储uri和module的映射
var cachedMods = seajs.cache = {} ;
var anonymousMeta ;
//正在加载的模块
var fetchingList = {} ;
//已经获取的模块
var fetchedList = {} ;
//ajax请求完成之后module的回调函数。通过回调函数加载模块依赖的js
var callbackList = {} ;
var STATUS = Module.STATUS = {
// 1 - The `module.uri` is being fetched
FETCHING: 1,
// 2 - The meta data has been saved to cachedMods
SAVED: 2,
// 3 - The `module.dependencies` are being loaded
LOADING: 3,
// 4 - The module are ready to execute
LOADED: 4,
// 5 - The module is being executed
EXECUTING: 5,
// 6 - The `module.exports` is available
EXECUTED: 6,
// 7 - 404
ERROR: 7
}
//seajs的基础模块类,seajs.use 或 require都是以该类为基础
function Module(uri, deps) {
this.uri = uri ;
//存储依赖的名称
this.dependencies = deps || [] ;
//存储依赖名称和访问url的映射
this.deps = {} ;
//当前模块及依赖加载的加载的阶段
this.status = 0 ;
//当前模块的入口模块,例如使用seajs.use(id , factory),是一个顶层模块,其中id是entry,可以看做是一个入口
//entry在按照依赖的层级,按照层次不断传播
this._entry = [] ;
}
/**
* 解析模块依赖,返回依赖模块的实际访问地址
* @returns {Array}
*/
Module.prototype.resolve = function() {
var mod = this ;
var ids = mod.dependencies ;
var uris = [] ;
//解析各个依赖的的url
for (var i = 0, len = ids.length; i < len; i++) {
uris[i] = Module.resolve(ids[i], mod.uri) ;
}
return uris ;
}
/**
* 传递模块
*/
Module.prototype.pass = function() {
var mod = this ;
var len = mod.dependencies.length ;
//遍历当前模块的入口
for (var i = 0; i < mod._entry.length; i++) {
var entry = mod._entry[i] ;
var count = 0 ;
//遍历当前模块的依赖
for (var j = 0; j < len; j++) {
//遍历依赖的模块
var m = mod.deps[mod.dependencies[j]] ;
//如果模块没有装载 并且 模块没有被使用,则设置最初入口的
if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) {
//记录已经传递依赖的entry
entry.history[m.uri] = true ;
count++ ;
m._entry.push(entry) ;
if(m.status === STATUS.LOADING) {
m.pass() ;
}
}
}
//如果传递了entry到其依赖的模块中,就entry从当前模块中删掉。但是通过remain数量增加,来标识需要额外加载的模块
if (count > 0) {
//由于remain从1开始,包括当前模块。而开始的顶层依赖是没有module的及seajs.use
entry.remain += count - 1 ;
//将当前entry删掉,继续对下一个执行循环
mod._entry.shift() ;
i-- ;
}
}
}
/**
* 装载模块的依赖
*/
Module.prototype.load = function() {
var mod = this ;
// If the module is being loaded, just wait it onload call
if (mod.status >= STATUS.LOADING) {
return
}
mod.status = STATUS.LOADING ;
//解析依赖的各个插件的url
var uris = mod.resolve() ;
emit("load", uris) ;
//在依赖中存储相关对象
for (var i = 0, len = uris.length; i < len; i++) {
//console.log("存储模块依赖子模块:"+mod.dependencies[i])
mod.deps[mod.dependencies[i]] = Module.get(uris[i])
}
//每次状态完成在之后,都会执行load函数,在此处重新设置和检查依赖关系
mod.pass() ;
//如果当前模块的entry没有被传递,即没有依赖其它模块,则运行onload
if (mod._entry.length) {
mod.onload() ;
return
}
//开始并行加载
var requestCache = {} ;
var m ;
//开始加载依赖的模块
for (i = 0; i < len; i++) {
m = cachedMods[uris[i]] ;
if (m.status < STATUS.FETCHING) {
m.fetch(requestCache) ;
}else if (m.status === STATUS.SAVED) {
m.load() ;
}
}
//Send all requests at last to avoid cache bug in IE6-9. Issues#808
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
requestCache[requestUri]() ;
}
}
}
// Call this method when module is loaded
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
// When sometimes cached in IE, exec will occur before onload, make sure len is an number
for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
var entry = mod._entry[i] ;
if (--entry.remain === 0) {
entry.callback() ;
}
}
delete mod._entry ;
}
// Call this method when module is 404
Module.prototype.error = function() {
var mod = this ;
mod.onload() ;
mod.status = STATUS.ERROR ;
}
// Execute a module
Module.prototype.exec = function () {
var mod = this ;
// When module is executed, DO NOT execute it again. When module
// is being executed, just return `module.exports` too, for avoiding
// circularly calling
if (mod.status >= STATUS.EXECUTING) {
//如果返回的是一个全局的object对象
return mod.exports ;
}
mod.status = STATUS.EXECUTING ;
if (mod._entry && !mod._entry.length) {
delete mod._entry
}
//non-cmd module has no property factory and exports
if (!mod.hasOwnProperty('factory')) {
mod.non = true
return
}
// Create require
var uri = mod.uri
/**
* 传递到define的factory中作为参数
* @param id
* @returns {*|Array|{index: number, input: string}}
*/
function require(id) {
//模块已经在开始加载的时候解析完成了,此时可以直接获取相关的module
var m = mod.deps[id] || Module.get(require.resolve(id)) ;
if (m.status == STATUS.ERROR) {
throw new Error('module was broken: ' + m.uri);
}
return m.exec() ;
}
require.resolve = function(id) {
return Module.resolve(id, uri) ;
}
/**
* 此处的require.async和seajs.use实现的功能是类似的
* @param ids
* @param callback
* @returns {require}
*/
require.async = function(ids, callback) {
Module.use(ids, callback, uri + "_async_" + cid()) ;
return require ;
}
// Exec factory
var factory = mod.factory ;
var exports = isFunction(factory) ?
factory(require, mod.exports = {}, mod) :
factory ;
if (exports === undefined) {
exports = mod.exports ;
}
// Reduce memory leak
delete mod.factory ;
mod.exports = exports ;
mod.status = STATUS.EXECUTED ;
// Emit `exec` event
emit("exec", mod)
return mod.exports
}
/**
* 远程加载文件
* @param requestCache
*/
Module.prototype.fetch = function(requestCache) {
var mod = this ;
var uri = mod.uri ;
mod.status = STATUS.FETCHING ;
var emitData = { uri: uri } ;
emit("fetch", emitData) ;
var requestUri = emitData.requestUri || uri ;
// Empty uri or a non-CMD module
if (!requestUri || fetchedList.hasOwnProperty(requestUri)) {
mod.load() ;
return ;
}
//如果当前获取uri中已包含指定的uri,则回调列表中加入当前module
if (fetchingList.hasOwnProperty(requestUri)) {
callbackList[requestUri].push(mod) ;
return ;
}
fetchingList[requestUri] = true ;
//将当前模块加入回调列表,当前模块加载完成之后,再去加载其依赖的其它模块
callbackList[requestUri] = [mod] ;
//触发request请求
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: isFunction(data.charset) ? data.charset(requestUri) || 'utf-8' : data.charset
}) ;
//标识是否已经由事件处理函数对请求做了处理
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
function onRequest(error) {
delete fetchingList[requestUri] ;
fetchedList[requestUri] = true ;
// Save meta data of anonymous module
if (anonymousMeta) {
Module.save(uri, anonymousMeta) ;
anonymousMeta = null ;
}
// Call callbacks
var m, mods = callbackList[requestUri] ;
delete callbackList[requestUri] ;
while ((m = mods.shift())) {
// When 404 occurs, the params error will be true
if(error === true) {
m.error() ;
} else {
//执行当前模块的加载,加载当前模块依赖的其它模块
m.load() ;
}
}
}
}
/**
* 解析模块的完整访问地址。id为依赖模块的id,refUri源模块的uri
* @param id
* @param refUri
* @returns {*}
*/
Module.resolve = function(id, refUri) {
// Emit `resolve` event for plugins such as text plugin
var emitData = { id: id, refUri: refUri } ;
//console.log(id+"模块解析:"+refUri);
//此处提现出了很好的扩展性,使用事件机制,优先调用注册的解析方法。如果解析失败,才掉用seajs本身的resolve方法
emit("resolve", emitData) ;
return emitData.uri || seajs.resolve(emitData.id, refUri) ;
}
//Define a module
Module.define = function (id, deps, factory) {
var argsLen = arguments.length ;
// define(factory)
if (argsLen === 1) {
factory = id ;
id = undefined
}else if (argsLen === 2) {
factory = deps ;
// define(deps, factory)
if (isArray(id)) {
deps = id ;
id = undefined
}
// define(id, factory)
else {
deps = undefined
}
}
// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString())
}
var meta = {
id: id ,
uri: Module.resolve(id) ,
deps: deps ,
factory: factory
}
// Try to derive uri in IE6-9 for anonymous modules
if (!isWebWorker && !meta.uri && doc.attachEvent && typeof getCurrentScript !== "undefined") {
var script = getCurrentScript()
if (script) {
meta.uri = script.src
}
// NOTE: If the id-deriving methods above is failed, then falls back
// to use onload event to get the uri
}
// Emit `define` event, used in nocache plugin, seajs node version etc
emit("define", meta)
meta.uri ? Module.save(meta.uri, meta) :
// Save information for "saving" work in the script onload event
anonymousMeta = meta
}
// Save meta data to cachedMods
Module.save = function(uri, meta) {
var mod = Module.get(uri)
// Do NOT override already saved modules
if (mod.status < STATUS.SAVED) {
mod.id = meta.id || uri
mod.dependencies = meta.deps || []
mod.factory = meta.factory
mod.status = STATUS.SAVED
emit("save", mod)
}
}
// Get an existed module or create a new one
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}
// Use function is equal to load a anonymous module
Module.use = function (ids, callback, uri) {
var mod = Module.get(uri, isArray(ids) ? ids : [ids]) ;
mod._entry.push(mod)
mod.history = {}
mod.remain = 1
mod.callback = function() {
var exports = [] ;
var uris = mod.resolve() ;
for (var i = 0, len = uris.length; i < len; i++) {
exports[i] = cachedMods[uris[i]].exec() ;
}
if (callback) {
callback.apply(global, exports)
}
delete mod.callback
delete mod.history
delete mod.remain
delete mod._entry
}
mod.load()
}
// Public API
seajs.use = function(ids, callback) {
//debugger
Module.use(ids, callback, data.cwd + "_use_" + cid())
return seajs
}
Module.define.cmd = {}
global.define = Module.define
// For Developers
seajs.Module = Module
data.fetchedList = fetchedList
data.cid = cid
seajs.require = function(id) {
var mod = Module.get(Module.resolve(id)) ;
if (mod.status < STATUS.EXECUTING) {
mod.onload() ;
mod.exec() ;
}
return mod.exports
}
/**
* config.js - The configuration for the loader
*/
// The root path to use for id2uri parsing
data.base = loaderDir
// The loader directory
data.dir = loaderDir
// The loader's full path
data.loader = loaderPath
// The current working directory
data.cwd = cwd
// The charset for requesting files
data.charset = "utf-8"
// data.alias - An object containing shorthands of module id
// data.paths - An object containing path shorthands in module id
// data.vars - The {xxx} variables in module id
// data.map - An array containing rules to map module uri
// data.debug - Debug mode. The default value is false
seajs.config = function(configData) {
for (var key in configData) {
var curr = configData[key] ;
var prev = data[key] ;
// Merge object config such as alias, vars
if (prev && isObject(prev)) {
for (var k in curr) {
prev[k] = curr[k]
}
}else {
// Concat array config such as map
if (isArray(prev)) {
//连接prev和curr数组
curr = prev.concat(curr)
}
// Make sure that `data.base` is an absolute path
else if (key === "base") {
// Make sure end with "/"
if (curr.slice(-1) !== "/") {
curr += "/"
}
curr = addBase(curr)
}
// Set config
data[key] = curr
}
}
emit("config", configData)
return seajs
}
})(this);
seajs-wrap-debug.js,通过扩展request事件,对返回的js内容动态追加define(),避免在每个js中都要手写define函数。
/**
* The Sea.js plugin for loading CommonJS file
*/
var global = window
var wrapExec = {}
seajs.on("resolve" , function(data) {
var id = data.id
if (!id) {
return ""
}
// avoid seajs-css plugin conflict
if (/\.css\.js$/.test(id)) {
return;
}
var m = id.match(/[^?]+?(\.\w+)?(\?.*)?$/)
// not parse those types
var WhiteListReg = /\.(tpl|html|json|handlebars|css)/i
if (m && (!WhiteListReg.test(m[1]) || !m[1])) {
var uri = seajs.resolve(id, data.refUri)
var query = m[2] || '';
wrapExec[uri] = function(uri, content) {
var wrappedContent;
// var CMD_REG = /define\(.*function\s*\(\s*require\s*(.*)?\)\s*\{/;
var CMD_REG = /define\(.*function\s*\(\s*(.*)?\)\s*\{/;
//如果带有dowrap,则一定用define包装;否则根据是否带有define和nowrap判断是否封装
if (uri.indexOf('dowrap')==-1&&(CMD_REG.test(content) ||
query.indexOf('nowrap') > 0 || uri.indexOf('nowrap')>0)) {
wrappedContent= content;
} else {
wrappedContent = 'define(function(require, exports, module) {\n' +
content + '\n})';
}
wrappedContent = wrappedContent + '//# sourceURL=' + uri;
globalEval(wrappedContent, uri) ;
}
data.uri = uri
}
})
seajs.on("request", function(data) {
var exec = wrapExec[data.uri]
if (exec) {
xhr(data.requestUri, function(content) {
exec(data.uri, content)
data.onRequest()
})
data.requested = true
}
})
// Helpers
function xhr(url, callback) {
//modified , 解决ie10跨域加载js问题。首先使用XMLHttpRequest,ActiveXObject是ie7之前版本
var r = global.XMLHttpRequest ? new global.XMLHttpRequest() :
new global.ActiveXObject("Microsoft.XMLHTTP") ;
try{
r.open("GET", url, true) ;
}catch (e){
return failoverxhr(url, callback) ;
}
r.onreadystatechange = function() {
if (r.readyState === 4) {
// Support local file
if (r.status > 399 && r.status < 600) {
failoverxhr(url, callback) ;
}else {
callback(r.responseText)
}
}
}
//发送请求结果
var result = null ;
try{
result = r.send(null) ;
}catch (e){
return failoverxhr(url, callback) ;
}
return result ;
}
/**
* 静态资源请求失败的处理
*/
function failoverxhr(url, callback){
var r = global.XMLHttpRequest ? new global.XMLHttpRequest() :
new global.ActiveXObject("Microsoft.XMLHTTP") ;
url = getLocalAppUrl(url) ;
console.log("use backup "+url);
r.open("GET", url, true) ;
r.onreadystatechange = function() {
if (r.readyState === 4) {
// Support local file
if (r.status > 399 && r.status < 600) {
seajs.emit("failover error", {
uri: url ,
status: r.status
}) ;
}else {
callback(r.responseText)
}
}
}
return r.send(null) ;
}
/**
* 由本项目中获取请求的url
*/
function getLocalAppUrl(resourceURL) {
//获取静态资源的uri
var urlReg = /\/resources.*/ ;
var uri = urlReg.exec(resourceURL) ;
//如果配置了项目的url,则有url中请求静态资源
if(uri && window.resourceConfig && window.resourceConfig.path){
uri = window.resourceConfig.path + uri ;
}
return uri ;
}
function globalEval(content, uri) {
if (content && /\S/.test(content)) {
//global.execScript ||
(function(content) {
try {
//var startDate = new Date();
(global.eval || eval).call(global, content)
//var endDate = new Date();
//console.log(uri+" start-> "+(endDate.getTime() - startDate.getTime())+" ms") ;
} catch(ex) {
ex.fileName = uri ;
console.error(ex) ;
}
})(content)
}
}
seajs-css-debug.js : 支持seajs对css资源进行require,在git上提供了一个seajs-css插件,它是直接针对seajs源码进行的改动,有点不友好。以下代码是借助于seajs的事件机制实现css的加载。
(function(global){
var doc = document ;
var head = doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement ;
var baseElement = head.getElementsByTagName("base")[0] ;
//标识是否是css资源,"?:"取消“()”的捕获能力,只是用它来做分组
var IS_CSS_RE = /\.css(?:\?|$)/i ;
seajs.on("resolve" , function(data) {
var id = data.id
if (!id) {
return ""
}
//只解析css结束的样式文件
if (! IS_CSS_RE.test(id) ) {
return ;
}
var uri = seajs.resolve(id, data.refUri) ;
//如果解析后的uri是以".css.js"结尾,是因为seajs对于css加载本身不支持导致
if( /\.css\.js$/.test(uri) ){
uri = uri.substring(0 , uri.length - 3 ) ;
}
//console.log(id+" 解析地址: "+uri);
data.uri = uri ;
}) ;
function cssOnload(node , data) {
node.onload = node.onerror = node.onreadystatechange = null ;
node = null ;
//console.log("资源加载完成:"+data.requestUri) ;
data.onRequest() ;
}
//只针对css进行处理
seajs.on("request", function(data) {
if(! IS_CSS_RE.test(data.requestUri) ){
return ;
}
//console.log("css请求地址:"+data.requestUri) ;
var node = document.createElement("link") ;
if (data.charset) {
node.charset = data.charset ;
}
//资源加载完成的处理函数
var supportOnload = "onload" in node ;
if (supportOnload) {
node.onload = cssOnload.bind(global , node , data) ;
node.onerror = function() {
seajs.emit("error", { uri: url, node: node })
cssOnload(node , data) ;
}
}else {
node.onreadystatechange = function() {
if (/loaded|complete/.test(node.readyState)) {
cssOnload(node , data) ;
}
}
}
//addOnload(node, callback, isCSS, url) ;
node.rel = "stylesheet" ;
node.href = data.requestUri ;
//加入元素
baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node) ;
//标识资源已经在此进行了处理
data.requested = true ;
}) ;
})(window) ;
seajs源码执行流程:
seajs源码中很好的设计,仅列出如下几点:
- 事件机制。seajs通过事件机制实现扩展,例如插件:seajs-wrap 和 seajs-css 都是借助于seajs的事件机制进行的扩展。这块也可以在前端业务的框架中借鉴。
- 依赖解析。在js加载完成之后,实际上执行的是define函数,在define中没有立刻执行factory函数,而是通过解析factory函数内容,获取其依赖模块并加载,待所有依赖的模块加载完成之后,再执行factory函数,即AMD推崇的依赖前置。此处也可以通过修改代码使用CMD的规则,即延迟加载依赖模块。
- 模块加载。seajs通过模块加载解决了命名冲突(相同名称的变量或函数、多个不同名称或层级的命名空间管理)、模块化、版本化等问题。在js加载速度上seajs体现的并不明显,特别是现在浏览器都支持多个js文件同时加载的情况下。(seajs模块化根本上是将js的执行结果封装到了module.exports中,并通过参数的方式传递到factory中)
- 正则表达式。在seajs源码中使用了很多正则对请求url、参数等进行判断和处理。通过seajs中正则,学习了正则表达式的贪婪匹配、非贪婪匹配、不捕捉模式、前瞻匹配、后瞻匹配、负前瞻匹配、负后瞻匹配、反向引用匹配等。