前端工程化-脚手架工具搭建

827 阅读13分钟

工程化概述

什么是工程化

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

工程化主要解决的问题

传统语言语法的弊端(ES6语法不支持,scss等css预编译语言不支持)
无法使用时模块化、组件化
重复的机械工作
代码风格统一、质量保证
依赖后端服务接口支持 整体依赖后端项目 在我们日常开发过程中,一切重复的工作都应该被自动化

创建项目:可以使用工程化的自动化脚手架工具
编码阶段:格式化代码,代码校验、编译、构建、打包等
测试阶段:source Map 热更新 mock等
提交阶段:git hook,git lint等 部署阶段:CI/CD 自动发布

我们首先要明确的是 工程化≠某个工具

以一个普通的项目为例,我们落实工程化的第一件事,规划整体的工作流架构,比如文件的组织结构、源代码的开发范式(语法、规范、标准)、前后端分离的方式等,然后考虑搭配相关的工具,做相关的配置选项实现工程化具体的规划。

从上面这段话来看,我们日常开发中所遇见的cli工具,不只是一个脚手架工具,而可以算作是成熟的工程化集成。

前端工程化的一切都应该归功于node,O(∩_∩)O哈哈~大家同意吗! 这篇文章我将会从脚手架工具开发自动化构建系统模块化打包项目代码规范化自动化部署五个方面,浅谈前端工程化

脚手架工具

脚手架作用

自动创建项目基础文件的工具
提供项目规范和约定(主治组织结构、开发范式、模块依赖、工具配置、基础代码)

都是为了解决创建项目中通用的复杂的工作

常用脚手架

三大框架的脚手架
react ==> create react app
vue ==> vue cli
angular ==> angular cli
Yeoman 等

通用脚手架工具

Yeoman的基础使用
Yeoman本质上就是一个Nodejs的CLI工具

快速上手

node --version && npm --version,安装之前需要确认node和npm是否已经安装完成
npm i -g yo 或者 yarn global add yo,全局安装yo
直接运行 yo命令,可以发现yeoman自己不能做任何操作,他的所有操作都是基于generator基本插件完成的,常见的generator插件有generator-nodegenerator-express等,这里我们要用到的是generator-node
npm i generator-node -g或者 yarn global add generator-node
切换目录,通过yo运行genenrator生成器
运行yo node 运行过程中会有一系列的问题,类似于vue cli的问题,基本都是配置相关 最后生成一个基本项目

Sub generator

有的时候,我们需要在已有的项目中,创建特定类型的文件,添加某些铁定的配置文件,需要生成器帮我们自动生成,可以使用Yeoman的Sub generator 运行sub generator的方式是在原有的后面加上yo node:cli 生成这些,我们是要把这个东东,当做全局的命令行来操作的,所以我们需要npm link使这个模块link到全局,并且可以用这个模块的名字,去运行命令my-cli-module --help

使用Yeoman的步骤

  1. 明确自己的要求
  2. 找到合适的Genenrator
  3. 全局安装
  4. 通过yo 运行对应的Genenrator
  5. 通过命令行交互填写选项配置
  6. 生成所需要的项目结构

开发一款自己的脚手架

自定义generator,基于Yeoman搭建自己的脚手架

创建Generator,就是创建一个Genenrator模块,而Genenrator本质上就是一个npm模块

Genenrator有特定的目录结构
Genenrator有一个基本的目录结构,在根目录下要有一个genenrators文件夹,下面要有一个app文件夹作为一个默认的生成器目录,里面有一个index.js是默认生成器的具体实现

如果我们需要在一个generator中提供多个Sub generator时,我们需要在app的同级目录添加新的生成器目录,

Genenrator有特定的命名方式 除了特定的结构,yeoman generator还有特殊的模块名称命名方式,generator-<name>,如果没有使用这样的命名方式,在yeoman在之后的工作过程中,无法找到提供的生成器模块

自定义generator的具体步骤

  1. 使用mkdir generator-simple,创建一个文件夹,作为生成器模块的根目录
  2. npm init 创建一个package.json
  3. yarn add yeoman-generator或者npm i yeoman-generator,yeoman-generator这个模块提供了生成器的一个基类,提供了一些工具函数,使我们创建生成器的时候,更加便捷
  4. 在generator-simple文件夹下新建generators/app/index.js

app文件夹下的index.js此文件作为Generator的 核心入口,需要导出一个继承自Yeoman Generator的类型,Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法,而我们在这些方法中可以通过调用父类的一些工具方法实现一些功能,例如文件写入,具体实现如下

  1. 使用npm link的方式,把这个模块链接到全局范围,使之成为一个全局模块包,这样的话yeoman在工作的时候就可以找到我们自己写的这个generator-simple
  2. 最后在新建的一个目录下运行这个生成器yo simple,可以看到创建了一个temp文件,是我们之前在代码中写入生成的那个文件

通过模板创建文件

app目录下创建templates目录,将需要生成的文件都放入templates目录作为模板,模板中完全遵循ejs模板引擎的模板语法,这里我们创建一个foo文件

然后回到入口的index.js,这个时候我们就不需要使用fs.write模块了,对了强调一下这里的fs模块不是node里面的那个,是generator自带的fs模块,功能只能说类似。

执行结果就是,在执行目录中创建了一个foo.txt文件,内容是

模板文件的字符串都被替换了

相对于手动创建每一个文件,模板的方式大大提高了了效率,特别是在文件比较多,比较复杂的情况下

命令行交互,接收用户询问

那在generator中想要发起一个命令行交互的询问,我们可以通过实现Generator类型当中的prompting方法,Yeoman 在询问用户环节会自动调用此方法,在此方法中可以调用父类的prompt() 方法发出对用户的命令行询问,这个方法返回的是promise,所以需要return,这样yeoman工作的时候就有更好的异步流程控制

对象里面的每一个key所代表的的意思,也不难理解

创建一个模板文件并执行,下面是writing方法里面的代码

// 通过模板方式写入文件到目标目录
const tmpl = this.templatePath('bar.html')  // 可以自动获取当前生成器下templates下的文件路径
// 输出目标路径
const output = this.destinationPath('bar.html')
// 模板数据上下文
const context = this.answers

this.fs.copyTpl(tmpl,output,context)

在命令行中执行yo simple

模板文件和执行结果

创建一个vue的Generator脚手架

让我们在这里重新复习一遍上面的步骤

  1. 新建一个generator-tang-vue的文件夹作为vue脚手架的根目录,npm init初始化package.json,安装yeoman generator依赖包,新建一个generators/app/index.js主入口文件,文件中写入一些必要的代码

app下面创建templates文件夹,把我们自定义的vue的模板拷贝进去

进行挖坑,就是使用者需要自定义的东西,这里我们指的就是prompting方法中定义的变量,由于我们这边只定义了一个name变量,所以,随便找了几个文件进行挖坑

接下来我们应该是一个一个的把文件生成到目标路径,这个时候我们需要准备一个数组,里面的每一项是templates下所有文件的相对路径,然后通过数组方法,把每个模板循环生成到对应的目标文件

writing () {
        // 把每一个文件通过模板切换到目标路径
        // 数组循环的方式批量生成每一个文件
        const templates = [     // 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) => {
            this.templatePath(item),
            this.destinationPath(item),
            this.answers
          })
    }

接下来,我们需要把这个我们定义的模块link到全局,命令行运行npm link,到这里我们自己的vue-generator的脚手架已经完成了
切换目录,我们需要用这个脚手架生成一个demo项目
mkdir my-vue-project新建一个目录
yo tang-vue运行我们刚才创建的generator

发现什么都没做,怎么回事呢?
经检查是我们的writing()方法中 循环遍历的时候,我们只是循环遍历了,没有调用操作文件方法拷贝模板文件啊,正确的结果应该是

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

再运行,发现抛错了,显示BASE_URL is not defined

原来是在我们的模板文件中的index.html文件中有一个模板标记,这个模板标记也是EJS语法,而BASE_URL并不像我们自己的定义的name,是模板中正常使用的数据输出,它需要按照它原本定义的原封不动的输出的,所以我们需要修改它的EJS的模板语法

后面多加一个%,这样的话ejs的模板标记就会被转义,就不会有问题了
重新运行,成功!之前挖坑的替换的数据也被替换了,完美无瑕!
当我们真正需要弄一个自己的脚手架的时候,需要更多的自定义的东西,让他更加灵活更加通用

发布Generator

因为generator是一个npm模块,所以发布Generator就是发布一个npm模块,所以我们只需要将自己写好的Generator通过npm pubilsh去发布成一个公开的模块

还是以刚才的generator-tang-vue来说

  1. 首先执行echo node_modules > .gitignore新建一个gitignore文件,在文件提交的时候忽略node_modules
  2. git init初始化本地的一个仓库,git status来查看未被跟踪的文件,通过git add .提交暂存,然后创建一次提交git commit -m "feat: initial commit"
  3. 创建完提交之后,我们需要把本地的提交同步到远端仓库git remote add origin <远端仓库地址>,这样为我们本地仓库添加了一个远端仓库的别名,然后git push -u origin master把本地master分支的代码推送到远端的master分支了
  4. npm publish去发布这个模块,注意发布之前需要登录npm login输入用户名密码 如果你是第一次发布的话,遇见了以下这个问题,可能是你刚刚注册了npm账号,你需要去验证你的邮箱

    正常情况下的话,已经是发布成功!这样的你就可以通过npm去安装,然后通过yeoman去使用它了

Plop-一款小而美的脚手架工具

用于创建项目中特定类型的小工具,类似于yeoman中的sub genenrator,一般都是集成在项目中,用来创建同类型的文件 日常开发中,经常需要重复创建相同类型的文件,比如Angular/react/vue新建一个组件的时候,需要同时创建css html js等多个文件及创建的文件中的默认代码,这个时候Plop自动化创建文件能提高我们每次创建文件的效率

plop的使用

  1. 首先全局安装npm i -g plop,方便我们直接使用plop命令,yarn可以自动找到node_modules/bin目录下的命令行工具,而npm不行,所以我们干脆直接全局安装plop
不全局安装的话package.json中 加入
"scripts": {
    "p": "plop"
 },
  1. npm i plop --dev第一步就是把polp安装到我们的开发依赖当中,
  2. 然后在项目的根目录当中新建一个plopfile.js的文件,这个文件是plop工作的一个入口文件,需要导出一个函数,此函数接受一个plop对象,提供了一系列的工具函数,用于创建生成器任务
    plop有一个成员setGenerator

同时我们需要在项目的根目录创建一个模板文件夹plop-templates,创建一个模板文件,.hbs文件是模板引擎Handlebars,同yeoman一样,插入一些模板数据和自定义的数据

  1. 完成之后,可以直接plop component执行这个生成器,跟yeoman一样,问一些问题,答完之后,会创建我们所需要的新文件 如果需要同时创建多个文件,就是在action添加多个对象语法,生成多个文件,官网中有更多的type,有需要自己去找

这里我们发现在项目中使用plop创建同类型的文件还是非常方便的

总结:
将plop模块作为项目开发依赖安装
在项目根目录下创建一个plopfile.js文件
plopfile.js文件中定义脚手架任务
编写用于生成特定类型文件的模板
通过plop提供的cli运行脚手架任务,从而生成我们在项目中所需要的特定类型的文件

脚手架的工作原理

通过nodejs开发一个小型的脚手架工具,脚手架工具其实就是一个node cli应用,创建脚手架工具就是创建一个cli应用,

  1. mkdir sample-scaffolding先创建一个目录,然后通过npm init初始化一个package.json文件,在package.json添加一个"bin": "cli.js"
  2. 在根目录新建一个cli.js
#!/usr/bin/env node

// Node CLI 应用入口文件必须要有这样的文件头
// Linux、MacOS 系统需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js

然后脚手架的工作过程:

1)通过命令行交互询问用户问题
2)根据用户回答的结果生成文件

  1. 在node中发起命令行交互使用inquirer模块 npm i inquirer
  2. 使用ejs模板引擎渲染文件 npm i ejs
const inquirer = require('inquirer')
const path = require('path')
const fs =require('fs')
const ejs = require('ejs')
inquirer.prompt([
    {
        type: 'input',
        name: 'name',
        message: "project name?"
    }
])
.then(answers => {
    // console.log(answers);
    // 根据用户回答结果生成文件

    // 模板目录     项目当前目录下的templates
    const tmplDir = path.join(__dirname,'templates')
    // 目标目录  一般命令行所在的目录
    const destDir = process.cwd()

    // 通过fs模块去读取模板目录下的文件,输出到目标目录
    fs.readdir(tmplDir, (err, files) => {
        if (err) throw err
        files.forEach( file => {
            console.log(file); // 相对于templates的相对路径
            // 通过模板引擎renderFile方法渲染这个路径对应的文件
            // 第一个参数 文件的绝对路径
            // 第二个参数 模板引擎工作的时候的数据上下文
            // 第三个参数 就是成功或者失败的回调函数
            ejs.renderFile(path.join(tmplDir,file),answers,  (err, result) => {
                if (err) throw err
                console.log(result);  // 这个结果就是模板引擎工作过后的结果,我们只需要将这个结果通过文件写入的方式,写到目标目录

                fs.writeFileSync(path.join(destDir,file), result)
            })
        })
    })

})