Node + Express 後臺開發 —— 起步

彭加李發表於2023-04-21

Node + Express 後臺開發 —— 起步

前面陸續學習了一下 node、npm、模組,也稍嘗試 Express,感覺得換一個思路加快進行。

比如筆者對前端的開發已較熟悉,如果領導給一個內部小網站的需求,難道說你得給我配置一個後端

又不是做一個複雜的後端,只是簡單的資料儲存(增刪改查)、上傳檔案、下載csv或excel,無需考慮效能、穩定性、負載均衡等等,怎麼就不能做

目標

實現簡單後臺的開發和部署

Express 專案生成器

生成專案

可透過應用生成器工具 express-generator 可以快速建立一個應用的骨架

建立專案資料夾 spug-back-end,進入專案後執行 npx express-generator:

Administrator@ /e/spug-back-end
$ npx express-generator
npm WARN exec The following package was not found and will be installed: express-generator@4.16.1
npm WARN deprecated mkdirp@0.5.1: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)

  warning: the default view engine will not be jade in future releases
  warning: use `--view=jade' or `--help' for additional options

   create : public\
   create : public\javascripts\
   create : public\images\
   create : public\stylesheets\
   create : public\stylesheets\style.css
   create : routes\
   create : routes\index.js
   create : routes\users.js
   create : views\
   create : views\error.jade
   create : views\index.jade
   create : views\layout.jade
   create : app.js
   create : package.json
   create : bin\
   create : bin\www

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=spug-back-end:* npm start

生成如下內容:

Administrator@ /e/spug-back-end
$ ll
total 5
-rw-r--r-- 1 Administrator 197121 1075 Apr 14 15:06 app.js
drwxr-xr-x 1 Administrator 197121    0 Apr 14 15:06 bin/
-rw-r--r-- 1 Administrator 197121  301 Apr 14 15:06 package.json
drwxr-xr-x 1 Administrator 197121    0 Apr 14 15:06 public/
drwxr-xr-x 1 Administrator 197121    0 Apr 14 15:06 routes/
drwxr-xr-x 1 Administrator 197121    0 Apr 14 15:06 views/

Tip: 對於較老的 Node 版本(8.2.0 以下),請透過 npm 將 Express 應用程式生成器安裝到全域性環境中並使用

$ npm install -g express-generator
$ express

根據上文提示安裝依賴 npm install

Administrator@ /e/spug-back-end
$ npm install
npm WARN deprecated constantinople@3.0.2: Please update to at least constantinople 3.1.1
npm WARN deprecated transformers@2.1.0: Deprecated, use jstransformer
npm WARN deprecated jade@1.11.0: Jade has been renamed to pug, please install the latest version of pug instead of jade

added 99 packages, and audited 100 packages in 22s

1 package is looking for funding
  run `npm fund` for details

8 vulnerabilities (1 low, 4 high, 3 critical)

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

全域性安裝 nodemon(在Node.js應用程式開發過程中使用的簡單監視器指令碼),編碼過程中無需重啟 node 服務即可生效:

PS E:\spug-back-end> npm i -g nodemon

changed 32 packages, and audited 33 packages in 1s

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

修改啟動指令碼:

   "version": "0.0.0",
   "private": true,
   "scripts": {
-    "start": "node ./bin/www"
+    "start": "nodemon ./bin/www"

透過 npm run start 本地啟動服務:

PS E:\spug-back-end> npm run start

> spug-back-end@0.0.0 start
> nodemon ./bin/www

[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./bin/www`

入口檔案 /bin/www.js 預設使用的是 3000 埠(var port = normalizePort(process.env.PORT || '3000');

瀏覽器訪問 http://localhost:3000/,頁面顯示:

Express
Welcome to Express

專案解讀

模板

views 目錄中存放的是 jade 模板檔案:

Administrator@ /e/spug-back-end/views (master)
$ ls
error.jade  index.jade  layout.jade

首頁對應的模板是 index.jade:

Administrator@ /e/spug-back-end/views (master)
$ cat index.jade
extends layout

block content
  h1= title
  p Welcome to #{title}

Express 生成器預設使用 jade 模板,對前端不是很友好,筆者透過 npx express-generator --view=ejs 選用 ejs模板(對前端更友好)重新建立。

首頁對應模板 index.ejs:

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
  </body>
</html>
入口檔案

bin/www.js 是應用的入口檔案。

核心程式碼如下:

// 載入 app.js
var app = require('../app');
var http = require('http');

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

// 建立 http server
var server = http.createServer(app);
server.listen(port);

完整程式碼如下:

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('spug-back-end:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Tip: 和我們之前建立的最簡單的伺服器類似

app.js

入口檔案中引入 app.js,完整程式碼如下:

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// Creates an Express application
var app = express();

// view engine setup
// 模板引擎相關程式碼
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
// for parsing application/json
app.use(express.json());
// for parsing application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// 放開靜態資源
app.use(express.static(path.join(__dirname, 'public')));

// 定義兩個路由。一個返回頁面一個返回後端資料
// app.use([path,] callback [, callback...]) - 在指定路徑掛載指定的一個或多箇中介軟體函式:當請求路徑的基匹配路徑時,執行中介軟體函式。
app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

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

module.exports = app;

和我們之前用 Express 實現的報名系統 非常類似。這裡建立了一個 Express 應用,並定義了兩個示例路由:

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

// 返回頁面
app.use('/', indexRouter);
// 返回後端資料
app.use('/users', usersRouter);
路由

app.js 使用了兩個路由。內容如下:

// routes/index.js
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

module.exports = router;
// routes/users.js
var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

module.exports = router;

瀏覽器訪問 http://localhost:3000/users,頁面顯示 respond with a resource

mongodb

資料庫通常會安裝到 linux 中,這裡以 ubuntu 為例,透過 apt-get install mongodb 即可安裝,非常方便:

// 筆者剛已執行
root@linux:/home/pjl# apt-get install mongodb
Reading package lists... Done
Building dependency tree
Reading state information... Done
mongodb is already the newest version (1:3.6.9+really3.6.8+90~g8e540c0b6d-0ubuntu5.3).
0 upgraded, 0 newly installed, 0 to remove and 150 not upgraded.

資料庫現在已經啟動,我們透過mongo -version驗證安裝成功:

root@linux:/home/pjl# mongo -version
MongoDB shell version v3.6.8
git version: 8e540c0b6db93ce994cc548f000900bdc740f80a
OpenSSL version: OpenSSL 1.1.1f  31 Mar 2020
allocator: tcmalloc
modules: none
build environment:
    distarch: x86_64
    target_arch: x86_64

mongodb 配置檔案是 /etc/mongodb.conf。請注意下面兩項配置:

// 遠端連線
#bind_ip = 127.0.0.1
bind_ip = 0.0.0.0

// 開機自啟動
#auth = true
auth = true

MongoDB shell

輸入 mongo 即可進入 MongoDB shell(操作mongo):

root@linux:/home/pjl# mongo
MongoDB shell version v3.6.8
connecting to: mongodb://127.0.0.1:27017
Implicit session: session { "id" : UUID("aedc6541-4a67-4e60-8eb4-d1325c82d061") }
MongoDB server version: 3.6.8
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
        http://docs.mongodb.org/
Questions? Try the support group
        http://groups.google.com/group/mongodb-user
Server has startup warnings:
2023-04-15T14:34:32.327+0800 I STORAGE  [initandlisten]
2023-04-15T14:34:32.327+0800 I STORAGE  [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2023-04-15T14:34:32.327+0800 I STORAGE  [initandlisten] **          See http://dochub.mongodb.org/core/prodnotes-filesystem
2023-04-15T14:34:34.861+0800 I CONTROL  [initandlisten]
2023-04-15T14:34:34.861+0800 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
2023-04-15T14:34:34.861+0800 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
2023-04-15T14:34:34.861+0800 I CONTROL  [initandlisten]
> help
        db.help()                    help on db methods
        db.mycoll.help()             help on collection methods
        sh.help()                    sharding helpers
        rs.help()                    replica set helpers
        help admin                   administrative help
        help connect                 connecting to a db help
        help keys                    key shortcuts
        help misc                    misc things to know
        help mr                      mapreduce

        show dbs                     show database names
        show collections             show collections in current database
        show users                   show users in current database
        show profile                 show most recent system.profile entries with time >= 1ms
        show logs                    show the accessible logger names
        show log [name]              prints out the last segment of log in memory, 'global' is default
        use <db_name>                set current database
        db.foo.find()                list objects in collection foo
        db.foo.find( { a : 1 } )     list objects in foo where a == 1
        it                           result of the last line evaluated; use to further iterate
        DBQuery.shellBatchSize = x   set default number of items to display on shell
        exit                         quit the mongo shell
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

透過 help 可以檢視幫助,透過show dbs 發現現在有三個資料庫。

新建資料庫

透過 use 可以建立或切換資料庫。比如:

// use - 建立資料庫 pjl_db。如果存在則直接切換
> use pjl_db
switched to db pjl_db
// db - 目前操作的是 pjl_db 資料庫
> db
pjl_db

由於新建的資料庫 pjl_db 什麼都沒有,所以透過 show dbs 顯示不出來,可透過 createCollection 建立表,再次查詢即可顯示該資料庫。比如

// 新建資料庫未能展示
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
// 透過 tab 建有提示
> db.create
db.createCollection(  db.createRole(        db.createUser(        db.createView(
// 建立表 users。這裡叫集合
> db.createCollection("users")
{ "ok" : 1 }
// 再次查詢則可顯示新建資料庫 pjl_db
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
pjl_db  0.000GB

Tipdb.createCollection("pjl_db", {size: 1024*1024, capped: true, max: 1000}) - 建立 pjl_db 資料庫,同時這個資料庫最多 1M,記錄數只能有1000條,在多一條則把第一條給替代。db.createCollection("users") 則不作限制。

刪除資料庫

透過 db.dropDatabase() 即可刪除資料庫。比如:

> db.dropDatabase()
{ "dropped" : "pjl_db", "ok" : 1 }
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

新增、檢視和刪除表

透過 db.createCollection("users") 新建 users 表。比如:

// 建立表 users。這裡叫集合
> db.createCollection("users")
{ "ok" : 1 }
// 再次查詢則可顯示新建資料庫 pjl_db
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
pjl_db  0.000GB
  • db.getCollectionNames - 檢視有哪些表
  • db.tableName.drop - 刪除某張表

示例:

// 新建兩張表: table-b、table-c
> db.createCollection("table-b")
{ "ok" : 1 }
> db.createCollection("table-c")
{ "ok" : 1 }
// tab 提示
> db.getCollection
db.getCollection(       db.getCollectionInfos(  db.getCollectionNames(
// 有哪些表
> db.getCollectionNames()
[ "table-b", "table-c", "users" ]
// 刪除table-b表失敗
> db.table-b.drop()
2023-04-15T15:17:10.232+0800 E QUERY    [thread1] ReferenceError: b is not defined :
@(shell):1:1
// 刪除table-b表成功
> db["table-b"].drop()
true
> db.getCollectionNames()
[ "table-c", "users" ]

表格-新增資料

透過 db.users.save 可以向 users 表中單條、批次插入資料。甚至欄位名不同,欄位數量不同也能插入。請看示例:

// 給 users 表插入一條資料 {"name": 'pjl', age: 18}
> db.users.save({"name": 'pjl', age: 18})
WriteResult({ "nInserted" : 1 })
// 查詢users表
> db.users.find()
{ "_id" : ObjectId("643a513d73f16a13ae248f42"), "name" : "pjl", "age" : 18 }
// 批次插入資料
> db.users.save([{"name": 'pjl2', age: 19}, {"name": 'pjl3', age: 20}])
BulkWriteResult({
        "writeErrors" : [ ],
        "writeConcernErrors" : [ ],
        "nInserted" : 2,
        "nUpserted" : 0,
        "nMatched" : 0,
        "nModified" : 0,
        "nRemoved" : 0,
        "upserted" : [ ]
})
// 批次插入成功
> db.users.find()
{ "_id" : ObjectId("643a513d73f16a13ae248f42"), "name" : "pjl", "age" : 18 }
{ "_id" : ObjectId("643a51db73f16a13ae248f44"), "name" : "pjl2", "age" : 19 }
{ "_id" : ObjectId("643a51db73f16a13ae248f45"), "name" : "pjl3", "age" : 20 }
// 換欄位名和欄位長度,也能插入成功
> db.users.save({"name2": 'pjl', age2: 18, tel: 131111111})
WriteResult({ "nInserted" : 1 })
> db.users.find()
{ "_id" : ObjectId("643a513d73f16a13ae248f42"), "name" : "pjl", "age" : 18 }
{ "_id" : ObjectId("643a51db73f16a13ae248f44"), "name" : "pjl2", "age" : 19 }
{ "_id" : ObjectId("643a51db73f16a13ae248f45"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643a529a73f16a13ae248f46"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }

Tip:向 mongo 表中插入資料非常自由,什麼欄位、什麼型別都可以。

表格-刪除資料

透過 db.users.remove({age2:18}) 可以刪除 age2=18 的資料,透過 db.users.remove({}) 刪除 users 表中所有資料。請看示例:

// 目前有4條資料
> db.users.find()
{ "_id" : ObjectId("643a513d73f16a13ae248f42"), "name" : "pjl", "age" : 18 }
{ "_id" : ObjectId("643a51db73f16a13ae248f44"), "name" : "pjl2", "age" : 19 }
{ "_id" : ObjectId("643a51db73f16a13ae248f45"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643a529a73f16a13ae248f46"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }

// 可以刪除 age2=18 的資料
> db.users.remove({age2:18})
WriteResult({ "nRemoved" : 1 })
> db.users.find()
{ "_id" : ObjectId("643a513d73f16a13ae248f42"), "name" : "pjl", "age" : 18 }
{ "_id" : ObjectId("643a51db73f16a13ae248f44"), "name" : "pjl2", "age" : 19 }
{ "_id" : ObjectId("643a51db73f16a13ae248f45"), "name" : "pjl3", "age" : 20 }

// 刪除 users 表中所有資料是 `db.users.remove({})`。`db.users.remove()` 會報錯
> db.users.remove()
2023-04-16T09:10:55.859+0800 E QUERY    [thread1] Error: remove needs a query :
DBCollection.prototype._parseRemove@src/mongo/shell/collection.js:357:1
DBCollection.prototype.remove@src/mongo/shell/collection.js:382:18
@(shell):1:1
// 刪除 users 表中所有資料
> db.users.remove({})
WriteResult({ "nRemoved" : 3 })
> db.users.find()
>

表格-修改資料

透過 db.users.update({name:'pjl2'}, {$set: {age: 20}}) 修改 name=pjl2 的資料,將 age 改為 20。

Tip:直接 db.users.update({name:'pjl'}, {age: 19}) 會替換整條資料。

\$inc 指增加,如果需要減去,則將數字改成負數。

示例如下:

> db.users.find()
{ "_id" : ObjectId("643b4d0cd21fdd4d6f0b0484"), "name" : "pjl", "age" : 18 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0486"), "name" : "pjl2", "age" : 19 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0487"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643b4d1fd21fdd4d6f0b0488"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }
// 修改 name=pjl 的資料,將 age改為19
> db.users.update({name:'pjl'}, {age: 19})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
// 替換了條資料
> db.users.find()
{ "_id" : ObjectId("643b4d0cd21fdd4d6f0b0484"), "age" : 19 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0486"), "name" : "pjl2", "age" : 19 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0487"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643b4d1fd21fdd4d6f0b0488"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }
// 透過 $set 成功更改 age 而不影響其他欄位
> db.users.update({name:'pjl2'}, {$set: {age: 20}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.find()
{ "_id" : ObjectId("643b4d0cd21fdd4d6f0b0484"), "age" : 19 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0486"), "name" : "pjl2", "age" : 20 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0487"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643b4d1fd21fdd4d6f0b0488"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }
// 給 age 增加1
> db.users.update({name:'pjl2'}, {$inc: {age: 1}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.find()
{ "_id" : ObjectId("643b4d0cd21fdd4d6f0b0484"), "age" : 19 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0486"), "name" : "pjl2", "age" : 21 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0487"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643b4d1fd21fdd4d6f0b0488"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }
// 給 age 減去1
> db.users.update({name:'pjl2'}, {$inc: {age: -1}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.find()
{ "_id" : ObjectId("643b4d0cd21fdd4d6f0b0484"), "age" : 19 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0486"), "name" : "pjl2", "age" : 20 }
{ "_id" : ObjectId("643b4d17d21fdd4d6f0b0487"), "name" : "pjl3", "age" : 20 }
{ "_id" : ObjectId("643b4d1fd21fdd4d6f0b0488"), "name2" : "pjl", "age2" : 18, "tel" : 131111111 }

表格-查詢資料

首先清空 users 表,並插入6條資料。

find

透過 db.users.find() 查詢所有資料:

> db.users.find()
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "name" : "li", "age" : 39, "tel" : "0730-1233" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "name" : "pengjia", "age" : 49, "tel" : "0730-1234" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali", "age" : 59, "tel" : "0730-1235" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
>
大於小於
  • $gt - 大於
  • $gte - 大於等於
  • $lt - 小於
  • $lte - 小於等於
// 查詢 age 大於 50 的資料
> db.users.find({age:{$gt: 50}})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali", "age" : 59, "tel" : "0730-1235" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
// 查詢 age 大於 69 的資料
> db.users.find({age:{$gt: 69}})
// 查詢 age 大於等於 69 的資料
> db.users.find({age:{$gte: 69}})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
// age 小於 20 
> db.users.find({age:{$lt: 20}})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
// 查詢 age 大於 10,小於 40 的資料
> db.users.find({age:{$gt:10, $lt:40}})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "name" : "li", "age" : 39, "tel" : "0730-1233" }
>
單條件
  • db.users.find({name: "jia"}) - 查詢 name 等於 jia 的資料
  • db.users.find({name: /jia/}) - 查詢 name 中包含 jia 的資料
  • db.users.find({name: /jia$/}) - 查詢 name 中以 jia 結尾的資料
// 查詢 name 等於 jia 的資料
> db.users.find({name: "jia"})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
// 查詢 name 中包含 jia 的資料
> db.users.find({name: /jia/})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "name" : "pengjia", "age" : 49, "tel" : "0730-1234" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali", "age" : 59, "tel" : "0730-1235" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
// 查詢 name 中以 jia 結尾的資料
> db.users.find({name: /jia$/})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "name" : "pengjia", "age" : 49, "tel" : "0730-1234" }
多條件

db.users.find({$or: [{age: 19}, {name: 'jia'}]}) 查詢 age=19 或 name=jia

// 查詢 age=19 或 name=jia
> db.users.find({$or: [{age: 19}, {name: 'jia'}]})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
列過濾
  • db.users.find({}, {name: 1}) - 只顯示 name 這個欄位。預設主鍵回返回。1 是顯示,0指不顯示
  • db.users.find({}, {name: 1, age: 1, _id: 0}) - 只要 name 和 age 欄位,主鍵不要
// 只顯示 name 這個欄位。預設主鍵回返回
> db.users.find({}, {name: 1})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "name" : "li" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "name" : "pengjia" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali" }
// 只顯示 name 這個欄位,主鍵也不要
> db.users.find({}, {name: 1, _id: 0})
{ "name" : "peng" }
{ "name" : "jia" }
{ "name" : "li" }
{ "name" : "pengjia" }
{ "name" : "pengjiali" }
{ "name" : "jiali" }
// 只要 name 和 age 欄位,主鍵不要
> db.users.find({}, {name: 1, age: 1, _id: 0})
{ "name" : "peng", "age" : 19 }
{ "name" : "jia", "age" : 29 }
{ "name" : "li", "age" : 39 }
{ "name" : "pengjia", "age" : 49 }
{ "name" : "pengjiali", "age" : 59 }
{ "name" : "jiali", "age" : 69 }
排序

透過 db.users.find().sort({age: -1}) 執行 age 逆序,正序則為1。

// age 逆序
> db.users.find({}, {age:1}).sort({age: -1})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "age" : 69 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "age" : 59 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "age" : 49 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "age" : 39 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "age" : 29 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "age" : 19 }
// age 正序
> db.users.find({}, {age:1}).sort({age: 1})
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "age" : 19 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "age" : 29 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "age" : 39 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "age" : 49 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "age" : 59 }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "age" : 69 }
分頁

分頁可以透過 skip 和 limit實現。例如 db.users.find().skip(2).limit(2) 查詢第二頁。跳過2條,查詢2條。

> db.users.find()
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "name" : "li", "age" : 39, "tel" : "0730-1233" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "name" : "pengjia", "age" : 49, "tel" : "0730-1234" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali", "age" : 59, "tel" : "0730-1235" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
// 查詢前3條
> db.users.find().limit(3)
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "name" : "li", "age" : 39, "tel" : "0730-1233" }
// 第一頁。跳過0條,查詢2條
> db.users.find().skip(0).limit(2)
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"), "name" : "peng", "age" : 19, "tel" : "0730-1231" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048b"), "name" : "jia", "age" : 29, "tel" : "0730-1232" }
// 第二頁。跳過2條,查詢2條
> db.users.find().skip(2).limit(2)
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048c"), "name" : "li", "age" : 39, "tel" : "0730-1233" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048d"), "name" : "pengjia", "age" : 49, "tel" : "0730-1234" }
// 第三頁。跳過4條,查詢2條
> db.users.find().skip(4).limit(2)
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali", "age" : 59, "tel" : "0730-1235" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
// 先 age 逆序,在取2條
> db.users.find().sort({age: -1}).skip(0).limit(2)
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048f"), "name" : "jiali", "age" : 69, "tel" : "0730-1236" }
{ "_id" : ObjectId("643bb3dfd21fdd4d6f0b048e"), "name" : "pengjiali", "age" : 59, "tel" : "0730-1235" }

透過 count() 查詢記錄數,例如 db.users.find({name: 'jia'}).count()

// 總共 6 條
> db.users.find().count()
6
// 符合條件的共 1 條
> db.users.find({name: 'jia'}).count()
1
> db.users.find({name: /jia/}).count()
4

db.users.findOne() - 查詢第一條

// 查詢第一條
> db.users.findOne()
{
        "_id" : ObjectId("643bb3dfd21fdd4d6f0b048a"),
        "name" : "peng",
        "age" : 19,
        "tel" : "0730-1231"
}

專案目錄劃分

程式那麼複雜,不可能全寫在一起,筆者做如下分層:

  • 路由層 - 匹配路由,呼叫控制層
  • 控制層 - 取得前端請求資料,加工處理,比如呼叫資料庫(services層),在返回資料給前端
  • 服務層 - 引用建立的資料庫模型,對資料進行增刪改查
  • 模型層 - 建立資料庫模型(這裡是建立表)

使用者模組為例,在這4層中分別建立如下 js:

- services            // 服務層
  - UserService.js    // User 模組的服務層
- routes              // 路由
  - UserRouter.js     // User 模組的路由。比如登入、登入
- models              // 模型層
  - UserModel.js      // User Model
- controllers         // 控制層
  - UserController.js 

連線資料庫

前面我們運算元據庫是直接進入 mongo shell 操作:

// 建立並切換到資料庫 pjl_db
use pjl_db

// 建立 users 表(這裡叫集合)
db.createCollection("users")

現在我們需要透過 node 來運算元據庫。

啟動mongo服務

透過 mongod --dbpath=/var/lib/mongodb --bind_ip=0.0.0.0 --port=27017 啟動mongo服務(未設定資料庫密碼)。其中 0.0.0.0 使用者遠端連線,埠是 27017

如果需要後臺啟動(關閉終端服務也不會停止),需要指定日誌路徑,就像這樣:

root@linux:/home/pjl# mongod --dbpath=/var/lib/mongodb --fork --logpath=/var/log/mongodb/mongodb.log --bind_ip=0.0.0.0 --port=27017
about to fork child process, waiting until server is ready for connections.
forked process: 133354
child process started successfully, parent exiting

檢視 mongod 程式:

root@linux:/home/pjl# ps aux |grep mongod
root      133354  7.5  1.5 976348 62496 ?        Sl   09:46   0:00 mongod --dbpath=/var/lib/mongodb --fork --logpath=/var/log/mongodb/mongodb.log --bind_ip=0.0.0.0 --port=27017
root      133383  0.0  0.0  12120   716 pts/0    S+   09:47   0:00 grep --color=auto mongod
root@linux:/home/pjl#

Tip:還可以透過指定配置檔案啟動:

root@linux:/home/pjl# mongod -f /etc/mongodb.conf
// 沒反應,透過 `tail -10 日誌` 能看到輸出

另起視窗進入 mongo shell,執行 show dbs 報錯如下:

// 筆者將配置檔案中埠改為 27027
root@linux:/var/log/mongodb# mongo --port=27027
MongoDB shell version v3.6.8
connecting to: mongodb://127.0.0.1:27027/
Implicit session: session { "id" : UUID("dc184887-824d-474a-a942-3d42ff1a21bf") }
MongoDB server version: 3.6.8
> show dbs
2023-04-21T09:52:04.824+0800 E QUERY    [thread1] Error: listDatabases failed:{
        "ok" : 0,
        "errmsg" : "there are no users authenticated",
        "code" : 13,
        "codeName" : "Unauthorized"
} :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
Mongo.prototype.getDBs@src/mongo/shell/mongo.js:67:1
shellHelper.show@src/mongo/shell/utils.js:860:19
shellHelper@src/mongo/shell/utils.js:750:15
@(shellhelp2):1:1
>

網友說是 安裝mongo資料庫時,配置檔案中加了安全許可權的設定,解決請看這裡

安裝 mongoose

透過 npm i mongoose -D 安裝 mongoose —— Mongoose is a MongoDB object modeling tool)。

透過 mongoose 運算元據庫非常方便

連線資料庫

連線資料庫很簡單,只需要先啟動資料庫服務,然後在啟動應用前連線資料庫即可。程式碼如下:

// bin/www.js

var http = require('http');
+ // 引入資料庫
+ require('../config/db.config')
var port = normalizePort(process.env.PORT || '3000');
// config/db.config.js

const mongoose = require("mongoose")
// mongo服務 ip
mongoose.connect("mongodb://192.168.1.223:27017/pjl_db")
mongoose.connection.once("open", () => {
    console.log('資料庫連線成功')
})

透過 mongo shell 查到目前只有3個資料庫:

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

啟動應用,控制檯輸出 資料庫連線成功:

PS E:\spug-back-end> npm run start

> spug-back-end@0.0.0 start
> nodemon ./bin/www

[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./bin/www`
資料庫連線成功

發現 pjl_db 資料庫自動被建立。

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
pjl_db  0.000GB

Tip:有了資料庫,還得需要表才能對資料進行增、刪、改、查。在 mongoose 中,需要建立一個 model(模型),可以把他當做一張表(或一條記錄),比如下文登入模組的 UserModel.js 就是一個 model,向 model 插入一條資料時,mongoose 會自動建立一張名為 users 的表(或集合)。

登入

這裡我們完成系統登入模組的開發。

app.js

app.js 引入使用者路由。

// app.js
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
+ const UserRouter = require('./routes/UserRouter')

app.use('/users', usersRouter);
+ app.use(UserRouter);

UserRouter.js

定義登入路由(/user/login),路由匹配成功,進入控制層處理。

// routes/UserRouter.js

// 使用者路由
var express = require('express');
var router = express.Router();
const UserController = require('../controllers/UserController.js')
/* POST users listing. */
router.post('/user/login', UserController.login);

module.exports = router;

UserController.js

控制層呼叫服務層,如果資料庫能透過使用者名稱和密碼查詢到該使用者則表明登入成功,否則返回使用者名稱密碼不匹配

// controllers/UserController.js

const UserService = require('../services/UserService')

const UserModel = require('../models/UserModel')
const UserController = {
    login: async (req, res) => {
        // req.body - 例如 {"username":"pjl","password":"123456"}
        console.log('res.body', JSON.stringify(res.body))
        var result = await UserService.login(req.body)
        if(result.length === 0){
            res.send({
                code: '-1',
                error: '使用者名稱密碼不匹配'
            })
        }else{
            res.send({
                code: '0',
                error: ''
            })
        }
    }
}

module.exports = UserController

UserService.js

模型層透過 model 查詢資料庫

// services/UserService.js

const UserModel = require('../models/UserModel.js')
const UserService = {
    login: async ({username, password}) => {
        return UserModel.find({
            username,
            password
        })
    }
}

module.exports = UserService

UserModel.js

mongoose 透過 model 建立表。

// models/UserModel.js

// model 與表一一對應
const mongoose = require('mongoose')
const Schema = mongoose.Schema
// 透過 schema 限制一下集合(表),否則什麼都能傳入,太自由了不好
const Usertype = {
    username: String,
    password: String,
    // 性別
    gender: Number,
    // 頭像
    avatar: String,
    // 角色
    role: Number, // 管理員1,編輯2
}

const UserModel = mongoose.model("user", new Schema(Usertype))
module.exports = UserModel

測試

這裡筆者在 git bash 中使用 curl(客戶端的url) 模擬登入(post 使用者名稱+密碼):

Administrator@ ~/Desktop
$ curl -X POST -d 'username=pjl' -d 'password=123456' http://localhost:3000/user/login
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    76  100    48  100    28   1425    831 --:--:-- --:--:-- --:--:--  2533

{"code":"-1","error":"使用者名稱密碼不匹配"}

由於 users 表中沒有資料,當然也就查詢不到(返回 {"code":"-1","error":"使用者名稱密碼不匹配"})。

筆者手動插入該條使用者資訊:

> use pjl_db
switched to db pjl_db
> db.users.save({username: 'pjl', password: '123456'})
WriteResult({ "nInserted" : 1 })

再次登入就能查詢到 {"code":"0","error":""}

Administrator@ ~/Desktop
$ curl -X POST -d 'username=pjl' -d 'password=123456' http://localhost:3000/user/login
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    51  100    23  100    28   1730   2106 --:--:-- --:--:-- --:--:--  5100

{"code":"0","error":""}

相關文章