├── .babelrc
├── .gitignore
├── Redis-x64-3.0.504.7z
├── bundles
└── client.html
├── components
├── Repo.js
├── container.js
├── layout.css
├── layout.js
└── pageLoading.js
├── lib
├── clientConfig.js
├── requestApi.js
├── routeEvent.js
└── uniqueStore.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── detail.js
├── index.js
└── search.js
├── pages_test
├── author.js
└── index.js
├── server.js
├── server
├── api.js
├── auth.js
├── route.js
└── sessionStore.js
├── store
└── store.js
└── test-redis.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "import",
6 | {
7 | "libraryName": "antd",
8 | "style": "css" // 可能 minicss 会有bug
9 | }
10 | ],[
11 | "styled-components", {
12 | "ssr": true
13 | }
14 | ]
15 | ]
16 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | dist
4 | .history
5 | .DS_Store
6 | .vscode
7 | .next
--------------------------------------------------------------------------------
/Redis-x64-3.0.504.7z:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herorest/nextGitHub/e2b3f1b2a6c4fd94fd285bfa7b6538b46f97e200/Redis-x64-3.0.504.7z
--------------------------------------------------------------------------------
/components/Repo.js:
--------------------------------------------------------------------------------
1 | import { Icon } from "antd";
2 | import Link from 'next/link';
3 | import moment from 'moment';
4 |
5 | function getLicense(license){
6 | return license ? `${license.spdx_id} license` : '';
7 | }
8 |
9 | function getLastUpdated(time){
10 | return moment(time).fromNow();
11 | }
12 |
13 | export default ({repo}) => {
14 | return (
15 |
16 |
17 |
22 |
{repo.description}
23 |
24 | {getLastUpdated(repo.updated_at)}
25 | {repo.open_issues_count}
26 | {getLicense(repo.license)}
27 |
28 |
29 |
30 | {repo.language}
31 |
32 | {repo.stargazers_count}
33 |
34 |
35 |
61 |
62 | )
63 | }
--------------------------------------------------------------------------------
/components/container.js:
--------------------------------------------------------------------------------
1 | import React, { cloneElement } from 'react';
2 |
3 | const style = {
4 | width: '100%',
5 | maxWidth: 1200,
6 | marginLeft: 'auto',
7 | marginRight: 'auto'
8 | }
9 |
10 | // 批量批注提示
11 | export default ({children, renderer = }) => {
12 | const newElement = cloneElement(renderer, {
13 | style: Object.assign({}, renderer.props.style, style),
14 | children
15 | });
16 | return newElement;
17 | }
--------------------------------------------------------------------------------
/components/layout.css:
--------------------------------------------------------------------------------
1 | .logo i{
2 | color:#fff;
3 | font-size:40px;
4 | display:block;
5 | padding-top:10px;
6 | margin-right:20px;
7 | }
8 |
9 | .ant-layout{
10 | background:#fff;
11 | }
12 |
13 | .ant-layout-content{
14 | min-height: auto;
15 | }
--------------------------------------------------------------------------------
/components/layout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import { Layout, Menu, Breadcrumb, Icon, Input, Avatar, Dropdown } from 'antd';
3 | import Container from './container';
4 | import getConfig from 'next/config';
5 | import {connect} from 'react-redux';
6 | import {logout} from '../store/store';
7 | import {withRouter} from 'next/router';
8 | import './layout.css'
9 | import axios from 'axios';
10 |
11 | const { Header, Content, Footer } = Layout;
12 | const {publicRuntimeConfig} = getConfig();
13 |
14 |
15 |
16 | // 批量批注提示
17 | const LayoutComp = ({children, user, userLogout, router}) => {
18 | const [searchVal, setSearchVal] = useState('');
19 |
20 | const handleSearchChange = useCallback((e) => {
21 | setSearchVal(e.target.value);
22 | }, []);
23 |
24 | const handleOnSearch = useCallback((e) => {
25 |
26 | }, []);
27 |
28 | const handleLogout = useCallback((e) => {
29 | userLogout();
30 | }, [userLogout]);
31 |
32 | const handleGoOAuth = useCallback((e) => {
33 | e.preventDefault();
34 | axios.get(`/prepare-auth?url=${router.asPath}`).then(res => {
35 | if(res.status === 200){
36 | location.href = publicRuntimeConfig.oAuthUrl;
37 | }else{
38 | console.log('prepare auth failed', res);
39 | }
40 | }).catch(err => {
41 | console.log('prepare auth ajax failed', err);
42 | });
43 | }, []);
44 |
45 | const menu = (
46 |
51 | );
52 |
53 |
54 | return (
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
82 |
83 |
84 |
85 |
86 | {children}
87 |
88 |
89 |
90 |
102 |
118 |
119 | )
120 | }
121 |
122 | LayoutComp.getInitialProps = (ctx) => {
123 | const store = ctx.store;
124 | return {
125 |
126 | }
127 | };
128 |
129 | export default connect(function mapStateToProps(state){
130 | return {
131 | user: state.user
132 | }
133 | }, function mapDispatchToProps(dispatch){
134 | return {
135 | dispatch,
136 | userLogout: () => dispatch(logout())
137 | }
138 | })(withRouter(LayoutComp));
--------------------------------------------------------------------------------
/components/pageLoading.js:
--------------------------------------------------------------------------------
1 | import { Spin } from 'antd';
2 |
3 |
4 | // 批量批注提示
5 | export default () => {
6 | return (
7 |
8 |
9 |
25 |
26 | )
27 | }
--------------------------------------------------------------------------------
/lib/clientConfig.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | github: {
3 | clientID: '59730c1402c763b84c5e',
4 | clientSecret: 'b39e6b57910d94f3552bb21584b1aa5e7fbea676',
5 | authUrl: 'https://github.com/login/oauth/authorize',
6 | accessTokenUrl: 'https://github.com/login/oauth/access_token',
7 | userInfo: 'https://api.github.com/user'
8 | }
9 | }
--------------------------------------------------------------------------------
/lib/requestApi.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 兼容node及浏览器的请求函数
3 | */
4 | const axios = require('axios');
5 | const isServer = typeof window === 'undefined';
6 | const github_base_url = 'https://api.github.com';
7 |
8 | async function requestGithub(method, url, data, headers){
9 | return await axios({
10 | method,
11 | url: `${github_base_url}${url}`,
12 | data,
13 | headers
14 | });
15 | }
16 |
17 | async function request({method = 'GET', url, data}, req, res){
18 | if(!url){
19 | console.log('url error');
20 | }
21 | if(isServer){
22 | const session = req.session;
23 | const githubAuth = session.githubAuth || {};
24 | const headers = {};
25 | if(githubAuth.access_token){
26 | headers['Authorization'] = `${githubAuth.token_type} ${githubAuth.access_token}`;
27 | }
28 | return await requestGithub(method, url, data, headers);
29 | }else{
30 | return await axios({
31 | method,
32 | url: `/github${url}`,
33 | data
34 | })
35 | }
36 | }
37 |
38 | module.exports = {
39 | request,
40 | requestGithub
41 | }
--------------------------------------------------------------------------------
/lib/routeEvent.js:
--------------------------------------------------------------------------------
1 | export default {
2 | changeStart: 'routeChangeStart' ,
3 | changeComplete: 'routeChangeComplete' ,
4 | changeError: 'routeChangeError' ,
5 | beforChange: 'beforeHistoryChange' ,
6 | hashChangeStart: 'hashChangeStart' ,
7 | hashChangeComplete: 'hashChangeComplete' ,
8 | }
--------------------------------------------------------------------------------
/lib/uniqueStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 唯一化store
3 | * 此处为App的高阶组件,同时在服务端及客户端中,利用getInitialProps,封装了store的创建
4 | */
5 |
6 | import {Component} from 'react';
7 | import CreateStore from '../store/store';
8 |
9 | const isServer = typeof window === 'undefined';
10 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__';
11 |
12 | function getStore(state){
13 | if(isServer){
14 | return CreateStore(state);
15 | }
16 |
17 | if(!window[__NEXT_REDUX_STORE__]){
18 | window[__NEXT_REDUX_STORE__] = CreateStore(state);
19 | }
20 |
21 | return window[__NEXT_REDUX_STORE__];
22 | }
23 |
24 | export default function (App){
25 |
26 | class Hoc extends Component {
27 | constructor(props) {
28 | super(props);
29 | this.store = getStore(props.initialReduxState);
30 | }
31 |
32 | render(){
33 | const {Component, props, ...rest} = this.props;
34 | return ;
35 | }
36 | }
37 |
38 | Hoc.getInitialProps = async (ctx) => {
39 | let store;
40 | if(isServer){
41 | const {req} = ctx.ctx;
42 | const session = req.session;
43 | if(session && session.userInfo){
44 | store = getStore({
45 | user: session.userInfo
46 | });
47 | }else{
48 | store = getStore();
49 | }
50 | }else{
51 | store = getStore();
52 | }
53 |
54 | ctx.store = store;
55 |
56 | let appProps = {};
57 | if(typeof App.getInitialProps === 'function'){
58 | appProps = await App.getInitialProps(ctx);
59 | }
60 |
61 | return {
62 | ...appProps,
63 | initialReduxState: store.getState()
64 | };
65 | }
66 |
67 |
68 | return Hoc;
69 | };
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // 让 nextjs 支持引入css
2 | const withCss = require('@zeit/next-css');
3 | const webpack = require('webpack');
4 | const withBundleAnalyzer = require('@zeit/next-bundle-analyzer');
5 | const clientConfig = require('./lib/clientConfig');
6 |
7 | if(typeof require !== 'undefined'){
8 | require.extensions['.css'] = file => {}
9 | }
10 |
11 | module.exports = withBundleAnalyzer(withCss({
12 | distDir: 'dist',
13 | webpack(config){
14 | //忽略moment中的多语言
15 | config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));
16 | return config;
17 | },
18 | env: {
19 | entry: 'default',
20 | },
21 | serverRuntimeConfig: {
22 | mySecret: 'secret',
23 | secondSecret: process.env.SECOND_SECRET
24 | },
25 | publicRuntimeConfig: {
26 | staticFolder: '/static',
27 | githubOauthUrl: clientConfig.github.authUrl,
28 | oAuthUrl: `${clientConfig.github.authUrl}?client_id=${clientConfig.github.clientID}&scope=${'user'}`
29 | },
30 | analyzeBrowser: ['browser', 'both'].includes(process.env.BUNDLE_ANALYZE),
31 | bundleAnalyzerConfig: {
32 | server: {
33 | analyzerMode: 'static',
34 | reportFilename: '../bundles/server.html'
35 | },
36 | browser: {
37 | analyzerMode: 'static',
38 | reportFilename: '../bundles/client.html'
39 | },
40 | }
41 | }));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "newreact",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "node server.js",
8 | "build": "next build",
9 | "start": "next start",
10 | "next": "next",
11 | "dev2": "cross-env DEBUG=server:*,-not_this NODE_ENV=development node server.js",
12 | "start2": "cross-env NODE_ENV=production node server.js",
13 | "test": "echo \"Error: no test specified\" && exit 1",
14 | "analyze": "cross-env BUNDLE_ANALYZE=browser next build"
15 | },
16 | "author": "",
17 | "license": "ISC",
18 | "dependencies": {
19 | "@zeit/next-css": "^1.0.1",
20 | "antd": "^3.23.2",
21 | "atob": "^2.1.2",
22 | "axios": "^0.18.0",
23 | "babel-plugin-import": "^1.11.0",
24 | "babel-plugin-styled-components": "^1.10.6",
25 | "cross-env": "^5.2.0",
26 | "debug": "^4.1.1",
27 | "http-proxy": "^1.17.0",
28 | "ioredis": "^4.6.2",
29 | "koa": "^2.7.0",
30 | "koa-body": "^4.1.1",
31 | "koa-router": "^7.4.0",
32 | "koa-session": "^5.10.1",
33 | "lru-cache": "^5.1.1",
34 | "markdown-it": "^8.4.2",
35 | "next": "^8.0.3",
36 | "nprogress": "^0.2.0",
37 | "react": "^16.8.3",
38 | "react-dom": "^16.8.3",
39 | "react-redux": "^6.0.1",
40 | "redux": "^4.0.1",
41 | "redux-devtools-extension": "^2.13.8",
42 | "redux-thunk": "^2.3.0",
43 | "styled-components": "^4.3.2"
44 | },
45 | "devDependencies": {
46 | "@zeit/next-bundle-analyzer": "^0.1.2"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import App, {Container} from 'next/app';
2 | import {Provider} from 'react-redux'
3 | import routeEvent from '../lib/routeEvent';
4 | import {Router} from 'next/router';
5 | import uniqueStore from '../lib/uniqueStore';
6 | import Layout from '../components/layout';
7 | import PageLoading from '../components/pageLoading';
8 | import axios from 'axios';
9 |
10 | // import 'antd/dist/antd.css';
11 |
12 | // function makeEvent(type){
13 | // return (...args) => {
14 | // console.log(type, ...args);
15 | // }
16 | // }
17 |
18 | // for (const key in routeEvent) {
19 | // if (routeEvent.hasOwnProperty(key)) {
20 | // const event = routeEvent[key];
21 | // Router.events.on(event, makeEvent(event));
22 | // }
23 | // }
24 |
25 | class MyApp extends App {
26 | state = {
27 | loading: false
28 | }
29 |
30 | componentDidMount(){
31 | Router.events.on(routeEvent['changeStart'], this.startLoading);
32 | Router.events.on(routeEvent['changeComplete'], this.stopLoading);
33 | Router.events.on(routeEvent['changeError'], this.stopLoading);
34 | }
35 |
36 | componentWillUnmount(){
37 | Router.events.off(routeEvent['changeStart'], this.startLoading);
38 | Router.events.off(routeEvent['changeComplete'], this.stopLoading);
39 | Router.events.off(routeEvent['changeError'], this.stopLoading);
40 | }
41 |
42 | startLoading = () => {
43 | this.setState({
44 | loading: true
45 | });
46 | }
47 |
48 | stopLoading = () => {
49 | this.setState({
50 | loading: false
51 | });
52 | }
53 |
54 | static getInitialProps = async (ctx) => {
55 | const Component = ctx.Component;
56 | let pageProps;
57 | if(Component.getInitialProps){
58 | pageProps = await Component.getInitialProps(ctx);
59 | }
60 | return {
61 | pageProps
62 | };
63 | }
64 |
65 | render(){
66 | const {Component, pageProps, store} = this.props;
67 |
68 | return (
69 |
70 |
71 | {
72 | this.state.loading &&
73 |
74 | }
75 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 | }
83 |
84 | export default uniqueStore(MyApp);
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, {Html, Head, Main, NextScript} from 'next/document';
2 | import { ServerStyleSheet } from 'styled-components';
3 |
4 |
5 |
6 | class MyDocument extends Document {
7 |
8 | static async getInitialProps(ctx){
9 | const sheet = new ServerStyleSheet();
10 | const originalRenderPage = ctx.renderPage;
11 |
12 | try{
13 | ctx.renderPage = () => originalRenderPage({
14 | enhanceApp: App => (props) => sheet.collectStyles(),
15 | enhanceComponent: Component => Component
16 | });
17 |
18 | const props = await Document.getInitialProps(ctx);
19 | return {
20 | ...props,
21 | styles: <>{props.styles}{sheet.getStyleElement()}>
22 | }
23 | }finally{
24 | sheet.seal();
25 | }
26 |
27 | }
28 |
29 | render(){
30 | return (
31 |
32 |
33 |
34 |
35 |
36 | {/* */}
37 |
38 |
39 | )
40 | }
41 | }
42 |
43 | export default MyDocument;
--------------------------------------------------------------------------------
/pages/detail.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef, useMemo, memo, useCallback} from 'react'
2 | import Link from 'next/link';
3 | import {connect} from 'react-redux';
4 | import getConfig from 'next/config';
5 | import axios from 'axios';
6 |
7 | export default () => {
8 | return
9 | }
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef, useMemo, memo, useCallback} from 'react'
2 | import api from '../lib/requestApi';
3 | import {Button, Icon, Tabs} from 'antd';
4 | import {connect} from 'react-redux';
5 | // import Repo from '../components/Repo';
6 | import Router, {withRouter} from 'next/router';
7 | import dynamic from 'next/dynamic';
8 |
9 | const Repo = dynamic(
10 | () => import('../components/Repo'),
11 | {
12 | loading: () => loading
13 | }
14 | );
15 |
16 |
17 | let cacheUserRepos, cacheUserStarred;
18 | const isServer = typeof window === 'undefined';
19 |
20 | function Index({repos, starred, user, router}){
21 | if(!user || !user.id){
22 | return (
23 |
24 |
亲,你还没有登录哦!
25 |
26 |
27 |
36 |
37 | );
38 | }
39 | const tabkey = router.query.tabkey || 1;
40 |
41 | const handleTabChange = (key) => {
42 | Router.push(`/?tabkey=${key}`);
43 | }
44 |
45 | useEffect(() => {
46 | if(!isServer){
47 | cacheUserRepos = repos;
48 | cacheUserStarred = starred;
49 | }
50 | setTimeout(() => {
51 | cacheUserRepos = null;
52 | cacheUserStarred = null;
53 | }, 1000 * 50);
54 | }, [repos, starred]);
55 |
56 | return (
57 |
58 |
59 |

60 |
{user.login}
61 |
{user.name}
62 |
{user.bio}
63 |
64 |
65 | {user.email}
66 |
67 |
68 |
69 |
70 |
71 | {
72 | repos.length > 0 &&
73 | repos.map(repo => )
74 | }
75 |
76 |
77 | {
78 | starred.length > 0 &&
79 | starred.map(repo => )
80 | }
81 |
82 |
83 |
84 |
85 |
119 |
120 | );
121 | }
122 |
123 | Index.getInitialProps = async ({ctx, store}) => {
124 | const user = store.getState().user;
125 | if(!user || !user.id){
126 | return {}
127 | }
128 |
129 | if(!isServer && cacheUserRepos && cacheUserStarred){
130 | return {
131 | repos: cacheUserRepos,
132 | starred: cacheUserStarred
133 | }
134 | }
135 |
136 | const userRepos = await api.request({url: '/user/repos'}, ctx.req, ctx.res);
137 | const userStarred = await api.request({url: '/user/starred'}, ctx.req, ctx.res);
138 |
139 | return {
140 | repos: userRepos.data,
141 | starred: userStarred.data
142 | }
143 | };
144 |
145 | export default withRouter(connect(function mapStateToProps(state){
146 | return {
147 | user: state.user
148 | }
149 | })(Index));
150 |
--------------------------------------------------------------------------------
/pages/search.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef, useMemo, memo, useCallback} from 'react'
2 | import Link from 'next/link';
3 | import {connect} from 'react-redux';
4 | import getConfig from 'next/config';
5 | import Router, {withRouter} from 'next/router';
6 | import api from '../lib/requestApi';
7 | import {Row, Col, List, Pagination} from 'antd'
8 | import Repo from '../components/Repo';
9 |
10 | const LANGUAGES = ['javascript', 'html', 'css', 'typescript', 'python', 'java'];
11 |
12 | const SORTTYPES = [
13 | {
14 | name: 'best match'
15 | },
16 | {
17 | name: 'most stars',
18 | value: 'stars',
19 | order: 'desc'
20 | },
21 | {
22 | name: 'most forks',
23 | value: 'forks',
24 | order: 'desc'
25 | }
26 | ]
27 |
28 | const selectedStyle = {
29 | borderLeft: '2px #e36209 solid',
30 | fontWeight:'100px'
31 | }
32 |
33 | const pagesize = 10;
34 |
35 | const FilterLink = ({name, query, lang, sort, order, page}) => {
36 | let queryString = `?query=${query}`;
37 | if(lang) queryString += `&lang=${lang}`;
38 | if(sort) queryString += `&sort=${sort}&order=${order || 'desc'}`;
39 | if(page) queryString += `&page=${page}`;
40 | queryString += `&per_page=${pagesize}`;
41 | return {name}
42 | }
43 |
44 | function Search({router, repos}){
45 | const {sort, order, lang, query, page} = router.query;
46 |
47 | return (
48 |
49 |
50 |
51 | 语言}
54 | dataSource={LANGUAGES}
55 | renderItem={item => {
56 | const selected = lang === item;
57 | return (
58 |
59 |
60 |
61 | )
62 | }}>
63 |
64 | 条件}
67 | dataSource={SORTTYPES}
68 | renderItem={item => {
69 | let selected = false;
70 | return (
71 |
72 |
73 |
74 | )
75 | }}>
76 |
77 |
78 | {repos.total_count} 个仓库
79 | {
80 | repos.items && repos.items.length > 0 &&
81 | repos.items.map(repo => )
82 | }
83 |
84 | {
85 | repos.items && repos.items.length > 0 &&
86 |
87 |
1000 ? 1000 : repos.total_count} itemRender={(page, type, ol) => {
88 | const p = type === 'page' ? page : type === 'prev' ? page - 1 : page + 1;
89 | const name = type === 'page' ? page : ol;
90 | return
91 | }} />
92 |
93 | }
94 |
95 |
96 |
110 |
111 |
119 |
120 |
121 | )
122 | }
123 |
124 | Search.getInitialProps = async ({ctx}) => {
125 | const {query, sort, lang, order, page} = ctx.query;
126 |
127 | if(!query){
128 | return {
129 | repos: {
130 | total_count: 0
131 | }
132 | }
133 | }
134 |
135 | let queryString = `?q=${query}`;
136 |
137 | if(lang) queryString += `+language:${lang}`;
138 | if(sort) queryString += `&sort=${sort}&order=${order || 'desc'}`;
139 | if(page) queryString += `&page=${page}`;
140 | queryString += `&per_page=${pagesize}`;
141 |
142 | const result = await api.request({ url: `/search/repositories${queryString}` }, ctx.req, ctx.res);
143 |
144 | return {
145 | repos: result.data
146 | }
147 | }
148 |
149 | export default withRouter(Search)
--------------------------------------------------------------------------------
/pages_test/author.js:
--------------------------------------------------------------------------------
1 | import {withRouter} from 'next/router';
2 | import Link from 'next/link';
3 | import {Button, Input} from 'antd';
4 | import styled from 'styled-components';
5 | import dynamic from 'next/dynamic';
6 | import {useState} from 'react'
7 |
8 | //next按需引入组件
9 | const Rc = dynamic(import('../components/rc'));
10 |
11 | const Title = styled.h1`color: yellow; font-size: 40px`;
12 |
13 | const author = (props) => {
14 | const [name, setName] = useState('hello');
15 |
16 | return (
17 | <>
18 | author
19 | setName(e.target.value)}>
20 | {props.router.query.id} {props.name}
21 |
22 | a 标签3
23 | a 标签2
24 |
31 | >
32 | )
33 | } ;
34 |
35 |
36 | author.getInitialProps = () => {
37 | return {
38 | name: 'jim'
39 | }
40 | };
41 |
42 | export default withRouter(author);
--------------------------------------------------------------------------------
/pages_test/index.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef, useMemo, memo, useCallback} from 'react'
2 | import Link from 'next/link';
3 | import {connect} from 'react-redux';
4 | import getConfig from 'next/config';
5 | import axios from 'axios';
6 |
7 | const {publicRuntimeConfig} = getConfig();
8 |
9 | function MyCountFunc(props){
10 | const spanRef = useRef();
11 | const config = useMemo(() => (
12 | {
13 | text: `count is ${props.count}`,
14 | color: props.count >= 10 ? 'red' : 'blue'
15 | }
16 | ), [props.count]);
17 |
18 | useEffect(() => {
19 | axios.get('/api/user/info').then(resp => console.log(resp));
20 |
21 | // 闭包陷阱
22 | const interval = setInterval(() => {
23 | props.add()
24 | }, 2000)
25 |
26 | // 组件卸载时执行
27 | return () => clearInterval(interval)
28 | }, []);
29 |
30 | const onClickDecrease = useCallback(() => {
31 | props.dispatch({type: 'INCREASE'})
32 | }, []);
33 |
34 | return (
35 | <>
36 | {props.count}
37 |
38 |
39 | 登录
40 | >
41 | );
42 | }
43 |
44 |
45 | const ChildEle = memo(function ({config, decrease}){
46 | return
49 | });
50 |
51 |
52 | MyCountFunc.getInitialProps = (ctx) => {
53 | const store = ctx.store;
54 | // store.dispatch({type: 'INCREASE'});
55 | return {
56 | name: process.env.entry
57 | }
58 | };
59 |
60 | export default connect(function mapStateToProps(state){
61 | return {
62 | count: state.counter.count
63 | }
64 | }, function mapDispatchToProps(dispatch){
65 | return {
66 | dispatch,
67 | add: (num) => dispatch({type: 'INCREASE' , num})
68 | }
69 | })(MyCountFunc);
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const Router = require('koa-router');
3 | const session = require('koa-session');
4 | const next = require('next');
5 | const Redis = require('ioredis');
6 | const sessionStore = require('./server/sessionStore');
7 | const auth = require('./server/auth');
8 | const routeConfig = require('./server/route');
9 | const api = require('./server/api');
10 | const koaBody = require('koa-body');
11 |
12 | const dev = process.env.NODE_ENV !== 'production';
13 | const app = next({dev});
14 | const handle = app.getRequestHandler();
15 | const redis = new Redis();
16 |
17 | app.prepare().then(() => {
18 | const server = new Koa();
19 | const router = new Router();
20 |
21 | server.use(koaBody());
22 |
23 | // 为cookie加密
24 | server.keys = ['ff8789sb'];
25 | const SESSION_CONFIG = {
26 | key: 'key',
27 | store: new sessionStore(redis)
28 | };
29 |
30 | server.use(session(SESSION_CONFIG, server));
31 |
32 | // 授权中间件
33 | auth(server);
34 |
35 | // api中间件
36 | api(server);
37 |
38 | // 使用路由中间件
39 | routeConfig(router);
40 | server.use(router.routes());
41 |
42 | // 默认中间件
43 | server.use(async(ctx, next) => {
44 | ctx.req.session = ctx.session;
45 | await handle(ctx.req, ctx.res);
46 | ctx.respond = false;
47 | });
48 |
49 | server.listen(3000, () => {
50 | console.log('koa server listening on 3000');
51 | });
52 | });
--------------------------------------------------------------------------------
/server/api.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 处理github数据请求服务端代理,免于在前端直接暴露token等,且可以走登录后请求,这样ratelimit比较大
3 | */
4 | const api = require('../lib/requestApi');
5 |
6 | module.exports = (server) => {
7 | server.use(async (ctx, next) => {
8 | const path = ctx.path;
9 | const method = ctx.method;
10 | if(path.startsWith('/github/')){
11 | const result = await api.request({method, url: ctx.url.replace('/github/', '/'), data: ctx.request.body || {}}, ctx);
12 | ctx.status = result.status;
13 | if(result.status === 200){
14 | ctx.body = result.data;
15 | }else{
16 | ctx.body = {
17 | success: false
18 | };
19 | }
20 | ctx.set('Content-type', 'application/json');
21 | }else{
22 | await next();
23 | }
24 | });
25 | }
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const clientConfig = require('../lib/clientConfig');
3 |
4 | const {accessTokenUrl, userInfo, clientID, clientSecret} = clientConfig.github;
5 |
6 | module.exports = (server) => {
7 | server.use(async (ctx, next) => {
8 |
9 | // path 带 /
10 | // 客户端根据client_id跳转授权页面后,返回code到页面上
11 | if(ctx.path === '/auth'){
12 | const code = ctx.query.code;
13 | if(!code){
14 | ctx.body = 'code not exist';
15 | return;
16 | }
17 |
18 | // 服务端根据code请求授权的token
19 | const result = await axios({
20 | method: 'POST',
21 | url: accessTokenUrl,
22 | data: {
23 | client_id: clientID,
24 | client_secret: clientSecret,
25 | code
26 | },
27 | headers: {
28 | Accept: 'application/json'
29 | }
30 | });
31 |
32 | if(result.status === 200 && result.data && !(result.data.error)){
33 | const {access_token, token_type} = result.data;
34 | ctx.session.githubAuth = result.data;
35 | const userRes = await axios({
36 | method: 'GET',
37 | url: userInfo,
38 | headers: {
39 | Authorization: `${token_type} ${access_token}`
40 | }
41 | });
42 | ctx.session.userInfo = userRes.data;
43 | if(ctx.session && ctx.session.urlBeforeOAuth){
44 | ctx.redirect(ctx.session.urlBeforeOAuth);
45 | ctx.session.urlBeforeOAuth = '';
46 | }else{
47 | ctx.redirect('/');
48 | }
49 |
50 |
51 | }else{
52 | console.log(result.message);
53 | ctx.body = result.message;
54 | }
55 |
56 | }else{
57 | await next();
58 | }
59 | });
60 |
61 |
62 | // 退出
63 | server.use(async (ctx, next) => {
64 | if(ctx.path === '/logout' && ctx.method === 'POST'){
65 | console.log('用户退出');
66 | ctx.session = null;
67 | ctx.body = '退出成功'
68 | }else{
69 | await next();
70 | }
71 | });
72 |
73 | // 记录登录前的页面地址
74 | server.use(async (ctx, next) => {
75 | if(ctx.path === '/prepare-auth' && ctx.method === 'GET'){
76 | const {url} = ctx.query;
77 | ctx.session.urlBeforeOAuth = url;
78 | console.log('记录成功', url);
79 | ctx.body = '记录成功';
80 | }else{
81 | await next();
82 | }
83 | });
84 | }
--------------------------------------------------------------------------------
/server/route.js:
--------------------------------------------------------------------------------
1 | module.exports = (router) => {
2 |
3 | // 反向映射
4 | router.get('/author/:id', async (ctx) => {
5 | const id = ctx.params.id;
6 | await handle(ctx.req, ctx.res, {
7 | pathname: '/author',
8 | query: {id}
9 | });
10 |
11 | // 把body的内容交由代码处理,koa不再自行处理
12 | ctx.respond = false;
13 | });
14 |
15 | // 设置用户信息
16 | // router.get('/set/user', async (ctx) => {
17 | // ctx.session.user = {
18 | // name: 'user',
19 | // age: 18
20 | // }
21 | // ctx.body = 'set session success';
22 | // });
23 |
24 | // router.get('/clear/user', async (ctx) => {
25 | // ctx.session = null;
26 | // ctx.body = 'clear session success';
27 | // });
28 |
29 | router.get('/api/user/info', async (ctx) => {
30 | const user = ctx.session.userInfo;
31 | if(!user){
32 | ctx.status = 401;
33 | ctx.body = 'need login'
34 | }else{
35 | ctx.body = user;
36 | ctx.set('Content-type', 'application/json');
37 | }
38 | });
39 |
40 | }
--------------------------------------------------------------------------------
/server/sessionStore.js:
--------------------------------------------------------------------------------
1 | function prefixSessionId(sid){
2 | return `ssid:${sid}`;
3 | }
4 |
5 | module.exports = class RedisSessionStore{
6 | constructor(client){
7 | this.client = client;
8 | }
9 |
10 | /**
11 | * 获取key
12 | * @param {*} sid
13 | */
14 | async get(sid){
15 | const data = await this.client.get(prefixSessionId(sid));
16 |
17 | if(!data){
18 | return null;
19 | }
20 |
21 | try{
22 | const result = JSON.parse(data);
23 | return result;
24 | } catch (e) {
25 | console.log(e);
26 | }
27 | }
28 |
29 | /**
30 | * 添加key
31 | * @param {*} sid
32 | * @param {*} value
33 | * @param {*} ttl 毫秒
34 | */
35 | async set(sid, value, ttl){
36 | const id = prefixSessionId(sid);
37 | if(typeof ttl === 'number'){
38 | ttl = Math.ceil(ttl / 1000);
39 | }
40 | try{
41 | const str = JSON.stringify(value);
42 | if(ttl){
43 | await this.client.setex(id, ttl, str);
44 | }else{
45 | await this.client.set(id, str);
46 | }
47 | } catch (e) {
48 | console.log(e);
49 | }
50 | }
51 |
52 | /**
53 | * 删除key
54 | * @param {*} sid
55 | */
56 | async destroy(sid){
57 | await this.client.del(prefixSessionId(sid));
58 | }
59 | }
--------------------------------------------------------------------------------
/store/store.js:
--------------------------------------------------------------------------------
1 | import {createStore, combineReducers, applyMiddleware} from 'redux';
2 | import ReduxThunk from 'redux-thunk';
3 | import {composeWithDevTools} from 'redux-devtools-extension';
4 | import axios from 'axios';
5 |
6 | const LOGOUT = 'LOGOUT'
7 |
8 | const userInitialState = {};
9 |
10 | function userReducer(state = userInitialState, action){
11 | switch(action.type){
12 | case LOGOUT:
13 | return {}
14 | default:
15 | return state;
16 | }
17 | }
18 |
19 | const AllReducer = combineReducers({
20 | user: userReducer
21 | });
22 |
23 | const AllState = {
24 | user: userInitialState
25 | }
26 |
27 |
28 | export function logout(){
29 | return dispatch => {
30 | axios.post('/logout').then(res => {
31 | console.log(res);
32 | if(res.status === 200){
33 | dispatch({
34 | type: LOGOUT
35 | });
36 | }else{
37 | console.log('logout failed', res);
38 | }
39 | }).catch(e => {
40 | console.log('logout post failed', e);
41 | });
42 | }
43 | }
44 |
45 | export default function initialStore(state){
46 | return createStore(
47 | AllReducer,
48 | Object.assign({}, AllState, state),
49 | composeWithDevTools(applyMiddleware(ReduxThunk))
50 | );
51 | };
--------------------------------------------------------------------------------
/test-redis.js:
--------------------------------------------------------------------------------
1 | const Redis = require('ioredis');
2 |
3 | const redis = new Redis({
4 | port: '6300',
5 | password: '123456'
6 | });
7 |
8 | const getKeys = async function(){
9 | await redis.set('mem', 1);
10 | const keys = await redis.keys('*');
11 | console.log(keys);
12 | }
13 |
14 | getKeys();
--------------------------------------------------------------------------------