前言
纸上学来终觉浅,绝知此事要躬行。经过一段时间 React
和 Egg.js
的学习,就打算动手写一个功能完善的古诗文app来实战一波,希望本文可以帮助到一些刚刚入门前端的同学,一起来看看吧👀👀👀。
成果预览
gif成果图
项目简介
- 技术栈:
React Hooks
+Ant Design Mobile
+Redux
+Egg.js
+Mysql
- 使用
React Hooks
对前端页面的编写 - 使用
Redux
进行全局数据流管理 - 使用
styled-components
样式组件进行样式编写 - 使用
React-Router V6
进行路由配置编写 - 贯彻前端
MVVM
的设计理念,遵循 组件化、模块化 编程思想 - 后端使用
node
的企业级框架Egg.js
编写后端接口 - 使用
Mysql
存储二十多万条古诗文数据,以及用户信息数据
前端部分
项目前端结构
├─ node_modules // 依赖包
├─ public // 项目使用的公共文件
favicon.ico // 网站标签图标
index.html // 首页的模板文件
manifest.json // 移动端配置文件
├─ src
├─ application // 四个主页面
├─ assets // 字体配置及图片
├─ components // 其他子页面及组件
├─ config // 请求路径配置
├─ routes // 路由配置文件
├─ store // redux 相关文件
App.js // 根组件
index.js // 项目的入口文件
style.js // 全局样式
.babelrc // babel配置文件
.gitignore // 上传Git需要忽略的文件
package-lock.json // 锁定安装时的包的版本号
package-json // 项目及模块包的描述
README.md // 项目介绍
yarn.lock // 锁定安装时的包的版本号
路由配置
本项目较为简单就直接使用react-router-dom
配置,若项目复杂可以使用react-router
的useRouter
front-end/src/App.js
代码如下:
<Routes>
<Route path="/*" element={<ButtomTab />} ></Route> {/*tabbar界面(整个app的界面:tabbar及四个子路由)*/}
<Route path="/login" element={<Login />} ></Route> {/*登录界面*/}
<Route path="/register" element={<Register />} ></Route> {/*注册界面*/}
<Route path="/index/my/myDetail" element={<MyDetail />} ></Route> {/*个人信息详情界面*/}
<Route path="/index/index/poemDetail/:id" element={<PoemDetail />} ></Route> {/*诗词详情界面*/}
<Route path="/index/index/search" element={<Search />} ></Route> {/*搜索详情界面*/}
<Route path="/index/index/search/searchPoemDetail/:id" element={<SearchPoemDetail />} ></Route> {/*搜索诗词详情界面*/}
<Route path="/index/index/sort/sortDetail/:type/:id" element={<SortPoemDetail />} ></Route> {/*分类诗词详情界面*/}
<Route path="/index/sort/sortDetail/:type" element={<SortDetail />} ></Route> {/*分类详情界面*/}
<Route path="/index/find/findDetail" element={<FindDetail />} ></Route> {/*发现详情界面*/}
<Route path="/index/my/myBrowsing" element={<MyBrowsing />} ></Route> {/*我的浏览历史界面*/}
<Route path="/index/my/myBrowsing/onePoemDetail/:name" element={<OnePoemDetail />} ></Route> {/*我的浏览历史界面*/}
<Route path="/index/my/myCollection" element={<MyCollection />} ></Route> {/*我的收藏界面*/}
<Route path="/index/my/myPoemList" element={<MyPoemList />} ></Route> {/*我的诗单界面*/}
<Route path="/index/my/myPoemList/addMyPoemList" element={<AddMyPoemList />} ></Route> {/*添加我的诗单界面*/}
<Route path="/index/my/myPoemList/updateMyPoemList/:title/:listid" element={<UpdateMyPoemList />} ></Route> {/*修改我的诗单界面*/}
<Route path="/index/my/myRecitation" element={<MyRecitation />} ></Route> {/*我的背诵界面*/}
<Route path="/index/my/myDetail/updateHeadPic" element={<UpdateHeadPic />} ></Route> {/*修改头像界面*/}
<Route path="/index/my/myDetail/updateUserName" element={<UpdateUserName />} ></Route> {/*修改用户名界面*/}
<Route path="/index/my/myDetail/updatePassword" element={<UpdatePassword />} ></Route> {/*修改密码界面*/}
<Route path="/index/my/myDetail/updateSex" element={<UpdateSex />} ></Route> {/*修改性别界面*/}
<Route path="/index/my/myDetail/updatePersonalizedSig" element={<UpdatePersonalizedSig />} ></Route> {/*修改个性签名界面*/}
<Route path="/index/my/myDetail/updateBrith" element={<UpdateBrith />} ></Route> {/*修改生日界面*/}
</Routes>
数据流管理 Redux
action
代码如下
const action = {
type: 'changeSearchPoemList',
value: res.data.res,
}
store.dispatch(action)
store/index.js
代码如下
import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) // 创建数据存储仓库
export default store
store/reducer.js
代码如下
const defaultState = { // 默认数据
itemCount: 0,
poemList: [[], [], [], [], [], [], [], [], [], [], [], [], []],
searchPoemList: [],
key: '2',
index: 0,
feihualing:[]
}
export default (state = defaultState, action) => { // 就是一个方法函数
if (action.type === 'changePoemList') {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.poemList = action.value
newState.itemCount = action.itemCount
newState.key = action.key
return newState
}
if (action.type === 'changeSearchPoemList') {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.searchPoemList = action.value
return newState
}
if (action.type === 'changeIndex') {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.index = action.index
return newState
}
if (action.type === 'changeFeiHuaLing') {
let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
newState.feihualing = action.feihualing
return newState
}
return state
}
页面编写(简介四个主页面)
首页页面
- 首页页面结构
<div className='Container '>
<List> // 搜索框
<List.Item>
<SearchBar placeholder='搜索' onFocus={goSearch} />
</List.Item>
</List>
<JumboTabs defaultActiveKey={dataInit.key} onChange={queryPoem}> // 导航栏
<JumboTabs.Tab title='足迹' description='' key='1'> // 导航栏每个tab对应的内容
<PullToRefresh
onRefresh={async () => {
}}
>
{poem.map((item, index) => { // 循环输出每个tab对应的自定义组件PoemCard
// {console.log(item);}
return <PoemCard key={index} id={index} title={item.title} poet={item.poet} content={item.content} isShowPop={isShowPop} />
})}
{/* <InfiniteScroll loadMore={loadMore} hasMore={hasMore} /> */}
</PullToRefresh>
</JumboTabs.Tab>
... // 此处省略了其他JumboTabs.Tab
</JumboTabs>
分类页面
- 分类页面结构
<div>
<Tabs>
<Tabs.Tab title='诗文' key='1'>
<div style={{ height: window.innerHeight }}>
<IndexBar>
{groups.map(group => {
const { title, items } = group
return (
<IndexBar.Panel
index={title}
title={`${title}`}
key={`标题${title}`}
>
<Grid columns={4} gap={18}>
{items.map((item, index) => (
<Grid.Item key={index} onClick={(e) => { goSortDetail(e) }}>
<div className='grid-demo-item-block'>{item.name}</div>
</Grid.Item>
))}
</Grid>
</IndexBar.Panel>
)
})}
</IndexBar>
</div>
</Tabs.Tab>
<Tabs.Tab title='名句' key='2'>
诗文
</Tabs.Tab>
<Tabs.Tab title='古籍' key='3'>
古籍
</Tabs.Tab>
<Tabs.Tab title='作者' key='4'>
<div style={{ height: window.innerHeight }}>
<IndexBar>
{groups2.map(group => {
console.log('group is:', group);
const { title, items } = group
console.log('items is:', items)
return (
<IndexBar.Panel
index={title}
title={`${title}`}
key={`标题${title}`}
>
<Grid columns={4} gap={18}>
{items.map((item, index) => (
<Grid.Item key={index} onClick={(e) => { goSortDetail(e) }}>
<div className='grid-demo-item-block'>{item.name}</div>
</Grid.Item>
))}
</Grid>
</IndexBar.Panel>
)
})}
</IndexBar>
</div>
</Tabs.Tab>
</Tabs>
</div>
发现页面
- 分类页面结构
<div>
<Swiper autoplay>{items}</Swiper>
<Divider />
<Grid columns={2} gap={8}>
<Grid.Item onClick={goFindDetail}>
<div className='grid-demo-item-block find-grid ' style={{ 'backgroundColor': '#fbf7ee' }}>飞花令</div>
</Grid.Item>
<Grid.Item onClick={goFindDetail}>
<div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#fef2ef' }}>诗词接龙</div>
</Grid.Item>
<Grid.Item onClick={goFindDetail}>
<div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#f2fdee' }}>成语接龙</div>
</Grid.Item>
<Grid.Item>
<div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#f6effc' }}>考一考</div>
</Grid.Item>
<Grid.Item>
<div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#fff' }}>敬请期待</div>
</Grid.Item>
</Grid>
</div>
我的页面
- 我的页面结构
<div>
{userName === null
?
<div className="">
<List>
<Button block color='primary' size='large' onClick={goLogin}>未登录,去登录</Button>
</List>
<List header=' '>
<List.Item prefix={<EyeInvisibleOutline />} onClick={goMyBrowsing}>
我的浏览
</List.Item>
<List.Item prefix={<StarOutline />} onClick={goMyCollection}>
我的收藏
</List.Item>
<List.Item prefix={<FileOutline />} onClick={goMyPoemList}>
我的诗单
</List.Item>
<List.Item prefix={<ContentOutline />} onClick={goMyRecitation}>
我的背诵
</List.Item>
<List.Item prefix={<FillinOutline />} onClick={() => { }}>
我的标注
</List.Item>
</List>
<List header=' '>
<List.Item prefix={<EyeOutline />} onClick={() => { }}>
护眼模式
</List.Item>
<List.Item prefix={<AntOutline />} onClick={() => { }}>
深色模式
</List.Item>
<List.Item prefix={<CollectMoneyOutline />} onClick={() => { }}>
列表模式
</List.Item>
<List.Item prefix={<EditSOutline />} onClick={() => { }}>
字体选择
</List.Item>
<List.Item prefix={<SetOutline />} onClick={() => { }}>
更多设置
</List.Item>
</List>
</div>
: <div className="">
<List>
<List.Item extra='主页' onClick={goMyDetail} >
<div className="myInfo">
<div className=""><Avatar size={80} src={headPicPath} className='myPic' /></div>
<div className="myName">
<p className='myName-userName'>用户:{userName}</p>
<p className='myName-personalizedSig'>个性签名:{personalizedSig}</p>
</div>
</div>
</List.Item>
</List>
<List header=' '>
<List.Item prefix={<EyeInvisibleOutline />} onClick={goMyBrowsing}>
我的浏览
</List.Item>
<List.Item prefix={<StarOutline />} onClick={goMyCollection}>
我的收藏
</List.Item>
<List.Item prefix={<FileOutline />} onClick={goMyPoemList}>
我的诗单
</List.Item>
<List.Item prefix={<ContentOutline />} onClick={goMyRecitation}>
我的背诵
</List.Item>
<List.Item prefix={<FillinOutline />} onClick={() => { }}>
我的标注
</List.Item>
</List>
<List header=' '>
<List.Item prefix={<EyeOutline />} onClick={() => { }}>
护眼模式
</List.Item>
<List.Item prefix={<AntOutline />} onClick={() => { }}>
深色模式
</List.Item>
<List.Item prefix={<CollectMoneyOutline />} onClick={() => { }}>
列表模式
</List.Item>
<List.Item prefix={<EditSOutline />} onClick={() => { }}>
字体选择
</List.Item>
<List.Item prefix={<SetOutline />} onClick={() => { }}>
更多设置
</List.Item>
</List>
</div>
}
</div>
后端部分
项目后端结构
├─ node_modules // 依赖包
├─ app
├─ controller // 控制层
├─ middleware // 中间件
├─ public/upload // 上传图片保存
├─ config // 请求路径配置
router.js // 路由接口配置文件
├─ config
config.default.js// 插件配置文件
plugin.js // 插件引入文件
├─ test/app/controller// 测试相关文件
.autod.conf.js
.eslintignore
.eslintrc
.gitignore // 上传Git需要忽略的文件
.travis.yml
README.md // 项目介绍
appveyor.yml
jsconfig.json
package-json // 项目及模块包的描述
数据库sql数据
数据库连接及跨域配置
- 导出插件
/back-end/config/plugin.js
module.exports.mysql = { // mysql插件
enable: true,
package: 'egg-mysql'
}
module.exports.cors = { // cors插件
enable: true,
package: 'egg-cors'
}
- 配置插件
back-end/config/config.default.js
config.mysql = { // 配置mysql数据库
// database configuration
// client: {
// // host
// host: '127.0.0.1',
// // port
// port: '3306',
// // username
// user: 'root',
// // password
// password: '',
// // database
// database: 'ancientpoems',
// },
client: {
// host
host: '127.0.0.1',
// port
port: '3306',
// username
user: 'root',
// password
password: '',
// database
database: 'ancientpoems',
},
// load into app, default is open
app: true,
// load into agent, default is close
agent: false,
};
config.cors = {
origin: ctx => ctx.get('origin'),
// origin: '*', //只允许这个域进行访问接口
credentials: true, // 开启认证 支持cookie跨域
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
};
路由接口配置
'use strict';
/**
* @param {Egg.Application} app - egg application
* @description 后端路由文件
*/
module.exports = app => {
const { router, controller } = app;
let loginAuth = app.middleware.loginAuth()
// 登录注册
router.get('/login', controller.login.login)
router.post('/checkLogin', controller.login.checkLogin)
router.post('/register', controller.register.register)
// 用户个人信息修改
router.post('/userNameUpdate', controller.userInfoUpdate.userNameUpdate)
router.post('/passwordUpdate', controller.userInfoUpdate.passwordUpdate)
router.post('/sexUpdate', controller.userInfoUpdate.sexUpdate)
router.post('/personalizedSigUpdate', controller.userInfoUpdate.personalizedSigUpdate)
router.post('/birthUpdate', controller.userInfoUpdate.birthUpdate)
router.post('/headPicUpdate', controller.userInfoUpdate.headPicUpdate)
// 查询诗词
router.post('/QuerySomeonePoems', controller.queryPoems.QuerySomeonePoems)
router.post('/QueryRandomTenPoems', controller.queryPoems.QueryRandomTenPoems)
router.post('/QuerySomeTypePoem', controller.queryPoems.QuerySomeTypePoem)
// 搜索功能
router.post('/FuzzySearch', loginAuth, controller.search.FuzzySearch)
router.post('/Search', loginAuth, controller.search.Search)
router.post('/NameSearch', controller.search.NameSearch)
router.post('/SearchType', controller.search.SearchType)
router.post('/SearchPoet', controller.search.SearchPoet)
router.post('/SearchDynasty', controller.search.SearchDynasty)
// 查询诗人
router.post('/QueryADynastyPoets', controller.queryPoets.QueryADynastyPoets)
router.post('/QuerySortDynastyPoets', controller.queryPoets.QuerySortDynastyPoets)
// 收藏
router.post('/AddCollection', controller.collection.AddCollecions)
router.post('/QueryCollection', controller.collection.QueryCollections)
// 浏览记录
router.post('/AddHistory', controller.browseHistory.AddHistory) // 添加历史记录
router.post('/SearchHistory', controller.browseHistory.SearchHistory) //搜索某用户id的历史记录
// 诗单的创建
router.post('/CreateList', controller.poemList.CreateList);
router.post('/UpdateList', controller.poemList.UpdateList);
router.post('/DeleteList', controller.poemList.DeleteList);
router.post('/CheckList', controller.poemList.CheckList) // 查看某用户的所以诗单
// 添加诗词到诗单
router.post('/AddPoemToList', controller.poemList.AddPoemToList);
router.post('/CheckPoemList', controller.poemList.CheckPoemList);
router.post('/DeletePoemList', controller.poemList.DeletePoemList);
// 用户自创作品
router.post('/CreateWork', controller.forum.CreateWork); // 创建作品,保存在草稿箱中, 默认状态为0,在草稿箱中
router.post('/CreateComment', controller.forum.CreateComment); // 创建评论
router.post('/CheckWork', controller.forum.CheckWork); // 按时间顺序 查看论坛中 state = 1 的发布作品
router.post('/PostWork', controller.forum.PostWork) // 发布作品,将草稿箱中的作品进行发布 state改为1
// 添加考一考题目路由
router.post('/AddTest', controller.test.AddTest)
router.get('/QueryTest', controller.test.QueryTest)
// 飞花令
router.post('/GetAnswer', controller.feiHuaLing.GetAnswer) // 用来根据随机数活得答案
router.post('/CheckAnswer', controller.feiHuaLing.CheckAnswer) // 用来检测用户的答案
router.get('/ChangeAllAnswer', controller.feiHuaLing.ChangeAllAnswer) // 用来改变所有答案状态,飞花令结束时候出触发
};
具体接口方法实现
- 示例查询诗词
/* 'use strict';
/**
* @description 查询诗词controller
*/
const Controller = require('egg').Controller;
class QueryPoemsController extends Controller {
async QuerySomeonePoems() { // 查询某个诗人的诗文
let author = this.ctx.request.body.author
// let author = '李白'
const sql = " SELECT id, author, name, dynasty, content, annotation, translation, analyse, background FROM t_poems_poem WHERE author = '" + author + "' ORDER BY RAND() LIMIT 10 OFFSET 0 "
const res = await this.app.mysql.query(sql)
if (res.length > 0) {
this.ctx.body = { 'data': '查询诗词十首成功', 'res': res }
}
}
async QuerySomeTypePoem() { // 查询某种类型的诗文
let type = this.ctx.request.body.type
// type = '夏天'
const sql = "SELECT * FROM t_poems_poem WHERE type = '" + type + "' ORDER BY RAND() LIMIT 10"
const res = await this.app.mysql.query(sql)
if (res.length > 0) {
this.ctx.body = { 'data': '查询某种类型诗词十首成功', 'res': res }
}
}
async QueryRandomTenPoems() { // 随机查询十首古诗
const sql = 'SELECT * FROM t_poems_poem ORDER BY RAND() LIMIT 10'
const res = await this.app.mysql.query(sql)
if (res.length > 0) {
this.ctx.body = { 'data': '随机查询诗词十首成功', 'res': res }
}
}
}
module.exports = QueryPoemsController;
部署部分
部署部分记录在另外一篇文章内,想要部署的同学可以看看,使用的是阿里云的服务器来部署的,使用宝塔来管理服务器的软件资源,非常简单方便。(点这里去看看)
源码
总结
本项目总的来说比较简单,但是前端后端部署都有涉及到,如果你需要一个项目来学习学习,参考参考,这么写出一个可以跑的全栈项目,本项目还是有一定的参考价值🤣🤣🤣。根据这个项目,你去做一个什么管理系统,购物系统啥的都是可以的,简单的增删改查就可以。最后,祝你成功嘿嘿嘿~