
前言
我的个人博客样式布局是仿的稀土掘金 ,个人博客线上网址: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 
模块页面
- 首页 
- 登录注册 
- 文章详情 
- 文章评论 
- 圈子 
- 写圈子 
- 搜索页 
- 权限页 
- 写文章 
项目配置
项目目录

在这里插入图片描述
前台搭建项目步骤
一、使用稳定依赖管理工具
推荐你使用淘宝源
npm config set registry https://registry.npm.taobao.org  
还有就是搭配依赖管理工具yarn
二、使用官方React脚手架
create-react-app my-project  
三、精简项目文件夹
使用脚手架搭建的初始文件夹是这样的。 那么我们需要精简一下。注意原来的
那么我们需要精简一下。注意原来的App.js我改成App.jsx。因为 React 使用 JSX 来替代常规的 JavaScript,所以用JSX比较好。 下面我们将要编辑几个文件:
下面我们将要编辑几个文件: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目录」下分别创建以下几个文件夹
五、安装依赖
dependencies:
- antd 
- axios 
- for-editor 
- immutable 
- react-loadable 
- react-redux 
- react-router-dom 
- react-transition-group 
- redux 
- redux-immutable 
- redux-thunk 
- styled-components 
六、配置自定义主题
按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能。我们可以引入 craco-less 来帮助加载 less 样式和修改变量。
- 首先在src目录下创建一个App.less文件,编辑内容如下:
@import '~antd/dist/antd.less';  
- 然后在App.jsx内引入App.less文件(上面已经编辑过App.jsx文件的这里不用管) 
- 然后安装 - 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文件夹。依次创建如下图所示文件:
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>© {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」
微信公众号:「前端历劫之路」
❞
本文转转自微信公众号前端历劫之路原创https://mp.weixin.qq.com/s/B4_r-QJmyD73tXT-EHsktQ,如有侵权,请联系删除。

 
  
  
  
 
 
 
 
 