create-react-app 实现原理
通过分析 create-react-app 的原理,学习如何自己写一个 cli。
命令行工具基本原理
首先创建一个项目是必须的,新建一个文件夹并执行 npm init -y
新建 index.js 放入想要执行的命令,并在顶部添加以下 shebang, 表示使用 node 解释执行该脚本
#!/usr/bin/env node
在 package 中添加 bin,就可以在安装时注入全局命令了
{
"bin": {
"command-name": "./index.js"
}
}
然后全局安装新建的这个包即可,记住安装路径为你本地包的路径(获取将其发布到 npm 再安装)
npm install -g "path to your project"
commander 常用选项
version 定义版本号,可以通过-V 查看
option 选项,默认为布尔值,可以通过 --opts <foo>
添加参数。使用[foo]
表示可选 类型
再新建 commander 的时候可以传入 名称作为后面提示时的 name 使用:
const program = new commander.Command("yourName");
当然直接使用.name
也是一样的
CRA 项目分析(基于 v4.0.3)
github 上的 cra 由多个包组成:
- packages/cra-template 默认的模板文件
- packages/cra-template-typescript 基于 ts 的模板文件
packages/create-react-app
注入安装命令和参数、node 兼容性检验和安装依赖- packages/react-dev-utils packages/react-scripts 的工具库
packages/react-scripts
我们执行 npm install create-react-app 的时候其实就是在安装 packages/create-react-app,所以若无特殊说明,以下 CRA 均指 packages/create-react-app。
CRA 中用到的工具库
chalk 在 命令行中显示丰富多彩的文字
commander 方便的处理参数和添加命令
cross-spawn 跨平台设置环境变量
envinfo 获取运行环境信息
fs-extra 快捷操作文件夹和文件
hyperquest 同时发送更多 http 请求与持久化链接
prompts 简单漂亮的命令行输入提示
semver npm 版本判断相关工具
tar-pack 将文件夹打包为压缩文件
tmp 创建临时文件目录
validate-npm-package-name 很直白的包名,告诉你这是否为一个合法的 npm 包名
分析创建项目
创建项目主要有两个过程,第一个是执行 CRA 创建基本的环境,包括 react 生态和 template,另一个是执行 react-scripts 对项目(template)进行初始化(安装依赖、readme、git 和 ts 等等)。
CRA
通过了解主要执行的 createApp 方法的参数就知道要做什么了,有六个参数分别是:
- name 项目名,目录名
- verbose 打印额外的日志
- version react-scripts 的版本
- template 初始化使用的模板
- useNpm 是否使用 npm
- usePnp 是否使用 yarn PnP 特性
执行完 CRA 后(不包括 react-scripts 的执行)可以观察生成的文件下,目前就只有 package.json 和 node_modules 中刚才安装的依赖(cra-template、react-scripts、react 和 react-dom)。
react-scripts
合并 package.json 与模板的 template.json 中的 script 和 dependencies
默认情况下合并后的结果如下
{
"dependencies": {
"cra-template": "1.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": ["react-app", "react-app/jest"]
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
可以看到,主要就是添加了基于 react-scripts 的开发命令、eslint 规则和构建代码的目标浏览器列表
eslintConfig 的 extends 参考官方文档,实际就是在请求 node_modules 中eslint-config-react-app
导出的规则。
其余的读写操作细节比较多,完整的代码走读如下:
分析脚本执行
项目创建好了,剩下的就是分析脚本是如何执行的了,通过查看 package.json 可以看到主要有四个脚本
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
npm 脚本执行的原理可以参考阮一峰的npm scripts 使用指南,简单来说就是执行 npm run start
就相当于执行 react-scripts start
,执行 react-scripts start
就相当于执行 ./node_modules/.bin/react-scripts start
,所以直接看对应的脚本即可。
简单分析./node_modules/.bin/react-scripts 可以知道其实 react-scripts scriptName
就是在执行 node_modules/react-scripts/scripts 目录下的同名脚本,所以可以进一步聚焦到 node_modules/react-scripts/scripts/start.js 中,这回真的不用再去看其他文件了。
react-scripts start
整个过程主要就是读取配置并调用 WebpackDevServer 起服务。
默认读取的配置文件是 node_modules/react-scripts/config/webpack.config.js,里面逻辑还挺多暂时不深究。
react-scripts eject
执行该方法会弹出配置,其实就是把 react-scripts 从 node_modules 中搬出来丢到你的项目中,然后将依赖中的 react-scripts 从 package.json 去掉。看看那些配置和脚本,我相信应该没有人会喜欢去增进一个 700+行的 webpack 配置。