前情提要
- 本篇博客整理內容都是本人在學習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('[email protected]'))// 驗證郵箱
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 [email protected]:/home/git/project/sample.git
之前,必須要先驗證ssh是否接通:ssh [email protected]
- 在執行
- 使用sourcetree連接git服務(推薦,可視化界面)
- 需要注意選用OpenSSH,可以使用任何一對公、私鑰進行驗證(本地電腦的或者任何一個服務器用戶的)
- 將公鑰傳到git服務器的
/home/git/.ssh/authorized_keys
中 - 在sourcetree上選擇相對應的私鑰
- 遠程地址爲
[email protected]:/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 入門教程