node.js学习笔记
引用参考2021了,真的不要再说 Node.js 是一门编程语言了
一、node.js概述
Node.js 不是一门编程语言,它是一个执行 JavaScript 代码的运行环境
1、node.js和浏览器
-
因为浏览器和 Node.js 都内置了 JavaScript V8 Engine。所以他们都可以运行 JavaScript
-
浏览器的JavaScript可以操作HTML和浏览器窗口(在JavaScript V8引擎中添加了控制他们的API,即BOM和DOM)
-
Node.js 中没有 DOM 和 BOM,但是添加了很多系统级别的 API,如对系统文件和文件夹的操作。获取操作系统信息,比如系统内存总量是多少,系统临时目录在哪
-
JavaScript 运行在浏览器中(即客户端 JavaScript),用于创建前端工程化工具。
-
JavaScript 运行在 Node.js 中(即服务器端 JavaScript),用于构建服务器端应用。
2、ES6,JavaScript,node.js区别
-
ECMAScript 6.0(简称 ES6)是 JavaScript 语言的标准(即语法规范),写javascript和nodejs都必须遵守。如:变量定义,循环、判断、函数、原型和原型链、作用域和闭包、异步。
不能操作DOM,不能监听click事件,不能发送ajax请求,不能处理http请求,不能操作文件。即,只有ECMAScript ,几乎做不了任何实际的项目 -
JavaScript:ECMAScript语法规范++Web API。可以进行DOM操作,BOM操作,事件绑定,Ajax等(浏览器端的任何操作)
-
node.js: ECMAScript语法规范++nodejs API。如:处理http ,处理文件等(完成server端的任何操作)
3、模块系统
在 Node.js 环境中,默认就支持模块系统,该模块系统遵循 CommonJS 规范。
-
__filename:当前模块文件名称。
-
__dirname:当前文件所在路径。
3.1、Node.js 内置模块
- Path:模块内提供了一些和路径操作相关的方法。
const path = require("path")
console.log(path.parse(__filename))
{
root: '/',
dir: '/Users/administrators/Desktop/node_test',
base: 'app.js',
ext: '.js',
name: 'app'
}
- File system:文件操作系统,提供了和操作文件相关的方法。
const fs = require("fs")
const files = fs.readdirSync("./")
console.log(files) [ 'app.js', 'logger.js' ]
fs.readdir("./", function (error, files) {
console.log(error) // null | Error {}
console.log(files) // [ 'app.js', 'logger.js' ] | undefined
})
- 在引入内置模块时, 使用的是模块的名字,前面不需要加任何路径。
4、npm
Node.js 软件包都是基于Node.js平台开发的,这些软件包都被托管在 www.npmjs.com 中,通过npm来管理
npm随node.js一起安装,和node是两个独立的应用程序(可以通过版本号证明)
4.1、package.json
-
Node.js 规定在每一个软件包(或工程项目)都必须包含一个叫做 package.json 的文件
-
它是应用程序的描述文件,包含和应用程序相关的信息,比如应用名称,应用版本,应用作者等等。
-
通过 package.json 文件可以方便管理应用和发布应用。
-
创建 package.json 文件: npm init
-
快速创建 package.json 文件: npm init --yes
{
"name": "project-name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
4.2、下载 Node.js 软件包
在应用程序的根目录执行命令:npm install < pkg > 或者 npm i < pkg >
软件包下载完成后会发生三件事:
-
软件包会被存储在 node_modules 文件夹中,如果在应用中不存在此文件夹,npm 会自动创建。
-
软件包会被记录在 package.json 文件中. 包含软件包的名字以及版本号。
-
npm 会在应用中创建 package-lock.json 文件, 用于记录软件包及软件包的依赖包的下载地址及版本。
4.3、node_modules
node_modules 文件夹中的软件包不需要提交到 git 仓库中,因为文件夹较大,可达到几百兆。
-
在 package.json 文件中都会记录项目都依赖了哪些包,在package-lock.json 查看对应包的版本
-
可以通过 npm install 命令重新下载它们
将应用程序提交到版本库之前,切记将 node_modules 文件夹添加到 .gitignore 文件中,这样就不会连同node_modules一块上传。
git init
git status
echo "node_modules/" > .gitignore
git status
git add .
git commit -m "our first commit"
4.4、删除和更新软件包
npm uninstall <pkg>
npm un <pkg>
npm outdated ,可以查看哪些软件包已经过期,对应的新版本是什么。
npm update ,更新过期的软件包,更新操作遵循语义版本控制规则。
4.5、项目依赖和开发依赖
在 package.json 文件中, 项目依赖和开发依赖要分别记录
- 项目依赖被记录在 dependencies 对象中
- 开发依赖被记录在 devDependencies 中
- 使开发者可以在不同的环境中下载不同的依赖软件包。
下载开发依赖,在命令后加上 --save-dev 或者 -D。如:npm i eslint -D
4.6、本地安装和全局安装
本地安装:将软件包下载到应用根目录下的 node_modules 文件夹中,软件包只能在当前应用中使用
全局安装:将软件包下载到操作系统的指定目录中,可以在任何应用中使用
- 通过 -g 选项将软件包安装到全局:npm install < pkg > -g
4.7、设置或更改npm镜像地址
由于 npmjs.com 是国外的网站,大多数时候下载软件包的速度会比较慢
更改 npm 镜像地址
npm config set registry https://registry.npm.taobao.org
npm config set registry https://registry.npmjs.org/
cat .npmrc
4.8、npx命令
npx 是 npm 软件包提供的命令,它是 Node.js 平台下软件包执行器。主要用途有两个:
- 第一个是临时安装软件包执行后删除它
有些提供命令的软件包使用的频率并不高,比如 create-react-app 脚手架工具,我能不能临时下载使用,然后再删掉它。
npx create-react-app react-test
- 第二个是执行本地安装的提供命令的软件包
现在有两个项目都依赖了某个命令工具软件包,但是项目 A 依赖的是它的 1 版本,项目 B 依赖的是它的 2 版本,我在全局到底应该安装什么版本呢 ?
该软件包可以在本地进行安装,在 A 项目中安装它的 1 版本, 在 B 项目中安装它的 2 版本,在应用中可以通过 npx 调用 node_modules 文件夹中安装的命令工具。
- 总结:将所有软件包安装到应用本地是现在最推荐的做法,一是可以防止软件包的版本冲突问题,二是其他开发者在恢复应用依赖时可以恢复全部依赖,因为软件包安装到本地后会被 package.json 文件记录,其他开发者在运行项目时不会因为缺少依赖而报错
二、server端介绍
1、服务稳定性
- server端可能会遭受各种恶意攻击和误操作
- 单个客户端可以意外挂掉,但是服务端不能
- 使用PM2做进程守候(当服务一旦挂掉,会自动重启)
2、考虑CPU 和内存(优化、扩展)
- 客户端独占一个浏览器,内存和CPU都不是问题
- server端要承载很多请求,CPU和内存都是稀缺资源
- 使用stream 写日志,使用redis存session
3、日志记录
- 前端也会参与写日志,但只是日志的发起方,不关心后续
- server端要记录日志、存储日志、分析日志,前端不关心
- 多种日志记录方式,以及如何分析日志
4、安全
- server端要随时准备接收各种恶意攻击,前端则少很多
- 如︰越权操作,数据库攻击等
- 登录验证,预防xss攻击和sql注入
5、集群和服务拆分
- 产品发展速度快,流量可能会迅速增加
- 如何通过扩展机器和服务拆分来承载大流量?
- 本课程在设计上支持服务拆分
三、node简单demo实战
一、项目介绍
1、需求
首页,作者主页,博客详情页
登录页
管理中心,新建页,编辑页
2、技术方案
数据如何存储
- 博客数据
- 用户数据
如何与前端对接,即接口设计
二、开发博客项目之接口
1、开发接口(不用express、koa)
- nodejs处理http请求
- 搭建开发环境
- 开发接口(暂不连接数据库,暂不考虑登录)
2、处理get请求
- 先在项目路径下运行npm init -y,初始化,生成一个package.json文件
- 将package.json里,改为"main": “app.js”, 并新建一个app.js文件
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req,res) => {
console.log('method',req.method);
const url = req.url;
console.log('url',req.url);
req.query = querystring.parse(url.split('?')[1]);
let routerPath = url.split('?')[0];//返回路由
console.log('query', req.query);
res.end(JSON.stringify(req.query))
})
server.listen(8000);
console.log('OK')
- 在浏览器输入地址:http://localhost:8000/api/blog/list?userName=zhangsan&password=1234
打印 {“userName”:“zhangsan”,“password”:“1234”}
3、处理post请求
const http = require('http');
const server = http.createServer((req,res) => {
console.log('method:',req.method);
if(req.method === "POST"){
//req 数据格式
console.log('request content-type:',req.headers['content-type']);
let postData = '';
req.on('data',chuck=>{
postData += chuck.toString();
})
req.on('end',()=>{
console.log('postData:',postData);
res.end('hello world!');
})
}
})
server.listen(8000);
console.log('OK')
打印如下:
method: POST
request content-type: application/json
postData: {
“username”:“admin”,
“password”:“123456”
}
4、处理http请求综合案例
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req,res) => {
const method = req.method;
const url = req.url;
const routerPath = url.split('?')[0];
const query = querystring.parse(url.split('?')[1]);
//设置返回格式为 JSON
res.setHeader('content-type','application/json');
//返回的数据
const resData = {
method,
url,
routerPath,
query
};
//返回数据
if(method === "GET"){
res.end(JSON.stringify(resData));//输出的是JSON字符串
}
if (method === "POST"){
let postData = '';
req.on('data',chuck=>{
postData += chuck.toString();
})
req.on('end',()=>{
resData.postData = postData;
res.end(JSON.stringify(resData));
})
}
})
server.listen(8000);
console.log('OK')
5、搭建开发环境
- 从О开始搭建,不适用任何框架
- 使用nodemon 监测文件变化,自动重启node
- 使用cross-env设置环量,兼用linux和windows
项目文件夹blog1 => npm init -y => 安装依赖 => 新建文件
“cross-env”: “^5.2.0”,
“nodemon”: “^1.18.9”
www.js
const http = require('http');
const PORT = 8000;
const serverHandle = require('../app.js');
const server = http.createServer(serverHandle);
server.listen(PORT);
console.log('OK1')
app.js
const handleBlogRouter = require('./src/router/blog');
const handleUserRouter = require('./src/router/user');
const serverHandle = (req,res)=>{
//设置返回格式
res.setHeader('Content-type','application/json');
//处理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;
router/blog.js
const handleBlogRouter = (req,res)=>{
const method = req.method;
const url = req.url;
const path = url.split('?')[0];
//获取博客列表
if (method === 'GET' && path === '/api/blog/list'){
return {
msg:'这是获取博客列表的接口'
}
}
//获取博客详情
if (method === 'GET' && path === '/api/blog/detail'){
return {
msg:'这是获取博客详情的接口'
}
}
//新建一篇博客
if (method === 'POST' && path === '/api/blog/new'){
return {
msg:'这是新建一篇博客的接口'
}
}
//更新一篇博客
if (method === 'POST' && path === '/api/blog/update'){
return {
msg:'这是更新一篇博客的接口'
}
}
//删除一篇博客
if (method === 'POST' && path === '/api/blog/del'){
return {
msg:'这是删除一篇博客的接口'
}
}
}
module.exports = handleBlogRouter;
router/user.js
const handleUserRouter = (req,res)=>{
const method = req.method;
const url = req.url;
const path = url.split('?')[0];
//获取博客列表
if (method === 'POST' && path === '/api/user/login'){
return {
msg:'这是登录的接口'
}
}
}
module.exports = handleUserRouter;
6、路由和 API
- API:
前端和后端、不同端(子系统)之间对接的一个术语
url(路由)‘/api/blog/list`,get,输入,输出 - 路由:
API的一部分
后端系统内部的一个定义
三、开发博客项目之数据存储
- node.js连接mysql
- API连接mysql
1、为什么选择mysql,而不是mogondb
- mysql是企业内、社区内最常用的存储工具,一般都有专人运维,有问题随时可查
- 另: mysql本身是一个复杂的数据库软件,本课只讲基本使用
2、mysql服务启动
net start mysql
3、mysql
- 安装:npm i mysql --registry=https://registry.npm.taobao.org 使用淘宝镜像
- config/db.js
const env = process.env.NODE_ENV // 环境参数
//配置参数
let MYSQL_CONFIG = {};
if(env === 'dev'){
MYSQL_CONFIG = {
host:'localhost',
user:'root',
password:'123456',
port:'3306',
database:'hzy_blog'
}
}
if(env === 'production'){
MYSQL_CONFIG = {
host:'localhost',
user:'root',
password:'123456',
port:'3306',
database:'hzy_blog'
}
}
module.exports = MYSQL_CONFIG;
- db/mysql.js
const mysql = require('mysql');
const MYSQL_CONFIG = require('../config/db');
// 创建链接对象
const connection = mysql.createConnection(MYSQL_CONFIG);
//开始连接
connection.connect();
//统一执行sql 函数
const exec = (sql) => {
return new Promise((resolve,reject) => {
connection.query(sql,(err,res) => {
if(err){
reject(err);
return;
}
resolve(res);
})
})
}
module.exports = {exec};
//执行sql,增删改查
// const sql = `insert into blogs (title,content,createtime,author) values ('标题C','内容C','2021-06-26 20:32:58','cccc')`
// 删除操作 ---软删除,可恢复,借助‘改’操作
// const sql = `update users set state='1' where username='lisi'`
// const sql = `delete from users where username='lisi'`
// 修改操作
// const sql = `update users set state='1' where username='lisi'`
// 查询操作
// const sql = `select *from users where username='lisi'`
#为了兼容后面如果某些条件不满足的情况SQL报错
where 1=1
# 用到的基本语句
USE myblog;
#插入语句
INSERT INTO `user` (username,`password`,realname) VALUES ('瓜瓜',12345678,'木瓜')
#查询语句
SELECT * FROM `user`;
#查询某个字段
SELECT id,username FROM `user`;
#查询条件 WHERE
select * FROM `user` WHERE username='一咻';
#多个条件共存
#AND
SELECT * FROM `user` WHERE username ='一咻' AND realname = 'wf';
#或者条件
#OR
SELECT * FROM `user` WHERE username='一咻' OR username='瓜瓜';
#模糊查询
#LIKE
SELECT * FROM `user` WHERE `password` LIKE '%1%';
#排序
#ORDER BY
#默认情况下是正序 DESC 倒序
SELECT * FROM `user` WHERE `password` LIKE '%1%' ORDER BY id DESC;
#更新,必须添加 WHERE 条件
UPDATE `user` SET realname='木瓜2' WHERE realname='木瓜';
#删除 物理删除
DELETE FROM `user` WHERE realname='木瓜2';
# 逻辑删除
UPDATE `user` SET state='0' WHERE realname='木瓜';
#插入数据
INSERT INTO blog (title,content,create_time,author) VALUES ('文','文章内容4','2020-11-04 19:49:55','一咻');
#修改文章
UPDATE blog SET title='修改文章',content='修改内容' WHERE id=5;
#根据关键字进行查询
SELECT * FROM blog WHERE 1=1 AND author='一咻' AND title LIKE '%文章%' ORDER BY create_time DESC;
#查询博客详情
SELECT * FROM blog WHERE id=1;
#删除博客
DELETE FROM blog WHERE id=5;
# 新增用户
INSERT INTO `user` (username,`password`,realname) VALUES ('一咻',123456,'天明');
四、开发博客项目之用户登录
已有用户登录通用设计经验
- 核心∶登录校验&登录信息存储
- redis是内存数据库,mysql是硬盘数据库,session要写入redis
1、cookie
(a)cookie特点
- 存储在浏览器的一段字符串(最大5kb)
- 跨域不共享(不同浏览器的cookie是不一样的,但同源窗口可共享数据)
- cookie是key:value格式
- cookie数据始终在同源的http请求中携带(在broswer和server间来回传递)
- server可以修改cookie,并返回给broswer
- broswer可通过js修改cookie(有限制)
- 淘宝的网页请求淘宝的服务器,就将淘宝的cookie带过去
(b)浏览器查看cookie
- 控制台network
- 控制台,application,cookie。控制台删除cookie
- document.cookie
(c)server端nodejs操作cookie
- node查看cookie(在app.js)
//解析 cookie
req.cookie = {};
const cookieStr = req.headers.cookie || ''; // k1=y1;k2=y2....
cookieStr.split(';').forEach(item => {
if(!item){
return;
}
const arr = item.split('=');
const key = arr[0].trim();
const val = arr[1].trim();
req.cookie[key] = val;
})
- node修改cookie(在router.js\user.js)
const getCookieExpires = () => {
const d = new Date();
// console.log(d,d.getTime()) //时间格式,时间戳
d.setTime(d.getTime() + (24*60*60*1000));
// console.log(d.toGMTString())//转化成cookie的时间格式
return d.toGMTString();
}
//操作cookie path 必须为根路径:让这个cookie在这个网站前端的所有路由全部生效 设置httpOnly防止客户端对cookie操作
res.setHeader('Set-Cookie',`username='${data.username}'; path=/; httpOnly; expires='${getCookieExpires()}'`)
2、session
(a)session介绍
- cookie缺点:会暴露username,很危险
- 如何解决:cookie存储userid,server端对应username
- 解决方案:session:即server端存储用户信息
3、redis
(a)session存在的问题
- 目前,session直接是js 变量,放在nodejs进程内存中
- 进程内存有限,多个用户登录,访问量过大,内存暴增怎么办?
- 正式线上运行是多进程,进程之间内存无法共享,仍需重新登录
(b)解决方案:redis(内存数据库 )
- 是web server最常用的缓存数据库,数据存放在内存中(问题2)
- 相比于mysql ,访问速度快(内存和硬盘不是一个数量级的)
- 可扩展内存大小(问题1)
(c)为何session适合用redis ?
- session访问频繁,对性能要求极高(数据存取快)
- session 可不考虑断电丢失数据的问题(内存的硬伤)
- session数据量不会太大(相比于mysql中存储的数据)
(d)为何网站数据不适合用redis ?
- 操作频率不是太高(相比于session操作)
- 断电不能丢失,必须保留
- 数据量太大,内存成本太高
(e)redis安装
- redis-server.exe redis.windows.conf ----- 服务启动方式
- redis-cli ----- redis客户端(client)进行数据存取
-
- set myname zhangsan ----- 存
-
- get myname ----- 取
-
- keys * 查看所有的key
-
- del myname 删除myname的数据
-
- flushdb 清空当前数据库中的所有 key
-
- flushall 清空整个 Redis 服务器的数据(删除所有数据库的所有 key )
和前端联调
登录功能依赖cookie ,必须用浏览器来联调
cookie跨域不共享的,前端和server端必须同域
需要用到nignx做代理,让前后端端同域
正向代理:如,校园内网,我在家,通过正向代理的方式访问校园内网(客户端能控制得代理)
反向代理:(客户端控制不了,对于客户端是一个黑盒)
nginx命令
测试配置文件格式是否正确nginx -t
启动nginx ;重启nginx -s reload
停止nginx -s stop,
五、安全
-
sql注入∶窃取数据库内容
XSS攻击:窃取前端的cookie内容
密码加密∶保障用户信息安全(重要!) -
server端攻击方式非常多,预防手段也非常多
本课只讲解常见的、能迪wee
有些攻击需要硬件和服务来支持(需要OP支持),如DDOS -
最原始、最简单的攻击,从有了web2.0就有了sql注入攻击
攻击方式:输入一个sql片段,最终拼接成一段攻击代码
预防措施:使用mysql的escape i函数处理输入内容即可
1、sql注入
改sql语句,使得有些语句部分注释
module.exports = {
exec,
escape:mysql.escape
};
const login = (username,password) => {
// 防止sql注入,后果非常严重
// select username, realname from users where username='zhangsan' -- ' and password='123456'
// select username, realname from users where username='zhangsan';delete from users;-- ' and password='123456'
username = escape(username);
password = escape(password);
// const sql = `select username, realname from users where username='${username}' and password='${password}'`;
const sql = `select username, realname from users where username=${username} and password=${password}`;
// console.log('sql is',sql);
return exec(sql).then(rows => {
// console.log(rows);
return rows[0] || {};
});
}
2、xss攻击
- 攻击方式:在页面展示内容中掺杂js 代码,以获取网页信息
- 预防措施∶转换生成js 的特殊字符
如:创建博客标题为
<script>alert(document.cookie)</script>
提交后,会在界面显示一个警告提示框,然后,发现新建的博客标题为空
- 安装
npm i xss --save --registry=https://registry.npm.taobao.org
//防止xss攻击,将输入的内容的特殊字符转义
// const title = blogData.title;
const title = xss(blogData.title);
const content = xss(blogData.content);
const createtime = Date.now();
const author = blogData.author;
const sql = `insert into blogs (title,content,createtime,author) values ('${title}','${content}','${createtime}','${author}')`
3、密码加密
- 万一数据库被用户攻破,最不应该泄漏的就是用户信息
- 攻击方式:获取用户名和密码,再去尝试登录其他系统
- 预防措施:将密码加密,即便拿到密码也不知道明文
src/utils/cryp.js
const crypto = require('crypto');
//密匙
const SECRET_KEY = 'Hzy_Hjy611'
//md5 加密
function md5(content){
let md5 = crypto.createHash('md5');
return md5.update(content).digest('hex')
}
//加密函数
function genPassword(password){
const str = `password=${password}&key=${SECRET_KEY}`
return md5(str);
}
module.exports = {genPassword};
// const result = genPassword('1234');
// console.log(result);
//密码加密后放入数据库中
password = genPassword(password);