yeoman自定义方法

11 3月

前一篇只是生成了个空壳子,最重要的就是自定义方法。源代码点这里

  • 执行顺序
  • 私有方法
  • prompting
  • writing
  • install

执行顺序

方法的运行顺序,自定义的方法名都在 default 阶段运行:

initializing()    // 例如检查项目state,获取configs等
prompting()       // 用户交互,例如调用 this.prompt()
configuring()     // 创建工程,例如 .editorconfig 文件等
default           // 方法的默认优先级
writing()         // 执行文件动作
conflicts()       // 解决冲突等(内部使用)
install()         // 安装依赖,例如npm,bower等
end()             // 收尾

例如你可以在prompting()让用户输入脚手架的信息,writing()里执行模板文件的拷贝,install()里执行脚手架依赖的npm安装,end()里,

私有方法

默认自定义方法都会被自动执行,如果不想方法被自动执行,有3种方式。

方式一:方法名加上下划线前缀,例如_private_method

方式二:将方法定义在 constructor 里,例如this.helperMethod = function () { … }

方式三:扩展一个父generator:

class MyBase extends Generator {
  helper() {
    console.log('父generator的方法不会被自动执行');
  }
}

module.exports = class extends MyBase {
  exec() {
    this.helper();
  }
};

prompting

Yeoman通常是在终端与用户进行交互,也可以自定义UI界面。最常用的就是Prompts,由inquirer.js提供。注意以下几点:

  • prompt是异步的,返回promise
  • 不推荐用 console.log() 或 process.stdout.write(),推荐用this.log()
  • 公共变量可以保存到this上,共后续方法使用
  • 想记住用户的选择,可以设置store: true
module.exports = class extends Generator {
    constructor(args, opts) { ... }

    prompting() {
        const questions = [
            {
                name: 'projectAssets',
                type: 'list',
                message: '请选择模板:',
                choices: [
                    {
                        name: 'PC',
                        value: 'pc',
                        checked: true
                    },{
                        name: 'App',
                        value: 'app'
                    }
                ]
            },
            {
                type: 'input',
                name: 'projectAuthor',
                message: '项目开发者',
                store: true,           // 记住用户的选择
                default: 'zhangxinlin'
            },
            {
                type: 'input',
                name: 'pageTitle',
                message: '页面标题',
                default: 'Your project title'
            }
        ];
        return this.prompt(questions).then((answers) => {
            this.log("answers: ", JSON.stringify(answers));     // 推荐用 this.log,而不是 console.log
            this.myInfo = answers;                              // 公共变量可以保存到 this 上,共后续方法使用
        });
    }
};

writing

脚手架本质上是将本地的template文件拷贝到目标文件夹中,操作文件的动作在writing()方法内执行。你需要知道以下几点:

  • 如果你在constructor里执行过this.config.save();,会在目标文件夹的根目录下自动生成.yo-rc.json文件,Yeoman文件系统以来这个json文件来获知工程的根目录
  • 可以通过this.destinationRoot()获取工程根目录。你可以传路径参数来更改,但不推荐
  • 可以通过this.destinationPath()增加文件,baseDir是根目录
  • 模板的默认路径为./templates/,可以通过this.sourceRoot()获取模板路径。你可以传路径参数来更改,但不推荐
  • 可以通过this.templatePath(file)指定模板,baseDir是templates目录
  • 可以通过this.fs.copyTpl拷贝模板文件。Yeoman把所有的文件方法都放在this.fs中了,本质上是mem-fs-editor的一个实例对象
const fse = require('fs-extra');
const glob = require("glob-promise");

const copyFiles = async(src, dest) => {
    // package.json 和 模板文件不拷贝,后续单独处理
    const files = await glob(`${src}/**/!(package.json|index.html)`, { nodir: true });

    files.forEach(async (file) => {
        const dir = path.relative(src, file);
        await fse.copy(file, path.join(dest, dir), { overwrite: true });
    });

    // .gitignore 点开头的文件单独拷贝
    await fse.copySync(`${src}/.gitignore`, `${dest}/.gitignore`, { overwrite: true });
};

module.exports = class extends Generator {
    constructor(args, opts) { ... }

    prompting() { ... }

    async writing() {
        this.log(`开始将 ${this.sourceRoot()} 里的模板拷贝至 ${this.destinationRoot()} 目录中...`);
        
        // templates目录里,除模板文件外都无脑拷贝
        await copyFiles(path.join(__dirname, 'templates'), this.destinationRoot());

        // 开始处理模板文件
        this.fs.copyTpl (
            this.templatePath('package.json'),      // 默认路径就是 templates 目录
            this.destinationPath('package.json'),   // 目标文件夹的根目录下新建文件
            {
                projectName: this.options.appname,
                projectAuthor: this.myInfo.projectAuthor
            }
        );
        this.fs.copyTpl(
            this.templatePath('./src/index.html'),
            this.destinationPath('./src/index.html'),
            { title: this.myInfo.pageTitle }
        );
    }
}

install

如果想让脚手架生成后自动安装npm依赖,可以在install()方法里定义。可以调用this.npmInstall()this.yarnInstall()来安装。

this.npmInstall(['lodash'], { 'save-dev': true });
// 等价于 npm install lodash --save-dev

this.yarnInstall(['lodash'], { 'dev': true });
// 等价于 yarn add lodash --dev

但一般不会这样单独安装,通常是通过package.json来自动安装。不确定用户使用npm还是yarn的话,可以用this.installDependencies()定义优先级:

module.exports = class extends Generator {
    constructor(args, opts) { ... }

    prompting() { ... }

    async writing() {
        ...
        const pkgJson = {
            devDependencies: {
                eslint: '^3.15.0'
            },
            dependencies: {
                react: '^16.2.0'
            }
        };
        this.fs.extendJSON(this.destinationPath('package.json'), pkgJson);
    }

    install() {
        this.installDependencies({
            yarn: true,
            npm: true,
            bower: false   // 不使用 bower
        });
    }
}

其他安装程序,可以调用this.spawnCommand(),基本不会用。例如:

this.spawnCommand('composer', ['install']);

发表评论

电子邮件地址不会被公开。 必填项已用*标注