- Published on
关于nextjs重构那点事
- Authors
- Name
- lcorz
技术追溯
- php
- 原生js + jquery
- avalonjs (早期model–view–viewmodel)
- vue (model–view–viewmodel)
- react (model–view–controller)
- nextjs (react ssr)
mvc 和 mvvm
dom
// js
var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
// jquery
$('#name').text('Homer').css('color', 'red');
mvc
- 视图 (View):用户界面(dom 操作)
- 控制器 (Controller): 业务逻辑(也可用户直接触发例如修改url)
- 模型 (Model): 数据保存
<div>{count}</div>
<div onClick={handleClick}>+1</div>
state = {
count: 0
}
function handleClick() {
this.setState((prevState) => ({
count: prevState.count + 1
}));
}
mvvm
- 视图 (View):用户界面(dom 操作)
- 抽象视图 (Viewmodel): 数据绑定(Input等)
- 模型 (Model): 数据保存
<input type="text" v-model={{text}} />
data () {
return {
text: ''
}
}
目的都是关注Model的变化,让框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来
选型
因为网站seo需求所以需要服务端渲染, 之前的方法是通过页面中嵌套php代码, 通过外层的php进行seo部分预渲染. 随着现在技术的发展, 诞生了一些同构方案可以使用, 前端代码经历了多年的前后端拆分独立和spa后, 又重新回到了服务器渲染...
因为对偏爱react, 所以选择了react框架下热度最高的nextjs作为重构选择.
SSR(Server-Side-Render)
最近几年浏览器端框架很繁荣, 以至于很多新入行的开发者只知道浏览器端渲染框架, 都不知道存在服务器端渲染这回事, 其实, 网站应用最初全都是服务器端渲染, 由服务器端用 PHP、Java 或者 Python 等其他语言产生 HTML 来给浏览器端解析。
好处:
- 缩短首屏加载时间. 浏览器端渲染框架, 服务器端返回的 html 就是一个空荡荡的框架和几个js css文件,然后浏览器下载 js,再根据 js 中的 ajax 调用获取服务器端数据,再渲染出 dom 来填充网页内容,就需要多个请求才能渲染出页面, 服务端渲染的好处是东西请求过来的时候就已经带着数据了, 直接渲染就可以
- 有利于爬虫抓取数据. 仅针对百度这种不能自己跑js的爬虫, spa 页面他们访问进来就仅仅是个空白页面什么都抓不到.
react 对服务端渲染的支持:
// react v16之前
const html = ReactDOMServer.renderToString(<Hello />);
// react v16之后
const html = ReactDOMServer.renderToNodeStream(<Hello />).pipe(response);;
新api renderToNodeStream
主要解决的问题是当项目结构庞大时候 string 方法需要同步生成 html, 而使用流的方式塞给 response, 可以加快页面的渲染时间.
脱水 注水: 服务端渲染因为只渲染html, 但是没有js去辅助是无法进行交互的, 所以这一部分需要在浏览器端配合渲染, 但是这就存在会有服务端数据和客户端数据两次可能不一样的情况, react本身会对两次渲染的html进行对比, 如果不一致会直接抛弃服务端 html 在浏览器端重新渲染. 所以为了解决这一问题就需要将数据保留在页面中随着html传过来, 这一过程就称为脱水, 在浏览器端直接用这个数据去初始化组件, 这个过程就是注水.
nextjs
通过前面的脱水注水步骤大体已经是服务端渲染的雏形了, 但是还需要解决一个问题是如何与单页面应用的结合. 因为两者带来的体验还是不同的, 之所以vue和react现在大行其道的一部分原因就是因为单页面应用带来的切换顺滑. 用户感觉是多个页面其实仅仅是一个页面的局部更新. 所以就需要解决如何同时满足两种渲染方式.
getInitialProps
框架核心点独立于 react 之外挂载的getInitialProps生命周期, 不影响react原生生命周期, 组件该怎么写怎么写, 这个生命周期主要负责页面初始化数据的获取, 在服务端和浏览器端会智能的执行, 当页面初次加载时会动态服务端执行周期函数将内容以 props 形式注入到页面中. 而在页面通过路由切换的过程中, 数据获取和渲染则完全在浏览器端进行.
import React from 'react'
class Page extends React.Component {
static async getInitialProps(ctx) {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
render() {
return <div>Next stars: {this.props.stars}</div>
}
}
export default Page
脱水注水
服务端在渲染过程中会将数据挂载到__NEXT_DATA__
上, 这样在浏览器二次渲染的时候就不需要重新获取数据, 直接拿来用就行了
<script>
__NEXT_DATA__ = {
props: {
"pageProps": {"userName": "Morgan"},
"page":"/","pathname":"/","query": {},"buildId":"-","assetPrefix":"","nextExport":false,"err":null,"chunks":[]
}
}
</script>
Axios
因为页面请求数据可能在服务端, 也可能在浏览器端, 所以请求方式就不能只用单纯的ajax或者fetch, 这种方案在node端或因为没有XMLHttpRequest
报错, 选用axios就是因为在浏览器端他会使用XMLHttpRequest
的方式, 而在node端他将使用http
模块进行请求.
router
nextjs自带一套文件式路由, 也就是pages中的文件即可以通过链接直接访问, 如果是复杂度较低的项目, 可以直接使用, 但因为需要兼容旧项目的路由方式, 所以采用自定义路由, 可供使用的方案比较多, 我这里使用的是koa, 自定义路由相当于将node服务从nextjs手中接过来转到自己手里.
pages/index.js
→/
pages/blog/index.js
→/blog
pages/blog/first-post.js
→/blog/first-post
pages/blog/[slug].js
→/blog/:slug
(/blog/hello-world)
const Koa = require('koa')
const next = require('next')
const Router = require('koa-router')
const dev = process.env.NODE_ENV !== 'production'
const port = dev ? 3001 : 3333
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare()
.then(() => {
const server = new Koa()
const router = new Router()
router.get('/word', async ctx => {
if(!ctx.query.w || ctx.query.w === ''){
ctx.redirect(`/`);
ctx.status = 302;
}else{
await app.render(ctx.req, ctx.res, '/word', ctx.query)
ctx.respond = false
}
})
router.get('/:word', async ctx => {
ctx.redirect(`/word?w=${ctx.params.word}`);
ctx.status = 301;
})
router.get('*', async ctx => {
await handle(ctx.req, ctx.res)
ctx.respond = false
})
server.use(async (ctx, nextTo) => {
ctx.res.statusCode = 200
await nextTo()
})
server.use(router.routes())
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`)
})
})
Dynamic Import
组件可以动态加载, 而且自带ssr设置可以控制是否在服务端加载, 因为在node环境中是不存在window的, 所以在项目中如果存在使用某些三方库内部挂载到window上的时候要避免组件在服务端时候渲染
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
SSG(Static Site Generation)
nextjs 9 版本后更新了两个新生命周期用来替代getInitialProps
, 以用来更精确你需要干的事, 一个是getStaticProps
, 另一个是getServerSideProps
, 相当于细化拆分了两个在客户端和服务端的生命周期, 而主要目的在于, nextjs当判断你在页面中没有 getServerSideProps
或者getInitialProps
的时候, 他将启用静态打包的方式, 直接将页面打包成html + json的形式, 当请求时直接输出.
pm2 (cluster)
这个本来没啥可说的, 但是踩了个坑就记录一下, node服务需要在服务器端值守, 所以用到pm2, 众所周知, nodejs是个单线程的东西, 当服务器不止一核的时候无法完全利用服务器性能, 所以nodejs创造了一个cluster
模块将主进程衍生出多个进程共享同一个端口.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
}
pm2本身自带cluster模式, 当直接使用时启用fork模式直接启动node, 当配置参数 -i
时会根据后续参数启动对应数量进程, 然后问题来了= =... 不管怎么启动pm2都无法跑出多核性能, 一度认为是哪里配置的不对, 后来排查后发现, 虽然pm2可以通过npm去启动node服务, 但是当使用cluster模式的时候, 其实多进程启用的是npm并非node, 实际启用的node还是只有一个...必须直接去执行node文件才可以达到想要的效果
// package.json 修改前
{
"scripts": {
"dev": "cross-env NODE_ENV=development node server.js",
"build": "next build",
"start": "pm2 start npm -i 0 --name 'xxx' -- run next",
"stop": "pm2 stop xxx && pm2 delete xxx",
"next": "cross-env NODE_ENV=production node server.js",
},
}
// package.json 修改后
{
"scripts": {
"dev": "cross-env NODE_ENV=development node server.js",
"build": "next build",
"start": "cross-env NODE_ENV=production pm2 start server.js -i 0 --name 'xxx'",
"stop": "pm2 stop xxx && pm2 delete xxx",
},
}