React与Koa一起打造一个仿稀土掘金全栈个人博客(技术篇)

前言

我的个人博客样式布局是仿的稀土掘金 ,个人博客线上网址: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

模块页面

  1. 首页

  2. 登录注册

  3. 文章详情

  4. 文章评论

  5. 圈子

  6. 写圈子

  7. 搜索页

  8. 权限页

  9. 写文章

项目配置

项目目录

在这里插入图片描述

前台搭建项目步骤

一、使用稳定依赖管理工具

推荐你使用淘宝源

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 样式和修改变量。

  1. 首先在src目录下创建一个App.less文件,编辑内容如下:
1@import '~antd/dist/antd.less'; 2
  1. 然后在App.jsx内引入App.less文件(上面已经编辑过App.jsx文件的这里不用管)

  2. 然后安装 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>&copy; {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,如有侵权,请联系删除。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java将前端的json数组字符串转换为列表

记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang