Plop.js:一键生成代码模板,提升开发效率的利器

658 阅读10分钟

github.com/plopjs/plop

1. Plop

Plop 是一个小型的命令行工具,专注于帮助开发者自动生成代码片段和文件结构,类似于代码生成器。它可以通过简单的模板定义和 CLI(命令行界面)交互,在项目中快速生成预定义的代码片段,从而提升开发效率。

Plop 的主要功能

  1. 模板生成:通过模板和自定义脚本,生成特定的代码或文件结构。通常用于创建组件、服务、模块等常用代码片段。
  2. 快速配置:使用简单的 JavaScript 配置文件 plopfile.js 来定义生成器,避免手动创建重复的代码。
  3. 交互式 CLI:运行 plop 后,它会提示输入相关参数,并根据这些参数生成定制的代码。
  4. 灵活性:支持自定义脚本和多种模板格式,适用于不同类型的项目(如 React、Vue 等框架)。

2. 诞生背景

为了帮助开发者简化代码生成和项目中的重复性操作。尤其是在前端开发中,开发者通常会创建大量的类似代码结构(如组件、服务模块、Redux actions 等),这些操作容易出错且耗时。Plop 的设计初衷就是以简单、灵活的方式,帮助开发者快速生成标准化的代码结构和文件,从而减少手动创建的工作量。

Plop 诞生背景与要解决的问题

  1. 减少重复性代码创建:前端和全栈开发项目中,常见的文件和组件需要遵循统一的结构和命名规则,手动创建不仅繁琐,而且容易出错。Plop 能够将这些操作自动化,确保一致性。
  2. 提升开发效率:对于团队而言,代码模板生成工具能帮助快速搭建项目框架、减少开发者在基础文件上的时间投入,使其将精力集中在业务逻辑和核心功能开发上。
  3. 支持模板化和定制化:与传统的文件拷贝方法相比,Plop 的交互式 CLI 和自定义模板系统,能根据用户输入创建定制化文件内容,从而满足更灵活的需求。

3. Plop 与竞品对比

在代码生成方面,Plop 的竞品主要有 Yeoman、Hygen、Slush 等。这些工具的核心功能类似,但侧重点略有不同:

  1. Yeoman

    • 特点:Yeoman 是更大型的项目生成器,包含丰富的生成器插件生态系统,支持从项目模板到文件结构的自动生成。Yeoman 适用于初始化复杂项目。
    • 优缺点:功能非常强大,但配置复杂,依赖于庞大的插件生态,适合大型项目的初始化,但不如 Plop 轻便。
    • 适用场景:更适合用作项目初始模板生成,而不是简单的文件或组件生成。
  2. Hygen

    • 特点:Hygen 是另一个文件生成工具,主要通过模板来快速生成代码,并且支持纯本地运行,无需依赖外部包。
    • 优缺点:相比 Plop,Hygen 更加偏向脚本式运行,可以用配置文件定义一系列生成操作,并通过命令行运行。但其灵活性不及 Plop,交互性较差。
    • 适用场景:适用于项目中的静态文件生成或模板代码生成。
  3. Slush

    • 特点:Slush 是基于 Gulp 的项目生成器工具,以任务流的方式自动化代码生成。
    • 优缺点:依赖 Gulp,对代码生成流程有较多定制需求的开发者可能会喜欢,但整体配置相对繁琐。
    • 适用场景:适合复杂任务流的代码生成场景,但不如 Plop 轻量和易上手。

为什么选择 Plop

Plop 的优势在于:

  • 简单易用:配置文件简单明了,适合快速上手。
  • 灵活性:可以根据需求定制生成器,与现有项目无缝集成。
  • CLI 交互友好:支持用户输入动态生成代码,适合文件和代码片段生成。

4. 核心构成

Plop 的核心构成可以分为以下几个部分:

  1. 生成器(Generator):生成器是 Plop 的基本构成单位,负责定义代码生成的规则。每个生成器都是由一系列提示和操作组成的,描述了代码生成的逻辑。

  2. 提示(Prompts):提示用于收集用户输入的参数,如文件名称、路径等。这些输入会被传递给后续的操作,用于动态生成内容。Plop 使用 Inquirer.js 来处理交互式 CLI 提示。

  3. 操作(Actions):操作是生成器的核心,它定义了在生成过程中如何处理用户的输入。例如,操作可以将模板内容写入文件、追加内容到文件中、或是执行自定义脚本。Plop 内置了一些基本操作类型(如 addmodify),同时也支持自定义操作。

  4. 模板(Templates):Plop 使用 Handlebars 语法来处理模板,使得生成的文件内容能够包含动态内容(如变量、循环等)。模板文件通常保存在 plop-templates 目录中,Plop 读取这些模板并替换变量生成最终代码文件。

image.png

export default function (plop) {
  // create your generators here
  plop.setGenerator("basics", {
    description: "this is a skeleton plopfile",
    prompts: [], // array of inquirer prompts
    actions: [], // array of actions
  });
}

5. 快速上手

以下是使用 Plop 快速生成 Vue 3 组件的上手示例。

Step 1: 安装 Plop

首先,全局安装 Plop:

npm install -g plop

Step 2: 在项目中创建 plopfile.js

在项目根目录创建一个 plopfile.js 文件,定义生成器逻辑。我们创建一个 component 生成器,用于生成 Vue 3 组件文件。

module.exports = function (plop) {
  plop.setGenerator("component", {
    description: "Create a new Vue 3 component",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "Component name:",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/components/{{pascalCase name}}.vue",
        templateFile: "plop-templates/Component.vue.hbs",
      },
      {
        type: "add",
        path: "src/components/{{pascalCase name}}.css",
        templateFile: "plop-templates/Component.css.hbs",
      },
    ],
  });
};

在这个文件中,我们定义了一个 component 生成器:

  • prompts:提示用户输入组件名称。
  • actions:根据用户输入生成 Vue 文件和 CSS 文件,路径和模板文件使用变量来生成动态内容。

Step 3: 创建模板文件

在项目根目录下创建 plop-templates 文件夹,并添加两个模板文件:Component.vue.hbsComponent.css.hbs

  • Component.vue.hbs

    <template>
      <div class="{{camelCase name}}">
        <h1>{{pascalCase name}} Component</h1>
      </div>
    </template>
    
    <script>
    import './{{pascalCase name}}.css';
    
    export default {
      name: '{{pascalCase name}}'
    };
    </script>
    
    <style scoped>
    </style>
    
  • Component.css.hbs

    .{{camelCase name}} {
      /* Add your styles here */
    }
    

这里使用了 {{pascalCase name}}{{camelCase name}},根据用户输入将组件名称转为不同格式。

Step 4: 使用 Plop 生成组件

在项目根目录运行以下命令:

plop

会看到 Plop 的交互界面,选择 component 生成器:

[plop] Please choose a generator.
? component: Create a new Vue 3 component

然后 Plop 会提示输入组件名称,比如 MyComponent

? Component name: MyComponent

根据输入的组件名称,Plop 会生成以下两个文件:

  1. src/components/MyComponent.vue

    <template>
      <div class="myComponent">
        <h1>MyComponent Component</h1>
      </div>
    </template>
    
    <script>
    import './MyComponent.css';
    
    export default {
      name: 'MyComponent'
    };
    </script>
    
    <style scoped>
    </style>
    
  2. src/components/MyComponent.css

    .myComponent {
      /* Add your styles here */
    }
    

6. 方法

方法参数返回值描述
setGeneratorString, GeneratorConfigPlopGenerator设置一个生成器
setHelperString, Function设置一个 Handlebars 助手函数
setPartialString, String设置一个 Handlebars partial 模板片段
setActionTypeString, CustomAction注册一个自定义的操作类型
setPromptString, InquirerPrompt使用 Inquirer 注册一个自定义的提示类型
loadArray[String], Object, Object从另一个 plopfile 或 npm 模块加载生成器、助手或 partial 模板

1. setGenerator

用于定义一个生成器。在 Plop.js 中,生成器是一个集合,包含生成文件所需的模板、操作和用户输入提示。

示例:

plop.setGenerator("component", {
  description: "创建一个新的组件",
  prompts: [
    {
      type: "input",
      name: "name",
      message: "组件名称是什么?",
    },
  ],
  actions: [
    {
      type: "add",
      path: "src/components/{{name}}.js",
      templateFile: "plop-templates/component.hbs",
    },
  ],
});

2. setHelper

用于注册一个 Handlebars helper 函数。在模板中,可以使用 helper 处理或格式化数据。

示例:

plop.setHelper("uppercase", (text) => text.toUpperCase());

在模板中,可以这样使用该 helper:

<p>{{uppercase name}}</p>

3. setPartial

用于注册一个 Handlebars 部分模板(partial),partial 可以用于模板复用。

示例:

plop.setPartial("myPartial", "<div>部分模板内容:{{name}}</div>");

在主模板中,可以通过 {{> myPartial}} 引用该部分模板:

<div>
  {{> myPartial}}
</div>

4. setActionType

注册一个自定义的操作类型,用于扩展生成器的操作(默认操作有 addmodify 等)。

示例:

plop.setActionType("customLog", (answers, config) => {
  console.log(`自定义日志操作:${config.message}`);
  return "日志已输出";
});

// 在生成器中使用
plop.setGenerator("logExample", {
  description: "示例生成器,包含自定义日志操作",
  actions: [
    {
      type: "customLog",
      message: "Hello from custom action!",
    },
  ],
});

5. setPrompt

注册一个自定义的提示类型,扩展 Inquirer.js 提示种类。

示例:

const inquirerDate = require("inquirer-datepicker");
plop.setPrompt("date", inquirerDate);

// 使用自定义提示
plop.setGenerator("birthday", {
  description: "输入生日日期",
  prompts: [
    {
      type: "date",
      name: "birthday",
      message: "你的生日是什么时候?",
    },
  ],
  actions: [
    {
      type: "add",
      path: "src/birthday.txt",
      template: "生日:{{birthday}}",
    },
  ],
});

6. load

用于从其他 plopfile 或 npm 模块中加载生成器、助手或 partial。

示例:

// 加载本地 plopfile 模块
plop.load("./path/to/other/plopfile.js");

// 加载 npm 模块
plop.load("plop-pack-fancy-generators");
方法参数返回值描述
getHelperStringFunction获取指定名称的助手函数
getHelperListArray[String]获取所有助手函数名称的列表
getPartialStringString通过名称获取一个 Handlebars partial 模板片段
getPartialListArray[String]获取所有 partial 模板片段名称的列表
getActionTypeStringCustomAction通过名称获取一个自定义的操作类型
getActionTypeListArray[String]获取所有自定义操作类型名称的列表
setWelcomeMessageString自定义 Plop 运行时的欢迎信息(即选择生成器时显示的消息)
getGeneratorStringGeneratorConfig通过名称获取一个生成器
getGeneratorListArray[Object]获取所有生成器名称及描述的数组
setPlopfilePathString设置 plopfilePath 值,用于在内部定位模板文件等资源
getPlopfilePathString返回当前使用的 plopfile 的绝对路径
getDestBasePathString返回用于创建文件的基准路径
setDefaultIncludeObjectObject设置当前 plopfile 的默认配置(若被其他 plopfile 引入时将使用此默认配置)
getDefaultIncludeStringObject获取当前 plopfile 的默认配置(若被其他 plopfile 引入时将使用此默认配置)
renderStringString, ObjectString使用 Handlebars 渲染模板字符串(第一个参数),并用第二个参数提供的数据填充模板,返回渲染结果

7. 内置 Actions

Action 类型描述必需参数示例
add创建一个新文件path, templatetemplateFilesrc 文件夹下创建一个新的组件文件。
示例见下方代码块。
addMany批量创建多个文件destination, base, templateFiles从指定的模板批量创建多个文件到目标文件夹中。
示例见下方代码块。
modify修改已有文件的内容path, pattern, template在已有文件中替换或插入内容,可用于更新导出、路由等。
示例见下方代码块。
custom执行自定义操作function执行自定义的 JavaScript 函数,可以执行特定的操作。
示例见下方代码块。

1. add

用于创建新文件。可以在指定路径生成一个新文件,并将模板填充到该文件中。

示例

{
  type: 'add',
  path: 'src/components/{{name}}.js',
  templateFile: 'plop-templates/component.hbs'
}

上述代码将会根据 component.hbs 模板创建一个新的组件文件,路径为 src/components/{{name}}.js,其中 {{name}} 会被替换为用户输入的名称。

2. addMany

用于批量创建多个文件,通常用于生成多个文件或整个文件夹结构。

示例

{
  type: 'addMany',
  destination: 'src/components/{{name}}',
  base: 'plop-templates/component',
  templateFiles: 'plop-templates/component/*.hbs'
}

addMany 操作会在 src/components/{{name}} 目录下创建多个文件,基于 plop-templates/component 目录中的 .hbs 模板文件。

3. modify

用于修改已存在的文件,可以使用正则表达式定位插入点或进行替换。

示例

{
  type: 'modify',
  path: 'src/index.js',
  pattern: /\/\/ Components export/,
  template: "export { default as {{name}} } from './{{name}}';"
}

modify 操作会在 src/index.js 文件中,找到 // Components export 标记的位置,然后插入或替换模板内容 export { default as {{name}} } from './{{name}}';

4. custom

用于执行自定义操作。通过传入一个函数,可以灵活定义任何操作逻辑。

示例

{
  type: "custom",
    function (answers, config, plop) {
      console.log(`Generating custom files for ${answers.name}`);
      return `Custom action completed for ${answers.name}`;
    };
}

上述代码中的 custom 操作会在生成器运行时输出一段日志,或者可以自定义任意 JavaScript 逻辑。

8. 内置 Helper

Helper 名称转换后的格式示例
camelCasechangeFormatToThis{{camelCase name}}
snakeCasechange_format_to_this{{snakeCase name}}
dashCase / kebabCasechange-format-to-this{{dashCase name}}{{kebabCase name}}
dotCasechange.format.to.this{{dotCase name}}
pathCasechange/format/to/this{{pathCase name}}
properCase / pascalCaseChangeFormatToThis{{properCase name}}{{pascalCase name}}
lowerCasechange format to this{{lowerCase name}}
sentenceCaseChange format to this{{sentenceCase name}}
constantCaseCHANGE_FORMAT_TO_THIS{{constantCase name}}
titleCaseChange Format To This{{titleCase name}}
pkg从与 plopfile.js 位于相同文件夹的 package.json 文件中查找属性值{{pkg "version"}} 获取版本号
  • pkgpkg 可用于查找 package.json 中的属性。可以通过 {{pkg "property_name"}} 来访问特定属性,例如 {{pkg "name"}} 获取包名,{{pkg "version"}} 获取版本号等。

9. Prompt 类型

Prompt 类型描述示例
input文本输入框,用户可以输入任意文本{ type: 'input', name: 'name', message: '请输入组件名称' }
confirm确认框,用户可以选择 yesno{ type: 'confirm', name: 'isCool', message: '是否很酷?' }
list列表选择,用户可以从提供的选项中选择一个{ type: 'list', name: 'color', message: '选择颜色', choices: ['红色', '蓝色'] }
rawlist数字列表选择,用户可以通过数字选择一个选项{ type: 'rawlist', name: 'version', message: '选择版本', choices: ['1.0', '2.0'] }
expand扩展列表选择,使用字母快捷键选择选项{ type: 'expand', name: 'flavor', message: '选择口味', choices: [{ key: 'c', name: '巧克力', value: 'chocolate' }] }
checkbox多选框,用户可以选择多个选项{ type: 'checkbox', name: 'features', message: '选择特性', choices: ['性能优化', '安全增强'] }
password密码输入框,输入的内容以 * 号显示{ type: 'password', name: 'password', message: '输入密码' }
editor文本编辑器,打开一个多行文本输入窗口,适用于编辑长文本或代码片段(在系统支持时可用){ type: 'editor', name: 'longText', message: '输入详细描述' }

示例:

module.exports = function (plop) {
  plop.setGenerator("component", {
    description: "创建新组件",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "请输入组件名称",
      },
      {
        type: "list",
        name: "color",
        message: "选择颜色",
        choices: ["红色", "蓝色", "绿色"],
      },
      {
        type: "confirm",
        name: "isCool",
        message: "组件是否很酷?",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/components/{{name}}.js",
        templateFile: "plop-templates/component.hbs",
      },
    ],
  });
};

可自行扩展,或者参考社区:github.com/plopjs/awes…

10. CLI 设置工程目录

为了将 Plop 集成到你自己的命令行工具中,你只需要一个 plopfile.jspackage.json 和一个模板文件即可。核心代码结构如下:

index.js
#!/usr/bin/env node
import path from "node:path";
import minimist from "minimist";
import { Plop, run } from "plop";

// 解析命令行参数
const args = process.argv.slice(2);
const argv = minimist(args);

// 获取当前文件路径
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

// 配置 Plop
Plop.prepare(
  {
    cwd: argv.cwd, // 当前工作目录
    configPath: path.join(__dirname, "plopfile.js"), // plopfile.js 的路径
    preload: argv.preload || [], // 预加载的文件
    completion: argv.completion, // 完成提示
  },
  (env) => Plop.execute(env, run)
);
package.json
{
  "name": "create-your-name-app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "plop"
  },
  "bin": {
    "create-your-name-app": "./index.js"
  },
  "preferGlobal": true,
  "dependencies": {
    "plop": "^3.0.0"
  }
}

package.json 文件中,设置 bin 字段,允许通过命令行调用你的工具,并确保 preferGlobaltrue,这样你的工具可以在全局环境中使用。

2. 设置目标路径

当将 Plop 包装成 CLI 时,你可能希望根据运行时的工作目录(cwd)来设置目标文件的路径。你可以通过以下代码来实现:

Plop.prepare(
  {
    // 配置项,如上所示
  },
  (env) =>
    Plop.execute(env, (env) => {
      const options = {
        ...env,
        dest: process.cwd(), // 将目标路径设置为当前工作目录
      };
      return run(options, undefined, true);
    })
);

这会使得文件生成路径相对于你当前的工作目录。


微信搜索“好朋友乐平”关注公众号。

github原文地址