create-vue快速生成项目,到底是怎么做的

前言

前段时间我们有去探索了一下vue-clicra的原理,生成项目的过程,他是基于webpack的,但是今天我们的主角是create-vue,他是基于vite的,为什么要使用vite而不是webpack呢?因为vitewebpack快。 我们使用vue-clivite快速生成一个项目分别耗时如下:

webpack

vite

就目前的图来看vite在构建、启动项目的时候会比webpack快10倍,这还是仅仅只是简单的项目,前端的项目复杂程度本来就是呈指数上升的,这时候vitewebpack可谓是天差地别。那么接下来我将带领大家一步一步去探索vite的各种优点。

准备工作

首先我们接着前言里面如何用vite去初始化一个Vue3的项目吧。 官方推荐使用

npm init vue@latest 这个命令将会去创建一个create-vue脚手架,实际的本质就是去下载create-vue这个包,然后执行index.ts文件。

前置导入模块

import * as fs from 'node:fs'
import * as path from 'node:path'
// fileURLToPath用来获取根路径的,源码里面没有,在下文path.resolve(__dirname, 'template') 那里会报错。
// 原因:因为在package.json里面定义的为ESM规范,而在index.ts文件里面,
出现了Common.js规范,两种规范下的实现是不同的,所以会报错:__dirname is not define
import { fileURLToPath } from 'url' 

import minimist from 'minimist' // 解析命令行参数
import prompts from 'prompts' // 命令行交互
import { red, green, bold } from 'kolorist' // 有色输出

// 在控制台打印的banner
// gradientBanner 内容为下面的a变量
// const defaultBanner = 'Vue.js - The Progressive JavaScript Framework'
import * as banners from './utils/banners'

import renderTemplate from './utils/renderTemplate'
import { postOrderDirectoryTraverse, preOrderDirectoryTraverse } from './utils/directoryTraverse'
import generateReadme from './utils/generateReadme'
import getCommand from './utils/getCommand'
import renderEslint from './utils/renderEslint' 

工具函数

isValidPackageName

我们发现在cra、vue-cli中都是借助于validate-npm-package-name这个包实现的,这里面选择了重写这个库,省去了库的安装,读取等操作,进而提升速度。

function isValidPackageName(projectName) {return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)
} 

toValidPackageName

如果包名检查不合法就要把他变为合法的,比如toValidPackageName(myProject) => myproject,还会去空格等。

function toValidPackageName(projectName) {return projectName.trim().toLowerCase().replace(/\s+/g, '-') // 匹配空白字符到的第一个字符开始,直到匹配失败 eg:'lov e' => 'lov-e'.replace(/^[._]/, '') // 匹配 '.' '_' 为 ''.replace(/[^a-z0-9-~]+/g, '-') // 包含a-z、 0-9 - ~ 重复多个
} 

canSkipEmptying

顾名思义,读取操作文件是否可以跳过

function canSkipEmptying(dir: string) {if (!fs.existsSync(dir)) { // fs.exists() 如果路径存在,则返回 `true`,否则返回 `false`。return true}const files = fs.readdirSync(dir) // 返回一个不包括 `'.'` 和 `'..'` 的文件名的数组。if (files.length === 0) {return true}if (files.length === 1 && files[0] === '.git') {return true}return false
} 

emptyDir

判断是否是一个空目录

function emptyDir(dir) {if (!fs.existsSync(dir)) { // fs.exists() 如果路径存在,则返回 `true`,否则返回 `false`。return}postOrderDirectoryTraverse(dir,(dir) => fs.rmdirSync(dir), // 同步移除文件夹(file) => fs.unlinkSync(file) // 同步删除文件,但不删除文件夹)
} 

index.ts入口文件中,我们发现init作为入口函数,我们来调试一下init函数。

init

async function init() {console.log()console.log( // 判断是否在终端上下文中和颜色深度???process.stdout.isTTY && process.stdout.getColorDepth() > 8? banners.gradientBanner // 彩色的: banners.defaultBanner // 黑白的)console.log()const cwd = process.cwd() // 获取当前根路径// possible options:// --default// --typescript / --ts// --jsx// --router / --vue-router// --pinia// --with-tests / --tests (equals to `--vitest --cypress`)// --vitest// --cypress// --playwright// --eslint// --eslint-with-prettier (only support prettier through eslint for simplicity)// --force (for force overwriting)const argv = minimist(process.argv.slice(2), { // 解析命令行参数 argv位启动目录与当前文件目录alias: {typescript: ['ts'],'with-tests': ['tests'],router: ['vue-router']},string: ['_'],// all arguments are treated as booleansboolean: true})// if any of the feature flags is set, we would skip the feature prompts如果设置了这些特性,那么下一次会跳过走预设配置缓存const isFeatureFlagsUsed =typeof (argv.default ??argv.ts ??argv.jsx ??argv.router ??argv.pinia ??argv.tests ??argv.vitest ??argv.cypress ??argv.playwright ??argv.eslint) === 'boolean'let targetDir = argv._[0]const defaultProjectName = !targetDir ? 'vue-project' : targetDir // 默认目录const forceOverwrite = argv.force // 强制覆盖 

可选配置项

 // 这时候会在终端弹出可选命令项,这个result就是保存这些选项的。let result: {projectName?: stringshouldOverwrite?: booleanpackageName?: stringneedsTypeScript?: booleanneedsJsx?: booleanneedsRouter?: booleanneedsPinia?: booleanneedsVitest?: booleanneedsE2eTesting?: false | 'cypress' | 'playwright'needsEslint?: booleanneedsPrettier?: boolean} = {}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 Pinia for state management?// - Add Cypress for testing?// - Add Playwright for end-to-end testing?// - Add ESLint for code quality?// - Add Prettier for code formatting?result = await prompts([{name: 'projectName',type: targetDir ? null : 'text',message: 'Project name:',initial: defaultProjectName,onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)},{name: 'shouldOverwrite',type: () => (canSkipEmptying(targetDir) || forceOverwrite ? null : 'confirm'),message: () => {const dirForPrompt =targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`return `${dirForPrompt} is not empty. Remove existing files and continue?`}},{name: 'overwriteChecker',type: (prev, values) => {if (values.shouldOverwrite === false) {throw new Error(red('✖') + ' Operation cancelled')}return null}},{name: 'packageName',type: () => (isValidPackageName(targetDir) ? null : 'text'),message: 'Package name:',initial: () => toValidPackageName(targetDir),validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'},{name: 'needsTypeScript',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add TypeScript?',initial: false,active: 'Yes',inactive: 'No'},{name: 'needsJsx',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add JSX Support?',initial: false,active: 'Yes',inactive: 'No'},{name: 'needsRouter',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add Vue Router for Single Page Application development?',initial: false,active: 'Yes',inactive: 'No'},{name: 'needsPinia',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add Pinia for state management?',initial: false,active: 'Yes',inactive: 'No'},{name: 'needsVitest',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add Vitest for Unit Testing?',initial: false,active: 'Yes',inactive: 'No'},{name: 'needsE2eTesting',type: () => (isFeatureFlagsUsed ? null : 'select'),message: 'Add an End-to-End Testing Solution?',initial: 0,choices: (prev, answers) => [{ title: 'No', value: false },{title: 'Cypress',description: answers.needsVitest? undefined: 'also supports unit testing with Cypress Component Testing',value: 'cypress'},{title: 'Playwright',value: 'playwright'}]},{name: 'needsEslint',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add ESLint for code quality?',initial: false,active: 'Yes',inactive: 'No'},{name: 'needsPrettier',type: (prev, values) => {if (isFeatureFlagsUsed || !values.needsEslint) {return null}return 'toggle'},message: 'Add Prettier for code formatting?',initial: false,active: 'Yes',inactive: 'No'}],{onCancel: () => {throw new Error(red('✖') + ' Operation cancelled')}})} catch (cancelled) {console.log(cancelled.message)process.exit(1)} 

合并默认配置与选择性配置

 // `initial` won't take effect if the prompt type is null// so we still have to assign the default values hereconst {projectName,packageName = projectName ?? defaultProjectName,shouldOverwrite = argv.force,needsJsx = argv.jsx,needsTypeScript = argv.typescript,needsRouter = argv.router,needsPinia = argv.pinia,needsVitest = argv.vitest || argv.tests,needsEslint = argv.eslint || argv['eslint-with-prettier'],needsPrettier = argv['eslint-with-prettier']} = resultconst { needsE2eTesting } = resultconst needsCypress = argv.cypress || argv.tests || needsE2eTesting === 'cypress'const needsCypressCT = needsCypress && !needsVitestconst needsPlaywright = argv.playwright || needsE2eTesting === 'playwright'const root = path.join(cwd, targetDir) // 获取根目录if (fs.existsSync(root) && shouldOverwrite) {emptyDir(root) //如果根目录是空的,并且强制覆盖那就先删除,再新建} else if (!fs.existsSync(root)) {fs.mkdirSync(root) // 如果没有文件,则直接创建}console.log(`\nScaffolding project in ${root}...`)// 生成package.json,写入name、versionconst pkg = { name: packageName, version: '0.0.0' }fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))// 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 __filenameNew = fileURLToPath(import.meta.url) // 获取rootconst __dirnameNew = path.dirname(__filenameNew)const templateRoot = path.resolve(__dirnameNew, 'template')const render = function render(templateName) {const templateDir = path.resolve(templateRoot, templateName)// 这个函数,就是按照配置加载包的defaultConfig的,之后merge进,package.json中renderTemplate(templateDir, root) } 

根据用户配置加载包的config

 // Render base templaterender('base')// Add configs.if (needsJsx) {render('config/jsx')}if (needsRouter) {render('config/router')}if (needsPinia) {render('config/pinia')}if (needsVitest) {render('config/vitest')}if (needsCypress) {render('config/cypress')}if (needsCypressCT) {render('config/cypress-ct')}if (needsPlaywright) {render('config/playwright')}if (needsTypeScript) {render('config/typescript')// Render tsconfigsrender('tsconfig/base')if (needsCypress) {render('tsconfig/cypress')}if (needsCypressCT) {render('tsconfig/cypress-ct')}if (needsPlaywright) {render('tsconfig/playwright')}if (needsVitest) {render('tsconfig/vitest')}}// Render ESLint configif (needsEslint) {renderEslint(root, { needsTypeScript, needsCypress, needsCypressCT, needsPrettier })}// Render code template.// prettier-ignoreconst codeTemplate =(needsTypeScript ? 'typescript-' : '') +(needsRouter ? 'router' : 'default')render(`code/${codeTemplate}`)// Render entry file (main.js/ts).if (needsPinia && needsRouter) {render('entry/router-and-pinia')} else if (needsPinia) {render('entry/pinia')} else if (needsRouter) {render('entry/router')} else {render('entry/default')}// Cleanup.// We try to share as many files between TypeScript and JavaScript as possible.// If that's not possible, we put `.ts` version alongside the `.js` one in the templates.// So after all the templates are rendered, we need to clean up the redundant files.// (Currently it's only `cypress/plugin/index.ts`, but we might add more in the future.)// (Or, we might completely get rid of the plugins folder as Cypress 10 supports `cypress.config.ts`)if (needsTypeScript) {// Convert the JavaScript template to the TypeScript// Check all the remaining `.js` files:// - If the corresponding TypeScript version already exists, remove the `.js` version.// - Otherwise, rename the `.js` file to `.ts`// Remove `jsconfig.json`, because we already have tsconfig.json// `jsconfig.json` is not reused, because we use solution-style `tsconfig`s, which are much more complicated.preOrderDirectoryTraverse(root,() => {},(filepath) => {if (filepath.endsWith('.js')) {const tsFilePath = filepath.replace(/\.js$/, '.ts')if (fs.existsSync(tsFilePath)) {fs.unlinkSync(filepath)} else {fs.renameSync(filepath, tsFilePath)}} else if (path.basename(filepath) === 'jsconfig.json') {fs.unlinkSync(filepath)}}) 

生成入口index.html文件

 // 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'))} else {// Remove all the remaining `.ts` filespreOrderDirectoryTraverse(root,() => {},(filepath) => {if (filepath.endsWith('.ts')) {fs.unlinkSync(filepath)}})} 

包管理器的选择使用

 const userAgent = process.env.npm_config_user_agent ?? ''const packageManager = /pnpm/.test(userAgent) ? 'pnpm' : /yarn/.test(userAgent) ? 'yarn' : 'npm' 

生成README.md

 fs.writeFileSync(path.resolve(root, 'README.md'),generateReadme({projectName: result.projectName ?? result.packageName ?? defaultProjectName,packageManager,needsTypeScript,needsVitest,needsCypress,needsPlaywright,needsCypressCT,needsEslint})) 

终端输出,初始化完成

 console.log(`\nDone. Now run:\n`)if (root !== cwd) {console.log(`${bold(green(`cd ${path.relative(cwd, root)}`))}`) // cd myproject}console.log(`${bold(green(getCommand(packageManager, 'install')))}`) // npm|yarn|pnpm installif (needsPrettier) {console.log(`${bold(green(getCommand(packageManager, 'lint')))}`) // eslint检查npm|yarn|pnpm lint}console.log(`${bold(green(getCommand(packageManager, 'dev')))}`) // 启动 npm|yarn|pnpm devconsole.log()
} 

到这里就可以在根目录生成一个项目,我们可以用vite来启动项目啦!

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/web2022050903/article/details/129494756