Next.js简介

Next.js作为React官方钦定的轻量级同构框架,有以下几个特性:

  • 开箱即用,支持0配置开发
  • 支持约定式路由
  • 支持自定义服务端路由,可以配合其他Node框架使用
  • 渐进式babel和webpack配置
  • 支持客户端渲染
  • 社区活跃,团队不太监
  • 对前端新技术支持较为全面,比如AMP、TypeScript、PWA、Serverless等

在Nemo FE以React为主要开发框架的前提下SEO项目重构选择Next.js就非常合适

Next.js自定义配置

官方文档在自定义服务端路由的例子中给出了一个demo,可以给后面和Egg.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
25
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl

if (pathname === '/a') {
app.render(req, res, '/b', query)
} else if (pathname === '/b') {
app.render(req, res, '/a', query)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

上面的代码大概可以拆分成两个部分:

1
2
3
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

1.对next进行实例化,缓存环境变量以及一个处理页面资源请求的方法

1
2
3
4
app.prepare().then(() => {
createServer((req, res) => {
...
})

2.当前期的实例和方法都准备好以后,下一步就是启动服务了,启动http服务之前先跑了一次app.prepare,而这个app.prepare是干什么用的呢,在这里可以先不急着去看prepare的源码,可以先去看看next官方脚手架是怎么跑起来的。

官方给出的命令配置如下:
01.png
dev为例,可以从next源码中找到对应的cli/next-dev.ts文件

next-dev.ts

02.png
可以看到在dev启动命令同样调用了app.prepare这个方法,而前面那个startServer又是个什么东西,可以根据路径找到对应文件

start-server.ts

03.png

看到这里就大概知道官方启动next的流程是先启动一个http服务,启动完后再去调用app.prepare方法,除了dev生产模式下的start也是同样流程,所以这个时候prepare内部是干什么的已经不需要去知道,源码怎么做这边自定义启动next就怎么做

至于是先创建http服务还是先跑app.prepare方法也不重要,只要在开始做路由匹配前都启动完成即可

Egg.js自定义配置

Egg.js自定义配置较简单,只需要在根目录新建一个app.ts,调用beforeStart即可

1
2
3
4
5
6
import {Application} from 'egg'
export default (app:Application) => {
app.beforeStart(() => {
...
});
};

beforeStart定义

所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务

搭建思路

本文采用的Next9.x版本进行配置,已成功进行上线验证

Ok,在分析完Next.js和Egg.js的自定义配置过程后,下面开始思考下🤔怎么把这两个官方团队相互排斥的框架结合在一起做成自己想要的配置

先捋一下搭建思路:

  • 首先路由是由Egg控制而不是使用Next的约定式路由,因为契合SEO需求一个页面可能被多个路由使用,所以用Egg控制路由更合适
  • 既然项目用Egg启动也就是Next需要自定义在Egg启动的时候实例化,实例化要做两件事情:
    • 1.自定义一个render方法然后挂载在上Egg上,因为View层渲染是由Next去完成,然后通过的Egg的Controller返回给客户端
    • 2.上文提到Next有一个getRequestHandler的方法用于处理路由和静态文件,放到这里路由是不用处理因为这事是Egg做的,但是静态资源还是要由Next做的,所以也应该想办法把这个方法挂到Egg上
  • 考虑到SEO需求需要使用Google的AMP框架,鉴于之前使用Egg模版引擎Nunjucks在写H5页和AMP页的时候HTML和CSS都需要分开写&&打包,既然Next支持AMP,为了体现重构的价值争取用最少的代码实现一步到位

大概就是这三点是整个搭建过程需要考虑的

Egg.js+Next.js Initialize Combine

首先使用egg脚手架初始化项目

egg-init [project] –type=simple

按上面说的到的思路在Egg启动的时候对Next进行实例化,那么先来建一个ssr.js,对next的实例化主要分为3步:

  • next实例化,然后调用prepare方法
  • 因为需要通过next生成html模版,已经不需要使用Egg的render方法,所以要自行封装renderTsx方法,方法内部调用render方法返回渲染好的html模版
  • 然后把renderTsx方法挂载到Egg的ctx

server/ssr.ts

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
35
36
37
38
39
40
import {Application} from 'egg'
const next = require('next')
const dev = process.env.NODE_ENV !== 'production';

module.exports = async (app:Application) => {
// next实例化
const nextServer = next({ dev });
// 缓存getRequestHander方法
const requestHandler = nextServer.getRequestHandler();
// 按照上面分析先调用prepare方法
await nextServer.prepare();

const renderTsx = async function (options) {
const ctx = this;
// 调用nextServer的render方法,里面传入的page路径和参数都通过在Egg端传入
const html = await nextServer.render(
ctx.req,
ctx.res,
`/${options.page}`,
options.props
);
// 模版挂载到body上
ctx.body = html;
return html
};

// 使用Object.defineProerty方法挂载renderTsx方法
Object.defineProperty(app.context, 'renderTsx', {
writable: false,
configurable: false,
value: renderTsx
});

// 使用Object.defineProerty方法挂载requestHandler方法,在Egg中间件调用
Object.defineProperty(app.context, 'requestHandler', {
writable: false,
configurable: false,
value: requestHandler
});
}

根据Egg.js文档自定义Egg服务需要在根目录建一个app.ts,并要默认导出一个类,然后引入刚才在ssr.ts中封装的启动函数

app.ts

1
2
3
4
5
6
7
8
import { Application } from 'egg'
const SSR = require('./server/ssr')

module.exports = (app:Application) => {
app.beforeStart(async () => {
await SSR(app)
})
}

上面说到requestHandler在挂载到ctx上后需要在中间件调用,为什么要在中间件调用呢,

通过阅读Next的源码分析了getRequestHandler调用的全过程,简单总结下调用的过程大概是:

  • Next在启动Node服务createServer后会调用getRequestHandler
  • 调用后截取全部请求URL(这个URL不单指只在浏览器输入的,还包括其他静态脚本文件),同时与内部的Router数组做一次匹配
  • 如果匹配到对应的前缀比如/_next/static/后会回调Router中的fn方法,而这个fn做的事情不限于渲染页面、页面热更替等等

既然要截取全部请求URL那放在中间件就再适合不过了,至于为什么要手动挂载到Egg的中间件上,原因也很简单,因为我们并没有启动Next内部的Node服务,所以要手动调用挂载这个方法

middleware/next.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Application } from 'egg'
const { parse } = require('url')

module.exports = (app: Application) => {
return async (ctx, next) => {
const isNextStatic = /\/_next\//.test(ctx.url)
if (isNextStatic) {
const reqeustUrl = parse(ctx.url, true)
await ctx.requestHandler(ctx.req, ctx.res, reqeustUrl)
} {
await next()
}
}
}

然后在config文件夹中配置config.default.ts文件,默认开启中间件

config/config.default.ts

1
2
3
4
5
6
module.exports = (appInfo: EggAppInfo) => {
...
config.middleware = ['next']
...
return { ...config }
};

看到这里看官们应该会有一个疑问🤔️,为什么egg的自定义配置用的是common.js规范而不是esmodule规范,在我的Next.js使用总结文章中有总结原因,在这里就不再赘述,欢迎各位看官去看一看,说不定还有一些你现在使用Next.js的一些疑难杂症(手动斜眼。

至此Next.js+Egg.js配置已基本结束,楼主还是会一直持续关注Next.js的变化,顺便写一些Next.js源码解析文章,毕竟工欲善其事必先利其器,灵活运用一个框架了解其原理是必不可少的,所以敬请期待🎉🎉🎉