Github地址:https://github.com/bing-zhub/fullStackVueExpress
OverView
在很久之前, 使用Vue做前端, Express做后端, 但是都沒有整理過. 先把內(nèi)容貼出來, 過段時間詳細(xì)整理.
這是一個基于vue和express的全棧項(xiàng)目,目標(biāo)是實(shí)現(xiàn)一個符合Material Design風(fēng)格的CMS系統(tǒng).
前端基于vue-cli提供的webpack模板進(jìn)行開發(fā), 使用vuetify作為前端組件庫, 配合一些的第三方組件化工具,完成前端的開發(fā).
后端使用express.采用Sequelize對數(shù)據(jù)庫進(jìn)行增刪改查操作.
前后端使用axios完成RESTfull交互
這里主要對項(xiàng)目進(jìn)行一個頂層的描述, 同時提供一個前后端分離進(jìn)行請求的案例.
Getting start
git clone https://github.com/bing-zhub/fullStackVueExpress.git
cd fullStackVueExpress
client
cd client
cnpm install
npm run start
server
cd server
cnpm install
npm run start
注意 你需要在config目錄下配置config.js以實(shí)現(xiàn)數(shù)據(jù)庫連接等, 如果不進(jìn)行配置 將無法運(yùn)行
module.exports = {
port:process.env.PORT || 8081,
db:{
database: process.env.DB_NAME || 'your dbname',
user: process.env.DB_USER || 'your db user',
password: process.env.DB_PWD || 'your db password',
options:{
dialect: process.env.DIALECT || 'your db version',
host: process.env.HOST || 'your db host',
storage: './example.mysql'
}
},
authentication:{
jwtSecret: process.env.JWT_SECRET || 'bing'
}
}
技術(shù)棧
前端 vue 使用vue-cli的webpack模板
axios api請求工具
基于promise的HTTP客戶端
vuex狀態(tài)管理
用戶的登錄狀態(tài), 頁面路由狀態(tài)等
vue-router 前端路由
對前端url進(jìn)行解析, 指向不同界面
vuetify 前端組件
一個符合MaterialDesign的前端組件庫 對vue支持甚好
font awesome
前端圖標(biāo)庫: 圖標(biāo)組件庫
quillEditor
富文本編輯器: 作用戶編輯用(todo)
editor.md,simplemde-markdown-editor
Markdown編輯器: 一個高生產(chǎn)力markdown工具(todo)
video player (to do)
后端 express
bcrypt-nodejs sha256加密工具
bluebird promisify工具
body-parser api請求解析工具
cors 跨域請求工具
joi 數(shù)據(jù)模型驗(yàn)證
jsonwebtoken 用戶身份信息認(rèn)證
morgan 日志中間件
sequelize 基于promise的ORM工具,用于數(shù)據(jù)庫交互
數(shù)據(jù)庫 mysql
目錄結(jié)構(gòu)
--client
--build build文件夾**vue-cli生成**
--config webpack等配置文件**vue-cli生成**
--node_modules 各種依賴庫
--src vue前端源碼
-- assets 放置一些靜態(tài)文件
-- highlight markdown語法高亮樣式
-- style 公用css樣式庫
-- components 組件庫使用大駝峰命名
-- Blank.vue 空白模板便于創(chuàng)建新組件
-- CreateSong.vue 用于創(chuàng)建新內(nèi)容
-- Dialog.vue 會話彈出框
-- FloatingButton.vue 浮動按鈕
-- Footer.vue 頁腳
-- Header.vue 頁眉
-- HelloWorld.vue vue-cli自動生成的首頁
-- Login.vue 登錄界面
-- Markdown.vue Markdown編輯器
-- Page404.vue 404頁面
-- Panel.vue 面板公用組件
-- QuillEditor.vue 富文本編輯器
-- Register.vue 注冊頁面
-- Songs.vue 主要內(nèi)容加載頁
-- ViewSong.vue 詳情頁
-- config
-- router 路由處理
-- index.js 前端路由
-- services 服務(wù)處理
-- Api.js 發(fā)送api請求
-- AuthenticationServices.js 處理登錄/注冊
-- SongServices.js 處理歌曲查看/添加/詳情
-- store 狀態(tài)倉庫
-- 放置state mutation actions
-- App.vue 主入口
-- main.js 主入口
--static 靜態(tài)資源
--test 測試文件
--server
-- node_modules 服務(wù)端第三方庫
-- src 服務(wù)端源碼
-- config
-- config.js 配置端口/數(shù)據(jù)庫服務(wù)器/JWT 密碼
-- controller api詳細(xì)處理
-- AuthenticationController.js 處理來自前端的登錄注冊請求
-- SongsController.js 處理來自前端的檢索添加請求
-- model 數(shù)據(jù)庫Schema
-- index.js 數(shù)據(jù)庫Schema索引
-- Song.js 歌曲Schema
-- User.js 用戶Schema
-- policy 輸入驗(yàn)證規(guī)則
-- AuthenticationControllerPolicy.js 驗(yàn)證注冊時用戶名與密碼是否規(guī)范
-- app.js 后端主入口
-- routes.js 后端路由,對api進(jìn)行路由
示例 -- 用戶注冊
前端 (client)
入口文件 index.html, 在div標(biāo)簽中id為app的位置會由vue渲染頁面
vue主文件在src/app.vue
<template>
<div id="app">
<v-app>
<!-- 注冊頁面入口在header中 -->
<page-header/>
<main>
<v-container fluid>
<router-view></router-view>
<v-flex offset-xs5>
<floating-button/>
</v-flex>
</v-container>
</main>
<page-footer/>
</v-app>
</div>
</template>
page-header page-footer與floating-button會在全局出現(xiàn)(任何頁面都包含著三個組件)
router-view會根據(jù)當(dāng)前url渲染不同的內(nèi)容
<page-header/>為自定義組件 為頁面頁面的header 通過
import PageHeader from '@/components/Header'
//引入header @是webpack的alias 配置為src目錄
import PageFooter from '@/components/Footer'
import FloatingButton from '@/components/FloatingButton'
export default {
name: 'App',
components: {
PageHeader, //在組件中注冊header, 不注冊無法直接使用自定義組件
PageFooter,
FloatingButton
}
}
進(jìn)行組件注冊,在header中放置一些導(dǎo)航信息
下面就進(jìn)入header.vue
<template>
<v-toolbar fixde dark class="cyan" color="primary">
<v-toolbar-title class="mr-4" light>
<!--工具欄-->
<span
@click="navigateTo({name: 'root'})" class="home">
<v-icon>home</v-icon>
<span md>Homepage</span>
</span>
</v-toolbar-title>
<v-toolbar-items>
<v-btn
flat
dark
@click="navigateTo({name: 'songs'})">
發(fā)現(xiàn)
</v-btn>
</v-toolbar-items>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn
flat
dark
v-if='!$store.state.isUserLoggedIn'
@click="navigateTo({name: 'register'})">
注冊 <v-icon>fas fa-user-plus</v-icon>
</v-btn>
<!-- @click會注冊一個監(jiān)聽器 但點(diǎn)擊時調(diào)用navigateTo方法(自定義方法) -->
<!-- 傳入router對象-->
<!-- v-if是一個條件渲染 當(dāng)v-if后的值為false時 不渲染(隱藏) -->
<!-- 是為了在用戶登陸后隱藏注冊和登錄按鈕 v-if是由vuex管理的全局狀態(tài) -->
<v-btn
flat
dark
v-if='!$store.state.isUserLoggedIn'
@click="navigateTo({name: 'login'})">
登錄 <v-icon>fas fa-sign-in-alt</v-icon>
</v-btn>
<v-btn
flat
dark
v-if='$store.state.isUserLoggedIn'
@click="logout">
退出登錄 <v-icon>fas fa-sign-out-alt</v-icon>
</v-btn>
</v-toolbar-items>
</v-toolbar>
</template>
Script
export default {
methods: {
//自定義方法 在單擊時 將route放入全局router 實(shí)現(xiàn)頁面跳轉(zhuǎn)
navigateTo (route) {
this.$router.push(route)
},
logout () {
this.$store.dispatch('setToken', null)
this.$store.dispatch('setUser', null)
this.$router.push({
name: 'root'
})
}
}
}
根據(jù)路由檢索 route/index.js 中的定義
import Register from '@/components/Register'
export default new Router({
routes: [
{
path: '/register',
name: 'register',
component: Register
}
]
})
'/register' 被指向 'src/components/Register'
<v-card-text>
<v-form autocomplete="off">
<v-text-field
prepend-icon="person"
palceholder="email"
name="login"
label="Email"
type="text"
:rules="[rules.required]"
v-model="email">
</v-text-field>
<v-text-field
prepend-icon="lock"
palceholder="password"
name="password"
label="Password"
id="password1"
type="password"
:rules="[rules.required]"
v-model="password1"
autocomplete="new-password">
</v-text-field>
<v-text-field
prepend-icon="lock"
palceholder="password"
name="password"
label="Confirm Password"
id="password2"
type="password"
:rules="[rules.required]"
v-model="password2"
autocomplete="new-password">
</v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" @click.stop="dialog = !dialog" @click="register">Sigup</v-btn>
</v-card-actions>
dialog組件
<v-dialog v-model="dialog" max-width="500px">
<v-card>
<v-card-title>
<span>Information</span>
<v-spacer></v-spacer>
<v-menu bottom left>
</v-menu>
</v-card-title>
<span>{{ message }}</span>
<v-card-actions>
<v-btn color="primary" flat @click.stop="dialog=false">Close</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
注冊ui組件主要有vuetify的v-form提供 三個v-form-field分別定義了三個字段郵箱/密碼/確認(rèn)密碼
在v-card-actions 定義了兩個監(jiān)聽 當(dāng)事件觸發(fā) 打開dialog被設(shè)置為true 顯示出來 并且觸發(fā)自定義的方法register :rules用來限制用戶輸入
import AuthenticationService from '@/services/AuthenticationService'
import Panel from '@/components/Panel'
export default {
data () {
return {
email: '',
password1: '',
password2: '',
error: null,
drawer: null,
dialog: false,
message: '注冊成功',
rules: {
required: (value) => !!value || '這是一個必填項(xiàng)'
}
}
},
methods: {
async register () {
try {
if (this.email === '' || this.password1 === '' || this.password2 === '') {
this.message = '信息未填寫完整'
} else if (this.password1 !== this.password2) {
this.message = '兩次密碼輸入不一致,請重試'
} else {
const response = await AuthenticationService.register({
email: this.email,
password: this.password
})
this.message = '登錄成功'
this.$store.dispatch('setToken', response.data.token)
this.$store.dispatch('setUser', response.data.user)
}
} catch (err) {
this.message = err.response.data.error
}
}
},
components: {
Panel
},
props: {
source: String
}
}
在register中引用AuthenticationService提供的register方法 并傳入一個對象 這是一個異步操作用到了async/await 在調(diào)用成功后 將message設(shè)置為'登陸成功'
之后對store進(jìn)行dispatch
只有通過dispatch才能觸發(fā)store中的action從而修改state
我們在main.js中進(jìn)行了如下的聲明
import store from '@/store/store'
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
從而將store注冊為一個全局組件
store.js
import vue from 'vue'
import vuex from 'vuex'
vue.use(vuex)
export default new vuex.Store({
strict: true,
state: {
token: null,
user: null,
isUserLoggedIn: false
},
mutations: {
setToken (state, token) {
state.token = token
if (token) {
state.isUserLoggedIn = true
} else {
state.isUserLoggedIn = false
}
},
setUser (state, user) {
state.user = user
}
},
actions: {
setToken ({ commit }, token) {
commit('setToken', token)
},
setUser ({ commit }, user) {
commit('setUser', user)
}
}
})
這里設(shè)置了三個state:token,user,isUserLoggedIn
isUserLoggedIn通過判斷token和user是否為空實(shí)現(xiàn)
user與token的更改則由action和mutation實(shí)現(xiàn)
回到register.vue 我們通過import AuthenticationService from '@/services/AuthenticationService'
引用了AuthenticationService 在這個模塊中 實(shí)現(xiàn)了與后端的交互 注冊/登錄
import Api from '@/services/Api'
export default {
register (credentials) {
return Api().post('register', credentials)
},
login (credentials) {
return Api().post('login', credentials)
}
}
在這個模塊中我們向后端'/register'發(fā)出post請求 參數(shù)為credentials即{ email: this.email, password: this.password }
在這里我們還引用Api模塊
import axios from 'axios'
export default() => {
return axios.create({
baseURL: `http://localhost:8081/`
})
}
在這個模塊中我們導(dǎo)入了axios用來實(shí)現(xiàn)http請求,并配置了后端的基地址.
至此,前端已經(jīng)發(fā)出了API請求,等待后端處理
后端(server)
后端主入口為src/app.js
const express = require("express")
const cors = require("cors")
const bodyParser = require("body-parser")
const morgan = require("morgan")
const {sequelize} = require('./models')
const config = require('./config/config')
const routes = require('./routes')
const app = express();
app.use(morgan('combined'))
app.use(bodyParser.json())
app.use(cors())
require('./routes')(app)
sequelize.sync({force:false})
.then(() => {
app.listen(config.port)
console.log(`Server started on port ${config.port}`)
})
通過require將所有的外部引用文件導(dǎo)入到項(xiàng)目中,通過app.use將引入的文件注冊到全局 使之可以在全局進(jìn)行訪問
require('./routes')(app)
完成路由的注冊
在routes.js
中
const AuthenticationControllerPolicy = require('./policies/AuthenticationControllerPolicy')
const AuthenticationController = require('../src/controller/AuthenticationController')
module.exports = (app) => {
app.post('/register',
AuthenticationControllerPolicy.register,
AuthenticationController.register)
}
通過require導(dǎo)入AuthenticationControllerPolicy和AuthenticationController,其中前者主要用于檢測用戶的輸入的合法性(通過joi及正則表達(dá)式進(jìn)行驗(yàn)證),后者用于數(shù)據(jù)庫操作.
AuthenticationControllerPolicy作為一個中間件,先于AuthenticationController執(zhí)行.
在AuthenticationControllerPolicy.js
中
const Joi = require('joi')
module.exports = {
register(req, res, next){
const schema = {
email:Joi.string().email(),
password:Joi.string().regex(
new RegExp('^[a-zA-Z0-9]{8,32}$')
),
}
const {error, value} = Joi.validate(req.body, schema)
if(error){
switch(error.details[0].type){
case 'string.email':
res.status(400).send({
error : 'you have to provide a validate email address'
})
break
case 'string.regex.base':
res.status(400).send({
error: `you have to provide a validate password:
1. upper case 2. lower case 3.numerics 4. 8-32 in length`
})
break
default:
res.status(400).send({
error: 'invalidated registration information'
})
}
}else{
next();
}
}
}
使用joi驗(yàn)證用戶的郵箱是否合法,密碼是否由大小寫數(shù)字8-32位組成.
調(diào)用語句Joi.validate
去用創(chuàng)建schema驗(yàn)證請求體
如果不符合schema則報(bào)錯,下面就通過一個條件分支進(jìn)行判斷,把具體的出錯原因返回到前端,以支持用戶修改. 如果沒有出現(xiàn)錯誤,即符合schema則調(diào)用next(),執(zhí)行AuthenticationController.js
的內(nèi)容
AuthenticationController.js
中
const {User} = require('../models')
module.exports = {
async register (req, res) {
try{
const user = await User.create(req.body)
res.send(user.toJSON())
}catch (err){
res.status(400).send({
error:"this email is already in use."
})
}
}
}
User為數(shù)據(jù)庫的Schema定義了表的字段以及數(shù)據(jù)類型,通過require導(dǎo)入進(jìn)來
這個模塊導(dǎo)出的為一個異步方法 register()
首先通過User.create(req.body)
創(chuàng)建一個User實(shí)例,數(shù)據(jù)位請求中的body
創(chuàng)建成功后將示例user析為json格式返回到前端
至此后端的任務(wù)也已經(jīng)完成.
本項(xiàng)目采取前后端分離的方式進(jìn)行開發(fā), 以最大化開發(fā)靈活度. 并且使用eslint規(guī)范,代碼具有一定的規(guī)范性