安装Nodejs
1. 安装分布式版本管理系统Git
shell
yum install git -y
2. 使用Git将NVM的源码克隆到本地的~/.nvm目录下,并检查最新版本
shell
git clone https://gitee.com/mirrors/nvm.git ~/.nvm && cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`
3. 依次运行以下命令,配置NVM的环境变量
shell
echo ". ~/.nvm/nvm.sh" >> /etc/profile
shell
source /etc/profile
4. 运行以下命令,查看Node.js版本
shell
nvm list-remote
5. 安装 Node.js 的20.11.1版本
shell
nvm install v20.11.1
创建Nextjs项目
1. 使用 create-next-app 创建项目
shell
npx create-next-app@latest
根据提示一路回车即可
vbnet
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
2. 进入项目并启动
arduino
cd my-app
npm run dev
Linux系统使用 Puppeteer
1. 在 Linux 系统上会报如下错误
arduino
Collecting page data .Error: Could not find Chrome (ver. 119.0.6045.105).
找不到 Chrome (ver. 119.0.6045.105)。
解决方案:在项目根目录下配置 .puppeteerrc.cjs 文件
js
const { join } = require('path');
module.exports = {
cacheDirectory: join(__dirname, 'node_modules', '.puppeteer_cache'),
};
切记,配置完文件后,最好删除node_modules目录,重新运行
npm install
否则可能无效。
2. 运行 npm run build 报如下错误
vbnet
Collecting page data .Error: Failed to launch the browser process!
/root/my-app/node_modules/.puppeteer_cache/chrome/linux-119.0.6045.105/chrome-linux64/chrome: error while loading shared libraries: libdrm.so.2: cannot open shared object file: No such file or directory
libdrm.so.2 共享库文件找不到。
解决方案:安装 libdrm
dnf install libdrm
3. 继续运行 npm run build 报如下错误
vbnet
Collecting page data ..Error: Failed to launch the browser process!
/root/my-app/node_modules/.puppeteer_cache/chrome/linux-119.0.6045.105/chrome-linux64/chrome: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directory
libgbm.so.1 共享库文件找不到。
解决方案: 安装 mesa-libgbm
dnf install mesa-libgbm
4. 以上问题都解决后运行 npm run build 成功
使用 Puppeteer 导出 PDF 供前端接口下载
Puppeteer 的 page.pdf() 方法返回的是 Promise,前端接口一般希望返回的对象是 JSON 类似 {code: 0, data: pdf文件内容, msg: '导出成功'}
。
所以需要做如下处理,先把 buffer 处理成 base64 再存储在 JSON 中传递给前端:
php
const pdf = await page.pdf({ format: 'A4' });
await page.close();
// 先转成base64,在浏览器端再转换为arraybuffer,然后再用Blob进行下载
// json里是不能直接存储二进制数据的,直接赋值二进制的话,前端下载的文件无法预览
const pdfBase64 = Buffer.from(pdf).toString('base64');
return Response.json({ data: pdfBase64, code: 0, msg: 'pdf' });
前端拿到返回对象后,先对 base64 转换为 buffer 再用 Blob 转换为 pdf 文件
ini
// 把base64转换为buffer
import { decode } from 'base64-arraybuffer';
if (res && res.code === 0) {
// 转换base64为buffer
const pdfBuffer = decode(res.data);
// 生成 Blob 对象
const blob = new Blob([pdfBuffer], { type: 'application/pdf' });
// 生成 URL
const url = window.URL.createObjectURL(blob);
// 创建 a 标签并设置下载属性
const link = document.createElement('a');
link.href = url;
link.download = 'filename.pdf';
// 模拟用户点击下载链接
link.click();
// 释放 URL 对象
window.URL.revokeObjectURL(url);
}
nextjs 允许多个跨域源
1. 首先拷贝如下代码到项目的 utils/cors.ts 中
ts
/**
* Multi purpose CORS lib.
* Note: Based on the `cors` package in npm but using only
* web APIs. Feel free to use it in your own projects.
*/
type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[];
type OriginFn = (
origin: string | undefined,
req: Request
) => StaticOrigin | Promise<StaticOrigin>;
interface CorsOptions {
origin?: StaticOrigin | OriginFn;
methods?: string | string[];
allowedHeaders?: string | string[];
exposedHeaders?: string | string[];
credentials?: boolean;
maxAge?: number;
preflightContinue?: boolean;
optionsSuccessStatus?: number;
}
const defaultOptions: CorsOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
};
function isOriginAllowed(origin: string, allowed: StaticOrigin): boolean {
return Array.isArray(allowed)
? allowed.some((o) => isOriginAllowed(origin, o))
: typeof allowed === 'string'
? origin === allowed
: allowed instanceof RegExp
? allowed.test(origin)
: !!allowed;
}
function getOriginHeaders(reqOrigin: string | undefined, origin: StaticOrigin) {
const headers = new Headers();
if (origin === '*') {
// Allow any origin
headers.set('Access-Control-Allow-Origin', '*');
} else if (typeof origin === 'string') {
// Fixed origin
headers.set('Access-Control-Allow-Origin', origin);
headers.append('Vary', 'Origin');
} else {
const allowed = isOriginAllowed(reqOrigin ?? '', origin);
if (allowed && reqOrigin) {
headers.set('Access-Control-Allow-Origin', reqOrigin);
}
headers.append('Vary', 'Origin');
}
return headers;
}
// originHeadersFromReq
async function originHeadersFromReq(
req: Request,
origin: StaticOrigin | OriginFn
) {
const reqOrigin = req.headers.get('Origin') || undefined;
const value =
typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
if (!value) return;
return getOriginHeaders(reqOrigin, value);
}
function getAllowedHeaders(req: Request, allowed?: string | string[]) {
const headers = new Headers();
if (!allowed) {
allowed = req.headers.get('Access-Control-Request-Headers')!;
headers.append('Vary', 'Access-Control-Request-Headers');
} else if (Array.isArray(allowed)) {
// If the allowed headers is an array, turn it into a string
allowed = allowed.join(',');
}
if (allowed) {
headers.set('Access-Control-Allow-Headers', allowed);
}
return headers;
}
export default async function cors(
req: Request,
res: Response,
options?: CorsOptions
) {
const opts = { ...defaultOptions, ...options };
const { headers } = res;
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
const mergeHeaders = (v: string, k: string) => {
if (k === 'Vary') headers.append(k, v);
else headers.set(k, v);
};
// If there's no origin we won't touch the response
if (!originHeaders) return res;
originHeaders.forEach(mergeHeaders);
if (opts.credentials) {
headers.set('Access-Control-Allow-Credentials', 'true');
}
const exposed = Array.isArray(opts.exposedHeaders)
? opts.exposedHeaders.join(',')
: opts.exposedHeaders;
if (exposed) {
headers.set('Access-Control-Expose-Headers', exposed);
}
// Handle the preflight request
if (req.method === 'OPTIONS') {
if (opts.methods) {
const methods = Array.isArray(opts.methods)
? opts.methods.join(',')
: opts.methods;
headers.set('Access-Control-Allow-Methods', methods);
}
getAllowedHeaders(req, opts.allowedHeaders).forEach(mergeHeaders);
if (typeof opts.maxAge === 'number') {
headers.set('Access-Control-Max-Age', String(opts.maxAge));
}
if (opts.preflightContinue) return res;
headers.set('Content-Length', '0');
return new Response(null, { status: opts.optionsSuccessStatus, headers });
}
// If we got here, it's a normal request
return res;
}
export function initCors(options?: CorsOptions) {
return (req: Request, res: Response) => cors(req, res, options);
}
2. 在路由文件中使用它:
javascript
import puppeteer from 'puppeteer';
// 代码重点begin
import cors from '@/utils/cors';
const allowed = [
'http://localhost:3000',
'https://test.abc.com',
'https://abc.com',
'https://www.abc.com',
];
// 代码重点end
export async function POST(request: Request, res: Response) {
// 代码重点begin
const corsOptions = {
origin: allowed,
credentials: true,
allowedHeaders: [
'X-CSRF-Token',
'Withcredentials',
'Authorization',
'X-Requested-With',
'Accept',
'Accept-Version',
'Content-Length',
'Content-MD5',
'Content-Type',
'Date',
'X-Api-Version',
],
};
// 代码重点end
//连接浏览器
const browser = await puppeteer.connect({ browserWSEndpoint });
// 打开浏览器页面tab
const page = await browser.newPage();
await page.setContent(pageContent);
const pdf = await page.pdf({ format: 'A4' });
await page.close();
const pdfBase64 = Buffer.from(pdf).toString('base64');
// 代码重点begin
return cors(
request,
Response.json({ data: pdfBase64, code: 0, msg: 'pdf' }),
corsOptions
);
// 代码重点end
}
// 代码重点begin
export async function OPTIONS(request: Request, res: Response) {
const corsOptions = {
origin: allowed,
credentials: true,
allowedHeaders: [
'X-CSRF-Token',
'Withcredentials',
'Authorization',
'X-Requested-With',
'Accept',
'Accept-Version',
'Content-Length',
'Content-MD5',
'Content-Type',
'Date',
'X-Api-Version',
],
};
return cors(
request,
new Response(null, {
status: 200,
}),
corsOptions
);
}
// 代码重点end
重点看代码注释中的
代码重点
因为 nextjs 14中的 app 路由请求 api 方法必须具名,所以要定义 POST 和 OPTIONS 两个方法。
OPTIONS 方法是预检请求必须定义它,否则请求在这就会因为跨域问题被终止,根本到不了 POST 请求方法中。
Puppeteer 导出 PDF 中文乱码
如果遇到导出的 PDF 中文是乱码,在启动 puppeteer 方法中配置中文 --lang=zh-CN 参数。
php
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--lang=zh-CN',
],
});
若仍没解决问题,就找一些中文字体上传到服务器中。
CentOS系统的字体位置是 /usr/share/fonts。
再刷新系统字体即可
fc-cache -fv