前情提要
- 本篇博客整理内容都是本人在学习node过程中所遇到重要知识点,作用如下:
- 为初学者整理出学习node过程中会遇到的知识点,有针对性的快速定位知识点
- 方便有基础者快速查找重点知识,回忆不清晰的知识点
开发环境
Node 6.9.1
Express 4.15.3
Mongoose 4.10.5
Window 10
ubuntu 16.04
(服务器环境)
要点整理
全局安装npm包目录(如:cnpm install xxx -g)
- C:\Users\Think\AppData\Roaming\npm\node_modules(本人电脑)
Node 版本切换
- 安装过n和nvmw,出现过各种各样的问题,都没有实现正常切换node版本的目的
- 最后在淘宝镜像下载多个node版本(下载压缩包,这样可以共存多个版本),手动的改变环境变量,来达到切换node版本的目的
- 阿里云购买的服务器(linux版本:ubuntu 16.04)上安装nvm成功
telnet 命令必须在cmd中才能生效
Win10正式版telnet不是内部或外部命令怎么办 [1]
Content-Length 设置
var http = require('http'); var server = http.createServer(function(req, res) { var body = '你好'; res.writeHead(200, { 'Content-Type': 'text/plain;charset=utf8', 'Content-Length': Buffer.byteLength(body),// 这个值可以<=Buffer.byteLength(body) }) res.end(body); }) server.listen(3000, function() { console.log('server...') })
curl 命令
- 使用 git bash 可以直接运行 curl 命令
curl -X DELETE 127.0.0.1:3000
表示发送一个删除请求
表单提交模拟 REST 架构
> [REST架构中的PUT、DELETE请求如何实现?](https://segmentfault.com/q/1010000000123615)
> [AJAX如何实现PUT和DELETE方法](https://segmentfault.com/q/1010000002581227)
> [method-override(改写前端form表单的请求)](https://github.com/expressjs/method-override)
Buffer.from() 在 node6.x 中才能使用
Getting TypeError: this is not a typed array using Buffer.from in mocha
参数解析
req.body
- 表单提交的数据
服务端
... var bodyParser = require('body-parser') app.use(bodyParser.json()) ... app.all('/list', function(req, res, next) { console.log(req.body) })
客户端(下例使用FormData提交数据,或者你可以使用原生表单submit提交,不推荐)
var formData = new FormData(); formData.append('id', 1); $.ajax({ type: 'post', url: '/list', data: formData, contentType: false, processData: false, })
req.query
- url中?后面带的参数
服务端
app.all('/list', function(req, res, next) { console.log(req.query) })
客户端
$.ajax({ url: '/list?id=1', })
req.params
- rest风格时,url中路径参数
服务端
app.get('/list/:id', function(req, res, next) { console.log(req.params) })
客户端
$.ajax({ url: '/list/1', })
req.files
- 表单提交的数据()
服务端
... var multer = require('multer') var upload = multer({dest: './uploads'}) ... app.all('/add', upload.fields([ {name: 'file'}, ]), function(req, res, next) { console.log(req.files) })
客户端(下例使用FormData提交数据,或者你可以使用原生表单submit提交,不推荐)
var formData = new FormData(); formData.append('file', document.querySelector('[type=file]').files[0]); $.ajax({ type: 'post', url: '/add', data: formData, contentType: false, processData: false, })
解析url参数 querystring.parse(url.parse(req.url).query)
process.env.NODE_ENV
- cmd 中
SET NODE_ENV=prod
- git bash 中
- 直接运行
NODE_ENV=prod node app
- 或者先运行
export NODE_ENV=prod
,再运行node app
- 直接运行
connect 中间件
- 常规处理中间件函数有3个参数:req、res、next,错误处理中间件函数必须接收4个参数:err、req、res、next
- 调用next()的时候,如果向里面传递参数,如:
next(new Error('not found'))
,那么它接下来会自动寻找错误处理中间件 - 以下中间件都独立于connect本身
- cnpm install cookie-parser –save
- cnpm install body-parser –save
- connect.limit中间件已经整合到body-parser中
res.setHeader('Set-Cookie', ['a=1', 'b=2'])
响应头设置多个 Set-Cookie
cookie 的设置和获取
var express = require('express')
var cookieParser = require('cookie-parser')
var moment = require('moment')
var app = express()
app.use(cookieParser('sign'))// 设置签名标记
app.all('/', function(req, res) {
res.cookie('name', 'hvb', {
expires: new Date(moment().add(30, 's')),
httpOnly: true,// 仅服务端可访问
})
res.cookie('sex', 'male', {signed: true})// 设置签名的cookie(签名≠加密,为了检验客户端cookie是否伪造,以上不设置签名标记,这里就会报错)
res.cookie('obj', {a: 1, b: 2})// cookie内容为对象
res.send({message: '/'})
})
app.all('/get', function(req, res) {
console.log(req.cookies)// 获取所有普通cookie
console.log(req.signedCookies)// 获取所有签名cookie
res.send({message: '/get'})
})
app.all('/del', function(req, res) {
res.clearCookie('name')// 删除cookie,对httpOnly为true的cookie也会生效
res.send({message: '/get'})
})
...
加密、摘要和签名的区别
express中cookie的使用和cookie-parser的解读
cookie 和 session
npm模块-bcryptjs(密码加密)
- 建议安装密码加密模块时,不要选用bcrypt模块(安装依赖问题比较多),使用bcryptjs模块即可(API跟bcrypt是一样的,而且不会出现问题)
使用connect-mongo将session存储到数据库
- 一定要注意如果有2个接口都需要写入session到数据库,那么这2个接口在前端千万不能同时请求,否则,后面的会完全覆盖前面的session,如下图:
- app.js
- 数据库数据:
- app.js
var express = require('express')
var mongoose = require('mongoose')
var session = require('express-session')
var MongoStore = require('connect-mongo')(session)
mongoose.connect('127.0.0.1:27017/foobar')
var app = express()
app.use(session({
secret: 'sessiontest',
resave: true,
saveUninitialized:true,
store: new MongoStore({
mongooseConnection: mongoose.connection,
ttl: 10,// 表示10秒钟后该session过期,如果不设置session过期时间默认为2周(如果关闭了浏览器,那么该session会立即失效,也就说打开浏览器还需要再次登录,但是数据库的相应字段不会被立即删除,如果想实现自动登录,那么需要借助cookie来实现)
})
}))
// 存储session
app.get('/', function(req, res) {
// 这里存储session(具体值)到数据库,可以打开数据库查看(客户端是以cookie的方式来存储session(唯一id),字段为connect.sid)
req.session.user = {
a: 1,
b: 2,
}
res.send({message: '/'})
})
// 查看session
app.get('/a', function(req, res) {
console.log(req.session)// 将在这里输出之前存储的session,或者可以打开数据库查看
res.send({message: '/a'})
})
...
Session会在浏览器关闭后消失吗?
nodesj中 中间件express-session的理解
connect-mongo和mongoose的区别联系
connect-mongo
自动登录
npm模块-serve-favicon(设置网站图标)
var express = require('express')
var favicon = require('serve-favicon')
var path = require('path')
var app = express()
app.use(favicon(path.join(__dirname, 'public', 'dog.png')))// 后缀名可以不是ico且设置之后要重启浏览器才能生效
...
npm模块-morgan(请求日志)
var express = require('express')
var moment = require('moment')
var morgan = require('morgan')
var uuid = require('uuid')// node-uuid已合并为uuid
var path = require('path')
var fs = require('fs')
var app = express()
// 添加唯一标识
morgan.token('id', function(req) {
return req.id
})
function assignId (req, res, next) {
req.id = uuid.v4()
next()
}
// 自定义时间格式
morgan.token('date', function(req) {
return req.date
})
function assignDate (req, res, next) {
req.date = moment().format('YYYY-MM-DD HH:mm:ss')// 2017-06-16 17:43:24
next()
}
// 创建可写流
var testLogStream = fs.createWriteStream(path.join(__dirname, 'test.log'))
app.use(assignId)
app.use(assignDate)
app.use(morgan(':id :method :url :response-time ms :date\\r', {stream: testLogStream}))// 以自定义的格式写入到test.log,结尾换行
...
npm模块-compression(gzip)
- 图片使用gzip后体积反而会变大,所以compression没有对图片进行压缩
npm模块-csurf(防止csrf攻击)
server.js
var express = require('express') var cookieParser = require('cookie-parser') var bodyParser = require('body-parser') var csrf = require('csurf') var app = express() app.set('views', './views') app.set('view engine', 'ejs') app.use(express.static('./public')) app.use(bodyParser.urlencoded({ extended: false })) app.use(cookieParser()) app.use(csrf({ cookie: true, //ignoreMethods: ['GET', 'HEAD', 'OPTIONS'],// 以上3种方法默认不会进行安全验证,所以注意不要把更新、删除操作使用get请求接收 })) // 自定义错误信息 app.use(function (err, req, res, next) { if (err.code !== 'EBADCSRFTOKEN') return next(err) res.status(403) res.send({message: '非法请求'}) }) // 渲染模板 app.all('/', function (req, res) { res.render('nav', {csrfToken: req.csrfToken()}) }) // 默认不对get请求进行验证,所以即使客户端传来错误的_csrf,也会成功返回 app.get('/a', function (req, res) { res.send({message: '/a'}) }) // 会进行验证 app.post('/b', function (req, res) { res.send({message: '/b'}) }) ...
test.ejs
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test</title> <script src="js/jquery-1.11.3.min.js"></script> </head> <body> <form> <input type="hidden" name="_csrf" value="<%= csrfToken %>2321312312"> <input type="text" name="username"> </form> <button class="get">get请求</button> <button class="post">post请求</button> </body> <script> $(function() { // 发送get请求 $('.get').on('click', function() { $.ajax({ url: '/a', data: $("form").serialize(), }) }) // 发送post请求 $('.post').on('click', function() { $.ajax({ url: '/b', data: $("form").serialize(), type: 'post', }) }) }) </script> </html>
浅谈CSRF攻击方式
npm模块csurf(防止csrf攻击)
为什么http用的时候不能用POST方式替代全部的GET方式?
npm模块-serve-index(生成静态资源目录)
var express = require('express')
var serveIndex = require('serve-index')
var app = express()
app.use('/dir', express.static('./public'))// 托管静态资源
app.use('/dir', serveIndex('./public', {'icons': true}))// 生成静态资源目录
app.all('/', function (req, res) {
res.send({message: '/'})
})
app.listen(3000, function() {
console.log('server...')
})
客户端访问http://127.0.0.1:3000/dir即可看到效果
npm模块-connect-flash(携带一些信息给重定向后的页面)
var express = require('express')
var flash = require('connect-flash')
var app = express()
app.use(flash())
app.get('/', function(req, res) {
res.send({message: req.flash()})// 访问'/a'时会重定向到'/',且页面显示出从'/a'带来的信息,该信息只显示一次,再次刷新'/'页面,就不会有了
})
app.get('/a', function(req, res) {
req.flash('name', 'hvb')// 携带信息name
req.flash('age', 1, 2, 3)// 携带信息age
res.redirect('/')// 重定向到'/'
})
...
客户端访问http://127.0.0.1:3000/a即可看到效果
npm模块-fs-extra(对fs模块的扩展)
- 可以实现递归删除文件夹、递归创建文件夹
- 还支持promise方式调用
示例
var fs = require('fs-extra') fs.ensureDir('./a/b/c') .then(function() { console.log('done') }) .catch(function(e) { console.log(e.message) })
npm模块-validator(各种验证)
var validator = require('validator')
console.log(validator.equals('vcxiaohan', 'vcxiaohan'))// 验证2个字符串是否相等(如果都传空,也返回true)
console.log(validator.isAlphanumeric('vcxiaohan'))// 验证是否只包含字母和数字
console.log(validator.isByteLength('vcxiaohan', {min: 4, max: 12}))// 验证字符串的长度是否在4-12之间
console.log(validator.isURL('http://v35new.faqrobot.org/web/common/index.html#material/menuList'))// 验证网址
console.log(validator.isMobilePhone('+8618752011111', 'zh-CN'))// 验证电话(不能验证固定电话)
console.log(validator.isEmail('vcxiaohan@foxmail.com'))// 验证邮箱
npm模块-@fnando/password_strength(验证密码强度)
var PasswordStrength = require('@fnando/password_strength')
var strength = PasswordStrength.test('vcxiaohan', '10hvb29@')// 分别传入用户名和密码,会检测用户名和密码是否相同,若相同,那么强度很低
console.log(strength.isWeak())// false
console.log(strength.isGood())// false
console.log(strength.isStrong())// true
console.log(strength.isValid('good'))// true(我们使用这个方法就可以验证密码强度是否满足需要了)
console.log(strength.isGood() == strength.isValid('good'))// false(注意区别)
验证重复用户名
- 如果要改变索引,最好删除数据库且重启服务(因为索引已经建立好了),否则有可能出现改变索引不生效的情况
...
// 定义 Schema
var userSchema = new Schema({
name: {
type: String,
unique: true,// 建立唯一索引
sparse: true,// 建立稀疏索引(允许值为null的字段同时存在)
},
})
var User = mongoose.model('User', userSchema)
var app = express()
app.get('/addUser', function(req, res) {
Blog.create({name: '1234'}).then(function(data) {
res.send({message: '创建用户成功'})
}).catch(function(e) {
console.log(e.message)
if(e.message.indexOf('E11000 duplicate key')+1) {// 当设为唯一索引的字段有重复时,会报出此错误
res.send({message: '用户名重复'})
}else {
res.send({message: '其他错误'})
}
})
})
...
打印对象到控制台
var foo = function() {
console.log(1)
}
console.log(foo)// 打印出[Function],很不明显
console.log(foo.toString())// 详细的打印出函数内容
Express 4.x
Express 4.x API 英文手册
Express 4.x API 中文手册
Express4.X中的bin/www是作什么用的?为什么没有后缀?
Express Migrating from 3.x to 4.x
nodejs取参四种方法req.body,req.params,req.param,req.body
What is the difference between res.end() and res.send()?
前端ajax,后端res.redirect重定向
body-parser(请求参数解析)
bodyParser中间件的研究
问一个bodyParser获取不到对象参数的问题
bodyParser.urlencoded({extended: true})
模板引擎
文件上传 [#f]
文件下载
- 前端千万不要使用ajax请求,否则不会激活下载功能
- 前端调用下载接口
<img src="/download/img">
这样服务端返回的图片可以直接显示在页面中<a href="/download/file" target="_blank">点击下载</a>
这样用户点击链接后,会直接下载此文件
Content-disposition中Attachment和inline的区别
File not downloading express.js res.download
nodejs+express4.X的文件下载
res.download() not working in my case
格式化时间
- moment().add()(当前时间向后推迟,可用来设置cookie过期时间)
Moment.js
https
Promise
supervisor(保存文件后自动重启服务)
- 该模块主要用在开发环境中,只要你对项目进行更改,再保存后,该模块就会自动重启你的项目,省去了手动重启服务的麻烦
cnpm -g install supervisor
安装supervisor到全局supervisor app
启动app.jssupervisor -i public app
启动app.js,且忽略public文件夹的监视
运维与部署
mongod操作
- 本人自定义的一种结构(仅供参考)
配置文件(适用于linux,根据你的需要配置数据库路径、错误日志路径、开启的端口号)
# mongod.conf # for documentation of all options, see: # http://docs.mongodb.org/manual/reference/configuration-options/ # Where and how to store data. storage: dbPath: /home/vc/data/blog/lib journal: enabled: true # where to write logging data. systemLog: destination: file logAppend: true path: /home/vc/data/blog/log/mongod.log # network interfaces net: port: 30001 bindIp: 127.0.0.1
配置文件(适用于window,根据你的需要配置数据库路径、错误日志路径、开启的端口号)
# mongod.conf #数据库路径 dbpath=E:\data\blog\lib #日志输出文件路径 logpath=E:\data\blog\log\mongod.log #错误日志采用追加模式 logappend=true #启用日志文件,默认启用 journal=true #这个选项可以过滤掉一些无用的日志信息,若需要调试使用请设置为false quiet=true #端口号 默认为27017 port=30002
sudo mongod -f /home/vc/data/blog/mongod.conf &
根据配置文件启动(结尾加的&表示在后台运行此服务,这样关闭窗口,服务依然运行)
sudo netstat -lanp | grep 30001
查看mongod在linux上的30001端口是否启动成功,如下表示已经启动成功
sudo kill 15192
杀死mongod进程(以便于我们重新启动)- 如果启动了多个mongod服务,而我们只需要杀死其中一个,为了防止杀错进程,我们可以去相应的mongod.log里面查看当前的服务对应的pid,再确定是否是需要杀死的进程
- 本人自定义的一种结构(仅供参考)
- pm2操作
pm2 start app.js
千万不能简写成pm2 start app
,2个是不一样的pm2 deploy ecosystem.json production setup
初始化项目,如果不setup,下一步会报错pm2 deploy ecosystem.json production
启动遇到错误:bash: pm2: command not found
,则需要使用cnpm install -g pm2
命令在git服务器上安装pm2
- nginx操作
- 把下载的nginx传到服务器,进入到configure所在目录,执行
./configure --prefix=/home/vc/nginx --with-http_ssl_module
- –prefix=/home/vc/nginx为指定编译的目录为/home/vc/nginx
- –with-http_ssl_module为使用ssl模块,用来提供https服务
/home/vc/nginx/conf/nginx.conf
nginx配置文件位置make && make install
安装nginxsudo /home/vc/nginx/sbin/nginx -t
检测配置文件是否正确sudo /home/vc/nginx/sbin/nginx
启动sudo /home/vc/nginx/sbin/nginx -s stop
停止sudo /home/vc/nginx/sbin/nginx -s reload
重启- 如遇到
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
或nginx: [error] open() "/home/vc/nginx/logs/nginx.pid" failed (2: No such file or directory)
错误,说明你之前安装的nginx,在没有正常停止nginx服务的情况下,又重新安装了一次
sudo fuser -n tcp 80
查看nginx所占用的进程,比如:80/tcp: 7811 7812
- 则使用
sudo kill 7811 7812
杀死这2个进程,重新启动nginx即可解决以上问题
- 把下载的nginx传到服务器,进入到configure所在目录,执行
How can I make nginx support @font-face formats and allow access-control-allow-origin?
nginx 出现413 Request Entity Too Large问题的解决方法
PM2 介绍
Win10怎么修改hosts文件 Win10系统hosts修改不了
NodeJS on Nginx: 使用nginx反向代理处理静态页面
windows下nginx安装、配置与使用
ubuntu16.04 nginx安装
Ubuntu16.04安装mongodb
ssh免密码登录
- 要特别注意.ssh父目录的权限问题,如:
/home/vc/.ssh
,则vc文件夹权限应该是755
解决linux中ssh登录Warning:Permanently added (RSA) to the list of known hosts
SSH localhost免密码后依然需要输入密码问题的解决
在oschina上添加SSH公钥
git管理
git init --bare sample.git
git创建一个裸仓库,裸仓库没有工作区- 请一定注意新建的仓库的权限以及所属者,防止本地提交时报错,还不知道是权限不足造成的
- 先要 获取 git仓库所有分支,才能继续其他操作,否则会报错
- 有时间我会写一篇搭建git服务及远程连接git服务的文章
- 使用putty连接git服务
- 在执行
git clone git@116.62.xxx.xx:/home/git/project/sample.git
之前,必须要先验证ssh是否接通:ssh git@116.62.xxx.xx
- 在执行
- 使用sourcetree连接git服务(推荐,可视化界面)
- 需要注意选用OpenSSH,可以使用任何一对公、私钥进行验证(本地电脑的或者任何一个服务器用户的)
- 将公钥传到git服务器的
/home/git/.ssh/authorized_keys
中 - 在sourcetree上选择相对应的私钥
- 远程地址为
git@116.62.xxx.xx:/home/git/project/sample.git
爬虫
SuperAgent中文使用文档
用node爬数据遇到的charset编码转换问题的解决方案
ajax返回数据成功 却进入error方法
常见的反爬虫和应对方法
发送邮件验证
- 请注意一定要在本地或服务器上使用Node.js v6+,之前我用pm2在服务器部署nodemailer项目不成功,就是node版本太低了
图片处理(图片水印、图片验证码、头像裁剪)
视频处理
- 转换视频格式
- 制作视频缩略图
- 多视频合成一个
监听任务进度
var ffmpeg = require('fluent-ffmpeg') ffmpeg(...) .on('progress', function(progress) { console.log(progress.percent) })
fluent-ffmpeg/node-fluent-ffmpeg
FFmpeg安装(windows环境)
Merge Multiple Videos using node fluent ffmpeg
定时任务
'* * * * * *'
每秒钟执行一次'*/30 * * * * *'
每隔30秒执行一次'0 */5 * * * *'
和'*/5 * * * *'
秒数可以省略,所以以上都是每隔5分钟执行一次'30 */5 * * * *'
每隔5分钟30秒执行一次'0 */5 * * *'
每隔5小时执行一次'* 14 * * *'
每天的14点开始,每隔1分钟执行一次,直到15点为止- 以此类推…
权限控制
全文检索
jsonp(解决跨域问题)
- 跨域环境:本地协议打开html文件,则会使用
file://
调用http://127.0.0.1:3002
的接口,可构成跨域调用 index.html(在本地打开)
... <script> $(function() { $.ajax({ url: 'http://127.0.0.1:3002/jsonp', dataType: 'jsonp',// 这句不写的时候,相当于同域名发送xhr,写的时候,相当于不同域名发送jsonp }) .then(function(data) { console.log(data) }) }) </script> ...
app.js
app.all('/jsonp', (req, res, next) => { res.jsonp({message: 'jsonp...'})// 既可以响应xhr请求,又可以响应jsonp请求 })
直播
- 选用七牛直播云服务,按照控制台快速入门配置好直播服务,获取推流地址和播放地址
- 使用OBS Studio配置好推流地址,进行推流
- 使用Video.js配置好播放地址,进行播放
PC端播放rtmp和hls视频流
videojs播放不了提示 (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED) No compatible source was found for this video.
日志记录+配置环境
(new Image()).src = 'http://xxx/xxx'
通过这种方式可以很方便的调用一个接口来实现错误日志的记录- pm2自带错误日志,运行
pm2 logs blog
可以展示blog项目的错误日志,该日志包括编写错误bug等,很方便的可以快速定位到项目运行失败的问题 - 开发阶段:
node app
直接运行app.js,运行后因为没有配置环境,所以默认是development - 生产阶段:
pm2 deploy ecosystem.json production
使用pm2部署服务,部署成功后,环境是production - 在不同的环境下加载不同的日志配置
- 配置文件目录
default.js
module.exports = { logger: require('tracer').colorConsole(),// 日志记录 }
production.js
module.exports = { logger: require('tracer').dailyfile({root:'./logs', allLogsFileName: 'all'}),// 日志记录 }
- 效果:开发环境日志会打印到命令面板上,生产环境日志会写入文件中
- 进阶:可以添加一个日志中间件,这样每次请求信息都可以被记录下来
单元测试
- 疑问:为什么需要单元测试?平时写项目,开发环境跑起来,各种流程走一遍,保证不出bug不就行了吗?
- 写成测试用例可以很方便的每次运行,否则比较麻烦
Node.js 单元测试:我要写测试
测试框架 Mocha 实例教程
代码覆盖率工具 Istanbul 入门教程