使用 Express 腳手架創(chuàng)建Node項(xiàng)目

安裝


npm install  -g express-generator // 全局安裝 express 腳手架
express '項(xiàng)目名稱(chēng)'
cd '項(xiàng)目名稱(chēng)'
npm install // 安裝依賴(lài)
npm start // 啟動(dòng)項(xiàng)目

使用 nodemon 和 cross-env


安裝這兩個(gè)插件可以讓我們通過(guò) npm run dev 來(lái)啟動(dòng)項(xiàng)目,并對(duì)項(xiàng)目實(shí)時(shí)修改進(jìn)行自動(dòng)響應(yīng)。

npm install nodemon cross-env --save-dev

修改 package.json 中的 scripts

"scripts": {
    "start": "node ./bin/www",
    "dev": "cross-env NODE_ENV=dev nodemon ./bin/www"
},

分析 app.js 中的代碼


var createError = require('http-errors'); // 404
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser'); // 引入解析 cookie 的中間件
var logger = require('morgan'); // 引入記錄日志文件插件

// 引入了2個(gè)路由,routers 文件夾的的路由文件
var indexRouter = require('./routes/index'); 
var usersRouter = require('./routes/users');

var app = express();

// app.use() 使用中間件
app.use(logger('dev')); // 使用引入的日志文件
app.use(express.json()); // 響應(yīng) post 請(qǐng)求 json 格式
app.use(express.urlencoded({ extended: false })); // 響應(yīng) post 請(qǐng)求非 json 格式,常用表單結(jié)構(gòu)數(shù)據(jù) 
app.use(cookieParser()); // 使用引入的中間件 cookie-parser 解析 cookie

app.use('/', indexRouter); // 跟路由指向 indexRouter
app.use('/users', usersRouter); // /users 這個(gè)路由指向 usersRouter 下級(jí)路由 /users/info等等

app.use(function(req, res, next) { // 找不到的路徑跳轉(zhuǎn) 404
  next(createError(404));
});

// 對(duì)程序錯(cuò)誤的一些處理,拋出錯(cuò)誤信息和對(duì)應(yīng)的狀態(tài)碼
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  // 根據(jù)環(huán)境判斷,只在本地環(huán)境輸出錯(cuò)誤信息
  res.locals.error = req.app.get('env') === 'dev' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

Express 如何處理路由


var express = require('express');
var router = express.Router();
 // get 請(qǐng)求寫(xiě)法
router.get('/list', function(req, res, next) {
  // req.query 解析 get 請(qǐng)求中傳遞過(guò)來(lái)的參數(shù)
  const { author, keyword } = req.query
  res.json({
    author: author
  })
});
// post 請(qǐng)求寫(xiě)法
router.post('/login', function(req, res, next) {
  // req.body 解析 post 請(qǐng)求中傳遞過(guò)來(lái)的參數(shù)
  const { username, password } = req.body
  res.json({
      errno: 0,
      data: {
          username,
          password
      }
  })
});
module.exports = router;

了解 Express 中間件


通過(guò)上面的代碼,我們發(fā)現(xiàn)了一堆 app.use(){req, res, next} ,那么 app.use()next() 有啥用呢?那么它們到底有啥用呢,來(lái)看下面這個(gè)栗子

// 新開(kāi)一個(gè)文件夾 test.js 只引入 express
const express = require('express')
const app = express()
app.use((req, res, next) => {
  console.log('請(qǐng)求開(kāi)始...', req.method, req.url)
  next()
})
app.use((req, res, next) => {
  // 假設(shè)在處理 cookie
  req.cookie = {
    userId: 'abc123'
  }
  next()
})
app.use((req, res, next) => {
  // 假設(shè)在處理 post data
  // 異步
  setTimeout(() => {
    req.body = {
      a: 100,
      b: 200
    }
    next()
  })
})
app.use('/api', (req, res, next) => {
  console.log('處理 /api 路由')
  next()
})
// 使用 app.get
app.get('/api', (req, res, next) => {
  console.log('處理 get /api 路由')
  next()
})
// 使用 app.post
app.post('/api', (req, res, next) => {
  console.log('處理 post /api 路由')
  next()
})

上面的代碼中,我們一共用了 3 次 app.use() 未加請(qǐng)求路徑的情況,1 次 app.user('/api') 的情況,然后又分別用了 app.get()app.post() ,并且每個(gè)回調(diào)函數(shù)里面都加上了 next 并在結(jié)尾處使用了 next() 方法。那么我們接下來(lái)去通過(guò)一個(gè)不加 nextget 請(qǐng)求來(lái)試一試,上面哪些會(huì)被執(zhí)行

app.get('/api/get-cookie', (req, res, next) => {
  console.log('get /api/get-cookie')
  res.json({
    errnon: 0,
    data: req.cookie
  })
})
app.listen(3000, () => {
  console.log("server is running...")
})

命令行輸出了如下內(nèi)容

// 命令行輸出
請(qǐng)求開(kāi)始... GET /api/get-cookie
處理 /api 路由
get /api/get-cookie

// 頁(yè)面輸出
{
    errnon: 0,
    data: {
        userId: "abc123" // cookie 被寫(xiě)入
    }
}

這里我們發(fā)現(xiàn),app.use() 未加請(qǐng)求路徑和 app.user('/api') 都被執(zhí)行打印了出來(lái),而 app.get('/api')app.post('/api') 都未被執(zhí)行,這里我們先得出結(jié)論,只要我們?cè)L問(wèn)一個(gè)路徑,那么我們?nèi)肟谖募?app.js 中的所有 app.use() 不加路徑的部分都會(huì)被執(zhí)行。即使加了路徑,例如我們?cè)L問(wèn)的路徑是 app.use(/api/get-cooki) 而上面定義的路徑是 app.use('/api') 它是我們?cè)L問(wèn)路徑的上一級(jí)路徑,那么它也會(huì)被執(zhí)行。而這些 app.use(() => {}) 里面被執(zhí)行的里面的函數(shù)就是中間件,它們通過(guò)最后的 next() 方法依次往下執(zhí)行(不符合執(zhí)行條件的自動(dòng)忽略,)。

我們通過(guò) post 請(qǐng)求訪(fǎng)問(wèn) /api/get-post-data 路徑

app.post('/api/get-post-data', (req, res, next) => {
  console.log('post /api/get-post-data')
  res.json({
    errno: 0,
    data: req.body
  })
})
app.listen(3000, () => {
  console.log("server is running...")
})

命令行輸出了如下內(nèi)容

// 命令行輸出
請(qǐng)求開(kāi)始... POST /api/get-post-data
處理 /api 路由
post /api/get-post-data

// 頁(yè)面輸出
{
    "errno": 0,
    "data": {
        "a": 100,
        "b": 200
    }
}

輸出基本和 get 相同,首先會(huì)去輸出 app.use() 中沒(méi)有路徑的,然后再輸出 app.use() 中當(dāng)前路徑的上一級(jí)路徑內(nèi)容,其它會(huì)忽略。

我們?nèi)ピL(fǎng)問(wèn)一個(gè)不存在的路由,看看 404 not found 的情況下上面的代碼是如何輸出的,這里如果我們直接訪(fǎng)問(wèn) /aaa

app.use((req, res, next) => {
  console.log('處理404')
  res.json({
    errno: '-1',
    msg: '404 not found'
  })
})

app.listen(3000, () => {
  console.log("server is running...")
})

命令行輸出如下內(nèi)容:

請(qǐng)求開(kāi)始... GET /api/aaaaa
處理404

// 頁(yè)面輸出
{
    errno: "-1",
    msg: "404 not found"
}

因?yàn)槲覀冊(cè)L問(wèn)的是 /aaa 它本身已經(jīng)是根路徑下的第一個(gè)路徑,所以只會(huì)執(zhí)行 app.use() 中沒(méi)有路徑的,其它都不會(huì)執(zhí)行,所以在這里我們可以得出 app.use() 里面是都是使用的中間件,而我們初始化好的一些內(nèi)容都是通過(guò) app.use() 應(yīng)用到全局中,每次我們請(qǐng)求一個(gè)接口地址,都會(huì)先從最上面的 app.use() 開(kāi)始去執(zhí)行。了解這些后這里我們可以做一個(gè)登陸驗(yàn)證的中間件,當(dāng)我們?cè)L問(wèn) /api/get-cookie 時(shí)如下栗子

function loginCheck(req, res, next) {
  console.log('登陸成功')
  setTimeout(() => {
    next()
  })
}
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  console.log('get /api/get-cookie')
  res.json({
    errnon: 0,
    data: req.cookie
  })
})

上面的栗子中,我們有一個(gè)假設(shè)有一個(gè)函數(shù) loginCheck 來(lái)驗(yàn)證登陸成功或者失敗,假設(shè)成功,然后給出 next(),那么請(qǐng)求 /api/get-cookie 之后命令行會(huì)輸出登陸成功。

如果我們給出失敗呢,如下栗子:

function loginCheck(req, res, next) {
  setTimeout(() => {
    res.json({
        errno: -1,
        msg: '登陸失敗'
    })
  })
}
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  console.log('get /api/get-cookie')
  res.json({
    errnon: 0,
    data: req.cookie
  })
})

登陸失敗的情況下我們不給 next(),也就是后面的回調(diào)函數(shù)(其實(shí)也可以理解為一個(gè)中間件)就不會(huì)執(zhí)行了,頁(yè)面只會(huì)輸出如下內(nèi)容,這樣我們就可以根據(jù)返回值來(lái)判斷該用戶(hù)是否登錄。

{
    errno: -1,
    msg: '登陸失敗'
}

了解 Express 中如何處理 session


  • 引入 express-session
npm install redis connect-redis --save-dev
  • 做一個(gè)用戶(hù)訪(fǎng)問(wèn)網(wǎng)頁(yè)次數(shù)的 session 案例
// app.js中
const session = require('express-session')
app.use(session({ // 執(zhí)行傳入對(duì)應(yīng)參數(shù)
  secret: 'CcWc#1993_28', // 隨便定義,用來(lái)生成cookie的密鑰
  cookie: {
    path: '/', // 默認(rèn)配置
    httpOnly: true, // 默認(rèn)配置
    maxAge: 24 * 60 * 60 * 1000 // cookie的生效時(shí)間 時(shí) * 分 * 秒 * 毫秒
  }
}))
app.use('/api/user', userRouter)

// user.js
router.get('/session-test', (req, res, next) => {
  const session = req.session
  if (session.viewNum == null) {
    session.viewNum = 0
  }
  session.viewNum++
  res.json({
    viewNum: session.viewNum
  })
})
使用 express-session 簡(jiǎn)單處理登陸信息
router.post('/login', function(req, res, next) {
  const { username, password } = req.body
  let result = loginCheck(username, password)
  return result.then(data => {
    if (data.username) {
      // 設(shè)置 session
      req.session.username = data.username
      res.json({
          new SuccessModel('已登錄')
      })
      return
    }
    res.json(
      new ErrorModel('未登陸')
    )
  })
});
// 是否登錄測(cè)試接口
router.get('/login-test', (req, res, next) => {
  if (req.session.username) {
    res.json({
      error: 0,
      msg: '已登錄'
    })
  } else {
    res.json({
      error: -1,
      msg: '未登錄'
    })
  }
})

將 session 數(shù)據(jù)存入 redis 中

  • 安裝 redisconnect-redis
npm install redis connect-redis --save-dev
  • 將安裝的 redisconnect-redis 引入
// app.js 中
const session = require('express-session')
const RedisStore = require('connect-redis')(session)

const redisClient = require('./db/redis')
const sessionStore = new RedisStore({
  client: redisClient
}) 
app.use(session({
  secret: 'CcWc#1993_28',
  cookie: { 
    maxAge: 24 * 60 * 60 * 1000
  },
  store: sessionStore
}));

// db => redis.js
const redis = require('redis')
const { REDIS_CONF } = require('../config/db')

const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
redisClient.on('error', err => {
  console.log(err)
})

module.exports = redisClient

// config => db.js
const env = process.env.NODE_ENV

// 配置
let REDIS_CONF

if (env === 'dev') {
  // redis
  REDIS_CONF = {
    port: 6379,
    host: '127.0.0.1'
  }
}

if (env === 'production') {
  // redis
  REDIS_CONF = {
    port: 6379,
    host: '127.0.0.1'
  }
}

module.exports = {
  REDIS_CONF
}

建立一個(gè)判斷是否登錄的中間件 loginCheck.js

// loginCheck.js
const { ErrorModel } = require('./../model/resModel')

module.exports = (req, res, next) => {
  // 能從 session 中獲取到 username,就允許往下執(zhí)行,否知就直接彈出'未登錄'提示
  if (req.session.username) {
    next()
    return
  }
  res.json(
    new ErrorModel('未登錄')
  )
}

如何使用這個(gè)中間件呢,如下栗子

// 引入上面的中間件 loginCheck.js
router.post('/del', loginCheck, function (req, res) {
  let result = delBlog(req.query.id, req.session.username)
  return result.then(data => {
    if (data) {
      res.json(
        new SuccessModel('刪除成功')
      )
    } else {
      new ErrorModel('刪除失敗')
    }
  })
}

利用 Express 中的 morgan 寫(xiě)日志


morgan 線(xiàn)上文檔地址

寫(xiě)入系統(tǒng)日志

我們分析過(guò) app.js 中知道腳手架本身提供了對(duì)應(yīng)的寫(xiě)日志工具,我們將其中的代碼單獨(dú)拎出來(lái)

var logger = require('morgan');
app.use(logger('dev')); // dev 代表開(kāi)發(fā)模式
// app.use(logger('dev', {
//   stream: process.stdout // 默認(rèn)寫(xiě)入命令行中,無(wú)須配置
// }))

因?yàn)橄到y(tǒng)默認(rèn)幫我們配置了 logger('dev') 所以我們操作網(wǎng)頁(yè)的時(shí)候命令行就已經(jīng)為我們輸出了我們每一次點(diǎn)擊執(zhí)行的動(dòng)作,如下:

GET /api/blog/detail?id=14 304 6.149 ms - -
POST /api/blog/update?id=14 200 16.031 ms - 36
GET /api/blog/list?isadmin=1 200 2.083 ms - 995
POST /api/blog/del?id=13 200 13.825 ms - 36
GET /api/blog/list?isadmin=1 200 1.784 ms - 912
POST /api/blog/del?id=12 200 2.853 ms - 36
GET /api/blog/list?isadmin=1 200 1.994 ms - 829
POST /api/blog/del?id=11 200 14.773 ms - 36
GET /api/blog/list?isadmin=1 200 1.274 ms - 737
  • dev 開(kāi)發(fā)環(huán)境下的格式,簡(jiǎn)單
app.use(logger('dev', {
    stream: process.stdout // 默認(rèn)寫(xiě)入方式,命令行寫(xiě)入
}))
  • combined 比較完善的格式,一般線(xiàn)上環(huán)境用
app.use(logger('combined', {
    stream: process.stdout // 默認(rèn)寫(xiě)入方式,命令行寫(xiě)入
}))

combined 在命令行中記錄格式如下栗子:

::ffff:127.0.0.1 - - [29/Aug/2020:09:34:20 +0000] "GET /api/blog/detail?id=14 HTTP/1.0" 200 103 "http://localhost:8080/edit.html?id=14" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"
::1 - - [29/Aug/2020:09:34:23 +0000] "POST /api/blog/update?id=14 HTTP/1.0" 200 36 "http://localhost:8080/edit.html?id=14" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"
::ffff:127.0.0.1 - - [29/Aug/2020:09:34:25 +0000] "GET /api/blog/list?isadmin=1 HTTP/1.0" 200 737 "http://localhost:8080/admin.html" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"

還有一些其它的格式,如:common 、shorttiny 等,可以去官方文檔地址中進(jìn)行查看。

上面展示了 devcombined 在命令行寫(xiě)入日志的基本情況,線(xiàn)上我們一般希望將日志寫(xiě)入自定義的文件中,那么我們就要更改 stream 的值來(lái)實(shí)現(xiàn)寫(xiě)入。

我們首先在 package.json 中配置一個(gè)線(xiàn)上環(huán)境:

"scripts": {
    "start": "node ./bin/www",
    "dev": "cross-env NODE_ENV=dev nodemon ./bin/www", // dev 開(kāi)發(fā)測(cè)試環(huán)境
    "prd": "cross-env NODE_ENV=production nodemon ./bin/www" // prd 線(xiàn)上環(huán)境
},

因?yàn)橐獙?xiě)入文件中,所以我們應(yīng)該在 app.js 中引入 fs 模塊

// app.js
var fs = require('fs')
const ENV = process.env.NODE_ENV
if (ENV !== 'production') {
  // 開(kāi)發(fā)或測(cè)試環(huán)境
  app.use(logger('dev'))
} else {
  // 線(xiàn)上環(huán)境
  // 新建一個(gè) logs 日志文件夾,然后里面新建 access.log 文件,這是我們?nèi)罩敬娣诺牡刂? 
  const logFileName = path.join(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a' // flags:'a' 追加寫(xiě)入 flags: 'w' 覆蓋寫(xiě)入
  })
  app.use(logger('combined', {
    stream: writeStream
  }))
}

以線(xiàn)上環(huán)境重新啟動(dòng)程序

npm run prd

查看 access.log 文件中的寫(xiě)入內(nèi)容

::1 - - [29/Aug/2020:10:04:29 +0000] "GET /api/blog/list?isadmin=1 HTTP/1.0" 200 34 "http://localhost:8080/admin.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
::ffff:127.0.0.1 - - [29/Aug/2020:10:04:42 +0000] "POST /api/blog/new HTTP/1.0" 200 34 "http://localhost:8080/new.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
::1 - - [29/Aug/2020:10:04:56 +0000] "POST /api/user/login HTTP/1.0" 200 36 "http://localhost:8080/login.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"
::ffff:127.0.0.1 - - [29/Aug/2020:10:04:56 +0000] "GET /api/blog/list?isadmin=1 HTTP/1.0" 200 335 "http://localhost:8080/admin.html" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36 SLBrowser/6.0.1.6181"

我們所有項(xiàng)目上的點(diǎn)擊動(dòng)作就全部被寫(xiě)入到指定的日志文件中了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容