React + PDF.js 实战:构建一个带打印/下载功能的 PDF 查看器

本文深入解析基于 React 和 PDF.js 构建 PDF 查看器的实现方案,该组件支持 PDF 渲染、图片打印和下载功能,并包含完整的加载状态与错误处理机制。

完整代码在最后

一个PDF 文件:

mozilla.github.io/pdf.js/web/...

效果展示:

安装 PDF.js

要使用 pdf.js,你可以通过 npm 安装它。以下是安装和配置 pdf.js 的详细步骤:

1. 安装 pdfjs-dist

pdf.js 的官方 npm 包名为 pdfjs-dist。可以通过以下命令安装:

复制代码
npm install pdfjs-dist
  • pdfjs-dist 是 PDF.js 的分发版本,包含了核心功能和 Worker 文件。
  • 安装完成后,你可以在项目中直接引入并使用。

2. 设置 Worker 文件路径

PDF.js 需要一个 Worker 文件来处理 PDF 渲染任务。默认情况下,Worker 文件路径需要手动设置。

方法 1:使用 CDN

如果你不想将 Worker 文件打包到项目中,可以直接使用 CDN 提供的 Worker 文件:

js 复制代码
import { GlobalWorkerOptions } from 'pdfjs-dist';

// 设置 Worker 文件路径
GlobalWorkerOptions.workerSrc = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';

方法 2:本地 Worker 文件

如果你希望将 Worker 文件打包到项目中,可以这样做:

  1. 在代码中引入 Worker 文件:

    js 复制代码
    import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
    import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
    
    // 设置 Worker 文件路径
    GlobalWorkerOptions.workerSrc = pdfjsWorker;
  2. 这种方式适合离线环境或需要完全控制依赖的情况。

技术架构解析

1. 核心依赖

  • PDF.js:Mozilla 开源的 PDF 解析库,支持 Web 端 PDF 渲染
  • React Hooks:使用 useState 管理状态,useEffect 处理副作用,useRef 操作 DOM
  • Ant Design:采用 Button 组件构建操作界面

pdf.js 官网:mozilla.github.io/pdf.js/

2. 初始化配置

通过配置 Worker 文件实现 PDF 解析的 Web Worker 并行处理,避免阻塞主线程。

typescript 复制代码
GlobalWorkerOptions.workerSrc = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';

核心功能实现

1. PDF 渲染流程

这段代码的核心功能是加载一个 PDF 文件,并将其第一页渲染到 <canvas> 元素中。

typescript 复制代码
// 加载 PDF 并渲染到 canvas
  useEffect(() => {
    const abortController = new AbortController();

    if (!pdfUrl) return;

    setIsLoading(true);
    setError(null);

    getDocument(pdfUrl).promise
      .then((pdf) => {
        if (abortController.signal.aborted) return;

        console.log('PDF loaded');

        // 获取第一页
        pdf.getPage(1).then((page) => {
          if (abortController.signal.aborted) {
            page.cleanup();
            return;
          }

          console.log('Page 1 loaded');
          const scale = 1.5;
          const viewport = page.getViewport({ scale });

          if (!canvasRef.current) {
            throw new Error('Canvas element not found');
          }
          const canvas = canvasRef.current;
          const context = canvas.getContext('2d');

          // 清除之前的画布内容
          context?.clearRect(0, 0, canvas.width, canvas.height);

          canvas.height = viewport.height;
          canvas.width = viewport.width;
          console.log('Canvas尺寸设置:', canvas.width, 'x', canvas.height);

          // 渲染 PDF 页面到 canvas
          const renderContext: any = {
            canvasContext: context,
            viewport: viewport,
          };

          const renderTask = page.render(renderContext);

          renderTask.promise
            .then(() => {
              console.log('页面渲染完成');
              setIsLoading(false);
            })
            .catch((renderError) => {
              if (!abortController.signal.aborted) {
                setError('页面渲染失败: ' + renderError.message);
              }
            });
        });
      })
      .catch((error) => {
        if (!abortController.signal.aborted) {
          setError('PDF加载失败: ' + error.message);
          console.error('Error loading PDF:', error);
        }
      })
      .finally(() => {
        if (!abortController.signal.aborted) {
          setIsLoading(false);
        }
      });

    return () => {
      abortController.abort();
      const canvas = canvasRef.current;
      if (canvas) {
        const context = canvas.getContext('2d');
        context?.clearRect(0, 0, canvas.width, canvas.height);
      }
    };
  }, [pdfUrl]);
  • 初始化加载状态:设置加载提示和清理错误信息。
  • 加载 PDF 文件 :使用 getDocument 加载 PDF,并获取第一页。
  • 计算视口与画布尺寸 :根据缩放比例调整 <canvas> 的大小。
  • 渲染 PDF 页面 :将 PDF 页面绘制到 <canvas> 上。
  • 错误处理与清理:捕获加载和渲染中的错误,并在组件卸载时清理资源。

以下是核心实现原理与技术要点的详细解读:

1. useEffect 的作用

js 复制代码
useEffect(() => {
  // ...
}, [pdfUrl]);
  • useEffect 是 React 的生命周期钩子,用于处理副作用(如数据加载、DOM 操作等)。
  • 这里的依赖项 [pdfUrl] 表示当 pdfUrl 发生变化时,这个 useEffect 会重新执行。
  • 如果 pdfUrl 为空或未改变,则不会触发新的加载。

2. 创建 AbortController

javascript 复制代码
const abortController = new AbortController();
  • AbortController 是一个浏览器内置的 API,用于取消异步操作(如网络请求)。
  • 在这里,我们用它来确保在组件卸载或重新加载时,可以中断正在进行的 PDF 加载任务,避免内存泄漏或不必要的资源消耗。

3. 检查 pdfUrl 是否有效

javascript 复制代码
if (!pdfUrl) return;
  • 如果 pdfUrl 为空,则直接退出函数,避免无效操作。

4. 设置加载状态

javascript 复制代码
setIsLoading(true);
setError(null);
  • isLoading 设置为 true,表示开始加载 PDF。
  • 清空之前的错误信息(如果有)。

5. 加载 PDF 文件

javascript 复制代码
getDocument(pdfUrl).promise
  .then((pdf) => { ... })
  .catch((error) => { ... })
  .finally(() => { ... });
  • getDocument 是 PDF.js 提供的方法,用于加载 PDF 文件。
  • 它返回一个 Promise,成功时会返回一个 pdf 对象,失败时会抛出错误。
关键点:
  • abortController.signal.aborted

    javascript 复制代码
    if (abortController.signal.aborted) return;
    • 如果组件已经被卸载或取消了加载任务,则直接退出,避免继续执行无意义的操作。
  • pdf.getPage(1)

    javascript 复制代码
    pdf.getPage(1).then((page) => { ... });
    • getPage(1) 获取 PDF 的第一页(索引从 1 开始)。

6. 计算视口与画布尺寸

javascript 复制代码
const scale = 1.5;
const viewport = page.getViewport({ scale });

if (!canvasRef.current) {
  throw new Error('Canvas element not found');
}
const canvas = canvasRef.current;
const context = canvas.getContext('2d');

// 清除之前的画布内容
context?.clearRect(0, 0, canvas.width, canvas.height);

canvas.height = viewport.height;
canvas.width = viewport.width;
console.log('Canvas尺寸设置:', canvas.width, 'x', canvas.height);
关键点:
  1. scale

    • scale=1.5 表示将 PDF 页面放大 1.5 倍进行渲染。
    • 可以根据需求调整缩放比例。
  2. getViewport

    • getViewport({ scale }) 根据缩放比例计算页面的视口尺寸(宽度和高度)。
  3. 清空画布

    • 使用 context.clearRect 清除之前的内容,确保每次渲染都是干净的。
  4. 设置画布尺寸

    • <canvas> 的宽高设置为视口的宽高,以匹配 PDF 页面的比例。

7. 渲染 PDF 页面到 Canvas

javascript 复制代码
const renderContext: any = {
  canvasContext: context,
  viewport: viewport,
};

const renderTask = page.render(renderContext);

renderTask.promise
  .then(() => {
    console.log('页面渲染完成');
    setIsLoading(false);
  })
  .catch((renderError) => {
    if (!abortController.signal.aborted) {
      setError('页面渲染失败: ' + renderError.message);
    }
  });
关键点:
  1. renderContext

    • 包含两个主要属性:

      • canvasContext:指向 <canvas> 的 2D 上下文。
      • viewport:定义了渲染区域的尺寸和比例。
  2. page.render

    • 调用 page.render 方法将 PDF 页面渲染到 <canvas> 上。
    • 返回一个 renderTask 对象,包含一个 promise,用于监听渲染完成或失败的状态。
  3. 渲染完成后的回调

    • 当渲染成功时,将 isLoading 设置为 false,隐藏加载提示。
    • 如果渲染失败,则记录错误信息。

8. 错误处理

javascript 复制代码
.catch((error) => {
  if (!abortController.signal.aborted) {
    setError('PDF加载失败: ' + error.message);
    console.error('Error loading PDF:', error);
  }
})
.finally(() => {
  if (!abortController.signal.aborted) {
    setIsLoading(false);
  }
});
关键点:
  1. 捕获加载错误

    • 如果 PDF 加载失败(如 URL 错误或网络问题),则设置错误信息并打印日志。
  2. finally

    • 无论成功还是失败,都会执行 finally 块。
    • 确保即使发生错误,isLoading 也会被重置为 false,避免加载状态卡住。

9. 清理逻辑

javascript 复制代码
return () => {
  abortController.abort();
  const canvas = canvasRef.current;
  if (canvas) {
    const context = canvas.getContext('2d');
    context?.clearRect(0, 0, canvas.width, canvas.height);
  }
};
关键点:
  1. 取消加载任务

    • abortController.abort() 中断正在进行的 PDF 加载任务,避免资源浪费。
  2. 清理画布

    • 在组件卸载时清除 <canvas> 的内容,确保没有残留的绘制数据。

2. 打印功能实现

效果展示:

JS 复制代码
 // 打印 PDF 页面
  const handlePrint = () => {
    const canvas = canvasRef.current;
    if (canvas) {
      // 创建隐藏的iframe
      const iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      document.body.appendChild(iframe);

      // 将canvas内容转换为图片
      const imgData = canvas.toDataURL('image/png');

      const printDocument = iframe.contentWindow?.document;
      if (printDocument) {
        printDocument.open();
        printDocument.write(`
          <html>
            <head>
              <title>Print</title>
            </head>
            <body style="margin: 0;">
              <img src="${imgData}" style="width: 100%; height: auto;" />
            </body>
          </html>
        `);
        printDocument.close();

        // 延迟执行打印以确保内容加载
        setTimeout(() => {
          iframe.contentWindow?.print();
          document.body.removeChild(iframe);
        }, 500);
      }
    }
  };
  • 获取 <canvas> 内容:将画布内容转换为 Base64 编码的图片数据。
  • 创建隐藏的 <iframe> :提供一个独立的文档环境,用于打印特定内容。
  • <iframe> 中写入 HTML :将图片嵌入到 <iframe> 的文档中。
  • 触发打印 :延迟调用浏览器的打印功能,并在完成后清理 <iframe>

1. 函数定义

javascript 复制代码
const handlePrint = () => { ... }
  • 这是一个 React 组件中的方法,用于处理 PDF 页面的打印操作。
  • 当用户点击"打印"按钮时,会调用这个函数。

2. 获取 <canvas> 元素

javascript 复制代码
const canvas = canvasRef.current;
if (canvas) { ... }
  • 使用 canvasRef.current 获取当前组件中引用的 <canvas> 元素。
  • 如果 <canvas> 存在,则继续执行打印逻辑;否则直接退出。

3. 创建隐藏的 <iframe>

javascript 复制代码
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
关键点:
  1. 为什么使用 <iframe>

    • 浏览器的打印功能通常会打印整个页面的内容。为了只打印特定内容(如 <canvas> 的内容),我们将其封装到一个隐藏的 <iframe> 中。
    • <iframe> 是一个独立的文档环境,不会影响主页面的内容。
  2. 隐藏 <iframe>

    • 设置 iframe.style.display = 'none' 将其隐藏,避免用户看到额外的 UI 元素。
  3. 添加到 DOM

    • 使用 document.body.appendChild(iframe)<iframe> 添加到页面的 DOM 树中。

4. <canvas> 内容转换为图片

javascript 复制代码
const imgData = canvas.toDataURL('image/png');
关键点:
  1. toDataURL 方法

    • <canvas> 提供了 toDataURL 方法,可以将画布内容转换为 Base64 编码的图片数据。
    • 参数 'image/png' 指定输出的图片格式为 PNG。
  2. Base64 图片数据

    • imgData 是一个字符串,包含图片的 Base64 编码。例如:

      bash 复制代码
      data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...

5. <iframe> 中写入 HTML 内容

javascript 复制代码
const printDocument = iframe.contentWindow?.document;
if (printDocument) {
  printDocument.open();
  printDocument.write(`
    <html>
      <head>
        <title>Print</title>
      </head>
      <body style="margin: 0;">
        <img src="${imgData}" style="width: 100%; height: auto;" />
      </body>
    </html>
  `);
  printDocument.close();
}
关键点:
  1. 获取 <iframe> 的文档对象

    • iframe.contentWindow?.document 获取 <iframe>document 对象。
    • 使用可选链操作符 ?. 确保 contentWindow 存在。
  2. 写入 HTML 内容

    • 调用 printDocument.open() 打开文档流。

    • 使用 printDocument.write() 向文档中写入 HTML 内容。

    • 写入的内容包括:

      • 一个 <img> 标签,其 src 属性设置为 Base64 图片数据。
      • 样式设置为 width: 100%height: auto,确保图片自适应打印区域。
    • 调用 printDocument.close() 关闭文档流。


6. 延迟执行打印

javascript 复制代码
setTimeout(() => {
  iframe.contentWindow?.print();
  document.body.removeChild(iframe);
}, 500);
关键点:
  1. 延迟执行

    • 使用 setTimeout 延迟 500 毫秒后再执行打印操作。
    • 这是为了确保 <iframe> 中的内容已经完全加载,避免打印时出现空白或不完整的情况。
  2. 触发打印

    • 调用 iframe.contentWindow?.print() 触发浏览器的打印对话框。
    • 用户可以选择打印机或保存为 PDF。
  3. 清理 <iframe>

    • 打印完成后,使用 document.body.removeChild(iframe) 从 DOM 中移除 <iframe>,释放资源。

3. 下载功能实现

<canvas> 中的内容转换为图片,并提供下载功能。用户点击"下载"按钮后,会触发这个函数,将画布内容保存为 PNG 文件。

效果展示:

typescript 复制代码
// 下载 PDF 页面

  const handleDownload = () => {

    const canvas = canvasRef.current;

    if (canvas) {

      const imgData = canvas.toDataURL('image/png');

      const fileName = pdfUrl.split('/').pop()?.replace(/\.pdf$/i, '') || 'download';

      const link = document.createElement('a');

      link.download = `${fileName}.png`;

      link.href = imgData;

      document.body.appendChild(link);

      link.click();

      document.body.removeChild(link);

    }

  }
  • 获取 <canvas> 内容:将画布内容转换为 Base64 编码的图片数据。
  • 生成文件名 :根据 pdfUrl 提取文件名,并移除 .pdf 后缀。
  • 创建下载链接 :动态生成一个 <a> 元素,并设置其 hrefdownload 属性。
  • 触发下载:通过模拟点击事件触发浏览器的下载功能,并在完成后清理 DOM。

1. 函数定义

javascript 复制代码
const handleDownload = () => { ... }
  • 这是一个 React 组件中的方法,用于处理 PDF 页面的下载操作。
  • 当用户点击"下载"按钮时,会调用这个函数。

2. 获取 <canvas> 元素

javascript 复制代码
const canvas = canvasRef.current;
if (canvas) { ... }
  • 使用 canvasRef.current 获取当前组件中引用的 <canvas> 元素。
  • 如果 <canvas> 存在,则继续执行下载逻辑;否则直接退出。

3. <canvas> 内容转换为图片

javascript 复制代码
const imgData = canvas.toDataURL('image/png');
关键点:
  1. toDataURL 方法

    • <canvas> 提供了 toDataURL 方法,可以将画布内容转换为 Base64 编码的图片数据。
    • 参数 'image/png' 指定输出的图片格式为 PNG。
  2. Base64 图片数据

    • imgData 是一个字符串,包含图片的 Base64 编码。例如:

      bash 复制代码
      data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...

4. 生成文件名

javascript 复制代码
const fileName = pdfUrl.split('/').pop()?.replace(/.pdf$/i, '') || 'download';
关键点:
  1. 提取文件名

    • pdfUrl.split('/')pdfUrl/ 分隔成数组。
    • .pop() 获取数组的最后一个元素,通常是文件名(如 example.pdf)。
  2. 移除 .pdf 后缀

    • .replace(/.pdf$/i, '') 使用正则表达式将文件名中的 .pdf 后缀替换为空字符串。

    • /.pdf$/i 的含义:

      • . 匹配点号(. 是特殊字符,需要用反斜杠转义)。
      • pdf 匹配字符串 "pdf"。
      • $ 表示匹配字符串的结尾。
      • i 表示忽略大小写。
  3. 默认文件名

    • 如果 pdfUrl 为空或无法提取文件名,则使用默认值 'download'

5. 创建下载链接

javascript 复制代码
const link = document.createElement('a');
link.download = `${fileName}.png`;
link.href = imgData;
关键点:
  1. 创建 <a> 元素

    • document.createElement('a') 创建一个 HTML 锚点(<a>)元素。
    • 这个元素用于模拟下载行为。
  2. 设置下载属性

    • link.download = ${fileName}.png` 设置下载文件的名称。
    • 例如,如果 fileNameexample,则下载的文件名为 example.png
  3. 设置链接地址

    • link.href = imgData 将 Base64 图片数据赋值给 href 属性。
    • 浏览器会将其识别为一个可下载的资源。

6. 触发下载

javascript 复制代码
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
关键点:
  1. 添加到 DOM

    • 使用 document.body.appendChild(link)<a> 元素临时添加到页面的 DOM 树中。
    • 这是为了确保浏览器能够识别并触发下载行为。
  2. 触发点击事件

    • 调用 link.click() 模拟用户点击下载链接。
    • 浏览器会自动开始下载文件。
  3. 清理 DOM

    • 使用 document.body.removeChild(link) 从 DOM 中移除 <a> 元素。
    • 避免页面上留下多余的元素。

总结

本文实现的 PDF 查看器具有以下特点:

特性 实现方案 优势
按需加载 首屏仅加载第一页 快速呈现
打印/下载 Canvas 转图片方案 跨浏览器兼容性好
错误恢复 状态隔离 + 错误边界 增强组件健壮性
内存安全 AbortController + 清理函数 防止内存泄漏

该方案为基本的 PDF 预览需求提供了可靠实现,开发者可根据具体业务场景进行扩展优化。对于需要完整 PDF 功能(如文本选择、表单填写等)的场景,建议结合 PDF.js 的完整功能 API 进行深度定制。


完整代码:

js 复制代码
import React, { useEffect, useRef, useState } from 'react';
import { getDocument } from 'pdfjs-dist';
import { GlobalWorkerOptions } from 'pdfjs-dist';
import { Button } from 'antd';

// 设置 Worker 文件路径
GlobalWorkerOptions.workerSrc = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';

// 为 pdfUrl 参数添加 string 类型注解,解决隐式 any 类型问题
const PDFViewer = ({ pdfUrl }: { pdfUrl: string }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // 打印 PDF 页面
  const handlePrint = () => {
    const canvas = canvasRef.current;
    if (canvas) {
      // 创建隐藏的iframe
      const iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      document.body.appendChild(iframe);

      // 将canvas内容转换为图片
      const imgData = canvas.toDataURL('image/png');

      const printDocument = iframe.contentWindow?.document;
      if (printDocument) {
        printDocument.open();
        printDocument.write(`
          <html>
            <head>
              <title>Print</title>
            </head>
            <body style="margin: 0;">
              <img src="${imgData}" style="width: 100%; height: auto;" />
            </body>
          </html>
        `);
        printDocument.close();

        // 延迟执行打印以确保内容加载
        setTimeout(() => {
          iframe.contentWindow?.print();
          document.body.removeChild(iframe);
        }, 500);
      }
    }
  };

  // 下载 PDF 页面
  const handleDownload = () => {
    const canvas = canvasRef.current;
    if (canvas) {
      const imgData = canvas.toDataURL('image/png');
      const fileName = pdfUrl.split('/').pop()?.replace(/\.pdf$/i, '') || 'download';
      
      const link = document.createElement('a');
      link.download = `${fileName}.png`;
      link.href = imgData;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    }
  }

  // 加载 PDF 并渲染到 canvas
  useEffect(() => {
    const abortController = new AbortController();

    if (!pdfUrl) return;

    setIsLoading(true);
    setError(null);

    getDocument(pdfUrl).promise
      .then((pdf) => {
        if (abortController.signal.aborted) return;

        console.log('PDF loaded');

        // 获取第一页
        pdf.getPage(1).then((page) => {
          if (abortController.signal.aborted) {
            page.cleanup();
            return;
          }

          console.log('Page 1 loaded');
          const scale = 1.5;
          const viewport = page.getViewport({ scale });

          if (!canvasRef.current) {
            throw new Error('Canvas element not found');
          }
          const canvas = canvasRef.current;
          const context = canvas.getContext('2d');

          // 清除之前的画布内容
          context?.clearRect(0, 0, canvas.width, canvas.height);

          canvas.height = viewport.height;
          canvas.width = viewport.width;
          console.log('Canvas尺寸设置:', canvas.width, 'x', canvas.height);

          // 渲染 PDF 页面到 canvas
          const renderContext: any = {
            canvasContext: context,
            viewport: viewport,
          };

          const renderTask = page.render(renderContext);

          renderTask.promise
            .then(() => {
              console.log('页面渲染完成');
              setIsLoading(false);
            })
            .catch((renderError) => {
              if (!abortController.signal.aborted) {
                setError('页面渲染失败: ' + renderError.message);
              }
            });
        });
      })
      .catch((error) => {
        if (!abortController.signal.aborted) {
          setError('PDF加载失败: ' + error.message);
          console.error('Error loading PDF:', error);
        }
      })
      .finally(() => {
        if (!abortController.signal.aborted) {
          setIsLoading(false);
        }
      });

    return () => {
      abortController.abort();
      const canvas = canvasRef.current;
      if (canvas) {
        const context = canvas.getContext('2d');
        context?.clearRect(0, 0, canvas.width, canvas.height);
      }
    };
  }, [pdfUrl]);

  return (
    <div style={{ position: 'relative', width: '100%' }}>
      <Button onClick={handlePrint}>打印</Button>
      <Button onClick={handleDownload}>下载</Button>
      {isLoading && <div>加载中...</div>}
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <div id="printPdfButton"></div>
      <canvas
        key={pdfUrl}
        ref={canvasRef}
        style={{ width: '100%', height: 'auto' }}
      />
    </div>
  );
};

export default PDFViewer;

使用此组件:

js 复制代码
import React from "react";
import "./index.less";
import PDFViewer from "./PDFViewer";

const ImgePage = () => {
  return (
    <div
      style={{ height: "80vh", border: "1px solid #ddd", position: "relative" }}
    >
      <PDFViewer
        pdfUrl={
          "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"
        }
      />
    </div>
  );
};

export default ImgePage;
相关推荐
拉不动的猪40 分钟前
react基础2
前端·javascript·面试
kovlistudio40 分钟前
红宝书第二十九讲:详解编辑器和IDE:VS Code与WebStorm
开发语言·前端·javascript·ide·学习·编辑器·webstorm
拉不动的猪42 分钟前
react基础1
前端·javascript·面试
烛阴1 小时前
从零到RESTful API:Express路由设计速成手册
javascript·后端·express
ElasticPDF-新国产PDF编辑器1 小时前
Vue PDF Annotation plugin library online API examples
javascript·vue.js·pdf
鱼樱前端1 小时前
Vite 工程化深度解析与最佳实践
前端·javascript
鱼樱前端1 小时前
Webpack 在前端工程化中的核心应用解析-构建老大
前端·javascript
Moment1 小时前
多人协同编辑算法 —— CRDT 算法 🐂🐂🐂
前端·javascript·面试
ElasticPDF-新国产PDF编辑器3 小时前
Vue 项目使用 pdf.js 及 Elasticpdf 教程
javascript·vue.js·pdf
今天也想MK代码4 小时前
ReFormX:现代化的 React 表单解决方案 - 深度解析与最佳实践
前端·react.js·性能优化