likes
comments
collection
share

CentOS8.5部署nextjs和使用puppeteer总结

作者站长头像
站长
· 阅读数 3

安装Nodejs

1. 安装分布式版本管理系统Git
yum install git -y
2. 使用Git将NVM的源码克隆到本地的~/.nvm目录下,并检查最新版本
git clone https://gitee.com/mirrors/nvm.git ~/.nvm && cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`
3. 依次运行以下命令,配置NVM的环境变量
echo ". ~/.nvm/nvm.sh" >> /etc/profile
source /etc/profile
4. 运行以下命令,查看Node.js版本
nvm list-remote
5. 安装 Node.js 的20.11.1版本
nvm install v20.11.1

创建Nextjs项目

1. 使用 create-next-app 创建项目
npx create-next-app@latest

根据提示一路回车即可

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. 进入项目并启动
cd my-app
npm run dev

Linux系统使用 Puppeteer

1. 在 Linux 系统上会报如下错误
Collecting page data  .Error: Could not find Chrome (ver. 119.0.6045.105). 

找不到 Chrome (ver. 119.0.6045.105)。

解决方案:在项目根目录下配置 .puppeteerrc.cjs 文件

const { join } = require('path');

module.exports = {
  cacheDirectory: join(__dirname, 'node_modules', '.puppeteer_cache'),
};

切记,配置完文件后,最好删除node_modules目录,重新运行 npm install 否则可能无效。

2. 运行 npm run build 报如下错误
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 报如下错误
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 中传递给前端:

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 文件

// 把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 中
/**
 * 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. 在路由文件中使用它:
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 参数。

const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--lang=zh-CN',
    ],
  });

若仍没解决问题,就找一些中文字体上传到服务器中。

CentOS系统的字体位置是 /usr/share/fonts。

再刷新系统字体即可

fc-cache -fv
转载自:https://juejin.cn/post/7341304374755442715
评论
请登录