一個簡單的 CURD 實例 ---對圖書信息進行管理
目錄
1 開發環境
- 前端:vue、axios
- 后端:node.js、express
- 數據庫:mysql
1.1 前端開發環境
新建一個文件夾book-curd-example
,以下前后端代碼都將在該文件夾下進行
- 輸入以下命令,安裝 vue 框架
cnpm install vue-cli -g
vue init webpack "client" //建立一個名稱為client的前端項目
cnpm install // 安裝依賴
npm run dev
- 安裝完畢之后,輸入
npm run dev
,在瀏覽器輸入http://localhost:8080/#
后顯示以下界面,則 client 項目生成完畢!
1.2 后端開發環境
- 在
book-curd-example
下新建一個文件夾server
文件夾用於保存后端代碼 - 進入
server
文件夾 - 安裝
express
和其他模塊
npm install express body-parser cors morgan nodemon mysql2 sequelize --save
- body-parser 解析
- cors 跨域
- morgan 日志記錄
- nodemon 程序調試自啟
- mysql2 mysql 數據庫驅動管理工具
- sequelize mysql-ORM 工具
- 安裝完成之后建立以下目錄和文件
-
使用
npm init -f
生成一個package.json
文件 -
修改為使用 nodemon 啟動
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon server.js"
},
- 在
server.js
中寫入以下代碼用於測試,在server
文件夾下輸入npm start
啟動后台程序
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const morgan = require('morgan');
const app = express();
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());
app.get('/posts', (req, res) => {
res.send([
{
title: 'Hello World!',
description: 'Hi there! How are you?'
}
]);
});
app.listen(process.env.PORT || 8081);
8.在瀏覽器中訪問http://localhost:8081/posts
,顯示以下畫面,則后台環境搭建成功。
2 數據庫設計和創建
2.1 數據庫和表設計
- 數據庫名稱:book_curd_example
- 數據庫表名稱:book
2.2 book 表設計
字段 | 中文釋義 | 類型 | 是否可為空 | 鍵 | 默認值 | 其他 |
---|---|---|---|---|---|---|
id | 書籍 id | int(10) unsigned | NO | 主鍵 | null | auto_increment |
isbn | isbn 編號 | varchar(20) | NO | null | ||
name | 書名 | varchar(50) | NO | null | ||
author | 作者 | varchar(30) | NO | null | ||
出版社 | varchar(50) | null | ||||
publish_time | 出版日期 | date | null | |||
intro | 簡介 | varchar(255) | null | |||
remark | 備注 | varchar(200) | null |
2.3 sql 語句編寫
DROP DATABASE IF EXISTS book_curd_example;
CREATE DATABASE book_curd_example;
use book_curd_example;
DROP TABLE IF EXISTS book;
CREATE TABLE IF NOT EXISTS `book`(
`id` INT UNSIGNED AUTO_INCREMENT COMMENT '書籍id',
`isbn` VARCHAR(20) NOT NULL COMMENT 'isbn編號',
`name` VARCHAR(50) NOT NULL COMMENT '書名',
`author` VARCHAR(30) NOT NULL COMMENT '作者',
`print` VARCHAR(50) COMMENT '出版社',
`publish_time` DATE COMMENT '出版日期',
`intro` VARCHAR(255) COMMENT '簡介',
`remark` VARCHAR(200)COMMENT '備注',
PRIMARY KEY ( `id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '圖書信息表';
3 后台模塊開發
3.1 創建數據庫連接
- 創建
/server/config/env.js
文件
// 數據庫連接參數
const env = {
database: 'book_curd_example',
username: 'root',
password: '123456',
host: 'localhost',
dialect: 'mysql',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
};
module.exports = env;
- 創建
/server/config/db.config.js
文件
const env = require('./env.js');
const Sequelize = require('sequelize');
const sequelize = new Sequelize(env.database, env.username, env.password, {
host: env.host,
dialect: env.dialect,
operatorsAliases: false,
pool: {
max: env.max,
min: env.pool.min,
acquire: env.pool.acquire,
idle: env.pool.idle
}
});
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
// 引入表模型
db.book = require('../model/book.model.js')(sequelize, Sequelize);
module.exports = db;
3.2 創建表模型
- 安裝
sequelize-auto
模塊,利用sequelize-auto
模塊自動生成 book 表模型
npm install -g sequelize-auto
sequelize-auto -h localhost -d book_curd_example -u root -x 123456 -p 3306 -t book
2.復制生成的/models/book.js
文件,粘貼至/model
目錄下,並修改文件名為/model/book.model.js
,刪除生成的models
目錄
參考:Model definition - 模型定義 | sequelize-docs-Zh-CN
PS: 此處可能需要根據數據庫字段的屬性進行調整,比如自增屬性
3.3 編寫接口
- 創建
/server/route/book.route.js
文件,用來定義接口
// 圖書的增刪改查
module.exports = function(app) {
const book = require('../controller/book.controller');
// 新增圖書信息
app.post('/book/add', book.create);
// 刪除圖書
app.delete('/book/delete/:bookId', book.delete);
// 根據id更新圖書信息
app.put('/book/update/:bookId', book.update);
// 獲取圖書信息列表
app.get('/book/list', book.findAll);
// 根據Id查詢圖書信息
app.get('/book/:bookId', book.findById);
};
- 創建控制器文件
/server/controller/book.controller.js
const db = require('../config/db.config.js');
const Book = db.book; // 引入表模型
// 增加圖書
exports.create = (req, res) => {
Book.create({
isbn: req.body.isbn,
name: req.body.name,
author: req.body.author,
print: req.body.print,
publish_time: req.body.publish_time,
intro: req.body.intro,
remark: req.body.remark
})
.then(book => {
let msg = {
code: 200,
msg: '新增成功!',
id: book.id
};
res.status(200).json(msg);
})
.catch(err => {
res.status(500).json('Error -> ' + err);
});
};
// 刪除圖書
exports.delete = (req, res) => {
const id = req.params.bookId;
Book.destroy({
where: { id: id }
})
.then(() => {
let msg = {
code: 200,
msg: '刪除成功!'
};
res.status(200).json(msg);
})
.catch(err => {
res.status(500).json('Error -> ' + err);
});
};
// 更新圖書信息
exports.update = (req, res) => {
const id = req.params.bookId;
Book.update(req.body, { where: { id: req.params.bookId } })
.then(() => {
let msg = {
code: 200,
msg: '修改信息成功!'
};
res.status(200).json(msg);
})
.catch(err => {
res.status(500).json('Error -> ' + err);
});
};
// 查詢所有圖書信息
exports.findAll = (req, res) => {
Book.findAll()
.then(book => {
res.json(book);
})
.catch(err => {
res.status(500).json('Error -> ' + err);
});
};
// 根據id查詢圖書信息
exports.findById = (req, res) => {
Book.findById(req.params.bookId)
.then(book => {
res.json(book);
})
.catch(err => {
res.status(500).book('Error -> ' + err);
});
};
- 修改
server.js
服務器文件
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const cors = require('cors');
const corsOptions = {
origin: 'http://localhost:8080',
optionSuccessStatus: 200
};
app.use(cors(corsOptions));
const morgan = require('morgan');
app.use(morgan('combined'));
const db = require('./config/db.config');
require('./route/book.route')(app);
// 創建服務器
let server = app.listen(process.env.PORT || 8081, () => {
let host = server.address().address;
let port = server.address().port;
console.log('服務器啟動: http://%s:%s', host, port);
});
3.4 接口測試
使用postman
工具進行測試
-
新建 5 個接口進行測試
-
新增數據接口測試
-
刪除數據接口測試
-
修改數據接口測試
-
查詢所有數據接口測試
-
查詢單個實體數據接口測試
4 前端模塊開發
4.1 安裝並引入前端開發所需外部模塊
1.安裝axios
模塊
npm install axios --save
- 編寫文件
/src/utils/http.js
,引入封裝好的 axios 類
import axios from 'axios'
let httpInstance = axios.create()
httpInstance.defaults.baseURL = 'http://localhost:8081/'
httpInstance.defaults.timeout = 5000
httpInstance.formurl = (url, data, config) => {
return httpInstance.post(url, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
...config
})
};
// request攔截器
httpInstance.interceptors.request.use(
config => {
console.log(config)
return config
},
error => {
return Promise.reject(error)
}
)
// reponse攔截器
httpInstance.interceptors.response.use(
response => {
if (response.status === 200) {
return Promise.resolve(response)
}
},
error => {
return Promise.reject(error)
}
)
export default httpInstance
- 在
main.js
中引入http.js
文件,並將其注冊為 vue 全局變量
import http from './utils/http'
Vue.prototype.$http = http
- 安裝
element-ui
模塊
npm install element-ui --save
- 在
main.js
中引入element-ui
模塊
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
4.2 建立路由
- 建立文件
在 components 下新建 3 個文件book-list.vue
、book-detail.vue
、book-add.vue
。刪除原有的HelloWorld.vue
文件。 - 修改路由
在router/main.js
中將路由修改如下
import Vue from 'vue'
import Router from 'vue-router'
import BookList from '@/components/book-list'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'book-list',
component: BookList
}
]
})
- 刪除
App.vue
文件中以下代碼
<img src="./assets/logo.png">
- 在
book-list.vue
文件中寫入以下代碼
<template>
<div>
Hello World!
</div>
</template>
<script>
export default {}
</script>
<style scoped>
</style>
- 使用
npm start
運行項目,在瀏覽器中訪問,則會出現Hello World
的文字
4.3 編寫組件
book-list.vue
(1)效果圖
(2) 代碼
<template>
<div>
<header>圖書列表</header>
<div class="container">
<div class="operate-btn">
<el-button @click="addBook">新增圖書</el-button>
</div>
<el-table :data="tableData"
border
style="width: 100%">
<el-table-column type="index">
</el-table-column>
<el-table-column prop="name"
label="圖書名稱"
min-width="180px">
</el-table-column>
<el-table-column prop="isbn"
label="ISBN編號"
min-width="180px">
</el-table-column>
<el-table-column prop="author"
label="作者"
min-width="180px">
</el-table-column>
<el-table-column prop="print"
label="出版社"
min-width="180px">
</el-table-column>
<el-table-column prop="publish_time"
label="出版日期"
min-width="180px">
</el-table-column>
<el-table-column label="操作"
min-width="200px">
<template slot-scope="scope">
<el-button size="mini"
@click="handleDetail(scope.$index, scope.row)">查看</el-button>
<el-button size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<book-detail :bookId="bookId"
:visible="bookDetailVisible"
@closedDialog="closedDetailDialog">
</book-detail>
<book-add :visible="bookAddVisible"
@closedDialog="closeAddDialog"
@addNewBook="addNewBook">
</book-add>
</div>
</template>
<script>
import BookDetail from './book-detail';
import BookAdd from './book-add';
export default {
components: {
BookDetail,
BookAdd
},
mounted () {
this.getBookList()
},
data () {
return {
tableData: [],
bookId: null,
bookDetailVisible: false,
bookAddVisible: false
}
},
methods: {
addNewBook (val) {
this.bookId = val
this.bookDetailVisible = true
},
addBook () {
this.bookAddVisible = true
},
refreshBookList () {
this.getBookList()
},
closeAddDialog () {
this.bookAddVisible = false
this.refreshBookList()
},
closedDetailDialog () {
this.bookDetailVisible = false
this.refreshBookList()
},
handleDelete (index, row) {
this.$http
.delete(`/book/delete/${row.id}`)
.then(res => {
this.$message.success(res.data.msg)
this.refreshBookList()
})
.catch(err => {
console.log('err=>', err)
})
},
handleDetail (index, row) {
this.bookId = row.id
this.bookDetailVisible = true
},
getBookList () {
this.$http
.get('/book/list')
.then(res => {
this.tableData = res.data
})
.catch(err => {
console.log('err->', err)
})
}
}
}
</script>
<style scoped>
header {
font-size: 36px;
height: 60px;
padding-top: 30px;
padding-left: 40px;
box-shadow: 0px 15px 10px -15px #ccc;
margin-bottom: 10px;
}
.container {
text-align: center;
box-shadow: 0px -15px 10px -15px #ccc;
padding: 30px;
}
.el-table {
padding-top: 20px;
}
.operate-btn {
text-align: right;
margin-bottom: 10px;
}
</style>
book-add.vue
(1)效果圖
(2)代碼
<template>
<el-dialog :visible.sync="dialogVisible"
@closed="closedDialog"
min-width="360px">
<div slot="title">
<span class="title-name">
<span>新增圖書</span>
</span>
</div>
<el-row>
<el-col :span="4">
<div class="label">名稱</div>
</el-col>
<el-col :span="20">
<el-input v-model="bookInfo.name"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">ISBN編號</div>
</el-col>
<el-col :span="20">
<el-input v-model="bookInfo.isbn"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">作者</div>
</el-col>
<el-col :span="20">
<el-input v-model="bookInfo.author"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">出版社</div>
</el-col>
<el-col :span="20">
<el-input v-model="bookInfo.print"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">出版日期</div>
</el-col>
<el-col :span="20">
<el-date-picker v-model="bookInfo.publish_time"
type="date"
placeholder="選擇日期"
size="medium">
</el-date-picker>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">簡介</div>
</el-col>
<el-col :span="20">
<el-input type="textarea"
:autosize="{ minRows: 2, maxRows: 4}"
placeholder="請輸入內容"
v-model="bookInfo.intro"
max-length="200">
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">其他</div>
</el-col>
<el-col :span="20">
<el-input type="textarea"
:autosize="{ minRows: 2, maxRows: 4}"
placeholder="請輸入內容"
v-model="bookInfo.remark"
max-length="200">
</el-input>
</el-col>
</el-row>
<div slot="footer"
class="dialog-footer">
<el-button @click="cancelEdit"
size="medium">取 消</el-button>
<el-button type="primary"
@click="addBook"
size="medium">確 定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
props: {
visible: {
type: Boolean
}
},
watch: {
visible: {
handler (newV, oldV) {
this.dialogVisible = newV
}
}
},
mounted () {},
data () {
return {
dialogVisible: false,
bookInfo: {}
}
},
methods: {
addBook () {
this.$http
.post('/book/add', this.bookInfo)
.then(res => {
this.$message.success(res.data.msg)
let bookId = res.data.id
setTimeout(() => {
this.$emit('addNewBook', bookId)
this.closedDialog()
}, 1000)
})
.catch(err => {
console.log('err=>', err)
})
},
cancelEdit () {
this.closedDialog()
},
resetData () {
this.dialogVisible = false
this.bookInfo = {}
},
closedDialog () {
this.$emit('closedDialog')
this.resetData()
}
}
}
</script>
<style scoped>
.el-row {
line-height: 40px;
margin-top: 10px;
}
.label {
font-weight: bold;
}
.edit-btn {
margin-left: 10px;
}
.title-name {
font-size: 30px;
}
.dialog-footer {
text-align: center;
}
</style>
book-detail.vue
(1)效果圖
(2)代碼
<template>
<el-dialog :visible.sync="dialogVisible"
@closed="closedDialog">
<div slot="title">
<span class="title-name">圖書信息</span>
<el-button size="small"
icon="el-icon-edit"
round
class="edit-btn"
@click="editBookInfo">編輯</el-button>
</div>
<el-row>
<el-col :span="4">
<div class="label">名稱</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.name}}</span>
<el-input v-model="bookInfo.name"
v-if="isEdit"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">ISBN編號</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.isbn}}</span>
<el-input v-if="isEdit"
v-model="bookInfo.isbn"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">作者</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.author}}</span>
<el-input v-if="isEdit"
v-model="bookInfo.author"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">出版社</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.print}}</span>
<el-input v-if="isEdit"
v-model="bookInfo.print"
size="medium"></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">出版日期</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.publish_time}}</span>
<el-date-picker v-if="isEdit"
v-model="bookInfo.publish_time"
type="date"
placeholder="選擇日期"
size="medium">
</el-date-picker>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">簡介</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.intro}}</span>
<el-input v-if="isEdit"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4}"
placeholder="請輸入內容"
v-model="bookInfo.intro"
max-length="200">
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="4">
<div class="label">其他</div>
</el-col>
<el-col :span="20">
<span v-if="!isEdit">{{bookInfo.remark}}</span>
<el-input type="textarea"
v-if="isEdit"
:autosize="{ minRows: 2, maxRows: 4}"
placeholder="請輸入內容"
v-model="bookInfo.remark"
max-length="200">
</el-input>
</el-col>
</el-row>
<div slot="footer"
class="dialog-footer"
v-if="isEdit">
<el-button @click="cancelEdit"
size="medium">取 消</el-button>
<el-button type="primary"
@click="updateBookInfo"
size="medium">確 定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
props: {
bookId: {
type: Number
},
visible: {
type: Boolean
}
},
watch: {
visible: {
handler (newV, oldV) {
this.dialogVisible = newV
if (this.dialogVisible) {
this.getBookById()
}
}
}
},
mounted () {},
data () {
return {
dialogVisible: false,
bookInfo: {},
isEdit: false
}
},
methods: {
refreshBookInfo () {
this.getBookById()
},
updateBookInfo () {
this.$http
.put(`/book/update/${this.bookId}`, this.bookInfo)
.then(res => {
console.log(this.$message)
this.$message.success(res.data.msg)
this.isEdit = false
this.refreshBookInfo()
})
.catch(err => {
console.log('err->', err)
this.isEdit = false
})
},
cancelEdit () {
this.isEdit = false
},
resetData () {
this.dialogVisible = false
this.bookInfo = {}
this.isEdit = false
},
closedDialog () {
this.$emit('closedDialog')
this.resetData()
},
getBookById () {
this.$http
.get(`/book/${this.bookId}`)
.then(res => {
this.bookInfo = res.data
})
.catch(err => {
console.log('err->', err)
})
},
editBookInfo () {
this.isEdit = true
}
}
}
</script>
<style scoped>
.el-row {
line-height: 40px;
margin-top: 10px;
}
.label {
font-weight: bold;
}
.edit-btn {
margin-left: 10px;
}
.title-name {
font-size: 30px;
}
.dialog-footer {
text-align: center;
}
</style>