组件库设计 | 如何开发一个组件库CLI

8,217 阅读8分钟

写在前面

作者在之前的文章里介绍了开源组件库从0到1的开发过程,虽然让大伙对组件库开发有了一些了解,但是受篇幅和作者文笔所限,介绍的十分笼统。饭要一口一口吃,事要脚踏实地去做。所以作者决定分章节把每个部分尽可能介绍的清楚一些。如果你是对组件库实现原理十分好奇的人,本文对你应该会有所启发。如果你想快速具备开发组件库的能力,并不想对底层实现刨根问底,你可以尝试使用 Varlet-Cli 直接开始组件库开发,本文也是围绕它展开。(说句题外话,Varlet-Cli的开发环境已经从webpack5迁移到了vite,正在进行内部测试, 如果想要尝鲜的小伙伴可以安装@varlet/cli@alpha这个包进行体验)

Varlet组件库相关链接,希望多多鼓励和支持

Github仓库
中文文档
英文文档

为什么需要开发一个CLI

一个成熟的组件往往需要经历原型设计->开发->单元测试->文档编写->文档构建->文档发布->组件库构建->组件发布这一过程。在多人协作开发中我们还要对代码风格进行约束。这里的每一个环节看似都有一个最佳实践(比如单元测试可以使用jest + @vue/test-utils)来帮我们解决它,但是如果把这些最佳实践一股脑的堆在我们的项目中,会让我们的项目变得十分累赘。尤其是成堆的配置文件会让维护成本陡然提升。所以我们需要把他们全部装箱,对外提供统一的入口去解决不同的问题。cli的优势在于可以以命令的形式提供给用户使用,一个命令解决一个问题或者一类问题,这很不错。

关于node的一些基本概念

cli毕竟是在node上进行开发的,所以需要懂得一些前置知识,这里大概列举了一些必须知道的东西。

  • __dirname 文件所在位置
  • process.cwd() 命令执行的位置
  • path.resolve 拼接路径,生成绝对路径
  • path.join 拼接路径,生成非绝对路径
  • require node引入模块的方法,动态引入,引入一次后带缓存,引入时立即执行模块
  • require.resolve返回模块的文件位置,不执行文件
  • Windows和其他平台的文件路径是不一样的,需要适配

工具

开发cli也有一些好用的工具,这里为大家介绍一些作者最常用的工具。

  • commander 用于解析命令和参数,可以给不同的命令配置不同的处理函数,函数会接收到这个伴随命令传入的参数,可以说是非常好用了。
  • execa 用于调用其他的命令行工具,比如你安装了一个第三方的命令行工具,你可以用execa在程序中调用它让它工作。
  • fs-extra promise版本的fs模块,并且提供了很多工具函数,做文件操作必备。
  • ora 控制台的loading效果
  • inquirer 可以在控制台和用户一问一答,交互式的处理。
  • chalk 可以给控制台输出的文字加样式,改改颜色什么的。
  • hash-sum 可以生成hash
  • slash 用于转换Windows反斜杠路径转换为正斜杠路径 \ -> /

以上就是最最最最经常用到的几个包,具体的api,这里就不展开介绍了,感觉没什么营养

实现思路

这里只分享实现思路,并提供一些经过作者删减过的部分代码(不保证能运行,只是为了帮助大家理解,实际代码请查看 Github仓库 ),以下是咱们要介绍的三个主要命令

  • dev 启动开发服务器,启动之后就是文档站点,在文档实时预览
  • build 文档站点打包
  • compile 组件库代码打包

dev

我们需要启动一个开发服务器,来做组件的调试,我们同时也需要一个文档站点去介绍我们的组件,所以我们只需要将文档站点作为组件的调试环境就ok了 。这里我们选择vite作为开发服务器,首先我们需要用我们的前端技术写一个站点出来(这个自由发挥),里面留好路由视图方便之后渲染我们的组件文档。文档的编写我们通过markdown去编译成vue文件,这里通过@varlet/markdown-vite-plugin来实现该功能。在服务启动时我们需要根据用户的src目录去动态生成路由,并配置成alias,从而可以在站点中使用别名访问到路由配置挂载路由。

vite的相关配置可以查看packages/varlet-cli/src/config/vite.config.ts

以下是配置使用到的vite插件,仅供参考

  • @vitejs/plugin-vue 处理vue文件
  • @vitejs/plugin-vue-jsx 处理jsx,tsx
  • vite-plugin-html 对html文件做注入
  • @varlet/markdown-vite-plugin 把markdown文件编译成vue
import { createServer } from 'vite'
import { ensureDirSync } from 'fs-extra'
import { SRC_DIR } from '../shared/constant'
import { buildSiteEntry } from '../compiler/compileSiteEntry'
import { getDevConfig } from '../config/vite.config'
import { getVarletConfig } from '../config/varlet.config'
import { merge } from 'lodash'

export async function dev(cmd: { force?: boolean }) {
  // 设置环境
  process.env.NODE_ENV = 'development'
  // 确保src目录存在
  ensureDirSync(SRC_DIR)
  // 遍历src目录生成路由配置
  await buildSiteEntry()
  // 获取配置好的vite服务器配置
  const devConfig = getDevConfig(getVarletConfig())
  // 合并vite的强制刷新依赖选项
  const inlineConfig = merge(devConfig, cmd.force ? { server: { force: true } } : {})
  // 创建vite服务器实例
  const server = await createServer(inlineConfig)
  // 启动服务器
  await server.listen()
}

build

运行站点打包, 由于使用的vite, 这里推荐使用其自带的rollup进行打包, 流程代码如下。

import { ensureDirSync } from 'fs-extra'
import { SRC_DIR } from '../shared/constant'
import { build as buildVite } from 'vite'
import { getBuildConfig } from '../config/vite.config'
import { getVarletConfig } from '../config/varlet.config'
import { buildSiteEntry } from '../compiler/compileSiteEntry'

export async function build() {
  // 设置环境
  process.env.NODE_ENV = 'production'
  // 确保src目录存在
  ensureDirSync(SRC_DIR)
  // 遍历src目录生成路由配置
  await buildSiteEntry()
  // 获取vite的构建配置
  const varletConfig = getVarletConfig()
  const buildConfig = getBuildConfig(varletConfig)
  // 开始打包
  await buildVite(buildConfig)
}

compile

组件库打包也可以直接使用rollup的库模式进行打包,但是作者更推荐自行实现一个打包器,因为他可以足够轻量,打包速度也快,重要的是你可以自己掌控整个打包流程,很值得动手实现一下。

假设我们需要生成的文件夹为es,我们只需要把src目录先拷贝到es,然后对es中所有文件进行递归遍历,对不同的文件使用对应的包进行编译生成文件即可,用到的包如下

  • .vue -> @vue/compiler-sfc
  • .less -> less
  • .js、.ts、.jsx、.tsx -> @babel/core

需要花时间的反而是学习这些包的具体使用方法,不过也都是api层面的东西,难度是不大的,只要愿意花些时间,收获的结果是很好的。

流程代码如下

import { EXAMPLE_DIR_NAME, TESTS_DIR_NAME, DOCS_DIR_NAME, SRC_DIR, ES_DIR, STYLE_DIR_NAME } from '../shared/constant'
import { isDir, isLess, isScript, isSFC } from '../shared/fsUtils'
import { compileSFC } from './compileSFC'
import { compileScriptFile } from './compileScript'
import { compileLess } from './compileStyle'
import { copy, ensureFileSync, readdir, removeSync } from 'fs-extra'

// 处理文件, 什么类型的文件用对应的包进行处理
export async function compileFile(file: string) {
  isSFC(file) && (await compileSFC(file))
  isScript(file) && (await compileScriptFile(file))
  isLess(file) && (await compileLess(file))
  isDir(file) && (await compileDir(file))
}

// 处理目录
export async function compileDir(dir: string) {
  const dirs = await readdir(dir)

  await Promise.all(
    dirs.map((filename) => {
      const file = resolve(dir, filename)

      // 删除不需要编译的文件夹
      ;[TESTS_DIR_NAME, EXAMPLE_DIR_NAME, DOCS_DIR_NAME].includes(filename) && removeSync(file)

      // 跳过编译一些文件夹
      if (filename === STYLE_DIR_NAME) {
        return Promise.resolve()
      }

      // 剩下的进行编译
      return compileFile(file)
    })
  )
}

export async function compile() {
  // 设置环境
  process.env.NODE_ENV = 'compile'
  // 确保src目录存在
  ensureDirSync(SRC_DIR)
  // 拷贝源码包到目标位置, 为了源码安全. src -> es
  await copy(SRC_DIR, ES_DIR)
  // 读es文件夹
  const moduleDir: string[] = await readdir(ES_DIR)
  // 递归处理文件
  await Promise.all(
    moduleDir.map((filename: string) => {
      const file: string = resolve(ES_DIR, filename)

      return isDir(file) ? compileDir(file) : null
    })
  )
}    

编写入口

以上完成了三个命令的伪代码,然后我们在入口文件里注册

#!/usr/bin/env node 这行标记是表示这是一个可执行文件, 不可缺少
import { parse, command } from 'commander'
import { dev } from './commands/dev'
import { build } from './commands/build'
import { compile } from './commands/compile'

command('dev')
  .option('-f --force', 'Force dep pre-optimization regardless of whether deps have changed')
  .description('Run varlet development environment')
  .action(dev)

command('build')
  .description('Build varlet site for production')
  .action(build)

command('compile')
  .description('Compile varlet components library code')
  .action(compile)
  
parse()

到此为止一个简易的组件库命令行就完成了,当然实际上的版本比这个复杂很多,不过作为学习和了解,但求神似,不求形似。

写在最后

对于Varlet这个库来说,cli部分也是重写次数最多的部分了。从最早的webpack4 -> webpack5 -> vite, 这个过程也是越来越容易,获得的开发体验感也是越来越好了,维护压力也越来越小了。大家空闲之余也可以动动手,尝试制作自己的命令行工具(不一定是面向组件库),其实是很有乐趣的。我记得我第一个做的命令行是一个交互式拉取git仓库的工具,虽然也就几十行代码,但是它却实实在在的帮助到了我。 希望本文能对看文章的你起到一点点的帮助吧,那样我就会很开心了。赶在国庆放假前更新一篇文章,提前祝大家国庆愉快,也希望有兴趣的朋友们,多多关注我们的开源项目,感兴趣的内容也可以留言,随缘更文。