前言
最近想搞事情,搞个小说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>
最后进行测试:
今天先到这里,下章节继续。