前言
以一种要开发 Node.js 实战项目为最终目标
进行一系列的技术预研过程
有特点,有针对性,有目标
培养 Node 领域的全局观
1 关于 Nodejs
1.1 什么是 Node.js
官网的话:
- Node.js 是基于 ChromeV8 执行引擎的 JS 运行时环境
- Node.js 使用了一个事件驱动,非阻塞式 I/O 的模型,使其轻量又高效
每一个字其实都看得懂,聚合到一起就有点懵了

我们先不来说 nodejs 是什么,先根据以往的经验抛出问题
1.1.1 在 Node.js 里运行 Js 跟在 Chrome 运行 Js 有啥不同?
已知 Chrome 浏览器用的是同样的 Javascript 引擎和模型
其实,在 Node.js 里写 Js 和在 Chrome 里写 Js,几乎一样
晃眼的几乎一样 那就是有不一样的地方呗!
- Nodejs 没有浏览器 API,即 (Document,window 等)
- 相应的,也增加了它专属的 API,比如文件系统,进程.
有了这些差别,其实就不难理解了
对于开发者来说
- 你在 chrome 里写 js 控制浏览器
- Node.js 让你用类似的方式,控制整个计算机
Node.js 的真谛,也就是官方抽象的释义,我们完全可以在不断深入的过程中慢慢理解~
1.2 Node.js 可以用来做什么?
1.2.1 提供 Web 服务
- 搜索引擎优化 + 首屏速度优化 = 服务端渲染
- 服务端渲染 + 前后端同构 = Node.js
1.2.2 构建工作流
在 gulp webpack
之间,前端是如何做构建工具呢?
可能用 java,ruby 等
但
- 构建工具不会永远不出问题
- 构建工具不会永远满足需求
前端同学很难对这些工具进行修改或者升级
所以
用 Node.js 做 js 的构建工具,是最保险的选择
1.2.3 开发工具
VScode
在 nodejs 的基础上封装了 chrome 的内核,使 nodejs 具有控制计算机得到能力
1.2.3 可扩展性较强大的沙盒游戏
需要给使用者自定义模块的能力
使用 Nodejs 做复杂的本地应用
- 可以利用 js 大的灵活性实现外部扩展
- Js 庞大的的开发者基数让他们的灵活性得到利用
1.2.4 客户端应用
在已有网站的基础上需要开发新的客户端应用
使用 Node.js 客户端技术实现,可以最大限度的复用现有工程
2 Node.js 初探
2.1 实现剪刀石头布
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| var playerAction = process.argv[process.argv.length - 1]; console.log("playerAction", playerAction);
var random1 = Math.random() * 3;
if (random1 < 1) { var computerAction = "rock"; } else if (random1 > 2) { var computerAction = "scissor"; } else { var computerAction = "paper"; }
if (computerAction === playerAction) { console.log("平局"); } else if ( (computerAction === "rock" && playerAction === "paper") || (computerAction === "scissor" && playerAction === "rock") || (computerAction === "paper" && playerAction === "scissor") ) { console.log("你赢了!"); } else { console.log("你输了!"); }
|
2.1 使用 Node.js 模块规范改造游戏
2.1.1 如何加载 js
浏览器端
- 使用
<script/>
标签
- 脚本变多时,需要手动管理加载顺序
- 不同脚本之间的逻辑调用需要全局变量
Node 端
- 没有 html 文件,无法使用
<script/>
标签

所以 Node.js 要重新去搞一个模块管理机制来管理 js 的加载,就是现在我们熟悉的 CommonJS 规范
2.1.2 重构剪刀石头布游戏
games.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| module.exports = function(playerAction) { if (["rock", "scissor", "paper"].indexOf(playerAction) == -1) { throw new Error("invalid playerAction"); } // 计算电脑出的东西 var computerAction; var random = Math.random() * 3; if (random < 1) { computerAction = "rock"; console.log("电脑出了石头"); } else if (random > 2) { computerAction = "scissor"; console.log("电脑出了剪刀"); } else { computerAction = "paper"; console.log("电脑出了布"); }
if (computerAction == playerAction) { console.log("平局"); return 0; } else if ( (computerAction == "rock" && playerAction == "scissor") || (computerAction == "scissor" && playerAction == "paper") || (computerAction == "paper" && playerAction == "rock") ) { console.log("你输了"); return -1; } else { console.log("你赢了"); return 1; } };
|
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const game = require("./game.js");
var winCount = 0; // 获取进程的标准输入 process.stdin.on("data", buffer => { // 回调的是buffer,需要处理成string const action = buffer.toString().trim(); const result = game(action); if (result == 1) { winCount++; if (winCount == 3) { console.log("我不玩儿了!哼!"); process.exit(); } } });
|
3 Node 内置模块
内置模块合集
3.1 Node.js 系统架构图

3.2 理解 Node.js 精髓
Node.js 是基于 ChromeV8 执行引擎的 JS 运行时环境
ChromeV8 执行引擎的 JS 运行时环境:架构图的左侧部分就是其体现
- application 代表你写的 nodejs 的代码
- 通过 V8 引擎来来运行,里面会涉及到一些关于操作系统调用,这部分就由 V8 引擎帮你转发到操作系统层面
- 从操作系统层面得到返回结果之后再通过 V8 引擎返回到 Js 里去
- 从 Js 到 V8 再到操作系统的能力,大部分都是通过 Node.js 的内置模块来提供的
- 还有一些数据是从操作系统底层通知到我们的 Node.js 层
示例
1 2 3 4
| // 将进程设置为长期存在并且监听用户的输入 process.stdin.on('data',e=>{ const playerAction = e.toString().trim(); })
|
此时依赖的是 Node 的内置模块
process 实际上是 EventEmitter 的实例,继承了 EventEmitter 使其具备了向上抛事件的能力
引出
3.3 EventEmitter
3.3.1 解决了什么问题
- 解决两个对象之间的通信问题
- 函数调用
- 观察者模式(事件收发模式)- 抛事件
- addEventListener
- removeEventListener
3.3.2 普通调用应用场景
- 老板通知秘书
- 说是通知,但是直接调用比较合适
3.3.3 观察者模式应用场景
- 通知消息的人并不知道被通知者的存在(极客时间并不知道我的存在)
- 没有人接收事件,它还能继续下去(今天没有接收到 Geek 上新课的消息,但是它还是可以上新课)
lib.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| const EventEmitter = require('events') class Geektime extends EventEmitter{ constructor(){ super(); setInterval(() => { this.emit('newlesson',{price:Math.random()* 100}) //触发事件 }, 3000); } }
const geektime = new Geektime;
module.exports = geektime
|
index.js
1 2 3 4 5 6 7
| const geektime = require('./lib.js') geektime.addListener('newlesson',(res)=>{ if(res.price < 50){ console.log('buy!当前价格为---',res) } })
|
4 Nodejs 非阻塞 I/O 及异步编程
值得拿出来单独说,戳此一览
5 实现网页版石头剪刀布游戏
技术前置
5.1 什么是 HTTP 服务
一个网页请求,包含两次 HTTP 包交换
- 浏览器向 HTTP 服务器发送请求 HTTP 包
- HTTP 服务器向浏览器返回 HTTP 包
5.2 HTTP 服务要做什么事情
- 解析进来的 HTTP 请求报文
- 返回对应的 HTTP 返回报文
5.3 实现一个简单的 HTTP 服务器
1 2 3 4 5 6 7 8 9
| const http = require('http') const fs = require('fs') http .createServer((req,res)=>{ res.writeHead(200); res.end('hello')
}) .listen(8888)
|
5.4 server 端加载模版
1 2 3 4 5 6 7 8 9 10 11
| const http = require('http') const fs = require('fs') http .createServer((req,res)=>{ res.writeHead(200); res.end('hello') fs.createReadStream(__dirname + '/index.html') .pipe(res)
}) .listen(8888)
|
5.5 游戏逻辑
index.js 源码点击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| const http = require("http"); const fs = require("fs"); const url = require("url"); const querystring = require("queryString"); const game = require("./game.js");
let playerLastAction = null; //玩家上次出的 let playerWon = 0; //玩家赢得次数 let sameCount = 0; //统计相同操作统计次数
http .createServer((req, res) => { // 通过内置模块url,转换发送到该http服务上的http请求包的url, // 将其分割成 协议(protocol)://域名(host):端口(port)/路径名(pathname)?请求参数(query) const parsedUrl = url.parse(req.url); if (parsedUrl.pathname == "/game") { const query = querystring.parse(parsedUrl.query); // 玩家出的 const playerAction = query.action;
// 需求2:如果玩家赢了三次或者玩家作弊,则电脑不给他玩了 if (playerWon >= 3 || sameCount == 9) { res.writeHead(500); res.end("我再也不和你玩了!"); return; } // 需求1:如果玩家操作连续三次相同,视为玩家作弊 if (playerLastAction & (playerLastAction == playerAction)) { sameCount++; } else { sameCount++; }
playerLastAction = playerAction;
if (sameCount >= 3) { res.writeHead(400); res.end("你作弊"); // 将sameCount设置为9 sameCount = 9; return; } // 执行游戏逻辑 var gameResult = game(playerAction); res.writeHead(200); if (gameResult == 0) { res.end("平局!"); } else if (gameResult == 1) { res.end("你赢了!"); // 玩家胜利次数统计+1 playerWon++; } else { res.end("你输了!"); } } // 如果请求url是浏览器icon,比如 http://localhost:3000/favicon.ico的情况 // 就返回一个200就好了 if (parsedUrl.pathname == "/favicon.ico") { res.writeHead(200); res.end(); return; } // 如果访问的是根路径,就把游戏页面读出来返回出去 if (parsedUrl.pathname == "/") { fs.createReadStream(__dirname + "/index.html").pipe(res); } }) .listen(6001);
|
6 使用 express 优化石头剪刀布游戏
6.1 了解 express
要了解一个框架,最好的方法是
- 了解它的关键功能
- 推导出它要解决的问题是什么
核心功能
- 路由
- request/response 简化
- request:pathname、query 等
- response:send ()、json ()、jsonp () 等
- 中间件
- 更好地组织流程代码
- 异步会打破 Express 的洋葱模型
6.2 游戏逻辑
index.js 源码点击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| const fs = require("fs"); const game = require("./game"); const express = require("express");
// 玩家胜利次数,如果超过3,则后续往该服务器的请求都返回500 var playerWinCount = 0; // 玩家的上一次游戏动作 var lastPlayerAction = null; // 玩家连续出同一个动作的次数 var sameCount = 0;
const app = express();
// 通过app.get设定 /favicon.ico 路径的路由 // .get 代表请求 method 是 get,所以这里可以用 post、delete 等。这个能力很适合用于创建 rest 服务 app.get("/favicon.ico", function(request, response) { // 一句 status(200) 代替 writeHead(200); end(); response.status(200); return; });
// 设定 /game 路径的路由 app.get( "/game",
function(request, response, next) { if (playerWinCount >= 3 || sameCount == 9) { response.status(500); response.send("我不会再玩了!"); return; }
// 通过next执行后续中间件 next();
// 当后续中间件执行完之后,会执行到这个位置 if (response.playerWon) { playerWinCount++; } },
function(request, response, next) { // express自动帮我们把query处理好挂在request上 const query = request.query; const playerAction = query.action;
if (!playerAction) { response.status(400); response.send(); return; }
if (lastPlayerAction == playerAction) { sameCount++; if (sameCount >= 3) { response.status(400); response.send("你作弊!我再也不玩了"); sameCount = 9; return; } } else { sameCount = 0; } lastPlayerAction = playerAction;
// 把用户操作挂在response上传递给下一个中间件 response.playerAction = playerAction; next(); },
function(req, response) { const playerAction = response.playerAction; const result = game(playerAction);
// 如果这里执行setTimeout,会导致前面的洋葱模型失效 // 因为playerWon不是在中间件执行流程所属的那个事件循环里赋值的 // setTimeout(()=> { response.status(200); if (result == 0) { response.send("平局"); } else if (result == -1) { response.send("你输了"); } else { response.send("你赢了"); response.playerWon = true; } // }, 500) } );
app.get("/", function(request, response) { // send接口会判断你传入的值的类型,文本的话则会处理为text/html // Buffer的话则会处理为下载 response.send(fs.readFileSync(__dirname + "/index.html", "utf-8")); });
app.listen(6001);
|
7 使用 koa 优化石头剪刀布游戏
7.1 了解 koa
核心功能:
- 比 Express 更极致的 request/response 简化
- ctx.status=200
- ctx.body=’helloworld’
- 使用 async function 实现的中间件
- 有 “暂停执行” 的能力
- 在异步的情况下也符合洋葱模型
- 精简内核,所有额外功能都移到中间件里实现
7.2 Express vs Koa
- Express 门槛更低,Koa 更强大优雅。
- Express 封装更多东西,开发更快速,Koa 可定制型更高
7.3 孰 “优” 孰 “劣”
- 框架之间其实没有优劣之分
- 不同的框架有不同的适用场景
7.4 游戏逻辑
index.js 源码点击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| const fs = require("fs"); const game = require("./game"); const koa = require("koa"); const mount = require("koa-mount");
// 玩家胜利次数,如果超过3,则后续往该服务器的请求都返回500 var playerWinCount = 0; // 玩家的上一次游戏动作 var lastPlayerAction = null; // 玩家连续出同一个动作的次数 var sameCount = 0;
const app = new koa();
app.use( mount("/favicon.ico", function(ctx) { // koa比express做了更极致的response处理函数 // 因为koa使用异步函数作为中间件的实现方式 // 所以koa可以在等待所有中间件执行完毕之后再统一处理返回值,因此可以用赋值运算符 ctx.status = 200; }) );
const gameKoa = new koa(); app.use(mount("/game", gameKoa)); gameKoa.use(async function(ctx, next) { if (playerWinCount >= 3) { ctx.status = 500; ctx.body = "我不会再玩了!"; return; }
// 使用await 关键字等待后续中间件执行完成 await next();
// 就能获得一个准确的洋葱模型效果 if (ctx.playerWon) { playerWinCount++; } }); gameKoa.use(async function(ctx, next) { const query = ctx.query; const playerAction = query.action; if (!playerAction) { ctx.status = 400; return; } if (sameCount == 9) { ctx.status = 500; ctx.body = "我不会再玩了!"; }
if (lastPlayerAction == playerAction) { sameCount++; if (sameCount >= 3) { ctx.status = 400; ctx.body = "你作弊!我再也不玩了"; sameCount = 9; return; } } else { sameCount = 0; } lastPlayerAction = playerAction; ctx.playerAction = playerAction; await next(); }); gameKoa.use(async function(ctx, next) { const playerAction = ctx.playerAction; const result = game(playerAction);
// 对于一定需要在请求主流程里完成的操作,一定要使用await进行等待 // 否则koa就会在当前事件循环就把http response返回出去了 await new Promise(resolve => { setTimeout(() => { ctx.status = 200; if (result == 0) { ctx.body = "平局"; } else if (result == -1) { ctx.body = "你输了"; } else { ctx.body = "你赢了"; ctx.playerWon = true; } resolve(); }, 500); }); });
app.use( mount("/", function(ctx) { ctx.body = fs.readFileSync(__dirname + "/index.html", "utf-8"); }) ); app.listen(6001);
|