IT前沿技术之node.js篇<一>:Node.js与javascript

当今企业间的竞争,本质上是学习速度的竞争。在这样一个信息过载的时代,技术人员往往淹没于各种大量出现的新技术、新概念、新名词之中,应接不暇。然而这些新技术大部分出自于美国。有一个问题被严肃地提了出来:是跟随?还是超越?在一个闲适的周末,阳光满满,泡上一壶绿茶,借着清新的茶香,我们将学习一下近些年被IT领域密切关注的一些前沿技术。用周末的两天时间,我们将围绕Node.js为中心,学习JavaScript、redis、Nginx、HBase、JQuery、Ajax、HTML5等等这些可以构成新兴IT公司的技术元素,通过周末两天的学习,掌握这些技术元素的核心概念并可以将它们应用IT产品的技实现中。如果你的绿茶已经泡好,那么就让我们开始吧!

上面提到的这些技术元素之所以被密切关注,是因为它们可以帮助新兴IT公司以较低的开发部署成本,获得高性能的web应用服务的快速实现。“快速”、“低成本”、“高性能”是这些技术元素的目的,也是技术进步的要求,是新兴IT公司快速发布并迭代产品的技术需求,我们将反复提到。我们假定读者具有一定编程经验,懂得任意一门编程语言,了解IP/TCP网络的5层结构,http协议。我们将深入分析各个概念模型,上机实践每一个例程,并理解每一行代码的运行机理,浏览每一个链接。通过两天的学习,希望我们可以获得“用低成本的硬件资源和简单的开发方式,搭建高性能的web应用服务”的能力。

Google一下什么是Node.js,我们得知:

Node.js is a platform built on Chrome's JavaScript runtimefor easily building fast, scalable network applications.Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

中文:

Node.js是一个服务器端 JavaScript 解释器,提供一种简单的构建可伸缩网络程序的方法,运行它的服务器能支持数万个并发连接。Node.js采用一种事件驱动的非阻塞I/O模型,可以在分布式系统上运行轻量级高性能的数据密集型实时应用。

为了理解Node.js,我们先分析JavaScript的编程模型。
一. JavaScript的 编程模型

1995年5月,对JavaScript的设计,其实只用了十天。而且,当时在网景公司的设计师Brendan Eich,是为了向公司交差,本人并不愿意这样设计。这导致了JavaScript具有一些小小的缺陷。然而瑕不掩瑜,JavaScript的优秀,并不体现在一些语法细节上(不像Ruby那样地飘逸),不体现在清晰的代码组织上(不像Python那样地亲切),而是体现在它传达出来的设计思想。那就是:应用程序应该是web化的。

何谓web化,就是对桌面应用程序的颠覆,应用程序的显示、逻辑处理、数据资源、互操作和硬件操作都应该是分立,可以由不同的设备提供,也可以由不同的设备处理,而不是通过链接静态MFC框架调用windows系统函数那样的紧耦合来实现程序的功能。由于技术发展和商业利益(微软)的限制,这一颠覆过程至今没有完成。即使是缓慢,即使是饱受质疑,但JavaScript代表着技术发展的趋势。一切真理都必须经历三个阶段,首先是想法遭嘲笑的阶段,其次是想法遭强烈反对的阶段,最后是想法被认为是理所当然的真理的阶段。JavaScript正在成为IT技术创新的中心。所以JavaScript就是本次专题学习的第一个内容。

我们很认可《Android核心分析》的作者@maxleng的观点:

“研究分析是从设计者的意图出发,从抽象的甚至从哲学的高度,从最简单的系统原型开始,从设计猜想开始,而不是一开始就从代码分析展开。”

“各种计算机语言,建模工具,不外乎就是建立一个更接近人的思维方式的概念空间,再使用工具从该概念空间向另外一个概念空间映射,我称之为人性思维空间向01序列描述空间的一个映射。”

我们知道:

Node.js的源代码是由它有8000行C++代码,2000行JavaScript代码构成。JavaScript 解析引擎 V8是 Google 发布的开源 JavaScript 引擎,采用 C++ 编写,在 Google 的 Chrome浏览器中被使用。V8 引擎可以独立运行,也可以用来嵌入到 C++ 应用程序中执行。

然而我们不从Node.js和JavaScript的源代码研究它们的特性,而是从它们所体现出来的外部特性来来分析它们的编程模型。任何一个事物它都需要存在,需要和其他事物进行互操作,需要自身变化和引起其他事物变化。比方说人:

生存空间地球(空气、水、阳光 ……)你在哪?
接口制造工具、改变环境、改变他人、改变自己可以干什么?
行为吃饭、泡妞、学习……怎么干?

这些关于外部特性的问题可以清晰地定义人。以至于一些更深刻的问题,比如说:你是谁?从哪来?要到哪去?那是门口保安和哲学家们研究的问题,不属于我们的讨论范围。我们接下来继续分析JavaScript 。


1.1. 生存空间。

如果有JavaScript编程经验的人,可以试着回答这几个问题:JavaScript写成的逻辑可以运行在哪里?是什么给他提供了运行环境?这些逻辑之间是怎样组织的

?我们分析下面这个例程将解释客户端JavaScript写成的逻辑的运行环境和组织形式:

例程1:

test1.html如下:

<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
		<title>这是一个网页</title>
	</head>
	<body>
		这是网页的内容
		<div id ="testdiv"></div>

		<script type="text/javascript" src="/static/js/test1.js"></script>
		<script type="text/javascript" src="/static/js/test2.js"></script>

	</body>
</html>
test1.js如下:

var testdiv = document.getElementById("testdiv");
for (var i = 0; i < 3; i++) {
	var childDiv = document.createElement("div");
	childDiv.appendChild(document.createTextNode("这是第"+i+"个子节点"));
	testdiv.appendChild(childDiv);
}
var message="欢迎参加《一个周末掌握IT前沿技术》的极限学习活动!"

test2.js如下:

var childDiv = document.createElement("div");
childDiv.appendChild(document.createTextNode(message));
testdiv.appendChild(childDiv);

test1.js和test2.js就是被嵌入到网页test1.html中的客户端JavaScript逻辑。当网页被访问时,浏览器会形成如下图所示的数据结构。其中window.document.body.childNodes[3]和window.document.body.childNodes[5]就是我们的两个的JavaScript逻辑。这些逻辑在网页被加载时默认会被浏览器顺序执行一遍。而window正是JavaScript逻辑运行的变量空间。例程中test1.js里定义的message变量,就处于window之下。在调试窗口中键入window.message就可以看到该变量。所有被网页所引入的JavaScript逻辑都共享window这一变量空间,也是这些客户端JavaScript逻辑之间唯一的组织形式。

用Chrome浏览器打开网页,在网页空白处点击右键,选择“审查元素”,在调试窗口中键入“window”并回车,就可以看到上面的数据结构。

上面我们分析了客户端JavaScript逻辑的运行环境和组织形式,没有出现“include 'stdio.h'”这样的语法,也没用经过编译、链接静态库,依赖动态库这样的运行方式。那么服务端JavaScript逻辑Node.js需要怎样的运行环境,逻辑之间是怎样相互引用的呢?我们看下面一个例程。

例程2:

demo1.js如下:

/**
 * Date: 12-3-24
 * 演示程序1
 * 说明: 运行服务器,访问http://127.0.0.1
 */
var http = require("http");
var message = require("./demo1/message");
var i = 1;

http.createServer(
    function (request, response) {

        response.writeHead(200, {"Content-Type":"text/html; charset=UTF-8"});

        response.write("欢迎您第" + i + "次访问服务器!<br>Welcome!"+message);

        i++;
        console.log("服务器访问被访问次数: i = " + i);
        console.log(process.env);
        response.end();

    }).listen(80);

message.js如下:

/**
 * Date: 12-3-24
 * 说明: 存放message
 */
var message = "欢迎参加《一个周末掌握IT前沿技术》的极限学习活动!";
module.exports = message;

安装好Node.js,运行node demo1.js即可开启服务器。开发环境的搭建请参见:开发环境的搭建。demo1.js中,“var http = require("http");”类似于python的“import XXX”这样的模块导入机制。变量http是从Node.js平台的navite本地代码中得来,变量i是直接定义的数,我们重点分析变量message。字符串变量message由message.js定义,并由“module.exports = message;”指定为message模块的导出变量。当demo1.js被执行,所有被导入的模块都会被顺序加载,Node.js维护了一个根变量空间,与客户端JavaScript的window类似,只不过这里不叫window,而叫root。这里和客户端JavaScript形成区别的是,不是所有模块js代码里定义的变量都会被放在root之下。在root下除了一些运行环境要用到的变量对象外,只有被导入的和直接定义的这些变量。具体到例程2中,root之下可以找到的http、message、i这3个变量。


1.2 接口。

这里说的接口,并不是c++编程里说的接口。这里说的”接口“是指,我们研究的对象可以去操作、影响、改变其他对象的方式,和被其他对象操作、影响、改变的方式。比如说白炽灯,它的接口是给它通电。它会被外界这个动作通电所影响而发光

。c++编程里面的接口我们可以这么理解:比如说富士康定义了一种接口叫“i工人”,凡是被挂了“i工人”这个接口的人,富士康都可以叫他“去干活”。不管这个人是清洁工还是装配工,也不管这个人是叫张小五还是叫王晓丽。每一个具体的人的具体工作就是一个实现了的虚函数,而“排班表”就是传说中的“虚函数表”。但是我们这里所说的接口却不是这个概念,我们可以简单地这么来理解,我们为王晓丽提供了一种接口叫做工作。如果王晓丽愿意来工作,她可以操作生产线,维修生产线,改进生产线,工作本身也会对王晓丽提供工资和尊重的回报。

通过生存空间的分析我们知道,JavaScript是一种由浏览器或服务端的脚本引擎提供运行环境的程序逻辑。那么这些程序逻辑可以操作的资源对象有哪些呢?硬件设备为程序逻辑提供了“显示、逻辑处理、数据资源、互操作和硬件”等等计算资源,任何程序逻辑都难以做到全部访问,JavaScript作为脚本语言在这方面的限制则更多,它可以访问或操作的资源只是这个集合的一个真子集。

对于客户端:

(1). 操作浏览器(打开关闭网页,提示和日志……)

(2). 操作dom树,改变网页显示

(3). 操作ajax对象,发送ajax请求;操作WebSocket对象,建立WebSocket连接

(4). 操作特殊对象(canvas, WebGL, audio, video), 进行2d绘图、3d绘图、音频、视频

(5). 操作浏览器插件,与包括flash在内的各种插件通信

(6). 通过PhoneGap等平台访问移动设备的硬件资源,例如GPS、加速计、摄像头、触摸屏交互、触摸手势、振动等等

(7). 在一些可访问对象中注册回调函数,当注册事件发生后回调程序逻辑

对于服务端:

(1). 操作以“http”模块为代表的网络模块提供网络服务

(2). 操作以“fs”为代表的本地资源模块访问服务器本地资源

(3).在一些可访问对象中注册回调函数,当注册事件发生后回调程序逻辑

题外讨论:《Web开发的复杂性》

由于web应用开发涉及到了现代计算结构中最复杂的两个部分:网络访问和显示, 比起Android,IOS,Windows平台开发,web应用的开发难度、学习成本往往都是最高的。网络开发之所以复杂,因为一个完整的网络应用往往都会涉及到各种网络通信流程(http/DNS/push/pull),连接形式(P2P/BS),网络结构(IPv4/IPv6/net/route),网络安全(SSL/DES/RSA/)等等内容。显示的开发,无论是PC端还是移动端,无论用何种程序语言表达,都被证明是一个复杂的过程。就以dom树驱动的网页显示为例,一个非常简单的网站往往都会有数千行的css代码来定义它的布局流样式。显示开发复杂的原因是显而易见的。当程序仅仅指定要将一个文字或图形显示在屏幕上时,浏览器需要计算出它在网页中的位置和样式并指定它的位图编码,然后通知操作系统;操作系统结合应用程序或intent(浏览器)的显示情况,确定位图在屏幕中的显示,然后调用显示框架,或直接通知显卡;显卡根据指示计算位图的边界、旋转、透明处理、覆盖...等等,最后算出一块数据更新显存buffer,当显存buffer切换时,屏幕上才会出现程序指定的那个文字或图形。

下面的JS代码可以在屏幕上绘制“测试文字”4个字符:

/**
 * Date: 12-3-24
 * 说明: 在网页中加入<canvas id="text" height="100" width="500" onclick = "getrose()"></canvas>
 *       执行下面的JS代码,就会在屏幕上绘制“测试文字”4个字符。
 */
var canvasDiv = document.getElementById("text");
context = canvasDiv.getContext("2d");
context.clearRect(0, 0, 500, 200);
context.fillStyle = "#f2577f";
context.font = "bold 20px Arial";
context.textBaseline = "top";
context.textAlign = 'center';
context.fillText("测试文字", 250, 50);

对于显示内容的输入事件的处理过程则更为复杂。以Windows系统鼠标点击浏览器上某个位置为例。鼠标按下,ring0层检测到usb硬件中断,硬件中断形成的句柄随着硬件树向上传播处理,直到被识别为一个“鼠标按下”的事件;“鼠标按下”的事件带着坐标等等参数传给了ring3层,进入windows消息循环;windows消息循环分析得知该事件应该传给chrome浏览器处理;chrome浏览器MFC的消息循环接到windows传来的消息,分析处理后,又将其传给webkit;webkit的消息循环顺着dom树将消息向下传播,确定对应的节点;webkit分析对应节点的“鼠标按下”事件有没有被JS V8引擎注册了回调函数;如果有则将事件参数传给该回调函数处理。

从上面的分析我们可以发现,太多的层级关系太多的循环使得人类和计算机程序的交流相隔千山万水。真正完美的产品需要将这些复杂度完美地隐藏起来,给用户展现的是一种人性的交流。这个矛盾正是导致web开发复杂性的根源。

优秀的工具(包括协议和编程语言)正是为了降低这个复杂性而在不懈地努力。在这些努力的实践中我们渐渐地发现,我们需要的是更优秀的协议,而不是一层一层堆积起来的框架。由于历史的原因,HTML技术背负了太多沉重的包袱,这使得web技术发展的没有希望的那么快。HTML5依然如此,你绝对不可能通过HTML5技术写出一个“极品飞车”这样的流畅的游戏。虽然目前HTMl5技术已经展现出对客户端进行挑战的潜力,从界面的表现和渲染方式、与桌面的交互处理、多线程并行处理、本地离线存储、访问本地硬件资源等多个能力上都有较为成熟的技术方案,但是HTML的文档流式布局只适合浏览报纸,不太适合web应用。即使如此,人们对于技术进步的努力从来没有停止。如果你现在也在思考着这些问题,那么你也是其中之一。或许我们可以期待脱离浏览器的canvas.js或WebGL.js的出现,或许我们的设备会进步到有颠覆性的技术产生。再或许我们的文明不再需要计算机科学。

上述就是客户端和服务端JavaScript逻辑可以访问或操作的资源的集合。它们表明了运行在V8脚本引擎中的JavaScript除了执行自身的行为逻辑(if for while)之外的行为能力。那么要完成行为能力,JavaScript的内部机制有哪些呢?


1.3 行为。

这一节我们将分析JavaScript的一些特性,如果对于没有JavaScript编程经验的人来说,每一部分展开来都可以有一篇文章的内容。但我们是极限学习,我们的任务是用最短的时间理解核心概念,付诸实践。我们不需要去完整地认识这些概念的全部,但是一定要深入地理解下面例程中,每一行代码后面的含义。在项目实践中,配合上Google,它们够用了。本文的所有例程都会分享出来。

(1).面向实例(JSON)

学习JavaScript语言结构,我们一定要摆脱传统面向对象语言思维习惯的干扰。传统面向对象,既有对象的类,也有对象的实例。JavaScript只有对象的实例,没有类。如果我们熟悉python语言,分析下面这段代码:

class A(object):
	b1=[1,2,3,4]
	b2="这是字符串"
	def b3(self):
		print("这是一个函数")

a=A()
a.b3()#屏幕输出"这是一个函数"

上例中class A为类(class),而a是这个类的一个实例(instance)。要使用对象A中的变量或函数,必须先将对象实例化。我们可以这么理解,类A就是一张建筑图纸,而实例a是根据这张图纸建成的一座房子。而在JavaScript的对象模型里,没有类,只有实例。也就是说我们直接建设的是房子,而不需要图纸。没有继承,没有多态。JavaScript可以通过对象的原型(prototype)实现对象的复制,这就类似于根据一座房子的模样再建造另一座房子。很多受到脱传统面向对象语言思维习惯的干扰的人以为这就是JavaScript的继承机制。不过我还是建议大家不要这么干,因为这是不必要的。如果一个对象需要使用另一个对象的变量或函数,为什么不将它们组合起来?JavaScript已经是世界上最优秀的对象模型了,如果你觉得不够用,那就再分析一下你的程序到底是要实现什么功能。

请分析下面这段代码instance.js:这是全篇最重要的一个例程,一定要上机实践,分析清楚它的每一行代码。

var a = {
	b1 : {
		c1 : [1, 2, 3, 4]//这是数组
	},
	b2 : "这是字符串"
};
console.log(JSON.stringify(a)); //log输出为{"b1":{"c1":[1,2,3,4]},"b2":"这是字符串"}
a.b3 = function () {
	console.log("这是一个函数");
};
console.log(JSON.stringify(a)); //log输出仍然为{"b1":{"c1":[1,2,3,4]},"b2":"这是字符串"}
a.b1.c2 = a;
console.log(JSON.stringify(a)); //log输出"TypeError: Converting circular structure to JSON",提示有循环的结构是不能被字符串化的。
d=JSON.parse('{"b1":{"c1":[1,2,3,4]},"b2":"这是字符串"}');
console.log(a==d);//log输出false
console.log(a.b1==d.b1);//log输出false
console.log(a.b2==d.b2);//log输出true

JSON是字符串化的JavaScript对象。它可以和JavaScript对象相互转化,上例中JSON.stringify()就是将JavaScript对象转化为字符串,JSON.parse()就是将字符串转化为JavaScript对象。

(2).闭包性

JavaScript的闭包性深受人们的赞誉也深受人们的反对。对于初学者,闭包性会带来变量命名空间的混乱。然而它是一种非常优秀的变量空间模型。还记得C++里面的"using namespace XXX;"这样的语句吧?它可以使一段c++代码访问另一段代码的变量、函数和类。

JavaScript的闭包性(closure)就是:让函数在运行时能够访问到函数定义时的所处作用域内的所有变量。函数定义时能访问到什么变量,那么在函数运行时通过相同的变量名一样能访问到。

我们分析如下代码:

/**
 * Date: 12-3-24
 *
 * 说明: 这个例程说明闭包性和回调函数的原理
 * 执行程序的输出如下:
 **这是函数a.c()
 **这是字符串: 是一个字符串
 **这是局部函数inner()
 **这是函数a.d(callback)
 **这是匿名函数
 **这是函数a.e(string)
 **变量string是: 这是字符串
 */

var a = {
	b : "这是字符串"
};

a.c = function () {
	console.log("这是函数a.c()");
	console.log(this.b + ": 是一个字符串");
	var b = this.b;
	inner(this.d);
	function inner(callback) {
		console.log("这是局部函数inner()");
		callback(function () {
			console.log("这是匿名函数");
			a.e(b); //这里的匿名函数可以访问到函数a.c()中定义的变量b;a是全局对象。
		});
	};
};

a.d = function (callback) {
	console.log("这是函数a.d(callback)");
	callback()
};

a.e = function (string) {
	console.log("这是函数a.e(string)");
	console.log("变量string是: " + string);
};
a.c();


(3).回调函数

与JavaScript的闭包性深受人们的赞誉不同,对JavaScript的回调函数却是承受着更多的批评。回调函数,匿名回调函数,特别是嵌套匿名回调函数,是造成代码逻辑混乱的主要原因,也是大型JavaScript项目的代码可读性很差的原因。如果我们能够清晰地理解上面例程中”匿名函数“被调用的机制,那么JavaScript的回调函数就被我们掌握了。JavaScript的回调函数可以帮助我们更方便地实现异步逻辑,它的实现机理其实就是函数指针。如果阅读windows源代码,可以发现它的各种异步逻辑处理比如说消息循环,都是通过传递句柄也就是函数指针来实现。

(4).单线程(事件引擎消息循环)

既然提到了消息循环,我们就来分析一下JavaScript的事件机制。Node.js和webkit都采用的是一种事件驱动的非阻塞I/O模型。为什么使用单线程的Node.js可以提供高速的并发能力?实际上著名Nginx也是基于单线程的。对于Nginx,我们后面的内容将会详细讲到。不像Apache+PHP那样派生出很多的OS线程来解决并发的问题,Node.js只有1个线程轮询它周围发生的各个事件。而每个事件的”发生“和”处理“过程却和这个线程无关。这个线程只负责查看某个事件是不是已经”发生“了,如果”发生“了,就通知与事件相应的回调函数慢慢”处理“这个事件。轮询线程不会等待这个事件的”处理“过程,它只是通知一下,紧接着查看其它事件的”发生“状态。事件本质上一个时空不一致的非线性模型,或所谓的“异步(Asynchronization)”。事件发生的顺序按照外界对其发出的时刻而确定,有的在先,有的在后,有时也可以齐头并进,一起同时触发,结束时也可以快的快、慢的慢。为了加速Node.js对于网络的并发访问,在实现上Node.js采用了epoll技术。对于webkit事件引擎的机制也是类似,这不过它处理的对象不是网络事件,更多的是dom树上的用户操作,这不需要太多关注并发性能。这也就是Node.js出现以前,对于JavaScript事件引擎人们关注不多的原因。不管是服务端还是客户端JavaScript我们都需要理解,事件引擎+回调函数构成了JavaScript运行模型。

(5).迭代器

除了对象,一些JavaScript的内置数据结构,比如说字符串(string),数组(array),正则表达式(RegExp),可以大大方便我们程序逻辑的实现。对于有对象颗粒的数据结构,JavaScript内置的迭代器可以让我们的程序逻辑更加清晰明了。请看下面一段代码iterator.js,这是本章的最后一个例程,请大家思考程序的输出会是什么?

/**
 * Date: 12-3-24
 *
 * 说明: 这个例程说明迭代器的使用
 */

var a = {
	a1 : "这是字符串a1",
	a2 : "这是字符串a2",
	b : {
		b1 : "这是字符串b1",
		b2 : "这是字符串b2",
		b3 : "这是字符串b3",
		b4 : "这是字符串b4",
		c : {
			c1 : "这是字符串c1",
			c2 : "这是字符串c2",
			d : {
				d1 : "这是字符串d1",
				d2 : "这是字符串d2",
			}
		},
	}
};

function showContent(a, path) {
	for (key in a) {//迭代器
		var child = a[key];
		if (typeof(child) == "string") {
			console.log("变量'" + path + "." + key + "'的值是: " + child);
		} else {
			showContent(child, (path + "." + key));//递归遍历树形数据结构。
		}

	}
};

showContent(a, "a");

JavaScript还有很多其他特性,很多编程编程语言里面也有各种迭代器。JavaScript也可以使用”for (var i = 0; i < 10; i++) {}“的循环控制语法。JavaScript的迭代器看似没有什么稀奇的,为什么我们在这里要单独分析它呢?那是因为,它和回调函数、事件引擎一起,是JavaScript运行时模型的重要组成部分。JavaScript没有继承、封装和多态,没有代理、模板、预编译、接口、多继承这些在传统面向对象编程里习以为常的概念,在概念的世界里它是简单的。但是它有”反射“,编程语言对自身的操作。我们可以在调试窗口中执行这么一句代码:”eval('a="这是字符串";');“,它的含义就是将”a="这是字符串";“一句代码导入到程序空间中并执行。虽然正如我们上面讨论的web开发本身是非常复杂的,但JavaScript却是如此的简洁。正是它的简洁性,成为了JavaScript最大的优点,也为我们复杂的开发中提供了一种高生产率的工具。这就是我们要掌握JavaScript的理由,不管是客户端还是服务端。你可能会觉得困难,那是因为,世界上大部分值得去做的事情都有一个非常陡的学习曲线。但至少证明这个事情值得去做。

 c++JavaScript
对象模型继承对象组合
变量空间模型封装闭包
运行时模型多态

迭代器、回调函数、事件引擎

前面的路还很远,你可能会哭,但是一定要走下去,一定不能停。——电影《停车》

本文目录:《一个周末掌握IT前沿技术之node.js篇》

1.《Node.js与javascript》

2.《Node.js与redis》

3.《Node.js与服务端模板引擎》

4.《Node.js与Restful API》

5.《Node.js与Nginx》

6.《Node.js与客户端模板引擎》

7.《Node.js与HBase》文本粗陋,欢迎斧正!欢迎投稿!原创文章,转载请链接。联系邮箱:(it.Web.technical#gmail.com)

IT技术精研院

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值