Nextjs+React+KOA-2

OAuth 认证方式有多种,但常用的有两种

  • Password
  • Authorize Code

Password 方式

这种方式比较简单,就是把用户名和密码传递给认证平台,认证平台返回 token。
比如我们使用github,把在github上注册的用户名密码传给githubgithub认证后返回token
但这有个很明显的弊端:就是当前系统可以知道你在 github 上的账号密码,比较不安全。

Authorize Code

这种方式和Password方式最大的区别就是,这个登录是在认证平台(github)提供的登录页面,也就是在github的网站进行登录和验证,验证通过后返回一个code,然后系统拿着这个code再去认证平台获取token

这种方式比较安全,当前网站无法接触到你在认证平台上的任何信息。

Auth 字段

  • client_id
  • scope
  • redirect_uri
    如果传这个参数,则必须和注册的时候输入的地址完全一样,不然获取不到 code;不传则跳转到注册填写的地址。
  • login
  • state
    跳转的时候带上的这个 state 和返回 code 的时候返回的 state 保持一致。
  • allow_signup
    是否允许动态注册,就是如果用户没有注册,则自动注册,然后返回信息。

https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/

请求授权,获取 code

GET https://github.com/login/oauth/authorize
链接上可以传上面的 auth 字段参数

请求 token

请求 token 时,client_id、client_secret、code 是必须的,
redirect_uri、state 不是必须的

POST https://github.com/login/oauth/access_token

OAuth Code 如何保证安全

  • 一次性的 code
    code 获取了 token 后就失效了
  • id+secret
    没这两个值,是无法获取 token 的
  • redirect_uri
    如果这个和注册时不一致,就会报错

cookie 是存储在客户端,每次请求都会携带它,不管是文件请求还是 api 的请求,服务端可以读取到 cookie 的值。

登录后让在当前页

这个实现起来也很简单,就是登录之前先把当前页面的地址存起来,登录成功后再跳转到即可。
通过next/router中的withRouter可以获取到当前页面的url地址
实现思路有两种:

  • 在登录事件中,先请求服务器,把 url 地址存起来,然后再跳转到登录页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const onGoToAuth = useCallback(e => {
e.preventDefault();
axios.get(`prepare-auth?url=${router.asPath}`).then(res => {
if (res.status === 200) {
location.href = publicRuntimeConfig.OAUTH_URL;
} else {
console.log("prepare-auth failed");
}
});
}, []);

server.use(async (ctx, next) => {
const { path, method } = ctx;
if (path === "/prepare-auth") {
const { query } = ctx;
ctx.session.urlBeforeOAuth = query.url;
ctx.body = "ready";
} else {
await next();
}
});
  • 另一种是超链接直接链接过去,在服务端跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<a href={`prepare-auth?url=${router.asPath}`}>
<Avatar sieze={40} icon="user" />
</a>;

server.use(async (ctx, next) => {
const { path, method } = ctx;
if (path === "/prepare-auth") {
const { query } = ctx;
ctx.session.urlBeforeOAuth = query.url;
//ctx.body = "ready";
ctx.redirect(config.github.OAUTH_URL);
} else {
await next();
}
});

请求代理

上一个项目使用的是 express 的 http 代理插件,轻松搞定,这个是纯手写。
其实就是根据规则转发请求。

前端和服务端请求的解决

getInitalProps在客户端和服务端都会执行,当在客户端发起请求时比价好办,上面的代理直接转给了真正的服务服务器。但在服务端执行时,由于写的是相对路径,服务器默认会在自己身上找,那这个请求肯定是找不到的,所以请求是服务端发出的还是客户端发出的,需要区别对待。

上一个项目是通过createStore的第三个参数thunk.withExtraArgument来解决。可参考

这里做的比较直接,直接是在发出请求的时候判断下,是服务端还是客户端,然后做响应的处理,服务端的话,就要以全路径的方式请求。把路径拼全就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const isServer = typeof window === "undefined";
const github_base_url = "https://api.github.com";

async function requestGithub(method, url, data, headers) {
return await axios({
method,
url: `${github_base_url}${url}`,
data,
headers,
});
}

async function request({ method = "GET", url, data = {} }, req, res) {
if (!url) {
throw Error("url muste provide.");
}

if (isServer) {
const session = req.session;
const githubAuth = session.githubAuth || {};
const headers = {};

if (githubAuth.access_token) {
headers["Authorization"] = `${githubAuth.token_type} ${githubAuth.access_token}`;
}
return await requestGithub(method, url, data, headers);
} else {
return await axios({
method,
url: `/github${url}`,
data,
});
}
}

###获取 post 请求的数据
使用koa-body插件即可,安装完后在 server 中注册即可

1
2
3
const server = new Koa();
//这样就可以通过ctx.request.body来获取post的值
server.use(koaBody());

在 post 的请求里面,就可以通过ctx.request.body的方式来获取 post 传递的值。

getInitialProps 的参数为什么和官网的不一样

今天在做项目时,教程里getInitialProps的参数有ctx参数,而页面的getInitialProps中没有。结果看了下官网给的绑定 redux 的 demo,在绑定 redux 的地方也是有 ctx 的。因为我自定义了_app.js 文件,所以怀疑可能是这引起的。
因为所有的页面都会执行 app.js,所以这的定义可能会影响到组件中的功能。
看了下我的_app.js中是没有定义getInitialProps,所以各个组件的getInitialProps是默认的,官网上显示getInitialProps默认是没有 ctx 属性的,所以就在_app.js中自定义了getInitialProps,并验证这里是有ctx属性的,所以透传下去,各个组件也就也有ctx属性了。

getInitialProps返回的对象在组件中获取不到

原因是在_app.js中我自定义了getInitialProps,但是没有把从getInitialProps获取到的值返回方式和接收方式不一致

1
2
3
4
5
6
7
8
9
10
11
12
  static async getInitialProps(appContext) {
let pageProps = {};
if (appContext.Component.getInitialProps) {
pageProps = await appContext.Component.getInitialProps(appContext);
}

// return pageProps; 错误写法
return { pageProps: { ...pageProps } };
}

// 接收是这样的,要保持一致
<Component {...pageProps} />

解决路由切换回来 tab 选中丢失情况

使用next/router提供的 router 功能,把 tab key 的值放到 url 中,然后从 url 中获取 tab key 的值即可。
这样切换路由再回来时,url 中是带参数的。

缓存本页的数据

由于页面上数据变化不大,因此可以在本页缓存数据,以避免每次点击 tab 都会重新请求数据
在页面中声明一个全局变量,存储远程获取的数据,然后在getInitialProps中判断,如果缓存变量中有数据就直接返回不再请求。

// index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Index() {}

let cachedUserRepos;
Index.getInitialProps = async ({ ctx }) => {
if (cachedUserRepos) {
return {
userRepos: cachedUserRepos,
};
}

const userRepos = await api.request({ url: "/user/repos" }, ctx.req, ctx.res);
cachedUserRepos = userRepos.data;

return {
userRepos: cachedUserRepos,
};
};

但这有一个弊端,如果是首页,也就是当getInitialProps是在服务端执行时,全局变量的值会一直存在,即使在新的客户端用新的用户登录,全局变量中的数据仍然存在;这是为什么呢?
这是因为全局变量是这个模块的全局变量,并不是Index组件(或方法)的变量,即使重新渲染,原来缓存的数据仍然存在;当nextjs启动服务加载了index.js模块后,这个全局变量就一直存在。我们赋值过后它就一直有值。所以需要判断如果是服务端渲染就不从这个变量中取值,也不给他们赋值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const isServer = typeof window === "undefined";
Index.getInitialProps = async ({ ctx }) => {
if (!isServer) {
if (cachedUserRepos && cachedUserStaredRepos) {
return {
userRepos: cachedUserRepos,
userStaredRepos: cachedUserStaredRepos,
};
}
}

const userRepos = await api.request({ url: "/user/repos" }, ctx.req, ctx.res);
const userStaredRepos = await api.request({ url: "/user/starred" }, ctx.req, ctx.res);

if (!isServer) {
cachedUserRepos = userRepos.data;
cachedUserStaredRepos = userStaredRepos.data;
}

return {
userRepos: userRepos.data,
userStaredRepos: userStaredRepos.data,
};
};

模块中的其他全局变量也要注意这个问题的存在!!!

切换 tab 时,url 的 key 参数没有发生变化

这需要吧 withRouter 放到 connect 外面,放里面就有上面的说的问题

1
export default withRouter(connect(state => ({ user: state.user }))(Index));

第一次服务端获取到的数据没缓存

第一次服务端获取到的数据没缓存,当第一次切换 tab 时仍重新请求了数据,这可以使用useEffect来解决,第一次页面加载时,如果 props 中有值就缓存起来

1
2
3
4
5
6
useEffect(() => {
if (userRepos && userStaredRepos) {
cachedUserRepos = userRepos;
cachedUserStaredRepos = userStaredRepos;
}
}, []);

使用lru-cache指定缓存策略

上面的缓存没问题,但是缓存是一直存在的,不会过期,这不是太好,可以通过lru-cache来制定有时效的换成策略。
这个使用起来比较简单,查看官网写即可。

详情页

查询、翻页等操作都反应到 url 参数中,这样页面进行回退等操作时,能记录上次的状态。

github 限制了列表请求最多只返回 1000 条数据的请求,超过第 1000 条数据的请求就报错不返回了,比如你每页 20 条,最大只能访问 50 页;因为没人会翻在 000 条数据内还找不到要找的库。

客户端数据缓存

有很多数据是变化不大的,可以针对客户端请求做缓存策略。注意服务端渲染时不要做缓存。

1
const isServer = typeof window === "undefined";

转换 base64 编码成可识别文字

window 全局提供了一个方法可以转换,方法就是atob(content),但是服务端没这个方法,所以可以安装atob包,在服务端把这个包赋给全局变量

1
2
const atob = require("atob");
global.atob = atob;

转换显示 markdown 文件

markdown文件默认读取出的内容是 base64 字节流,通过atob方法可以转换成它本来的内容,但是特殊符号例如空格仍是&nbsp;,所以仍需转换真正的 html 字符串,通过markdown-it组件可以完成
react 做了 xss 攻击处理,所以需要通过下面的方式显示 html 字符串

1
2
3
4
5
6
// 把base64转换成了原来的字符串,但是特殊符号还不是html的显示方式
const content = atob(readme.content);
// 转换成html的方式显示
const html = md.render(content);
// 由于react做了防XSS(跨站脚本攻击)的处理,所以不能直接把html字符串进行显示
return <div dangerouslySetInnerHTML={{ __html: html }}></div>;

markdown 内容中文乱码问题

需要把 base64 转换 utf8

1
2
3
4
5
6
function base64_to_utf8(str) {
return decodeURIComponent(escape(atob(str)));
}

const content = base64_to_utf8(readme.content);
const html = md.render(content);

图片无法显示问题

markdown 中的 html 是无法直接显示,通过制定显示 markdown 中的 html 可以解决

1
2
3
4
const md = new MarkdownIt({
html: true /* 显示markdown中的html文件 */,
linkify: true /* 文档中的链接可以点击*/,
});

使用github-markdown-css样式,使 markdown 内容显示更完美

1
2
3
npm i github-markdown-css

import 'github-markdown-css'

生成分析文件

首先 next 对应的插件@zeit/next-bundle-analyzer,然后在next.config.js中进行配置,并设置打包命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// next.config.js
module.exports = withBundleAnalyzer(
withCss(
// 下面的配置就是使用webpack的分析工具,生成分析文件
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
bundleAnalyzerConfig: {
server: {
analyzerMode: "static",
// 服务端分析文件生成路径
reportFilename: "../bundles/server.html",
},
browser: {
analyzerMode: "static",
reportFilename: "../bundles/client.html",
},
},
})
);

// package.json
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build"

动态加载不变的文件

封装了一个 Markdown 文件显示的组件,这个组件的内容是不变的,所以可以动态加载,动态加载的文件的会被 webpack 单独打包,因为它的打包后的 hash 不变,可以方便浏览器缓存,提高体验。

优化 moment 包

moment中包含很多我们用不上的文件,比如它的国际化文件中包含了很多国家的语言文件,我们只需要中文的,所以只需要加载中文的语言包就行。

  • 配置webpack忽略moment组件的所有语言包
  • 在使用moment的地方手动引入要用的语言包
1
2
3
4
5
6
7
8
9
10
11
// next.config.js
withCss({
webpack(config) {
config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));
return config;
},
....

// 使用的地方
import moment from "moment";
import "moment/locale/zh-cn";

去抖动

做自动匹配查询时,可以使用lodash/debounce来去抖动

Gatsby

文章作者: wenmu
文章链接: http://blog.wangpengpeng.site/2020/01/09/Nextjs+React+KOA-2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 温木的博客
微信打赏