npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cloud-pdf

v1.0.5

Published

基于nb-fe-pdf源码调整的pdf生成,采用nodejs+puppeteer

Readme

目前将html页面转成pdf文件的主流方式

  • 完整demo,见example 目录

image.png

1.不论是哪种方式,只要是将h5/vue/react/原生js 页面生成pdf,都会遇到的问题

  1. 各个浏览器、手机兼容性问题;
  2. 内容截断问题; 包括不限于 echart图表截断、动态table行截断问题
  3. 业务关系紧密的内容和描述需要尽可能放在一起打印
  4. 生成动态内容pdf等问题
  5. 批量下载pdf稳定性问题
  6. 如果是大文件 前端等待时间较长,如果关闭页面生成失败

2.针对以上问题的解决方案

方案1.前端生成 页面转pdf工具 + nb-fe-pdf算法

页面转pdf工具,比如:htmlToCavas/window.print/jspdf

  • 这个方案可以解决内容截断和生成动态内容,但是有以下问题
    • 各个浏览器、手机兼容性问题;
    • 批量下载pdf稳定性问题
    • 如果是大文件 前端等待时间较长,如果关闭页面生成失败

方案2.node端 node + Puppeteer + nb-fe-pdf算法(推荐)

  • 这个方案可以解决以1到6所有问题,并且经过多个项目的验证,不管是用vue/react还是别的框架,pc端还是H5端,ui框架用的elementui/vant/antd等,只要最终渲染结果是 DOM 结构都可以理想的实现分页下载;
  • 同步方案:
    • 适用于并发比较小(10左右),要下载的内容比较少(1M左右)时以内的场景
  • 异步方案(墙裂推荐)
    • 适用于各种场景,高并发、大文件的情况也适用;
    • 需要处理队列中的任务状态,成功要做什么、失败了要做什么等等

3.nb-fe-pdf算法

3.1 nb-fe-pdf算法思想

分页效果图

image.png

nb-fe-pdf算法思想图

image.png

  • nb-fe-pdf算法是在页面dom结构生成完成之后,根据标记,将页面分成一个个模块,计算这些模块的高度,将一个个模块合理的放到A4纸中;
  • 类似于拼图游戏,这个拼图游戏是要将一个个模块合理的放到A4纸中;
  • 上图例子中,模块1可能处于pdf页面的尾部,标题1和文本1可能在上一页,说明1可能被分到下一页了, 说明1是描述文本1,我们希望他们放在一起,给模块1所在的外层div加一个flag标记;
  • 最终分页问题转化为将一个个flag,合理的放到A4纸中

view层约定-普通模块(高度是固定的)

  • 给业务关系比较紧密的模块的外层元素,加上 class="page-splite-flag"
   <div>
    <div class="page-splite-flag">
        模块1
        <title>标题1</title>
        <div>
            <p>文本1</p>
            <p>文本2</p>
            <p>文本3</p>
        </div>
        <p>说明1</p>
    </div>
    
    <div class="page-splite-flag">
        模块2
        动态table2
       <xx-tabel>
        我有多少行,取决于数据库有多少条数据
       </xx-table>
    </div>
    
     <div class="page-splite-flag">
        模块3
        <title>标题2</title>
        <div id="echarts1">饼状图、柱状图</div>
    </div>
  </div>

view层约定 - 带有table的模块(高度未知,根据数据多少来展示)

    • 默认ui组件是基于elementui
    • 如果table内容的长度是动态的 引入cardTable 组件,用slot的方式在对应的信息放进去;
    • 如果table长度不是动态的就可以不用
  • 其实在算法层,最终用到的是"card-table"、“card-table-top-wraper”、“card-table-wraper”、“card-table-bom-wraper” 这些class类名,意味着不论什么ui框架的table组件,或者是原生table,只要按照这种结构,给对应的位置加上这些class名,就可以正确的完成动态table分页;
// cardTable.vue
<template>
  <div class="card-table page-splite-flag">
    <div class="card-table-top-wraper">
      <slot name="card-table-header" />
    </div>
    <div class="card-table-wraper">
      <slot name="card-table" />
    </div>
    <div class="card-table-bom-wraper">
      <slot name="card-table-footer" />
    </div>
  </div>
</template>
// usage.vue
 <card-table >
      <template #card-table-header>
        <div>
          <h3>投资产品选择</h3>
        </div>
        <h4 style="padding-left: 20px">定投产品总览</h4>
      </template>
      <template #card-table>
        <ts-table
          :table-data="regularList"
          :table-head="regularHead"
          :table-title-obj="{ hide: true }"
          :paginationHide="true"
        />
      </template>

       <template #card-table-footer>
           <p>表格说明信息1</p>
           <p>表格说明信息2</p>
           <p>...</p>
      </template>
    </card-table>

3.2 nb-fe-pdf使用方式

安装

nbnpm i nb-fe-pdf -S // nbnpm 下载(内部下载)
or 
npm install nb-fe-pdf -S // 无法使用nbnpm,可以用npm或yarn

源码地址:
https://gitlab.newbanker.cn/nbnpm/nb-fe-pdf (内部访问)
or
https://www.npmjs.com/package/nb-fe-pdf

参数说明

export interface PrintParmas {
  moduleMap: ModuleMap | ModuleInfo; // moduleMap是由多个ModuleInfo 组成的
  selectModule?: string[]; // 要下载的模块名["analy", "pension"]
  injectClass?: BaseClass; // injectClass不传默认是elementui的el-table
  callback?: Function; // 分页执行完毕的回调函数
}

const moduleInfo: ModuleInfo = {
        moduleId: "#print-analy-wraper", // 模块id,给每个要下载的组合所有页面的根元素加上id
        pageInfo: {
            title?: string; // 模块标题
            needTpl?: boolean; // 是否需要头尾模板,默认为false
            defaultType?: PrintType; // 模板类型,需要needTpl为true,
            waterMark?: boolean // 是否需要水印, 默认为false
            waterMarkConfig: { // 需要waterMark为true
                waterMarkId: string; // 要做水印的根元素id
                waterMarkContent: string; // 水印内容
            }; 
        };
      }

// 模板类型
export enum PrintType {
  NORMAL_TYPE = "NORMAL_TYPE", // 无头无尾
  HEADER_TYPE = "HEADER_TYPE", // 有头无尾
  FOOTER_TYPE = "FOOTER_TYPE", // 无头有尾
  HEADER_FOOTER_TYPE = "HEADER_FOOTER_TYPE", // 有头有尾
}

适配不同的UI框架

  • 在pageInfo 传入对应ui框架的table 的injectClass类名即可
 // defaut use elementui table component classnames
  static cardTableTBHeaderWraper = "el-table__header-wrapper"; // table header wraper classname
  static cardElRowClass = "el-table__row"; // table body row  classname
  static elTableBodyWraper = "el-table__body-wrapper"; // table body wraper classname


const injectClass = {
  cardTableTBHeaderWraper: 've-table-header' 
  cardElRowClass: 've-table-body-tr',
  elTableBodyWraper: 've-table-body'
}

快速开始-下载单个模块语法

// javascript 引用方式
import { Print } from 'nb-fe-pdf/lib/src'

// typescript 引用方式
import { Print } from 'nb-fe-pdf'

语法 new Print(moduleInfo) // 下载单个模块
也就是
new Print({moduleId: "#print-operate-report-wrapper",
           pageInfo: {
              defaultType: 'HEADER_TYPE',
              needTpl: true,
            },
          })

下载单个模块 - demo

<section>
  <!-- 页眉页脚模板 -->
  <pdf-tpl><pdf-tpl/>
  <!-- 正文部分用自定义id包裹 用以分页 -->
  <div id="print-operate-report" class="页面样式">
     <!-- ※必须※ node中会根据查询isPDFVisible是否存在来判断页面是否加载完毕 然后继续向下执行 -->
     <div v-if="isVisible" id="isPDFVisible"></div>
     <!-- 使用时,只要是待分页的模块都需要用page-splite-flag包裹起来 (table类型除外,参考下方) -->
     <div class="page-splite-flag 页面样式">XXXXX</div>
     <!-- 表格的包裹方式区别于其它 -->
     <card-table>
     		<template #card-table-header>
     			<!-- 若有表格标题部分 用#card-table-header包裹-->
     		</template>
     		<template #card-table>
     			<!-- table部分 用#card-table包裹-->
     			<ve-table XXXX  />
     		</template>
     </card-table>
  </div>
</section>
<script>
    import PDFTpl from '@/components/PDFTpl/index.vue'
    import CardTable from '@/components/CardTable'
    import { Print } from '@/modules/nb-fe-pdf/lib/index'
    
    data () {
      return {
        isResponseSuccess: true,
        isVisible: false,
      }
    },
    async mounted () {
      try {
        // 渲染页面的相关请求
        await 请求1 请求2....
      } catch (err) {
        // 有JS异常时将isResponseSuccess置为false
        this.isResponseSuccess = false
      } finally {
        if (this.isResponseSuccess) {
          this.handleGeneratePDF()
          setTimeout(() => {
            this.isVisible = true
          }, 600)
        }
      }
    }
    methods:  {
      handleGeneratePDF () {
        // 生成PDF相关
        new Print({
          moduleId: '#print-operate-report', // 自定义页面id
          pageInfo: {
            defaultType: 'HEADER_TYPE',  // 页眉页脚类型:HEADER_TYPE  有头无尾;NORMAL_TYPE 无头无尾;FOOTER_TYPE  无头有尾;HEADER_FOOTER_TYPE  有头有尾
            needTpl: true,
            waterMark: true, // 是否需要水印, 默认为false
            waterMarkConfig: {
              waterMarkContent: this.pra,
              waterMarkId: 'print-operate-report', //需要做水印的元素的id
            },
          },
        })
      }
    }
 </script>
 <style lang="css">
 @import 'nb-fe-pdf/print.css';
 </style>
// 写在公用方法中
export const  downloadPdf = (blobData, downloadFileName) => {
  const link = document.createElement('a')
  const url = window.URL.createObjectURL(
    new Blob([blobData], { type: "application/pdf,charset=utf-8" })
  )
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', downloadFileName)
  document.body.appendChild(link)
  link.click()
}

export const downLoadPdf = (
  params: object,
  onDownloadProgress: OnDownloadProgress // 可选
) => {
  return request.get(`/pdfUploadUrl?${qs.stringify(params)}`, {
    baseURL: "/amc-pdf-server/api/pdf/v1",
    timeout: 300000,
    responseType: "blob", // 一定要加
    onDownloadProgress,
  });
};

下载多个模块语法

new Print(PrintParmas)
也就是
new Print({ // 下载多个模块
  [selectModule],
  moduleMap, 
  [injectClass],
  [callback: () =>{ console.log('分页算法执行完毕')}] 
})

下载多个模块demo

/**
 * string1: 组合名称1
 * string2: 组合名称1的页面的根元素id
*/
const moduleMap:Map<string1, string2> = new Map([
  [
    'analy'
    {
      moduleId: "#print-analy-wraper",
      pageInfo: {
        defaultType: PrintType.HEADER_TYPE,
        needTpl: true,
      },
    },
  ],
  [
    "pension",
    {
      moduleId: "#print-pension-wraper",
      pageInfo: {
        defaultType: PrintType.HEADER_TYPE,
        needTpl: true,
      },
    },
  ],
  [
    "base",
    {
      moduleId: "#print-base-wraper",
      pageInfo: {
        needTpl: true,
        defaultType: PrintType.HEADER_TYPE,
      },
    },
  ]);

const selectModule = ["family", "invest"]

const injectClass = {
  cardTableTBHeaderWraper: 've-table-header'
  cardElRowClass: 've-table-body-tr',
  elTableBodyWraper: 've-table-body'
}

/**
 * selectModule 当前要下载的页面组合名称
 * moduleMap 所有要下载的页面组合
 * injectClass 
*/

new Print({ // 下载多个模块
  selectModule,
  moduleMap,
  injectClass,
  [callback: () =>{ console.log('分页算法执行完毕')}] 
})

添加页眉、页脚、A4大小图片

引入css print css 样式 @import 'nb-fe-pdf/print.css';

pageInfo中的defaultType,有以下四种类型
export enum PrintType {
    NORMAL_TYPE = "NORMAL_TYPE", // 默认无头无尾 
    HEADER_TYPE = "HEADER_TYPE", // 有头无尾 
    FOOTER_TYPE = "FOOTER_TYPE", // 无头有尾 
    HEADER_FOOTER_TYPE = "HEADER_FOOTER_TYPE", // 有头有尾 
}

设置页面页脚优先级
元素class设置 PrintType > this.pageInfo.defaultType > PrintType.NORMAL_TYPE;

- 封面页设置
<planCover class="page-splite-flag FOOTER_TYPE"/>

- 根据PrintType,打印A4纸大小的图片
<img src="xx.png" class="print-img-wraper"/>

3.3 源码说明

  • Print class
    • 根据传入的要打印的模块启动dfs搜索
  • DfsChild class
    • 负责根据标识获取分页所需要的信息
  • SplitePage class
    • 负责计算pdf分页和table分页
  • PdfPage class
    • 负责生成每个pdf页面
  • Compose class
    • 负责将每个pdf页面放到原来根元素的位置

3.4 使用nb-fe-pdf注意点

①需要分页的原页面自定义样式要保证和page-splite-flag同级或在包裹内,table类型同上,否则样式不生效 ②page-splite-flag之间不能嵌套 ③如果分页出现切分异常的问题 ,可以检查原页面的自定义样式,不建议使用margin来设置竖向样式,例如margin-top、margin-bottom建议替换成paddingXX。当然也可以使用margin,但要保证用BFC解决外边距重叠问题

3.5 关于nb-fe-pdf常问的问题

为什么要在dom层做分页?

  • 可选择的做分页的地方有:vdom层、ast层、真实dom层
  • 如果在vue/react的vdom层做,由于不同的框架对vdom的处理方式不同,vue 用tamplate语法,是对组件做了依赖收集,经过vdom diff,vdom patch,最终vdom和dom有对应关系;react是jsx语法,fiber结构的vdom,分片渲染,最终vdom和dom有对应关系,但是结构不一样,无法复用算法,所以pass;
  • 在ast层做,不同框架用的ast解析器不一样,需要根据不同的解析器做计算,所以pass;
  • 真实dom层,不管是什么框架,最终渲染结果都是dom,可以统一计算;

如此大量的操作DOM会有性能问题吗?比如频繁导致回流影响页面渲染?

  • 只要操作dom元素的位置,肯定会产生回流的,主要是要将回流的次数控制在不影响页面加载的范围内;
  • 在nb-fe-pdf对元素的操作是批量的,批量读取元素属性,批量append元素,并且这些元素在读取阶段,并没有放到浏览器渲染队列里面,只存储在内存中,在批量append 元素完会统一放到渲染队列中,统一渲染,尽可能避免刷新渲染队列,以免频繁引起回流;

image.png

为什么要加类似class="page-split-pdf" flag 标记,为什么标记不能嵌套?

  • 利用html 双标签的闭合关系可以确定一个区域,这个区域我们叫模块,可以将业务关系紧密的放在这个区域内
  • 一个flag标记描述的是一个模块,会根据这个标记计算这个区域的高度(offsetHeight + marginTop + marginBottom),如果flag标记存在嵌套关系,在计算的时候会重复计算,没有意义,会导致产生分页问题;

为什么上下相邻的模块,不建议上模块使用marginBottom,下模块使用marginTop来控制模块之间的间距?

  • 在标准文档流中,两个上下相邻的模块,如果上模块marginBttom: 50px; 下模块marginTop:50px,渲染结果这两个模块之间的上下距离是50px,这就是css的margin塌陷问题,而根据dfs计算结果,他两个模块之间是100px;
  • margin-top、margin-bottom建议替换成paddingXX。当然也可以使用margin,但要保证用BFC解决外边距重叠问题

下载pdf空白

  1. 页面有权限,在puppteer访问路由的时候被拦截了
  2. 请求是否添加 responseType: 'Blob'
  3. 走内网的时候 需要配置nginx 内网协议、内网ip、内网端口
    • x-forwarded-proto (http、https、$scheme)
    • x-forwarded-host (domain、ip:port、$http_host),如果配置成域名需要在docker容器中配置hosts解析到内网IP。

分页不对

  1. margin 塌陷影响的
  2. table分页有问题
    • 图1
    • 图2image.png
    • 图2里面有红色标记的空白属于不正常的,原因是 ”vue-easytable“这个ui框架是通过行内样式来控制table高度的,正常应该是通过table中的内容来撑起来,写在行内的问题是会导致deepClone的时候会把这个行内样式也克隆一份,原table是440,克隆table高度本来只有200多,由于行内样式设置了height: 440px,导致克隆table高度也是440px,就会有图2中的空白;
    • 处理方式:nb-fe-pdf做了处理,会将行内height重置为:auto;