美国时间 2021 年 10 月 7 日早晨,布新版脚Vue 团队等主要贡献者举办了一个 Vue Contributor Days 在线会议,手架生蒋豪群[1](知乎胖茶[2],工具Vue.js 官方团队成员,行代Vue-CLI 核心开发),码轻在会上公开了create-vue[3],盈新一个全新的布新版脚脚手架工具。
create-vue使用npm init vue@next一行命令,手架生就能快如闪电般初始化好基于vite的工具Vue3项目。
本文就是行代通过调试和大家一起学习这个300余行的源码。
阅读本文,码轻你将学到:
1. 学会全新的盈新官方脚手架工具 create-vue 的使用和原理
2. 学会使用 VSCode 直接打开 github 项目
3. 学会使用测试用例调试源码
4. 学以致用,为公司初始化项目写脚手架工具。布新版脚
5. 等等
create-vue github README[4]上写着,手架生An easy way to start a Vue project。工具一种简单的初始化vue项目的方式。
npm init vue@next估计大多数读者,第一反应是这样竟然也可以,这么简单快捷?
忍不住想动手在控制台输出命令,我在终端试过,见下图。
npm init vue@next
最终cd vue3-project、npm install 、npm run dev打开页面http://localhost:3000[5]。
初始化页面
为啥 npm init 也可以直接初始化一个项目,源码库带着疑问,我们翻看 npm 文档。
npm init[6]
npm init 用法:
npm init [--force|-f|--yes|-y|--scope] npm init <@scope> (same as `npx <@scope>/create`) npm init [<@scope>/]<name> (same as `npx [<@scope>/]create-<name>`)npm init <initializer> 时转换成npx命令:
npm init foo -> npx create-foo npm init @usr/foo -> npx @usr/create-foo npm init @usr -> npx @usr/create看完文档,我们也就理解了:
# 运行 npm init vue@next # 相当于 npx create-vue@next我们可以在这里create-vue[7],找到一些信息。或者在npm create-vue[8]找到版本等信息。
其中@next是指定版本,通过npm dist-tag ls create-vue命令可以看出,next版本目前对应的是3.0.0-beta.6。
npm dist-tag ls create-vue - latest: 3.0.0-beta.6 - next: 3.0.0-beta.6发布时 npm publish --tag next 这种写法指定 tag。默认标签是latest。
可能有读者对 npx 不熟悉,这时找到阮一峰老师博客 npx 介绍[9]、nodejs.cn npx[10]
npx 是一个非常强大的命令,从 npm 的 5.2 版本(发布于 2017 年 7 月)开始可用。
简单说下容易忽略且常用的场景,npx有点类似小程序提出的随用随走。
轻松地运行本地命令
node_modules/.bin/vite -v # vite/2.6.5 linux-x64 node-v14.16.0 # 等同于 # package.json script: "vite -v" # npm run vite npx vite -v # vite/2.6.5 linux-x64 node-v14.16.0使用不同的 Node.js 版本运行代码某些场景下可以临时切换 node 版本,有时比 nvm 包管理方便些。
npx node@14 -v # v14.18.0 npx -p node@14 node -v # v14.18.0无需安装的命令执行 。
# 启动本地静态服务 npx http-server # 无需全局安装 npx @vue/cli create vue-project # @vue/cli 相比 npm init vue@next npx create-vue@next 很慢。 # 全局安装 npm i -g @vue/cli vue create vue-projectnpx vue-cli
npm init vue@next (npx create-vue@next) 快的站群服务器原因,主要在于依赖少(能不依赖包就不依赖),源码行数少,目前index.js只有300余行。
本文仓库地址 create-vue-analysis[11],求个star~
# 可以直接克隆我的仓库,我的仓库保留的 create-vue 仓库的 git 记录 git clone https://github.com/lxchuan12/create-vue-analysis.git cd create-vue-analysis/create-vue npm i当然不克隆也可以直接用 VSCode 打开我的仓库。https://open.vscode.dev/lxchuan12/create-vue-analysis
顺带说下:我是怎么保留 create-vue 仓库的 git 记录的。
# 在 github 上新建一个仓库 `create-vue-analysis` 克隆下来 git clone https://github.com/lxchuan12/create-vue-analysis.git cd create-vue-analysis git subtree add --prefix=create-vue https://github.com/vuejs/create-vue.git main # 这样就把 create-vue 文件夹克隆到自己的 git 仓库了。且保留的 git 记录关于更多 git subtree,可以看Git Subtree 简明使用手册[12]
bin指定可执行脚本。也就是我们可以使用 npx create-vue 的原因。
outfile.cjs 是打包输出的JS文件
{ "scripts": { "build": "esbuild --bundle index.js --format=cjs --platform=node --outfile=outfile.cjs", "snapshot": "node snapshot.js", "pretest": "run-s build snapshot", "test": "node test.js" }, }执行 npm run test 时,会先执行钩子函数 pretest。run-s 是 npm-run-all[13] 提供的命令。run-s build snapshot 命令相当于 npm run build && npm run snapshot。
根据脚本提示,源码下载我们来看 snapshot.js 文件。
这个文件主要作用是根据const featureFlags = [typescript, jsx, router, vuex, with-tests] 组合生成31种加上 default 共计 32种 组合,生成快照在 playground目录。
因为打包生成的 outfile.cjs 代码有做一些处理,不方便调试,我们可以修改为index.js便于调试。
// 路径 create-vue/snapshot.js const bin = path.resolve(__dirname, ./outfile.cjs) // 改成 index.js 便于调试 const bin = path.resolve(__dirname, ./index.js)我们可以在for和 createProjectWithFeatureFlags 打上断点。
createProjectWithFeatureFlags其实类似在终端输入如下执行这样的命令
node ./index.js --xxx --xxx --force function createProjectWithFeatureFlags(flags) { const projectName = flags.join(-) console.log(`Creating project ${ projectName}`) const { status } = spawnSync( node, [bin, projectName, ...flags.map((flag) => `--${ flag}`), --force], { cwd: playgroundDir, stdio: [pipe, pipe, inherit] } ) if (status !== 0) { process.exit(status) } } // 路径 create-vue/snapshot.js for (const flags of flagCombinations) { createProjectWithFeatureFlags(flags) }调试:VSCode打开项目,VSCode高版本(1.50+)可以在 create-vue/package.json => scripts => "test": "node test.js"。鼠标悬停在test上会有调试脚本提示,选择调试脚本。如果对调试不熟悉,可以看我之前的文章koa-compose,写的很详细。
调试时,大概率你会遇到:create-vue/index.js 文件中,__dirname 报错问题。可以按照如下方法解决。在 import 的语句后,添加如下语句,就能愉快的调试了。
// 路径 create-vue/index.js // 解决办法和nodejs issues // https://stackoverflow.com/questions/64383909/dirname-is-not-defined-in-node-14-version // https://github.com/nodejs/help/issues/2907 import { fileURLToPath } from url; import { dirname } from path; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);接着我们调试 index.js 文件,来学习。
回顾下上文 npm init vue@next 初始化项目的。
npm init vue@next
单从初始化项目输出图来看。主要是三个步骤。
1. 输入项目名称,默认值是 vue-project 2. 询问一些配置 渲染模板等 3. 完成创建项目,输出运行提示 async function init() { // 省略放在后文详细讲述 } // async 函数返回的是Promise 可以用 catch 报错 init().catch((e) => { console.error(e) })minimist[14]
简单说,这个库,就是解析命令行参数的。看例子,我们比较容易看懂传参和解析结果。
$ node example/parse.js -a beep -b boop { _: [], a: beep, b: boop } $ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz { _: [ foo, bar, baz ], x: 3, y: 4, n: 5, a: true, b: true, c: true, beep: boop }比如
npm init vue@next --vuex --force这种写法方便代码测试等。直接跳过交互式询问,同时也可以省时间。
// if any of the feature flags is set, we would skip the feature prompts // use `??` instead of `||` once we drop Node.js 12 support const isFeatureFlagsUsed = typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) === boolean // 生成目录 let targetDir = argv._[0] // 默认 vue-projects const defaultProjectName = !targetDir ? vue-project : targetDir // 强制重写文件夹,当同名文件夹存在时 const forceOverwrite = argv.force如上文npm init vue@next 初始化的图示
输入项目名称 还有是否删除已经存在的同名目录 询问使用需要 JSX Router vuex cypress 等。 let result = { } try { // Prompts: // - Project name: // - whether to overwrite the existing directory or not? // - enter a valid package name for package.json // - Project language: JavaScript / TypeScript // - Add JSX Support? // - Install Vue Router for SPA development? // - Install Vuex for state management? (TODO) // - Add Cypress for testing? result = await prompts( [ { name: projectName, type: targetDir ? null : text, message: Project name:, initial: defaultProjectName, onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName) }, // 省略若干配置 { name: needsTests, type: () => (isFeatureFlagsUsed ? null : toggle), message: Add Cypress for testing?, initial: false, active: Yes, inactive: No } ], { onCancel: () => { throw new Error(red(✖) + Operation cancelled) } } ] ) } catch (cancelled) { console.log(cancelled.message) // 退出当前进程。 process.exit(1) }重命名所有的 .js 文件改成 .ts。重命名 jsconfig.json 文件为 tsconfig.json 文件。
jsconfig.json[15] 是VSCode的配置文件,可用于配置跳转等。
把index.html 文件里的 main.js 重命名为 main.ts。
// Cleanup. if (needsTypeScript) { // rename all `.js` files to `.ts` // rename jsconfig.json to tsconfig.json preOrderDirectoryTraverse( root, () => { }, (filepath) => { if (filepath.endsWith(.js)) { fs.renameSync(filepath, filepath.replace(/\.js$/, .ts)) } else if (path.basename(filepath) === jsconfig.json) { fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, tsconfig.json)) } } ) // Rename entry in `index.html` const indexHtmlPath = path.resolve(root, index.html) const indexHtmlContent = fs.readFileSync(indexHtmlPath, utf8) fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace(src/main.js, src/main.ts)) }因为所有的模板都有测试文件,所以不需要测试时,执行删除 cypress、/__tests__/ 文件夹
if (!needsTests) { // All templates assumes the need of tests. // If the user doesnt need it: // rm -rf cypress **/__tests__/ preOrderDirectoryTraverse( root, (dirpath) => { const dirname = path.basename(dirpath) if (dirname === cypress || dirname === __tests__) { emptyDir(dirpath) fs.rmdirSync(dirpath) } }, () => { } ) }主要对生成快照时生成的在 playground 32个文件夹,进行如下测试。
pnpm test:unit:ci pnpm test:e2e:ci我们使用了快如闪电般的npm init vue@next,学习npx命令了。学会了其原理。
npm init vue@next => npx create-vue@next快如闪电的原因在于依赖的很少。很多都是自己来实现。如:Vue-CLI中 vue create vue-project 命令是用官方的npm包validate-npm-package-name[16],删除文件夹一般都是使用 rimraf[17]。而 create-vue 是自己实现emptyDir和isValidPackageName。
非常建议读者朋友按照文中方法使用VSCode调试 create-vue 源码。源码中还有很多细节文中由于篇幅有限,未全面展开讲述。