├── .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 |

18 | 19 | {repo.full_name} 20 | 21 |

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 | 47 | 48 | 登出 49 | 50 | 51 | ); 52 | 53 | 54 | return ( 55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 | { 69 | user && user.id ? 70 | 71 | 72 | 73 | 74 | 75 | : 76 | 77 | 78 | 79 | } 80 |
81 |
82 |
83 |
84 | 85 | 86 | {children} 87 | 88 | 89 |
©2018 Created by SJ
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(); --------------------------------------------------------------------------------