作为前端工程师,npm 包是我们再熟悉不过的东西。
但是,你有没有想过,开发一个 npm 包有多容易!
这篇文章,我将通过一个小项目,带你入门开发 npm 包,源码可在 Github 查看:
load-markdwon-image
首先,我们将开发一个小工具。
这个小工具可以将 markdown 文件中的网络图片下载到本地,并替换为本地路径。
使用这个小工具,只需命令行调用:
load-md-image test.md
运行效果如图所示:
也许,你可以先安装这个 npm 包体验一下:
npm install -g load-markdwon-image
# or
yarn global add load-markdown-image
话不多说,开始吧!
新建一个项目,名字随意,并初始化项目:
mkdir load-md-image && cd load-md-image
npm init -y
在根目录下创建 index.js 文件,并保存以下代码:
#! /usr/bin/env node
console.log('hello junbin123')
修改package.json文件,其中 bin 字段表示一组命令名到对应文件名的映射,你可已自定义命令名。
{
"name": "load-md-image",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"load-md-image": "index.js"
}
}
接着,在当前目录下执行 npm link
命令,如果出现以下打印,说明已经全局安装成功了。
此时,在终端执行 load-md-image
,可以看到 index.js 文件的输出结果:
这一步,将完成项目依赖包的安装,我将解释每个依赖包的作用。
你也可以一步到位,依次执行以下命令:
yarn add axios
yarn add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser -D
由于项目比较简单,纯粹的 JS 项目,非常适合用 rollup 来打包。
在根目录下创建 rollup.config.js文件,并保存以下代码。
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'
const banner = '#!/usr/bin/env node'
export default {
input: './index.js',
output: {
exports: 'auto',
file: './dist/index.js',
format: 'cjs',
name: 'load-md-image',
banner,
},
plugins: [
commonjs(),
nodeResolve({
preferBuiltins: true,
}),
terser(),
],
}
其中入口文件是 index.js,打包后的文件是 dist/index.js。
banner 字段的作用是:自动在打包后的文件首行引入#!/usr/bin/env node
。
这行代码表示此文件可以当做脚本运行,并且在 node 运行。
此时,可以删除 index.js 文件中的#!/usr/bin/env node
。
@rollup/plugin-node-resolve:用于打包第三方模块,此项目中,第三方模块只有 axios。
@rollup/plugin-commonjs:将 commonjs 模块转化为 ES6 模块,这样才能让 rollup 进行正常解析。
rollup-plugin-terser:压缩打包后的文件。
修改package.json文件,添加脚本命令:
{
...
"main": "dist/index.js",
"bin": {
"load-md-image": "dist/index.js"
},
"scripts": {
"build": "rollup -c rollup.config.js",
"dev": "rollup -c rollup.config.js -w"
},
...
}
运行 npm run build,可以看到 dist/index.js,就是打包后的文件。
axios 用于下载网络图片,没什么好说的。
yarn add axios
自此,项目搭建完成,看看你的项目文件是否如图:
下面,可以开始业务代码的开发。
根据需求,稍微动一下脑子,我们可以拆分成以下开发步骤:
获取 markdown 文件路径
读取 markdown 文件,获取一串字符串
正则匹配出所有图片
下载图片到本地
替换图片链接为本地路径
写入 markdown 文件
具体的步骤属于常规操作,这里我不会细讲。
根据上面的分析,我们先大致梳理 index.js 的代码结构。
你可以复制以下代码,打印运行一下。
const inputPath = process.argv[2] // 获取输入的文件路径
const path = require('path')
const fs = require('fs')
const mdPath = path.resolve(inputPath) // md文件绝对路径,如:/Users/xxx/Desktop/test.md
let rootPath = '' // md文件所在目录
// 在md文件所在目录创建images文件夹,保存本地图片
try {
const { dir } = path.parse(mdPath)
rootPath = dir
fs.mkdirSync(`${rootPath}/images`)
} catch (err) {
console.log(err)
}
// 主函数
function main() {}
// 根据文件路径获取所有图片链接
function getImgsByPath(filePath) {}
// 获取本地图片路径,下载图片并保存在images目录
function getLocalImgPath(imgUrl) {}
接下来继续完善,这都没什么难度,下面是我写好的,你可以直接复制:
const inputPath = process.argv[2] // 获取输入的文件路径
const path = require('path')
const fs = require('fs')
const axios = require('axios')
const mdPath = path.resolve(inputPath) // md文件绝对路径,如:/Users/xxx/Desktop/test.md
let rootPath = '' // md文件所在目录
// 在md文件所在目录创建images文件夹,保存本地图片
try {
const { dir } = path.parse(mdPath)
rootPath = dir
fs.mkdirSync(`${rootPath}/images`)
} catch (err) {
console.log(err.code)
}
main()
// 主函数
async function main() {
let content = fs.readFileSync(mdPath, 'utf8')
const imgList = getImgsByPath(mdPath)
for (const img of imgList) {
const { imageName } = await getLocalImgPath(img)
content = content.replace(img, `./images/${imageName}`)
fs.writeFileSync(mdPath, content)
}
}
// 根据文件路径获取所有图片链接
function getImgsByPath(filePath) {
let content = fs.readFileSync(filePath, 'utf8')
const pattern = /!\[(.*?)\]\((.*?)\)/gm // 匹配图片正则
const imgList = content.match(pattern) || [] // ![img](http://hello.com/image.png)
return imgList.map((item) => {
return item.split('](')[1].slice(0, -1) // http://hello.com/image.png
})
}
// 获取本地图片路径,下载图片并保存在images目录
function getLocalImgPath(imgUrl) {
const fileType = imgUrl.split('.').slice(-1)[0] // 获取图片格式
const imageName = `${new Date().getTime()}${fileType ? `.${fileType}` : ''}`
return axios({
url: imgUrl,
responseType: 'stream',
timeout: 10000,
}).then((response) => {
return new Promise((resolve, reject) => {
const imagePath = `${rootPath}/images/${imageName}`
response.data
.pipe(fs.createWriteStream(imagePath))
.on('finish', () => resolve({ imageName, imagePath }))
.on('error', (e) => reject(e))
})
})
}
自此,基本功能完成了,相信你已经迫不及待了。
执行以下命令看看吧!
npm run build
npm link
完成后,在当前目录创建一个 test.md 文件,保存以下内容:
![baidu](https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png)
对 test.md 文件执行命令:load-md-image test.md
,看看图片是否下载了,图片路径是否替换了。
到此为止,一个非常简单的 npm 包开发完成!
但这远远不够,一个健壮的 npm 包应该是身经百战,能应付各种测试用例。
这就涉及到比较多的细节问题,感兴趣可以看看这个项目load-markdwon-image,当然 star 一下就更棒了。
在这个项目中,实现了以下的功能:
发布环节就没什么好说的,先去npm (npmjs.com)注册个账号。
然后修改 packages.json文件的 版本号(version 字段)。
接着添加用户、登录、发布到 npm 供其他人使用。
npm adduser
npm login
npm publish
其他常用的命令有
# 删除指定版本
npm unpublish [email protected] --force
ok,就是这么简单,学会了吗!