前端(iview)+后端(nodejs+koa2+sequelize)分离框架搭建

后端源码:https://github.com/dumplingbao/dissplat

前端源码:https://github.com/dumplingbao/dissplat_web

概述

iview:一套基于 Vue.js 的高质量UI 组件库,主流vue前端框架,比较适合前后端分离框架的搭建,当然你也可以选择其他的

koa2:基于nodejs平台的下一代web开发框架,这里我们不选早期的目前用的最多的Express,也不选阿里开源的框架egg,我们选择则目前比较新的koa2,写起来简单,也易于学习

sequelize:这个是个nodejs的ORM框架,用的比较多,关于这个框架的介绍,可以看一下我的另一篇博客,node之ORM框架。

搭建这个前后端分离的框架纯属娱乐加学习,写此博客就是把搭建过程介绍一下,也作为自己的一点心得吧。

后端-koa

先找个轮子,这里用狼叔的koa-generator来生成项目架构

npm install koa-generator -g
koa2 dissplat //项目名称

生成文档结构

.
├── bin
├── public
├── routes
├── view
├── package.json
└── app.js

既然是轮子,直接就可以运行了

npm install
npm run dev

系统自动创建users表

(node:15140) [SEQUELIZE0002] DeprecationWarning: The logging-option should be either a function or false. Default: console.log
Executing (default): CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER auto_increment , `nickname` VARCHAR(255), `email` VARCHAR(128) UNIQUE, `password` VARCHAR(255), `created_at` DATETIME, `updated_at` DATETIME NOT NULL, `deleted_at` DATETIME, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `users`

sequelize持久化ORM框架

我们先按这个结构走,因为我们搭建前后端分离的框架,所以,public下面的图片、样式文件夹用不到,我们就把sequelize的model、dao、service、配置文件等放到public下面,删除public下面之前已有的文件

config

public下面新建一个config文件夹,里面创建config.js,放数据库的配置信息

module.exports = {
​    database: {
​        dbName: 'boblog',
​        host: 'localhost',
​        port: 3306,
​        user: 'root',
​        password: 'root'
​    }
}

db.js

新建一个utils文件夹,创建一个db.js,我们简单封装,创建一个连接数据库的工具类

const Sequelize = require('sequelize')
const {
​    dbName,
​    host,
​    port,
​    user,
​    password
} = require('../config/config').database
const sequelize = new Sequelize(dbName, user, password, {
​    dialect: 'mysql',
​    host,
​    port,
​    logging: true,
​    timezone: '+08:00',
​    define: {
​        // create_time && update_time
​        timestamps: true,
​        // delete_time
​        paranoid: true,
​        createdAt: 'created_at',
​        updatedAt: 'updated_at',
​        deletedAt: 'deleted_at',
​        // 把驼峰命名转换为下划线
​        underscored: true,
​        scopes: {
​            bh: {
​                attributes: {
​                    exclude: ['password', 'updated_at', 'deleted_at', 'created_at']
​                }
​            },
​            iv: {
​                attributes: {
​                    exclude: ['content', 'password', 'updated_at', 'deleted_at']
​                }
​            }
​        }
​    }
})
// 创建模型
sequelize.sync({
​    force: false
})
module.exports = {
​    sequelize
}

model

接下来新建一个model文件夹,创建一个user.js,创建一个user的model

const moment = require('moment');
const bcrypt = require('bcryptjs')
const {Sequelize, Model} = require('sequelize')
const {db} = require('../utils/db')

class User extends Model {}
User.init({
    // attributes
    id: {
        type: Sequelize.INTEGER,
        primaryKey: true,
        autoIncrement: true
    },
    // 昵称
    nickname: Sequelize.STRING,
    // 邮箱
    email: {
        type: Sequelize.STRING(128),
        unique: true
    },
    // 密码
    password: {
        type: Sequelize.STRING,
        set(val) {
            // 加密
            const salt = bcrypt.genSaltSync(10);
            // 生成加密密码
            const psw = bcrypt.hashSync(val, salt);
            this.setDataValue("password", psw);
        }
    },
    created_at: {
        type: Sequelize.DATE,
        get() {
            return moment(this.getDataValue('created_at')).format('YYYY-MM-DD');
        }
    }
}, {
    db,
    modelName: 'users'
    // options
});

dao

接下来新建一个dao文件夹,创建一个user.js,创建一个user的dao,负责CRUD

const {User} = require('../model/user')
const bcrypt = require('bcryptjs')
class UserDao {
​    // 创建用户
​    static async createUser(v) {
​        const hasUser = await User.findOne({
​            where: {
​                email: v.email,
​                deleted_at: null
​            }
​        });
​        
​        if (hasUser) {
​            throw new global.errs.Existing('用户已存在');

​        }
​        const user = new User();
​        user.email = v.email;
​        user.password = v.password;
​        user.nickname = v.nickname;
​        return user.save();
​    }
​    // 验证密码
​    static async verifyEmailPassword(email, plainPassword) {
​        // 查询用户是否存在
​        const user = await User.findOne({
​            where: {
​                email
​            }
​        })
​        if (!user) {
​            throw new global.errs.AuthFailed('账号不存在')
​        }
​        // 验证密码是否正确
​        const correct = bcrypt.compareSync(plainPassword, user.password);
​        if (!correct) {
​            throw new global.errs.AuthFailed('密码不正确')
​        }
​        return user
​    }
​    // 删除用户
​    static async destroyUser(id) {
​        const user = await User.findOne({
​            where: {
​                id,
​                deleted_at: null
​            }
​        });
​        if (!user) {
​            throw new global.errs.NotFound('没有找到此用户');
​        }
​        user.destroy()
​    }
​    // 获取用户详情
​    static async getUserInfo(id) {
​        const user = await User.findOne({
​            where: {
​                id
​            }
​        });
​        if (!user) {
​            throw new global.errs.NotFound('没有找到用户信息');
​        }
​        return user
​    }
​    // 更新用户
​    static async updateUser(id, v) {
​        const user = await User.findByPk(id);
​        if (!user) {
​            throw new global.errs.NotFound('没有找到用户信息');
​        }
​        user.email = v.get('query.email');
​        user.password = v.get('query.password2');
​        user.nickname = v.get('query.nickname');
​        user.save();
​    }

​    static async getUserList(page = 1) {
​        const pageSize = 10;
​        const user = await User.findAndCountAll({
​            limit: pageSize,//每页10条
​            offset: (page - 1) * pageSize,
​            where: {
​                deleted_at: null
​            },
​            order: [
​                ['created_at', 'DESC']
​            ]
​        })
​        return {
​            data: user.rows,
​            meta: {
​                current_page: parseInt(page),
​                per_page: 10,
​                count: user.count,
​                total: user.count,
​                total_pages: Math.ceil(user.count / 10),
​            }
​        };
​    }
}
module.exports = {
​    UserDao
}

前端iview

直接下载iview-admin项目DEMO

# clone the project
git clone https://github.com/iview/iview-admin.git

// install dependencies
npm install

// develop
npm run dev
.
├── config  开发相关配置
├── public  打包所需静态资源
└── src
├── api  AJAX请求
└── assets  项目静态资源
├── icons  自定义图标资源
└── images  图片资源
├── components  业务组件
├── config  项目运行配置
├── directive  自定义指令
├── libs  封装工具函数
├── locale  多语言文件
├── mock  mock模拟数据
├── router  路由配置
├── store  Vuex配置
├── view  页面文件
└── tests  测试相关

效果图

iview-login

菜单修改

默认菜单读取routers.js,可以根据权限组控制,也可以根据权限读取菜单进行加载,菜单里面meta的配置说明如下,因为有些是路由,不显示在菜单里面,比如表单的CRUD操作。

/**
 * iview-admin中meta除了原生参数外可配置的参数:
 * meta: {
 *  title: { String|Number|Function }
 *         显示在侧边栏、面包屑和标签栏的文字
 *         使用'{{ 多语言字段 }}'形式结合多语言使用,例子看多语言的路由配置;
 *         可以传入一个回调函数,参数是当前路由对象,例子看动态路由和带参路由
 *  hideInBread: (false) 设为true后此级路由将不会出现在面包屑中,示例看QQ群路由配置
 *  hideInMenu: (false) 设为true后在左侧菜单不会显示该页面选项
 *  notCache: (false) 设为true后页面在切换标签后不会缓存,如果需要缓存,无需设置这个字段,而且需要设置页面组件name属性和路由配置的name一致
 *  access: (null) 可访问该页面的权限数组,当前路由设置的权限会影响子路由
 *  icon: (-) 该页面在左侧菜单、面包屑和标签导航处显示的图标,如果是自定义图标,需要在图标名称前加下划线'_'
 *  beforeCloseName: (-) 设置该字段,则在关闭当前tab页时会去'@/router/before-close.js'里寻找该字段名对应的方法,作为关闭前的钩子函数
 * }
 */
iview-home

简单构建

登录

将main.js里面的mock注释掉,mock拦截并模拟后台数据

// 实际打包时应该不引入mock
/* eslint-disable */
// if (process.env.NODE_ENV !== 'production') require('@/mock')

config.js配置baseUrl

/**
   \* @description api请求基础路径
   */
  baseUrl: {
​    dev: 'http://localhost:8888/',
​    pro: 'http://localhost:8888/'
  },

jwt获取token

后端创建util.js 创建token

const jwt = require('jsonwebtoken')
const {security} = require('../config/config')
// 颁布令牌
const generateToken = function (uid, scope) {
​    const secretKey = security.secretKey;
​    const expiresIn = security.expiresIn;
​    const token = jwt.sign({
​        uid,
​        scope
​    }, secretKey, {
​        expiresIn: expiresIn
​    })
​    return token
}
module.exports = {
​    generateToken,
}

前端请求过滤,请求加token验证,axios.js修改

if (!config.url.includes('/login')) {
​        // const base64 = Base64.encode(token + ':');
​        config.headers['Authorization'] = 'Basic ' + Base64.encode(Cookies.get(TOKEN_KEY) + ':')
​      }

后端采用basic-auth登录认证,见auth.js

跨域问题

iview前端axios配置,找到axios.js配置文件

 getInsideConfig () {
​    const config = {
​      baseURL: this.baseUrl,
​      changeOrigin: true,
​      headers: {
​        'Content-Type': 'application/json; charset=utf-8',
​        'Access-Control-Allow-Origin': '*'
​      }
​    }
​    return config
  }

后端设置CORS来解决跨域问题,配置app.js,需要安装npm对应的包

const cors = require('@koa/cors');

app.use(cors({
  origin: function (ctx) {
​      // if (ctx.url === '/api') {
​      //     return "*"; // 允许来自所有域名请求
​      // }
​      // return 'http://localhost:8080';
​      return "*"; // 允许来自所有域名请求
  },
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
  maxAge: 5,
  credentials: true,
  allowMethods: ['OPTIONS','GET', 'PUT','POST', 'DELETE'], //设置允许的HTTP请求类型
  allowHeaders: ['Origin', 'Content-Type', 'Accept', 'Access-Control-Allow-Origin', 'Authorization', 'X-Requested-With'],
}));

简单封装

后端-util文件下

auth.js:访问认证

error.js:异常错误封装

help.js:请求封装

util.js:jwt获取token

blog链接:https://dumplingbao.github.io/2019/09/05/iview-koa2/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 一、背景 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 ...
    bayi_lzp阅读 10,478评论 6 26
  • 框架提出的背景 ES6/7带来的变革 自ES6确定和ES7中async/await开始普及,Node的发展变得更加...
    宫若石阅读 8,477评论 1 14
  • koa2,以前没有接触过,只知道是express的原班人马开发的,在一些方面优于express,又经历了一次从ko...
    suchcl阅读 18,663评论 4 22
  • 简介 参考博客: 全栈开发实战:用Vue2+Koa1开发完整的前后端项目(更新Koa2)前置技能: 具备Vue和K...
    Ghamster阅读 8,141评论 1 15
  • 前言 近段时间在学习react,想做个小项目,奈何没有后端,故自己上网查找一些资料,并通过自己的理解搭建后端服务...
    layne1993阅读 8,510评论 0 7