Vue.js實踐:一個Node.js+mongoDB+Vue.js的部落格內容管理系統
專案來源
以前曾用過WordPress搭建自己的部落格網站,但感覺WordPress很是臃腫。所以一直想自己寫一個部落格內容管理器。
正好近日看完了Vue各個外掛的文件,就用著Vue嘗試寫了這個簡約的部落格內容管理器(CMS)。
完成的功能
- 一個基本的部落格內容管理器功能,如後臺登陸,釋出並管理文章等 支援markdown語法編輯
- 支援程式碼高亮
- 可以管理部落格頁面的連結
- 部落格頁面對移動端適配優化
- 賬戶管理(修改密碼)
- 頁面足夠大氣、酷炫~
於是,為了彰顯一貫的酷炫,我給後臺寫了一套星空主題。。。
登陸頁面
後臺管理頁面
但部落格頁面最後沒用後臺的星空主題,主要是覺得黑色不太好搭配。
於是我想,既然前端都是用Vue.js寫的,那就參chao考xi一下Vue.js尤雨溪的部落格樣式吧!
在這基礎上部落格頁面又加了自己寫的canvas動畫,應該是足夠優雅了~
登陸後臺按鈕在頁面最下方“站長登陸”,可以以遊客身份登入後臺系統。
用到的技術和實現思路
前端:Vue全家桶
Vue.js
Vue-Cli
Vue-Resource
Vue-Validator
Vue-Router
Vuex
Vue-loader
後端
Node.js
mongoDB (mongoose)
Express
工具和語言
Webpack ES6 SASS Jade
整體思路
Node服務端除了主頁外,不做模板渲染,渲染交給瀏覽器完成
Node服務端不做任何路由切換的內容,這部分交給Vue-Router完成
Node服務端只用來接收請求,查詢資料庫並用來返回值
所以這樣做前後端幾乎完全解耦,只要約定好restful風格的資料介面,和資料存取格式就OK啦。
後端我用了mongoDB做資料庫,並在Express中通過mongoose操作mongoDB,省去了複雜的命令列,通過Javascript操作無疑方便了很多。
簡單的說一下Vue的各個外掛:
Vue-Cli:官方的腳手架,用來初始化專案
Vue-Resource:可以看作一個Ajax庫,通過在跟元件引入,可以方便的注入子元件。子元件以this. $http呼叫
Vue-Validator:用來驗證表單
Vue-Router:官方的路由工具,用來切換子元件,是用來做SPA應用的關鍵
Vuex:控制組件中資料的流動,使得資料流動更加清晰,有跡可循。通過官方的vue-devtools可以無縫對接
Vue-loader:webpack中對Vue檔案的載入器
檔案目錄
我將前端的檔案統一放到了src目錄下,其中的mian.js是webpack的入口。
所有頁面分割成一個單一的vue元件,放在componentss中,通過入口檔案mian.js,由webpack打包生成,生成的檔案放在public資料夾下。
後端檔案放在server資料夾內,這就是基於Express的node伺服器,在server資料夾內執行
node www
就可以啟動Node伺服器,預設偵聽3000埠。
關於Vue-Cli
我只是使用了simple的template,相對於預設的template,simple的配置簡單得多,但已經有了瀏覽器自動重新整理,生產環境程式碼壓縮等功能。
vue init simple CMS-of-Blog
以下是Vue-Cli生成的webpack的配置檔案,我只做了一點小改動。
話說React.js裡就沒用像Vue-Cli這樣好用的腳手架,還是Vue.js簡單優雅。。
Webpack.config.js
var path = require('path')
var webpack = require('webpack')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './public'),
publicPath: '/public/',
filename: 'build.js',
},
resolveLoader: {
root: path.join(__dirname, 'node_modules'),
},
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
},
{
test: /\.json$/,
loader: 'json'
},
{
test: /\.html$/,
loader: 'vue-html'
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url',
query: {
limit: 10000,
name: '[name].[ext]?[hash]'
}
}
, {
test: /\.(woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader?limit=50000&name=[path][name].[ext]'
}
]
},
babel: {
presets: ['es2015'],
},
devServer: {
historyApiFallback: true,
noInfo: true
},
devtool: '#eval-source-map'
}
if (process.env.NODE_ENV === 'production') {
module.exports.devtool = '#source-map'
// http://vue-loader.vuejs.org/en/workflow/production.html
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
output: {
comments: false,
},
compress: {
warnings: false
}
}),
new webpack.optimize.OccurenceOrderPlugin()
])
}
可以看出尤大大幫我們把瀏覽器自動重新整理,熱載入和生產環境的程式碼壓縮都寫好了,簡直超貼心。
然而實際專案中,我還是碰到一個麻煩的問題,下面是package.json中的script指令碼
"scripts": {
"dev": "webpack-dev-server --inline --hot",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
"watch": "webpack --progress --color --watch",
"server": "supervisor ./server/www"
},
執行
npm run dev
後,瀏覽器在8080埠開了一個伺服器,然而這個伺服器是用來服務前端頁面的,也就是說,從這裡啟動伺服器而不是開啟Node伺服器會造成資料無法互動,畢竟這個伺服器不能連線資料庫。但這個埠是可以在檔案修改之後自動重新整理瀏覽器的。
然後,通過分別執行
npm run watch
npm run server
來偵聽檔案改動,並重啟Node伺服器,此時瀏覽器是不能自動重新整理的。找了一些方法但終歸沒解決。習慣了自動瀏覽器重新整理,碰到這種情況蠻蛋疼的。
於是只好這樣:
在修改樣式的時候使用8080埠的伺服器,在修改資料互動的時候手動重新整理在3000埠伺服器的瀏覽器。
雖然不是很方便,但至少通過supervisor不用自己重啟Node伺服器了。。。
關於Vue-Router
因為寫的是但也應用(SPA),伺服器不負責路由,所以路由方面交給Vue-Router來控制。
下面是根元件,路由控制就在這裡,元件掛載在body元素下:
main.js
let router = new VueRouter()
router.map({
'/': {
component: Archive
},
'login': {
component: Login
},
'/article': {
component: Article
},
'/console': {
component: Console,
subRoutes: {
'/': {
component: ArticleList
},
'/editor': {
component: Editor
},
'/articleList': {
component: ArticleList
},
'/menu': {
component: Links
},
'account': {
component: Account
},
},
},
})
let App = Vue.extend({
data(){
return {}
},
components: {Waiting,Pop,NightSky,MyCanvas},
http: {
root: '/'
},
computed: {
waiting: ()=>store.state.waiting,
pop:()=>store.state.popPara.pop,
bg:()=>store.state.bg,
},
store
})
router.start(App, 'body')
對應的文件首頁 index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>Blog-CMS</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<waiting v-if="waiting"></waiting>
<pop v-show="pop"></pop>
<component :is="bg"></component>
<router-view></router-view>
<script src="public/build.js"></script>
</body>
</html>
可以看到路由控制在body元素下的router-view中。之前的waiting,pop和component元素分別是等待效果(就是轉圈圈)的彈出層,資訊的彈出層,和背景樣式的切換。
其實這個index.html是有express通過jade生成的,實際專案中並沒有html檔案,我是把生成好的html放在這裡方便展示。
關於Vue-loader
Vue-loader是Vue官方支援webpack的工具,用來將元件寫在一個檔案裡。之前的目錄中,有很多分割好的vue檔案,每一個檔案是一個獨立的元件。
比如,這是一個彈出層的元件:
Pop.vue
<template>
<div class="shade">
<div class="content">
<p>{{getPopPara.content}}</p>
<div class="button">
<button class="ok" @click="ok">確定</button>
<button class="cancel" @click="cancel"
v-if="getPopPara.cb2">取消
</button>
</div>
</div>
</div>
</template>
<script>
import {getPopPara} from '../vuex/getters'
export default{
vuex: {
getters: {
getPopPara,
}
},
methods: {
ok(){
let fn = this.getPopPara.cb1
typeof fn == 'function' && fn()
},
cancel(){
let fn = this.getPopPara.cb2
typeof fn == 'function' && fn()
}
}
}
</script>
<style lang="sass">
@import "../SCSS/Pop.scss";
</style>
每一個vue檔案都有三個部分(其實是可選的),分別是template,script和style,這也很好理解,就是把html,JS和CSS合併在一起寫了嘛。
這個彈窗元件,通過vuex獲得其他元件傳遞過來的引數,引數是一個物件,包括彈出層的展示資訊和點選確定或取消時的回撥函式。
因為編輯器不支援在vue檔案中用sass語法,所以我把sass檔案放在外部,通過@import引入。
關於Vue-Resource
Vue-Resource可以看成一個與Vue整合的Ajax庫,用來建立xhr和獲取xhr的response。
因為和Vue高度整合,所以在vue元件中使用很方便。
Article.vue
<template>
<div class="wrap">
<my-header></my-header>
<section class="article">
<article class="post-block">
<div class="post-title">{{title}}</div>
<div class="post-info">{{date}}</div>
<div class="post-content"
v-html="content | marked">
</div>
</article>
</section>
<my-footer></my-footer>
</div>
</template>
<script>
import myHeader from './MyHeader.vue'
import myFooter from './MyFooter.vue'
import marked from '../js/marked.min.js'
import {bgToggle} from '../vuex/actions'
export default{
data(){
return {
title: '',
date: '',
content: ''
}
},
filters: {
marked
},
created(){
let id = this.$route.query.id
this.$http.get('/article?id=' + id)
.then((response)=> {
let body = JSON.parse(response.body)
this.content = body.content
this.title = body.title
let d = new Date(body.date)
this.date = d.getFullYear() + '年' +
(d.getMonth() + 1) + '月' +
d.getDate() + '日'
}, (response)=> {
console.log(response)
})
},
components: {
myHeader,
myFooter
},
ready(){
this.bgToggle('MyCanvas')
},
vuex:{
actions:{
bgToggle
}
}
}
</script>
<style lang="sass">
@import "../SCSS/Article.scss";
</style>
Article.vue元件種在created生命週期時建立併發送了一個xhr的get請求,在獲取成果後把response物件中的屬性在賦值給data中相應的屬性,vue會自動更新檢視。
部落格所支援的markdown語法的關鍵所在也在這個元件裡。
<div class="post-content">
{{{content | marked}}}
</div>
通過引入markdown的filter使得輸出的html直接被轉換成html結構,還是很方便的。
關於後端
後端是用node.js作為伺服器的,使用了最流行Express框架。
主體是由Express生成,本身十分精簡。在實踐中修改的地方主要是添加了各種前端傳送的get和post請求。
router.get('/article', function (req, res, next) {
var id = req.query.id
db.Article.findOne({_id: id}, function (err, doc) {
if (err) {
return console.log(err)
} else if (doc) {
res.send(doc)
}
})
})
比如這個請求處理來自前端的get請求,通過mongoose來查詢資料庫並返回資料。
前端頁面通過promise控制非同步操作,把得到的資料放入元件的data物件中,Vue偵測變化並更新檢視。
資料庫的初始化檔案放在了init.js中,第一次執行的時候會新建名為admin的使用者,初始密碼為111,可以在控制檯的賬號管理中修改。
後記
其實還有很多很多沒有在這篇文章提及的地方。畢竟這個部落格框架相對是比較大的東西。
寫過這個部落格管理器後,感受還是蠻多的,對Vue.js中的資料繫結,元件化和資料流瞭解的更深入了一層,同時也對Node.js的後端有了一次優雅的實踐。
所以,學過東西之後,實踐是非常有必要的。前端很多時候就是不斷踩坑的過程。一路踩坑再爬坑,獲益匪淺。。。