├── .babelrc
├── .gitignore
├── README.md
├── bundles
└── client.html
├── components
├── Container.jsx
├── Layout.jsx
├── MarkdownRenderer.jsx
├── PageLoading.jsx
├── Repo.jsx
├── SearchUser.jsx
├── comp.jsx
└── with-repo-basic.jsx
├── ecosystem.config.js
├── global.config.js
├── lib
├── api.js
├── my-context.js
├── repo-basic-cache.js
├── utils.js
└── with-redux.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── detail
│ ├── index.js
│ └── issues.js
├── index.js
└── search.js
├── register-oauth.png
├── return.html
├── server.js
├── server
├── api.js
├── auth.js
└── session-store.js
├── static
└── favicon.png
└── store
└── store.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "import",
6 | {
7 | "libraryName": "antd"
8 | }
9 | ],
10 | ["styled-components", { "ssr": true }]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | config.js
3 | .next
4 | .idea
5 | dest
6 | .vscode
7 | local-code
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## React-SSR-With-NextJs
2 |
3 | #### 1、首先需要安装下载 redis
4 |
5 | • 官方版本是不支持 Windows 系统的,微软自己实现了支持 win64 位系统的版本
6 |
7 | • [redis-windows](https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504)
8 |
9 | • [Redis 服务端下载](https://github.com/ServiceStack/redis-windows/raw/master/downloads/redis-latest.zip)
10 |
11 | • [Redis 客户端下载](https://redisdesktop.com/download)
12 |
13 | • [Redis 官网](https://redis.io/)
14 |
15 | • [Redis 中文网](http://www.redis.cn/documentation.html)
16 |
17 | ***
18 |
19 | #### 2、找到 github 上的 [开发者设置](https://github.com/settings/developers) ,在上面注册自己的 OAuth App
20 |
21 | * 记录 Client ID 和 Client Secret 到 global.config.js 中
22 |
23 | * Homepage URL:填写应用的域名端口(如:http://localhost:3000)
24 |
25 | * Authorization callback URL:后台服务地址 + '/auth'(如:http://localhost:3000/auth)
26 |
27 | 
28 |
29 | ***
30 |
31 | #### 3、安装依赖
32 | `npm install `
33 |
34 | ***
35 |
36 | #### 4、启动调试项目
37 | `npm run dev `
38 |
39 | ***
40 |
41 |
--------------------------------------------------------------------------------
/components/Container.jsx:
--------------------------------------------------------------------------------
1 | import { cloneElement } from 'react';
2 |
3 | const style = {
4 | width: '100%',
5 | maxWidth: 1200,
6 | marginLeft: 'auto',
7 | marginRight: 'auto',
8 | paddingLeft: 20,
9 | paddingRight: 20,
10 | };
11 |
12 | export default ({ children, renderer =
}) => {
13 | const newElement = cloneElement(renderer, {
14 | style: Object.assign({}, renderer.props.style, style),
15 | children,
16 | });
17 |
18 | return newElement
19 | }
20 |
--------------------------------------------------------------------------------
/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import {useState, useCallback} from 'react';
2 | import {connect} from 'react-redux';
3 | import {withRouter} from 'next/router';
4 | import Link from 'next/link';
5 | import {
6 | Button,
7 | Layout,
8 | Icon,
9 | Input,
10 | Avatar,
11 | Tooltip,
12 | Dropdown,
13 | Menu,
14 | } from 'antd';
15 |
16 | import Container from './Container';
17 |
18 | import {logout} from '../store/store';
19 |
20 | const {Header, Content, Footer} = Layout;
21 |
22 | const githubIconStyle = {
23 | color: 'white',
24 | fontSize: 40,
25 | display: 'block',
26 | paddingTop: 10,
27 | marginRight: 20,
28 | };
29 |
30 | const footerStyle = {
31 | textAlign: 'center',
32 | };
33 |
34 | function MyLayout({children, user, logout, router}) {
35 | const urlQuery = router.query && router.query.query;
36 |
37 | const [search, setSearch] = useState(urlQuery || '');
38 |
39 | const handleSearchChange = useCallback(
40 | event => {
41 | setSearch(event.target.value);
42 | },
43 | [setSearch],
44 | );
45 |
46 | const handleOnSearch = useCallback(() => {
47 | router.push(`/search?query=${search}`);
48 | }, [search]);
49 |
50 | const handleLogout = useCallback(() => {
51 | logout();
52 | }, [logout]);
53 |
54 | const userDropDown = (
55 |
62 | );
63 |
64 | return (
65 |
66 |
67 | }>
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
81 |
82 |
83 |
84 |
85 | {user && user.id ? (
86 |
87 |
88 |
89 |
90 |
91 | ) : (
92 |
93 | {/* 点击 a 标签,浏览器会向服务器发起请求*/}
94 |
95 |
96 |
97 |
98 | )}
99 |
100 |
101 |
102 |
103 |
104 | {children}
105 |
106 |
119 |
134 |
135 | )
136 | }
137 |
138 | export default connect(
139 | function mapState(state) {
140 | return {
141 | user: state.user,
142 | }
143 | },
144 | function mapReducer(dispatch) {
145 | return {
146 | logout: () => dispatch(logout()),
147 | }
148 | },
149 | )(withRouter(MyLayout))
150 |
--------------------------------------------------------------------------------
/components/MarkdownRenderer.jsx:
--------------------------------------------------------------------------------
1 | import { memo, useMemo } from 'react'
2 | import 'github-markdown-css'
3 | import MarkdownIt from 'markdown-it'
4 |
5 | const md = new MarkdownIt({
6 | html: true,
7 | linkify: true,
8 | });
9 |
10 | function b64_to_utf8(str) {
11 | return decodeURIComponent(escape(atob(str)))
12 | }
13 |
14 | export default memo(function MarkdownRenderer({ content, isBase64 }) {
15 | const markdown = isBase64 ? b64_to_utf8(content) : content;
16 |
17 | const html = useMemo(() => md.render(markdown), [markdown]);
18 |
19 | return (
20 |
23 | )
24 | })
25 |
--------------------------------------------------------------------------------
/components/PageLoading.jsx:
--------------------------------------------------------------------------------
1 | import { Spin } from 'antd'
2 |
3 | export default () => (
4 |
5 |
6 |
20 |
21 | )
22 |
--------------------------------------------------------------------------------
/components/Repo.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Icon } from 'antd'
3 |
4 | import { getLastUpdated } from '../lib/utils'
5 |
6 | function getLicense(license) {
7 | return license ? `${license.spdx_id} license` : ''
8 | }
9 |
10 | export default ({ repo }) => {
11 | return (
12 |
13 |
14 |
19 |
{repo.description}
20 |
21 | {repo.license ? (
22 | {getLicense(repo.license)}
23 | ) : null}
24 |
25 | {getLastUpdated(repo.updated_at)}
26 |
27 |
28 | {repo.open_issues_count} open issues
29 |
30 |
31 |
32 |
33 | {repo.language}
34 |
35 | {repo.stargazers_count}
36 |
37 |
38 |
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/components/SearchUser.jsx:
--------------------------------------------------------------------------------
1 | import {useState, useCallback, useRef} from 'react';
2 | import {Select, Spin} from 'antd';
3 | import debounce from 'lodash/debounce';
4 |
5 | import api from '../lib/api';
6 |
7 | const Option = Select.Option;
8 |
9 | function SearchUser({onChange, value}) {
10 | const lastFetchIdRef = useRef(0);
11 | const [fetching, setFetching] = useState(false);
12 | const [options, setOptions] = useState([]);
13 |
14 | const fetchUser = useCallback(
15 | debounce(value => {
16 | lastFetchIdRef.current += 1;
17 | const fetchId = lastFetchIdRef.current;
18 | setFetching(true);
19 | setOptions([]);
20 |
21 | api.request({
22 | url: `/search/users?q=${value}`,
23 | }).then(resp => {
24 | if (fetchId !== lastFetchIdRef.current) {
25 | return
26 | }
27 | const data = resp.data.items.map(user => ({
28 | text: user.login,
29 | value: user.login,
30 | }));
31 |
32 | setFetching(false);
33 | setOptions(data)
34 | })
35 | }, 500),
36 | [],
37 | );
38 |
39 | const handleChange = value => {
40 | setOptions([]);
41 | setFetching(false);
42 | onChange(value);
43 | };
44 |
45 | return (
46 | : nothing}
50 | filterOption={false}
51 | placeholder="创建者"
52 | value={value}
53 | onChange={handleChange}
54 | onSearch={fetchUser}
55 | allowClear={true}
56 | >
57 | {options.map(op => (
58 |
61 | ))}
62 |
63 | )
64 | }
65 |
66 | export default SearchUser
67 |
--------------------------------------------------------------------------------
/components/comp.jsx:
--------------------------------------------------------------------------------
1 | export default ({ children }) => Lazy Component
2 |
--------------------------------------------------------------------------------
/components/with-repo-basic.jsx:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 | import Repo from './Repo';
3 | import Link from 'next/link';
4 | import {withRouter} from 'next/router';
5 |
6 | import api from '../lib/api';
7 | import {getCache, cache} from '../lib/repo-basic-cache';
8 |
9 | const isServer = typeof window === 'undefined';
10 |
11 | function makeQuery(queryObject) {
12 | const query = Object.entries(queryObject)
13 | .reduce((result, entry) => {
14 | result.push(entry.join('='));
15 | return result
16 | }, [])
17 | .join('&');
18 | return `?${query}`
19 | }
20 |
21 |
22 | export default function (Comp, type = 'index') {
23 | function WithDetail({repoBasic, router, ...rest}) {
24 | const query = makeQuery(router.query);
25 |
26 | useEffect(() => {
27 | if (!isServer) {
28 | cache(repoBasic)
29 | }
30 | },[repoBasic]);
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | {type === 'index' ? (
38 |
Readme
39 | ) : (
40 |
41 |
Readme
42 |
43 | )}
44 | {type === 'issues' ? (
45 |
Issues
46 | ) : (
47 |
48 |
Issues
49 |
50 | )}
51 |
52 |
53 |
54 |
55 |
56 |
72 |
73 | )
74 | }
75 |
76 | WithDetail.getInitialProps = async context => {
77 | const {router, ctx} = context;
78 | const {owner, name} = ctx.query;
79 |
80 | const full_name = `${owner}/${name}`;
81 |
82 | let pageData = {};
83 | // HOC 需要执行传递进来的组件的 getInitialProps
84 | if (Comp.getInitialProps) {
85 | pageData = await Comp.getInitialProps(context);
86 | }
87 | if (getCache(full_name)) {
88 | return {
89 | repoBasic: getCache(full_name),
90 | ...pageData,
91 | }
92 | }
93 | const repoBasic = await api.request(
94 | {
95 | url: `/repos/${owner}/${name}`,
96 | },
97 | ctx.req,
98 | ctx.res,
99 | );
100 |
101 | return {
102 | repoBasic: repoBasic.data,
103 | ...pageData,
104 | }
105 | };
106 |
107 | return withRouter(WithDetail);
108 | }
109 |
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [
3 | {
4 | name: 'next-project',
5 | script: './server.js',
6 | instances: 1,
7 | autorestart: true,
8 | watch: false,
9 | max_memory_restart: '1G',
10 | env: {
11 | NODE_ENV: 'production',
12 | },
13 | },
14 | ],
15 | }
16 |
--------------------------------------------------------------------------------
/global.config.js:
--------------------------------------------------------------------------------
1 | const GITHUB_OAUTH_URL = 'https://github.com/login/oauth/authorize';
2 |
3 | // 希望得到的授权
4 | const SCOPE = 'user';
5 |
6 | // https://github.com/settings/developers
7 | // 在 github 上注册的 App 的 Client ID
8 | const client_id = '';
9 | // 在 github 上注册的 App 的 Client Secret
10 | const client_secret = '';
11 |
12 | module.exports = {
13 | github: {
14 | request_token_url: 'https://github.com/login/oauth/access_token',
15 | client_id,
16 | client_secret,
17 | },
18 | GITHUB_OAUTH_URL,
19 | OAUTH_URL: `${GITHUB_OAUTH_URL}?client_id=${client_id}&scope=${SCOPE}`,
20 | };
21 |
--------------------------------------------------------------------------------
/lib/api.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | const github_base_url = 'https://api.github.com';
4 |
5 | async function requestGithub(method, url, data, headers) {
6 | return await axios({
7 | method,
8 | url: `${github_base_url}${url}`,
9 | data,
10 | headers,
11 | })
12 | }
13 |
14 | const isServer = typeof window === 'undefined';
15 |
16 |
17 | async function request({ method = 'GET', url, data = {} }, req, res) {
18 | if (!url) {
19 | throw Error('url must provide')
20 | }
21 | // 这里需要区分是客户端还是服务端
22 | if (isServer) {
23 | const session = req.session;
24 | const githubAuth = session.githubAuth || {};
25 | const headers = {};
26 | // 向 github 服务器发请求需要将 token 传入到 headers
27 | if (githubAuth.access_token) {
28 | headers['Authorization'] = `${githubAuth.token_type} ${
29 | githubAuth.access_token
30 | }`
31 | }
32 | return await requestGithub(method, url, data, headers);
33 | }
34 | else {
35 | // 因为客户端不直接向 github 服务器请求,而是向自己的服务器请求
36 | // 然后自己的服务器去请求github 服务器
37 | // 所以这里不需要将 token 传入到 headers
38 | return await axios({
39 | method,
40 | // 在客户端的请求前面统一加上 /github
41 | url: `/github${url}`,
42 | data,
43 | })
44 | }
45 | }
46 |
47 | module.exports = {
48 | request,
49 | requestGithub,
50 | };
51 |
--------------------------------------------------------------------------------
/lib/my-context.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | export default React.createContext('')
3 |
--------------------------------------------------------------------------------
/lib/repo-basic-cache.js:
--------------------------------------------------------------------------------
1 | // 按照时间来计算的缓存策略
2 | import LRU from 'lru-cache'
3 |
4 | // 使用 LRU 缓存更新策略
5 | const GITHUB_DATA_CACHE = new LRU({
6 | maxAge: 1000 * 60 * 60,
7 | });
8 |
9 | export function cache(data, key) {
10 | key = key || data.full_name || data.name;
11 | GITHUB_DATA_CACHE.set(key, data);
12 | }
13 |
14 | export function getCache(key) {
15 | return GITHUB_DATA_CACHE.get(key);
16 | }
17 |
18 | export function cacheArray(dataArr) {
19 | if (dataArr && Array.isArray(dataArr)) {
20 | dataArr.forEach(item => cache(item));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 |
3 | export function getLastUpdated(time) {
4 | return moment(time).fromNow()
5 | }
6 |
--------------------------------------------------------------------------------
/lib/with-redux.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 用于包裹 App 的 HOC Redux 组件
3 | * 为什么要用 HOC 尼?
4 | * 因为创建 redux 的逻辑是独立的
5 | * 如果放到 _app.js 里面的话,之后再新增一些别的逻辑代码
6 | * 那么会导致 _app.js 这个文件的逻辑很复杂,所以用 HOC 将 redux 的逻辑抽离处理
7 | */
8 | import React from 'react'
9 | import createSore from '../store/store'
10 |
11 | // 判断当前环境是否处于服务端
12 | const isServer = typeof window === 'undefined';
13 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__';
14 |
15 | function getOrCreateStore(initialState) {
16 | // 服务端需要每次都去新建一个 store
17 | if (isServer) {
18 | return createSore(initialState);
19 | }
20 |
21 | if (!window[__NEXT_REDUX_STORE__]) {
22 | window[__NEXT_REDUX_STORE__] = createSore(initialState);
23 | }
24 | // 在客户端页面切换时,每次都会调用这个方法,不能每次都去新建一个 store,所以需要缓存一个 store
25 | return window[__NEXT_REDUX_STORE__];
26 | }
27 |
28 | // 因为 Next.js 会给 _app.js 默认导出的 App 组件传递一些属性
29 | // 又因为这里的 HOC 包裹了 App,所以这里的 HOC 也会接收到 Next.js 传递过来的属性
30 | export default AppComp => {
31 | class WithReduxApp extends React.Component {
32 | constructor(props) {
33 | super(props);
34 | // 为防止每次服务端重新渲染时,都使用同一个 store,需要每次都重新创建一个 store
35 | // 如果不重新创建 store ,store 中的状态就不会重置,下一次服务端渲染的时候,还会保留上一次的值
36 | this.reduxStore = getOrCreateStore(props.initialReduxState);
37 | }
38 |
39 | render() {
40 | const { Component, pageProps, ...rest } = this.props;
41 |
42 | return (
43 |
49 | )
50 | }
51 | }
52 |
53 | // 如果这里不需要自定义 getInitialProps 方法的话,需要默认给 HOC 定义一个 getInitialProps 方法
54 | // 因为 Next.js 在页面渲染之前会去执行一下组件的 getInitialProps
55 | // WithReduxApp.getInitialProps = AppComp.getInitialProps
56 |
57 | // getInitialProps 方法会在服务端渲染时执行一次,客户端每次页面跳转,也会执行一次
58 | // 所以在客户端页面切换时,不能每次都去新建一个 store
59 |
60 | // getInitialProps 返回的内容,会被序列化成字符串,写到在服务端返回给客户端的页面中(查看 return.html)
61 | // 客户端会去读取这个 script 中的字符串内容,然后转换成 JS 对象,最终在客户端生成一个 store
62 |
63 |
64 | WithReduxApp.getInitialProps = async ctx => {
65 | let reduxStore;
66 |
67 | if (isServer) {
68 | const { req } = ctx.ctx;
69 | const session = req.session;
70 |
71 | if (session && session.userInfo) {
72 | // 在服务端渲染时,默认传递用户的数据
73 | // 当用户输入 url ,浏览器向服务端请求页面时,在服务端渲染时把数据放进去
74 | // 这样浏览器请求返回的页面中就已经有用户数据了,可以立马显示了
75 | // 不用像以前一样,浏览器先渲染好一部分内容,然后浏览器发送 ajax 请求去获取用户数据
76 | // 等待服务器响应完成后,获取数据再去渲染,这其中等待的时间还是挺长的
77 | reduxStore = getOrCreateStore({
78 | user: session.userInfo,
79 | })
80 | } else {
81 | reduxStore = getOrCreateStore();
82 | }
83 | }
84 | else {
85 | reduxStore = getOrCreateStore();
86 | }
87 |
88 | ctx.reduxStore = reduxStore;
89 |
90 | let appProps = {};
91 | // 如果 AppComp.getInitialProps 存在
92 | // 就去获取一下 App 的初始数据
93 | if (typeof AppComp.getInitialProps === 'function') {
94 | appProps = await AppComp.getInitialProps(ctx);
95 | }
96 |
97 | return {
98 | ...appProps,
99 | // 这里不能直接返回一个 store
100 | // 因为 store 里面会有很多方法,在服务端序列化时,很难转成字符串,在客户端又很难反序列化出来
101 | initialReduxState: reduxStore.getState(),
102 | }
103 | };
104 |
105 | return WithReduxApp
106 | }
107 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const withCss = require('@zeit/next-css');
3 | const withBundleAnalyzer = require('@zeit/next-bundle-analyzer');
4 | const config = require('./global.config');
5 |
6 | const configs = {
7 | // 编译文件的输出目录
8 | distDir: 'dest',
9 | // 是否给每个路由生成 Etag
10 | // 如果 Nginx 配置了,那么这里就不需要配置了
11 | generateEtags: true,
12 | // 页面内容缓存配置
13 | onDemandEntries: {
14 | // 内容在内存中缓存的时长(ms)
15 | maxInactiveAge: 25 * 1000,
16 | // 同时缓存多少个页面
17 | pagesBufferLength: 2,
18 | },
19 | // 在pages目录下那种后缀的文件会被认为是页面
20 | pageExtensions: ['jsx', 'js'],
21 | // 配置buildId
22 | generateBuildId: async () => {
23 | if (process.env.YOUR_BUILD_ID) {
24 | return process.env.YOUR_BUILD_ID
25 | }
26 |
27 | // 返回null使用默认的unique id
28 | return null
29 | },
30 | // 手动修改webpack config
31 | webpack(config, options) {
32 | return config
33 | },
34 | // 修改webpackDevMiddleware配置
35 | webpackDevMiddleware: config => {
36 | return config
37 | },
38 | // 可以在页面上通过 procsess.env.customKey 获取 value
39 | env: {
40 | customKey: 'value',
41 | },
42 | // 下面两个要通过 'next/config' 来读取
43 | // 只有在服务端渲染时才会获取的配置
44 | serverRuntimeConfig: {
45 | mySecret: 'secret',
46 | secondSecret: process.env.SECOND_SECRET,
47 | },
48 | // 在服务端渲染和客户端渲染都可获取的配置
49 | publicRuntimeConfig: {
50 | staticFolder: '/static',
51 | },
52 | };
53 |
54 | if (typeof require !== 'undefined') {
55 | require.extensions['.css'] = file => {
56 | }
57 | }
58 |
59 | module.exports = withBundleAnalyzer(
60 | withCss({
61 | webpack(config) {
62 | config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));
63 | return config
64 | },
65 | publicRuntimeConfig: {
66 | GITHUB_OAUTH_URL: config.GITHUB_OAUTH_URL,
67 | OAUTH_URL: config.OAUTH_URL,
68 | },
69 | analyzeBrowser: ['browser', 'both'].includes(process.env.BUNDLE_ANALYZE),
70 | bundleAnalyzerConfig: {
71 | server: {
72 | analyzerMode: 'static',
73 | reportFilename: '../bundles/server.html',
74 | },
75 | browser: {
76 | analyzerMode: 'static',
77 | reportFilename: '../bundles/client.html',
78 | },
79 | },
80 | }),
81 | );
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-project",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "node server.js",
8 | "build": "next build",
9 | "start": "cross-env NODE_ENV=production node server.js",
10 | "export": "next export",
11 | "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "@zeit/next-bundle-analyzer": "^0.1.2",
17 | "@zeit/next-css": "^1.0.1",
18 | "antd": "^3.15.2",
19 | "atob": "^2.1.2",
20 | "axios": "^0.18.0",
21 | "babel-plugin-import": "^1.11.0",
22 | "babel-plugin-styled-components": "^1.10.0",
23 | "cross-env": "^5.2.0",
24 | "github-markdown-css": "^3.0.1",
25 | "ioredis": "^4.9.0",
26 | "koa": "^2.7.0",
27 | "koa-body": "^4.1.0",
28 | "koa-router": "^7.4.0",
29 | "koa-session": "^5.10.1",
30 | "lodash": "^4.17.11",
31 | "lru-cache": "^5.1.1",
32 | "markdown-it": "^8.4.2",
33 | "moment": "^2.24.0",
34 | "next": "^8.0.3",
35 | "qiniu": "^7.2.1",
36 | "react": "^16.8.5",
37 | "react-dom": "^16.8.5",
38 | "react-redux": "^6.0.1",
39 | "redux": "^4.0.1",
40 | "redux-devtools-extension": "^2.13.8",
41 | "redux-thunk": "^2.3.0",
42 | "styled-components": "^4.2.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import 'antd/dist/antd.css'
2 | import App, {Container} from 'next/app'
3 | import {Provider} from 'react-redux'
4 | import Router from 'next/router'
5 |
6 | import Layout from '../components/Layout'
7 | import PageLoading from '../components/PageLoading'
8 |
9 | import WithReduxHoc from '../lib/with-redux'
10 |
11 | class MyApp extends App {
12 | state = {
13 | context: 'value',
14 | loading: false,
15 | };
16 |
17 | startLoading = () => {
18 | this.setState({
19 | loading: true,
20 | })
21 | };
22 |
23 | stopLoading = () => {
24 | this.setState({
25 | loading: false,
26 | })
27 | };
28 |
29 | componentDidMount() {
30 | Router.events.on('routeChangeStart', this.startLoading);
31 | Router.events.on('routeChangeComplete', this.stopLoading);
32 | Router.events.on('routeChangeError', this.stopLoading);
33 | }
34 |
35 | componentWillUnmount() {
36 | Router.events.off('routeChangeStart', this.startLoading);
37 | Router.events.off('routeChangeComplete', this.stopLoading);
38 | Router.events.off('routeChangeError', this.stopLoading);
39 | }
40 |
41 | static async getInitialProps(ctx) {
42 | const {Component} = ctx;
43 | console.log('app init');
44 | let pageProps = {};
45 | if (Component.getInitialProps) {
46 | pageProps = await Component.getInitialProps(ctx);
47 | }
48 | return {
49 | pageProps,
50 | }
51 | }
52 |
53 | render() {
54 | const {Component, pageProps, reduxStore} = this.props;
55 |
56 | return (
57 |
58 |
59 | {this.state.loading ? : null}
60 |
61 |
62 |
63 |
64 |
65 | )
66 | }
67 | }
68 |
69 | export default WithReduxHoc(MyApp)
70 |
--------------------------------------------------------------------------------
/pages/detail/index.js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic'
2 | import withRepoBasic from '../../components/with-repo-basic'
3 | import api from '../../lib/api'
4 | import {useEffect} from "react";
5 | import {cache, getCache} from "../../lib/repo-basic-cache";
6 |
7 | const MDRenderer = dynamic(() => import('../../components/MarkdownRenderer'));
8 | const isServer = typeof window === 'undefined';
9 |
10 | function Detail({readme, full_name}) {
11 | useEffect(() => {
12 | if (!isServer) {
13 | cache(readme,full_name)
14 | }
15 | }, [readme,full_name]);
16 | return
17 | }
18 |
19 | Detail.getInitialProps = async ({ctx}) => {
20 | const {
21 | query: {owner, name},
22 | req,
23 | res,
24 | } = ctx;
25 | const full_name = `${owner}/${name}/readme`;
26 | if (getCache(full_name)) {
27 | return {
28 | readme: getCache(full_name),
29 | }
30 | }
31 | const readmeResp = await api.request(
32 | {
33 | url: `/repos/${full_name}`,
34 | },
35 | req,
36 | res,
37 | );
38 | return {
39 | full_name,
40 | readme: readmeResp.data,
41 | }
42 | };
43 |
44 | export default withRepoBasic(Detail, 'index')
45 |
--------------------------------------------------------------------------------
/pages/detail/issues.js:
--------------------------------------------------------------------------------
1 | import {useState, useCallback, useEffect} from 'react'
2 | import {Avatar, Button, Select, Spin} from 'antd'
3 | import dynamic from 'next/dynamic'
4 |
5 | import {getLastUpdated} from '../../lib/utils'
6 |
7 | import withRepoBasic from '../../components/with-repo-basic'
8 | import SearchUser from '../../components/SearchUser'
9 |
10 | const MdRenderer = dynamic(() => import('../../components/MarkdownRenderer'));
11 |
12 | import api from '../../lib/api'
13 | import {cache, getCache} from "../../lib/repo-basic-cache";
14 |
15 |
16 | function IssueDetail({issue}) {
17 | return (
18 |
19 |
20 |
21 |
24 |
25 |
34 |
35 | )
36 | }
37 |
38 | function IssueItem({issue}) {
39 | const [showDetail, setShowDetail] = useState(false);
40 |
41 | const toggleShowDetail = useCallback(() => {
42 | setShowDetail(detail => !detail)
43 | }, []);
44 |
45 | return (
46 |
47 |
48 |
56 |
59 |
60 |
61 | {issue.title}
62 | {issue.labels.map(label => (
63 |
66 |
67 | Updated at {getLastUpdated(issue.updated_at)}
68 |
69 |
70 |
99 |
100 | {showDetail ?
: null}
101 |
102 | )
103 | }
104 |
105 | function makeQuery(creator, state, labels) {
106 | let creatorStr = creator ? `creator=${creator}` : '';
107 | let stateStr = state ? `state=${state}` : '';
108 | let labelStr = '';
109 | if (labels && labels.length > 0) {
110 | labelStr = `labels=${labels.join(',')}`
111 | }
112 |
113 | const arr = [];
114 |
115 | if (creatorStr) arr.push(creatorStr);
116 | if (stateStr) arr.push(stateStr);
117 | if (labelStr) arr.push(labelStr);
118 |
119 | return `?${arr.join('&')}`
120 | }
121 |
122 | function Label({label}) {
123 | return (
124 | <>
125 |
126 | {label.name}
127 |
128 |
138 | >
139 | )
140 | }
141 |
142 | const isServer = typeof window === 'undefined';
143 |
144 | const Option = Select.Option;
145 |
146 |
147 | function Issues({initialIssues, labels, owner, name}) {
148 | const [creator, setCreator] = useState();
149 | const [state, setState] = useState();
150 | const [label, setLabel] = useState([]);
151 | const [issues, setIssues] = useState(initialIssues);
152 | const [fetching, setFetching] = useState(false);
153 |
154 | useEffect(() => {
155 | if (!isServer) {
156 | cache(initialIssues, `${owner}/${name}/issues`);
157 | }
158 | }, [owner, name, initialIssues]);
159 |
160 | useEffect(() => {
161 | if (!isServer) {
162 | cache(labels, `${owner}/${name}/labels`);
163 | }
164 | }, [owner, name, labels]);
165 |
166 | const handleCreatorChange = useCallback(value => {
167 | setCreator(value)
168 | }, []);
169 |
170 | const handleStateChange = useCallback(value => {
171 | setState(value)
172 | }, []);
173 |
174 | const handleLabelChange = useCallback(value => {
175 | setLabel(value)
176 | }, []);
177 |
178 | const handleSearch = useCallback(() => {
179 | setFetching(true);
180 | api.request({
181 | url: `/repos/${owner}/${name}/issues${makeQuery(
182 | creator,
183 | state,
184 | label,
185 | )}`,
186 | })
187 | .then(resp => {
188 | setIssues(resp.data);
189 | setFetching(false)
190 | })
191 | .catch(err => {
192 | console.error(err);
193 | setFetching(false)
194 | })
195 | }, [owner, name, creator, state, label]);
196 |
197 | return (
198 |
199 |
200 |
201 |
211 |
224 |
227 |
228 | {fetching ? (
229 |
230 |
231 |
232 | ) : (
233 |
234 | {issues.map(issue => (
235 |
236 | ))}
237 |
238 | )}
239 |
256 |
257 | )
258 | }
259 |
260 | Issues.getInitialProps = async ({ctx}) => {
261 |
262 | const {owner, name} = ctx.query;
263 |
264 | const full_name = `${owner}/${name}`;
265 | const cachedIssues = getCache(full_name + '/issues');
266 | const cachedLabels = getCache(full_name + '/labels');
267 | const resultArr = await Promise.all([
268 | cachedIssues ? Promise.resolve({data: cachedIssues}) :
269 | await api.request(
270 | {
271 | url: `/repos/${full_name}/issues`,
272 | },
273 | ctx.req,
274 | ctx.res,
275 | ),
276 | cachedLabels
277 | ? Promise.resolve({data: cachedLabels})
278 | : await api.request(
279 | {
280 | url: `/repos/${full_name}/labels`,
281 | },
282 | ctx.req,
283 | ctx.res,
284 | )
285 | ]);
286 |
287 | return {
288 | owner,
289 | name,
290 | initialIssues: resultArr[0].data,
291 | labels: resultArr[1].data,
292 | }
293 | };
294 |
295 | export default withRepoBasic(Issues, 'issues')
296 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 | import {Button, Icon, Tabs} from 'antd';
3 | import getCofnig from 'next/config';
4 | import {connect} from 'react-redux';
5 | import Router, {withRouter} from 'next/router';
6 |
7 | import Repo from '../components/Repo';
8 | import {cacheArray,getCache} from '../lib/repo-basic-cache';
9 |
10 | const api = require('../lib/api');
11 |
12 | const {publicRuntimeConfig} = getCofnig();
13 |
14 | let cachedUserRepos, cachedUserStaredRepos;
15 |
16 | const isServer = typeof window === 'undefined';
17 |
18 | function Index({userRepos, userStaredRepos, user, router}) {
19 |
20 | const tabKey = router.query.key || '1';
21 |
22 | const handleTabChange = activeKey => {
23 | Router.push(`/?key=${activeKey}`);
24 | };
25 |
26 | useEffect(() => {
27 | if (!isServer) {
28 | // 这里对客户端做的数据缓存不大好,一旦 github 的数用户据发生了变化,除非页面刷新,否则无法同步数据
29 | cachedUserRepos = userRepos;
30 | cachedUserStaredRepos = userStaredRepos;
31 |
32 | // 所以这里使用 setTimeout 实现缓存更新策略
33 | setTimeout(() => {
34 | cachedUserRepos = null;
35 | cachedUserStaredRepos = null;
36 | }, 1000 * 60 * 10);
37 |
38 | }
39 | }, [userRepos, userStaredRepos]);
40 |
41 | useEffect(() => {
42 | if (!isServer) {
43 | // 使用 LRU 缓存数据
44 | cacheArray(userRepos);
45 | cacheArray(userStaredRepos);
46 | }
47 | }, [userRepos, userStaredRepos]);
48 |
49 | if (!user || !user.id) {
50 | return (
51 |
52 |
亲,您还没有登录哦~
53 |
56 |
65 |
66 | )
67 | }
68 |
69 | return (
70 |
71 |
72 |

73 |
{user.login}
74 |
{user.name}
75 |
{user.bio}
76 |
77 |
78 | {user.email}
79 |
80 |
81 |
82 |
83 |
84 | {userRepos.map(repo => (
85 |
86 | ))}
87 |
88 |
89 | {userStaredRepos.map(repo => (
90 |
91 | ))}
92 |
93 |
94 |
95 |
129 |
130 | )
131 | }
132 |
133 | Index.getInitialProps = async ({ctx, reduxStore}) => {
134 | // 这段代码在服务端运行时会报错,需要对请求区分处理
135 | // 因为在客户端运行的时候,浏览器会在 URL 前面加上域名和端口
136 | // 最终的 URL => http://localhost:3000/github/search/repositories
137 | // 但是在服务端运行的时候,由于服务端是没有域名的,只有 127.0.0.1 ,并且默认端口是 80
138 | // 最终的 URL => http://127.0.0.1:80/github/search/repositories ,这个请求会被发送到服务端本地的 80 端口去
139 | // 80 端口不是最终想要请求的地址,所以会报错
140 | // const result = await axios
141 | // .get('/github/search/repositories')
142 | // .then(resp => console.log(resp))
143 |
144 | const user = reduxStore.getState().user;
145 | if (!user || !user.id) {
146 | return {
147 | isLogin: false,
148 | }
149 | }
150 |
151 | if (!isServer) {
152 | if (cachedUserRepos && cachedUserStaredRepos) {
153 | return {
154 | userRepos: cachedUserRepos,
155 | userStaredRepos: cachedUserStaredRepos,
156 | }
157 | }
158 | }
159 |
160 | const userRepos = await api.request(
161 | {
162 | url: '/user/repos',
163 | },
164 | ctx.req,
165 | ctx.res,
166 | );
167 |
168 | const userStaredRepos = await api.request(
169 | {
170 | url: '/user/starred',
171 | },
172 | ctx.req,
173 | ctx.res,
174 | );
175 |
176 | return {
177 | isLogin: true,
178 | userRepos: userRepos.data,
179 | userStaredRepos: userStaredRepos.data,
180 | }
181 | };
182 |
183 | export default withRouter(
184 | connect(function mapState(state) {
185 | return {
186 | user: state.user,
187 | }
188 | })(Index),
189 | )
190 |
--------------------------------------------------------------------------------
/pages/search.js:
--------------------------------------------------------------------------------
1 | import { memo, isValidElement, useEffect } from 'react';
2 | import { withRouter } from 'next/router';
3 | import { Row, Col, List, Pagination } from 'antd';
4 | import Link from 'next/link';
5 |
6 | import Repo from '../components/Repo';
7 | import { cacheArray } from '../lib/repo-basic-cache';
8 |
9 | const api = require('../lib/api');
10 |
11 | const LANGUAGES = ['JavaScript', 'HTML', 'CSS', 'TypeScript', 'Java', 'Rust'];
12 | const SORT_TYPES = [
13 | {
14 | name: 'Best Match',
15 | },
16 | {
17 | name: 'Most Stars',
18 | value: 'stars',
19 | order: 'desc',
20 | },
21 | {
22 | name: 'Fewest Stars',
23 | value: 'stars',
24 | order: 'asc',
25 | },
26 | {
27 | name: 'Most Forks',
28 | value: 'forks',
29 | order: 'desc',
30 | },
31 | {
32 | name: 'Fewest Forks',
33 | value: 'forks',
34 | order: 'asc',
35 | },
36 | ];
37 |
38 | /**
39 | * sort: 排序方式
40 | * order: 排序顺序
41 | * lang: 仓库的项目开发主语言
42 | * page:分页页面
43 | */
44 |
45 | const selectedItemStyle = {
46 | borderLeft: '2px solid #e36209',
47 | fontWeight: 100,
48 | };
49 |
50 |
51 | const per_page = 20;
52 |
53 | const isServer = typeof window === 'undefined';
54 | const FilterLink = memo(({ name, query, lang, sort, order, page }) => {
55 | let queryString = `?query=${query}`;
56 | if (lang) queryString += `&lang=${lang}`;
57 | if (sort) queryString += `&sort=${sort}&order=${order || 'desc'}`;
58 | if (page) queryString += `&page=${page}`;
59 |
60 | queryString += `&per_page=${per_page}`;
61 |
62 | return (
63 |
64 | {isValidElement(name) ? name : {name}}
65 |
66 | )
67 | });
68 |
69 | function Search({ router, repos }) {
70 | const { ...querys } = router.query;
71 | const { lang, sort, order, page } = router.query;
72 |
73 | useEffect(() => {
74 | if (!isServer) cacheArray(repos.items)
75 | });
76 |
77 | return (
78 |
79 |
80 |
81 | 语言}
84 | style={{ marginBottom: 20 }}
85 | dataSource={LANGUAGES}
86 | renderItem={item => {
87 | const selected = lang === item;
88 |
89 | return (
90 |
91 | {selected ? (
92 | {item}
93 | ) : (
94 |
95 | )}
96 |
97 | )
98 | }}
99 | />
100 | 排序}
103 | dataSource={SORT_TYPES}
104 | renderItem={item => {
105 | let selected = false;
106 | if (item.name === 'Best Match' && !sort) {
107 | selected = true;
108 | } else if (item.value === sort && item.order === order) {
109 | selected = true;
110 | }
111 | return (
112 |
113 | {selected ? (
114 | {item.name}
115 | ) : (
116 |
122 | )}
123 |
124 | )
125 | }}
126 | />
127 |
128 |
129 | {repos.total_count} 个仓库
130 | {repos.items.map(repo => (
131 |
132 | ))}
133 |
134 |
{
139 | const p =
140 | type === 'page' ? page : type === 'prev' ? page - 1 : page + 1;
141 | const name = type === 'page' ? page : ol;
142 | return
143 | }}
144 | />
145 |
146 |
147 |
148 |
166 |
167 | )
168 | }
169 |
170 | Search.getInitialProps = async ({ ctx }) => {
171 | const { query, sort, lang, order, page } = ctx.query;
172 |
173 | if (!query) {
174 | return {
175 | repos: {
176 | total_count: 0,
177 | },
178 | }
179 | }
180 |
181 | // ?q=react+language:javascript&sort=stars&order=desc&page=2
182 | let queryString = `?q=${query}`;
183 | if (lang) queryString += `+language:${lang}`;
184 | if (sort) queryString += `&sort=${sort}&order=${order || 'desc'}`;
185 | if (page) queryString += `&page=${page}`;
186 | queryString += `&per_page=${per_page}`;
187 |
188 | const result = await api.request(
189 | {
190 | url: `/search/repositories${queryString}`,
191 | },
192 | ctx.req,
193 | ctx.res,
194 | );
195 |
196 | return {
197 | repos: result.data,
198 | }
199 | };
200 |
201 | export default withRouter(Search)
202 |
--------------------------------------------------------------------------------
/register-oauth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yjdjiayou/react-ssr-with-nextjs-demo/03f756b4b20338e3867351eeb1998e3cf9cf944f/register-oauth.png
--------------------------------------------------------------------------------
/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 |
9 | const auth = require('./server/auth');
10 | const api = require('./server/api');
11 |
12 | const RedisSessionStore = require('./server/session-store');
13 |
14 | const dev = process.env.NODE_ENV !== 'production';
15 | const app = next({dev});
16 | const handle = app.getRequestHandler();
17 |
18 | // 创建 Redis client
19 | // 如果通过这里连接 Redis 时,就不需要再通过命令行连接 Redis
20 | const redis = new Redis({
21 | port: 6379
22 | });
23 |
24 | // 因为 Node.js 没有 window 对象,所以无法使用 atob
25 | // 这里通过插件给 Nodejs 全局增加一个 atob 方法
26 | global.atob = atob;
27 |
28 | app.prepare().then(() => {
29 | const server = new Koa();
30 | const router = new Router();
31 |
32 | server.keys = ['develop Github App'];
33 |
34 | server.use(koaBody());
35 |
36 | const SESSION_CONFIG = {
37 | key: 'qwer',
38 | maxAge: 1000000,
39 | // 将 Koa 的 session 存储到 Redis 中
40 | store: new RedisSessionStore(redis),
41 | };
42 |
43 | server.use(session(SESSION_CONFIG, server));
44 |
45 | // 配置处理 Github OAuth 登录 的中间件
46 | // 必须放在上面的 session 中间件后面
47 | auth(server);
48 | api(server);
49 |
50 | server.use(router.routes());
51 |
52 | server.use(async (ctx, next) => {
53 | ctx.req.session = ctx.session;
54 | await handle(ctx.req, ctx.res);
55 | ctx.respond = false;
56 | });
57 |
58 | server.use(async (ctx, next) => {
59 | ctx.res.statusCode = 200;
60 | await next();
61 | });
62 |
63 | server.listen(3000, () => {
64 | console.log('koa server listening on 3000');
65 | });
66 |
67 | });
68 |
--------------------------------------------------------------------------------
/server/api.js:
--------------------------------------------------------------------------------
1 | const { requestGithub } = require('../lib/api');
2 |
3 | module.exports = server => {
4 | server.use(async (ctx, next) => {
5 | const path = ctx.path;
6 | const method = ctx.method;
7 |
8 | if (path.startsWith('/github/')) {
9 | console.log(ctx.request.body);
10 | const session = ctx.session;
11 | const githubAuth = session && session.githubAuth;
12 | const headers = {};
13 | if (githubAuth && githubAuth.access_token) {
14 | headers['Authorization'] = `${githubAuth.token_type} ${
15 | githubAuth.access_token
16 | }`
17 | }
18 | // 向 github 服务器发起请求
19 | const result = await requestGithub(
20 | method,
21 | ctx.url.replace('/github/', '/'),
22 | ctx.request.body || {},
23 | headers,
24 | );
25 |
26 | ctx.status = result.status;
27 | ctx.body = result.data
28 | } else {
29 | await next()
30 | }
31 | })
32 | };
33 |
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | const config = require('../global.config');
4 |
5 | const {client_id, client_secret, request_token_url} = config.github;
6 |
7 | module.exports = server => {
8 | server.use(async (ctx, next) => {
9 | if (ctx.path === '/auth') {
10 | const code = ctx.query.code;
11 | if (!code) {
12 | ctx.body = 'code not exist';
13 | return
14 | }
15 | const result = await axios({
16 | method: 'POST',
17 | url: request_token_url,
18 | data: {
19 | client_id,
20 | client_secret,
21 | code,
22 | },
23 | headers: {
24 | Accept: 'application/json',
25 | },
26 | });
27 |
28 | if (result.status === 200 && (result.data && !result.data.error)) {
29 | ctx.session.githubAuth = result.data;
30 |
31 | const {access_token, token_type} = result.data;
32 |
33 | const userInfoResp = await axios({
34 | method: 'GET',
35 | url: 'https://api.github.com/user',
36 | headers: {
37 | Authorization: `${token_type} ${access_token}`,
38 | },
39 | });
40 |
41 | ctx.session.userInfo = userInfoResp.data;
42 |
43 | // 认证成功后,返回到之前的页面
44 | ctx.redirect((ctx.session && ctx.session.urlBeforeOAuth) || '/');
45 | ctx.session.urlBeforeOAuth = ''
46 | } else {
47 | const errorMsg = result.data && result.data.error;
48 | ctx.body = `request token failed ${errorMsg}`
49 | }
50 | } else {
51 | await next()
52 | }
53 | });
54 |
55 | server.use(async (ctx, next) => {
56 | const path = ctx.path;
57 | const method = ctx.method;
58 | if (path === '/logout' && method === 'POST') {
59 | ctx.session = null;
60 | ctx.body = `logout success`
61 | } else {
62 | await next()
63 | }
64 | });
65 |
66 | server.use(async (ctx, next) => {
67 | const path = ctx.path;
68 | const method = ctx.method;
69 | // 客户端会通过点击 a 标签,向服务器发起请求
70 | if (path === '/prepare-auth' && method === 'GET') {
71 | const {url} = ctx.query;
72 | ctx.session.urlBeforeOAuth = url;
73 | ctx.redirect(config.OAUTH_URL)
74 | } else {
75 | await next()
76 | }
77 | });
78 | };
79 |
--------------------------------------------------------------------------------
/server/session-store.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 操作 Redis 的方法封装
3 | */
4 |
5 |
6 | function getRedisSessionId(sid) {
7 | return `ssid:${sid}`
8 | }
9 |
10 | class RedisSessionStore {
11 | constructor(client) {
12 | this.client = client;
13 | }
14 |
15 | /**
16 | * 获取 Redis 中存储的 session 数据
17 | * @param sid
18 | */
19 | async get(sid) {
20 | console.log('get session', sid);
21 | const id = getRedisSessionId(sid);
22 | // 相当于执行 Redis 的 get 命令
23 | const data = await this.client.get(id);
24 | if (!data) {
25 | return null;
26 | }
27 | try {
28 | const result = JSON.parse(data);
29 | return result;
30 | } catch (err) {
31 | console.error(err);
32 | }
33 | }
34 |
35 | /**
36 | * 存储 session 数据到 Redis
37 | * @param sid
38 | * @param sess => session
39 | * @param ttl => 过期时间
40 | */
41 | async set(sid, sess, ttl) {
42 | console.log('set session', sid);
43 | const id = getRedisSessionId(sid);
44 | if (typeof ttl === 'number') {
45 | ttl = Math.ceil(ttl / 1000);
46 | }
47 | try {
48 | const sessStr = JSON.stringify(sess);
49 | if (ttl) {
50 | await this.client.setex(id, ttl, sessStr);
51 | } else {
52 | await this.client.set(id, sessStr);
53 | }
54 | } catch (err) {
55 | console.error(err);
56 | }
57 | }
58 |
59 | /**
60 | * 从 Redis 当中删除某个 session
61 | * @param sid
62 | */
63 | async destroy(sid) {
64 | console.log('destroy session', sid);
65 | const id = getRedisSessionId(sid);
66 | await this.client.del(id);
67 | }
68 | }
69 |
70 | module.exports = RedisSessionStore;
71 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yjdjiayou/react-ssr-with-nextjs-demo/03f756b4b20338e3867351eeb1998e3cf9cf944f/static/favicon.png
--------------------------------------------------------------------------------
/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux';
2 | import ReduxThunk from 'redux-thunk';
3 | import { composeWithDevTools } from 'redux-devtools-extension';
4 | import axios from 'axios';
5 |
6 | const userInitialState = {};
7 |
8 | const LOGOUT = 'LOGOUT';
9 |
10 | function userReducer(state = userInitialState, action) {
11 | switch (action.type) {
12 | case LOGOUT: {
13 | return {}
14 | }
15 | default:
16 | return state
17 | }
18 | }
19 |
20 | const allReducers = combineReducers({
21 | user: userReducer,
22 | });
23 |
24 | // action creators
25 | export function logout() {
26 | return dispatch => {
27 | axios
28 | .post('/logout')
29 | .then(resp => {
30 | if (resp.status === 200) {
31 | dispatch({
32 | type: LOGOUT,
33 | })
34 | } else {
35 | console.log('logout failed', resp)
36 | }
37 | })
38 | .catch(err => {
39 | console.log('logout failed', err)
40 | })
41 | }
42 | }
43 |
44 | // 为防止每次服务端重新渲染时,都使用同一个 store,这里不能默认创建并导出 store
45 | // 需要用函数来创建 store
46 | // 如果不重新创建 store ,store 中的状态就不会重置,下一次服务端渲染的时候,还会保留上一次的值
47 | export default function initializeStore(state) {
48 | const store = createStore(
49 | allReducers,
50 | Object.assign(
51 | {},
52 | {
53 | user: userInitialState,
54 | },
55 | state,
56 | ),
57 | composeWithDevTools(applyMiddleware(ReduxThunk)),
58 | );
59 |
60 | return store
61 | }
62 |
--------------------------------------------------------------------------------