IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue 团队公开快如闪电的全新脚手架工具,未来将替代 Vue-CLI,才300余行代码,学它!... -> 正文阅读

[JavaScript知识库]Vue 团队公开快如闪电的全新脚手架工具,未来将替代 Vue-CLI,才300余行代码,学它!...

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

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

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

ff34aafecca8451782b3d485ea71f8c3.png
npm init vue@next

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

0cacb1d47e454fe0a2faac6ef3d40468.png
初始化页面

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?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-project
d0c2c92db8d148cd440835d9214b1ed4.png
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/2907

import?{?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 初始化项目的。

4dfac8627c39d70d8924629dc9152729.png
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?booleans
????boolean:?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?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[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'))
??}

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.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:ci

pnpm?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?

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

48ea46ad379c287f6b6f7d3f2ead7785.gif

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

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

8b19b9b1a24f52305f09f5a006722c43.png

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

今日话题

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

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-23 12:23:03  更:2021-10-23 12:23:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:41:24-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码