百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Vue 发布新版脚手架工具,300 行代码轻盈新生

bigegpt 2024-08-29 11:27 1 浏览

作者:佚名来源:前端大全

1. 前言

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

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[4]上写着,An easy way to start a Vue project。一种简单的初始化vue项目的方式。

npm init vue@next 

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

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

npm init vue@next

最终cd vue3-project、npm install 、npm 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 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 

npx vue-cli

npm init vue@next (npx 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 时,会先执行钩子函数 pretest。run-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') 

我们可以在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.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 初始化项目的。

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-CLI中 vue create vue-project 命令是用官方的npm包validate-npm-package-name[16],删除文件夹一般都是使用 rimraf[17]。而 create-vue 是自己实现emptyDir和isValidPackageName。

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

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...