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

2021-11-30 20:56:02 浏览数 (1)

前言

我的个人博客样式布局是仿的稀土掘金 ,为了浏览体验,可以用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. 写文章

项目配置

项目目录

在这里插入图片描述

前台搭建项目步骤
一、使用稳定依赖管理工具

推荐你使用淘宝源

代码语言:javascript复制
npm config set registry https://registry.npm.taobao.org

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

二、使用官方React脚手架
代码语言:javascript复制
create-react-app my-project
三、精简项目文件夹

使用脚手架搭建的初始文件夹是这样的。

那么我们需要精简一下。注意原来的App.js我改成App.jsx。因为 React 使用 JSX 来替代常规的 JavaScript,所以用JSX比较好。

下面我们将要编辑几个文件:src/index.js

代码语言:javascript复制
// 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

代码语言:javascript复制
<!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

代码语言:javascript复制
// 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 样式和修改变量。

  1. 首先在src目录下创建一个App.less文件,编辑内容如下:
代码语言:javascript复制
@import '~antd/dist/antd.less';
  1. 然后在App.jsx内引入App.less文件(上面已经编辑过App.jsx文件的这里不用管)
  2. 然后安装 craco-less 并创建修改 craco.config.js(存放在项目根目录下) 文件如下:
代码语言:javascript复制
// craco.config.js
const CracoLessPlugin = require('craco-less');
const theme = require ('./theme');

module.exports = {
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          modifyVars: theme.theme,
          javascriptEnabled: true,
        },
      },
    }
  ],
};
代码语言:javascript复制
// 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

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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接口。

代码语言:javascript复制
// 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

请求配置。

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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.jsxindex.jsx

页面组件。

代码语言:javascript复制
// 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页面的样式。

代码语言:javascript复制
// 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请求。

代码语言:javascript复制
// 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

代码语言:javascript复制
// actionsTypes.js
export const DATA_LIST = 'home/DATA_LIST';
export const CHANGE_PAGE = 'home/CHANGE_PAGE';

store/index.js

home页面的store配置。

代码语言:javascript复制
// 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 快、安全、方便。

代码语言:javascript复制
// 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环境。数据库是采用了创建地址池的方法,数据库的连接池负责分配,管理和释放数据库链接的。它允许应用程序重复使用一个现有的数据库的链接。而不是重新创建一个。地址池这里可以优化,这里为了看的更清楚,统一放在了一个文件里。具体详解请看下面的注释。

代码语言:javascript复制
// 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('服务器运行中')

0 人点赞