浅谈 vue-cli 的插件设计
这是一个新开的’实验性’文章系列,如其名‘技术地图’,这个系列计划剖析一些前端开源项目,可能会探讨这些项目的设计和组织、整理他们使用到技术栈。 首先拿vue-cli
小试牛刀,再决定后续要不要继续这个系列.
我一直在思考我们编程主要在做什么?我们有一大部分工作就是选择各种工具/库/框架,来黏合业务. 工具和场景越匹配、原理了解越多,运用越娴熟,我们效率可能就越高. 这种说法很有争议,就像程序=算法+数据结构
不能完全表达现今的软件工程一样, 说我们的工作就是堆砌工具,黏合业务, 一定程度上有自贬的意思。 但这确实是大部分程序员的真实写照。
这系列文章其实有点类似于 github 上面的Awesome
项目. 这些 Awesome 项目就是一个生态展览馆, 里面项目琳琅满目. 因为数量太多了,而且缺少评分机制,大部分情况我们不可能一个个去查看,很难从中选择符合需求的项目(当然你带着明确的目的,且目标范围非常小,可能比较有用)。
是否可以尝试换个角度,选取一些有趣的开源项目,看看它是怎么应用这些工具的, 有序的罗列出来? 对于有相同场景的项目, 参考或者模仿价值可能会更大一些. 这些开源项目就是巨人,站在巨人肩膀上显然省事多了
只是技术栈罗列未免过于简单,笔者还希望从这些项目中学点东西,比如他的设计和项目组织. 我会尝试简化和通俗解释里面的关键知识或亮点, 但是不求甚解。为了避免陷入细节泥潭,我会尽量使用图形化方式展示他们程序流程,避免拘泥于细节。你也可以把这些文章作为深入阅读这些项目源码的引导
我也希望读者同我交流反馈,共同学习和进步。
vue-cli
说到 CLI, 不得不提Rails框架,它可能是框架提供 CLI 的先祖(具体历史没有深入考究). Rails 有一个重要的指导思想,即约定大于配置, 它为 Web 应用的大多数需求都提供了最好的解决方法,并且默认使用这些约定,而不是在长长的配置文件中设置每个细节。
CLI 也是这个指导思想下的产物, 例如通过它提供的 CLI,可以在15 分钟内构建一个简易的博客, 可以通过 CLI 启动服务器和 REPL、生成项目脚手架、生成代码文件、路由、数据库迁移等等:
Rails 的很多设计在那个年代就是就是一个明星(闪瞎 PHP、JSP、 ASP…, 想想要配置各种服务器,各种 xml 文件),它的很多设计模式深刻影响了后面的 web 框架,比如 Django、Laravel, 甚至很多模仿 Rails 命名的,如 Sails、Grails.
Rails 对于前端开发影响也很深远,比如在 Nodejs 出来之前,Rails 社区就开始使用 coffeescript + sass
预编译语言进行前端开发了, Asset Pipeline可以说是最早的’前端工程化’, 配合Turbolink可以让传统后端渲染页面拥有不亚于单页应用的用户体验…
当初 Rails 给我带来的各种震撼还历历在目, Ruby China 社区也是国内最好社区之一. 但是目前 Rails 的关注度不如从前, 在前端社区像 Rails 这种集大成的框架也早已不吃香(参考 Ember, 某种程度上 Angular 也算吧?).
说实在话如果一生只学一门语言,我会选 Ruby,如果选一个 web 框架,那就是 Rails。
推荐大家阅读The Rails Doctrine - Rails 信条 这篇文章里面有一句话笔者非常喜欢: “只要放下了自负的个人喜好,便可以跳过无谓的世俗决定,专注在最重要的地方下更快的决定。”。为人写程序,而不是为了机器写程序.
约定大于配置可以减少我们做决定的数量,减少无谓的争论和考虑,让我们可以专注于更重要的事情. 这个原则可以提高开发和团队协作效率, 甚至可以凝聚一个社区.
以 Webpack 为例,恶心复杂的配置被人诟病,所以才需要 vue-cli 或者 create-react-app 这些工具.
没有用 Ruby/Rails 工作过, 默默写了个 Ruby China 小程序(微信搜
Ruby CN
),算是感恩回馈社区吧
Ok, 忍不住吹了一波 Rails, 回到正题.
笔者是使用 React 作为主力开发的,Vue 也是我非常喜欢的一个开源项目,不说别的,在开发者的’用户体验’方面 Vue 是我见过最好之一,主要体现在 API 的简洁性和易用性、文档还有项目构建工具(今天的主角).
vue-cli-ui 是我想写这系列文章的动机之一. 前阵子用了一下vue-cli-ui
, 感觉很不错, 支持可视化配置和任务运行,比我在终端下一个项目一个项目跑 task 清爽多了. 很想在我们自家的构建工具上也搞一套,怎搞? 学习它的源码, 我觉得可以作为博客记录下来.
现在前端工程师也有‘webpack 配置工程师’的戏称,这能说明 webpack 配置是费时费力的苦事(Angular 例外). 这不后来就有了parcel
宣称零配置的轮子, 还有 React 社区的create-react-app
, vue-cli 前期是基于模板的创建项目, 不算此列。
后来 vue-cli 汲取着前者的很多优点,把这块做大做优了(看来 vue 很擅长做这些事情). 我们可以来对比一下这些工具:
Vue CLI | create-react-app | parcel | |
---|---|---|---|
快速原型开发 | 支持 | - | 支持 |
全局模式 | 零配置原型开发就是全局的 | - | 支持 |
插件 | 支持 | - | 支持,扩展文件类型和文件输出 |
扩展性 | 强,通过插件扩展 wepack 配置 | 弱, 强约定, 无法配置 webpack,可以 eject, 然后手工配置;支持 babel-macro;(严格说可以通过react-app-rewired进行扩展) | 中(可以配置 babel,postcss,Typescript); 提供了 Node API; 支持插件扩展文件类型 |
多页面 | 支持 | - | 支持 |
适用范围 | Vue 组件的第一公民。通过扩展可以支持任意前端框架 | 针对 React 开发,不支持其他框架 | parcel 是一个通用的打包工具,它的竞争对手是 webpack |
编译速度 | cache-loader,thread-loader 来加速 JS 和 TS 编译 | babel-loader 开启了 cache | 编译速度号称是 webpack 的两倍 |
可升级性 | 支持升级 cli-service, 插件需要单独升级, 插件需要遵循语义化版本. 太多插件存在升级风险 | 支持升级 react-script, 官方维护,且强约定基本可以保障向下兼容 | 支持升级 parcel-bundler |
UI | 图形化管理是 CLI 的特色之一 | - | - |
通过上面的对比,可以看出 vue-cli 是一个扩展性非常强的构建工具,以致于它不仅限于 Vue,也可以用来构建 React 甚至其他前端框架。
相比而言 create-react-app
就是一个非常 Opinionated(坚持己见) 的工具,强约定. 一个典型的例子就是它不内置开启 babel 装饰器转译,CRA 团队认为已经废弃(或者不成熟)的语言特性不应该带到 CRA 中; 后面为了给‘优雅’地给 babel 扩展插件,就捣鼓出来了babel-macro
, 这是一种’免配置’的 babel 插件规范.
这种强约定也是有好处的,比如不需要管理配置; 而且 CRA 团队谨慎可靠地维护着 CRA,这使得开发者可以一般无痛地升级 CRA. 如果要扩展 webpack,一般只有 eject,这就走回了手动配置 webpack 的老路, 不可取.
vue-cli 也是一个’渐进式’的 cli,vue-cli 提供了默认的 preset,但不阻止你对其进行扩展. vue-cli 的扩展接口也非常简洁(合理, 不多不少), 还有 UI 管理界面,可视化管理项目的配置和插件,用户体验很棒,计划在下一篇文章介绍 vue ui. 唯一比较不舒服的是如果滥用这种扩展性,装 N 多插件,而且插件之间还存在依赖关系时,也会成为升级维护的负担.
基本设计
注意,本文不是 vue-cli 的教程,最好的教程是官方文档.
目录结构
下面是 vue-cli 的基本目录结构. 大部分大型的前端项目都使用 lerna 实现 mono-repo 模式, 然后统一分发到 npm. 这种模式有利于项目模块组织
分离 CLI 层和 Service 层
这个设计是借鉴create-react-app
的, CLI 层只是一些基础的命令一般不需要频繁升级,而且是全局安装; 而 Service 层是多变的, 作为项目的局部依赖,不应该硬编码在 CLI 里面. CLI 和 Service 的职责划分如下:
CLI: 用于项目创建和管理
- 全局安装
vue create
创建项目脚手架. 拉取最新的 Service,并选择配置需要的插件vue ui
. 启动 UI 管理界面- 快速原型开发:
vue serve
|vue build
, 直接伺服和编译一个 Vue 文件 - 插件管理:
vue add
|vue invoke
安装插件和调用插件生成器
Service: 负责项目的实际构建
- 局部安装
- 集成 webpack 构建环境,Service 本身只有一个插件机制, 所有构建相关逻辑都由内置插件和外部插件提供
- 内置插件(命令): serve, build, inspect
插件系统
vue-cli 提供了类似 babel、eslint 的插件机制。
插件
插件机制是 vue-cli 的核心, 用于扩展 Service. Service 的命令
和 webpack 配置都由插件提供.
其实插件机制本身并没有什么技术难度, 换句话说插件其实就是一个协议的设计. vue-cli 插件的协议如下:
- 命名:
@vue/cli-plugin-*
或vue-cli-plugin-*
. package.json 中按着这个命名约定的依赖会被识别为 vue-cli 插件,另外命名约定也有利于在 github 或 npm 上筛选 - 生命周期:
一个插件的生命周期可以分为安装阶段
和运行阶段
.vue create
命令创建项目脚手架、vue add
以及vue invoke
插件安装命令都属于安装阶段; 而 cli-service 命令执行时属于运行阶段. 基本结构: 区分了生命周期后,插件的结构就比较清晰了:
.
├── README.md
├── generator.js # generator (可选)
├── prompts.js # prompt 文件 (可选)
├── index.js # service 插件
└── package.json- 安装阶段:
- prompts: 收集用户意见和配置
- gernerator: 在安装阶段生成模板文件
- 运行时: index.js
- 注入 service 命令
- 扩展和修改 webpack 配置. vue-cli 通过
webpack-chain
和webpack-merge
来实现 webpack 可配置化
- 安装阶段:
一个简单的插件结构是这样子的:
preset
这个 preset 和 babel 的 preset 概念实际上是不一样的:
vue-cli 的 preset 一个脚手架创建方案, 也就是说它只作用于vue create
阶段。比如vue create
时默认使用的就是 babel+eslint preset. preset 可以简化项目脚手架的创建。团队可以共享一个 preset 来创建脚手架。
而 babel 中的 preset 是一个插件集合,他可以统一收纳和管理一组插件方案. 例如babel-preset-react
、 babel-preset-env
. 上文说到如果扩展性被滥用,装 N 多插件,而且插件之间还存在依赖关系时,也会成为升级维护的负担. 而 ‘babel 式’的 preset 可以让插件更方便维护和和一键式升级。
尽管目前 vue 也提供了vue upgrade
对插件进行升级,这个是基于语义化版本约定的, 且当插件之间存在依赖关系时, 不排除升级存在风险. 尤其对于团队项目还是推荐有统一地管理这些插件, 实现傻瓜化的升级。 实际上这种 ‘babel 式’的 preset 是可以通过 vue-plugin 实现和转发的。
配置
vue 支持在 package.json 的 vue
字段或vue.config.js
中进行配置。这里可以对 Service 核心功能和插件进行配置, 也可以直接修改 webpack 配置. 另外部分构建行为是通过环境变量进行影响的,这些可以通过.env.*
文件进行配置
基本流程
现在来看看一个 vue-cli 内部的基本流程, Service 的插件实现是 vue-cli 比较有意思的点. 以vue serve
为例:
Service 对象是 vue-cli 的核心对象,负责管理和应用插件,所有命令和 webpack 配置都是以插件的形式存在:
首先划分为配置阶段和运行阶段。 配置阶段 vue-cli 会加载配置文件,并查找和应用所有插件。将 PluginAPI 实例和项目配置传递给插件运行时, 插件运行时通过 PluginAPI 注入命令(registerCommand)和 扩展 webpack 配置(chainWebpack, configureWebpack).
运行阶段则根据用户传入的命令名调用插件注入命令。在命令实现函数中,可以调用 resolveWebpackConfig()来生成最终的 webpack 配置。以 serve 命令为例,获取到 webpackConfig 后会创建一个 webpack 编译器,并开启 webpack-dev-server 开发服务器.
技术地图
- 组织
- cli 命令行相关工具
- chalk: 命令行字体颜色样式
- cli-highlight: 终端语法高亮输出, 类似于 Highlight.js
- cliui: 在终端中进行多列输出
- didyoumean: 根据单词相似度,来对用户输入纠正提示
- semver: 提供语义化版本号相关的工具函数。 例如比较,规范化
- commander TJ 写的命令行选项和参数解析器,支持子命令,选项校验和类型转换,帮组信息生成等等. API 简单优雅
- minimist: 一个极简的命令行参数解析器。如果只是简单的选项解析,可以用这个库
- inquirer 命令行询问
- ora 命令行 spinner
- launch-editor 打开编辑器. 通过 node 打开编辑器,前端可以 express 暴露接口调用打开
- open 打开 URL、文件、可执行文件
- execa 更好的 child_process,修复了原生 exec 的一些问题
- validate-npm-package-name: 验证 npm 包名称,比如创建的项目名是否合法
- dotenv & dotenv-expand: 从.env 文件中加载配置,环境变量
- 网络相关
- portfinder: 获取可用的端口
- address: 获取当前主机的 ip,MAC 和 DNS 服务器
- 文件处理相关
- 数据检验
- @hapi/joi JSON schema 校验
- 调试
- debug: 这是一个 debug 日志利器, 支持通过环境变量或动态设置来确定是否需要输出; 支持 printf 风格格式化
- 算法
- hash-sum: 散列值计算
- deepmerge 深合并
- 其他
- recast Javascript 语法树转换器,支持非破坏性的格式化输出. 常用于扩展 js 代码
- javascript-stringify: 类似于 JSON.stringify, 将对象字符串化。
- webpack
- 配置定义
- webpack-merge: 合并 webpack 配置对象
- webpack-chain: 链式配置 webpack. 这两个库是 vue-cli 插件的重要成员
- webpack-dev-server: webpack 开发服务器,支持代码热重载,错误信息展示,接口代理等等
- webpack-bundle-analyzer: webpack 包分析器
- 配置定义
- 扩展(一些相关的技术栈)
- http-server 快速伺服静态文件
- plop 模板生成器
- yeoman 项目脚手架工具