脚手架的本质作用就是创建项目基础结构、提供项目规范和约定。
脚手架工具的作用
因为在前端工程中,可能会有:
- 相同的组织结构
- 相同的开发范式
- 相同的模块依赖
- 相同的工具配置
- 相同的基础代码
脚手架就是解决上面问题的工具,通过创建项目骨架自动的执行工作。IDE创建项目的过程就是一个脚手架的工作流程。
由于前端技术选型比较多样,又没有一个统一的标准,所以前端脚手架不会集成在某一个IDE中,一般都是以一个独立的工具存在,相对会复杂一些。
常用的脚手架工具
- 第一类脚手架是根据信息创建对应的项目基础结构,适用于自身所服务的框架的那个项目。
- create-react-app
- vue-cli
- Angular-cli
- 第二类是像Yeoman为代表的通用型脚手架工具,会根据模板生成通用的项目结构,这种脚手架工具很灵活,很容易扩展。
- 第三类以Plop为代表的脚手架工具,是在项目开发过程中,创建一些特定类型的组件,例如创建一个组件/模块所需要的文件,这些文件一般都是由特定结构组成的,有相同的结构。
通用脚手架工具剖析
(1)Yeoman + Generator
如果使用Yeoman:
- 在电脑上全局安装Yeoman:
yarn global add yo
- Yeoman要搭配相应的Generator创建任务,所以要安装Generator。例如是创建node项目,则安装generator-node:
yarn global add generator-node
- 创建一个空文件夹:
mkdir my-module
, 然后进入文件夹:cd my-module
- 通过Yeoman的yo命令安装刚才的生成器(去掉生成器名字前的generator-):
yo node
- 交互模式填写一些项目信息,会生成项目基础结构,并且生成一些项目文件,然后自动运行
npm install
安装一些项目依赖。
(2)SubGenerator
- 运行SubGenerator的方式就是在原有Generator基础上加上:SubGenerator的名字,如:
yo node:cli
- 在使用SubGenerator前,要先去查看一下Generator之下有哪些SubGenerator
(3)Plop
如何使用Plop创建文件:
- 将plop模块作为项目开发依赖安装
- 在项目根目录下创建一个plopfile.js文件
- 在plopfile.js文件中定义脚手架任务
- 编写用于生成特定类型文件的模板
- 通过Plop提供的cli运行脚手架任务
脚手架工作原理
脚手架的工作原理就是在启动脚手架之后,回自动地去询问一些预设问题,通过回答的结果结合一些模板文件,生成项目的结构。
使用NodeJS开发一个小型的脚手架工具:
- 用
yarn init
初始化一个空文件夹:sample-scaffolding
- 在
package.json
中添加bin
属性指定脚手架的命令入口文件为cli.js
{
"name": "sample-scaffolding",
"version": "1.0.0",
"main": "index.js",
"bin": "cli.js",
"license": "MIT",
"dependencies": {
"ejs": "^3.1.3",
"inquirer": "^7.1.0"
}
}
- 编写
cli.js
#!/usr/bin/env node
// Node CLI 应用入口文件必须要有这样的文件头
// 如果Linux 或者 Mac 系统下,还需要修改此文件权限为755: chmod 755 cli.js
// 脚手架工作过程:
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件
const path = require('path')
const fs = require('fs')
const inquirer = require('inquirer') // 发起命令行交互询问
const ejs = require('ejs') // 模板引擎
inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Project name?'
}
]).then(answer => {
console.log(answer)
// 模板目录
const tempDir = path.join(__dirname, 'templates')
// 目标目录
const destDir = process.cwd()
// 将模板下的文件全部转换到目标目录
fs.readdir(tempDir, (err, files) => {
if (err) throw err
files.forEach(file => {
// 通过模板引擎渲染文件
ejs.renderFile(path.join(tempDir, file), answer, (err, result) => {
if(err) throw err
// 将结果写入到目标目录
fs.writeFileSync(path.join(destDir, file), result)
})
})
})
})
- 将该cli程序link到全局:
yarn link
- 然后在其他文件夹中执行:
sample-scaffolding
命令,就可以根据模板自动化创建文件了。
自定义Generator开发脚手架
|-- generators/ ······生成器目录
| |-- app/ ······默认生成器目录
| |–templates ······模板文件夹
| |–foo.txx ······模板文件
| |–index.js ······默认生成器实现
| |–component/ ······其他生成器目录
| |–index.js ······其他生成器实现
|–package.json ······模块包配置文件
创建Generator生成器的步骤:
-
mkdir generator-sample
-
cd generator-sample
-
yarn init
-
yarn add yeoman-generator
-
创建文件:generators/app/index.jsx
-
// 此文件作为Generator的核心入口 // 需要导出一个集成字Yeoman Generator的类型 // Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法 // 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,比如文件写入 const Generator = require('yeoman-generator') module.exports = class extends Generator { prompting () { // Yeoman 在询问用户环节会自动调用次方法 // 在此方法中可以调用父类的prompt()方法发出对用户命令行询问 return this.prompt([ { type: 'input', name: 'name', message: 'Your project name', default: this.appname // appname为项目生成目录 } ]).then( answers => { // answers => {name: 'user input value'} this.answers = answers }) } writing () { // Yeoman 自动在生成文件阶段调用次方法 // 我们这里尝试往项目目录中写入文件 // this.fs.write( // this.destinationPath('temp.txt'), // Math.random().toString() // ) // 通过模板方法导入文件到目标目录 // 模板文件路径 const tmpl = this.templatePath('foo.txt') // 输出目标路径 const output = this.destinationPath('foo.txt') // 模板数组上下文 const context = {title: 'Hello', success: false} // const context = this.answers // 从命令行获取的参数 this.fs.copyTpl(tmpl, output, context) } }
-
templates/foo.txt作为模板文件
这是一个模板文件 内部可以使用EJS模板标记输出数据 例如:<%= title %> 其他的EJS语法也支持 <%if (success) {%> hello world <%}%>
-
执行
yarn link
, 此时这个模块就会作为全局模块被link到全局,别的项目可以直接使用它。 -
创建一个别的文件夹my-proj, 在这个文件夹中执行:
yo sample
-
发布到npmjs网站上:
yarn publish --registry=https://registry.yarnpkg.com
Plop
yarn add plop
plopfile.js
// Plop 入口文件,需要导入一个函数
// 此函数接受一个plop对象,用户创建生成器任务
module.exports = 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',
templateFile: 'plop-templates/component.hbs'
},
{
type: 'add', // 代表添加文件
path: 'src/components/{{name}}/{{name}}.css',
templateFile: 'plop-templates/component.css.hbs'
},
]
})
}
编写模板:
component.hbs:
import React from 'react';
export default () => (
<div className="{{name}}">
<h1>{{name}} Component</h1>
</div>
)
Component.css.hbs:
import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';
it('renders without crashing', () => {
const div = documents.createElement('div');
ReactDOM.render(<{{name}}/>, div);
ReactDOM.unmountComponentAtNode(div)
})
执行命令:yarn plop component