本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第9期,链接:juejin.cn
1. 学习目标
- 学会全新的官方脚手架工具 create-vue 的使用和原理
- 学会使用 VSCode 直接打开 github 项目
- 学会使用测试用例调试源码
- 学以致用,为公司初始化项目写脚手架工具
2. 源码地址
vsCode打开github项目
用 vscode.dev/github/ 替换掉 github.com/ 即可
3. 关于create-vue的使用
执行命令npm init vue@next
,根据提示选择使用的技术栈,给出后续操作提示并生成项目。如图所示:
这里的npm init vue@next
实际是执行的npx create-vue@next
涉及的知识点:
npm init XX
等同于npx creat-XX
npm init- npx安装包时,会临时下载,用完就删除 (npm 5.2版本开始支持) npx
@next
是在发布时增加的标签npm publish --tag next
对应某个版本 默认是latest
- 查看tag对应的版本
npm dist-tag ls XX
creat-vue
比@vue/cli
快,原因在于相对依赖少,代码行数少
4. 调试准备
4.1 项目克隆
首先,将项目clone到本地
1 | bash复制代码git clone git@github.com:vuejs/create-vue.git |
如果想克隆到自己的项目并保留原代码库create-vue
的提交记录,参考如下操作:
- 新建仓库
- 克隆仓库到本地
1 | bash复制代码git clone git@github.com:baosisi07/create-vue-analysi.git |
- 使用Git Subtree将源代码clone到当前目录的
create-vue
下
1 | swift复制代码git subtree add --prefix=create-vue git@github.com:vuejs/create-vue.git main |
Git Subtree
用于在多个项目间双向同步子项目,比如A项目使用子项目S,S有单独的仓库进行管理,S项目更新可以在A项目中同步到,在A项目中对S进行修改提交,也会同步到S的代码库,如果B项目也使用了S,那么,B也可以同步到S的更新。
4.2 package.json解析
1 | json复制代码{ |
npm钩子
npm 脚本有pre和post两个钩子,完成一些准备工作和清理工作。npm钩子
除了常见的一些声明周期钩子,有些钩子会在除了pre-Event和Post-Event钩子之外执行,比如
prepare
,prepublish
,prepublishOnly
,prepack
,postpack
- prepare (npm 4 引入)等同于prepublish
+ 在pack和publish之前执行
+ install不带参数时运行 install的钩子postinstall之后执行
+ prepublish之后,prepublishOnly之前执行
- prepublish (已废弃)
+ 因为在publish和install时都会运行,令人疑惑,所以废弃,后来用prepare来代替
+ 不会在publish时执行,但会在ci和install时执行
- prepublishOnly
在prepared和packed之前执行,仅在publish时执行
自定义钩子可以通过npm_lifecycle_event
变量获取当前正在运行的脚本名称。如:
1 | js复制代码const target = process.env.npm_lifecycle_event |
husky + lint-staged
husky使得使用git hook变得容易
如果想在install之后自动开启git钩子,可以在prepare中定义,像上面package.json中的配置
lint-staged
对将要提交的内容进⾏lint校验或prettier格式化,结合husky使提交内容更规范
在package.json中配置即可,例如:
1 | json复制代码{ |
run-s
这个命令来自 npm-run-all
,它是一个CLI工具,可以并行或顺序运行多个npm脚本。
共提供了三个命令:
- npm-run-all 默认串行执行
- run-s npm-run-all -s (sequentially)简写 串行执行 等同于
run script1 && run script2
- run-p npm-run-all -p (parallel)简写 并行执行 等同于
run script1 & run script2
zx
bash命令虽然好,但是涉及一些复杂的操作时,并不能很好的书写脚本。zx提供了像书写js一样来写脚本,它对子进程进行合理的包装,通过传参的方式提供给我们简单的方法,使编写bash脚本变得更容易。
安装:
1 | css复制代码npm i -g zx |
使用:
- 首先,为了在最顶层使用await,我们将脚本文件后缀名改为.mjs
- 在zx脚本文件开头添加
1 | javascript复制代码#!/usr/bin/env zx |
- 运行脚本
1 | bash复制代码zx ./script.mjs |
zx常用函数 zx文档
1 | js复制代码// 所有函数都是直接使用,无需引入的 |
5. 源码预热
我们通过运行初始化命令可以看到,create-vue完成了以下功能:
- 创建默认文件vue-project,可以自定义输入文件名
- 提供使用频率比较高的库供用户选择并生成相应的模版
- 完成项目创建,并提供运行提示
由package.json中可以看到,执行create-vue实际是执行了outfile.cjs,而outfile.cjs是根目录下的index.ts所生成。
我们先看下index.ts的代码:
1 | js复制代码#!/usr/bin/env node |
5.1 使用的基础包:
minimist
主要作用就是解析命令行参数。看示例:
1 | js复制代码// example/parse.js |
prompts
prompts用于收集用户信息的交互式命令行工具。
语法
prompts(prompts, options)
prompts: Object | Array
options.onSubmit: Function
options.onCancel: Function
每项prompt
可能包含如下属性:
1 | json复制代码{ |
其中的Function可以接受三个参数(prev, values, prompt)
- prev指上一询问项的值
- valuses指前面所有的结果集合
- prompt指上一个prompt对象
使用类型即type
类型有:
若为null等falsey类值时则会跳过当前询问项
- text
- password
- invisible
- number
- confirm
- list
- toggle
- select
- multiselect
- autocompleteMultiselect
- autocomplete
- date
使用示例:
1 | js复制代码const prompts = require('prompts'); |
kolorist
定义标准输入/输出的颜色,颜色示例:
还有一个使用到的库 gradient-string用于定义渐变字符串
这里的banner就是这个库生成的结果,如图:
使用示例:
1 | js复制代码require('gradient-string')([ |
5.2 使用的工具函数:
renderTemplate(src, dist)
- 将src的目录或文件递归地拷贝到dist下
- 以
_
命名的文件会替换为以.
命名 package.json
如果已存在dist中,则对其内容进行merge处理,而不是替换
getCommand(manager, script)
通过参数选择包管理器和执行的脚本命令
1 | js复制代码getCommand('npm', 'test') => npm run test |
renderEslint(rootdir, options)
- 根据是否需要
typeScript
、prettier
、Cypress
等生成相应的dependencies及scripts到package.json中 - 生成相应的
.eslintrc.cjs
6. 源码解析
6.1 获取初始化时命令行的参数作为询问依赖
1 | js复制代码async function init() { |
传入的第一个参数作为projectName
,此时会跳过相关询问项,直接跳到后面的询问,如下图,继续询问ts配置。
除了传入projectName
,如果传入包含在isFeatureFlagsUsed
中的任一参数,并且值为true时,则直接跳过所有询问项,直接生成。可以看到minimist
处理的别名和原名的值都是存在的且是同步的。
6.2 交互式询问配置
1 | js复制代码result = await prompts( |
前面的prompts
有过了解的话,这里其实很好理解,大致会询问以下信息:
- 项目名称:
- 是否覆盖已存在的重名项目?
- 为
package.json
输入一个合法的名称
- 项目语言:
JavaScript
/TypeScript
- 是否支持
JSX
- 是否安装
Vue Router
以满足单页面应用 - 是否安装状态管理工具
Pinia
了解更多 - 是否安装单元测试工具
Vitest
了解更多 - 是否安装端到端或单元测试工具
Cypress
了解更多 - 是否支持代码质量检测
ESLint
- 是否安装
Prettier
对代码进行格式化
6.3 生成初始化项目所需文件
1. 根据询问结果定义各个配置项变量,供后续使用
1 | js复制代码const { |
2. 创建新项目目录,已存在则清空
1 | js复制代码if (fs.existsSync(root) && shouldOverwrite) { |
3. 新建package.json并添加name和version
1 | js复制代码const pkg = { name: packageName, version: '0.0.0' } |
4. 根据各个配置渲染引用模版生成文件
- 渲染基础模版
- 渲染包对应的配置项config,主要更新package.json中的依赖和配置项,添加config类的文件
- ts和Eslint需要对支持的包进行单独的渲染配置
- 生成示例代码文件
1 | js复制代码const templateRoot = path.resolve(__dirname, 'template') |
如图template目录为可以使用的模版,根据配置项来渲染相应的package.json
配置(多个配置项会做合并处理),或其他配置文件,比如vite.config.js
,这里的base是基础模版。
5. 支持TS与否,对模版文件进行处理
支持ts时
- 将模版中的js文件转换为ts
- 如果存在ts文件,则移除js文件
- 不存在则重命名为ts
- 移除jsconfig.json,因为有tsconfig.json
- 替换index.html的入口js文件为ts
不支持时 - 清理ts文件
6. 生成README.md文件并给出运行提示
process.env.npm_config_user_agent
动态取用户使用的包管理工具- 包管理工具使用优先级 pnpm > yarn > npm
- 动态生成
README.md
的安装提示及其他配置引导 - 打印运行提示,利用
kolorist
的方法添加样式
1 | js复制代码const userAgent = process.env.npm_config_user_agent ?? '' |
7. 测试
npm run test
实际先后执行了 build
、snapshot
和test
build就是我们上面分析的outfile.cjs的内容
7.1 snapshot的作用
- 找到
['typescript', 'jsx', 'router', 'pinia', 'vitest', 'cypress']
这几个包组合的所有可能性 (还有default
) ['typescript', 'jsx', 'router', 'pinia']
(测试无关)为前缀,with-tests
结尾,(包括with-tests
)生成所有可能的组合- 依次遍历以上所有的组合,以
'-'
拼接生成相应的目录名,在相应目录下生成项目文件(删除前一次的目录及文件) - 把所有的生成项目的组合放到
playground/
目录下
7.2 test做了啥
主要是对playground/
下的项目文件进行测试
需要注意的是: 这里的测试命令(如: pnpm test:unit
)在运行build
的时候就会从template中拿过来了
具体的行为如下:
- 如果目录名含有
vitest
,执行pnpm test:unit
- 如果目录名含有
cypress
,执行pnpm build
然后执行pnpm test:e2e:ci
(页面测试 url方式打开) - 如果目录名不含
vitest
,则使用cypress
的组件测试 - 项目名以
with-tests
结尾,依次执行pnpm test:unit
pnpm build
pnpm test:e2e:ci
1 | js复制代码for (const projectName of fs.readdirSync(playgroundDir)) { |
8. 总结
create-vue
确实很快很好用,以前只知道怎么用,这下知道怎么写了,哈哈哈哈哈 过程比较漫长,这篇源码读了很久,也写了很久,但是最终的目的达到了(也不能为了写文章而写文章,是吧)。学到了很多优秀的工具,比如zx
、start-server-and-test
,编码的思想,比如模版渲染,还有一些编码技巧,比如snapshot
生成组合那里(<<
运算符的使用)等等。
下面就差实践了,后面为公司写一个脚手架工具😊。
本文转载自: 掘金