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

Jacquelyn38
• 阅读 2601

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. 写文章

项目配置

项目目录

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

在这里插入图片描述

前台搭建项目步骤

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

推荐你使用淘宝源

npm config set registry https://registry.npm.taobao.org  

还有就是搭配依赖管理工具yarn

二、使用官方React脚手架
create-react-app my-project  
三、精简项目文件夹

使用脚手架搭建的初始文件夹是这样的。React与Koa一起打造一个仿稀土掘金全栈个人博客(技术篇)那么我们需要精简一下。注意原来的App.js我改成App.jsx。因为 React 使用 JSX 来替代常规的 JavaScript,所以用JSX比较好。React与Koa一起打造一个仿稀土掘金全栈个人博客(技术篇)下面我们将要编辑几个文件:src/index.js

// index.js  
import React from 'react';  
import ReactDOM from 'react-dom';  
import App from './App.jsx';  

ReactDOM.render(  
  <App />,  
  document.getElementById('root')  
);  

public/index.html

<!DOCTYPE html>  
<html lang="en">  
  <head>  
    <meta charset="utf-8" />  
    <link rel="shortcut icon"  href="./bitbug_favicon.ico" />  
    <meta name="viewport" content="width=device-width, initial-scale=1" />  
    <meta name="theme-color" content="#FFB90F" />  
    <meta name="keywords" content="前端历劫之路">  
    <meta name="description" content="如何从前端小仙历劫成为一个前端大神呢?这里就有答案。" />  
    <title>前端历劫之路</title>  
  </head>  
  <body>  
    <noscript>You need to enable JavaScript to run this app.</noscript>  
    <div id="root"></div>  
  </body>  
</html>  

App.jsx文件内的内容什么意思现在可以先不用去关心,可以先放这。

src/App.jsx

// App.jsx  
import React from 'react';  
import { Provider } from 'react-redux';  
import store from './store/';  
import Router from './router';  
import {BrowserRouter} from 'react-router-dom';  
import {Main} from './styled/'  
import { CSSTransition, TransitionGroup } from 'react-transition-group';  
import { GlobalStyle } from '../src/styled/index';  
import HeaderArea from './components/layout/Header';  
import './App.less';  

const Body = () => {  
  return (  
    <div>  
      <BrowserRouter>  
        <GlobalStyle />  
        <HeaderArea />  
        <Main>  
          <Router />  
        </Main>  
      </BrowserRouter>  
    </div>  
  )  
}  

const App = () => {  
  return (  
    <div>  
      <Provider store={store}>  
        <TransitionGroup appear={true} >  
          <CSSTransition timeout={10000} classNames='fade'>  
            <Body />  
          </CSSTransition>  
        </TransitionGroup>  
      </Provider>  
    </div>  
  )  
};  

export default App;  

四、创建文件夹

「src目录」下分别创建以下几个文件夹React与Koa一起打造一个仿稀土掘金全栈个人博客(技术篇)

五、安装依赖

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文件,编辑内容如下:
@import '~antd/dist/antd.less';  
  1. 然后在App.jsx内引入App.less文件(上面已经编辑过App.jsx文件的这里不用管)

  2. 然后安装 craco-less 并创建修改 craco.config.js(存放在项目根目录下) 文件如下:

// craco.config.js  
const CracoLessPlugin = require('craco-less');  
const theme = require ('./theme');  

module.exports = {  
  plugins: [  
    {  
      plugin: CracoLessPlugin,  
      options: {  
        lessLoaderOptions: {  
          modifyVars: theme.theme,  
          javascriptEnabled: true,  
        },  
      },  
    }  
  ],  
};  
// theme.js  
const theme = {  
  '@primary-color': '#FFB90F', // 全局主色  
  '@link-color': '#1890ff', // 链接色  
  '@success-color': '#52c41a', // 成功色  
  '@warning-color': '#faad14', // 警告色  
  '@error-color': '#f5222d', // 错误色  
  '@font-size-base': '14px', // 主字号  
  '@heading-color': 'rgba(0, 0, 0, 0.85)', // 标题色  
  '@text-color': 'rgba(0, 0, 0, 0.65)', // 主文本色  
  '@text-color-secondary': 'rgba(0, 0, 0, 0.45)', // 次文本色  
  '@disabled-color': 'rgba(0, 0, 0, 0.25)', // 失效色  
  '@border-radius-base': '4px', // 组件/浮层圆角  
  '@border-color-base': '#d9d9d9', // 边框色  
  '@box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)' // 浮层阴影  
}  

exports.theme = theme  
七、路由懒加载

在router文件夹下创建index.js和routes.js。

routes.js

// routes.js  
// 路由配置  
import React from 'react';  
import {Route } from 'react-router-dom';  

import {Home,About,Details,Write,Circle,Noauth,Search} from './routes'  

const APPRouter = () =>(  
            <div>  
                <Route exact={true} path="/" component={Home}/>  
                <Route exact={true} path="/about/" component={About}/>  
                <Route exact={true} path="/details/:id/" component={Details} />  
                <Route exact={true} path="/write" component={Write} />  
                <Route exact={true} path="/circle" component={Circle} />  
                <Route exact={true} path="/noauth" component={Noauth} />  
                <Route exact={true} path="/search" component={Search} />  
            </div>  
);  

export default APPRouter;  

index.js

// index.js  
// 页面组件  
import loadable from '../util/loadable';  

export const Home = loadable(()=> import('../views/Home/'));  
export const About = loadable(()=> import('../views/About/'));  
export const Details = loadable(()=> import('../views/Details'));  
export const Write = loadable(()=> import('../views/Write'));  
export const Circle = loadable(()=> import('../views/Circle'));  
export const Noauth = loadable(()=>import('../components/modules/Noauth'))  
export const Search = loadable(()=>import('../views/Search'))  

在util文件夹下创建一个loadable.js。

loadable.js

// loadable.js  
// 懒加载组件  
import React from 'react';  
import Loadable from 'react-loadable';  
import styled from 'styled-components';  
import { Spin } from 'antd';  

const loadingComponent =()=>{  
    return (  
        <Loading>  
             <Spin />     
        </Loading>  
    )  
};  

export default (loader,loading = loadingComponent)=>{  
    return Loadable({  
        loader,  
        loading  
    });  
};  

const Loading = styled.div`  
    text-align: center;  
    margin:50vh 0;  
`;  
八、全局样式与样式组件

这里我们使用styled-components这个依赖写样式组件,因为在react.js中存在组件样式污染的缘故。在styled创建一个index.js。

index.js

// index.js  
// 全局样式  
import styled,{createGlobalStyle} from 'styled-components';  

export const Content = styled.div`  
    border-radius: 2px;  
    width: 100%;  
    padding:20px;  
    margin:20px 0;  
    border:1px solid #f4f4f4;  
    background:#fff;  
    box-sizing:border-box;  


export const Main = styled.div`  
  position: relative;  
  margin: 100px auto 20px;  
  width: 100%;  
  max-width: 960px;  
`;  
export const GlobalStyle = createGlobalStyle`  
html, body, div, span, applet, object, iframe,  
h1, h2, h3, h4, h5, h6, p, blockquote, pre,  
a, abbr, acronym, address, big, cite, code,  
del, dfn, em, img, ins, kbd, q, s, samp,  
small, strike, strong, sub, sup, tt, var,  
b, u, i, center,  
dl, dt, dd, ol, ul, li,  
fieldset, form, label, legend,  
table, caption, tbody, tfoot, thead, tr, th, td,  
article, aside, canvas, details, embed,   
figure, figcaption, footer, header, hgroup,   
menu, nav, output, ruby, section, summary,  
time, mark, audio, video{  
  margin: 0;  
  padding: 0;  
  border: 0;  
  font-size: 100%;  
  font: inherit;  
  font-weight: normal;  
  vertical-align: baseline;  
}  
article, aside, details, figcaption, figure,   
footer, header, hgroup, menu, nav, section{  
  display: block;  
}  
ol, ul, li{  
  list-style: none;  
}  
blockquote, q{  
  quotes: none;  
}  
blockquote:before, blockquote:after,  
q:before, q:after{  
  content: '';  
  content: none;  
}  
table{  
  border-collapse: collapse;  
  border-spacing: 0;  
}  
a{  
  color: #7e8c8d;  
  text-decoration: none;  
  -webkit-backface-visibility: hidden;  
}  
::-webkit-scrollbar{  
  width: 5px;  
  height: 5px;  
}  
::-webkit-scrollbar-track-piece{  
  background-color: rgba(0, 0, 0, 0.2);  
  -webkit-border-radius: 6px;  
}  
::-webkit-scrollbar-thumb:vertical{  
  height: 5px;  
  background-color: rgba(125, 125, 125, 0.7);  
  -webkit-border-radius: 6px;  
}  
::-webkit-scrollbar-thumb:horizontal{  
  width: 5px;  
  background-color: rgba(125, 125, 125, 0.7);  
  -webkit-border-radius: 6px;  
}  
html, body{  
  width: 100% !important;  
  background:#E8E8E8;  
  font-size: 12px;  
  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;  
}  
body{  
  line-height: 1;  
  -webkit-text-size-adjust: none;  
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);  
}  
html{  
  overflow-y: scroll;  
}  
.clearfix:before,  
.clearfix:after{  
  content: " ";  
  display: inline-block;  
  height: 0;  
  clear: both;  
  visibility: hidden;  
}  
.clearfix{  
  *zoom: 1;  
}  
.ovf{  
  overflow:hidden;  
}  
.dn{  
  display: none;  
}  
/*自定义全局*/  
p{  
  margin:10px;  
}  
.fade-enter {  
  opacity: 0;  
}  
.fade-enter-active {  
  opacity: 1;  
  transition: all .5s;  
}  
.fade-exit {  
  opacity: 1;  
  transition: all .5s;  
}  
.fade-exit-active {  
  opacity: 0;  
}  
.hide{  
  opacity: 0;  
  height: 0px;  
  transform: translatey(-100px);  
 }  

::-webkit-scrollbar {  
  width:5px;  
  height:5px;  
}  
::-webkit-scrollbar-track {  
  width: 5px;  
  background-color:#fff;  
  -webkit-border-radius: 10px;  
  -moz-border-radius: 10px;  
  border-radius:10px;  
}  
::-webkit-scrollbar-thumb {  
  background-clip:padding-box;  
  min-height:28px;  
  -webkit-border-radius: 10px;  
  -moz-border-radius: 10px;  
  border-radius:10px;  
}  
::-webkit-scrollbar-thumb:hover {  
   background-color:#FFB90F;  
}  
`;  

九、封装axios请求

在request文件夹下创建api.js和http.js。

api.js

存放api接口。

// api.js  
// 接口地址  
import {get,post} from './http';  
const url= 'https://www.maomin.club/myblog/'; // api  
// post格式  
export const reg = g => post(`${url}register`, g); // 注册  
export const log = g => post(`${url}login`, g); // 登录  
export const write = g => post(`${url}write`, g); // 写文章  
export const circle = g => post(`${url}circle`, g); // 发圈子  
export const getCircle = g => post(`${url}getCircle`, g); // 获取圈子  
export const uploadImg = g => post(`${url}uploadImg`, g); // 写文章上传图片  
export const getListapi = g => post(`${url}getList`, g); // 获取文章列表  
export const getDetails = g => post(`${url}getDetails`, g); // 获取文章详情  
export const comment = g => post(`${url}comment`, g); // 发送评论  
export const getComment = g => post(`${url}getComment`, g); // 获取评论  
export const getinfo = g => post(`${url}getinfo`, g) // 获取用户信息  
// get格式  
export const alllist = g =>get(`${url}getAllList`,g);//获取所有文章列表 

http.js

请求配置。

// http.js  
// axios配置  
import axios from 'axios';  
import { message} from 'antd';  
// 请求拦截器  
axios.interceptors.request.use(  
  config => {  
    if (localStorage.getItem('Authorization')) {  
      config.headers.Authorization = localStorage.getItem('Authorization'); //查看是否存在token  
      return config;  
    } else if (config.isUpload) {  
      config.headers = { 'Content-Type': 'multipart/form-data' } // 根据参数是否启用form-data方式  
      return config;  
    } else {  
      config.headers = { 'Content-Type': 'application/json;charset=utf-8' }  
      return config;  
    }  
  },  
  error => {  
    return Promise.error(error)  
  })  

// 响应拦截器  
axios.interceptors.response.use(  
  // 服务码是200的情况  
  response => {  
    if (response.status === 200) {  
      switch (response.data.resultCode) {  
          // token过期  
        case 2:  
          message.error('登录过期,请重新登录');  
          localStorage.removeItem('Authorization');  
          setTimeout(() => {  
            window.location.href="/";  
          }, 1000);  
          break;  
        case 3:  
          message.error('未登录');  
          break;  
        case 4:  
          message.error('请输入正确的账号或者密码');  
          break;  
        default:  
          break;  
      }  
      return Promise.resolve(response);  
    } else {  
      return Promise.reject(response)  
    }  
  },  
  // 服务器状态码不是200的情况  
  error => {  
    if (error.response.status) {  
      switch (error.response.status) {  
        // 404请求不存在  
        case 404:  
          alert('网络请求不存在');  
          break;  
          // 其他错误,直接抛出错误提示  
        default:  
          alert('error.response.data.message');  
      }  
      return Promise.reject(error.response)  
    }  
  }  
)  

/**  
 * get方法,对应get请求  
 * @param {String} url [请求的url地址]  
 * @param {Object} params [请求时携带的参数]  
 */  
export function get(url, params, config = {  
  add: ''  
}) {  
  return new Promise((resolve, reject) => {  
    axios.get(url, {  
      params: params  
    }, config).then(res => {  
      resolve(res.data)  
    }).catch(err => {  
      reject(err.data)  
    })  
  })  
}  
/**  
 * post方法,对应post请求  
 * @param {String} url [请求的url地址]  
 * @param {Object} params [请求时携带的参数]  
 */  
export function post(url, params, config = {  
  isUpload: false  
}) {  
  return new Promise((resolve, reject) => {  
    axios.post(url, params, config)  
      .then(res => {  
        resolve(res.data)  
      })  
      .catch(err => {  
        reject(err.data)  
      })  
  })  
}  
十、状态管理总配置

在store文件夹创建一个index.js和reducer.js。因为每个页面模块都有一个状态,所以我们在这个项目里采用分模块。然后我们现在的需要做的是统一管理它们每一个模块。

index.js

// index.js  
// 全局store配置  
import {createStore,applyMiddleware,compose} from 'redux';  
import thunk from 'redux-thunk';  
import reducer from './reducer';  

// redux-devtools 配置  
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?     
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;  

const enhancer = composeEnhancers(  
  // 使用中间件 thunk  
  applyMiddleware(thunk)  
);  
const store = createStore(reducer,enhancer);  

export default store;  

reducer.js

// reducer.js  
// 分模块Reducer  
import { combineReducers } from 'redux-immutable';  
import { reducer as homeReducer } from '../views/Home/store/';  
import { reducer as layoutReducer } from '../components/layout/store';  
import { reducer as aboutReducer } from '../views/About/store';  
import { reducer as detailsReducer } from '../views/Details/store';  

const reducer = combineReducers({  
  home: homeReducer,  
  layout:layoutReducer,  
  about:aboutReducer,  
  details:detailsReducer  
});  

export default reducer;  
十一、页面模块与组件模块

因页面过多,这里只展示首页模块,其他逻辑思想大差不差,如果想详细了解的可以加我微信。在views文件夹创建一个Home文件夹。依次创建如下图所示文件:React与Koa一起打造一个仿稀土掘金全栈个人博客(技术篇)index.jsx

页面组件。

// index.jsx  
import React, { useEffect, Fragment } from 'react';  
import { Link } from 'react-router-dom';  
import { connect } from 'react-redux';  
import { Pagination, Spin } from 'antd';  
import styled from 'styled-components';  
import { LeftView, RightView, Item, ContentBox, InfoBox, Meta, Title, ImgBox, SidebarBlock, ImgBlock, MoreBlock } from './styleJs/style';  
import { actionsCreator } from './store/';  

const mapStateToProps = (state) => {  
  return {  
    datalist: state.getIn(['home', 'datalist']),  
    page: state.getIn(['home', 'page']),  
    defaultCurrent: state.getIn(['home', 'defaultCurrent'])  
  }  
};  

const mapDispatchToProps = (dispatch) => {  
  return {  
    getdata(v) {  
      dispatch(actionsCreator.getList(v))  
    },  
    pageChange(v) {  
      dispatch(actionsCreator.changePage(v))  
    }  
  }  
};  
const Loading = styled.div`  
    text-align: center;  
    margin:34vh 0;  
`;  
const Home = (props) => {  
  const { datalist, getdata, page, defaultCurrent, pageChange } = props;  
  const newList = datalist.toJS();  
  useEffect(() => {  
    getdata(defaultCurrent);  
  }, [defaultCurrent, getdata])  
  return (  
    <div>  
      <LeftView>  
        {  
          page === 0 ? <Loading>  
            <Spin tip="Loading..." />  
          </Loading> : <div><div style={{ 'height': '624px' }}>  
              {  
                newList.map((item) => {  
                  return (  
                    <Fragment key={item.id}>  
                      <Link to={'/details/' + item.id}>  
                        <Item>  
                          <ContentBox>  
                            <InfoBox>  
                              <Meta>{item.tab}</Meta>  
                              <Title>{item.title}</Title>  
                            </InfoBox>  
                            <ImgBox srci={item.context.substring(item.context.indexOf("<img src='"), item.context.indexOf("' alt=''>")).replace("<img src='", "")}></ImgBox>  
                          </ContentBox>  
                        </Item>  
                      </Link>  
                    </Fragment>  
                  )  
                })  
              }  

            </div>  
            <div style={{ 'margin': '20px' }}>  
                    <Pagination defaultCurrent={defaultCurrent} total={page}  pageSize={6} onChange={pageChange}></Pagination>  
            </div>  
            </div>  
        }  
      </LeftView>  

      <RightView>  
        <SidebarBlock>  
          <ImgBlock src={require("../../assets/images/gzh.jpg")} />  
        </SidebarBlock>  
        <SidebarBlock>  
          <ImgBlock src={require("../../assets/images/wx.jpg")} />  
        </SidebarBlock>  
        <MoreBlock>  
          <div>&copy; {new Date().getFullYear()}<span>maomin.club</span>版权所有</div>  
          <a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=37021302000701">公安备案号 37021302000701号 </a>  
          <a href="http://www.beian.miit.gov.cn/"> 鲁ICP备19020856号-1</a>  
        </MoreBlock>  

      </RightView>  
    </div>  
  )  
}  

export default connect(mapStateToProps, mapDispatchToProps)(Home);  

styles/style.js

home页面的样式。

// style.js  
import styled, {keyframes }  from 'styled-components';  

const fadeIn = keyframes`  
    from {  
        opacity:0;  
    }  
    to {  
        opacity:1;  
    }  
`  

export const LeftView = styled.div`  
    border-radius: 2px;  
    width: 700px;  
    margin-right: 21.667rem;  
    border:1px solid #f4f4f4;  
    background:#fff;  
    box-sizing:border-box;  
    animation: ${fadeIn} 1s ease-in;  
`  
export const RightView = styled.div`  
    position: absolute;  
    top: 0;  
    right: 0;  
    width:20rem;  
    @media (max-width: 960px){  
      display: none;  
    }  
`  
export const Item = styled.div`  
    border-bottom: 1px solid rgba(178,186,194,.15);  
`  
export const ContentBox = styled.div`  
    display: flex;  
    align-items: center;  
    padding: 1.5rem 2rem;  
`  
export const InfoBox = styled.div`  
    flex: 1 1 auto;  
    display: flex;  
    flex-direction: column;  
    justify-content: center;  
    min-width: 0;  
`  
export const Meta = styled.div`  
     color: #b2bac2;  
`  
export const Title = styled.div`  
    margin: 1rem 0 1rem;  
    white-space: nowrap;  
    overflow: hidden;  
    text-overflow: ellipsis;  
    font-size: 1.4rem;  
    font-weight: 600;  
    line-height: 1.2;  
    color: #2e3135;  
`  

export const ImgBox = styled.div`  
    background-image:url('${props => props.srci}');  
    background-repeat: no-repeat;  
    background-size: cover;  
    flex: 0 0 auto;  
    width: 5rem;  
    height: 5rem;  
    background-color:#f4f4f4;  
    margin-left: 2rem;  
    background-color: #fff;  
    border-radius: 2px;  
    background-position: 50%;  
    animation: ${fadeIn} 1s ease-in;  
`  
export const SidebarBlock = styled.div`  
    background-color: #fff;  
    box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);  
    border-radius: 2px;  
    margin-bottom: 1.3rem;  
    font-size: 1.16rem;  
    line-height: 1.29;  
    color: #333;  
`  
export const ImgBlock = styled.img`  
  width:100%;  
  animation: ${fadeIn} 1s ease-in;  
`  
export const MoreBlock =styled.div`  
    background-color: transparent;  
    box-shadow: none;  
    a{  
      display:block;  
      line-height:22px;  
      text-decoration: none;  
      cursor: pointer;  
      color: #909090;  
    }  
    div {  
      line-height:22px;  
    }  
    span{  
      margin:0 5px;  
    }  
`  

store/actionsCreator.js

react-thunk作用:使我们可以在action中返回函数,而不是只能返回一个对象。然后我们可以在函数中做很多事情,比如发送异步的ajax请求。

// actionsCreator.js  
import {actionsTypes} from './index';  
import {getListapi} from '../../../request/api';  
import {fromJS} from 'immutable';  

const dataList =(data,page) =>{  
    return {  
        type:actionsTypes.DATA_LIST,  
        data:fromJS(data),  
        page:fromJS(page)  
    }  
};  
const currentPage = (p) =>{  
    return {  
      type:actionsTypes.CHANGE_PAGE,  
      current:p  
    }  
  }  
export const getList = (p) =>{  
    return (dispatch) =>{  
        let postData ={  
            page:p  
        }  
        getListapi(postData).then((res)=>{  
            const data = res.data;  
            const page = res.page;  
            const action = dataList(data,page);  
            dispatch(action);  
        }).catch((err)=>{  
            console.log(err);  
        })  
    }  
};  

export const changePage=(page)=>{  
    return (dispatch) =>{  
      const action = currentPage(page);  
      dispatch(action);  
  }  
  }  

store/actionsTypes.js

// actionsTypes.js  
export const DATA_LIST = 'home/DATA_LIST';  
export const CHANGE_PAGE = 'home/CHANGE_PAGE';  

store/index.js

home页面的store配置。

// index.js  
import reducer from './reducer';  
import * as actionsTypes from './actionsTypes';  
import * as actionsCreator from './actionsCreator';  

export { reducer, actionsCreator,actionsTypes};  

store/reducer.js

由于是不可变的,可以放心的对对象进行任意操作。在 React 开发中,频繁操作state对象或是 store ,配合 immutableJS 快、安全、方便。

// reducer.js  
import {actionsTypes} from './index';  
import {fromJS} from 'immutable';  

let defaultState = fromJS({  
    datalist: [],  
    page:0,  
    defaultCurrent:1  
});  

export default (state = defaultState, action) => {  
    switch (action.type) {  
        case actionsTypes.DATA_LIST:  
        return state.merge({  
            'datalist':action.data,  
            'page':action.page  
        })  
        case actionsTypes.CHANGE_PAGE:  
        return state.set('defaultCurrent',action.current)  
        default:  
            return state;  
    }  
};  

后台角度

主要资源

  • https

  • fs

  • path

  • koa

  • koa-router

  • koa2-cors

  • jsonwebtoken

  • koa-body

  • koa-static

  • koa-sslify

  • mysql

  • node-schedule

源码

后台主要是用了Koa模块,下面的源码是基于https环境。数据库是采用了创建地址池的方法,数据库的连接池负责分配,管理和释放数据库链接的。它允许应用程序重复使用一个现有的数据库的链接。而不是重新创建一个。地址池这里可以优化,这里为了看的更清楚,统一放在了一个文件里。具体详解请看下面的注释。

// app.js  
var https = require("https");//https服务  
var fs = require("fs");  
var path = require('path');  
var Koa = require('koa');  
var Router = require('koa-router');  
var cors = require('koa2-cors');  
var jwt = require('jsonwebtoken');  
var koaBody = require('koa-body'); //文件保存库  
var serve = require('koa-static');  
var enforceHttps = require('koa-sslify').default;  
var mysql = require('mysql');  
var schedule = require('node-schedule');  
var app = new Koa();  
app.use(enforceHttps());  
var router = new Router();  
var secretkey = ''; // token的key  

// 这是我的https配置文件可忽略  
var options = {  
    key: fs.readFileSync('https/2_www.maomin.club.key'),  
    cert: fs.readFileSync('https/1_www.maomin.club_bundle.crt')  
}  

// 存文件配置  
const home = serve(path.join(__dirname) + '/public/');  
app.use(home);  
app.use(koaBody({  
    multipart: true  
}));  

// 跨域  
const allowOrigins = [  
    "https://www.maomin.club/"  
];  
app.use(cors({  
    origin: function (ctx) {  
        if (allowOrigins.includes(ctx.header.origin)) {  
            return ctx.header.origin;  
        }  
        return false;  
    },  
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],  
    maxAge: 5,  
    credentials: true,  
    withCredentials: true,  
    allowMethods: ['GET', 'POST', 'DELETE'],  
    allowHeaders: ['Content-Type', 'Authorization', 'Accept'],  
}));  

// 创建地址池  
var pool = mysql.createPool({  
    host: '', // 主机  
    port: 3306, // 端口  
    user: '', // 用户  
    password: '', // 密码  
    database: '', // 数据库  
    multipleStatements: true, // 允许每个mysql语句有多条查询  
    connectionLimit: 100 // 最大连接数  
})  

// 数据库操作  
// 定时置3  
schedule.scheduleJob('10 0 0 * * *', function () {  
    console.log('update!')  
    var updateStr = 'UPDATE login SET count = ?';  
    var modSqlParams = [3];  
    pool.getConnection(function (err, conn) {  
        if (err) {  
            //do something  
            console.log(err);  
        }  
        conn.query(updateStr, modSqlParams, function (err, results) {  
            if (err) {  
                //do something  
                throw err;  
            }   
            conn.release(); //释放连接  
        })  
    })  
});  

// 检查token  
const checkToken = function (tokenid) {  
    return new Promise((resolve) => {  
        if (tokenid) {  
            //校验tokenid  
            jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解码后用户信息  
                if (err) {   //如果tokenid过期则会执行err的代码块  
                    resolve({ success: false, resultCode: 2, message: err });  
                } else {  
                    resolve("notime");  
                }  
            })  
        } else { resolve({ success: false, resultCode: 3, message: '未登录' }) }  
    })  
}  

let json = {};  
// 通用查询方法  
const query = function (sql) {  
    return new Promise((resolve, reject) => {  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(sql, function (err, results) {  
                if (err) {  
                    //do something  
                    reject(error);  
                } else {  
                    //return data or anything you want do!  
                    resolve(results);  
                }  
                conn.release(); //释放连接  
            })  
        })  
    })  
}  

// 分页  
let all = "";  
const page = function (sql, p) {  
    return new Promise((resolve, reject) => {  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(sql, function (err, results) {  
                if (err) {  
                    //do something  
                    reject(error);  
                } else {  
                    //return data or anything you want do!  
                    var allCount = results[0][0]['COUNT(*)'];  
                    all = allCount;  
                    var allPage = parseInt(allCount) / p;  
                    var pageStr = allPage.toString();  
                    if (pageStr.indexOf('.') > 0) {  
                        allPage = parseInt(pageStr.split('.')[0]) + 1;  
                    }  
                    var List = results[1];  
                    resolve(List)  
                }  
                conn.release(); //释放连接  
            })  
        })  
    })  
}  

// 登录方法  
const logQuery = function (userStr, token) {  
    return new Promise((resolve, reject) => {  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(userStr, function (err, results) {  
                if (err) {  
                    //do something  
                    reject(error);  
                } else {  
                    //return data or anything you want do!  
                    if (results.length !== 0) {  
                        var dataString = JSON.stringify(results);  
                        var data = JSON.parse(dataString);  
                        json['message'] = '登录成功';  
                        json['resultCode'] = 200;  
                        json['username'] = data[0].username;  
                        json['token'] = token;  
                        var updateStr = 'UPDATE login SET token = ? WHERE Id = ?';  
                        var modSqlParams = [token, data[0].id];  
                        pool.getConnection(function (err, conn) {  
                            if (err) {  
                                //do something  
                                console.log(err);  
                            }  
                            conn.query(updateStr, modSqlParams, function (err, results) {  
                                if (err) {  
                                    //do something  
                                    throw err;  
                                } conn.release(); //释放连接  
                            })  
                        })  
                        resolve(json);  
                    } else {  
                        resolve({ success: false, resultCode: 4, message: '请输入正确的账号或密码' });  
                    }  
                }  
                conn.release(); //释放连接  
            })  
        })  
    })  
}  

//注册方法  
const regQuery = function (userStr, name, passwd, token, count) {  
    return new Promise((resolve, reject) => {  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(userStr, function (err, result) {  
                if (err) {  
                    //do something  
                    reject(error);  
                } else {  
                    //return data or anything you want do!  
                    if (result.length > 0) {  
                        json['message'] = '用户已经存在';  
                        json['resultCode'] = 1;  
                    } else {  
                        json['message'] = '注册成功';  
                        json['token'] = token;  
                        json['username'] = name;  
                        json['count'] = count;  
                        json['resultCode'] = 200;  
                        var insertStr = `insert into login (username, password,token,count) values ("${name}", "${passwd}","${token}","${count}")`;  
                        pool.getConnection(function (err, conn) {  
                            if (err) {  
                                //do something  
                                console.log(err);  
                            }  
                            conn.query(insertStr, function (err, results) {  
                                if (err) {  
                                    //do something  
                                    throw err;  
                                } conn.release(); //释放连接  
                            })  
                        })  
                    }  
                    resolve(json)  
                }  
                conn.release(); //释放连接  
            })  
        })  
    })  
}  

// 评论方法  
const commentQuery = function (userStr, aid) {  
    return new Promise((resolve, reject) => {  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(userStr, async function (err) {  
                if (err) {  
                    //do something  
                    reject(error);  
                } else {  
                    //return data or anything you want do!  
                    json['message'] = '评论成功';  
                    json['success'] = true;  
                    let sql = `select aid,username,com from comment where aid="${aid}"`;  
                    let results = await query(sql);  
                    json['data'] = results;  
                    resolve(json);  
                }  
                conn.release(); //释放连接  
            })  
        })  
    })  
}  

// 发圈子方法  
const setCount = function (userStr, username, imgsrc, inputValue, td) {  
    return new Promise((resolve, reject) => {  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(userStr, function (err, results) {  
                if (err) {  
                    //do something  
                    reject(error);  
                } else {  
                    //return data or anything you want do!  
                    var dataString = JSON.stringify(results);  
                    var data = JSON.parse(dataString);  
                    if (data[0].count > 0) {  
                        var newCount = data[0].count - 1;  
                        json['message'] = '发表成功';  
                        json['resultCode'] = 200;  
                        json['success'] = true;  
                        json['count'] = newCount;  
                        // 次数减一  
                        var updateStr = 'UPDATE login SET count = ? WHERE username = ?';  
                        var modSqlParams = [newCount, username];  
                        pool.getConnection(function (err, conn) {  
                            if (err) {  
                                //do something  
                                console.log(err);  
                            }  
                            conn.query(updateStr, modSqlParams, function (err) {  
                                if (err) {  
                                //do something  
                                throw err;  
                                } conn.release(); //释放连接  
                            })  
                        })  
                        // 存入圈子数据库  
                        var insetStr = `insert into circle (username, imgsrc, inputValue, td) values ("${username}","${imgsrc}","${inputValue}","${td}")`  
                        pool.getConnection(function (err, conn) {  
                            if (err) {  
                                //do something  
                                console.log(err);  
                            }  
                            conn.query(insetStr, modSqlParams, function (err) {  
                                if (err) {  
                                    //do something  
                                    throw err;  
                                } conn.release(); //释放连接  
                            })  
                        })  
                        resolve(json);  
                    } else {  
                        resolve({ success: false, resultCode: 5, message: '操作太频繁,请明天再发哦' });  
                    }  
                }  
                conn.release(); //释放连接  
            })  
        })  
    })  
}  

// 用户信息方法  
const getInfo = function (tokenid) {  
    return new Promise((resolve) => {  
        if (tokenid) {  
            //校验tokenid  
            jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解码后用户信息  
                if (err) {   //如果tokenid过期则会执行err的代码块  
                    resolve({ success: false, resultCode: 2, message: err });  
                } else {  
                    resolve(decoded);  
                }  
            })  
        } else { resolve({ success: false, resultCode: 3, message: '未登录' }) }  
    })  
}  

// 获取用户信息  
router.post('/getinfo', async (ctx, next) => {  
    var tokenid = ctx.request.body.token;  
    let results = await getInfo(tokenid);  
    ctx.body = results;  
})  

// 注册  
router.post('/register', async (ctx, next) => {  
    let name = ctx.request.body.username;  
    let passwd = ctx.request.body.password;  
    let count = 3;  
    let token = jwt.sign({  
        username: name  
    }, secretkey, {  
        expiresIn: 60 * 60 * 12 // 12h  
    });  
    let userStr = `select * from login where username="${name}"`;  
    let results = await regQuery(userStr, name, passwd, token, count);  
    ctx.body = results  
});  

// 登录  
router.post('/login', async (ctx, next) => {  
    let name = ctx.request.body.username;  
    let passwd = ctx.request.body.password;  
    let token = jwt.sign({  
        username: name  
    }, secretkey, {  
        expiresIn: 60 * 60 * 12 // 12h  
    });  
    let userStr = `select username,password,id from login where username="${name}" and password="${passwd}"`;  
    let results = await logQuery(userStr, token);  
    ctx.body = results  
});  

// 写评论  
router.post('/comment', async (ctx, next) => {  
    let aid = ctx.request.body.aid;  
    let username = ctx.request.body.username;  
    let com = ctx.request.body.com;  
    let td = ctx.request.body.td;  
    var tokenid = ctx.request.headers.authorization//获取前端请求头发送过来的tokenid  
    let trueFlase = await checkToken(tokenid);  
    if (trueFlase === "notime") {  
        let userStr = `insert into comment (aid, username, com, td) values ("${aid}","${username}","${com}","${td}")`  
        let results = await commentQuery(userStr, aid);  
        ctx.body = results;  
    } else {  
        ctx.body = trueFlase;  
    }  
})  

// 获取评论  
router.post('/getComment', async (ctx, next) => {  
    var start = (ctx.request.body.page - 1) * 3;  
    let aid = ctx.request.body.aid;  
    var count = `SELECT * FROM comment WHERE aid="${aid}"`;  
    let allnum = await query(count);  
    const len = allnum.length;  
    var sql = `SELECT COUNT(*) FROM comment ORDER BY id DESC;SELECT * FROM comment WHERE aid="${aid}" ORDER BY id DESC limit ${start},3`;  
    let results = await page(sql, 3);  
    ctx.body = {  
        data: results,  
        page: len  
    }  
}  
)  

// 写文章  
router.post('/write', async (ctx, next) => {  
    let title = ctx.request.body.title;  
    let tab = ctx.request.body.tab;  
    let context = ctx.request.body.context;  
    var tokenid = ctx.request.headers.authorization//获取前端请求头发送过来的tokenid  
    let trueFlase = await checkToken(tokenid);  
    if (trueFlase === "notime") {  
        var userStr = `insert into article (title, tab, context) values ("${title}","${tab}","${context}")`  
        pool.getConnection(function (err, conn) {  
            if (err) {  
                //do something  
                console.log(err);  
            }  
            conn.query(userStr, function (err) {  
                if (err) {  
                    //do something  
                    throw err;  
                } conn.release(); //释放连接  
            })  
        })          
        ctx.body = { success: true, message: '发送成功' } // echo the result back  
    } else {  
        ctx.body = trueFlase;  
    }  

});  

// 写文章上传图片  
router.post('/uploadImg', async (ctx, next) => {  
    if (ctx.request.files.file) {  
        var file = ctx.request.files.file;  
        // 创建可读流  
        var reader = fs.createReadStream(file.path);  
        // 修改文件的名称  
        var myDate = new Date();  
        var newFilename = myDate.getTime() + '.' + file.name.split('.')[1];  
        var targetPath = path.join(__dirname, './public/images/') + `${newFilename}`;  
        //创建可写流  
        var upStream = fs.createWriteStream(targetPath);  
        // 可读流通过管道写入可写流  
        reader.pipe(upStream);  
        var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename;  
        ctx.body = {  
            success: true,  
            imgsrc: imgsrc  
        };  
    }  
})  

// 发圈子  
router.post('/circle', async (ctx, next) => {  
    if (ctx.request.files.file) {  
        var file = ctx.request.files.file;  
        // 创建可读流  
        var reader = fs.createReadStream(file.path);  
        // 修改文件的名称  
        var myDate = new Date();  
        var newFilename = myDate.getTime() + '.' + file.name.split('.')[1];  
        var targetPath = path.join(__dirname, './public/images/') + `${newFilename}`;  
        //创建可写流  
        var upStream = fs.createWriteStream(targetPath);  
        // 可读流通过管道写入可写流  
        reader.pipe(upStream);  
        var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename;  
    } else {  
        var imgsrc = ""  
    }  
    let username = ctx.request.body.username;  
    let inputValue = ctx.request.body.inputValue;  
    let td = ctx.request.body.td;  
    var tokenid = ctx.request.headers.authorization//获取前端请求头发送过来的tokenid  
    let trueFlase = await checkToken(tokenid);  
    if (trueFlase === "notime") {  
        let userStr = `select count from login where username="${username}"`;  
        let results = await setCount(userStr, username, imgsrc, inputValue, td);  
        ctx.body = results;  
    } else {  
        ctx.body = trueFlase;  
    }  
});  

// 获取圈子  
router.post('/getCircle', async (ctx, next) => {  
    var start = (ctx.request.body.page - 1) * 3;  
    var sql = 'SELECT COUNT(*) FROM circle ORDER BY id DESC; SELECT * FROM circle ORDER BY id DESC limit ' + start + ',3';  
    let results = await page(sql, 3);  
    ctx.body = {  
        data: results,  
        page: all  
    }  
});  

// 获取文章列表(分页)  
router.post('/getList', async (ctx, next) => {  
    var start = (ctx.request.body.page - 1) * 6;  
    var sql = 'SELECT COUNT(*) FROM article ORDER BY id DESC; SELECT * FROM article ORDER BY id DESC limit ' + start + ',6';  
    let results = await page(sql, 6);  
    ctx.body = {  
        data: results,  
        page: all  
    }  
});  

// 获取文章列表(全部)  
router.get('/getAllList', async (ctx, next) => {  
    var sql = "select * from article";  
    let results = await query(sql);  
    ctx.body = results  
});  

// 获取文章详情  
router.post('/getDetails', async (ctx, next) => {  
    const id = ctx.request.body.id;  
    var sql = `select * from article where id="${id}"`;  
    let results = await query(sql);  
    ctx.body = results  
});  

//使用路由中间件  
app  
    .use(router.routes())  
    .use(router.allowedMethods());  

https.createServer(options, app.callback()).listen(8410);  
console.log('服务器运行中')  

作者:「Vam的金豆之路」

主要领域:「前端开发」

我的微信:「maomin9761」

微信公众号:「前端历劫之路」

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


本文转转自微信公众号前端历劫之路原创https://mp.weixin.qq.com/s/B4_r-QJmyD73tXT-EHsktQ,如有侵权,请联系删除。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
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
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这