寫在前面
經(jīng)常的寫作的人都有備份的好習(xí)慣,為了防止自己的文章丟失,簡書提供了下載所有文章
功能嘲恍,可以讓作者將文章下載到本地保存,或者上傳到自己的站點(diǎn)例驹。
但是簡書的圖片是放在專門的圖片服務(wù)器上的,下載所有文章并不包含文章中的所有圖片职抡。所以我們現(xiàn)在寫個(gè)小工具,通過命令行的方式將文章中的所有圖片下載本地保存误甚。
需求實(shí)現(xiàn)步驟
- 下載簡書文章缚甩,解壓到 A 目錄;
- 建一個(gè) TypeScript + Node 項(xiàng)目窑邦,讀取 A 目錄中的所有 .md 文件擅威;
- 提取文件內(nèi)容中的圖片鏈接,下載下來冈钦;
- 把下載的圖片放到 B 目錄/當(dāng)前文章/ 中郊丛,用來分類;
- 重構(gòu)優(yōu)化代碼瞧筛;
下面按照這幾個(gè)步驟一步步完成簡書下載圖片工具厉熟。
下載簡書文章
進(jìn)入我的簡書 ->賬號(hào)管理 打包下載全部的簡書文章
即可,我是下載到了這個(gè)目錄 D:\jianshu_article\user-5541401-1565071963
较幌,這個(gè)目錄下的所有文件都是文集/文章
的格式揍瑟。接下來開始搭建項(xiàng)目結(jié)構(gòu)。
TypeScript Node 搭建項(xiàng)目
先在 github 上新建一個(gè)倉庫乍炉,然后 clone 下來月培。開發(fā)工作一直在 master 分支上嘁字,然后每完成一步需求,新建一個(gè)分支用來保留記錄杉畜,以后看的時(shí)候更清晰。
新建一個(gè)倉庫然后 clone 下來:
git clone git@github.com:mxcz213/download-jianshu-images.git
開始項(xiàng)目搭建:
- 生成 package.json 文件衷恭;
npm init -f
- 下載項(xiàng)目依賴 :typescript node 的ts 版本此叠,download下載文件包,runscript 用來執(zhí)行 shell 命令随珠,ts-node 用來開發(fā)調(diào)試灭袁;
npm install @types/node download runscript ts-node typescript --save-dev
- 配置 tsconfig 文件,用來按照這個(gè)規(guī)則編譯 ts 文件為 js 文件窗看。執(zhí)行命令
tsc --init
茸歧,自動(dòng)生成tsconfig.json
文件;
//tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist/",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
- 配置 package.json 文件的 scripts 字段显沈,啟動(dòng)項(xiàng)目和編譯命令
{
"name": "download-jianshu-images",
"version": "1.0.0",
"description": "Node + typescript 實(shí)現(xiàn)下載簡書文章中所有的圖片鏈接",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
},
"repository": {
"type": "git",
"url": "git+https://github.com/mxcz213/download-jianshu-images.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/mxcz213/download-jianshu-images/issues"
},
"homepage": "https://github.com/mxcz213/download-jianshu-images#readme",
"devDependencies": {
"@types/node": "^12.6.9",
"download": "^7.1.0",
"runscript": "^1.4.0",
"ts-node": "^8.3.0",
"typescript": "^3.5.3"
}
}
- 配置 vscode 的調(diào)試腳本 launch.json
//.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Current TS File",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/ts-node/dist/bin.js",
// "program": "${workspaceRoot}/test.js",
"args": [
"${relativeFile}"
],
"cwd": "${workspaceRoot}",
"protocol": "inspector"
}
]
}
- 添加 .gitignore 文件配置忽略提交的目錄
//.gitignore
/node_modules
- 新建 dist 目錄用來放編譯之后的 js 文件
- 新建 src 源代碼文件目錄
具體代碼實(shí)現(xiàn)软瞎,新建 src/index.ts 文件
//src index.ts
const fs = require('fs')
const path = require('path')
const runScript = require('runscript')
const download = require('download')
//windows中用戶復(fù)制的目錄
let originDir: string = 'D:\\jianshu_article\\user-5541401-1565071963\\'
let targetDir: string = 'E:\\workCode\\download-jianshu-images\\jianshu_article\\'
const readmeUrlReg: RegExp = /\s!\[\]\(https:\/\/\upload-images.jianshu.io\/upload_images\/[a-zA-Z0-9-_?%./]+\)\s/g
const imageUrlReg: RegExp = /https:\/\/\upload-images.jianshu.io\/upload_images\/[a-zA-Z0-9-_?%./]+/g
//用戶通過命令行工具輸入命令比如:node dist/index.js 簡書解壓目錄 目標(biāo)存儲(chǔ)圖片目錄
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`)
});
try {
originDir = process.argv[2] ? process.argv[2] : originDir
targetDir = process.argv[3] ? process.argv[3] : targetDir
} catch(e) {
console.log('獲取命令參數(shù)錯(cuò)誤', e)
}
const downloadImages = (imgurl: string[], path: string) => {
let newUrlArr: any[] = []
imgurl.forEach((item: any) => {
if(item.match(imageUrlReg)){
newUrlArr.push(item.match(imageUrlReg)[0])
}
})
console.log(newUrlArr)
Promise.all(newUrlArr.map((url: string) => {
download(url, path)
})).then(() => {
console.log('all files downloaded')
})
}
const runFunction = async () => {
//shell ls拿到所有的.md文章
const { stdout } = await runScript('ls **/*.md', {
cwd: originDir,
stdio: 'pipe'
})
let files: string[] = stdout.toString().split('\n')
let num: number = 0
try {
files.forEach((fileitem: any, index: number) => {
if(fileitem){
let filepath: string = fileitem.split('.md')[0].split('/').join('\\')
let dirStr: string = `${targetDir}\\${filepath}`
runScript(`mkdir ${dirStr}`, { stdio: 'pipe' })
.then((stdio: any) => {
let fileContent = fs.readFileSync(path.join(originDir, fileitem.split('/').join('\\')), { encoding: 'utf8'})
let urlList: any = fileContent.match(readmeUrlReg)
if(urlList && urlList.length > 0){
downloadImages(urlList, dirStr)
}
})
}
})
} catch(e) {
console.log(e)
}
}
runFunction()
- 執(zhí)行命令
npm run build
編譯 ts 文件 - 執(zhí)行命令
node . D:\jianshu_article\user-5541401-1565071963 D:\jianshu_article\article_img
,下載圖片
node .
命令會(huì)到 package.json 文件中找到 main 字段執(zhí)行入口文件拉讯。
process.argv
會(huì)獲取到命令行參數(shù)涤浇。
接下來提交文件到 master 分支:
git add .
git commit -m "download jianshu images"
git push
然后根據(jù) master 新建一個(gè)分支,用來保存這次的提交歷史:
git checkout -b node_tool
git pull origin master
git push
實(shí)現(xiàn)工具命令魔慷,如 jianshu ...
配置命令行只锭,通過 package.json 文件的 bin 字段,然后新建 bin 目錄院尔,在 bin 目錄下新建 jianshu 文件蜻展;
//package.json
{
...
"bin": {
"jianshu": "bin/jianshu"
}
...
}
//bin/jianshu
#!/usr/bin/env node
require('../dist/index');
配置完就可以通過命令 jianshu D:\jianshu_article\user-5541401-1565071963 D:\jianshu_article\article_img
實(shí)現(xiàn)下載圖片。
通過const [, , sourceDir, targetDir] = process.argv;
來獲取命令行參數(shù)邀摆。
提交代碼之后纵顾,這一步同樣新建 node_cli 分支用來保存歷史:
git checkout -b node_cli
git pull origin master
git pull
代碼重構(gòu)優(yōu)化
上面的代碼只是實(shí)現(xiàn)的簡單的功能,流程并不清晰隧熙,現(xiàn)在來重構(gòu)代碼片挂,使主流程變的清晰。
代碼重構(gòu)的原則:主流程要清晰
每個(gè)函數(shù)只做一件事贞盯,有兩個(gè)以上的函數(shù)音念,有內(nèi)部函數(shù)式,就要考慮把這每個(gè)函數(shù)放到單獨(dú)的文件里躏敢,然后用模塊導(dǎo)入的方式闷愤。
以上代碼展現(xiàn)的問題:
- handleDir getArticleContent 重復(fù)判斷平臺(tái)和路徑,沒有把判斷平臺(tái)提出來
- getMarkdownImageUrls getRealImageUrl 重復(fù)使用相似的正則件余,沒使用exec和正則的捕獲組
- 小函數(shù)嵌套太嚴(yán)重讥脐,一個(gè)函數(shù)能搞定的
- 沒有異常判斷遭居,沒有l(wèi)og
- 邏輯層次不清晰,分了好多層
- 關(guān)鍵注釋缺失旬渠,例如files這個(gè)是相對(duì)路徑的列表俱萍,不注明的話,以后肯定不知道
所以接下來就要重構(gòu)這些代碼告丢,主要根據(jù)以下分類原則來實(shí)現(xiàn)模塊的拆分:
分類原則
哪些是項(xiàng)目獨(dú)有的邏輯(業(yè)務(wù)邏輯)枪蘑,
哪些是通用邏輯(可復(fù)用的),
哪些是模板代碼(沒啥用但是要寫的)
根據(jù)以上原則岖免,拆分出來工具函數(shù) log岳颇,文件操作;核心函數(shù) libs颅湘。
//src/utils/log.ts
const log = (str: string) => {
console.log(str);
}
const error = (str: string) => {
console.error(str);
}
const warn = (str: string) => {
console.warn(str);
}
export {
log,
error,
warn
}
//src/utils/fs.ts
const fs = require('fs');
const runScript = require('runscript');
const download = require('download');
const read = (path: string, options?: {}) => {
let fileContent = fs.readFileSync(path, options);
return fileContent;
}
const createDir = async (targetDir: string) => {
await runScript(`mkdir ${targetDir}`);
}
const deleteDir = async (targetDir: string) => {
await runScript(`rd /s/q ${targetDir}`);
}
const isExistDir = (targetDir: string): boolean => {
return fs.existsSync(targetDir);
}
const downloadFile = async(url: string, targetDir: string) => {
await download(url, targetDir);
}
export {
read,
createDir,
deleteDir,
isExistDir,
downloadFile
}
//src/libs/lib.ts
const runScript = require('runscript');
import { log } from '../utils/log';
//sourceDir:簡書文章目錄
export const getAllMarkdownFiles = async (sourceDir: string) => {
//ls **/*.md 查詢二級(jí)目錄下的所有.md后綴的文件
//stdio: pipe 在父進(jìn)程和子進(jìn)程之間建立管道
const { stdout } = await runScript('ls **/*.md', {
cwd: sourceDir,
stdio: 'pipe'
});
const files: string[] = stdout.toString().split('\n');
//去掉ls命令產(chǎn)生的尾部空行
files.pop();
log('獲取所有的簡書文章列表话侧;');
return files;
}
//獲取圖片url的markdown寫法![](https://....)
export const getMarkdownImageUrls = (fileContent: string) => {
const urlRegExp = /\!\[.*\]\((https?:\/\/.+?)\)/g;
const imageUrls: string[] = [];
while(true) {
const match = urlRegExp.exec(fileContent);
if(match === null) {
break;
}
const [, url] = match;
imageUrls.push(url);
}
return imageUrls;
}
主入口函數(shù):
//src/index.ts
import path from 'path';
import { log } from './utils/log';
import { read, createDir, deleteDir, isExistDir, downloadFile } from './utils/fs';
import { getAllMarkdownFiles, getMarkdownImageUrls } from './libs/lib';
//入口函數(shù)
const main = async () => {
//平臺(tái)判斷
const { platform } = process;
const isWindows: boolean = platform === 'win32';
//獲取命令行參數(shù)
const [, , sourceDir, targetDir] = process.argv;
//獲取markdown文件列表
const files: string[] = await getAllMarkdownFiles(sourceDir);
//下載文件列表中每個(gè)文章的圖片
for(const file of files){
// file 是相對(duì)路徑 例如:"2017-2018/前端模塊化總結(jié).md"
// 兼容 windows 系統(tǒng)路徑規(guī)則
let platFile: string = isWindows ? `${file.split('.md')[0].split('/').join('\\')}.md` : file;
const filepath: string = platFile.split('.md')[0];
//讀取文件內(nèi)容
const filecontent = read(path.join(sourceDir, platFile), { encoding: 'utf8'});
//根據(jù) md 文件名,創(chuàng)建目標(biāo)文件夾闯参,如果目標(biāo)文件夾存在瞻鹏,則刪除重建
const newTargetDir: string = path.join(targetDir, filepath);
if(isExistDir(newTargetDir)){
await deleteDir(newTargetDir);
}
await createDir(newTargetDir);
//找出圖片,下載圖片到目標(biāo)目錄
const urlList: string[] = getMarkdownImageUrls(filecontent);
for(const url of urlList){
await downloadFile(url, newTargetDir);
}
}
log('所有文章中的圖片已下載成功赢赊!');
}
main();
提交代碼到 master 分支乙漓,然后新建 node-cli-refactory 分支用來保存重構(gòu)歷史。
git checkout -b node-cli-refactory
git pull origin master
git push
最后這個(gè)下載圖片的小工具就做好释移。
總結(jié):在寫代碼的過程中叭披,一定要分析什么是通用工具類,什么是獨(dú)有的業(yè)務(wù)邏輯類玩讳,該模塊化的模塊化涩蜘,目的只有一個(gè)就是:主流程要清晰。
項(xiàng)目地址:https://github.com/mxcz213/download-jianshu-images
參考:
https://www.npmjs.com/package/runscript
https://www.npmjs.com/package/download
https://www.npmjs.cn/files/package.json/