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

在這裏插入圖片描述

前言

我的個人博客樣式佈局是仿的稀土掘金 ,個人博客線上網址爲https://www.maomin.club/ ,也可以百度搜索前端歷劫之路 。爲了瀏覽體驗,可以用PC瀏覽器瀏覽。

本篇文章將分爲前臺角度與後臺角度來分析我是怎麼開發的。

前臺角度

主要資源

  • react.js
  • ant Design
  • for-editor
  • axios
  • craco-less
  • immutable
  • react-loadable
  • react-redux
  • react-router-dom
  • react-transition-group
  • redux
  • redux-immutable
  • redux-thunk
  • styled-components

模塊頁面

  1. 首頁

  2. 登錄註冊

  3. 文章詳情

  4. 文章評論

  5. 圈子

  6. 寫圈子

  7. 搜索頁

  8. 權限頁

  9. 寫文章

項目配置

項目目錄

在這裏插入圖片描述

前臺搭建項目步驟

一、使用穩定依賴管理工具

推薦你使用淘寶源

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 樣式和修改變量。

  1. 首先在src目錄下創建一個App.less文件,編輯內容如下:
@import '~antd/dist/antd.less';
  1. 然後在App.jsx內引入App.less文件(上面已經編輯過App.jsx文件的這裏不用管)
  2. 然後安裝 craco-less 並創建修改 craco.config.js(存放在項目根目錄下) 文件如下:
// craco.config.js
const CracoLessPlugin = require('craco-less');
const theme = require ('./theme');

module.exports = {
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          modifyVars: theme.theme,
          javascriptEnabled: true,
        },
      },
    }
  ],
};
// theme.js
const theme = {
  '@primary-color': '#FFB90F', // 全局主色
  '@link-color': '#1890ff', // 鏈接色
  '@success-color': '#52c41a', // 成功色
  '@warning-color': '#faad14', // 警告色
  '@error-color': '#f5222d', // 錯誤色
  '@font-size-base': '14px', // 主字號
  '@heading-color': 'rgba(0, 0, 0, 0.85)', // 標題色
  '@text-color': 'rgba(0, 0, 0, 0.65)', // 主文本色
  '@text-color-secondary': 'rgba(0, 0, 0, 0.45)', // 次文本色
  '@disabled-color': 'rgba(0, 0, 0, 0.25)', // 失效色
  '@border-radius-base': '4px', // 組件/浮層圓角
  '@border-color-base': '#d9d9d9', // 邊框色
  '@box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)' // 浮層陰影
}

exports.theme = theme
七、路由懶加載

在router文件夾下創建index.js和routes.js。

routes.js

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

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

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

export default APPRouter;

index.js

// index.js
// 頁面組件
import loadable from '../util/loadable';

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

在util文件夾下創建一個loadable.js。

loadable.js

// loadable.js
// 懶加載組件
import React from 'react';
import Loadable from 'react-loadable';
import styled from 'styled-components';
import { Spin } from 'antd';

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

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

const Loading = styled.div`
    text-align: center;
    margin:50vh 0;
`;
八、全局樣式與樣式組件

這裏我們使用styled-components這個依賴寫樣式組件,因爲在react.js中存在組件樣式污染的緣故。
在styled創建一個index.js。

index.js

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

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

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

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

九、封裝axios請求

在request文件夾下創建api.js和http.js。

api.js
存放api接口。

// api.js
// 接口地址
import {get,post} from './http';
const url= 'https://www.maomin.club/myblog/'; // api
// post格式
export const reg = g => post(`${url}register`, g); // 註冊
export const log = g => post(`${url}login`, g); // 登錄
export const write = g => post(`${url}write`, g); // 寫文章
export const circle = g => post(`${url}circle`, g); // 髮圈子
export const getCircle = g => post(`${url}getCircle`, g); // 獲取圈子
export const uploadImg = g => post(`${url}uploadImg`, g); // 寫文章上傳圖片
export const getListapi = g => post(`${url}getList`, g); // 獲取文章列表
export const getDetails = g => post(`${url}getDetails`, g); // 獲取文章詳情
export const comment = g => post(`${url}comment`, g); // 發送評論
export const getComment = g => post(`${url}getComment`, g); // 獲取評論
export const getinfo = g => post(`${url}getinfo`, g) // 獲取用戶信息
// get格式
export const alllist = g =>get(`${url}getAllList`,g);//獲取所有文章列表 

http.js
請求配置。

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

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

/**
 * get方法,對應get請求
 * @param {String} url [請求的url地址]
 * @param {Object} params [請求時攜帶的參數]
 */
export function get(url, params, config = {
  add: ''
}) {
  return new Promise((resolve, reject) => {
    axios.get(url, {
      params: params
    }, config).then(res => {
      resolve(res.data)
    }).catch(err => {
      reject(err.data)
    })
  })
}
/**
 * post方法,對應post請求
 * @param {String} url [請求的url地址]
 * @param {Object} params [請求時攜帶的參數]
 */
export function post(url, params, config = {
  isUpload: false
}) {
  return new Promise((resolve, reject) => {
    axios.post(url, params, config)
      .then(res => {
        resolve(res.data)
      })
      .catch(err => {
        reject(err.data)
      })
  })
}
十、狀態管理總配置

在store文件夾創建一個index.js和reducer.js。因爲每個頁面模塊都有一個狀態,所以我們在這個項目裏採用分模塊。然後我們現在的需要做的是統一管理它們每一個模塊。

index.js

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

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

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

export default store;

reducer.js

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

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

export default reducer;
十一、頁面模塊與組件模塊

因頁面過多,這裏只展示首頁模塊,其他邏輯思想大差不差,如果想詳細瞭解的可以加我微信。
在views文件夾創建一個Home文件夾。依次創建如下圖所示文件:
在這裏插入圖片描述
index.jsx
頁面組件。

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

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

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

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

      <RightView>
        <SidebarBlock>
          <ImgBlock src={require("../../assets/images/gzh.jpg")} />
        </SidebarBlock>
        <SidebarBlock>
          <ImgBlock src={require("../../assets/images/wx.jpg")} />
        </SidebarBlock>
        <MoreBlock>
          <div>&copy; {new Date().getFullYear()}<span>maomin.club</span>版權所有</div>
          <a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=37021302000701">公安備案號 37021302000701</a>
          <a href="http://www.beian.miit.gov.cn/">ICP19020856-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

微信公衆號:前端歷劫之路


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章