前端工程化


前端工程化

Part 1 工程化概述

1、工程化的定义和主要解决的问题

前端工程化是指遵循一定标准及规范,通过工具去提高效率,降低成本的一种手段。

技术往往是为了解决问题而存在的,前端工程化也不例外。我们在日常的前端开发中就经常面临诸多问题,比如想用ES6+新特性,但兼容有问题。还有当我们想要使用Less/Sass/PostCSS来增强CSS的编程性时运行环境不能直接支持。再有当我们想要使用模块化的方式来提高项目的可维护性时运行环境也不能直接支持。此外,还有项目部署上线前需要手动压缩代码及资源文件,部署过程需要手动上传代码到服务器等重复性较强的工作。并且在多人协作开发时,无法硬性统一大家的代码风格,从仓库中pull回来的代码质量也无法保证。之前都是在编码中可能出现的问题,但在开发具体功能时也会遇到部分功能开发时需要等待后端服务接口提前完成的问题。

总而言之大概分为以下几类:1、传统语言或语法的弊端。2、无法使用模块化/组件化。3、重复的机械式工作。4、代码风格统一、质量保证。5、依赖后端服务接口支持。6、整体依赖后端项目。

2、一个项目过程中工程化的表现

一切以提高效率、降低成本、质量保证为目的的手段都属于「工程化」。

在创建项目时使用脚手架工具自动创建项目结构,创建特定类型文件,完成基础项目结构搭建。

在编码时可以自动格式化代码,校验代码风格,编译/构建/打包。

在预览/测试时可以使用Web Server/Mock,Live Reloading/HMR体验热更新(编码完成后直接在浏览器中看到最新结果)及在后端没有完全完成时进行开发(Mock假接口)。还有Source Map可以在编译后还能定位到源代码所在位置。

在代码提交环节,可以使用Git Hooks工具来在代码提交前做项目整体的检查(项目质量以及代码风格)。且对于提交日志(git log)也可以做严格的格式限制,这对于我们日后需要回滚代码时具有很大的参考价值。另外还有Lint-staged以及持续集成等。

在部署环节中,工程化表现的地方就更多了,我们可以用一行命令去代替传统的ftp上传,甚至还可以实现代码提交后通过CI/CD的方式自动将代码部署到服务器。

3、工程化不等于工具

注意,工程化并不等于某个具体的工具。因为现阶段有部分工具的功能过于强大,例如webpack,导致有部分人认为工程化就是webpack,只要用了webpack就等于有了工程化,但实际并非如此。工程化的核心应该是对项目整体的一个规划或说架构,而工具在这个过程中只是用来帮我们去落地实现这种规划架构的手段。

我们可以在一些成熟的集成式的工程化方案中找到一些思路,比如:create-react-app,vue-cli,angular-cli,gatsby-cli等,很多人认为这些工具都是官方给出的脚手架,实际上,这些工具应该都属于特定类型的项目官方给出的集成式工程化方案。如vue-cli中不仅仅为我们创建了项目,更多的是约定了vue项目是什么样的结构。在此之上vue-cli还提供了一些工具让我们可以有热更新的开发服务以及自动编译vue单文件组件与其他一些模块文件还有代码风格的校验,这些实际上都是集成在vue-cli内部的service之中的。而这些东西其实都是工程化中的几个维度。所以说像这些工具我们可以称之为工程化的集成。

4、工程化与 Node.js

有人说Ajax给前端带来了新的生命力,而Node的出现对于前端无异于工业革命,可以说前端工程化是由Node强力驱动的。工程化是一个非常庞大的概念,并且还在不断发展,值得强调的是不管他如何发展,其始终都是为了解决问题而存在的。

接下来我们将在:1、脚手架工具开发。2、自动化构建系统。3、模块化打包。4、项目代码规范化。5、自动化部署。这五个方面去具体来看如何在这些维度落实前端工程化。

Part 2 脚手架工具

5、脚手架工具概要

脚手架可以简单理解为自动帮我们去创建项目基础文件的一个工具,可以说是前端工程化在项目创建时的表现。但他最本质的作用其实不仅仅是创建项目的基础结构,更为重要的是他还提供了项目规范以及约定。其中包括:1、相同的组织结构。2、相同的代码开发范式。3、相同的模块依赖。4、相同的工具配置。5、相同的基础代码。因有很多需要相同的规范与约定,所以就会出现我们在搭建新项目时有大量的重复工作要去做。脚手架工具就是用来解决这样一个问题的,我们可以通过脚手架工具去快速搭建特定类型的项目骨架,然后基于这个骨架去进行后续的开发工作。

例如Visual Studio与Eclipse这样大型的IDE,其创建项目的过程就是一个脚手架的工作流程。而在前端项目创建过程当中,由于前端技术选型比较多样,又没有一个统一的标准,故前端方向的脚手架一般不会集成在某个IDE中,都是以一个独立的工具存在,并且相对会复杂一些。但本质上,脚手架的目标都是一样的,都是为了解决创建项目过程中那些复杂的工作。

接下来,我们先会介绍常用的脚手架工具,并对通用的脚手架工具进行剖析,最后还会去开发一款脚手架工具。

6、常用的脚手架工具

目前有很多成熟的前端脚手架工具,但大多都是为了特定的项目类型服务的,例如在React项目中可以使用create-react-app,在vue.js项目中可以使用vue-cli,在angular项目中可以使用angular-cli。这些工具的实现方式都大同小异,无外乎都是根据我们提供的信息自动创建对应的项目基础结构以及相关配置。不过他们一般只适用于自身所服务的那个框架的项目。还有就是以Yeoman为代表的通用型脚手架工具,他们可以根据一套模板生成一个对应的项目结构,这种脚手架一般都很灵活,而且容易扩展。除了以上这些在创建项目时才会用到的脚手架工具,还有一类脚手架工具也非常有用,代表性工具就是Plop,他们用来在项目开发过程中创建一些特定类型的文件,例如我们要想在一个组件化的项目中创建一个新的组件,或在模块化的项目中创建一个新的模块,这些模块和组件一般是由特定的几个文件组成的,而且每个文件都有基本的代码结构,相对于我们手动一个个去创建,脚手架会提供更为便捷更为稳定的一种操作方式。在了解了这些工具过后,我们接下来会重点关注几个有代表性的工具。

7、Yeoman简介

现今,React.js与Vue.js还有angular大行其道,这些框架的官方都提供了更为集成的脚手架工具链,所以大家在谈论到脚手架时往往最先想到的都是像Vue-cli,Angular-cli这样的工具。而对于这类的工具,由于他太过针对某一个框架,而且在使用上也非常普及,在此就不作过多的介绍了。

这里我们着重去探讨Yeoman这种工具,因为Yeoman作为最老牌,最强大,最通用的脚手架工具,有更多值得我们借鉴与学习的地方。Yeoman官方的定义说他是一款“用于创造现代Web应用的脚手架工具(The web’s scaffolding tool for modern webapps)”,不同于vue-cli这样的工具,Yeoman更像是一个脚手架运行平台,我们可以通过Yeoman搭配不同的Generator去创建任何类型的项目,也即我们可以通过创建自己的Generator从而去定制属于我们自己的前端脚手架。

而Yeoman的优点也同样是他的缺点,在很多基于框架开发的人眼中,由于Yeoman过于通用,不太专注,所以他们更愿意使用像vue-cli这样的脚手架。这也是像vue-cli这样的工具问什么现在这么成功的原因,但这并不妨碍我们去学习Yeoman,那接下来我们就快速去了解一下Yeoman的用法以及Generator的工作原理,为我们后面去开发自己的脚手架做出准备。

8、Yeoman 基础使用

Yeoman是一款基于Node.js开发的工具模块,使用Yeoman的第一步自然是通过npm在全局安装他。当然,使用npm去安装模块的前提是我们需要在机器上正常安装Node环境。注意后续我们会在使用npm时用一个叫做yarn的工具去取代他,yarn和npm在很多使用上是类似的,只不过他的体验可能会更好一些。

接下来我们会用yarn来安装yeoman,而yarn的全局安装命令是:yarn global add

yarn global add yo

yo就是yeoman的工具模块名。但在之前我们就介绍过,单有yo这个模块是不够的,因为yeoman是搭配特定的generator才能去使用,所以我们要使用yeoman去创建项目的话就必须要找到对应项目类型的generator。例如我们想要去生成一个node-module的项目,一个node的模块,我们就可以用一个叫generator-node的模块,而使用这个generator的方式也是先把他全局安装。

yarn global add generator-node

安装这两款模块之后,我们就可以使用yarn去运行刚刚所安装的generator-node这个生成器,自动地去帮我们创建一个全新的node module。

yo node

注意,运行特定的generator就是把包前面的generator-前缀给去掉。在创建过程中,yeoman还会提出一些问题,我们可以在命令行中通过命令行交互的方式把相关信息填写进去,而这些信息则会影响最终生成出来的项目结构。当所有选项都输入结束后,yeoman会在当前目录下创建一些基础文件,并帮我们在项目根目录下运行npm install去安装这个项目必要的一些依赖。在项目目录下,除了基本的文件外,内部的一些基础代码,包括一些基础配置都是帮我们提前配置好的,这也是脚手架工具的一个优势。

总结:1、在全局范围安装yo。2、安装对应的generator。3、通过yo运行generator(注意要先到想要创建项目的文件夹下再运行)。

9、Sub Generator

有时候我们并不需要创建完整的项目结构,可能只是在已有的项目基础之上去创建一些特定类型的文件,例如给已经存在的项目创建一个README,又或是在原有的项目之上去添加某些类型的配置文件,如ESLint或Babel配置文件。这些配置文件都有一些基础代码,如果我们手动去写的话很容易配错,而我们可以通过生成器自动帮我们去生成,来提高我们的效率。如果说我们需要这样的需求的话,可以使用Yeoman所提供的Sub Generator这样的特性来实现。具体而言就是通过在项目目录下运行一个特定的sub Generator命令去生成对应的文件。在此,我们就可以使用generator-node里所提供的子集生成器“cli”来帮我们生成一个cli应用所需要的一些文件,来让我们这个模块变为一个cli应用。

yo node:cli

运行Sub Generator的方式就是在原有的Generator名字后面跟上”:”和Sub Generator的名字。注意回车后他会提示我们是否要重写package.json这个文件,因为我们在添加cli支持的时候会添加一些新的模块和配置,这里我们选yes。完成之后会提示我们重写了package.json以及创建了一个cli.js的新文件。回到编辑器,在package.json中,我们会看到一个bin的配置以及新的dependencies,这些都是我们在新的cli应用中所需要的。除此之外我们在lib目录下的cli.js中也可以看到一些cli应用基础的代码结构,有了这些我们就可以将我们的模块作为一个全局的命令行模块去使用了。而本地的模块我们可以使用yarn link到全局范围,之后通过模块的名字去运行刚刚加进来的模块(注意在新加入配置文件后还需要输入yarn去安装相应的依赖,若还运行不了可以看看本节最下面的链接)。这个就是Generator的子集Generator的特性了,值得注意的是,并不是每一个generator都提供子集的生成器,所以我们在使用之前需要通过我们所使用的generator的官方文档来明确。例如在我们示例中的generator-node的官方文档(传送门)中就说明了这些相关的sub generator。

若yarn link后还运行不了相关命令请看:

记录Win上使用yarn link的那些坑

10、Yeoman 使用步骤总结

  1. 明确需求。
  2. 找到合适的Generator。(传送门
  3. 全局范围安装找到的Generator。
  4. 通过Yo命令运行对应的Generator。
  5. 通过命令行交互填写选项。
  6. 生成所需要的项目结构。

另外还需要注意的是,一些生成器生成的项目中会依赖一些C++的模块,而这些模块需要在安装过程中下载一些二进制文件,但这些文件并不能通过npm镜像去加速,所以速度会相对慢一点,不过我们也可以通过配置对应的镜像去提高这些二进制文件的下载速度。可以在下面的链接中查看相关镜像配置:

下载包总是出错的解决方案

11、自定义 Generator

通过前文对Yeoman的基本介绍,我们发现不同的Generator可以用来生成不同的项目,也即我们可以通过创造自己的Generator来帮我们去生成自定义的项目结构。而即便市面上已经有了各种各样的Generator我们还是有创造自己的Generator的必要,因为市面上的Generator都是通用的,而我们在实际开发中可能会出现一部分基础代码甚至是业务代码在相同类型项目时还是会重复,这时我们就可以把公共的部分都放在脚手架中去生成,让脚手架工具去发挥更大的价值。例如我们在创建Vue.js的项目的时候,官方默认的脚手架工具只会去创建一个最基础的项目骨架,这并不包含我们经常要用到的一些模块如Axios、vue-router或vuex,而我们就需要在每次项目创建完成之后手动去引入这些模块并去编写一些基础的使用代码。试想一下,如果我们把这些也放到脚手架之中,那就不存在我们刚刚说过的问题了。那么重点来了,自定义Generator该如何去实现呢,接下来我们就以自定义一个带有一定基础代码的Vue.js项目脚手架为目标来向大家介绍。具体内容请看下回分解。

12、创建Generator模块

在正式开始自定义Vue项目脚手架之前,先让我们来介绍一些基础的内容。创建Generator实际上就是创建一个npm模块,但Generator有特定结构,他需要在根目录下有一个generators的文件夹,然后在这个文件夹下再去存放一个app文件夹用于存放我们生成器对应的代码。如果我们需要提供多个的Sub generator则可以在app的同级目录再去添加一个新的生成器目录。例如我们添加一个component目录,此时我们的模块就有了一个叫做component的子生成器。

Generator目录结构

除了特定结构,还有一个与普通npm模块所不同的是,Yeoman的Generator的模块名称必须是generator-<name>这种格式,如果说我们在具体开发的时候没有去使用这样格式的名称,那么Yeoman在后续工作的时候就没办法找到我们所提供的生成器模块。

接下来我们按流程去做一个基本的演示。首先我们通过mkdir去创建一个叫generator-sample的文件夹作为生成器模块的目录,然后在这个生成器的目录下通过yarn init的方式去创建一个package.json。之后我们再运行yarn add yeoman-generator去安装一个yeoman-generator模块,这个模块提供了一个生成器基类,这个基类中提供了一些工具函数,让我们可以在创建生成器的时候更加便捷。安装完依赖之后,我们用vscode打开这个目录,然后在这个目录下按照项目结构要求去创建一个generators文件夹,并在这个文件夹下创建app文件夹,之后在app文件夹下创建一个index.js文件。而index.js文件会作为Generators的核心入口,他需要去导出一个继承自Yeoman Generator的类,Yeoman Generator在工作时会自动调用我们导出的类当中的一些生命周期方法,我们可以在这个文件中通过调用父类提供的一些工具方法去实现一些功能,比如文件写入。

// generators/app/index.js
// 我们先通过require的方式载入yeoman-generator
const Generator = require('yeoman-generator')
// 然后我们需要导出一个类,让这个类继承自Generator
module.exports = class extends Generator {
    // 在这个类中我们定义一个writing方法
    // 这个方法会在Yeoman工作时的生成文件阶段自动调用这个类中的writing方法
    // 在这个方法中,我们可以通过文件读写的方式往我们生成的目录下写入文件
    writing() {
        // 这里我们通过父类中的fs模块去写入文件到我们的生成目录。
        // 需要注意的是,这里的fs模块与我们在node中的fs不太一样,
        // 这是一个高度封装的file system模块,相对原生的fs模块功能会更加强些。
        // 这里的write方法有两个参数,一个是写入文件的绝对路径,另一个是写入文件的内容
        this.fs.write(this.destinationPath('temp.txt'), Math.random().toString());
    }
}

关于Yeoman封装的file system参见官方网站(传送门)。

这样一来,我们的一个简单的Generator就完成了。之后回到命令行,我们通过执行yarn link命令来把这个模块链接到全局范围,使之成为一个全局模块包,这样的话Yeoman在工作的时候就可以找到我们自己写的generator-sample了。然后我们就可以通过执行yo sample去运行这个生成器了,sample就是我们刚刚这个生成器的名字。

13、根据模板创建文件

很多时候我们需要自动去创建的文件有很多,而且文件的内容也相对复杂,在这种情况下我们就可以使用模板去创建文件,这样可以更加便捷一些。首先,我们要在generators/app目录下添加一个templates文件夹,然后将我们要去生成的文件都放入templates文件夹下作为模板,而模板中完全遵循EJS模板引擎的模板语法。

<% <!-- 在generators/app/templates下新建一个foo.txt文件 --> %><%= title %> 这种形式输出变量

<% <!-- 可以做一些判断与循环的操作 --> %>
<% if(success) { %>
success
<% } %>
还支持其他的EJS语法

有了模板后,我们在生成文件时就不需要再借助fs的write方法去写入文件,而是借助于fs中专门使用模板引擎的方法copyTpl。copyTpl具体使用时有三个参数,分别是模板文件的路径,输出文件的路径还有模板数据的上下文。

// generators/app/index.js
// 我们先通过require的方式载入yeoman-generator
const Generator = require('yeoman-generator')
// 然后我们需要导出一个类,让这个类继承自Generator
module.exports = class extends Generator {
    // 在这个类中我们定义一个writing方法
    // 这个方法会在Yeoman工作时的生成文件阶段自动调用这个类中的writing方法
    // 在这个方法中,我们通过模板方式写入文件到目标目录
    writing() {
        // 模板文件路径可以借助于templatePath方法自动获取当前生成器下templates的文件路径
        const tplPath = this.templatePath('foo.txt');
        // 输出文件路径还是使用destinationPath
        const output = this.destinationPath('foo.txt');
        // 模板数据上下文中只需要定义一个对象即可
        const context = { title: 'Hello George', success: true }
        // 将三个参数传入copyTpl方法,这个方法会自动把模板文件映射到生成的输出文件上
        this.fs.copyTpl(tplPath, output, context);
    }
}

我们回到命令行,再次执行yo sample去运行Generator,此时Yeoman的运行过程中就会自动使用模板引擎去渲染模板,并将渲染过后的结果放到输出目录中。相对于手动创建每一个文件,模板的方式大大提高了效率,特别是在文件比较多,比较复杂的情况下

14、接收用户输入

对于模板中的动态数据例如项目的标题,项目的名称等,这样的数据我们一般通过命令行交互的方式去询问使用者得到,而在Generator中想要发起一个命令行交互的询问则可以通过实现Generator类中的prompting方法。

// generators/app/index.js
// 我们先通过require的方式载入yeoman-generator
const Generator = require('yeoman-generator')
// 然后我们需要导出一个类,让这个类继承自Generator
module.exports = class extends Generator {
    prompting() {
        // Yeoman在询问用户环节会自动调用此方法
        // 在此方法中可以调用父类的prompt方法发出对用户的命令行询问
        // 这个方法返回一个Promise,所以我们在此对其返回,这样Yeoman在工作时就会有更好的异步流程控制。
        // 此方法接收一个数组参数,数组的每一项都是一个问题对象
        return this.prompt([
            // 在问题对象中可以传入type,name,message以及default
            {
                type: 'input',  //input表示以用户输入的方式接收用户的提交信息
                name: 'title',  //title为最终得到结果的键
                message: 'Your project name',  //message是在界面上给用户的提示
                default: this.appname,  //appname为当前项目生成文件夹的名字,作为问题的默认值
            },
            {
                type: 'confirm',  //confirm表示以用户选择Y/N的方式接收用户的提交信息
                name: 'success',  //success为最终得到结果的键
                message: 'Are Your Confirm?',  //message是在界面上给用户的提示
                default: true,  //true作为问题的默认值
            }
        ])
        //在promise执行完成之后可以得到一个answers,里面就是用户输入的结果
        .then(answers => {
            // answers会以对象的形式出现,对象里的键就是刚刚prompt里的name,值就是用户输入的value。
            // 我们将该对象挂载到this对象上面,以便后面在writing的时候使用到他
            this.answers = answers
        })
    }
    writing() {
        const tplPath = this.templatePath('foo.txt');
        const output = this.destinationPath('foo.txt');
        // 有了answer之后,我们就可以在writing时传入模板引擎,使用该数据去作为模板数据的上下文
        const context = this.answers
        this.fs.copyTpl(tplPath, output, context);
    }
}

回到命令行中再次运行yo sample,这时命令行中会提示我们一个问题,输入完成的结果会作为数据出现在数据上下文中,并最终在模板中被渲染出来。

15、Vue Generator案例

接下来就让我们正式按照之前的设想去自定义一个带有基础代码的Vue.js项目脚手架。大致步骤就是先按照原始的方式去创建一个理想项目结构,在这个项目结构中把我们需要重复使用的基础代码全部包含在里面,然后再去封装一个全新的Generator用于去生成这个理想的项目结构。

// 基础的理想项目结构
app
    │  index.js(generator的主入口文件)
    │  
    └─templates
        │  .browserslistrc
        │  .editorconfig
        │  .env.development
        │  .env.production
        │  .eslintrc.js
        │  .gitignore
        │  babel.config.js
        │  package.json
        │  postcss.config.js
        │  README.md
        │  
        ├─public
        │      favicon.ico
        │      index.html
        │      
        └─src
            │  App.vue
            │  main.js
            │  router.js
            │  
            ├─assets
            │      logo.png
            │      
            ├─components
            │      HelloWorld.vue
            │      
            ├─store
            │      actions.js
            │      getters.js
            │      index.js
            │      mutations.js
            │      state.js
            │      
            ├─utils
            │      request.js
            │      
            └─views
                    About.vue
                    Home.vue

项目地址:https://github.com/GeorgeSmith215/study-materials/tree/main/generator-vue

首先我们可以创建一个叫generator-czs-vue的文件夹,并在该文件夹下运行yarn init去初始化一个package.json。然后再运行yarn add yeoman-generator去安装一下yeoman的依赖,安装完成后我们用VSCode打开当前的项目文件夹,再新建一个generator的主入口文件,即generators/app/index.js文件。

// generators/app/index.js
// 我们还是和之前一样用require载入yeoman-generator这个基类
const Generator = require('yeoman-generator')

// 然后导出一个继承自generator的类
module.exports = class extends Generator {
  //在这个类中定义一个prompting方法,用于以命令行交互的方式询问用户一些问题
  prompting () {
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        default: this.appname
      }
    ]) 
    .then(answers => {
      this.answers = answers
    })
  }

  writing () {
    // 但这时的writing方法不再像之前只是写入单个文件,而是需要把提前准备好的文件批量生成。
    // 所以我们要先去准备一个templates目录,把项目的具体结构拷贝到templates中作为模板。
    // 有了模板之后我们就需要把项目结构里可能会发生变化的地方通过模板引擎语法的方式“挖坑”。
    // 因这里我们只在prompting里接收了名称,所以把能替换项目名称的地方都换为对应的EJS模板标记。
    // 在替换完模板之后,我们需要一个个地生成每一个文件到对应的目标路径中去。
    // 这里我们可以先把templates文件夹下的所有文件先以相对路径的形式放到一个数组中。
    // 再以数组循环的方式遍历每一个路径,从而为每一个模板生成他在目标目录中的对应文件。

    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 => {
      // item => 每个文件路径
      this.fs.copyTpl(
        this.templatePath(item),
        this.destinationPath(item),
        this.answers
      )
    })
  }
}

在完成全部的过程后,我们回到项目根目录下后就可以通过运行yarn link把对应的模块link到全局,再执行yo czs-vue去用yeoman运行刚刚创建的generator(因文件较多,建议更换到空项目文件夹下运行命令)。之后命令行中会需要我们去输入一个项目名称,我们输入相应的名称即可。注意若项目的源文件中正好出现了EJS的模板标记的话,因其并不是我们在模板中正常定义的数据输出,所以需要原封不动地输出,而原封不动输出EJS模板标记就可以使用<%%= XXX %>来代替<%= XXX %>。在我们生成的新项目结构中,所有那些需要被替换的数据就已经被替换了,我们原始的项目结构也就能被复用了。

16、发布Generator

因为Generator实际上就是一个npm模块,所以我们去发布Generator实际上就是去发布npm模块。故我们只需要将自己已经写好的Generator模块去通过npm publish命令去发布成一个公开模块就可以了。在具体发布之前,我们一般会将项目的源代码托管到公开的源代码仓库上。首先,让我们先通过命令行去创建一个本地仓库(注意先到项目的根目录下再运行)。

# 在创建本地仓库前先创建.gitignore去忽略一下node_modules目录
echo node_modules > .gitignore
# 之后再通过git init去初始化一个本地的空仓库
git init

之后再通过git add .去添加当前目录下的所有文件,然后运行git commit -m "initial commit"去提交文件。提交之后,我们就需要一个远程的仓库,把本地的提交同步到远程仓库。运行git remote add origin <你的远程仓库地址>为本地仓库添加一个远程仓库的别名,然后运行git push -u origin master把本地master分支的代码推送到远程端的master分支。再创建完仓库之后,我们就可以去项目根目录下通过运行npm publish或建议使用yarn publish去发布对应的模块。在publish的时候会提示我们是否对我们的package.json中的版本做修改,在此不做修改。接着还会要求我们输入对应的用户名以及密码(之后只需要输入密码即可)。再输入完用户名及密码后,可能还可能会提示一个错误,因为我们国内的开发者一般都会使用淘宝的npm镜像源去取代官方的镜像,这时再往npm的仓库上去发布时就会出现问题,因为淘宝的镜像是只读镜像。这时我们可以修改本地的镜像配置或通过运行yarn publish --registry=https://registry.yarnpkg.com在publish后跟–registry参数,并让他等于对应的yarn官方镜像地址即可。之后就会自动推送项目到yarn的官方镜像,而yarn的官方镜像和npm是保持同步的,所以此时我们的模块就发布成功了,我们可以访问这个地址https://npmjs.com/package/<你的项目名称>看到我们刚上传的模块。有了这样一个模块我们就可以在全局范围通过npm或yarn的方式去安装,再通过yeoman使用他。另外还有一个需要注意的点,如果我们需要让我们的generator在官方的仓库列表中也会出现的话,可以为我们的项目添加一个yeoman-generator的关键词,这时yeoman的官方就会发现到我们的这个项目了。

17、Plop简介

除了yeoman这样的大型脚手架工具,还有一些小型的脚手架工具也非常出色,比如接下来要给大家介绍的一款小型脚手架工具Plop。Plop其实是一款主要用于去创建项目中特定类型文件的小工具,有点类似于yeoman中的sub generator,不过他一般不会去独立使用,而是集成到项目中用来自动化去创建同类型的项目文件。

接下来让我们用一个简单的案例对比来体会一下Plop的具体作用以及优势。想象我们有两个相同的react项目,有所不同的是,一个项目中使用了Plop,而他们之间具体的差异则需要我们从日常开发中经常面临的问题说起。

我们在开发过程中经常需要去重复创建相同类型的文件,例如在本案例中,我们每一个组件都由三个文件组成,分别为一个js文件,一个css文件以及一个test.js文件。如果我们需要去创建一个新的react组件,就需要同时创建三个文件,而且每一个文件中都有一些基础的代码,整个的过程非常的繁琐,并且很难统一每个组件中那些基础的代码。而在使用了Plop的项目中,面对相同的问题,使用Plop就会方便很多,我们只需要在命令行中运行例如yarn plop component这样的命令,命令行就会根据plop的一些配置,自动地询问我们一些信息,然后根据我们所输入的结果,自动帮我们创建这些文件。这样就确保了我们每次创建的这些文件都是统一的,而且整个过程是自动化的,这样就大大提高了我们在项目中每次去创建重复文件时的效率。

18、Plop的基本使用

接下来我们在一个react项目中加入plop集成,去了解一下plop该如何去具体使用。

我们使用plop的第一件事就是通过命令yarn add plop --dev将plop作为一个npm模块安装到我们的开发依赖中。安装完成后我们需要在项目的根目录下去新建一个plopfile.js的文件,这个文件是plop工作的入口文件,他需要去导出一个函数,且在这个函数中可以接收一个叫plop的对象,这个对象中提供了一系列的工具函数用于帮助我们去创建生成器的任务,具体就是通过module.exports导出一个函数,并在函数之中接收一个形参。

// plopfile.js
// 该文件是Plop的入口文件,需要导出一个函数,此函数接收一个plop对象,用于创建生成器任务

module.exports = plop => {
    //plop内部有一个成员函数setGenerator,接收两个参数,一为生成器的名字,二为生成器的配置。
    plop.setGenerator('component', {
        //在配置选项中需要指定一下生成器的描述
        description: 'create a component',
        //还可以在generator中用prompts数组指定generator工作时会发出的命令行问题
        prompts: [
            {
                type: 'input',  //用type去指定问题的输入方式
            	name: 'name',  //name去指定问题返回值的键
            	message: 'componet name',  //message是屏幕上的提示
            	default: 'MyComponent',  //default为问题的默认答案
            },
        ],
        //actions指定命令行交互过后需要执行的一些动作,可以为一个数组,数组中保存每一个动作对象
        actions: [
            {
                type:'add',  //用type属性去指定动作的类型,add代表添加文件
                //path属性指定需要添加的文件会被添加到哪个具体路径,
                //可以使用'{{}}'插值表达式去插入在命令行交互中得到的数据。
                path: 'src/components/{{name}}/{{name}}.js',
                //templateFile属性指定本次添加文件的模板文件,
                //模板文件一般都会放在项目根目录下的plop-templates文件夹下,具体内容看下部分代码。
                //在编写了对应的模板文件后,把相应的文件路径填写进去
                templateFile: 'plop-templates/component.js.hbs',
            },
        ],
    });
    
}

项目根目录下的plop-templates文件夹内通过handlerbars模板引擎的方式去创建模板文件,这些模板都遵循handlebars的模板语法。在这些文件中我们可以通过{{}}这种小胡子语法去插入对应的数据。

{{! plop-templates/component.js.hbs }}
import React from 'react';

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

因我们在安装plop时,plop会提供一个cli程序,可以通过yarn去启动这个plop程序。所以在完成了plop任务的定义之后,我们可以回到命令行,通过运行yarn plop component命令,yarn会自动找到node_modules下面bin目录下的对应命令行工具运行。运行后,命令行会像之前我们使用yeoman一样去发出一些命令行的问题,这些命令行问题都是我们在plopfile中定义的。在输入完问题之后,我们就可以看到一个新文件会根据模板创建成功。

有了基础的体验后我们就可以尝试为生成器去添加多个模板。因为在react中,组件是由多个文件组成的,所以我们可以各自为组件的css文件和test文件去添加相应的模板并把与名称相关的部分用插值表达式去替换。有了这些模板之后我们就可以回到plopfile中去添加多个action。

// plopfile.js
// 该文件是Plop的入口文件,需要导出一个函数,此函数接收一个plop对象,用于创建生成器任务

module.exports = plop => {
    //plop内部有一个成员函数setGenerator,接收两个参数,一为生成器的名字,二为生成器的配置。
    plop.setGenerator('component', {
        //在配置选项中需要指定一下生成器的描述
        description: 'create a component',
        //还可以在generator中用prompts数组指定generator工作时会发出的命令行问题
        prompts: [
            {
                type: 'input',  //用type去指定问题的输入方式
            	name: 'name',  //name去指定问题返回值的键
            	message: 'componet name',  //message是屏幕上的提示
            	default: 'MyComponent',  //default为问题的默认答案
            },
        ],
        //actions指定命令行交互过后需要执行的一些动作,可以为一个数组,数组中保存每一个动作对象
        actions: [
            {
                type:'add',  //用type属性去指定动作的类型,add代表添加文件
                path: 'src/components/{{name}}/{{name}}.js',
                templateFile: 'plop-templates/component.js.hbs'
            },
            {
                type:'add',  //用type属性去指定动作的类型,add代表添加文件
                path: 'src/components/{{name}}/{{name}}.css',
                templateFile: 'plop-templates/component.css.hbs'
            },
            {
                type:'add',  //用type属性去指定动作的类型,add代表添加文件
                path: 'src/components/{{name}}/{{name}}.test.js',
                templateFile: 'plop-templates/component.test.js.hbs'
            },
        ],
    });
    
}

各文件对应内容如下:

{{! plop-templates/component.js.hbs }}
import React from 'react';

import './{{name}}.css';

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

  </div>
)
{{! plop-templates/component.test.js.hbs }}
import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<{{name}} />, div);
  ReactDOM.unmountComponentAtNode(div);
});
{{! plop-templates/component.css.hbs }}
.{{name}} {
  
}

另附plop的官方文档,若需要更多信息可参看:传送门

完成之后,我们回到命令行,重新运行yarn plop component去运行plop生成器,则会生成三个对应的文件。以上就是Plop的基本使用,在这个过程中我们可以发现Plop用来创建项目中同类型的文件还是非常方便的。

总结一下,我们在一个项目中具体去使用Plop需要这几个步骤:1、将plop模块作为项目开发依赖去安装。2、在项目根目录下创建一个plopfile.js文件。3、在plopfile.js文件中定义脚手架任务。4、编写用于生成特定类型文件的模板。5、通过Plop提供的cli运行脚手架任务。

19、脚手架的工作原理

通过前面部分的介绍,我们不难发现大部分脚手架工具的工作原理都很简单,无外乎就是我们在启动他后会询问一些预设的问题,然后将我们回答的结果结合一些模板文件生成一些项目结构。接下来我们就通过nodejs去开发一个小型的脚手架工具去深入体会一下脚手架工具的工作过程。

我们都知道脚手架工具实际上就是一个node-cli应用,去创建脚手架工具实际上就是去创建一个cli应用。

首先,我们进入命令行,执行mkdir <项目名称>命令为新项目创建一个项目文件夹,在目录下通过yarn init的方式去初始化一个package.json文件,有了这个文件后我们通过vscode打开这个文件夹。紧接着我们在package.json中添加一个bin字段,用于指定cli应用的入口文件,在此我们命名为cli.js,并在项目根目录下添加该文件。

跟以往我们在nodejs中编写的js文件有所不同的是,cli的入口文件必须要有一个特定的文件头:#!/usr/bin/env node。并且如果我们的操作系统是Linux或macOS的话,还需要修改此文件的读写权限为755,这样该文件才可以作为一个cli的应用入口。同样,我们可以在命令行中通过yarn link的方式把该模块link到全局,并在命令行中通过执行<项目名称>使用该cli应用。

接下来我们实现一下脚手架的具体业务,也就是脚手架的工作过程:首先我们需要通过命令行交互的方式去询问用户的一些信息,然后根据用户回答的结果生成文件。

#!/usr/bin/env node
// 上面一行是cli的入口文件必须有的一个特定文件头

// 在node中发起命令行交互询问使用inquirer模块,注意需要先执行yarn add inquirer去安装。
const inquirer = require('inquirer');
// inquirer模块提供一个prompt方法,用于发起一个命令行询问。
// 他可以接收一个数组参数,数组中每个成员就是我们发起的命令行问题。
inquirer.prompt([
    {
        type:'input',  //可以通过type去指定问题的输入方式
        name:'name',  //name去指定问题返回值的键
        message:'Project name?',  //message去指定屏幕上给用户的提示
    },
])
.then(answer => {
    // 运行命令并根据问题输入回答后可以在answer中打印输入的结果
    console.log(answer)
})

有了answer之后我们接下来需要考虑的就是动态去生成我们的项目文件。而生成项目文件则一般都会通过模板去生成,所以还需要在项目根目录下新建一个templates文件夹。在templates文件夹下我们新建一个模板,由于我们重点关注的是脚手架的工作过程,所以没必要关心模板里有什么,这里我们用index.html文件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%= name %></title>
</head>
<body>
    <!-- 此文件为index.html文件 -->
    <h1>Hello <%= name %></h1>
</body>
</html>

注意在模板中我们可以用<%= <问题对应键> %>的方式输出之前在询问环节得到的问题答案。

有了模板之后,回到cli.js中,我们在得到问题的答案之后可以根据用户回答的结果去生成对应的文件。不过在生成文件之前我们一般都会先明确模板的根目录和目标路径。

#!/usr/bin/env node

const inquirer = require('inquirer');
// 引入path来获取文件的路径
const path = require('path');
// 引入fs去写入文件
const fs = require('fs');
// 引入模板引擎去渲染文件,注意需要先用yarn add ejs安装
const ejs = require('ejs');

inquirer.prompt([
    {
        type:'input',  //可以通过type去指定问题的输入方式
        name:'name',  //name去指定问题返回值的键
        message:'Project name?',  //message去指定屏幕上给用户的提示
    },
])
.then(answers => {
    //模板目录应该为项目当前目录下的templates
    const tplDir = path.join(__dirname, 'templates');
    //目标目录路径一般就是命令行在哪个目录下执行,就是哪个路径,也即cwd目录
    const destDir = process.cwd();
    //明确了两个目录之后,我们就可以通过fs模块去读取模板目录下的文件,并全部转换到目标目录下
    fs.readdir(tplDir, (err, files) => {
        if(err) throw err;
        files.forEach(file => {
            //通过模板引擎渲染文件,需要先执行yarn add ejs去安装一个叫ejs的模板引擎。
            //renderFile方法的第一个参数为文件绝对路径,第二参数为模板工作时需要的数据上下文。
            //第三参数为一个回调函数,即渲染结束后的回调函数,渲染后的内容为result。
            ejs.renderFile(path.join(tplDir, file), answers, (err, result) => {
                if(err) throw err;
                // 将结果写入目标文件路径
                fs.writeFileSync(path.join(destDir, file), result)
            })
        })
    })
})

完成编写后,我们回到命令行,定位到一个全新的目录执行<项目名称>,在输入了对应的名称后,脚手架工具就会自动把模板里的文件生成到对应目录中去。

其实脚手架的工作原理并不复杂,但他的意义却是巨大的,因为他确实在创建新项目的环节大大提高了我们的效率。


文章作者: 智升
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 智升 !
评论
  目录