[译] Node.js 基础知识:没有依赖关系的 Web 服务器

Node.js 是构建 web 应用服务端的一种很是流行的技术选择,而且有许多成熟的网络框架,好比 express, koa, hapijs。尽管如此,在这篇教程中咱们不用任何依赖,仅仅使用 Node 核心的 http 包搭建服务端,并一点点地探索全部的重要细节。这不是你能常常看到的一种情况,它能够帮助你更好地理解上面说起的全部框架--现有的许多库不只在底层使用这个包,并且常常会将原始对象暴露出来,使得你能够在某些特殊任务中应用他们。html

目录表

Hello, world

首先,让咱们开始一个最简单的程序--返回那句经典的响应『hello,world』。为了用 Node.js 构建一个服务程序,咱们须要使用 http 内建模模块,尤为是 createServer 函数。前端

const { createServer } = require("http");

// 这是一种好的实现
// 容许运行在不一样的端口
const PORT = process.env.PORT || 8080;

const server = createServer();

server.on("request", (request, response) => {
  response.end("Hello, world!");
});

server.listen(PORT, () => {
  console.log(`starting server at port ${PORT}`);
});
复制代码

让咱们列出这个简短示例的全部内容:node

  1. 使用 createServer 函数建立一个服务对象实例。
  2. 为咱们的服务程序中 request 事件添加一个事件监听器
  3. 在环境变量指定的端口运行咱们的服务程序,缺省时使用 8080 端口。

咱们建立的服务程序是 http.Server 类的一个实例,继承自对象 net.Server,而它又继承自类 EventEmitter。有许多咱们能够监听的事件,但最重要的事件是 request,而且在建立服务时提供它的监听,常见的实现方式以下:android

const { createServer } = require("http");

// 这样等同于 `server.on('request', fn);`
createServer((request, response) => {
  response.end("Hello, world!");
}).listen(8080);
复制代码

最后一步是启动咱们的服务。我经过调用 server.listen 方法来启动,而且你能够指定端口和启动后执行内容。有一点要注意的是:服务并不会当即开始,它接入来访的请求时必须先和一个端口绑定,然而在实践中这点并非很是重要,由于这个过程几乎是瞬间完成。你也能够经过 listening 事件方法来单独监听这个特殊事件。ios

响应细节

如今,在咱们学会了如何实例化一个新服务应用后,让咱们看看如何实际回复用户的请求。在咱们惟一的事件处理器中,咱们使用 response.end 方法以常规经典响应 Hello, world! 来回复。你能够看出这个签名与可写流方法 writable.end 很是类似,这是由于请求和响应对象都是流对象 streams,同时请求只是可读流,并且响应只是可写流。为何它们必须是流对象呢?为何咱们不能发送整个回复?git

答案是在回复前咱们不是非得作完全部的事。想象这种情景,当咱们从文件系统中读取一个文件时,而这个文件比较大。所以咱们能够经过 fs.createReadStream 方法打开了一个文件流,这样咱们就能够当即写入响应。此外咱们还能够直接将输入经过管道链接到输出!github

如今由于它是流对象,咱们能够作下面的事:web

const { createServer } = require("http");

createServer((request, response) => {
  response.write("Hello");
  response.write(", ");
  response.write("World!");
  response.end();
}).listen(8080);
复制代码

所以咱们能够直接屡次写入咱们流对象。在任何形式的循环中这么作时要当心,由于你必须本身处理背压问题,另外最好直接管道链接到流对象。一样的,请注意在结尾时使用 response.end() 方法。这是强制的,若是没有这个调用,Node 将保持此链接处于打开状态,形成内存泄漏和客户端处于等待状态。数据库

最后,让咱们演示一下流的管道方法是如何为响应对象和其余流起做用的。为了这么作,咱们使用 __filename 变量来读取源文件:express

const { createReadStream } = require("fs");
const { createServer } = require("http");

createServer((request, response) => {
  createReadStream(__filename).pipe(response);
}).listen(8080);
复制代码

咱们不必定要手动调用 res.end 方法,由于在原始流结束时,它也会自动地关闭管道传输的流。

HTTP 报文

咱们的服务程序实现了 HTTP 协议,它是一种文本集的规则,容许客户端以本身首选格式请求特定信息,也容许服务程序以数据和附加信息来回复,例如格式、链接状态、缓存信息等等。

让咱们看一个对 web 页面的典型请求:

GET / HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko)
Host: blog.bloomca.me
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
复制代码

这是当你请求页面时,咱们浏览器发送的内容,除了上面这些它还发送更多的 headers,传输 cookies(也是一种 header),还有其余信息。对咱们来讲重要的是要理解:全部的请求有方法、路径(路由)以及 headers 列表,这些都是键值对(若是你想了解 cookies,它们只是一种具备特殊含义的 header)。HTTP 是一种文本协议,正如你所看到的,你本身能够读懂它。虽然它只是一组协议,实现此协议的浏览器和服务程序都试图遵照这个协议规定,这就是整个互联网的运转方式。并不是全部规则都被遵照,但主要规则 - HTTP 操做、路由、cookie 都足够可靠,您应该始终追求可预测的行为。

HTTP Headers 报文头

我能够经过 request.headers 属性来访问客户端发送的全部 header。例如为了识别客户端选择的语言类型,咱们能够像下面这样作:

const { createServer } = require("http");

createServer((request, response) => {  
  // 这个对象中全部的 header 都是小写  
  const languages = request.headers["accept-language"];

  response.end(languages);  
}).listen(8080);
复制代码

我我的对语言的选择,使用『en-US,en;q=0.9,ru;q=0.8,de;q=0.7』,也就是说我首选英语,其次俄语,最后是德语。通常状况下浏览器使用你的操做系统语言,可是它会被替换,不是最好的依赖,由于用户不能直接控制它(而且不一样浏览器对这行代码有不一样的选择)。

为了写一个 header,你须要理解 HTTP 是一种协议,这个协议规定首先是元数据,而后在一个分隔符(两个换行符)以后才是真正的报文体。这意味着一旦你开始发送内容,你就不能变动你的报文头!若是这么作会在 Node 中抛出错误以及实际会停止你的程序。

有两种设置 header 的方法: response.setHeader 方法和 response.writeHead 方法。 二者的区别是前者更特殊,而且若是二者都被使用的状况下,全部的 header 会被合并,且以 writeHead 方式设置的 header 取值具备更高的优先级。writeHeadwrite 方法的做用相同,也就是说你不能够在后续修改 header。

const { createServer } = require("http");

createServer((req, res) => {
  res.setHeader("content-type", "application/json");

  // 咱们须要发送 Buffer 或者 String 类型数据,咱们不能直接传递一个对象  
  res.end(JSON.stringify({ a: 2 }));
}).listen(8080);
复制代码

HTTP Status Codes 状态码

HTTP 定义了每一个响应都必需要有的状态码,列表 中定义了各个状态码的含义。一样,并不是全部人都严格遵照这个列表

让咱们列出最重要的状态码:

2xx – 成功码:

  • 200:最多见的状态码,在 Node.js 中默认表示『OK』。
  • 201:新实体被建立。
  • 204:成功码,可是没有响应返回。例如,在移除一个实体后的状态码。

3xx – 重定向码

  • 301:永久迁移,返回信息中有新的 URL。
  • 302:临时迁移,可是有另外一个新 URL。成功向重定向页发起 POST 请求后,新建的实体页可访问。

注意 301/302 状态码。浏览器倾向于记住 301,若是你偶然地把一些 URL 标记上 301 状态码,浏览器在收到新响应后也许仍然会这么作(它们甚至都不检查)。

4xx - 客户端错误码

  • 400:错误请求,好比传递参数错误,或者缺乏一些参数
  • 401:未受权,用户未被认证,所以没法访问。
  • 403:禁止访问,用户一般已被认证,可是这项操做未被受权,一样,在某些服务端可能会与 401 状态码混淆。
  • 404:未找到,提供的 URL 找不到指定页面或数据。

5xx – 服务器错误码

  • 500:服务器内部错误,例如数据库链接错误。

这些错误码是最多见的类型,而且足够让你为请求匹配正确的状态码。在 Node.js 中,咱们既可使用 response.statusCode 方法,也可使用 response.writeHead 方法。此次就让咱们使用 writeHead 方法来设置一个自定义 HTTP 消息:

const { createServer } = require("http");

createServer((req, res) => {
  // 代表没有内容
  res.writeHead(204, "My Custom Message");
  res.end();
}).listen(8080);
复制代码

若是你尝试在浏览器中打开这些代码,而且在『网络』标签中浏览 HTML 请求,你将会看到『状态码:204 个人自定义消息』。

路由

在 Node.js 服务程序中,全部的请求都由单个请求处理程序处理。咱们能够经过运行咱们的任何服务来测试这点,或者经过请求不一样的 URL 地址,例如地址 http://localhost:8080/homehttp://localhost:8080/about。你能够看到测试将返回一样的响应。然而,在请求对象中咱们有一个属性 request.url,咱们可使用它构建一个简单的路由功能:

const { createServer } = require("http");

createServer((req, res) => {
  switch (req.url) {
    case "/":
      res.end("You are on the main page!");
      break;
    case "/about":
      res.end("You are on about page!");
      break;
    default:
      res.statusCode = 404;
      res.end("Page not found!");
  }
}).listen(8080);
复制代码

有不少警告(尝试在 /about/ 页面添加一个尾部斜杠),可是你有办法。在全部的框架中,有一个主处理程序,它将全部请求导向已注册的处理程序。

HTTP 方法

你可能熟悉 HTTP methods/verbs,例如 GETPOST。它们是 HTTP 协议自己的一部分,且含义很明显。然而,它们也有许多我不想深挖的微妙细节,为了简洁起见,我想说 GET 是为了获取数据,而 POST 是为了建立新的实体对象。没人不让你拿它们另作他用,可是标准和惯例建议你不要这么作。

上面已经说到,在 Node.js 中服务程序有 request.method 属性,能够用于咱们内部逻辑处理。一样,Node.js 自己没有任何内容可供咱们使用,对不一样方法抽象出处理方法。咱们须要本身构建抽象处理方法:

const { createServer } = require("http");

createServer((req, res) => {
  if (req.method === "GET") {
    return res.end("List of data");
  } else if (req.method === "POST") {
    // 建立新实体
    return res.end("success");
  } else {
    res.statusCode(400);
    return res.end("Unsupported method");
  }
}).listen(8080);
复制代码

Cookies 缓存

Cookies 值得单独开一个文章来介绍,因此请随时阅读更多关于它们的内容 MDN guide

两个关键词,cookie 用于在请求过程当中保留一些数据,由于 HTTP 是一种无状态协议,从技术上讲,若是没有 cookies(或者本地存储),咱们必须在每次须要身份验证的操做以前都得执行登陆操做。咱们在客户端保留 cookie(一般在浏览器中),这样浏览器能够给咱们发送一个名为 Cookie 且包含全部 cookie 对象的 header,咱们能够经过一个 Set-Cookie header 来响应请求,告诉客户端设置哪一个 cookie(例如访问 token);客户端保存它以后,就会在每次后续请求中将它发回服务端。

让咱们运行下面的代码:

const { createServer } = require("http");

createServer((req, res) => {
  res.setHeader(
    "Set-Cookie",
    ["myCookie=myValue"],
    ["mySecondCookie=mySecondValue"]
  );
  res.end(`Your cookies are: ${req.headers.cookie}`);
}).listen(8080);
复制代码

你第一次刷新浏览器时,可能会看到一些旧缓存 cookie,可是你看不到 myCookie 或者 mySecondCookie。然而,若是你再刷新浏览器,你将会看到二者的值!这个状况的缘由是在响应客户端会在 cookies 中设置它们的值,正是这个响应渲染了咱们页面。所以咱们只会在下一次请求发生后才会从客户端接收到这些返回的缓存 cookies。

如今,若是咱们想在代码中使用 cookie 值该怎么办呢?Cookie 在 HTTP 中只是一个 header,所以它是一个有着本身规则的字符串--cookie 使用 key=value 的模式来编写,包含参数,以 ; 符号分割。你能够编写本身的解析器(相似这篇文章这样this SO answer),可是我建议你使用与你的框架或库兼容的其余外部库做选择就好了。

一样地,请注意你不能删除 cookie,由于它属于客户端,可是你能够经过设置它为一个空值或一个过去的失效日期这种方式,使它变得无效

查询参数

给特殊处理器设置参数很常见:例如,你但愿显示全部图片,咱们能够指定一个页面,这经过能够经过查询参数来实现。它们被添加到 URL,经过符号 ? 与路径分隔开:http://localhost:8080/pictures?page=2,你能够看出,咱们请求了图片库的第二个页面。或者咱们能够只须要把它嵌入到 URL 连接自己,可是这里的问题是:若是有不止一个参数,URL 会很快变得混乱。查询参数并不固定,所以咱们能够添加任意数量的内容,也能够在未来删除/添加新内容。

为了在咱们的服务程序中获取到它,咱们使用 request.url 属性,在 路由 小节中咱们已经用到过。如今,咱们须要将咱们的 URL 与查询参数分开,虽然咱们能够手动这么作,可是没有必要,由于它已经在 Node.js 中实现了:

const { createServer } = require("http");

createServer((req, res) => {
  const { query } = require("url").parse(req.url, true);
  if (query.name) {
    res.end(`You requested parameter name with value ${query.name}`);
  } else {
    res.end("Hello!");
  }
}).listen(8080);
复制代码

如今,若是你添加查询参数来请求任何页面,你将会在响应中看到效果,例如这个 http://localhost:8080/about?name=Seva 的请求将会返回带有咱们标识名的字符串:

你的请求参数名带有值 Seva
复制代码

请求体内容

咱们最后要看的是请求体内容。以前咱们已知道,你能够从 URL 自己获取全部信息(路由和查询参数),可是咱们如何从客户端获取到真实数据?你不用直接访问它,但咱们能够直接经过读取流来得到传递的数据,这也是为何请求对象是流对象的一个缘由。让咱们写一个简单的服务程序,这个程序指望从 POST 请求中获取一个 JSON 对象,而且当获取的并不是有效 JSON 时将返回 400 状态码。

const { createServer } = require("http");

createServer((req, res) => {
  if (req.method === "POST") {
    let data = "";
    req.on("data", chunk => {
      data += chunk;
    });

    req.on("end", () => {
      try {
        const requestData = JSON.parse(data);
        requestData.ourMessage = "success";
        res.setHeader("Content-Type", "application/json");
        res.end(JSON.stringify(requestData));
      } catch (e) {
        res.statusCode = 400;
        res.end("Invalid JSON");
      }
    });
  } else {
    res.statusCode = 400;
    res.end("Unsupported method, please POST a JSON object");
  }
}).listen(8080);
复制代码

最简单的测试它的方法是使用 curl。首先,使用一个 GET 方法来查询:

> curl http://localhost:8080
Unsupported method, please POST a JSON object
复制代码

如今,使用一个随机字符串做为咱们的数据来发起一个 POST 请求

> curl -X POST -d "some random string" http://localhost:8080
Invalid JSON
复制代码

最后,产生一个正确的响应并查看结果:

> curl -X POST -d '{"property": true}' http://localhost:8080
{"property":true,"ourMessage":"success"}
复制代码

结尾

你能够看出,有在仅使用内建模块来处理每一个请求时有许多繁琐工做 - 好比记住每次都要关闭响应流,或者每次你发送对象时都要以字符串化的 JSON 来设置一个 Content-Type: application/json 类型的 header,或者分析查询参数,或者编写你本身的路由系统.....全部这些都被完成,只须要记住在框架引擎下,它使用这些核心方法,你不用担忧它的内部实际如何运行。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索