Bootstrap

如何开发一个 npm 包?

作为前端工程师,npm 包是我们再熟悉不过的东西。

但是,你有没有想过,开发一个 npm 包有多容易!

这篇文章,我将通过一个小项目,带你入门开发 npm 包,源码可在 Github 查看:

load-markdwon-image

首先,我们将开发一个小工具。

这个小工具可以将 markdown 文件中的网络图片下载到本地,并替换为本地路径。

使用这个小工具,只需命令行调用:

load-md-image test.md

运行效果如图所示:

load-markdown-image

也许,你可以先安装这个 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 命令,如果出现以下打印,说明已经全局安装成功了。

image-20220221222521492.png

此时,在终端执行 load-md-image,可以看到 index.js 文件的输出结果:

image-20220221222356236.png

二、项目配置

这一步,将完成项目依赖包的安装,我将解释每个依赖包的作用。

你也可以一步到位,依次执行以下命令:

yarn add axios
yarn add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser -D

安装 rollup

由于项目比较简单,纯粹的 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,就是打包后的文件。

image-20220222223650089.png

安装 axios

axios 用于下载网络图片,没什么好说的。

yarn add axios

自此,项目搭建完成,看看你的项目文件是否如图:

image-20220222223936183.png

下面,可以开始业务代码的开发。

三、业务代码开发

根据需求,稍微动一下脑子,我们可以拆分成以下开发步骤:

  1. 获取 markdown 文件路径

  2. 读取 markdown 文件,获取一串字符串

  3. 正则匹配出所有图片

  4. 下载图片到本地

  5. 替换图片链接为本地路径

  6. 写入 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,看看图片是否下载了,图片路径是否替换了。

image-20220222230120856.png

到此为止,一个非常简单的 npm 包开发完成!

但这远远不够,一个健壮的 npm 包应该是身经百战,能应付各种测试用例。

这就涉及到比较多的细节问题,感兴趣可以看看这个项目load-markdwon-image,当然 star 一下就更棒了。

在这个项目中,实现了以下的功能:

  • 自动识别图片格式;
  • 友好的打印提示;
  • 支持处理文件夹所有 md 文件;
  • 支持绝对路径、相对路径;

四、发布 npm

image-20220222233329290.png

发布环节就没什么好说的,先去npm (npmjs.com)注册个账号。

然后修改 packages.json文件的 版本号(version 字段)。

接着添加用户、登录、发布到 npm 供其他人使用。

npm adduser
npm login
npm publish

其他常用的命令有

# 删除指定版本
npm unpublish [email protected] --force

ok,就是这么简单,学会了吗!

;