eggjs+vue 全栈进行时(一)

1,379 阅读6分钟

前言

最近想搞事情,搞个小说app来玩下,顺便回顾下eggjs,向着全栈方向出发。
做个app也算个大工程了,主要分为server服务,后台管理系统,app端。 这篇文章主要记录下server项目的搭建,废话不多说开始动手。

项目开始

server端

server主要使用Egg.js、mysql。
小说的内容去爬取起点小说网,只爬免费章节。
不了解Egg.js的同学可以去官网看下文档。Egg.js

初始化

mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i

目录结构

初始化完毕目录结构如下图: 然后把项目结构进行改造下,

1、安装所需依赖:

npm i  egg-cors jsonwebtoken await-stream-ready cheerio cross-env egg-multipart egg-mysql egg-onerror egg-validate iconv-lite md5 nanoid stream-wormhole -S

2、区分开发测试生产环境

首先在正常的项目都需要进行环境的区分,常常分成:
local开发环境
test测试环境
prod生产环境 所以我们在package.json文件下新增几个scripts:

  "scripts": {
    "server:local": "cross-env EGG_SERVER_ENV=local egg-bin debug",
    "server:test": "cross-env EGG_SERVER_ENV=test egg-bin debug",
    "server:prod": "cross-env EGG_SERVER_ENV=prod egg-bin debug",
    "start:dev": "cross-env EGG_SERVER_ENV=dev egg-scripts start --daemon --title=egg-server-wx-admin ",
    "start:test": "cross-env EGG_SERVER_ENV=test egg-scripts start --daemon --title=egg-server-wx-admin ",
    "start:prod": "egg-scripts start --daemon --title=egg-server-wx-admin ",
    "stop": "egg-scripts stop --title=egg-server-wx-admin "
  },

使用cross-env,主要是在winodw和mac系统下可以区分环境变量EGG_SERVER_ENV

使用EGG_SERVER_ENV 需要注意个问题,EGG_SERVER_ENV=local 时(默认local),热更新才可以使用,设置为其他的参数热更新会失效。

在config目录下新增对应环境的config,config.default.js的配置是默认的,对应环境的config会覆盖config.default的配置

config.default.js 默认配置的开发环境配置


'use strict';

module.exports = appInfo => {
  const config = exports = {};
  config.keys = appInfo.name + '_1568685835614_3976';
  // 全局常量
  config.CONST = {
    ROOT: '',
    UPLOAD_URL: 'http://127.0.0.1:7001',
    BOOK_SOURCE_MAP: {
      1: {
        url: 'https://www.qidian.com',  // 主要是爬起点的免费章节,后续可以加入其它网站源
        name: '起点',
      },
    },
  };
  const userConfig = {
    // myAppName: 'egg',
  };
  config.cluster = {
    listen: {
      port: 7001,
      hostname: '0.0.0.0', // 不建议设置 hostname 为 '0.0.0.0',它将允许来自外部网络和来源的连接,请在知晓风险的情况下使用
      // path: '/var/run/egg.sock',
    },
  };
  config.mysql = {
    // 单数据库信息配置
    client: {
      // host
      host: '127.0.0.1',
      // 端口号
      port: '3306',
      // 用户名
      user: 'root',
      // 密码
      password: '1234567890',
      // 数据库名
      database: 'wx',
    },
    // 是否加载到 app 上,默认开启
    app: true,
    // 是否加载到 agent 上,默认关闭
    agent: false,
  };

  // 报错处理
  config.onerror = {
    errorPageUrl: (err, ctx) => ctx.errorPageUrl || '/500',
    json: (err, ctx) => {
      ctx.body = {
        code: err.status,
        msg: err.message,
      };
    },
  };
  // 中间件
  config.middleware = [ 'httpError', 'verLogin' ];
  config.httpError = {
    match: '/',
  };
  config.verLogin = {
    match: '/token',
  };
  // 跨域配置
  config.cors = {
    origin: [ '*' ],
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
    credentials: true,
  };
  config.security = {
    // csrf: false,
    csrf: {
      enable: false, // 前后端分离,post请求不方便携带_csrf
      ignoreJSON: true,
      headerName: 'authorization',
    },
    methodnoallow: {
      enable: false,
    },

  };
  // 上传文件
  config.multipart = {
    mode: 'stream',
  };


  return {
    ...config,
    ...userConfig,
  };
};


测试环境配置config.test.js


'use strict';
module.exports = () => {
  const config = exports = {};
  config.CONST2 = 'const2';
  config.mysql = {
    // 单数据库信息配置
    client: {
      // host
      host: 'xx.xxx.xxx.xxx',
      // 端口号
      port: '3306',
      // 用户名
      user: 'root',
      // 密码
      password: 'xxxxxxx',
      // 数据库名
      database: 'wx',
    },
    // 是否加载到 app 上,默认开启
    app: true,
    // 是否加载到 agent 上,默认关闭
    agent: false,
  };
  return {
    ...config,
  };
};

其他环境配置略过...

插件配置 config/plugin.js

'use strict';

exports.validate = {
  enable: true,
  package: 'egg-validate',
};
// 跨域
exports.cors = {
  enable: true,
  package: 'egg-cors',
};
// mysql
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};
// 上传
exports.multipart = {
  enable: true,
};


配置自定义的中间件(Middleware)

全局报错中间件

新建 app/middleware/httpError.js

'use strict';
module.exports = () => {
  return async function httpError(ctx, next) {
    try {
      await next();
    } catch (err) {
      console.log(err);
      if (err.msg) {
        ctx.body = {
          code: err.code,
          data: '',
          msg: err.msg,
        };
      } else {
        ctx.body = {
          code: 500,
          data: '',
          msg: '服务器内部错误:' + err,
        };
        ctx.status = 500;
      }

    }
  };
};

在config/config.default.js,下配置自定义中间件:

  // 自定义中间件
  config.middleware = [ 'httpError' ];
  config.httpError = {
    match: '/',
  };

这个中间件是全局捕捉错误返回,有这个中间件遇到需要报错的逻辑只需

 const err = new Error('报错啦');
 throw err;

就能返回错误信息。
然后我们还可以继续优化下,把新建个报错函数,把它挂在app对象下: 新建 app/extend/application.js

'use strict';
module.exports = {
  // 报错扩展
  throwError(code = 400, msg = '服务器错误') {
    const err = new Error(msg);
    err.code = code;
    err.msg = msg;
    throw err;
  },
};

需要调用时:

this.app.throwError(400, '账号不能为空');

就会返回:

{
    code: 400,
    data: '',
    msg: '账号不能为空',
}

3、创建数据库

打开mysql,新建个数据库,上面的数据库名为 wx

在根目录下新建 db/wx.sql 文件

新建个user,books表

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `userId` int(20) DEFAULT NULL,
  `age` int(3) DEFAULT NULL,
  `tel` varchar(11) DEFAULT NULL,
  `account` varchar(50) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `token` varchar(255) DEFAULT NULL,
  `headImg` varchar(255) DEFAULT NULL,
  `authority` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `bookId` varchar(255) NOT NULL,
  `bookName` varchar(255) DEFAULT NULL,
  `author` varchar(255) DEFAULT NULL,
  `bookImage` varchar(255) DEFAULT NULL,
  `bookUrl` varchar(255) DEFAULT NULL,
  `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci,
  `bookType` varchar(255) DEFAULT NULL,
  `bookSource` varchar(255) DEFAULT NULL,
  `bookSourceUrl` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=224 DEFAULT CHARSET=utf8;

4、先撸个后台登录模块

登录功能使用 jsonwebtoken 来进行token校验

先写个登录校验的中间件

新建 app/middleware/verLogin.js

'use strict';
const jwt = require('jsonwebtoken');
module.exports = () => {
  return async function verLogin(ctx, next) {
    const token = ctx.request.header.authorization;
    if (token) {
      // 解码token
      try {
        jwt.verify(token, ctx.app.config.keys);
      } catch (error) {
        if (error.name === 'TokenExpiredError') {
          ctx.app.throwError(3000, 'token时间到期');
        }
        ctx.app.throwError(3000, 'token失效');
      }
      const user = await ctx.service.user.get({
        token,
      });
      if (!user) {
        ctx.app.throwError(3000, '账号不存在');
      }
      await next();
    } else {
      ctx.app.throwError(3000, '没有token');
    }
  };
};

在config/config.default.js,下配置登录中间件:

  // 自定义中间件
  config.middleware = [ 'verLogin' ];
  config.verLogin = {
    match: '/token',
  };

登录需要的接口:app/router.js

'use strict';
module.exports = app => {
  const { router, controller } = app;
  // 运营后台端
  router.post('/admin/user/login', controller.admin.user.login);
  router.post('/token/admin/user/logout', controller.admin.user.logout);
  router.get('/token/admin/user/get', controller.admin.user.get); // 获取用户信息
};

app/controller/admin/user.js

'use strict';
const Controller = require('egg').Controller;
const jwt = require('jsonwebtoken');
class UserController extends Controller {
  /**
   * 接口描述:后台登录
   * 请求方式:post
   * 参数:{
   *          account:string  //账号
   *          password:string  //密码
   *       }
   *  header:authorization
   */
  async login() {
    const { ctx, app } = this;
    const query = ctx.request.body;
    const option = {
      account: query.account,
      password: query.password,
    };
    if (!query.account) {
      app.throwError(400, '账号不能为空');
    }
    if (!query.password) {
      app.throwError(400, '密码不能为空');
    }
    const user = await ctx.service.user.get(option);
    if (!user) {
      app.throwError(400, '账号密码错误');
    }
    const content = { name: user.userId }; // 要生成token的主题信息
    const secretOrPrivateKey = app.config.keys; // 这是加密的key(密钥)
    const token = jwt.sign(content, secretOrPrivateKey, {
      expiresIn: 60 * 60 * 1, // 1小时过期
    });
    const result = await ctx.service.user.update({ token }, {
      userId: user.userId,
    });
    if (result) {
      ctx.body = {
        code: 200,
        data: token,
      };
    }
  }
  /**
   * 接口描述:退出登陆
   * 请求方式:post
   * 参数:{}
   * header:authorization
   */
  async logout() {
    const { ctx, app } = this;
    const token = ctx.request.header.authorization;
    if (!token) {
      app.throwError(400, '需要token');
    }
    const user = await ctx.service.user.get({
      token,
    });
    if (!user) {
      app.throwError(400, '账号不存在');
    }

    const result = await ctx.service.user.update({ token: '' }, {
      userId: user.userId,
    });
    if (result) {
      ctx.body = {
        code: 200,
        data: '',
      };
    }

  }
  async get() {
    const { ctx } = this;
    console.log(this.app.config.keys);
    const token = ctx.request.header.authorization;

    const data = await ctx.service.user.get({ token });
    if (data) {
      ctx.body = {
        code: 200,
        data,
        msg: '',
      };
      return;
    }
    this.app.throwError(400, '用户不存在');
  }
module.exports = UserController;

app/service/user.js

/**
 * user数据库封装
 */

'use strict';
const Service = require('egg').Service;

class UserService extends Service {
  // 获取信息
  async get(param) {
    const { app } = this;
    const user = await app.mysql.get('user', param);
    return user;
  }
  // 修改user  param ={}  修改的参数  where={} 修改的条件
  async update(param, where) {
    const { app } = this;
    const options = {
      where,
    };
    const result = await app.mysql.update('user', {
      ...param,
    }, options);
    if (result.affectedRows !== 1) {
      app.throwError(500, '数据库错误');
    }
    return true;
  }
}

module.exports = UserService;

登录接口开发完毕,向user表插入一条admin数据:

INSERT INTO `user` VALUES ('1', '小米发s撒2', '1128228373', '11', '17688946637', 'admin', 'admin', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxMTI4MjI4MzczLCJpYXQiOjE1OTg2MDgxMDksImV4cCI6MTU5ODYxMTcwOX0.6AA8XQQgwFd6yI0H4_JfggjhVOaMQS6kdWV1u9tbXqk', 'http://127.0.0.1:7001/public/uploads/94a46ce74d34f75495a3a43136032af7.jpg', 'admin');

后台管理系统

使用vue-cli 3.0创建,使用element-ui
目录:

新建页面login.vue:

<template>
  <div class="login-wrap">
    <div class="login-mask"></div>
    <div class="ms-login">
      <div class="ms-title">后台管理系统</div>
      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="0px" class="ms-content">
        <el-form-item prop="username">
          <el-input v-model="ruleForm.username" placeholder="username">
            <el-button slot="prepend" icon="el-icon-lx-people"></el-button>
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            type="password"
            placeholder="password"
            v-model="ruleForm.password"
            @keyup.enter.native="submitForm('ruleForm')"
          >
            <el-button slot="prepend" icon="el-icon-lx-lock"></el-button>
          </el-input>
        </el-form-item>
        <div class="login-btn">
          <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
        </div>
        <p class="login-tips">Tips : 用户名和密码随便填。</p>
      </el-form>
    </div>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      ruleForm: {
        username: "admin",
        password: "admin"
      },
      rules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" }
        ],
        password: [{ required: true, message: "请输入密码", trigger: "blur" }]
      }
    };
  },
  created(){
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate(valid => {
        if (valid) {
          this.$api
            .common.login({
              account: this.ruleForm.username,
              password: this.ruleForm.password
            })
            .then(res => {
              if (res.code === 200) {
                this.$myLocalStorage.set("token", res.data);
                this.$router.push('/')
              }
            });
        } else {
          return false;
        }
      });
    }
  }
};
</script>

<style scoped>
.login-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  background-image: url(../assets/img/login-bg.jpg);
  background-size: 100%;
}
.login-mask{
  position: absolute;
  width: 100%;height: 100%;top: 0;left: 0;
  background-color: rgba(0, 0, 0, 0.3);
}
.ms-title {
  width: 100%;
  line-height: 50px;
  text-align: center;
  font-size: 20px;
  color: #fff;
  border-bottom: 1px solid #ddd;
}
.ms-login {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 350px;
  margin: -190px 0 0 -175px;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.3);
  overflow: hidden;
}
.ms-content {
  padding: 30px 30px;
}
.login-btn {
  text-align: center;
}
.login-btn button {
  width: 100%;
  height: 36px;
  margin-bottom: 10px;
}
.login-tips {
  font-size: 12px;
  line-height: 30px;
  color: #fff;
}
</style>

最后进行测试:

今天先到这里,下章节继续。