使用Vue與Express搭建一個網(wǎng)站

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'})">
            注冊&nbsp;&nbsp;<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'})">
            登錄&nbsp;&nbsp;<v-icon>fas fa-sign-in-alt</v-icon>
        </v-btn>
        <v-btn
          flat
          dark
          v-if='$store.state.isUserLoggedIn'
          @click="logout">
            退出登錄&nbsp;&nbsp;<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


vuex

只有通過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ī)范性

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末邪锌,一起剝皮案震驚了整個濱河市彭羹,隨后出現(xiàn)的幾起案子媳禁,更是在濱河造成了極大的恐慌,老刑警劉巖垦梆,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡看政,警方通過查閱死者的電腦和手機(jī)更米,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進(jìn)店門欺栗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人征峦,你說我怎么就攤上這事迟几。” “怎么了栏笆?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵类腮,是天一觀的道長。 經(jīng)常有香客問我竖伯,道長存哲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任七婴,我火速辦了婚禮祟偷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘打厘。我一直安慰自己修肠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布户盯。 她就那樣靜靜地躺著嵌施,像睡著了一般饲化。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吗伤,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天吃靠,我揣著相機(jī)與錄音,去河邊找鬼足淆。 笑死巢块,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的巧号。 我是一名探鬼主播族奢,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丹鸿!你這毒婦竟也來了越走?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤靠欢,失蹤者是張志新(化名)和其女友劉穎廊敌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掺涛,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡庭敦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了薪缆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秧廉。...
    茶點(diǎn)故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拣帽,靈堂內(nèi)的尸體忽然破棺而出疼电,到底是詐尸還是另有隱情,我是刑警寧澤减拭,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布蔽豺,位于F島的核電站,受9級特大地震影響拧粪,放射性物質(zhì)發(fā)生泄漏修陡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一可霎、第九天 我趴在偏房一處隱蔽的房頂上張望魄鸦。 院中可真熱鬧,春花似錦癣朗、人聲如沸拾因。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绢记。三九已至扁达,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蠢熄,已是汗流浹背跪解。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留签孔,地道東北人惠遏。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像骏啰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抽高,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內(nèi)容