Node基础概念
什么是node
node
提供一个可以让js
跑在服务端的环境
node
底层用c++
写。性能OK
node
基于js
核心(ecm
) 系统及api
文件操作 网络编程 来实现web
服务
解决了什么问题
web
服务器瓶颈 并发
java
php
每个客户链接服务器。产生一个新线程,
node
不会创建新线程。发射事件 (发布订阅模式)
node
主要解决 前后端分离 写一些工具库 cli等
服务器与服务器之间发送清求。不用跨域
跨域是浏览器的
比较适合 文件读写 效率大概是java
的200倍
node 核心特点
事件启动 node的api
是基于事件的 异步的
Node 采用的是单线程 node
可以开启个进程
不适合 Cpu
密集型 适合i/o密集型
同步异步 阻塞非阻塞
同步和异步 关注的是 消息通知的机制
阻塞和非阻塞 程序等待消息结果的状态
全局属性
能直接访问的属性被称之为全局属性
有关this
node为了实现模块化。在每个文件上都包裹了一个函数。所以这个函数的this
被改变
console.log(this) // {}
// 在命令行中 node 直接打印this 打印出global this === global true
有关文件
console.log(__filename) // 代码文件所在的绝对目录
console.log(__dirname) // 当前文件的运行文件夹
console.log(process.cwd()) // 代表当前工作目录 可以更改
导入导出
exports
require
module
模块化常用属性
解决了哪些问题
-
可以帮助我们解决 命名冲突 问题
-
node的规范,
commonjs
规范。 -
- 每个
JS
都是一个模块 module.exports
模块的导出- 模块的导入
require
- 每个
-
esModule
es6模块规范import export
-
还有
cmd
seajsamd
requirejsumd
统一模块规范 -
node
默认不支持es模块
-
commonjs
动态引入.import
静态引入 -
if(true){ // 动态引入 require(./1.js) }
模块分类
- 核心/内置模块 不需要引入 直接用
- 第三方模块
reuqirs('vue')
需要安装 - 自定义模块 通过路径进行引入
node
采用 同步的方式 读取文件
异步代码 需要使用 回调的方式解决
通过独去文件内容,把内容包裹在一个自执行函数中。默认返回module.exports
function (exports,require,module,__filename,__dirname){
return module.exports
}(exports,require,module,xxx,xxx)
node调试
- 浏览器调试 调试包
vscode
进行代码调试 自己写的文件等- 命令行
核心模块
fs
读取文件没有指定 都是buffer
格式
默认写入都会转成utf-8
格式
读取文件必须采用绝对路径
readFileSync
同步独去 读文件existSync
同步 判断是否存在fs.existSync
是否存在路径fs.accessSync
同步地测试用户对path
指定的文件或目录的权限
文件拷贝
let path = require('path')
let fs = require('fs')
// w 写 有则清空 跟C语言 一样
fs.readFile(path.resolve(__dirname, 'a.txt'), function(err, data) {
if (err) {
console.log(err)
} else {
fs.writeFile('b.txt', data, { flag: 'w' }, function(err) {
if (err) {
console.log(err)
} else {
console.log('成功')
}
})
}
})
不适合大文件读取。
小于64k
用这个 大于 64k
用流
同时也包含 操作文件夹的
fs.mkdirSync
创建文件夹
创建多个文件夹
// 创建多个文件夹的实现
let fs = require('fs')
// fs.mkdirSync('a/b/c') 不能直接这样 因为 没有父不能创建子
// 同步的实现
function mkdirSync(path) {
let arr = path.split('/')
for (let i = 0; i < arr.length; i++) {
let p = arr.slice(0, i + 1).join('/')
try {
fs.accessSync(p)
} catch (e) {
console.log(e)
fs.mkdirSync(p)
}
}
}
mkdirSync('a/b/c')
// 异步逻辑基本相似
// 异步
function mkdir(p,cb){
let arr = p.split('/')
let index = 0
function next() {
if (index === arr.length) return cb()
let p = arr.slice(0,index+1).join('/')
fs.access(p,(err)=>{
index ++
if (err){
fs.mkdir(p,next)
}else {
next()
}
})
}
next()
}
mkdir('a/b/c/d/e/f',function(){
console.log('创建成功')
})
删除多个文件夹
let fs = require('fs')
let path = require('path')
// 同步 测试 只能删除二级目录
// function remDir(p){
// let dirs = fs.readdirSync(p)
// dirs = dirs.map(dir=>path.join(p,dir))
// // console.log(dirs)
// dirs.forEach(dir=>{
// let stateObj = fs.statSync(dir); // 返回文件状态
// if (stateObj.isDirectory()){ // 是文件夹的话
// fs.rmdirSync(dir) // 删除文件夹
// }else { // 是文件
// fs.unlinkSync(dir) // 删除文件
// }
// })
// fs.rmdirSync(p)
// }
// 同步 深度遍历
// function remDir(p) {
// let stateObj
// console.log(p)
// try {
// stateObj = fs.statSync(p) // 返回文件状态
// } catch (e) {
// console.log(e)
// }
// if (stateObj.isDirectory()) {
// let dirs = fs.readdirSync(p)
// dirs.forEach(dir=>{
// let current = path.join(p,dir)
// remDir(current)
// })
// fs.rmdirSync(p)
// } else {
// fs.unlinkSync(p)
// }
// }
// 同步 广度
function remDir(p){
let arr = [p]
let index = 0
let current
while (current = arr[index++]){
let stateObj = fs.statSync(current)
if (stateObj.isDirectory()){
let dirs = fs.readdirSync(current) // 返回数组 所有文件名称
dirs = dirs.map(dir=> path.join(current,dir))
arr = [...arr,...dirs]
}
}
for (let i = arr.length-1; i >=0 ; i--) {
let current = arr[i]
let stateObj = fs.statSync(current)
if (stateObj.isDirectory()){
fs.rmdirSync(current)
}else {
fs.unlinkSync(current)
}
}
}
remDir('a')
边读边写
熟悉api
利用fs.open
fs.read
fs.write
fs.close
(参数太多)
// 实现一个拷贝功能
// 利用 fs.readFile fs.writeFile
// 容易淹没用户内存。 原因 先读取 再写入 文件太大 占用内存
// new api fs.copy 也不能用大文件
// 边读边写入
// 利用 fs.open fs.read fs.write fs.close (参数太多)
// 读取文件 把文件读取到内存 写入 内存中文件读取出来
let fs = require('fs')
let path = require('path')
// 申请内存大小
// fd 文件描述符 代表拥有了读取这个文件的权限 number 类型
// 且 fd 是唯一的 有最大数量限制。 开一个需要关一个
// let buf = Buffer.alloc(3)
// fs.open(path.resolve(__dirname,'b.txt'),'r',(err,fd)=>{
// // 1.文件描述符
// // 2.读取到哪个内存
// // 3.从内存的哪个位置开始写入
// // 4.写入多少个
// // 5.从文件的什么位置开始读
// // 6.回调函数
// fs.read(fd,buf,0,3,0,(err, bytesRead, buffer)=>{
// console.log(bytesRead) // 代表真实的去读个数
// // 读取到的内存 可以直接用buf
// console.log(buffer.toString())
// console.log(buf.toString())
// })
// fs.close(fd,()=>{
// console.log('关闭成功')
// })
// })
// 写入
let buf = Buffer.from('叫我阿琛')
fs.open(path.resolve(__dirname, 'c.txt'), 'w', (err, fd)=>{
fs.write(fd, buf, 6, 6, 0, (err1, written)=>{
console.log(buf.toString())
console.log(written)
})
fs.close(fd, ()=>{
console.log('关闭成功')
})
})
边读边写
// 一边读取 一边写入
let Buffer_size = 3 // 3*1024 3K
let buf = Buffer.alloc(Buffer_size)
let readOffset = 0
let fs = require('fs')
let path = require('path')
fs.open(path.resolve(__dirname, 'b.txt'), 'r', (err, rfd)=>{
fs.open(path.resolve(__dirname, 'c.txt'), 'w', (err, wfd)=>{
function next() {
fs.read(rfd, buf, 0, Buffer_size, readOffset, (err1, bytesRead)=>{
if (!bytesRead) {
fs.close(rfd, ()=>{})
fs.close(wfd, ()=>{
console.log('结束')})
return
}
readOffset += bytesRead
// position 可以 不写 默认递增
fs.write(wfd, buf, 0, bytesRead, (err,written)=>{
next()
})
})
}
next()
})
})
这样写代码太乱。异步嵌套太多。
可以使用 发布订阅模式,把 读取 和 写入 分开
所以引入了 流的概念
文件流
createReadStream使用
// readSteam
let fs= require('fs')
let rs = fs.createReadStream('./b.txt',{
flags:'r', // fs.open
encoding:null,// 编码格式 默认buffer
mode:0o666, //fs.open
autoClose:true, // 自动关闭
start:0, // 从哪里开始读
end:5, // 读到哪里 0-4
highWaterMark:2 // 每次读几个 默认 64*1024
})
let arr = []
rs.on('open',(fd)=>{
console.log('文件打开了' + fd)
})
rs.on('data',(data)=>{
// console.log(data.toString())
// 读取汉字会出错
arr.push(data)
})
rs.on('end',()=>{
console.log(Buffer.concat(arr).toString())
console.log('end')
})
rs.on('close',()=>{
console.log('close')
})
createReadStream实现
let fs = require('fs')
let events = require('events')
class createReadStream extends events {
constructor(path, options = {}) {
super();
this.path = path
this.flags = options.flags || 'r'
this.mode = options.mode || 438
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.end = options.end
this.highWaterMark = options.highWaterMark || 64 * 1024
// 读取文件要用
this.pos = this.start
this.open()
this.flowing = true
this.on('newListener', (type)=>{
if (type === 'data') this.read()
})
}
pause(){
this.flowing = false
}
resume(){
this.flowing = true
this.read()
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd)=>{
if (err) return this.emit('error', err)
this.fd = fd
this.emit('open', fd)
})
}
close() {
fs.close(this.fd, ()=>{
this.emit('close')
})
}
read() {
// 因为fs.open 是异步打开 不一定有fd所以 发布订阅解决了异步问题
if (typeof this.fd !== 'number') {
return this.once('open', this.read)
}
let buf = Buffer.alloc(this.highWaterMark)
// 计算一下停止位置
let end = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark
fs.read(this.fd, buf, 0, end, this.pos, (err, bytesRead)=>{
if (err) return this.emit('error', err)
if (bytesRead) {
this.pos += bytesRead
this.emit('data', buf)
if (this.flowing)
this.read()
} else {
this.emit('end')
this.close()
}
})
}
}
module.exports = createReadStream
createWriteStream使用
let fs = require('fs')
let path = require('path')
// let ws = fs.createWriteStream(path.resolve(__dirname, 'd.txt'), {
// flags: 'w',
// encoding: 'utf8',// 编码格式 默认buffer
// mode: 438,
// autoClose: true,
// start: 0,
// highWaterMark: 10 // 代表一个预估量 默认16k
// })
// // 有返回值。如果超过 预期 即highWaterMark 返回false 这个预期是总和 不是单独
//
// // 异步 内部排队 依次执行
// let flag = ws.write('我爱你', ()=>{
// console.log('写入成功')
// })
//
// console.log(flag)
// flag = ws.write('我也是', ()=>{
// console.log('写入成功')
// })
// console.log(flag)
//
// // 如果关闭 不能写入
// ws.end('我也可以写内容,代表写入结束') // = write + close方法
// ws.close()
// drain 事件
ws = fs.createWriteStream(path.resolve(__dirname, 'text.txt'), {
highWaterMark: 1 // 代表一个预估量 默认16k
})
let index = 0
let flag = true
function write(){
flag = true
while (flag && index < 10){
flag = ws.write(index++ + '')
}
}
write()
// 当写入内容达到预估标准之后。并清空数据之后。 触发次方法
ws.on('drain',()=>{
console.log('drain')
write()
})
// 这样写比 直接 ws.write 好在哪里 不需要占用内存消耗
配置和上面的createReadStream
差不多。有个别特例。主要方法
ws.write(contain,cb)
向文件中写入内容,结束执行回调ws.end(contain,cb)
相当于write 和 close
方法的结合体
如何区分两者
createReadStream
方法 有on('data')
on('close')
createWriteStream
方法有write end
方法。on('drain')
方法
拷贝pipe
原理 模拟实现
let r= fs.createReadStream('./b.txt',{
highWaterMark:4
})
let w = fs.createWriteStream('./d.txt',{
highWaterMark:1
})
r.on('data',(data)=>{
let flag = w.write(data)
if (!flag) r.pause()
})
w.on('drain',()=>{
r.resume()
})
使用
// pipe 模拟 64k / 16k
let r= fs.createReadStream('./b.txt',{
highWaterMark:4
})
let w = fs.createWriteStream('./d.txt',{
highWaterMark:1
})
r.pipe(w)
缺点
- 你看不到读写过程
- 如果需要获取结果,需要等会
pipe
完成。 或者使用readFile
Readable, Writable,Duplex
用来实现自定义 可读可写流
class Myread extends Readable {
_read() {// 子类实现这个方法
this.push('123') // buffer 或者 字符串类型
this.push(null) // 不加这个不能停止
}
}
class myWrite extends Writable {
_write(chunk, encoding, callback) {// 子类实现这个方法
console.log(chunk.toString())
callback() // 完成后必须调用cb
}
}
let a = new Myread()
a.on('data', function(data) {
console.log(data)
})
let b = new myWrite()
b.write('ok', ()=>{
console.log('完成')
})
b.write('ok2', ()=>{
console.log('完成2')
})
// 双工流 需要实现这两个方法
class myDuplex extends Duplex {
_write(chunk, encoding, callback) {
}
_read(size) {
}
}
let duplex = new myDuplex()
Transform
转换流 例如压缩
class myTransform extends Transform {
_transform(chunk, encoding, callback) { // 子类实现这个方法
}
}
path
path.join
拼接路径 可以拼接/
path.resolve
解析路径 这个里面 加入这个/
会回到 根路径path.extname
读取扩展名path.basename
获取基础名字 a.js a
vm 模块
如何让一个字符串执行
-
eval
-
let number = 10000 let a = 'console.log('a')' let b = 'console.log(b)' eval(a) // a eval(b) // 10000 作用域不干净
-
new Function
-
let fn = new function('a','b',`console.log(a)`) // 有独立干净的作用域 console.log(fn(1,2)) // 1 undefined
npm
npm link
把当前目录放到全局下 方便调试
全局的模块
必须使用pack-json
里面的bin
选项
指明编译 #! /usr/bin/env node
本地安装
npm i xx -S-D
仅在开发下使用
npm i xx -S
开发 上线都用
buffer
0x
十六进制
0b
2进制
0
开头 8进制
base64
解密
arrayBuffer
前端二进制
buffer.from
把一段内容写入到内存中
buffer.alloc
申请一段内存
原型继承
events
[中文文档] (nodejs.cn/api/events.…)
简单来讲。就是帮我们实现了一个 发布订阅模式
let EventEmitter = require('events')
let util = require('util')
// util.inherits() // 继承
// 1
function Girl() {
}
// 四种原型继承的方式
// Girl.prototype.__proto__ = EventEmitter.prototype
// Girl.prototype = Object.create(EventEmitter.prototype)
// Object.setPrototypeOf(Girl.prototype,EventEmitter.prototype)
util.inherits(Girl,EventEmitter)
let girl = new Girl()
girl.on('失恋',(name)=>{
console.log(name + ' 失恋了')
})
girl.emit('失恋','ni')
http
状态码和含义
1XX :信息状态码
一般是websocket 升级
2XX :成功状态码
200 OK 正常返回信息 201 Created 请求成功并且服务器创建了新的资源 202 Accepted 服务器已接受请求,但尚未处理
206 范围清求
3XX :重定向
301 Moved Permanently 请求的⽹⻚已永久移动到新位置。 302 Found 临时性重定向。 303 See Other 临时性重定向,且总是使⽤ GET 请求新的 URI 。 304 Not Modified ⾃从上次请求后,请求的⽹⻚未修改过。 前端无法更改
4XX :客户端错误
400 Bad Request 服务器⽆法理解请求的格式,客户端不应当尝试再次使⽤相同的内 容发起请求。 401 Unauthorized 请求未授权。 403 Forbidden 禁⽌访问。 404 Not Found 找不到如何与 URI 相匹配的资源。
405 方法不允许
5XX: 服务器错误
500 Internal Server Error 最常⻅的服务器端错误。 503 Service Unavailable 服务器端暂时⽆法处理请求(可能是过载或维护)
url uri
url
统一资源定位符,表示资源位置
uri
统一资源标识符。标识资源
模块
let http = require('http')
let url = require('url')
// 清求 请求行 请求头 请求体
// 响应 响应行 响应头 响应体
let server = http.createServer((res, req)=>{
// 常用请求行相关信息
let {pathname,query} = url.parse(res.url) // 默认清求路径/ 到#后面
console.log(pathname,query)
console.log(res.method.toLocaleLowerCase())
console.log(res.httpVersion) // 获取http 版本
// 请求头 获取的全是小写
console.log(res.headers)
// 收到请求体
// tcp 分段传输。先收集起来最后一起 res.on('data') 获取数据 res('end') 清求结束
let arr= []
// 可读流内部读取不到数据 会push(null)
res.on('data',(data)=>{ // data 只有有数据的时候才会触发
arr.push(data)
})
res.on('end',()=>{ // end一定会触发
console.log('数据')
console.log(Buffer.concat(arr).toString())
})
// 响应端 相应行
req.statusCode = 200 // 100-500
//content-type 返回服务端内容 需要加类型
// 文本格式 需要设置文本格式 否则容易乱码
req.setHeader('Content-Type','text/plain;charset=utf-8')
req.end('hello ') // 跟写的可写流差不多
})
// 第二种方式 推荐第一种
// server.on('request',function(res) {
// console.log(456)
// })
let port = 3000
// 端口被占用
server.on('error', (err)=>{
if (err.code === 'EADDRINUSE') {
server.listen(++port) // 不用再写回调 发布订阅模式 会调用之前那的回调
}
})
server.listen(port, ()=>{
console.log('监听成功')
})
利用createServer
API 可以创建一个服务器。res
清求,req
响应
上述代码打印了常用的 res
和req
属性
有关中间层
解决了 跨域 问题( 跨域存在于浏览器端) 实例代码如下
也可以在这里面对数据进行格式化
// 中间层 可以解决跨域 原因 跨域只存在浏览器
const http = require('http')
let options = {
port:3000,
hostname:'localhost',
path:'/?a=1',
method:'POST'
}
// get请求用这种
// http.get(options,(res)=>{
// })
// 创建一个服务器去清求别人的 模拟ajax
let server = http.createServer((req, res)=>{
let clint = http.request(options,(response)=>{ // 是一个客户端
let arr= []
response.on('data',(data)=>{
arr.push(data)
})
response.on('end',()=>{
console.log(Buffer.concat(arr).toString())
res.end(Buffer.concat(arr).toString() + 'world456789')
})
})
})
server.listen(3001)
可能对这段代码有点难以理解的地方。
- 我们先创建了一个服务器
- 利用
http.request
清求我们所需要的url
把内容取回来 - 获取完成之后。通过
res
返回给我们
head
资源防盗
referer refererr 和host 对比
多语言
accept-language
正向和反向代理
代理服务器是帮助客户端的 就是正向代理
帮助服务器的就是反向代理
http-proxy
koa框架
基本使用
const koa = require('koa')
const app = new koa()
app.use((ctx)=>{
console.log(ctx)
ctx.body = 'hello'
})
app.listen(3000)
源文件目录结构
开始模仿
package.json
配置
{
// 作用 引入的时候主文件入口
"main": "./applaction.js"
}
第一步 搭建基本架子
// application.js
const http = require('http')
module.exports = class Application {
constructor() {
}
use(cb) {
this.cb = cb
}
handleRequest(req,res){
this.cb(req,res)
}
listen(...args) {
let server = http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
}
- 我们需要实现里面的
use
listen
方法,在使用的过程中,我们是new Koa
。所以导出的是一个类 - 调用
listen
方法的时候,我们需要开启一个服务。同时,listen
方法可能有多个参数,所以我们选择使用 解构 - 为什么要有
handleRequest
存在,因为你会发现我们使用koa
的过程中。是ctx
上下文,所以我们需要在这里面来一层封装。同时调用use
方法,把所需要的参数传入。(这里使用了一种简写的方法)
到此,基础搭建已经完成。下一步 开始封装ctx
封装ctx
首先 做一个区别
ctx.req
是原生的ctx.request
是koa
给我们封装好的- 见官网
const http = require('http')
const request = require('./request')
const response = require('./response')
const context = require('./context')
module.exports = class Application {
constructor() {
this.request = Object.create(request)
this.response = Object.create(response)
this.context = Object.create(context)
}
// use 代码没有改变 就直接省略
createCtx(req, res) {
// 为了防止使用同一个对象。 保证每次清求数据独立
let request = Object.create(this.request)
let response = Object.create(this.response)
let context = Object.create(this.context)
// 处理
context.request = request
context.req = request.req = req
context.response = response
context.req = response.res = res
return context
}
handleRequest(req, res) {
// 3. 清求到来的时候需要执行use 方法
let ctx = this.createCtx(req, res)
this.cb(ctx)
// 4 执行后把结果返回
res.end(ctx.body)
}
// listener
}
- 为了 保证数据的独立性。我们使用了
Object.create
进行创建,以防止每次都在同一个对象上添加属性 createCtx
方法。是把原生req res
和自己封装的request reponse
方法进行整合- 在执行完回调之后。调用
end
方法,并把结果返回 结束这一次清求
request等文件代码
// request
const url = require('url')
module.exports = {
get url() {
// 这里谁调用this 指向谁
return this.req.url
},
get path() {
// 这里谁调用this 指向谁
return url.parse(this.req.url).pathname
},
get query() {
// 这里谁调用this 指向谁
return url.parse(this.req.url).query
}
}
// context
module.exports = {
get url() {
// 代理机制
return this.request.url
},
get path() {
return this.request.path
},
get query() {
return this.request.query
},
get body() {
return this.response._body
},
set body(val) {
this.response._body = val
}
}
// 嫌弃麻烦的可以使用 一个函数封装 使用 defineGetter
// response
module.exports = {
get body() {
return this._body
},
set body(val) {
this._body = val
}
}
request
中this
。因为我们在createCtx
里面进行了调用。谁调用就是this
就是谁context
里面代码就是做了一层代理。可以选择直接用函数封装代替。
koa-compose
作用:个人理解
- 把各个中间件进行组合
- 统一为
promise处理
核心就是把n个函数组合到一起
首先先看一张图, 洋葱圈模型
解释。
app.use((ctx,next)=>{
// 处理
ctx.body = '123456'
})
app.use((ctx)=>{
// 处理
ctx.body = '123456'
})
清求到来得时候,先要经过我们use
里面得函数。也就称为 中间件。
中间件会一层一层得把我们得清求和响应进行处理。next
代表 下一个中间件。
开始改造我们得代码
// ... require导入之类
const Event = require('events')
module.exports = class Application extends Event{
constructor() {
super()
this.request = Object.create(request)
this.response = Object.create(response)
this.context = Object.create(context)
this.middlewares = []
}
use(cb) {
this.middlewares.push(cb)
}
compose(ctx) {
let index = -1
const dispatch = (i)=>{
let middleware = this.middlewares[i]
if (index === i)
return Promise.reject(new Error('next() is called multiple times'))
index = i
if (i === this.middlewares.length)
return Promise.resolve()
// 返回得这个函数即 next
// 防止出错
try {
return Promise.resolve(middleware(ctx, ()=>dispatch(i + 1)))
}catch (e){
return Promise.reject(e)
}
}
return dispatch(0)
}
handleRequest(req, res) {
let ctx = this.createCtx(req, res)
this.compose(ctx).then(()=>{
res.end(ctx.body)
}).catch((err)=>{
this.emit('err',err)
})
}
// listen
}
- 创建一个
middlewares
进行存储use
传入进来得callback
- 开始执行每一个
use
里面得cb
。并用promise
进行包裹。 index
作用 提示next
函数被多次调用return Promise.resolve(middleware(ctx, ()=>dispatch(i + 1)))
即 执行中间件,再把下一个中间件传过去- 处理错误时候,继承
events
事件。利用发布订阅模式
。把错误抛出去 - 防止在执行过程中出错。要加
try catch
- 所以
use
里面尽量要使用async
根据类型处理body
const stream = require('stream')
// 。。。 省略
// applaction
handleRequest(req, res) {
let ctx = this.createCtx(req, res)
res.statusCode = 404
this.compose(ctx).then(()=>{
let body = ctx.body
if (typeof body === 'string' || Buffer.isBuffer(body)) {
res.end(body)
} else if (body instanceof stream) {
// 设置下载头
res.statusCode = 200
// 下载文件
res.setHeader('Content-Disposition', `attachment;filename=${ ctx.path.slice(1)?ctx.path.slice(1):'downLoad' }.js`)
body.pipe(res)
} else if (typeof body === 'object') {
res.end(JSON.stringify(body))
} else if (typeof body === 'number') {
res.end(body + '')
} else {
res.end('Not Found')
}
}).catch((err)=>{
this.emit('err', err)
})
}
// response
module.exports = {
_body:undefined,
get body() {
return this._body
},
set body(val) {
this.res.statusCode = 200
this._body = val
}
}
思路
- 我们首先设置默认为
404
- 如果设置了
body
属性。把satauscode
设置为200返回 - 应为
res.end()
只接受buffer
和String
类型。 所以我们要对类型进行分布处理
koa-bodyparser
const koa = require('koa')
const bodyparser = require('koa-bodyparser')
const app = new koa()
app.use(bodyparser())
app.use((ctx, next)=>{
if (ctx.method === 'GET' && ctx.path === '/form') {
ctx.body = '<form action="/form" method="post">\n' +
' 用户名<input type="text" name="username">\n' +
' <br>\n' +
' 密码<input type="password" name="username">\n' +
' <button>提交</button></form>'
} else {
next()
}
})
app.use(async (ctx, next)=>{
if (ctx.method === 'POST' && ctx.path === '/form') {
ctx.body = await ctx.request.body
} else {
next()
}
})
app.listen(3000)
简单模仿
const bodyparser = ()=>{
return async (ctx,next)=>{
await new Promise((resolve, reject)=>{
let arr = []
ctx.req.on('data',(data)=>{
arr.push(data)
})
ctx.req.on('end',()=>{
ctx.request.body = Buffer.concat(arr).toString()
resolve()
})
})
await next()
}
}
module.exports = bodyparser
// 导入自己得就可以
方法
- 首先需要返回一个函数
- 把数据收集起来。再赋值给
ctx.request.body
- 记得调用next()方法
koa-static
使用
app.use(KoaStatic(__dirname))
app.use(KoaStatic(path.resolve(__dirname,'node_modules')))
// 可以直接访问静态资源目录
简易模仿
let fs = require('fs').promises
let path = require('path')
const koaStatic = (filePath)=>{
return async (ctx, next)=>{
let pathUrl = path.join(filePath, ctx.path)
let stats = await fs.stat(pathUrl)
if (stats.isFile()) {
ctx.set('Content-type', 'text/html;charset=utf-8')
ctx.body = await fs.readFile(pathUrl)
} else {
await next()
}
}
}
module.exports = koaStatic
require('fs').promises
是可以用promise
而不用回调得方式来写- 实现就是拼接路径 读取文件。把
ctx.body
赋值
以下只作为提及一下
koa-router
路由管理
koa/multer
文件上传
koa-genetator
脚手架
express框架
基本使用
来自官网
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => res.send('Hello World!'))
app.post('/', function (req, res) {
res.send('Got a POST request')
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
多个回调函数的使用
express
里面有类似koa``next
的函数。作用也几乎相同
const express = require('express')
const app = express()
const port = 3001
// 访问输出 1234
app.get('/', function(req, res, next) {
console.log(1)
next()
},function(req, res, next) {
console.log(2)
next()
console.log(4)
},function(req, res, next) {
console.log(3)
next()
res.end('Hello World!')
})
app.listen(port, ()=>console.log(`Example app listening on port ${ port }!`))
中间件的使用
const express = require('express')
const app = express()
const port = 3001
app.use(function(req, res, next) {
console.log('Time:', Date.now())
next()
})
app.get('/', function(req, res, next) {
// 这里面可以做一些 路由中间件处理 如 处理路径之类
console.log(1)
next()
}, function(req, res, next) {
console.log(2)
next()
console.log(4)
}, function(req, res) {
console.log(3)
res.end('Hello World!')
})
app.listen(port, ()=>console.log(`Example app listening on port ${ port }!`))
先输出时间戳,再后来是 1 2 3 4 .则证明 use
中间件的使用在路由之前
与koa区别
相同点
- 都具有 中间件的概念
不同点
koa
里面使用的promise async await
,而express
使用的是 回调 的方式express
内置了很多中间件,比如路由 静态服务,模板等。而Koa
核心很小express
使用了ES5
。koa
使用的基本是ES6
开始模仿
测试代码
const express = require('./myExpress')
const app = express()
const port = 3001
app.get('/', (req, res)=>res.end('Hello World!'))
app.listen(port, ()=>console.log(`Example app listening on port ${ port }!`))
主代码
首先有一个思路。
- 我们需要创建一个服务。需要
http
模块,解析路由,需要url
模块 - 可以访问
.get
方法。证明返回的是一个 对象 - 有
get
和listen
方法 - 路由匹配原则。把路由存到数组里。接收到
request
之后。遍历数组进行匹配。成功之后执行对应的handler
.失败执行对应的处理函数
const http = require('http')
const url = require('url')
let routers = [{ // 用来处理 匹配不到路由
path: '*',
method: 'all',
handler(req, res) {
res.end(`Cannot ${ req.method } ${ req.url }`)
}
}]
function createApplication() {
// 返回的应该是一个对象
return {
get(path, handler) {
routers.push({
path,
method: 'get',
handler
})
},
listen(...args) {
let server = http.createServer((req, res)=>{
let { pathname } = url.parse(req.url)
let m = req.method.toLocaleLowerCase()
for (let i = 0; i < routers.length; i++) {
let { path, method, handler } = routers[i]
if (path === pathname && method === m) {
return handler(req, res)
}
}
routers[0].handler(req, res)
})
server.listen(...args)
}
}
}
module.exports = createApplication
开始拆分
作用。写在一起不容易维护。单独抽离,更加方便管理
目录
|-- myExpress
|-- package.json
|-- lib
|-- createApplication.js
|-- express.js
|-- router
|-- index.js
express
const Application = require('./createApplication')
function createApplication() {
return new Application()
}
module.exports = createApplication
createApplication
const http = require('http')
const Router = require('./router')
function Application() {
this.routers = new Router()
}
Application.prototype.get = function(path, handler) {
this.routers.get(path,handler)
}
Application.prototype.listen = function(...args) {
let server = http.createServer((req, res)=>{
function done(){
res.statusCode = 404
res.end(`Cannot ${ req.method } ${ req.url }`)
}
this.routers.match(req,res,done)
})
server.listen(...args)
}
module.exports = Application
router/index
const url = require('url')
function Router() {
this.stack = []
}
Router.prototype.get = function(path, handler) {
this.stack.push({
path,
method: 'get',
handler
})
}
Router.prototype.match = function(req, res, done) {
let { pathname } = url.parse(req.url)
let m = req.method.toLocaleLowerCase()
for (let i = 0; i < this.stack.length; i++) {
let { path, method, handler } = this.stack[i]
if (path === pathname && method === m) {
return handler(req, res)
}
}
done()
}
module.exports = Router
多个回调的模仿
先给一个流程图
解释一下流程图
- 请求到来的时候在
Router Stack
里面匹配路径 - 如果匹配成功,调用
dispatch
方法 dispatch
在route Stack
里面吧对应的回调函数执行
通俗理解。就是用了一个数组吧所有的回调函数存了起来。当匹配到这个路径的时候,把这个数组里面的回调函数 按条件 依次执行
// router / index
const url = require('url')
const Route = require('./route')
const Layer = require('./layer')
function Router() {
this.stack = []
}
Router.prototype.route = function(path) {
let route = new Route()
let layer = new Layer(path, route.dispatch.bind(route))
layer.route = route
this.stack.push(layer)
return route
}
Router.prototype.get = function(path, handler) {
let route = this.route(path)
route.get(handler) // 传入的是数组
}
Router.prototype.match = function(req, res, done) {
let { pathname } = url.parse(req.url)
let m = req.method.toLocaleLowerCase()
let idx = 0
let next = ()=>{
if (idx >= this.stack.length) return done()
let layer = this.stack[idx++]
if (layer.path === pathname) {
layer.handler(req, res, next)
} else {
next()
}
}
next()
}
module.exports = Router
// router layer
function Layer(path,handler){
this.path = path
this.handler = handler
}
module.exports = Layer
// router route.js
const Layer = require('./layer')
function Route() {
this.stack = []
}
Route.prototype.get = function(handler) {
handler.forEach(handle=>{
let layer = new Layer('/', handle)
layer.method = 'get'
this.stack.push(layer)
})
}
Route.prototype.dispatch = function(req, res, done) {
let idx = 0
let next = ()=>{
if (idx > this.stack.length) return done()
let layer = this.stack[idx++]
if (layer.method === req.method.toLowerCase()) {
layer.handler(req, res, next)
} else {
next()
}
}
next()
}
module.exports = Route
测试结果和原生的一样
优化
延迟加载路由
在我们写的过程中,当创建了express
的时候,就初始化了路由。当使用者不使用的时候,这就会造成了浪费,所以我们应该延迟加载路由系统,当需要使用的时候在进行使用
// lib/createApplication
const http = require('http')
const Router = require('./router')
function Application() {
}
Application.prototype.lazy_router = function() {
if (!this.routers) {
this.routers = new Router()
}
}
Application.prototype.get = function(path, ...handler) {
this.lazy_router()
this.routers.get(path, handler)
}
Application.prototype.listen = function(...args) {
let server = http.createServer((req, res)=>{
this.lazy_router()
function done() {
res.statusCode = 404
res.end(`Cannot ${ req.method } ${ req.url }`)
}
this.routers.match(req, res, done)
})
server.listen(...args)
}
module.exports = Application
就是在原型上加了一个lazy_router
方法,在使用的过程中先调用一下这个方法,以防止万一没有路由系统
补全请求方式
在我们代码中,我们只写了一个get
请求。所以我们需要补全所有请求方式。一个个写当然麻烦。肯定是用到循环,这里推荐一个包methods
这里还是以createApplication
作为模板,了解一下用法。其他需要修改的请自行修改
// lib/createApplication
const http = require('http')
const Router = require('./router')
const methods = require('methods')
function Application() {
}
// ..........
methods.forEach(method=>{
Application.prototype[method] = function(path, ...handler) {
this.lazy_router()
this.routers[method](path, handler)
}
})
// ....
module.exports = Application
减少循环次数
按照我们写的来说。如果当前路径没有对应的请求方法。依旧会循环所有handler
。这样的花效率不高,所以我们应该加一个标识,标识这个回调处理的何种方法的请求。当请求方式不匹配的时候。直接return
这样就减少了次数
中间件实现
思路流程
- 首先在
Application
原型上挂在一个use
方法。(只做分发,发给路由) - 在路由上创建
use
方法。根据所传入参数做处理 - 创建
layer
丢入stack
中 - 更改
match
匹配规则。判断条件后执行
代码实现
// createApplication
Application.prototype.use = function(path,handler) {
this.lazy_router()
this.routers.use(path,handler)
}
//Layer方法扩展
Layer.prototype.match = function(pathname) {
if (this.path === pathname) {
return true
}
if (!this.route) {
if (this.path === '/') return true
return pathname.startsWith(this.path + '/') // /aa/b /a 不能执行
}
return false
}
module.exports = Layer
// router/index
// 。。。。省略部分代码
Router.prototype.use = function(path, handler) {
if (typeof handler !== 'function') {
handler = path
path = '/'
}
let layer = new Layer(path, handler)
layer.route = undefined // 证明是中间件
this.stack.push(layer)
}
Router.prototype.match = function(req, res, done) {
let { pathname } = url.parse(req.url)
let m = req.method.toLocaleLowerCase()
let idx = 0
let next = ()=>{
if (idx >= this.stack.length) return done()
let layer = this.stack[idx++]
// 开始改造
if (layer.match(pathname)) { // 首先匹配路径
if (!layer.route) { // 中间件
layer.handler(req, res, next)
} else { // 路由 需要匹配方法
if (layer.route.methods[m]) {
layer.handler(req, res, next)
} else {
next()
}
}
} else {
next()
}
}
next()
}
module.exports = Router
具体实现过程
- 因为用户可能传入一个或者两个参数,在
use
里进行判断。如果传入的是一个函数,则path
默认是/
layer.route = undefined
这句代码是为了 标识中间件. 路由中 这里面放的是route
- 接下来就是匹配规则。重点说一下
Layer / match
方法 - 如果传入的不匹配。观察是不是中间件 ,是的话看有没有路由限制。在选择执行
- 注意,中间件的
handler
就是传入的handler
不是dispatch
。因为中间件不支持传入多个函数参数
错误中间件
app.use(function(req, res, next) {
if (req.url === '/a') next('error')
next()
})
// 错误中间件
app.use(function (err, req, res, next) {
console.error(err.stack)
})
即 在next()
里面写入内容。如果发生错误 就走到下面这个 错误中间件
当 发生错误之后。跳过。直接 执行错误中间件里面的代码
由此我们可以知道。主要修改的代码就是 match
过程中。还有中间件的dispatch
// router/route
Route.prototype.dispatch = function(req, res, done) {
let idx = 0
let next = (err)=>{
if (err) return done(err)
if (idx > this.stack.length) return done()
let layer = this.stack[idx++]
if (layer.method === req.method.toLowerCase()) {
layer.handler(req, res, next)
} else {
next()
}
}
next()
}
// router/index
Router.prototype.match = function(req, res, done) {
let { pathname } = url.parse(req.url)
let m = req.method.toLocaleLowerCase()
let idx = 0
let next = (err)=>{
if (idx >= this.stack.length) return done()
let layer = this.stack[idx++]
if (err) { // 如果有错误 找错误处理中间件
if (!layer.route) { // 判断参数时候是4个
layer.handler(err, req, res, next)
} else {
next(err)
}
} else {
if (layer.match(pathname)) { // 首先匹配路径
if (!layer.route) { // 是中间件 且非错误中间件
if (layer.handler.length !== 4) {
layer.handler(req, res, next)
} else {
next()
}
} else { // 路由 需要匹配方法
if (layer.route.methods[m]) {
layer.handler(req, res, next)
} else {
next()
}
}
} else {
next()
}
}
}
next()
}
- 其实就是判断是否有
err
,如果在 路由系统中匹配到了话,直接跳过 - 在中间件中。调用
next
方法。把err
一直往下传递 layer.handler.length
是指 函数的参数个数,错误中间件的参数个数为四个,且必须放在最后