使用Vue与Express搭建一个网站

2019-01-03  本文已影响0人  梅干菜烧饼不加肉

Github地址:https://github.com/bing-zhub/fullStackVueExpress

OverView

在很久之前, 使用Vue做前端, Express做后端, 但是都没有整理过. 先把内容贴出来, 过段时间详细整理.
这是一个基于vue和express的全栈项目,目标是实现一个符合Material Design风格的CMS系统.

前端基于vue-cli提供的webpack模板进行开发, 使用vuetify作为前端组件库, 配合一些的第三方组件化工具,完成前端的开发.

后端使用express.采用Sequelize对数据库进行增删改查操作.

前后端使用axios完成RESTfull交互

这里主要对项目进行一个顶层的描述, 同时提供一个前后端分离进行请求的案例.

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以实现数据库连接等, 如果不进行配置 将无法运行

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'
    }
}

技术栈

前端 vue 使用vue-cli的webpack模板

axios api请求工具

基于promise的HTTP客户端

vuex状态管理

用户的登录状态, 页面路由状态等

vue-router 前端路由

对前端url进行解析, 指向不同界面

vuetify 前端组件

一个符合MaterialDesign的前端组件库 对vue支持甚好

font awesome

前端图标库: 图标组件库

quillEditor

富文本编辑器: 作用户编辑用(todo)

editor.md,simplemde-markdown-editor

Markdown编辑器: 一个高生产力markdown工具(todo)

video player (to do)

后端 express

bcrypt-nodejs sha256加密工具

bluebird promisify工具

body-parser api请求解析工具

cors 跨域请求工具

joi 数据模型验证

jsonwebtoken 用户身份信息认证

morgan 日志中间件

sequelize 基于promise的ORM工具,用于数据库交互

数据库 mysql

目录结构

--client
    --build build文件夹**vue-cli生成**
    --config webpack等配置文件**vue-cli生成**
    --node_modules 各种依赖库
    --src vue前端源码
        -- assets 放置一些静态文件
            -- highlight markdown语法高亮样式
            -- style 公用css样式库
        -- components 组件库使用大驼峰命名
            -- Blank.vue 空白模板便于创建新组件
            -- CreateSong.vue 用于创建新内容
            -- 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 主要内容加载页
            -- ViewSong.vue 详情页
        -- config
        -- router 路由处理
            -- index.js 前端路由
        -- services 服务处理
            -- Api.js 发送api请求
            -- AuthenticationServices.js 处理登录/注册
            -- SongServices.js 处理歌曲查看/添加/详情
        -- store 状态仓库
            -- 放置state mutation actions
        -- App.vue 主入口
        -- main.js 主入口
    --static 静态资源
    --test 测试文件
--server
    -- node_modules 服务端第三方库
    -- src 服务端源码
        -- config
            -- config.js 配置端口/数据库服务器/JWT 密码
        -- controller api详细处理
            -- AuthenticationController.js 处理来自前端的登录注册请求
            -- SongsController.js 处理来自前端的检索添加请求
        -- model 数据库Schema
            -- index.js 数据库Schema索引
            -- Song.js 歌曲Schema
            -- User.js 用户Schema
        -- policy 输入验证规则
            -- AuthenticationControllerPolicy.js 验证注册时用户名与密码是否规范
    -- app.js 后端主入口
    -- routes.js 后端路由,对api进行路由

示例 -- 用户注册

前端 (client)

入口文件 index.html, 在div标签中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会在全局出现(任何页面都包含着三个组件)
router-view会根据当前url渲染不同的内容

<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
  }
}

进行组件注册,在header中放置一些导航信息

下面就进入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'})">
            发现
        </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会注册一个监听器 但点击时调用navigateTo方法(自定义方法) -->
        <!-- 传入router对象-->
        <!-- v-if是一个条件渲染 当v-if后的值为false时 不渲染(隐藏) -->
        <!-- 是为了在用户登陆后隐藏注册和登录按钮 v-if是由vuex管理的全局状态 -->
        <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 实现页面跳转
    navigateTo (route) {
      this.$router.push(route)
    },
    logout () {
      this.$store.dispatch('setToken', null)
      this.$store.dispatch('setUser', null)
      this.$router.push({
        name: 'root'
      })
    }
  }
}

根据路由检索 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分别定义了三个字段邮箱/密码/确认密码
在v-card-actions 定义了两个监听 当事件触发 打开dialog被设置为true 显示出来 并且触发自定义的方法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 || '这是一个必填项'
      }
    }
  },
  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 在调用成功后 将message设置为'登陆成功'

之后对store进行dispatch


vuex

只有通过dispatch才能触发store中的action从而修改state

我们在main.js中进行了如下的声明

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)
    }
  }
})

这里设置了三个state:token,user,isUserLoggedIn
isUserLoggedIn通过判断token和user是否为空实现
user与token的更改则由action和mutation实现

回到register.vue 我们通过import AuthenticationService from '@/services/AuthenticationService'引用了AuthenticationService 在这个模块中 实现了与后端的交互 注册/登录

import Api from '@/services/Api'

export default {
  register (credentials) {
    return Api().post('register', credentials)
  },
  login (credentials) {
    return Api().post('login', credentials)
  }
}

在这个模块中我们向后端'/register'发出post请求 参数为credentials即{ email: this.email, password: this.password }

在这里我们还引用Api模块

import axios from 'axios'

export default() => {
  return axios.create({
    baseURL: `http://localhost:8081/`
  })
}

在这个模块中我们导入了axios用来实现http请求,并配置了后端的基地址.

至此,前端已经发出了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将所有的外部引用文件导入到项目中,通过app.use将引入的文件注册到全局 使之可以在全局进行访问
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导入AuthenticationControllerPolicy和AuthenticationController,其中前者主要用于检测用户的输入的合法性(通过joi及正则表达式进行验证),后者用于数据库操作.
AuthenticationControllerPolicy作为一个中间件,先于AuthenticationController执行.
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验证用户的邮箱是否合法,密码是否由大小写数字8-32位组成.
调用语句Joi.validate去用创建schema验证请求体
如果不符合schema则报错,下面就通过一个条件分支进行判断,把具体的出错原因返回到前端,以支持用户修改. 如果没有出现错误,即符合schema则调用next(),执行AuthenticationController.js的内容

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为数据库的Schema定义了表的字段以及数据类型,通过require导入进来
这个模块导出的为一个异步方法 register()
首先通过User.create(req.body)创建一个User实例,数据位请求中的body
创建成功后将示例user析为json格式返回到前端

至此后端的任务也已经完成.

本项目采取前后端分离的方式进行开发, 以最大化开发灵活度. 并且使用eslint规范,代码具有一定的规范性

上一篇下一篇

猜你喜欢

热点阅读