脚手架概念
自动帮我们创建项目基础文件的工具。
脚手架的本质作用:创建项目基础结构、提供项目规范和约定。
常用的脚手架工具
目前市场上有很多成熟的脚手架工具。
为特定的项目类型服务的脚手架:其实现方式都是通过提供一下项目信息,脚手架根据信息创建对应的项目基础结构。只适用于自己所服务的那个项目。
React项目:create-react-app
Angular项目:angular-cli
通用型项目脚手架工具:
Yeoman, 可以根据一套模板生成对应的项目结构。
Plop, 用于创建特定类型文件,小而美的脚手架工具
Yeoman
用于创建现代化Web应用的脚手架工具
可以通过Yeoman创建Generator创建任何类型的项目,即我么可以通过创建自己的Generator来创建自己的脚手架。
基本使用
需要node环境的支持。
安装
安装yeoman
1 | yarn global add yo |
安装对应generator (比如想要生成一个node模块的项目,需要安装generator-node
)
Generators 是一个名字为 generator-XXX
的 npm 包。
Generators 列表,或者可以使用
1 | yarn global add generator-node |
创建项目目录
比如创建yeoman-node-module
目录
1 | mkdir yeoman-node-module |
运行generator创建项目
1 | yo node |
根据命令行信息提示,输入相关信息
Yeoman Sub Generator
在已有的项目基础之上添加特定文件。
比如通过generator-node下的cli sub generator生成一个cli应用所需要的文件。yo <generator>:<sub-generator>
1 | yo node:cli |
项目中package.json中添加了
1 | "bin": "lib/cli.js", |
并创建了lib/cli.js
文件
此时,我们就可以将我们的模块作为一个全局的命令行模块去使用了。
将本地模块link到全局范围
1 | yarn link |
安装项目依赖后,我们就可以通过模块名称来运行我们生产的模块了
1 | yeoman-node-module --help |
若发生了权限相关问题的报错,可以使用
chmod 755 <filemane>
来修改文件权限。
若使用yarn发生command not found的错误,需要确认命令是否正确,或需要添加 yarn 到 PATH 环境变量中。
步骤总结
- 明确需求—要做一个什么类型的项目
- 找到合适的Generator
- 全局范围安装找到的Generator
- 通过
yo
运行对应的Generator - 通过命令行交互填写选项信息
- 生成需要的项目结构
例子:webapp
- 需求,开发一款webapp
- 合适的Generator–generator-webapp
- 全局范围安装Generator–
yarn global add generator-webapp
- 运行Generator –
yo webapp
Plop
小型脚手架工具,用于创建项目中特定类型文件的小工具。
一般不会独立去使用,一般将Plop集成到项目中,用于自动生成同类型的项目文件。
解决的痛点:
比如在开发中,每个组件由js、css、test.js三个文件组成,那我们每次创建一个组件就需要手动创建三个文件,并且需要在文件中重复写入一些基础代码。
基本使用
安装
将plop作为一个基本模块安装到开发依赖中
1 | yarn add plop --dev |
创建plop生成器
在项目根目录下创建
plopfile.js
—plop工作的入口文件plopfile.js
需要接收一个plop对象,用于创建生成器任务使用
module.exports
导出一个函数,使plop
接收一个形式参数,plop.setGenerator接收两个参数:Generator名称,生成器的配置选项
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//plopfile.js
module.export = plop=>{
plop.setGenerator('component',{
//描述
description:"create a component",
//命令行交互
prompts:[
{
type:"input",
name:"name",
message:"component name",//提示
default:"Mycomponent"//默认值
}
],
//创建完成后执行动作对象
actions:[
{
type:"add",//添加一个全新的文件
path:"src/components/{{name}}/{{name}}.js",//name即命令行交互中输入的name值
templateFile:"plop-templates/components.hbs"
},{
type:"add",//添加一个全新的文件
path:"src/components/{{name}}/{{name}}.css",//name即命令行交互中输入的name值
templateFile:"plop-templates/components.css.hbs"
},{
type:"add",//添加一个全新的文件
path:"src/components/{{name}}/{{name}}.test.js",//name即命令行交互中输入的name值
templateFile:"plop-templates/components.test.hbs"
}
]
})
}添加模板文件
使用plop generator
因为在安装plop模块的时候,plop提供了一个cli程序,所以可以通过yarn来找到plop命令。
yarn plop <generator-name>
1 | yarn plop component |
步骤总结
- 将plop模块作为项目开发依赖安装
- 在项目根目录下创建
plopfile.js
作为plop入口文件 - 在
plopfile.js
文件中定义脚手架任务 - 编写用于生成特定类型文件的模板
- 通过plop提供的CLI运行脚手架任务
开发一款脚手架
使用Yeoman搭建自己的手脚架 — 自定义Generator
Generator名称
yeoman规定,Generator的名称必须是generator-<name>
的形式
Generator基础结构介绍
1 | |-generators/ # 生成器目录 |
实例:创建一个generator-sample的Generator
创建generator目录
1 | # 创建目录并进入目录 |
添加yeoman-generator模块
yeoman-generator模块提供了生成器的一个基类,在这个基类中提供了一些工具函数,让我们在创建生成器的时候更加便捷。
1 | yarn add yeoman-generator |
按照Generator结构要求,添加结构文件
generators/app/index.js 是Generator的核心入口。
需要导出一个继承自Yeoman Generator的类型。
Yeoman Gernerator在工作的时候会自动调用我们在此类型中定义的一些生命周期函数
我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能:如文件写入
1 | //index.js |
将generator link到全局,成为全局模块包
为了让Yeoman能够找到我们写的
generator-sample
1 | yarn link |
使用自定义的generator
- 创建项目文件夹
- 在项目文件夹下执行
yo sample
我们会发现,项目文件夹下有了temp.txt
文件,并在文件内写入了随机数。
实例改进1:通过模板文件创建文件
相对于手动创建每一个文件,模板的方式大大提高了效率
在生成器目录下添加templates
目录,在目录下创建模板文件。
模板文件内部可以使用EJS模板标记输出数据,例如<%= title %>
,
也可以使用其他的EJS语法,例如:
1 | <% if (success) {%> |
在生成文件时,就不用借助于fs.write
方法去写入文件,可以使用fs中专门使用模版引擎的方法–copyTpl
copyTpl
方法接收三个参数:模板文件路径、输出文件路径、模板数据上下文
1 | <!-- templates/index.html --> |
1 | const Generator =require('yeoman-generator') |
项目文件夹下执行yo sample
,会发现,创建了index.html
文件,并自动将“Hello sample generator”填充到了<%= titlte %>
位置
实例改进2:接收用户输入数据
在Generator中,想要发起一个命令行交互的询问,可以通过实现Generator中的
prompting()
方法。
在prompting
方法中,可以通过调用父类提供的prompt()
方法,发出对用户的询问。
prompt()
是一个Promise方法,返回一个Promise。在prompting
中将prompt
返回,Yeoman在工作中将会获得更好的异步流程控制。
prompt()
接收一个数组参数,数组中的每一项都是一个问题对象。
1 | prompting(){ |
1 | //index.js |
运行yo sample
,要求输入title,执行完成后,发现index.html
文件中,将输入的title内容填充到了<%= titlte %>
位置
实例改进3: 创建一系列文件
在templates文件夹下,放置一些列模版文件,在index.js中,循环拷贝映射文件到目标文件上。
1 | |-tempaltes |
简单的写下文件内容
1 | README.md 文件 |
1 | <!--index.html --> |
1 | //packaje.json |
1 | // index.js |
模板相关问题
- 当需要原封不动的输出EJS模板标记时,将模板改为
<%%= template info %>
,输出文件的模板标记即为<%= template info%>
发布Generator
Generator实际为一个npm模块,发布Generator即发布一个npm模块
将已写好Generator模块通过npm publish
发布为一个公开模块即可。
- 将源代码托管于一个开源的代码管理仓库
- 使用
npm publish
或yarn publish
命令发布 - 若使用的是淘宝镜像,发布时需要指定为官方镜像
1 | yarn publish --registry=https://registry.yarnpkg.com |
脚手架的工作原理
启动脚手架过后,通过询问相关信息,将回答的结果结合模板文件,生成项目结构
脚手架工具即一个node cli应用,所以创建一个脚手架工具即创建一个node cli工具。
实例:创建一个node cli应用
创建项目目录
sample-cli
通过
yarn init
创建package.json
文件修改
package.json
文件,添加bin
属性,指定为cli.js
1
2
3
4
5
6
7{
"name": "sample-cli",
"version": "1.0.0",
"bin":"cli.js",
"main": "index.js",
"license": "MIT"
}添加
cli.js
cli的入口文件必须有个特定的文件头
#!/usr/bin/env node
。若系统为Luinx或MacOS的话,需要修改文件权限为755
1
2
3
console.log('cli working')通过
yarn link
将模块link到全局在全局使用
sample-cli
命令,若console.log正常执行,则cli工具的基础已经可以正常工作了。完善脚手架命令
脚手架工作过程:
- 通过命令行交互询问用户问题
- 根据用户回答结果生成文件
node中使用命令行交互需要使用
inquirer
模块,安装该模块。1
yarn add inquirer
inquirer
模块通过prompt
方法发起命令行询问1
2
3
4
5
6
7
8
9
10
11
12
13//cli.js
const inquirer = require('inquirer')
inquirer.prompt([
{
type:"input",
name:"name",
message:"Project name",
}
]).then(answers=>{
console.log(answers)
})创建模板
创建
tenplates
目录,在目录下创建模板文件(可以使用EJS模板语法)1
2
3
4
5
6
7
8
9
10
11
12
13<!--index.html-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= name %></title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
</body>
</html>1
2
3
4//style.css
body{
background-color: #cccccc;
}根据模板文件创建目标文件
通过
path.join
获取tempaltes文件目录通过node 的
process.cwd()
获取命令执行的文件目录通过
fs.readdir
获取templates文件夹下的所有文件,fs.writeFileSync()
方法写入文件通过
ejs
模块的ejs.renderFile()
方法,读取文件内容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//cli.js
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(answers => {
//模板目录
const tempDir = path.join(__dirname, 'templates')
//目标目录
const distDir = process.cwd()
//将模板目录下为文件输出到目标目录
fs.readdir(tempDir, (err, files) => {
if (err) throw err
files.forEach(item => {
//item是相对template的相对路径
ejs.renderFile(path.join(tempDir, item), answers, (err, result) => {
if (err) throw err
//写入文件
fs.writeFileSync(path.join(distDir, item), result)
})
})
})
})