[TOC]
Node.js
前言
浏览器的组成
- 人机交互部分 UI
- 网络请求部分 socket
- javascript 引擎部分(解析之行 javascript)
- 渲染引擎部分(渲染 HTML、CSS)
- 数据存储部分(cookie、localStorage、sessionStorage)
渲染引擎
主流渲染引擎
- chrome Blink(Webkit 分支)
- safari Webket 引擎
- firefox Gecko
- opera Blink
- IE Trident
- Edge EdgeHTML 引擎(Trident 的一个分支
引擎工作原理
DOM 树 CSS 规则树 合并一起成 渲染树。
Layout、reflow 的过程
用
documentFragment
浏览器访问网址过程
- 把网址封装成 http 请求报文(包括 get、host、connection 等..)
- 浏览器发起 DNS 解析请求,将域名转换成 IP 地址
- 浏览器将请求报文发送给服务器
- 服务器接收请求报文,并解析
- 服务器处理用户请求,将处理结果封装成 http 响应报文(包括 ContentType、Timing-Allow-Origin 等)
- 服务器将 HTTP 响应报文发送给浏览器
- 浏览器接收服务器响应报文,解析
- 浏览器解析 HTML 页面并展示,在解析过程中遇到新的资源时需要再次发起请求
- 最终展示页面
请求报文/响应报文的格式
开始行/起始行
start line
(请求行/响应行请求行
例
POST /infoNewsAction_uploadxheditorfile.action?immediate=1 HTTP/1.1
- 方法:GET、POST、PUT、HEAD、DELETE、OPTIONS、TRACE、CONNECT、LINK、UNLINK
- URL
- HTTP 版本
响应行
例
HTTP/1.1 200 OK
- 状态码:1xx、2xx、3xx、4xx、5xx
- HTTP 版本
首部行/报文头
header
(用来说明浏览器、服务器或报文主题的一些信息。每个首部行在结束地方都有CRLF换行
例
Cache-Control:max-age=60
- 首部字段名:通用首部字段、请求首部字段、响应首部字段、实体首部字段
- 值
CRLF空行
:主体和首部行之间有空行【可选】实体/主体
entity-body
在请求报文中,一般是 post/put 提交的表单信息
DNS 解析过程 教程
WEB 开发本质
- 请求,客户端发起请求
- 处理,服务器处理请求
- 响应,服务器将处理结果发送给客户端
- 客户端处理响应:C/S 架构和 B/S 架构
介绍
Node.js 是个一开发平台【开发平台的概念:有对应的变成语言,有语言运行时(Runtime),有能实现特定功能的 API(SDK:Software Development Kit)】,类似于 PHP 开发平台、Apple 开发平台、.net 开发平台。
- 该平台使用的语言是 javascript。
- 该平台 Runtime 是基于 Chrome V8 Javascript 引擎构建。
- 基于 node.js 可以开发控制台程序(命令行程序、CLI 程序)、桌面应用程序(GUI,借助 electron 等)、Web 应用程序(网站)
Node.js 全栈开发技术栈-MEAN:MongoDb 数据库、Express WEB 开发框架、Angular 前台、Node.js 后台
特点:
- 事件驱动(当事件被触发时,执行传递过去的回调函数)
- 非阻塞 I/O
- 单线程
- 拥有开源生态环境 - npm
应用场景:
服务器开发、web 请求和响应过程、了解服务器端如何和客户端配合
了解服务器端渲染
服务器端为客户端写接口
NODE & NVM
nvm
工具 可以实现在随时切换和管理 node 版本:建议先装 nvm 再装 node
- nvm version
- nvm install stable
- nvm install 版本号
- nvm uninstall 版本号
- nvm list
- nvm use 版本号 来应用该版本
快速入门几个要点
nodejs 和传统 php 等开发网站的区别
传统的后端:需要处于一个服务器容器 apache 等-监听用户请求并且根据不同请求作出不处理
nodeJS:既是 http 服务器,继续自己处理
REPL 介绍 read-eval-print-loop(交互式解释器)
类似于 devtool 里的 console tab
terminal 输入 nodej 进入,control+C 退出
第一个项目小实践
- 新建 js 文件
- 执行 node file.js
全局模块 Globals
比如 process,其他的 非全局模块需要 reqire 来加载
查看 API 的稳定性 分 3 级:stablity0 表示该 api 已经过时了红色;1 表示正在测试开发阶段橙色;2 是可正常使用蓝色
Buffer 类型
二进制数组对象。主要用于方便数据缓冲,易于传输。
js 中没有读取或操作饿紧致数据流的机制。NodeJS 中引入了 Buffer 使我们可以操作 TCP 流或文件流;Buffer 累踩坑的对象类似于整数数组,萏 Buffer 的大小是固定的,且在 V8 堆外分配物理内存。Buffer 的大小(Buffer.length)在被创建时确定,且无法调整;Buffer 是全局的,所以使用的时候无需 require()的方式加载
Buffer.from()
创建一个 Buffer 对象Buffer.byteLength
获取字符串对应的字节个数Buffer.isBuffer(obj)
判断一个对象是否是 Buffer 类型对象buf[index]
某个 buffer 实例的某个字节bug.length
某个 buffer 实例的字节个数
fs 读写文件操作
let fs = require('fs')
- 读文件 fs.writeFile(file, data [,options], callback)
- 写文件 fs.readFile(file [,options], callback(err,Bufferdata))
node 的单线程的异步操作 - 非阻塞 I/O 解释
node 的 event loop: 6 步
链接(https://developer.aliyun.com/lesson_1730_14094#_14094)
在线动画演示网址:http://latentflip.com/loupe
参考视频:www.youtube.com/watch?v=8aGhZQkoFbQ
文件路径
js 文件内的./路径是指 执行 js 文件时所处的目录,执行时是相对于这个来查找文件;
而非根据 js 文件所在目录 来查找文件。
__dirname 表示该 js 文件所在的绝对路径名
__filename 表示该 js 文件的完整绝对路径名(相比于 dirname 多一个自身的名字)
这两个变量并非全局变量,可以理解成 node 执行时将文件内代码封装成(functiong(**dirname,**filename){xxx})(‘/c/user/local/sss’, ‘c/user/local/sss/xx.js’)
使用 path 模块进行路径拼接
为了替代var filePath = __dirname + '/'+ 'xx.txt'
,不同操作系统或**dirname 内多少个/的边界问题var path require('path')
var filePath = path.join(**dirname, 'xx.txt')
http 服务
var http = require("http");
var server = http.createServer();
// 监听用户的请求事件(request事件) 回调函数两个行参(request,response)
server.on("request", function (req, res) {
// if(req ???)
res.setHeader("Content-Type", "text/html;charset=utf-8"); // 服务器设置http响应报文头,告诉浏览器使用响应的编码来解析网页
res.write("响应内容");
res.end(); // 对于每个请求,服务器必须结束响应,否则客户端会一直等待服务器响应结束
});
// 启动服务
server.listen(8080, function () {
console.log("服务器启动了");
});
回调函数行参-request 对象 常用 api
http.IncomingMessage
- .headers 报文头
- .rawHeaders 原生报文头(和 headers 的区别是:headers 返回
key1:val1,key2:val2...
的对象,而 rawHeaders 返回[key1,val1,key2,val2...]
) - .httpVersion 拿到请求客户端所使用的 http 协议版本
- .method 客户端请求方式 get、post、delete 等
- .url 获取本次请求的路径 不包含主机名称 端口号 域名等
回调函数行参-response 对象 常用 api
http.httpServerResponse
- .setHeader() 设置响应报文头
- .statusCode = 404 设置 http 响应状态码
- .statusMessage = ‘message’ 设置 http 响应状态码对应的消息
- .writeHead() 直接向客户端写入 http 响应报文头,优先级大于其他所有,如果没写这个,end()时默认执行。如 res.writeHead(404, ‘not found’, {‘Content-Type’: ‘text/html;charset=utf-8’})
- .write() 响应内容
- .end() 通知服务器 所有响应头和响应主体已被发送,服务器将其视为已完成
NPM & NRM
Express 框架
基于 NodeJs 的 web 开发框架
介绍
- 实现路由功能 没必要自己写很多 if(req.url) else
- 中间件(函数)功能 把监听 request 事件拆分成了很多方法
- 对 req 和 res 对象的扩展
- 本身并没有模版引擎,可以集成其他模版引擎
路由
- get/post/put/delete…
/**
* 通过中间件监听指定的路由的请求
* 支持:put/delete/get/post 等http methods
* 虚拟路径pathname 接受 正则RegExp 匹配
*/
app.get("/", (req, res) => {
// 只监听get请求
res.send("在首面");
/**
* send()相当于原生的end();
* 区别如下:
* 1. send()自动封装了很多比如setHeader(‘Content-Type’, ‘text/html;charset=utf-8’)等优化
* 2. end()只接受string or Buffer;send()可以是 string, Buffer, Object or Array
*/
});
app.get(/^\/index(\/.+)*$/, (req, res) => {
res.send("在index及子路径下 的 get请求");
});
app.post("/add", (req, res) => {
res.send("post add");
});
- use
/**
* mark 当两次相同路由匹配,执行了不同的express.static到不同文件夹下请求回调,则会优先第一次回调结果。逻辑是先找第一个资源 找不到的话再找第二个
*/
const fn = express.static(path.join(__dirname, "static")); // 处理静态资源的方法,指定静态资源路径
app.use("/", fn); // 实现所有静态资源 托管
/**
* use 和 以上几种 method 的区别是:
* 1. 路由匹配时 不限定方法,什么请求方法都可以
* 2. 请求路径中的第一部分(以/分割)只要与 /index 相等即可,并不要求pathname ===
*/
app.use("/home", (req, res) => {
res.send("在home及子路径下");
});
- all
/**
* all
* 1. 路由匹配时 不限定方法,什么请求方法都可以
* 2. 请求路径中 pathname 必须 === 完全匹配
*/
app.all("/all", (req, res) => {});
- paramas
/**
* req.params获取路由中的参数
* :开头 表示占位符
* 并且需要严格匹配占位符数量
*/
app.get("/news/:year/:month/:day", (req, res) => {
res.send(req.params);
});
res API
res.json({ a: "1", b: 2 }); // 将object或array转为json作为响应发,等同与res.send(json)
res.redirect([状态码默认302], path); // 重定向 封装原生流程:1. 设置状态码res.statusCode = 301或302 2. 设置消息 res.statusMessage = '重定向' 3. 设置相应报文头setHeader('location', 'path') 4. res.end()
res.redirect("https://google.com");
res.sendFile(path, function (err) {
if (err) throw err;
}); // 封装原生的 readFile('xx.txt', (err,data)=>{res.end(data)})
res.status(404).end("文件不存在"); // 封装原生流程: 1. 设置状态码res.statusCode = 301或302 2. 设置消息 res.statusMessage = '重定向' 3. res.end()
/**
* res.render(viewpath模版文件路径 [,locals一个替换模版中占位符key的值val的object] [, callback(err, html)])
* 需要给express配置一个模版引擎render才能工作,比如jade,ejs,pug, 然后在应用中进行如下设置才能让Express渲染模版文件
* 1. views 放模版文件的目录,比如 app.set('views', './views')
* 2. view engine 模版引擎,比如 app.set('view engine', 'ejs')
* 3. res.render('xx.html', {username: 'x'}, (err,html)=>{})
*/
req API
拆分封装 config & router
// 不推荐该方法
// in router.js
module.exports = function(app) {
app.get('/', (req,res)=>{
res.send('所有')
})
}
// in server.js
const router = require(./router.js)
const express = require('express');
const app = express()
router(app)
更推荐如下
// in handleRouter.js 封装所有路由监听回调
module.exports.index = function(req,res){
res.send('这里是handler index')
}
// in router.js
const express = require('express');
const handler = require('./handleRouter');
const router = express.Router(); // 创建一个router对象
router.app.get('/', handler.index)
router.app.get('/home', (req,res)=>{
res.send('首页')
})
module.exports = router
// in server.js
const router = require(./router.js)
const express = require('express');
const app = express()
// 设置app与router相关联,此处router是作为中间件 既是object又是function
app.use('/', router) // 关键点去看下use源码逻辑 并且tips一下:等价于app.use(function(req,res){}) 即此处 app.use(router)
安装
全局安装 npm i nodemon -g
代替 node 自动重启 nodemon file.js
运行
- bash 运行:
node xx.js
- nodemon
- vs code 进行 debug: RUN and Debug
单元测试
npm install jest -g
__test__ > xxname.spec.js
jest foldername --watch
test('测试备注名', ()=>{
const ret = require('../index)
console.log(ret)
expect(ret)
.toBe('期望值')
})
测试代码 自动生成工具
// index.spec.js
const fs = require("fs");
const path = require("path");
test("集成测试 测试生成测试代码文件", () => {
// 删除测试文件夹
fs.rmdirSync(path.join(__dirname, "..", "/data/__test__"), {
recursive: true, // 递归为true 则同时迭代清除文件夹下的所有文件
});
const src = new (require("../index"))();
src.getJsetSource(path.join(__dirname, "..", "/data"));
});
const path = require("path");
const fs = require("fs");
module.exports = class TestGenerator {
getJsetSource(sourcePath = path.resolve("./")) {
const testPath = `${sourcePath}/__test__`;
if (!fs.existsSync(testPath)) fs.mkdirSync(testPath);
// 遍历代码文件
let list = fs.readdirSync(sourcePath);
list
.map((v) => `${sourcePath}/${v}`) // 添加为完整路径
// 过滤文件
.filter((v) => fs.statSync(v).isFile())
// 排除测试代码
.filter((v) => v.indexOf(".spec") === -1)
.map((v) => this.genTestFile(v));
}
genTestFile(filename) {
const testFileName = this.getTestFileName(filename);
// 判断此文件是否存在
if (fs.existsSync(testFileName)) {
console.log(`该测试代码已存在${testFileName}`);
return;
}
const mod = require(filename);
let source;
if (typeof mod === "object") {
const baseName = path.basename(filename);
source = Object.keys(mod)
.map((v) => this.getTestSource(v, baseName, true))
.join("\n");
} else if (typeof mod === "function") {
const baseName = path.basename(filename);
source = this.getTestSource(baseName.replace(".js", ""), baseName);
}
fs.writeFileSync(testFileName, source);
}
getTestSource(methodName, classFile, isClass = false) {
return `
test('TEST ${methodName}', ()=>{
const ${
isClass ? "{" + methodName + "}" : methodName
} = require('../${classFile}')
const ret = ${methodName}()
// expect(ret)
// .toBe('test return')
})
`;
}
getTestFileName(filename) {
const dirName = path.dirname(filename);
const baseName = path.basename(filename);
const extName = path.extname(filename);
const testName = baseName.replace(extName, `.spec${extName}`);
return path.format({
root: dirName + "/__test__/",
base: testName,
});
}
};
异步编程
- js 的执行环境是单线程
- I/O 处理需要回调函数异步处理(异步 I/O)
- 前端异步 IO 可以消除 UI 阻塞,提高用户体验
- 后端异步可以提高 CPU 和内存利用率
javascript 异步解决方案的进化
- callback
- promise
- generator
- async & await:
任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。
async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。 - eventEmitter 事件监听方式 event.emit()&event.on()
node.js 基础
I/O 处理
- 同步阻塞
- 同步非阻塞
- 异步阻塞
- 异步非阻塞
node 文档
英文 https://nodejs.org/dist/latest-v10.x/docs/api/
中文 http://nodejs.cn/api/
基础 API
- readFileSync & readFile
- promisify
const { promisify } = require('util')
const readFile = promisify(fs.readFile)
- Buffer 读取数据类型为 Buffer。用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。 八位字节组 成的数组,可以有效的在 JS 中存储二进制数据
- readFile 和 writeFile 是会占用服务器缓存空间的,所以用 stream
- Http 创建一个 http 服务器
- response.end() 即 中止这个 stream
- 流 stream
- res.setHeader()和 res.writeHead()
CLI 工具
实现一个 cli 工具(vue 路由约定)
- npm init
- 新建脚本 xx.js,开头是
#!/usr/bin/env node
对 shell 指定使用 node 解析脚本 - 在 package.json > bin > xx.js 指定开始脚本
- init初始化 clone & spawn & open: spawn
- refresh约定路由: hbs(handlebars 模版引擎/hbs 最佳实践)
- serve
- npm link 链接 2 其实就是相当于 ln 指令操作
- publish 发布自己的库 执行
publish.sh
脚本
Koa 源码
- koa 的产生原因: 原生 http 的不足:1.令人困惑的 request 和 response;2. 对描述复杂业务逻辑的描述比如 AOP(Aspect-oriented-programing)/切面描述需要
- 为了简化 API,引入上下文 context 概念,核心是
洋葱圈模型 - use, next
- 简单实现一个 koa 框架
- index.js
- use
- listen
- kkb.js
- use
- createContext context.js
- request.js
- response.js
- createContext context.js
- listen
- use
- index.js
网络工程学
TCP 面向可靠性连接,UDP 不可靠
TELNET 传输的是明文,SSH 更安全
TCP 协议写个聊天室 js
require('net')
**http 协议 - 前端角度http 协议- 网工角度**查看 req & res 例子:
curl -v http://www.baidu.com
- request: 请求行(method.etc),消息报头(Accept 系,Content-Type.etc),请求正文(根据 Content-Type 确定)
- response: 状态行(1xx…5xx),实体报头,响应正文
- http 缓存
- http 例子, img.src 埋点
- 浏览器跨域三层封印: http request 发送时,已经发送给了后端也返回给了前端,但是浏览器不显示 response。协议端口域名 3 者任一不同就是非同源.
- node 层设置
res.setHeader('Access-Control-Allow-Origin', '*'或特写某个url)
- 预检请求,即在请求阶段被浏览器拦截 option
- 如果携带 cookie 信息:
res.setHeader('Access-Control-Allow-Credentials', 'true')
- 服务器反向代理: 让同源服务器去请求非同源服务器,返回给同源的前端。
const proxy = require('http-proxy-middleware')
- node 层设置
bodyParer
实现一个爬虫
request
又补充了 http/socket.io 两种方法写 im 通讯程序
用 socket.io 写了浏览器模拟 terminal,推拉流:后端的流推到前端
monaco-editor 编辑器,尤雨溪也写了个基于这个的 docker 在线编辑器
持久化
数据就是放在磁盘里,所谓的持久化,就是指 关了机再开还是能读取到数据
文件系统 fs
mysql 和 SQL 语句
数据库中间件 ORM(Object Relation Mapping),把数据库映射成对象,像操作对象一样操作数据库: sequelize
Transactions - 事务MySql Workbench: 画 ER 图的工具;不存在多对多的关系,多对多之间一定是用一个中间 mapping 表来联系;反向工程:比如用 Workbench 通过数据库反向生成 ER 表
用 sequelize 实现简单的电商系统 ./shop 文件夹
mongodb
非传统的关系型数据库- eventEmmiter 在文件夹./mongo & initData.js
- 一个水果商店 例子 在文件夹./example
mongoose
提供的本质就是 domain 领域模型,而一旦定义了模型就可以自动基于约定产生 api
开发方法论
原型 ->ER -> DB -> 后端代码 -> UI
有 sequlee 后改变成了
原型 -> 后端代码(domain 领域模型)-> (DB -> ER 两者沦为持久化服务)-> api ->UI一个 team 需要的东西:
技术框架
开发方法论
管理方法CRUD 基于约定,基于 restful
UI admin 基于约定
restful api 实现小例子在 ./restful 文件夹
鉴权
三种常见鉴权方式
- Session/Cookie
- Token
- OAuth
- SSO
Session/Cookie
http 协议本身是无状态的
- cookie 有大小限制,格式问题,明文状态不安全
- session 所以 后端放一个 key-value 的集合,前端只存一个 key
- koa-session
- hash 防篡改
- 使用 redis 存储 session;redis 是高性能的 key-value 数据库,支持持久化,但是绝大多时间存在内存中
- 例子:使用 koa-session,koa-router 等 鉴权例子 在文件夹./session 里
Token
- session 的不足,离开浏览器玩不转;并且服务器需要状态保持
- token 是密码形式,不需要服务器保持状态。
- token 是有 3 段(令牌头 加密规则 base64 加密;payload(载和)token 内容「并且一般不要放敏感信息」 base64 加密;根据密钥对前两两部分全文 hash 运算的结果)组成
- jwt 验证的原理
OAuth
SSO
eggjs
ts
docker 部署
todo
path
相关方法
- path.dirname
- path.basename
- path.extname
- path.resolve
- __dirname
fs
相关方法
- fs.mkdirSync
- fs.rmdirSync
- fs.existsSync
- fs.statSync 是不是一个文件
参考
restful 风格 > GraphQL 下一代接口风格 > 一个用 node 和 graphQL 风格 API 写的 博客系统