Skip to content

vitepress-export-pdf

介绍

vitepress-export-pdf是什么?

vitepress-export-pdf allows you to export your sites to a PDF file.

Installation

sh
npm install vitepress-export-pdf -D

then add script to your package.json:

json
{
  "scripts": {
    "export-pdf": "press-export-pdf export [path/to/your/docs]"
  }
}

Then run:

sh
npm run export-pdf

原理

研究原理之前先clone源码:

sh
git clone git@github.com:ZhongxuYang/vitepress-export-pdf.git

Installation中的package.json可以知道,vitepress-export-pdf通过cli的方式启用。

可以在package.json中通过以下思路找到源代码:

package.json

json
"bin": {
  "press-export-pdf": "bin/press-export-pdf.mjs"
}

bin/press-export-pdf.mjs

js
import '../dist/press-export-pdf.mjs'

package.json

json
"scripts": {
  "build": "unbuild"
}

build.config.ts

js
entries: [
  ...fg.sync('src/commands/*.ts').map(i => i.slice(0, -3)),
]

src/commands/press-export-pdf.ts

ts
import { afterParse, beforeParse, runCli } from '../runner'

runCli(
  'press-export-pdf',
  // 在该方法里:调用registerCommands注册主函数
  beforeParse,
  // 在该方法里:检查cli入参,如不正确打印help log
  afterParse,
)

src/runner.ts

ts
import type { CAC } from 'cac'
import cac from 'cac'
import pkg from '../package.json'
import { registerCommands } from './registerCommands'

type InterfaceParse = (cliInstance: CAC) => void

export const beforeParse = (cliInstance: CAC) => {
  registerCommands(cliInstance)

  // display cli version, display help message
  cliInstance.version(pkg.version).help()
}

export const afterParse = (cliInstance: CAC) => {
  if (!process.argv.slice(2).filter(Boolean).length)
    cliInstance.outputHelp()
}

/**
 * Parse CLI.
 * @param programName - Name of program
 * @param beforeParse - Function to run before parse
 * @param afterParse - Function to run after parse
 */
export const runCli = (programName: string, beforeParse: InterfaceParse, afterParse: InterfaceParse) => {
  try {
    // create cac instance
    const program = cac(programName)
    beforeParse && beforeParse(program)
    console.log(process.argv)
    program.parse(process.argv)
    afterParse && afterParse(program)
  }
  catch (error) {
    process.exit(1)
  }
}

src/registerCommands.ts

ts
export const registerCommands = (program: CAC) => {
  // register `export` command
  program
    .command('export [sourceDir]', 'Export current VitePress site to a PDF file(default: docs)')
    .allowUnknownOptions()
    .option('-c, --config <config>', 'Set path to config file')
    .option('--outFile <outFile>', 'Name of output file')
    .option('--outDir <outDir>', 'Directory of output files')
    .option('--debug', 'Enable debug mode')
    .action(wrapCommand(serverApp))

  // register `info` command
  program
    .command('info', 'Display environment information')
    .action(wrapCommand(systemInfo))
}

上面的serverApp就是主要函数了,我们来看一下它的实现:

ts
import { createServer as createDevApp } from 'vitepress'
import pkg from '../package.json'

export const serverApp = async (dir = 'docs', commandOptions) => {
  
  // ...省略处理参数的代码 

  const {
    sorter,
    puppeteerLaunchOptions,
    pdfOptions,
    outFile = vitepressOutFile,
    outDir = vitepressOutDir,
    routePatterns,
    enhanceApp,
  } = userConfig

  // 调用vitepress的createServer启动服务
  const devServer = await createDevApp(sourceDir, {})

  const port = Number(devServer.config.preview.host)
  const servePort = Number.isFinite(port) ? port : 16762
  const host = devServer.config.preview.host
  const serveHost = typeof host === 'string' ? host : 'localhost'

  const devApp = await devServer.listen(servePort)
  devApp.printUrls()

  logger.log('\n')
  logger.tip('Start to generate current site to PDF ...\n')

  try {
    // 开始生成PDF
    await generatePdf({
      root: devApp.config.root,
      port: servePort,
      host: serveHost,
      outFile,
      outDir,
      sorter,
      puppeteerLaunchOptions,
      pdfOptions,
      routePatterns,
      enhanceApp,
    })
  }
  catch (error) {
    logger.error(error)
  }
  // 首先关闭vitepress的服务
  await devApp.close()
  // 退出当前程序
  process.exit(0)
}

下面是生成PDF的方法:

src/utils/generatePdf.ts

ts
export const generatePdf = async ({
  root,
  port,
  host,
  sorter,
  outFile,
  outDir,
  puppeteerLaunchOptions,
  pdfOptions,
  routePatterns,
  enhanceApp,
}: IGeneratePdfOptions) => {
  
  // ...

  // 记录所有的页面path
  let exportPages = filterRoute(hashPages, routePatterns)

  // 创建浏览器
  const browser = await puppeteer.launch(puppeteerLaunchOptions)

  // 循环访问所有页面
  for (const { location, pagePath, url } of normalizePages) {

    await browserPage.goto(
      location,
      { waitUntil: 'networkidle2' },
    )

    // 生成pdf
    await browserPage.pdf({
      path: pagePath,
      format: 'A4',
      ...pdfOptions,
    })

    const title = await browserPage.title()
    browserPage.close()

  }

  singleBar.stop()
  await browser.close()

  await mergePDF(normalizePages, outFile, outDir)

  fs.removeSync(tempPdfDir)
  !fs.readdirSync(tempDir).length && fs.removeSync(tempDir)
}

释译

Dependencies

Dependencies
作用
enso用来直接运行ts文件,可以在dev环境不经过打包运行
cacCommand And Conquer是一个用于构建 CLI 应用程序的 JavaScript 库。
playwright模拟浏览器的渲染效果,抓取控制台信息等信息。支持Chorme,Firefox,Opera,Safari,IE
picocolors打印带样式的log
debug只有在携带指定参数时才打印对应的log,对于开发控制台工具类很有用
puppeteerPuppeteer 是一个 Node 库,它提供了一个高级 API 来通过DevTools 协议控制 Chrome 或 Chromium 。在浏览器中可以做的大多数操作都可以使用Puppeteer完成

Code

process.exit()

在node程序中遇到process.exit时

  1. 如果传入1代表程序执行失败
  2. 如果传入0代表程序执行成功

process.argv

cli传入的参数。

  • process.argv[0]: Node安装路径
  • process.argv[1]: 当前Cli执行路径
  • process.argv[2...]: 传入的参数

后言

其实我们可以发现,通过vitepress-export-pdf导出的文件与通过浏览器的保存为PDF文件本质上是一样的。使用vitepress-export-pdf导出的唯一不同点在于,会把所有的页面集合到一个pdf文件中,而直接通过浏览器保存的pdf只有当前页面。😊