
前言
我的个人博客样式布局是仿的稀土掘金 ,个人博客线上网址:https://www.maomin.club/ ,也可以百度搜索前端历劫之路 。为了浏览体验,可以用PC浏览器浏览。
本篇文章将分为前台角度与后台角度来分析我是怎么开发的。
前台角度
主要资源
-
react.js
-
ant Design
-
for-editor
-
axios
-
craco-less
-
immutable
-
react-loadable
-
react-redux
-
react-router-dom
-
react-transition-group
-
redux
-
redux-immutable
-
redux-thunk
-
styled-components
模块页面
-
首页
-
登录注册
-
文章详情
-
文章评论
-
圈子
-
写圈子
-
搜索页
-
权限页
-
写文章
项目配置
项目目录

在这里插入图片描述
前台搭建项目步骤
一、使用稳定依赖管理工具
推荐你使用淘宝源
1npm config set registry https://registry.npm.taobao.org 2
还有就是搭配依赖管理工具yarn
二、使用官方React脚手架
1create-react-app my-project 2
三、精简项目文件夹
使用脚手架搭建的初始文件夹是这样的。
那么我们需要精简一下。注意原来的App.js我改成App.jsx。因为 React 使用 JSX 来替代常规的 JavaScript,所以用JSX比较好。
下面我们将要编辑几个文件:src/index.js
1// index.js 2import React from 'react'; 3import ReactDOM from 'react-dom'; 4import App from './App.jsx'; 5 6ReactDOM.render( 7 <App />, 8 document.getElementById('root') 9); 10
public/index.html
1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="utf-8" /> 5 <link rel="shortcut icon" href="./bitbug_favicon.ico" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 <meta name="theme-color" content="#FFB90F" /> 8 <meta name="keywords" content="前端历劫之路"> 9 <meta name="description" content="如何从前端小仙历劫成为一个前端大神呢?这里就有答案。" /> 10 <title>前端历劫之路</title> 11 </head> 12 <body> 13 <noscript>You need to enable JavaScript to run this app.</noscript> 14 <div id="root"></div> 15 </body> 16</html> 17 18
App.jsx文件内的内容什么意思现在可以先不用去关心,可以先放这。
src/App.jsx
1// App.jsx 2import React from 'react'; 3import { Provider } from 'react-redux'; 4import store from './store/'; 5import Router from './router'; 6import {BrowserRouter} from 'react-router-dom'; 7import {Main} from './styled/' 8import { CSSTransition, TransitionGroup } from 'react-transition-group'; 9import { GlobalStyle } from '../src/styled/index'; 10import HeaderArea from './components/layout/Header'; 11import './App.less'; 12 13const Body = () => { 14 return ( 15 <div> 16 <BrowserRouter> 17 <GlobalStyle /> 18 <HeaderArea /> 19 <Main> 20 <Router /> 21 </Main> 22 </BrowserRouter> 23 </div> 24 ) 25} 26 27const App = () => { 28 return ( 29 <div> 30 <Provider store={store}> 31 <TransitionGroup appear={true} > 32 <CSSTransition timeout={10000} classNames='fade'> 33 <Body /> 34 </CSSTransition> 35 </TransitionGroup> 36 </Provider> 37 </div> 38 ) 39}; 40 41export default App; 42 43
四、创建文件夹
在**「src目录」**下分别创建以下几个文件夹
五、安装依赖
dependencies:
-
antd
-
axios
-
for-editor
-
immutable
-
react-loadable
-
react-redux
-
react-router-dom
-
react-transition-group
-
redux
-
redux-immutable
-
redux-thunk
-
styled-components
六、配置自定义主题
按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能。我们可以引入 craco-less 来帮助加载 less 样式和修改变量。
- 首先在src目录下创建一个App.less文件,编辑内容如下:
1@import '~antd/dist/antd.less'; 2
-
然后在App.jsx内引入App.less文件(上面已经编辑过App.jsx文件的这里不用管)
-
然后安装
craco-less并创建修改craco.config.js(存放在项目根目录下) 文件如下:
1// craco.config.js 2const CracoLessPlugin = require('craco-less'); 3const theme = require ('./theme'); 4 5module.exports = { 6 plugins: [ 7 { 8 plugin: CracoLessPlugin, 9 options: { 10 lessLoaderOptions: { 11 modifyVars: theme.theme, 12 javascriptEnabled: true, 13 }, 14 }, 15 } 16 ], 17}; 18
1// theme.js 2const theme = { 3 '@primary-color': '#FFB90F', // 全局主色 4 '@link-color': '#1890ff', // 链接色 5 '@success-color': '#52c41a', // 成功色 6 '@warning-color': '#faad14', // 警告色 7 '@error-color': '#f5222d', // 错误色 8 '@font-size-base': '14px', // 主字号 9 '@heading-color': 'rgba(0, 0, 0, 0.85)', // 标题色 10 '@text-color': 'rgba(0, 0, 0, 0.65)', // 主文本色 11 '@text-color-secondary': 'rgba(0, 0, 0, 0.45)', // 次文本色 12 '@disabled-color': 'rgba(0, 0, 0, 0.25)', // 失效色 13 '@border-radius-base': '4px', // 组件/浮层圆角 14 '@border-color-base': '#d9d9d9', // 边框色 15 '@box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)' // 浮层阴影 16} 17 18exports.theme = theme 19
七、路由懒加载
在router文件夹下创建index.js和routes.js。
routes.js
1// routes.js 2// 路由配置 3import React from 'react'; 4import {Route } from 'react-router-dom'; 5 6import {Home,About,Details,Write,Circle,Noauth,Search} from './routes' 7 8const APPRouter = () =>( 9 <div> 10 <Route exact={true} path="/" component={Home}/> 11 <Route exact={true} path="/about/" component={About}/> 12 <Route exact={true} path="/details/:id/" component={Details} /> 13 <Route exact={true} path="/write" component={Write} /> 14 <Route exact={true} path="/circle" component={Circle} /> 15 <Route exact={true} path="/noauth" component={Noauth} /> 16 <Route exact={true} path="/search" component={Search} /> 17 </div> 18); 19 20export default APPRouter; 21 22
index.js
1// index.js 2// 页面组件 3import loadable from '../util/loadable'; 4 5export const Home = loadable(()=> import('../views/Home/')); 6export const About = loadable(()=> import('../views/About/')); 7export const Details = loadable(()=> import('../views/Details')); 8export const Write = loadable(()=> import('../views/Write')); 9export const Circle = loadable(()=> import('../views/Circle')); 10export const Noauth = loadable(()=>import('../components/modules/Noauth')) 11export const Search = loadable(()=>import('../views/Search')) 12
在util文件夹下创建一个loadable.js。
loadable.js
1// loadable.js 2// 懒加载组件 3import React from 'react'; 4import Loadable from 'react-loadable'; 5import styled from 'styled-components'; 6import { Spin } from 'antd'; 7 8const loadingComponent =()=>{ 9 return ( 10 <Loading> 11 <Spin /> 12 </Loading> 13 ) 14}; 15 16export default (loader,loading = loadingComponent)=>{ 17 return Loadable({ 18 loader, 19 loading 20 }); 21}; 22 23const Loading = styled.div` 24 text-align: center; 25 margin:50vh 0; 26`; 27
八、全局样式与样式组件
这里我们使用styled-components这个依赖写样式组件,因为在react.js中存在组件样式污染的缘故。在styled创建一个index.js。
index.js
1// index.js 2// 全局样式 3import styled,{createGlobalStyle} from 'styled-components'; 4 5export const Content = styled.div` 6 border-radius: 2px; 7 width: 100%; 8 padding:20px; 9 margin:20px 0; 10 border:1px solid #f4f4f4; 11 background:#fff; 12 box-sizing:border-box; 13 14 15export const Main = styled.div` 16 position: relative; 17 margin: 100px auto 20px; 18 width: 100%; 19 max-width: 960px; 20`; 21export const GlobalStyle = createGlobalStyle` 22html, body, div, span, applet, object, iframe, 23h1, h2, h3, h4, h5, h6, p, blockquote, pre, 24a, abbr, acronym, address, big, cite, code, 25del, dfn, em, img, ins, kbd, q, s, samp, 26small, strike, strong, sub, sup, tt, var, 27b, u, i, center, 28dl, dt, dd, ol, ul, li, 29fieldset, form, label, legend, 30table, caption, tbody, tfoot, thead, tr, th, td, 31article, aside, canvas, details, embed, 32figure, figcaption, footer, header, hgroup, 33menu, nav, output, ruby, section, summary, 34time, mark, audio, video{ 35 margin: 0; 36 padding: 0; 37 border: 0; 38 font-size: 100%; 39 font: inherit; 40 font-weight: normal; 41 vertical-align: baseline; 42} 43article, aside, details, figcaption, figure, 44footer, header, hgroup, menu, nav, section{ 45 display: block; 46} 47ol, ul, li{ 48 list-style: none; 49} 50blockquote, q{ 51 quotes: none; 52} 53blockquote:before, blockquote:after, 54q:before, q:after{ 55 content: ''; 56 content: none; 57} 58table{ 59 border-collapse: collapse; 60 border-spacing: 0; 61} 62a{ 63 color: #7e8c8d; 64 text-decoration: none; 65 -webkit-backface-visibility: hidden; 66} 67::-webkit-scrollbar{ 68 width: 5px; 69 height: 5px; 70} 71::-webkit-scrollbar-track-piece{ 72 background-color: rgba(0, 0, 0, 0.2); 73 -webkit-border-radius: 6px; 74} 75::-webkit-scrollbar-thumb:vertical{ 76 height: 5px; 77 background-color: rgba(125, 125, 125, 0.7); 78 -webkit-border-radius: 6px; 79} 80::-webkit-scrollbar-thumb:horizontal{ 81 width: 5px; 82 background-color: rgba(125, 125, 125, 0.7); 83 -webkit-border-radius: 6px; 84} 85html, body{ 86 width: 100% !important; 87 background:#E8E8E8; 88 font-size: 12px; 89 font-family: Avenir,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji,sans-serif; 90} 91body{ 92 line-height: 1; 93 -webkit-text-size-adjust: none; 94 -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 95} 96html{ 97 overflow-y: scroll; 98} 99.clearfix:before, 100.clearfix:after{ 101 content: " "; 102 display: inline-block; 103 height: 0; 104 clear: both; 105 visibility: hidden; 106} 107.clearfix{ 108 *zoom: 1; 109} 110.ovf{ 111 overflow:hidden; 112} 113.dn{ 114 display: none; 115} 116/*自定义全局*/ 117p{ 118 margin:10px; 119} 120.fade-enter { 121 opacity: 0; 122} 123.fade-enter-active { 124 opacity: 1; 125 transition: all .5s; 126} 127.fade-exit { 128 opacity: 1; 129 transition: all .5s; 130} 131.fade-exit-active { 132 opacity: 0; 133} 134.hide{ 135 opacity: 0; 136 height: 0px; 137 transform: translatey(-100px); 138 } 139 140::-webkit-scrollbar { 141 width:5px; 142 height:5px; 143} 144::-webkit-scrollbar-track { 145 width: 5px; 146 background-color:#fff; 147 -webkit-border-radius: 10px; 148 -moz-border-radius: 10px; 149 border-radius:10px; 150} 151::-webkit-scrollbar-thumb { 152 background-clip:padding-box; 153 min-height:28px; 154 -webkit-border-radius: 10px; 155 -moz-border-radius: 10px; 156 border-radius:10px; 157} 158::-webkit-scrollbar-thumb:hover { 159 background-color:#FFB90F; 160} 161`; 162 163
九、封装axios请求
在request文件夹下创建api.js和http.js。
api.js
存放api接口。
1// api.js 2// 接口地址 3import {get,post} from './http'; 4const url= 'https://www.maomin.club/myblog/'; // api 5// post格式 6export const reg = g => post(`${url}register`, g); // 注册 7export const log = g => post(`${url}login`, g); // 登录 8export const write = g => post(`${url}write`, g); // 写文章 9export const circle = g => post(`${url}circle`, g); // 发圈子 10export const getCircle = g => post(`${url}getCircle`, g); // 获取圈子 11export const uploadImg = g => post(`${url}uploadImg`, g); // 写文章上传图片 12export const getListapi = g => post(`${url}getList`, g); // 获取文章列表 13export const getDetails = g => post(`${url}getDetails`, g); // 获取文章详情 14export const comment = g => post(`${url}comment`, g); // 发送评论 15export const getComment = g => post(`${url}getComment`, g); // 获取评论 16export const getinfo = g => post(`${url}getinfo`, g) // 获取用户信息 17// get格式 18export const alllist = g =>get(`${url}getAllList`,g);//获取所有文章列表
http.js
请求配置。
1// http.js 2// axios配置 3import axios from 'axios'; 4import { message} from 'antd'; 5// 请求拦截器 6axios.interceptors.request.use( 7 config => { 8 if (localStorage.getItem('Authorization')) { 9 config.headers.Authorization = localStorage.getItem('Authorization'); //查看是否存在token 10 return config; 11 } else if (config.isUpload) { 12 config.headers = { 'Content-Type': 'multipart/form-data' } // 根据参数是否启用form-data方式 13 return config; 14 } else { 15 config.headers = { 'Content-Type': 'application/json;charset=utf-8' } 16 return config; 17 } 18 }, 19 error => { 20 return Promise.error(error) 21 }) 22 23// 响应拦截器 24axios.interceptors.response.use( 25 // 服务码是200的情况 26 response => { 27 if (response.status === 200) { 28 switch (response.data.resultCode) { 29 // token过期 30 case 2: 31 message.error('登录过期,请重新登录'); 32 localStorage.removeItem('Authorization'); 33 setTimeout(() => { 34 window.location.href="/"; 35 }, 1000); 36 break; 37 case 3: 38 message.error('未登录'); 39 break; 40 case 4: 41 message.error('请输入正确的账号或者密码'); 42 break; 43 default: 44 break; 45 } 46 return Promise.resolve(response); 47 } else { 48 return Promise.reject(response) 49 } 50 }, 51 // 服务器状态码不是200的情况 52 error => { 53 if (error.response.status) { 54 switch (error.response.status) { 55 // 404请求不存在 56 case 404: 57 alert('网络请求不存在'); 58 break; 59 // 其他错误,直接抛出错误提示 60 default: 61 alert('error.response.data.message'); 62 } 63 return Promise.reject(error.response) 64 } 65 } 66) 67 68/** 69 * get方法,对应get请求 70 * @param {String} url [请求的url地址] 71 * @param {Object} params [请求时携带的参数] 72 */ 73export function get(url, params, config = { 74 add: '' 75}) { 76 return new Promise((resolve, reject) => { 77 axios.get(url, { 78 params: params 79 }, config).then(res => { 80 resolve(res.data) 81 }).catch(err => { 82 reject(err.data) 83 }) 84 }) 85} 86/** 87 * post方法,对应post请求 88 * @param {String} url [请求的url地址] 89 * @param {Object} params [请求时携带的参数] 90 */ 91export function post(url, params, config = { 92 isUpload: false 93}) { 94 return new Promise((resolve, reject) => { 95 axios.post(url, params, config) 96 .then(res => { 97 resolve(res.data) 98 }) 99 .catch(err => { 100 reject(err.data) 101 }) 102 }) 103} 104
十、状态管理总配置
在store文件夹创建一个index.js和reducer.js。因为每个页面模块都有一个状态,所以我们在这个项目里采用分模块。然后我们现在的需要做的是统一管理它们每一个模块。
index.js
1// index.js 2// 全局store配置 3import {createStore,applyMiddleware,compose} from 'redux'; 4import thunk from 'redux-thunk'; 5import reducer from './reducer'; 6 7// redux-devtools 配置 8const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 9 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; 10 11const enhancer = composeEnhancers( 12 // 使用中间件 thunk 13 applyMiddleware(thunk) 14); 15const store = createStore(reducer,enhancer); 16 17export default store; 18
reducer.js
1// reducer.js 2// 分模块Reducer 3import { combineReducers } from 'redux-immutable'; 4import { reducer as homeReducer } from '../views/Home/store/'; 5import { reducer as layoutReducer } from '../components/layout/store'; 6import { reducer as aboutReducer } from '../views/About/store'; 7import { reducer as detailsReducer } from '../views/Details/store'; 8 9const reducer = combineReducers({ 10 home: homeReducer, 11 layout:layoutReducer, 12 about:aboutReducer, 13 details:detailsReducer 14}); 15 16export default reducer; 17
十一、页面模块与组件模块
因页面过多,这里只展示首页模块,其他逻辑思想大差不差,如果想详细了解的可以加我微信。在views文件夹创建一个Home文件夹。依次创建如下图所示文件:
index.jsx
页面组件。
1// index.jsx 2import React, { useEffect, Fragment } from 'react'; 3import { Link } from 'react-router-dom'; 4import { connect } from 'react-redux'; 5import { Pagination, Spin } from 'antd'; 6import styled from 'styled-components'; 7import { LeftView, RightView, Item, ContentBox, InfoBox, Meta, Title, ImgBox, SidebarBlock, ImgBlock, MoreBlock } from './styleJs/style'; 8import { actionsCreator } from './store/'; 9 10const mapStateToProps = (state) => { 11 return { 12 datalist: state.getIn(['home', 'datalist']), 13 page: state.getIn(['home', 'page']), 14 defaultCurrent: state.getIn(['home', 'defaultCurrent']) 15 } 16}; 17 18const mapDispatchToProps = (dispatch) => { 19 return { 20 getdata(v) { 21 dispatch(actionsCreator.getList(v)) 22 }, 23 pageChange(v) { 24 dispatch(actionsCreator.changePage(v)) 25 } 26 } 27}; 28const Loading = styled.div` 29 text-align: center; 30 margin:34vh 0; 31`; 32const Home = (props) => { 33 const { datalist, getdata, page, defaultCurrent, pageChange } = props; 34 const newList = datalist.toJS(); 35 useEffect(() => { 36 getdata(defaultCurrent); 37 }, [defaultCurrent, getdata]) 38 return ( 39 <div> 40 <LeftView> 41 { 42 page === 0 ? <Loading> 43 <Spin tip="Loading..." /> 44 </Loading> : <div><div style={{ 'height': '624px' }}> 45 { 46 newList.map((item) => { 47 return ( 48 <Fragment key={item.id}> 49 <Link to={'/details/' + item.id}> 50 <Item> 51 <ContentBox> 52 <InfoBox> 53 <Meta>{item.tab}</Meta> 54 <Title>{item.title}</Title> 55 </InfoBox> 56 <ImgBox srci={item.context.substring(item.context.indexOf("<img src='"), item.context.indexOf("' alt=''>")).replace("<img src='", "")}></ImgBox> 57 </ContentBox> 58 </Item> 59 </Link> 60 </Fragment> 61 ) 62 }) 63 } 64 65 </div> 66 <div style={{ 'margin': '20px' }}> 67 <Pagination defaultCurrent={defaultCurrent} total={page} pageSize={6} onChange={pageChange}></Pagination> 68 </div> 69 </div> 70 } 71 </LeftView> 72 73 <RightView> 74 <SidebarBlock> 75 <ImgBlock src={require("../../assets/images/gzh.jpg")} /> 76 </SidebarBlock> 77 <SidebarBlock> 78 <ImgBlock src={require("../../assets/images/wx.jpg")} /> 79 </SidebarBlock> 80 <MoreBlock> 81 <div>© {new Date().getFullYear()}<span>maomin.club</span>版权所有</div> 82 <a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=37021302000701">公安备案号 37021302000701号 </a> 83 <a href="http://www.beian.miit.gov.cn/"> 鲁ICP备19020856号-1</a> 84 </MoreBlock> 85 86 </RightView> 87 </div> 88 ) 89} 90 91export default connect(mapStateToProps, mapDispatchToProps)(Home); 92
styles/style.js
home页面的样式。
1// style.js 2import styled, {keyframes } from 'styled-components'; 3 4const fadeIn = keyframes` 5 from { 6 opacity:0; 7 } 8 to { 9 opacity:1; 10 } 11` 12 13export const LeftView = styled.div` 14 border-radius: 2px; 15 width: 700px; 16 margin-right: 21.667rem; 17 border:1px solid #f4f4f4; 18 background:#fff; 19 box-sizing:border-box; 20 animation: ${fadeIn} 1s ease-in; 21` 22export const RightView = styled.div` 23 position: absolute; 24 top: 0; 25 right: 0; 26 width:20rem; 27 @media (max-width: 960px){ 28 display: none; 29 } 30` 31export const Item = styled.div` 32 border-bottom: 1px solid rgba(178,186,194,.15); 33` 34export const ContentBox = styled.div` 35 display: flex; 36 align-items: center; 37 padding: 1.5rem 2rem; 38` 39export const InfoBox = styled.div` 40 flex: 1 1 auto; 41 display: flex; 42 flex-direction: column; 43 justify-content: center; 44 min-width: 0; 45` 46export const Meta = styled.div` 47 color: #b2bac2; 48` 49export const Title = styled.div` 50 margin: 1rem 0 1rem; 51 white-space: nowrap; 52 overflow: hidden; 53 text-overflow: ellipsis; 54 font-size: 1.4rem; 55 font-weight: 600; 56 line-height: 1.2; 57 color: #2e3135; 58` 59 60export const ImgBox = styled.div` 61 background-image:url('${props => props.srci}'); 62 background-repeat: no-repeat; 63 background-size: cover; 64 flex: 0 0 auto; 65 width: 5rem; 66 height: 5rem; 67 background-color:#f4f4f4; 68 margin-left: 2rem; 69 background-color: #fff; 70 border-radius: 2px; 71 background-position: 50%; 72 animation: ${fadeIn} 1s ease-in; 73` 74export const SidebarBlock = styled.div` 75 background-color: #fff; 76 box-shadow: 0 1px 2px 0 rgba(0,0,0,.05); 77 border-radius: 2px; 78 margin-bottom: 1.3rem; 79 font-size: 1.16rem; 80 line-height: 1.29; 81 color: #333; 82` 83export const ImgBlock = styled.img` 84 width:100%; 85 animation: ${fadeIn} 1s ease-in; 86` 87export const MoreBlock =styled.div` 88 background-color: transparent; 89 box-shadow: none; 90 a{ 91 display:block; 92 line-height:22px; 93 text-decoration: none; 94 cursor: pointer; 95 color: #909090; 96 } 97 div { 98 line-height:22px; 99 } 100 span{ 101 margin:0 5px; 102 } 103` 104
store/actionsCreator.js
react-thunk作用:使我们可以在action中返回函数,而不是只能返回一个对象。然后我们可以在函数中做很多事情,比如发送异步的ajax请求。
1// actionsCreator.js 2import {actionsTypes} from './index'; 3import {getListapi} from '../../../request/api'; 4import {fromJS} from 'immutable'; 5 6const dataList =(data,page) =>{ 7 return { 8 type:actionsTypes.DATA_LIST, 9 data:fromJS(data), 10 page:fromJS(page) 11 } 12}; 13const currentPage = (p) =>{ 14 return { 15 type:actionsTypes.CHANGE_PAGE, 16 current:p 17 } 18 } 19export const getList = (p) =>{ 20 return (dispatch) =>{ 21 let postData ={ 22 page:p 23 } 24 getListapi(postData).then((res)=>{ 25 const data = res.data; 26 const page = res.page; 27 const action = dataList(data,page); 28 dispatch(action); 29 }).catch((err)=>{ 30 console.log(err); 31 }) 32 } 33}; 34 35export const changePage=(page)=>{ 36 return (dispatch) =>{ 37 const action = currentPage(page); 38 dispatch(action); 39 } 40 } 41
store/actionsTypes.js
1// actionsTypes.js 2export const DATA_LIST = 'home/DATA_LIST'; 3export const CHANGE_PAGE = 'home/CHANGE_PAGE'; 4
store/index.js
home页面的store配置。
1// index.js 2import reducer from './reducer'; 3import * as actionsTypes from './actionsTypes'; 4import * as actionsCreator from './actionsCreator'; 5 6export { reducer, actionsCreator,actionsTypes}; 7
store/reducer.js
由于是不可变的,可以放心的对对象进行任意操作。在 React 开发中,频繁操作state对象或是 store ,配合 immutableJS 快、安全、方便。
1// reducer.js 2import {actionsTypes} from './index'; 3import {fromJS} from 'immutable'; 4 5let defaultState = fromJS({ 6 datalist: [], 7 page:0, 8 defaultCurrent:1 9}); 10 11export default (state = defaultState, action) => { 12 switch (action.type) { 13 case actionsTypes.DATA_LIST: 14 return state.merge({ 15 'datalist':action.data, 16 'page':action.page 17 }) 18 case actionsTypes.CHANGE_PAGE: 19 return state.set('defaultCurrent',action.current) 20 default: 21 return state; 22 } 23}; 24
后台角度
主要资源
-
https
-
fs
-
path
-
koa
-
koa-router
-
koa2-cors
-
jsonwebtoken
-
koa-body
-
koa-static
-
koa-sslify
-
mysql
-
node-schedule
源码
后台主要是用了Koa模块,下面的源码是基于https环境。数据库是采用了创建地址池的方法,数据库的连接池负责分配,管理和释放数据库链接的。它允许应用程序重复使用一个现有的数据库的链接。而不是重新创建一个。地址池这里可以优化,这里为了看的更清楚,统一放在了一个文件里。具体详解请看下面的注释。
1// app.js 2var https = require("https");//https服务 3var fs = require("fs"); 4var path = require('path'); 5var Koa = require('koa'); 6var Router = require('koa-router'); 7var cors = require('koa2-cors'); 8var jwt = require('jsonwebtoken'); 9var koaBody = require('koa-body'); //文件保存库 10var serve = require('koa-static'); 11var enforceHttps = require('koa-sslify').default; 12var mysql = require('mysql'); 13var schedule = require('node-schedule'); 14var app = new Koa(); 15app.use(enforceHttps()); 16var router = new Router(); 17var secretkey = ''; // token的key 18 19// 这是我的https配置文件可忽略 20var options = { 21 key: fs.readFileSync('https/2_www.maomin.club.key'), 22 cert: fs.readFileSync('https/1_www.maomin.club_bundle.crt') 23} 24 25// 存文件配置 26const home = serve(path.join(__dirname) + '/public/'); 27app.use(home); 28app.use(koaBody({ 29 multipart: true 30})); 31 32// 跨域 33const allowOrigins = [ 34 "https://www.maomin.club/" 35]; 36app.use(cors({ 37 origin: function (ctx) { 38 if (allowOrigins.includes(ctx.header.origin)) { 39 return ctx.header.origin; 40 } 41 return false; 42 }, 43 exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'], 44 maxAge: 5, 45 credentials: true, 46 withCredentials: true, 47 allowMethods: ['GET', 'POST', 'DELETE'], 48 allowHeaders: ['Content-Type', 'Authorization', 'Accept'], 49})); 50 51// 创建地址池 52var pool = mysql.createPool({ 53 host: '', // 主机 54 port: 3306, // 端口 55 user: '', // 用户 56 password: '', // 密码 57 database: '', // 数据库 58 multipleStatements: true, // 允许每个mysql语句有多条查询 59 connectionLimit: 100 // 最大连接数 60}) 61 62// 数据库操作 63// 定时置3 64schedule.scheduleJob('10 0 0 * * *', function () { 65 console.log('update!') 66 var updateStr = 'UPDATE login SET count = ?'; 67 var modSqlParams = [3]; 68 pool.getConnection(function (err, conn) { 69 if (err) { 70 //do something 71 console.log(err); 72 } 73 conn.query(updateStr, modSqlParams, function (err, results) { 74 if (err) { 75 //do something 76 throw err; 77 } 78 conn.release(); //释放连接 79 }) 80 }) 81}); 82 83// 检查token 84const checkToken = function (tokenid) { 85 return new Promise((resolve) => { 86 if (tokenid) { 87 //校验tokenid 88 jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解码后用户信息 89 if (err) { //如果tokenid过期则会执行err的代码块 90 resolve({ success: false, resultCode: 2, message: err }); 91 } else { 92 resolve("notime"); 93 } 94 }) 95 } else { resolve({ success: false, resultCode: 3, message: '未登录' }) } 96 }) 97} 98 99let json = {}; 100// 通用查询方法 101const query = function (sql) { 102 return new Promise((resolve, reject) => { 103 pool.getConnection(function (err, conn) { 104 if (err) { 105 //do something 106 console.log(err); 107 } 108 conn.query(sql, function (err, results) { 109 if (err) { 110 //do something 111 reject(error); 112 } else { 113 //return data or anything you want do! 114 resolve(results); 115 } 116 conn.release(); //释放连接 117 }) 118 }) 119 }) 120} 121 122// 分页 123let all = ""; 124const page = function (sql, p) { 125 return new Promise((resolve, reject) => { 126 pool.getConnection(function (err, conn) { 127 if (err) { 128 //do something 129 console.log(err); 130 } 131 conn.query(sql, function (err, results) { 132 if (err) { 133 //do something 134 reject(error); 135 } else { 136 //return data or anything you want do! 137 var allCount = results[0][0]['COUNT(*)']; 138 all = allCount; 139 var allPage = parseInt(allCount) / p; 140 var pageStr = allPage.toString(); 141 if (pageStr.indexOf('.') > 0) { 142 allPage = parseInt(pageStr.split('.')[0]) + 1; 143 } 144 var List = results[1]; 145 resolve(List) 146 } 147 conn.release(); //释放连接 148 }) 149 }) 150 }) 151} 152 153// 登录方法 154const logQuery = function (userStr, token) { 155 return new Promise((resolve, reject) => { 156 pool.getConnection(function (err, conn) { 157 if (err) { 158 //do something 159 console.log(err); 160 } 161 conn.query(userStr, function (err, results) { 162 if (err) { 163 //do something 164 reject(error); 165 } else { 166 //return data or anything you want do! 167 if (results.length !== 0) { 168 var dataString = JSON.stringify(results); 169 var data = JSON.parse(dataString); 170 json['message'] = '登录成功'; 171 json['resultCode'] = 200; 172 json['username'] = data[0].username; 173 json['token'] = token; 174 var updateStr = 'UPDATE login SET token = ? WHERE Id = ?'; 175 var modSqlParams = [token, data[0].id]; 176 pool.getConnection(function (err, conn) { 177 if (err) { 178 //do something 179 console.log(err); 180 } 181 conn.query(updateStr, modSqlParams, function (err, results) { 182 if (err) { 183 //do something 184 throw err; 185 } conn.release(); //释放连接 186 }) 187 }) 188 resolve(json); 189 } else { 190 resolve({ success: false, resultCode: 4, message: '请输入正确的账号或密码' }); 191 } 192 } 193 conn.release(); //释放连接 194 }) 195 }) 196 }) 197} 198 199//注册方法 200const regQuery = function (userStr, name, passwd, token, count) { 201 return new Promise((resolve, reject) => { 202 pool.getConnection(function (err, conn) { 203 if (err) { 204 //do something 205 console.log(err); 206 } 207 conn.query(userStr, function (err, result) { 208 if (err) { 209 //do something 210 reject(error); 211 } else { 212 //return data or anything you want do! 213 if (result.length > 0) { 214 json['message'] = '用户已经存在'; 215 json['resultCode'] = 1; 216 } else { 217 json['message'] = '注册成功'; 218 json['token'] = token; 219 json['username'] = name; 220 json['count'] = count; 221 json['resultCode'] = 200; 222 var insertStr = `insert into login (username, password,token,count) values ("${name}", "${passwd}","${token}","${count}")`; 223 pool.getConnection(function (err, conn) { 224 if (err) { 225 //do something 226 console.log(err); 227 } 228 conn.query(insertStr, function (err, results) { 229 if (err) { 230 //do something 231 throw err; 232 } conn.release(); //释放连接 233 }) 234 }) 235 } 236 resolve(json) 237 } 238 conn.release(); //释放连接 239 }) 240 }) 241 }) 242} 243 244// 评论方法 245const commentQuery = function (userStr, aid) { 246 return new Promise((resolve, reject) => { 247 pool.getConnection(function (err, conn) { 248 if (err) { 249 //do something 250 console.log(err); 251 } 252 conn.query(userStr, async function (err) { 253 if (err) { 254 //do something 255 reject(error); 256 } else { 257 //return data or anything you want do! 258 json['message'] = '评论成功'; 259 json['success'] = true; 260 let sql = `select aid,username,com from comment where aid="${aid}"`; 261 let results = await query(sql); 262 json['data'] = results; 263 resolve(json); 264 } 265 conn.release(); //释放连接 266 }) 267 }) 268 }) 269} 270 271// 发圈子方法 272const setCount = function (userStr, username, imgsrc, inputValue, td) { 273 return new Promise((resolve, reject) => { 274 pool.getConnection(function (err, conn) { 275 if (err) { 276 //do something 277 console.log(err); 278 } 279 conn.query(userStr, function (err, results) { 280 if (err) { 281 //do something 282 reject(error); 283 } else { 284 //return data or anything you want do! 285 var dataString = JSON.stringify(results); 286 var data = JSON.parse(dataString); 287 if (data[0].count > 0) { 288 var newCount = data[0].count - 1; 289 json['message'] = '发表成功'; 290 json['resultCode'] = 200; 291 json['success'] = true; 292 json['count'] = newCount; 293 // 次数减一 294 var updateStr = 'UPDATE login SET count = ? WHERE username = ?'; 295 var modSqlParams = [newCount, username]; 296 pool.getConnection(function (err, conn) { 297 if (err) { 298 //do something 299 console.log(err); 300 } 301 conn.query(updateStr, modSqlParams, function (err) { 302 if (err) { 303 //do something 304 throw err; 305 } conn.release(); //释放连接 306 }) 307 }) 308 // 存入圈子数据库 309 var insetStr = `insert into circle (username, imgsrc, inputValue, td) values ("${username}","${imgsrc}","${inputValue}","${td}")` 310 pool.getConnection(function (err, conn) { 311 if (err) { 312 //do something 313 console.log(err); 314 } 315 conn.query(insetStr, modSqlParams, function (err) { 316 if (err) { 317 //do something 318 throw err; 319 } conn.release(); //释放连接 320 }) 321 }) 322 resolve(json); 323 } else { 324 resolve({ success: false, resultCode: 5, message: '操作太频繁,请明天再发哦' }); 325 } 326 } 327 conn.release(); //释放连接 328 }) 329 }) 330 }) 331} 332 333// 用户信息方法 334const getInfo = function (tokenid) { 335 return new Promise((resolve) => { 336 if (tokenid) { 337 //校验tokenid 338 jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解码后用户信息 339 if (err) { //如果tokenid过期则会执行err的代码块 340 resolve({ success: false, resultCode: 2, message: err }); 341 } else { 342 resolve(decoded); 343 } 344 }) 345 } else { resolve({ success: false, resultCode: 3, message: '未登录' }) } 346 }) 347} 348 349// 获取用户信息 350router.post('/getinfo', async (ctx, next) => { 351 var tokenid = ctx.request.body.token; 352 let results = await getInfo(tokenid); 353 ctx.body = results; 354}) 355 356// 注册 357router.post('/register', async (ctx, next) => { 358 let name = ctx.request.body.username; 359 let passwd = ctx.request.body.password; 360 let count = 3; 361 let token = jwt.sign({ 362 username: name 363 }, secretkey, { 364 expiresIn: 60 * 60 * 12 // 12h 365 }); 366 let userStr = `select * from login where username="${name}"`; 367 let results = await regQuery(userStr, name, passwd, token, count); 368 ctx.body = results 369}); 370 371// 登录 372router.post('/login', async (ctx, next) => { 373 let name = ctx.request.body.username; 374 let passwd = ctx.request.body.password; 375 let token = jwt.sign({ 376 username: name 377 }, secretkey, { 378 expiresIn: 60 * 60 * 12 // 12h 379 }); 380 let userStr = `select username,password,id from login where username="${name}" and password="${passwd}"`; 381 let results = await logQuery(userStr, token); 382 ctx.body = results 383}); 384 385// 写评论 386router.post('/comment', async (ctx, next) => { 387 let aid = ctx.request.body.aid; 388 let username = ctx.request.body.username; 389 let com = ctx.request.body.com; 390 let td = ctx.request.body.td; 391 var tokenid = ctx.request.headers.authorization//获取前端请求头发送过来的tokenid 392 let trueFlase = await checkToken(tokenid); 393 if (trueFlase === "notime") { 394 let userStr = `insert into comment (aid, username, com, td) values ("${aid}","${username}","${com}","${td}")` 395 let results = await commentQuery(userStr, aid); 396 ctx.body = results; 397 } else { 398 ctx.body = trueFlase; 399 } 400}) 401 402// 获取评论 403router.post('/getComment', async (ctx, next) => { 404 var start = (ctx.request.body.page - 1) * 3; 405 let aid = ctx.request.body.aid; 406 var count = `SELECT * FROM comment WHERE aid="${aid}"`; 407 let allnum = await query(count); 408 const len = allnum.length; 409 var sql = `SELECT COUNT(*) FROM comment ORDER BY id DESC;SELECT * FROM comment WHERE aid="${aid}" ORDER BY id DESC limit ${start},3`; 410 let results = await page(sql, 3); 411 ctx.body = { 412 data: results, 413 page: len 414 } 415} 416) 417 418// 写文章 419router.post('/write', async (ctx, next) => { 420 let title = ctx.request.body.title; 421 let tab = ctx.request.body.tab; 422 let context = ctx.request.body.context; 423 var tokenid = ctx.request.headers.authorization//获取前端请求头发送过来的tokenid 424 let trueFlase = await checkToken(tokenid); 425 if (trueFlase === "notime") { 426 var userStr = `insert into article (title, tab, context) values ("${title}","${tab}","${context}")` 427 pool.getConnection(function (err, conn) { 428 if (err) { 429 //do something 430 console.log(err); 431 } 432 conn.query(userStr, function (err) { 433 if (err) { 434 //do something 435 throw err; 436 } conn.release(); //释放连接 437 }) 438 }) 439 ctx.body = { success: true, message: '发送成功' } // echo the result back 440 } else { 441 ctx.body = trueFlase; 442 } 443 444}); 445 446// 写文章上传图片 447router.post('/uploadImg', async (ctx, next) => { 448 if (ctx.request.files.file) { 449 var file = ctx.request.files.file; 450 // 创建可读流 451 var reader = fs.createReadStream(file.path); 452 // 修改文件的名称 453 var myDate = new Date(); 454 var newFilename = myDate.getTime() + '.' + file.name.split('.')[1]; 455 var targetPath = path.join(__dirname, './public/images/') + `${newFilename}`; 456 //创建可写流 457 var upStream = fs.createWriteStream(targetPath); 458 // 可读流通过管道写入可写流 459 reader.pipe(upStream); 460 var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename; 461 ctx.body = { 462 success: true, 463 imgsrc: imgsrc 464 }; 465 } 466}) 467 468// 发圈子 469router.post('/circle', async (ctx, next) => { 470 if (ctx.request.files.file) { 471 var file = ctx.request.files.file; 472 // 创建可读流 473 var reader = fs.createReadStream(file.path); 474 // 修改文件的名称 475 var myDate = new Date(); 476 var newFilename = myDate.getTime() + '.' + file.name.split('.')[1]; 477 var targetPath = path.join(__dirname, './public/images/') + `${newFilename}`; 478 //创建可写流 479 var upStream = fs.createWriteStream(targetPath); 480 // 可读流通过管道写入可写流 481 reader.pipe(upStream); 482 var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename; 483 } else { 484 var imgsrc = "" 485 } 486 let username = ctx.request.body.username; 487 let inputValue = ctx.request.body.inputValue; 488 let td = ctx.request.body.td; 489 var tokenid = ctx.request.headers.authorization//获取前端请求头发送过来的tokenid 490 let trueFlase = await checkToken(tokenid); 491 if (trueFlase === "notime") { 492 let userStr = `select count from login where username="${username}"`; 493 let results = await setCount(userStr, username, imgsrc, inputValue, td); 494 ctx.body = results; 495 } else { 496 ctx.body = trueFlase; 497 } 498}); 499 500// 获取圈子 501router.post('/getCircle', async (ctx, next) => { 502 var start = (ctx.request.body.page - 1) * 3; 503 var sql = 'SELECT COUNT(*) FROM circle ORDER BY id DESC; SELECT * FROM circle ORDER BY id DESC limit ' + start + ',3'; 504 let results = await page(sql, 3); 505 ctx.body = { 506 data: results, 507 page: all 508 } 509}); 510 511// 获取文章列表(分页) 512router.post('/getList', async (ctx, next) => { 513 var start = (ctx.request.body.page - 1) * 6; 514 var sql = 'SELECT COUNT(*) FROM article ORDER BY id DESC; SELECT * FROM article ORDER BY id DESC limit ' + start + ',6'; 515 let results = await page(sql, 6); 516 ctx.body = { 517 data: results, 518 page: all 519 } 520}); 521 522// 获取文章列表(全部) 523router.get('/getAllList', async (ctx, next) => { 524 var sql = "select * from article"; 525 let results = await query(sql); 526 ctx.body = results 527}); 528 529// 获取文章详情 530router.post('/getDetails', async (ctx, next) => { 531 const id = ctx.request.body.id; 532 var sql = `select * from article where id="${id}"`; 533 let results = await query(sql); 534 ctx.body = results 535}); 536 537//使用路由中间件 538app 539 .use(router.routes()) 540 .use(router.allowedMethods()); 541 542https.createServer(options, app.callback()).listen(8410); 543console.log('服务器运行中') 544
❝
作者:「Vam的金豆之路」
主要领域:「前端开发」
我的微信:「maomin9761」
微信公众号:「前端历劫之路」
❞
本文转转自微信公众号前端历劫之路原创https://mp.weixin.qq.com/s/B4_r-QJmyD73tXT-EHsktQ,如有侵权,请联系删除。

❞