目录

前言

程序入口

requirejs初始化流程简述

一些重要的函数的解析


前言

现在已经2022年了,大家都已经用上webpack来进行各种打包了,webpack也能很好的兼容各种模块化方案,但是requirejs这个曾经很流行的模块化方案还是值得学习一下的。

本次学习的目的并不是为了弄清楚requirejs的方方面面,而是为了弄清楚其模块化加载原理。

注意:本文只探讨浏览器环境下的requirejs。

程序入口

本文分析的requirejs版本:2.3.6。

本人菜鸡一个,直接阅读源码,显然不太可能,我采用F12动态调试的办法来追踪它的运行流程。

<!--我在这里并不使用data-main方式,具体原因参考官网-->
<script src="require.js" type="text/javascript"></script>
<script>require.config({baseUrl:"js",paths:{app:"./app",}});require(["app"],function(){console.log("This is index.html,require app success!");});
</script>

先来看第一行代码:

这是立即执行函数的原型,有两个参数一个是global,一个是setTimeout(奇了怪了,为什么要弄一个setTimeout的参数呢?)。它的调用参数如下:

第一个this没有质疑的,全局环境下this指向global object。

第二个参数是一个表达式,用来计算sitTimeout是否定义了。

这里回答一下为什么要在函数原型中声明一个setTimeout?

我也不知道什么原因,但是我通过去除这个setTimeout参数,requirejs在浏览器环境下依然可以正常运行。由于requirejs也可以被用于node环境下,我猜想,可能在早期的node环境下setTimeout没有实现,于是乎就有了这个setTimeout。

另外这第一行也直接声明了三个全局变量:

  • requirejs
  • require
  • define

这三个变量,接下来会被初始化为对应的函数,也就是我们使用的API。

接着往下看

闭包环境下初始化了一大堆的变量,其中几个重要的变量:

  • contexts:所有的上下文都保存在这里(实际上,我通过跟踪,发现在浏览器环境下,就只有一个上下文)。
  • cfg:后面的配置依靠它来传递,保存data-main的相关配置信息。
  • globalDefQueue:全局依赖队列,可以认为它是模块依赖加载的数据中转站。
  • isBrowser:确定是否在浏览器环境下。
  • defContextName:这是默认的上下文的名字,浏览器环境下只有这一个上下文。

继续往下看

这一段代码应该是为了防止requirejs重复加载或者与其它的AMD规范的实现方案相冲突。

接下来的代码,从199行到1749行全部是function newContext()的定义,这基本是requirejs的核心了,但是它并不在这里立刻执行,我们先跳过不看。

接着继续执行来到:

这里定义了req和requirejs为同一个匿名函数。

到这里,第一个API被定义了requirejs,哈哈。

继续执行:

这一段定义了req(与requirejs指向同一个匿名函数)的各种properties。

其中1822行定义了require=req,全局变量require得到初始化,require这个API被初始化了。

现在,req=requirejs=require。

与此同时require.config也得到了初始化,这个API是非常重要的。

继续往下,遇到了req({}),这是初始化过程中遇到的第一个函数。

# 创建默认上下文req({})

我们跟进去这个函数,看看它具体做了什么。

正如注释所说,这里初始化一个默认上下文,并没有做什么其它的东西。

1790行,这里将获得的默认上下文存储在contexts中,名字为一开始定义的上下文名字,也就是一个_(下划线符号)。

但是newContext()这个函数首次被使用,这是整个requirejs的核心所在,跟进去看看。

由于newContext太长,这里不贴所有代码了。

函数首部做了一些变量的初始化,这些变量就是当前newContext闭包环境所生命的一些变量,其中config是最重要的一个,其保存的都是默认的requirejs的配置信息,而这些变量,最后都会被context对象保存。

然后在,在中间又定义了一系列的内部函数,接着定义了handler与Module对象。

那么接下来,就是真正context定义的地方了。这个定义的内容挺多的,直接截图简要看一下。

可以看到,之前定义的一些变量,全部都被context保存起来了,其中重要的几个properties:

config,Module,nextTick。

在newContext的最后,调用了context.makeRequire(),将context.require初始化为一个其内部的一个函数,localRequire,下面看看localRequire究竟做了什么。

//makeRequire的代码makeRequire: function (relMap, options) {options = options || {};function localRequire(deps, callback, errback) {var id, map, requireMod;if (options.enableBuildCallback && callback && isFunction(callback)) {callback.__requireJsBuild = true;}if (typeof deps === 'string') {if (isFunction(callback)) {//Invalid callreturn onError(makeError('requireargs', 'Invalid require call'), errback);}//If require|exports|module are requested, get the//value for them from the special handlers. Caveat://this only works while module is being defined.if (relMap && hasProp(handlers, deps)) {return handlers[deps](registry[relMap.id]);}//Synchronous access to one module. If require.get is//available (as in the Node adapter), prefer that.if (req.get) {return req.get(context, deps, relMap, localRequire);}//Normalize module name, if it contains . or ..map = makeModuleMap(deps, relMap, false, true);id = map.id;if (!hasProp(defined, id)) {return onError(makeError('notloaded', 'Module name "' +id +'" has not been loaded yet for context: ' +contextName +(relMap ? '' : '. Use require([])')));}return defined[id];}//Grab defines waiting in the global queue.intakeDefines();//Mark all the dependencies as needing to be loaded.context.nextTick(function () {//Some defines could have been added since the//require call, collect them.intakeDefines();requireMod = getModule(makeModuleMap(null, relMap));//Store if map config should be applied to this require//call for dependencies.requireMod.skipMap = options.skipMap;requireMod.init(deps, callback, errback, {enabled: true});checkLoaded();});return localRequire;}

实际上,这个localRequire函数实际上是真正载入script的地方,具体它怎么做的,这里不做分析,后续再详细分析。

这里我们得到context.require=localRequire,它是实际载入script的地方,所有的script最后都通过调用它来实现加载。

总结:这个地方真的就是只做了默认的上下文初始化工作,其初始化了requirejs的配置,以及一些保存模块信息的数据结构和这些数据结构对应的方法。

继续执行
define这个API在2061行获得定义。

至此,所有的requirejs的API全部获得了初始化。

继续执行,会再次执行req(cfg)。

我这里没有采用data-main的方式载入,所以这里cfg还是一个空对象,于是这里的执行与上一次req({})没有什么不同,当前上下文的config不会有所改变,也不会有其它的什么操作。

到这里,requirejs初始化完成。

注意:如果使用的data-main作为作为入口,此时就有所不一样了,其中cfg中会包含baseUrl的配置,也包含data-main对应的脚本作为deps,所以这一次执行req(cfg)会将当前上下文的config中的baseUrl改为data-main的所在的路径。这与requirejs文档中的说法是一致的,同时,这里还会加载data-main对应的脚本。

为什么要执行两次req()?

1、第一次执行是为了创建默认的上下文,初始化相关数据结构与工具函数。

2、第二次执行是为了处理data-main的相关的逻辑,将baseUrl设置为data-main的路径,同时最后通过context.require()调用来加载data-main对应的脚本。

requirejs初始化流程简述

1、12-36行初始化闭包环境所使用的一些数据结构

重要的一些数据结构:

  • contexts:所有的上下文都保存在这里(实际上,我通过跟踪,发现在浏览器环境下,就只有一个上下文)。
  • cfg:后面的配置依靠它来传递,保存data-main的相关配置信息。
  • globalDefQueue:全局依赖队列,可以认为它是模块依赖加载的数据中转站。
  • isBrowser:确定是否在浏览器环境下。
  • defContextName:这是默认的上下文的名字,浏览器环境下只有这一个上下文。

2、接下来定义了一系列的工具函数,以及重要的newContext函数,并且初始化了一些API

1764-1833行都是对req的初始化过程,其中req=requirejs=require。

1)、1764-1798行初始化req与requirejs,1821-1823行初始化require。

2)、1804-1806行初始化req.config,即初始化了require.config这个API,这是暴露给外部的使用的requirejs的配置API。

3)、1814-1816行初始化req.nextTick,这个是接下来用于创建任务的重要函数,内部使用setTimeout。

3、1836行执行req({})创建默认上下文

比较重要的一点信息:创建了默认的上下文context,并且把这默认上下文保存在contexts中,后面所有API对这个默认context的访问都通过contexts,这个contexts正好是开头初始化的闭包环境数据结构之一;另外,创建了一个重要的函数,context.require=context.makeRequire()=localRequire,这个函数就是后面加载模块的核心入口。

4、接下来又是一系列的初始化与函数定义的过程

其中,1875-1991行,这一段定义了及其重要的两个函数,createNode与load(都是req的方法),其中createNode是创建html标签的地方,load是真正创建script标签加载js脚本的地方,load内部调用createNode创建script标签,并且load内部处理了事件onreadystatechange和load,它们的事件处理函数保证了模块执行的顺序性。

5、然后就是处理data-main的逻辑

2007行-2052行对应于data-main的处理逻辑,这里会获取data-mian的路径以及对应的js脚本名,然后保存在cfg中(cfg是开头初始化的闭包数据结构)。

6、紧接着就是define函数的定义了

2061-2126行对应于define的定义,至此所有的API初始化完成了,define,require,require.config。

7、最后执行了req(cfg)

如果定了data-main的话,这一次的执行就会有所不同了,cfg中有baseUrl(data-main对应的路径),还有deps(保存的是data-main中的js脚本),这一次执行会修改context的配置,其中baseUrl会被修改,并且会调用context.require加载cfg.deps中脚本模块。

如果没有定义data-main的话,这一次执行就不会有什么改变了。

这里要吐槽一下:

requirejs调用req,如果参数是config,那一定会在内部产生一次无意义的参数的context.require的调用,然后一次无意义的nextTick调用,这就不能在调用之前加一个条件判断吗?真的是....

我一开始在这里饶了好几次,半天找不到data-main到底是在哪一次nextTick产生的task上被加载的。

一些重要的函数的解析

1、一系列的工具函数

其中55-79行的两个函数each与eachReverse这两个函数,遍历数组(each从左至右,eachReverse从右至左),对每个元素调用对应的回调,如果回调返回为真,就停止遍历。这与foreach方法有所区别。

81-87行的hasProp与getOwn也是两个比较重要的工具函数,hasProp判断一个obj中是否具有某个Property,getOwn用于获取一个obj中property的值。

94-103行的eachProp与each和eachReverse类似,不过这里是遍历对象的可迭代property。

109-128行的mixin会将会将source中的property合并到target中,合并规则是source中有target中没有的property。

132-136行的bind会将obj与func绑定起来,类似于function.bind。

138-140行的scripts会获取html文档中的所有script标签,返回一个script数组。

2、req函数(require或者requirejs)

    req = requirejs = function (deps, callback, errback, optional) {//Find the right context, use defaultvar context, config,contextName = defContextName;// Determine if have config object in the call.if (!isArray(deps) && typeof deps !== 'string') {// deps is a config objectconfig = deps;if (isArray(callback)) {// Adjust args if there are dependenciesdeps = callback;callback = errback;errback = optional;} else {deps = [];}}if (config && config.context) {contextName = config.context;}context = getOwn(contexts, contextName);if (!context) {context = contexts[contextName] = req.s.newContext(contextName);}if (config) {context.configure(config);}return context.require(deps, callback, errback);};/*** Support require.config() to make it easier to cooperate with other* AMD loaders on globally agreed names.*/req.config = function (config) {return req(config);};

这个函数是requirejs暴露的API,对应于require。

这个函数其实就做两件事:通过req加载依赖项或者通过req修改context的配置项。

加载依赖项通过context.require()函数这个加载入口。

设置配置项通过context.configure()函数这个加载入口。

其中req.config对应于require.config这个API,其内部就是最终调用req()完成配置。

    req.config = function (config) {return req(config);};

3、req.load函数,这个函数是载入模块的最终函数。

    req.createNode = function (config, moduleName, url) {var node = config.xhtml ?document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :document.createElement('script');node.type = config.scriptType || 'text/javascript';node.charset = 'utf-8';node.async = true;return node;};req.load = function (context, moduleName, url) {var config = (context && context.config) || {},node;if (isBrowser) {//In the browser so use a script tagnode = req.createNode(config, moduleName, url);node.setAttribute('data-requirecontext', context.contextName);node.setAttribute('data-requiremodule', moduleName);//Set up load listener. Test attachEvent first because IE9 has//a subtle issue in its addEventListener and script onload firings//that do not match the behavior of all other browsers with//addEventListener support, which fire the onload event for a//script right after the script execution. See://https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution//UNFORTUNATELY Opera implements attachEvent but does not follow the script//script execution mode.if (node.attachEvent &&//Check if node.attachEvent is artificially added by custom script or//natively supported by browser//read https://github.com/requirejs/requirejs/issues/187//if we can NOT find [native code] then it must NOT natively supported.//in IE8, node.attachEvent does not have toString()//Note the test for "[native code" with no closing brace, see://https://github.com/requirejs/requirejs/issues/273!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&!isOpera) {//Probably IE. IE (at least 6-8) do not fire//script onload right after executing the script, so//we cannot tie the anonymous define call to a name.//However, IE reports the script as being in 'interactive'//readyState at the time of the define call.useInteractive = true;node.attachEvent('onreadystatechange', context.onScriptLoad);//It would be great to add an error handler here to catch//404s in IE9+. However, onreadystatechange will fire before//the error handler, so that does not help. If addEventListener//is used, then IE will fire error before load, but we cannot//use that pathway given the connect.microsoft.com issue//mentioned above about not doing the 'script execute,//then fire the script load event listener before execute//next script' that other browsers do.//Best hope: IE10 fixes the issues,//and then destroys all installs of IE 6-9.//node.attachEvent('onerror', context.onScriptError);} else {node.addEventListener('load', context.onScriptLoad, false);node.addEventListener('error', context.onScriptError, false);}node.src = url;//Calling onNodeCreated after all properties on the node have been//set, but before it is placed in the DOM.if (config.onNodeCreated) {config.onNodeCreated(node, config, moduleName, url);}//For some cache cases in IE 6-8, the script executes before the end//of the appendChild execution, so to tie an anonymous define//call to the module name (which is stored on the node), hold on//to a reference to this node, but clear after the DOM insertion.currentlyAddingScript = node;if (baseElement) {head.insertBefore(node, baseElement);} else {head.appendChild(node);}currentlyAddingScript = null;return node;} else if (isWebWorker) {。。。。。。}};

rquire使用的是script标签去拿js,细心的同学会注意到node上设定了 async 属性(异步加载script标签),并且在标签上绑定了load等事件,而load事件对应的处理函数是context中的onScriptLoad,如下面所示,其最终调用了context中的completeLoad函数来做处理。

当文件loading完成后,则要做的主要工作是执行 completeLoad 事件函数,但是要注意的是这时候把script加载完成后,立即执行的是script标签内部的内容,执行完后才触发的 completeLoad事件处理函数。

onScriptLoad: function (evt) {//Using currentTarget instead of target for Firefox 2.0's sake. Not//all old browsers will be supported, but this one was easy enough//to support and still makes sense.if (evt.type === 'load' ||(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {//Reset interactive script so a script node is not held onto for//to long.interactiveScript = null;//Pull out the name of the module and the context.var data = getScriptData(evt);context.completeLoad(data.id);}
},

4、define函数

这个函数是requirejs暴露的核心API,通过它来定义模块。

define = function (name, deps, callback) {var node,context;//do for multiple constructor......//If no name, and callback is a function, then figure out if it a//CommonJS thing with dependencies.if (!deps && isFunction(callback)) {deps = [];//Remove comments from the callback string,//look for require calls, and pull them into the dependencies,//but only if there are function args.if (callback.length) {callback.toString().replace(commentRegExp, '').replace(cjsRequireRegExp, function (match, dep) {deps.push(dep);});deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);}}//If in IE 6-8 and hit an anonymous define() call, do the interactive work.if (useInteractive) {node = currentlyAddingScript || getInteractiveScript();if (node) {if (!name) {name = node.getAttribute('data-requiremodule');}context = contexts[node.getAttribute('data-requirecontext')];}}//add to queue lineif (context) {context.defQueue.push([name, deps, callback]);context.defQueueMap[name] = true;} else {globalDefQueue.push([name, deps, callback]);}
};

这就是define函数,代码不是很多,但是新奇的东西却是有一个!!!那就是代码中对 callback.toString() 文本来进行 正则匹配 ,哇,这是什么鬼呢?我们看看这两个replace中的正则表达式是什么样的

commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g;

第一个正则是用来支掉callback中的注释的,而第二个正则是用来匹配callback.toString() 文本中的 require(.....) ,并将 ..... 这个字段push到deps中,这个方法是不是很变态?通过这个办法,实现了对依赖项的获取,高招。(这里又学到了,以前不明白toString有什么用,现在懂了,把对象转换成string之后,那就可以当做字符串来处理了。)

在define最后,会把define的参数作为一个数组扔到开头初始化的数据结构globalDefQueue中,在后续的解读中,可以看到,该数据结构被会context访问,并将其内部的数据转到context中,然后加载,所以globalDefQueue实际上就是一个数据中转中心。

5、context.require

这个函数是模块加载的核心函数,通过makeRequire()函数初始化,context.require实际指向localRequire。

            makeRequire: function (relMap, options) {options = options || {};function localRequire(deps, callback, errback) {var id, map, requireMod;if (options.enableBuildCallback && callback && isFunction(callback)) {callback.__requireJsBuild = true;}if (typeof deps === 'string') {if (isFunction(callback)) {//Invalid callreturn onError(makeError('requireargs', 'Invalid require call'), errback);}//If require|exports|module are requested, get the//value for them from the special handlers. Caveat://this only works while module is being defined.if (relMap && hasProp(handlers, deps)) {return handlers[deps](registry[relMap.id]);}//Synchronous access to one module. If require.get is//available (as in the Node adapter), prefer that.if (req.get) {return req.get(context, deps, relMap, localRequire);}//Normalize module name, if it contains . or ..map = makeModuleMap(deps, relMap, false, true);id = map.id;if (!hasProp(defined, id)) {return onError(makeError('notloaded', 'Module name "' +id +'" has not been loaded yet for context: ' +contextName +(relMap ? '' : '. Use require([])')));}return defined[id];}//Grab defines waiting in the global queue.intakeDefines();//Mark all the dependencies as needing to be loaded.context.nextTick(function () {//Some defines could have been added since the//require call, collect them.intakeDefines();requireMod = getModule(makeModuleMap(null, relMap));//Store if map config should be applied to this require//call for dependencies.requireMod.skipMap = options.skipMap;requireMod.init(deps, callback, errback, {enabled: true});checkLoaded();});return localRequire;}mixin(localRequire, {isBrowser: isBrowser,/*** Converts a module name + .extension into an URL path.* *Requires* the use of a module name. It does not support using* plain URLs like nameToUrl.*/toUrl: function (moduleNamePlusExt) {。。。。。。},defined: function (id) {return hasProp(defined, makeModuleMap(id, relMap, false, true).id);},specified: function (id) {id = makeModuleMap(id, relMap, false, true).id;return hasProp(defined, id) || hasProp(registry, id);}});//Only allow undef on top level require callsif (!relMap) {localRequire.undef = function (id) {//Bind any waiting define() calls to this context,//fix for #408takeGlobalQueue();var map = makeModuleMap(id, relMap, true),mod = getOwn(registry, id);mod.undefed = true;removeScript(id);delete defined[id];delete urlFetched[map.url];delete undefEvents[id];//Clean queued defines too. Go backwards//in array so that the splices do not//mess up the iteration.eachReverse(defQueue, function(args, i) {if (args[0] === id) {defQueue.splice(i, 1);}});delete context.defQueueMap[id];if (mod) {//Hold on to listeners in case the//module will be attempted to be reloaded//using a different config.if (mod.events.defined) {undefEvents[id] = mod.events;}cleanRegistry(id);}};}return localRequire;}

makeRequire闭包环境中定义了localRequire,并且给localRequire中通过mixin添加了一些新的properties,最后返回了localRequire。

localRequire这个函数是模块加载的入口函数,在内部通过intakequeue这个函数将globalDefQueue中的对应define拿到context中,然后通过req.nextTick创建了一个模块加载任务。

这个模块加载任务分为三部分,调用intakequeue;定义module对象,并利用这个module对象完成module的加载;最后调用checkLoaded。

6、checkLoaded函数

该函数主要就是用于确认所有模块是否加载完成,如果未加载完成是否超时,如果超时就报错。

函数代码就不放了,知道功能就行了。

7、module对象及其properies

这个module对象是及其重要的,它是requirejs对模块的抽象,是requirejs操作的基本单元,是精髓所在,这里就先不分析了,后续扣细节的时候,再来讨论它。

requirejs的源码学习(01)——初始化流程相关推荐

  1. 【iScroll源码学习01】准备阶段 - 叶小钗

    [iScroll源码学习01]准备阶段 - 叶小钗 时间 2013-12-29 18:41:00 博客园-原创精华区 原文  http://www.cnblogs.com/yexiaochai/p/3 ...

  2. android 输入法如何启动流程_android输入法02:openwnn源码解析01—输入流程

    android 输入法 02:openwnn 源码解析 01-输入流程 之后要开始 android 日文输入法的测试,因此现在开始研究 android 输入法.之前两 篇文章已经对 android 自 ...

  3. 从Spring源码探究IOC初始化流程

    随着不断地使用Spring,以及后续的Boot.cloud,不断的体会到这个拯救Java的生态体系的强大,也使我对于这个框架有了极大的好奇心,以至于产生了我为什么不能写一个这样的框架的思考. 通过自学 ...

  4. 跟雨痕大大看golang源码-01初始化

    最近看了下的雨痕大大写的go1.5源码剖析的pdf,觉得有必要写点东西来巩固一下. 本人水平有限,第一次写,大家轻喷啊. 01初始化 一开始雨痕大大就通过汇编代码找到了初始化里面几个关键的函数. ru ...

  5. android源码学习- APP启动流程(android12源码)

    前言: 百度一搜能找到很多讲APP启动流程的,但是往往要么就是太老旧(还是基于android6去分析的),要么就是不全(往往只讲了整个流程的一小部分).所以我结合网上现有的文章,以及源码的阅读和调试, ...

  6. Dart源码学习01

    1.Dart历史 2.dart设计基础思想 3.Dart的编译模式 Dart VM 支持五种编译模式: 1, :最常见的 JIT 模式,可以直接在虚拟机中执行 Dart 源码,像解释型语言一样使用.通 ...

  7. NFC源码分析之初始化流程

    Nfc的app代码位于: android/package/apps/Nfc/... 编译生成Nfc的apk和libnfc_nci_jni.so Nfc的协议栈和Hal相关的代码位于: system/n ...

  8. netty 关闭chnnal_Netty 源码学习——服务端流程分析

    在上一篇我们已经介绍了客户端的流程分析,我们已经对启动已经大体上有了一定的认识,现在我们继续看对服务端的流程来看一看到底有什么区别. 服务端代码 public class NioServer { pr ...

  9. JDK11源码学习05 | HashMap类

    JDK11源码学习05 | HashMap类 JDK11源码学习01 | Map接口 JDK11源码学习02 | AbstractMap抽象类 JDK11源码学习03 | Serializable接口 ...

最新文章

  1. 手写html5游戏,HTML5 手写输入法
  2. 麦子的第一个注解+spring小案例 欢迎指点学习。
  3. H5新增的标签以及属性 2
  4. 家庭装修里最大的问题
  5. LTE Module User Documentation(翻译5)——Mobility Model with Buildings
  6. 关于MySQL 8.0
  7. mpu6050 重力加速度_2021年高考物理一轮复习学与练 重力、弹力 、摩擦力 高中物理知识总结大全...
  8. php checkbox后台判断是否选中,jquery如何判断是否选中checkbox
  9. 怎么看so文件是哪个aar引进来的_运城人才引进7月5号面试
  10. 压力测试神器stresstester源码分析
  11. 关于STC8H8K64U单片机IAP升级过程
  12. 微软 Access数据库操作 中文教程
  13. CSS度量单位rem、em、vw、vh详解
  14. STM32串口IAP
  15. java_批量处理图片转文字
  16. 【设计教程】在PS里面制作简单的下雨效果!
  17. 纳税信用等级怎么划分 激励惩戒措施有哪些
  18. winserver-记录共享文件夹操作日志
  19. 老韩思考:为什么毕业后3年,同学之间相差如此大?
  20. vue 移动端项目字体适配

热门文章

  1. 【图像分割】Segment Anything(Meta AI)论文解读
  2. 不积硅步 无以至千里
  3. Linux命令man echo,Linux Shell man 命令详细介绍
  4. 观《一个也不能少》后感
  5. 小程序 解决iPhone机型屏幕下边横杠遮住底部按钮问题
  6. 无法在windows资源管理器中查看此FTP站点
  7. 手机扫描身份证识别AND行驶证识别
  8. 全球及中国射频器件市场规模格局与投资战略决策报告2022版
  9. 监控摄像头卡顿_监控画面卡顿的解决方法
  10. java-net-php-python-jspm光影婚纱影楼系统设计与实现系统计算机毕业设计程序