Vue2 配 Express 搭建个人博客

前言

这个示例没有过多的修饰,没有好看的界面,只是从功能实现出发,不保证好看,但保证能用,这个示例通过 vue+express+ mongodb 搭建极简版个人博客,实现博客文章管理中简单的增删改查。

宏观把握

简单的文件结构

├── server
│ ├── modules
│ │ └── index.js // mongooes 模型
│ ├── api.js // 后台的所有接器都放在这里
│ ├── db.js // 数据库连接信息
│ └── index.js
└── src
├── components
│ ├── DetailPost.vue // 详情页
│ ├── EditPost.vue // 编辑页
│ └── Home.vue // 主页
├── resources
│ └── postResource.js // 业务逻辑代码(发起后端请求)
├── router
│ └── index.js
├── App.vue
└── main.js

涉及到的技术栈:

  • Vue.js
  • Axios(Vue.js 的一个 http 请求模块)
  • Express(基于 Node.js 的一个 WEB 框架)
  • Node.js
  • MongoDB
  • mongoose(用来操作 MongoDB 数据库的一个模块 )

微观审察

直接上代码,在关键代码都会带上注释。

后端代码

index.js

const express = require('express');
const fs = require('fs');
const path = require('path');
const api = require('./api');
// bodyParser 变量是对中间件的引用。
// 请求体解析后,解析值都会被放到 req.body 属性,内容为空时是一个{}空对象。
// 客户端发送的 HTTP 请求体里本应是纯文本的内容(包括一些参数),bodyParser 就是帮你把请求转换为对象的形式,你可以在路由中通过形如 req.body 来获取相关的参数
const bodyParser = require('body-parser');
// 创建一个 Express 应用。express() 是一个由 express 模块导出的入口(top-level)函数。
const app = express();
// 创建 application/json 解析,解析json数据格式
app.use(bodyParser.json());
// 创建 application/x-www-form-urlencoded 解析,解析我们通常的 form 表单提交的数,也就是请求头中包含这样的信息: Content-Type: application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: false}));
// 引入 api 中间件
app.use(api);
// 设置默认的静态资源文件访问目录,这个不加的话,vue 打包完后里面的访问资源的路径是不对的
app.use(express.static(path.resolve(__dirname, '../dist')))
app.get('/', function (req, res) {
  const html = fs.readFileSync(path.resolve(__dirname,'../dist/index.html'),'utf-8')
  res.send(html);
});
const server = app.listen(3000, function () {
  const host = server.address().address;
  const port = server.address().port;
});

app.js

const models = require('./modules');
const express = require('express');
// 引入 express 路由
const router = express.Router();
const date = new Date;
// 获取所有文章
router.get('/api/posts/getPostList',(req,res) => {
// 通过模型去查找数据库(根据 _id 进行倒序),有关 mongooes 的更多用法可以阅读官方文档
  models.article.find().sort({_id:-1}).exec((err,data) => {
    if (err) {
      res.send(err);
    } else {
      res.send(data)
    }
  });
});
// 新增文章
router.post('/api/posts/savePost',(req,res) => {
  // 前端 post 请求,直接传个对象,后端通过 req.body 来拿参数
  let post = {
    title: req.body.title,
    description: req.body.description,
    createDate: date.getTime()
  }
  models.article.create(post,(err) => {
    if (err) {
      res.send(err);
    } else {
    // 返回 1 表示操作成功
      res.send("1");
    }
  });
});
// 删除文章
router.get('/api/posts/deletePost',(req,res) => {
  // 前端 get 请求,params 传参,后端用 req.query 来拿参数
  const id = req.query.id;
  models.article.remove({_id:id},(err) => {
    if (err) {
      res.send(err);
    } else {
    // 返回 1 表示操作成功
      res.send("1");
    }
  });
});
// 更新文章
router.post('/api/posts/updatePost',(req,res) => {
  // 前端 get 请求,params 传参,后端用 req.query 来拿参数
  const id = req.body._id;
  let post = {
    title: req.body.title,
    description: req.body.description,
    createDate: date.getTime()
  }
  models.article.findOneAndUpdate({_id:id},post,(err) => {
    if (err) {
      res.send(err);
    } else {
    // 返回 1 表示操作成功
      res.send("1");
    }
  });
});
// 获取文章明细
router.get('/api/posts/getPostDetail',(req,res) => {
  const id = req.query.id
  models.article.findById(id,(err,data) => {
    if (err) {
      res.send(err);
    } else {
      res.send(data);
    }
  });
});
module.exports = router;

db.js

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/blogdb');
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function (callback) {});
module.exports = mongoose;

modules/index.js

const mongoose = require('../db');
/************** 定义模式 usersSchema **************/
const Schema = mongoose.Schema;
// 这里配置的 Schema 相当于一个声明,指定接收的字段
const userSchema = new Schema({
  name : String,
  password : String
});
const articleSchema = new Schema({
  title : String,
  description : String,
  createDate: String
});
/************** 定义模型 Model **************/
const Models = {
  user : mongoose.model('user',userSchema),
  article : mongoose.model('article',articleSchema),
}
module.exports = Models;

前端代码

components/home.vue

<template>
  <div>
    <div>
      <button @click="edit()">新增</button>
    </div>
    <ul>
      <li v-for="item in dataList" :key="item._id">
        <h3 @click="detailPost(item._id)">{{item.title}}</h3>
        <p>{{item.description}}</p>
        <button @click="deletePost(item._id)">删除</button>
        <button @click="edit(item._id)">修改</button>
      </li>
    </ul>
  </div>
</template>
<script>
import postResource from "@/resources/postResource";
export default {
  name: 'Home',
  data () {
    return {
      dataList:[]
    }
  },
  created(){
    this.getPosts()
  },
  methods:{
    getPosts(){
      postResource.getPostList().then(response => {
        this.dataList = response.data;
      })
      .catch(error => {
        console.log(error);
      });
    },
    edit(editId){
      if(editId){
        // 编程式导航实现跳转,这里用 query 来传参,这样话话,新增页和修改页使用同一个路由就可以,不需要写两个路由
        this.$router.push({path:"/editPost",query:{id:editId}});
      }else{
        // 编程式导航实现跳转
        this.$router.push({path:"/editPost"});
      }
    },
    detailPost(id){
      // 编程式导航实现跳转,这里需要注意的时使用 this.$router.push() 跳转的时候,通过 params 来传参的话,
      // 前面配置的属性是 name (这里的 name 就是在配置路由时给路由添加的 name )而不是 path 不然参数会传不过去的。
      this.$router.push({name:"detailPost",params:{id:id}});
    },
    deletePost(id){
      postResource.deletePost(id).then(response => {
      if(response.data === 1){
        console.log("删除成功");
        this.getPosts();
      }else{
        console.log("删除失败");
      }
      })
      .catch(error => {
        console.log(error);
      });
    }
  }
}
</script>
<style scope>
ul{
  padding: 0
}
ul li{
  padding: 10px 0;
  border-bottom: 1px solid #ccc;
  list-style: none;
}
ul li h3{
  cursor: pointer;
}
button{
  background: transparent;
  border: 1px solid #ccc;
  padding: 5px 18px;
  cursor: pointer;
}
h3{
  padding: 10px 0
}
</style>

components/EditPost.vue

<template>
<div>
  <div>
    <div>
      <label>标题:</label>
      <div><input type="email" v-model="postVo.title" placeholder="请输入内容"></div>
    </div>
    <div>
      <label>内容:</label>
      <div><textarea v-model="postVo.description" rows="13"></textarea></div>
    </div>
  </div>
  <button @click="save">保存</button>
</div>
</template>
<script>
import postResource from "@/resources/postResource"
import marked from "marked"
export default {
  name: 'EditPost',
  data () {
    return {
      postVo:{
        title:"",
        description:""
      },
      dataList:[],
      id:""
    }
  },
  created(){
    this.id = this.$route.query.id;
    // 如果 id 存在,则说明是编辑
    if(this.id){
      postResource.getPostDetail(this.id).then(response => {
      this.postVo = response.data;
      })
      .catch(error => {
        console.log(error);
      });
    }
  },
  methods:{
    save(){
      if(this.id){ // 更新
        postResource.updatePost(this.postVo).then(response => {
        console.log(response);
        if(response.data === 1){
        this.$router.push({path:"/"});
      }else{
        console.log("更新失败");
      }})
      .catch(error => {
        console.log(error);
      });
      }else{ // 新增
        postResource.savePost(this.postVo).then(response =>{
        console.log(response);
        if(response.data === 1){
          this.$router.push({path:"/"});
        }else{
          console.log("保存失败");
        }}).catch(error => {
            console.log(error);
        })
      }
    }
  }
}
</script>

components/DetailPost.vue

<template>
  <div class="container-fluid">
    <h1>{{dataDetail.title}}</h1>
    <div v-html="dataDetail.description"></div>
  </div>
</template>
<script>
import postResource from "@/resources/postResource"
export default {
  name: 'DetailPost',
  data () {
    return {
      dataDetail:{}
    }
    },
  created(){
    const id = this.$route.params.id;
    postResource.getPostDetail(id).then(response => {
      this.dataDetail = response.data;
    }).catch(error => {
      console.log(error);
    });
  }
}
</script>

resources/postResource.vue

/*
* @Author: zhaoxixiong
* @Date: 2018-04-02 20:59:23
* @Last Modified by: zhaoxixiong
* @Last Modified time: 2018-04-04 13:21:30
*
*
*/
import axios from "axios";
let userResource = {
  getPostList:function(){
    return axios.get("http://localhost:3000/api/posts/getPostList");
  },
  // get 直接传含有参数键值对的 params 对象
  getPostDetail:function(id){
    console.log(id);
    return axios.get("http://localhost:3000/api/posts/getPostDetail",{
      params:{
         id:id
      }
    });
  },
  // post 直接传对象
  updatePost:function(postVo){
    return axios.post("http://localhost:3000/api/posts/updatePost",postVo);
  },
  deletePost:function(id){
    return axios.get("http://localhost:3000/api/posts/deletePost",{
      params:{
        id:id
      }
    });
  },
  savePost:function(postVo){
    return axios.post("http://localhost:3000/api/posts/savePost",postVo);
  },
  getUserList:function(){
    return axios.get("http://localhost:3000/api/user/getUserList");
  },
  login:function(){
    return axios.post("http://localhost:3000/api/login",{
      title:title,
      descrition:descrition
    });
  }
}
export default userResource;

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import EditPost from '@/components/EditPost'
import DetailPost from '@/components/DetailPost'
Vue.use(Router)
export default new Router({
  routes: [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/editPost',
    name: 'editPost',
    component: EditPost
  },
  {
    path: '/detailPost/:id',
    name: 'detailPost',
    component: DetailPost
  }]
})

前端通过 vue-cli 创建项目,所以直接使用 npm run build 命令对前端代码进行打包。打包生成 dist 目录。然后在服务配置下访问路径:

server/index.js

// 设置默认的静态资源文件访问目录,这个不加的话,vue 打包完后里面的访问资源的路径是不对的
app.use(express.static(path.resolve(__dirname, '../dist')))
app.get('/', function (req, res) {
  const html = fs.readFileSync(path.resolve(__dirname,'../dist/index.html'),'utf-8')
  res.send(html);
});

也就是上面的server/index.js 中的代码,这里只是把其中一段代码拷贝过来而已,只是作个提醒,别忘记了配置这个,不然打完包之后通过后端访问不到正常的页面。

在这个示例中,在还没打包之前,在开发中,如果你想访问后端接口,你就得配置下跨域。方法有两种:

第一种是直接配置浏览器,右键浏览器桌面图标,【属性】-【快捷方式】,在目标中追加一行代码:

--disable-web-security --user-data-dir=C:\MyChromeDevUserData --enable-file-cookies

还有一种方法就是在 Vue-cli 的配置文件(config/index.js)中配置下:

proxyTable: {
  '/api': {
    target: 'http://localhost:3000/',
    changeOrigin: true,
    pathRewrite: {
      '^/api': '/api'
    }
  }
},

但是这种方法千万记得重启 Vue 服务,不然不会起作用的,我用了第一种方法。

当你把前端打包后(打包后的目录 dist),放到后端中就不会有跨域的问题了,后端只需要在上面的 server/index.js 配置下默认的首页 dist/index.html 就可以。

这样一通搞下来,学到了不少,也发现了自己还有很多要学,比如:koa2、egg.js ,egg.js 是基于 koa2 基础上进行开发的。koa2 比 express 框架更新。虽然不能说 express 很老旧,但也快完成了它的使命,前端技术就是这样,日新月异,不学就只能被行业抛弃。