3 Node 基础

3.1 安装

打开 Node官网 ,引入眼帘的就是它的下载地址了,windows下提供的是安装程序(下载完之后直接双击安装),linux下提供的是源码包(需要编译安装),详细安装流程这里省略掉,我想这个不会难倒各位好汉。

3.2 旋风开始

在讲 Node 语法之前先直接引入一段 Node 的小例子,我们就从这个例子着手。首先我们在随意目录下创建两个文件 a.js b.js

  1. exports.doAdd = function(x,y) {
  2. return x+y;
  3. };

代码 3.2.1 a.js

  1. const a = require('./a');
  2. console.log(a.doAdd(1,2));

代码 3.2.2 b.js

和普通前端 javascript 不同的是,这里有两个关键字 exportsrequire。这就牵扯到化的概念了,javascript 这门语言设计的初衷是开发一门脚本语言,让美工等从业人员也能快速掌握并做出各种网页特效来,加之当初语言创作者开发这门语言的周期非常之短,所以在 javascript 漫长的发展过程中一直是没有模块这个语言特性的(直到最近ES6的出现才打破了这个格局)。

>Node 是最近几年才发展起来的语言,前端 js 发展的历史要远远长于他,2000年以后随着 Ajax技术越来越流行,js的代码开始和后端代码进行交互,逻辑越来越复杂,也越来越需要以工程化的角度去组织它的代码。模块化就是其中一项亟待解决的问题,期间出现了很多模块化的规范,CommonJS就是其中的一个解决方案。由于其采用同步的模式加载模块,逐渐被前端所抛弃,但是却特别适合服务器端的架构,服务器端只需要在启动前的时候把所有模块加载到内存,启动完成后所有模块就都可以被调用了。

我们在命令行中进入刚才我们新创建的那个文件夹下,然后运行 node b.js,会输出 3 ,这就意味着你的第一个node程序编写成功了。

在a.js中 exports 对象会被 导出,在 b.js 中通过require 就能得到这个被导出的对象,所以我们能访问这个被导出对象的 doAdd 函数。假设我们在 a.js 中还有一个局部变量:

  1. var tag = 'in a.js';
  2. exports.doAdd = function(x,y) {
  3. console.log(tag,x,y);
  4. return x+y;
  5. };

代码 3.2.3 a.js

这里定义的 tag 变量是没法在 b.js 中读取的,其作用区域仅仅被局限在 a.js 中。如果在 b.js 中打印 console.log(a.tag) 会输出 undefined

这里 b.js 里我们引用 a.js 时用 require(‘./a’),假设我们现在的目录结构是这样的

  1. ---a.js
  2. ---b.js
  3. ---lib
  4. |-------c.js

目录3.2.1

这个时候我们在b.js 中就可以通过 const c = require('./lib/c'); 来引入 c.js。同时 node 本身还包含了各种系统API。比如通过require(‘fs’),可以引入系统自带的 文件操作库。下面就举一个操作文件的栗子:

  1. const fs = require('fs');
  2. exports.getData = function(path,callback) {
  3. fs.exists(path,statCallback);
  4. function statCallback(exists) {
  5. if (!exists) {
  6. return callback(path+'不存在');
  7. }
  8. const stream = fs.createReadStream(path);
  9. let data = '';
  10. stream.on('data',function(chunk) {
  11. data += chunk;
  12. });
  13. stream.on('end',function() {
  14. callback(false,data);
  15. });
  16. }
  17. };

代码 3.2.4 c.js

代码 3.2.4中 函数 exists 用来判断文件是否存在, createReadStream 函数返回一个 readable stream(可读流),node 中IO(包括文件IO和网络IO)处理采用stream(流)的方式进行处理。同时在流的内部还使用EventEmitter来触发事件,具体到 代码 3.2.4中,我们会看到 data 事件和 end 事件,分别表示当前有新读入的数据、当前的数据全都读取完毕了。

接下来我们写一个测试代码来对 c.js 进行测试:

  1. const path = require('path');
  2. const c = require('./lib/c');
  3. c.getData(path.join(__dirname,'test.txt'),function(err,data) {
  4. console.log(err,data);
  5. });

代码 3.2.5 fs_test.js

注意上述代码中的全局变量 __dirname 他获取的是当前代码文件所在的路径,我们在 fs_test.js 同级目录下放了一个 test.txt,所以这里使用 path.join(__dirname,'test.txt') 来获取一个绝对路径。假设我们的test.txt 在目录data中,

  1. ---fs_test.js
  2. ---data
  3. |------test.txt

目录 3.2.2那么就写 path.join(__dirname,'data/test.txt')

3.3 做一个Apache

现在我们做个更让人兴奋的栗子,做一个 Apache,当然这里的 Apache 不是武装直升机,而是一个服务器,熟悉php的人对他肯定不会陌生。你在本地安装它之后,然后在其默认的网站目录中放一张图片,我们假设它为a.jpg,然后你就可以通过 http://localhost/a.jpg 来访问它了。下面的内容就是要模拟这个过程。

要做这个处理,我们首先要搞懂 node 中的 http 包。我们抄一段 node 官网给出的快速搭建 http 服务器的代码吧:

  1. const http = require('http');
  2. const hostname = '127.0.0.1';
  3. const port = 3000;
  4. const server = http.createServer((req, res) => {
  5. res.statusCode = 200;
  6. res.setHeader('Content-Type', 'text/plain');
  7. res.end('Hello World\n');
  8. });
  9. server.listen(port, hostname, () => {
  10. console.log(`Server running at http://${hostname}:${port}/`);
  11. });

代码 3.3.1 example.js

直接运行 node example.js,然后我们打开 chrome ,输入网址 http://localhost:3000,就会在网页上看到 Hello world。OK,我们回头看一下代码,关键部分在于 createServer 的回调函数上,这里有两个参数 reqres,这两个变量也是 stream 类型,前者是readable stream(可读流),后者是writeable stream(可写流),从字面意思上推测出前者是用来读取数据的,而后者是用来写入数据的。大家还有没有记得我们在代码 3.2.4中函数fs.createReadStream 也返回一个 readable stream。接下来就是一个见证奇迹的时刻, stream 类上有一个成员函数叫做 pipe,就像它的名字 管道 一样,他可以将两个流通过管子连接起来:

pipe原理示意图

图 3.3.1 pipe原理

有了pipe这个功能,我们就能将 fs.createReadStream 函数得到的可读流转接到res这个可写流上去了。说干就干,我们简单修改一下代码 3.3.1,就可以让其成为一个 Apache:

  1. const http = require('http');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const hostname = '127.0.0.1';
  5. const port = 3000;
  6. const imageDir = __dirname + '/images';
  7. const server = http.createServer((req, res) => {
  8. const url = req.url;
  9. const _path = path.join(imageDir , url);
  10. fs.exists(_path,function(exists) {
  11. if (exists) {
  12. res.statusCode = 200;
  13. res.setHeader('Content-Type', `image/${path.extname(url).replace('.','')}`);
  14. fs.createReadStream(_path).pipe(res);
  15. } else {
  16. res.statusCode = 404;
  17. res.end('Not Found');
  18. }
  19. });
  20. });
  21. server.listen(port, hostname, () => {
  22. console.log(`Server running at http://${hostname}:${port}/`);
  23. });

代码3.3.2 app.js

我们仅仅使用了一句fs.createReadStream(_path).pipe(res);,就便捷的将文件流输出到HTTP的响应流中了,是不是很强大。OK来看一下效果,运行 node app.js,在浏览器中打开 http://localhost:3000/a.png 就能看到显示效果。

最终我们的apache显示效果图 3.3.2 最终我们的apache显示效果

3.4 HTTP请求参数

既然我们称 Node.js 是一门后端语言,那他就应该能处理 HTTP 请求中的请求参数,比如说我在 URL 上添加查询参数(类似于这种 /xxx?a=1&b=2),再比如说通过表单提交数据。 Node 确实提供了处理这两种数据的能力,只不过让人感觉到稍显“低级”。为什么这么说来,下面就一一道来。

3.4.1 GET 请求

首先我们来简单描述一下 HTTP 请求,打开浏览器,并且打开开发者工具,然后使用谷歌搜 node,我们定向到开发者工具的 Network 标签页,然后开第一条网络请求,鼠标单击点开这条网络请求,会显示格式化好的HTTP 请求和响应的数据包内容:谷歌搜索图3.4.1.2 使用谷歌搜索 node

我们这里仅仅关注一下 General 部分,Request URL? 后面是一大串请求参数,在我们这里是

?newwindow=1&safe=strict&site=&source=hp&q=node&oq=node

,由于参数太长了,我们故意省略掉一些。然后还有一个参数 Request Method(请求方法) 为 GET,HTTP 协议中很几种常用的请求方法:GET POST HEAD PUT DELETE,其中最常用的就是前两者。这个栗子中给出的请求方法正是 GET 请求,它将参数类似 ?key1=value&key2=value2 方式组织在一起,拼接在请求地址的后面,发送到服务器端,服务器要解析这个请求参数,然后得到参数 key1 的值是 value1, key2 的值是 value2,继而进行逻辑处理。具体到上面这个栗子中,我们要在后端取到 source 这个参数的值,应该怎么做呢? 可能很多之前有 php 或者 java 经验的开发者,会通过 $_GET['source'] 或者 request.getParamater("source") 能够轻而易举的把这件事给办了,但是 node 不行。下面我们对代码 3.3.1稍加改造来实现这个目的:

  1. const http = require('http');
  2. const url = require('url');
  3. const hostname = '127.0.0.1';
  4. const port = 3000;
  5. const server = http.createServer((req, res) => {
  6. res.statusCode = 200;
  7. const query = url.parse(req.url,true).query;
  8. console.log(query);
  9. res.setHeader('Content-Type', 'text/plain');
  10. res.end(`Hello World from ${query['source']}\n`);
  11. });
  12. server.listen(port, hostname, () => {
  13. console.log(`Server running at http://${hostname}:${port}/`);
  14. });

代码 3.4.1.1 get.js GET 请求参数处理

运行 node get.js,打开浏览器输入 http://127.0.0.1:3000/?newwindow=1&safe=strict&site=&source=hp&q=node&oq=node,会打印出 Hello World from hp

注意代码 3.4.1.1 中的 url 库的 parse函数,它对 URL 地址进行解析,得到 URL 中的域名、请求路径等参数,其中第二参数在这里要设置true,否则它不会解析请求参数。

3.4.2 POST 请求

刚才我们在演示 GET 请求的使用的是谷歌搜索的栗子,它的请求参数在地址栏中都可以看到,这种方式简介明了,你甚至可以给朋友发一个网址,他就可以搜出来跟你一样的内容。但是有时候,我们还要在互联网上做一些比较隐私的事情,比如说登录某个网站,你肯定不想让你的用户名、密码出现在浏览器的地址栏中,这个时候 POST 请求就派上用场了。

我们把代码 3.4.1.1稍微修改一番,来演示一下后端怎样读取 POST 参数:

  1. const http = require('http');
  2. const url = require('url');
  3. const qs = require('querystring');
  4. const hostname = '127.0.0.1';
  5. const port = 3000;
  6. const server = http.createServer((req, res) => {
  7. res.statusCode = 200;
  8. const query = url.parse(req.url,true).query;
  9. let postStr = '';
  10. req.on('data',function(data) {
  11. postStr += data;
  12. });
  13. req.on('end', function() {
  14. const post = qs.parse(postStr);
  15. res.setHeader('Content-Type', 'text/plain');
  16. res.end(`Hello ${post['name']} World from ${query['source'] || 'unknown'}\n`);
  17. });
  18. });
  19. server.listen(port, hostname, () => {
  20. console.log(`Server running at http://${hostname}:${port}/`);
  21. });

代码 3.4.2.1 post.js POST参数处理

代码 3.4.2.1可以看出处理 POST 参数时竟然要需要从流中读取数据,这是由于在 HTTP 协议中 POST 数据被认为可以发送大容量的请求数据,所以为了防止堵塞,Node 强制使用流来处理这部分数据。

由于不能直接通过浏览器输入地址来测试 POST 请求,所以推荐大家使用一个 chrome app来测试 POST 请求,它就是 Postman。安装完成之后,打开主界面,下拉菜单区域选择 POST,然后输入地址 http://localhost:3000,打开 Body 标签,选择x-www-form-urlencoded,最后点击 Send 按钮,完成后就会输出Hello sunny World from unknown

Postman 发送 POST 请求图3.4.2.1 Postman 发送 POST 请求

这里之所以需要选择x-www-form-urlencoded,是由于 POST 数据包有很多组织方式,我们最常用的就是这种方式,其数据格式依然是key1=value1&key2=value2,而 Node 自带的 querystring.parse函数 正是用来处理这种字符串的。同时我们可以点击 Code 按钮,来查看 POST请求发送的请求数据包:

POST请求数据包图3.4.2.2 POST请求数据包

你可以留意到请求包中当中有一个空行(HTTP协议中用空行来分割请求头和请求正文),空格下面的name=sunny&password=1234就是我们 POST 到后台的数据。

3.5 总结

我们用两个小节讲述了 Node 中如何处理静态资源和动态请求,看完这些之后,如果你是一个初学者,可能会因此打退堂鼓,这也太麻烦了,如果通过这种方式来处理数据,跟 php java 之类的比起来毫无优势可言嘛。大家不要着急,Node 社区已经给大家准备了各种优秀的 Web 开发框架,比如说 ExpressKoa,绝对让你爱不释手。你可以从本书的第5章中学习到 express 基本知识。

本章示例代码可以从这里找到:https://github.com/yunnysunny/nodebook-sample/tree/master/chapter2