0%

目录


1. 持续集成介绍

1.1 概念

1.2 持续集成的好处

2. GitLab持续集成(CI)

2.1 简介

2.2 GitLab简单原理图

2.3 GitLab持续集成所需环境

2.4 需要了解知识

3. 搭建GitLab持续集成环境(NET版)

3.1 环境搭建

3.1.1 基础环境搭建

3.1.2 Git安装

3.1.3 NuGet安装

3.2 相关配置

3.2.1 Git环境变量配置

3.2.2 PowerShell调用测试

3.2.3 GitLab-Runner下载

3.3 GitLab查看项目的Runners

3.4 构建GitLab-Runner服务

3.4.1 介绍

3.4.2 下载软件(没下载的请下载)

3.4.3 注册信息

3.4.4 开启gitlab-runner服务

3.4.5 修改协议config.toml文件(重要)

3.5 构建.gitlab-ci.yml脚本

3.6 完成配置

4. 常见问题解决

4.1 GitLab出现Pending卡住

4.2 GitLab CI乱码问题

4.3 明明错误,但Build成功

4.4 .gitlab-ci.yml脚本错误


1. 持续集成介绍


1.1 概念

持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

1.2 持续集成的好处

1)快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。

2)防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。

2. GitLab****持续集成(CI)


2.1 简介

在GitLab 8.0+提供了持续集成的功能,在GitLab中有个Runners的概念。

Runner一共有三种类型

  1. 本地Runner

  2. 普通的服务器上的Runner

  3. 基于Docker的Runner

2.2 GitLab****简单原理图

本文只介绍GitLab对NET进行持续集成

2.3 GitLab****持续集成所需环境

开发环境:VS2015、Git

GitLab****服务器环境:GitLab 8.0+

Runner-CI****服务器:window、Git、Msbuild、Nuget、PowerShell、GitLab-Runner

2.4 需要了解知识

Git操作、GitLab、Msbuild&Nuget命令行、Powershell命令行

3. 搭建GitLab持续集成环境(NET版)


3.1 环境搭建

3.1.1 基础环境****搭建

找一台电脑(服务器最好)系统安装为window 7(x64,改成英文版最好),并且机子安装了.net framework4.0运行环境(里面要有MsBuild)

3.1.2 Git****安装

安装Git,下载地址 https://git-scm.com/download/win

3.1.3 NuGet****安装

安装NuGet.exe,下载地址:http://nuget.codeplex.com/downloads/get/669083

3.2 相关配置

3.2.1 Git****环境变量配置

计算机右键—>属性里单击选择—>环境变量

Git 目录下的 bin(如 C:\Program Files (x86)\Git\bin)添加到 PATH 环境变量。

如下图:选择 PATH编辑,将 bin 的路径(C:\Program Files (x86)\Git\bin)添加到变量值

详细配置参考方法(二选一即可)

【手动配置环境变量】

http://jingyan.baidu.com/article/fec4bce271601ff2618d8be3.html

【Git安装自动配置环境变量】

http://jingyan.baidu.com/article/9f7e7ec0b17cac6f2815548d.html

3.2.2 PowerShell****调用测试

PowerShell是调用方式(GitLab提供很多种方式),本文只针对PowerShell方式进行演示。PowerShell可以理解为就是cmd的升级版。

打开PowerShell,测试Git、MsBuild、NuGet命令行能否在PowerShell中使用(如果不想测试,请继续往下看)。

举例:

测试Git

3.2.3 GitLab-Runner****下载

首先,下载gitlab-ci-multi-runner-windows-amd64,并将其放到C:\CI

下载地址:

https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-ci-multi-runner-windows-amd64.exe

3.3 GitLab****查看项目的Runners

点击一个项目->Settings->Runners, 得到Url****地址①registration token****②

3.4 构建GitLab-Runner服务

3.4.1 介绍

基础环境已经搭建完成,如何将这台计算机真正变成一台Runner-CI服务器,我们需要详细介绍一下。

3.4.2 下载软件(没下载的请下载)

首先,下载gitlab-ci-multi-runner-windows-amd64,并将其放到 D:\CI_Test

下载地址:

https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-ci-multi-runner-windows-amd64.exe

3.4.3 注册信息

运行cmd命令(以管理员身份打开cmd)

输入命令为:

cd \

cd ci

gitlab-ci-multi-runner-windows-amd64.exe register

根据提示,填写

  1. GitLab->Runners的Url地址①

  2. GitLab->Runners的registration token②

  3. runner名称,这个随便写

  4. 分支名,master

  5. 协议方式,shell

如下图填写信息红色部分

3.4.4 开启gitlab-runner服务

输入开启命令,并检查window服务中和GitLab->Runners中是否开启成功

gitlab-ci-multi-runner-windows-amd64.exe install

gitlab-ci-multi-runner-windows-amd64.exe start

[

3.4.5 修改协议config.toml文件(重要)

注册成功后,在文件夹中找到config.toml,在[[runners]]后面添加**shell = “powershell”**节点

3.5 构建.gitlab-ci.yml脚本

【.gitlab-ci.yml内容为】

复制代码

stages: - build

job:

stage: build

script: - echo “Restoring NuGet Packages…”

  • C:\test\nuget.exe restore “ConsoleApplication1.sln”

  • echo “Solution Build…”

  • C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /p:Configuration=Debug /p:Platform=”Any CPU” /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet “ConsoleApplication1.sln” tags:

except: - tags

复制代码

下图红框中的命令,只要将路径修改为”ConsoleApplication1.sln”的实际路径就能直接从Powershell中运行。注意:如果报错”ConsoleApplication1.sln”找不到可以尝试变为”src/ConsoleApplication1.sln”

3.6 完成配置

提交代码测试

成功:点击查看成功日志

失败:点击查看错误日志

编译中:点击查看编译中的日志

.gitlab-ci.yml脚本错误:,点击跳转到.gitlab-ci.yml验证页面

4. 常见问题解决


4.1 GitLab****出现Pending卡住

请检查Runner-CI服务器的GitLab-Runner服务是否安装成功,Runners中的Url地址①是否正确。

4.2 GitLab CI****乱码问题

GitLab返回信息乱码,一般是因为GitLab不能识别中文,一般乱码是PowerShell返回的中文,把PowerShell脚本独立运行看看是否报错。所以推荐window搞成英文版的,要是哪位大侠知道GitLab怎么识别中文麻烦分享下哈。

4.3 明明错误,但Build成功

错误截图如下,请检查Git环境变量是否配置,PowerShell脚本是否独立为一个文件

4.4 .gitlab-ci.yml****脚本错误

点击Lint,进行脚本验证,参考资料http://docs.gitlab.com/ee/ci/yaml/README.html

注意

image058

感谢


劈荆斩棘:Gitlab 部署 CI 持续集成  感谢这篇文章让我少走了很多弯路

https://juejin.im/post/6887751398499287054?utm\_source=gold\_browser\_extension#heading-3

Gitea 用于构建 Git 局域网服务器,Jenkins 是 CI/CD 工具,用于部署前端项目。

配置 Gitea

下载Gitea,选择一个喜欢的版本,例如 1.13,选择gitea-1.13-windows-4.0-amd64.exe下载。

下载完后,新建一个目录(例如 gitea),将下载的 Gitea 软件放到该目录下,双击运行。

打开localhost:3000就能看到 Gitea 已经运行在你的电脑上了。

点击注册,第一次会弹出一个初始配置页面,数据库选择SQLite3。另外把localhost改成你电脑的局域网地址,例如我的电脑 IP 为192.168.0.118。

![](Gitea + Jenkins 自动构建前端项目并部署到服务器/webp.webp)

![](Gitea + Jenkins 自动构建前端项目并部署到服务器/webp.webp)

填完信息后,点击立即安装,等待一会,即可完成配置。

继续点击注册用户,第一个注册的用户将会成会管理员。

打开 Gitea 的安装目录,找到custom\conf\app.ini,在里面加上一行代码START_SSH_SERVER = true。这时就可以使用 ssh 进行 push 操作了。

8. 如果使用 http 的方式无法克隆项目,请取消 git 代理。

git config –global –unset http.proxygit config –global –unset https.proxy复制代码

配置 Jenkins

需要提前安装 JDK,JDK 安装教程网上很多,请自行搜索。

打开Jenkins下载页面。

安装过程中遇到Logon Type时,选择第一个。

端口默认为 8080,这里我填的是 8000。安装完会自动打开http://localhost:8000网站,这时需要等待一会,进行初始化。

按照提示找到对应的文件(直接复制路径在我的电脑中打开),其中有管理员密码。

6. 安装插件,选择第一个。

创建管理员用户,点击完成并保存,然后一路下一步。

8. 配置完成后自动进入首页,这时点击Manage Jenkins->Manage plugins安装插件。

9. 点击可选插件,输入 nodejs,搜索插件,然后安装。10. 安装完成后回到首页,点击Manage Jenkins->Global Tool Configuration配置 nodejs。如果你的电脑是 win7 的话,nodejs 版本最好不要太高,选择 v12 左右的就行。

创建静态服务器

建立一个空目录,在里面执行npm init -y,初始化项目。

执行npm i express下载 express。

然后建立一个server.js文件,代码如下:

constexpress =require(‘express’)constapp = express()constport =8080app.use(express.static(‘dist’))app.listen(port,() =>{console.log(`Example app listening at http://localhost:${port}\`)})复制代码

它将当前目录下的dist文件夹设为静态服务器资源目录,然后执行node server.js启动服务器。

由于现在没有dist文件夹,所以访问网站是空页面。

不过不要着急,一会就能看到内容了。

自动构建 + 部署到服务器

下载 Jenkins 提供的 demo 项目building-a-multibranch-pipeline-project,然后在你的 Gitea 新建一个仓库,把内容克隆进去,并提交到 Gitea 服务器。

2. 打开 Jenkins 首页,点击新建 Item创建项目。

3. 选择源码管理,输入你的 Gitea 上的仓库地址。

你也可以尝试一下定时构建,下面这个代码表示每 5 分钟构建一次。

选择你的构建环境,这里选择刚才配置的 nodejs。

6. 点击增加构建步骤,windows 要选execute windows batch command,linux 要选execute shell。

输入npm i && npm run build && xcopy .\build\* G:\node-server\dist\ /s/e/y,这行命令的作用是安装依赖,构建项目,并将构建后的静态资源复制到指定目录G:\node-server\dist\ 。这个目录是静态服务器资源目录。

8. 保存后,返回首页。点击项目旁边的小三角,选择build now。

9. 开始构建项目,我们可以点击项目查看构建过程。

10. 构建成功,打开http://localhost:8080/看一下结果。

11. 由于刚才设置了每 5 分钟构建一次,我们可以改变一下网站的内容,然后什么都不做,等待一会再打开网站看看。

12. 把修改的内容提交到 Gitea 服务器,稍等一会。打开网站,发现内容已经发生了变化。

使用 pipeline 构建项目

使用流水线构建项目可以结合 Gitea 的webhook钩子,以便在执行git push的时候,自动构建项目。

点击首页右上角的用户名,选择设置。

添加 token,记得将 token 保存起来。

打开 Jenkins 首页,点击新建 Item创建项目。

4. 点击构建触发器,选择触发远程构建,填入刚才创建的 token。

5. 选择流水线,按照提示输入内容,然后点击保存。

6. 打开 Jenkins 安装目录下的jenkins.xml文件,找到标签,在里面加上-Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true。它的作用是关闭CSRF验证,不关的话,Gitea 的webhook会一直报 403 错误,无法使用。加好参数后,在该目录命令行下输入jenkins.exe restart重启 Jenkins。

7. 回到首页,配置全局安全选项。勾上匿名用户具有可读权限,再保存。

打开你的 Gitea 仓库页面,选择仓库设置。

点击管理 web 钩子,添加 web 钩子,钩子选项选择Gitea。

目标 URL 按照 Jenkins 的提示输入内容。然后点击添加 web 钩子。

11. 点击创建好的 web 钩子,拉到下方,点击测试推送。不出意外,应该能看到推送成功的消息,此时回到 Jenkins 首页,发现已经在构建项目了。

12. 由于没有配置Jenkinsfile文件,此时构建是不会成功的。所以接下来需要配置一下Jenkinsfile文件。将以下代码复制到你 Gitea 项目下的Jenkinsfile文件。jenkins 在构建时会自动读取文件的内容执行构建及部署操作。

pipeline {    agent any    stages {        stage(‘Build’) {            steps {  // window 使用 bat, linux 使用 sh                bat ‘npm i’                bat ‘npm run build’            }        }        stage(‘Deploy’) {            steps {                bat ‘xcopy .\\build\\* D:\\node-server\\dist\\ /s/e/y’ // 这里需要改成你的静态服务器资源目录            }        }    }}复制代码

每当你的 Gitea 项目执行push操作时,Gitea 都会通过webhook发送一个 post 请求给 Jenkins,让它执行构建及部署操作。

小结

如果你的操作系统是 Linux,可以在 Jenkins 打包完成后,使用 ssh 远程登录到阿里云,将打包后的文件复制到阿里云上的静态服务器上,这样就能实现阿里云自动部署了。具体怎么远程登录到阿里云,请看下文中的 《Github Actions 部署到阿里云》 一节。

Github Actions 自动构建前端项目并部署到服务器

如果你的项目是 Github 项目,那么使用 Github Actions 也许是更好的选择。

部署到 Github Page

接下来看一下如何使用 Github Actions 部署到 Github Page。

在你需要部署到 Github Page 的项目下,建立一个 yml 文件,放在.github/workflow目录下。你可以命名为ci.yml,它类似于 Jenkins 的Jenkinsfile文件,里面包含的是要自动执行的脚本代码。

这个 yml 文件的内容如下:

name:BuildandDeployon:# 监听 master 分支上的 push 事件push:branches:-masterjobs:build-and-deploy:runs-on:ubuntu-latest# 构建环境使用 ubuntusteps:-name:Checkoutuses:actions/checkout@v2.3.1with:persist-credentials:false-name:InstallandBuild# 下载依赖 打包项目run:|

          npm install

          npm run build-name:Deploy# 将打包内容发布到 github pageuses:JamesIves/github-pages-deploy-action@3.5.9# 使用别人写好的 actionswith:# 自定义环境变量ACCESS_TOKEN:$# VUE_ADMIN_TEMPLATE 是我的 secret 名称,需要替换成你的BRANCH:masterFOLDER:distREPOSITORY_NAME:woai3c/woai3c.github.io# 这是我的 github page 仓库TARGET_FOLDER:github-actions-demo# 打包的文件将放到静态服务器 github-actions-demo 目录下复制代码

上面有一个ACCESS_TOKEN变量需要自己配置。

打开 Github 网站,点击你右上角的头像,选择settings。

>need-to-insert-img

点击左下角的developer settings。

>need-to-insert-img

在左侧边栏中,单击Personal access tokens(个人访问令牌)。

>need-to-insert-img

单击Generate new token(生成新令牌)。

输入名称并勾选repo。

拉到最下面,点击Generate token,并将生成的 token 保存起来。

打开你的 Github 项目,点击settings。

点击secrets->new secret。

创建一个密钥,名称随便填(中间用下划线隔开),内容填入刚才创建的 token。

将上文代码中的ACCESS_TOKEN: $替换成刚才创建的 secret 名字,替换后代码如下ACCESS_TOKEN: $。保存后,提交到 Github。

以后你的项目只要执行git push,Github Actions 就会自动构建项目并发布到你的 Github Page 上。

Github Actions 的执行详情点击仓库中的Actions选项查看。

具体详情可以参考一下我的 demo 项目**github-actions-demo**。

>need-to-insert-img

构建成功后,打开 Github Page 网站,可以发现内容已经发布成功。

Github Actions 部署到阿里云

初始化阿里云服务器

购买阿里云服务器,选择操作系统,我选的 ubuntu

在云服务器管理控制台选择实例->更多->密钥->重置实例密码(一会登陆用)

选择远程连接->VNC,会弹出一个密码,记住它,以后远程连接要用(ctrl + alt + f1~f6 切换终端,例如 ctrl + alt + f1 是第一个终端)

进入后是一个命令行 输入root(默认用户名),密码为你刚才重置的实例密码

登陆成功, 更新安装源sudo apt-get update && sudo apt-get upgrade -y

安装 npmsudo apt-get install npm

安装 npm 管理包sudo npm install -g n

安装 node 最新稳定版sudo n stable

创建一个静态服务器

mkdir node-server// 创建 node-server 文件夹cd node-server// 进入 node-server 文件夹npm init -y// 初始化项目npm i expresstouch server.js// 创建 server.js 文件vim server.js// 编辑 server.js 文件复制代码

将以下代码输入进去(用 vim 进入文件后按 i 进行编辑,保存时按 esc 然后输入 :wq,再按 enter),更多使用方法请自行搜索。

constexpress =require(‘express’)constapp = express()constport =3388// 填入自己的阿里云映射端口,在网络安全组配置。app.use(express.static(‘dist’))app.listen(port,’0.0.0.0’,() =>{console.log(`listening`)})复制代码

执行node server.js开始监听,由于暂时没有dist目录,先不要着急。

注意,监听 IP 必须为0.0.0.0,详情请看部署Node.js项目注意事项

阿里云入端口要在网络安全组中查看与配置。

创建阿里云密钥对

请参考创建SSH密钥对绑定SSH密钥对,将你的 ECS 服务器实例和密钥绑定,然后将私钥保存到你的电脑(例如保存在 ecs.pem 文件)。

打开你要部署到阿里云的 Github 项目,点击 setting->secrets。

点击 new secret

secret 名称为SERVER_SSH_KEY,并将刚才的阿里云密钥填入内容。

点击 add secret 完成。

在你项目下建立.github\workflows\ci.yml文件,填入以下内容:

name:Buildappanddeploytoaliyunon:#监听push操作push:branches:# master分支,你也可以改成其他分支-masterjobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v1-name:InstallNode.jsuses:actions/setup-node@v1with:node-version:’12.16.2’-name:Installnpmdependenciesrun:npminstall-name:Runbuildtaskrun:npmrunbuild-name:DeploytoServeruses:easingthemes/ssh-deploy@v2.1.5env:SSH_PRIVATE_KEY:$ARGS:’-rltgoDzvO –delete’SOURCE:dist# 这是要复制到阿里云静态服务器的文件夹名称REMOTE_HOST:’118.190.217.8’# 你的阿里云公网地址REMOTE_USER:root# 阿里云登录后默认为 root 用户,并且所在文件夹为 rootTARGET:/root/node-server# 打包后的 dist 文件夹将放在 /root/node-server复制代码

保存,推送到 Github 上。

以后只要你的项目执行git push操作,就会自动执行ci.yml定义的脚本,将打包文件放到你的阿里云静态服务器上。

这个 Actions 主要做了两件事:

克隆你的项目,下载依赖,打包。

用你的阿里云私钥以 SSH 的方式登录到阿里云,把打包的文件上传(使用 rsync)到阿里云指定的文件夹中。

如果还是不懂,建议看一下我的demo

作者:谭光志

链接:https://juejin.im/post/6887751398499287054

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

写在开头:
1看这里的时候,请确保你已将熟悉JavaScript以及了解Vue的语法, Django的语法也略懂一二。
如果不是很了解,请点击这里查看学习文档VueDjango,否则下文可能有些不好理解。
2文章有点长 ,因为包含了一个Index.vue页面。
3第一次写长文章,所以排版很尴尬,请指正。

  1. 安装Vue环境
  2. 安装element-ui组件 使用其组件美化界面
1
npm i element-ui -S ||  npm install element-ui --save
1
2
3
4
5
main.js 
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI)
  1. 安装axios 使用其完成前端到后端的请求

由于axios 使用Vue.use(无效),所以要将其绑定在Vue原型上

1
npm install axios --save |  brew install axios --save
1
2
3
4
5
import axios from 'axios'

axios.defaults.baseURL = 'http://localhost:8000'

Vue.prototype.$axios = axios
  1. 安装Django及配置环境
  2. 配置mysql数据库,使用sqlite3的 跳过此步骤无需配置
1
2
3
4
5
6
7
8
9
10
11
settings.py
DATABASES = {
'default': { #
'ENGINE': 'django.db.backends.mysql', # 不同库有不同的殷勤
'NAME': 'python_use', # 使用的库名
'USER': 'root',
'PASSWORD': '',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}

配置完成后请查看django是否报错,不报错即连接成功

  1. 安装 pipdjango-cors-headers
1
2

pip install django-cors-headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
settings.py

INSTALLED_APPS = {
...
'corsheaders',
...
}


MIDDLEWARE = [
...

'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]


CORS_ORIGIN_ALLOW_ALL = True





至此,已将Vue和Django安装并配置好,接下来写一个简单的CRUD操作。
请确认你的整个项目目录与此类似

![](Vue + Django/2064404-d3a828d4530715b4.png)

项目目录结构

以下使用的目录均为此图所示


  1. 配置路由
1
2
3
4
5
first/urls.py 
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'', include('crud.urls')),
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
crud/urls.py




from django.conf.urls import url
from . import views

urlpatterns = [
url('create/', views.create, name = 'create'),
url('read', views.read, name = 'read'),
url('update/', views.update, name = 'update'),
url('delete/', views.delete, name = 'delete'),
url('search', views.search, name = 'search')
]
  1. 创建models,即在数据库中创建表
1
2
3
4
5
from django.db import models
class Books ( models.Model ):
book_name = models.CharField( max_length = 255 )
book_price = models.DecimalField( max_digits = 5, decimal_places = 2 )
book_time = models.DateTimeField( '保存日期', auto_now_add = True )

Models创建完成后运行命令 将其应用到数据库中并创建表
如果不懂 请返回顶部阅读Django文档

1
2
python manage.py makemigrations
python manage.py migrate
  1. 编写views.py 完成增删改查的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
# 1 获取前端传递来的参数
# 1.1 get方法发送的参数
request.GET['content']
# 1.2 post方法发送的参数
obj = json.loads(request.body)
name = obj['name']
# 2 由于使用Books.objects下的方法,获取到的数据为Query Set类型,
# 所以需要使用serializers.serialize("json", books)
# 将查询到的数据进行序列化,变成可读的对象。
# 3 向前端返回处理结果
return HttpResponse(json.dumps(res), content_type="application/json")
# 将res变成json字符串返回给前端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

from __future__ import unicode_literals
from django.shortcuts import render
from django.http import HttpResponse
import json
from django.core import serializers
from django.utils import timezone
from crud.models import Books

def search(request):
content = request.GET['content']
try:
books = serializers.serialize("json",Books.objects.filter(book_name__contains=content))
res = {
"code": 200,
"data": books
}
print(books)
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def create(request):
print('create')
obj = json.loads(request.body)
name = obj['name']
price = obj['price']
try:
book = Books(book_name=name, book_price=price, book_time=timezone.now())
book.save()
res = {
"code": 200,
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def read(request):
print('read')
try:
res = {
"code": 200,
"data": serializers.serialize("json",Books.objects.filter())
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def update(request):
print('update')
obj = json.loads(request.body)
pid = obj['id']
name = obj['name']
price = obj['price']
try:
Books.objects.filter(id=pid).update(book_price=price, book_name=name)
res = {
"code": 200
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

def delete(request):
print('delete')
obj = json.loads(request.body)
print(obj)
pid = obj['id']
try:
Books.objects.filter(id=pid).delete()
res = {
"code": 200
}
except Exception,e:
res = {
"code": 0,
"errMsg": e
}
return HttpResponse(json.dumps(res), content_type="application/json")

  1. 配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
frontend/src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'index',
component: Index
}
]
})
  1. 编写路由中使用到的组件 与上面import所用名称和路径需要一致,请耐心看完注释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21








this.$axios.get('/search', {
params: {
content: this.search
}
}).then(res => {
console.log(res)
})
this.$axios.post('/delete/', JSON.stringify(row)).then(res => {

console.log(res)


})

以下为Index.vue的全部页面,包含增删改查的基本操作,以及更改和新增时的弹出框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
frontend/src/components/Index.vue
<template>
<div>
<el-button type="primary" round @click="handleShowCreate">增加书籍</el-button>
<el-input v-model="search" placeholder="请输入内容" style="width: 200px" @keyup.enter.native="handleSearch"/>
<el-button type="primary" round @click="handleSearch">搜索</el-button>
<el-table :data="booksData" height="250" border style="width: 600px; margin: 40px auto;" v-loading="loading">
<el-table-column
prop="book_name"
label="书名"
align="center"
width="200">
</el-table-column>
<el-table-column
prop="book_price"
label="价格"
align="center"
width="200">
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleUpdate(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>
<el-dialog title="修改书籍" :visible.sync="dialogUpdateVisible">
<el-form :model="updateData">
<el-form-item label="书籍名称">
<el-input auto-complete="off" v-model="updateData.name"></el-input>
</el-form-item>
<el-form-item label="书籍价格">
<el-input-number v-model="updateData.price" :precision="2" :step="0.01" :max="9999"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel('dialogUpdateVisible')">Cancel</el-button>
<el-button type="primary" @click="handleConfirm('dialogUpdateVisible')">Submit</el-button>
</div>
</el-dialog>
<el-dialog title="增加书籍" :visible.sync="dialogCreateVisible">
<el-form :model="createData">
<el-form-item label="书籍名称">
<el-input auto-complete="off" v-model="createData.name"></el-input>
</el-form-item>
<el-form-item label="书籍价格">
<el-input-number v-model="createData.price" :precision="2" :step="0.01" :max="9999"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel('dialogCreateVisible')">Cancel</el-button>
<el-button type="primary" @click="handleCreate('dialogCreateVisible')">Submit</el-button>
</div>
</el-dialog>
</div>
</template>

<script>
export default {
name: 'index',
data () {
return {
search: '',
booksData: [],
oldData: {},
updateData: {},
createData: {
name: '',
price: 0
},
dialogUpdateVisible: false,
dialogCreateVisible: false,
loading: true
}
},
methods: {
handleShowCreate () {
this.dialogCreateVisible = true
},
handleCreate () {
if (this.createData.name === '') {
this.$message.error('please input book name')
return
}
if (this.createData.price === 0) {
this.$message.error('please input book price')
return
}
this.$axios.post('/create/', JSON.stringify(this.createData)).then(res => {
if (res.data.code === 200) {
this.$message.success(`create ${this.createData.name} success`)
this.dialogCreateVisible = false
this.handleRead()
} else {
this.$message.error("can't read books database")
}
})
console.log(this.createData)
},
handleRead () {
this.booksData = []
this.$axios.get('/read').then(res => {
this.loading = false
if (res.data.code === 200) {
let books = JSON.parse(res.data.data)
for (let i in books) {
books[i].fields.id = books[i].pk
books[i].fields.book_price = Number(books[i].fields.book_price)
this.booksData.push(books[i].fields)
}
console.log(this.booksData)
} else {
this.$message.console.error("can't read books database")
}
}).catch((res) => {
console.log(res)
})
},
handleUpdate (index, row) {
this.dialogUpdateVisible = true
this.updateData = Object.assign({}, {
id: row.id,
name: row.book_name,
price: row.book_price,
time: row.book_time
})
this.oldData = Object.assign({}, {
id: row.id,
name: row.book_name,
price: row.book_price,
time: row.book_time
})
},
handleDelete (index, row) {
this.$confirm(`are you sure to delete ${this.updateData.name} ?`, '', {
confirmButtonText: 'submit',
cancelButtonText: 'cancel',
type: 'warning'
}).then(() => {
this.$axios.post('/delete/', JSON.stringify(row)).then(res => {
if (res.data.code === 200) {
this.$message.success(`delete ${this.updateData.name} success`)
this.handleRead()
} else {
this.$message.error("can't read books database")
}
})
}).catch(() => {
this.$message.info('cancel delete')
})
},
handleCancel (arg) {
this.$message.info('cancel')
this[arg] = false
},
handleConfirm (arg) {
if (this.updateData.name === this.oldData.name && this.updateData.price === this.oldData.price) {
this.$message.error('please update something or cancel')
return
}
this[arg] = false
this.$axios.post('/update/', JSON.stringify(this.updateData)).then(res => {
if (res.data.code === 200) {
this.$message.success(`update ${this.updateData.name} success`)
this.handleRead()
} else {
this.$message.error("can't read books database")
}
})
},
handleSearch () {
this.$axios.get('search', {
params: {
content: this.search
}
}).then(res => {
if (res.data.code === 200) {
if (res.data.data && JSON.parse(res.data.data).length > 0) {
this.booksData = []
let books = JSON.parse(res.data.data)
for (let i in books) {
let obj = {
id: books[i].pk,
book_name: books[i].fields.book_name,
book_price: Number(books[i].fields.book_price),
book_time: books[i].fields.book_time
}
this.booksData.push(obj)
}
} else {
this.$message.error(`can't search contains of '${this.search}' in database`)
}
} else {
this.$message.error(`can't search books in database`)
}
})
}
},
mounted () {
this.handleRead()
}
}
</script>

到这里,一个增删改查基本操作的页面就写完了,如果哪里有问题可以留言指正。 git源码以上传, 没事可以star/fork 更新将在以下附注后增加。

https://github.com/RogersLei/django-vue


附注 :

  1. Vue添加事件所用到的修饰符:

    ![](Vue + Django/2064404-1aa984b701bf3e11.png)

    Vue事件绑定修饰符

  2. Django中模糊查询用到的语法:

YourModels.objects.filter(headline__contains=str)
字段名__contains / __icontains 忽略大小写

更多精彩内容,就在简书APP

“小礼物走一走,来简书关注我”

还没有人赞赏,支持一下

总资产23共写了2.2W字获得33个赞共22个粉丝

推荐阅读更多精彩内容

  • 一.前言 最近接手了一个项目,后端是django,前端是django自带的模板,用的是jinja2,写了一段时间发…

  • 组织文章借鉴 ——培训师的21项修炼 书籍结构:错误的案例情景重现-抛出问题,传道受业解惑也 我们假设一个场景,大…

  • 每天总是忙忙碌碌,感觉时间完全不够用,更不要说是学习了,可是忙忙碌碌到最后感觉收获也很小,就像大家说的,瞎忙活。…

  • 和姑姑聊起当时借钱给已故父亲治病时的场景,我依稀记得当时我和涛古,妈妈给厂里老板下跪借那三万块的场景。这辈子希望以…

本文整合Django和Vue.js  并引入elementUi 实现前后端分离项目环境

最终的成品是设计出一个ElementUi风格的页面可以添加和显示学生信息.

Django作为Python 三大Web框架之一,因其集成了大量的组件(例如: Models Admin Form 等等)而大受欢迎,但是他本身自带的template模板实在是有点弱.于是考虑整合Vue.js同时引入ElementUI 组件,可以更加快速高效的开发出美观实用的Web页面.

Python

本文版本:Python 3.5

安装教程: https://www.runoob.com/python3/python3-install.html

Pycharm

本文版本:2019.1.3

PyCharm 2019.1.3 (Community Edition)

安装教程:https://www.runoob.com/w3cnote/pycharm-windows-install.html

Django

本文版本:2.2.3

安装教程:https://www.runoob.com/django/django-install.html

node.js

本文版本:10.16.3

安装教程:https://www.runoob.com/nodejs/nodejs-install-setup.html

MySQL

本文版本: 8.0.13 for Win64

安装教程:https://www.runoob.com/mysql/mysql-install.html

本文的Pycharm为社区版,如果为专业版则字段Django项目的创建选项,创建项目将更加简单.

1.创建django项目:DjangoElementUI

创建文件夹E:\PycharmProjects:

在项目文件夹目录输入Windows 命令行如下

1
django-admin.py startproject DjangoElementUI

成功创建项目完成后文件夹结构如下图:

进入项目文件夹目录,在目录中输入命令

1
python manage.py runserver 0.0.0.0:8000

看到如下提示则为项目创建成功

在浏览器输入你服务器的 ip(这里我们输入本机 IP 地址: 127.0.0.1:8000) 及端口号,如果正常启动,输出结果如下:

2.数据库配置

Django 对各种数据库提供了很好的支持,包括:PostgreSQL、MySQL、SQLite、Oracle。

Django 为这些数据库提供了统一的调用API。 我们可以根据自己业务需求选择不同的数据库。

MySQL 是 Web 应用中最常用的数据库。

本文采用MySQL

第一次使用MySQL需要安装 MySQL驱动,在项目文件夹目录下执行以下命令安装:

1
pip install pymysql

Django无法直接创建数据库(只能操作到数据表层),我们需要手工创建MySQL数据库.

以下通过命令行创建 MySQL 数据库:Django_ElementUI

登录数据库:

数据库安装文件夹bin文件夹下输入命令

1
mysql -u root -p 

创建数据库:

1
create DATABASE Django_ElementUI DEFAULT CHARSET utf8;

Django配置数据库

在项目的 settings.py 文件中找到 DATABASES 配置项,将其信息修改为:

1
'ENGINE': 'django.db.backends.mysql',  'NAME': 'Django_ElementUI',  

在与 settings.py 同级目录下的 __init__.py 中引入模块和进行配置 (告诉 Django 使用 pymysql 模块连接 mysql 数据库)

1
pymysql.install_as_MySQLdb()

3.利用Django模型设计数据库表

Django 规定,如果要使用模型,必须要创建一个 app。

创建Django APP:myApp

我们使用以下命令创建一个Django app:myApp

1
django-admin.py startapp myApp

成功后的项目文件夹目录如下:

设计数据库表

在myApp下的models.py设计表:

这里我们设计一个Student表,用来存储学生信息.

表字段

字段类型

含义

student_name

Varchar类型

学生姓名

student_sex

Varchar类型

学生性别

create_time

Datetime类型

创建日期时间

1
from django.db import modelsclass Student(models.Model):    student_name = models.CharField(max_length=64)    student_sex = models.CharField(max_length=3)    create_time = models.DateTimeField(auto_now=True)

在 settings.py 中找到INSTALLED_APPS这一项,如下:

1
'django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles',

生成数据库迁移文件

在命令行中运行:

1
python manage.py makemigrations myApp

执行成功后结果:

执行迁移文件来完成数据库表的创建

在命令行中运行:

1
python manage.py migrate myApp

执行成功后结果:

查看数据库中数据库表已经生成成功

(django默认在makemigrations会为表对象创建主键id,id = models.AutoField(primary_key=True))

4.Django创建新增和查询学生信息接口

在myApp目录下的views.py中创建两个视图函数

1
from __future__ import unicode_literalsfrom django.http import JsonResponsefrom django.core import serializersfrom django.shortcuts import renderfrom django.views.decorators.http import require_http_methodsfrom myApp.models import Student@require_http_methods(["GET"])def add_student(request):        student = Student(student_name=request.GET.get('student_name'))        response['msg'] = 'success'        response['error_num'] = 0        response['error_num'] = 1return JsonResponse(response)@require_http_methods(["GET"])def show_students(request):        students = Student.objects.filter()        response['list'] = json.loads(serializers.serialize("json", students))        response['msg'] = 'success'        response['error_num'] = 0        response['error_num'] = 1return JsonResponse(response)

5.配置路由

1.在myApp目录下,新增一个urls.py文件,用于创建此APP下的分支路由,把新增的两个视图函数添加到路由里面.

1
from django.conf.urls import url    url(r'^add_book/', views.add_book),    url(r'^show_books/', views.show_books),

2.把上面创建的myApp下的分支路由加到DjangoElementUI下的主路由中urls.py.

1
from django.contrib import adminfrom django.urls import pathfrom django.conf.urls import urlfrom django.conf.urls import include    url(r'^admin/', admin.site.urls),    url(r'^api/', include(urls)),

至此Django部分已经完成,总结下我们利用Django完成了数据库的创建,并创建了两个视图函数作为接口给前端调用.

1.安装vue-cli脚手架

在DjangoElementUI根目录下输入命令:

1
npm install -g vue-cli

2.安装好后,新建一个前端工程目录:appfront

在DjangoElementUI项目根目录下输入命令:

1
vue-init webpack appfront

3.进入appfront目录安装vue所需要的依赖

1
npm install

4.安装ElementUI

1
npm i element-ui -S

5.创建新vue页面

在src/component文件夹下新建一个名为Studengt.vue的组件,通过调用之前在Django上写好的api,实现添加学生和展示学生信息的功能.

1
<el-row display="margin-top:10px"><el-input v-model="input" placeholder="请输入学生姓名" style="display:inline-table; width: 30%; float:left"></el-input><el-button type="primary" @click="addStudent()" style="float:left; margin: 2px;">新增</el-button><el-table :data="studentList" style="width: 100%" border><el-table-column prop="id" label="编号" min-width="100"><template scope="scope"> {{ scope.row.pk }} </template><el-table-column prop="student_name" label="姓名" min-width="100"><template scope="scope"> {{ scope.row.fields.student_name }} </template><el-table-column prop="student_sex" label="性别" min-width="100"><template scope="scope"> {{ scope.row.fields.student_sex }} </template><el-table-column prop="add_time" label="添加时间" min-width="100"><template scope="scope"> {{ scope.row.fields.create_time }} </template>this.$http.get('http://127.0.0.1:8000/api/add_student?student_name=' + this.input)var res = JSON.parse(response.bodyText)if (res.error_num === 0) {this.$message.error('新增学生失败,请重试')this.$http.get('http://127.0.0.1:8000/api/show_students')var res = JSON.parse(response.bodyText)if (res.error_num === 0) {this.studentList = res['list']this.$message.error('查询学生失败')<!-- Add "scoped" attribute to limit CSS to this component only -->

6.配置路由

appfront/router文件夹下的index.js中增加页面路由.

1
import Router from 'vue-router'import HelloWorld from '@/components/HelloWorld'import Student from '@/components/Student'export default new Router({

appfront文件夹下的main.js中引入ElementUI并注册.

1
import router from './router'import '../node_modules/element-ui/lib/theme-chalk/index.css'import ElementUI from 'element-ui'Vue.config.productionTip = false

7.打包并启动前端项目

打包vue项目

1
npm run build

启动前端项目

1
npm run dev

出现下面信息则说明我们前端项目已经构建成功.

去浏览器访问页面地址:http://localhost:8080/#/student

出现如下页面说明我们的页面已经成功.

截止到目前,我们已经成功通过Django创建了一个后端服务,通过Vue.js + ElementUI 实现了前端页面的构建,但是他们运行在各自的服务器,而且前端页面还无法调用后端的接口.

接下来我们需要将两个项目真正的整合到一个成一个项目.

1.引入用于HTTP解析的vue-resource

前端vue项目调用后端需要引入vue-resource

在appfront文件下运行命令:

1
npm install 

安装完成后在main.js中引入vue-resource

1
import router from './router'import '../node_modules/element-ui/lib/theme-chalk/index.css'import ElementUI from 'element-ui'import VueResource from 'vue-resource'Vue.config.productionTip = false

2.在Django层注入header

为了让后端可以识别前端需求,我们须要在Django层注入header,用Django的第三方包django-cors-headers来解决跨域问题:

在DjangoElementUI根目录下输入命令:

1
pip install django-cors-headers

在settings.py中增加相关中间件代码

1
'django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','corsheaders.middleware.CorsMiddleware',     'django.middleware.common.CommonMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',CORS_ORIGIN_ALLOW_ALL = True   

3.修改Django路由

这一步我们通过Django路由配置连接前后端资源.

首先我们把Django的TemplateView指向我们刚才生成的前端dist文件

在DjangoElementUI目录下的urls.py中增加代码:

1
from django.conf.urls import urlfrom django.contrib import adminfrom django.conf.urls import includefrom django.views.generic import TemplateView    url(r'^admin/', admin.site.urls),    url(r'^api/', include(urls)),    url( r'^vue/', TemplateView.as_view( template_name="index.html" ) )

接着修改静态资源文件路径也指向前端appfront 相关文件

在DjangoElementUI目录下的setting.py中增加代码:

1
'BACKEND': 'django.template.backends.django.DjangoTemplates','DIRS': [os.path.join(BASE_DIR, 'appfront/dist')],  'django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages',    os.path.join(BASE_DIR, "appfront/dist/static")

3.重新构建前端项目

appfront目录下输入命令:

1
npm run build

重新启动Django项目

1
python manage.py runserver

输入地址:http://localhost:8000/vue/#/student

添加一条记录

至此,大功告成!

此份指南在配置的过程踩过不少坑,以下是踩的印象较深的坑.

1.数据库创建的过程中务必注意大小写的问题,数据库字段和Django的Models页面,View页面和Vue中的组件页面都有关联.很容易一个大小写不注意,导致整个接口无法使用.

2.连接MySQL需要按照对应的包,同时需要在根目录的_ini_.py中引入pymysql

3.在整个环境的搭建过程中VUE环境的搭建需要耗费较长的npm安装时间,需要耐心等待.

4.前后台连接需要在前端引入vue-resource,Django需要引入django-cors-headers

引言

大U的技术课堂 的新年第一课,祝大家新的一年好好学习,天天向上:)

本篇将手把手教你如何快速而优雅的构建前后端分离的项目,想直接上手请往后翻!

目录:

  1. 我为什么要选择Django与VueJS?

  2. Django和VueJS是如何结合起来的?

  3. 实操

  4. 创建 Django 项目

  5. 创建 Django App 做为后端

  6. 创建 VueJS 项目作为前端

  7. 使用 Webpack 处理前端代码

  8. 配置 Django 模板的搜索路径

  9. 配置 Django 静态文件搜索路径

  10. 开发环境

  11. 生产环境(部署到 UCloud)

正文:

我为什么要选择Django与VueJS?

首先介绍一下我看重的点:

Django (MVC框架) - The Web framework for perfectionists with deadlines

  • Python

  • ORM

  • 简单、清晰的配置

  • Admin app

Django 仅因为 Python 的血统,就已经站在了巨人的肩膀上,配置管理( SaltStack、Ansible )
,数据分析( Pandas ),任务队列( Celery ),Restful API( Django REST framework ),HTTP请求( requests ),再加上高度抽象的ORM,功能强大的 Query Expressions,简单清晰的配置,着重提一下堪称神器的自带App: Admin,有了它你再也不用将一些经常变化的配置写在文件里面,每次增删改都重新发布一次,你只需要定义出配置的 data scheme ,只需要几行代码,Django Admin便为你提供美观,并带有权限控制的增删改查界面,而且可以通过ORM为它生成的API来做到定制化的更新,比如直接读某个wiki上的配置,自动的写入数据库,伪代码如下:

1
2
3
4
import pandas as pd
settings = pd.read_html('http://某个gitlab的README 或者 某个redmine wiki')
settings = clean(settings)
update(settings)

最后还可以使用 django-celery 的 celery-beat 按 Interval/crontab 的方式扔更新配置的任务到 celery 队列里面,最最重要的是,这些都可以在Django Admin后台直接配置哦,还不够优雅?请联系我

VueJS (MVVM框架) - Vue.js

  • 数据双向绑定
  • 单文件组件
  • 清晰的生命周期
  • 学习曲线平滑
  • vue-cli

前端是DevOps的弱项,我需要一个 MVVM 框架来提升交互和节约时间,在试过 AngularJS ,ReactJS,VueJS之后我选择了VueJS,因为我觉得写 VueJS 代码的感觉最接近写 Python

着重提一下单文件组件:

特别清晰,一个文件包含且仅包含三块

  1. 前端渲染的模板
  2. 专为此模板写渲染逻辑的
  3. 专为此模板写样式的

这样可以达到什么效果呢?一个文件一个组件,每个组件有它自己的逻辑与样式,你不用关心什么 local 什么 global ,CSS样式加载先后、覆盖问题,因为它是『闭包』的,而且『自给自足』,不知道这样说好不好理解

当然组件之间也是可以通信的,举个例子,我有一个组件叫 ListULB ,使用表格展示了我拥有的所有 ULB (负载均衡),ListULB 做了一件事,从 API 获取 ULB 对象列表并 for 循环展现出来, ListULB 可以放到某个页面里,可以放到弹框里,放到模态框里,任何地方都可以,因为这个组件对外交互的只有API

如果我现在要写一个组件叫 AddVServer ,功能是可以为任意一个 ULB 对象添加VServer,我的写法是将在 AddVServer 组件创建的时候,将 ULB 对象传给 AddVServer 组件,这样AddVServer 组件拿到这个对象,就可以直接根据对象的ID等,创建出当前行的ULB的VServer了,伪代码如下:

1
2
3
4
5
6
<ListULB>
for **ulb_object** in ulbs_list:
{{ ulb_object.name }}
{{ ulb_object.id }}
<AddVServer :current_ulb='**ulb_object**'></AddVServer>
</ListULB>

注意双星号包着的对象,在 ListULB 组件里面是每行的ULB,传给AddServer组件之后,变成了 current_ulb 对象,拿到id为 current_ulb.id 尽情的为它创建 VServer 吧

如果我要为指定 VServer 创建 RServer 呢,一样的

看出来了吧,进行开发之前,前端组件的结构与数据的结构对应起来可以省好多时间,数据驱动前端组件,棒吗?

谁不喜欢优雅的代码呢, 『Data drive everything』 多么的省脑细胞

以上就是我选择Python与VueJS的原因

Django与VueJS是如何结合起来?

  • 首先我选择了VueJS的前端渲染,自然放弃了Django的后端模板引擎渲染
  • 然后业务逻辑放到了前端,放弃了Django的View(其实也就是前后端分离必要的条件)
  • 保留了Django的 Controller (URLconf) 来实现前端路由的父级路由,可以达到不同页面使用不同的前端框架, 页面内部使用各自独有的前端路由的效果,万一老大给你配了前端呢,万一前端只想写 ReactJS 呢
  • 保留了Django的 Model ,前面说了Django的ORM太好用了,而且可以配合Django Admin

所以综合来说就是:

M(Django) + C(Django) + MVVM (VueJS) = M + MVVM + C = MMVVMC

(为了容易理解,并没有使用Django自称的MTV模式理解,感兴趣看看我画的图)

总结:作为以改变世界为己任的 DevOps ,MVC框架后端渲染的柔弱表现力与繁杂的交互已经不能满足我们了,…..省略1000子…..,所以我选择这样构建项目,嗯…

好吧,也该开始了

代码块中的修改都会用爽星号括起来,比如: **changed**

本文为了精简篇幅,默认您已经安装了必要的 命令行界面(CLI),比如 vue-cli等

1. 创建Django项目

命令:

1
django-admin startproject ulb_manager

结构:

1
2
3
4
5
6
7
.
├── manage.py
└── ulb_manager
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

2. 进入项目根目录,创建一个 app 作为项目后端

命令:

1
2
cd ulb_manager
python manage.py startapp backend

即:app 名叫做 backend

结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── backend
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
└── ulb_manager
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

3. 使用vue-cli创建一个vuejs项目作为项目前端

命令:

1
vue-init webpack frontend

即:项目名叫 frontend

结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.
├── backend
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── frontend
│ ├── README.md
│ ├── build
│ │ └── ....
│ ├── config
│ │ ├── dev.env.js
│ │ ├── index.js
│ │ ├── prod.env.js
│ │ └── test.env.js
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ └── logo.png
│ │ ├── components
│ │ │ └── Hello.vue
│ │ └── main.js
│ ├── static
│ └── test
│ └── ...
├── manage.py
└── ulb_manager
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

结构总结:

可以看到项目根目录有两个新文件夹,一个叫 backend ,一个叫 frontend,分别是:

  • backend Django的一个app
  • frontend Vuejs项目

4. 接下来我们使用 webpack 打包Vusjs项目

命令:

1
2
3
cd frontend
npm install
npm run build

结构:

我引入了一些包,比如element-ui等,你的static里面的内容会不同,没关系 index.html 和 static 文件夹相同就够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dist
├── index.html
└── static
├── css
│ ├── app.42b821a6fd065652cb86e2af5bf3b5d2.css
│ └── app.42b821a6fd065652cb86e2af5bf3b5d2.css.map
├── fonts
│ ├── element-icons.a61be9c.eot
│ └── element-icons.b02bdc1.ttf
├── img
│ └── element-icons.09162bc.svg
└── js
├── 0.8750b01fa7ffd70f7ba6.js
├── vendor.804853a3a7c622c4cb5b.js
└── vendor.804853a3a7c622c4cb5b.js.map

构建完成会生成一个 文件夹名字叫dist,里面有一个 index.html 和一个 文件夹static ,

5. 使用Django的通用视图 TemplateView

找到项目根 urls.py (即ulb_manager/urls.py),使用通用视图创建最简单的模板控制器,访问 『/』时直接返回 index.html

1
2
3
4
5
urlpatterns = [
url(r'^admin/', admin.site.urls),
**url(r'^$', TemplateView.as_view(template_name="index.html")),**
url(r'^api/', include('backend.urls', namespace='api'))
]

6. 配置Django项目的模板搜索路径

上一步使用了Django的模板系统,所以需要配置一下模板使Django知道从哪里找到index.html

打开 settings.py (ulb_manager/settings.py),找到TEMPLATES配置项,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# 'DIRS': [],
**'DIRS': ['frontend/dist']**,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

注意这里的 frontend 是VueJS项目目录,dist则是运行 npm run build 构建出的index.html与静态文件夹 static 的父级目录

这时启动Django项目,访问 / 则可以访问index.html,但是还有问题,静态文件都是404错误,下一步我们解决这个问题

7. 配置静态文件搜索路径

打开 settings.py (ulb_manager/settings.py),找到 STATICFILES_DIRS 配置项,配置如下:

1
2
3
4
# Add for vuejs
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "frontend/dist/static"),
]

这样Django不仅可以将/ulb 映射到index.html,而且还可以顺利找到静态文件

此时访问 /ulb 我们可以看到使用Django作为后端的VueJS helloworld

ALL DONE.

8. 开发环境

因为我们使用了Django作为后端,每次修改了前端之后都要重新构建(你可以理解为不编译不能运行)

除了使用Django作为后端,我们还可以在dist目录下面运行以下命令来看效果:

但是问题依然没有解决,我想过检测文件变化来自动构建,但是构建是秒级的,太慢了,所以我直接使用VueJS的开发环境来调试

毫秒,但是有个新问题,使用VueJS的开发环境脱离了Django环境,访问Django写的API,出现了跨域问题,有两种方法解决,一种是在VueJS层上做转发(proxyTable),另一种是在Django层注入header,这里我使用后者,用Django的第三方包 django-cors-headers 来解决跨域问题

安装

1
pip install django-cors-headers

配置(两步)

1. settings.py 修改

1
2
3
4
5
6
7
8
9
10
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
**'corsheaders.middleware.CorsMiddleware',**
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

这里要注意中间件加载顺序,列表是有序的哦

2. settings.py 添加

1
CORS_ORIGIN_ALLOW_ALL = True

至此,我的开发环境就搭建完成了

9. 生产环境部署(部署到 UCloud )

9.1 创建主机

  1. 注册 UCloud - 专业云计算服务商
  2. 点击左侧的 云主机,然后点击 创建主机
  3. 右侧选择 付费方式,点击 立即购买
  4. 在支付确认页面,点击 确认支付

购买成功后回到主机管理列表,如下所示:

这里注意记住你的外网IP,下面的ip替换成你的

9.2 环境搭建与部署

登录主机,用你刚填写的密码:

ssh root@120.132.**.75

CentOS 系统可以使用 yum 安装必要的包

1
2
3
4
5
6
7
8
# 如果你使用git来托管代码的话
yum install git

# 如果你要在服务器上构建前端
yum install nodejs
yum install npm

yum install nginx

我们使用 uwsgi 来处理 Django 请求,使用 nginx 处理 static 文件(即之前 build 之后 dist 里面的static,这里默认前端已经打包好了,如果在服务端打包前端需要安装nodejs,npm等)

安装uWsgi

1
2
3
yum install uwsgi
# 或者
pip install uwsgi

我们使用配置文件启动uwsgi,比较清楚

uwsgi配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
[uwsgi]
socket = 127.0.0.1:9292
stats = 127.0.0.1:9293
workers = 4
# 项目根目录
chdir = /opt/inner_ulb_manager
touch-reload = /opt/inner_ulb_manager
py-auto-reload = 1
# 在项目跟目录和项目同名的文件夹里面的一个文件
module= inner_ulb_manager.wsgi
pidfile = /var/run/inner_ulb_manager.pid
daemonize = /var/log/inner_ulb_manager.log

nginx 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 8888;
server_name 120.132.**.75;
root /opt/inner_ulb_manager;
access_log /var/log/nginx/access_narwhals.log;
error_log /var/log/nginx/error_narwhals.log;

location / {
uwsgi_pass 127.0.0.1:9292;
include /etc/nginx/uwsgi_params;
}
location /static/ {
root /opt/inner_ulb_manager/;
access_log off;
}
location ^~ /admin/ {
uwsgi_pass 127.0.0.1:9292;
include /etc/nginx/uwsgi_params;
}
}

/opt/inner_ulb_manager/static 即为静态文件目录,那么现在我们静态文件还在 frontend/dist 怎么办,不怕,Django给我们提供了命令:

先去settings里面配置:

1
STATIC_ROOT = os.path.join(BASE_DIR, "static")

然后在存在manage.py的目录,即项目跟目录执行:

1
python manage.py collectstatic

这样frontend/dist/static里面的东西就到了项目根目录的static文件夹里面了

那么为什么不直接手动把构建好的dist/static拷过来呢,因为开始提过Django自带的App:admin 也有一些静态文件(css,js等),它会一并collect过来,毕竟nginx只认项目跟目录的静态文件,它不知道django把它自己的需求文件放到哪了

开头说过Django配置灵活,那么我们专门为Django创建一个生产环境的配置 prod.py

prod.py 与 默认 settings.py 同目录

1
2
3
4
5
6
7
8
9
10
11
# 导入公共配置
from .settings import *

# 生产环境关闭DEBUG模式
DEBUG = False

# 生产环境开启跨域
CORS_ORIGIN_ALLOW_ALL = False

# 特别说明,下面这个不需要,因为前端是VueJS构建的,它默认使用static作为静态文件入口,我们nginx配置static为入口即可,保持一致,没Django什么事
STATIC_URL = '/static/'

如何使用这个配置呢,进入 wisg.py 即uwsgi配置里面的module配置修改为:

1
2
3
4
5
6
7
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "**inner_ulb_manager.prod**")

application = get_wsgi_application()

启动uwsgi

1
uwsgi --ini inner_ulb_manager.ini

启动ngingx

至此,部署就完成了

10. 效果图

List 组件:

传单个 ULB 对象给 Detail 组件使用即可

Detail 组件:

当然里面还实现了前面提到的 ULB 的 VServer 创建,VServer 的 RServer 的创建等。

————————

本文由『UCloud平台产品研发团队』提供。

项目源码文件戳下面链接查看,大家可以马上拿源码上手试起来,操作过程中遇到问题也可直接在github上留言:)https://github.com/tmpbook/django-with-vuejs

现在注册使用UCloud,还免费试用 及 首充返现优惠,最高可返3000元代金券!活动传送门:用UCloud!3000元限量版礼盒等你来拆!

另,欢迎添加UCloud运营小妹个人微信号:Surdur,陪聊很专业:)

关于作者:

星辰(@星辰), UCloud平台产品研发工程师,DevOps一枚。你也可以去他的知乎专栏 《随心DevOps》 上逛逛,干货满满,带你更优雅的改变世界。

相关阅读推荐:

机器学习进阶笔记之八 | TensorFlow与中文手写汉字识别

机器学习进阶笔记之七 | MXnet初体验
机器学习进阶笔记之六 | 深入理解Fast Neural Style
机器学习进阶笔记之五 | 深入理解VGG\Residual Network
机器学习进阶笔记之四 | 深入理解GoogLeNet
机器学习进阶笔记之三 | 深入理解Alexnet
机器学习进阶笔记之二 | 深入理解Neural Style
机器学习进阶笔记之一 | TensorFlow安装与入门

「UCloud机构号」将独家分享云计算领域的技术洞见、行业资讯以及一切你想知道的相关讯息。

欢迎提问&求关注 o(*////▽////*)q~

以上。

npm-check

 npm-check 是一个检查依赖包是否存在过期、不正确、未使用等情况的工具。

 全局安装:

npm  install  -g  npm-check

 使用:

npm-check

上述指令会自动检查当前目录下的依赖包情况。

 这里我们重点关注下未使用的依赖包。npm-check 在检查依赖包是否使用时判断的依据是文件中是否存在 require(package) 这条语句,例如:

const lodash = require(‘lodash’);

只要存在这条语句,即使我并未在其它任何地方使用(也就是说这是个无用的包),但是 npm-check 是不会将其判定为未使用的。

 ESLint

为了解决上述存在的这种情况,我们可以借助 ESLint 先去检查代码是否存在未使用的变量(no-unused-vars),这样就可以检查某个包 require 了但并未在后续使用的情况。

全局安装:

npm install -g eslint

编写 .eslintrc.js 配置文件:

 

eslint  –config  .eslintrc.js  ./

执行上述指令便会检查当前目录下的所有代码是否存在定义了但未使用的变量。删除掉未使用的变量(包含对依赖包的引用)之后,再运行 npm-check 便能正确的找出那些在项目中已不再使用的依赖包了。

1
2
3
4
5
6
7
8
9
10
#配置用户名和邮箱
git config --global user.name
git config --global user.email

#中文路径和文件名乱码
git config --global core.quotePath false

#修改commit编码方式
git config --global i18n.commitEncoding utf-8
git config --global i18n.logOutputEncoding

在CMD命令行中切换到管理员权限模式

方式1:

搜索CMD Ctrl+Shift+Enter

方式2:

打开CMD,输入

1
runas /noprofile /user:Administrator cmd

输入Administrator账户的密码

runas 允许用户用其他权限运行指定的工具和程序

/noprofile 指定不加载用户的配置文件

/user:UserAccountName 指定在其下运行程序的账户

常见问题

运行runas 指令输入密码报错“无法启动服务,原因可能是已被禁用或与其关联的设备没有启动。”

这是因为“Secondary Logo”服务没有启动,这个服务是”在不同凭据下启用启动过程“。直接在cmd中输入services.msc,将服务从禁用改为手动就好了,之后再次输入runas命令就可以使用administrator账户运行。

识别接口名称

1
2
# 需要 net-tools
ifconfig

如果使用标准的ifconfig命令没有显示出接口,尝试使用带有-a选项的相同的命令。这个选项强制这个工具去显示系统检测到的所有的网络接口,不管他们是up或down状态。如果ifconfig -a没有提供结果,则硬件有错误或者接口驱动没有加载到内核中。

1
2
# 新版本系统大部分支持
ip addr

dhcp

DHCP(动态主机配置协议)使自动接受网络信息(IP地址、掩码、广播地址、网关、名称服务器等)变得容易。这只在网络中有DHCP服务器(或者如果ISP提供商提供一个DHCP服务)时有用.

1
dhcpcd eth0 # eth0 为网口名称,根据上一步识别出的接口名称修改

ifconfig命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启用/禁用网卡
ifconfig eth0 up/down

# 设置IP地址及掩码
ifconfig eth0 {IP地址} netmask {掩码} up

# 设置默认网关
route add default gw {网关}

# 配置DNS
nano -w /etc.resolv.conf

#使用下边模板填充
nameserver {名称服务器}

花括号中内容使用具体的地址填充

ip命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 启用/禁用网卡
ip link set dev eth0 up/down

# 设置Ip地址及掩码,掩码一般用 24 相当于255.255.255.0
ip addr add {IP地址}/{掩码} dev eth0

# 删除
ip addr del dev eth0 {IP}/{掩码}

# 刷新接口IP(删除所有)
ip addr flush eth0

# 设置默认网关
ip route add default via {网关}

网关的配置参考

ip route命令

Linux上添加路由,删除路由,修改路由配置(route add, route del, 路由表项基本知识)

| 子网掩码用来划分网络区域
| 子网掩码非0的位对应的ip上的数字表示这个ip的网络位
| 子网掩码0位对应的数字是ip的主机位
| 网络位表示网络区域
| 主机位表示网络区域里某台主机
|
| 11111111.11111111.11111111.00000000 = 255.255.255.0 = 24
| —————————————— —————
| 网络位 主机位

| 网络位一致,主机位不一致的2个IP可以直接通讯
|
| 172.25.254.10/24 #24=255.255.255.0
|
| 172.25.254.20/24
|
| 172.25.0.1/16 #16=255.255.0.0
| 前两个可以直接通讯,最后一个与其他俩个不能直接通讯

无线网连接

当使用一块无线(802.11)网卡,在继续之前需要先配置无线设置。要查看当前无线网卡的设置,你可以使用iw

1
2
3
4
5
6
7
8
# 查看连接信息
iw dev wlan0 info

# 检查连接状态
iw dev wlan0 link

# 连接网络 (确保接口处于活动状态)
iw dev wlan0 connect -w {网络名称} key 0:d:{密码}

如果无线网络配置为WPA或WPA2,则需要使用wpa_supplicant

1
2
3
4
5
6
7
8
9
10
11
12
# 查找附近热点
wpa_cli -i wlan0 scan

# 生成连接配置文件
wpa_passphrase {网络名称} {密码} > /etc/wpa_supplicant.conf

# 连接网络
# -D 驱动程序名称(可以是多个驱动程序:nl80211,wext)
# -i 接口名称
# -c 配置文件
# -B 在后台运行守护进程
wpa_supplicant -D nl80211 -i wlan0 -c /etc/wpa_supplicant.conf -B

SSH配置

1
2
3
4
5
6
7
8
9
nano -w /etc/ssh/sshd_config

# 放开注释
PasswordAuthentication yes
PermitRootLogin yes

# 启用SSH密钥对登录,取消如下行的注释符
PubkeyAuthentication yes
AuthorizeKeysFile .ssh/authorized_keys

启动SSHD

1
2
# 启动SSH服务(需要有可登录的账户)
/etc/init.d/sshd start

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

先罗列一下主流开源流媒体服务器

  1. 流媒体解决方案 Live555
  2. 流媒体平台框架 EasyDarwin
  3. 实时流媒体播放服务器程序DarwinStreamingSrvr
  4. 流媒体实时传输开发包 jrtplib
  5. 多媒体处理工具 ffmpeg
  6. 多媒体编码工具包Libav
  7. Flash流媒体服务器 Red5
  8. 流媒体服务器 Open Streaming Server
  9. FMS流媒体服务器
  10. Wowza流媒体服务器
  11. 开源流媒体平台FreeCast
  12. 最后补充一个 Ngix+RTMP插件

这里我选择 Darwin Streaming Server (达尔文),原因在于:

  • 因为它是很老牌产品,稳定
  • C++写的,性能好。
  • 以前用过,配置方便

一、概要

Darwin Streaming Server简称DSS。DSS是Apple公司提供的开源实时流媒体播放服务器程序。整个程序使用C++编写,在设计上遵循高性能,简单,模块化等程序设计原则,务求做到程序高效,可扩充性好。并且DSS是一个开放源代码的,基于标准的流媒体服务器,可以运行在Windows NT和Windows 2000,以及几个UNIX实现上,包括Mac OS X,Linux,FreeBSD,和Solaris操作系统上的。

二、Darwin streaming server的特性

  • 支持MP4、3GPP等文件格式;
  • 支持MPEG-4、H.264等视频编解码格式;
  • 支持RTSP流控协议,支持HTTP协议;
  • 支持RTP流媒体传输协议;
  • 支持单播和组播;
  • 支持基于Web的管理;
  • 具有完备的日志功能。

三、DDS安装配置

第一步:安装Darwin

  • 从:http://dss.macosforge.org/downloads/DarwinStreamingSrvr5.5.5-Windows.exe (只有5.5的) 这里下载 DSS for Windows
  • 下载后解压,会看到一个 Install.bat 的文件,Win10下最好从CMD管理员运行,直接运行可能存在路径拷贝问题。
  • 执行批处理后会安装到 C:\Program Files\Darwin Streaming Server 并还会在 系统服务里面加一个号Darwin Streaming Server 的服务程序,这个就是 DSS 的 RTSP 服务器。

第二步:安装Perl解释器

注意:如果安装后perl路径没有自动添加到Path,就自己添加一下。

第三步:配置管理的用户密码

# 根据提示创建 WebAdmin 的账号和密码

C:\Program Files\Darwin Streaming Server> perl WinPasswdAssistant.pl

比如 用户 admin 密码 123456

# 运行 WebAdmin 管理器

C:\Program Files\Darwin Streaming Server> perl streamingadminserver.pl

第四步:进入管理界面对dss服务器进行管理

1)在浏览器中,输入打http://127.0.0.1:1220/,打开管理界面

2)选择流媒体存放路径,默认存放在流媒体服务器下的:c:\Program Files\Darwin Streaming Server\目录下

3)更改服务器服务端口,可以在streaingloadtool.cfg文件中指定其他端口;

第五步:播放测试

安装vcl播放器,检测dss能不能正常播放

rtsp://localhost/sample_300kbit.mp4

四、流化处理

DSS提供的视频发现都能用,自己考个视频进去咋就播放不了呢?这里涉及到一个概念叫“流化 ”。DSS本身不提供素材的流化操作,但是我们可以借助第三方工具进行处理。

然后执行命令:

C:\Program Files\Darwin Streaming Server\Movies> mp4box mymovie.mp4 -hint

流媒体视频就转换好了,现在文件大小就会有变动,变大了一些。

然后再用VLC打开就可以播放了:

rtsp://localhost/mymovie.mp4

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

在第 教程[2] 中,我们学习了如何使用工作队列在多个工作单元之间分配耗时任务。

但是如果我们想要运行一个在远程计算机上的函数并等待其结果呢?这将是另外一回事了。这种模式通常被称为 远程过程调用RPC

在本篇教程中,我们将使用 RabbitMQ 构建一个 RPC 系统:一个客户端和一个可扩展的 RPC 服务器。由于我们没有什么耗时任务值得分发,那干脆就创建一个返回斐波那契数列的虚拟 RPC 服务吧。

客户端接口#

为了说明如何使用 RPC 服务,我们将创建一个简单的客户端类。该类将暴露一个名为Call的方法,用来发送 RPC 请求并且保持阻塞状态,直到接收到应答为止。

var rpcClient = new RPCClient();

Console.WriteLine(" [x] Requesting fib(30)");
var response = rpcClient.Call("30");
Console.WriteLine(" [.] Got '{0}'", response);

rpcClient.Close();

关于 RPC 的说明

尽管 RPC 在计算机中是一种很常见的模式,但它经常受到批评。问题出现在当程序员不知道一个函数是本地调用还是一个耗时的 RPC 请求。这样的混淆,会导致系统不可预测,以及给调试增加不必要的复杂性。误用 RPC 可能会导致不可维护的混乱代码,而不是简化软件。

牢记这些限制,请考虑如下建议:

  • 确保可以明显区分哪些函数是本地调用,哪些是远程调用。
  • 为您的系统编写文档,明确组件之间的依赖关系。
  • 捕获异常,当 RPC 服务长时间宕机时客户端该如何应对。

当有疑问的时候可以先避免使用 RPC。如果可以的话,考虑使用异步管道 - 而不是类似 RPC 的阻塞,其会将结果以异步的方式推送到下一个计算阶段。

回调队列#

一般来讲,基于 RabbitMQ 进行 RPC 通信是非常简单的,客户端发送一个请求消息,然后服务端用一个响应消息作为应答。为了能接收到响应,我们需要在发送请求过程中指定一个’callback’队列地址。

var props = channel.CreateBasicProperties();
props.ReplyTo = replyQueueName;

var messageBytes = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "",
                     routingKey: "rpc_queue",
                     basicProperties: props,
                     body: messageBytes);


消息属性

AMQP 0-9-1 协议在消息中预定义了一个包含 14 个属性的集合,大多数属性很少使用,但以下情况除外:
Persistent:将消息标记为持久的(值为2)或者瞬时的(其他值),可以参考 教程[2]
DeliveryMode:熟悉 AMQP 协议的人可以选择此属性而不是熟悉协议的人可以选择使用此属性而不是Persistent,它们控制的东西是一样的。
ContentType:用于描述编码的 mime 类型。例如,对于经常使用的 JSON 编码,将此属性设置为:application/json是一种很好的做法。
ReplyTo:通常用于命名回调队列。
CorrelationId:用于将 RPC 响应与请求相关联。

关联ID#

在上面介绍的方法中,我们建议为每个 RPC 请求创建一个回调队列,但是这种方式效率低。幸运的是我们有一种更好的方式,那就是为每个客户端创建一个独立的回调队列。

这种方式会引出一个新的问题,在收到响应的回调队列中,它无法区分响应属于哪一个请求,此时便是CorrelationId属性的所用之处。我们将为每个请求的CorrelationId设置一个唯一值。之后当我们在回调队列接收到响应的时候,再去检查下这个属性是否和请求中的值匹配,如此一来,我们就可以把响应和请求关联起来了。如果出现一个未知的CorrelationId值,我们可以安全的销毁这个消息,因为这个消息不属于我们的请求。

你可能会问,为什么我们应该忽略回调队列中的未知的消息,而不是用错误来标识失败呢?这是因为于服务器端可能存在竞争条件。虽然不太可能,但是 RPC 服务器可能在仅发送了响应消息而未发送消息确认的情况下挂掉,如果出现这种情况,RPC 服务器重启之后将会重新处理该请求。这就是为什么在客户端上我们必须优雅地处理重复的响应,并且理想情况下 RPC 应该是幂等的。

总结#

我们的 RPC 会是这样工作:

  • 客户端启动时,会创建一个匿名的独占回调队列。
  • 对于 RPC 请求,客户端发送带有两个属性的消息:ReplyTo(设置为回调队列)和CorrelationId(为每个请求设置唯一值)。
  • 请求被发送到rpc_queue队列。
  • RPC 工作线程(或者叫:服务器)正在等待该队列上的请求。当出现请求时,它会执行该作业,并使用ReplyTo属性设置的队列将带有结果的消息发送回客户端。
  • 客户端等待回调队列上的数据。出现消息时,它会检查CorrelationId属性。如果它与请求中的值匹配,则返回对应用程序的响应。

组合在一起#

斐波纳契 任务:

private static int fib(int n)
{
    if (n == 0 || n == 1) return n;
    return fib(n - 1) + fib(n - 2);
}

我们宣布我们的斐波那契函数。并假定只允许有效的正整数输入。 (不要期望这个适用于大数字,它可能是最慢的递归实现)。

我们的 RPC 服务端代码 RPCServer.cs 看起来如下所示:

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

class RPCServer
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using (var connection = factory.CreateConnection())
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "rpc_queue", durable: false,
              exclusive: false, autoDelete: false, arguments: null);
            channel.BasicQos(0, 1, false);
            var consumer = new EventingBasicConsumer(channel);
            channel.BasicConsume(queue: "rpc_queue",
              autoAck: false, consumer: consumer);
            Console.WriteLine(" [x] Awaiting RPC requests");

            consumer.Received += (model, ea) =>
            {
                string response = null;

                var body = ea.Body;
                var props = ea.BasicProperties;
                var replyProps = channel.CreateBasicProperties();
                replyProps.CorrelationId = props.CorrelationId;

                try
                {
                    var message = Encoding.UTF8.GetString(body);
                    int n = int.Parse(message);
                    Console.WriteLine(" [.] fib({0})", message);
                    response = fib(n).ToString();
                }
                catch (Exception e)
                {
                    Console.WriteLine(" [.] " + e.Message);
                    response = "";
                }
                finally
                {
                    var responseBytes = Encoding.UTF8.GetBytes(response);
                    channel.BasicPublish(exchange: "", routingKey: props.ReplyTo,
                      basicProperties: replyProps, body: responseBytes);
                    channel.BasicAck(deliveryTag: ea.DeliveryTag,
                      multiple: false);
                }
            };

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }







​ private static int fib(int n)
​ {
​ if (n == 0 || n == 1)
​ {
​ return n;
​ }

​ return fib(n - 1) + fib(n - 2);
​ }
​ }

服务端代码非常简单:

  • 像往常一样,首先建立连接,通道和声明队列。
  • 我们可能希望运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要设置channel.BasicQos中的prefetchCount值。
  • 使用BasicConsume访问队列,然后注册一个交付处理程序,并在其中完成工作并发回响应。

我们的 RPC 客户端 RPCClient.cs 代码:

using System;
using System.Collections.Concurrent;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

public class RpcClient
{
    private readonly IConnection connection;
    private readonly IModel channel;
    private readonly string replyQueueName;
    private readonly EventingBasicConsumer consumer;
    private readonly BlockingCollection<string> respQueue = new BlockingCollection<string>();
    private readonly IBasicProperties props;

public RpcClient()
{
        var factory = new ConnectionFactory() { HostName = "localhost" };

        connection = factory.CreateConnection();
        channel = connection.CreateModel();
        replyQueueName = channel.QueueDeclare().QueueName;
        consumer = new EventingBasicConsumer(channel);

        props = channel.CreateBasicProperties();
        var correlationId = Guid.NewGuid().ToString();
        props.CorrelationId = correlationId;
        props.ReplyTo = replyQueueName;

        consumer.Received += (model, ea) =>
        {
            var body = ea.Body;
            var response = Encoding.UTF8.GetString(body);
            if (ea.BasicProperties.CorrelationId == correlationId)
            {
                respQueue.Add(response);
            }
        };
    }

    public string Call(string message)
    {
        var messageBytes = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish(
            exchange: "",
            routingKey: "rpc_queue",
            basicProperties: props,
            body: messageBytes);

        channel.BasicConsume(
            consumer: consumer,
            queue: replyQueueName,
            autoAck: true);

        return respQueue.Take(); ;
    }

    public void Close()
    {
        connection.Close();
    }
}

public class Rpc
{
    public static void Main()
    {
        var rpcClient = new RpcClient();

        Console.WriteLine(" [x] Requesting fib(30)");
        var response = rpcClient.Call("30");

        Console.WriteLine(" [.] Got '{0}'", response);
        rpcClient.Close();
    }
}

客户端代码稍微复杂一些:

  • 建立连接和通道,并为响应声明一个独有的 ‘callback’ 队列。
  • 订阅这个 ‘callback’ 队列,以便可以接收到 RPC 响应。
  • Call方法用来生成实际的 RPC 请求。
  • 在这里,我们首先生成一个唯一的CorrelationId编号并保存它,while 循环会使用该值来捕获匹配的响应。
  • 接下来,我们发布请求消息,其中包含两个属性:ReplyToCorrelationId
  • 此时,我们可以坐下来稍微一等,直到指定的响应到来。
  • while 循环做的工作非常简单,对于每个响应消息,它都会检查CorrelationId是否是我们正在寻找的那一个。如果是这样,它就会保存该响应。
  • 最后,我们将响应返回给用户。

客户发出请求:

var rpcClient = new RPCClient();

Console.WriteLine(" [x] Requesting fib(30)");
var response = rpcClient.Call("30");
Console.WriteLine(" [.] Got '{0}'", response);

rpcClient.Close();

现在是查看 RPCClient.csRPCServer.cs 的完整示例源代码(包括基本异常处理)的好时机哦。

像往常一样设置(请参见 教程[1]):

我们的 RPC 服务现已准备就绪,现在可以启动服务端:

cd RPCServer
dotnet run

要请求斐波纳契数,请运行客户端:

cd RPCClient
dotnet run

这里介绍的设计并不是 RPC 服务的唯一可能实现,但它仍具有一些重要优势:

  • 如果 RPC 服务器太慢,您可以通过运行另一个服务器来扩展。尝试在新开一个控制台,运行第二个 RPCServer。
  • 在客户端,RPC 只需要发送和接收一条消息。不需要像QueueDeclare一样同步调用。因此,对于单个 RPC 请求,RPC 客户端只需要一次网络往返。

我们的代码很简单,也并没有尝试去解决更复杂(但很重要)的问题,比如就像:

  • 如果服务端没有运行,客户端应该如何反应?
  • 客户端是否应该为 RPC 设置某种超时机制?
  • 如果服务端出现故障并引发异常,是否应将其转发给客户端?
  • 在处理之前防止无效的传入消息(例如:检查边界、类型)。

如果您想进行实验,您可能会发现 管理 UI 对于查看队列非常有用。

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。