自定义脚手架实战
开始之前
原因
- 规范项目结构,方便维护和优化
- 避免互相拷贝代码,导致bug扩散
- 减少重复代码,降低门槛
目标
自动生成一个 react型组件项目,使用eslint、stylelint、.editorconfig、postcssrc、babelrc、rollup。
模板
按照自己需求,先定义一个模板,并上传到github
脚手架模板搭建过程
脚手架模板地址: bulletin
使用 Yeoman
1、安装脚手架工具
npm install -g yo
验证是否安装成功:yo --version
报错信息:yo command not found
执行下语句
npm install -y -g yo
echo export PATH="$HOME/npm/bin:$PATH" >> ~/.bashrc
npm config set prefix ~/npm
echo "export NODE_PATH=$NODE_PATH:/home/$USER/npm/lib/node_modules" >> ~/.bashrc && source ~/.bashrc
npm install -y -g yo
2、 安装yo成功后, 生成 generator
npm install -g generator-generator
yo generator
3、用户交互
在prompting
中询问用户,设置默认值“default”, 通过name取用户的输入值
prompting() {
....
const prompts = [
{
type: "input",
name: "name",
message: "Your component name",
default: this.appname
},
{
type: "input",
name: "description",
message: "Your component description",
default: this.appname
}
];
return this.prompt(prompts).then(props => {
this.props = props;
});
}
writing() {
this.log("*****appname***", this.appname);
this.log("*****name***", this.props.name);
this.log("****description", this.props.description);
}
3、下载模板
在 generators/app/index.js 编写代码
destinationPath()方法,该方法主要用于获取路径。不传参时返回当前命令行运行的目录;如果收到多个参数,则会进行路径的拼接。
const download = require("download-git-repo");
module.exports = class extends Generator {
// ...
_downloadTemplate() {
return new Promise((resolve, reject) => {
const dirPath = this.destinationPath(this.dirName, '.tmp');
download('qqxu/bulletin', dirPath, err => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
// ...
}
_downloadTemplate()方法带了一个下划线前缀。Yeoman 执行顺序中有个default阶段,该阶段包含了所有用户自定义的类方法。如果某些方法不希望被 Yeoman 的脚手架流程直接调用,则可以添加一个下划线前缀。这种命名的方法,会在default阶段被忽略。
4、拷贝模板中的文件
module.exports = class extends Generator {
// 拷贝模板的文件
_walk(filePath, templateRoot) {
if (fs.statSync(filePath).isDirectory()) {
fs.readdirSync(filePath).forEach(name => {
this._walk(path.resolve(filePath, name), templateRoot);
});
return;
}
const relativePath = path.relative(templateRoot, filePath);
const destination = this.destinationPath(this.dirName, relativePath);
this.fs.copyTpl(filePath, destination, {
dirName: this.dirName
});
}
// ...
}
5、writing阶段
module.exports = class extends Generator {
// ...
writing() {
const done = this.async();
this._downloadTemplate()
.then(() => {
const templateRoot = this.destinationPath(this.dirName, '.tmp');
this._walk(templateRoot, templateRoot);
fs.removeSync(templateRoot);
done();
})
.catch(err => {
this.env.error(err);
});
}
// ...
}
6、利用用户交互数据更新package.json
_initPackage() {
let pkg = this.fs.readJSON(this.destinationPath("package.json"), {});
const { props } = this;
pkg = _.merge(pkg, {
name: props.name,
description: props.description,
author: props.author,
keywords: props.keywords,
});
this.fs.writeJSON(this.destinationPath("package.json"), pkg);
}
测试
npm link 使用
利用npm link
进行测试
在脚手架项目generator-component-lego
目录下执行
npm link
在新文件夹中执行
yo component-qqxu
1、遇到的问题
问题一:The "path" argument must be of type string. Received undefined
使用this.log
发现this.dirName
为undefined,导致上述错误,所以:
this.destinationPath(this.dirName || "", relativePath);
问题二:fs is not defined
Node fs文件系统
安装fs模块并引入npm install -S fs
const fs = require("fs");
module.exports = class extends Generator {
...
}
问题三:fs.removeSync is not a function
更改上述fs模块为 fs-extra
npm install -S fs-extra
const fs = require("fs-extra");
源码地址