1. 前言

大家好,我是若川。欢迎关注我的公众号若川视野源码共读活动ruochuan12

想学源码,极力推荐之前我写的《学习源码整体架构系列》jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4koa-composevue-next-releasevue-this等十余篇源码文章。

美国时间 2021 年 10 月 7 日早晨,Vue 团队等主要贡献者举办了一个 Vue Contributor Days 在线会议,蒋豪群[1](知乎胖茶[2],Vue.js 官方团队成员,Vue-CLI 核心开发),在会上公开了`create-vue`[3],一个全新的脚手架工具。

create-vue使用npm init vue@next一行命令,就能快如闪电般初始化好基于viteVue3项目。

本文就是通过调试和大家一起学习这个300余行的源码。

阅读本文,你将学到:

1. 学会全新的官方脚手架工具 create-vue 的使用和原理
2. 学会使用 VSCode 直接打开 github 项目
3. 学会使用测试用例调试源码
4. 学以致用,为公司初始化项目写脚手架工具。
5. 等等

2. 使用 npm init vue@next 初始化 vue3 项目

create-vue github README[4]上写着,An easy way to start a Vue project。一种简单的初始化vue项目的方式。

npm init vue@next

估计大多数读者,第一反应是这样竟然也可以,这么简单快捷?

忍不住想动手在控制台输出命令,我在终端试过,见下图。

npm init vue@next

最终cd vue3-projectnpm installnpm run dev打开页面http://localhost:3000[5]

初始化页面

2.1 npm init && npx

为啥 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 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/cli
vue create vue-project

npx vue-cli

npm init vue@nextnpx create-vue@next) 快的原因,主要在于依赖少(能不依赖包就不依赖),源码行数少,目前index.js只有300余行。

3. 配置环境调试源码

3.1 克隆 create-vue 项目

本文仓库地址 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]

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 时,会先执行钩子函数 pretestrun-s 是 npm-run-all[13] 提供的命令。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.js
const bin = path.resolve(__dirname, './outfile.cjs')
// 改成 index.js 便于调试
const bin = path.resolve(__dirname, './index.js')

我们可以在forcreateProjectWithFeatureFlags 打上断点。

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/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 初始化项目的。

npm init vue@next

单从初始化项目输出图来看。主要是三个步骤。

1. 输入项目名称,默认值是 vue-project
2. 询问一些配置 渲染模板等
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 booleansboolean: true
})

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

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 supportconst isFeatureFlagsUsed =typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ==='boolean'// 生成目录let targetDir = argv._[0]// 默认 vue-projectsconst 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 hereconst {packageName = toValidPackageName(defaultProjectName),shouldOverwrite,needsJsx = argv.jsx,needsTypeScript = argv.typescript,needsRouter = argv.router,needsVuex = argv.vuex,needsTests = argv.tests} = resultconst 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).pathnameconst templateRoot = path.resolve(__dirname, 'template')const render = function render(templateName) {const templateDir = path.resolve(templateRoot, templateName)renderTemplate(templateDir, root)}// Render base templaterender('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-ignoreconst 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[15] 是VSCode的配置文件,可用于配置跳转等。

index.html 文件里的 main.js 重命名为 main.ts

// Cleanup.if (needsTypeScript) {// rename all `.js` files to `.ts`// rename jsconfig.json to tsconfig.jsonpreOrderDirectoryTraverse(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 generationfs.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.js
import 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-CLIvue create vue-project 命令是用官方的npm包validate-npm-package-name[16],删除文件夹一般都是使用 rimraf[17]。而 create-vue 是自己实现emptyDirisValidPackageName

非常建议读者朋友按照文中方法使用VSCode调试 create-vue 源码。源码中还有很多细节文中由于篇幅有限,未全面展开讲述。

学完本文,可以为自己或者公司创建类似初始化脚手架。

目前版本是3.0.0-beta.6。我们持续关注学习它。除了create-vue 之外,我们还可以看看create-vite[18]、create-umi[19] 的源码实现。

最后欢迎加我微信 ruochuan12源码共读 活动,大家一起学习源码,共同进步。

7. 参考资料

发现 create-vue 时打算写文章加入到源码共读比我先写完文章。

@upupming  vue-cli 将被 create-vue 替代?初始化基于 vite 的 vue3 项目为何如此简单?

参考资料

[1]

点击阅读原文查看更多

最近组建了一个湖南人的前端交流群,如果你是湖南人可以加我微信 ruochuan12 私信 湖南 拉你进群。


推荐阅读

1个月,200+人,一起读了4周源码
我历时3年才写了10余篇源码文章,但收获了100w+阅读

老姚浅谈:怎么学JavaScript?

我在阿里招前端,该怎么帮你(可进面试群)

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动

识别方二维码加我微信、拉你进源码共读

今日话题

略。欢迎分享、收藏、点赞、在看我的公众号文章~

Vue 团队公开快如闪电的全新脚手架工具,未来将替代 Vue-CLI,才300余行代码,学它!...相关推荐

  1. Vue团队核心成员开发的39行小工具 install-pkg 安装包,值得一学!

    1. 前言 大家好,我是若川.最近组织了源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构 ...

  2. “树懒”用超表也能快如闪电|微商案例

    超级表格 典型案例  使用场景:订单收集,仓库发货 典型用户:微商-现代东方贸易 协作如果跟不上,微商必然变"树懒" 随着消费群体日益庞大,国人需求日新月异.微商开始基于微信&qu ...

  3. python读什么文件最快的软件_这些方法,能够让你的 Python 程序快如闪电

    原标题:这些方法,能够让你的 Python 程序快如闪电 来源:机器之心 讨厌 Python 的人总是会说,他们不想用 Python 的一个重要原因是 Python 很慢.而事实上,无论使用什么编程语 ...

  4. 快如闪电的开源搜索引擎:Typesense ,比Elasticsearch更快更易用

    一个快如闪电的开源搜索引擎,就如同Redis使用内存存储数据一样(这在Redis出现之前是不敢想象的,几乎没有人把全部的mysql数据存储到内存中),搜索引擎也是,之前各家做法都是尽量存磁盘,需要的时 ...

  5. 快如闪电的安装LNMP套件

    快如闪电的安装php7.4套件(centos 7) 快如闪电的安装php7.4套件(centos 8) win 10系统使用 docker 快速搭建 centos 8 的 php7.4 和 mysql ...

  6. 计算机l特键,高逼格的三大电脑快捷键,快如闪电!

    原标题:高逼格的三大电脑快捷键,快如闪电! 我们日常的工作和生活都高不开电脑的使用,不断增多的工作逐渐要求我们对电脑的熟练度不断提高,作为一名职场小白,有没有快速的方法能提高我们的操作速度度,从而提高 ...

  7. 不用着急换新电脑了,「Macbooster」让您的旧Mac一样快如闪电~☛完美破解版☜

    每年的三月份,苹果总会推出一系列新品,如果您选择换新的话,那您又得心疼自己的小金库了吧!纵使是强大的Macbook,使用久了都可能出现卡顿,运行缓慢的情况.我想最经济的方法就是通过修改配置或更好的管理 ...

  8. vue理由设置_在你的下一个Web应用中使用Vue.js的三个理由

    Vue.js是那么地易上手,它在提供了大量开箱即用的功能的同时也提供了良好的性能.请继续阅读以下事例及代码片段以便更加了解Vue.js. 选择一个JavaScript框架真是太难了--因为有太多的框架 ...

  9. vue学习-v-if v-for优先级、data、key、diff算法、vue组件化、vue设计原则、组件模板只有一个根元素、MVC.MVP,MVVM

    1:v-if和v-for哪个优先级更高?如果两个同时出现,应该怎么优化得到更好的性能? //在vue页面中 同时使用v-for与v-if后,打印渲染函数. console.log(app.$optio ...

最新文章

  1. 现在学java还是python好_该学Java还是Python?
  2. hdu 2988 Strange fuction【模拟退火】
  3. 关于现代软件工程学习
  4. C++ 泛型编程(二):非类型模板参数,模板特化,模板的分离编译
  5. Git学习笔记(2) --- References探寻
  6. 穿戴式设备的用户体验设计-郝华奇
  7. centos mysql无法登录,解决centos下MySQL登录1045问题
  8. 一名计算机专业新生代农民工的五年求学之路,从“低谷”到“山峰”
  9. mysql行级锁 select for update
  10. iOS 编译后的Archiveing 界面在 Windows-organizer 下
  11. 数据结构和算法——八种常用的排序算法------基数排序
  12. 简单理解javascript中的原型对象,实现对之间共享属性和行为
  13. Schedule定时器cron表达式
  14. 当失控的预装行为以非正当手段伸向行货机时_北京鼎开预装刷机数据统计apk(rom固化版)分析...
  15. 教大家一个可以用迅雷全速下载百度网盘文件的方法
  16. 前端工程师的摸鱼日常(12)
  17. 操作系统的发展历程及linux的发展
  18. Emacs Stardict
  19. 蓝色的建站网站页脚布局代码
  20. 使用Docker搭建Nextcloud个人工作中心(同步盘+离线下载等功能)以及DNS服务器搭建

热门文章

  1. matlab emf 读取,20140219-Emf_Demo EMF 矢量图 可以读取和保存EMF 的封闭类 非常实用 matlab 238万源代码下载- www.pudn.com...
  2. 第2章 Python 数字图像处理(DIP) --数字图像基础3 - 图像内插 - 最近邻内插 - 双线性插值 - 双三次内插 - 图像放大
  3. excel换行按什么键_电脑结束任务按什么键
  4. spring整合mybatis接口无法注入问题
  5. windebug常用命令
  6. (转)java内部类详解
  7. 获取文本中你须要的字段的 几个命令 grep awk cut tr sed
  8. 安装SQL提示重启电脑失败,解决办法
  9. ios apple pay 证书配置
  10. Install OpenStack Kilo Dashboard wiht Nginx + uWSGI On RHEL7.1