前端工程化的理解

973 阅读8分钟

前端工程化:前端工程化就是通过各种工具和技术,提升前端开发效率的过程

  • 前端工程化的内容:各种工具和技术
  • 前端工程化的作用:通过使用工具,提升开发效率

前端工程化的内容

  1. 压缩工具:项目上线前,压缩代码。例如将文件内容中的注释、空格等删除,见换行变成一行,例如 minify
  2. 转换工具:例如将ES6、CSS3新特性进行转换,解决兼容性问题
  3. Less、Sass等 CSS 的预编译语言进行编译处理
  4. Vue、React等 JS 库进行转换
  5. 格式化格局:多人开发、统一开发规范,例如ESLint、Prettier等
  6. 自动化工具:自动化构建(npm script & script hooks等)、自动化部署(Git Hook、CI/CD等)、自动化测试(Jest、mocha等)
  7. 脚手架工具:专用脚手架、通用脚手架
    • 通用脚手架:Yeoman、Plop
    • 专用脚手架:vue-cli、create-react-app、angular-cli
  8. 模块化打包:Webpack、Rollup、Parcel
  9. 前端使用到大部分工具都是用 Node.js 进行开发的
  10. 工程化 !== 某个工具(例如webpack)
  11. 脚手架

脚手架

前端工程化的发起者

作用:创建项目基础结构、提供项目规范和约定

通用脚手架:Yeoman

  • Yeoman 是一款脚手架工具:可以帮助开发人员创建项目的基础结构代码

  • yo 是 Yeoman 的命令行管理工具:可以在命令行运行yeoman的命令

  • 生成器:Yeoman中具体的脚手架:针对不同项目有不同的脚手架(例如:网站、APP、小程序等)

  • Yeoman使用说明

// 全局安装 
yo npm install --global yo 
// 安装 generator(根据项目需求安装 generator-webapp、generator-node) 
npm install --global generator-webapp
npm install --global generator-node 
// 通过 yo 运行 generator 
mkdir project-name 
cd project-nam 
yo webapp 
// 启动运行 
npm run start
  • 构建自己的脚手架工具(自定义 Generator)
// 全局安装 yo(前提)
npm install--global yo
// 首先创建一个 generator-文件名 的文件(下面以 generator-oyzxvues 为例)
mkdir generator - oyzxvues
cd generator - oyzxvues
// 在 generator-oyzxvues 中 npm init 初始化项目,创建一个 package.json 文件
// 在 generator-oyzxvues 文件下创建 /generators/app/index.js 文件
// 在 generator-oyzxvues 项目中使用 yarn add yeoman-generator 安装 yeoman-generator,或者使用npm install yeoman-generator --save
yarn add yeoman - generator
// 在 /generators/app/ 文件夹中创建一个 templates 文件夹,把要安装的文件放在 templates 文件夹下,以 vue 项目为例,这里就是后面要安装的模板文件,
// 在 /generators/app/index.js 中写入

const Generator = require('yeoman-generator')
module.exports = class extends Generator {
  prompting() {
    return this.prompt([
      {
        type: 'input',
        name: 'title',
        message: '你的项目名称是:',
        default: this.appname
      }
    ]).then(answers => {
      this.answers = answers
    })
  }
  writing() {
    // 创建一个类似 vue 的项目路径
    const templates = [
      'public/favicon.ico',
      'public/index.html',
      'src/assets/logo.png',
      'src/components/HelloWorld.vue',
      'src/App.vue',
      'src/main.js',
      '.gitignore',
      'babel.config.js',
      'package.json',
      'README.md',
      'yarn.lock'
    ]
    templates.forEach(item => {
      this.fs.copyTpl(
        this.templatePath(item),
        this.destinationPath(item),
        this.answers
      )
    })
    // 单个模板文件的生成
    // const tmpl = this.templatePath('index.html')
    // const output = this.destinationPath('index.html')
    // const context = this.answers
    // this.fs.copyTpl(tmpl, output, context)
  }
}
// 在 generator-oyzxvues 命令行中输入 npm link 将当前项目放入到本地 npm 模块中,使用 npm unlink 卸载(手动删除也可)
// 创建一个新文件夹,使用 yo oyzxvues 导入 generator-oyzxvues 脚手架,导入后会根据脚手架中的 package.json 下载依赖文件(其实就是 npm install)
  • 专用脚手架 vue-cli、create-react-app、angular-cli
  • 脚手架实现的原理(使用纯node进行实现,不引入其它脚手架模块)
// 步骤
// 1、创建一个文件夹 myclis,并 npm init -y 初始化项目(-y 就是后面执行中不停选择 yes 回车)
// 2、安装 ejs(模板引擎)、inquirer(用户与命令行交互的工具),npm install ejs inquirer --save
// 3、在当前文件夹 myclis 下创建 cli.js,并且在 package.json 中添加一行 bin 属性且添加需要的可执行js文件名(bin 是指定脚本的入口文件)
// package.json
{
  "name": "myclis",
    "version": "1.0.0",
      "description": "",
        "main": "index.js",
          "bin": "cli.js",
            "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
    "author": "",
      "license": "ISC",
        "dependencies": {
    "ejs": "^3.1.6",
      "inquirer": "^8.2.0"
  }
}
// 4、在当前文件夹 myclis 下创建 templates文件夹,并且添加一些文件
// 5、在 cli.js 文件输入要执行的命令
#!/usr/bin / env node
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')
// 用户与命令行的交互,获取输入信息,当前是一个 promise
inquirer.prompt([
  {
    type: 'input',
    name: 'name',
    message: '您的项目名称:',
    default: 'oyzx-project'
  }
]).then(answers => {
  // 在回调中去获取 templates 里面的文件
  // 获取模板导入的路径
  const tmpDir = path.join(__dirname, 'templates')
  // 写入的路径,process.cwd() 方法返回 Node.js 进程的当前工作目录
  const destDir = process.cwd()
  fs.readdir(tmpDir, (err, files) => {
    if (err) throw err
    files.forEach(file => {
      ejs.renderFile(path.join(tmpDir, file), answers, (err, result) => {
        if (err) throw err
        fs.writeFileSync(path.join(destDir, file), result)
      })
    })
  })
})

// 最后使用 npm link 将当前项目放入到本地 npm 模块中,使用 npm unlink 卸载(手动删除也可)
// 创建一个新文件夹,使用 myclis(项目创建的名称)导入模板

// 循环多层文件
#!/usr/bin / env node
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')
// 递归遍历所有文件,并且进行读写
const RecursionFile = (pImportDir, pExportDir) => {
  fs.readdir(pImportDir, (err, files) => {
    if (err) throw err
    files.forEach(file => {
      const vPath = path.join(pImportDir, file)
      const vState = fs.lstatSync(vPath);
      if (vState.isDirectory()) {
        // 如果需要对path进行操作 需要转字符串toString()
        const vSubPath = pImportDir.slice(path.join(__dirname, 'templates').length, pImportDir.length)
        fs.mkdirSync('./' + path.join(vSubPath, file))
        RecursionFile(path.join(pImportDir, file), pExportDir)
      } else {
        fs.readFile(path.join(pImportDir, file), (err, result) => {
          if (err) throw err
          const vSubPath = pImportDir.slice(path.join(__dirname, 'templates').length, pImportDir.length)
          fs.writeFileSync(path.join(pExportDir, vSubPath, file), result)
        })
      }
    })
  })
}
// 用户与命令行的交互,获取输入信息,当前是一个 promise
inquirer.prompt([
  {
    type: 'input',
    name: 'name',
    message: '您的项目名称:',
    default: 'oyzx-project'
  }
]).then(answers => {
  // 在回调中去获取 templates 里面的文件
  // 获取模板导入的路径
  const tmpDir = path.join(__dirname, 'templates')
  // 写入的路径,process.cwd() 方法返回 Node.js 进程的当前工作目录
  const destDir = process.cwd()
  RecursionFile(tmpDir, destDir)
})
  • 脚手架的工作流程
    • 通过命令行交互询问用户问题
    • 根据用户回答的结果生成文件
  • 自动化构建
    • 自动化构建是指手动构建任务,通过命令自动执行的过程
    • npm scripts 实现自动化构建的最简方式,npm 允许在 package.json 文件中,使用 scripts 字段定义脚本命令,例如:我们在项目中常用的 npm run [-name] 命令就是 npm scripts 构建
    • 把多行执行命令用一行代替
      • 执行过程中有两种方式:并行、串行
      • 并行:在 scripts 字段命令行中用 空格 隔开命令
      • 串行:在 scripts 字段命令中使用 && 命令隔开

AST

  • 前言:前端工程化基本上都离不开 AST,底层都是用到 AST 的思想逻辑。首先要理解什么是 AST 抽象语法树 概念,因为 webpack、babel、Vue中的template、React中的JSX等都要用到它。
  • 什么是 AST 抽象语法树:
    • AST 抽象语法树 是源代码的抽象语法结构树状表现形式,Webpack、ESLint、JSX、TypeScript 的编译和模块化规则之间的转化都是通过 AST 来实现对代码的检查、分析以及编译等操作,AST 将 JS 语法编译成 抽象语法树需要先对其代码块进行遍历、修改并重新编译,遍历树结构是 深度遍历。
  • 代码的转义可以通过下面这个网站进行:esprima.org/demo/parse.…
// js代码转义前
const a = 20;
// 使用转义工具转换后
{
  "type": "Program",
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "a"
            },
            "init": {
              "type": "Literal",
              "value": 20,
              "raw": "20"
            }
          }
        ],
        "kind": "const"
      }
    ],
      "sourceType": "script"
}
  • 实现转换需要用到的工具
    • esprima、estraverse 和 escodegen 模块是操作 AST 的三个重要模块,也是实现 babel 的核心依赖,下面是分别介绍三个模块的作用。
    • esprima 模块的用法:可以看到通过 esprima 模块中的 parseScript 方法将 JS 代码块转换成 AST 抽象语法树,代码需要将原代码块转换成字符串(esprima 中也提供了一个转换方法 parseModule),然后再将其进行转换。
// 需要安装 esprima 依赖
const esprima = require("esprima");
const code = 'function fn(){}'
// 生成语法树
const tree = esprima.parseScript(code);
console.log(tree);
// tree 的输出
{
  "type": "Program",
    "body": [
      {
        "type": "FunctionDeclaration",
        "id": {
          "type": "Identifier",
          "name": "fn"
        },
        "params": [],
        "body": {
          "type": "BlockStatement",
          "body": []
        },
        "generator": false,
        "expression": false,
        "async": false
      }
    ],
      "sourceType": "script"
}

estraverse 遍历和修改AST

  • 下面代码通过 extraverse 模块中 traverse 方法,将 code 转换后的 AST(使用 esprima.parseScript(code)) 进行了遍历,并且打印出 node 上面的 type 属性,type 属性就是 AST抽象语法树 中的一个树节点,修改是获取对应的类型,并对此类型对应的节点上的属性即可。 - 在 AST 的深度遍历中,其实遍历的就是每一层的 type 属性,我们根据 type 属性对当前代码进行解读,且遍历会有两个阶段,进入阶段(enter)和离开阶段(leave),在 estraverse 模块中的 traverse 里面分别用参数指定了 enter 和 leave 两个函数监听,一般我们的侧重点是 enter。
// 安装 esprima、estraverse 依赖
const esprima = require('esprima');
const estraverse = require('estraverse');
// 生产代码字符串
const code = 'function fn(){}';
const ast = esprima.parseScript(code);
// 遍历语法树
estraverse.traverse(ast, {
    enter(node) {
        console.log('enter', node.type);    
    },
    leave() {
        console.log('leave', node.type);    
    }
})
// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// leave Program

escodegen 将 AST 转换成 JS

  • 下面代码就是将 JS 代码块转成 AST,并将遍历、修改后的 AST 重新转成 JS 的全过程
// 安装 esprima、estraverse、escodegen 依赖
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
// 将代码块转成字符串
const code = 'function fn(){}';
// 将代码块转成字符串后的生成 ast抽象语法树
const ast = esprima.parseScript(code);
// 遍历语法树,并且对指定 type 的节点进行修改
estraverse.traverse(ast, {
  // 修改函数名
  if(node.type === "FunctionDeclaration") {
  node.id.name = "ast";
}
})
// 编译语法树,重新生成code模块
const result = escodegen.generate(tree);
console.log(result);
// function ast() {
// }