一、学习目的和任务
1. 学习目的
- 了解服务端编程;
- 使用 Express 构建 Web 项目;
2. 学习任务
- Node.js 是什么,诞生的作用;
- Node.js 的基本使用;
- 如何使用 Express 构建项目;
3. 什么时候适合进阶
- 学习后端开发;
- Node.js 作为主要后端语言学习;
二、Node.js概述
参见上一篇文章《Node.js的异步I/O》
三、Node.js的基本使用
1. 模块化
Javascript 存在的历史性问题:
-
文件依赖:文件的依赖按照导入的先后顺序确定,依赖不明确且需要手动控制导入顺序;
-
命名冲突:不同文件中,同一个变量、方法等重复命名,后命名的会覆盖前者;
规范:
- 使用 exports 导出本模块中的成员、方法等;
- 使用 require 导入其他模块;
导入系统模块
const http = require('http');
导出自定义方法:
function test() {
console.log('test');
}
exports.test = test;
导入自定义方法:
const file = require('../Server/01-server.js')
file.test();
另外,也可以使用 module.exports 导出,效果一样:
module.exports.test = test;
2. module.exports VS exports
本质是:*obj exports = module.exports
也就是说,exports 是一个指针,指向 module 对象的 exports 属性。默认情况下两者的使用结果一致,但是如果手动修改了module 对象的 exports 属性,exports 对象的指向不会一起改变,仍然指向原来的那个内存地址;
比如:
console.log(module.exports);
console.log(exports);
module.exports = {param1: 'heheda'}
console.log(module.exports);
总结:
- 如果使用了两者,那么以 module.exports 为准;
- 两个导出都一样,别闲的没事去改 module.exports ~~~
3. 系统模块的使用
fs.readFile:读取文件
path.join:系统不一样,分隔符也不一样;
__dirname:当前文件所在文件夹绝对路径;
4. 第三方模块的使用
本地安装:在指定的文件夹下安装,只有当前目录可以使用;
全局安装:在根目录下安装(~),所有项目都可以使用;或者使用 -g
一般而言,命令行工具全局安装,库文件使用本地安装。项目中要用到哪些模块时,应该使用本地安装,不要把全局文件夹弄得很复杂很大。需要注意的是,需要先运行npm init -y 初始化项目之后才能安装第三方库。
npm init -y
npm install express
四、创建Web服务器
导入模块:
// 创建服务器
const http = require('http')
// url解析模块
const url = require('url')
- 开启服务器
const app = http.createServer()
- 监听request事件
app.on('request', (req, res) => {
// next codeing...
});
- 判断请求方法
var method = req.method.toLowerCase();
if (method == 'post') {
} else if (method == 'get'){
}
- 解析出请求路径
var path = '';
if (method == 'post') {
path = req.url;
} else if (method == 'get'){
path = url.parse(req.url).pathname;
}
- 获取参数
if (method == 'post') {
// 事件驱动
// 开始传递参数时,触发data时间
var params = '';
req.on('data', (param) =>{
console.log('params:' + param);
params += param;
});
// 参数传递完成时触发end时间
req.on('end', () =>{
console.log(params);
});
} else if (method == 'get'){
var params = url.parse(req.url, true).query;
}
- 路由
if (path == '/' || path == '/index') {
res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
res.end('<h1>北京欢迎您!</h1>')
} else if (path == '/list'){
res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
res.end('<h1>列表页</h1>')
} else {
res.writeHead('404', {'content-type': 'text/html;charset=UTF8'});
res.end('<h1>404 NOT FOUND!</h1>')
}
- 监听端口
app.listen('3000');
console.log('服务器启动');
- 最终代码
const http = require('http')
const app = http.createServer()
const url = require('url')
app.on('request', (req, res) => {
var path = '';
var method = req.method.toLowerCase();
if (method == 'post') {
// 事件驱动 开始传递参数时,触发data事件
var params = '';
req.on('data', (param) => {
console.log('params:' + param);
params += param;
});
// 参数传递完成时触发end时间
req.on('end', () => {
console.log(params);
});
path = req.url;
} else if (method == 'get') {
params = url.parse(req.url, true).query;
path = url.parse(req.url).pathname;
}
if (path == '/' || path == '/index') {
res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
res.end('<h1>北京欢迎您!</h1>')
} else if (path == '/list') {
res.writeHead('200', {'content-type': 'text/html;charset=UTF8'});
res.end('<h1>列表页</h1>')
} else {
res.writeHead('404', {'content-type': 'text/html;charset=UTF8'});
res.end('<h1>404 NOT FOUND!</h1>')
}
});
app.listen('3000');
console.log('服务器启动');
总结:
Node 开发中,都是以事件驱动,其意义就是什么事件在何时触发做出怎样的响应。
五、 静态资源的处理
静态资源:同一个请求地址,没有参数,直接返回对应的资源文件;
动态资源:同一个请求地址,参数不同,返回不同的资源文件
实现:
创建一个静态资源文件夹,当客户端请求时,通过路由的方式,直接将文件夹中对应的文件响应给客户端。
最简单的处理就是将 http://domain/staticFile 路由到服务器中的 http://domain/public/staticFile,复杂点的会包含文件的递归查询等
缺点:
url 中包含其他路径,不直接请求文件时会出现错误;
其中涉及到的几个点:
-
url.parse
url.parse 方法可以对传入的 url 进行解析,返回一个对象,其中几个常用的属性包括:
hostname:域名
query:参数部分(?后面的内容不包含?)
pathname:访问的文件名
path:完整的访问路径(包括query和?) -
path.join
因为系统的不同会导致文件路径中的分割符的不同,所以不能直接使用/分割符,而是使用系统提供的 join 方法来拼接字符串,方法内部会判断系统从而决定分隔符。 -
第三方插件mime
使用mime来根据路径分析出请求文件的格式并在响应头中设置。虽然大多数高级浏览器都可以自动分析文件的类型从而进行展示,但是安全起见,最好是指定类型。这样设置之后,响应头中也就有了Content-Type字段。
最终代码:
// 用于创建http服务器
const http = require('http');
// 用于将url字符串处理成对象
const url = require('url');
// 用于拼接路径
const path = require('path');
// 用于读取文件
const fs = require('fs');
// 根据请求路径分析文件类型
const mime = require('mime');
// 开启服务器
const app = http.createServer();
// 监听请求事件
app.on('request',(req,res)=>{
var method = req.method.toLowerCase();
// get方法
if (method == 'get') {
var pathName = url.parse(req.url).pathname;
// 拼接文件路径
var filePath = path.join(__dirname, 'public', pathName);
if (pathName == '/'){
// 首页处理
filePath = filePath + 'index.html';
}
// 获取请求文件的类型
var fileType = mime.getType(filePath);
if (pathName.length) {
// 读取文件,异步操作
fs.readFile(filePath, (error, result)=>{
if (!error) {
// 设置文件格式
res.writeHead('200',{
'Content-Type': fileType
});
res.end(result);
} else {
// 设置响应头的响应码、文件类型、编码方式
res.writeHead('404',{
'Content-Type': 'text/html;charset=utf8'
})
// 返回响应
res.end('error');
}
});
}
}
})
// 开启监听
app.listen(3000);
六、异步编程
Node 中的异步 I/O 相关的概念请查阅:Node.js的异步I/O
1. 什么是Promise
Promise 是一个容器,存放着一个操作的执行状态。这个操作通常是一个异步操作,状态分为 pending、resolved、reject。promise 实例对象在创建之后会立即执行,此时状态为 pending,而 resolved() 和 reject() 函数是改变 pending 状态并传递参数的执行函数,而 then 方法则是设置对应的回调函数。当状态改变,对应的回调函数就会异步执行。
2. 基本使用:
resolve():将异步操作的结果置为 resolve 并将数据作为入参传递;
reject():将异步操作的结果置为 reject 并将数据作为入参传递;
then():绑定回调,可以接收两个函数作为参数,第一个为 resolve 的回调函数,第二个为 reject 的回调函数;
catch():相当于 then() 的第二个参数,绑定 reject 的回调
3. 几个比较重要的点
-
Promise()是对象的构造方法,入参函数会被立即执行;
function f2() {
return new Promise((resolve, reject) => {
// Promise 新建之后内部的代码会立即执行,因为Promise()是构造函数
console.log('Promise-f2');
// resolve 和 reject 是改变异步操作的结果,但是触发回调的时机并不只是调用这两个方法
resolve();
});
}
var promise = f2();
// then是异步操作,其内部应该是通过消息来传递和调用
promise.then(()=>{
console.log('resolve');
},(error)=>{
console.log('reject');
});
console.log('sync');
输出结果:
'Promise-f2
sync
resolve
- 异步操作一旦发生改变就永远固定,且状态固定之后再绑定回调仍然能够收到回调;
- 绑定回调的方法内部应该是包含一个查询操作和一个绑定操作。查询操作确保结果已经固定时至少收到一次回调,绑定操作确保结果从未固定(pending)变成已固定时,能够收到通知;
- 绑定回调的方法(then、catch)无论结果是进行中(pending)还是已经固定(resolve、reject),入参函数都是通过类似于消息的机制来传递结果,所以是异步执行;
-
then()只负责绑定回调,resolve()、reject()只负责改变结果并传递参数;内部的消息传递机制不仅仅是靠这两个方法的调用来触发的,比如状态已固定时使用 then 绑定仍然能够进行回调。
上代码:
function f2() {
return new Promise((resolve, reject) => {
// Promise 新建之后内部的代码会立即执行,因为Promise()是构造函数
console.log('Promise-f2');
// resolve 和 reject 是改变异步操作的结果,但是触发回调的时机并不只是调用这两个方法
resolve();
var filePath = path.join(__dirname, 'file3.txt');
// fs.readFile(filePath, (error, result) => { console.log('文件读取完毕:' +
// result); });
});
}
/*
then可以接收两个参数:resolve 的回调、reject 的回调;
*/
var promise = f2();
/**
* then、resolve、reject虽然可以触发回调,但是严格来讲,回调的触发有一个类似于消息系统的存在来进行分发;
* 基本上就是先有一个查询操作,然后:
* 状态为pending,即为未固定。监听状态并者进行绑定,改变之后从绑定表中循环发送消息执行回调;
* 状态已经固定时(resolve、reject),直接发送一个消息,执行回调
* 查询操作保证状态已经固定的情况下至少收到一次结果变更的通知;
* 绑定操作保证状态处于pending时的操作在结果改变时能够收到通知;
* 所以,所有的回调都是异步操作,即使状态已固定时设置回调
*/
promise.then(()=>{
console.log('resolve');
// 结果一旦改变,之后就永远不会变;
promise.then(()=>{
console.log('resolve-inner');
})
console.log('sync-inner');
},(error)=>{
console.log('reject');
});
console.log('sync');
输出结果:
Promise-f2
sync
resolve
sync-inner
resolve-inner
4. 回调地狱和Promise的代码对比
不使用 Promise:
const fs = require('fs')
fs.readFile('./1.txt', 'utf-8', (err, result) => {
console.log(result);
fs.readFile('./2.txt', 'utf-8', (err, result) => {
console.log(result);
fs.readFile('./3.txt', 'utf-8', (err, result) => {
console.log(result);
});
});
});
使用 Promise:
const fs = require('fs');
const path = require('path');
function f1() {
return new Promise((resolve,reject) => {
var filePath = path.join(__dirname, 'file1.txt');
fs.readFile(filePath, (error, result) => {
resolve(result);
console.log('第一个文件读取完毕:' + result);
});
});
}
function f2() {
return new Promise((resolve,reject) => {
var filePath = path.join(__dirname, 'file2.txt');
fs.readFile(filePath, (error, result) => {
resolve(result);
console.log('第二个文件读取完毕:' + result);
});
});
}
function f3() {
return new Promise((resolve,reject) => {
var filePath = path.join(__dirname, 'file3.txt');
fs.readFile(filePath, (error, result) => {
resolve(result);
console.log('第三个文件读取完毕:' + result);
});
});
}
f1().then(f2).then(f3);
f1().then(f2).then(f3);必须这么写,f2 和 f3 都不能加括号,否则函数作为参数就变成了函数的执行结果成了参数,最后 then 接收的是一个 Promise 对象作为参数,会报错;
如果使用 Promise 版本的 readFile:
const path = require('path');
const readFile = require('fs-readfile-promise');
var filePath = path.join(__dirname, 'file1.txt');
readFile(filePath)
// 返回的是包含readFile操作的promise对象
.then((buffer)=>{
// 这里包含文件读取结果的数据,此第三方框架都使用buffer来返回数据
console.log(buffer.toString());
})
// 上一个then方法返回的是一个空的promise对象
.then((data)=>{
// promise内部应该只是简单的调用了resolve,以此来完成链式编程或者实现同步的代码形式
// data为空
console.log(data);
var filePath2 = path.join(__dirname, 'file2.txt');
// readFile经过包装会返回一个promise对象
return readFile(filePath2);
})
// 上一个then方法返回的是一个包含了readfile操作的promise对象
.then((buffer)=>{
console.log(buffer.toString());
});
fs-readfile-promise 这个第三方框架会把 readFile 和 then 方法进行包装返回一个 Promise 对象,这样就不需要手动去包装异步操作了,具体的内容和进阶用法等可以去官网查阅。
另外,写一个返回 Promise 对象的函数进行封装,操作可以简化成:
const fs = require('fs');
const path = require('path');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
console.log(data.toString());
resolve(data);
});
});
};
readFile(path.join(__dirname,'file1.txt'))
.then((result)=>{
return readFile(path.join(__dirname,'file2.txt'));
})
.then((result)=>{
return readFile(path.join(__dirname,'file3.txt'));
})
注意,千万不能写成如下的方式:
readFile(path.join(__dirname,'file1.txt'))
.then(readFile(path.join(__dirname,'file2.txt')))
.then(readFile(path.join(__dirname,'file3.txt')));
这样 then 方法接收的参数是一个 Promise 对象,执行结果就可能不如预期了。
七、异步编程解决方案的演变
1. 遍历器iterator
遍历器是一个对象,一般是由集合对象返回。这个对象可以通过执行 next() 方法来遍历集合对象中的每一个元素。执行 next() 方法返回一个对象,包含集合当前元素的值、是否遍历完成,即{ value: 'a', done: false }
遍历器一般部署在 Symbol.iterator 中,系统为 Array 集合提供了遍历器:
var array = ['a', 'b', 'c', 'd'];
var notDone = true;
var iter = array[Symbol.iterator]();
while (notDone) {
var obj = iter.next();
console.log(obj);
notDone = !obj.done;
}
输出:
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: 'd', done: false }
{ value: undefined, done: true }
2. Generator
首先,Generator 是一个函数,但是这个函数比较特殊:
- 这个函数的形式需要加 * :
function* helloWorldGenerator() {
}
-
返回值是一个遍历器 iterator 对象,可以执行 next() 方法;
-
惰性执行,只有运行 next() 方法之后才会依次执行代码并获取返回值;
-
根据关键字
yeild分段执行代码并返回对应的值,具备记忆功能; -
可以获取多个返回值,return为最后一个返回值,表示函数执行完毕;
上代码:
function* helloWorldGenerator() {
console.log('第一次调用next后本行代码执行');
yield 'hello';// 第一次next()后在此处暂停
console.log('第二次调用next后本行代码执行');
yield 'world';// 第二次next()后在此处暂停
console.log('第三次调用next后本行代码执行');
return function(){
console.log('ending function excuted');
};// 函数全部执行完毕
}
var hw = helloWorldGenerator();
var isDone = false
var obj;
var index = 0;
while(!isDone) {
index++;
obj = hw.next();
console.log('第' + index + '次执行next后的返回值:', obj);
isDone = obj.done;
}
// 执行Generator的最终返回值
obj.value();
输出结果:
第一次调用next后本行代码执行
第1次执行next后的返回值: { value: 'hello', done: false }
第二次调用next后本行代码执行
第2次执行next后的返回值: { value: 'world', done: false }
第三次调用next后本行代码执行
第3次执行next后的返回值: { value: [Function], done: true }
ending function excuted
3. yeild的由来
异步操作顺序执行编程解决方案有好几种:
- 循环嵌套
例子就不举了,上文顺序读取文件的最初代码就是最好的例子;
其最大的缺点是代码横向发展、上下关系嵌套严重,动一发儿牵全身。
- Promise
例子也不举了,上文使用 Promise 写出来的顺序读取文件的代码就是例子;
给异步操作包装 Promise 对象会引起代码量的增加,同时各种 then 方法看起来很混乱、语义不清、不容易理解,头大~~~
- 协程
多个线程互相协作,使用 yield 关键字来交出执行权,暂停代码的运行,当相关操作完成之后再来执行 next 获取执行权继续运行代码,而Generator 就是对协程的实现,所以 Generator 是相对于 Promise 更优的方案;
举个栗子🌰:
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
上文中的 readFile 这一样将会暂停,等到下一次主动调用 next() 后继续执行后面的代码,通常上后续代大概是这样的:
var generator = asyncJob();
var promiseObj = generator.next();
promiseObj.then((error, result)=>{
// ...其他代码
generator.next();
});
那到底什么是协程,Generator 又是怎样应用在异步编程上的呢??
4. Generator在异步函数中的应用
- Generator 函数是协程在 ES6 的实现;
- Generator 是通过关键字 yield 来暂停任务并交出控制权,通过 next() 重新获取控制权,继续执行代码;
- Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因;总体流程大概是:执行完异步操作之后,代码暂停,等到异步操作完成之后,继续执行。
- 数据交换,不仅 Generator 函数可以直接传参,next() 方法也可以直接传参:
略,见https://es6.ruanyifeng.com/#docs/generator-async
- 错误处理机制
Generator 函数可以不用 yield 表达式,这时就变成了一个单纯的暂缓执行函数。
正因为暂缓执行的特性,才有了 Generator 异步函数中的应用。
同样是读取文件的例子,肯定不能使用嵌套,那么必须使用 Promise,然而利用一个函数封装 Promise 的包装操作和异步操作显然更好,所以
基础代码如下:
const fs = require('fs');
const path = require('path');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
console.log(data.toString());
resolve(data);
});
});
};
如果是使用 Promise,代码的执行如下:
readFile(path.join(__dirname,'file1.txt'))
.then((result)=>{
return readFile(path.join(__dirname,'file2.txt'));
})
.then((result)=>{
return readFile(path.join(__dirname,'file3.txt'));
})
而使用 Generator 则是这样的:
function* gen(){
var result1 = yield readFile(path.join(__dirname,'file1.txt'));
var result2 = yield readFile(path.join(__dirname,'file2.txt'));
var result3 = yield readFile(path.join(__dirname,'file3.txt'));
}
var g = gen();
var result1 = g.next();
result1.value.then((data) => {
return g.next().value;
})
.then(() => {
return g.next().value;
})
注意,return g.next().value; 不能写成 return g.next();,否则返回值是一个对象而不是一个 Promise 对象,运行时顺序会和预期不一样;
Generator 有一个优点,如果不看下面的代码,只看上面 gen() 函数的代码,除了 yield 关键字,那么整体代码执行起来简直和同步操作的代码一模一样,如果能封装起来,那岂不是完美?
而就有这样的第三方框架:
var co = require('co');
co(gen);
上面两行代码就可以直接替代后面的 then、next 的代码;
那么,第三方框架是怎么实现的呢?现在就要解决个问题:co模块时如何实现自动执行 next() 的呢?
原理就暂时就不讨论了,太深入了。
此时,只需要知道怎么使用就好了。
async函数
async 函数的本质是 Generator 语法的封装,但是有几个特点:
-
内置执行器
不需要再手动next()或者使用co模块来自动执行了。async 函数和普通函数一样,直接执行即可; -
语法更加清晰
async 代替 * ,await 代替 yield,语义上更清楚;另外,co模块限制很多,比如 yield 后面的返回值必须是 Promise 对象等。异步函数 await 后面可以是基础类型,会被包装成 resolve 的 Promise 对象。
总之,使用异步函数就像使用普通函数一样即可;
- 异步函数返回值是 Promise。Generator 函数返回值时 Iterator 对象,需要手动执行,且继续执行需要包装成 Promise 对象,而异步函数直接返回 Promise 对象,在所有异步操作执行完毕之后可以直接使用 then 来继续执行后续代码;
上述的例子用 async 函数写,就是这个样子:
const asyncReadFile = async function () {
var result1 = await readFile(path.join(__dirname, 'file1.txt'));
var result2 = await readFile(path.join(__dirname, 'file2.txt'));
var result3 = await readFile(path.join(__dirname, 'file3.txt'));
};
asyncReadFile().then(()=>{
console.log('所有文件读取完毕');
});
至此,代码算是相对完美了。。
另外,暂时不需要关注异步函数的实现原理,先会用,以后真正搞服务端了再去好好学习 Thunk 函数等知识;
总结
说了这么多,异步操作的顺序执行的演变经历了如下阶段:
回调地狱 -> Promise -> Generator -> 异步函数
这里将四种方法的代码都贴出来,方便以后对比学习:
const fs = require('fs');
const path = require('path');
const co = require('co');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error);
console.log(data.toString());
resolve(data);
});
});
};
function promiseMethod() {
readFile(path.join(__dirname, 'file1.txt'))
.then((result) => {
return readFile(path.join(__dirname, 'file2.txt'));
})
.then((result) => {
return readFile(path.join(__dirname, 'file3.txt'));
})
}
function genMethod() {
function* gen() {
var result1 = yield readFile(path.join(__dirname, 'file1.txt'));
var result2 = yield readFile(path.join(__dirname, 'file2.txt'));
var result3 = yield readFile(path.join(__dirname, 'file3.txt'));
}
var g = gen();
var result1 = g.next();
result1.value.then((data) => {
return g.next().value;
})
.then(() => {
return g.next().value;
})
}
function coMethod() {
function* gen() {
var result1 = yield readFile(path.join(__dirname, 'file1.txt'));
var result2 = yield readFile(path.join(__dirname, 'file2.txt'));
var result3 = yield readFile(path.join(__dirname, 'file3.txt'));
}
co(gen);
}
function asyncMethod() {
const asyncReadFile = async function () {
var result1 = await readFile(path.join(__dirname, 'file1.txt'));
var result2 = await readFile(path.join(__dirname, 'file2.txt'));
var result3 = await readFile(path.join(__dirname, 'file3.txt'));
};
asyncReadFile().then(()=>{
console.log('所有文件读取完毕');
});
}
// Promise方式调用
promiseMethod();
// Generator方式调用 这个函数还有问题。执行顺序不对
genMethod();
// co模块方式调用
coMethod();
//async方式调用
asyncMethod()
执行结果:
[Running] node "/Users/caoxk/Demo/WebDev/Promise/generator02.js"
hellow Promise!
hellow Promise!
hellow Promise!
hellow Promise!
hellow Promise!--2
hellow Promise!--2
hellow Promise!--2
hellow Promise!--2
hellow Promise!--3
hellow Promise!--3
hellow Promise!--3
hellow Promise!--3
所有文件读取完毕
[Done] exited with code=0 in 0.24 seconds













网友评论