前言
最近终于把个人网站正式上线了,先放成果!
www.ssevenk.com
这是我的个人博客,主要记录自己的前端学习心得
项目前后端都是一个人自己运作起来的
脚手架:Vuecli
服务端:node的express
数据库:mongodb
第三方插件:element-ui,simpleMDE等等
本文会从构思,开发,上线,备案全流程记录一下网站的搭建过程
超长文预警!!!
基本架构
整个项目其实是一个单页面应用,所有的页面跳转都是前端控制的 后端只是提供数据接口,来对数据库进行增删查改
- 文章:我的原创文章
- 杂谈:与技术不太相关的个人杂谈
- 收藏:别人的优秀文章,做成收藏夹的形式,点击直接跳转至对应网站链接
- 留言板:供浏览者留言
因此,这里也就有四种数据表需要设计
后端
Mongodb-数据定义与存放
我用的数据库是mongodb
比较灵活,而且与node配合使用起来更为简洁,可以直接用js操作
如上文所说,我们需要四种数据表结构,于是可以直接用js建立
新建一个curd.js文件
引入mongoose(第三方的API库)并连接数据库(第一次连接并没有这个数据库,会帮我们自动创建)
//curd.js
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/gblog')
在其中定义数据结构,比如定义一个文章的表结构
const MonBlog = mongoose.model('monblog', {
title: {
type: String,
required: true //表示这个属性是必需的
},
content: {
type: String,
required: true
},
date: {
type: String,
required: true
},
zan: {
type: Number,
required: true
},
comments:[]
})
其他三种表同理进行设计,然后将这四种数据模型导出给router.js,让其进行增删查改的数据接口设计
//curd.js
module.exports = {
MonBlog: MonBlog,
MonEssay: MonEssay,
MonArticle: MonArticle
MonMessage: MonMessage
}
增删查改
新建一个router.js引入curd.js导出的三种数据模型
const express = require('express')
const curd = require('./curd')
var router = express.Router()
const MonBlog = curd.MonBlog
const MonEssay = curd.MonEssay
const MonArticle = curd.MonArticle
const MonMessage = curd.MonMessage
然后就可以运用mongoose提供的API来进行增删查改
比如我们通过前端post的信息,来新增数据,可以设计这么一个接口
router.post('/data/createBlog', (req, res) => {
new MonBlog(req.body).save((err) => {
if (err) res.send(err)
})
})
查询接口
router.get('/data/blog', function (req, res) {
MonBlog.find((err, data) => {
if (err) {
res.send(err)
return
}
res.send(data)
})
})
删除接口
router.post('/data/deleteBlog',(req, res) => {
MonEssay.findByIdAndDelete(req.params.id, function (err, data) {
if (err) res.send(err)
})
}))
增删查改的守护——token
很显然,类似增删改的接口,是不能让游客或者恶意攻击者去调用的
为了让后端能认识我,知道是管理员在调用
我们就要用到token
在前端设计一个登录页,我通过密码登陆后,把后端发给我的token存进localStorage中,在调用一些高风险级别的接口时带上这个token
这里我们先安装两个插件,express-jwt和jsonwebtoken
新建jwtAuth.js文件
const expressJwt = require("express-jwt");
const secretOrPrivateKey = "woshisiyao"
const jwtAuth = expressJwt({
secret: secretOrPrivateKey
}).unless({
path: ['/data/blog',
'/data/essay',
'/data/article',
'/data/message',
/^\/data\/blog\/.*/,
/^\/data\/essay\/.*/,
/^\/data\/article\/.*/,
'/data/password'
] });
module.exports = jwtAuth;
上面的secretOrPrivateKey自定义了一个私钥,这是安全性的保障
用它来给我们的token加上尾部签名
最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。
所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,就会拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
在项目中,并不是所有的接口都是需要被保护的,比如获取数据的接口,以及特定文章内容的接口都是需要向游客开放的
上面的unless就是用来告诉后端,哪些接口不用token验证
其中,/data/blog/:id,这种形式的url是不行的,需要用正则表达式来写 /^\/data\/blog\/.*/
然后在router.js中,引入jwtAuth文件和jsonwebtoken插件
设计密码登陆的接口
router.post('/data/password', (req, res) => {
if (req.body.password == '123456') {
res.json({
result: 'succeed',
token: jwt.sign({
name: "ssevenk"
}, secretOrPrivateKey, {
expiresIn: 60 * 60 * 24
})
})
}
else {
res.send('验证失败')
}
})
如果密码正确,就向前端发送token,其中expiresln可以设置token的时效
前端收到token,存进localStorage中,比如
localstorage.setItem('token',token)
然后在发送请求的时候,需要写在headers中
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
}
如果没有成功,后端会报401 Unauthorized的错误
相关知识可以参考这几篇文章
juejin.cn/post/684490…
blog.leapoahead.com/2015/09/06/…
www.ruanyifeng.com/blog/2018/0…
后端结构
最终的后端结构,就分为四个文件
//app.js
const express=require('express')
const bodyParser=require('body-parser')
const router=require('./router')
const app=express()
app.use(bodyParser.urlencoded({extended:false}))
app.use(bodyParser.json())
app.use('/',router)
//划分一个端口给后端,这里监听7000端口
app.listen(7000)
在命令行敲上node app.js,就成功启动了我们的后端!
开发过程的跨域
可以注意到,虽然开发过程,前后端都在我自己的电脑上
但后端监听的是7000端口,而前台页面在8080端口访问
所以为了实现跨域请求
我们需要对config文件夹中的index.js文件进行一些修改
proxyTable: {
'/data': {
target: 'http://localhost:7000',
changeORIGIN: true
}
},
给proxyTable添加一种跨域访问规则,书写api的时候也都以/data开头
这样所有的请求都可以跨域访问到在7000端口的后端了
至此我们便完成了项目中的这一块部分
前端
后台管理系统
后端已经配置好了,但现在数据库里没有数据啊
于是需要有一个后台管理系统,来可视化管理数据
参考官网做法,很快就能撸一个出来
element.eleme.cn/#/zh-CN/com…
唯一需要额外提两句的,一个是右上角那个搜索
输入后可以即时显示搜索的结果在数据表格里
直接在el-table表格绑定的数据上进行操作
:data="((things.filter(data=>!search||data.title.toLowerCase().includes(search.toLowerCase())“
另外一个是分页,el-pagination
<el-pagination
@current-change="handleCurrentChange"
:page-size="pageSize"
:current-page="currentPage"
:total=" things.length"
layout="total, prev, pager, next"
></el-pagination>
当点击页码切换的时候,把页码更新
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
}
然后再一次对我们之前的el-table标签上的数据进行改进
:data="((things.filter(data=>!search||data.title.toLowerCase().includes(search.toLowerCase())).slice((currentPage-1)*pageSize,currentPage*pageSize)))"
这里的逻辑先后要理清,是先对数据进行搜索的过滤再分页
路由守卫
这里你们应该也注意到了,管理的入口是出现在页面上的tab选项卡里的
所以其实后台管理也是单页面应用的一部分,只是这个入口没有对你们开放
为了避免游客在地址栏直接输入路由闯入管理系统,我们要给需要保护的路由添加守卫
{
path: '/Back',
name: 'Back',
component: () => import('../components/back.vue'),
beforeEnter: (to, from, next) => {
next({ path: '/Login' })
}}
使用beforeEnter,把需要守卫的路由引入到登陆页去
在登陆页,输入管理员密码提交给后台
前台把后台返回的token存进localStorage中,比如就叫做‘token’
那么如果在路由的时候,从localStorage中能取到这个符号,那么守卫就允许进行下一步跳转
所以上面的守卫改成
{
path: '/Back',
name: 'Back',
component: () => import('../components/back.vue'),
beforeEnter: (to, from, next) => {
if(localStorage.getItem('token') { next() }
else {
next({ path: '/Login' })}
}}
这种方式是需要给每个要守卫的路由都添加一遍beforeEnter
还有一种方法是进行全局守卫
router.beforeEnter: (to, from, next) => {
if(to.meta&&!localStorage.getItem('token') { next({ path: '/Login' })} }
else {
next()
}}
添加全局守卫后,给要守卫的路由添加一个meta属性,值为true就可以了
markdown编辑器
管理系统最重要的功能就是写文章和修改文章内容
并添加了本来没有的本地上传图片功能
可以参见我的另一篇文章
juejin.cn/post/684490…
前台路由
来到了我们的前端展示页
<myHeader></myHeader>
<router-view class="main"></router-view>
所谓的路由跳转,其实只是在更新main这块内容
可以看一下前端的路由文件router.js
export default new Router({
routes: [
// 前台页面路由
{
path: '/',
redirect: '/Blogs'
},
{
path: '/Blogs',
name: 'Blogs',
component: () => import('../components/ShowBlogs.vue')
},
{
path: '/Essays',
name: 'Essays',
component: () => import('../components/ShowEssays.vue')
},
{
path: '/Articles',
name: 'Articles',
component: () => import('../components/ShowArticles.vue')
},
{
path: '/Message',
name: 'Message',
component: () => import('../components/ShowMessages.vue')
}]
四大功能的路由,其中首页做了重定向,直接默认跳转到文章页面 而跳转的实现都在抬头的tab选项卡中
<router-link to="/Blogs">文章</router-link>
<router-link to="/Essays">杂谈</router-link>
<router-link to="/Articles">收藏</router-link>
<router-link to="/Message">留言板</router-link>
<router-link v-if="admin" to="/Back">管理</router-link>
其实每一个按钮都是个router-link,其中如果能在localStorage中取到token,就把管理入口也暴露
选中的颜色样式都是通过.router-link-active:nth-of-type(x)来设计的,类似于这样
#myHeader-link .router-link-active:nth-of-type(1) {
border-bottom: solid rgb(255, 184, 126) 3px;}
列表页数据过滤
在列表页,我们只需要知道id,标题和时间,并不需要获取文章的具体内容(content)
为了避免不必要的数据传输浪费时间,我们在后端要对数据进行一次过滤
//router.js
router.get('/data/blog', function (req, res) {
MonBlog.find((err, data) => {
if (err) {
res.send(err)
return
}
var simpleData = data.map(item => {
return {
title: item.title,
date: item.date,
_id: item._id
}
})
res.send(simpleData.reverse())
})
})
使用map映射,只把需要的东西传过去
搜索功能
搜索功能有点和后台管理不一样
这一次我定义了一个show数组
点击搜索之后,调用函数来进行搜索,把搜索出来的结果存放在show中
所以我们展示的一直都是show数组
由于有三个功能用到了搜索框
所以我把搜索框单独做成了一个组件
并没有注册为全局组件
因为我们希望它是作为ShowBlogs、ShowEssays、ShowArticles这三个组件的子组件存在的,方便调用父组件提供的方法
import mySearch from "./mySearch";
每个父组件都引入一次
点击搜索时,向父组件发射搜索框里的内容,并调用父组件的方法
//mySearch.vue
methods:{
search() {
this.$emit('search',this.content)
}
}
在父组件中
<mySearch @search="searchfor"></mySearch>
methods:{
searchfor(s) {
this.show = (this.blogs.filter(item => {
if (item.title.includes(s)) {
return item;
}
}));
}}
针对每个组件,搜索框的颜色不一样
是通过$route.path来判断,动态绑定样式
computed: {
mySearch: function() {
return {
mySearch1: this.$route.path == "/Blogs",
mySearch2: this.$route.path == "/Essays",
mySearch3: this.$route.path == "/Articles"
};
}
}
具体的文章内容
点进具体的文章或杂谈时,把id传进路由,页面拿着id去请求后端的数据
然后调用simpleMDE的原型方法将拿到的字符串转换为html格式
this.contentMarkdown=SimpleMDE.prototype.markdown(this.theOne.content)
用v-html展示出来
<div id='markdownArticle' v-html="contentMarkdown"></div>
点赞&评论功能
vue-clap-button,一个第三方小组件
github.com/AJLoveChina…
<vue-clap-button v-if="flag" size="60" :clicked="isClicked" />
原组件缺少一个绑定的属性,来传递是否已经点过赞这个状态
所以改了一下源码,增加了一个clicked的属性,写在props里面
props: {
clicked: {
type: Boolean,
default: false
}
},
由于我的网页是没有游客登陆功能的,所以要判断该游客是否已经对这篇文章点过赞了,我用的是localStorage
//判断是否已经点过赞
ifClicked() {
var zanList = JSON.parse(localStorage.getItem("zanList"));
if (!zanList) return;
if (
zanList.some(item => {
return item == this.theOne._id;
})
)
this.isClicked = true;
}
当点赞的时候,给用户localStorage存一个数组,保存已经点过赞的文章的id
就可以在页面初始化的时候,通过判断文章id是否在这个数组中,来告诉点赞按钮呈现红色还是灰色状态
评论功能
从功能上来说其实没什么好讲的,不过由于comments不是一个数据,只是文章数据身上的一个属性
所以数据库并不会给每条评论自动分配一个id
而为了便于删除之类的操作,我们需要在游客提交评论的时候,给它加上一个随机的id
id:Math.random().toString(36).substr(2)
网站的整体风格就是简洁,从评论的头像来说,我也只是在用户提交的时候,随机给了一个整数
headIndex: Math.floor(Math.random() * 6)
用来分配一个匿名头像
<img class="cmt-div-img" :src="require('../assets/img/head/'+item.headIndex+'.jpeg')" alt />
注意因为是动态引入,图片需要用require的方法引入
收藏&留言功能
收藏其实就是一个个超链接,引向外部的网站
留言功能类似评论,这里不再赘述
响应式设计
网站只做了非常简单的响应式设计,基本就是把列表页的横向布局设置成了竖向布局 而有一点需要注意,是原来如果竖着排会在最下面的搜索按钮和公告跑到了最上面
@media screen and(max-width:500px) {
.mySlider {
order: -1;
}
至此,本网站的开发过程到一段落,下面就要把它上线了!
上线
购买服务器及域名
我买的是阿里云的服务器,学生党100多一年还挺划算的
要记住购买时设置的用户名和密码
然后买了ssevenk.com这个域名(ssevenk是我常用的用户名),本来想买真名的gaoyufeng.com,但已经被人买走了
然后我们就要在服务器上配置我们的环境了,相当于把本地的那一套搬到服务器上去
安装node
首先安装一个可以远程操纵服务器的软件 我用的是puTTy
安装node的方法看了很多网上的教程,我只能说不靠谱!
各种复杂,一顿操作,最后还报错
后来偶然发现阿里云的官方手册上就有安装node的教程 按照阿里云的步骤,简单又有效
安装mongodb
整个上线过程中最令人蛋疼的一步! 踩坑无数,整整装了一天才装好
直接上图
noauth改为auth=true然后设置文件夹权限
$ cd /usr/mongodb
$ chmd 777 db
$ chmod 777 log
启动mongodb
$ cd ~
$ mongod -f /usr/mongodb/mongodb.conf
由于mongodb默认使用的是27017端口,所以我们登陆阿里云,开放这个端口
下面我们要给数据库增加权限 在mongodb已启动的情况下,命令行输入
use admin
db.createUser(
{
user:'root',
pwd:'root',
roles:[ { role: 'userAdminAnyDatebase',db:'admin' } ]
}
创建超级用户 不过mongodb有一点比较特殊,超级用户并不是在数据库和子数据库都是畅通无阻的
事实上,mongdb的用户权限和数据库是绑定的,也就是创建一个新的数据库,要想在这个新的数据库插入数据,是需要创建一个与之对应的用户的
说的很绕,操作逻辑也很诡异(所以在这一步卡了很久) 在创建超级用户后,正确的操作步骤是:
- use admin 进入admin数据库
- db.auth('root','root') 超级用户认证
- db.createUser({user:'gyf',pwd:'123456',roles:[{role:'readWrite',db:'gblog'}]}) 创建gyf用户,并为它指定新的数据库gblog
- db.auth('gyf','123456')切换成gyf用户
- use gblog 切换至gblog数据库
- db.repo.insert({'name':'hhh'}) 这样就可以正确插入一条数据了
单单只有一个超级用户是不能操作其他新建的数据库的
使用mongodb compass(就是安装时被捆绑下载的那个),可以可视化的看到这个过程
目标数据库的名字都是admin,但是root用户能看到所有的数据库,gyf用户只能看到gblog数据库
在原来后端中连接数据库的地方,改成
mongoose.connect('mongodb://gyf:123456@0.0.0.0:27017/gblog?authSource=admin')
前面输入用户名和密码,0.0.0.0的地方输入ip地址,最后输入要操作的数据库和权限来源(都是admin给的权限)
部署服务器
环境已经配置好了,下面就要把我们的后端代码部署上去
上传代码我一开始用的是Xftp 6 这个软件,后来发现vscode里面有个sftp的插件
可以直接右键上传,更快捷
安装完插件后,ctrl+shift+p,然后输入sftp:congfig,进行配置,记得要把自动上传关闭
{
"name": "My Server",
"host": "IP地址",
"protocol": "sftp",
"port": 22,
"username": "用户名",
"password": "密码",
"remotePath": "路径",
"uploadOnSave": false
}
然后直接在左侧文件目录,右击sync Local -> Remote把后端代码上传
在本机上我们用的是node\ app.js来启动后端的,不过在服务器上,为了保证它能一直运行,同时提高cpu利用率,我们要使用进程守护工具pm2来启动
先安装pm2
npm install pm2@latest -g
cd /home/blog 切换到项目目录
ln -s /root/node-v10.2.0-linux-x64/bin/pm2 /user/local/bin/ 中间的路径是node的安装位置
然后启动我们的后端
pm2 start app.js --name 'app'
我们的服务器就跑起来了!
上一句话后面如果再跟一个--watch\ ,就会在文件或者文件夹变更时自动重启,我这里因为有上传图片的功能,会改变文件夹,导致图片传到一半服务器就重启了,因此没有使用这个功能
最后把我们的node服务加到进程,保证NodeJs一直在后台运行,就算重启也自动运行
pm2 startup centos #pm2 stratup ubuntu
pm2 save
网页部署
在vuecli中,npm build一下,就可以把我们的工程打包成html文件
不过在这之前,要把build配置中的assetsPublicPath改成“ ./ ” 本来是 ”/“,以避免图片缺失
打包结束后,把整个dist文件夹丢到服务器上 然后如何实现访问它呢,我们就需要在后端中做一些修改 在后端路由中,监听到首页的”/“就把html文件发给客户端
router.get('/', (req, res) => {
res.setHeader("Content-Type", "text/html;charset='utf-8'");
//读文件
fs.readFile("./dist/index.html", "utf-8", function (err, data) {
if (err) {
console.log("index.html loading is failed :" + err);
}
else {
//返回index.html页面
console.log(data)
res.end(data);
}
})
})
这里用到了node的fs模块,可以读取文件
还记得我们后端监听的是7000端口吗,所以此时,如果你的ip地址为1.2.3.4
那么在地址栏输入1.2.3.4:7000,就能访问到这个网页了(记得要去安全组开放7000端口)
DNS解析
不过没有哪个网站是直接让别人去访问ip地址的
这时就要把我们买的域名,通过DNS解析绑定到我们的ip地址上来了
这一步在阿里云的官网就可操作,比较简单
不过,我们只能绑定到ip地址,而默认的网页端口其实是80端口
所以在后端,我们把原来监听的7000端口,改成80端口(同样记得安全组开放80端口)
//app.js
app.listen(80)
就可以通过www.ssevenk.com访问到了!!
首屏空白优化
服务器运行不比本地,网络传输速度突然就重要了起来
一开始,网页刚部署上去,打开网址到完整看见内容,整整花了10s!!
那一刻才知道什么叫天荒地老
后来进行了一下打包的优化,可以看我的这篇文章
juejin.cn/post/684490…
不过还是存在首屏一定时间的空白
这时候,就有一个东西很重要:加载动画!
比起3s的纯空白,游客反而更能接受4s带着加载动画的空白
你得让游客知道,我的网站可以打开的,你只要等一会就行
<div id="Loading">
<div class="loader-inner ball-beat">
<div></div>
<div></div>
<div></div>
</div>
</div>
<style type="text/css">
#Loading {
top: 50%;
left: 50%;
position: absolute;
-webkit-transform: translateY(-50%) translateX(-50%);
transform: translateY(-50%) translateX(-50%);
z-index: 100;
}
@-webkit-keyframes ball-beat {
50% {
opacity: 0.2;
-webkit-transform: scale(0.75);
transform: scale(0.75);
}
100% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes ball-beat {
50% {
opacity: 0.2;
-webkit-transform: scale(0.75);
transform: scale(0.75);
}
100% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1);
}
}
.ball-beat>div {
background-color: rgb(255, 184, 126);
width: 20px;
height: 20px;
border-radius: 100% !important;
margin: 3px;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
display: inline-block;
-webkit-animation: ball-beat 0.7s 0s infinite linear;
animation: ball-beat 0.7s 0s infinite linear;
}
.ball-beat>div:nth-child(2n-1) {
-webkit-animation-delay: 0.35s !important;
animation-delay: 0.35s !important;
}
</style>
然后在最先出来的文章列表组件中,在mounted生命周期移除这个加载
mounted() {
document.body.removeChild(document.getElementById("Loading"));
},
备案
由于我买的是国内的主机,所以还要去备案
而这,又是一部血泪史
ICP备案
首先进行的是ICP备案,直接在阿里云上申请
强烈建议即便网站还没做好,也先去把ICP备了
不然等你急着上线的时候,就知道什么叫度日如年了
跟着阿里云的流程走,只要符合规定就能通过
这里有一点很坑
因为我是在上海上学,家乡江苏,但是我没有上海居住证就无法在上海备案
填江苏省备案吧,我的江苏手机卡又早就不用了,不能用上海手机号备案
两难之下,只好又去买了张江苏的电话卡
总之真的是非常繁琐,还有幕布拍照什么的,前前后后花了我20天
不过,没想到麻烦还没结束
公安备案
现在好像有新规定,ICP备案好了,还要在30天内进行公安备案
看网上回馈,好像这个会比ICP快很多,而且不繁琐
但没想到我又掉坑了
发给别的朋友看,也都可以看到网站内容啊
IE兼容
苦思冥想,终于想到了一种可能
他们还在使用IE!
打开ie11,输入ssevenk.com,果然,一片空白
于是,开始与这个老古董搏斗
vuecli其实有与ie浏览器兼容的办法,参考官网,利用polyfill
cli.vuejs.org/zh/guide/br…
但是没想到官网的例子怎么也不能生效
我打开ie11永远是一片空白
后来无奈,从vuecli3又用回了vuecli2 在webpack.base.conf中设置
entry: {
app: ['babel-polyfill', './src/main.js']
},
在index.html中设置
<meta http-equiv="X-UA-Compatible" content="IE=edge">
终于成功了!!!!!!!
纪念一下
总结
网站的构思是在4月份,其实5月初网站雏形就做好了
后来增增改改,基本上6月份就基本完成了服务器部署
但是直到7月中旬,才正式合法地上线了
整个就是一血泪史,不过不得不说,只有实际操作了一下,才能更理解一个网站的创作流程
对于各种技能的使用,服务端客户端的理解也会上一个台阶
其实中间有不少小细节都没讲,挑了主要的部分,不过也已经是长篇大论了\
开发不易,不过最棒的还是那份正式上线的成就感!!
www.ssevenk.com
码字也不易,希望我的经历对你们有所帮助