前后端分离架构如下图,后端只负责动态提供数据,而前端存放静态文件,使得后端服务器不必每次都返回一整个HTML页面,节省了时间开销和资源开销,提高了整个网站系统的性能。
RESTful架构,是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。
REST(Representational State Transfer)翻译为"表现层状态转化",所谓表现层,正如下图中的前端静态文件服务器,在此前端服务器上部署nginx服务,搭建起一个网站前端,使得用户可以通过访问前端服务器网址来浏览前端页面。
而所谓状态转化,即当用户访问一个网站时,意味着客户端和服务器有一个互动过程。在这个过程中,势必涉及到数据和状态的变化。在前后端分离架构中,前端只提供静态页面,而数据则由后端提供,使得前端呈现出来的数据得以动态变化。
实验名称:基于Golang+Vue+Nginx搭建图书管理系统
实验环境:两台Centos7虚拟机
1、后端:node1,ip:192.168.141.53
2、前端:node2,ip:192.168.141.69
1、node1安装go,配置好环境变量。
[root@node1 ~]# wget https://dl.google.com/go/go1.14.linux-amd64.tar.gz
[root@node1 ~]# tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz
[root@node1 ~]# rm -f go1.14.linux-amd64.tar.gz
[root@node1 ~]# mkdir -p /home/gopath
[root@node1 ~]# vim /etc/profile
#文档末尾追加下列四行
export GOROOT=/usr/local/go
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN
export GOPATH=/home/gopath
[root@node1 ~]# source /etc/profile
[root@node1 ~]# go version
go version go1.14 linux/amd64
[root@node1 ~]#
2、准备工作
1)node2安装nginx,放行80端口,关闭selinux,重启nginx服务。
[root@node2 ~]# yum install epel-release -y
[root@node2 ~]# yum install nginx -y
[root@node2 ~]# firewall-cmd --add-port=80/tcp --permanent
success
[root@node2 ~]# firewall-cmd --reload
[root@node2 ~]# vim /etc/selinux/config
[root@node2 ~]# cat /etc/selinux/config | grep SELINUX
SELINUX=disabled
[root@node2 ~]# setenforce 0
success
[root@node2 ~]# systemctl restart nginx
[root@node2 ~]# nginx -s reload
[root@node2 ~]#
现在打开浏览器输入node2的ip即可浏览到nginx的默认index页面。
2)node1放行9090端口,关闭selinux。
[root@node1 ~]# firewall-cmd --add-port=9090/tcp --permanent
success
[root@node1 ~]# firewall-cmd --reload
[root@node1 ~]# vim /etc/selinux/config
[root@node1 ~]# cat /etc/selinux/config | grep SELINUX
SELINUX=disabled
[root@node1 ~]# setenforce 0
success
[root@node1 ~]#
3、node1运行后端go文件
[root@node1 ~]# cd /home/gopath/
[root@node1 gopath]# go get github.com/gorilla/mux
[root@node1 gopath]# vim rest.go
[root@node1 gopath]# cat rest.go
package main
import (
"encoding/json"
"github.com/gorilla/mux"
"log"
"math/rand"
"net/http"
"strconv"
)
//Book Struct
type Book struct{
ID string `json:"id"`
Isbn string `json:"isbn"`
Title string `json:"title"`
Author *Author `json:"author"`
}
//Author Struct
type Author struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
}
//Init books var as a slice(切片) Book struct
var books []Book
//Get all Books
func getBooks(w http.ResponseWriter,r *http.Request){
w.Header().Set("Content-Type","application/json")
json.NewEncoder(w).Encode(books)
}
//Get single Books
func getBook(w http.ResponseWriter,r *http.Request){
w.Header().Set("Content-Type","application/json")
params:=mux.Vars(r) //参数
for _,item:=range books{
if item.ID==params["id"]{
json.NewEncoder(w).Encode(item)
return
}
}
json.NewEncoder(w).Encode(&Book{})
}
//Create a new Book
func createBook(w http.ResponseWriter,r *http.Request){
w.Header().Set("Content-Type","application/json")
var book Book
_ = json.NewDecoder(r.Body).Decode(&book)
book.ID=strconv.Itoa(rand.Intn(10000000)) //ID用随机数模拟,不安全
books=append(books,book)
json.NewEncoder(w).Encode(book)
}
func updateBook(w http.ResponseWriter,r *http.Request){
w.Header().Set("Content-Type","application/json")
params:=mux.Vars(r) //参数
for index,item:=range books{
if item.ID==params["id"]{
var book Book
_ = json.NewDecoder(r.Body).Decode(&book)
book.ID=params["id"] //ID用随机数模拟,不安全
books[index]=book
json.NewEncoder(w).Encode(book)
}
}
json.NewEncoder(w).Encode(&Book{})
}
func deleteBook(w http.ResponseWriter,r *http.Request){
w.Header().Set("Content-Type","application/json")
params:=mux.Vars(r) //参数
for index,item:=range books{
if item.ID==params["id"]{
books=append(books[:index],books[index+1:]...)
break
}
}
json.NewEncoder(w).Encode(books)
}
func main(){
//Init Router
r:=mux.NewRouter()
//mock Data 模拟数据
books=append(books,Book{
ID:"1",
Isbn:"448743",
Title:"Book one",
Author:&Author{
Firstname:"John",
Lastname:"Doe",
},
})
books=append(books,Book{
ID:"2",
Isbn:"448232",
Title:"Book two",
Author:&Author{
Firstname:"Tom",
Lastname:"Das",
},
})
books=append(books,Book{
ID:"3",
Isbn:"412343",
Title:"Book three",
Author:&Author{
Firstname:"Jerry",
Lastname:"Dac",
},
})
//Route Handlers
r.HandleFunc("/api/books",getBooks).Methods("GET")
r.HandleFunc("/api/books/{id}",getBook).Methods("GET")
r.HandleFunc("/api/books",createBook).Methods("POST")
r.HandleFunc("/api/books/{id}",updateBook).Methods("PUT")
r.HandleFunc("/api/books/{id}",deleteBook).Methods("DELETE")
log.Fatal(http.ListenAndServe(":9090",r))
}
[root@node1 gopath]# go build rest.go
[root@node1 gopath]# chmod +x rest
[root@node1 gopath]# ./rest
此时node1上就运行着后端程序了,使用浏览器访问上述程序指定的链接地址即可获取后端数据,如下图:
node1后端数据链接地址:http://192.168.141.53:9090/api/books
4、node2编写前端网页
[root@node2 ~]# cd /usr/share/nginx/html/
[root@node2 html]# rm -f index.html
[root@node2 html]# vim index.html
[root@node2 html]# cat index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<link rel="shortcut icon" href="images/favicon.ico">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.staticfile.org/vue/2.4.2/vue.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<style>
#add{
width:800px;
padding:20px;
margin:10px auto;
}
.tb{
border-collapse:collapse;
width: 100%;
}
.tb th{
background-color: #0094ff;
color:white;
}
.tb td,.tb th{
padding:5px;
border:1px solid black;
text-align: center;
}
.add{
padding: 5px;
border:1px solid black;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="app">
<div class="add">
isbn:<input type="text" v-model="newIsbn" ref="idRef">
title:<input type="text" v-model="newTitle" ref="titleRef">
author's firstname:
author's lastname:<input type="text" v-model="lastName" ref="firstnameRef">
<input type="button" value="添加" @click="addData">
</div>
<br>
<div class="add">
搜索书本:<input type="text" placeholder="请输入书本ID:" v-model="searchId" @keydown.enter="search">
<table class="tb">
<tr>
<th>编号</th>
<th>ISBN</th>
<th>标题</th>
<th>作者</th>
</tr>
<tr>
<td><b>{{searchResult.id}}</b></td>
<td><b>{{searchResult.isbn}}</b></td>
<td><b>{{searchResult.title}}</b></td>
<td><b v-for="(v, k) in searchResult.author">{{ v }} </b></td>
</tr>
<tr>
<td colspan="4"><br></td>
</tr>
</table>
</div>
<br>
<div>
<table class="tb">
<tr>
<th>编号</th>
<th>ISBN</th>
<th>标题</th>
<th>作者</th>
<th>操作</th>
</tr>
<tr v-for="item in list">
<td>{{item.id}}</td>
<td>{{item.isbn}}</td>
<td>{{item.title}}</td>
<td>{{item.author.firstname}}-{{item.author.lastname}}</td>
<td>
<button @click="deleteData(item.id)">删除</button>
</td>
</tr>
<tr v-if="list.length === 0">
<td colspan="5">没有书本数据</td>
</tr>
</table>
</div>
</div>
<div align="center">
<br>
<br>
<img border="0" src='images/2.gif' alt="Pulpit rock">
</div>
</body>
<script>
let vm = new Vue({
el: '#app',
data: {
newIsbn: '',
newTitle: '',
firstName:'',
lastName:'',
list: [],
searchResult:{},
searchId: ''
},
methods: {
// 添加数据
addData() {
let url = 'http://192.168.141.69/api/books'
axios.post(url, {
"isbn": this.newIsbn,
"title": this.newTitle,
"author": {
"firstname": this.firstName,
"lastname": this.lastName
}
})
.then(res => {
// console.log(res);
if(res.status === 200) {
// 重新刷新一下列表数据
this.getList()
}
})
},
// 获取列表数据方法
getList() {
axios
.get('http://192.168.141.69/api/books')
.then(response => {
Vue.set(this.list = response.data);
console.log(this.list);
this.getSearchList()
})
.catch(err => {
console.log(err);
})
},
// 搜索方法
search() {
let url = 'http://192.168.141.69/api/books/'
axios.get(url+this.searchId)
.then(response => {
Vue.set(this.searchResult = response.data);
console.log(this.searchResult);
})
.catch(err => {
console.log(err);
})
},
// 删除方法
deleteData(id) {
let url = `http://192.168.141.69/api/books/${id}`
axios.delete(url)
.then(res => {
this.getList()
})
}
},
mounted () {
this.getList()
}
})
</script>
</html>
[root@node2 html]#
说明:在上述html代码中,vue获取后端数据时,本来一开始使用的是node1后端数据链接地址(http://192.168.141.53:9090/api/books),因为在上一步中我们也的确成功得从这个链接地址获取到了后台的json数据。但是后面访问前端服务器网站主页(http://192.168.141.69/)的时候,就会发现数据没办法显示出来,这就是跨域问题。
跨域是浏览器行为,不是服务器行为。例如两个链接(http://192.168.141.53:9090/api/books、http://192.168.141.69/)域名(ip)和端口都不一样,浏览器不会允许你html内嵌的js代码去跨域获取数据。
因此,在上述html代码中,必须将所有后端链接http://192.168.141.53:9090/api/books替换为与http://192.168.141.69/相同域的前端链接,以此来避免跨域。
那么,如何使访问前端链接http://192.168.141.69/api/books同样能得到后端的数据呢?这就需要用到反向代理了。利用反向代理将用户对动态数据的请求转发到后端服务器,而用户对静态资源的请求则由自己前端来响应。
5、node2配置反向代理
[root@node2 ~]# cd /etc/nginx/conf.d/
[root@node2 conf.d]# vim proxy.conf
[root@node2 conf.d]# cat proxy.conf
server{
listen 80;
server_name 192.168.141.69;
location / {
root html;
index index.html index.htm;
}
location /api/books {
rewrite ^/api/books/(.*)$ /$1 break;
proxy_pass http://192.168.141.53:9090;
}
location /api/books/ {
rewrite ^/api/books//(.*)$ /$2 break;
proxy_pass http://192.168.141.53:9090;
}
}
[root@node2 conf.d]#
说明:在html的vue部分代码中,请求后端的数据总共有以下链接:
http://192.168.141.69/api/books
http://192.168.141.69/api/books/
http://192.168.141.69/api/books/${id}
因此上述proxy.conf就有了针对/api/books和/api/books/的两个location代理配置。
6、node2添加图片资源文件
在如下目录新建images目录,找两个图片,上传到该目录下即可
[root@node2 conf.d]# cd /usr/share/nginx/html/
[root@node2 html]# ls
404.html 50x.html en-US icons images img index.html nginx-logo.png poweredby.png
[root@node2 html]# cd images/
[root@node2 images]# ls
2.gif favicon.ico
[root@node2 images]#
7、重启nginx
[root@node2 images]# cd
[root@node2 ~]# systemctl restart nginx
[root@node2 ~]# nginx -s reload
[root@node2 ~]#
8、浏览前端网页(http://192.168.141.69/),如下图:
9、功能调试
如下图,直接不输入信息添加,则生成只有随机ID的表项。
如上图,增加删除查找功能均正常,至于数据更新可以直接删除后再添加即可。
前后端分离实验成功,但是没有数据库的系统不能称之为一个完整的系统。上述图书管理系统依托于后端运行着的rest文件,一旦该程序关闭或者重启,那么必将导致数据的丢失。
现在的系统相当于将数据保存在内存而并非硬盘里,没有持久化存储,因此需要加上后端数据库,用以将用户数据永久保存下来,这样即使rest重启,数据依然还是上次用户存储好的数据。
篇幅有限,给这个图书馆里系统加上后端数据库的实验将在另一篇博客中进行记录。
Glang+Vue+Mysql+Nginx实现前后端分离实例(二)