Giter Site home page Giter Site logo

alvin0216 / react-blog Goto Github PK

View Code? Open in Web Editor NEW
729.0 12.0 194.0 5.98 MB

react hooks + koa2 + sequelize + mysql 构建的个人博客。具备评论、通知、上传文章等等功能

License: MIT License

JavaScript 78.87% HTML 6.06% CSS 6.25% Less 8.81%
react koa2 mysql sequelize react-router-v4 redux jwt bycrypt create-react-app react-hooks

react-blog's Introduction

声明

本项目已不再维护,项目已经升级到 ssr 版本,沿用并改进了 UI。新项目地址 remix-ssr-blog。欢迎继续关注!

react hooks + koa + mysql

一个及其简洁的个人博客系统、即插即用,如果你想使用这个博客、动动手改改配置即可使用!!

  • 前后台分离式开发(项目中也包含博客的后台管理系统),为了方便记录后端开发过程,笔者将后端也一起放在同个项目文件夹中。
  • 博客样式几乎借助于 antd 这个优秀的 UI 框架,主打简约风格,是笔者借鉴了 antd 官方的风格所设计。
  • 具备了代码高亮、权限管理、第三方 github 登录、评论与通知、以及邮件通知功能的个人博客...
  • 具备文件导入导出功能,假如你之前用 hexo 博客, 那么你可以直接通过导入 md 文件迁移你的文章。

声明:博客仅做展示使用,(之前被比特币攻击了),所需数据已重置。

MIT Licence LICENSE 996.icu

实现功能

  • 前台:主页 + 列表页 + 搜索页 + 分类页 + 标签页
  • 后台:文章管理 + 用户管理
  • 响应式、文章锚点导航、回到顶部、markdown 代码高亮
  • 用户:站内用户、github 第三方授权登录的用户
  • 用户可以评论与回复、以及邮件通知回复的状态
  • md 文件导入导出功能!可以直接上传 md 文件生成文章

技术栈

  • 前端 (基于 create-react-app eject 后的配置)

    • react v16.9.0 hooks + redux + react-router4
    • marked highlight.js
    • webpack 打包优化
    • axios 封装
  • 后端 (自构建后台项目)

    • koa2 + koa-router
    • sequelize + mysql
    • jwt + bcrypt
    • nodemailer
    • koa-send archiver

博客预览

pc 端

移动端

项目结构

目录结构

.

├─config                // 构建配置
├─public                // html 入口
├─scripts               // 项目脚本
└─server                // 后端
    ├─config            // 项目配置 github、email、database、token-secret 等等
    ├─controllers       // 业务控制层
    ├─middlewares       // 中间件
    ├─models            // 数据库模型
    ├─router            // 路由
    ├─utils             // 工具包
    ├─  app.js          // 后端主入口文件
    ├─  initData.js     // 初始化基础数据脚本
    └─...

└─src                   // 前端项目源码
   ├─assets             // 静态文件
   ├─components         // 公用组件
   ├─layout             // 布局组件
   ├─redux              // redux 目录
   ├─routes             // 路由
   ├─styles             // 样式
   ├─utils              // 工具包
   ├─views              // 视图层
   ├─  App.jsx          // 后端主入口文件
   ├─  config.js        // 项目配置 github 个人主页、个人介绍等等
   ├─  index.js         // 主入口文件
   └─...

数据库模型

role === 1: 博主用户 role === 2: 普通用户

权限管理 server/middlewares/authHandler.js

const { checkToken } = require('../utils/token')

/**
 * role === 1 需要权限的路由
 * @required 'all': get post put delete 均需要权限。
 */
const verifyList1 = [
  { regexp: /\/article\/output/, required: 'get', verifyTokenBy: 'url' }, // 导出文章 verifyTokenBy 从哪里验证 token
  { regexp: /\/article/, required: 'post, put, delete' }, // 普通用户 禁止修改或者删除、添加文章
  { regexp: /\/discuss/, required: 'delete, post' }, // 普通用户 禁止删除评论
  { regexp: /\/user/, required: 'get, put, delete' }, // 普通用户 禁止获取用户、修改用户、以及删除用户
]

// role === 2 需要权限的路由
const verifyList2 = [
  { regexp: /\/discuss/, required: 'post' }, // 未登录用户 禁止评论
]

/**
 * 检查路由是否需要权限,返回一个权限列表
 *
 * @return {Array} 返回 roleList
 */
function checkAuth(method, url) {
  function _verify(list, role) {
    const target = list.find((v) => {
      return v.regexp.test(url) && (v.required === 'all' || v.required.toUpperCase().includes(method))
    })

    return target
  }

  const roleList = []
  const result1 = _verify(verifyList1)
  const result2 = _verify(verifyList2)

  result1 && roleList.push({ role: 1, verifyTokenBy: result1.verifyTokenBy || 'headers' })
  result2 && roleList.push({ role: 2, verifyTokenBy: result1.verifyTokenBy || 'headers' })

  return roleList
}

// auth example token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoyLCJpZCI6MSwiaWF0IjoxNTY3MDcyOTE4LCJleHAiOjE1Njk2NjQ5MTh9.-V71bEfuUczUt_TgK0AWUJTbAZhDAN5wAv8RjmxfDKI
module.exports = async (ctx, next) => {
  const roleList = checkAuth(ctx.method, ctx.url)
  //  该路由需要验证
  if (roleList.length > 0) {
    if (checkToken(ctx, roleList)) {
      await next()
    } else {
      ctx.status = 401
      ctx.client(401)
    }
  } else {
    await next()
  }
}

关于使用这个项目需要的配置

前端 src/config.js

import React from 'react'
import MyInfo from '@/views/web/about/MyInfo'

// API_BASE_URL
export const API_BASE_URL = 'http://127.0.0.1:6060'

// project config
export const HEADER_BLOG_NAME = '郭大大的博客' // header title 显示的名字

// === sidebar
export const SIDEBAR = {
  avatar: require('@/assets/images/avatar.jpeg'), // 侧边栏头像
  title: '郭大大', // 标题
  subTitle: '前端打杂人员,略微代码洁癖', // 子标题
  // 个人主页
  homepages: {
    github: 'https://github.com/gershonv',
    juejin: 'https://juejin.im/user/5acac6c4f265da2378408f92',
  },
}

// === discuss avatar
export const DISCUSS_AVATAR = SIDEBAR.avatar // 评论框博主头像

// github
export const GITHUB = {
  enable: true, // github 第三方授权开关
  client_id: '', // Setting > Developer setting > OAuth applications => client_id
  url: 'https://github.com/login/oauth/authorize', // 跳转的登录的地址
}

export const ABOUT = {
  avatar: SIDEBAR.avatar,
  describe: SIDEBAR.subTitle,
  discuss: true, // 关于页面是否开启讨论
  renderMyInfo: <MyInfo />, // 我的介绍 自定义组件 => src/views/web/about/MyInfo.jsx
}

后端 server/config.js

const devMode = process.env.NODE_ENV === 'development'

const config = {
  PORT: 6060, // 启动端口
  ADMIN_GITHUB_LOGIN_NAME: 'gershonv', // 博主的 github 登录的账户名 user
  GITHUB: {
    client_id: 'c6a96a84105bb0be1fe5',
    client_secret: '',
    access_token_url: 'https://github.com/login/oauth/access_token',
    fetch_user_url: 'https://api.github.com/user', // 用于 oauth2
    fetch_user: 'https://api.github.com/users/', // fetch user url https://api.github.com/users/gershonv
  },
  EMAIL_NOTICE: {
    // 邮件通知服务
    // detail: https://nodemailer.com/
    enable: true, // 开关
    transporterConfig: {
      host: 'smtp.163.com',
      port: 465,
      secure: true, // true for 465, false for other ports
      auth: {
        user: '[email protected]', // generated ethereal user
        pass: '123456', // generated ethereal password 授权码 而非 密码
      },
    },
    subject: '郭大大的博客 - 您的评论获得新的回复!', // 主题
    text: '您的评论获得新的回复!',
    WEB_HOST: 'http://127.0.0.1:3000', // email callback url
  },
  TOKEN: {
    secret: 'guo-test', // secret is very important!
    expiresIn: '720h', // token 有效期
  },
  DATABASE: {
    database: 'test',
    user: 'root',
    password: '123456',
    options: {
      host: 'localhost', // 连接的 host 地址
      dialect: 'mysql', // 连接到 mysql
      pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000,
      },
      define: {
        timestamps: false, // 默认不加时间戳
        freezeTableName: true, // 表名默认不加 s
      },
      timezone: '+08:00',
    },
  },
}

// 部署的环境变量设置
if (!devMode) {
  console.log('env production....')

  // ==== 配置数据库
  config.DATABASE = {
    ...config.DATABASE,
    database: '', // 数据库名
    user: '', // 账号
    password: '', // 密码
  }

  // 配置 github 授权
  config.GITHUB.client_id = ''
  config.GITHUB.client_secret = ''

  // ==== 配置 token 密钥
  config.TOKEN.secret = ''

  // ==== 配置邮箱

  // config.EMAIL_NOTICE.enable = true
  config.EMAIL_NOTICE.transporterConfig.auth = {
    user: '[email protected]', // generated ethereal user
    pass: '123456XXX', // generated ethereal password 授权码 而非 密码
  }
  config.EMAIL_NOTICE.WEB_HOST = 'https://guodada.fun'
}

module.exports = config

关于 github 第三方授权和 email 授权,可以参考

使用这个项目

git clone https://github.com/gershonv/react-blog.git

## 安装依赖以及开启开发模式
cd react-blog
yarn
yarn dev

## 安装依赖以及开启开发模式 注意必须先配置好数据库、个人github账户登录名,配置文件在 server/config/index.js
## 笔者采用的数据库字符集为 utf8mb4 排序规则 utf8mb4_general_ci
cd server
yarn
yarn dev


## 打包前端
cd react-blog
yarn build

## 后端笔者则是采用pm2
cd server
pm2 start app.js

导入功能说明

导入 md 文件是按照 hexo 生成的前缀去解析的, 比如

---
title: ES6 - Class
date: 2018-07-16 22:19:09
categories: Javascript
tags:
  - Javascript
  - ES6
---

对应会解析为

  • 标题:ES6 - Class
  • 创建日期:2018-07-16 22:19:09
  • 分类:Javascript
  • 标签:Javascript ES6

如果导入标题一样的文件,可以确认是否覆盖原来的文章!

PS : 觉得不错的伙伴可以给个 star ~~~ 或者 fork 下来看看哦。如果有什么建议,也可以提 issue 哦

react-blog's People

Contributors

alvin0216 avatar crazylxr avatar dependabot[bot] avatar edwardwang0302 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-blog's Issues

大佬可以讲一下思路吗?

研究源码,感觉就是不知从何处下手。大佬可以简单讲一下后台项目的思路吗?比如先写哪个模块,后写哪个模块。谢谢!

useMount

请问,这个useMount自定义的hook的用处是什么,我水平不够,不知道有什么意义
`
import { useEffect } from 'react'

export default function useMount(func) {
useEffect(() => {
typeof func === 'function' && func()
}, [])
}
`

打扰一下

你好 非常开心看到你的开源项目。我们团队最近在做一个和信息平台相关的项目(目前在北京和北美注册的公司)。我们正好缺具有react开发经验的小伙伴(可以通过多种方式加入,未必全职),如果你感兴趣加入我们,想进一步了解我们的团队。可以添加我的微信 SJHBXS

这个是什么原因呢?

http://127.0.0.1:6060/article/list

{
stack: "SequelizeDatabaseError: Unknown column 'tags.articleId' in 'on clause' at Query.formatError (F:\github\react-blog\server\node_modules\sequelize\lib\dialects\mysql\query.js:244:16) at Query.handler [as onResult] (F:\github\react-blog\server\node_modules\sequelize\lib\dialects\mysql\query.js:51:23) at Query.execute (F:\github\react-blog\server\node_modules\mysql2\lib\commands\command.js:30:14) at Connection.handlePacket (F:\github\react-blog\server\node_modules\mysql2\lib\connection.js:408:32) at PacketParser.Connection.packetParser.p [as onPacket] (F:\github\react-blog\server\node_modules\mysql2\lib\connection.js:70:12) at PacketParser.executeStart (F:\github\react-blog\server\node_modules\mysql2\lib\packet_parser.js:75:16) at Socket.Connection.stream.on.data (F:\github\react-blog\server\node_modules\mysql2\lib\connection.js:77:25) at emitOne (events.js:115:13) at Socket.emit (events.js:210:7) at addChunk (_stream_readable.js:266:12) at readableAddChunk (_stream_readable.js:253:11) at Socket.Readable.push (_stream_readable.js:211:10) at TCP.onread (net.js:587:20) From previous event: at Query.run (F:\github\react-blog\server\node_modules\sequelize\lib\dialects\mysql\query.js:39:12) at runHooks.then.then (F:\github\react-blog\server\node_modules\sequelize\lib\sequelize.js:645:29) From previous event: at Promise.try.then.connection (F:\github\react-blog\server\node_modules\sequelize\lib\sequelize.js:645:12) From previous event: at Promise.resolve.retry (F:\github\react-blog\server\node_modules\sequelize\lib\sequelize.js:641:10) at F:\github\react-blog\server\node_modules\retry-as-promised\index.js:70:21 at Promise () at retryAsPromised (F:\github\react-blog\server\node_modules\retry-as-promised\index.js:60:10) at Promise.try (F:\github\react-blog\server\node_modules\sequelize\lib\sequelize.js:631:30) From previous event: at Sequelize.query (F:\github\react-blog\server\node_modules\sequelize\lib\sequelize.js:580:23) at QueryInterface.rawSelect (F:\github\react-blog\server\node_modules\sequelize\lib\query-interface.js:1163:27) at Function.aggregate (F:\github\react-blog\server\node_modules\sequelize\lib\model.js:1993:32) at Promise.try.then (F:\github\react-blog\server\node_modules\sequelize\lib\model.js:2045:19) at runCallback (timers.js:781:20) at tryOnImmediate (timers.js:743:5) at processImmediate [as _immediateCallback] (timers.js:714:5) From previous event: at Function.count (F:\github\react-blog\server\node_modules\sequelize\lib\model.js:2029:8) at Function.findAndCountAll (F:\github\react-blog\server\node_modules\sequelize\lib\model.js:2097:12) at getList (F:\github\react-blog\server\controllers\article.js:129:39) at dispatch (F:\github\react-blog\server\node_modules\koa-compose\index.js:44:32) at next (F:\github\react-blog\server\node_modules\koa-compose\index.js:45:18) at F:\github\react-blog\server\node_modules\koa-router\lib\router.js:346:16 at dispatch (F:\github\react-blog\server\node_modules\koa-compose\index.js:44:32) at F:\github\react-blog\server\node_modules\koa-compose\index.js:36:12 at dispatch (F:\github\react-blog\server\node_modules\koa-router\lib\router.js:351:31) at dispatch (F:\github\react-blog\server\node_modules\koa\node_modules\koa-compose\index.js:42:32) at logger (F:\github\react-blog\server\node_modules\koa-logger\index.js:67:13) at dispatch (F:\github\react-blog\server\node_modules\koa\node_modules\koa-compose\index.js:42:32) at module.exports (F:\github\react-blog\server\middlewares\authHandler.js:56:11) at dispatch (F:\github\react-blog\server\node_modules\koa\node_modules\koa-compose\index.js:42:32) at jsonError (F:\github\react-blog\server\node_modules\koa-json-error\lib\middleware.js:49:12) at dispatch (F:\github\react-blog\server\node_modules\koa\node_modules\koa-compose\index.js:42:32) at F:\github\react-blog\server\node_modules\koa-body\index.js:148:14 at at process._tickCallback (internal/process/next_tick.js:188:7)",
name: "SequelizeDatabaseError",
parent: {
code: "ER_BAD_FIELD_ERROR",
errno: 1054,
sqlState: "42S22",
sqlMessage: "Unknown column 'tags.articleId' in 'on clause'",
sql: "SELECT count(DISTINCT(article.id)) AS count FROM article AS article LEFT OUTER JOIN tag AS tags ON article.id = tags.articleId LEFT OUTER JOIN category AS categories ON article.id = categories.articleId LEFT OUTER JOIN comment AS comments ON article.id = comments.articleId LEFT OUTER JOIN reply AS comments->replies ON comments.id = comments->replies.commentId WHERE article.id != -1 AND (article.title LIKE '%%' OR article.content LIKE '%%');"
},
original: {
code: "ER_BAD_FIELD_ERROR",
errno: 1054,
sqlState: "42S22",
sqlMessage: "Unknown column 'tags.articleId' in 'on clause'",
sql: "SELECT count(DISTINCT(article.id)) AS count FROM article AS article LEFT OUTER JOIN tag AS tags ON article.id = tags.articleId LEFT OUTER JOIN category AS categories ON article.id = categories.articleId LEFT OUTER JOIN comment AS comments ON article.id = comments.articleId LEFT OUTER JOIN reply AS comments->replies ON comments.id = comments->replies.commentId WHERE article.id != -1 AND (article.title LIKE '%%' OR article.content LIKE '%%');"
},
sql: "SELECT count(DISTINCT(article.id)) AS count FROM article AS article LEFT OUTER JOIN tag AS tags ON article.id = tags.articleId LEFT OUTER JOIN category AS categories ON article.id = categories.articleId LEFT OUTER JOIN comment AS comments ON article.id = comments.articleId LEFT OUTER JOIN reply AS comments->replies ON comments.id = comments->replies.commentId WHERE article.id != -1 AND (article.title LIKE '%%' OR article.content LIKE '%%');",
message: "Unknown column 'tags.articleId' in 'on clause'",
status: 500
}

源码解读不解的地方

/**

  • 获取 elements 的最后一个 index (用于决定 this.props.children 的渲染)
  • @param {Boolean} dynamic - 是否为动态表单
  • @param {Array} elements
    */
    function getLastIndex(dynamic = false, elements) {
    if (!dynamic) return elements.length - 1
    const els = elements.filter(v => !!v)
    return els.length - 1
    }

请问在dynamic == true时filter有何意义?

hljs is not defined

最近在报错 hljs is not defined 能处理这个问题吗?
好像是CDN失效了

svg文件使用

大佬,能解答下,项目中svg文件的引入步骤吗,在配置中没有找到呢

请问如何通过Tag模糊查询获取所有包含该标签的文章列表

例如: 用户按标签查询,输入关键字 Javascript 获取如下结果


[
  {
    createdAt: "2020-01-14 15:51:24",
    updatedAt: "2020-04-13 14:35:55",
    id: 87,
    title: " React Hooks 笔记",
    content: "content值",
    viewCount: 66,
    tags: [{name: "Javascript", name: "react"}],
    categories: [{name: "Javascript"}],
    comments: []
  },
  {
    createdAt: "2020-01-14 15:51:24",
    updatedAt: "2020-04-13 14:35:55",
    id: 88,
    title: " React Hooks 笔记1",
    content: "content值",
    viewCount: 66,
    tags: [{name: "Javascript", name: "vue"}],
    categories: [{name: "Javascript"}],
    comments: []
  }
]
const { tag_name } = ctx.query
.....
const data = await ArticleModel.findAndCountAll({
   这个查询sql 该怎么写
})

怎么部署到服务器

打包后无法访问文章管理页面啊,刷新页面404,还有你这后台怎么部署?能说一下吗?新手

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.