1. 前言
大家好,我是若川。欢送关注我的公众号若川视线,最近组织了源码共读流动,感兴趣的能够加我微信 ruochuan12 参加,已进行两个多月,大家一起交流学习,共同进步。
想学源码,极力推荐之前我写的《学习源码整体架构系列》 蕴含jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
、koa-compose
、vue-next-release
、vue-this
等十余篇源码文章。
美国工夫 2021 年 10 月 7 日晚上,Vue 团队等次要贡献者举办了一个Vue Contributor Days
在线会议,蒋豪群(知乎胖茶,Vue.js 官网团队成员,Vue-CLI 外围开发),在会上公开了create-vue
,一个全新的脚手架工具。
create-vue
应用npm init vue@next
一行命令,就能快如闪电般
初始化好基于vite
的Vue3
我的项目。
本文就是通过调试和大家一起学习这个300余行的源码。
浏览本文,你将学到:
1. 学会全新的官网脚手架工具 create-vue 的应用和原理2. 学会应用 VSCode 间接关上 github 我的项目3. 学会应用测试用例调试源码4. 学以致用,为公司初始化我的项目写脚手架工具。5. 等等
2. 应用 npm init vue@next 初始化 vue3 我的项目
create-vue github README上写着,An easy way to start a Vue project
。一种简略的初始化vue我的项目的形式。
npm init vue@next
预计大多数读者,第一反馈是这样居然也能够,这么简略快捷?
忍不住想入手在控制台输入命令,我在终端试过,见下图。
最终cd vue3-project
、npm install
、npm run dev
关上页面http://localhost:3000。
2.1 npm init && npx
为啥 npm init
也能够间接初始化一个我的项目,带着疑难,咱们翻看 npm
文档。
npm init
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,找到一些信息。或者在npm create-vue找到版本等信息。
其中@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 介绍、nodejs.cn npx
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 vitenpx vite -v# vite/2.6.5 linux-x64 node-v14.16.0
应用不同的 Node.js 版本运行代码
某些场景下能够长期切换 node
版本,有时比 nvm
包治理不便些。
npx node@14 -v# v14.18.0npx -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/clivue create vue-project
npm init vue@next
(npx create-vue@next
) 快的起因,次要在于依赖少(能不依赖包就不依赖),源码行数少,目前index.js
只有300余行。
3. 配置环境调试源码
3.1 克隆 create-vue 我的项目
本文仓库地址 create-vue-analysis,求个star
~
# 能够间接克隆我的仓库,我的仓库保留的 create-vue 仓库的 git 记录git clone https://github.com/lxchuan12/create-vue-analysis.gitcd create-vue-analysis/create-vuenpm i
当然不克隆也能够间接用 VSCode
关上我的仓库
顺带说下:我是怎么保留 create-vue
仓库的 git
记录的。
# 在 github 上新建一个仓库 `create-vue-analysis` 克隆下来git clone https://github.com/lxchuan12/create-vue-analysis.gitcd create-vue-analysisgit subtree add --prefix=create-vue https://github.com/vuejs/create-vue.git main# 这样就把 create-vue 文件夹克隆到本人的 git 仓库了。且保留的 git 记录
对于更多 git subtree
,能够看Git Subtree 扼要使用手册
3.2 package.json 剖析
// create-vue/package.json{ "name": "create-vue", "version": "3.0.0-beta.6", "description": "An easy way to start a Vue project", "type": "module", "bin": { "create-vue": "outfile.cjs" },}
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 提供的命令。run-s build snapshot
命令相当于 npm run build && npm run snapshot
。
依据脚本提醒,咱们来看 snapshot.js
文件。
3.3 生成快照 snapshot.js
这个文件次要作用是依据const featureFlags = ['typescript', 'jsx', 'router', 'vuex', 'with-tests']
组合生成31种
加上 default
共计 32种
组合,生成快照在 playground
目录。
因为打包生成的 outfile.cjs
代码有做一些解决,不不便调试,咱们能够批改为index.js
便于调试。
// 门路 create-vue/snapshot.jsconst 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.jsfor (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/2907import { fileURLToPath } from 'url';import { dirname } from 'path';const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);
接着咱们调试 index.js 文件,来学习。
4. 调试 index.js 主流程
回顾下上文 npm init vue@next
初始化我的项目的。
单从初始化我的项目输入图来看。次要是三个步骤。
1. 输出项目名称,默认值是 vue-project2. 询问一些配置 渲染模板等3. 实现创立我的项目,输入运行提醒
async function init() { // 省略放在后文具体讲述}// async 函数返回的是Promise 能够用 catch 报错init().catch((e) => { console.error(e)})
4.1 解析命令行参数
// 返回运行以后脚本的工作目录的门路。const cwd = process.cwd()// possible options:// --default// --typescript / --ts// --jsx// --router / --vue-router// --vuex// --with-tests / --tests / --cypress// --force (for force overwriting)const argv = minimist(process.argv.slice(2), { alias: { typescript: ['ts'], 'with-tests': ['tests', 'cypress'], router: ['vue-router'] }, // all arguments are treated as booleans boolean: true})
minimist
简略说,这个库,就是解析命令行参数的。看例子,咱们比拟容易看懂传参和解析后果。
$ 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
4.2 如果设置了 feature flags 跳过 prompts 询问
这种写法不便代码测试等。间接跳过交互式询问,同时也能够省工夫。
// 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
4.3 交互式询问一些配置
如上文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) }
4.4 初始化询问用户给到的参数,同时也会给到默认值
// `initial` won't take effect if the prompt type is null // so we still have to assign the default values here const { packageName = toValidPackageName(defaultProjectName), shouldOverwrite, needsJsx = argv.jsx, needsTypeScript = argv.typescript, needsRouter = argv.router, needsVuex = argv.vuex, needsTests = argv.tests } = result const root = path.join(cwd, targetDir) // 如果须要强制重写,清空文件夹 if (shouldOverwrite) { emptyDir(root) // 如果不存在文件夹,则创立 } else if (!fs.existsSync(root)) { fs.mkdirSync(root) } // 脚手架我的项目目录 console.log(`\nScaffolding project in ${root}...`) // 生成 package.json 文件 const pkg = { name: packageName, version: '0.0.0' } fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))
4.5 依据模板文件生成初始化我的项目所需文件
// todo: // work around the esbuild issue that `import.meta.url` cannot be correctly transpiled // when bundling for node and the format is cjs // const templateRoot = new URL('./template', import.meta.url).pathname const templateRoot = path.resolve(__dirname, 'template') const render = function render(templateName) { const templateDir = path.resolve(templateRoot, templateName) renderTemplate(templateDir, root) } // Render base template render('base') // 增加配置 // Add configs. if (needsJsx) { render('config/jsx') } if (needsRouter) { render('config/router') } if (needsVuex) { render('config/vuex') } if (needsTests) { render('config/cypress') } if (needsTypeScript) { render('config/typescript') }
4.6 渲染生成代码模板
// Render code template. // prettier-ignore const codeTemplate = (needsTypeScript ? 'typescript-' : '') + (needsRouter ? 'router' : 'default') render(`code/${codeTemplate}`) // Render entry file (main.js/ts). if (needsVuex && needsRouter) { render('entry/vuex-and-router') } else if (needsVuex) { render('entry/vuex') } else if (needsRouter) { render('entry/router') } else { render('entry/default') }
4.7 如果配置了须要 ts
重命名所有的 .js
文件改成 .ts
。
重命名 jsconfig.json
文件为 tsconfig.json
文件。
jsconfig.json 是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')) }
4.8 配置了不须要测试
因为所有的模板都有测试文件,所以不须要测试时,执行删除 cypress
、/__tests__/
文件夹
if (!needsTests) { // All templates assumes the need of tests. // If the user doesn't need it: // rm -rf cypress **/__tests__/ preOrderDirectoryTraverse( root, (dirpath) => { const dirname = path.basename(dirpath) if (dirname === 'cypress' || dirname === '__tests__') { emptyDir(dirpath) fs.rmdirSync(dirpath) } }, () => {} ) }
4.9 依据应用的 npm / yarn / pnpm 生成README.md 文件,给出运行我的项目的提醒
// Instructions: // Supported package managers: pnpm > yarn > npm // Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved, // it is not possible to tell if the command is called by `pnpm init`. const packageManager = /pnpm/.test(process.env.npm_execpath) ? 'pnpm' : /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm' // README generation fs.writeFileSync( path.resolve(root, 'README.md'), generateReadme({ projectName: result.projectName || defaultProjectName, packageManager, needsTypeScript, needsTests }) ) console.log(`\nDone. Now run:\n`) if (root !== cwd) { console.log(` ${bold(green(`cd ${path.relative(cwd, root)}`))}`) } console.log(` ${bold(green(getCommand(packageManager, 'install')))}`) console.log(` ${bold(green(getCommand(packageManager, 'dev')))}`) console.log()
5. npm run test => node test.js 测试
// create-vue/test.jsimport fs from 'fs'import path from 'path'import { fileURLToPath } from 'url'import { spawnSync } from 'child_process'const __dirname = path.dirname(fileURLToPath(import.meta.url))const playgroundDir = path.resolve(__dirname, './playground/')for (const projectName of fs.readdirSync(playgroundDir)) { if (projectName.endsWith('with-tests')) { console.log(`Running unit tests in ${projectName}`) const unitTestResult = spawnSync('pnpm', ['test:unit:ci'], { cwd: path.resolve(playgroundDir, projectName), stdio: 'inherit', shell: true }) if (unitTestResult.status !== 0) { throw new Error(`Unit tests failed in ${projectName}`) } console.log(`Running e2e tests in ${projectName}`) const e2eTestResult = spawnSync('pnpm', ['test:e2e:ci'], { cwd: path.resolve(playgroundDir, projectName), stdio: 'inherit', shell: true }) if (e2eTestResult.status !== 0) { throw new Error(`E2E tests failed in ${projectName}`) } }}
次要对生成快照时生成的在 playground
32个文件夹,进行如下测试。
pnpm test:unit:cipnpm test:e2e:ci
6. 总结
咱们应用了快如闪电般的npm init vue@next
,学习npx
命令了。学会了其原理。
npm init vue@next => npx create-vue@next
快如闪电的起因在于依赖的很少。很多都是本人来实现。如:Vue-CLI
中 vue create vue-project
命令是用官网的npm
包validate-npm-package-name,删除文件夹个别都是应用 rimraf。而 create-vue
是本人实现emptyDir
和isValidPackageName
。
十分倡议读者敌人依照文中办法应用VSCode
调试 create-vue
源码。源码中还有很多细节文中因为篇幅无限,未全面开展讲述。
学完本文,能够为本人或者公司创立相似初始化脚手架。
目前版本是3.0.0-beta.6
。咱们继续关注学习它。除了create-vue 之外,咱们还能够看看create-vite、create-umi 的源码实现。
最初欢送加我微信 ruochuan12 交换,参加 源码共读 流动,大家一起学习源码,共同进步。
7. 参考资料
发现 create-vue 时打算写文章退出到源码共读打算中,大家一起学习。而源码共读群里小伙伴upupming
比我先写完文章。
@upupming vue-cli 将被 create-vue 代替?初始化基于 vite 的 vue3 我的项目为何如此简略?