# 基于Plop快速生成模板文件

# 想法产生

使用umi开发时,新建一个组件就要手动创建index.tsx文件和index.scss文件,并且在tsx中还要编写一些模板代码,例如:

import React from 'react';
import styles from './index.scss';

interface IAvatar {}

const Avatar: React.FC<IAvatar> = () => {
  return (
    <div>{{name}}</div>
  );
};

export default Avatar;

写过一点nest,接触到其cli,因此决定做一个快速生成文件的demo。

# 开始

# 项目搭建

新建文件夹,并在该文件夹路径下初始化npm信息:

npm init

# 设置eslint

npm i eslint -D
npx eslint --init

# below are choices about eslint prompts
To check syntax, find problems, and enforce code style
JavaScript modules (import/export)
None of these
Does your project use TypeScript >> yes
Node
Use a popular style guide
Airbnb
Javascript

可以在将配置改为typescript-eslint/recommended

// .eslintrc.js
module.exports = {
    // ...
    'extends': [
        'plugin:@typescript-eslint/recommended',
     ],
    // ...
}

# 设置typescript

安装依赖:

npm i -D typescript ts-node -D

ts-node是为了可以直接类似node index.js这样直接运行ts文件。

初始化配置:

npx tsc --init
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",                         
    "module": "commonjs",                     
    "outDir": "dist",                       
    "rootDir": "src",                       
    "importHelpers": true,                 
    "strict": true,                       
    "moduleResolution": "node",            
    "allowSyntheticDefaultImports": true,  
    "esModuleInterop": true,                  
    "skipLibCheck": true,                     
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["src/templates/**/*"]
}

配置文件需要配置编译后js的输出目录(outDir),为了编译后的运行js文件的路径查找与src中的ts一直,设置rootDir。

开启allowSyntheticDefaultImportsesModuleInterop,import导入包时,无需每次都从deafult中取值。

为了让tsc编译过程中能过包括一些环境声明,设置includes。

# 设置package.json中的script

"scripts": {
  "start": "ts-node --files src/index.ts",
  "build": "tsc -p tsconfig.json ",
  "rebuild": "rm -r dist && npm run build",
  "lint": "eslint --ext .ts --fix src"
}

ts-node需要--files参数,否则不会包含声明文件。

tsc需要指明配置文件,否则不会包含声明文件。

# 新建templates文件夹

新建文件:templates/component/index.tsxtemplate/scss/index.scss

其中index.tsx文件内容为:

import React from 'react';
import styles from './index.scss';

interface I{{capitalize name}} {}

const {{capitalize name}}: React.FC<I{{capitalize name}}> = () => {
  return (
    <div>{{name}}</div>
  );
};

export default {{capitalize name}};

该文件将会被handlebars进行处理,并且会引入handlebars helpers中的string helper,对名字进行首字母大写。

上述文件内容了解简单的handlebars和helpers用法即可了解。plop库封装了对handlebars的使用方式,只需要指定模板文件路径和输出文件路径,以及设置helper即可。

# 使用设计

输入命令行create-umi -c avatar,生成src/components/avatar/index.tsxsrc/components/avatar/index.scss

其中index.tsx内容为:

import React from 'react';
import styles from './index.scss';

interface IAvatar {

}

export const Avatar: FC<IAvatar> = () => {
  return (
    <div>
      avatar
    </div>
  );
};

export default Avatar;

# 使用到的库

  • chalk:输出terminal的字体带上指定颜色。
  • handlebars:处理模板字符串。
  • handlebars-helpers:增强处理模板的工具。
  • minimist:处理命令行参数。
  • node-plop:通过api的方式调用plop。
  • plop:快速生成模板文件的库,集成了对话、模板复制等功能。

# 代码编写

// src/index.ts
#!/usr/bin/env node
import minimist from 'minimist';
import nodePlop from 'node-plop';

import { error } from './util';
import plopFile from './plopfile';

function start() {
  const argv = minimist(process.argv.slice(2));

  const { c, component, p, page } = argv;
  const componentName = c || component;
  const pageName = p || page;

  if (!componentName && !pageName) {
    return console.log(
      error('请选择生成的类型(组件 or 页面)')
    );
  }

  if (componentName && pageName) {
    return console.log(
      error('只能选择一种类型')
    );
  }

  plopFile({
    type: componentName ? 'component' : 'page',
    name: componentName || pageName
  })(nodePlop(''));
}

start();

文件第一行shebang,告诉系统使用何种程序执行文件。

start函数主要做的事情包括:

  • 参数读取
  • 判断参数合法性
  • 调用plop执行文件复制

util中导入的使chalk的基本封装。

// src/plopfile.ts
import { NodePlopAPI } from 'plop';

import helpers from 'handlebars-helpers';

import { setHelper } from './plop/setHelper';
import { setGenerator } from './plop/setGenerator';
import { runActions } from './plop/runActions';

export interface IPlopfile {
  type: 'component' | 'page',
  name: string;
}
export default function ({ name, type }: IPlopfile) {
  return (plop: NodePlopAPI): void => {
    const stringHelpers = helpers.string();

    setHelper({
      plop,
      helpers: stringHelpers,
      helperName: 'capitalize'
    });

    const componentGenerator = setGenerator({
      name,
      plop,
      description: '生成组件',
      actions: [{
        type,
        ext: 'tsx'
      }, {
        type,
        ext: 'scss'
      }]
    });

    runActions({
      generator: componentGenerator
    });

  };
}

该函数做的事情主要包括:

  • 设置handlebars helper
  • 设置plop generator
    • generator中主要设置action,做的工作就是按照模板和命令行参数输出文件,这里没有用到对话。
    • 该文件封装了设置generator的工作,外界只需要传入文件类型即可。
import { NodePlopAPI, ActionConfig } from 'plop';
import { getPath } from './getPath';
import { getTemplate } from './getTemplate';

interface IGenerate {
  type: 'component' | 'page';
  ext: 'tsx' | 'scss';
}

interface ISetGenerator {
  name: string;
  plop: NodePlopAPI;
  description: string;
  actions: IGenerate[];
}

export function setGenerator({
  name,
  plop,
  description,
  actions
}: ISetGenerator) {
  return plop.setGenerator(name, {
    prompts:[],
    description,
    actions: actions.map<ActionConfig>(({ type, ext }) => ({
      type: 'add',
      path: getPath({ type, name, ext }),
      templateFile: getTemplate({ type, ext }),
      data: {
        name
      }
    }))
  });
}

setGenerator做的事情就是根据plop api对action进行赋值。

getPath和getTemplate封装的操作就是路径映射:

  • getPath:src/components/avatar/index.xxx
  • getTemplate:../../templates/component/index.xxx

getPath的路径相对的是使用者当前的文件路径,例如umi项目根路径。

getTemplate的路径相对的是该工具的路径。

# 测试

# 设置package.json的bin

"bin": {
  "create-umi": "dist/index.js"
}

# 构建

npm run build

执行构建命令后,文件会打包至dist文件夹中。

在该工具项目中执行npm link

在umi项目中执行npm link create-umi,其中create-umi为该工具的名字,执行create-umi -c avatar即可生成文件。

# 总结

  • 环境搭建
  • 进一步理解tsconfig.json
  • npm link
  • package.json bin
  • node plop
  • handlebars

# 后续

  • jest测试
  • 功能迭代