Node.js从零开发web server博客项目
篇幅过长, 仅展示目录和部分代码
点击去GitHub查看完整版
开发接口(不用框架)
http请求概述
- DNS 解析,建立 TCP 连接,发送 http 请求
- server 接收到 http 请求,处理并返回数据
- 客户端接收到返回数据,处理数据(例如渲染、执行JS)
Node.js 处理http 请求
- get 请求和 querystring
- post 请求和 postdata
- 路由(接口、地址)
const http = require('http');
const server = http.createServer((req,res) => {
res.end('hello world!');
});
server.listen(8000);
//浏览器访问 http://localhost:8000/
Node.js 处理 get 请求
- get 请求,客户端向 server 端获取数据,如查询博客列表
- 通过 querystring 来传递数据,如 a.html?a=100&b=200
- 浏览器直接访问,发送 get 请求
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req,res) => {
console.log(req.method) //GET
const url = req.url //获取请求的完整 URL
//关键解析[0]是'?'前的内容, [1]是'?'后内容
req.query = querystring.parse(url.split('?')[1])
res.end(JSON.stringify(req.query)); //将 querystring 返回
});
server.listen(8000);
//浏览器访问 http://localhost:8000/
Node.js 处理 post 请求
- post 请求,即客户端要像服务端传递数据,如新建博客
- 通过 post data 传递数据,后面解释
- 浏览器无法直接模拟,需要手写JS,或者使用 postman app
const http = require('http')
const server = http.createServer((req, res) => {
if (req.method === 'POST'){
// POST 必须大写
//数据格式
console.log('content-type: ', req.headers['content-type'])
//接收数据
let postData = ''
//开始接收数据
req.on('data', chunk => {
postData += chunk.toString()
})
//结束数据接收
req.on('end', () => {
console.log('postData:', postData)
res.end('hello world!') //在这里返回,因为是异步
})
}
})
server.listen(300)
Node.js 处理路由
https://github.com/username/xxx
每个斜线后面的唯一标识就是路由
Node.js 综合应用
const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
const method = req.method
const url = req.url
const path = url.split('?')[0] //重点:split('?'[0])语法弄清楚
const query = querystring.parse(url.split('?')[1])
//设置返回值格式为 JSON
res.setHeader('Content-type', 'application/json')
//返回的数据
const resData = {
method,
url,
path,
query
}
//返回
if (method === 'GET') {
res.end(
JSON.stringify(req.query)
)
}
if (req.method === 'POST'){
let postData = ''
//res.on('data')指每次发送的数据
//chunk 逐步接收数据 req绑定一个data方法 chunk是变量
req.on('data', chunk => {
postData += chunk.toString()
})
//req.on(end)数据发送完成;
req.on('end', () => {
console.log('postData:', postData)
console.log('resData:', resData)
resData.postData = postData
//返回
res.end(
JSON.stringify(resData)
)
})
}
})
server.listen(300)
console.log('OK')
搭建开发环境
- 从零搭建,不使用框架
- 使用 nodemon 监测文件变化,自动重启 node
- 使用 cross-env 设置环境变量,兼容Mac Linux 和 Windows
- 配置完后使用
$ npm run dev
命令启动项目
开始搭建
使用npm安装上述插件,设置npm镜像源
查看npm源地址
npm config list
结果:
metrics-registry = "http://registry.npm.taobao.org/"
修改registry地址,比如修改为淘宝镜像源。
npm set registry https://registry.npm.taobao.org/
如果有一天你肉身FQ到国外,用不上了,用rm命令删掉它
npm config rm registry
npm install -g nodemon
npm install -g cross-env
新建文件夹blog_1,在里面新建bin文件夹和app.js,在bin里面新建www.js文件
// package.json 代码 注意: =不能有空格
{
"name": "blog_1",
"version": "1.0.0",
"description": "",
"main": "www.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
"prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"
},
"author": "",
"license": "ISC"
}
// ./bin/www.js 代码
const http = require('http')
const PORT = 300
const serverHandle = require('../app')
const server = http.createServer(serverHandle)
server.listen(PORT)
// app.js 代码
const handleBlogRouter = require('./src/router/blog')
const handleUserRouter = require('./src/router/user')
const serverHandle = (req, res) => {
//设置返回值格式 JSON
res.setHeader('Content-type', 'application/json')
// const resData = {
// name: 'zhang',
// site: 'imooc',
// env: process.env.NODE_ENV
// }
// res.end(
// JSON.stringify(resData)
// )
//处理 blog 路由
const blogData = handleBlogRouter(req, res)
if (blogData) {
res.end(
JSON.stringify(blogData)
)
return
}
//处理 user 路由
const userData = handleUserRouter(req, res)
if (userData) {
res.end(
JSON.stringify(userData)
)
return
}
//未命中路由,返回404
res.writeHead(404, {"Content-type": "text/plain"})
res.write("404 Not Found\n")
res.end()
}
module.exports = serverHandle
开发接口
初始化路由
- 初始化路由:根据之前设计方案,做出路由
- 返回假数据:将路由和数据处理分离,以符合设计原则
接口设计方案
描述 | 接口 | 方法 | url参数 | 备注 |
---|---|---|---|---|
获取博客列表 | /api/blog/list | get | author 作者,keyword 搜索关键字 | 参数为空则不进行查询过滤 |
获取一篇博客的内容 | /api/blog/detail | get | id | |
新增一篇博客 | /api/blog/new | post | post 中有新增的信息 | |
更新一篇博客 | /api/blog/update | post | id | postData 中有更新信息 |
删除一篇博客 | /api/blog/del | post | id | |
登录 | /api/user/login | post | postData 中有用户名和密码 |
具体代码:
// ./src/router/user.js
const handleUserRouter = (req, res) => {
const method = req.method //GET POST
//登录
if (method === 'POST' && req.path === '/api/user/login') {
return {
msg: '这是登录的接口'
}
}
}
module.exports = handleUserRouter
// ./src/router/blog.js
const handleUserRouter = (req, res) => {
const method = req.method //GET POST
//登录
if (method === 'POST' && req.path === '/api/user/login') {
return {
msg: '这是登录的接口'
}
}
}
module.exports = handleUserRouter
开发路由 博客列表
业务分层 拆分业务
- createServer 业务单独放在
./bin/www.js
- 系统基本设置、基本信息
app.js
放在根目录 - 路由功能
./src/router/xxx.js
- 数据管理
./src/contoller/xxx.js
- 数据处理
- createServer 业务单独放在
博客列表代码
开发路由 博客详情
博客代码同上一章
使用 promise 读取文件,避免 callback-hell
开发路由 (处理POSTData)
开发路由 (新建和更新博客路由)
开发路由 (删除博客路由和登录博客路由)
删除博客
登录博客
总结
node.js 处理 http 请求的常用技能,postman 的使用
node.js 开发博客项目的接口(未连接数据库,未登录使用)
为何要将 router 和 controller 分开?
路由和 API 区别:
- API :前后端、不同端(子系统)之间对接的通用术语
- 路由:系统内部的接口定义,是 API 的一部分
使用MySQL数据库
MySQL安装
讲解步骤:
MySQL 的介绍、安装和使用
node.js 连接 MySQL
API 连接 MySQL
为什么使用MySQL?
- MySQL 最常用,有专人运维
- MySQL 有问题可以随时查到
- MySQL 本身是复杂的,本课只讲使用
MySQL 介绍:
- web server 中最流行的关系型数据库
- 免费下载学习
- 轻量级,易学易用
MySQL 下载: https://dev.mysql.com/downloads/mysql/
MySQL 安装:
- 解压,打开根目录初始化
my.ini
文件, 自行创建在安装根目录下创建my.ini
[mysqld]
# 设置3306端口
port=3306
# 设置mysql的安装目录
basedir=C:\Program Files\MySQL
# 设置mysql数据库的数据的存放目录
datadir=C:\Program Files\MySQL\Data
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。
max_connect_errors=10
# 服务端使用的字符集默认为utf8mb4
character-set-server=utf8mb4
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用“mysql_native_password”插件认证
#mysql_native_password
default_authentication_plugin=mysql_native_password
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8mb4
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8mb4
配置文件中的路径要和实际存放的路径一致(要手动创建Data文件夹)
打开系统设置,配置环境变量 ` Path = ‘解压目录’\bin
初始化安装:
mysqld --initialize --console
注意输出信息:
root @ localhost:后面是初始密码(不含首位空格)
,后续登录需要用到,复制密码先保存起来安装MySQL 服务:
mysqld --install[服务名]
不填默认mysql
启动MySQL:
net start mysql
使用官方客户端管理mysql
Wrokbench 下载地址:https://dev.mysql.com/downloads/
默认安装,打开后输入之前保存的默认密码登录
弹出修改密码界面,修改密码再登录
MySQL基本使用
根据需求设计表
users:
id | username | password | realname |
---|---|---|---|
1 | zhangsan | 123 | 张三 |
2 | lisi | 1234 | 李四 |
blogs:
id | title | content | createtime | author |
---|---|---|---|---|
1 | 标题A | 内容A | 1573989043149 | zhangsan |
2 | 标题B | 内容B | 1573989111301 | lisi |
MySQL语法和操作
右键表 Drop table
删除
右键表 Alter table
修改
总结
- 如何建库、如何建表
- 建表时常用数据类型( int bigint varchar longtext)
- SQL 语句实现增删改查
Node.js 操作 MySQL
示例:用 demo 演示 Node.js 操作 MySQL
封装:将其封装为系统可用的工具
使用:让 API 直接操作 MySQL
Node.js 操作 MySQL demo
总结
- Node.js 连接 MySQL,如何执行 sql 语句
- 根据 NODE_ENV 区分设置
- 封装 exec 函数,API 使用 exec 操作数据库
用户登录
- 核心:登录校验 & 登录信息存储
- 为何只讲登录,不讲注册?
- 注册复杂程度低,涉及内容少
- 登录有统一解决方案
Cookie
- 什么是 Cookie
Session
- Cookie 存放信息非常危险
- 如何解决:cookie 中存储 userId, server 端对应 username
- 解决方案:session ,即 server 端储存用户信息
当前代码 session 代码的问题
- session 是 JS 变量,放在 Node.js 进程内存中
- 进程内存有限,访问量过大,内存暴增怎么办?
- 正式上线是多进程,进程之间内存无法共享
Redis
Redis 特点
- Web Server 最常用的缓存数据库,数据储存在内存中
- 相比于 MySQL ,访问速度极快
- 成本更高,储存空间小
- 将 Web Server 和 Redis 拆分为两个单独服务
- 双方独立,可扩展
- 像 MySQL 一样
安装 Redis
- Windows http://www.runoob.com/redis/redis-install.html
- Mac 使用 brew install redis
打开系统设置,配置环境变量 Path = C:\Program Files\Redis
开发登录 前端联调
- 登录依赖 Cookie,必须用浏览器
- Cookie 跨域不共享,前端和 server 端必须同域
- 需要用到 Nginx 做代理,让前后端共域
Stream
- IO 操作的性能瓶颈
- IO 包括 “网络 IO” 和 “文件 IO”
- 相对于 CPU 计算和内存读写, IO 的突出特点就是:慢
- 如何在有限的硬件资源下提高 IO 的操作效率
写日志
日志拆分
- 日志内容会慢慢积累,放在一个文件中不好处理
- 按时间划分日志文件,如 2019-02-10.access.log
- 实现方式:Linux 的 crontab 命令,即定时任务
Crontab
设置定时任务,格式:
***** command
*分钟*小时*天*月*星期 command脚本命令
- 将 access.log 拷贝并重命名为 2019-02-10.access.log
- 清空 access.log 文件,继续积累日志
代码演示
总结
- 日志对 server 的重要性
- IO 性能瓶颈,使用 stream 提高性能, node.js 中如何操作
- 使用 Crontab 拆分日志,使用 readline 分析日志内容
Web 安全
常见安全问题和解决方案
- SQL 注入:窃取数据库内容
- XSS攻击:窃取前端的 Cookie 内容
- 密码加密:保障用户信息安全(重要)
- Server 端攻击方式非常多,预防手段也非常多
- 本科只讲常见的、能通过 Web Server ( Node.js ) 层面预防的
- 有些攻击需要硬件和服务来支持(需要 OP 支持),如 DDOS
SQL 注入
- 最原始、最简单的攻击,从有了 Web2.0 就有了 SQL 注入攻击
- 攻击方式:输入一个 SQL 片段,最终拼接成一段攻击代码
- 预防措施:使用 MySQL 的 escape 函数处理输入数据内容即可
不用框架开发博客总结
主要课程
- 处理 Http 接口
- 连接数据库
- 实现登录
- 安全
- 日志
- 上线(最后一起讲)
Server 和前端区别
- 服务稳定性(zui后讲)
- 内存 CPU (优化 扩展)
- 日志记录
- 安全(包括登录验证)
- 集群和服务拆分
下一步要怎么做
- 不使用框架开发,从零开始,关注底层 API
- 很琐碎、复杂,没有标准,很容易写乱
- 适合学习,但不适合应用,接下来开始 Express 和 Koa2
Express 框架
Express 介绍
- Express 是 Node.js 最常用的 Web Server 框架
- 什么是框架?
- 不要以为 Express 框架过时了
目录
- Express 下载、安装和使用,了解 Express 中间件机制
- 开发接口、连接数据库、实现登录、记录日志
- 分析 Express 中间件原理
介绍 Express
- 安装(使用脚手架 Express-grnerator)
- 初始化代码介绍,处理路由
- 使用中间件
总结
- 使用框架开发的好处(相比之前不使用框架)
- express 的使用和路由处理,以及操作 session redis 日志等
- express 中间件的使用和原理
下一步
- JS 的异步回调带来了很多问题,Promise 也不能解决所有
- Node.js 已经支持 async/await 语法,要用起来
- Ko2 也已经原生支持 async/await 语法,接下来讲解
Koa2 框架
总结
- 使用 async/await 的好处
- koa2 的使用,以及如何操作 session redis 日志
- koa2 中间件的使用和原理
下一步:
- 一直处于开发环境,和前端联调过,但从未上线
- 如何实现线上服务稳定性?PM2 是什么?
- nginx 在线上环境扮演了什么重要作用?
线上环境
- 服务器稳定性
- 充分利用服务器硬件资源,以便提高性能
- 线上日志记录
PM2 功能:
- 进程守护,系统崩溃自动重启
- 启动多进程,充分利用 CPU 和内存
- 自带日志记录功能
多进程
- 为何使用多进程?
- 回顾之前讲 session 时说过, 操作系统限制一个进程的内存
- 内存:无法充分利用机器全部内存
- CPU:无法充分利用多核CPU
- 多进程和 redis
- 多进程之间,内存无法共享
- 多进程访问一个 redis ,实现数据共享
关于服务器运维
- 服务器运维,一般都由专业的 OP 人员和部门来处理
- 大公司都有自己的运维团队
- 中小型工期推荐使用一些云服务,如阿里云的 node 平台
总结
- PM2 的核心价值
- PM2 的常用命令和配置,日志记录
- 多进程