國慶七天樂,Node來敲門

前言

一轉眼九月又過去了,最近沒怎麼寫博客是由於事情太多了,感受心一直在路上,歷來沒有時間停下來棲息。從畢業到如今,剛入職便被大量的業務需求所圍繞。看到排期已經排到明年的時候我陷入了沉思,曾幻想着利用工做之餘的時間作一些本身喜歡作的事。慢慢的發現弱小的身體根本支撐不住。很早買的《深刻淺出node.js》翻看的次數也寥寥無幾,每當想到這些臉上總會帶着一絲惆悵。趁着國慶,因而便有機會能夠看看node方面的書籍。紙上得來終覺淺,瞭解大概的node基礎知識後蠢蠢欲動,最終咬牙切齒的拿出了我一禮拜的飯錢買了雙越老師的:前端晉升全棧工程師必備課程 Node.js 從零開發web server博客項目。接下來的一週餓的時候我都會來看看視頻,讓知識填充個人肚子。課程講解的很周到,值得推薦。可是爲了避免讓你們跟我同樣沒錢吃飯。因此我就詳細的一步步帶你們過一遍,相信你會跟我同樣收穫滿滿。若是你是高富帥、白富美的話買買體驗感會更棒。html

本篇你能學到什麼

  • 接口,包括node.js處理http請求、搭建開發環境、處理路由、開發各個接口
  • 數據存儲,MySql建庫建表、Node連接MySql、接口對接Mysql
  • 登陸,cookie和session、使用Redis存儲Session
  • Ngnix配置,先後端聯調
  • 日誌,Node.js文件操做、stream 流、日誌功能的開發、日誌文件拆分、日誌分析
  • 安全,預防SQL、XSS攻擊
  • Express框架,中間件實現原理、開發api接口、結合經常使用插件
  • koa2框架,
  • 線上部署,PM2 介紹和配置、PM2 多進程模型
    需求分析

1、博客項目之接口

要開發一個博客項目的 server 端,首先要實現技術方案設計中的各個 API。本章主要講解如何使用原生 nodejs 處理的 http 請求,包括路由分析和數據返回,而後代碼演示各個API的開發。可是本章還沒有鏈接數據庫,所以 API 返回的都是假數據。前端

1. 工具準備

  • 使用nodemon 檢測文件變化,自動重啓node
  • 使用cross-env 設置環境變量,兼容mac linux 和window

npm install nodemon cress-env -d --savenode

新建一個項目名node-blog的文件,用npm init -y 初始化項目。並在package.json配置script,使用 npm run dev 啓動咱們的項目mysql

"dev": "cross-env NODE_ENV=dev nodemon ./bin/api.js",
"prd": "cross-env NODE_ENV=production nodemon ./bin/api.js"
複製代碼

2. 模塊化--目錄介紹

.node-blog
├── bin // 項目啓動文件
├── node_modules
├── src
|   ├── conf // 數據庫配置
|   └── controller // 接口api
|   └── db // 數據庫連接
|   └── model // 輸出格式
|   └──router // 路由
├── app.js
├── package.json

複製代碼

3. 項目開發

👉在bin下新建一個api.js做爲node啓動一個服務的模塊linux

const http = require('http')
const serverHandle = require('../app')

const PORT = 8000

const server = http.createServer(serverHandle)

server.listen(PORT)
複製代碼

👉app.js中建立咱們的配置服務配置:git

const serverHandle = (req, res) => {
    // 設置返回格式 JSON
    res.setHeader('Content-type', 'application/json')
}
module.exports = serverHandle
複製代碼

👉 在router中建立blog.js、user.js文件,其中blog.js做爲博客的路由,這裏以獲取博客列表的接口爲例,其餘的接口只是換了一個名字而已:github

const handleBlogRouter = (req, res) => {
  const method = req.method // GET POST
  const url = req.url
  const path = url.split('?')[0]
  // 獲取博客列表
  if (method === 'GET' && req.path === '/api/blog/list') {
    return {
        msg: '這是獲取博客列表的接口'
    }
  }
}

module.exports = handleBlogRouter
複製代碼

👉 在app.js中引用建立的路由web

const handleBlogRouter = require('./src/router/blog')
const handelUserRouter = require('./src/router/user')
const serverHandle = (req, res) => {
    // 設置返回格式 JSON
    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
複製代碼

這裏啓動咱們的服務,輸入對應的接口api,就能拿到咱們返回的假數據了。關於詳細的接口開發。在controller中新建blog.js、user.js處理,這裏就不展開。詳細可查看接口開發redis

2、博客項目之數據存儲

API 實現了,就須要鏈接數據庫,實現真正的數據存儲和查詢,再也不使用假數據。本章主要講解 mysql 的安裝、使用,以及用 nodejs 鏈接 mysql ,最後將 mysql 應用到各個已經開發完的 API 中。sql

爲了下降本幅的篇長,這裏將省略如何安裝mysql,其實步驟很簡單,也不是本文的主要講解點,這裏找了一個還不錯的安裝教程,能夠供你們參考:mysql安裝教程,另外不習慣操做控制檯的能夠自行下個圖形化界面。我用的是MySql Workbench。

1. 建立數據庫和數據表

  • 利用MySql Workbench建立myBlog數據庫,在此數據庫下建立blogs、users兩張數據表,分別存儲博客和用戶登陸數據
  • blogs表建立以自增id做爲主鍵,title、contnet、createtime、author共5個字段;users表建立也以自增id做爲主鍵,username、password、realname共四個字段;下圖爲blogs和users數據表各字段的結構:
    blogs表
    users表

2. node鏈接數據庫

  • 在項目中安裝mysql。> npm install mysql
  • 在db目錄下新建一個mysql.js。用於鏈接mysql
const mysql = require('mysql')

// 建立連接對象
const con = mysql.createConnection(
  {
    host: 'localhost',
    user: 'root',
    password: '123456',
    port: '3306',
    database: 'myblog'
  }
)

// 開始連接
con.connect()

// 執行sql語句的函數
function exec(sql) {
  const promise = new Promise((resolve, reject) => {
    con.query(sql, (err, result) => {
      if (err) {
        reject(err)
        return
      }
      resolve(result)
    })  
  })
  return promise
}

module.exports = {
  exec
}
複製代碼

3. 接口對接Mysql

在上章中講到的假數據替換成數據庫中的真實數據。在controller目錄下的blog、user引入剛建立的mysql.js。在各接口完成mysql語句完成接口對接mysql。全部的接口都是同樣的處理,只是執行的sql語句不同,詳細可查看各接口對接Mysql,這裏以獲取數據列表的接口爲例:

const getList = (author, keyworld) => {
  let sql = `select * from blogs where 1=1 `
  if (author) {
    sql += `and author = '${author}' `
  }
  if (keyworld) {
    sql += `and title like '%${keyworld}%' `
  }
  sql += `order by createtime desc;` 
  return exec(sql)
}
複製代碼

3、博客項目之登陸

1. cookie作限制

設置用戶名的cookie, 其中getCookieExpires爲cookie的過時時間, path=/ 設置全部路由,httpOnly不容許前端更改cookie。

res.setHeader('Set-Cookie', username=${username}; path=/; httpOnly; expires=${getCookieExpires()})

步驟:訪問login,將用戶名密碼傳過去,驗證登陸,登陸以後將用戶信息寫入到cookie返回前端。經過cookie測試判斷有無登陸。

// 解析cookie 
  req.cookie = {}
  const cookiestr = req.headers.cookie || ''
  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
  })
複製代碼

4、博客項目之日誌

日誌記錄和日誌分析是 server 端的重要模塊,前端涉及較少。本章主要講解如何使用原生 nodejs 實現日誌記錄、日誌內容分析和日誌文件拆分。其中包括 stream readline 和 crontab 等核心知識點。

1. stream介紹和使用

什麼是stream? 👉官方解釋

流(stream)是 Node.js 中處理流式數據的抽象接口。 stream 模塊用於構建實現了流接口的對象。流能夠是可讀的(Readable)、可寫的(Writable)、或者可讀可寫的(Duplex)。抽象理解爲兩個水桶經過水管連接,將其中的一個水桶的水滿滿流入到另外一個水桶。

stream能作什麼? 👉 IO(網絡IO和文件IO)操做的性能瓶頸,如何在有限的硬件資源下提升IO的操做效率。

stream(流) 拷貝代碼演示 :

const fs = require('fs')
const path= require('path')
// 兩個文件名
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
// 讀取文件的 stream 對象
const readStream = fs.createReadStream(fileName1)
// 寫入文件的 stream 對象
const writeStream = fs.createWriteStream(fileName2)
// 執行拷貝,經過pipe
readStream.pipe(writeStream)
// 監聽每次拷貝的內容
readStream.on('data', chunk => {
  console.log(chunk.toString())
})
// 數據讀取完成,即拷貝完成
readStream.on('end', () => {
  console.log('copy done')
})
複製代碼

2.寫日誌

在blog-node的目錄下新建一個logs文件夾,在其下面新建access.log、error.log、event.log。並在src下新建一個utils > log.js 這裏就以access爲例子,其代碼爲:

const fs = require('fs')
const path = require('path')

// 寫日誌
function writeLog (writeStream, log) {
  writeStream.write(log + '\n')
}

// 生成 write stream
function createWriteStream (fileName) {
  const fullFileName = path.join(__dirname, '../', '../', 'logs', fileName)
  const writeStream = fs.createWriteStream(fullFileName, {
    flags: 'a'
  })
  return writeStream
}

// 寫訪問日誌
const accessWriteStream = createWriteStream('access.log')
function access (log) {
  writeLog(accessWriteStream, log)
}

module.exports = {
  access
}
複製代碼

在app.js中引入剛寫的log.js文件中access方法並在serverHandle方法中記錄access log。當咱們的接口被執行的時候就會記錄接口的信息等。

access(${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()})

3.日誌拆分

  • 按時間劃分日誌文件,如2019-09-18.access.log
  • 實現方式:linux的crontab命令,即定時任務

在src新建一個utils > copy.sh 寫入sh命令。執行sh copy.sh,在上小節建立的logs會多出一個文件名爲2019-09-17.access.log 文件名即完成日誌拆分。下面是sh命令:

!/bin/sh
cd /Users/wusimin7/Documents/jd_code/node-blog/blog-node/logs
cp access.log $(date +%Y-%m-%d).access.log
echo "" > access.log

複製代碼

在咱們的總項目下執行crontab -e 建立定時任務。輸入如下內容。wq!保存後經過crontab -l 查看剛建立的crontab命令。

*0 * * * sh /Users/wusimin7/Documents/jd_code/node-blog/blog-node/src/utils/copy.sh

5、博客項目之安全

安全是 server 端須要考慮的重點內容,本章主要講解 nodejs 如何防範 sql 注入,xss 攻擊,以及數據庫的密碼加密 —— 以防被黑客獲取明文密碼。

1. sql注入

  • 最原始、最簡單的攻擊
  • 攻擊方法:輸入一個sql片斷,最終拼接成一段攻擊代碼
  • 預防錯誤:使用mysql的 escape 函數處理輸入內容(從server端考慮)

攻擊方法演示: 👉 在咱們的sql中輸入

select username, realname from users where username='zhangsan'-- and password="123";

這個sql語句中能查出用戶名"zhangsan",密碼就不會顯示,被 -- 所註釋了。利用這個就能夠進行sql注入攻擊了

sql注入攻擊
你會發現只輸入用戶名也能登入。這就是簡單的sql注入攻擊了。固然這個跟你登陸查詢的sql語句有關。

escape 函數預防 👉 利用mysql中的escape函數包裹咱們的登陸名和密碼

//  db文件夾下導出escape函數。在user.js中引用
escape: mysql.escape

username = escape(username)
password = escape(password)
複製代碼

2. xss攻擊

  • 攻擊方式: 在頁面展現的內容中摻雜js代碼,以獲取網頁信息
  • 預防措施: 轉換生成js的特殊字符,(npm install xss -d --save)

攻擊方法演示: 👉 在新建博客的時候標題輸入下面內容便可查看本網站的cookie。

<script>alert(document.cookie)</script>
複製代碼

3. 密碼加密

在utils > crpy.js 加密文件

const crypto = require('crypto')

// 密匙
const SECRET_KEY = 'WJiol_8776#'

// 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
}
複製代碼

在controller > user.js中引入genPassword方法對輸入的密碼加密

password = genPassword(password)

6、使用 express 重構博客項目

1. express安裝(使用腳手架express-generator)

npm install express-generator -g

經過 express express-test 命令生成一個項目。npm install 去下載依賴包運行npm start 訪問localhist:3000。再安裝監聽文件的修改

npm install nodemon cross-env --save-dev

再package.json新增一個scripts命令:

"dev": "cross-env NODE_ENV=dev nodemon ./bin/www"

2. express中app.js介紹

  1. http-errors 處理404
  2. cookie-parser 解析cookie
  3. morgan 自動生成日誌
  4. app.use(express.json()); 處理post data
  5. app.use(express.urlencoded({ extended: false })); post兼容其餘格式

3. express中間件原理和實現

中間件原理分析:

  • app.use 用來註冊中間件,先手機起來
  • 遇到http請求,根據path和method判斷出發哪些
  • 實現next機制,即上一個經過next出發下一個
const http = require('http')
const slice = Array.prototype.slice

class LikeExpress {
  constructor() {
    // 存放中間件列表
    this.routes = {
      all: [],
      gte: [],
      post: []
    }
  }

  register(path) {
    const info = {}
    // 分析第一個參數是否爲路由
    if (typeof path === 'string') {
      info.path = path
      // 從第二個參數開始,轉換成數組,存入 stack
      info.stack = slice.call(arguments, 1) // 數組
    } else {
      info.path = '/'
       // 從第一個參數開始,轉換成數組,存入 stack
      info.stack = slice.call(arguments, 0) // 數組
    }
    return info
  }

  use() {
    const info = this.register.apply(this, arguments)
    this.routes.all.push(info)
  }

  get() {
    const info = this.register.apply(this, arguments)
    this.routes.get.push(info)
  }

  post() {
    const info = this.register.apply(this, arguments)
    this.routes.post.push(info)
  }

  match(method, url) {
    let stack = []
    if (url === '/favico.ico') {
        return stack
    } 
    // 獲取 routes
    let curRoutes = []
    curRoutes = curRoutes.concat(this.routes.all)
    curRoutes = curRoutes.concat(this.routes[method])

    curRoutes.forEach(routerInfo => {
      if (url.indexOf(routerInfo.path) === 0) {

        stack = stack.concat(routerInfo.stack)
      }
    })
    return stack
  }

  // 核心的next機制
  handle(req, res, stack) {
    const next = () => {
      // 拿到第一個匹配的中間件
      const middleware = stack.shift()
      if (middleware) {
        // 執行中間件函數
        middleware(req, res, next)
      }
    }
    next()
  }

  callback() {
    return (req, res) => {
      res.json = (data) => {
        res.setHeader('Content-type', 'application/json')
        res.end(JSON.stringify(data))
      }
      const url = req.url
      const method = req.method.toLowerCase()

      const resultList = this.match(method, url)
      this.handle(req, res, resultList)
    }
  }
  listen(...args) {
    const server = htpp.createServer(this.callback())
    server.listen(...args)
  }
}

module.exports = () => {
  return new LikeExpress()
}
複製代碼

7、使用 Koa2 重構博客項目

1. koa2安裝(使用腳手架koa-generator)

npm install koa-generator -g

經過 koa2 express-koa2 命令生成一個項目。npm install 去下載依賴包運行npm start 訪問localhist:3000。再安裝監聽文件的修改

npm install cross-env --save-dev

再package.json新增一個scripts命令:

"dev": "cross-env NODE_ENV=dev nodemon ./bin/www"

2. koa2 開發接口

  • 實現登陸session 基於koa-generic-session 和koa-redis
  • 開發路由,基本就是複用express

3. koa2 中間件原理和實現

  • app.use 用來註冊中間件,先收集起來
  • 實現 next 機制,即上一個next出發下一個
const http = require('http')

// 組合中間件
function compose(middlewareList) {
  return function (ctx) {
    // 中間件調用
    function dispatch(i) {
      const fn = middlewareList[i]
      try {
        return Promise.resolve(fn(ctx, dispatch.bind(null, i+1)))
      } catch(err) {
        return Promise.reject(err)
      }
    }
    return dispatch(0)
  }
}

class LikeKoa2 {
  constructor() {
    this.middlewareList = []
  }

  use(fn) {
    this.middlewareList.push(fn)
    return this
  }

  createCtx(req, res) {
    const ctx = {
      req,
      res
    }
    return ctx
  }

  handleRequest(ctx, fn) {
    return fn(ctx)
  }

  callback() {
    const fn = compose(this.middlewareList)
    return (req, res) => {
      const ctx = this.createCtx(req, res)
      return this.handleRequest(ctx, fn)
    }
  }

  lsiten(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
}
module.exports = {
  LikeKoa2
}
複製代碼

8、上線與配置(pm2)

代碼開發完畢要線上運行,而且保證服務穩定性,將使用 PM2 工具。本章講解 PM2 的配置使用和進程守護,以及 PM2 多進程模型。最後,還介紹了服務器運維的相關方法。

1. pm2 介紹

  • 進程守護,系統崩潰自動重啓 node app.js和 nodemon app.js,進程奔潰不能訪問,pm2遇到奔潰,會自動重啓。
  • 啓動多進程,充分利用cpu和內存
  • 自帶日誌記錄功能

2. pm2 配置與多進程

優勢: 單個進程內存受限,操做系統會限制一個進程的最大可用內存。沒法充分利用多核cpu優點。

缺點: 多進程之間,內存沒法共享;多進程訪問redis,實現數共享

{
    "apps": {
        "name": "pm2-test-server",
        "script": "app.js",
        "watch": true, // 實時監聽
        "ignore_watch": [
            "node_modules",
            "logs"
        ],
        "instances": 4, // 進程數
        "error_file": "logs/err.log",
        "out_file": "logs/out.log",
        "log_date_format": "YYYY-MM-DD HH:mm:ss"
    }
}
複製代碼

9、總結

看到這裏差很少都講完了,只是大體的說了下步驟。具體的可查看👉源碼地點,感興趣的同窗能夠去慕課上學學,這裏不是推銷,課程仍是很棒的。紙上得來終覺淺,準備講所學知識加以運用,接下來會出一個node全棧的仿掘金,有興趣的也能夠加入和我一塊兒快樂的學習吧。項目目前已經進展到一半多了。

仿掘金
仿掘金
相關文章
相關標籤/搜索