首页 - 部署 原创
NOTE
这里简单介绍我的首页创建过程,以及邮件功能的设计过程。
2021-12-09 @Young Kbt
序言
在学习完 Nginx 的知识后,我看着 Nginx 的欢迎页面细想了很久,眼中的世界太过单调,总觉得不够好看,而且无法给我的服务器提供任何信息介绍,而周围的朋友直接是将其代理到其他页面。
当时我就有了一些想法,替换 Nginx 的欢迎页面,将新的页面作为入口页面,介绍网站功能的同时,提供博客、项目导航入口。比如我部署的一个项目,那么在首页就会有提示,如点击跳转,这样就不必记住项目的 URL 地址,只需要记住服务器地址,那么服务器其他的内容,都汇聚于首页。
如果你是从 Github 或者 Gitee 进入到我的博客,那么可以去看看我的服务器首页,希望不会让你失望,点击跳转。
如果你看完了下面的内容,需要我的服务器首页、404 页面、邮箱功能、下载站点功能的源码,那么 点击跳转。
首页部署
首先准备好一个首页,不需要打包之类的,Nginx 的首页只是一个 index.html 加点 CSS 和 JS 文件即可,不需要像一个项目那样完整。
利用工具连接服务器,我是用的是 Xftp,将其上传到 Nginx 的默认路径下。
Nginx 的默认路径下如果你不知道,打开 Nginx 的配置文件,看 80 或者 431 端口的 location / { ... } 里的 root 指定的路径,那就是 Nginx 的默认路径。

这就是我的默认路径,所以将首页以及静态文件上传到 Nginx 默认路径下。
在本地,我的首页结构如下:
.
├── index.html
│ ├── assets (静态文件目录)
│ │ ├── css(样式目录)
│ │ ├── fonts(字体目录)
│ │ ├── images(图片目录)
│ │ ├── js(JavaScript 目录)
│ ├── vendor(JavaScript 库)
│ │ ├── bootstrap
| │ ├── jquery上传到服务器后,因为默认路径下可能有太多文件,所以我对其分类,创建一个 static 文件夹。
根目录
├── index.html
|—— static(静态文件目录)
│ ├── assets
│ │ ├── css(样式目录)
│ │ ├── fonts(字体目录)
│ │ ├── images(图片目录)
│ │ ├── js(JavaScript 目录)
│ ├── vendor(JavaScript 库)
│ │ ├── bootstrap
| │ ├── jquery根目录,就是 /usr/local/openresty/nginx/html 目录。
上传首页后,记得修改 index.html 有关 css 和 js 的引入路径,因为 Nginx 的获取资源规则和本地的不一样。
在本地,我们在 index.html 可以这样写:(部分)
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<link href="assets/fonts/iconfont.css" rel="stylesheet" />
<script src="vendor/jquery/jquery.min.js"></script>但是上传到 Nginx 后,我们必须在开头加上 /,代表 Nginx 的根目录,所以我们需要这样写:(部分)
<link href="/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<link href="/static/assets/fonts/iconfont.css" rel="stylesheet" />
<script src="/static/vendor/jquery/jquery.min.js"></script>然后,访问服务器的域名,即可访问这个首页。当然,如果希望访问域名的后面再加个 /home,如访问 https://www.youngkbt.cn/home ,也能访问首页的话,需要在配置文件进行配置。
server {
listen 80; # 431 是 https 的默认端口,80 是 http 的默认端口
server_name www.youngkbt.cn;
# ...... 其他 location
location /home {
alias /usr/local/openresty/nginx/html;
index index.html;
}
# ...... 其他 location
}这里使用的是 alias 指令,因为 root 指令会把 location 后面匹配的请求拼接到目录后,而 alias 指令则会忽略 location 后匹配的请求。
alias 指令不会忽略匹配后面的请求,如访问 /home/aa,则忽略 /home,但是把 /aa 拼接到目录后,但是这不影响我们访问 /home 就能访问到首页,这里只是介绍使用 alias 指令的特点。
JS 文件
在首页,我简单实现了 Element UI 的消息提示效果,也实现了邮箱发送功能,这里提供源码:
$(function () {
var storageName = "isSend";
// 防止重复发送
var isSend = false;
$(".button").on("click", function () {
if ($("#name").val() == "") {
addTip("姓名不能为空,请填写", "danger");
return;
} else if ($("#email").val() == "") {
addTip("邮箱不能为空,请填写", "danger");
return;
} else if ($("#subject").val() == "") {
addTip("标题不能为空,请填写", "danger");
return;
} else if ($("#message").val() == "") {
addTip("消息不能为空,请填写", "danger");
return;
} else {
var email = $("#email").val();
var reg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/;
if (
reg.test(email) &&
(sessionStorage.getItem(storageName) != "true" || isSend == false)
) {
if (confirm("确认要发送吗?")) {
sendEmail();
sessionStorage.setItem(storageName, true);
isSend = true;
}
} else if (
sessionStorage.getItem(storageName) == "true" ||
isSend == true
) {
addTip("请不要重复发送消息", "warning");
} else {
addTip("邮箱格式不正确,请填写", "danger");
}
}
// 获取表单信息
// console.log($("#contact").serialize());
});
function sendEmail() {
$.post("/sendEmail", $("#contact").serialize(), function (res, error) {
if (res == "OK") {
addTip("发送消息成功", "success");
setTimeout(() => {
window.location.reload();
sessionStorage.removeItem(storageName);
isSend == false;
}, 1500);
} else {
console.log("失败的原因:", error);
addTip("发送失败,可能发送超时或消息被拦截,请稍后再重试", "tip");
}
});
}
// 添加消息提示
function addTip(content, type) {
var time = new Date().getTime();
// 获取最后消息提示元素的高度
var top =
$(".tip:last").attr("data-top") == undefined
? 0
: $(".tip:last").attr("data-top");
// 如果产生两个以上的消息提示,则出现在上一个提示的下面,即高度添加,否则默认 20
var lastTop =
parseInt(top) +
($(".tip").length > 0 ? $(".tip:last").outerHeight() + 17 : 20);
if (type == "success" || type == 1) {
$("#page-wraper").append(
`<div class="tip tip-success ${time}" style="top: ${parseInt(
top
)}px" data-top="${lastTop}"><i class="iconfont icon-dagouyouquan icon"></i><p class="tip-success-content">${content}</p></div>`
);
} else if (type == "danger" || type == 2) {
$("#page-wraper").append(
`<div class="tip tip-danger ${time}" style="top: ${parseInt(
top
)}px" data-top="${lastTop}><i class="iconfont icon-cuowu icon"></i><p class="tip-danger-content">${content}</p></div>`
);
} else if (type == "info" || type == 3) {
$("#page-wraper").append(
`<div class="tip tip-info ${time}" style="top: ${parseInt(
top
)}px" data-top="${lastTop}><i class="iconfont icon-info icon"></i><p class="tip-info-content">${content}</p></div>`
);
} else if (type == "warning" || type == 4) {
$("#page-wraper").append(
`<div class="tip tip-warning ${time}" style="top: ${parseInt(
top
)}px" data-top="${lastTop}><i class="iconfont icon-gantanhao icon"></i><p class="tip-warning-content">${content}</p></div>`
);
}
// 动画往下滑动
$("." + time).animate({
top: parseInt(lastTop) + "px",
opacity: "1",
});
// 消息提示 3 秒后隐藏并被删除
setTimeout(() => {
$("." + time).animate({
top: "0px",
opacity: "0",
});
setTimeout(() => {
$("." + time).remove();
}, 500);
}, 3000);
}
});消息提示效果的 CSS 文件内容:
/* 消息提示样式 */
.tip {
position: fixed;
display: flex;
height: 48px;
top: -10px;
left: 50%;
opacity: 0;
min-width: 320px;
transform: translateX(-50%);
transition: opacity 0.3s linear, top 0.4s, transform 0.4s;
z-index: 99999;
padding: 15px 15px 15px 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
grid-row: 1;
line-height: 17px;
}
.tip p {
line-height: 17px;
margin: 0;
font-size: 14px;
}
.icon {
margin-right: 10px;
line-height: 17px;
}
.tip-success {
color: #67c23a;
background-color: #f0f9eb;
border-color: #e1f3d8;
}
.tip-success .tip-success-content {
color: #67c23a;
}
.tip-danger {
color: #f56c6c;
background-color: #fef0f0;
border-color: #fde2e2;
}
.tip-danger .tip-danger-content {
color: #f56c6c;
}
.tip-info {
background-color: #edf2fc;
border-color: #ebeef5;
}
.tip-info .tip-info-content {
color: #909399;
}
.tip-warning {
color: #e6a23c;
background-color: #fdf6ec;
border-color: #faecd8;
}
.tip-warning .tip-warning-content {
margin: 0;
color: #e6a23c;
line-height: 21px;
font-size: 14px;
}
/* 下面是二维码样式 */
.social-tip {
margin-bottom: 170px;
display: none;
}
.square {
width: 0;
height: 0;
border-bottom: 7px solid rgba(118, 25, 172, 0.3);
border-right: 7px solid transparent;
border-left: 7px solid transparent;
position: relative;
left: 36%;
}
.social-info {
width: 200px;
position: absolute;
line-height: 48px;
left: -95%;
margin-left: -40px;
background-color: rgba(118, 25, 172, 0.3);
color: #fff;
padding: 0 15px 15px;
}
.social-info img {
width: 160px;
height: 160px;
}如下效果:

我设计了四个提示,分别是成功绿色,提示灰色,警告黄色,错误红色。而提示语前面的图标,需要自己去阿里云的矢量库进行获取,并在首页引用,矢量库跳转。
404 部署
你喜欢 Nginx 自带的 404 页面吗?我可能不是特别喜欢,所以我们可以自定义好看的 404 页面,又或者去网上下载别人做好的 404 页面模板。如我的 404 页面如图:

点击头像还能播放音乐。
利用 xftp 将 404 页面上传到服务器上,我在 Nginx 根目录下创建了一个 404 文件夹,将 404 页面和其 CSS 和 JS 文件放到这个 404 文件夹里。
目录结构:
根目录
|—— 404(这是个目录)
│ ├── 404.html( 404 页面)
│ ├── css(样式目录)
│ ├── img(图片目录)
│ ├── js(JavaScript 目录)
| ├── music(音乐目录)
|
├── index.html(其他页面)上传后到 Nginx 后,养成好习惯,打开 404.html,修改 CSS、JS 文件的引入路径,因为 Nginx 的获取资源规则和本地的不一样。
记得开头加 /,改为:(部分)
<link rel="stylesheet" href="/404/css/ghost.css" />
<script type="text/javascript" src="/404/js/jquery.min.js"></script>然后在配置文件修改 404 页面的访问路径
server {
listen 431; # 431 是 https 的默认端口,80 是 http 的默认端口
server_name youngkbt.cn;
# ...... 其他 location
error_page 404 500 502 503 504 /404.html;
location = /404.html {
root /usr/local/openresty/nginx/html/404;
index 404.html;
}
# ...... 其他 location
}此时你随便访问我的服务器地址,如 https://www.youngkbt.cn/aaa 那么就会显示我的 404 页面。在该页面中,点击我的头像后,音乐会伴随头像的旋转而缓缓响起,静静享受 404 带有的静谧时刻。
博客部署
我的博客已经部署在 Github 和 Gitee 中,如果你看了上一个文章,部署自己的博客到了服务器,那么就请在 Nginx 设置一个 location 模块,进行跳转吧。
我设置了两个 location 模块,当输入 /notes 或者 /note-blog 的时候,都会跳转到我的博客首页
location /notes {
rewrite ^/notes/(\w*)$ /notes-blog/$1;
}
location /notes-blog {
root /home/kbt;
index index.html;
}实际上,最终都会跳到 /note-blog 的 location 模块里。
我们不仅可以设置博客的 location,也可以设置浏览器的缓存静态文件时间,因为博客的静态文件太多,当用户每次访问都从 Nginx 服务器获取静态文件,那显然不理智,我们可以让用户访问过的静态文件,缓存到用户的浏览器中,这样,用户再次访问博客的时候,直接从浏览器本地获取,打开的速度非常快。
我设置了静态文件缓存 7 天,html 文件缓存 1 天
location ~ /note-blog/.*\.(js|css|png|jpg|jpeg|gif)$ {
root /home/kbt;
expires 7d; # 缓存七天
}
location ~ /note-blog/.*\.(html)$ {
root /home/kbt;
expires 1d; # 缓存一天
}看到 root 了吗,我的博客并没有放在 Nginx 的默认路径下,而是由普通用户 kbt 管理。防止滥用 root 权限,避免被别人恶意访问。
邮箱部署
建议你看到这里马上停住,然后点击 测试发送邮箱,输入你的邮箱,进行发送,体验之后再来学习,会有更大的收获和兴趣。
邮箱项目我使用了 node 和 express 来搭建简单的服务器,然后利用 nodemailer 进行邮件发送,log4j.js 进行日志信息存储。
这是一个 node 简单项目,安装的依赖只有三个,express、nodemailer、log4js。
邮箱源码
我的 package.json 文件内容如下:
{
"name": "email",
"version": "1.0",
"private": true,
"scripts": {
"dev": "node app.js"
},
"dependencies": {
"express": "^4.17.1",
"log4js": "^6.3.0",
"nodemailer": "^6.7.2"
}
}依赖只有三个,如何安装呢,我使用 yarn 进行安装。
yarn add express --save
yarn add log4js --save
yarn add nodemailer --save安装完,我们先编写 log.js 文件,实现日志功能
// 引入插件 log4js
var log4js = require("log4js");
log4js.configure({
appenders: {
// 配置日志文件
access: {
// 访问日志的 name
type: "file",
filename: "logs/access.log",
layout: {
type: "pattern",
pattern: "[%d{yyyy-MM-dd hh:mm:ss SSS}] [%p] %c - %m",
},
},
error: {
// 错误日志的 name
type: "file",
filename: "logs/error.log",
layout: {
// 定义日志输出的样式
type: "pattern",
pattern: "[%d{yyyy MM dd hh:mm:ss SSS}] [%p] %c - %m%n", // 日志输出时间格式
},
},
},
categories: {
// 配置日志级别,以及引用 appenders 配置的日志文件
default: { appenders: ["access"], level: "info" }, // 上方 appenders 的 name
error: { appenders: ["error"], level: "error" },
},
});
exports.logger = function (name) {
// name 取 categories 的 name
return log4js.getLogger(name || "default");
};
exports.use = function (app, logger) {
app.use(
log4js.connectLogger(logger || log4js.getLogger("default"), {
level: "info",
format: "请求类型/URI:「 :method:url 」",
})
); // 请求类型/URI 格式设置
};文件内容不难理解,我们首先配置要输出的日志文件路径以及名字,然后配置日志输出的信息时间格式,然后配置日志的格式,如 info 或者 error,接着配置请求类型/URI 格式,最后暴露出去。
日志的输出内容如图所示:

编写 route.js 文件,相信学过 express 的伙伴已经非常熟悉了
const express = require("express");
const router = express.Router();
// 引入日志
const logger = require("./log").logger();
const errLogger = require("./log").logger("error");
// 引入邮件发送功能
var nodemailer = require("nodemailer");
router.post("/email", (req, res) => {
var data = req.body;
// 获取发送的模板
const myHtml = require("./email/emailHtml").myHtml(data);
const otherHtml = require("./email/emailHtml").otherHtml(data);
// 创建连接
var transporter = nodemailer.createTransport({
host: "smtp.163.com",
port: 465, // SMTP 端口
secureConnection: true, // 使用 SSL 方式(安全方式,防止被窃取信息)
auth: {
user: "kele_bingtang@163.com",
// 这里密码不是 qq 密码,是你设置的 smtp 密码
pass: "GJQDWNWVGYEVTSMB",
},
});
// 邮件参数(我的)
var myOptions = {
from: "kele_bingtang@163.com", // 发件地址
to: "kele_bingtang@163.com,2456019588@qq.com", // 收件列表
subject: data.subject, // 标题
// text 和 html 同时发送只支持一种
html: myHtml,
// text: "测试",
/* attachments:[{ // 附件
filename: '', // 附件名
path: '' // 附件路径
}] */
};
// 邮件参数(发件人)
var otherOptions = {
from: "kele_bingtang@163.com", // 发件地址
to: `kele_bingtang@163.com,${data.email}`, // 收件列表
subject: "Young Kbt 的致谢", // 标题
// text 和 html 同时发送只支持一种
html: otherHtml,
};
// 发送邮件(给发件人)
transporter.sendMail(otherOptions, function (error, info) {
if (error) {
errLogger.error(
"发送给「 " + data.name + " 」失败,原因:「 " + error + " 」"
);
res.status(200).send("error:" + error);
} else {
logger.info("发送给「 " + data.name + " 」成功");
}
console.log("发送给发件人的响应信息:");
console.log(info);
});
// 发送邮件(给我)
transporter.sendMail(myOptions, function (error, info) {
console.log("错误信息:" + error);
if (error) {
errLogger.error("发送给「 Young Kbt 」失败,原因:「 " + error + " 」");
res.status(200).send("error:" + error);
} else {
res.status(200).send("OK");
logger.info(
"发件邮箱:「 " +
info.envelope.from +
" 」,收件邮箱:「 " +
info.envelope.to +
" 」"
);
logger.info("响应结果:「 " + info.response + " 」");
console.log("发送给我的响应信息:");
console.log(info);
}
});
logger.info(
"发件人:「 " +
data.name +
" 」,发件人的邮箱:「 " +
data.email +
" 」,发件主题:「 " +
data.subject +
" 」,发件消息:「 " +
data.message +
" 」"
);
setTimeout(() => {
logger.info("发送一次邮件的日志分割线 ------------------------\n");
}, 4000);
setTimeout(() => {
transporter.close();
}, 10000);
});
module.exports = router;第 12 和 13 行引用自定义的邮件页面模板,模板大家根据自己的需求设计邮件页面。
我的邮件页面代码位于根目录下的 email 目录,名字叫 emailHtml,源码为:
// 发送给我
exports.myHtml = function (data) {
return `<div style="width: 600px;margin: 0 auto;">
<includetail>
<table style="text-align: center; font-size: 16px; color: #333333; border-spacing: 0px; border-collapse: collapse; width: 580px; direction: ltr">
<tbody>
<tr>
<td style="font-size: 14px; padding: 0px 0px 7px 0px; text-align: center;color: #0044CC">
${data.name} 在 Young Kbt 首页发送给您
</td>
</tr>
<tr style="background-color: #2279BD">
<td style="padding: 0px">
<table style="border-spacing: 0px; border-collapse: collapse; width: 100%">
<tbody>
<tr>
<td style="padding: 0px; text-align: center;">
<img src="https://cdn.jsdelivr.net/gh/Kele-Bingtang/static/user/20211205131212.jpg" alt="请在上方选择信任,以此显示头像">
</td>
</tr>
<tr>
<td style="font-size: 38px; color: #FFFFFF; padding: 12px 22px 4px 22px; text-align: center;" colspan="3">
Young Kbt
</td>
</tr>
<tr>
<td style="font-size: 20px; color: #FFFFFF; padding: 0px 22px 18px 22px; text-align: center;" colspan="3">
人闲车马慢,路遥星亦辞
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td style="background-color: #5BA9DF; border-bottom-style: solid; border-bottom-color: #2279BD; border-bottom-width: 4px;">
<table style="color: #333333; border-spacing: 0px; border-collapse: collapse; width: 100%; color: #fff">
<tbody>
<tr>
<td style="font-size: 18px; padding: 0px 0px 5px 0px;">
<p style="text-align: center">
<span style="font-weight:bold;">${
data.name
} </span>
<span>发送的主题:</span></p>
<p style="font-size: 16px; letter-spacing: 0.5px; text-indent: 16px; padding:0 20px; line-height: 30px; text-align: left;">
${data.subject}
</p>
</td>
</tr>
<tr>
<td style="font-size: 18px; padding: 0px 0px 5px 0px;">
<p style="text-align: center; margin-top: 0;">
<span style="font-weight:bold;">${
data.name
} </span>
<span>发送的内容:</span>
</p>
<p style="font-size: 16px;letter-spacing: 0.5px; text-indent: 16px; padding:0 20px; line-height: 30px; text-align: left;">
${data.message}
</p>
</td>
</tr>
<tr>
<td style="font-size: 16px; padding: 30px 20px; text-align: center">
如果您希望回复他/她,请发送到他/她的邮箱:
<p style="color: #0044CC; font-weight: bold">${
data.email
}</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td style="padding: 35px 0px; color: #B2B2B2; font-size: 12px">
From Young Kbt
<br>
This is a WebSite
<br>
${new Date().getFullYear()}-${
new Date().getMonth() + 1 == 13 ? 12 : new Date().getMonth() + 1
}-${
new Date().getDate() > 10
? new Date().getDate()
: "0" + new Date().getDate()
}
${
new Date().getHours() > 10
? new Date().getHours()
: "0" + new Date().getHours()
}:${
new Date().getMinutes() > 10
? new Date().getMinutes()
: "0" + new Date().getMinutes()
}:${
new Date().getSeconds() > 10
? new Date().getSeconds()
: "0" + new Date().getSeconds()
}
</td>
</tr>
<tr>
<td style="padding: 0px 0px 10px 0px; color: #B2B2B2; font-size: 12px">
Copyright Young Kbt WebSite
</td>
</tr>
</tbody>
</table>
</includetail>
</div`;
};
// 发送给发件人
exports.otherHtml = function (data) {
return `<div style="width: 600px;margin: 0 auto;">
<includetail>
<table
style="text-align: center; font-size: 16px; color: #333333; border-spacing: 0px; border-collapse: collapse; width: 580px; direction: ltr">
<tbody>
<tr>
<td style="font-size: 14px; padding: 0px 0px 7px 0px; text-align: center;color: #0044CC">
尊敬的 <span style="font-weight: bold;">${
data.name
}</span>,Young Kbt 感谢您的邮件
</td>
</tr>
<tr style="background-color: #2279BD">
<td style="padding: 0px">
<table style="border-spacing: 0px; border-collapse: collapse; width: 100%">
<tbody>
<tr>
<td style="padding: 0px; text-align: center;">
<img src="https://cdn.jsdelivr.net/gh/Kele-Bingtang/static/user/20211205131212.jpg" alt="请在上方选择信任,以此显示头像">
</td>
</tr>
<tr>
<td style="font-size: 38px; color: #FFFFFF; padding: 12px 22px 4px 22px; text-align: center;"
colspan="3">
Young Kbt
</td>
</tr>
<tr>
<td style="font-size: 20px; color: #FFFFFF; padding: 0px 22px 18px 22px; text-align: center;"
colspan="3">
人闲车马慢,路遥星亦辞
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
style="background-color: #5BA9DF; border-bottom-style: solid; border-bottom-color: #2279BD; border-bottom-width: 4px;">
<table style="color: #333333; border-spacing: 0px; border-collapse: collapse; width: 100%; color: #fff">
<tbody>
<tr>
<td style="font-size: 18px; padding: 0px 0px 5px 0px;">
<p style="text-align: center">
<span style="font-weight:bold">Young Kbt</span>
<span>提示您:</span>
</p>
<p
style="font-size: 16px; letter-spacing: 0.5px; text-indent: 32px; padding:0 20px; line-height: 30px; text-align: left;">
本邮件由 Young Kbt's index 网站发送给您,
<span style="color: #1546a8; font-weight: bold;">如果非本人操作,请忽略即可。</span>
</p>
</td>
</tr>
<tr>
<td style="font-size: 18px; padding: 0px 0px 5px 0px;">
<p style="text-align: center; margin-top: 0;">
<span style="font-weight:bold">Young Kbt</span>
<span>回复您:</span>
</p>
<p
style="font-size: 16px;letter-spacing: 0.5px; text-indent: 32px; padding:0 20px; line-height: 30px; text-align: left;">
感谢您提供的宝贵消息,我会根据您的内容尽快回复您,如果时间较延迟,请您见谅。
</p>
</td>
</tr>
<tr>
<td style="font-size: 16px; padding: 30px 20px; text-align: center">
如果您对我的网站感兴趣,请访问:
<p style="color: #0044CC; font-weight: bold">
<a href="youngkbt.cn" style="color: #0044CC; text-decoration: none">youngkbt.cn</a>
</p>
<p style="color: #0c3388;font-size: 14px; margin: 15px 0 0 0;">本网站仅是个人使用,并不带有商业用途</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td style="padding: 35px 0px; color: #B2B2B2; font-size: 12px">
From Young Kbt
<br>
This is a WebSite
<br>
${new Date().getFullYear()}-${
new Date().getMonth() + 1 == 13 ? 12 : new Date().getMonth() + 1
}-${
new Date().getDate() > 10
? new Date().getDate()
: "0" + new Date().getDate()
}
${
new Date().getHours() > 10
? new Date().getHours()
: "0" + new Date().getHours()
}:${
new Date().getMinutes() > 10
? new Date().getMinutes()
: "0" + new Date().getMinutes()
}:${
new Date().getSeconds() > 10
? new Date().getSeconds()
: "0" + new Date().getSeconds()
}
</td>
</tr>
<tr>
<td style="padding: 0px 0px 10px 0px; color: #B2B2B2; font-size: 12px">
Copyright Young Kbt WebSite
</td>
</tr>
</tbody>
</table>
</includetail>
</div>`;
};编写 express 的入口文件 app.js,监听 5678 端口
const express = require("express");
const router = require("./router");
const log = require("./log");
const app = express();
log.use(app);
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use((request, response, next) => {
response.header("Access-Control-Allow-Origin", "*"); // 跨域最重要的一步 设置响应头
next(); // 执行 next 函数执行后续代码
});
app.use("/", router);
app.listen("5678", () => {
console.log("5678 端口的服务器启动成功");
});写完邮箱的功能,我们可以进行测试,进入邮箱项目的根目录,使用 node app.js 启动 node 项目。
node app.js因为是 POST 请求,所以使用 Postman 等工具进行测试,访问 localhost:5678/email,填写 JSON 格式,包含 name、email、subject、message 四个参数的数据即可。

官方镜像部署
在本地编写测试完邮箱的功能后,我们将其上传到服务器,我上传的目录是 docker/node/email 下,然后需要服务器安装 node 环境,我使用的是 docker,所以直接拉取 node。
docker pull node下载完 node 镜像后,不要马上启动它,因为 node 项目需要执行打包命令,生成 node_modules 目录,所以使用 Dockerfile 文件来执行打包命令。
在 docker/node/email 目录下,创建并编写 Dockerfile 文件
cd docker/node/email
vim Dockerfile添加如下内容:
FROM node # 基于 node 镜像创建新的镜像
WORKDIR /home/email # 创建默认工作目录
COPY . /home/email # 将当前目录的所有内容拷贝到容器中
RUN npm i # 执行打包命令
EXPOSE 5678 # 暴露 5678 端口
ENTRYPOINT node ./app.js # 启动容器时,启动 app.js 文件不难看出,在生成 docker 镜像的同时,它会将当前目录下的所有内容拷贝到容器中,这些内容就是我们写的邮箱项目。接着它会执行打包命令 npm i,i 代表 install。然后暴露 5678 端口,最后在启动的时候,启动 app.js 文件,也就是执行项目。
写好 Dockerfile 文件,我们执行这个文件,构建基于 node 环境而搭建的「邮箱发送」镜像
docker build -t email:1.0 .启动这个名叫 email、版本为 1.0 的容器,当启动这个容器的时候,内部就会执行 node ./app.js 命令,也就是 Dockerfile 文件的 ENTRYPOINT 指令,部署项目。
docker run -d --name email \
-v /docker/node/email/router.js:/home/email/router.js \
-v /docker/node/email/app.js:/home/email/app.js \
-v /docker/node/email/log.js:/home/email/log.js \
-v /docker/node/email/logs:/home/email/logs \
-v /docker/node/email/email:/home/email/email \
--network web --network-alias email \
email:1.0不要惊讶会有那么多启动命令,这里主要实现挂载功能,只要宿主机的文件发生改变,则容器内的文件也会同步改变,毕竟谁也无法确定自己写的文件以后都不会修改,对吧。
至于第 7 行的 network 网络,因为 Nginx 所处的网络是 web 网络,而想让 Nginx 访问 node 项目,则需要让两者处于同一个网络上。 network-alias 是网络别名,Nginx 会根据这个网络别名找到处于相同网络下的 node 项目,尽量与容器名保持一致。
此时邮箱项目已经部署成功了,但是 Nginx 还无法访问这个邮箱项目,因为我们没有配置 location 模块来访问邮箱项目。
server {
listen 5678;
server_name localhost;
location /email {
proxy_pass http://email:5678; # email 就是网桥别名,Nginx 通过它找到 email 容器
}
}实际上我并没有直接将上面的 server 模块放在配置文件里,而是将上面的内容放在新创建的 email_5678.conf 文件里,然后在配置文件进行转发。

因为核心配置文件使用了 include 指令将 conf 目录下的所有 .conf 文件引入,所以我们只需要创建新的配置文件 email_5678.conf 即可。
然后新的配置文件添加如下内容:
server {
listen 431; # 431 是 https 的默认端口,80 是 http 的默认端口
server_name www.youngkbt.cn;
# ...... 其他 location
# 转发给其他的 conf 文件
location /email {
proxy_pass http://localhost:5678;
}
# ...... 其他 location
}当访问 /email 的时候,触发 431 端口的的 /email,然后内部就会执行 proxy_pass 指令,将请求转发给本地的 5678 端口,此时 email_5678.conf 文件正好监听这个 5678 端口,所以就会将 /email 请求发给自己的 location ,执行新的 proxy_pass 指令,而这个指令才是将请求发给 node 邮箱项目,触发邮箱的发送。
记得重启 Ngixn,使得配置文件生效。
什么时候外界会访问 /email?
自己写一个 <a> 标签,填写你的服务器的地址加上 /email 即可。
我自己写的不是 <a> 标签,而是一个 js 文件,点击按钮触发 ajax 请求,具体源码请看 首页部署 的 JS 文件。
自定义镜像部署
我在使用了基于官方镜像的部署几天后,发现这个镜像太大了,有 999MB,如图:

于是我打算基于 Centos7.9 构建一个 node 环境的镜像,这里提供 Dockerfile 文件内容:
# 这是早期的版本,直接引入 node 镜像,但是该镜像太大了,构建后 999MB,所以我就自己创建一个 nodejs 镜像
# FROM node
FROM centos:7.9
# nodejs 版本
ARG NODE_VERSION="v16.13.1"
WORKDIR /opt/sendEmail
# 将当前目录的所有文件放入容器的 /opt/sendEmail
COPY . /opt/sendEmail
# 因为 COPY 也会将当前的 nodejs 压缩包添加进入,所以可以删除掉
RUN rm -f /opt/sendEmail/node-${NODE_VERSION}-linux-x64.tar.xz
# ------ 二选一,可注释 ------
# 如果事先下载的 node 压缩包,则放入 Dockerfile 所在的目录下,利用 ADD 传入并自动解压
ADD node-${NODE_VERSION}-linux-x64.tar.xz /usr/local/
# ADD 指令自动解压,所以可以删除传入的压缩包
RUN rm -f /usr/local/node-${NODE_VERSION}-linux-x64.tar.xz
# ------ ------ ------ ------ ------ ------ ------ ------ ------ ------
# ------ 二选一,可注释 ------
# 如果想要远程下载 node 压缩包,则取消下面的 RUN 指令注释
#RUN yum install -y wget tar \
# && cd /usr/local/ \
# && wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz \
# && tar xvf node-${NODE_VERSION}-linux-x64.tar.xz \
# && rm -f node-${NODE_VERSION}-linux-x64.tar.xz
# ------ ------ ------ ------ ------ ------ ------ ------ ------ ------
# 将解压的 node 目录重命名
RUN cd /usr/local/ \
&& mv node-${NODE_VERSION}-linux-x64 node
# 将 node 命令添加至全局变量,包括了 node 和 npm
ENV PATH=$PATH:/usr/local/node/bin
# 安装依赖包
RUN cd /opt/sendEmail \
&& npm install
# 暴露 7272 端口
EXPOSE 7272
# 启动容器后,自动执行下面的命令来启动项目
ENTRYPOINT node ./app.js我都有注释说明,这里大概总结下:
基于 Centos7.9 系统搭建 node 镜像,首先下载 nodejs,传到与 Dockerfile 同目录下,然后执行 Dockerfile 的时候,将 nodejs 传入镜像里,自动解压,并将 node 和 npm 命令添加至全局变量,然后把邮箱项目的代码传入镜像,接着利用全局变量 npm 进行安装 nodes_modules,最后写 ENTRYPOINT 指令,该指令会在启动容器的时候自动启动项目,这样就不需要我们亲自进入容器里启动项目。
如果不喜欢先下载 nodejs,传入服务器里,那么就在构建容器时使用 wget 下载即可,上面内容的注释就是 wget 相关操作,二选一。
执行如下命令执行 Dockerfile,构建镜像:
docker build -t email:2.0 .可以看到构建的新镜像大小已经缩小一半左右,并且功能没有任何缺失,如图:

2.0 版本的邮箱镜像构建源码我已经放到下载站点里了,如果需要,我在上方提供了下载地址,点击 序言 直达。
消息提示效果代码
在发送邮箱的过程,如果没有填写一些必要的信息,则会被提示,首页的提示是仿照 Element UI,只有两个函数,使用起来非常简单:
/**
* 添加消息提示
* content:内容
* type:弹窗类型(tip、success、warning、danger)
* startHeight:第一个弹窗的高度,默认 50
* dieTime:弹窗消失时间(毫秒),默认 3000 毫秒
*/
function addTip(content, type, startHeight = 50, dieTime = 3000) {
var tip = document.querySelectorAll(".tip");
var time = new Date().getTime();
// 获取最后消息提示元素的高度
var top = tip.length == 0 ? 0 : tip[tip.length - 1].getAttribute("data-top");
// 如果产生两个以上的消息提示,则出现在上一个提示的下面,即高度添加,否则默认 50
var lastTop =
parseInt(top) +
(tip.length != 0 ? tip[tip.length - 1].offsetHeight + 17 : startHeight);
let div = document.createElement("div");
div.className = `tip tip-${type} ${time}`;
div.style.top = parseInt(top) + "px";
div.setAttribute("data-top", lastTop);
if (type == "info" || type == 1) {
div.innerHTML = `<i class="iconfont icon-info icon"></i><p class="tip-info-content">${content}</p>`;
} else if (type == "success" || type == 2) {
div.innerHTML = `<i class="iconfont icon-dagouyouquan icon"></i><p class="tip-success-content">${content}</p>`;
} else if (type == "danger" || type == 3) {
div.innerHTML = `<i class="iconfont icon-cuowu icon"></i><p class="tip-danger-content">${content}</p>`;
} else if (type == "warning" || type == 4) {
div.innerHTML = `<i class="iconfont icon-gantanhao icon"></i><p class="tip-warning-content">${content}</p>`;
}
document.body.appendChild(div);
let timeTip = document.getElementsByClassName(time)[0];
setTimeout(() => {
timeTip.style.top = parseInt(lastTop) + "px";
timeTip.style.opacity = "1";
}, 10);
// 消息提示 dieTime 秒后隐藏并被删除
setTimeout(() => {
timeTip.style.top = "0px";
timeTip.style.opacity = "0";
// 下面的所有元素回到各自曾经的出发点
var allTipElement = nextAllTipElement(timeTip);
for (let i = 0; i < allTipElement.length; i++) {
var next = allTipElement[i];
var top =
parseInt(next.getAttribute("data-top")) - next.offsetHeight - 17;
next.setAttribute("data-top", top);
next.style.top = top + "px";
}
setTimeout(() => {
timeTip.remove();
}, 500);
}, dieTime);
}
/**
* 获取后面的兄弟元素
*/
function nextAllTipElement(elem) {
var r = [];
var n = elem;
for (; n; n = n.nextSibling) {
if (n.nodeType === 1 && n !== elem) {
r.push(n);
}
}
return r;
}CSS 样式:
/* 提示框元素 */
.tip {
position: fixed;
display: flex;
top: -10px;
left: 50%;
opacity: 0;
min-width: 320px;
transform: translateX(-50%);
transition: opacity 0.3s linear, top 0.4s, transform 0.4s;
z-index: 99999;
padding: 15px 15px 15px 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
grid-row: 1;
line-height: 17px;
}
.tip p {
line-height: 17px;
margin: 0;
font-size: 14px;
}
.icon {
margin-right: 10px;
line-height: 17px;
}
.tip-success {
color: #67c23a;
background-color: #f0f9eb;
border-color: #e1f3d8;
}
.tip-success .tip-success-content {
color: #67c23a;
}
.tip-danger {
color: #f56c6c;
background-color: #fef0f0;
border-color: #fde2e2;
}
.tip-danger .tip-danger-content {
color: #f56c6c;
}
.tip-info {
background-color: #edf2fc;
border-color: #ebeef5;
}
.tip-info .tip-info-content {
color: #909399;
}
.tip-warning {
color: #e6a23c;
background-color: #fdf6ec;
border-color: #faecd8;
}
.tip-warning .tip-warning-content {
margin: 0;
color: #e6a23c;
line-height: 21px;
font-size: 14px;
}图片是阿里云的图标库,在线地址:
<link rel="stylesheet" href="//at.alicdn.com/t/font_3114978_qe0b39no76.css" />使用只需要调用 addTip 即可:
function test() {
var hours = new Date().getHours();
var minutes = new Date().getMinutes();
var seconds = new Date().getSeconds();
hours = hours < 10 ? "0" + hours : hours;
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
if (hours >= 6 && hours < 11) {
addTip(
`早上好呀~~,现在是 ${hours}:${minutes}:${seconds},吃早餐了吗?😊🤭`,
"info",
50,
4000
);
} else if (hours >= 12 && hours <= 16) {
addTip(
`下午好呀~~,现在是 ${hours}:${minutes}:${seconds},繁忙的下午也要适当休息哦🥤🏀~~`,
"info",
50,
4000
);
} else if (hours >= 16 && hours <= 19) {
addTip(
`到黄昏了~~,现在是 ${hours}:${minutes}:${seconds},该准备吃饭啦🥗🍖~~`,
"info",
50,
4000
);
} else if (hours >= 19 && hours < 24) {
addTip(
`晚上好呀~~,现在是 ${hours}:${minutes}:${seconds},该准备洗漱睡觉啦🥱😪~~`,
"info",
50,
4000
);
} else if (hours >= 0 && hours < 6) {
addTip(
`别再熬夜了~~,现在是 ${hours}:${minutes}:${seconds},早点睡吧,让我们一起欣赏早上的太阳~~😇🛏`,
"info",
50,
4000
);
}
}