nodejs服务端开发

nodejs服务端开发

1.1. nodejs介绍
1.2. REPL环境
1.3. 模块化
1.4. 异常处理
1.5. 核⼼模块 url、querystring

1. nodejs介绍

http://nodejs.cn/learn/introd...

nodejs在2009年诞⽣,到⽬前为⽌,也仅12年。Node.js 是⼀个开源与跨平台的
JavaScript 运⾏环境。所谓“运⾏环境”有两层意思:⾸先,JavaScript语⾔通过Node在服务器运⾏,在这个意义上,Node有点像JavaScript虚拟机;其次,Node提供⼤量⼯具库,使得JavaScript语⾔与操作系统互动(⽐如读写⽂件、新建⼦进程),在这个意义上,Node⼜是JavaScript的⼯具库。Node内部采⽤Google公司的V8引擎,作为JavaScript语⾔解释器;通过⾃⾏开发的libuv库,调⽤操作系统资源。

2. REPL环境

使⽤node命令可以执⾏⼀个js⽂件,如果node后参数缺省,则进⼊REPL环境

$ node

3. 模块化

3.1. package.json
是模块的清单⽂件,记录了当前模块的基本信息、依赖信息等

nodejs服务端开发_第1张图片
3.2. CommonJS模块化

早期的Javascript(ECMAScript5)中是没有模块化的,nodeJS推出后,使⽤的是

CommonJS 模块规范。后来ES6出现后才出现了ECMAScript模块化,在node-v12后可以使⽤ECMAScript模块化。
CommonJS规定,每个⽂件就是⼀个模块,有⾃⼰的作⽤域。在⼀个⽂件⾥⾯定义的变量、函数、类,都是私有的,对其他⽂件不可⻅。每个模块内部, module 变量代表当前模块。这个变量是⼀个对象,它的 exports 属性(即 module.exports )是对外的接⼝。加载某个模块,其实是加载该模块的 module.exports 属性。 require ⽅法⽤于加载模块。

var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
  1. 所有代码都运⾏在模块作⽤域,不会污染全局作⽤域。
  2. 模块可以多次加载,但是只会在第⼀次加载时运⾏⼀次,然后运⾏结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运⾏,必须清除缓存。
  3. 模块加载的顺序,按照其在代码中出现的顺序。
  • module
    每个模块内部,都有⼀个 module 对象,代表当前模块。
    nodejs服务端开发_第2张图片
  • require
    Node使⽤CommonJS模块规范,内置的 require 命令⽤于加载模块⽂件。 require 命令的基本功能是,读⼊并执⾏⼀个JavaScript⽂件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。 需要注意的是:
    1、如果参数字符串以“/”开头,则表示加载的是⼀个位于绝对路径的模块⽂件。
    比如,require('/home/marco/foo.js') 将加载 /home/marco/foo.js 。
    2、如果参数字符串以“./”开头,则表示加载的是⼀个位于相对路径(跟当前执⾏脚本的位置相⽐)的模块⽂件。⽐如, require('./circle') 将加载当前脚本同⼀⽬录的
    circle.js 。
    3、如果参数字符串不以“./“或”/“开头,则表示加载的是⼀个默认提供的核⼼模块(位于Node的系统安装⽬录中),或者⼀个位于各级node_modules⽬录的已安装模块(全局安装或局部安装)。

在ES6中每⼀个模块即是⼀个⽂件,在⽂件中定义的变量,函数,对象在外部是⽆法获取
的。如果你希望外部可以读取模块当中的内容,就必须使⽤export来对其进⾏暴露(输出)。
 export
export命令规定的是对外的接⼝,必须与模块内部的变量建⽴⼀⼀对应关系,也就是说外部接⼝需要⽤这个接⼝名来引⽤。

 var firstName = 'Michael';
 var lastName = 'vicky';
 export { firstName, lastName }; //列表导出

 export { firstName as first, lastName as last}; //重命名导出

 export var a = 3; //导出单个属性
 export function multiply(x, y) { return x * y; }; //导出单个属性

 //默认导出,一个模块只能有一个默认导出,不能使用 var、let 或 const 用于导出默认值 ex
 export default {}
 export default function foo(){}
 var a = 1;
 export a; //报错,因为没有提供对外的接口。应该export var a = 1; 或者export {a}

 import
静态的import 语句⽤于导⼊由另⼀个模块导出的绑定。

 import * as person from './person.js' //导入整个模块内容
 import {firstName,lastName} from './person.js' //导入多个接口
 import {firstName as name} from './person.js' //重命名
 import '/modules/my-module.js'; //运行整个模块而不导入任何值
 import myDefault from './my-module.js'; // 导入使用export default导出的

3.4. 核心模块
 http 提供HTTP服务器功能。
 fs 与⽂件系统交互。
 url 解析url字符串
 querystring 解析URL的查询字符串。
 util 提供⼀系列实⽤⼩⼯具。
 path 处理⽂件路径。
 crypto提供加密和解密功能,基本上是对OpenSSL的包装。

4. 异常处理

Node是单线程运⾏环境,⼀旦抛出的异常没有被捕获,就会引起整个进程的崩溃。
所以,Node的异常处理对于保证系统的稳定运⾏⾮常重要。⼀般来说,Node有三种⽅法,传播⼀个错误。

  • 使⽤throw语句抛出⼀个错误对象,即抛出异常。
  • 将错误对象传递给回调函数,由回调函数负责发出错误。
  • 通过EventEmitter接⼝,发出⼀个error事件。
    4.1. try-catch
    ⼀般来说,Node只在很少场合才⽤try/catch语句,⽐如使⽤ JSON.parse 解析JSON⽂
    本。这个结构⽆法捕获异步运⾏的代码抛出的异常。

     // 可以捕获
     try {
     console.log(a);
     } catch (err) {
     console.log("捕获异常:",err);
     }
     // 无法捕获
     try {
     setTimeout(()=>{
     console.log(a);
     },0)
     } catch (err) {
     console.log("捕获异常:",err);
     }

    4.2. 回调函数
    Node采⽤的⽅法,是将错误对象作为第⼀个参数,传⼊回调函数。这样就避免了捕获代码
    与发⽣错误的代码不在同⼀个时间段的问题。

     fs.readFile('/foo.txt', function(err, data) {
     if (err !== null) throw err;
     console.log(data);
     });

    4.3. EventEmitter接⼝的error事件
    发⽣错误的时候,也可以⽤EventEmitter接⼝抛出error事件。

     var EventEmitter = require('events').EventEmitter;
    
     var emitter = new EventEmitter();
     emitter.emit('error', new Error('something bad happened'));
     emitter.on('error', function(err) {
     console.error('出错:' + err.message);
     });

    5. url模块

    提供解析url的⼯具,⼀个完整的href结构如下:
    nodejs服务端开发_第3张图片
    5.1. url.parse()
    将url字符串地址转换为URL对象

     let url = require('url')
     const myURL = url.parse('https://user:[email protected]:8080/p/a/t/h?que
    ry=string#hash');
     console.log(myURL);

    5.2. url.format()
    构建⼀个URL字符串

     const url = require('url');
     url.format({
     protocol: 'https',
     hostname: 'example.com',
     pathname: '/some/path',
     query: {
    page: 1,
    format: 'json'
     }
     });  // => 'https://example.com/some/path?page=1&format=json'

    5.3. url.resolve(from,to)
    合并url字符串

     const url = require('url');
     url.resolve('/one/two/three', 'four'); // '/one/two/four'
     url.resolve('http://example.com/', '/one'); // 'http://example.com/one'
     url.resolve('http://example.com/one', '/two'); // 'http://example.com/two'

    5.4. url.hash
    获取或设置URL中hash值

     const myURL = new URL('https://example.org/foo#bar');
     console.log(myURL.hash);// Prints #bar
     myURL.hash = 'baz';
     console.log(myURL.href);// Prints https://example.org/foo#baz

    5.5. url.host
    获取或者设置URL中的host

     const myURL = new URL('https://example.org:81/foo');
     console.log(myURL.host); // Prints example.org:81
     myURL.host = 'example.com:82';
     console.log(myURL.href); // Prints https://example.com:82/foo

    5.6. url.hostname
    设置或获取URL中的hostname,与host不同的是,hostname不包含端⼝

     const myURL = new URL('https://example.org:81/foo');
     console.log(myURL.hostname);
     // Prints example.org

    5.7. url.origin
    获取URL中的origin

     const myURL = new URL('https://example.org/foo/bar?baz');
     console.log(myURL.origin);
     // Prints https://example.org

    5.8. url.pathname
    获取或设置URL中的路径

     const myURL = new URL('https://example.org/abc/xyz?123');
     console.log(myURL.pathname);// Prints /abc/xyz

    5.9. url.port
    获取或设置URL中的端⼝号

     const myURL = new URL('https://example.org:8888');
     console.log(myURL.port);
     // Prints 8888

    5.10. url.protocol
    获取或设置URL的协议

     const myURL = new URL('https://example.org');
     console.log(myURL.protocol);// Prints https:
    
     myURL.protocol = 'ftp';
     console.log(myURL.href); // Prints ftp://example.org/

    5.11. url.search
    获取或设置URL的查询字符串

     const myURL = new URL('https://example.org/abc?123');
     console.log(myURL.search);
     // Prints ?123
    
     myURL.search = 'abc=xyz';
     console.log(myURL.href); // Prints https://example.org/abc?abc=xyz
  • querystring模块
    7.1. querystring.parse(str[, sep[, eq[, options]]])
    将查询字符串解析为⼀个对象

     let querystring = require('querystring');
     let qs = "name=terry&age=12&gender=male"
     console.log(querystring.parse(qs));// { name: 'terry', age: '12', gender: 'male' }

    7.2. querystring.stringify(obj[, sep[, eq[, options]]])
    将⼀个对象序列化为查询字符串

      let querystring = require('querystring');
      let qs_obj = { name: 'terry', age: '12', gender: 'male' };
      console.log(querystring.stringify(qs_obj))
      name=terry&age=12&gender=male
     

    7.3. querystring.escape
    对查询字符串进⾏编码

     let querystring = require('querystring');
    
     console.log(querystring.escape('name=张三&age=12'));
     console.log(querystring.unescape('name%3D%E5%BC%A0%E4%B8%89%26age%3D12'));
    
     //name%3D%E5%BC%A0%E4%B8%89%26age%3D12
     //name=张三&age=12

    7.4. querystring.unescape
    对查询字符串进⾏解码

  • path模块
    提供了⼀系列解析路径的⼯具
    8.1. path.basename(str[, ext]))
    返回路径中最后⼀部分

      let path = require('path')
      let name = '/Users/lichunyu/Desktop/2021/web2102/nodejs/prepare/3-path.js'
      console.log(path.basename(name));
      console.log(path.basename(name,'.js'));
      //3-path.js
      //3-path

8.2. path.dirname(path)
返回路径中代表⽂件夹的部分

1 let path = require('path')
2 let name = '/Users/lichunyu/Desktop/2021/web2102/nodejs/prepare/3-path.js'
3 console.log(path.dirname(name));
4 // /Users/lichunyu/Desktop/2021/web2102/nodejs/prepare

8.3. path.extname(path)
返回路径中⽂件的后缀名,即路径中最后⼀个'.'之后的部分。如果⼀个路径中并不包含'.'或
该路径只包含⼀个'.' 且这个'.'为路径的第⼀个字符,则此命令返回空字符串。

1 let path = require('path')
2 let name = '/Users/lichunyu/Desktop/2021/web2102/nodejs/prepare/3-path.js'
3 console.log(path.extname(name));
4 // .js

8.4. path.format(pathObject)
从对象中返回路径字符串,和 path.parse 相反。

1 path.format({root: '/',name: 'file',ext: '.txt'});
2 // Returns: '/file.txt'
3 path.format({root: '/',base: 'file.txt',ext: 'ignored'});
4 // Returns: '/file.txt'
5 path.format({ root: '/ignored',dir: '/home/user/dir',base: 'file.txt'});
6 // Returns: '/home/user/dir/file.txt'
7 path.format({dir: 'C:\\path\\dir',base: 'file.txt'});
8 // Returns: 'C:\\path\\dir\\file.txt'

8.5. path.isAbsolute(path)
判断参数 path 是否是绝对路径。

1 let path = require('path')
2 let name = '/Users/lichunyu/Desktop/2021/web2102/nodejs/prepare/3-path.js'
3 console.log(path.isAbsolute(name));
4 // true

8.6. path.join([...paths])
连接多个地址

1 path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
2 // Returns: '/foo/bar/baz/asdf'

8.7. path.normalize(path)
标准化路径,可以识别"." ".."

1 path.normalize('/foo/bar//baz/asdf/quux/..');
2 // Returns: '/foo/bar/baz/asdf'

8.8. path.parse(path)
返回路径字符串的对象。

1 let path = require('path')
2 let name = '/Users/lichunyu/Desktop/2021/web2102/nodejs/prepare/3-path.js'
3 console.log(path.parse(name));
4 /*
5 {
6 root: '/',
7 dir: '/Users/lichunyu/Desktop/2021/web2102/nodejs/prepare',
8 base: '3-path.js',
9 ext: '.js',
10 name: '3-path'
11 }
12 */

8.9. path.relative(from, to)
⽤于将绝对路径转为相对路径,返回从 from 到 to 的相对路径(基于当前⼯作⽬录)

1 path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
2 // Returns: '../../impl/bbb'

8.10. path.resolve([...paths])
解析为绝对路径,给定的路径的序列是从右往左被处理的,后⾯每个 path 被依次解析,直
到构造完成⼀个绝对路径。

1 path.resolve('/foo/bar', './baz');
2 // Returns: '/foo/bar/baz'
3 path.resolve('/foo/bar', '/tmp/file/');
4 // Returns: '/tmp/file'
5 path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
6 // If the current working directory is /home/myself/node,
7 // this returns '/home/myself/node/wwwroot/static_files/gif/image.gif'

8.11. path.sep
平台的⽂件路径分隔符,'\' 或 '/'。

1 'foo/bar/baz'.split(path.sep);
2 //linux中 Returns: ['foo', 'bar', 'baz']

8.12. path.win32
提供包含上述 path 的⽅法的对象,不过总是以 win32 兼容的⽅式交互。
8.13. path.posix
提供包含上述 path 的⽅法的对象
8.14. path.delimiter
路径分隔符,linux操作系统分隔符为":", windows操作系统分隔符为";"

1 let path = require('path')
2 console.log(path.delimiter);
3 //mac :

9. util模块

util模块提供了很多⼯具函数,具体可参照:
https://nodejs.org/dist/lates...
9.1. util.callbackify
将 async 异步函数(或者⼀个返回值为 Promise 的函数)转换成遵循异常优先的回调
⻛格的函数,例如将 (err, value) => ... 回调作为最后⼀个参数。 在回调函数中,第⼀个参数为拒绝的原因(如果 Promise 解决,则为 null ),第⼆个参数则是解决的值。

1 const util = require('util');
2 async function fn() {
3 return 'hello world';
4 }
5 const callbackFunction = util.callbackify(fn);
6 callbackFunction((err, ret) => {
7 if (err) throw err;
8 console.log(ret);
9 });

1.1. nodejs架构组成
1.2. 事件循环
1.3. 模拟事件
1.4. fs模块

2. nodejs架构组成

Node.js是⼀个构建在Chrome浏览器 V8引擎 上的JavaScript运⾏环境,使⽤ 单线程 、 事件驱动 、 非阻塞I/O 的⽅式实现了⾼并发请求, libuv 为其提供了异步编程的能⼒。

Node.js标准库由Javascript编写的,也就是我们使⽤过程中直接能调⽤的API,在源码中的lib⽬录下可以看到,诸如 http、fs、events 等常⽤核⼼模块

Node bindings 可以理解为是javascript与C/C++库之间建⽴连接的 桥 ,通过这个桥,底层实现的C/C++库暴露给javascript环境,同时把 js传入V8 , 解析后交给 libuv 发起 非阻塞I/O , 并等待 事件循环 调度;

V8: Google推出的Javascript虚拟机,为Javascript提供了在⾮浏览器端运⾏的环境;libuv:为Node.js提供了跨平台,线程池,事件池,异步I/O 等能⼒,是Nodejs之所以⾼效的主要原因;C-ares:提供了异步处理DNS相关的能⼒;http_parser、OpenSSL、zlib等:提供包括http解析、SSL、数据压缩等能⼒;
nodejs服务端开发_第4张图片

2.1. 单线程
任务调度⼀般有两种⽅案: ⼀是 单线程串行执行 ,执⾏顺序与编码顺序⼀致,最⼤的问题是⽆法充分利⽤多核CPU,当并⾏极⼤的时候,单核CPU理论上计算能⼒是100%; 另⼀种就是 多线程并行处理 ,优点是可以有效利⽤多核CPU,缺点是创建与切换线程开销⼤,还涉及到锁、状态同步等问题, CPU经常会等待I/O结束,CPU的性能就⽩⽩消耗。

通常为客户端连接创建⼀个线程需要消耗2M内存,所以理论上⼀台8G的服务器,在Java应⽤中最多⽀持的并发数是4000。⽽Node.js只使⽤⼀个线程,当有客户端连接请求时,触发内部事件,通过⾮阻塞I/O,事件驱动机制,让其看起来是并⾏的。理论上⼀台8G内存的服务器,可以同时容纳3到4万⽤户的连接。Node.js采⽤单线程⽅案,免去锁、状态同步等繁杂问题,⼜能提⾼CPU利⽤率。Node.js⾼效的除了因为其单线程外,还必须配合下⾯要说的⾮阻塞I/O。

2.2. ⾮阻塞IO

发起I/O操作不等得到响应或者超时就⽴即返回,让进程继续执⾏其他操作,但是要通过轮询⽅式不断地去check数据是否已准备好。Java属于阻塞IO,即在发起I/O操作之后会⼀直阻塞着进程,不执⾏其他操作,直到得到响应或者超时为⽌;

Node.js中采⽤了⾮阻塞型I/O机制,因此在执⾏了读数据的代码之后,将⽴即转⽽执⾏其后⾯的代码,把读数据返回结果的处理代码放在回调函数中,从⽽提⾼了程序的执⾏效率。当某个I/O执⾏完毕时,将以事件的形式通知执⾏I/O操作的线程,线程执⾏这个事件的回调函数。

例如:当你的 JavaScript 程序发出了⼀个 Ajax 请求(异步)去服务器获取数据,在回调函数中写了相关 response 的处理代码。 JavaScript 引擎就会告诉宿主环境: “嘿,我现在要暂停执⾏了,但是当你完成了这个⽹络请求,并且获取到数据的时候,请回来调⽤这个函数。“然后宿主环境(浏览器)设置对⽹络响应的监听,当返回时,它将会把回调函数插⼊到事件循环队列⾥然后执⾏。

3. 事件循环

nodejs服务端开发_第5张图片
3.1. 基本流程

  1. 每个Node.js进程只有⼀个主线程在执⾏程序代码,形成⼀个 执行栈 (execution contextstack);
  2. 主线程之外,还维护⼀个 事件队列 (Event queue),当⽤户的 网络请求或者其它的异步操作到来时,会先进⼊到事件队列中排队,并不会⽴即执⾏它,代码也不会被阻塞,继续往下⾛,直到主线程代码执⾏完毕;
  3. 主线程代码执⾏完毕完成后,然后通过 事件循环机制 (Event Loop),检查队列中是否有要处理的事件,从队头取出第⼀个事件,从 线程池 分配⼀个线程来处理这个事件,然后是第⼆个,第三个,直到队列中所有事件都执⾏完了。当有事件执⾏完毕后,会通知主线程,主线程执⾏回调,并将线程归还给线程池。这个过程就叫 事件循环 (Event Loop);
  4. 不断重复上⾯的第三步;

3.2. 事件循环的6个阶段
nodejs服务端开发_第6张图片
 timers: 这个阶段执⾏定时器队列中的回调如 setTimeout() 和 setInterval() 。

 I/O callbacks: 这个阶段执⾏⼏乎所有的回调。但是不包括close事件,定时器和
setImmediate() 的回调。

 idle, prepare: 这个阶段仅在内部使⽤,可以不必理会。

 poll: 等待新的I/O事件,node在⼀些特殊情况下会阻塞在这⾥。v8引擎将js代码解析后传⼊libuv引擎后,循环⾸先进⼊poll阶段。poll阶段的执⾏逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执⾏回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进⼊check阶段执⾏这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调⽤顺序放到timerqueue中,之后循环会进⼊timer阶段执⾏queue中的 callback。 这两者的顺序是不固定的,收到代码运⾏的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有⼀个i/o事件返回,循环会进⼊i/o callback阶段并⽴即执⾏这个事件的callback。

 check: setImmediate() 的回调会在这个阶段执⾏。

 close callbacks: 例如 socket.on('close', ...) 这种close事件的回调。

  • Event Loop 从任务队列获取任务,然后将任务添加到执⾏栈中( 动态,根据函数调
    ⽤),JavaScript 引擎获取执⾏栈最顶层元素(即正在运⾏的执⾏上下⽂)进⾏运⾏!

3.3. 宏任务
Event Loop 有⼀个或多个任务(宏任务)队列,当 执⾏栈 为空时,会从任务队列⾥获取任务,加⼊到执⾏栈中,这⾥的任务就是 宏任务。如下都是宏任务:
 Events
 Parsing
 Callbacks
 Using a resource(I/O)
 Reacting to DOM manipulation
 script
 setTimeout/setInterval/setImmediate
 requestAnimationFrame

3.4. 微任务
每个 Event Loop 有⼀个微任务队列,同时有⼀个 microtask checkpoint ,即每执⾏完成⼀个宏任务后,就会 check 微任务。所以,当某个 宏任务 执⾏完成后,会先执⾏ 微任务 队列,执⾏完成后,再次获取新的 宏任务。这⾥微任务相当于插队操作!!如下都是微任务:
 Process.nextTick
 Promise(aync/await)
 Object.observe(已过期)
 MutationObserver(监听dom树变化)

3.5. process.nextTick()
当将⼀个函数传给 process.nextTick() 时,则指示引擎在当前操作结束(在下⼀个事
件循环滴答开始之前)时调⽤此函数 ,意味着插队。所以,当要确保在下⼀个事件循环迭代中代码已被执⾏,则使⽤ nextTick().

3.6. setImmediate()
作为 setImmediate() 参数传⼊的任何函数都是在事件循环的下⼀个迭代中执⾏的回调。需要注意的是:传给 process.nextTick() 的函数会在事件循环的当前迭代中(当前操作结束之后)被执⾏。 这意味着它会始终在 setTimeout 和 setImmediate 之前执⾏;延迟 0 毫秒的 setTimeout() 回调与 setImmediate() ⾮常相似。 执⾏顺序取决于各种因素,但是它们都会在事件循环的下⼀个迭代中运⾏。

4. 模拟事件

Node.js 的⼤部分核⼼ API 都是围绕惯⽤的异步事件驱动架构构建的,在该架构中,某些类型的对象(称为"触发器")触发命名事件,使 Function 对象("监听器")被调⽤。

1 const EventEmitter = require('events');
2 class MyEmitter extends EventEmitter {}
3 const myEmitter = new MyEmitter();
4 myEmitter.on('event', () => {
5 console.log('an event occurred!');
6 });
7 myEmitter.emit('event');

4.1. API
 emit(eventName[,param,param])
事件发射器,eventName表示模拟时间名,param为传递的参数。

 on(eventName, (param,param...)=>{}
事件注册

 once(eventName, (param,param...)=>{}
事件注册⼀次,该监听器最多为特定事件调⽤⼀次。 ⼀旦事件被触发,则监听器就会被注
销然后被调⽤。

 emitter.addListener(eventName, listener)

 emitter.eventNames()
返回列出触发器已为其注册监听器的事件的数组。 数组中的值是字符串或 Symbol

 emitter.listeners(eventName)
返回名为 eventName 的事件的监听器数组的副本。

 emitter.off(eventName, listener)
事件解绑

 emitter.removeListener(eventName, listener)
事件解绑

 emitter.removeAllListeners([eventName])
事件全部解绑

4.2. 模拟事件机制

1 /**
2 * 事件机制
3 */
4 class EventEmitter{
5 constructor(){
6 this.listeners = {}; // 存放事件监听函数{ "event1": [f1,f2,f3], "eve
7 }
8 // 在jq中,一个事件源可以绑定多个事件处理函数,同一个事件可以绑定多个
9 addEventListener(eventName, handler){
10 let listeners = this.listeners;
11 // 先判断这个如果这个事件类型对应的值是一个数组,说明其中已经绑定过了事件处理
12 // 如果该事件处理函数未绑定过,进行绑定
13 if (listeners[eventName] instanceof Array) {
14 if (listeners[eventName].indexOf(handler) === -1) {
15 listeners[eventName].push(handler);
16 }
17 } else {
18 // 否则,完成该事件类型的首次绑定
19 listeners[eventName] = [].concat(handler);
20 }
21 }
22 // 移除事件处理函数
23 removeEventListener(eventName, handler){
24 let listeners = this.listeners;
25 let arr = listeners[eventName] || [];
26 // 找到该事件处理函数在数组中的位置
27 let i = arr.indexOf(handler);
28 // 如果存在,则删除
29 if (i >= 0) {
30 listeners[eventName].splice(i, 1);
31 }
32 }
33 // 派发事件,本质上就是调用事件
34 dispatch(eventName, params){
35 this.listeners[eventName].forEach(handler => {
36 handler.apply(null, params);
37 });
38 }
39 }

5. fs模块

fs 是 filesystem 的缩写,该模块提供本地⽂件的读写能⼒,这个模块⼏乎对所有操作
提供异步和同步两种操作⽅式,供开发者选择。
5.1. readFile()
异步读取数据。该⽅法的第⼀个参数是⽂件的路径,可以是绝对路径,也可以是相对路径。注意,如果是相对路径,是相对于当前进程所在的路径。第⼆个参数是读取完成后的回调函数。该函数的第⼀个参数是发⽣错误时的错误对象,第⼆个参数是代表⽂件内容的 Buffer 实例。

1 fs.readFile('./image.png', function (err, buffer) {
2 if (err) throw err;
3
4 });
5

5.2. readFileSync()
⽤于同步读取⽂件,返回⼀个Buffer实例。法的第⼀个参数是⽂件路径,第⼆个参数可以是⼀个表示配置的对象,也可以是⼀个表示⽂本⽂件编码的字符串。默认的配置对象是 {
encoding: null, flag: 'r' } ,即⽂件编码默认为 null ,读取模式默认为 r (只读)。

1 var text = fs.readFileSync(fileName, 'utf8');
2 // 将文件按行拆成数组
3 text.split(/\r?\n/).forEach(function (line) {
4 // ...
5 });

5.3. writeFile()
⽤于异步写⼊⽂件。该⽅法的第⼀个参数是写⼊的⽂件名,第⼆个参数是写⼊的字符串,第三个参数是回调函数。回调函数前⾯,还可以再加⼀个参数,表示写⼊字符串的编码(默认是utf8 )

1 fs.writeFile('message.txt', 'Hello Node.js', (err) => {
2 if (err) throw err;
3 console.log('It\'s saved!');
4 });

5.4. writeFileSync()
该⽅法⽤于同步写⼊⽂件

1 fs.writeFileSync(fileName, str, 'utf8');
5.5. exists(path, callback)
5.6. mkdir()
5.7. mkdirSync()
5.8. readdir()
5.9. readdirSync()
5.10. stat()
5.11. createReadStream()
5.12. createWriteStream()

1.1. http模块api
1.2. http协议
1.3. http缓存机制
1.4. 三次握⼿四次挥⼿

2. Http模块

1 var http = require('http');
2
3 http.createServer(function (request, response){
4 response.writeHead(200, {'Content-Type': 'text/plain'});
5 response.write("Hello World");
6 response.end();
7 }).listen(8080, '127.0.0.1');

2.1. http
 http.METHODS

 http.STATUS_CODES

 http.createServer(options[, callback]
创建服务,callback中的参数类型为ClientRequest,ServerResponse

 http.request(options[, callback])
发送requt请求,callback中的参数类型为IncomingMessage

 http.get(url,options])
客户端发送请求,由于多数请求都是get请求,get请求没有请求体,nodejs提供了http.get
这个简便⽅法。callback中的参数类型为IncomingMessage

2.2. http.Server
 事件: upgrate
 事件: connect
 事件: connection
 事件: request
 server.headersTimeout
 server.maxHeadersCount
 server.requestTimeout
 server.timeout
 server.keepAliveTimeout
 server.listen()
 server.close()
 server.setTimeout(msecs)

2.3. http.IncomingMessage
 data
当请求接收到数据⽚段

 事件:end
当请求接收到完数据

 事件:response
当请求完成获取响应对象

 message.url
获取请求的路径及参数

 message.method
获取请求⽅法

 message.statusCode
 message.statusMessage
 message.rawHeaders
 message.headers
 message.httpVersion

2.4. http.ClientRequest
客户端请求,该请求对象可以向后台发送请求,后台也可以接受前台发过来的请求对象。
http.request()返回⼀个http.ClientRequest对象,

 事件:response
当请求完成获取响应对象
 request.path
获取请求的路径及参数
 request.method
获取请求⽅法
 request.host
 request.protocol
 request.write(chunk, encoding)
向请求体中可以写⼊很多数据⽚段,encoding表示编码,callback为请求输⼊写⼊完成后调⽤。
 request.end([data[, encoding]][, callback])
结束发送请求,如果data有数据,表示先write数据,再end请求
 request.writableEnded
请求是否写⼊完成
 request.removeHeader(name)
删除请求头
1 request.removeHeader('Content-Type');
 request.setHeader(name, value)
设置请求头信息,value可以为单值也可以为⼀个字符串数组

1 request.setHeader('Content-Type', 'application/json');
2 request.setHeader('Cookie', ['type=ninja', 'language=javascript']);

 request.getHeader(name)
获取请求头信息
 request.setTimeout(timeout[, callback])
设置超时时间
 request.destroy([error])
销毁⼀个请求
 request.destroyed
判断⼀个请求是否被销毁
 request.flushHeaders()
刷新请求头信息到请求中
 request.getRawHeaderNames()
获取请求头key值数组

2.5. http.ServerResponse
该实例创建于服务器端,⽤于封装或封装了服务器端响应信息。
 事件:data
 response.write(chunk, encoding)
写⼊响应数据
 response.end([data[, encoding]][, callback])
表示当前response已经完成响应头、响应体的发送
 response.flushHeaders()
刷新response的响应头
 response.writeHead(statusCode, statusMessage)
写⼊响应头
 response.getHeader(name)
获取响应头信息
 response.getHeaderNames()
获取响应头key值集合
 response.getHeaders()
获取响应头
 response.hasHeader(name)
判断响应头信息中是否包含name
 response.removeHeader(name)
从响应头信息中移除key为name的值
 response.socket
从响应头中获取socket信息
 response.statusCode
获取状态码
 response.statusMessage
获取状态信息

3. http协议

HTTP是⼀种能够获取如 HTML 这样的⽹络资源的 protocol(通讯协议)。它是在 Web 上进⾏数据交换的基础,是⼀种 client-server 协议,也就是说,请求通常是由像浏览器这样的接受⽅发起的。⼀个完整的Web⽂档通常是由不同的⼦⽂档拼接⽽成的,像是⽂本、布局描述、图⽚、视频、脚本等等。
nodejs服务端开发_第7张图片
3.1. 请求报⽂
请求由以下元素组成:
 ⼀个HTTP的method,经常是由⼀个动词像 GET , POST 或者⼀个名词像 OPTIONS ,
HEAD 来定义客户端的动作⾏为。通常客户端的操作都是获取资源(GET⽅法)或者发送H
TML form表单值(POST⽅法),虽然在⼀些情况下也会有其他操作。
 要获取的资源的路径,通常是上下⽂中就很明显的元素资源的URL,它没有protocol
( http:// ),domain( developer.mozilla.org ),或是TCP的port (en-US)
(HTTP⼀般在80端⼝)。
 HTTP协议版本号。
 为服务端表达其他信息的可选头部headers。
 对于⼀些像POST这样的⽅法,报⽂的body就包含了发送的资源,这与响应报⽂的body类似。
nodejs服务端开发_第8张图片
3.2. 响应报⽂
响应报⽂包含了下⾯的元素:
 HTTP协议版本号。
 ⼀个状态码(status code),来告知对应请求执⾏成功或失败,以及失败的原因。
 ⼀个状态信息,这个信息是⾮权威的状态码描述信息,可以由服务端⾃⾏设定。
 HTTP headers,与请求头部类似。
 可选项,⽐起请求报⽂,响应报⽂中更常⻅地包含获取的资源body。
nodejs服务端开发_第9张图片

4. http缓存机制

缓存的种类有很多,其⼤致可归为两类:私有与共享缓存。共享缓存存储的响应能够被多个⽤户使⽤。私有缓存只能⽤于单独⽤户。本⽂将主要介绍浏览器与代理缓存,除此之外还有⽹关缓存、CDN、反向代理缓存和负载均衡器等部署在服务器上的缓存⽅式,为站点和 web 应⽤提供更好的稳定性、性能和扩展性。
nodejs服务端开发_第10张图片
HTTP/1.1定义的 Cache-Control 头⽤来区分对缓存机制的⽀持情况, 请求头和响应头
都⽀持这个属性。通过它提供的不同的值来定义缓存策略。

1 # 没有缓存
2 Cache-Control: no-store
3 # 缓存但需要重新验证:每次有请求发出时,缓存会将此请求发到服务器,服务器端会验证请求
中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。
4 Cache-Control: no-cache
5 # 公共缓存,表示该响应可以被任何中间件(比如中间代理、CDN等)缓存
6 Cache-Control: public
7 # 私有缓存,表示该响应只能被浏览器缓存
8 Cache-Control: private
9 # 表示资源能够被缓存(保持新鲜)的最大时间。
10 Cache-Control: max-age=31536000

当客户端发起⼀个请求时,缓存检索到已有⼀个对应的陈旧资源(缓存副本),则缓存会先将此请求附加⼀个 If-None-Match 头,然后发给⽬标服务器,以此来检查该资源副本是否是依然还是算新鲜的,若服务器返回了 304 (Not Modified)(该响应不会有带有实体信息),则表示此资源副本是新鲜的,这样⼀来,可以节省⼀些带宽。若服务器通过 If-None-Match 或If-Modified-Since判断后发现已过期,那么会带有该资源的实体内容返回。

If-Modified-Since,和 Last-Modified ⼀样都是⽤于记录⻚⾯最后修改时间的 HTTP 头信息,只是 Last-Modified 是由服务器往客户端发送的 HTTP 头,⽽ If-Modified-Since 则是由客户端往服务器发送的头。If-None-Match的⼯作原理是在HTTP Response中添加ETags信息。

浏览器服务器交互过程:
1.客户端请求⼀个⻚⾯(A)。
2.服务器返回⻚⾯A,并在给A加上⼀个ETag。
3.客户端展现该⻚⾯,并将⻚⾯连同ETag⼀起缓存。
4.客户再次请求⻚⾯A,并将上次请求时服务器返回的ETag⼀起传递给服务器。
5.服务器检查该ETag,并判断出该⻚⾯⾃上次客户端请求之后还未被修改,直接返回响应304
(未修改——Not Modified)和⼀个空的响应体。
nodejs服务端开发_第11张图片

4.1. http1.0、http1.1、http2.0
在HTTP1.0中默认是短连接:每次与服务器交互,都需要新开⼀个连接!
在HTTP1.1中默认就使⽤持久化连接来解决:建⽴⼀次连接,多次请求均由这个连接完
成!(如果阻塞了,还是会开新的TCP连接的)
HTTP2与HTTP1.1最重要的区别就是解决了线头阻塞的问题!其中最重要的改动是:多
路复⽤ (Multiplexing)

5. 三次握⼿

 ACK (Acknowledge character)确认字符,表示接收到的字符⽆错误
 SYN(Synchronize Sequence Numbers)同步序列编号
 FIN(Finish)结束编号
 MSL( Maximum Segment Lifetime) 表示“最⻓报⽂段寿命”,它是任何报⽂在⽹络上存
在的最⻓时间,超过这个时间报⽂将被丢弃。
进⾏三次握⼿的主要作⽤就是为了确认双⽅的接收能⼒和发送能⼒是否正常、指定⾃⼰的初
始化序列号为后⾯的可靠性传送做准备。
nodejs服务端开发_第12张图片

6. 四次挥手

TCP 的连接的拆除需要发送四个包,因此称为四次挥⼿(Four-way handshake),客户端或服务器均可主动发起挥⼿动作。

为什么要挥⼿四次?当服务端收到客户端的SYN连接请求报⽂后,可以直接发送SYN+ACK报文。其中ACK报⽂是⽤来应答的,SYN报⽂是⽤来同步的。但是关闭连接时,当服务端收到FIN报⽂时,很可能并不会⽴即关闭SOCKET,所以只能先回复⼀个ACK报⽂,告诉客户
端,“你发的FIN报⽂我收到了”。只有等到我服务端所有的报⽂都发送完了,我才能发送FIN报⽂,因此不能⼀起发送。故需要四次挥⼿。

1.1. express
1.2. mysql DML
1.3. 数据建模

2. express

express是基于 Node.js 平台,快速、开放、极简的 Web 开发框架。
2.1. helloworld
 ⼿动创建⼯程

1 $ mkdir myapp
2 $ cd myapp
3 $ npm init
4 $ cnpm install express --save

 核⼼代码

1 const express = require('express')
2 const app = express()
3 const port = 3000
4
5 app.use(express.static('public'))
6 app.get('/', (req, res) => {
7 res.send('Hello World!')
8 })
9 app.listen(port, () => {
10 console.log(`Example app listening at http://localhost:${port}`)
11 })

 路由定义
定义了针对客户端请求的处理函数,不同uri对应不同的处理函数。通过Express的实例对象app上的请求⽅法定义不同的路由,例如:get、post、delete、put、all...

1 var express = require('express');
2 var router = express.Router();
3
4 /* GET users listing. */
5 router.get('/', function(req, res, next) {
6 res.send('respond with a resource');
7 });
8
9 module.exports = router;
1 var usersRouter = require('./routes/users');
2 app.use('/users', usersRouter);

 路由参数

Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }
Route path: /users
Request URL: http://localhost:3000/users?id=1&name=terry
req.query: { "id": "1", "name": "terry" }

 response ⽅法
nodejs服务端开发_第13张图片
 中间件
body-parser
cookie-parser
cors
serve-static

2.2. 脚⼿架
通过应⽤⽣成器⼯具 express-generator 可以快速创建⼀个应⽤的⻣架。

1 $ mkdir cms
2 $ cd cms
3 $ npx express-generator

3. sql-ddl

3.1. 数据库定义

1 mysql> CREATE DATABASE poll;
2 mysql> USE poll
3 Database changed

3.2. 表定义

1 create table <表名>(
2 <列名><数据类型>[列级完整约束条件]
3 [, <列名><数据类型>[列级完整约束条件]]
4 ...
5 [,<表级完整性约束条件>]
6 );
7
8 DROP TABLE IF EXISTS `tbl_exam_paper`;
9 CREATE TABLE `tbl_exam_paper` (
10 `id` bigint(20) NOT NULL AUTO_INCREMENT,
11 `description` varchar(255) DEFAULT NULL,
12 `title` varchar(255) DEFAULT NULL,
13 `department_id` bigint(20) DEFAULT NULL,
14 `user_id` bigint(20) DEFAULT NULL,
15 PRIMARY KEY (`id`),
16 UNIQUE KEY `id` (`id`),
17 KEY `FK92534C8DF2C32590` (`department_id`),
18 CONSTRAINT `FK92534C8DF2C32590` FOREIGN KEY (`department_id`) REFERENCES
19 )

3.3. 表修改

1 #添加列
2 mysql> alter table tableName add columnName dataType
3 #删除列
4 mysql> alter table tableName drop columnName dataType
5 #修改列
6 mysql> alter table tableName modify columnName dataType
7 #删除数据库表
8 mysql> drop table tableName
9 #清空数据库表内容
10 mysql> truncate table s_item

4. sql-dml

⽤于访问和处理数据库的标准的计算机语⾔。
4.1. 查询语句
 简单查询

1 mysql> select [all|distinct] <目标列表达式>
2 -> from <表或视图>[, <表或视图>
3 -> [where <条件表达式>]
4 -> [group by <列名1>
5 -> [having<条件表达式>]]
6 -> [order by <列名2>[asc|desc]];
7 select name
8 from tbl_student
9 where gender = 'male';

 连接查询

1 select s.name,c.name
2 from tbl_student s, tbl_student_course sc ,tbl_course c
3 where s.id = sc.student_id
4 and c.id = sc.course_id;

4.2. 插⼊语句

1 insert into tbl_student(id,name,gender)
2 values (null,'terry','gender')

4.3. 修改语句

1 update tbl_student
2 set name = '王五'
3 where id = 1;

4.4. 删除语句
`1 delete from tbl_user
2 where id = 1;`

  1. 数据库建模
    关系型数据库中常⻅的关系有⼀对⼀、⼀对多、多对多;其中⼀对⼀是⼀对多的⼀种特例;
    多对多可以拆分成两个⼀对多。所以⼀对多⾄关重要。
     ⼀对多
    ⼀对多关系是项⽬开发过程中最常⻅的关联关系。例如,⼀个⽼师可以在任教多⻔课程。在⼀对多关系中,外键通常维护在多的⼀⽅。
    nodejs服务端开发_第14张图片
     ⼀对⼀
    ⼀对⼀关系是⼀对多关系的⼀种特例(为外键添加唯⼀约束)。例如,⼀个⼈只能对应⼀个身份证。在⼀对⼀关系中,外键可以维护在任意⼀⽅
    nodejs服务端开发_第15张图片
     多对多
    多对多关系是也可以理解为是⼀对多关系的⼀种特例(两个⼀对多)。例如,学⽣和课程的关系,⼀个学⽣可以选修多⻔课程,⼀个⻔课程也可以被多个学⽣选修。在多对多关系中,外键维护在桥表中
    nodejs服务端开发_第16张图片
  2. 数据类型
     整数
    nodejs服务端开发_第17张图片
     ⼩数
    image.png
     字符类型
    nodejs服务端开发_第18张图片
     ⽇期类型
    nodejs服务端开发_第19张图片

    7. mysql模块

    mysql模块提供了nodejs操作mysql数据库的api
    https://www.npmjs.com/package...

1.1. mysql模块
1.2. 学⽣选课系统综合演练

2. mysql模块

mysql模块提供了nodejs操作mysql数据库的api
https://www.npmjs.com/package...
2.1. 安装

$ cnpm install mysql --save

2.2. 应⽤
每次进⾏sql操作的时候都创建⼀个连接对象,使⽤完成后将连接对象关闭,关闭后的连接对象⽆法再次使⽤。这不利于我们进⾏代码封装。

1
2 var connection = mysql.createConnection({
3 host : '121.199.29.84',
4 user : 'briup',
5 password : 'briup',
6 database : 'briup-sc'
7 });
8 connection.connect();
9
10 connection.query('SELECT * FROM tbl_student', function (error, results, fiel
11 if (error) throw error;
12 console.log('The solution is: ', results);
13 });
14
15 connection.end();

2.3. 连接池
连接池技术可以创建多个连接放到连接池中,我们可以将连接池的代码进⾏封装,每次需要连接的的时候,通过连接池获取⼀个连接对象即可,使⽤完成后将该连接对象释放。

1 var mysql = require('mysql');
2 var pool = mysql.createPool({
3 connectionLimit : 10,
4 host : '121.199.29.84',
5 user : 'briup',
6 password : 'briup',
7 database : 'briup-sc'
8 });
9 // 获取连接
10 pool.getConnection((err,connection)=>{
11 if(err) throw err;
12 // 查询
13 connection.query(sql, function (error, results, fields) {
14 if (error) throw error;
15 // 释放
16 connection.release();
17 resp.send(Message.success(results));
18 });
19 })
20 })
  1. 综合演练
    截⽌⽬录,我们已经完成了nodejs基础语法、核⼼模块、http服务器编程、数据库编程的学
    习,接下来通过这些技术完成后端服务的开发。
    3.1. 数据建模
    在学⽣选课业务中,⼀个学⽣可以选多⻔课程,⼀⻔课程可以被多个⼈来选,⼀个课程只能由⼀个教师来负责。
    nodejs服务端开发_第20张图片
    3.2. 初始化⼯程

  2. $ mkdir sc-server
  3. $ cd sc-server
  4. $ npx express-generator
  5. $ cnpm install
  6. $ cnpm install mysql --save
  7. $ cnpm install cors --save
  8. $ npm start

    经过上述操作,我们会创建⼀个基于express的sc-server的⼯程,该⼯程中默认包含了
    cookie-parser、debug、express、jade、morgan、body-parser(内置)、serve-static依赖,我们还需要⼿动安装cors和mysql,npm start命令会启动该⼯程,默认占据3000端⼝,需要注意的是,如果后台接⼝代码更新,请务必重启该⼯程。

3.3. Message封装
我们期望,后端提供的所有端⼝都具有统⼀的规范,例如:

1 {
2 status:200,
3 message:"",
4 data:null,
5 timestamp:1630492833755
6 }

这样,⽅便前端统⼀处理。如下是封装代码。

1 class Message {
2 constructor(status,message,data){
3 this.status = status;
4 this.message = message;
5 this.data = data;
6 this.timestamp = new Date().getTime();
7 }
8
9 static success(param){
10 if(typeof param == 'string'){
11 return new Message(200,param,null)
12 } else if (typeof param == 'object'){
13 return new Message(200,'success',param)
14 }
15 }
16
17 static error(message){
18 return new Message(500,message,null);
19 }
20 }
21
22 module.exports = Message;

3.4. 接⼝编写
接⼝开发的时候注意要分模块开发,即每个模块创建⼀个新的router,每个接⼝沿着 获取参数-> 业务逻辑处理 -> 数据库操作 -> 结果返回来进⾏。如下是实例代码:

1 const express = require('express')
2 const Message = require('../utils/Message')
3 const pool = require('../utils/Connection')
4
5 const router = express.Router();
6 // 1. 查询
7 router.get('/findAll',(req,resp)=>{
8 let sql = "select * from tbl_student"
9 // 获取连接
10 pool.getConnection((err,connection)=>{
11 if(err) throw err;
12 // 查询
13 connection.query(sql, function (error, results, fields) {
14 if (error) throw error;
15 // 释放
16 connection.release();
17 resp.send(Message.success(results));
18 });
19 })
20 })
21 // 2. 删除
22 router.delete('/deleteById',(req,resp)=>{
23 let id = req.query.id;
24 let sql = "delete from tbl_student where id = "+ id;
25 pool.getConnection((err,connection)=>{
26 if(err) throw err;
27 connection.query(sql,(error,results)=>{
28 if(error) throw error;
29 connection.release();
30 resp.send(Message.success('删除成功'))
31 })
32 })
33 })
34 // 3. 保存或更新
35 router.post('/saveOrUpdate',(req,resp)=>{
36 let stu = req.body;
37
38 let sql = "insert into tbl_student(id,name,gender,birth) values(null,'"+st
39 if(stu.id){
40 sql = "update tbl_student set name = '"+stu.name+"',gender='"+stu.gender
41 }
42
43 pool.getConnection((err,connection)=>{
44 if(err) throw err;
45 connection.query(sql,(error,results)=>{
46 if(error) throw error;
47 connection.release();
48 resp.send(Message.success('操作成功!'))
49 })
50 })
51 })

3.5. 前端开发
前端开发依旧使⽤vue + axios + elementui来进⾏

1 
2 
3 
4 
5 
6 
7 学生选课
8 
9 
121 
122 

1.1. egg⼊⻔
1.2. egg路由
1.3. egg控制器
1.1. egg服务
1.2. egg中间件

2. egg⼊⻔

2.1. 初始化
2.2. ⽬录结构
2.3. 内置对象
这⾥介绍⼀下框架中内置的⼀些基础对象,包括从 Koa 继承⽽来的 4 个对象
(Application, Context, Request, Response) 以及框架扩展的⼀些对象(Controller, Service,Helper, Config, Logger)
 Application
全局应⽤对象,在⼀个应⽤中,只会实例化⼀个,它继承⾃ Koa.Application,在它上⾯我
们可以挂载⼀些全局的⽅法和对象。

$ mkdir app && cd app
$ npm init egg --type=simple
$ cnpm install
$ npm start
...
$ npm stop

2.2. ⽬录结构
nodejs服务端开发_第21张图片

2.3. 内置对象
这⾥介绍⼀下框架中内置的⼀些基础对象,包括从 Koa 继承⽽来的 4 个对象
(Application, Context, Request, Response) 以及框架扩展的⼀些对象(Controller, Service,Helper, Config, Logger)

 Application
全局应⽤对象,在⼀个应⽤中,只会实例化⼀个,它继承⾃ Koa.Application,在它上⾯我们可以挂载⼀些全局的⽅法和对象。

/*
可以通过 ctx.app 访问到 Application 对象
在继承于 Controller, Service 基类的实例中,可以通过 this.app 访问到 Applicatio
*/
// app/controller/user.js
class UserController extends Controller {
async fetch() {
this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id);
}
}

 Context
Context 是⼀个请求级别的对象,继承⾃ Koa.Context。在每⼀次收到⽤户请求时,框架会实例化⼀个 Context 对象,这个对象封装了这次⽤户请求的信息,并提供了许多便捷的⽅法来获取请求参数或者设置响应信息。框架会将所有的 Service 挂载到 Context 实例上,⼀些插件也会将⼀些其他的⽅法和对象挂载到它上⾯

// app/controller/user.js
class UserController extends Controller {
async fetch() {
this.ctx.body = "";
}
}

 Request
Request 是⼀个请求级别的对象,继承⾃ Koa.Request。封装了 Node.js 原⽣的 HTTPRequest 对象,提供了⼀系列辅助⽅法获取 HTTP 请求常⽤参数

 Response
Response 是⼀个请求级别的对象,继承⾃ Koa.Response。封装了 Node.js 原⽣的
HTTP Response 对象,提供了⼀系列辅助⽅法设置 HTTP 响应。

// 可以在 Context 的实例上获取到当前请求的 Request(ctx.request) 和 Response(ct
x.response) 实例。
1
2 // app/controller/user.js
3 class UserController extends Controller {
4 async fetch() {
5 const { app, ctx } = this;
6 const id = ctx.request.query.id;
7 ctx.response.body = app.cache.get(id);
8 }
9 }

 Controller
框架提供了⼀个 Controller 基类,并推荐所有的 Controller 都继承于该基类实现。这个
Controller 基类有下列属性:

  • ctx - 当前请求的 Context 实例。
  • app - 应⽤的 Application 实例。
  • config - 应⽤的配置。
  • service - 应⽤所有的 service。
  • logger - 为当前 controller 封装的 logger 对象。

 Service
框架提供了⼀个 Service 基类,并推荐所有的 Service 都继承于该基类实现。

 Helper
Helper ⽤来提供⼀些实⽤的 utility 函数。它的作⽤在于我们可以将⼀些常⽤的动作抽离在helper.js ⾥⾯成为⼀个独⽴的函数,这样可以⽤ JavaScript 来写复杂的逻辑,避免逻辑分散各处,同时可以更好的编写测试⽤例。Helper ⾃身是⼀个类,有和 Controller 基类⼀样的属性,它也会在每次请求时进⾏实例化,因此 Helper 上的所有函数也能获取到当前请求相关的上下⽂信息。

// app/controller/user.js
class UserController extends Controller {
async fetch() {
const { app, ctx } = this;
const id = ctx.query.id;
const user = app.cache.get(id);
ctx.body = ctx.helper.formatUser(user);
}
}
// app/extend/helper.js
module.exports = {
formatUser(user) {
return only(user, [ 'name', 'phone' ]);
}
};

 Config
我们推荐应⽤开发遵循配置和代码分离的原则,将⼀些需要硬编码的业务配置都放到配置⽂件中,同时配置⽂件⽀持各个不同的运⾏环境使⽤不同的配置,使⽤起来也⾮常⽅便,所有框架、插件和应⽤级别的配置都可以通过 Config 对象获取到。我们可以通过 app.config 从Application 实例上获取到 config 对象,也可以在 Controller, Service, Helper 的实例上通过
this.config 获取到 config 对象。
 Logger
框架内置了功能强⼤的⽇志功能,可以⾮常⽅便的打印各种级别的⽇志到对应的⽇志⽂件
中,每⼀个 logger 对象都提供了 4 个级别的⽅法:

  • logger.debug()
  • logger.info()
  • logger.warn()
  • logger.error()
  1. egg路由
    Router 主要⽤来描述请求 URL 和具体承担执⾏动作的 Controller 的对应关系, 框架约定了 app/router.js ⽂件⽤于统⼀所有路由规则。通过统⼀的配置,我们可以避免路由规则逻辑散落在多个地⽅,从⽽出现未知的冲突,集中在⼀起我们可以更⽅便的来查看全局的路由规则
'use strict';
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/student/findById', controller.student.findById);
router.post('/student/saveOrUpdate', controller.student.saveOrUpdate);
router.delete('/student/deleteById', controller.student.deleteById);
router.post('/student/pageQuery', controller.student.pageQuery);
};
  1. egg控制器
    Controller 负责解析⽤户的输⼊,处理后返回相应的结果。框架推荐 Controller 层主要对
    ⽤户的请求参数进⾏处理(校验、转换),然后调⽤对应的 service ⽅法处理业务,得到业务结
    果后封装并返回。

4.1. 获取请求参数
 query、queries

1 const query = this.ctx.query;
2 const queries = this.ctx.queries

 routerParams
1 const params = this.ctx.params;

 body
1 const data = this.ctx.request.body;
 header
ctx.headers,ctx.header,ctx.request.headers,ctx.request.header:这⼏个⽅法是等价的,都是获取整个 header 对象。ctx.get(name) ctx.request.get(name):获取请求 header 中的⼀个字段的值,如果这个字段不存在,会返回空字符串。我们建议⽤ ctx.get(name) ⽽不是ctx.headers['name'],因为前者会⾃动处理⼤⼩写。

1 ctx.host
2 ctx.protocol
3 ctx.ips
4 ctx.ip

 cookie

1 let count = ctx.cookies.get('count');
2 count = count ? Number(count) : 0;
3 ctx.cookies.set('count', ++count);

 session
通过 Cookie,我们可以给每⼀个⽤户设置⼀个 Session,⽤来存储⽤户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的⽤户身份保持。

1 const ctx = this.ctx;
2 // 获取 Session 上的内容
3 const userId = ctx.session.userId;
4 const posts = await ctx.service.post.fetch(userId);

4.2. 调⽤Service
我们并不想在 Controller 中实现太多业务逻辑,所以提供了⼀个 Service 层进⾏业务逻辑的封装,这不仅能提⾼代码的复⽤性,同时可以让我们的业务逻辑更好测试。在 Controller 中可以调⽤任何⼀个 Service 上的任何⽅法,同时 Service 是懒加载的,只有当访问到它的时候框架才会去实例化它。

1 const res = await ctx.service.post.create(req);

4.3. 发送HTTP响应
 status
1 this.ctx.status = 201;
 body
绝⼤多数的数据都是通过 body 发送给请求⽅的,和请求中的 body ⼀样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进⾏解析。

 作为⼀个 RESTful 的 API 接⼝ controller,我们通常会返回 Content-Type 为
application/json 格式的 body,内容是⼀个 JSON 字符串。

 作为⼀个 html ⻚⾯的 controller,我们通常会返回 Content-Type 为 text/html 格式的 body,内容是 html 代码段。

1 this.ctx.body = {
2 name: 'egg',
3 category: 'framework',
4 language: 'Node.js',
5 };
6
7 this.ctx.body = '

Hello

';

 重定向
1 ctx.redirect(url)

  1. egg服务
    简单来说,Service 就是在复杂业务场景下⽤于做业务逻辑封装的⼀个抽象层,提供这个抽象有以下⼏个好处:
     保持 Controller 中的逻辑更加简洁。
     保持业务逻辑的独⽴性,抽象出来的 Service 可以被多个 Controller 重复调⽤。
     将逻辑和展现分离,更容易编写测试⽤例,
1 const {Service} = require('egg')
2
3 class StudentService extends Service {
4 async findById(id){
5 let sql = "select * from tbl_student where id = ?"
6 let student =await this.app.mysql.query(sql,[id]);
7 return student;
8 }
9
10 // student {id,name,gender,birth}
11 async saveOrUpdate(student){
12 let result = null;
13 if(student.id){
14 result = await this.app.mysql.update('tbl_student',student)
15 } else {
16 result = await this.app.mysql.insert('tbl_student',student)
17 }
18 return result;
19 }
20
21 async deleteById(id){
22 let result = await this.app.mysql.delete('tbl_student',{id})
23 }
24 }
25 module.exports = StudentService;

6. egg-mysql

6.1. 安装
1 $ cnpm install --save egg-mysql

6.2. 配置

1 // config/plugin.js
2 'use strict';
3
4 /** @type Egg.EggPlugin */
5 module.exports = {
6 mysql :{
7 enable: true,
8 package: 'egg-mysql',
9 }
10 };
11
12 // config.default.js
13 //...
14 const userConfig = {
15 // myAppName: 'egg',
16 mysql : {
17 // 单数据库信息配置
18 client: {
19 // host
20 host : '121.199.29.84',
21 user : 'briup',
22 password : 'briup',
23 database : 'briup-sc',
24 port: '3306',
25 },
26 // 是否加载到 app 上,默认开启
27 app: true,
28 // 是否加载到 agent 上,默认关闭
29 agent: false,
30 }
31 };
32 //...

6.3. 基本使⽤

1 // service/student.js
2 const {Service} = require('egg')
3 class StudentService extends Service {
4 async findById(id){
5 let student = await this.app.mysql.query('select * from tbl_student wh
ere id = ?',[id])
6 //let student = await this.app.mysql.get('tbl_student',{id})
7 return student;
8 }
9 }
10
11 module.exports = StudentService;

6.4. 快捷⽅法

 insert(table,{})
table为表名,{} 为插⼊的字段与值的映射对象

1 mysql.insert('tbl_student',{name:"terry",gender:"男"})
2
3 // insert into tbl_student (name,gender) values("terry","男")

 get(table,{})
table为表名,{}为where条件

1 mysql.get('tbl_student',{id:1})
2
3 // select * from tbl_student where id = 1;

 select(table,{})
table为表名,{} 为对象,对象中包含了wher、columns、orders、limit、offset等属性

1 mysql.select('tbl_student',{
2 where:{gender:"男"},
3 columns: ['name', 'gender'], // 要查询的表字段
4 orders: [['id','desc'], ['name','desc']], // 排序方式
5 limit: 10, // 返回数据量
6 offset: 0,
7 })
8
9 /*
10 select name,gender
11 from tbl_student
12 where gender="男"
13 order by id desc, name desc
14 limit 0,10;
15 */

 update(table,{}[,options])
table为表名,{}为更新后的值,默认根据id来更新

1 mysql.update(tbl_student,{id:1,name:"terry",gender:'女'})
2 mysql.update(tbl_student,{id:1,name:"terry",gender:'女'},{
3 where:{id:1}
4 })
5 /*
6 update tbl_student
7 set name = 'terry',gender = '女'
8 where id = 1;
9 */

 delete(table,{})
table为表名,{}为删除条件

1 mysql.delete(tbl_student, {id:1})
2
3 //delete from tbl_student where id = 1;

1.1. 中间件
1.2. jwt

2. 中间件

2.1. 中间件
Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是⼀样的,都是基
于洋葱圈模型。每次我们编写⼀个中间件,就相当于在洋葱外⾯包了⼀层。

1 module.exports = options => {
2 return async function gzip(ctx, next) {
3 await next();
4 //----
5 }
6 }

2.2. 统⼀异常处理
通过同步⽅式编写异步代码带来的另外⼀个⾮常⼤的好处就是异常处理⾮常⾃然,使⽤
try catch 就可以将按照规范编写的代码中的所有错误都捕获到。这样我们可以很便捷的编
写⼀个⾃定义的错误处理中间件。

1 // error_handler.js
2
3 let Message = require('../utils/Message');
4 module.exports = () => {
5 return async function errorHandler(ctx, next) {
6 try {
7 await next();
8 } catch (err) {
9 // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
10 ctx.app.emit('error', err, ctx);
11
12 const status = err.status || 500;
13 // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
14 const error = status === 500 ? 'Internal Server Error' : err.message;
15
16 // 从 error 对象上读出各个属性,设置到响应中
17 ctx.body = Message.error(error);
18 if (status === 422) {
19 ctx.body.data = err.errors;
20 }
21 ctx.status = status;
22 }
23 };
24 };
25
26 // config.default.js
27 config.middleware = ['errorHandler'];
  1. jwt
    https://www.jianshu.com/p/576...
    使⽤jwt进⾏⽤户身份认证⾮常⽅便。其语法格式如下:
    jwt.sign(payload, secretOrPrivateKey, [options, callback])
    jwt.verify(token, secretOrPublicKey, [options, callback])

 payload
为⼀个对象,后期可以根据token解析出这个对象的信息
 secretOrPrivateKey
秘钥
 options
algorithms : HS256
exp : Math.floor(Date.now() / 1000) + (60 * 60),

1 $ cnpm install egg-jwt --save
2
3 // plugins.js
4 jwt : {
5 enable: true,
6 package: 'egg-jwt',
7 },
8 // config.default.js
9 jwt:{
10 secret:"888888"
11 },

3.1 controller中处理登录

1 const {Controller} = require('egg');
2 const Message = require('../utils/Message')
3 class UserController extends Controller{
4
5 // 验证token,请求时在header配置 Authorization=`Bearer ${token}`
6 async login() {
7 const { ctx, app } = this;
8 const data = ctx.request.body;
9 // 验证用户信息代码....
10 const token = app.jwt.sign({ username: data.username, }, app.config.jwt.
11 expiresIn: '60s',
12 });
13
14 ctx.body = Message.success({token}) ;
15 }
16 }
17
18 module.exports = UserController;

参考
http://javascript.ruanyifeng....
https://nodejs.org/dist/lates...

你可能感兴趣的:(前端node.js)