# create-nuxt-app
- 创建一个nuxt
npx create-nuxt-app front
- 创建一个egg后台
npm init egg --type=simple
在package.json中ci命令测试代码覆盖率
"ci": "npm run lint && npm run cov",
# nuxt配置代理 nuxt.config.js
- @nuxtjs/proxy
- proxy对象
module.exports = {
head: {
title: '小开社区',//配置title
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
modules: [
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
'@nuxtjs/proxy'
],
proxy:{
"/api/":{
target:"http://localhost:7001",
secure:false,
pathRewrite:{
'^/api':""
}
}
}
}
# nuxt plugins axios配置
import Vue from 'vue'
import axios from 'axios'
import {MessageBox} from 'element-ui'
let service = axios.create({
timeout:5000,
// 前缀
baseURL:'/api'
})
const TOKEN_KEY = 'KKB_USER_TOKEN'
// @ todo 拦截器 管理token
// redirect:跳转
export default({store, redirect})=>{
// 请求拦截
service.interceptors.request.use(
config=>{
// 请求加token
const token = window.localStorage.getItem(TOKEN_KEY)
// 设置url白名单
if(token){
config.headers.common['Authorization'] = 'Bearer '+token
}
return config
},
err=>{
return Promise.reject(err)
}
)
// 响应拦截
service.interceptors.response.use(
async response=>{
let {data, config} = response
// console.log('响应拦截',response)
// 写token
// 也可以卸载login的逻辑李
if(data.code===0){
if(config.url ==='/api/user/login'){
localStorage.setItem(TOKEN_KEY, data.data.token)
}
}else if(data.code===-666){
// code是-666的 意味着token过期
// @todo
MessageBox.confirm('登录过期了','过期',{
confirmButtonText:'登录',
showCancelButton:false,
type:'warning'
}).then(()=>{
localStorage.removeItem(TOKEN_KEY)
redirect({ path:'/login'})
})
}
return data
},
err=>{
return Promise.reject(err)
}
)
}
// 这里要设置token管理和路由跳转
Vue.prototype.$http =service
export const http = service
# 后台配置的egg相关内容
# egg部分使用插件安装
egg-router-group : 后端路由接口分组
egg-mongoose : 连接mongdb使用
egg-validate : 校验接口数据
md5 : 加密使用
jsonwebtoken : jwt
- app/router.js:定制api名称和请求类型,引入controller具体模块里写的方法
'use strict'
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app
const jwt = app.middleware.jwt({ app })
router.get('/', controller.home.index)
// 验证码
router.get('/captcha', controller.util.captcha)
router.group({ name: 'article', prefix: '/article' }, router => {
const { create, detail, index } = controller.article
router.post('/create', jwt, create)
router.get('/:id', detail)
router.get('/', index)
})
}
- app/controller/base.js:定制规范,创建一个基于egg Controller的基类,便于返回值展示给前端
// 定制规范
const { Controller } = require('egg')
class BaseController extends Controller {
success(data) {
this.ctx.body = {
code: 0,
data,
}
}
message(message) {
this.ctx.body = {
code: 0,
message,
}
}
error(message, code = -1, errors = {}) {
this.ctx.body = {
code,
message,
errors,
}
}
}
module.exports = BaseController
- 其中之一模块:导出具体的方法,在这里实现请求数据,然后给出返回信息。
const BaseController = require('./base')
const marked = require('marked')
class ArticleController extends BaseController {
async index() {
const { ctx } = this
const articles = await ctx.model.Article.find().populate('author').sort({ createdAt:-1})
this.success(articles)
}
async detail() {
// 访问量统计
const { ctx } = this
const { id } = ctx.params
const article = await ctx.model.Article.findOneAndUpdate({ _id: id }, { $inc: { views: 1 } }).populate('author')
this.success(article)
}
async create() {
const { ctx } = this
const { userid } = ctx.state
const { content } = ctx.request.body
const title = content.split('\n').find(v => {
return v.indexOf('# ') === 0
})
const obj = {
title: title.replace('# ', ''),
article: content, // 内部编辑的时候看的
article_html: marked(content), // 给外部显示看的
author: userid,
}
const ret = await ctx.model.Article.create(obj)
if (ret._id) {
this.success({
id: ret.id,
title: ret.title,
})
} else {
this.error('创建失败')
}
}
}
module.exports = ArticleController
# egg配置 csrf jwt
config.default.js,默认security中的csrf是开启的,所以没配token会自动返回403,在项目刚搭建时可以关闭,手动改为false即可。
/* eslint valid-jsdoc: "off" */
'use strict'
const path = require('path')
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = exports = {}
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1584231361040_3227'
config.multipart = {
mode: 'file',
whitelist: () => true,
}
config.UPLOAD_DIR = path.resolve(__dirname, '..', 'app/public')
// add your middleware config here
config.middleware = []
// add your user config here
const userConfig = {
// myAppName: 'egg',
}
return {
...config,
...userConfig,
security: {
csrf: {
enable: false,
},
},
mongoose: {
client: {
url: 'mongodb://127.0.0.1:27017/kkbhub',
options: {},
},
},
jwt: {
secret: '@Kaikeba!123Abc!:',
},
}
}
# egg查询数据库和校验以及组装信息
- egg-validate完成校验
- ctx.validate(createRule)结合校验规则
- ctx.request.body:前端传递的参数在这里可以获取到
- ctx.params.id:params中也可能传递了某些参数
- ctx.model.User => 在Schema中定义好的模型使用
- this可以解构出ctx和app
- 在注册和登录时对密码加盐,提高安全性。
- jwt.sign设token
- app.config.jwt.secret => 在config.default.js中return中进行配置
const md5 = require('md5')
const jwt = require('jsonwebtoken')
const BaseController = require('./base')
const HashSalt = ':Kaikeba@good!@123'
const createRule = {
email: { type: 'email' },
nickname: { type: 'string' },
passwd: { type: 'string' },
captcha: { type: 'string' },
}
class UserController extends BaseController {
async login() {
// this.success('token')
const { ctx, app } = this
const { email, captcha, passwd, emailcode } = ctx.request.body
if (captcha.toUpperCase() !== ctx.session.captcha.toUpperCase()) {
return this.error('验证码错误')
}
if (emailcode !== ctx.session.emailcode) {
return this.error('邮箱验证码错误')
}
const user = await ctx.model.User.findOne({
email,
passwd: md5(passwd + HashSalt),
})
if (!user) {
return this.error('用户名密码错误')
}
// 用户的信息加密成token 返回
const token = jwt.sign({
_id: user._id,
email,
}, app.config.jwt.secret, {
expiresIn: '100h',
})
this.success({ token, email, nickname: user.nickname })
}
async register() {
const { ctx } = this
try {
// 校验传递的参数
ctx.validate(createRule)
} catch (e) {
// console.log(e)
return this.error('参数校验失败', -1, e.errors)
}
const { email, passwd, captcha, nickname } = ctx.request.body
if (captcha.toUpperCase() !== ctx.session.captcha.toUpperCase()) {
return this.error('验证码错误')
}
if (await this.checkEmail(email)) {
this.error('邮箱重复啦')
} else {
const ret = await ctx.model.User.create({
email,
nickname,
passwd: md5(passwd + HashSalt),
})
if (ret._id) {
this.message('注册成功')
}
}
// this.success({name:'kkb'})
}
async checkEmail(email) {
const user = await this.ctx.model.User.findOne({ email })
return user
}
async verify() {
// 校验用户是否存在
}
async detail() {
// 只有token怎么获取详情
const { ctx } = this
const user = await this.checkEmail(ctx.state.email)
this.success(user)
}
async info() {
const { ctx } = this
const { email } = ctx.state
const user = await this.checkEmail(email)
this.success(user)
}
async updateInfo() {
const { ctx } = this
const url = ctx.request.body.url
await ctx.model.User.updateOne(
{ _id: ctx.state.userid },
{ avatar: url }
)
this.success()
}
async isfollow() {
const { ctx } = this
const me = await ctx.model.User.findById(ctx.state.userid)
// 我的follow字段李,有没有传来的这个用户id
const isFollow = !!me.following.find(id => id.toString() === ctx.params.id)
this.success({ isFollow })
}
async follow() {
const { ctx } = this
const me = await ctx.model.User.findById(ctx.state.userid)
const isFollow = !!me.following.find(id => id.toString() === ctx.params.id)
if (!isFollow) {
me.following.push(ctx.params.id)
me.save()
this.message('关注成功')
}
}
async cancelFollow() {
const { ctx } = this
const me = await ctx.model.User.findById(ctx.state.userid)
// 把用户从我的following数组中删掉
const index = me.following.map(id => id.toString()).indexOf(ctx.params.id)
if (index > -1) {
me.following.splice(index, 1)
me.save()
this.message('取消成功')
}
}
async following() {
const { ctx } = this
const users = await ctx.model.User.findById(ctx.params.id).populate('following')
this.success(users.following)
}
async followers() {
const { ctx } = this
const users = await ctx.model.User.find({ following: ctx.params.id })
this.success(users)
}
async likeArticle() {
const { ctx } = this
const me = await ctx.model.User.findById(ctx.state.userid)
if (!me.likeArticle.find(id => id.toString() === ctx.params.id)) {
me.likeArticle.push(ctx.params.id)
me.save()
await ctx.model.Article.findByIdAndUpdate(ctx.params.id, { $inc: { like: 1 } })
return this.message('点赞成功')
}
}
async cancelLikeArticle() {
const { ctx } = this
const me = await ctx.model.User.findById(ctx.state.userid)
const index = me.likeArticle.map(id => id.toString()).indexOf(ctx.params.id)
if (index > -1) {
me.likeArticle.splice(index, 1)
me.save()
await ctx.model.Article.findByIdAndUpdate(ctx.params.id, { $inc: { like: -1 } })
return this.message('取消点赞成功')
}
}
async articleStatus() {
const { ctx } = this
const me = await ctx.model.User.findById(ctx.state.userid)
console.log(me)
const like = !!me.likeArticle.find(id => id.toString() === ctx.params.id)
const dislike = !!me.disLikeArticle.find(id => id.toString() === ctx.params.id)
this.success({
like, dislike,
})
}
}
module.exports = UserController
# model层
- 在app文件夹下创建model文件夹,创建对应的数据库模型(egg-mongoose已经安装)
- timestamps:用来生成新建事件和更新时间
- 返回mongoose.model
- app.mongoose
- const Schema = mongoose.Schema=> new Schema()
某些数据不想被直接查出来,在Schema中的字段上添加select:false
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const UserSchema = new Schema({
__v: { type: Number, select: false },
email: { type: String, required: true },
passwd: { type: String, required: true, select: false },
nickname: { type: String, required: true },
avatar: { type: String, required: false, default: '/user.png' },
following: {
type: [{ type: Schema.Types.ObjectId, ref: 'User' }],
default: [],
},
likeArticle: {
type: [{ type: Schema.Types.ObjectId, ref: 'Article' }],
default: [],
},
disLikeArticle: {
type: [{ type: Schema.Types.ObjectId, ref: 'Article' }],
default: [],
},
}, { timestamps: true })
return mongoose.model('User', UserSchema)
}
# 发送邮件
cnpm i nodemailer --save
- const transporter = nodemailer.createTransport
- transporter.sendMail(mailOptions)
const { Service } = require('egg')
const path = require('path')
const nodemailer = require('nodemailer')
// 发出邮件方
const userEmail = 'shengxinjing@126.com'
// 创建通道
const transporter = nodemailer.createTransport({
service: '126',
//安全链接
secureConnection: true,
auth: {
user: userEmail,
pass: 'a316783812',
},
})
class ToolService extends Service {
async sendMail(email, subject, text, html) {
console.log(email, subject, html)
const mailOptions = {
from: userEmail,
//抄送,规避掉一些邮箱垃圾邮件的规则
cc: userEmail,
to: email,
subject,
text,
html,
}
try {
await transporter.sendMail(mailOptions)
return true
} catch (err) {
console.log('email error', err)
return false
}
}
}
module.exports = ToolService
- 通过邮箱发送验证码
async sendcode() {
const { ctx } = this
const email = ctx.query.email
const code = Math.random().toString().slice(2, 6)
console.log('邮箱' + email + '验证码:' + code)
ctx.session.emailcode = code
const subject = '开课吧验证码'
const text = ''
const html = `<h2>小开社区</h2><a href="https://kaikeba.com"><span>${code}</span></a>`
const hasSend = await this.service.tools.sendMail(email, subject, text, html)
if (hasSend) {
this.message('发送成功')
} else {
this.error('发送失败')
}
}
# 解析token的中间件
cnpm i egg-jwt --save
也可以自己手动去实现,在egg的app文件夹下,创建一个middleware文件夹,创建jwt.js
- jwt.verify => 解析jwt所加密的信息
- 执行中间件返回处理结果
// 解析token的中间件,也可以用egg-jwt,自己封装更适合了解原理
const jwt = require('jsonwebtoken')
module.exports = ({ app }) => {
return async function verify(ctx, next) {
if (!ctx.request.header.authorization) {
ctx.body = {
code: -666,
message: '用户没有登录',
}
return
}
const token = ctx.request.header.authorization.replace('Bearer ', '')
try {
const ret = await jwt.verify(token, app.config.jwt.secret)
ctx.state.email = ret.email
ctx.state.userid = ret._id
await next()
} catch (err) {
console.log(err)
if (err.name === 'TokenExpiredError') {
ctx.body = {
code: -666,
message: '登录过期了',
}
} else {
ctx.body = {
code: -1,
message: '用户信息出错',
}
}
}
}
}
- 在需要加jwt校验的路由中使用
'use strict'
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app
const jwt = app.middleware.jwt({ app })
router.group({ name: 'user', prefix: '/user' }, router => {
const {
info, register
} = controller.user
router.post('/register', register)
//中间件使用
router.get('/info', jwt, info)
})
}
← ssr