前端进阶:前端工程化之脚手架

1,599 阅读15分钟

前言:博主2020.3正式进军前端,目标高级前端工程师,经验尚浅,文章内容如若有误,欢迎指正。

说到前端的脚手架,大家肯定立马就能想到vue-cli、crate-react-app以及angular-cli等lib,但是事实上,它们并不是脚手架,它们只是用来创建脚手架的脚手架工具罢了。

在日常开发当中,我们经常使用这些脚手架工具来帮助我们创建脚手架项目。但是到底什么是脚手架,什么是脚手架工具,它解决的问题是什么,我们可能未必有很清晰的认识。接下来本文会先从认识脚手架开始,再到探讨脚手架自动化搭建脚本工具的原理,而后学习如何使用Plop以及Yeoman工具来帮助我们搭建脚手架,最后学习如何使用Yeoman工具自定义脚手架工具

一:认识脚手架

1.脚手架的概念

百度百科中的脚手架概念:脚手架是为了保证各施工过程顺利进行而搭设的工作平台。概念有点生涩,上图理解:

  • 建筑脚手架
  • 床脚手架

结合这两幅图以及百度百科中的概念,我们应该对脚手架有了更清晰的认识。脚手架提供了一个基础工作平台,使得后续的工作人员能够更加规范便捷的进行工作。基于它,改变它,可以称得上是对脚手架的灵魂描述。

回到前端,工作人员自然是指代码开发人员,而工作平台往大了说可以是指整个项目,往小了说可以是指几个相关联的文件或者一个文件。

基于上面对脚手架的理解,下面我们列举几个在日常开发当中常见的脚手架,并对它们进行分类:

日常示例脚手架 / 基础工作平台脚手架类型
创建html文件H5模板代码单文件脚手架
创建react组件一个模板.js文件、一个模板.css文件、一个模板.test.js文件多文件脚手架
创建react工程react模板项目项目脚手架

好了,经过上面的分析总结,我们基本上已经达到了认识脚手架的目的。下面我们再讨论解决另一个脚手架的现象级问题,这个问题就是脚手架的复用。

2.脚手架的复用

除了定制的脚手架之外,大部分的脚手架都是可以复用的,也就是说可以抽取出一个脚手架模板。这和它本身概念,是一个基础的工作平台很有关系,很多时候就是因为我们需要对将要进行的工作进行一些模板性质的规范、约定以及前置工作,才具象化出一个脚手架。在前端当中,如上表示例中的基础工作平台,都有模板二字,它们都是使用脚手架模板搭建的脚手架。

换一个角度来说,脚手架可以也经常用来解决复用问题,所以在前端开发当中,我们很多时候使用脚手架的初衷,就是解决创建文件或者项目时复用问题(注意不仅仅代码结构以及代码上的复用,更是一种开发模式的复用,涉及到开发套路、规范、约定等等)。

说到这里,我们日常使用create-react-app,vue-cli等脚手架创建项目时,我们的主要目的是复用这些脚手架工具生成的脚手架所遵从的某种开发模式(通常我们会把它们遵从的开发模式叫做最佳实践),而创建项目以及代码结构只是顺便操作以及表面现象而已

在还没理解脚手架工具所创建脚手架的开发模式之前,就匆忙使用这些脚手架工具进行业务开发可能是我们日常开发当中处处不顺,还写得一手臭代码的根本原因之一

扯远了,经过上面的探讨,我们已经了解了脚手架的复用问题,下面我们再探讨一下脚手架的搭建问题。

3.脚手架的搭建

对前端而言,脚手架是一种开发模式的载体,它的具象化特征就是一 / 多个带有特定结构和内容的文件或者项目。所以对于搭建一个脚手架来说,准备这些原材料(特定结构和内容,复用时表现为模板文件或项目)之后然后按照一定行为逻辑(人工 / 脚本)搭建脚手架(创建文件、文件)即可。当然,通常而言我们不会搭建一个和原材料一样的脚手架,我们会接收用户的输入或配置文件而后去搭建脚手架,实现脚手架的部分定制

原材料(模板文件 / 项目)由具体场景决定,接下来本文不再探讨。接下来行文会关注准备好原材料之后,脚手架的搭建过程。

在往前,需要项目 / 文件内容复用时,我们通常都是通过人工拷贝来解决的,但是这都快2021年了,按照任何简单机械的重复劳动都应该让机器去完成的思想,我们应该自动化去搭建一个脚手架,更方便的是,自定义一个脚手架生成工具。

在探讨脚手架的自动化搭建之前,我觉得有必要再重申一点,脚手架的灵魂不在于脚手架的搭建过程,更不在于自动化,脚手架的灵魂在于原材料(通常也就是文件模板、项目模板)

好的,经过上面的分析认识,接下来我们先探讨一下脚手架的自动化搭建脚本 / 工具的原理,而后探讨一下如何利用Plop搭建单 / 多文件类型的脚手架,最后探讨一下如何利用Yeoman搭建一个项目类型的脚手架以及如何基于Yeoman搭建一个脚手架工具

二:脚手架自动化搭建脚本 / 工具的原理

我们在使用react-create-app、vue-cli等脚手架工具搭建脚手架项目时,它会先询问一些问题,而后这些脚手架会根据这些问题的答案并结合它自带的项目模板创建出一个脚手架项目。

根据这个需求以及上文对脚手架的认识,我们来探讨一下这些脚手架工具的原理。我们先用函数式编程的思想来思考一下脚手架的自动化脚本该如何编写:

  • 输入:脚手架模板(原材料)、用户输入的模板参数
  • 输出:单文件 / 多文件 / 项目
  • 映射关系:模板解析、IO读写等操作

根据这个思路,我们实现一个单文件类型的脚手架工具(多文件以及项目类型脚手架只不过复杂一些,原理一致),它可以根据模板文件内容和用户输入自动生成一个结果模板解析后的指定命名的文件,示例如下:

1.原理探索示例:单文件脚手架自动化创建脚本

  • 模板(template.txt):ejs模板引擎规范
xxxx
<%= param1 %>
xxxx
  • 自动化创建脚本(cli.js)
const inquirer = require('inquirer'); // 用于与命令行交互
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');	// 用于解析ejs模板
const { Transform } = require('stream');	// 用于流式传输

inquirer.prompt([{
      type: 'input',
      name: 'name',
      message: 'file name?'
    },
    {
      type: 'input',
      name: 'param1',
      message: 'param1?'
    }
  ])
  .then(anwsers => {
    // 1.根据用户输入:得到文件名和文件夹路径(用户命令路径)
    const fileName = anwsers.name;
    const param1 = anwsers.param1;
    const dirPath = process.cwd();
    // 2.得到模板文件路径
    const tmplPath = path.join(__dirname, 'template.txt');
    const filePath = path.join(dirPath, fileName + '.txt');
    // 3.读取模板文件内容,写入到新创建的文件
    const read = fs.createReadStream(tmplPath);
    const write = fs.createWriteStream(filePath);
    // 转换流:用于ejs模板解析
    const transformStream = new Transform({
      transform: (chunk, encoding, callback) => {
        const input = chunk.toString();// 模板内容
        const output = ejs.render(input, {param1}); // 模板解析
        callback(null, output);
      }
    })
    read.pipe(transformStream).pipe(write);
  })

  • 执行脚本
c:\workspace\automate node cli.js
? project Name? result
? param1? oooo
  • 模板渲染结果(result.txt)
xxxx
oooo
xxxx

代码中用到了文件流,这里简单提一提。上述示例中肯定是不用文件流也没问题的,因为它的模板文件内容不大,直接用readFileSync加上writeFileSync先读再写即可。文件流主要用来解决文件内容过大的问题,防止读的文件内容过大进而占用程序的运行内存太多甚至占满

好的,上述示例就是一个最基本的脚手架自动化搭建脚本,虽然简单,但我认为这就是脚手架自动化搭建脚本以及工具的核心逻辑

对于一个优秀的自动化脚本和工具而言,不但要功能强大、性能优越,更要简单易用。对于这个改造优化,参考成熟脚手架工具的做法,我们可以通过把它抽取成一个独立的npm cli模块,并且链接到全局中的方式来实现。此外,如果希望share出去提供给别人使用,那么我们可以把这个cli模块发布到github和npm repository中。接下来我们探讨一下具体做法。

2.脚本 / 工具易用性考量:打包成一个独立的npm cli全局模块并发布

打包成一个独立的脚手架普通模块很简单,把模板和脚本抽取到一个新的npm 模块当中即可。我们这里探讨一下如何把一个普通模块配置成一个cli全局模块。首先明确打包成npm cli全局模块的目的是为了让其它模块可以访问并且调用,按照这个寻找访问以及调用的思路,我们把一个普通模块配置成cli模块,可以通过以下步骤实现:

  • 为入口cli脚本加上文件头(cli.js)
#!/usr/bin/env node

# 开始脚本内容...
  • 配置入口(package.json):配置bin选项 { "命令名称": "入口文件路径" }
	"bin": {
		"testCli": "./cli.js"
	}
  • 链到全局
yarn link
# 成功后可以在yarn的全局bin(通过命令yarn global bin查看)中看到testCli.cmd文件。
  • 测试使用
testCli
# 如果找不到命令,则考虑是不是没有把yarn的全局bin路径配置为环境变量。

通过以上步骤,即可在其它模块中使用配置的bin命令调用这个cli模块,它会自动找到这个cli模块的这个入口文件并执行脚手架脚本。

如果需要方便其它人使用,那么可以考虑把这个模块发布到github以及npm repository中,思路以及步骤如下:

git init
git remote add origin https://github.com/iamjwe/scaffolding-demo
git add .
git commit -m "initial"
git push -u origin master
  • 发布到npmpkg / yarnpkg
# 权限认证通过后
npm/yarn publish
# 注意点1:使用了镜像源时需加上--registry参数显示指定推送指定
npm publish --registry=https://registry.npmjs.org
# 注意点2:很容易出现npm包的同名以及触发垃圾邮件检测的命名问题,此时需要修改包名重新发布

至此,一个简单的脚手架工具从开发到发布的过程我们都已经走通了。在日常开发中,对于复杂的脚手架,我们通常不会自己从0到1去实现脚手架工具,因为基于node的api去编写这个脚本要大量的关注IO读写以及模板解析等等逻辑过程。这偏离了初衷,毕竟我们使用脚本或者工具实现自动化的目的是为了提高效率。

下面我们站在巨人的肩膀上,探讨一下如何利用Plop搭建单 / 多文件类型的脚手架,最后探讨一下如何利用Yeoman搭建一个项目类型的脚手架以及如何基于Yeoman搭建一个脚手架工具。以达到提高搭建脚手架和打造脚手架工具的效率的目的。

三:基于Plop搭建单 / 多文件类型的脚手架

Plop是一个在日常开发当中非常值得且频繁使用的小工具,在我们碰到需要搭建一个单 / 多文件类型的脚手架需求,比如搭建一个react组件脚手架(需要创建一个.js模板文件、一个.css模板文件以及一个.test.js模板文件),Plop就会是一个不错的助手。相对于完全自定义实现脚手架脚本 / 工具而言,使用Plop虽然不能实现完全的自定义,但在它的规范下编写脚本可以调用它封装好的Api,实现配置化的脚手架工具。这让我们能够更加关注任务本身,而无需关注过多任务的实现细节,进而提高开发效率

上面已经讲了脚手架的原理,所以我们接下来主要关注Plop自动化搭建脚手架的工作流程,具体的使用细节请查看官方文档,GitHub:Plop repo

Step1:安装Plop工具

yarn add plop --dev

执行上述命令后,会在node_module下出现一个plop包以及在node_module/.bin下出现一个plop.cmd文件,这使得我们在调用Plop工具时,使用命令yarn / npx plop 任务名 即可。

Step2:准备模板内容文件

如react组件的目标js文件:component.hbs

import React from 'react';

export default () => (
  <div className="{{name}}">
    <h1>{{name}} Component</h1>
  </div>
)

Step3:编写Plop任务脚本

先在项目根目录下创建一个plopfile.js作为plop执行任务时的入口文件。而后在这个入口文件中编写注册我们希望它做的任务,就可以实现配置化的脚手架工具的功能。

接下来我们编写一个plop自动化创建任务,方便我们创建react组件时调用,它会根据预定义好的模板内容自动创建一个.js模板内容文件、一个.css模板内容文件以及一个.test.js模板内容文件,实现代码如下:

module.exports = plop => {
  // generator是Plop的任务单位,component是任务名
  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'
      },
      {
        type: 'add',
        path: 'src/components/{{name}}/{{name}}.test.js',
        templateFile: 'plop-templates/component.test.hbs'
      }
    ]
  })
}

Step4:使用Plop执行Plop任务

yarn plop component

四:基于Yeoman搭建一个项目类型的脚手架

Yeoman是一个现代WEB应用程序的WEB脚手架工具,个人认识它主要用来创建项目类型的脚手架。相比较于create-react-app、vue-cli等专注于某一个框架的脚手架工具而言,Yeoman是一个更加通用的脚手架工具。接下来通过使用别人的项目模板(generator),我们探讨一下使用Yeoman搭建一个脚手架项目的工作流。

Step1:安装Yeoman

yarn global add yo

Step2:安装项目模板:generator

如下示例安装一个node项目模板

// generator repo https://yeoman.io/generators/
yarn global add generator-node

Step3:使用yo运行generator创建模板项目

项目模板名去掉generator-,即可作为yo的参数创建项目

yo node

五:基于Yeoman自定义一个脚手架工具

基于Yeoman自定义一个脚手架工具,这个脚手架工具通常用来搭建项目类型的脚手架。实现上其实也就是自定义Yeoman的Generator,在这个过程中我们需要准备自己的脚手架模板项目并编写自己的generator模块。与Plop一样,相对于完全自定义实现脚手架脚本 / 工具而言,使用Yeoman虽然不能实现完全的自定义,但在它的规范下编写脚本可以调用它封装好的Api,可以简化脚本的编写。这让我们能够更加关注任务本身,而无需关注过多任务的实现细节,进而提高开发效率

Step1:安装Yeoman

Step2:准备项目模板

如以下Vue模板项目目录结构图: 在这里插入图片描述

Step3:编写自己的generator模块:根据模板项目创建项目的逻辑

这里自定义实现一个简单的generator:拷贝并解析模板项目创建一个项目到当前路径

  • 以generator-开头创建并初始化一个模块项目(generator-demo)
  • 拷贝项目模板到当前模块下
  • 编写generator入口文件:解析项目模板创建项目
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 () {
  	// 此处文件名列表应该使用IO脚本读取
    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 => {
      // 利用fs的copyTpl方法:解析ejs模板文件后放入目标路径下
      this.fs.copyTpl(
        this.templatePath(item),
        this.destinationPath(item),
        this.answers
      )
    })
  }
}
  • 使用yarn link 将当前模块链接到全局以备使用时能把yo找到
  • 选择性地将generator模块发布到GitHub、npm以及generator repo中

思路和实现上面已经讲过,此处不再展开赘述。

Step4:使用yo运行generator创建模板项目

yo demo

本文结束,观众老爷您慢走,欢迎下次光临。