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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 本篇文章将分为前台角度与后台角度来分析我是怎么开发的。

前台角度


主要资源


  • 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/"> 鲁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('服务器运行中')



相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
20天前
|
开发框架 Dart 前端开发
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
【4月更文挑战第30天】对比 Flutter(Dart,强类型,Google支持,快速热重载,高性能渲染)与 React Native(JavaScript,庞大生态,热重载,依赖原生渲染),文章讨论了开发语言、生态系统、性能、开发体验、学习曲线、社区支持及项目选择因素。两者各有优势,选择取决于项目需求、团队技能和长期维护考虑。参考文献包括官方文档和性能比较文章。
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
|
20天前
|
存储 前端开发 中间件
🚀React+Node全栈无死角解析,吃透文件上传的各个场景
🚀React+Node全栈无死角解析,吃透文件上传的各个场景
|
7天前
|
前端开发 NoSQL JavaScript
基于 React + Nest 全栈开发的后台系统
这篇文章介绍了一个基于React+Nest全栈开发的后台系统XmwAdmin。项目包括前端和后端技术栈,线上预览地址和登录信息。作者推荐使用pnpm包管理工具和特定的环境依赖。文章提供了项目的运行和编译代码,以及各个功能模块的介绍。还包括演示图和项目活动以及总结部分。数据库下载链接也提供了,该项目已完成后台的核心功能。
基于 React + Nest 全栈开发的后台系统
|
13天前
|
前端开发 JavaScript Android开发
使用React Native开发跨平台移动应用的技术详解
【5月更文挑战第22天】本文详述了使用React Native开发跨平台移动应用的技术,该框架由Facebook推出,基于JavaScript,支持iOS和Android。React Native通过JNI/JSI实现JavaScript到原生代码的转换,提供高效性能和原生体验。其优势包括跨平台性、原生体验、开发速度及社区支持。开发流程涉及环境搭建、项目创建、编码、调试与测试,以及构建与发布。注意事项包括性能优化、平台适配、利用第三方库和持续学习。React Native为开发者构建高质量跨平台应用提供了便捷途径,未来潜力无限。
|
20天前
|
前端开发 JavaScript 开发者
【专栏:HTML与CSS前端技术趋势篇】前端框架(React/Vue/Angular)与HTML/CSS的结合使用
【4月更文挑战第30天】前端框架React、Vue和Angular助力UI开发,通过组件化、状态管理和虚拟DOM提升效率。这些框架与HTML/CSS结合,使用模板语法、样式管理及组件化思想。未来趋势包括框架简化、Web组件标准采用和CSS在框架中角色的演变。开发者需紧跟技术发展,掌握新工具,提升开发效能。
|
20天前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript与React的完美结合
【4月更文挑战第30天】React和TypeScript在前端开发中备受推崇。React以其组件化、高性能和灵活的生态系统引领UI构建,而TypeScript通过静态类型检查和面向对象特性增强了代码的健壮性和可维护性。两者结合,能提升开发效率,降低错误,使React组件结构更清晰。通过安装TypeScript,配置tsconfig.json,然后用TypeScript编写和打包代码,可实现两者的无缝集成。这种结合为前端开发带来更强的代码质量和团队协作效果,随着技术发展,其应用将更加广泛。
|
20天前
|
SQL 存储 前端开发
React&Nest.js全栈社区平台(五)——👋封装通用分页Service实现文章流与详情
React&Nest.js全栈社区平台(五)——👋封装通用分页Service实现文章流与详情
React&Nest.js全栈社区平台(五)——👋封装通用分页Service实现文章流与详情
|
20天前
|
存储 前端开发 API
React&Nest.js全栈社区平台(三)——🐘对象存储是什么?为什么要用它?
React&Nest.js全栈社区平台(三)——🐘对象存储是什么?为什么要用它?
|
20天前
|
存储 前端开发 中间件
切图仔做全栈:React&Nest.js社区平台(二)——👋手把手实现优雅的鉴权机制
切图仔做全栈:React&Nest.js社区平台(二)——👋手把手实现优雅的鉴权机制
|
20天前
|
前端开发 NoSQL 数据库
切图仔做全栈:React&Nest.js社区平台(一)——基础架构与邮箱注册、JWT登录实现
切图仔做全栈:React&Nest.js社区平台(一)——基础架构与邮箱注册、JWT登录实现