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中正则,学习了正则表达式的贪婪匹配、非贪婪匹配、不捕捉模式、前瞻匹配、后瞻匹配、负前瞻匹配、负后瞻匹配、反向引用匹配等。