抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

概述

来源

该项目是技术胖的一个项目,涉及了前台中台后台,觉得是个不错的练手项目.

概述

博客前台: React Hooks + Next.js + marked + highlight + Ant Design
数据中台: Egg.js(阿里的一个基于 Koa2 的框架) + MySQL
管理后台: React Hooks + Ant Design (不用 Next.js)

Next.js 是一个轻量级的 React 服务端渲染应用框架。有了它我们可以简单轻松的实现 React 的服务端渲染,从而加快首屏打开速度,也可以作 SEO(搜索引擎优化了)。并且路由,webpack 配置等也是框架配置好的。
个人看法:配置太全了,自己写着玩的项目还好,企业级的项目用起来可能不够灵活。

前台

布局

组件内的布局基本都是用 Antd 的 栅格-响应式布局 ,<Col> 的 xs,sm 等这些属性本质上是媒介查询;给 <Row> 添加属性实现 flex 布局 type="flex" justify="center"

组件一般都是用 Antd 的组件组合而成

请求数据

数据来源于请求中台接口

函数组件的 getInitialProps 方法写一个 promise 并返回 promise 的结果(Next.js 框架)

文档
该方法返回的应该是一个普通的 JS 对象;不能用于子组件中,只能用于 pages 页面组件中;该方法只会加载在服务端,所以在该方法中 console 是不会显示的;

该方法用于一开始就请求一次的数据(之后不用请求了),然后 promise 的 resolve 的结果这个对象,会变成函数组件的 props 对象。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数组件
const Home = props => {
const [mylist, setMylist] = useState(props.data)
// ...组件逻辑
}

// 函数组件外
Home.getInitialProps = async () => {
const promise = new Promise(resolve => {
axios(servicePath.getArticleList).then(res => {
resolve(res.data) // resovle()的结果这个对象变成上面的props对象
})
})
return await promise
}

这种写法其实和写在最普通的 useEffect 的写法效果一样

Hook

跨域

中台安装 egg-cors ,并做相应配置。本质是设置 Access-Control-Allow-Origin

跨域白名单

Egg.js 的跨域不支持白名单,只支持单个域名,这样前后台项目不能一起跑。解决方案是需要自己写一个中间件实现白名单,这是别人写的中间件Github,这里直接用了这个中间件

最后线上版本舍弃了这个插件,见博客项目上线部署博文的 跨域

涉及 id 的页面

像不同种类的列表页,文章详情页这种涉及 id 的页面,路由设置 和 数据获取 稍微复杂一些

例如,列表页中每篇文章的标题,是一个带 id 的 <Link>

1
2
3
4
5
<div className="list-title">
<Link href={{ pathname: '/detailed', query: { id: item.id } }}>
<a>{item.title}</a>
</Link>
</div>

点击后跳转带详情页,所以详情页的 getInitialProps 获取数据的时候是根据路由的 id 去 post

1
2
3
4
5
6
7
8
9
Detailed.getInitialProps = async context => {
let id = context.query.id
const promise = new Promise(resolve => {
axios(servicePath.getArticleById + id).then(res => {
resolve(res.data.data[0])
})
})
return await promise
}

对应的接口则是由文章的 id 在数据库查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async getArticleById() {
const id = this.ctx.params.id;
const sql = 'SELECT article.id as id,' +
'article.title as title,' +
'article.introduce as introduce,' +
'article.article_content as article_content,' +
'article.view_count as view_count ,' +
'type.typeName as typeName ,' +
'type.id as typeId ' +
'FROM article LEFT JOIN type ON article.type_id = type.Id ' +
'WHERE article.id=' + id;
const result = await this.app.mysql.query(sql);
this.ctx.body = { data: result };
}

PS:前端在路由跳转的时候(Router.push()<Link href=‘xx’>),路由地址最后得是 xx?id=1 这样,而不是 xx/?id=1。但是后端在配置接口的时候依旧是 xx/:id

详情页 中对 Markdown 语法的解析

把从数据库取出来的 markdown 语法的字符串,编译成正常的文章并显示样式,是用 marked + heighlight.js

详情页 中的 导航目录组件

效果如下所示:

把这个组件固定在页面右侧是用了 Antd 的 Affix 固钉 组件,Anchor 锚点

解析文章内容生成目录是用了阿里的一个插件 tocify.tsx

中台

概况 & RESTful 规范

配置了两套路由,/default/开头的用于前台,/admin/开头的用于后台
所有数据的获得和业务逻辑的操作都是通过中台实现的,也就是说中台只提供接口,这里的设计我们采用 RESTful 的规则,让 egg 为前端提供 Api 接口,实现中台主要的功能

RESTful 是目前最流行的网络应用程序设计风格和开发方式,大量使用在移动端 App 上和前后端分离的接口设计。这种形式更直观并且接口也有了一定的约束性。

约束的请求方式和对应的操作:

  • GET(SELECT) : 从服务端取出资源,可以同时取出一项或者多项。
  • POST(CREATE) :在服务器新建一个资源。
  • PUT(UPDATE) :在服务器更新资源(客户端提供改变后的完整资源)。
  • DELETE(DELETE) :从服务器删除资源。

数据库

数据库采用的是关系型数据库 MySQL,安装了官方带的 WorkBench 对数据库进行可视化管理。把 Egg.js 项目进行连接数据库的配置后即能使用

数据库设计

表 和 Cloum 如下:

  • type

    • id : 类型编号 int 类型
    • typeName: 文章类型名称 varchar 类型
    • orderNum: 类型排序编号 int 类型
  • article

    • id : 文章编号 int 类型
    • type_id : 文章类型编号 int 类型
    • title : 文章标题,varchar 类型
    • article_cointent : 文章主体内容,text 类型, TEXT(n),这里的 n 是文本长度,可以设置一个较大值
    • introduce: 文章简介,text 类型
    • addTime : 文章发布时间,int(11)类型
    • view_count :浏览次数, int 类型
  • admin_user

    • id
    • userName
    • password

数据库语法

参考
W3School
SQL 时间戳日期时间转换

常用例子

  • 增,直接对某个表进行 insert
  • 删,直接根据 id,对某个表进行 delete
  • 改,直接对某个表进行 udate
  • 查,eg:
1
2
'SELECT article.id as id, article.title as title, article.article_content as article_content, FROM_UNIXTIME(article.addTime,'%Y-%m-%d' ) as addTime, type.typeName as typeName, FROM article LEFT JOIN type ON article.type_id = type.Id ORDER BY article.addTime DESC'; // DESC是逆序的意思,ASC是正序`
也可以加`'WHERE type_id= 2` 这样的语法

后台

直接用 React + Antd 写的,不用 Next.js

React 项目中的根目录的 index.js 应该是渲染一个路由组件(自己配的),由这个路由组件再去加载其他页面组件。例如路由组件可以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import Login from './Login'
import AdminIndex from './AdminIndex'

function Main() {
return (
<Router>
<Route path="/login/" exact component={Login} />
<Route path="/index/" component={AdminIndex} />
</Router>
)
}
export default Main

登录页面

样式

样式上比较简单,是由 Antd 的 Card, Input, Icon, Button, Spin, message 组件写的

逻辑

目前没有 注册 这个操作,用户名密码及 id 是采用直接在 workbench 插入新数据的方式注册
点击登录按钮的时候,向接口 post 用户名和密码,向数据库查询这个用户名和密码。如果登录成功就返回登录成功的信息和一个 token,组件就把 token 存到 localstorage 中(localStorage.setItem('openId', res.data.openId)),并跳转到首页(props.history.push('/index')

附上接口代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 判断用户名密码是否正确
async checkLogin() {
const userName = this.ctx.request.body.userName;
const password = this.ctx.request.body.password; // 这里也可以进行加密和解密
const sql = " SELECT userName FROM admin_user WHERE userName = '" + userName + "' AND password = '" + password + "'";

const res = await this.app.mysql.query(sql);
if (res.length > 0) {
// 登录成功,进行session缓存
const openId = new Date().getTime(); // 传给前端的token,用当前时间的时间戳做token
this.ctx.session.openId = { openId }; // 并且把这个token放到session中
this.ctx.body = { data: '登录成功', openId };
} else {
this.ctx.body = { data: '登录失败' };
}
}

路由守卫

登录后,我们生成了 session,通过后台是不是存在对应的 session,作一个中台的路由守卫。如果没有登录,是不允许访问后台对应的接口,也没办法作对应的操作。这样就实现了接口的安全
其实就是路由守卫,没有 token 的时候不让访问接口(等同于不让访问某些页面)。在这里我们是通过 egg.js 的中间件实现的(详情版见教程)

  • 写一个中间件文件,并应用在中台路由中 (目前只有getTypeInfo这个接口有守卫
1
2
3
4
5
6
7
8
9
10
module.exports = options => {
return async function adminauth(ctx, next) {
console.log(ctx.session.openId)
if (ctx.session.openId) {
await next()
} else {
ctx.body = { data: '没有登录' }
}
}
}
1
2
// 中台路由:对某个接口进行路由守卫
router.get('/admin/getTypeInfo', adminauth, controller.admin.main.getTypeInfo)
  • 在正常情况下前后台是不能共享session的,需要要在 egg 端的/config/config.default.js里的config.cor配置项增加credentials:true;并且前台的请求中,需要带 withCredentials: true

管理页

布局上用 Antd 的 layout-侧边布局。再做相应的路由配置就能分页

管理页之添加文章

直接在 编辑文章 分页进行编辑并保存
前端:把文章的数据都打包好之后 POST 给后端接口
后端:数据库对表进行插入数据的操作。id 方面是先找到最大 id,然后让新文章的 id 等于最大值+1

管理页之修改文章

在 文章列表 分页,点击某篇文章的 修改 按钮,就会进行路由跳转到 编辑文章 分页并附上文章 id 保存在路由中

所以在 编辑文章 分页还有这样一个逻辑:有一个getArticleById方法,会根据 id 向接口请求对应的文章的数据并显示在页面上,这个方法会在 useEffect 中执行让它在页面刚刷新的时候就执行(如果路由有带 id 的话)。此外,改页面的 发布文章 这个按钮也会判断是否有 id,无的话就是请求发布文章的接口,有的话就是请求更新文章的接口

项目优化计划

前台

  • markdown 语法中图片的解析和存储
    markdown 语法中图片是直接写成 ![](/img/二叉树的遍历/二叉树的遍历_1.png) 这种形式,完全不做配置的话会出现下图这种 404 的情况
  • 文章简介的实现
  • 访问人数的实现。目前的访问人数是随机生成的
  • UI 美化

后台

  • 登录账号和密码加密
  • 实现手机号注册?或者说这种管理中台一般是管理者才能使用,管理者账号直接写在数据库就行了,不必要有注册功能
  • 页面 UI 优化,使该系统美观一些
  • 添加文章能插入图片

上线及部署

见另一篇文章

参考资料
技术胖博客

评论