美文网首页
使用Vue与Express搭建一个网站

使用Vue与Express搭建一个网站

作者: 梅干菜烧饼不加肉 | 来源:发表于2019-01-03 22:04 被阅读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规范,代码具有一定的规范性

    相关文章

      网友评论

          本文标题:使用Vue与Express搭建一个网站

          本文链接:https://www.haomeiwen.com/subject/difcrqtx.html