写在前面
作者在之前的文章里介绍了开源组件库从0到1的开发过程,虽然让大伙对组件库开发有了一些了解,但是受篇幅和作者文笔所限,介绍的十分笼统。饭要一口一口吃,事要脚踏实地去做。所以作者决定分章节把每个部分尽可能介绍的清楚一些。如果你是对组件库实现原理十分好奇的人,本文对你应该会有所启发。如果你想快速具备开发组件库的能力,并不想对底层实现刨根问底,你可以尝试使用 Varlet-Cli 直接开始组件库开发,本文也是围绕它展开。(说句题外话,Varlet-Cli的开发环境已经从webpack5迁移到了vite,正在进行内部测试, 如果想要尝鲜的小伙伴可以安装@varlet/cli@alpha这个包进行体验)
Varlet组件库相关链接,希望多多鼓励和支持
为什么需要开发一个CLI
一个成熟的组件往往需要经历原型设计->开发->单元测试->文档编写->文档构建->文档发布->组件库构建->组件发布这一过程。在多人协作开发中我们还要对代码风格进行约束。这里的每一个环节看似都有一个最佳实践(比如单元测试可以使用jest + @vue/test-utils)来帮我们解决它,但是如果把这些最佳实践一股脑的堆在我们的项目中,会让我们的项目变得十分累赘。尤其是成堆的配置文件会让维护成本陡然提升。所以我们需要把他们全部装箱,对外提供统一的入口去解决不同的问题。cli的优势在于可以以命令的形式提供给用户使用,一个命令解决一个问题或者一类问题,这很不错。
关于node的一些基本概念
cli毕竟是在node上进行开发的,所以需要懂得一些前置知识,这里大概列举了一些必须知道的东西。
__dirname文件所在位置process.cwd()命令执行的位置path.resolve拼接路径,生成绝对路径path.join拼接路径,生成非绝对路径requirenode引入模块的方法,动态引入,引入一次后带缓存,引入时立即执行模块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,tsxvite-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仓库的工具,虽然也就几十行代码,但是它却实实在在的帮助到了我。
希望本文能对看文章的你起到一点点的帮助吧,那样我就会很开心了。赶在国庆放假前更新一篇文章,提前祝大家国庆愉快,也希望有兴趣的朋友们,多多关注我们的开源项目,感兴趣的内容也可以留言,随缘更文。