├── 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 |
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 |
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 | 
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 |
37 | )
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
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 |
51 | )
52 |
53 | return (
54 |
55 |
56 | }>
57 |
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 |
--------------------------------------------------------------------------------