帮助你建立前端脚手架的系统化认识

3,335 阅读18分钟

摸鱼酱的文章声明:内容保证原创,纯技术干货分享交流,不打广告不吹牛逼。

前言:我们每天都在基于脚手架开发,但你真的清楚什么是脚手架,如何搭建自己的脚手架吗?掌握前端脚手架知识,是学习前端工程化和进阶高级前端所必不可少的部分。这篇文章,我尝试把我自己对于脚手架相关知识的所有认知托盘而出,以求能够帮助你建立对前端脚手架的系统化认识。

对于前端脚手架的系统化认识,我从个人认识出发,用脑图做了以下整理:

接下来的行文,我都会围绕这副脑图展开,如果您有兴趣继续往下看下去,我希望您能在这幅图上停留多一些时间。

好地,按照上述脑图中的逻辑,接下来我会分成以下几个部分来展开探讨本文。

  • 理解前端脚手架
  • 基于IDE搭建脚手架
  • 原生脚本搭建脚手架
  • 基于Plop搭建脚手架
  • 基于Yeoman搭建脚手架

好的,理清了行文思路之后,下面我们进入第一部分,理解前端脚手架。

一:理解前端脚手架

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

好的,不卖关子,下面我们就进入对脚手架概念的理解。

1.脚手架的概念

在其它行业,其实早已有了脚手架这个概念。所以前端脚手架的概念,只是在前端工程化的发展进程中,从其它行业中借鉴并逐渐稳定下来罢了。在百度百科中,脚手架的概念是:脚手架是为了保证各施工过程顺利进行而搭设的工作平台。概念有点生涩,我们上图理解:

  • 建筑脚手架

  • 床脚手架

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

回到前端,工作人员自然是指前端开发者,而工作平台往大了说可以是指整个项目,往小了说可以是指几个相关联的文件或者一个文件,甚至是一个代码段。基于这个认识,我们根据工作平台的大小,对脚手架做出以下分类:

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

好的,经过上面的分析总结,我们基本上已经达到了认识脚手架的目的。只理解脚手架的概念对搭建前端脚手架来说还是不够,下面我们再通过模板这个词来建立对脚手架的实践理解。

我这么用心写博客分享,难道不值得你点个赞和关注支持一下吗?(^-^

2.脚手架和模板

脚手架搭建的是一个基础工作平台,因为这个基础工作平台往往会被复用,所以脚手架概念和模板概念会有不少的联系。这一点我并不想通过太多文字性的东西去解释,我想要做到的只是引导你把及脚手架和模板这两个概念联系起来,然后自行领会,目的也是为了提高阁下对脚手架的理解程度以及更好地理解脚手架的搭建思路和过程。

伴着模板这个词,我引出几个项目搭建脚手架化可以带来的几个优点:

  • 可复用
  • 统一化
  • 规范化

3.前端脚手架的搭建思路

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

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

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

在取得以上的认识之后,下面我们就进入脚手架搭建的探讨,如上述脑图所示,在本文中,我会依次介绍基于IDE搭建、原生脚本搭建、基于Plop搭建和基于Yeoman搭建四种方式。

好的,我们先进入最易于接受也很常见的基于IDE搭建脚手架方式的探讨,为后面我们原生脚本搭建脚本以及工具搭建脚本的方式做一个铺垫。

二:基于IDE搭建脚手架

这里假设我们的需求就是生成一个react hook组件,一个代码段类型的脚手架。这种方式很好理解,阁下好好看看我下面的这个demo即可,下面会按照顺序分成如下三个步骤来实现脚手架的搭建:

  • 准备脚手架模板
  • IDE注册脚手架
  • IDE生成脚手架

1.准备脚手架模板

假设我们的react hook组件脚手架模板,动态部分为模板名称(如下代码中的$0):

import React, { useState, useEffect } from 'react';
import type { FC } from 'react';

export interface $0Props {}

const $0: FC<$0Props> = (props) => {
  useEffect(() => {
    console.log('init data');
  }, []);

  return (
    <>
      <p> $0 component </p>
    </>
  );
};

export default $0;

2.IDE注册脚手架

打开用于配置代码模板的json文件,vscode打开方式为:file -> preferences -> users snippets -> typescipt react。

这里以vscode为例,其它IDE可以自行搜索对应的代码模板如何配置。

下面是对如上demo脚手架模板的配置:

{
	"react_component": {
		"prefix": "rc",
		"body": [
			"import React, { useState, useEffect } from 'react';",
			"import type { FC } from 'react';",
			"\n",
			"export interface $0Props {",
			"}",
			"\n",
			"const $0: FC<TestProps> = (props) => {",
				"useEffect(() => {",
					"console.log('init data')",
				"}, [])",
				"\r\n",
				"return (",
					"<>",
					"<p> $0 component </p>",
				"</>",
				")",
			"}",
			"\r\n",
			"export default $0;"
		],
		"description": "a react component template with hooks"
	}
}

试了一下之后,感觉vscode配置代码模板好麻烦呀,也可能是我姿势不对吧。

上述配置中,

  • 脚手架模板名称为react_component
  • 触发模板提示的输入为prefix配置项:rc
  • 脚手架的模板内容为body配置项:一个row数组(麻烦的一匹,拷贝过来还要拆分)
  • 脚手架的描述为description配置项:a react component template with hooks

3.IDE生成脚手架

新建demo.tsx文件,输入rc触发IDE的脚手架模板react_component代码提示,选择后即可得到我们的代码模板。

4.总结

代码段类型的脚手架我们通常都会叫他代码模板,实际上这也可以算作一种脚手架。

实践证明,实现这种类型的脚手架还是很简单的,好好利用吧,可以提高不少开发效率哦,毕竟我们日常开发的重复代码还是挺多的!

事实上,因为懒的关系,我自己很少用,还是复制粘贴的多,哈哈哈。

代码段类型的脚手架并不会涉及到文件的创建,其它三种则都会涉及到,所以在探讨基于工具如Plop和Yeoman搭建文件类型(项目也是由文件组成)脚手架之前,我们先通过探讨原生脚本搭建脚手架来学习一下文件类型脚手架的原理(ps:也是vue-cli、create-react-app等等脚手架工具创建脚手架的原理哦)。

三:原生脚本搭建脚手架

为了更贴近于实际开发,这里我们模仿react-create-app、vue-cli等脚手架工具搭建脚手架项目时的做法,做一个简单的单文件类型脚手架的搭建demo(多文件和项目类型的脚手架原理也一样)。

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

1.准备脚手架模板

xxxx
<%= param1 %>
xxxx

为了便于理解,上述模板我已经尽可能简单化了,其中模板中的动态部分就是param1参数。

我们的需求就是实现的一个脚本,这个脚本在运行之后能够根据模板内容创建得到一个单文件脚手架,并且其中模板中的param1参数需要替换成我们的输入参数。

2.编写脚手架创建脚本

我们用函数式编程的思想来思考一下我们的单文件脚手架的自动化脚本该如何编写:

  • 输入:脚手架模板、用户输入的模板参数
  • 输出:目标单文件类型脚手架
  • 映射关系:模板解析、IO读写等操作

有了以上思路之后,可以coding出如下代码:

代码里的注释很完整哦!

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);
  })

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

如此良苦用心,不值得你点个赞关注一下吗?

3.执行脚手架脚本创建脚手架

命令如下:

c:\workspace\automate node cli.js
? project Name? result
? param1? oooo

看以上命令时,建议类比一下你用vue-cli和react-creat-app创建项目的流程哦!

执行结果:

xxxx
oooo
xxxx

看到以上执行结果,就知道我们的需求得以成功实现,脚手架搭建成功!

4.示例回顾

如上简单示例,就已经揭晓了我们所常用脚手架的核心原理了。

为了更贴近于我们常用的脚手架,下面我们再给我们上面实现的原生脚本脚手架搭建脚本加一个需求,即把它封装成一个工具,并保证易用性。毕竟对于一个优秀的自动化脚本和工具而言,不但要功能强大、性能优越,更要简单易用

对于工具化这个需求,参考成熟脚手架工具的做法,我们可以通过把它抽取成一个独立的npm cli模块,并且链接到全局中的方式来实现。此外,如果希望share出去提供给别人使用,那么我们可以把这个cli模块发布到github和npm repository中。

好的,接下来我们探讨一下具体做法。

5.打包成脚手架工具模块

打包成一个独立的脚手架普通模块很简单,把模板和脚本抽取到一个新的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包的同名以及触发垃圾邮件检测的命名问题,此时需要修改包名重新发布

6.实践总结

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

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

四:基于Plop搭建脚手架

有不少朋友可能没有接触过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

经过以上步骤,我们就可以通过Plop工具而不是原生脚本去实现搭建一个react组件脚手架的需求。

建议对比一下原生脚本方式,感受一下脚本化和配置化两种方式搭建脚手架的区别。

好的,下面我们进入基于Yeoman搭建脚手架部分的探讨。

五:基于Yeoman搭建脚手架

Yeoman很多朋友可能没有听过,下面我先对他简单介绍一下。

Yeoman是一个现代WEB应用程序的WEB脚手架工具,个人认识它主要用来创建项目类型的脚手架。相比较于create-react-app、vue-cli等专注于某一个框架的脚手架工具而言,Yeoman是一个更加通用的脚手架工具。

接下来我们先通过使用别人的项目模板(generator),省略我们搭建我们脚手架项目模板的步骤,以便简化阁下理解Yeoman搭建一个脚手架项目的工作流。而后我们再以自己自定义的脚手架项目模板来搭建脚手架,实现项目类型脚手架的最终搭建实现。

脚手架的灵魂从来不在于脚手架的搭建方式,更不在于自动化,而在于脚手架模板(模板代码块/模板文件/模板项目),这点认识很重要。

下面我们就从Yeoman官方的generator repo中拉取一个node项目模板,搭建一个脚手架,作为Yeoman搭建项目类型脚手架的工作流探讨示例。

1.使用别人的项目模板搭建项目

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

2.使用自定义的项目模板搭建项目

Step1:安装Yeoman

yarn global add yo

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

经过以上步骤,即可以达到创建我们自定义项目模板的项目啦。能够复用自己搭建的模板项目,可是通往架构师的必经之路哦!

行文结束,写作不易,别忘了给个点赞和关注哦。么么哒(✿◡‿◡)