• 【原创】seajs1.3.0源码解析之module


    这里是seajs loader的核心部分,有些IE兼容的部分还不是很明白,大虾路过的话,求教~

    主要是理解各个模块如何依赖有序加载,以及CMD规范。

    代码有点长,需要耐心看:

      1 /**
      2  * The core of loader
      3  */
      4 ;(function(seajs, util, config) {
      5   // 模块缓存
      6   var cachedModules = {}
      7   // 接口修改缓存
      8   var cachedModifiers = {}
      9   // 编译队列
     10   var compileStack = []
     11   // 模块状态
     12   var STATUS = {
     13     'FETCHING': 1,  // The module file is fetching now.   模块正在下载中
     14     'FETCHED': 2,   // The module file has been fetched.  模块已下载
     15     'SAVED': 3,     // The module info has been saved.    模块信息已保存
     16     'READY': 4,     // All dependencies and self are ready to compile. 模块的依赖项都已下载,等待编译
     17     'COMPILING': 5, // The module is in compiling now.    模块正在编译中
     18     'COMPILED': 6   // The module is compiled and module.exports is available.  模块已编译
     19   }
     20 
     21 
     22   function Module(uri, status) {
     23     this.uri = uri
     24     this.status = status || 0
     25 
     26     // this.id is set when saving
     27     // this.dependencies is set when saving
     28     // this.factory is set when saving
     29     // this.exports is set when compiling
     30     // this.parent is set when compiling
     31     // this.require is set when compiling
     32   }
     33 
     34 
     35   Module.prototype._use = function(ids, callback) {
     36     //转换为数组,统一操作
     37     util.isString(ids) && (ids = [ids])
     38     // 使用模块系统内部的路径解析机制来解析并返回模块路径
     39     var uris = resolve(ids, this.uri)
     40 
     41     this._load(uris, function() {
     42       // Loads preload files introduced in modules before compiling.
     43       // 在编译之前,再次调用preload预加载模块
     44       // 因为在代码执行期间,随时可以调用seajs.config配置预加载模块
     45       preload(function() {
     46         // 编译每个模块,并将各个模块的exports作为参数传递给回调函数
     47         var args = util.map(uris, function(uri) {
     48           return uri ? cachedModules[uri]._compile() : null
     49         })
     50 
     51         if (callback) {
     52           // null使回调函数中this指针为window
     53           callback.apply(null, args)
     54         }
     55       })
     56     })
     57   }
     58 
     59   // 主模块加载依赖模块(称之为子模块),并执行回调函数
     60   Module.prototype._load = function(uris, callback) {
     61     // 过滤uris数组
     62     // 情况一:缓存中不存在该模块,返回其uri
     63     // 情况二:缓存中存在该模块,但是其status < STATUS.READY(即还没准备好编译)
     64     var unLoadedUris = util.filter(uris, function(uri) {
     65       return uri && (!cachedModules[uri] ||
     66           cachedModules[uri].status < STATUS.READY)
     67     })
     68 
     69     var length = unLoadedUris.length
     70     // 如果length为0,表示依赖项为0或者都已下载完成,那么执行回调编译操作
     71     if (length === 0) {
     72       callback()
     73       return
     74     }
     75 
     76     var remain = length
     77 
     78     for (var i = 0; i < length; i++) {
     79       // 闭包,为onFetched函数提供上下文环境
     80       (function(uri) {
     81         // 创建模块对象
     82         var module = cachedModules[uri] ||
     83             (cachedModules[uri] = new Module(uri, STATUS.FETCHING))
     84         //如果模块已下载,那么执行onFetched,否则执行fetch操作(请求模块)
     85         module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)
     86 
     87         function onFetched() {
     88           // cachedModules[uri] is changed in un-correspondence case
     89           module = cachedModules[uri]
     90           // 如果模块状态为SAVED,表示模块的依赖项已经确定,那么下载依赖模块
     91           if (module.status >= STATUS.SAVED) {
     92             // 从模块信息中获取依赖模块列表,并作循环依赖的处理
     93             var deps = getPureDependencies(module)
     94             // 如果存在依赖项,继续下载
     95             if (deps.length) {
     96               Module.prototype._load(deps, function() {
     97                 cb(module)
     98               })
     99             }
    100             // 否则直接执行cb
    101             else {
    102               cb(module)
    103             }
    104           }
    105           // Maybe failed to fetch successfully, such as 404 or non-module.
    106           // In these cases, just call cb function directly.
    107           // 如果下载模块不成功,比如404或者模块不规范(代码出错),导致此时模块状态可能为fetching,或者fetched
    108           // 此时直接执行回调函数,在编译模块时,该模块就只会返回null
    109           else {
    110             cb()
    111           }
    112         }
    113 
    114       })(unLoadedUris[i])
    115     }
    116 
    117     function cb(module) {
    118       // 更改模块状态为READY,当remain为0时表示模块依赖都已经下完,那么执行callback
    119       (module || {}).status < STATUS.READY && (module.status = STATUS.READY)
    120       --remain === 0 && callback()
    121     }
    122   }
    123 
    124 
    125   Module.prototype._compile = function() {
    126     var module = this
    127     // 如果该模块已经编译过,则直接返回module.exports
    128     if (module.status === STATUS.COMPILED) {
    129       return module.exports
    130     }
    131 
    132     // Just return null when:
    133     //  1. the module file is 404.
    134     //  2. the module file is not written with valid module format.
    135     //  3. other error cases.
    136     // 这里是处理一些异常情况,此时直接返回null
    137     if (module.status < STATUS.SAVED && !hasModifiers(module)) {
    138       return null
    139     }
    140     // 更改模块状态为COMPILING,表示模块正在编译
    141     module.status = STATUS.COMPILING
    142 
    143     // 模块内部使用,是一个方法,用来获取其他模块提供(称之为子模块)的接口,同步操作
    144     function require(id) {
    145       // 根据id解析模块的路径
    146       var uri = resolve(id, module.uri)
    147       // 从模块缓存中获取模块(注意,其实这里子模块作为主模块的依赖项是已经被下载下来的)
    148       var child = cachedModules[uri]
    149 
    150       // Just return null when uri is invalid.
    151       // 如果child为空,只能表示参数填写出错导致uri不正确,那么直接返回null
    152       if (!child) {
    153         return null
    154       }
    155 
    156       // Avoids circular calls.
    157       // 如果子模块的状态为STATUS.COMPILING,直接返回child.exports,避免因为循环依赖反复编译模块
    158       if (child.status === STATUS.COMPILING) {
    159         return child.exports
    160       }
    161       // 指向初始化时调用当前模块的模块。根据该属性,可以得到模块初始化时的Call Stack.
    162       child.parent = module
    163       // 返回编译过的child的module.exports
    164       return child._compile()
    165     }
    166     // 模块内部使用,用来异步加载模块,并在加载完成后执行指定回调。
    167     require.async = function(ids, callback) {
    168       module._use(ids, callback)
    169     }
    170     // 使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
    171     require.resolve = function(id) {
    172       return resolve(id, module.uri)
    173     }
    174     // 通过该属性,可以查看到模块系统加载过的所有模块。
    175     // 在某些情况下,如果需要重新加载某个模块,可以得到该模块的 uri, 然后通过 delete require.cache[uri] 来将其信息删除掉。这样下次使用时,就会重新获取。
    176     require.cache = cachedModules
    177 
    178     // require是一个方法,用来获取其他模块提供的接口。
    179     module.require = require
    180     // exports是一个对象,用来向外提供模块接口。
    181     module.exports = {}
    182     var factory = module.factory
    183 
    184     // factory 为函数时,表示模块的构造方法。执行该方法,可以得到模块向外提供的接口。
    185     if (util.isFunction(factory)) {
    186       compileStack.push(module)
    187       runInModuleContext(factory, module)
    188       compileStack.pop()
    189     }
    190     // factory 为对象、字符串等非函数类型时,表示模块的接口就是该对象、字符串等值。
    191     // 如:define({ "foo": "bar" });
    192     // 如:define('I am a template. My name is {{name}}.');
    193     else if (factory !== undefined) {
    194       module.exports = factory
    195     }
    196 
    197     // 更改模块状态为COMPILED,表示模块已编译
    198     module.status = STATUS.COMPILED
    199     // 执行模块接口修改,通过seajs.modify()
    200     execModifiers(module)
    201     return module.exports
    202   }
    203 
    204 
    205   Module._define = function(id, deps, factory) {
    206     var argsLength = arguments.length
    207     // 根据传入的参数个数,进行参数匹配
    208 
    209     // define(factory)
    210     // 一个参数的情况:
    211     // id : undefined
    212     // deps : undefined(后面会根据正则取出依赖模块列表)
    213     // factory : function
    214     if (argsLength === 1) {
    215       factory = id
    216       id = undefined
    217     }
    218     // define(id || deps, factory)
    219     // 两个参数的情况:
    220 
    221     else if (argsLength === 2) {
    222       // 默认情况下 :define(id, factory)
    223       // id : '...'
    224       // deps : undefined
    225       // factory : function
    226       factory = deps
    227       deps = undefined
    228 
    229       // define(deps, factory)
    230       // 如果第一个参数为数组 :define(deps, factory)
    231       // id : undefined
    232       // deps : [...]
    233       // factory : function
    234       if (util.isArray(id)) {
    235         deps = id
    236         id = undefined
    237       }
    238     }
    239 
    240     // Parses dependencies.
    241     // 如果deps不是数组(即deps未指定值),那么通过正则表达式解析依赖
    242     if (!util.isArray(deps) && util.isFunction(factory)) {
    243       deps = util.parseDependencies(factory.toString())
    244     }
    245 
    246     // 元信息,之后会将信息传递给对应的module对象中
    247     var meta = { id: id, dependencies: deps, factory: factory }
    248     var derivedUri
    249 
    250     // Try to derive uri in IE6-9 for anonymous modules.
    251     // 对于IE6-9,尝试通过interactive script获取模块的uri
    252     if (document.attachEvent) {
    253       // Try to get the current script.
    254       // 获取当前的script
    255       var script = util.getCurrentScript()
    256       if (script) {
    257         // 将当前script的url进行unpareseMap操作,与模块缓存中key保持一致
    258         derivedUri = util.unParseMap(util.getScriptAbsoluteSrc(script))
    259       }
    260 
    261       if (!derivedUri) {
    262         util.log('Failed to derive URI from interactive script for:',
    263             factory.toString(), 'warn')
    264 
    265         // NOTE: If the id-deriving methods above is failed, then falls back
    266         // to use onload event to get the uri.
    267       }
    268     }
    269 
    270     // Gets uri directly for specific module.
    271     // 如果给定id,那么根据id解析路径
    272     // 显然如果没指定id:
    273     //   对于非IE浏览器而言,则返回undefined(derivedUri为空)
    274     //   对于IE浏览器则返回CurrentScript的src
    275     // 如果指定id:
    276     //   则均返回有seajs解析(resolve)过的路径url
    277     var resolvedUri = id ? resolve(id) : derivedUri
    278     // uri存在的情况,进行模块信息存储
    279     if (resolvedUri) {
    280       // For IE:
    281       // If the first module in a package is not the cachedModules[derivedUri]
    282       // self, it should assign to the correct module when found.
    283       if (resolvedUri === derivedUri) {
    284         var refModule = cachedModules[derivedUri]
    285         if (refModule && refModule.realUri &&
    286             refModule.status === STATUS.SAVED) {
    287           cachedModules[derivedUri] = null
    288         }
    289       }
    290       // 存储模块信息
    291       var module = save(resolvedUri, meta)
    292 
    293       // For IE:
    294       // Assigns the first module in package to cachedModules[derivedUrl]
    295       if (derivedUri) {
    296         // cachedModules[derivedUri] may be undefined in combo case.
    297         if ((cachedModules[derivedUri] || {}).status === STATUS.FETCHING) {
    298           cachedModules[derivedUri] = module
    299           module.realUri = derivedUri
    300         }
    301       }
    302       else {
    303         // 将第一个模块存储到firstModuleInPackage
    304         firstModuleInPackage || (firstModuleInPackage = module)
    305       }
    306     }
    307     // uri不存在的情况,在onload回调中进行模块信息存储,那里有个闭包
    308     else {
    309       // Saves information for "memoizing" work in the onload event.
    310       // 因为此时的uri不知道,所以将元信息暂时存储在anonymousModuleMeta中,在onload回调中进行模块save操作
    311       anonymousModuleMeta = meta
    312     }
    313 
    314   }
    315 
    316   // 获取正在编译的模块
    317   Module._getCompilingModule = function() {
    318     return compileStack[compileStack.length - 1]
    319   }
    320 
    321   // 从seajs.cache中快速查看和获取已加载的模块接口,返回值是module.exports数组
    322   // selector 支持字符串和正则表达式
    323   Module._find = function(selector) {
    324     var matches = []
    325 
    326     util.forEach(util.keys(cachedModules), function(uri) {
    327       if (util.isString(selector) && uri.indexOf(selector) > -1 ||
    328           util.isRegExp(selector) && selector.test(uri)) {
    329         var module = cachedModules[uri]
    330         module.exports && matches.push(module.exports)
    331       }
    332     })
    333 
    334     return matches
    335   }
    336 
    337   // 修改模块接口
    338   Module._modify = function(id, modifier) {
    339     var uri = resolve(id)
    340     var module = cachedModules[uri]
    341     // 如果模块存在,并且处于COMPILED状态,那么执行修改接口操作
    342     if (module && module.status === STATUS.COMPILED) {
    343       runInModuleContext(modifier, module)
    344     }
    345     // 否则放入修改接口缓存中
    346     else {
    347       cachedModifiers[uri] || (cachedModifiers[uri] = [])
    348       cachedModifiers[uri].push(modifier)
    349     }
    350 
    351     return seajs
    352   }
    353 
    354 
    355   // For plugin developers
    356   Module.STATUS = STATUS
    357   Module._resolve = util.id2Uri
    358   Module._fetch = util.fetch
    359   Module.cache = cachedModules
    360 
    361 
    362   // Helpers
    363   // -------
    364   // 正在下载的模块列表
    365   var fetchingList = {}
    366   // 已下载的模块列表
    367   var fetchedList = {}
    368   // 回调函数列表
    369   var callbackList = {}
    370   // 匿名模块元信息
    371   var anonymousModuleMeta = null
    372   var firstModuleInPackage = null
    373   // 循环依赖栈
    374   var circularCheckStack = []
    375 
    376   // 批量解析模块的路径
    377   function resolve(ids, refUri) {
    378     if (util.isString(ids)) {
    379       return Module._resolve(ids, refUri)
    380     }
    381 
    382     return util.map(ids, function(id) {
    383       return resolve(id, refUri)
    384     })
    385   }
    386 
    387   function fetch(uri, callback) {
    388     // fetch时,首先将uri按map规则转换
    389     var requestUri = util.parseMap(uri)
    390     // 在fethedList(已下载的模块列表)中查找,有的话,直接返回,并执行回调函数
    391     // TODO : 为什么这一步,fetchedList可能会存在该模?
    392     if (fetchedList[requestUri]) {
    393       // See test/issues/debug-using-map
    394       cachedModules[uri] = cachedModules[requestUri]
    395       callback()
    396       return
    397     }
    398     // 在fetchingList(正在在下载的模块列表)中查找,有的话,只需添加回调函数到列表中去,然后直接返回
    399     if (fetchingList[requestUri]) {
    400       callbackList[requestUri].push(callback)
    401       return
    402     }
    403     // 如果走到这一步,表示该模块是第一次被请求,
    404     // 那么在fetchingList插入该模块的信息,表示该模块已经处于下载列表中,并初始化该模块对应的回调函数列表
    405     fetchingList[requestUri] = true
    406     callbackList[requestUri] = [callback]
    407 
    408     // Fetches it
    409     // 获取该模块,即发起请求
    410     Module._fetch(
    411         requestUri,
    412 
    413         function() {
    414           // 在fetchedList插入该模块的信息,表示该模块已经下载完成
    415           fetchedList[requestUri] = true
    416 
    417           // Updates module status
    418           var module = cachedModules[uri]
    419           // 此时status可能为STATUS.SAVED,之前在_define中已经说过
    420           if (module.status === STATUS.FETCHING) {
    421             module.status = STATUS.FETCHED
    422           }
    423 
    424           // Saves anonymous module meta data
    425           // 因为是匿名模块(此时通过闭包获取到uri,在这里存储模块信息)
    426           // 并将anonymousModuleMeta置为空
    427           if (anonymousModuleMeta) {
    428             save(uri, anonymousModuleMeta)
    429             anonymousModuleMeta = null
    430           }
    431 
    432           // Assigns the first module in package to cachedModules[uri]
    433           // See: test/issues/un-correspondence
    434           if (firstModuleInPackage && module.status === STATUS.FETCHED) {
    435             cachedModules[uri] = firstModuleInPackage
    436             firstModuleInPackage.realUri = uri
    437           }
    438           firstModuleInPackage = null
    439 
    440           // Clears
    441           // 在fetchingList清除模块信息,因为已经该模块fetched并save
    442           if (fetchingList[requestUri]) {
    443             delete fetchingList[requestUri]
    444           }
    445 
    446           // Calls callbackList
    447           // 依次调用回调函数,并清除回调函数列表
    448           if (callbackList[requestUri]) {
    449             util.forEach(callbackList[requestUri], function(fn) {
    450               fn()
    451             })
    452             delete callbackList[requestUri]
    453           }
    454 
    455         },
    456 
    457         config.charset
    458     )
    459   }
    460 
    461   function save(uri, meta) {
    462     var module = cachedModules[uri] || (cachedModules[uri] = new Module(uri))
    463 
    464     // Don't override already saved module
    465     // 此时status可能有两个状态:
    466     // STATUS.FETCHING,在define里面调用(指定了id),存储模块信息
    467     // STATUS.FETCHED,在onload的回调函数里调用,存储模块信息
    468     if (module.status < STATUS.SAVED) {
    469       // Lets anonymous module id equal to its uri
    470       // 匿名模块(即没有指定id),用它的uri作为id
    471       module.id = meta.id || uri
    472       // 将依赖项(数组)解析成的绝对路径,存储到模块信息中
    473       module.dependencies = resolve(
    474           util.filter(meta.dependencies || [], function(dep) {
    475             return !!dep
    476           }), uri)
    477       // 存储factory(要执行的模块代码,也可能是对象或者字符串等)
    478       module.factory = meta.factory
    479 
    480       // Updates module status
    481       // 更新模块状态为SAVED,(注意此时它只是拥有了依赖项,还未全部下载下来(即还未READY))
    482       module.status = STATUS.SAVED
    483     }
    484 
    485     return module
    486   }
    487 
    488   // 根据模块上下文执行模块代码
    489   function runInModuleContext(fn, module) {
    490     // 传入与模块相关的两个参数以及模块自身
    491     // exports用来暴露接口
    492     // require用来获取依赖模块(同步)(编译)
    493     var ret = fn(module.require, module.exports, module)
    494     // 支持返回值暴露接口形式,如:
    495     // return {
    496     //   fn1 : xx
    497     //   ,fn2 : xx
    498     //   ...
    499     // }
    500     if (ret !== undefined) {
    501       module.exports = ret
    502     }
    503   }
    504   // 判断模块是否存在接口修改
    505   function hasModifiers(module) {
    506     return !!cachedModifiers[module.realUri || module.uri]
    507   }
    508   // 修改模块接口
    509   function execModifiers(module) {
    510     var uri = module.realUri || module.uri
    511     var modifiers = cachedModifiers[uri]
    512     // 内部变量 cachedModifiers 就是用来存储用户通过 seajs.modify 方法定义的修改点
    513     // 查看该uri是否又被modify更改过
    514     if (modifiers) {
    515       // 对修改点统一执行factory,返回修改后的module.exports
    516       util.forEach(modifiers, function(modifier) {
    517         runInModuleContext(modifier, module)
    518       })
    519       // 删除 modify 方法定义的修改点 ,避免再次执行
    520       delete cachedModifiers[uri]
    521     }
    522   }
    523 
    524   //获取纯粹的依赖关系,得到不存在循环依赖关系的依赖数组
    525   function getPureDependencies(module) {
    526     var uri = module.uri
    527     // 对每个依赖项进行过滤,对于有可能形成循环依赖的进行剔除,并打印出警告日志
    528     return util.filter(module.dependencies, function(dep) {
    529       // 首先将被检查模块的uri放到循环依赖检查栈中,之后的检查会用到
    530       circularCheckStack = [uri]
    531       //接下来检查模块uri是否和其依赖的模块存在循环依赖
    532       var isCircular = isCircularWaiting(cachedModules[dep])
    533       if (isCircular) {
    534         // 如果循环,则将uri放到循环依赖检查栈中
    535         circularCheckStack.push(uri)
    536         // 打印出循环警告日志
    537         printCircularLog(circularCheckStack)
    538       }
    539 
    540       return !isCircular
    541     })
    542   }
    543 
    544   function isCircularWaiting(module) {
    545     // 如果依赖模块不存在,那么返回false,因为此时也无法获得依赖模块的依赖项,所以这里无法做判断
    546     // 或者如果模块的状态值等于saved,也返回false,因为模块状态为saved的时候代表该模块的信息已经有了,
    547     // 所以尽管形成了循环依赖,但是require主模块时,同样可以正常编译,返回主模块接口(好像nodejs会返回undefined)
    548     if (!module || module.status !== STATUS.SAVED) {
    549       return false
    550     }
    551     // 如果不是以上的情况,那么将依赖模块的uri放到循环依赖检查栈中,之后的检查会用到
    552     circularCheckStack.push(module.uri)
    553     // 再次取依赖模块的依赖模块
    554     var deps = module.dependencies
    555 
    556     if (deps.length) {
    557       // 通过循环依赖检查栈,检查是否存在循环依赖(这里是第一层依赖模块检查,与主模块循环依赖的情况)
    558       if (isOverlap(deps, circularCheckStack)) {
    559         return true
    560       }
    561       // 如果不存在上述情形,那么进一步查看,依赖模块的依赖模块,查看他们是否存在对循环依赖检查栈中的uri的模块存在循环依赖
    562       // 这样的话,就递归了,循环依赖检查栈就像形成的一条链,当前模块依次对主模块,主模块的主模块...直到最顶上的主模块,依次进行判断是否存在依赖
    563       for (var i = 0; i < deps.length; i++) {
    564         if (isCircularWaiting(cachedModules[deps[i]])) {
    565           return true
    566         }
    567       }
    568     }
    569     // 如果不存在循环依赖,那么pop出之前已经push进的模块uri,并返回false
    570     circularCheckStack.pop()
    571     return false
    572   }
    573   // 打印出循环警告日志
    574   function printCircularLog(stack, type) {
    575     util.log('Found circular dependencies:', stack.join(' --> '), type)
    576   }
    577   //判断两个数组是否有重复的值
    578   function isOverlap(arrA, arrB) {
    579     var arrC = arrA.concat(arrB)
    580     return arrC.length > util.unique(arrC).length
    581   }
    582   // 从配置文件读取是否有需要提前加载的模块
    583   // 如果有预先加载模块,首先设置预加载模块为空(保证下次不必重复加载),并加载预加载模块并执行回调,如果没有则顺序执行
    584   function preload(callback) {
    585     var preloadMods = config.preload.slice()
    586     config.preload = []
    587     preloadMods.length ? globalModule._use(preloadMods, callback) : callback()
    588   }
    589 
    590 
    591   // Public API
    592   // 对外暴露的API
    593   // ----------
    594   // 全局模块,可以认为是页面模块,页面中的js,css文件都是通过它来载入的
    595   // 模块初始状态就是COMPILED,uri就是页面的uri
    596   var globalModule = new Module(util.pageUri, STATUS.COMPILED)
    597 
    598   // 页面js,css文件加载器
    599   seajs.use = function(ids, callback) {
    600     // Loads preload modules before all other modules.
    601     // 预加载模块
    602     preload(function() {
    603       globalModule._use(ids, callback)
    604     })
    605 
    606     // Chain
    607     return seajs
    608   }
    609 
    610 
    611   // For normal users
    612   // 供普通用户调用
    613   seajs.define = Module._define
    614   seajs.cache = Module.cache
    615   seajs.find = Module._find
    616   seajs.modify = Module._modify
    617 
    618 
    619   // For plugin developers
    620   // 供开发者使用
    621   seajs.pluginSDK = {
    622     Module: Module,
    623     util: util,
    624     config: config
    625   }
    626 
    627 })(seajs, seajs._util, seajs._config)
  • 相关阅读:
    windows下安装rocketmq采坑全记录
    测试日常使用---网络协议与抓包
    python重写及重写后调用父类方法
    python继承和多态
    python私有成员都以双下划线“__”开头,仅类内部可访问
    http中的post请求发生了两次(多了一次options请求)的原因
    测试日常使用---linux命令:
    数据库性能优化
    pytest常用配置文件之pytest.ini
    pytest.main()的使用
  • 原文地址:https://www.cnblogs.com/lovesueee/p/2757566.html
Copyright © 2020-2023  润新知