React與Koa一起打造一個(gè)仿稀土掘金全棧個(gè)人博客(技術(shù)篇)

前言

我的個(gè)人博客樣式布局是仿的稀土掘金 ,個(gè)人博客線上網(wǎng)址為https://www.maomin.club/ ,也可以百度搜索前端歷劫之路 。為了瀏覽體驗(yàn),可以用PC瀏覽器瀏覽。

本篇文章將分為前臺(tái)角度與后臺(tái)角度來(lái)分析我是怎么開發(fā)的。
前臺(tái)角度
主要資源

    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

模塊頁(yè)面

    首頁(yè)

    登錄注冊(cè)

    文章詳情

    文章評(píng)論

    圈子

    寫圈子

    搜索頁(yè)

    權(quán)限頁(yè)

    寫文章

項(xiàng)目配置
項(xiàng)目目錄





























在這里插入圖片描述
前臺(tái)搭建項(xiàng)目步驟
一、使用穩(wěn)定依賴管理工具

推薦你使用淘寶源

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

 

還有就是搭配依賴管理工具yarn
二、使用官方React腳手架

create-react-app my-project

 

三、精簡(jiǎn)項(xiàng)目文件夾

使用腳手架搭建的初始文件夾是這樣的。





































在這里插入圖片描述
那么我們需要精簡(jiǎn)一下。注意原來(lái)的App.js我改成App.jsx。因?yàn)?React 使用 JSX 來(lái)替代常規(guī)的 JavaScript,所以用JSX比較好。































在這里插入圖片描述
下面我們將要編輯幾個(gè)文件:
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="如何從前端小仙歷劫成為一個(gè)前端大神呢?這里就有答案。" />
    <title>前端歷劫之路</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

 

App.jsx文件內(nèi)的內(nèi)容什么意思現(xiàn)在可以先不用去關(guān)心,可以先放這。

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;

 

四、創(chuàng)建文件夾

在src目錄下分別創(chuàng)建以下幾個(gè)文件夾












在這里插入圖片描述
五、安裝依賴

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 來(lái)幫助加載 less 樣式和修改變量。

    首先在src目錄下創(chuàng)建一個(gè)App.less文件,編輯內(nèi)容如下:

@import '~antd/dist/antd.less';

    1

    然后在App.jsx內(nèi)引入App.less文件(上面已經(jīng)編輯過(guò)App.jsx文件的這里不用管)
    然后安裝 craco-less 并創(chuàng)建修改 craco.config.js(存放在項(xiàng)目根目錄下) 文件如下:

// 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', // 錯(cuò)誤色
  '@font-size-base': '14px', // 主字號(hào)
  '@heading-color': 'rgba(0, 0, 0, 0.85)', // 標(biāo)題色
  '@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文件夾下創(chuàng)建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
// 頁(yè)面組件
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文件夾下創(chuàng)建一個(gè)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這個(gè)依賴寫樣式組件,因?yàn)樵趓eact.js中存在組件樣式污染的緣故。
在styled創(chuàng)建一個(gè)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請(qǐng)求

在request文件夾下創(chuàng)建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); // 注冊(cè)
export const log = g => post(`${url}login`, g); // 登錄
export const write = g => post(`${url}write`, g); // 寫文章
export const circle = g => post(`${url}circle`, g); // 發(fā)圈子
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); // 發(fā)送評(píng)論
export const getComment = g => post(`${url}getComment`, g); // 獲取評(píng)論
export const getinfo = g => post(`${url}getinfo`, g) // 獲取用戶信息
// get格式
export const alllist = g =>get(`${url}getAllList`,g);//獲取所有文章列表

 

http.js
請(qǐng)求配置。

// http.js
// axios配置
import axios from 'axios';
import { message} from 'antd';
// 請(qǐng)求攔截器
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' } // 根據(jù)參數(shù)是否啟用form-data方式
      return config;
    } else {
      config.headers = { 'Content-Type': 'application/json;charset=utf-8' }
      return config;
    }
  },
  error => {
    return Promise.error(error)
  })

// 響應(yīng)攔截器
axios.interceptors.response.use(
  // 服務(wù)碼是200的情況
  response => {
    if (response.status === 200) {
      switch (response.data.resultCode) {
          // token過(guò)期
        case 2:
          message.error('登錄過(guò)期,請(qǐng)重新登錄');
          localStorage.removeItem('Authorization');
          setTimeout(() => {
            window.location.href="/";
          }, 1000);
          break;
        case 3:
          message.error('未登錄');
          break;
        case 4:
          message.error('請(qǐng)輸入正確的賬號(hào)或者密碼');
          break;
        default:
          break;
      }
      return Promise.resolve(response);
    } else {
      return Promise.reject(response)
    }
  },
  // 服務(wù)器狀態(tài)碼不是200的情況
  error => {
    if (error.response.status) {
      switch (error.response.status) {
        // 404請(qǐng)求不存在
        case 404:
          alert('網(wǎng)絡(luò)請(qǐng)求不存在');
          break;
          // 其他錯(cuò)誤,直接拋出錯(cuò)誤提示
        default:
          alert('error.response.data.message');
      }
      return Promise.reject(error.response)
    }
  }
)

/**
 * get方法,對(duì)應(yīng)get請(qǐng)求
 * @param {String} url [請(qǐng)求的url地址]
 * @param {Object} params [請(qǐng)求時(shí)攜帶的參數(shù)]
 */
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方法,對(duì)應(yīng)post請(qǐng)求
 * @param {String} url [請(qǐng)求的url地址]
 * @param {Object} params [請(qǐng)求時(shí)攜帶的參數(shù)]
 */
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)
      })
  })
}

 

十、狀態(tài)管理總配置

在store文件夾創(chuàng)建一個(gè)index.js和reducer.js。因?yàn)槊總€(gè)頁(yè)面模塊都有一個(gè)狀態(tài),所以我們?cè)谶@個(gè)項(xiàng)目里采用分模塊。然后我們現(xiàn)在的需要做的是統(tǒng)一管理它們每一個(gè)模塊。

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;

 

十一、頁(yè)面模塊與組件模塊

因頁(yè)面過(guò)多,這里只展示首頁(yè)模塊,其他邏輯思想大差不差,如果想詳細(xì)了解的可以加我微信。
在views文件夾創(chuàng)建一個(gè)Home文件夾。依次創(chuàng)建如下圖所示文件:

















index.jsx
頁(yè)面組件。

// 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>版權(quán)所有</div>
          <a >公安備案號(hào) 37021302000701號(hào) </a>
          <a > 魯ICP備19020856號(hào)-1</a>
        </MoreBlock>

      </RightView>
    </div>
  )
}

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

 

styles/style.js
home頁(yè)面的樣式。

// 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中返回函數(shù),而不是只能返回一個(gè)對(duì)象。然后我們可以在函數(shù)中做很多事情,比如發(fā)送異步的ajax請(qǐng)求。

// 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頁(yè)面的store配置。

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

export { reducer, actionsCreator,actionsTypes};

 

store/reducer.js
由于是不可變的,可以放心的對(duì)對(duì)象進(jìn)行任意操作。在 React 開發(fā)中,頻繁操作state對(duì)象或是 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;
    }
};

 

源碼

后臺(tái)主要是用了Koa模塊,下面的源碼是基于https環(huán)境。數(shù)據(jù)庫(kù)是采用了創(chuàng)建地址池的方法,數(shù)據(jù)庫(kù)的連接池負(fù)責(zé)分配,管理和釋放數(shù)據(jù)庫(kù)鏈接的。它允許應(yīng)用程序重復(fù)使用一個(gè)現(xiàn)有的數(shù)據(jù)庫(kù)的鏈接。而不是重新創(chuàng)建一個(gè)。地址池這里可以優(yōu)化,這里為了看的更清楚,統(tǒng)一放在了一個(gè)文件里。具體詳解請(qǐng)看下面的注釋。

// app.js
var https = require("https");//https服務(wù)
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'); //文件保存庫(kù)
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'],
}));

// 創(chuàng)建地址池
var pool = mysql.createPool({
    host: '', // 主機(jī)
    port: 3306, // 端口
    user: '', // 用戶
    password: '', // 密碼
    database: '', // 數(shù)據(jù)庫(kù)
    multipleStatements: true, // 允許每個(gè)mysql語(yǔ)句有多條查詢
    connectionLimit: 100 // 最大連接數(shù)
})

// 數(shù)據(jù)庫(kù)操作
// 定時(shí)置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) {
            //校驗(yàn)tokenid
            jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解碼后用戶信息
                if (err) {   //如果tokenid過(guò)期則會(huì)執(zhí)行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(); //釋放連接
            })
        })
    })
}

// 分頁(yè)
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: '請(qǐng)輸入正確的賬號(hào)或密碼' });
                    }
                }
                conn.release(); //釋放連接
            })
        })
    })
}

//注冊(cè)方法
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'] = '用戶已經(jīng)存在';
                        json['resultCode'] = 1;
                    } else {
                        json['message'] = '注冊(cè)成功';
                        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(); //釋放連接
            })
        })
    })
}

// 評(píng)論方法
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'] = '評(píng)論成功';
                    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(); //釋放連接
            })
        })
    })
}

// 發(fā)圈子方法
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'] = '發(fā)表成功';
                        json['resultCode'] = 200;
                        json['success'] = true;
                        json['count'] = newCount;
                        // 次數(shù)減一
                        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(); //釋放連接
                            })
                        })
                        // 存入圈子數(shù)據(jù)庫(kù)
                        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: '操作太頻繁,請(qǐng)明天再發(fā)哦' });
                    }
                }
                conn.release(); //釋放連接
            })
        })
    })
}

// 用戶信息方法
const getInfo = function (tokenid) {
    return new Promise((resolve) => {
        if (tokenid) {
            //校驗(yàn)tokenid
            jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解碼后用戶信息
                if (err) {   //如果tokenid過(guò)期則會(huì)執(zhí)行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;
})

// 注冊(cè)
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
});

// 寫評(píng)論
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//獲取前端請(qǐng)求頭發(fā)送過(guò)來(lái)的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;
    }
})

// 獲取評(píng)論
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//獲取前端請(qǐng)求頭發(fā)送過(guò)來(lái)的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: '發(fā)送成功' } // 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;
        // 創(chuàng)建可讀流
        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}`;
        //創(chuàng)建可寫流
        var upStream = fs.createWriteStream(targetPath);
        // 可讀流通過(guò)管道寫入可寫流
        reader.pipe(upStream);
        var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename;
        ctx.body = {
            success: true,
            imgsrc: imgsrc
        };
    }
})

// 發(fā)圈子
router.post('/circle', async (ctx, next) => {
    if (ctx.request.files.file) {
        var file = ctx.request.files.file;
        // 創(chuàng)建可讀流
        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}`;
        //創(chuàng)建可寫流
        var upStream = fs.createWriteStream(targetPath);
        // 可讀流通過(guò)管道寫入可寫流
        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//獲取前端請(qǐng)求頭發(fā)送過(guò)來(lái)的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
    }
});

// 獲取文章列表(分頁(yè))
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('服務(wù)器運(yùn)行中')
 

 

作者:Vam的金豆之路

主要領(lǐng)域:前端開發(fā)

我的微信:maomin9761

微信公眾號(hào):前端歷劫之路