React與Koa一起打造一個(gè)仿稀土掘金全棧個(gè)人博客(技術(shù)篇)
前言
我的個(gè)人博客樣式布局是仿的稀土掘金 ,個(gè)人博客線上網(wǎng)址為https://www.maomin.club/ ,也可以百度搜索前端歷劫之路 。為了瀏覽體驗(yàn),可以用PC瀏覽器瀏覽。
本篇文章將分為前臺(tái)角度與后臺(tái)角度來(lái)分析我是怎么開發(fā)的。
前臺(tái)角度
主要資源
react.js
ant Design
for-editor
axios
craco-less
immutable
react-loadable
react-redux
react-router-dom
react-transition-group
redux
redux-immutable
redux-thunk
styled-components
模塊頁(yè)面
首頁(yè)
登錄注冊(cè)
文章詳情
文章評(píng)論
圈子
寫圈子
搜索頁(yè)
權(quán)限頁(yè)
寫文章
項(xiàng)目配置
項(xiàng)目目錄
在這里插入圖片描述
前臺(tái)搭建項(xiàng)目步驟
一、使用穩(wěn)定依賴管理工具
推薦你使用淘寶源
npm config set registry https://registry.npm.taobao.org
還有就是搭配依賴管理工具yarn
二、使用官方React腳手架
create-react-app my-project
三、精簡(jiǎn)項(xiàng)目文件夾
使用腳手架搭建的初始文件夾是這樣的。
在這里插入圖片描述
那么我們需要精簡(jiǎn)一下。注意原來(lái)的App.js我改成App.jsx。因?yàn)?React 使用 JSX 來(lái)替代常規(guī)的 JavaScript,所以用JSX比較好。
在這里插入圖片描述
下面我們將要編輯幾個(gè)文件:
src/index.js
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.jsx';
ReactDOM.render(
<App />,
document.getElementById('root')
);
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="./bitbug_favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#FFB90F" />
<meta name="keywords" content="前端歷劫之路">
<meta name="description" content="如何從前端小仙歷劫成為一個(gè)前端大神呢?這里就有答案。" />
<title>前端歷劫之路</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
App.jsx文件內(nèi)的內(nèi)容什么意思現(xiàn)在可以先不用去關(guān)心,可以先放這。
src/App.jsx
// App.jsx
import React from 'react';
import { Provider } from 'react-redux';
import store from './store/';
import Router from './router';
import {BrowserRouter} from 'react-router-dom';
import {Main} from './styled/'
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { GlobalStyle } from '../src/styled/index';
import HeaderArea from './components/layout/Header';
import './App.less';
const Body = () => {
return (
<div>
<BrowserRouter>
<GlobalStyle />
<HeaderArea />
<Main>
<Router />
</Main>
</BrowserRouter>
</div>
)
}
const App = () => {
return (
<div>
<Provider store={store}>
<TransitionGroup appear={true} >
<CSSTransition timeout={10000} classNames='fade'>
<Body />
</CSSTransition>
</TransitionGroup>
</Provider>
</div>
)
};
export default App;
四、創(chuàng)建文件夾
在src目錄下分別創(chuàng)建以下幾個(gè)文件夾
在這里插入圖片描述
五、安裝依賴
dependencies:
antd
axios
for-editor
immutable
react-loadable
react-redux
react-router-dom
react-transition-group
redux
redux-immutable
redux-thunk
styled-components
六、配置自定義主題
按照 配置主題 的要求,自定義主題需要用到類似 less-loader 提供的 less 變量覆蓋功能。我們可以引入 craco-less 來(lái)幫助加載 less 樣式和修改變量。
首先在src目錄下創(chuàng)建一個(gè)App.less文件,編輯內(nèi)容如下:
@import '~antd/dist/antd.less';
1
然后在App.jsx內(nèi)引入App.less文件(上面已經(jīng)編輯過(guò)App.jsx文件的這里不用管)
然后安裝 craco-less 并創(chuàng)建修改 craco.config.js(存放在項(xiàng)目根目錄下) 文件如下:
// craco.config.js
const CracoLessPlugin = require('craco-less');
const theme = require ('./theme');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
modifyVars: theme.theme,
javascriptEnabled: true,
},
},
}
],
};
// theme.js
const theme = {
'@primary-color': '#FFB90F', // 全局主色
'@link-color': '#1890ff', // 鏈接色
'@success-color': '#52c41a', // 成功色
'@warning-color': '#faad14', // 警告色
'@error-color': '#f5222d', // 錯(cuò)誤色
'@font-size-base': '14px', // 主字號(hào)
'@heading-color': 'rgba(0, 0, 0, 0.85)', // 標(biāo)題色
'@text-color': 'rgba(0, 0, 0, 0.65)', // 主文本色
'@text-color-secondary': 'rgba(0, 0, 0, 0.45)', // 次文本色
'@disabled-color': 'rgba(0, 0, 0, 0.25)', // 失效色
'@border-radius-base': '4px', // 組件/浮層圓角
'@border-color-base': '#d9d9d9', // 邊框色
'@box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)' // 浮層陰影
}
exports.theme = theme
七、路由懶加載
在router文件夾下創(chuàng)建index.js和routes.js。
routes.js
// routes.js
// 路由配置
import React from 'react';
import {Route } from 'react-router-dom';
import {Home,About,Details,Write,Circle,Noauth,Search} from './routes'
const APPRouter = () =>(
<div>
<Route exact={true} path="/" component={Home}/>
<Route exact={true} path="/about/" component={About}/>
<Route exact={true} path="/details/:id/" component={Details} />
<Route exact={true} path="/write" component={Write} />
<Route exact={true} path="/circle" component={Circle} />
<Route exact={true} path="/noauth" component={Noauth} />
<Route exact={true} path="/search" component={Search} />
</div>
);
export default APPRouter;
index.js
// index.js
// 頁(yè)面組件
import loadable from '../util/loadable';
export const Home = loadable(()=> import('../views/Home/'));
export const About = loadable(()=> import('../views/About/'));
export const Details = loadable(()=> import('../views/Details'));
export const Write = loadable(()=> import('../views/Write'));
export const Circle = loadable(()=> import('../views/Circle'));
export const Noauth = loadable(()=>import('../components/modules/Noauth'))
export const Search = loadable(()=>import('../views/Search'))
在util文件夾下創(chuàng)建一個(gè)loadable.js。
loadable.js
// loadable.js
// 懶加載組件
import React from 'react';
import Loadable from 'react-loadable';
import styled from 'styled-components';
import { Spin } from 'antd';
const loadingComponent =()=>{
return (
<Loading>
<Spin />
</Loading>
)
};
export default (loader,loading = loadingComponent)=>{
return Loadable({
loader,
loading
});
};
const Loading = styled.div`
text-align: center;
margin:50vh 0;
`;
八、全局樣式與樣式組件
這里我們使用styled-components這個(gè)依賴寫樣式組件,因?yàn)樵趓eact.js中存在組件樣式污染的緣故。
在styled創(chuàng)建一個(gè)index.js。
index.js
// index.js
// 全局樣式
import styled,{createGlobalStyle} from 'styled-components';
export const Content = styled.div`
border-radius: 2px;
width: 100%;
padding:20px;
margin:20px 0;
border:1px solid #f4f4f4;
background:#fff;
box-sizing:border-box;
`
export const Main = styled.div`
position: relative;
margin: 100px auto 20px;
width: 100%;
max-width: 960px;
`;
export const GlobalStyle = createGlobalStyle`
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video{
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
font-weight: normal;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section{
display: block;
}
ol, ul, li{
list-style: none;
}
blockquote, q{
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after{
content: '';
content: none;
}
table{
border-collapse: collapse;
border-spacing: 0;
}
a{
color: #7e8c8d;
text-decoration: none;
-webkit-backface-visibility: hidden;
}
::-webkit-scrollbar{
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track-piece{
background-color: rgba(0, 0, 0, 0.2);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical{
height: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal{
width: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
html, body{
width: 100% !important;
background:#E8E8E8;
font-size: 12px;
font-family: Avenir,-apple-system,BlinkMacSystemFont,segoe
ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color
emoji,segoe ui emoji,segoe ui symbol,noto color emoji,sans-serif;
}
body{
line-height: 1;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html{
overflow-y: scroll;
}
.clearfix:before,
.clearfix:after{
content: " ";
display: inline-block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix{
*zoom: 1;
}
.ovf{
overflow:hidden;
}
.dn{
display: none;
}
/*自定義全局*/
p{
margin:10px;
}
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: all .5s;
}
.fade-exit {
opacity: 1;
transition: all .5s;
}
.fade-exit-active {
opacity: 0;
}
.hide{
opacity: 0;
height: 0px;
transform: translatey(-100px);
}
::-webkit-scrollbar {
width:5px;
height:5px;
}
::-webkit-scrollbar-track {
width: 5px;
background-color:#fff;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius:10px;
}
::-webkit-scrollbar-thumb {
background-clip:padding-box;
min-height:28px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius:10px;
}
::-webkit-scrollbar-thumb:hover {
background-color:#FFB90F;
}
`;
九、封裝axios請(qǐng)求
在request文件夾下創(chuàng)建api.js和http.js。
api.js
存放api接口。
// api.js
// 接口地址
import {get,post} from './http';
const url= 'https://www.maomin.club/myblog/'; // api
// post格式
export const reg = g => post(`${url}register`, g); // 注冊(cè)
export const log = g => post(`${url}login`, g); // 登錄
export const write = g => post(`${url}write`, g); // 寫文章
export const circle = g => post(`${url}circle`, g); // 發(fā)圈子
export const getCircle = g => post(`${url}getCircle`, g); // 獲取圈子
export const uploadImg = g => post(`${url}uploadImg`, g); // 寫文章上傳圖片
export const getListapi = g => post(`${url}getList`, g); // 獲取文章列表
export const getDetails = g => post(`${url}getDetails`, g); // 獲取文章詳情
export const comment = g => post(`${url}comment`, g); // 發(fā)送評(píng)論
export const getComment = g => post(`${url}getComment`, g); // 獲取評(píng)論
export const getinfo = g => post(`${url}getinfo`, g) // 獲取用戶信息
// get格式
export const alllist = g =>get(`${url}getAllList`,g);//獲取所有文章列表
http.js
請(qǐng)求配置。
// http.js
// axios配置
import axios from 'axios';
import { message} from 'antd';
// 請(qǐng)求攔截器
axios.interceptors.request.use(
config => {
if (localStorage.getItem('Authorization')) {
config.headers.Authorization = localStorage.getItem('Authorization'); //查看是否存在token
return config;
} else if (config.isUpload) {
config.headers = { 'Content-Type': 'multipart/form-data' } // 根據(jù)參數(shù)是否啟用form-data方式
return config;
} else {
config.headers = { 'Content-Type': 'application/json;charset=utf-8' }
return config;
}
},
error => {
return Promise.error(error)
})
// 響應(yīng)攔截器
axios.interceptors.response.use(
// 服務(wù)碼是200的情況
response => {
if (response.status === 200) {
switch (response.data.resultCode) {
// token過(guò)期
case 2:
message.error('登錄過(guò)期,請(qǐng)重新登錄');
localStorage.removeItem('Authorization');
setTimeout(() => {
window.location.href="/";
}, 1000);
break;
case 3:
message.error('未登錄');
break;
case 4:
message.error('請(qǐng)輸入正確的賬號(hào)或者密碼');
break;
default:
break;
}
return Promise.resolve(response);
} else {
return Promise.reject(response)
}
},
// 服務(wù)器狀態(tài)碼不是200的情況
error => {
if (error.response.status) {
switch (error.response.status) {
// 404請(qǐng)求不存在
case 404:
alert('網(wǎng)絡(luò)請(qǐng)求不存在');
break;
// 其他錯(cuò)誤,直接拋出錯(cuò)誤提示
default:
alert('error.response.data.message');
}
return Promise.reject(error.response)
}
}
)
/**
* get方法,對(duì)應(yīng)get請(qǐng)求
* @param {String} url [請(qǐng)求的url地址]
* @param {Object} params [請(qǐng)求時(shí)攜帶的參數(shù)]
*/
export function get(url, params, config = {
add: ''
}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params
}, config).then(res => {
resolve(res.data)
}).catch(err => {
reject(err.data)
})
})
}
/**
* post方法,對(duì)應(yīng)post請(qǐng)求
* @param {String} url [請(qǐng)求的url地址]
* @param {Object} params [請(qǐng)求時(shí)攜帶的參數(shù)]
*/
export function post(url, params, config = {
isUpload: false
}) {
return new Promise((resolve, reject) => {
axios.post(url, params, config)
.then(res => {
resolve(res.data)
})
.catch(err => {
reject(err.data)
})
})
}
十、狀態(tài)管理總配置
在store文件夾創(chuàng)建一個(gè)index.js和reducer.js。因?yàn)槊總€(gè)頁(yè)面模塊都有一個(gè)狀態(tài),所以我們?cè)谶@個(gè)項(xiàng)目里采用分模塊。然后我們現(xiàn)在的需要做的是統(tǒng)一管理它們每一個(gè)模塊。
index.js
// index.js
// 全局store配置
import {createStore,applyMiddleware,compose} from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
// redux-devtools 配置
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
// 使用中間件 thunk
applyMiddleware(thunk)
);
const store = createStore(reducer,enhancer);
export default store;
reducer.js
// reducer.js
// 分模塊Reducer
import { combineReducers } from 'redux-immutable';
import { reducer as homeReducer } from '../views/Home/store/';
import { reducer as layoutReducer } from '../components/layout/store';
import { reducer as aboutReducer } from '../views/About/store';
import { reducer as detailsReducer } from '../views/Details/store';
const reducer = combineReducers({
home: homeReducer,
layout:layoutReducer,
about:aboutReducer,
details:detailsReducer
});
export default reducer;
十一、頁(yè)面模塊與組件模塊
因頁(yè)面過(guò)多,這里只展示首頁(yè)模塊,其他邏輯思想大差不差,如果想詳細(xì)了解的可以加我微信。
在views文件夾創(chuàng)建一個(gè)Home文件夾。依次創(chuàng)建如下圖所示文件:
index.jsx
頁(yè)面組件。
// index.jsx
import React, { useEffect, Fragment } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { Pagination, Spin } from 'antd';
import styled from 'styled-components';
import { LeftView, RightView, Item, ContentBox, InfoBox, Meta, Title,
ImgBox, SidebarBlock, ImgBlock, MoreBlock } from './styleJs/style';
import { actionsCreator } from './store/';
const mapStateToProps = (state) => {
return {
datalist: state.getIn(['home', 'datalist']),
page: state.getIn(['home', 'page']),
defaultCurrent: state.getIn(['home', 'defaultCurrent'])
}
};
const mapDispatchToProps = (dispatch) => {
return {
getdata(v) {
dispatch(actionsCreator.getList(v))
},
pageChange(v) {
dispatch(actionsCreator.changePage(v))
}
}
};
const Loading = styled.div`
text-align: center;
margin:34vh 0;
`;
const Home = (props) => {
const { datalist, getdata, page, defaultCurrent, pageChange } = props;
const newList = datalist.toJS();
useEffect(() => {
getdata(defaultCurrent);
}, [defaultCurrent, getdata])
return (
<div>
<LeftView>
{
page === 0 ? <Loading>
<Spin tip="Loading..." />
</Loading> : <div><div style={{ 'height': '624px' }}>
{
newList.map((item) => {
return (
<Fragment key={item.id}>
<Link to={'/details/' + item.id}>
<Item>
<ContentBox>
<InfoBox>
<Meta>{item.tab}</Meta>
<Title>{item.title}</Title>
</InfoBox>
<ImgBox
srci={item.context.substring(item.context.indexOf("<img src='"),
item.context.indexOf("' alt=''>")).replace("<img src='",
"")}></ImgBox>
</ContentBox>
</Item>
</Link>
</Fragment>
)
})
}
</div>
<div style={{ 'margin': '20px' }}>
<Pagination defaultCurrent={defaultCurrent}
total={page} pageSize={6} onChange={pageChange}></Pagination>
</div>
</div>
}
</LeftView>
<RightView>
<SidebarBlock>
<ImgBlock src={require("../../assets/images/gzh.jpg")} />
</SidebarBlock>
<SidebarBlock>
<ImgBlock src={require("../../assets/images/wx.jpg")} />
</SidebarBlock>
<MoreBlock>
<div>© {new Date().getFullYear()}<span>maomin.club</span>版權(quán)所有</div>
<a
>公安備案號(hào)
37021302000701號(hào) </a>
<a > 魯ICP備19020856號(hào)-1</a>
</MoreBlock>
</RightView>
</div>
)
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
styles/style.js
home頁(yè)面的樣式。
// style.js
import styled, {keyframes } from 'styled-components';
const fadeIn = keyframes`
from {
opacity:0;
}
to {
opacity:1;
}
`
export const LeftView = styled.div`
border-radius: 2px;
width: 700px;
margin-right: 21.667rem;
border:1px solid #f4f4f4;
background:#fff;
box-sizing:border-box;
animation: ${fadeIn} 1s ease-in;
`
export const RightView = styled.div`
position: absolute;
top: 0;
right: 0;
width:20rem;
@media (max-width: 960px){
display: none;
}
`
export const Item = styled.div`
border-bottom: 1px solid rgba(178,186,194,.15);
`
export const ContentBox = styled.div`
display: flex;
align-items: center;
padding: 1.5rem 2rem;
`
export const InfoBox = styled.div`
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
`
export const Meta = styled.div`
color: #b2bac2;
`
export const Title = styled.div`
margin: 1rem 0 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.4rem;
font-weight: 600;
line-height: 1.2;
color: #2e3135;
`
export const ImgBox = styled.div`
background-image:url('${props => props.srci}');
background-repeat: no-repeat;
background-size: cover;
flex: 0 0 auto;
width: 5rem;
height: 5rem;
background-color:#f4f4f4;
margin-left: 2rem;
background-color: #fff;
border-radius: 2px;
background-position: 50%;
animation: ${fadeIn} 1s ease-in;
`
export const SidebarBlock = styled.div`
background-color: #fff;
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
border-radius: 2px;
margin-bottom: 1.3rem;
font-size: 1.16rem;
line-height: 1.29;
color: #333;
`
export const ImgBlock = styled.img`
width:100%;
animation: ${fadeIn} 1s ease-in;
`
export const MoreBlock =styled.div`
background-color: transparent;
box-shadow: none;
a{
display:block;
line-height:22px;
text-decoration: none;
cursor: pointer;
color: #909090;
}
div {
line-height:22px;
}
span{
margin:0 5px;
}
`
store/actionsCreator.js
react-thunk作用:使我們可以在action中返回函數(shù),而不是只能返回一個(gè)對(duì)象。然后我們可以在函數(shù)中做很多事情,比如發(fā)送異步的ajax請(qǐng)求。
// actionsCreator.js
import {actionsTypes} from './index';
import {getListapi} from '../../../request/api';
import {fromJS} from 'immutable';
const dataList =(data,page) =>{
return {
type:actionsTypes.DATA_LIST,
data:fromJS(data),
page:fromJS(page)
}
};
const currentPage = (p) =>{
return {
type:actionsTypes.CHANGE_PAGE,
current:p
}
}
export const getList = (p) =>{
return (dispatch) =>{
let postData ={
page:p
}
getListapi(postData).then((res)=>{
const data = res.data;
const page = res.page;
const action = dataList(data,page);
dispatch(action);
}).catch((err)=>{
console.log(err);
})
}
};
export const changePage=(page)=>{
return (dispatch) =>{
const action = currentPage(page);
dispatch(action);
}
}
store/actionsTypes.js
// actionsTypes.js
export const DATA_LIST = 'home/DATA_LIST';
export const CHANGE_PAGE = 'home/CHANGE_PAGE';
store/index.js
home頁(yè)面的store配置。
// index.js
import reducer from './reducer';
import * as actionsTypes from './actionsTypes';
import * as actionsCreator from './actionsCreator';
export { reducer, actionsCreator,actionsTypes};
store/reducer.js
由于是不可變的,可以放心的對(duì)對(duì)象進(jìn)行任意操作。在 React 開發(fā)中,頻繁操作state對(duì)象或是 store ,配合 immutableJS 快、安全、方便。
// reducer.js
import {actionsTypes} from './index';
import {fromJS} from 'immutable';
let defaultState = fromJS({
datalist: [],
page:0,
defaultCurrent:1
});
export default (state = defaultState, action) => {
switch (action.type) {
case actionsTypes.DATA_LIST:
return state.merge({
'datalist':action.data,
'page':action.page
})
case actionsTypes.CHANGE_PAGE:
return state.set('defaultCurrent',action.current)
default:
return state;
}
};
源碼
后臺(tái)主要是用了Koa模塊,下面的源碼是基于https環(huán)境。數(shù)據(jù)庫(kù)是采用了創(chuàng)建地址池的方法,數(shù)據(jù)庫(kù)的連接池負(fù)責(zé)分配,管理和釋放數(shù)據(jù)庫(kù)鏈接的。它允許應(yīng)用程序重復(fù)使用一個(gè)現(xiàn)有的數(shù)據(jù)庫(kù)的鏈接。而不是重新創(chuàng)建一個(gè)。地址池這里可以優(yōu)化,這里為了看的更清楚,統(tǒng)一放在了一個(gè)文件里。具體詳解請(qǐng)看下面的注釋。
// app.js
var https = require("https");//https服務(wù)
var fs = require("fs");
var path = require('path');
var Koa = require('koa');
var Router = require('koa-router');
var cors = require('koa2-cors');
var jwt = require('jsonwebtoken');
var koaBody = require('koa-body'); //文件保存庫(kù)
var serve = require('koa-static');
var enforceHttps = require('koa-sslify').default;
var mysql = require('mysql');
var schedule = require('node-schedule');
var app = new Koa();
app.use(enforceHttps());
var router = new Router();
var secretkey = ''; // token的key
// 這是我的https配置文件可忽略
var options = {
key: fs.readFileSync('https/2_www.maomin.club.key'),
cert: fs.readFileSync('https/1_www.maomin.club_bundle.crt')
}
// 存文件配置
const home = serve(path.join(__dirname) + '/public/');
app.use(home);
app.use(koaBody({
multipart: true
}));
// 跨域
const allowOrigins = [
"https://www.maomin.club/"
];
app.use(cors({
origin: function (ctx) {
if (allowOrigins.includes(ctx.header.origin)) {
return ctx.header.origin;
}
return false;
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
withCredentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
// 創(chuàng)建地址池
var pool = mysql.createPool({
host: '', // 主機(jī)
port: 3306, // 端口
user: '', // 用戶
password: '', // 密碼
database: '', // 數(shù)據(jù)庫(kù)
multipleStatements: true, // 允許每個(gè)mysql語(yǔ)句有多條查詢
connectionLimit: 100 // 最大連接數(shù)
})
// 數(shù)據(jù)庫(kù)操作
// 定時(shí)置3
schedule.scheduleJob('10 0 0 * * *', function () {
console.log('update!')
var updateStr = 'UPDATE login SET count = ?';
var modSqlParams = [3];
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(updateStr, modSqlParams, function (err, results) {
if (err) {
//do something
throw err;
}
conn.release(); //釋放連接
})
})
});
// 檢查token
const checkToken = function (tokenid) {
return new Promise((resolve) => {
if (tokenid) {
//校驗(yàn)tokenid
jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解碼后用戶信息
if (err) { //如果tokenid過(guò)期則會(huì)執(zhí)行err的代碼塊
resolve({ success: false, resultCode: 2, message: err });
} else {
resolve("notime");
}
})
} else { resolve({ success: false, resultCode: 3, message: '未登錄' }) }
})
}
let json = {};
// 通用查詢方法
const query = function (sql) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(sql, function (err, results) {
if (err) {
//do something
reject(error);
} else {
//return data or anything you want do!
resolve(results);
}
conn.release(); //釋放連接
})
})
})
}
// 分頁(yè)
let all = "";
const page = function (sql, p) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(sql, function (err, results) {
if (err) {
//do something
reject(error);
} else {
//return data or anything you want do!
var allCount = results[0][0]['COUNT(*)'];
all = allCount;
var allPage = parseInt(allCount) / p;
var pageStr = allPage.toString();
if (pageStr.indexOf('.') > 0) {
allPage = parseInt(pageStr.split('.')[0]) + 1;
}
var List = results[1];
resolve(List)
}
conn.release(); //釋放連接
})
})
})
}
// 登錄方法
const logQuery = function (userStr, token) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(userStr, function (err, results) {
if (err) {
//do something
reject(error);
} else {
//return data or anything you want do!
if (results.length !== 0) {
var dataString = JSON.stringify(results);
var data = JSON.parse(dataString);
json['message'] = '登錄成功';
json['resultCode'] = 200;
json['username'] = data[0].username;
json['token'] = token;
var updateStr = 'UPDATE login SET token = ? WHERE Id = ?';
var modSqlParams = [token, data[0].id];
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(updateStr, modSqlParams, function (err, results) {
if (err) {
//do something
throw err;
} conn.release(); //釋放連接
})
})
resolve(json);
} else {
resolve({ success: false, resultCode: 4, message: '請(qǐng)輸入正確的賬號(hào)或密碼' });
}
}
conn.release(); //釋放連接
})
})
})
}
//注冊(cè)方法
const regQuery = function (userStr, name, passwd, token, count) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(userStr, function (err, result) {
if (err) {
//do something
reject(error);
} else {
//return data or anything you want do!
if (result.length > 0) {
json['message'] = '用戶已經(jīng)存在';
json['resultCode'] = 1;
} else {
json['message'] = '注冊(cè)成功';
json['token'] = token;
json['username'] = name;
json['count'] = count;
json['resultCode'] = 200;
var insertStr = `insert into login (username,
password,token,count) values ("${name}",
"${passwd}","${token}","${count}")`;
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(insertStr, function (err, results) {
if (err) {
//do something
throw err;
} conn.release(); //釋放連接
})
})
}
resolve(json)
}
conn.release(); //釋放連接
})
})
})
}
// 評(píng)論方法
const commentQuery = function (userStr, aid) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(userStr, async function (err) {
if (err) {
//do something
reject(error);
} else {
//return data or anything you want do!
json['message'] = '評(píng)論成功';
json['success'] = true;
let sql = `select aid,username,com from comment where aid="${aid}"`;
let results = await query(sql);
json['data'] = results;
resolve(json);
}
conn.release(); //釋放連接
})
})
})
}
// 發(fā)圈子方法
const setCount = function (userStr, username, imgsrc, inputValue, td) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(userStr, function (err, results) {
if (err) {
//do something
reject(error);
} else {
//return data or anything you want do!
var dataString = JSON.stringify(results);
var data = JSON.parse(dataString);
if (data[0].count > 0) {
var newCount = data[0].count - 1;
json['message'] = '發(fā)表成功';
json['resultCode'] = 200;
json['success'] = true;
json['count'] = newCount;
// 次數(shù)減一
var updateStr = 'UPDATE login SET count = ? WHERE username = ?';
var modSqlParams = [newCount, username];
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(updateStr, modSqlParams, function (err) {
if (err) {
//do something
throw err;
} conn.release(); //釋放連接
})
})
// 存入圈子數(shù)據(jù)庫(kù)
var insetStr = `insert into circle (username,
imgsrc, inputValue, td) values
("${username}","${imgsrc}","${inputValue}","${td}")`
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(insetStr, modSqlParams, function (err) {
if (err) {
//do something
throw err;
} conn.release(); //釋放連接
})
})
resolve(json);
} else {
resolve({ success: false, resultCode: 5, message: '操作太頻繁,請(qǐng)明天再發(fā)哦' });
}
}
conn.release(); //釋放連接
})
})
})
}
// 用戶信息方法
const getInfo = function (tokenid) {
return new Promise((resolve) => {
if (tokenid) {
//校驗(yàn)tokenid
jwt.verify(tokenid, secretkey, function (err, decoded) { // decoded:指的是tokneid解碼后用戶信息
if (err) { //如果tokenid過(guò)期則會(huì)執(zhí)行err的代碼塊
resolve({ success: false, resultCode: 2, message: err });
} else {
resolve(decoded);
}
})
} else { resolve({ success: false, resultCode: 3, message: '未登錄' }) }
})
}
// 獲取用戶信息
router.post('/getinfo', async (ctx, next) => {
var tokenid = ctx.request.body.token;
let results = await getInfo(tokenid);
ctx.body = results;
})
// 注冊(cè)
router.post('/register', async (ctx, next) => {
let name = ctx.request.body.username;
let passwd = ctx.request.body.password;
let count = 3;
let token = jwt.sign({
username: name
}, secretkey, {
expiresIn: 60 * 60 * 12 // 12h
});
let userStr = `select * from login where username="${name}"`;
let results = await regQuery(userStr, name, passwd, token, count);
ctx.body = results
});
// 登錄
router.post('/login', async (ctx, next) => {
let name = ctx.request.body.username;
let passwd = ctx.request.body.password;
let token = jwt.sign({
username: name
}, secretkey, {
expiresIn: 60 * 60 * 12 // 12h
});
let userStr = `select username,password,id from login where username="${name}" and password="${passwd}"`;
let results = await logQuery(userStr, token);
ctx.body = results
});
// 寫評(píng)論
router.post('/comment', async (ctx, next) => {
let aid = ctx.request.body.aid;
let username = ctx.request.body.username;
let com = ctx.request.body.com;
let td = ctx.request.body.td;
var tokenid = ctx.request.headers.authorization//獲取前端請(qǐng)求頭發(fā)送過(guò)來(lái)的tokenid
let trueFlase = await checkToken(tokenid);
if (trueFlase === "notime") {
let userStr = `insert into comment (aid, username, com, td) values ("${aid}","${username}","${com}","${td}")`
let results = await commentQuery(userStr, aid);
ctx.body = results;
} else {
ctx.body = trueFlase;
}
})
// 獲取評(píng)論
router.post('/getComment', async (ctx, next) => {
var start = (ctx.request.body.page - 1) * 3;
let aid = ctx.request.body.aid;
var count = `SELECT * FROM comment WHERE aid="${aid}"`;
let allnum = await query(count);
const len = allnum.length;
var sql = `SELECT COUNT(*) FROM comment ORDER BY id DESC;SELECT *
FROM comment WHERE aid="${aid}" ORDER BY id DESC limit ${start},3`;
let results = await page(sql, 3);
ctx.body = {
data: results,
page: len
}
}
)
// 寫文章
router.post('/write', async (ctx, next) => {
let title = ctx.request.body.title;
let tab = ctx.request.body.tab;
let context = ctx.request.body.context;
var tokenid = ctx.request.headers.authorization//獲取前端請(qǐng)求頭發(fā)送過(guò)來(lái)的tokenid
let trueFlase = await checkToken(tokenid);
if (trueFlase === "notime") {
var userStr = `insert into article (title, tab, context) values ("${title}","${tab}","${context}")`
pool.getConnection(function (err, conn) {
if (err) {
//do something
console.log(err);
}
conn.query(userStr, function (err) {
if (err) {
//do something
throw err;
} conn.release(); //釋放連接
})
})
ctx.body = { success: true, message: '發(fā)送成功' } // echo the result back
} else {
ctx.body = trueFlase;
}
});
// 寫文章上傳圖片
router.post('/uploadImg', async (ctx, next) => {
if (ctx.request.files.file) {
var file = ctx.request.files.file;
// 創(chuàng)建可讀流
var reader = fs.createReadStream(file.path);
// 修改文件的名稱
var myDate = new Date();
var newFilename = myDate.getTime() + '.' + file.name.split('.')[1];
var targetPath = path.join(__dirname, './public/images/') + `${newFilename}`;
//創(chuàng)建可寫流
var upStream = fs.createWriteStream(targetPath);
// 可讀流通過(guò)管道寫入可寫流
reader.pipe(upStream);
var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename;
ctx.body = {
success: true,
imgsrc: imgsrc
};
}
})
// 發(fā)圈子
router.post('/circle', async (ctx, next) => {
if (ctx.request.files.file) {
var file = ctx.request.files.file;
// 創(chuàng)建可讀流
var reader = fs.createReadStream(file.path);
// 修改文件的名稱
var myDate = new Date();
var newFilename = myDate.getTime() + '.' + file.name.split('.')[1];
var targetPath = path.join(__dirname, './public/images/') + `${newFilename}`;
//創(chuàng)建可寫流
var upStream = fs.createWriteStream(targetPath);
// 可讀流通過(guò)管道寫入可寫流
reader.pipe(upStream);
var imgsrc = 'https://www.maomin.club/myblog/images/' + newFilename;
} else {
var imgsrc = ""
}
let username = ctx.request.body.username;
let inputValue = ctx.request.body.inputValue;
let td = ctx.request.body.td;
var tokenid = ctx.request.headers.authorization//獲取前端請(qǐng)求頭發(fā)送過(guò)來(lái)的tokenid
let trueFlase = await checkToken(tokenid);
if (trueFlase === "notime") {
let userStr = `select count from login where username="${username}"`;
let results = await setCount(userStr, username, imgsrc, inputValue, td);
ctx.body = results;
} else {
ctx.body = trueFlase;
}
});
// 獲取圈子
router.post('/getCircle', async (ctx, next) => {
var start = (ctx.request.body.page - 1) * 3;
var sql = 'SELECT COUNT(*) FROM circle ORDER BY id DESC; SELECT * FROM circle ORDER BY id DESC limit ' + start + ',3';
let results = await page(sql, 3);
ctx.body = {
data: results,
page: all
}
});
// 獲取文章列表(分頁(yè))
router.post('/getList', async (ctx, next) => {
var start = (ctx.request.body.page - 1) * 6;
var sql = 'SELECT COUNT(*) FROM article ORDER BY id DESC; SELECT * FROM article ORDER BY id DESC limit ' + start + ',6';
let results = await page(sql, 6);
ctx.body = {
data: results,
page: all
}
});
// 獲取文章列表(全部)
router.get('/getAllList', async (ctx, next) => {
var sql = "select * from article";
let results = await query(sql);
ctx.body = results
});
// 獲取文章詳情
router.post('/getDetails', async (ctx, next) => {
const id = ctx.request.body.id;
var sql = `select * from article where id="${id}"`;
let results = await query(sql);
ctx.body = results
});
//使用路由中間件
app
.use(router.routes())
.use(router.allowedMethods());
https.createServer(options, app.callback()).listen(8410);
console.log('服務(wù)器運(yùn)行中')
作者:Vam的金豆之路
主要領(lǐng)域:前端開發(fā)
我的微信:maomin9761
微信公眾號(hào):前端歷劫之路