{"id":19069530,"url":"https://github.com/srect/juejin-save","last_synced_at":"2026-05-17T04:30:15.832Z","repository":{"id":57286799,"uuid":"457719619","full_name":"sRect/juejin-save","owner":"sRect","description":"打造一个属于自己的cli","archived":false,"fork":false,"pushed_at":"2022-02-21T06:09:49.000Z","size":166,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-03T04:51:10.597Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sRect.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-02-10T09:49:32.000Z","updated_at":"2022-02-17T09:26:37.000Z","dependencies_parsed_at":"2022-09-20T00:22:11.469Z","dependency_job_id":null,"html_url":"https://github.com/sRect/juejin-save","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2Fjuejin-save","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2Fjuejin-save/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2Fjuejin-save/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sRect%2Fjuejin-save/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sRect","download_url":"https://codeload.github.com/sRect/juejin-save/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240122573,"owners_count":19751142,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T01:14:42.438Z","updated_at":"2026-05-17T04:30:15.731Z","avatar_url":"https://github.com/sRect.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 打造一个保存掘金文章的 cli\n\n### 安装\n\n```bash\nnpm i juejin-save -g\n```\n\n### 使用\n\n```\njuejin-save save https://xxx\n```\n\n### 效果\n\n![gif](./img/GIF.gif)\n\n---\n\n### 1. 主要 package version\n\n\u003e 注意：文章重点在于打造 cli，不是 puppeteer\n\n| package                                                                                      | version | 功能                               |\n| :------------------------------------------------------------------------------------------- | :------ | :--------------------------------- |\n| [commander](https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md#%E5%91%BD%E4%BB%A4) | ^9.0.0  | 创建处理命令                       |\n| [inquirer](https://github.com/SBoudrias/Inquirer.js/#objects)                                | ^8.2.0  | 处理交互                           |\n| [ora](https://www.npmjs.com/package/ora)                                                     | ^5.4.1  | 处理 loading                       |\n| [puppeteer](https://github.com/puppeteer/puppeteer)                                          | ^13.3.2 | 通过 api 来控制 Chromium 或 Chrome |\n\n### 2. 项目目录结构\n\n```\n├── bin\n|  └── cli.js          // 入口文件\n├── LICENSE\n├── package-lock.json\n├── package.json\n├── puppeteer.js       // puppeteer保存文章文件\n└── README.md\n```\n\n### 2. package.json 中添加`bin`字段\n\n\u003e 添加`juejin-save`命令，指定运行文件为 bin 目录 cli.js\n\n```diff\n+ {\n+   \"bin\": {\n+     \"juejin-save\": \"bin/cli.js\"\n+   }\n+ }\n```\n\n### 3. 主要 package api 介绍\n\n#### 3.1 commander\n\n```javascript\nconst { Command } = require(\"commander\");\nconst program = new Command();\n\nprogram\n  .command(\"clone \u003csource\u003e [destination]\")\n  .description(\"clone a repository into a newly created directory\")\n  .action((source, destination) =\u003e {\n    console.log(\"clone command called\");\n  });\n\nprogram.parse();\n```\n\n#### 3.2 inquirer\n\n```javascript\nvar inquirer = require(\"inquirer\");\ninquirer\n  .prompt([\n    /* Pass your questions in here */\n  ])\n  .then((answers) =\u003e {\n    // Use user feedback for... whatever!!\n  })\n  .catch((error) =\u003e {\n    if (error.isTtyError) {\n      // Prompt couldn't be rendered in the current environment\n    } else {\n      // Something else went wrong\n    }\n  });\n```\n\n#### 3.3 ora\n\n```javascript\nimport ora from \"ora\";\n\nconst spinner = ora(\"Loading unicorns\").start();\n\nsetTimeout(() =\u003e {\n  spinner.color = \"yellow\";\n  spinner.text = \"Loading rainbows\";\n}, 1000);\n```\n\n#### 3.4 puppeteer\n\n```javascript\nconst puppeteer = require(\"puppeteer\");\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.goto(\"https://example.com\");\n  await page.screenshot({ path: \"example.png\" });\n\n  await browser.close();\n})();\n```\n\n### 4. 主要逻辑代码\n\n1. bin/cli.js\n\n```javascript\n#!/usr/bin/env node\n\nconst inquirer = require(\"inquirer\");\nconst ora = require(\"ora\");\nconst { Command } = require(\"commander\");\nconst { puppeteerInit, saveToHtml, saveToMd, saveToPdf } = require(path.resolve(\n  __dirname,\n  \"../puppeteer\"\n));\n\nconst program = new Command();\nconst spinner = ora();\n\n// 交互式询问\nasync function handlePrompt() {\n  return await inquirer.prompt([\n    {\n      name: \"autoCreateFolder\",\n      message: `Automatically create folders?`,\n      type: \"confirm\",\n    },\n    //...\n  ]);\n}\n\n// 询问过后的处理,开始puppeteer初始化\nasync function AfterePrompt(articleUrl, answers) {\n  spinner.color = \"yellow\";\n  spinner.start(\"puppeteer intial...\");\n\n  const obj = await puppeteerInit(articleUrl, answers);\n\n  spinner.stopAndPersist({\n    symbol: chalk.green(\"✓\"),\n    text: chalk.green(\"puppeteer init ok\"),\n  });\n\n  return obj;\n}\n\n// 导出文件\nasync function exportFile(arg) {\n  const { page, outMdFilePath, outPdfFilePath, outHtmlfFilePath } = arg;\n  await saveToMd(page, outMdFilePath);\n  await saveToPdf(page, outPdfFilePath);\n  await saveToHtml(page, outHtmlfFilePath);\n}\n\n// 第一步：创建命令\nprogram\n  .version(require(path.resolve(__dirname, \"../package.json\")).version)\n  .command(\"save  \u003carticle-url\u003e\")\n  .description(\"save https://xxx\")\n  .action(async (articleUrl) =\u003e {\n    // 第二步：交互式询问\n    const answers = await handlePrompt(articleUrl);\n    // 第三步：拿到交互结果\n    const data = await AfterePrompt(articleUrl, answers);\n    // 第四步：导出文件\n    await exportFile(data);\n\n    process.exit(1);\n  });\n\nprogram.parse();\n```\n\n2. puppeteer.js\n\n```javascript\nconst puppeteer = require(\"puppeteer\");\n\n// 保存html\nasync function saveToHtml(page, outHtmlfFilePath) {\n  // ...\n}\n\n// 保存markdown\nasync function saveToMd(page, outMdFilePath) {\n  // ...\n}\n\n// 保存pdf\nasync function saveToPdf(page, outPdfFilePath) {\n  // ...\n}\n\n// puppeteer初始化\nasync function puppeteerInit(href) {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  page.setViewport({\n    width: 1920,\n    height: 1080,\n  });\n\n  await page.goto(href, {\n    waitUntil: \"domcontentloaded\",\n    referer: href,\n  });\n  await page.waitForTimeout(3000); // 确保页面加载完毕\n\n  return {\n    browser,\n    page,\n  };\n}\n\nmodule.exports = {\n  puppeteerInit,\n  saveToHtml,\n  saveToMd,\n  saveToPdf,\n};\n```\n\n### 5. 本地测试\n\n1. 在项目根目录执行\n\n```bash\nnpm link\n```\n\n执行完之后，成功提示：\n\n```\nadded 1 package, and audited 3 packages in 1s\n\nfound 0 vulnerabilities\n```\n\n也可以在本机的 npm 全局安装里找到一个软链接，如图：\n\n![](./img/cli.jpg)\n\n2. 在任意目录打开命令行，执行\n\n```bash\njuejin-save save  https://juejin.cn/post/xxxx\n```\n\n不出意外，可以看到，多出了一个文件夹，文章被保存在文件夹里面了。\n\n### 6. 参考资料\n\n1. [手写一个合格的前端脚手架](https://mp.weixin.qq.com/s/AH9fQdZnwMUcuczIVLOLVQ)\n\n2. [实现 CLI 常用工具包 - 终端交互相关](https://mp.weixin.qq.com/s/1jzwybwyH80uDzfvvmDe_Q)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrect%2Fjuejin-save","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsrect%2Fjuejin-save","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrect%2Fjuejin-save/lists"}