[.NET大牛之路 032] 实战:书大师第一个版本开发和部署

上节课讲了我们实战项目(书大师)的开发思想:MVP 理念。并且带大家准备基于 VSC 的 .NET 6 开发环境,创建好了项目,但还没有开始添加任何代码。本节课继续上一课内容,带大家从原型开始,完成基于 .NET 6 的书大师项目最小化功能开发和部署。

本教程的书大师项目源代码放在 GitHub 上,地址为:

https://github.com/liamwang/bookist.cc

大家可以在 Commit 历史记录中参照提交日期查看每节课程所完成的部分。

1 原型设计

为了快速上线,我们先实现网站的最小基础功能。网站第一版功能需求很简单,罗列如下:

  • 每天分享一本好书(更新时间间隔随意);
  • 用户可以查看电子书信息,如书名、出版年月等;
  • 用户可以下载电子书文件,网站本身不做文件存储,通过第三方存储链接下载或转存。

一般自己个人独立开发一个小项目,可以不画原型,因为需求都是自己定的,在脑海里会有产品大概的设计。而团队项目开发,往往需求提出者和开发者不是同一个人,所以要通过需求文档和原型来确保各方在理解上不存在偏差。

现在假设上面列举的需求点是用户或上级领导提供给我们的。拿到需求后,我们先画个原型给对方确认一下,这是不是他们想要的产品:

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署
原型

在得到需求方的确认后,我们就开始编码了。

这里只是为了演示一下 MVP 理念的软件开发流程,后面我们新增功能就不再画原型了。

2 功能开发

用 VSC 打开我们上节课创建好的项目,打开 Program.cs 文件进行编辑,添加如下有注释的三行代码:

var builder = WebApplication.CreateBuilder(args);

// 注册服务
builder.Services.AddRazorPages();

var app = builder.Build();

// 注册中间件
app.UseStaticFiles();
app.MapRazorPages();

app.Run();

由于我们的网站第一个版本只有一个网页,使用 Razor Pages 框架比 MVC 框架更快捷,因为它可以不需要 Controller 和 Model。关于 Razor Pages 和 MVC 的内容我们后面的课程再展开讲。

这里使用 builder.Services.AddRazorPages() 方法注册所需的 Razor Pages 服务组件,比如视图渲染和模型绑定等。app.UseStaticFiles() 用于响应 wwwroot 目录下的静态文件,如图片和样式文件等。app.MapRazorPages() 方法用于注册 Razor Pages 路由中间件,它可以根据 Razor 文件和目录结构按照默认规则自动实现路由映射。

Razor Pages 框架默认把 Pages 目录映射为 Web 网页 URL 的根路径,*.cshtml 文件的目录路径就是对应网页的 URL 路径。比如 Pages/Foo/Bar.cshtml 文件对应的 URL 路径就是 /foo/bar。默认页约定为 Index.cshtml

现在,我们开始编写前端的展现页面吧。

我们需要在 Bookist.Web 目录下创建一个 Pages 文件夹,并添加一个 Index.cshtml 文件作为网站的首页。你暂时可以把 *.cshtml 文件理解为普通的 HTML 文件,只是在 HTML 基础增加了上支持 C# 语言的的 Razor 语法(@)。

按照原型的设计,我们先在 Index.cshtml 文件定义一个 book 匿名对象,用来承载一本电子书的信息:

@page
@{
    var book = new
    {
        Title = "CLR via C# (4th Edition)",
        Cover = "https://img.geekgist.com/Foc1d-NbacAQ6D1WSQ_3UndhaOuR-w2h3",
        Author = "Jeffrey Richter",
        PubDate = "2012年10月",
        Description = "Dig deep and master the intricacies of the CLR, C#, and .NET development. You’ll gain pragmatic insights for building robust, reliable, and responsive apps and components.",
        Format = "PDF",
        FetchUrl = "https://url19.ctfile.com/f/15677019-228693113-e89578",
        FetchCode = "bookist"
    };
}

然后继续在 Index.cshtml 中用 HTML 把这些信息在页面中呈现出来:

...(接上面代码)

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>书大师 • 每天分享一本好书</title>
    <link rel="stylesheet" href="~/index.css" />
  </head>

  <body>
    <header>
      <div class="brand-logo">
        <span>书大师</span>
        <small>•</small>
        <span>每天分享一本好书</span>
      </div>
    </header>
    <main>
      <div class="book-card">
        <img src="@book.Cover" />
        <div class="book-card__body">
          <div class="book-card__info">
            <h3>@book.Title</h3>
            <dl>
              <dt>作者:</dt>
              <dd>@book.Author</dd>
            </dl>
            <dl>
              <dt>日期:</dt>
              <dd>@book.PubDate</dd>
            </dl>
            <dl>
              <dt>简介:</dt>
              <dd>@book.Description</dd>
            </dl>
          </div>
          <div class="book-card__oprate">
            <a class="btn-primary" href="@book.FetchUrl" target="_blank">
              下载 @book.Format
            </a>
            <span>提取码:<b class="book-card__code">@book.FetchCode</b></span>
          </div>
        </div>
      </div>
    </main>
  </body>
</html>

这里我们引入了一个 ~/index.css 样式文件,~ 表示网站根目录。在 Bookist.Web 目录创建一个名为 wwwroot 目录,该目录会被 app.UseStaticFiles() 指定为网站的根目录。在该目录中创建我们需要的 index.css 文件,其内容如下:

*,
::after,
::before {
  box-sizing: border-box;
}

* {
  margin: 0;
  padding: 0;
}

body {
  background: #f5f6f7;
  font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue",
    "PingFang SC", "Microsoft YaHei";
}

.btn-primary {
  color: #fff;
  background-color: #0d6efd;
  border-color: #0d6efd;
  display: inline-block;
  font-weight: 400;
  line-height: 1.5;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
  user-select: none;
  border: 1 px solid transparent;
  padding: 7px 14px;
  font-size: 14px;
  border-radius: 3px;
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
    border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.btn-primary:hover {
  color: #fff;
  background-color: #0b5ed7;
  border-color: #0a58ca;
}

header {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 50px;
  color: #353535;
  background: #fff;
  border-bottom: 1px solid #eee;
}
.brand-logo {
  font-size: 16px;
  font-weight: bold;
}
.brand-logo > small {
  margin: 0 4px;
  color: #999;
}

main {
  display: flex;
  align-items: center;
  justify-content: center;
}

.book-card {
  position: relative;
  display: flex;
  max-width: 660px;
  background: #fff;
  margin: 80px 20px;
  padding: 12px;
  border-radius: 4px;
  box-shadow: 0 5px 10px rgb(0 0 0 / 10%);
}
.book-card > img {
  width: 180px;
  margin-right: 15px;
}
.book-card__body {
  display: flex;
  flex-direction: column;
  padding-bottom: 4px;
}
.book-card__info {
  flex: 1;
}
.book-card__info h3 {
  font-size: 16px;
}
.book-card__info dl {
  display: flex;
  margin: 5px 0;
  font-size: 14px;
  text-align: justify;
}
.book-card__info dt {
  width: 40px;
}
.book-card__info dd {
  flex: 1;
}
.book-card__oprate .btn-primary {
  margin-right: 15px;
}
.book-card__code {
  color: #ff8850;
}

到这,我们的代码基本编写好了,我们总共编写了三个文件。在编写代码时,我们可以使用如下命令在本地边查看页面效果边修改代码:

$ dotnet watch run

这个命令与 dotnet run 的区别是,它是热重载的,修改代码后浏览器会同步更新,不用每次都重新运行。

运行后效果如图:

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署

大家可以根据自己喜好修改页面内容和样式。

3 生产环境准备

我个人目前用的是腾讯云的服务器,操作系统为 CentOS 8,大家可以根据自己的情况来。推荐有条件的直接买云服务器,没条件的可以买了之后体验几天再无理由退款,阿里云和腾讯云都支持 5 天无理退款。现在刚好双 11,各云服务商对新人都有非常大的优惠,不到一百元就可以入手一台用于学习的云服务器(时长 1 年)。如果实在不想用云服务器也可以在自己电脑安装虚拟机进行部署。

拿到云服务器后请确保端口 22、80、443 是开放的,一般默认是开放的。以腾讯服务器为例,在实例列表的右边依次点击 更多->安全组->配置安全组,再点击关联的安全组即可查看已开放的端口。

现在我们开始安装环境,使用免费的 Xshell 或 Putty 终端工具连接上我们的服务器(我个人用的是 Xshell):

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署

先执行下面的命令对系统基础包进行升级:

$ dnf update -y

dnf 是新一代的 rpm 软件包管理器,它取代了 yum,已经预装在 CentOS 8 中了。

如果你对 Linux 命令很陌生,没关系,我们用到的命令很少,后面你跟着我操作就行。对于文件的操作,不熟悉命令行的同学可以使用 Xftp 或 WinSCP 工具来替代命令行操作。

4 安装 Nginx

使用如下命令查看一下软件库中 Nginx 的版本:

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署
nginx version

可以看到,软件库中的 Nginx 版本是 1.14.1,直接安装将会安装软件库中的版本。但这个版本太旧了,我们需要安装最新的稳定版本。我们可以通过自定义 Nginx 包源来操作,方式如下。

使用如下命令创建并打开一个 /etc/yum.repos.d/nginx.repo 文件:

$ vim /etc/yum.repos.d/nginx.repo

这里会进入 vim 的编辑界面,先按 i 键切换到插入模式,复制下面内容粘贴进去:

[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

再按 Esc 键回到查看模式,输入 :wq 保存退出。这一步你也可以通过 Xftp 等工具进行可视化操作。

然后再使用下面命令进行安装:

$ dnf install nginx -y
...
Installed:
  nginx-1:1.20.1-1.el8.ngx.x86_64

安装完成后,可以在输出信息中看到我们安装的 Nginx 最新稳定版号(这里是 1.20.1)。

再使用如下命令启动 Nginx 服务:

$ systemctl start nginx

使用浏览器访问服务器 IP,可以看到 Nginx 的默认页:

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署
nginx index

这样我们就完成了 nginx 的安装。

5 安装 .NET 6 运行时

截止本文写作时,.NET 6 的最新版本是 RC 2,由于不是正式版本,Linux 各发行版的包源中还没有,所以需要从二进制安装。在生产环境,我们只需安装 ASP.NET Core 运行时而不用安装 SDK。依次执行如下命令进行安装:

~$ curl -Lo dotnet.tar.gz https://download.visualstudio.microsoft.com/download/pr/a38f03ab-cab1-4dc9-9632-ac8f3ce4541a/af681d66907ead1d52c7187e50bccf0f/aspnetcore-runtime-6.0.0-rc.2.21480.10-linux-x64.tar.gz
~$ mkdir dotnet
~$ tar -C dotnet -xf dotnet.tar.gz
~$ rm dotnet.tar.gz
~$ export DOTNET_ROOT=~/dotnet
~$ export PATH=$PATH:~/dotnet
~$ dotnet --info
Host (useful for support):
  Version: 6.0.0-rc.2.21480.5
  Commit:  6b11d64e7e

.NET SDKs installed:
  No SDKs were found.

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.0-rc.2.21480.10 [/root/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.0-rc.2.21480.5 [/root/dotnet/shared/Microsoft.NETCore.App]

$ 前面的 ~ 表示在用户目录下操作(root 用户对应的是 /root 目录)。如果你不是 Linux x64 架构,请至这里找到对应的软件包链接替换上面的连接:

https://dotnet.microsoft.com/download/dotnet/6.0

待 .NET 6 正式版本发布后即可直接通过 dnf install dotnet 安装了,现在直接用这个命令安装的是 .NET 5 版本。

6 部署应用

1、发布文件到服务器

在项目的 Bookist.Web 目录下,运行如下命令生成发布文件:

$ dotnet publish -c release -o bin/Publish

这里 -c 参数用于指定编译模式(为 Release),-o 参数指定发布文件输出目录,这里我们将发布文件生成到 bin/Publish 目录中。

接着,在服务器根目录(/)下创建一个名 /app/bookist 的目录,用来存放我们的发布文件。这里我们使用 Xftp 工具把文件传到服务器该目录中(直接拖或者复制粘贴):

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署

后面我们发布新版本时,直接打开 Xftp 把文件复制替换上去就可以了。

2、配置 Linux 服务

为使每次服务器重启能自动启动我们的应用程序和监测应用程序的运行状态,我们需要把我们的应用配置为 Linux 服务。使用 Xshell 等工具在 /etc/systemd/system/ 目录下创建一个 bookist.service 文件:

$ vim /etc/systemd/system/bookist.service

切换到 vim 插入模式,复制粘贴以下内容:

[Unit]
Description=Bookist Website

[Service]
WorkingDirectory=/app/bookist
ExecStart=/root/dotnet/dotnet /app/bookist/Bookist.Web.dll --urls=http://localhost:3000
Restart=always
RestartSec=10
SyslogIdentifier=bookist
User=root
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

通过字面意思大家应该能理解文件的内容,这里就不一一解释了。注意,这里我们指定了应用的端口为 3000。指定应用的端口方式有很多种,在服务器环境使用这种方式指定是有好处的,这个我们后续讲到相关知识点的时候再讲。

然后执行下面命令将服务设置开机自动启动,并启动服务和检查服务状态并:

$ systemctl enable bookist.service
$ systemctl start bookist.service
$ systemctl status bookist.service

看到下图所示绿色 active 字样说明服务正常启动了:

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署

3、配置 Nginx 站点

在目录 /etc/nginx/conf.d/ 中创建一个名为 bookist.cc.conf 的文件:

$ vim /etc/nginx/conf.d/bookist.cc.conf

编辑内容如下:

server {
    listen        80;
    server_name   bookist.com *.bookist.com;
    location / {
        proxy_pass         http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

如果你没有域名,这里的 server_name 可以指定为 localhost,但需要把 /etc/nginx/conf.d/ 中的 default 文件删除或改为其它端口号,不然会冲突。

当然,如果你使用自己的域名,需要到 DNS 服务端将域名指向你的服务器 IP。

配置完站点文件,记得执行如下命令然后重新加载一下 Nginx 配置:

$ nginx -s reload

最后,打开浏览器访问我们的网站:

[.NET大牛之路 032] 实战:书大师第一个版本开发和部署

到这,我们的第一个版本就成功上线了,后续会逐步迭代增加新的功能。

大家在部署过程中如果遇到什么问题请尽量在星球文章链接下面留言交流,方便其他人也可以随时查看。

本文来自http://cnblogs.com/willick,经授权后发布,本文观点不代表Chaoqiang's Blog立场,转载请联系原作者。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

近期个人博客正在迁移中,原博客请移步此处,抱歉!