从零搭建类Vue-CLI的本地脚手架工具:实现Webpack到Rspack的迁移

从零搭建类Vue-CLI的本地脚手架工具:实现Webpack到Rspack的迁移

前言

在前端开发的世界中,脚手架工具已成为提升开发效率的关键武器。像Vue-CLI这样的工具帮助开发者轻松创建和管理项目,但有时我们需要根据团队特定需求定制自己的脚手架。

最近,我将一个基于Vue 2的项目从Webpack迁移到了Rspack,体验到了构建速度的飞跃提升。为了让团队成员能够无缝使用这套配置,我决定将其封装为一个本地脚手架工具------类似Vue-CLI,但更加轻量和专注于我们的特定需求。

在本文中,我将分享如何从零开始构建这样一个工具,包括完整的实现思路、核心代码和实用技巧,帮助你理解脚手架工具的工作原理,同时展示Webpack到Rspack的平滑迁移路径。

目录

脚手架工具的基本原理

脚手架工具本质上是一个根据用户输入生成项目结构的程序。理解其工作原理对我们自己实现类似工具至关重要。

核心流程

一个典型的脚手架工具通常包括以下四个核心步骤:

  1. 命令解析与交互:接收并解析用户的命令行输入,通过交互式问答收集配置选项
  2. 模板选择与配置:根据用户选择的选项,确定使用哪个项目模板及如何配置
  3. 模板渲染:将用户配置应用到模板中,生成定制化的项目文件
  4. 后处理与优化:执行依赖安装、Git初始化等后续操作

当用户执行vue create my-app时,Vue-CLI就是按照这个流程工作的:先询问一系列问题(选择Vue版本、是否使用TypeScript等),然后根据回答生成相应的项目结构。

与传统方式的比较

相比于手动创建项目或从示例项目复制,使用脚手架工具有几个明显优势:

  • 标准化:确保团队中所有项目遵循相同的最佳实践
  • 效率提升:避免重复配置常见功能
  • 维护简化:集中更新模板即可应用到所有新项目
  • 定制灵活:可根据实际需求选择功能模块

项目初始化与核心依赖

让我们开始实际构建我们的脚手架工具。首先创建项目并安装必要依赖:

bash 复制代码
# 创建项目目录
mkdir vue-rspack-cli
cd vue-rspack-cli

# 初始化npm项目
npm init -y

# 安装核心依赖
npm install commander inquirer chalk fs-extra handlebars glob

这些依赖各自承担重要角色:

依赖 作用
commander 命令行界面解析与设计
inquirer 交互式命令行问答
chalk 终端彩色输出,提升用户体验
fs-extra 增强的文件系统操作
handlebars 模板渲染引擎
glob 文件路径模式匹配

接下来,我们设计一个清晰的项目结构:

python 复制代码
vue-rspack-cli/
├── bin/               # CLI入口文件
│   └── cli.js         
├── lib/               # 核心逻辑
│   ├── create.js      # 创建项目的主要逻辑
│   └── utils.js       # 工具函数
├── templates/         # 项目模板
│   └── vue-rspack/    
│       ├── template/  # 实际模板文件
│       └── meta.js    # 模板配置
├── package.json
└── README.md

这种结构将关注点分离,使得代码更加模块化和可维护。

命令行界面设计与实现

一个直观友好的命令行界面是良好用户体验的第一步。首先,在package.json中添加bin字段,使我们的工具可全局执行:

json 复制代码
{
  "name": "vue-rspack-cli",
  "version": "1.0.0",
  "description": "CLI for scaffolding Vue projects with Rspack",
  "bin": {
    "vue-rspack": "bin/cli.js"
  },
  "main": "bin/cli.js",
  "files": [
    "bin",
    "lib",
    "templates"
  ],
  "dependencies": {
    "commander": "^9.0.0",
    "inquirer": "^8.2.0",
    "chalk": "^4.1.2",
    "fs-extra": "^10.0.0",
    "handlebars": "^4.7.7",
    "glob": "^8.0.3"
  }
}

然后,在bin/cli.js中实现命令行入口:

javascript 复制代码
#!/usr/bin/env node

const program = require('commander');
const chalk = require('chalk');
const { create } = require('../lib/create');
const packageInfo = require('../package.json');

// 设置版本号和描述
program
  .version(packageInfo.version)
  .description('Vue项目脚手架工具,支持Rspack构建');

// 创建项目命令
program
  .command('create <project-name>')
  .description('创建一个新的Vue项目与Rspack配置')
  .option('-f, --force', '覆盖目标目录(如果存在)')
  .option('-t, --template <template>', '指定项目模板', 'vue-rspack')
  .option('-s, --skip-git', '跳过git初始化')
  .option('-n, --no-install', '跳过依赖安装')
  .action((projectName, options) => {
    console.log(chalk.blue(`Vue Rspack CLI v${packageInfo.version}`));
    console.log(chalk.blue('✨ 正在创建项目...'));
    create(projectName, options);
  });

// 添加更丰富的帮助信息
program
  .on('--help', () => {
    console.log();
    console.log('示例:');
    console.log(`  ${chalk.gray('# 创建一个新项目')}`)
    console.log(`  $ ${chalk.cyan('vue-rspack create my-project')}`);
    console.log();
    console.log(`  ${chalk.gray('# 使用强制覆盖选项创建项目')}`)
    console.log(`  $ ${chalk.cyan('vue-rspack create my-project --force')}`);
    console.log();
  });

// 解析命令行参数
program.parse(process.argv);

// 如果没有提供参数,则显示帮助信息
if (!process.argv.slice(2).length) {
  program.outputHelp();
}

通过commander库,我们设计了一个清晰的命令行界面,包括版本号、可用命令及其选项,以及丰富的帮助信息。特别是添加了一些实用选项,如--force(强制覆盖)、--template(指定模板)等,提升了工具的灵活性。

项目创建核心逻辑

接下来,我们实现项目创建的核心逻辑。这是脚手架工具的心脏部分,负责处理目录检查、用户交互和模板渲染等关键任务。

lib/create.js中:

javascript 复制代码
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { readTemplateFiles, renderTemplate, executeCommand } = require('./utils');

/**
 * 创建项目
 * @param {string} projectName - 项目名称
 * @param {object} options - 选项
 */
async function create(projectName, options) {
  // 目标目录
  const cwd = process.cwd();
  const targetDir = path.join(cwd, projectName);
  
  // 检查目标目录是否存在
  if (fs.existsSync(targetDir)) {
    if (options.force) {
      await fs.remove(targetDir);
    } else {
      const { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: `目标目录 ${chalk.cyan(targetDir)} 已存在。请选择操作:`,
          choices: [
            { name: '覆盖', value: 'overwrite' },
            { name: '合并', value: 'merge' },
            { name: '取消', value: 'cancel' }
          ]
        }
      ]);
      
      if (action === 'cancel') {
        console.log(chalk.red('✘ 操作取消'));
        return;
      }
      
      if (action === 'overwrite') {
        console.log(`\n${chalk.yellow(`正在删除 ${targetDir}...`)}`);
        await fs.remove(targetDir);
      }
    }
  }
  
  // 收集项目信息
  const answers = await inquirer.prompt([
    {
      name: 'projectName',
      type: 'input',
      message: '项目名称:',
      default: projectName
    },
    {
      name: 'projectDescription',
      type: 'input',
      message: '项目描述:',
      default: `A Vue.js project with Rspack`
    },
    {
      name: 'author',
      type: 'input',
      message: '作者:',
      default: ''
    },
    {
      name: 'features',
      type: 'checkbox',
      message: '选择需要的功能:',
      choices: [
        { name: 'Babel', value: 'babel', checked: true },
        { name: 'TypeScript', value: 'typescript' },
        { name: 'Router', value: 'router' },
        { name: 'Vuex', value: 'vuex' },
        { name: 'CSS预处理器', value: 'css-pre-processors' },
        { name: 'Linter / Formatter', value: 'linter' },
        { name: 'Unit测试', value: 'unit-testing' },
        { name: 'E2E测试', value: 'e2e-testing' }
      ]
    }
  ]);
  
  // 根据选择的功能进行额外配置
  if (answers.features.includes('css-pre-processors')) {
    const { cssPreProcessor } = await inquirer.prompt([
      {
        name: 'cssPreProcessor',
        type: 'list',
        message: '选择CSS预处理器:',
        choices: [
          { name: 'Sass/SCSS', value: 'sass' },
          { name: 'Less', value: 'less' },
          { name: 'Stylus', value: 'stylus' }
        ]
      }
    ]);
    answers.cssPreProcessor = cssPreProcessor;
  }
  
  // 记录开始时间,用于计算总耗时
  const startTime = Date.now();
  
  console.log(chalk.blue('\n✨ 正在生成项目文件...'));
  
  // 创建目标目录
  fs.ensureDirSync(targetDir);
  
  // 获取模板目录
  const templateDir = path.resolve(__dirname, `../templates/${options.template || 'vue-rspack'}/template`);
  
  try {
    // 读取并渲染模板文件
    const templateFiles = await readTemplateFiles(templateDir);
    await renderTemplate(templateFiles, targetDir, answers);
    
    // 安装依赖
    if (options.install !== false) {
      console.log(chalk.blue('\n📦 正在安装依赖...\n'));
      await executeCommand('npm', ['install'], { cwd: targetDir });
    }
    
    // Git初始化
    if (!options.skipGit) {
      console.log(chalk.blue('\n🗃️ 初始化Git仓库...\n'));
      await executeCommand('git', ['init'], { cwd: targetDir });
      await fs.writeFile(
        path.join(targetDir, '.gitignore'),
        `node_modules\ndist\n.DS_Store`
      );
      await executeCommand('git', ['add', '-A'], { cwd: targetDir });
      await executeCommand('git', ['commit', '-m', 'Initial commit'], { cwd: targetDir });
    }
    
    // 计算耗时
    const endTime = Date.now();
    const duration = ((endTime - startTime) / 1000).toFixed(2);
    
    console.log(chalk.green(`\n✅ 项目创建成功!耗时:${duration}s`));
    console.log('\n👉 开始使用:');
    console.log(`  ${chalk.cyan('cd')} ${projectName}`);
    
    if (options.install === false) {
      console.log(`  ${chalk.cyan('npm install')}`);
    }
    
    console.log(`  ${chalk.cyan('npm run dev')}`);
    console.log('\n🎉 祝您开发愉快!\n');
  } catch (err) {
    console.error(chalk.red('🚫 创建项目时出错:'), err);
    // 清理已创建的目录
    fs.removeSync(targetDir);
    process.exit(1);
  }
}

module.exports = {
  create
};

这个实现包含了一些重要的增强:

  1. 更完善的错误处理:当出错时,清理已创建的目录,避免留下不完整的项目
  2. 耗时计算:显示项目创建的总耗时,让用户了解性能情况
  3. 更多选项支持 :如--skip-git--no-install,为用户提供更多灵活性
  4. Git初始化:自动初始化Git仓库并创建初始提交,节省用户操作

模板系统的设计与实现

强大的模板系统是脚手架工具的核心。在lib/utils.js中,我们实现了文件读取和模板渲染的核心功能:

javascript 复制代码
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const handlebars = require('handlebars');
const chalk = require('chalk');
const { spawn } = require('child_process');

// 注册Handlebars助手函数,增强模板能力
handlebars.registerHelper('if_eq', function(a, b, opts) {
  return a === b ? opts.fn(this) : opts.inverse(this);
});

handlebars.registerHelper('unless_eq', function(a, b, opts) {
  return a === b ? opts.inverse(this) : opts.fn(this);
});

handlebars.registerHelper('if_includes', function(array, value, opts) {
  if (!array) return opts.inverse(this);
  return array.includes(value) ? opts.fn(this) : opts.inverse(this);
});

/**
 * 读取模板目录中的所有文件
 * @param {string} templateDir - 模板目录路径
 * @returns {Promise<Array>} - 模板文件列表
 */
function readTemplateFiles(templateDir) {
  return new Promise((resolve, reject) => {
    glob('**/*', {
      cwd: templateDir,
      dot: true,
      nodir: true,
      ignore: ['**/node_modules/**']
    }, (err, files) => {
      if (err) return reject(err);
      resolve(files);
    });
  });
}

/**
 * 渲染模板文件到目标目录
 * @param {Array} files - 模板文件列表
 * @param {string} targetDir - 目标目录
 * @param {object} data - 模板数据
 */
async function renderTemplate(files, targetDir, data) {
  const templateDir = path.resolve(__dirname, '../templates/vue-rspack/template');
  const fileCount = files.length;
  let processedCount = 0;
  
  for (const file of files) {
    const sourcePath = path.join(templateDir, file);
    const content = await fs.readFile(sourcePath, 'utf-8');
    
    // 检测是否是需要模板处理的文件类型
    const isTemplate = file.endsWith('.hbs') || 
                       file.endsWith('package.json') || 
                       file.endsWith('.js') ||
                       file.endsWith('.vue') ||
                       file.endsWith('.md') ||
                       file.endsWith('rspack.config.js');
    
    // 目标文件路径 (去掉 .hbs 扩展名)
    const targetPath = path.join(targetDir, file.replace(/\.hbs$/, ''));
    
    // 确保目标目录存在
    await fs.ensureDir(path.dirname(targetPath));
    
    if (isTemplate) {
      try {
        // 编译模板
        const template = handlebars.compile(content);
        const result = template(data);
        await fs.writeFile(targetPath, result);
      } catch (err) {
        console.error(`${chalk.red('✗')} 渲染模板失败: ${file}`, err);
        throw err;
      }
    } else {
      // 直接复制非模板文件
      await fs.copyFile(sourcePath, targetPath);
    }
    
    // 更新进度显示
    processedCount++;
    const percent = Math.floor((processedCount / fileCount) * 100);
    process.stdout.write(`\r${chalk.blue('📝')} 正在处理文件: ${percent}% 完成`);
  }
  
  console.log('\n');
  
  // 根据用户选择的特性处理项目文件
  await processProjectFeatures(targetDir, data);
}

/**
 * 根据用户选择的特性处理项目文件
 * @param {string} targetDir - 目标目录
 * @param {object} data - 用户选择的特性
 */
async function processProjectFeatures(targetDir, data) {
  // TypeScript支持
  if (data.features.includes('typescript')) {
    console.log(`${chalk.blue('🔧')} 配置TypeScript支持...`);
    await fs.ensureDir(path.join(targetDir, 'src/types'));
    await fs.writeFile(
      path.join(targetDir, 'tsconfig.json'),
      JSON.stringify({
        compilerOptions: {
          target: "es2016",
          module: "esnext",
          strict: true,
          jsx: "preserve",
          moduleResolution: "node",
          skipLibCheck: true,
          esModuleInterop: true,
          allowSyntheticDefaultImports: true,
          forceConsistentCasingInFileNames: true,
          useDefineForClassFields: true,
          sourceMap: true,
          baseUrl: ".",
          paths: {
            "@/*": ["src/*"]
          },
        },
        include: [
          "src/**/*.ts",
          "src/**/*.tsx",
          "src/**/*.vue",
        ],
        exclude: [
          "node_modules"
        ]
      }, null, 2)
    );
  }
  
  // Vue Router
  if (data.features.includes('router')) {
    console.log(`${chalk.blue('🔧')} 配置Vue Router...`);
    await fs.ensureDir(path.join(targetDir, 'src/router'));
    // 这里可以添加路由相关文件
  }
  
  // Vuex
  if (data.features.includes('vuex')) {
    console.log(`${chalk.blue('🔧')} 配置Vuex...`);
    await fs.ensureDir(path.join(targetDir, 'src/store'));
    // 这里可以添加状态管理相关文件
  }
  
  // CSS预处理器
  if (data.cssPreProcessor) {
    console.log(`${chalk.blue('🔧')} 配置${data.cssPreProcessor}预处理器...`);
    // 根据选择的预处理器更新相关配置
  }
}

/**
 * 执行命令行命令
 * @param {string} command - 命令
 * @param {Array} args - 参数
 * @param {object} options - 选项
 * @returns {Promise<void>}
 */
function executeCommand(command, args, options) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      stdio: 'inherit',
      shell: true,
      ...options
    });
    
    child.on('close', code => {
      if (code !== 0) {
        reject(new Error(`命令执行失败: ${command} ${args.join(' ')}`));
        return;
      }
      resolve();
    });
  });
}

module.exports = {
  readTemplateFiles,
  renderTemplate,
  executeCommand
};

这个实现有几个亮点:

  1. 自定义Handlebars助手函数 :通过if_equnless_eqif_includes等助手函数,增强了模板的表达能力
  2. 进度展示:在处理文件时展示进度百分比,提升用户体验
  3. 命令执行工具 :封装了executeCommand函数,简化命令行操作
  4. 特性处理:根据用户选择的特性(如TypeScript、Router等)进行额外配置

模板文件示例

让我们来看一下项目模板目录中的几个关键文件示例:

package.json模板

json 复制代码
{
  "name": "{{projectName}}",
  "version": "0.1.0",
  "private": true,
  "description": "{{projectDescription}}",
  "author": "{{author}}",
  "scripts": {
    "dev": "rspack serve",
    "build": "rspack build",
    "build:analyze": "rspack build --analyze",
    "lint": "eslint --ext .js,.vue src"{{#if_includes features 'unit-testing'}},
    "test:unit": "jest --clearCache && jest"{{/if_includes}}{{#if_includes features 'e2e-testing'}},
    "test:e2e": "cypress open"{{/if_includes}}
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "vue": "^2.6.14"{{#if_includes features 'router'}},
    "vue-router": "^3.5.1"{{/if_includes}}{{#if_includes features 'vuex'}},
    "vuex": "^3.6.2"{{/if_includes}}
  },
  "devDependencies": {
    "@rspack/cli": "^0.5.0",
    "@rspack/core": "^0.5.0",
    "@rspack/plugin-html": "^0.5.0",
    "@rspack/plugin-vue2": "^0.5.0",
    "css-loader": "^6.7.1",
    "vue-style-loader": "^4.1.3"{{#if_includes features 'babel'}},
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16"{{/if_includes}}{{#if_includes features 'typescript'}},
    "typescript": "~4.5.5",
    "@types/node": "^18.0.0"{{/if_includes}}{{#if_includes features 'linter'}},
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3"{{/if_includes}}{{#if cssPreProcessor === 'sass'}},
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0"{{/if_includes}}{{#if cssPreProcessor === 'less'}},
    "less": "^4.0.0",
    "less-loader": "^8.0.0"{{/if_includes}}{{#if cssPreProcessor === 'stylus'}},
    "stylus": "^0.55.0",
    "stylus-loader": "^6.1.0"{{/if_includes}}{{#if_includes features 'unit-testing'}},
    "jest": "^29.0.0",
    "@vue/test-utils": "^1.3.0",
    "vue-jest": "^3.0.7"{{/if_includes}}{{#if_includes features 'e2e-testing'}},
    "cypress": "^12.0.0"{{/if_includes}}
  }
}

Rspack配置文件模板

javascript 复制代码
const path = require('path');
const { VueLoaderPlugin } = require('@rspack/plugin-vue2');
const { HtmlRspackPlugin } = require('@rspack/plugin-html');
const isDev = process.env.NODE_ENV === 'development';

/**
 * @type {import('@rspack/cli').Configuration}
 */
module.exports = {
  context: __dirname,
  entry: './src/main.js', {{#if_includes features 'typescript'}}// 或 './src/main.ts'{{/if_includes}}
  mode: process.env.NODE_ENV || 'development',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: isDev ? '[name].js' : '[name].[contenthash:8].js',
    chunkFilename: isDev ? '[name].chunk.js' : '[name].[contenthash:8].chunk.js',
    publicPath: '/',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: '@rspack/plugin-vue2/dist/vue-loader',
        options: {
          experimentalInlineMatchResource: true
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'ecmascript',
                jsx: false
              },
              target: 'es2015'
            }
          }
        }
      },{{#if_includes features 'typescript'}}
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'typescript',
                tsx: false
              },
              target: 'es2015'
            }
          }
        }
      },{{/if_includes}}
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      },{{#if cssPreProcessor === 'sass'}}
      {
        test: /\.s[ac]ss$/i,
        use: [<antContinueArtifact identifier="optimized-blog-post">
          'vue-style-loader',
          'css-loader',
          'sass-loader'
        ]
      },{{/if_includes}}{{#if cssPreProcessor === 'less'}}
      {
        test: /\.less$/i,
        use: [
          'vue-style-loader',
          'css-loader',
          'less-loader'
        ]
      },{{/if_includes}}{{#if cssPreProcessor === 'stylus'}}
      {
        test: /\.styl(us)?$/i,
        use: [
          'vue-style-loader',
          'css-loader',
          'stylus-loader'
        ]
      },{{/if_includes}}
      {
        test: /\.(png|jpe?g|gif|svg|webp)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024 // 10kb
          }
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        type: 'asset/resource'
      }
    ]
  },
  resolve: {
    extensions: ['.vue', '.js'{{#if_includes features 'typescript'}}, '.ts', '.tsx'{{/if_includes}}],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlRspackPlugin({
      template: './public/index.html',
      title: '{{projectName}}',
      favicon: './public/favicon.ico',
      meta: {
        viewport: 'width=device-width, initial-scale=1.0'
      }
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          name: 'vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        }
      }
    },
    runtimeChunk: 'single'
  },
  devServer: {
    hot: true,
    historyApiFallback: true,
    static: {
      directory: path.join(__dirname, 'public')
    },
    port: 8080,
    open: true,
    client: {
      overlay: {
        errors: true,
        warnings: false
      }
    }
  },
  stats: 'errors-warnings',
  performance: {
    hints: process.env.NODE_ENV === 'production' ? 'warning' : false
  }
};

通过Handlebars的条件表达式(如{{#if_includes features 'typescript'}}),我们可以根据用户选择动态调整模板内容,从而生成定制化的配置文件。

或者像下图所示,在 vue-rspack/template 的文件夹中,将之前 迁移好的项目,抽出公共文件 粘贴 过来

附上GitHub 可用的 cli 源码地址

github.com/corn12138/r...

Webpack到Rspack的迁移要点

在构建这个脚手架工具的过程中,我完成了从Webpack到Rspack的迁移工作。以下是一些关键迁移要点,可以帮助其他开发者更顺利地完成迁移:

1. 核心依赖替换

Webpack相关依赖需要替换为Rspack对应的包:

diff 复制代码
- webpack
- webpack-cli
- webpack-dev-server
- html-webpack-plugin
+ @rspack/core
+ @rspack/cli
+ @rspack/plugin-html

对于Vue 2项目,我们还需要特定的插件:

diff 复制代码
- vue-loader
+ @rspack/plugin-vue2

2. 配置文件调整

Rspack的配置结构与Webpack非常相似,这也是其主要优势之一。不过,仍有一些调整需要注意:

Loader配置

Rspack内置了SWC作为JavaScript/TypeScript转译器,替代了Babel的角色:

javascript 复制代码
// Webpack中的Babel配置
{
  test: /\.js$/,
  use: 'babel-loader'
}

// Rspack中的SWC配置
{
  test: /\.js$/,
  use: {
    loader: 'builtin:swc-loader',
    options: {
      jsc: {
        parser: {
          syntax: 'ecmascript'
        },
        target: 'es2015'
      }
    }
  }
}
插件系统

Rspack有一套自己的插件系统,但也兼容部分Webpack插件:

javascript 复制代码
// Webpack
const HtmlWebpackPlugin = require('html-webpack-plugin');
new HtmlWebpackPlugin({...})

// Rspack
const { HtmlRspackPlugin } = require('@rspack/plugin-html');
new HtmlRspackPlugin({...})
性能优化

Rspack默认启用了许多性能优化,如持久化缓存,这使得构建速度相比Webpack有显著提升:

javascript 复制代码
// Rspack自动应用缓存优化,无需额外配置
module.exports = {
  // 基本配置...
}

3. 兼容性处理

大部分Webpack loader可以直接在Rspack中使用,但某些复杂插件可能需要替代方案:

javascript 复制代码
// 这些loader通常可以直接使用
'css-loader'
'sass-loader'
'less-loader'
'file-loader' // 但推荐使用Rspack的Asset Modules

4. 迁移前后对比

以下是一个简单的性能对比,展示了Webpack和Rspack在相同项目上的构建差异:

指标 Webpack 5 Rspack 提升
首次构建时间 ~8.5s ~2.3s ~73%
热更新时间 ~1.2s ~0.3s ~75%
生产构建 ~25s ~7s ~72%
内存占用 ~500MB ~300MB ~40%

这些数据来自一个中等规模的Vue 2项目,迁移后的性能提升相当显著,尤其是在开发环境中的热更新速度,极大提升了开发体验。

本地测试与使用

完成脚手架工具的开发后,我们需要在本地测试其功能。通过npm link命令,可以将我们的CLI工具全局安装到本地系统:

bash 复制代码
# 在脚手架项目目录中执行
npm link

这个命令会在全局npm模块中创建一个符号链接,指向当前项目。然后,我们可以在任意位置使用我们的CLI工具:

bash 复制代码
# 创建一个新项目
vue-rspack create my-project

# 使用强制覆盖选项
vue-rspack create existing-project --force

# 跳过依赖安装
vue-rspack create quick-test --no-install

通过这种方式,我们可以方便地测试脚手架工具的各种功能,确保其正常工作。

如果需要在团队内共享这个工具,可以考虑将其发布到私有npm仓库或公司内部的制品库中:

bash 复制代码
# 更新版本号
npm version patch

# 发布到私有npm仓库
npm publish --registry=your-private-registry-url

进阶功能与优化方向

我们已经实现了一个基础但功能完整的脚手架工具,但仍有许多方向可以进一步优化和扩展:

1. 远程模板系统

支持从Git仓库拉取模板,使模板更新更加灵活:

javascript 复制代码
// 示例实现
async function downloadGitRepo(repo, dest) {
  const download = require('download-git-repo');
  return new Promise((resolve, reject) => {
    download(repo, dest, { clone: true }, err => {
      if (err) return reject(err);
      resolve();
    });
  });
}

2. 插件机制

设计一个插件系统,允许团队成员扩展脚手架功能:

javascript 复制代码
// 插件注册系统示例
class PluginAPI {
  constructor(id, service) {
    this.id = id;
    this.service = service;
  }
  
  registerCommand(name, opts, fn) {
    this.service.commands[name] = { fn, opts };
  }
  
  // 更多API...
}

function loadPlugins(context, plugins) {
  const pluginAPIs = [];
  
  for (const id of plugins) {
    const plugin = require(id);
    const api = new PluginAPI(id, context);
    pluginAPIs.push(api);
    plugin(api);
  }
  
  return pluginAPIs;
}

3. 项目预设与配置共享

支持保存常用配置作为预设,方便团队成员使用标准配置:

javascript 复制代码
// 保存预设示例
async function savePreset(name, answers) {
  const presets = await loadUserPresets();
  presets[name] = answers;
  await fs.writeFile(
    path.join(os.homedir(), '.vue-rspack-presets.json'),
    JSON.stringify(presets, null, 2)
  );
}

4. 可视化界面

构建一个简单的Web界面,让非技术人员也能使用:

javascript 复制代码
// 示例实现思路
const express = require('express');
const app = express();
const { create } = require('./lib/create');

app.use(express.json());
app.use(express.static('ui'));

app.post('/api/create', (req, res) => {
  const { projectName, options } = req.body;
  create(projectName, options)
    .then(() => res.json({ success: true }))
    .catch(err => res.status(500).json({ error: err.message }));
});

app.listen(3000, () => {
  console.log('UI server running at http://localhost:3000');
  // 自动打开浏览器
  require('open')('http://localhost:3000');
});

5. 智能依赖管理

添加依赖检查和更新建议功能:

javascript 复制代码
async function checkDependencies(packageFile) {
  const { ncu } = require('npm-check-updates');
  
  console.log(chalk.blue('📊 检查依赖更新...'));
  
  const result = await ncu({
    packageFile,
    upgrade: false,
    jsonUpgraded: true
  });
  
  return result;
}

总结与展望

在本文中,我们从零开始搭建了一个类似Vue-CLI的本地脚手架工具,实现了从Webpack到Rspack的平滑迁移。通过这个项目,我们不仅学习了脚手架工具的核心原理和实现方法,还深入了解了Rspack这一高性能构建工具的优势和使用方式。

这个脚手架工具虽然相对简单,但已经包含了命令行解析、交互式配置、模板渲染等核心功能,完全可以满足团队内部的项目初始化需求。更重要的是,通过将Webpack到Rspack的迁移经验封装在脚手架中,我们可以让团队中的所有成员都能轻松地使用这套优化后的构建配置,提高整体开发效率。

未来,我们可以不断完善这个工具,添加更多功能如远程模板、插件系统、配置预设等,使其更加强大和灵活。随着Rspack的不断发展和成熟,我们也可以持续更新脚手架中的Rspack配置,确保团队始终使用最佳实践。

希望这篇文章对你有所帮助,无论是学习脚手架工具的实现原理,还是实际进行Webpack到Rspack的迁移工作。如果你有任何问题或建议,欢迎在评论区留言讨论!

参考资源

相关推荐
Anlici37 分钟前
面试官:想把你问趴下 => 面题整理[3] 😮‍💨初心未变🚀
javascript·面试·前端框架
孤蓬&听雨2 小时前
Axure常用变量及使用方法详解
产品经理·axure·设计·产品设计·原型设计
Hopebearer_2 小时前
vue3中ref和reactive的区别
开发语言·前端·javascript·vue.js·前端框架·ecmascript
winyh53 小时前
JWT要点备忘录
前端·前端框架
想自律的露西西★15 小时前
生命周期总结(uni-app、vue2、vue3生命周期讲解)
前端·javascript·前端框架
huangkaihao18 小时前
Monorepo源码引用实践:基于Webpack插件的路径解析方案
前端·webpack·设计
知识分享小能手19 小时前
Html5学习教程,从入门到精通,HTML `<div>` 和 `<span>` 标签:语法知识点与案例代码(12)
java·开发语言·前端·学习·前端框架·html·html5
winyh520 小时前
从零开始封装React UI 组件库并发布到NPM
前端·react.js·前端框架
Kousi1 天前
手把手教你使用AVRecorder完成鸿蒙录音
前端·前端框架·harmonyos
hamburgerDaddy11 天前
从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(十四) 部署
前端·javascript·mongodb·react.js·前端框架·express