源码学习目录

1. 前言

1.1 环境

  1. 操作系统: macOS 11.5.2
  2. 浏览器: Chrome 94.0.4606.81
  3. create-vue 3.0.1

    1.2 阅读该文章可以get以下知识点

  4. 学会全新的官方脚手架工具 create-vue 的使用和原理

  5. 学会使用 VSCode 直接打开 github 项目
  6. 学会使用测试用例调试源码

    2. 开始

    2.1 如何使用

    ```javascript // 安装vue3工程 npm init vue@3 // 安装vue2工程 npm init vue@2

// 通过输入下面的命令,可以看到npm 的用法 npm init -h
npm init [—force|-f|—yes|-y|—scope] npm init <@scope> (same as npx <@scope>/create) npm init [<@scope>/] (same as npx [<@scope>/]create-<name>)

npm init => npx create-vue

  1. npx create-vue会去下载包,并执行bin里面的脚本<br />[npm init文档](https://docs.npmjs.com/cli/v6/commands/npm-init)
  2. <a name="t3FLa"></a>
  3. ## 2.2 index.js 引入的npm包
  4. ```javascript
  5. import fs from 'fs'
  6. import path from 'path'
  7. // 命令行args获取
  8. import minimist from 'minimist'
  9. // 交互式命令行
  10. import prompts from 'prompts'
  11. // 标准输入的颜色
  12. import { red, green, bold } from 'kolorist'

kolorist
minimist
prompts
这些常用库,基本用法都差不多,之前有讲过

2.3 index.js源码

  1. // 校验包名称
  2. function isValidPackageName(projectName) {
  3. return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)
  4. }
  5. // 将一些情况转义成中划线或删除
  6. function toValidPackageName(projectName) {
  7. return projectName
  8. .trim()
  9. .toLowerCase()
  10. .replace(/\s+/g, '-')
  11. .replace(/^[._]/, '')
  12. .replace(/[^a-z0-9-~]+/g, '-')
  13. }
  14. // 判断是否存在该目录
  15. function canSafelyOverwrite(dir) {
  16. return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0
  17. }
  18. // 清空文件和文件夹
  19. function emptyDir(dir) {
  20. postOrderDirectoryTraverse(
  21. dir,
  22. (dir) => fs.rmdirSync(dir),
  23. (file) => fs.unlinkSync(file)
  24. )
  25. }
  26. async function init() {
  27. // 脚本运行地址
  28. const cwd = process.cwd()
  29. // possible options:
  30. // --default
  31. // --typescript / --ts
  32. // --jsx
  33. // --router / --vue-router
  34. // --pinia
  35. // --with-tests / --tests / --cypress
  36. // --force (for force overwriting)
  37. // 参数
  38. const argv = minimist(process.argv.slice(2), {
  39. // 设置别名
  40. alias: {
  41. typescript: ['ts'],
  42. 'with-tests': ['tests', 'cypress'],
  43. router: ['vue-router']
  44. },
  45. // all arguments are treated as booleans
  46. // 参数处理成boolean
  47. boolean: true
  48. })
  49. // if any of the feature flags is set, we would skip the feature prompts
  50. // use `??` instead of `||` once we drop Node.js 12 support
  51. // 这段代码的目的是如果传入了下面的这些参数,不需要进入prompt去设置了
  52. const isFeatureFlagsUsed =
  53. typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.pinia || argv.tests) ===
  54. 'boolean'
  55. // 目标目录
  56. let targetDir = argv._[0]
  57. // 默认工程名,存在目标文件夹使用文件夹名称,不存在使用vue-project
  58. const defaultProjectName = !targetDir ? 'vue-project' : targetDir
  59. // 强制写入
  60. const forceOverwrite = argv.force
  61. let result = {}
  62. try {
  63. // Prompts:
  64. // - Project name:
  65. // - whether to overwrite the existing directory or not?
  66. // - enter a valid package name for package.json
  67. // - Project language: JavaScript / TypeScript
  68. // - Add JSX Support?
  69. // - Install Vue Router for SPA development?
  70. // - Install Pinia for state management?
  71. // - Add Cypress for testing?
  72. // 交互命令,生成配置
  73. result = await prompts(
  74. [
  75. // 项目名称
  76. {
  77. name: 'projectName',
  78. // 如果type=null是不会显示的
  79. type: targetDir ? null : 'text',
  80. message: 'Project name:',
  81. initial: defaultProjectName,
  82. onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
  83. },
  84. // 是否覆盖
  85. {
  86. name: 'shouldOverwrite',
  87. // 校验目标目录是否有效
  88. type: () => (canSafelyOverwrite(targetDir) || forceOverwrite ? null : 'confirm'),
  89. message: () => {
  90. const dirForPrompt =
  91. targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`
  92. return `${dirForPrompt} is not empty. Remove existing files and continue?`
  93. }
  94. },
  95. // 覆盖检测,不覆盖,会抛出一下错误,直接阻塞后面的任务
  96. {
  97. name: 'overwriteChecker',
  98. type: (prev, values = {}) => {
  99. if (values.shouldOverwrite === false) {
  100. throw new Error(red('✖') + ' Operation cancelled')
  101. }
  102. return null
  103. }
  104. },
  105. // 包名称
  106. {
  107. name: 'packageName',
  108. // 校验包名是否有效,自己实现的,没有用validate-npm-package-name这个库,减少项目依赖
  109. type: () => (isValidPackageName(targetDir) ? null : 'text'),
  110. message: 'Package name:',
  111. initial: () => toValidPackageName(targetDir),
  112. // 非法校验,如果校验不通过,不会执行到下一步
  113. validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'
  114. },
  115. // ts
  116. {
  117. name: 'needsTypeScript',
  118. type: () => (isFeatureFlagsUsed ? null : 'toggle'),
  119. message: 'Add TypeScript?',
  120. initial: false,
  121. active: 'Yes',
  122. inactive: 'No'
  123. },
  124. //jsx
  125. {
  126. name: 'needsJsx',
  127. type: () => (isFeatureFlagsUsed ? null : 'toggle'),
  128. message: 'Add JSX Support?',
  129. initial: false,
  130. active: 'Yes',
  131. inactive: 'No'
  132. },
  133. // router
  134. {
  135. name: 'needsRouter',
  136. type: () => (isFeatureFlagsUsed ? null : 'toggle'),
  137. message: 'Add Vue Router for Single Page Application development?',
  138. initial: false,
  139. active: 'Yes',
  140. inactive: 'No'
  141. },
  142. // pinia 状态管理库
  143. {
  144. name: 'needsPinia',
  145. type: () => (isFeatureFlagsUsed ? null : 'toggle'),
  146. message: 'Add Pinia for state management?',
  147. initial: false,
  148. active: 'Yes',
  149. inactive: 'No'
  150. },
  151. // 测试
  152. {
  153. name: 'needsTests',
  154. type: () => (isFeatureFlagsUsed ? null : 'toggle'),
  155. message: 'Add Cypress for testing?',
  156. initial: false,
  157. active: 'Yes',
  158. inactive: 'No'
  159. }
  160. ],
  161. // 第二个参数,如果取消操作,可以返回一些信息
  162. {
  163. onCancel: () => {
  164. throw new Error(red('✖') + ' Operation cancelled')
  165. }
  166. }
  167. )
  168. } catch (cancelled) {
  169. console.log(cancelled.message)
  170. //结束进程
  171. process.exit(1)
  172. }
  173. // `initial` won't take effect if the prompt type is null
  174. // so we still have to assign the default values here
  175. // 通过参数设置默认值,有可能不存在
  176. const {
  177. packageName = toValidPackageName(defaultProjectName),
  178. shouldOverwrite,
  179. needsJsx = argv.jsx,
  180. needsTypeScript = argv.typescript,
  181. needsRouter = argv.router,
  182. needsPinia = argv.pinia,
  183. needsTests = argv.tests
  184. } = result
  185. // 获取目标目录
  186. const root = path.join(cwd, targetDir)
  187. // 强制写入
  188. if (shouldOverwrite) {
  189. emptyDir(root)
  190. // 不存在创建
  191. } else if (!fs.existsSync(root)) {
  192. fs.mkdirSync(root)
  193. }
  194. console.log(`\nScaffolding project in ${root}...`)
  195. // 包名称和版本
  196. const pkg = { name: packageName, version: '0.0.0' }
  197. fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))
  198. // todo:
  199. // work around the esbuild issue that `import.meta.url` cannot be correctly transpiled
  200. // when bundling for node and the format is cjs
  201. // const templateRoot = new URL('./template', import.meta.url).pathname
  202. // 临时文件夹
  203. const templateRoot = path.resolve(__dirname, 'template')
  204. // 渲染模板
  205. const render = function render(templateName) {
  206. const templateDir = path.resolve(templateRoot, templateName)
  207. renderTemplate(templateDir, root)
  208. }
  209. // 基于配置渲染相关的模板
  210. // Render base template
  211. render('base')
  212. // Add configs.
  213. if (needsJsx) {
  214. render('config/jsx')
  215. }
  216. if (needsRouter) {
  217. render('config/router')
  218. }
  219. if (needsPinia) {
  220. render('config/pinia')
  221. }
  222. if (needsTests) {
  223. render('config/cypress')
  224. }
  225. if (needsTypeScript) {
  226. render('config/typescript')
  227. }
  228. // Render code template.
  229. // prettier-ignore
  230. const codeTemplate =
  231. (needsTypeScript ? 'typescript-' : '') +
  232. (needsRouter ? 'router' : 'default')
  233. render(`code/${codeTemplate}`)
  234. // Render entry file (main.js/ts).
  235. if (needsPinia && needsRouter) {
  236. render('entry/router-and-pinia')
  237. } else if (needsPinia) {
  238. render('entry/pinia')
  239. } else if (needsRouter) {
  240. render('entry/router')
  241. } else {
  242. render('entry/default')
  243. }
  244. // Cleanup.
  245. // 如果使用ts
  246. if (needsTypeScript) {
  247. // rename all `.js` files to `.ts`
  248. // rename jsconfig.json to tsconfig.json
  249. // 替换文件名
  250. preOrderDirectoryTraverse(
  251. root,
  252. () => {},
  253. (filepath) => {
  254. if (filepath.endsWith('.js')) {
  255. fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))
  256. } else if (path.basename(filepath) === 'jsconfig.json') {
  257. fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))
  258. }
  259. }
  260. )
  261. // Rename entry in `index.html`
  262. const indexHtmlPath = path.resolve(root, 'index.html')
  263. const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
  264. fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
  265. }
  266. // 不需要测试,直接删除
  267. if (!needsTests) {
  268. // All templates assumes the need of tests.
  269. // If the user doesn't need it:
  270. // rm -rf cypress **/__tests__/
  271. preOrderDirectoryTraverse(
  272. root,
  273. (dirpath) => {
  274. const dirname = path.basename(dirpath)
  275. if (dirname === 'cypress' || dirname === '__tests__') {
  276. emptyDir(dirpath)
  277. fs.rmdirSync(dirpath)
  278. }
  279. },
  280. () => {}
  281. )
  282. }
  283. // Instructions:
  284. // Supported package managers: pnpm > yarn > npm
  285. // Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
  286. // it is not possible to tell if the command is called by `pnpm init`.
  287. // 包管理工具 pnpm > yarn > npm
  288. const packageManager = /pnpm/.test(process.env.npm_execpath)
  289. ? 'pnpm'
  290. : /yarn/.test(process.env.npm_execpath)
  291. ? 'yarn'
  292. : 'npm'
  293. // README generation
  294. fs.writeFileSync(
  295. path.resolve(root, 'README.md'),
  296. // 生成readme
  297. generateReadme({
  298. projectName: result.projectName || defaultProjectName,
  299. packageManager,
  300. needsTypeScript,
  301. needsTests
  302. })
  303. )
  304. console.log(`\nDone. Now run:\n`)
  305. if (root !== cwd) {
  306. console.log(` ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
  307. }
  308. console.log(` ${bold(green(getCommand(packageManager, 'install')))}`)
  309. console.log(` ${bold(green(getCommand(packageManager, 'dev')))}`)
  310. console.log()
  311. }
  312. // 初始化运行init,捕获异常,打印error
  313. init().catch((e) => {
  314. console.error(e)
  315. })

3. 总结

  1. 整个index.js代码非常简练,熟悉了prompt的一些用法,ru type=null,不显示,validate校验输入
  2. 为了使npx下载速度快,使用的包非常少
  3. render渲染模板,模板都是写在香港文件夹下的,需要渲染生成的模块直接拷贝,非常方便

    4. 参考文档

  4. https://juejin.cn/post/7018344866811740173

  5. https://github.com/vuejs/create-vue