脚手架

Yeoman

构建定制化的脚手架.

安装

1
yarn global add yo
1
yarn global add generator-node
1
yo node
1
yo node:cli

小问题

这里执行当前项目的命令时会遇到权限问题

1
yarn link
1
sudo chmod -R 777 目录名

这里需要目录名,要么是绝对路径,要么可以先返回到上一层,再给这个文件夹权限.

自定义 generator

Yeoman 使用步骤总结

  • 明确需求
  • 找到合适的Generator
  • 全局范围安装找到 Generator
  • 通过 yo 运行对应的 Generator
  • 通过命令行交互填写选项
  • 生成你需要的项目结构

创建 Generator 模块

Generator 本质上就是一个 NPM 模块
Generator 基本结构

1
2
3
4
5
6
|— generator-sample
| |— generators/ ------------- 生成器目录
| | |— app/ ----------------- 默认生成器目录
| |— components -------- 模板文件
| |— index.js ---------- 默认生成器实现
|— package.json --------------- 模块包配置文件

如果需要提供多个 Sub Generator,可以在 app 目录添加新的生成器目录

1
2
3
4
5
6
7
8
|— generator-sample
| |— generators/ ------------ 生成器目录
| | |— app/ ---------------- 默认生成器目录
| | | |— components ------- 模板文件
| | | |— index.js --------- 默认生成器实现
+| |— component/ ------------- 其他生产器目录
+| |— index.js ------------ 其他生成器实现
|— package.json -------------- 模块包配置文件

Yeoman Generator 模快的名称必须是generator-,否则 Yeoman 后续工作无法找到你的生成器模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/index.js

// 此文件为 Generator 的核心入口
// 需要导出一个继承自 Yeoman Generator 的类
// Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些生命周期方法
// 我们在这些生命周期方法中通过调用父类提供的一些工具方法实现一些功能,例如文件写入

const Generator = require("yeoman-generator");

module.exports = class extends Generator {
writing() {
// Yeoman 自动在生成文件阶段调用此方法
// 我们尝试往项目目录中写入文件
this.fs.write(this.destinationPath("temp.txt"), Math.random().toString());
}
};
  • 在生成器目录终端执行yarn link
  • 在新建项目终端执行yo sample

根据模板创建文件

在生成器目录下添加 template 目录,模板中遵循 EJS 模板语法

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = class extends Generator {
writing() {
// Yeoman 自动在生成文件阶段调用此方法
// 通过模板方式写入文件到目标目录
// 模板文件路径
const tmpl = this.templatePath("foo.txt"); // 自动获取当前生成器下template目录下的文件路径
// 输出目标路径
const outPut = this.destinationPath("foo.txt");
// 模板数据上下文
const context = { title: "Hello zyh~", success: false };
this.fs.copyTpl(tmpl, outPut, context);
}
};

接收用户输入(终端命令行交互)prompting()方法

Yeoman 在询问用户环节会调用prompting()方法,在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问
prompt()方法参数为一个数组,数组的每一个元素是一个问题对象(命令行交互)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const Generator = require("yeoman-generator");

module.exports = class extends Generator {
prompting() {
// Yeoman 在询问用户环节会自动调用此方法
// 在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问

// 数组的每个元素都是一个问题(终端命令行交互)
return this.prompt([
{
type: "input", // input:使用用户输入的方式
name: "name", // value是最终得到结果的键(用户输入的值的键)
message: "Your project name", // 终端界面给用户的提示(问题)
default: this.appname, // 默认值,如果用户不输入则使用默认值,appname 为项目生成目录名称
},
]).then((answers) => {
// 用户交互选择的结果
// answers => { name: 'user input value' }
this.answers = answers;
});
}

writing() {
// Yeoman 自动在生成文件阶段调用此方法

// 模板文件路径
const tmp = this.templatePath("bar.html");
//输出模板路径
const outPut = this.destinationPath("bar.html");
// 模板数据上下文
const context = this.answers;
this.fs.copyTpl(tmp, outPut, context);
}
};

生成 vue 项目结构

如果模板文件中有'<%= BASE_URL %>'需要添加一个%进行转义,'<%%= BASE_URL %>'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const Generator = require("yeoman-generator");

module.exports = class extends Generator {
prompting() {
return this.prompt([
{
type: "input",
name: "name",
message: "Your project name",
default: this.appname,
},
{
type: "list",
name: "tool",
choices: ["yarn", "npm"],
message: "请选择版本管理工具",
default: "yarn",
},
]).then((answers) => {
this.answers = answers;
});
}
writing() {
// 把每一个文件都通过模板转换到目标路径

// templates目录下的模板文件相对路径
const templates = [
".browserslistrc",
".editorconfig",
".env.development",
".env.production",
".eslintrc.js",
".gitignore",
"babel.config.js",
"package.json",
"postcss.config.js",
"README.md",
"public/favicon.ico",
"public/index.html",
"src/App.vue",
"src/main.js",
"src/router.js",
"src/assets/logo.png",
"src/components/HelloWorld.vue",
"src/store/actions.js",
"src/store/getters.js",
"src/store/index.js",
"src/store/mutations.js",
"src/store/state.js",
"src/utils/request.js",
"src/views/About.vue",
"src/views/Home.vue",
];
templates.forEach((item) => {
const temp = this.templatePath(item);
const outPut = this.destinationPath(this.answers.name + "/" + item);
this.fs.copyTpl(temp, outPut, this.answers);
});
}

// 进入项目目录,根据用户选择的管理工具安装依赖
install() {
const { name, tool } = this.answers;
const npmDir = process.cwd() + "/" + name;
process.chdir(npmDir);
tool === "yarn" ? this.yarnInstall() : this.npmInstall();
// this.installDependencies({
// bower: true,
// npm: true
// })
}
};

发布 Generator

  1. 上传 GitHub
  2. yarn publish填写相关信息后,如果出现错误是因为使用淘宝镜像取代官方镜像,所以往 npm 仓库发布代码就会出现问题,淘宝镜像是只读镜像;
    • 可以修改本地的镜像配置;
    • yarn publish --registry=https://registry.yarnpkg.com(官方的镜像)
  3. 发布成功

Plop

小型脚手架工具.
相当于一个可定制化的自动生成代码文件的工具.
比如有一套文件,包括 js,css,还要复制很多几乎相同的页面文件,使用 Plop 就可以一键生成.

可以帮助我们自动生成文件

  • 将 plop 模快作为项目开发依赖安装yarn add plop --dev
  • 在项目根目录新建文件 plopfile.js 作为 Plop 入口文件,需要导出一个函数,此函数接收一个 plop 对象,用于创建生成器任务
  • 在 plopfile.js 文件中定义脚手架任务
  • 编写用于生成特定类型文件的模板
  • 通过 Plop 提供的 CLI 运行脚手架任务yarn plop component
  • 如果执行 yarn plop Home,则直接把Home作为文件名创建相应的目录文件,如何输入的是yarn plop component,则会提示输入组件名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
module.exports = (plop) => {
// 第一个:生成器名字 第二个:生成器的配置选项
plop.setGenerator("component", {
description: `cretate a component`,
prompts: [
// 命令行问题
{
type: "input",
name: "name",
message: "component name",
default: "MyComponent",
},
{
type: "input",
name: "path",
message: "output path",
default: "src/components",
},
],
// 完成命令行交互后,执行的一些动作
actions: [
{
type: "add", // 代表添加文件
path: "{{path}}/{{name}}/{{name}}.js", // 输出路径;name为上面命令行交互中得到的name值
templateFile: "plop-templates/component.hbs", // 添加的文件的模板文件
},
{
type: "add", // 代表添加文件
path: "{{path}}/{{name}}/{{name}}.css", // name为上面命令行交互中得到的name值
templateFile: "plop-templates/component.css.hbs", // 添加的文件的模板文件
},
{
type: "add", // 代表添加文件
path: "{{path}}/{{name}}/{{name}}.test.js", // name为上面命令行交互中得到的name值
templateFile: "plop-templates/component.test.hbs", // 添加的文件的模板文件
},
],
});
};

脚手架的工作原理

工作原理:启用脚手架后询问用户一些问题,将用户回答结果结合一些模板文件生成项目结构

  • 新建一个项目
  • yarn init -y初始化,在 package.json 文件下添加bin字段,用于指定 CLI 的入口文件:如:"bin": "cli.js"
  • 在项目新增 bin 入口文件 cli.js
  • Node CLI 应用入口文件必须要有这样的文件头:#!/usr/bin/env node,如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755,具体就是通过 chmod 755 cli.js 实现修改
  • 脚手架的工作过程:
    • 通过命令行交互询问用户问题
    • 根据用户回答的结果生成文件
  • node 中发起命令行交互方式使用inquirer库。安装 inquirer 库 yarn add inquirer;inquirer 库提供一个 prompt 方法用于发起命令行交互
  • commander库,命令行工具
  • chalk库,命令行输出美化
  • child_process模块,nodejs 的子进程模块,可以运行命令、检查 npm、yarn
    • child_process.execSync('yarnpkg --version').toString()
    • child_process.spawn('npm', ['isntall', 'webpack', '-D'], {shell: true})
  • download-git-repo库,下载仓库模板
  • 编写用于生成特定类型文件的模板
  • 命令行执行yarn link把模块 link 到全局
  • 新建项目,在项目命令行执行sample-scaffolding
  • 另外 webpack 的ProgressPlugin插件,用于监听编译进度
  • webpack 启动的 devServer,可以通过 nodejs 的os模块获取本机 IP 地址
1
2
3
4
sample-scaffolding
|— templates
|— cli.js
|— package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require("fs");
const path = require("path");
const inquirer = require("inquirer");
const ejs = require("ejs");

inquirer
.prompt([
{
// 命令行问题
type: "input",
name: "name",
message: "Project name?",
},
])
.then((anwsers) => {
// 用户回答
// console.log(anwsers)
// 根据用户回答的结果生成文件

// 模板目录
const tmplDir = path.join(__dirname, "templates");
// 目标目录
const destDir = process.cwd(); // 输出目录(命令行在那执行,就输出到哪个目录)
// 将模板下的文件全部转换到目标目录
fs.readdir(tmplDir, (err, files) => {
if (err) throw err;
files.forEach((file) => {
// 通过模板引擎渲染文件
ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
if (err) throw err;

// 将结果写入目标文件路径
fs.writeFileSync(path.join(destDir, file), result);
});
});
});
});