二、脚手架

163 阅读7分钟

什么是脚手架

  1. 前端工程化的发起者
  2. 脚手架的本质作用是创建项目的基础机构、提供项目规范和约定
  3. 例如 vue 项目的 vue-cli

常用脚手架使用

yeoman

THE WEB'S SCAFFOLDING TOOL FOR MODERN WEBAPPS

1 基本用法

1-1 安装

// 在全局范围安装yo,通过yo --version查看版本
npm install yo --global # or yarn global add yo

// 全局安装对应的generator(yeoman中生成器都是generator+名称样式)
npm install generator-node -- global # or yarn global add generator-node

1-2 运行

mkdir my-module
yo node // 这个生成器是 generator-node, generator-webapp 生成器执行 yo webapp,也就是 generator- 后面的

执行 yo node(生成器名) 就和使用 vue created xx 一样。不同的是使用yeoman是先建一个文件夹,在执行命令,命令还可以直接 yo,然后会出来生成器选项。

1-3 yeoman使用步骤总结

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

更多信息请查看: www.cnblogs.com/nzbin/p/575…

2 自定义Generator

其实就是创建一个npm模块
官网教程:yeoman.io/authoring/
注意点:mac 如果报错了,别急,更新一下 yeoman,老版本不支持 venture 系统

2-1 基本的Generator实现

创建文件夹并初始化
// 模块名前面一定是generator-
mkdir generator-zqt && cd generator-zqt
npm init -y #or yarn init
// 创建yeoman的生成器需要用到 yeoman-generator
npm install yeoman-generator #or yarn add yeoman-generator
创建如下目录结构
generator-zqt
|-- generators
    |-- app
        index.js
|-- node_modules
package.json
yarn.lock
编写核心入口index.js
// 此文件作为 Generator 的核心入口
// 需要导出一个继承自 Yeoman Generator 的类型
// Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些生命周期方法
// 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,例如文件写入

const Generator = require('yeoman-generator')

module.exports = class extends Generator{
    writing(){
        // Yeoman 自动生成文件阶段调用此方法
        // 我们这里尝试往项目目录中写入文件 这个fs不是node的fs
        // 第一个参数是生成的文件名,第二个是文件内容
        this.fs.write(
            this.destinationPath('temp.txt'),
            Math.random().toString()
        )
    }
}
发布生成器

根目录下执行yarn link,把这个模块链接到全局范围,使之成为全局模块包。

npm link  # or sudo npm link #or yarn link
使用

使用 yo zqt;或者 yo ,下面就会出现 zqt 的选项。运行后,执行此命令的根目录下就会生成 temp.txt 文件。

以上就是一个最简单的生成器案例,现在我们修改生成器,不需要再次 yarn link

2-2 根据模板创建文件

目录结构
generator-zqt
|-- generators
    |-- app
        |-- templates
            |-- foo.txt // 模板
        |-- index.js
|-- node_modules
|-- package.json
|-- yarn.lock
核心入口 index.js
const Generator = require('yeoman-generator')

module.exports = class extends Generator{
    writing(){
        // 通过模板方式写入文件到目标目录
        // 模板文件路径
        const tmp1 = this.templatePath('foo.txt')
        // 输出目标路径
        const output = this.destinationPath('temp.txt')
        // 模板数据上下文
        const context = {title: 'hello world', success: false}

        this.fs.copyTpl(tmp1, output, context)
    }
}
模板和生成的结果

foo.txt

这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:<%= title %>

其它的 EJS 语法也支持

<% if(success) {%>
哈哈哈哈啊哈哈
<% }%>

temp.txt

这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:hello world

其它的 EJS 语法也支持

2-3 接收用户输入

我们使用vue/cli创建项目时,会提示你项目名,选择 less 什么的,这时候就是用到的接收用户输入

核心入口index.js
const Generator = require('yeoman-generator')

module.exports = class extends Generator{
    prompting(){
        // Yeoman 在询问用户环节会自动调用此方法
        // 在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问
        return this.prompt([
            {
                type: 'input',
                name: 'title', // 接收值的建
                message: 'Your project name is',
                default: this.appname  // appname 为项目生成目录名称
            }
        ])
        .then(answers => {
            // answers => {name: 'user input value'}
            this.answers = answers
        })
    }
    writing(){
        const tmp1 = this.templatePath('bar.html')
        const output = this.destinationPath('bar.html')
        const context = this.answers

        this.fs.copyTpl(tmp1, output, context)
    }
}
模板bar.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <%= title%>
</body>
</html>

2-4 比较完整的Genrator案例

在templates里添加模板

使用 vue create 创建一个默认项目,然后拷贝你需要的,放入 templates 里面;当然你也可以手动添加你所需要的西; 现在templates里面是这样子

templates
|-- pulblic
    |-- index.html
|-- src
    |-- assets
    |-- components
    |-- App.vue
|-- babel.config
|-- package.json
|-- package-lock.json
|-- README.md
核心入口index.js
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
            }
        ])
        .then(answers => {
            this.answers = answers
        })
    }

    writing() {
        const templates = [
            'babel.config.js',
            'package.json',
            'README.md',
            'yarn.lock',
            'public/favicon.ico',
            'public/index.html',
            'src/App.vue',
            'src/main.js',
            'src/assets/logo.png',
            'src/components/HelloWorld.vue',
            'src/router/index.js',
            'src/store/index.js',
            'src/views/About.vue',
            'src/views/Home.vue'
        ]

        templates.forEach(item => {
            this.fs.copyTpl(
                this.templatePath(item),
                this.destinationPath(item),
                this.answers
            )
        })
    }
}

如果需要根据输入改变项目中的内容,记得别忘记用Ejs语法修改模板,<%%=%>前面多一个%的就是原封输出。

发布generator

将本地编写的 generator 提交到 gitee(其它能被别人访问到的地方都可以)。然后使用 npm publish 命令来发布模块。(这时候不能使用淘宝镜像,要切到npm原来的镜像)
根目录下运行

yarn publish --registry=https://registry.yarnpkg.com
// 或者

然后过一会在npm官网就能看到了,yarn logout 退出

二、 Plop

这是一个小的脚手架工具,常常用来生成文件夹,就像小程序开发中新建page的功能一样。

1 plop基本使用

将plop安装到开发依赖中
npm install -D plop #or yarn add plop --dev
在项目根目录下新建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/components.hbs'
            }
        ]
    })
}
在项目根目录下创建模板文件
plop-templates
|-- component.hbs

components.hbs

export default () => {
    <div className="{{name}}">
        <h1>{{name}}component</h1>
    </div>
}
使用
npm plop component #or yarn plop component
// 这个component就是plopfile.js文件中setGenerator函数的第一个参数。
plop使用总结
  • 将plop模块作为项目开发依赖安装
  • 在项目根目录下创建一个plopfile.js文件
  • 在plopfile.js文件中定义脚手架任务
  • 编写用于生产特定类型文件的模板
  • 通过plop提供的CLI运行脚手架任务

三、脚手架工作原理

  • 启动时候会询问你一些预设的问题
  • 根据答案结合模板文件生产相应的目录结构

通过node开发一个小型的脚手架工具,深入体会脚手架工作原理。

1 简单的脚手架

创建文件夹并初始化
mkdir xx
cd xx
init -y #or yarn init
根目录添加cli.js文件

cli.js

#!/usr/bin/env node

// Node CLI 应用入口文件必须要有这样的文件夹
// 如果是 Linux 或者 macOs 系统下还需要修改此文件的读写权限为 755
// 具体操作 chmod 755 cli.js

console.log('cli working');
package.json添加bin
"main": "index.js",
+ "bin": "cli.js"
通过 npm link link到全局
npm link  // sudo npm link #or yarn link
运行
sample-scaffolding

以上是一个基础的脚手架例子,本质就是运行cli.js,然后cli.js执行相关读写操作。

2. 实现脚手架的具体业务

2-1. 基本代码

安装 inquirer,用于询问用户
npm install inquirer #or yarn add inquirer
根目录下创建cli.js
#!/usr/bin/env node

// 脚手架的工作过程
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件

const inquirer = require('inquirer')

inquirer.prompt([
    {
        type: 'input',
        name: 'title',
        message: 'project name is'
    }
]).then(answers => {
    console.log(answers);
})
直接运行 sample-scaffolding ,不需要在link了
scample-scaffolding

这时就要求你输入,并且打印了你输入的。

2-2. 使用模板

在根目录下创建模板文件夹
templates
|-- index.html|
|-- ...
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title %></title>
</head>
<body>
    <%= title %>
</body>
</html>
安装ejs模块
npm install ejs
cli.js
#!/usr/bin/env node

const inquirer = require('inquirer')
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')

inquirer.prompt([
    {
        type: 'input',
        name: 'title',
        message: 'project name is'
    }
]).then(answers => {
    console.log(answers);
    // 模板目录
    const tmplDir = path.join(__dirname, 'templates')
    // 目标目录
    const destDir = process.cwd()

    // 将磨板下的文件全部在目标目录下生成
    fs.readdir(tmplDir, (err, files) => {
        if(err) throw err
        files.forEach(file => {
            console.log(file);
            ejs.renderFile(path.join(tmplDir, file), answers, (err, result) => {
                if(err) throw err
                console.log(result);
                // 将结果写入目标文件路径
                fs.writeFileSync(path.join(destDir, file), result)
            })
        })
    })
})

然后在某个文件夹里运行 sample-scaffolding,就会在前目录下生成相应文件