├── static ├── favicon.ico └── img │ └── 16ca8dc70d421934.png ├── store ├── actionTypes.js ├── actionCreatores.js ├── store.js └── reducer.js ├── .gitignore ├── pm2.yml ├── .babelrc ├── Dockerfile ├── lib ├── test-hoc.js ├── util.js ├── api.js ├── client-cache.js └── with-redux.js ├── components ├── Container.js ├── PageLoading.js ├── MarkdownRenderer.js ├── Repo.js ├── WithRepoBasic.js ├── Layout.js └── Layout copy.js ├── config.js ├── pages-test ├── time.js ├── a.js ├── Index.js └── c.js ├── test └── test-redis.js ├── pages ├── _document.js ├── detail │ └── index.js ├── _app.js ├── Index.js └── search.js ├── server ├── api.js ├── session-store.js └── auth.js ├── next.config.js ├── package.json ├── next.config copy.js ├── server.js └── README.md /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmj0920/next-github/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/img/16ca8dc70d421934.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmj0920/next-github/HEAD/static/img/16ca8dc70d421934.png -------------------------------------------------------------------------------- /store/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD = 'ADD' 2 | export const UPDATE_USERNAME = 'UPDATE_USERNAME' 3 | export const LOGOUT='LOGOUT' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # dependencies 3 | /node_modules 4 | 5 | # testing 6 | /coverage 7 | 8 | # production 9 | /.next 10 | 11 | /dest 12 | /bundles 13 | -------------------------------------------------------------------------------- /pm2.yml: -------------------------------------------------------------------------------- 1 | apps: 2 | - script: ./server.js 3 | name: next-github 4 | env_production: 5 | NODE_ENV: production 6 | HOST: localhost 7 | PORT: 3001 -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "import", 6 | { 7 | "libraryName": "antd" 8 | } 9 | ], 10 | [ 11 | "styled-components", {"ssr": true} 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /store/actionCreatores.js: -------------------------------------------------------------------------------- 1 | import { ADD, UPDATE_USERNAME } from './actionTypes' 2 | 3 | //action creatore 4 | // function add(num) { 5 | // return { 6 | // type: ADD, 7 | // num 8 | // } 9 | // } 10 | 11 | export const add = (num) => ({ 12 | type: ADD, 13 | num 14 | }) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:9 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | RUN rm -f package-lock.json \ 7 | ; rm -rf .idea \ 8 | ; rm -rf node_modules \ 9 | ; npm config set registry "https://registry.npm.taobao.org/" \ 10 | && npm install 11 | 12 | EXPOSE 3001 13 | CMD ["npm", "run", "dev"] 14 | -------------------------------------------------------------------------------- /lib/test-hoc.js: -------------------------------------------------------------------------------- 1 | export default (Comp)=>{ 2 | return function TestHocComp({Component,pageProps ,...rest}){ 3 | if(pageProps){ 4 | pageProps.test='123' 5 | } 6 | 7 | return 8 | } 9 | TestHocComp.getInitialProps=Comp.getInitialProps 10 | return TestHocComp 11 | } -------------------------------------------------------------------------------- /store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { composeWithDevTools } from 'redux-devtools-extension' 4 | import { allReducers, userinitialState } from "./reducer"; 5 | 6 | export default function initializeStore(state) { 7 | const store = createStore( 8 | allReducers, 9 | { ...userinitialState, ...state }, 10 | composeWithDevTools(applyMiddleware(thunk)), 11 | ) 12 | return store 13 | } 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /components/Container.js: -------------------------------------------------------------------------------- 1 | import { cloneElement } from 'react' 2 | 3 | const style = { 4 | width:'100%', 5 | maxWidth:1200, 6 | marginRight:'auto', 7 | marginLeft: 'auto', 8 | paddingLeft: 20, 9 | paddingRight:20 10 | } 11 | 12 | export default ({ children, renderer =
}) => { 13 | 14 | const newElement = cloneElement(renderer, { 15 | style: Object.assign({}, renderer.props.style, style), 16 | children 17 | }) 18 | 19 | return newElement 20 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const GITHUB_OAUTH_URL = 'https://github.com/login/oauth/authorize' 2 | const SCOPE = 'user' 3 | const github = { 4 | request_token_url: 'https://github.com/login/oauth/access_token', 5 | client_id: 'e04b253391e64ce559aa', 6 | client_secret: '616cb01993e6f10b77bf127ba437adebb76ba55e', 7 | github_user_url:'https://api.github.com/user' 8 | } 9 | module.exports = { 10 | github, 11 | GITHUB_OAUTH_URL, 12 | OAUTH_URL: `${GITHUB_OAUTH_URL}?client_id=${github.client_id}&scope=${SCOPE}`, 13 | } 14 | -------------------------------------------------------------------------------- /pages-test/time.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | //删除import moment 3 | function Time(){ 4 | 5 | const [nowTime,setTime] = useState(Date.now()) 6 | 7 | const changeTime= async ()=>{ //把方法变成异步模式 8 | const moment = await import('moment') //等待moment加载完成 9 | setTime(moment.default(Date.now()).format()) //注意使用defalut 10 | } 11 | return ( 12 | <> 13 |
显示时间为:{nowTime}
14 |
15 | 16 | ) 17 | } 18 | export default Time -------------------------------------------------------------------------------- /test/test-redis.js: -------------------------------------------------------------------------------- 1 | const Redis=require("ioredis") 2 | async function test(){ 3 | const redis=new Redis( 4 | { 5 | port: 6379, // Redis port 6 | // host: "127.0.0.1", // Redis host 7 | // family: 4, // 4 (IPv4) or 6 (IPv6) 8 | password: "123456", 9 | // db: 0 10 | } 11 | ) 12 | 13 | 14 | await redis.set('c',123) 15 | //await redis.setex('a', 10,123) //时间存储 16 | const key = await redis.keys('*') 17 | console.log(key) 18 | console.log(await redis.get('c')) 19 | } 20 | 21 | test(); -------------------------------------------------------------------------------- /components/PageLoading.js: -------------------------------------------------------------------------------- 1 | import { Spin } from 'antd' 2 | 3 | export default () => ( 4 |
5 | 6 | 20 |
21 | ) -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | 4 | 5 | export const genDetailCacheKey = (ctx) => { 6 | const { query, pathname } = ctx 7 | const { owner, name } = query 8 | return `${pathname}-${owner}-${name}` 9 | } 10 | 11 | export const genDetailCacheKeyStrate = (context) => { 12 | const { ctx } = context 13 | return genDetailCacheKey(ctx) 14 | } 15 | 16 | export const genCacheKeyByQuery = (query) => { 17 | return Object.keys(query).reduce((prev, cur) => { 18 | prev += query[cur] 19 | return prev 20 | }, '') 21 | } 22 | 23 | //中文 24 | // moment.locale('zh-cn') 25 | export function getTimeFromNow(time) { 26 | return moment(time).fromNow() 27 | } 28 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document' 2 | import { ServerStyleSheet } from 'styled-components' 3 | export default class MyDocument extends Document { 4 | static getInitialProps({ renderPage }) { 5 | const sheet = new ServerStyleSheet() 6 | const page = renderPage(App => props => sheet.collectStyles()) 7 | const styleTags = sheet.getStyleElement() 8 | return { ...page, styleTags } 9 | } 10 | render() { 11 | return ( 12 | 13 | 14 | {this.props.styleTags} 15 | 16 | 17 |
18 | 19 | 20 | 21 | ) 22 | } 23 | } -------------------------------------------------------------------------------- /components/MarkdownRenderer.js: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from 'react' 2 | import MarkdownIt from 'markdown-it' 3 | import 'github-markdown-css' 4 | const md = new MarkdownIt({ 5 | html: true, //讲html标签转化 6 | linkify: true, //实现url跳转 7 | }) 8 | 9 | //字符串转化 10 | const b64ToUtf8 = (str) => { 11 | return decodeURIComponent(escape(atob(str))) 12 | } 13 | //memo 优化 props不发生变化不重新渲染组件 14 | export default memo(({ content, isBase64 }) => { 15 | //判断是否是 base64进行转化 16 | const base64Converted = isBase64 ? b64ToUtf8(content) : content 17 | //优化 base64Converted 不发生变化不重新渲染 18 | const html = useMemo(() => md.render(base64Converted), [base64Converted]) 19 | return ( 20 |
21 |
22 |
23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /pages-test/a.js: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'next/router' 2 | import axios from 'axios' 3 | import styled from 'styled-components' 4 | 5 | const Title = styled.h1` 6 | color:yellow; 7 | font-size:40px; 8 | ` 9 | 10 | const A = ({ router, name }) => { 11 | return (<>title) 12 | } 13 | A.getInitialProps = async (ctx) => { 14 | const promise = new Promise((resolve) => { 15 | axios('http://47.95.225.57:3000/banner').then( 16 | (res) => { 17 | console.log('远程数据结果:', res.data.banners) 18 | resolve({ 19 | name: res.data.banners[1].imageUrl 20 | }) 21 | } 22 | ) 23 | }) 24 | return await promise 25 | } 26 | export default withRouter(A) -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | const { requestGithub } = require('../lib/api') 2 | 3 | module.exports = (server) => { 4 | server.use(async (ctx, next) => { 5 | const { path, url, method } = ctx 6 | 7 | const proxyPrefix = '/github/' 8 | if (path.startsWith(proxyPrefix)) { 9 | const { session } = ctx 10 | const { githubAuth } = session || {} 11 | const { access_token, token_type } = githubAuth || {} 12 | const headers = {} 13 | if (access_token) { 14 | headers.Authorization = `${token_type} ${access_token}` 15 | } 16 | const result = await requestGithub( 17 | method, 18 | url.replace('/github/', '/'), 19 | ctx.request.body || {}, 20 | headers, 21 | ) 22 | ctx.status = result.status 23 | ctx.body = result.data 24 | } else { 25 | await next() 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withCss = require('@zeit/next-css') 2 | const withBundleAnalyzer = require('@zeit/next-bundle-analyzer') 3 | const webpack = require('webpack') 4 | const config = require('./config') 5 | 6 | if (typeof require !== 'undefined') { 7 | require.extensions['.css'] = () => { } 8 | } 9 | 10 | const { GITHUB_OAUTH_URL } = config 11 | // withCss得到的是一个nextjs的config配置 12 | module.exports = withBundleAnalyzer(withCss({ 13 | 14 | webpack(webpackConfig) { 15 | webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)) 16 | return webpackConfig 17 | }, 18 | publicRuntimeConfig: { 19 | GITHUB_OAUTH_URL, 20 | OAUTH_URL: config.OAUTH_URL, 21 | }, 22 | analyzeBrowser: ['browser', 'both'].includes(process.env.BUNDLE_ANALYZE), 23 | analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE), 24 | 25 | bundleAnalyzerConfig: { 26 | server: { 27 | analyzerMode: 'static', 28 | reportFilename: '../bundles/server.html', 29 | }, 30 | browser: { 31 | analyzerMode: 'static', 32 | reportFilename: '../bundles/client.html', 33 | }, 34 | }, 35 | })) 36 | -------------------------------------------------------------------------------- /pages/detail/index.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import WithRepoBasic from '../../components/WithRepoBasic' 3 | import { request } from '../../lib/api' 4 | import initCache from '../../lib/client-cache' 5 | import { genDetailCacheKeyStrate, genDetailCacheKey } from '../../lib/util' 6 | import dynamic from 'next/dynamic' 7 | const MarkdownRenderer = dynamic(() => import('../../components/MarkdownRenderer'),{ 8 | loading:()=>

Loading

9 | }) 10 | 11 | const { cache, useCache } = initCache({ 12 | genCacheKeyStrate: (context) => { 13 | return genDetailCacheKeyStrate(context) 14 | }, 15 | }) 16 | const Detail = ({ readme }) => { 17 | 18 | const router = useRouter() 19 | 20 | //根据路由缓存 21 | useCache(genDetailCacheKey(router), { 22 | readme 23 | }) 24 | 25 | return ( 26 | 30 | ) 31 | } 32 | 33 | Detail.getInitialProps = cache(async ({ ctx }) => { 34 | const { owner, name } = ctx.query; 35 | const { data: readme } = await request({ 36 | url: `/repos/${owner}/${name}/readme`, 37 | }, ctx.req, ctx.res) 38 | return { 39 | readme 40 | } 41 | }) 42 | 43 | export default WithRepoBasic(Detail) 44 | -------------------------------------------------------------------------------- /store/reducer.js: -------------------------------------------------------------------------------- 1 | import { LOGOUT } from './actionTypes' 2 | import { combineReducers } from 'redux' 3 | import { message } from 'antd'; 4 | import axios from 'axios' 5 | 6 | export const userinitialState = {} 7 | 8 | export const userReducers = (state = userinitialState, action) => { 9 | switch (action.type) { 10 | case LOGOUT: 11 | return {} 12 | default: 13 | return state 14 | } 15 | } 16 | 17 | export function logout() { 18 | return (dispatch) => { 19 | axios.post('/logout') 20 | .then((resp) => { 21 | if (resp.status === 200) { 22 | dispatch({ 23 | type: LOGOUT 24 | }) 25 | message.success('注销成功') 26 | } else { 27 | console.log('logout failed', resp) 28 | } 29 | }) 30 | .catch((e) => { 31 | console.log('logout failed', e) 32 | }) 33 | } 34 | } 35 | 36 | export const allReducers = combineReducers({ 37 | user: userReducers, 38 | }) 39 | 40 | // export default { 41 | // allReducers, 42 | // userinitialState 43 | // } 44 | 45 | // export default combineReducers({ 46 | // user: userReducers, 47 | // }); -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const GITHUB_BASE_URL = 'https://api.github.com' 4 | 5 | const isServer = typeof window === 'undefined' 6 | 7 | // 服务端环境需要手动拼接url 8 | async function requestGithub(method, url, data, headers) { 9 | if (!url) { 10 | throw new Error('url must be provided') 11 | } 12 | const result = await axios({ 13 | method, 14 | url: `${GITHUB_BASE_URL}${url}`, 15 | data, 16 | headers, 17 | }) 18 | 19 | return result 20 | } 21 | 22 | async function request( 23 | { 24 | method = 'GET', 25 | url, 26 | data = {}, 27 | }, 28 | req, 29 | res, 30 | ) { 31 | if (isServer) { 32 | const { session } = req 33 | const { githubAuth } = session || {} 34 | const { access_token, token_type } = githubAuth || {} 35 | const headers = {} 36 | if (access_token) { 37 | headers.Authorization = `${token_type} ${access_token}` 38 | } 39 | // 服务端走requestGithub方法, 40 | // 补全api的前缀 41 | const serverResult = await requestGithub(method, url, data, headers) 42 | return serverResult 43 | } 44 | 45 | // 客户端需要拼接github前缀 让koa的server可以识别并代理 46 | const clientResult = await axios({ 47 | method, 48 | url: `/github${url}`, 49 | data, 50 | }) 51 | return clientResult 52 | } 53 | 54 | module.exports = { 55 | request, 56 | requestGithub, 57 | } 58 | -------------------------------------------------------------------------------- /pages-test/Index.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import axios from 'axios' 3 | import Head from 'next/head' 4 | import { connect } from 'react-redux' 5 | import { ADD, UPDATE_USERNAME } from '../store/actionTypes' 6 | 7 | import getCofnig from 'next/config' 8 | const { publicRuntimeConfig } = getCofnig() 9 | 10 | const Index = ({ counter, username, add, rename }) => { 11 | 12 | useEffect(() => { 13 | axios.get('/api/user/info').then(res => { 14 | console.log(res) 15 | }) 16 | },[]) 17 | 18 | 19 | return ( 20 | <> 21 | 22 | 首页 23 | 24 |
25 |
Count:{counter}
26 |
UserName:{username}
27 | rename(e.target.value)} /> 28 | 29 | 登录 30 |
31 | 32 | ) 33 | 34 | 35 | } 36 | 37 | 38 | 39 | export default connect(function mapStateToProps(state) { 40 | return { 41 | counter: state.counter.count, 42 | username: state.user.username 43 | } 44 | }, function mapDispatchToProps(dispatch) { 45 | return { 46 | add: (num) => dispatch({ type: ADD, num }), 47 | rename: (name) => dispatch({ type: UPDATE_USERNAME, name }) 48 | } 49 | })(Index) 50 | -------------------------------------------------------------------------------- /lib/client-cache.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import LRU from 'lru-cache' 3 | 4 | const isServer = typeof window === 'undefined' 5 | const DEFAULT_CACHE_KEY = 'cache' 6 | export default function clientCache({ lruConfig = {}, genCacheKeyStrate } = {}) { 7 | // 默认10分钟缓存 8 | const { 9 | maxAge = 1000 * 60 * 10, 10 | ...restConfig 11 | } = lruConfig || {} 12 | 13 | const lruCache = new LRU({ 14 | maxAge, 15 | ...restConfig, 16 | }) 17 | 18 | function getCacheKey(context) { 19 | return genCacheKeyStrate ? genCacheKeyStrate(context) : DEFAULT_CACHE_KEY 20 | } 21 | 22 | function cache(fn) { 23 | // 服务端不能保留缓存 会在多个用户之间共享 24 | if (isServer) { 25 | return fn 26 | } 27 | 28 | return async (...args) => { 29 | const key = getCacheKey(...args) 30 | const cached = lruCache.get(key) 31 | if (cached) { 32 | return cached 33 | } 34 | const result = await fn(...args) 35 | lruCache.set(key, result) 36 | return result 37 | } 38 | } 39 | 40 | function setCache(key, cachedData) { 41 | lruCache.set(key, cachedData) 42 | } 43 | 44 | // 允许客户端外部手动设置缓存数据 45 | function useCache(key, cachedData) { 46 | useEffect(() => { 47 | if (!isServer) { 48 | setCache(key, cachedData) 49 | } 50 | }, []) 51 | } 52 | 53 | return { 54 | cache, 55 | useCache, 56 | setCache, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-github", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "node server.js", 9 | "build": "next build", 10 | "start": "NODE_ENV=production node server.js", 11 | "export": "next export", 12 | "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@zeit/next-bundle-analyzer": "^0.1.2", 18 | "@zeit/next-css": "^1.0.1", 19 | "antd": "^3.23.5", 20 | "atob": "^2.1.2", 21 | "axios": "^0.19.0", 22 | "babel-plugin-import": "^1.12.1", 23 | "babel-plugin-styled-components": "^1.1.5", 24 | "classnames": "^2.2.6", 25 | "cross-env": "^6.0.0", 26 | "github-markdown-css": "^3.0.1", 27 | "ioredis": "^4.14.0", 28 | "koa": "^2.8.1", 29 | "koa-body": "^4.1.1", 30 | "koa-router": "^7.4.0", 31 | "koa-session": "^5.12.3", 32 | "lru-cache": "^5.1.1", 33 | "markdown-it": "^10.0.0", 34 | "moment": "^2.24.0", 35 | "next": "^9.0.5", 36 | "react": "^16.9.0", 37 | "react-dom": "^16.9.0", 38 | "react-redux": "^7.1.1", 39 | "redux": "^4.0.4", 40 | "redux-devtools-extension": "^2.13.8", 41 | "redux-thunk": "^2.3.0", 42 | "styled-components": "^2.1.0", 43 | "webpack": "^4.41.0" 44 | }, 45 | "devDependencies": { 46 | "eslint": "^6.3.0", 47 | "eslint-config-airbnb": "^18.0.1", 48 | "eslint-plugin-import": "^2.18.2", 49 | "eslint-plugin-jsx-a11y": "^6.2.3", 50 | "eslint-plugin-react": "^7.14.3", 51 | "eslint-plugin-react-hooks": "^1.7.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pages-test/c.js: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import Link from 'next/link' 3 | import Router from 'next/router' 4 | const Index = () => { 5 | function gotoA() { 6 | // Router.push('/jspangA?name=渣渣新A') 7 | Router.push({ 8 | pathname: '/a', 9 | query: { 10 | name: '渣渣新' 11 | } 12 | },'/a/渣渣新') 13 | } 14 | const events = [ 15 | 'routeChangeStart', 16 | 'routeChangeComplete', 17 | 'beforeHistoryChange', 18 | 'routeChangeError', 19 | 'hashChangeStart', 20 | 'hashChangeComplete' 21 | ] 22 | 23 | function makeEvent(type){ 24 | return(...args)=>{ 25 | console.log(type, ...args) 26 | } 27 | } 28 | 29 | events.forEach(event=>{ 30 | Router.events.on(event,makeEvent(event)) 31 | }) 32 | 33 | return ( 34 |
35 | 36 | 37 | 38 | 39 | 40 |
41 | ) 42 | } 43 | 44 | export default Index 45 | 46 | 47 | import Head from 'next/head' 48 | import {connect} from 'react-redux' 49 | 50 | 51 | 52 | const Index = () => { 53 | return ( 54 | <> 55 | 56 | 首页 57 | 58 |
59 | index 60 |
61 | 66 | 67 | 72 | 73 | ) 74 | } 75 | 76 | export default connect()(Index) 77 | -------------------------------------------------------------------------------- /server/session-store.js: -------------------------------------------------------------------------------- 1 | // 加上前缀 2 | function getRedisSessionId(sessionId) { 3 | return `ssid:${sessionId}` 4 | } 5 | 6 | class RedisSessionStore { 7 | constructor(client) { 8 | // node.js的redis-client 9 | this.client = client 10 | } 11 | 12 | // 获取redis中存储的session数据 13 | async get(sessionId) { 14 | // console.log(" get session "+sessionId) 15 | const id = getRedisSessionId(sessionId) 16 | // 对应命令行操作redis的get指令,获取value 17 | const data = await this.client.get(id) 18 | if (!data) { 19 | return null 20 | } 21 | try { 22 | const result = JSON.parse(data) 23 | return result 24 | } catch (err) { 25 | console.error(err) 26 | } 27 | } 28 | 29 | // 在redis中存储session数据 30 | async set(sessionId, session, ttl /** 过期时间 */) { 31 | // console.log("set session "+sessionId) 32 | const id = getRedisSessionId(sessionId) 33 | let ttlSecond 34 | if (typeof ttl === 'number') { 35 | // 毫秒转秒 36 | ttlSecond = Math.ceil(ttl / 1000) 37 | } 38 | try { 39 | const sessionStr = JSON.stringify(session) 40 | // 根据是否有过期时间 调用不同的api 41 | if (ttl) { 42 | // set with expire 43 | await this.client.setex(id, ttlSecond, sessionStr) 44 | } else { 45 | await this.client.set(id, sessionStr) 46 | } 47 | } catch (error) { 48 | console.error('error: ', error); 49 | } 50 | } 51 | 52 | // 从resid中删除某个session 53 | // 在koa中 设置ctx.session = null时,会调用这个方法 54 | async destroy(sessionId) { 55 | // console.log("destroy session "+sessionId) 56 | const id = getRedisSessionId(sessionId) 57 | await this.client.del(id) 58 | } 59 | } 60 | 61 | module.exports = RedisSessionStore 62 | -------------------------------------------------------------------------------- /components/Repo.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { Icon } from 'antd' 3 | import { getTimeFromNow } from '../lib/util' 4 | 5 | function getLicense(license) { 6 | return license ? `${license.spdx_id} license` : '' 7 | } 8 | 9 | export default ({ repo }) => { 10 | return ( 11 |
12 |
13 |

14 | 15 | {repo.full_name} 16 | 17 |

18 |

19 | {repo.description} 20 |

21 |

22 | {repo.license ? ( 23 | {getLicense(repo.license)} 24 | ) : null} 25 | {getTimeFromNow(repo.updated_at)} 26 | {repo.open_issues_count} open issues 27 |

28 |
29 |
30 | 31 | {repo.language} 32 | 33 | 34 | {repo.stargazers_count} 35 | 36 |
37 | 70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, { Container } from 'next/app' 2 | import { Provider } from 'react-redux' 3 | import 'antd/dist/antd.css' 4 | import React from 'react' 5 | import Layout from '../components/Layout' 6 | import withRedux from '../lib/with-redux' 7 | import PageLoading from '../components/PageLoading' 8 | import Router from 'next/router' 9 | class Myapp extends App { 10 | 11 | // App组件的getInitialProps比较特殊 12 | // 能拿到一些额外的参数 13 | // Component: 被包裹的组件 14 | 15 | static async getInitialProps(ctx) { 16 | const { Component } = ctx 17 | let pageProps = {} 18 | 19 | // 拿到Component上定义的getInitialProps 20 | if (Component.getInitialProps) { 21 | 22 | // 执行拿到返回结果 23 | pageProps = await Component.getInitialProps(ctx) 24 | } 25 | // 返回给组件 26 | return { pageProps } 27 | } 28 | 29 | state = { 30 | context: 'value', 31 | loading: false, 32 | } 33 | //开始loading 34 | startLoading = () => { 35 | this.setState({ 36 | loading: true, 37 | }) 38 | } 39 | //关闭 loading 40 | stopLoading = () => { 41 | this.setState({ 42 | loading: false, 43 | }) 44 | } 45 | 46 | componentDidMount() { 47 | Router.events.on('routeChangeStart', this.startLoading) 48 | Router.events.on('routeChangeComplete', this.stopLoading) 49 | Router.events.on('routeChangeError', this.stopLoading) 50 | } 51 | 52 | componentWillUnmount() { 53 | Router.events.off('routeChangeStart', this.startLoading) 54 | Router.events.off('routeChangeComplete', this.stopLoading) 55 | Router.events.off('routeChangeError', this.stopLoading) 56 | } 57 | 58 | 59 | render() { 60 | const { Component, pageProps, reduxStore } = this.props 61 | return ( 62 | 63 | 64 | 65 | {this.state.loading ? : null} 66 | {/* 把pageProps解构后传递给组件 */} 67 | 68 | 69 | 70 | 71 | ) 72 | } 73 | } 74 | 75 | export default withRedux(Myapp) -------------------------------------------------------------------------------- /next.config copy.js: -------------------------------------------------------------------------------- 1 | const withCss = require('@zeit/next-css') 2 | const config = require('./config') 3 | const withBundleAnalyzer = require('@zeit/next-bundle-analyzer') 4 | const { GITHUB_OAUTH_URL } = config 5 | // 配置说明 6 | const configs = { 7 | // 编译文件的输出目录 8 | distDir: 'dest', 9 | // 是否给每个路由生成Etag 10 | // Etag是用来做缓存验证的,如果路由执行的时候,新的Etag是相同的,那么就会复用当前内容,而无需重新渲染 11 | // 默认情况下,nextJS是会对每个路由生成Etag的。但是如果我们部署的时候,ngx已经做了Etag的配置, 12 | //那么就可以关闭nextJS的Etag,节省性能 13 | generateEtags: true, 14 | // (不常用)页面内容缓存配置,只针对开发环境 15 | onDemandEntries: { 16 | // 内容在内存中缓存的时长(ms) 17 | maxInactiveAge: 25 * 1000, 18 | // 最多同时缓存多少个页面 19 | pagesBufferLength: 2, 20 | }, 21 | // 在pages目录下那种后缀的文件会被认为是页面 22 | pageExtensions: ['jsx', 'js'], 23 | // (不常用)配置buildId,一般用于同一个项目部署多个节点的时候用到 24 | generateBuildId: async () => { 25 | if (process.env.YOUR_BUILD_ID) { 26 | return process.env.YOUR_BUILD_ID 27 | } 28 | 29 | // 返回null,使用nextJS默认的unique id 30 | return null 31 | }, 32 | // (重要配置)手动修改webpack config 33 | webpack(config, options) { 34 | return config 35 | }, 36 | // (重要配置)修改webpackDevMiddleware配置 37 | webpackDevMiddleware: config => { 38 | return config 39 | }, 40 | // (重要配置)可以在页面上通过 procsess.env.customKey 获取 value。跟webpack.DefinePlugin实现的一致 41 | env: { 42 | customKey: 'value', 43 | }, 44 | // 下面两个要通过 'next/config' 来读取 45 | // 只有在服务端渲染时才会获取的配置 46 | serverRuntimeConfig: { 47 | mySecret: 'secret', 48 | secondSecret: process.env.SECOND_SECRET, 49 | }, 50 | // 在服务端渲染和客户端渲染都可获取的配置 51 | publicRuntimeConfig: { 52 | GITHUB_OAUTH_URL, 53 | OAUTH_URL: config.OAUTH_URL, 54 | staticFolder: '/static', 55 | }, 56 | // 上面这两个配置在组件里使用方式如下: 57 | // import getCofnig from 'next/config' 58 | // const { serverRuntimeConfig,publicRuntimeConfig } = getCofnig() 59 | // console.log( serverRuntimeConfig,publicRuntimeConfig ) 60 | } 61 | 62 | if (typeof require !== 'undefined') { 63 | require.extensions['.css'] = file => { } 64 | } 65 | 66 | // 虽然next-css看起来是一个处理样式的插件,实则它是接收的一个对象,可以把传入的其他非css相关的webpack配置一并处理。 67 | // 建议不要直接写一个新的webpack配置,因为next-css里面的webpack的配置是非常全面的,如果被覆盖了,可能会导致报错。 68 | module.exports =withBundleAnalyzer(withCss(configs)) 69 | 70 | -------------------------------------------------------------------------------- /lib/with-redux.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import initializeStore from '../store/store' 3 | 4 | const isServer = typeof window === 'undefined' 5 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__' 6 | 7 | //继承 redux 8 | function getOrCreateStore(initialState) { 9 | if (isServer) { 10 | // 服务端每次执行都重新创建一个store 11 | return initializeStore(initialState) 12 | } 13 | // 在客户端执行这个方法的时候 优先返回window上已有的store 14 | // 而不能每次执行都重新创建一个store 否则状态就无限重置了 15 | if (!window[__NEXT_REDUX_STORE__]) { 16 | window[__NEXT_REDUX_STORE__] = initializeStore(initialState) 17 | } 18 | return window[__NEXT_REDUX_STORE__] 19 | } 20 | 21 | export default (Comp) => { 22 | class withReduxApp extends React.Component { 23 | constructor(props) { 24 | super(props) 25 | // getInitialProps创建了store 这里为什么又重新创建一次? 26 | // 因为服务端执行了getInitialProps之后 返回给客户端的是序列化后的字符串 27 | // redux里有很多方法 不适合序列化存储 28 | // 所以选择在getInitialProps返回initialReduxState初始的状态 29 | // 再在这里通过initialReduxState去创建一个完整的store 30 | this.reduxStore = getOrCreateStore(props.initialReduxState) 31 | } 32 | 33 | render() { 34 | const { Component, pageProps, ...rest } = this.props 35 | return ( 36 | 42 | ) 43 | } 44 | } 45 | 46 | // 这个其实是_app.js的getInitialProps 47 | // 在服务端渲染和客户端路由跳转时会被执行 48 | // 所以非常适合做redux-store的初始化 49 | withReduxApp.getInitialProps = async (ctx) => { 50 | let reduxStore 51 | 52 | if (isServer) { 53 | // const {req} = ctx.ctx.req 54 | const { ctx: { req } } = ctx 55 | const { session } = req 56 | if (session && session.userInfo) { 57 | reduxStore = getOrCreateStore({ 58 | user: session.userInfo, 59 | // user: { 60 | // name: 'zmj', 61 | // age: 18 62 | // } 63 | }) 64 | } else { 65 | reduxStore = getOrCreateStore() 66 | } 67 | } else { 68 | reduxStore = getOrCreateStore() 69 | } 70 | ctx.reduxStore = reduxStore 71 | 72 | let appProps = {} 73 | if (typeof Comp.getInitialProps === 'function') { 74 | appProps = await Comp.getInitialProps(ctx) 75 | } 76 | // const reduxStore = getOrCreateStore() 77 | // ctx.reduxStore = reduxStore 78 | return { 79 | ...appProps, 80 | initialReduxState: reduxStore.getState(), 81 | } 82 | } 83 | 84 | return withReduxApp 85 | } 86 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const Router = require('koa-router') 3 | const next = require('next') 4 | const session = require('koa-session') 5 | const Redis = require('ioredis') 6 | const koaBody = require('koa-body') 7 | const atob = require('atob') 8 | const auth = require('./server/auth') 9 | const api = require('./server/api') 10 | const RedisSessionStore = require('./server/session-store') 11 | 12 | const dev = process.env.NODE_ENV !== 'production' 13 | const app = next({ dev }) 14 | const handle = app.getRequestHandler() 15 | // 实例化一个redisClient 16 | const redisClient = new Redis( 17 | { 18 | port: 6379, // Redis port 19 | host: "47.95.225.57", // Redis host 20 | // family: 4, // 4 (IPv4) or 6 (IPv6) 21 | // password: "123456", 22 | // db: 0 23 | } 24 | ) 25 | const PORT = 3001 26 | // 给node全局增加atob方法 27 | global.atob = atob 28 | // 等到pages目录编译完成后启动服务响应请求 29 | app.prepare().then(() => { 30 | const server = new Koa() 31 | const router = new Router() 32 | 33 | // 用于给session加密 34 | server.keys = ['ssh develop github app'] 35 | // 解析post请求的内容 36 | server.use(koaBody()) 37 | 38 | const sessionConfig = { 39 | // 设置到浏览器的cookie里的key 40 | key: 'sid', 41 | // 将自定义存储逻辑传给koa-session 42 | store: new RedisSessionStore(redisClient), 43 | } 44 | server.use(session(sessionConfig, server)) 45 | 46 | // 处理github Oauth登录 47 | auth(server) 48 | // 处理github请求代理 49 | api(server) 50 | 51 | 52 | 53 | router.get('/api/user/info', async (ctx) => { 54 | const { userInfo } = ctx.session 55 | if (userInfo) { 56 | ctx.body = userInfo 57 | // 设置头部 返回json 58 | ctx.set('Content-Type', 'application/json') 59 | } else { 60 | ctx.status = 401 61 | ctx.body = 'Need Login' 62 | } 63 | }) 64 | 65 | server.use(router.routes()) 66 | 67 | server.use(async (ctx) => { 68 | 69 | // req里获取session 70 | ctx.req.session = ctx.session 71 | await handle(ctx.req, ctx.res) 72 | ctx.respond = false 73 | }) 74 | 75 | server.listen(PORT, () => { 76 | console.log(`koa server listening on ${PORT}`) 77 | }) 78 | }) 79 | 80 | 81 | // router.get('/a/:id', async (ctx) => { 82 | // const { id } = ctx.params 83 | // await handle(ctx.req, ctx.res, { 84 | // pathname: '/a', 85 | // query: { 86 | // id, 87 | // }, 88 | // }) 89 | // ctx.respond = false 90 | // }) 91 | 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 教你玩转react、next、hooks 、koa、antd 2 | 3 | #### 前言 4 | 以下文章纯属个人理解,便于记录学习,肯定有理解错误或理解不到位的地方, 5 | 意在站在前辈的肩膀,分享个人对技术的通俗理解,共同成长! 6 | 7 | 8 | > 官方网站:https://nextjs.org 9 | 10 | > 中文官网:https://nextjs.frontendx.cn 11 | 12 | > API: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ 13 | 14 | > 项目体验地址 :http://www.taobaods.xin:5000 15 | 16 | 17 | 本项目使用Next.js开发 React 服务端渲染,因为当我们要做SEO优化时是一件很麻烦的事情, 18 | 常常需要配置很多繁琐的参数使我们开发增加难度,然而Next.js框架帮助我们解决了很多的配置问题, 19 | 使我们开发的时候变得简单本项目使用技术有 next react-hooks redux react-redux redux-thunk 20 | 服务端使用 koa redis 21 | 22 | > Author:君吟 23 | 24 | > Email: 506499594@qq.com 25 | 26 | > github: https://github.com/zmj0920/ 27 | 28 | 29 | #### 运行 30 | ``` 31 | npm run dev 32 | 33 | ``` 34 | 35 | #### 目录结构 36 | ``` 37 | ├── components 非页面级共用组件 38 | │   └── Layout.jsx 路由配置文件 39 | ├── lib 一些通用的js 40 | │   └── with-redux.js 继承redux 41 | ├── pages 页面级组件 会被解析成路由 42 | │   └── _app.js 自定义 app配置 43 | │   └── _document.js 自定义 document 配置 44 | │   └── index.js 首页 45 | ├── server 服务端文件 46 | │   └── auth.js github auth 授权 47 | │   └── session-store.js 使用redis缓存store 48 | ├── static 静态资源 49 | ├── store redux使用相关文件 50 | ├── test 测试文件 51 | ├── babelrc babel 编译配置 52 | ├── gitignore git不需要上传文件配置 53 | ├── next.config.js next 相关配置 54 | ├── package.json 项目依赖配置文件 55 | ├── README.md 项目说明文件 56 | ├── server.js 服务端文件 57 | ``` 58 | 59 | 安装依赖 60 | 61 | ``` 62 | "@zeit/next-bundle-analyzer": "^0.1.2", //观察打包代码 63 | "@zeit/next-css": "^1.0.1", //支持css文件引入配置 64 | "antd": "^3.23.1", // ui库使用 65 | "atob": "^2.1.2", //对base64编码过的字符串进行解码 66 | "axios": "^0.19.0", // 数据请求 67 | "babel-plugin-import": "^1.12.1", //在编译过程中将 import 的写法自动转换为按需引入ui组件 68 | "github-markdown-css": "^3.0.1", // github-markdown 的样式美化 69 | "ioredis": "^4.14.0", //链接redis使用 70 | "koa": "^2.8.1", //koa框架 71 | "koa-body": "^4.1.1", 72 | "koa-router": "^7.4.0", 73 | "koa-session": "^5.12.3", 74 | "lru-cache": "^5.1.1", //数据缓存 75 | "moment": "^2.24.0", //日期格式化 76 | "next": "^9.0.5", 77 | "react": "^16.9.0", 78 | "react-dom": "^16.9.0", 79 | "react-redux": "^7.1.1", //状态管理 80 | "redux": "^4.0.4", 81 | "redux-devtools-extension": "^2.13.8", //监听redux状态调试工具 82 | "redux-thunk": "^2.3.0", 83 | "styled-components": "^2.1.0" //css 组件 84 | ``` 85 | 86 | 87 | ## ssr 流程 88 | ![ssr渲染流程](./static/img/16ca8dc70d421934.png) 89 | -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | // 处理github返回的auth code 2 | const axios = require('axios') 3 | const config = require('../config') 4 | const { client_id, client_secret, request_token_url, github_user_url } = config.github 5 | 6 | module.exports = (server) => { 7 | server.use(async (ctx, next) => { 8 | if (ctx.path === '/auth') { 9 | const { code } = ctx.query 10 | if (code) { 11 | // 获取Oauth鉴权信息 12 | const result = await axios({ 13 | method: 'POST', 14 | url: request_token_url, 15 | data: { 16 | client_id, 17 | client_secret, 18 | code, 19 | }, 20 | headers: { 21 | Accept: 'application/json', 22 | }, 23 | }) 24 | // github 可能会在status是200的情况下返回error信息 25 | if (result.status === 200 && (result.data && !result.data.error)) { 26 | ctx.session.githubAuth = result.data 27 | const { access_token, token_type } = result.data 28 | // 获取用户信息 29 | const { data: userInfo } = await axios({ 30 | method: 'GET', 31 | url: github_user_url, 32 | headers: { 33 | Authorization: `${token_type} ${access_token}`, 34 | }, 35 | }) 36 | 37 | ctx.session.userInfo = userInfo 38 | // 重定向到登录前的页面或首页 39 | ctx.redirect((ctx.session && ctx.session.urlBeforeOAuth) || '/') 40 | ctx.session.urlBeforeOAuth = '' 41 | } else { 42 | ctx.body = `request token failed ${result.data && result.data.error}` 43 | } 44 | } else { 45 | ctx.body = 'code not exist' 46 | } 47 | } else { 48 | await next() 49 | } 50 | }) 51 | 52 | // 登出逻辑 53 | server.use(async (ctx, next) => { 54 | const { path, method } = ctx 55 | if (path === '/logout' && method === 'POST') { 56 | ctx.session = null 57 | ctx.body = 'logout success' 58 | } else { 59 | await next() 60 | } 61 | }) 62 | 63 | // 在进行auth之前 记录请求时的页面url 64 | server.use(async (ctx, next) => { 65 | const { path, method } = ctx 66 | if (path === '/prepare-auth' && method === 'GET') { 67 | const { url } = ctx.query 68 | //存储 url 69 | ctx.session.urlBeforeOAuth = url 70 | //跳转授权重定向 维持OAuth之前得页面访问 71 | ctx.redirect(config.OAUTH_URL) 72 | } else { 73 | await next() 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /components/WithRepoBasic.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { withRouter } from 'next/router' 3 | import Repo from './Repo' 4 | import { request } from '../lib/api' 5 | import initCache from '../lib/client-cache' 6 | import { genDetailCacheKeyStrate, genDetailCacheKey } from '../lib/util' 7 | 8 | //截取 将 query 转化成url参数 9 | function makeQuery(queryObject) { 10 | const query = Object.entries(queryObject) 11 | .reduce((result, entry) => { 12 | result.push(entry.join('=')) 13 | return result 14 | }, []) 15 | .join('&') 16 | return `?${query}` 17 | } 18 | 19 | const { cache, useCache } = initCache({ 20 | genCacheKeyStrate: genDetailCacheKeyStrate, 21 | }) 22 | export default (Comp, type = 'index') => { 23 | //把剩余的props 传递下去 rest 24 | const WithDetail = ({ repoBasic, router, ...rest }) => { 25 | useCache(genDetailCacheKey(router), { repoBasic, ...rest }) 26 | const query = makeQuery(router.query) 27 | return ( 28 |
29 |
30 | 31 |
32 | 33 | {type === 'index' ? ( 34 | Readme 35 | ) : ( 36 | Readme 37 | )} 38 | 39 | 40 | {type === 'issues' ? ( 41 | Issues 42 | ) : ( 43 | 44 | )} 45 | 46 |
47 |
48 |
49 | {/*hoc常用操作 不用的 props都要传递下去给目标组件 把剩余的props 传递下去 rest */} 50 | 51 |
52 | 69 |
70 | ) 71 | } 72 | 73 | WithDetail.getInitialProps = cache(async (context) => { 74 | const { ctx } = context 75 | const { owner, name } = ctx.query 76 | const { data: repoBasic } = await request({ 77 | url: `/repos/${owner}/${name}`, 78 | }, ctx.req, ctx.res) 79 | //吧传过来的getInitialProps进行赋值 80 | let pageData = {} 81 | if (Comp.getInitialProps) { 82 | pageData = await Comp.getInitialProps(context) 83 | } 84 | return { 85 | repoBasic, 86 | ...pageData, 87 | } 88 | }) 89 | 90 | return withRouter(WithDetail) 91 | } 92 | -------------------------------------------------------------------------------- /pages/Index.js: -------------------------------------------------------------------------------- 1 | import { Button, Icon, Tabs } from 'antd' 2 | import getConfig from 'next/config' 3 | import Router, { withRouter } from 'next/router' 4 | import { useSelector } from 'react-redux' 5 | import { request } from '../lib/api' 6 | import initCache from '../lib/client-cache' 7 | import Repo from '../components/Repo' 8 | 9 | const { publicRuntimeConfig } = getConfig() 10 | 11 | const { cache, useCache } = initCache() 12 | 13 | const Index = ({ userRepos, starred, router }) => { 14 | const user = useSelector((store) => store.user) 15 | const tabKey = router.query.key || '1' 16 | 17 | useCache('cache', { 18 | userRepos, 19 | starred, 20 | }) 21 | 22 | if (!user || !user.id) { 23 | return ( 24 |
25 |

亲,您还没有登录哦

26 | 29 | 40 |
41 | ) 42 | } 43 | 44 | const { 45 | avatar_url, login, name, bio, email, 46 | } = user 47 | 48 | const handleTabChange = (activeKey) => { 49 | Router.push(`/?key=${activeKey}`) 50 | } 51 | 52 | return ( 53 |
54 |
55 | 56 | {login} 57 | {name} 58 | {bio} 59 |

60 | 61 | {email} 62 |

63 |
64 |
65 | 66 | 67 | {userRepos.map((repo) => ( 68 | 69 | ))} 70 | 71 | 72 | {starred.map((repo) => ( 73 | 74 | ))} 75 | 76 | 77 |
78 | 124 |
125 | ) 126 | } 127 | 128 | Index.getInitialProps = cache(async ({ ctx, reduxStore }) => { 129 | //判断用户是否登出 130 | const { user } = reduxStore.getState() 131 | if (!user || !user.id) { 132 | return {} 133 | } 134 | //个人仓库 135 | const { data: userRepos } = await request( 136 | { 137 | url: '/user/repos', 138 | }, 139 | ctx.req, 140 | ctx.res, 141 | ) 142 | //关注的仓库 143 | const { data: starred } = await request( 144 | { 145 | url: '/user/starred', 146 | }, 147 | ctx.req, 148 | ctx.res, 149 | ) 150 | return { 151 | userRepos, 152 | starred, 153 | } 154 | }) 155 | 156 | export default withRouter(Index) 157 | -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import { 2 | Layout, Icon, Input, Avatar, Button, Tooltip, Dropdown, Menu, 3 | } from 'antd' 4 | import { withRouter } from 'next/router' 5 | import Link from 'next/link' 6 | import { useState } from 'react' 7 | import { useSelector, useDispatch } from 'react-redux' 8 | import Container from './Container' 9 | // import { logout } from '../store/store' 10 | import { logout } from '../store/reducer' 11 | const { Header, Content, Footer } = Layout 12 | 13 | const AppLayout = ({ children, router }) => { 14 | const { query: { query = '' } = {} } = router 15 | const user = useSelector((store) => store.user) 16 | const dispatch = useDispatch() 17 | 18 | const [search, setSearch] = useState(query) 19 | const handleSearchChange = (e) => { 20 | setSearch(e.target.value) 21 | } 22 | 23 | const handleOnSearch = () => { 24 | router.push(`/search?query=${search}`) 25 | } 26 | 27 | const handleLogout = () => { 28 | dispatch(logout()) 29 | } 30 | 31 | const UserDropDown = ( 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | 39 | return ( 40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 59 |
60 |
61 |
62 |
63 | {user.id ? ( 64 | 65 | 66 | 67 | 68 | 69 | ) : ( 70 | 71 | 72 | 73 | 74 | 75 | )} 76 |
77 |
78 |
79 |
80 |
81 | 82 | 83 | {children} 84 | 85 | 86 | 91 | 111 | 136 |
137 | ) 138 | } 139 | 140 | export default withRouter(AppLayout) 141 | -------------------------------------------------------------------------------- /components/Layout copy.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | import { Layout, Icon, Input, Avatar, Tooltip, Dropdown, Menu, Button } from 'antd'; 3 | import Container from './Container' 4 | import { connect, useSelector, useDispatch } from 'react-redux' 5 | import { logout } from '../store/reducer' 6 | import { withRouter } from 'next/router' 7 | import Link from 'next/link' 8 | const { Header, Content, Footer } = Layout; 9 | //图标样式 10 | const githubIconStyle = { 11 | color: 'white', 12 | fontSize: 40, 13 | display: 'block', 14 | paddingTop: 10, 15 | marginRight: 20 16 | } 17 | //底部样式 18 | const footerStyle = { 19 | textAlign: 'center' 20 | } 21 | 22 | const AppLayout= ({ children, user, router }) => { 23 | // const {query} = router.query.router 24 | // const {query:{query}}=router 25 | const { query: { query = '' } = {} } = router 26 | const [search, setSeach] = useState(query) 27 | //useSelector(),而不用担心重复渲染的情况 28 | // const user = useSelector((store) => store.user) 29 | const dispatch = useDispatch() 30 | 31 | //搜索事件 32 | const handleSearchChange = useCallback((event) => { 33 | setSeach(event.target.value) 34 | }, [setSeach]) 35 | 36 | //搜索按钮触发事件 37 | const handleOnSearch = useCallback(() => { 38 | router.push(`/search?query=${search}`) 39 | }, [search]) 40 | //登出 41 | const handleLogout = useCallback(() => { 42 | dispatch(logout()) 43 | }, [dispatch]) 44 | 45 | const UserDropDown = ( 46 | 47 | 48 | 49 | 50 | 51 | ) 52 | 53 | return ( 54 | 55 |
56 | }> 57 |
58 |
59 | 60 | 61 | 62 |
63 |
64 | 70 |
71 |
72 |
73 |
74 | { 75 | user && user.id ? ( 76 | 77 | 78 | 79 | 80 | 81 | ) : ( 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | 90 |
91 |
92 |
93 |
94 | 95 | 96 | {children} 97 | 98 | 99 | 104 | 114 | 129 |
130 | ) 131 | } 132 | 133 | // export default AppLayout 134 | //connect映射state 135 | 136 | export default connect(function mapState(state) { 137 | return { 138 | user: state.user 139 | } 140 | })(withRouter(AppLayout)) -------------------------------------------------------------------------------- /pages/search.js: -------------------------------------------------------------------------------- 1 | import { memo, isValidElement } from 'react' 2 | import { withRouter } from 'next/router' 3 | import { 4 | Row, Col, List, Pagination, 5 | } from 'antd' 6 | import Link from 'next/link' 7 | import classNames from 'classnames' 8 | import Repo from '../components/Repo' 9 | import initCache from '../lib/client-cache' 10 | import { genCacheKeyByQuery } from '../lib/util' 11 | import { request } from '../lib/api' 12 | 13 | const { cache, useCache } = initCache({ 14 | genCacheKeyStrate: (context) => { 15 | return genCacheKeyByQuery(context.ctx.query) 16 | }, 17 | }) 18 | 19 | /** 20 | * 关心的search条件 21 | * sort: 排序方式 22 | * order: 排序升降顺序 23 | * lang: 仓库开发主语言 24 | * page: 分页 25 | */ 26 | const LANGUAGES = ['JavaScript', 'HTML', 'CSS', 'TypeScript', 'Java', 'Vue', 'React'] 27 | const SORT_TYPES = [ 28 | { 29 | name: 'Best Match', 30 | }, 31 | { 32 | name: 'Most Starts', 33 | sort: 'stars', 34 | order: 'desc', 35 | }, 36 | { 37 | name: 'Fewest Starts', 38 | sort: 'stars', 39 | order: 'asc', 40 | }, 41 | { 42 | name: 'Most Forks', 43 | sort: 'forks', 44 | order: 'desc', 45 | }, 46 | { 47 | name: 'Fewest Forks', 48 | sort: 'forks', 49 | order: 'asc', 50 | }, 51 | ] 52 | 53 | const PER_PAGE = 20 54 | 55 | const FilterLink = memo(({ 56 | children, query, lang, sort, order, page, selected, 57 | }) => { 58 | if (selected) { 59 | return {children} 60 | } 61 | 62 | let queryString = `?query=${query}` 63 | 64 | if (lang) { 65 | queryString += `&lang=${lang}` 66 | } 67 | 68 | if (sort) { 69 | queryString += `&sort=${sort}&order=${order}` 70 | } 71 | 72 | if (page) { 73 | queryString += `&page=${page}` 74 | } 75 | 76 | queryString += `&per_page=${PER_PAGE}` 77 | 78 | return ( 79 | 80 | {/* 判断是否是Element*/} 81 | {isValidElement(children) ? children : {children}} 82 | 83 | ) 84 | }) 85 | 86 | const Search = ({ router, searchRepos }) => { 87 | const { query } = router 88 | const { 89 | sort, order, lang, page = 1, 90 | } = query 91 | 92 | useCache(genCacheKeyByQuery(query), { searchRepos }) 93 | 94 | return ( 95 |
96 | 97 | 98 | 语言 104 | )} 105 | renderItem={(language) => { 106 | const selected = lang === language 107 | 108 | return ( 109 | 113 | 118 | {language} 119 | 120 | 121 | ) 122 | }} 123 | /> 124 | 排序 130 | )} 131 | renderItem={(sortItem) => { 132 | const { name: itemName, sort: itemSort, order: itemOrder } = sortItem 133 | let selected = false 134 | if (itemName === 'Best Match' && !sort) { 135 | selected = true 136 | } else if (itemSort === sort && itemOrder === order) { 137 | selected = true 138 | } 139 | return ( 140 | 144 | 150 | {itemName} 151 | 152 | 153 | ) 154 | }} 155 | /> 156 | 157 | 158 |

{searchRepos.total_count} 个仓库

159 | { 160 | searchRepos.items.map((repo) => ) 161 | } 162 |
163 | { }} 169 | //页码 向前向后按钮 图标 170 | itemRender={(renderPage, renderType, renderOl) => { 171 | const targetPage = renderType === 'page' 172 | ? renderPage 173 | : renderType === 'prev' 174 | ? page - 1 175 | : page + 1 176 | 177 | const name = renderType === 'page' ? renderPage : renderOl 178 | 179 | return 182 | {name} 183 | 184 | }} 185 | /> 186 |
187 | 188 |
189 | 216 |
217 | ) 218 | } 219 | 220 | Search.getInitialProps = cache(async ({ ctx }) => { 221 | const { 222 | query, sort, lang, order = 'desc', page, 223 | } = ctx.query 224 | 225 | if (!query) { 226 | return { 227 | repos: { 228 | total_count: 0, 229 | }, 230 | } 231 | } 232 | 233 | let queryString = `?q=${query}` 234 | 235 | if (lang) { 236 | queryString += `+language:${lang}` 237 | } 238 | 239 | if (sort) { 240 | queryString += `&sort=${sort}&order=${order}` 241 | } 242 | 243 | if (page) { 244 | queryString += `&page=${page}` 245 | } 246 | 247 | queryString += `&per_page=${PER_PAGE}` 248 | 249 | const { data: searchRepos } = await request({ 250 | url: `/search/repositories${queryString}`, 251 | }, ctx.req, ctx.res) 252 | 253 | return { 254 | searchRepos 255 | } 256 | }) 257 | 258 | export default withRouter(Search) 259 | --------------------------------------------------------------------------------