最近終於把我的網站正式上線了,先放成果!
www.ssevenk.comcss
這是個人我的博客,主要記錄本身的前端學習心得
項目先後端都是一我的本身運做起來的
腳手架:Vuecli
服務端:node的express
數據庫:mongodb
第三方插件:element-ui,simpleMDE等等html
本文會從構思,開發,上線,備案全流程記錄一下網站的搭建過程
超長文預警!!!前端
整個項目實際上是一個單頁面應用,全部的頁面跳轉都是前端控制的 後端只是提供數據接口,來對數據庫進行增刪查改vue
所以,這裏也就有四種數據表須要設計node
我用的數據庫是mongodb
比較靈活,並且與node配合使用起來更爲簡潔,能夠直接用js操做
如上文所說,咱們須要四種數據表結構,因而能夠直接用js創建linux
新建一個curd.js文件
引入mongoose(第三方的API庫)並鏈接數據庫(第一次鏈接並無這個數據庫,會幫咱們自動建立)webpack
//curd.js
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/gblog')
複製代碼
在其中定義數據結構,好比定義一個文章的表結構git
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,讓其進行增刪查改的數據接口設計github
//curd.js
module.exports = {
MonBlog: MonBlog,
MonEssay: MonEssay,
MonArticle: MonArticle
MonMessage: MonMessage
}
複製代碼
新建一個router.js引入curd.js導出的三種數據模型web
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存進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.im/post/5ac6fc…
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端口的後端了
至此咱們便完成了項目中的這一塊部分
後端已經配置好了,但如今數據庫裏沒有數據啊
因而須要有一個後臺管理系統,來可視化管理數據
惟一須要額外提兩句的,一個是右上角那個搜索
輸入後能夠即時顯示搜索的結果在數據表格裏
直接在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就能夠了
管理系統最重要的功能就是寫文章和修改文章內容
來到了咱們的前端展現頁
<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,但已經被人買走了
而後咱們就要在服務器上配置咱們的環境了,至關於把本地的那一套搬到服務器上去
首先安裝一個能夠遠程操縱服務器的軟件 我用的是puTTy
安裝node的方法看了不少網上的教程,我只能說不靠譜!
各類複雜,一頓操做,最後還報錯
後來偶然發現阿里雲的官方手冊上就有安裝node的教程 按照阿里雲的步驟,簡單又有效
整個上線過程當中最使人蛋疼的一步! 踩坑無數,整整裝了一天才裝好
直接上圖
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的用戶權限和數據庫是綁定的,也就是建立一個新的數據庫,要想在這個新的數據庫插入數據,是須要建立一個與之對應的用戶的
說的很繞,操做邏輯也很詭異(因此在這一步卡了好久) 在建立超級用戶後,正確的操做步驟是:
單單隻有一個超級用戶是不能操做其餘新建的數據庫的
使用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端口)
不過沒有哪一個網站是直接讓別人去訪問ip地址的
這時就要把咱們買的域名,經過DNS解析綁定到咱們的ip地址上來了
這一步在阿里雲的官網就可操做,比較簡單
不過,咱們只能綁定到ip地址,而默認的網頁端口實際上是80端口
因此在後端,咱們把原來監聽的7000端口,改爲80端口(一樣記得安全組開放80端口)
//app.js
app.listen(80)
複製代碼
就能夠經過www.ssevenk.com訪問到了!!
服務器運行不比本地,網絡傳輸速度忽然就重要了起來
一開始,網頁剛部署上去,打開網址到完整看見內容,整整花了10s!!
那一刻才知道什麼叫天荒地老
後來進行了一下打包的優化,能夠看個人這篇文章
juejin.im/post/5d0730…
不過仍是存在首屏必定時間的空白
這時候,就有一個東西很重要:加載動畫!
比起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備了
否則等你急着上線的時候,就知道什麼叫度日如年了
跟着阿里雲的流程走,只要符合規定就能經過
這裏有一點很坑
由於我是在上海上學,家鄉江蘇,可是我沒有上海居住證就沒法在上海備案
填江蘇省備案吧,個人江蘇手機卡又早就不用了,不能用上海手機號備案
兩難之下,只好又去買了張江蘇的電話卡
總之真的是很是繁瑣,還有幕布拍照什麼的,前先後後花了我20天
不過,沒想到麻煩還沒結束
如今好像有新規定,ICP備案好了,還要在30天內進行公安備案
看網上回饋,好像這個會比ICP快不少,並且不繁瑣
但沒想到我又掉坑了
苦思冥想,終於想到了一種可能
他們還在使用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
碼字也不易,但願個人經歷對大家有所幫助