前言
Nextjs是一个使用react作为前端框架底层的支持SSR(请求时渲染)、SSG(构建时渲染)等技术的全栈框架,在2022年的服务端框架中排名第一。
它的优点非常明显,既支持react的虚拟dom形式快捷完成开发,又支持访问即可看到完整内容,友好的SEO/浏览器直出形式。结合了静态分离和服务器渲染的双重优势。
同时在服务端也非常容易做缓存相关的处理,甚至是做一些中间件的开发,简直是前端开发的神兵利器。
当前缺点也有一些,包括跳转的时候会重复下载内容,开发的时候需要一些服务端开发能力,甚至是部署的时候没点本事都部署不明白。
以上这些都是Nextjs的内容,作为一个合格的开发者,研究未来趋势的开发能力,使用更有成长潜力的技术,都是我辈需要实践的真理。
创建项目
可以使用官方脚手架创建项目。默认使用js文件,截止当前使用的版本是Nextjs 13+。
npx create-next-app@latest
# or
yarn create next-app
更推荐的是使用typescript加持下的项目。有更好的开发体验和对项目更高的掌控力。
npx create-next-app@latest --typescript
# or
yarn create next-app --typescript
项目核心依赖库next、react、react-dom三个库。
可以看到,在创建新项目的过程中会询问几个选项:
- 项目名称,这里输入
nextjs-demo。 - 项目是否需要使用
ESLint。 - 是否需要在项目中使用
src目录,默认会吧所有文件放在根目录,为了方便开发,这里启用src目录。 - 是否启用
app目录,默认会放一些框架相关的东西。这里启用之后会在app目录中生成首页内容。 - 是否启用别名,方便使用,直接启用了。
执行开发环境命令npm run dev就可以在了,打开http://localhost:300可以看到,默认的页面是Nextjs的官方介绍。
项目介绍
当前项目创建好了。这里再简单介绍一下项目的目录和使用方式。从上到下按顺序来看:
.next目录。这是Nextjs的缓存目录,在执行dev或者build等命令的时候,会在本地项目的根目录下生成此目录,开发不需要关注。想要了解更多的可以稍微研究一下,使用缓存/已生成的方式加速编译。.vscode目录。看名字就知道,这个是vscode编辑器使用到的配置目录。node_modules目录。这是三方依赖的目录,这里不多介绍。public目录。这个主要放置静态资源,默认没有二级目录,为了方便可以简单创建几个目录来放相关资源。默认路径是在根目录,使用的时候可以使用类似/favicon.ico的形式引用。src目录。这个目录是主要源代码的位置,初始目录下有app默认页和pages其他页面目录。在pages目录下还有一个默认api目录,主要放置Nextjs提供的服务端API。可以简单看一下默认提供的hello.ts文件内容。.eslintrc.json。主要是eslint的规则。.gitignore。git排除文件。next-env.d.ts。nextjs的一些ts相关内容,目前只有默认引用。next.config.js。Nextjs的配置文件,这里默认只有appDir参数。package-lock.json。项目依赖lock文件。package.json。项目npm相关文件。README.md。文档说明。tsconfig.json。typescript相关配置文件。
使用自定义入口文件_app.tsx
创建文件src/pages/_app.tsx。这个文件主要是作为所有页面的入口文件,可以简单做一些统一处理的逻辑。需要可以创建,不需要可以删除,不影响项目运行。
从演示文件可以看到,这里给所有页面的head标签中增加了一个meta。
不要忘了把参数传递到组件中。
Nextjs服务端渲染会从
getServerSideProps方法中注入新的返回值,在这里的pageProps参数中可以获取到。
使用自定义页面_document.tsx
上面在创建项目的时候有一个选项,是否开启app。如果选择YES,这个文件就可以放弃了。所有的入口文件的修改可以使用app目录下的内容。如果没有选择,这里可以创建文件src/pages/_document.tsx。
使用自定义此自定义默认,需要在
next.config.js的参数中去掉appDir参数,或者让他是false。同时删掉src/app目录。如果原来有
app目录,经过上面的操作,首页就会跟着app目录被删掉,需要重新创建。
可以从上面的代码看到,这个自定义页面的主要功能,类似于index.html的作用。同时也可以做一些自己想要自定义的内容。比如增加其他head中的标签,或者在最后增加script内容,同时做一些自己的处理。
import Document, { DocumentContext } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
return initialProps
}
}
export default MyDocument
这个文件的定义主要是基础页面的自定义。它不支持getServerSideProps等方法,但是在页面内有getServerSideProps方法的时候,可以使用getInitialProps方法做一些服务端的操作。
使用自定义server.js
前面的自定义都是在Nextjs范围内操作,这里还可以通过自定义server.js的方式在请求还未进入SSR模式的Nextjs前进行操作。
修改package.json文件的启用命令
这里使用马上要创建的服务端文件来代替默认的启动命令。
创建对应的使用文件
- 根目录下创建
index.js,这里为了方便,文件内容只有一个执行引用。require("./server"); - 根目录下创建文件
server/index.js。这个文件才是真正的要执行的内容。之所以这么做是因为项目后面会有大量的服务端代码,将这些逻辑单独处理必然要放在独立的文件中。创建一个专用的目录,方便管理这些文件。
编写服务内容
上面是一个简单的服务内容。使用了express来拦截请求。这里能看到主要逻辑是在Nextjs准备完成之后,通过express来拦截请求,然后再把请求使用Nextjs的方法处理一次。
使用server.set("x-powered-by", dev);来关闭express返回的响应数据中,head自定增加的x-powered-by:express的内容。
使用get("/api_test",xxx)的形式,可以拦截到所有path是/api_test开头的请求。这里使用res对象设置返回内容是一个JSON对象。
自定义配置
Nextjs在默认的时候还有一些配置,需要我们根据自身情况来决定是否修改。这里简单说明一下配置文件的含义。
/** @type {import('next').NextConfig} */
const { SERVER_ENV } = process.env;
const nextConfig = {
experimental: {
// appDir: true, //使用启用app目录
urlImports: [], // 可使用三方地址,在引用css、img等位置需要配置
},
reactStrictMode: false, //是否使用严格模式
publicRuntimeConfig: {}, // 导入全局配置,服务端和客户端都可以访问
poweredByHeader: SERVER_ENV !== "production", // 是否在head中增加Nextjs信息
images: {
unoptimized: true,
},
swcMinify: true, //使用swc压缩
assetPrefix: "/", //静态资源引用的前缀,默认不需要处理。CDN优化需要修改。
};
module.exports = nextConfig;
发布和部署
经过简单的修改,我们已经有了一个可以使用的项目。这个项目使用自定义的形式,返回了一些简单的数据。
编译
编译是最简单的一个步骤。只需要执行npm run build即可。
可以看到输出的信息。包括输出目录、文件大小、初次加载大小、哪些使用SSR等。默认出书目录在.next文件夹,如果需要修改,可以在next.config.js中增加配置distDir:'build'来修改最终编译之后的文件地址。
部署
将项目部署到服务器有几个方向。简单的就是自己启动一个服务,然后让上层应用转发请求就可以了。常用的还有一种就是打到镜像里。
服务器部署
这里假设服务上使用pm2来做守护进程,这里简单说一下配置文件改怎么写。根目录下新建配置文件pm2.json。
{
"apps": [
{
"name": "nextjs-demo@8082",
"script": "index.js",
"args": [],
"env": {
"PORT": "8082"
}
}
]
}
apps,要启动的应用的内容,这里是一个数组,支持一次性配置多个。name,应用的名称,会显示在pm2的列表中。script,启动脚本的位置。这里默认使用nodejs启动,所有就不要写其他类型的脚本了。args,启动参数,这里一般不需要。env,环境变量,根据实际情况写就可以了。这里我单独设置了一个端口。
服务器上安装pm2只需要简单的执行npm i -g pm2即可,使用起来非常方便。还有一些插件,比如日志分割等,有需要的可以自己研究。这里简单说明一下常用命令。
pm2 start pm2.json,在根目录执行,使用配置文件启动项目。pm2 delete 0,删除第0个项目,序号0也可以替换成具体的配置文件中的name值。pm2 restart 0,重启第0个项目,序号0也可以替换成具体的配置文件中的name值。pm2 logs,查看所有日志。pm2 info 0,查看第0个项目的信息。
只需要将项目发到服务器上,比如使用git统一管理。就可以在推送成功的时候触发webhook来自定处理项目的部署、重启等操作。
镜像部署
现在越来越多的项目使用容器部署,前端项目当然不例外。这里简单说一下镜像打包文件怎么写。
首先需要修改package.json的命令。安装cross-env库并修改启动命令。
注意,需要将环境变量
NODE_ENV设置为生产,可以搭配生成更小的文件。
其次,在项目根目录创建Dockerfile文件。
FROM node:16.16.0
LABEL MAINTAINER="疯狂紫萧"
# 指定工作目录
WORKDIR /app
#RUN apt update
# 容器默认时区为UTC,如需使用上海时间请启用以下时区设置命令
ENV TZ=Asia/Shanghai \
DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata
# npm 源,选用国内镜像源以提高下载速度
#RUN npm config set registry https://mirrors.cloud.tencent.com/npm/
# 拷贝包管理文件
COPY . /app
RUN npm install --only=production
#CMD 运行以下命令
CMD ["npm", "start"]
从注释也能看到,镜像打包有以上几个步骤:
- 选择基础镜像,这里使用标准nodejs镜像
node:16.16.0。这个镜像的好处是功能比较全,坏处就是比较大,有优化需求的,可以再试试其他基础镜像。 - 设置镜像信息,比如:作者名称。
- 指定工作目录,这个没什么好说的。如果不是需要多层构建,建议尽早设置。
- 指定时区。这是一个容易忽略的问题。默认时区和中国有8小时差距,如果在项目中使用时间日期,可能有隐形bug,最好直接设置,一步到胃。
- 设置npm源,可选项。
- 复制文件到镜像。这里稍微注意一下,复制前保证项目中没有
node_modules目录,否则镜像会非常大。建议在打包服务器上做上面这些内容,不影响开发,也不容易变大。 - 安装依赖。这个地方的安装依赖是安装镜像内的依赖。它的前提是本地已经通过打包生成了静态资源。这里可以稍微修改一下,在打包之后直接使用打包内容(nextjs相关内容也要有)来构建,忽略源文件。
- 运行启动命令。这里不使用守护进程的一个原因是,docker相关服务本身就有重启的能力,不需要再额外增加一个进程来处理此类问题。其次,可以通过直接运行来使用docker内部的日志,省去了,有问题还要到容器内查看的麻烦。
好了,经过上面的几个步骤,我们的项目就完成了创建、修改、上线的过程。剩下的就很简单了,修改源代码,增加自己的业务逻辑就好了。
大肆改造一:修改界面
页面上的内容是相当容易的,大家都搞了很久了。这里简单写一个页面就好了。顺便注意下以下情况:
- Nextjs的渲染分2阶段。第一阶段是服务端渲染,走一遍渲染要求不能有客户端相关的内容,比如不能有直接调用
window对象的地方,这里可以通过放在方法里或者判断是否客户端渲染的方式处理。第二阶段是客户端渲染,就和正常的方式一样了。 - 文件路径是固定的。所有
src/pages下的文件都会处理成一个独立的页面入口。所以需要非入口页面的内容放在其他目录下。(也可以关闭这个选项,但是复杂度会上升,需要手动处理的内容变多)。页面路径就是路由地址。 - 基于上一条,所有页面之外的内容可以放在其他文件夹。部分api可以放在
src/pages/api下。
新增组件
在根目录下新增组件src/components/button.tsx。这个组件只有简单的接受事件的能力,模拟一个简单的组件。
组件内容之间简单的设置按钮事件和内容。正常的组件写法,没有特殊的地方。
新增页面
在根目录下创建新页面src/pages/test.tsx。这个页面主要用来使用刚刚创建的组件。
可以看到,这里也是简单的页面使用方式。引用组件,然后设置事件等等内容。
展示效果如上图。点击按钮之后按钮的统计值会自动增加。
这些都是基础应用。但是,当我们使用客户端API的使用就会遇到报错了。
<div>
<div>这是一行</div>
<Button onClick={() => setCount(count + 1)}>确定按钮:{count}</Button>
<div>宽度:{window.document.body.offsetWidth}</div>
</div>
比如这里我增加一行获取页面宽度的方法。假如直接设置值,按道理我们是可以使用的。但是在Nextjs上由于它自身的水合方式,会导致渲染报错。
报错内容也很简单,就是提示window对象不存在。(第一次刷新会报错,如果是热更新是没事的)
只有像上面这样处理才能正常访问。判断是否客户端渲染,然后再走下面的方法。但是这个方式很明显打断了页面的渲染流程,更好的方式是用组件的时候,在组件不得已使用的情况下直接打断渲染。
在根目录下新建方法文件src/service/win.ts。这个目录下使用的就是纯方法,我们模拟调用纯方法的过程。
export function getWinWidth() {
return window.document.body.offsetWidth;
}
简单写一个获取内容宽度的方法,类似上面的直接使用。
假如我们直接使用方法,同样也会出现上面的window对象不存在的情况。这个时候我们是可以用其他方式阻止方法执行的。
- 第一种方法就是放在
useEffect里面,同样是在客户端渲染之后再调用就没问题。 - 第二种方法就是直接判断
window对象是否存在,不存在的时候使用默认值。
这种方法相当于先设置一个默认值,然后在正式使用的时候再使用真实的值。
大肆改造二:增加API
增加API的方式有2种,一种就是使用Nextjs提供的api文件夹,使用固定目录实现。第二种就是在自定义的server中自己创建api,路径比较随意,看个人需要。
固定路径API
固定路径的实现可以直接参考默认文件src/pages/api/hello.ts的内容。可以看到函数参数有2个,一个是req,它代表的是请求对象,常用的header、cookie、url、query等都在上面。另外一个res就是返回对象,常用来设置返回值,可以设置返回类型text、json、流等,也可以直接返回对象。
export default function handler(
req: NextApiRequest,
res: NextApiResponse<any>
) {
const data = { header: req.headers, url: req.url, text: "返回内容" };
res.status(200).json(data);
}
设置一下返回值,我们看看效果。
打开API地址http://localhost:8082/api/hello,我们就可以看到刚才设置的值了。路径的前缀/api就是固定前缀,后面的hello就是文件名。
自定义API
自定义API的实现实在根目录下的server/index.js文件中。可以看到,这里同样返回了req、res2个参数,含义和方法基本一致。这里简单设置了API的路径是/api_test,返回的内容是一个JSON对象。
大肆改造三:使用数据库
这里演示使用流行库mongodb,使用方式比较简单。考虑的使用场景基本在中小型项目。有兴趣的也可以研究一下mysql相关的信息,比如使用sequelize实现的多数据库的使用方式。参考模版
创建一个简单的数据库连接文件。这里每次调用都是创建一个新连接,没有连接池的概念。上面的数据库连接使用的是字符串形式,自己研究的时候最好调换成实际的地址。
我们在根目录下创建新的Model文件,主要放数据库操作的实现方法。src/model/user.ts,这个文件住要实现了DB库的连接建立和使用,每个方法都要在实际使用前确保连接建立,调用init方法,如果对象未创建则创建,如果已创建则直接使用现有的连接对象。
这是简单的查询某个符合条件的对象方法。参数使用any类型不做校验(有需要的可以给定特定类型,减少意外值传入)。查询前使用init方法返回数据库操作对象,然后再调用三方库给的findOne方法,这个方法的第一个参数是查询条件,使用的是已有的属性和值。第二个参数是配置,可以配置返回值等内容。
其他内容
除了上面的简单使用,也可以参考官方文档调用对应的方法来实现自己的业务逻辑。这里简单提几个常用的可能。
使用自定义的方法初始化mongodb的
_id值。默认使用的是一个长字符串,可以自定义为其他值,比如数据预计不大的情况下,缩短id的长度。可以增加一个检查文档(表)是否存在的方法,在项目启动是执行。这样可以做到自动创建对应的文档集。至于内容就比较简单了。mongodb自己适配任意形式的数据,随意传入就好。扩展性真的比较高。
除了上面的查询单个方法外,还有查询总数,查询列表等方法。为了能让数据按照顺序返回,可以安排一个字段作为排序的基准值。上面我写的例子使用了更新时间戳来作为排序值。
大肆改造四:定时任务
定时任务用到的地方真的特别多,这里简单介绍一下定时任务的使用方式。
首先需要安装定时任务的三方库,这里使用npm i -S cron安装。定时任务大部分情况下需要一直运行,少部分情况下需要根据场景触发,这里实现一个自定运行的例子。
在根目录server/index.js文件中增加定时任务的引用。这里放在自定义服务端,保质任务能够尽早启动执行。
// server/task/index.js
require("./test");
新增文件server/task/index.js,这个是所有定时任务的入口,如果只有一个任务,可以直接写任务内容。这里考虑以后会增加更多任务,所有统一了任务入口。所有需要运行的任务全部放在这里统一执行。
也可以考虑一下自定调用,扫文件目录,然后把所有文件引用并启动。
这样看是不是非常简单,三行代码实现一个定时任务!使用这个方法需要3个步骤:
- 使用new初始化函数对象。
- 传入参数,A:时间规则。B:每次执行的回调函数。
- 调用链式方法
start启动定时任务。
定时任务的时间规则,使用的是一个叫cron表达式的形式,它有6个参数,每个代表的含义不同。
- 第一个代表秒。
*是每秒,*/3是每3秒(一分钟执行20次),3是第3秒(一分钟执行一次)。 - 第二个代表分钟。
- 第三个代表小时。
- 第四个代表日期。
- 第五个代表月份。
- 第六个代表星期。
//几个例子
0 * * * * ? 每1分钟触发一次
0 0 * * * ? 每天每1小时触发一次
0 0 10 * * ? 每天10点触发一次
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 30 9 1 * ? 每月1号上午9点半
0 15 10 15 * ? 每月15日上午10:15触发
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0 0 1 * ? 每月1号凌晨执行一次
大肆改造五:消息队列
要想要好分布式或者处理大量的消息,一般都会接入消息队列来做处理。这里简单讲一下怎么使用消息队列。npm i -S amqplib先安装依赖库,这是一个实现了消息队列协议的库。
新建消息初始化文件server/db/mq.jd,这个文件主要用来创建对象,后面的业务逻辑全部使用这个文件来发送消息。
这里主要实现连接MQ服务器,同时创建一个广播队列。
广播队列的主要作用是,通知在线的所有服务器,某个特定的消息。
其次还有常用的1对一的通知,消息会堆积在MQ服务器,只有某个监听队列的服务消费了消息,消息才会消失。常见的使用场景包括抢购、下单等情况。
还有一个是ack机制,即消息消费之后还要在通知MQ消费成功,否则消息不会消失。在保证稳定的场景下会需要。(MQ不能保证100%稳定,会出现消息丢失,消费失败等情况。)
//server/index.js中新增消息监听引用
require("./message");
放在入口处理,保证监听服务跟随系统启动。
可以看到简单的消息监听,发送消息的实现。在业务需要的时候调用PushMQMessage发送消息,这里使用的是发送字符串的形式。
默认设置了一个监听消息,消息渠道和发送的渠道一致,标识这个消息是要发送到所有服务器上。除了其他的收到消息的服务器,自己本身也会收到自己发送的消息。
除了上面的业务逻辑之外,我单独写了一个定时发送消息的逻辑。主要是保证在长时间不使用之后连接还是正常的。
结束了
到这来整个教程基本结束了。文章内容简单介绍了使用Nextjs开发网站的流程,同时涉及到了数据库、定时任务、MQ等知识。虽然是简单的介绍,但是在看过之后已经可以做到简单的上手和上线。
更多的内容可以期待后面出的高阶教程,会讲更多的内容。不再是简单介绍,会包含在原有脚手架的基础上实现自定义更多的内容,包括多环境处理、代理设置、镜像优化、甚至会有无头浏览器自动化等内容。