玩转自动化工具开发

在平常工做当中除了要实现业务功能外, 每每还会遇到一些须要进行自动化处理的场景。大多数状况下咱们都会采用脚本的方式进行。那么做为前端工程师,若是你要用node.js去作一些自动化的工做,就须要掌握一些文本处理的技巧。 接下来这篇文章将介绍在开发一个自动化工具所会用到的一些技巧。javascript

处理用户输入

针对所开发命令行工具类型的区别,咱们一般有如下两种处理方式:前端

纯命令行工具

先完成一个欢迎界面:java

const chalk = require('chalk');
const boxen = require('boxen');
const yargs = require('yargs');

const greeting = chalk.white.bold('欢迎使用xxx工具');
const boxenOptions = {
  padding: 1,
  margin: 1,
  borderStyle: 'round',
  borderColor: 'green',
  backgroundColor: '#555555',
};

const msgBox = boxen(greeting, boxenOptions);
console.log(msgBox);
复制代码

参数处理使用 yargs 这个工具,自动解析用户输入命令行:node

const options = yargs
  .usage('Usage: --inject-janus|--inject-kani')
  .option('inject-janus', {
    describe: '注入janus',
    type: 'boolean',
    demandOption: false,
  })
  .option('inject-kani', {
    describe: '注入kani',
    type: 'boolean',
  }).argv;
复制代码

用来解析相似下面这种命令的菜单python

./cli --inject-janus
复制代码

交互式命令行工具

Nodejs命令行工具对于用户输入的处理,咱们能够采用inquirer这个库:react

import inquirer from 'inquirer';

await inquirer.prompt([
  {
    name: 'repoName',
    type: 'input',
    message:
      '请输入项目名:',
  },
  {
    name: 'repoNamespace',
    type: 'input',
    message: '请输入 gitlab 命名空间,如 gfe',
    default: 'gfe',
  }]);
复制代码

内嵌脚本

Node命令行中每每须要借助系统原生的一些工具,针对linux、osx等,能够借助 shelljs 这个包来调用shell脚本,从而扩展咱们自动化工具的能力。linux

const shell = require('shelljs');
 
shell.exec('git commit -am "Auto-commit"'
复制代码

文件读写

项目的配置信息基本上都会放在一个个独立的文件上,那么咱们就须要借助处理文件相关的接口去进行处理。经常使用的文件处理接口有:git

  • fs.access: 文件访问github

  • fs.chmod: 修改文件读写权限shell

  • fs.copyFile: 复制文件

  • fs.link: 连接文件

  • fs.watch: 监听文件变化

  • fs.readFile: 读取文件(高频)

  • fs.mkdir: 建立文件夹

  • fs.writeFile: 写文件 (高频)

import {promises as fs} from 'fs';

async function readJson() {
    return fs.readFile('./snowflake.txt', 'utf8');
}

async function saveFile() {
    await fs.mkdir('./saved/snowfalkes', {recursive: true});
    await fs.writeFile('./saved/snowflakes/xx.txt', data);
}

console.log(await readSnowflake());
复制代码

JSON

JSON文件在前端项目中经常做为配置文件的形式存在,那么最多见的操做配置文件的方式就是处理JSON文本。 如代码所示:

const data = require('../test.json');

data['xxx'] = 'a';
复制代码

在作序列化的时候 JSON.stringify 接口的第二个参数(用来格式化),也十分经常使用:

// 保持两个空格的缩进
JSON.stringify(a, null, 2)
复制代码

路径

在读写文件过程中路径的解析也是常常须要进行的。咱们一般会借助 path.resolvepath.parse 这两个接口来进行相对路径和绝对路径的处理。前者用来作路径的转换,后者则主要用来获取路径上更详细的信息。

import * as path from 'path';

const relativeToThisFile = path.resolve(__dirname, './test.txt');
const parsed = path.parse(relativeToThisFile);

// interface ParsedPath {
// root: string;
// dir: string;
// base: string;
// ext: string;
// name: string;
// }
复制代码
  1. __dirname: 当前文件所处路径

  2. process.cwd: 执行命令所在路径。

须要注意的是,在实际写工具的过程中,须要区分好你所须要操做的文件路径以及当前命令行的相对路径信息。前者一般是用项目路径地址,后者一般是当前工做路径。

文本处理

有了文件读写等能力以后,在开发自动化工具的过程中,咱们还须要对文本进行替换修改。在实际开发过程当中,文本处理一般采用两种方式:正则替换和抽象语法树转换。

正则替换

针对简单的文本,咱们通常是采用正则的方式进行替换。好处是代码相对来讲比较简洁,并且利用语言内生的接口就能够实现,无需借助额外的工具库。 JS里最经常使用的接口处理方式是 string.replace 或借助 shelljs 模块执行 shell 脚本。前者主要针对常规的正则处理,然后者则能够借助 shell 脚本强大的文本处理工具如 sedawk 等。 以下面这代码:

import { promises as fs } from 'fs';

const code = await fs.readFile('./test-code.js');

code = code.replace(/Hello/, 'World');
code = code.replace(/console.log\\((.*)\\)/, 'console.log($1.toUpperCase())');

await fs.writeFile('./test-new-code.js', code); 
复制代码

AST(抽象语法树)

使用正则方法针对常规的文件修改是足够用的,可是在使用过程当中还会碰到一个问题,那就是字符串一般是非结构化的,因此使用正则的可读性不是十分良好。同时,针对复杂场景,如须要一些逻辑判断等,使用正则也很难很好的覆盖到。

那么咱们就有了另外一个方案,就是能够直接将源码解析成结构化的数据(AST),并直接在抽象语法树上进行增删改查,替换成咱们最终想要的结果。最后再将转码后的AST写回文件当中去。 这一整个过程其实就有点像babel转译所作的工做同样。

学会操做AST,不只有利于咱们开发自动化工具,也能实现下面这些功能:

  1. JS 代码语法风格检查,(参考eslint)

  2. 在 IDE 中的错误提示、自动补全,重构

  3. 代码的压缩和混淆、代码的转换 (参考prettier, babel)

要学会使用AST作文本转换,首先须要先了解一下抽象语法树的常见结构。 它其实就是一个附带有语言编程信息的树形结构,里面包含的节点是词法解析后的产物,好比有字面量,标识和方法,调用声明等等。 下面是一些经常使用的语法节点信息(token):

  • Literal:字面量

  • Identifier: 标识符

  • CallExpression: 方法调用

  • VariableDeclaration: 变量声明

要查看一个代码解析后的抽象语法书,能够借助AST EXplorer.net astexplorer.net/ 这个工具。

esprima + esquery + escodegen

esprima + esquery + escodegen 的组合是操做AST经常使用的工具。 其中 esprima esprima.org/ 这个库主要用来解析js语法树,用法以下面代码所示:

import { parseScript } from 'esprima';

const code = `let total = sum(1 + 1);`
const ast = parseScript(code);
console.log(ast)
复制代码

经过 parseScript 接口就能够从源码文件中提取语法树结构。 获得下面的结构:

是一个嵌套的树形结构,能够经过深度遍从来获取全部节点信息。 那么在解析完源码获得语法树以后,咱们就能够像操做dom结构同样去操做这些节点结构。这里借助 esquery 工具来找到所须要修改的节点:

import { parseScript } from 'esprima';
import { query } from 'esquery';

const code = 'let total = say("hello world")';
const ast = parseScript(code);
const nodes = query(ast, 'CallExpression:has(Identifier[name="say"]) > Literal');

console.log(nodes);
复制代码

最后就能够获得 say 方法调用的参数值:

[
  Literal {
    type: 'Literal',
    value: 'hello world',
    raw: '"hello world"'
  }
]
复制代码

接着,咱们就能够尝试本身修改这些AST节点的信息, 好比这里我想将代码里的参数改为“hello bytedance", 最终生成代码。 代码以下:

import { parseScript } from 'esprima';
import { query } from 'esquery';
import { generate } from 'escodegen';

const code = 'let total = say("hello world")';
const ast = parseScript(code);
const [literal] = query(ast, 'CallExpression:has(Identifier[name="say"]) > Literal');
literal.value = 'hello bytedance';

// 借助escodegen生成最终代码, escodegen: 接受一个有效的ast,并生成js代码
const result = generate(ast);
console.log(result);
// 最终结果: let total = say("hello bytedance");
复制代码

固然,有时候是须要替换整个语法树,那么就可使用 estemplate 这个库来快速生成对应的ast信息,并拼装到原有的ast上。 好比下面这段代码:

var ast = estemplate('var <%= varName %> = <%= value %> + 1;', {
  varName: {type: 'Identifier', name: 'myVar'},
  value: {type: 'Literal', value: 123}
});
console.log(escodegen.generate(ast));
// > var myVar = 123 + 1;
复制代码

能够用模板化语言的方式生成AST,从而在新增节点或替换节点的时候便于咱们修改旧有的AST结构。

例子

下面咱们来运用上面的知识点来实现几个有趣的小功能:

1. 实现一个自定义的eslint规则

import { parseScript } from 'esprima';
import { query } from 'esquery';

const code = `Object.freeze()`;
const ast = parseScript(code);
const queryStatement = 
  'CallExpression:has(MemberExpression[object.name="Object"][property.name="freeze"])';
const nodes = query(ast, queryStatement);

if (nodes.length !== 0) {
  throw new Error(`不要使用Object.freeze!`);
}
复制代码

能够把它们类比为: 在实际使用过程当中,我的比较喜欢作一个jscodeshift这个工具,它是由Facebook官方提供的一个codemode的工具。底层封装了 recast github.com/benjamn/rec… 这个库。

在这个文件整个处理流程,原理同上面同样。也是包含解析语法树、修改语法树并最终生成代码等步骤。并且是经过 transform函数 对外暴露接口,它的优势是接口十分简洁,同时最终输出的代码还能保留原有代码的编程风格,因此很是适合代码重构、修改配置文件等场景。

它的整个工做原理以下图所示:

AST == DOM树 AST-EXPLORER == 浏览器 JSCODESHIFT == Jquery

find=>查找操做

节点查找是要AST操做的最核心的一步,咱们一般能够借助ast-explorer这个平台来可视化节点信息。而后利用查询语句,定位到想要的节点路径。

以下面这段代码:

find(j.Property, {value: { type: 'literal',  raw: 'xxx' }   })
复制代码

replace=>替换操做

替换节点在实际开发过程当中也是很是经常使用的一项功能,而新增的节点构造方式要遵照 ast-types github.com/benjamn/ast… 的类型定义:

node.replaceWith(j.literal('test')); // 替换成字符串节点

node.insertBefore(j.literal("test")); // 在该节点后插入新构造的ast

node.insertAfter(j.literal()); // 在该节点前插入新构造的ast
复制代码

这里记住API有个小诀窍就是:"找东西用大写,建立节点小写"。

create=>建立节点

j.template.statements`var a = 1 + 1`;


j.template.expression`{a: 1}`;
复制代码
export default function transformer(file, api) {

  // import jscodeshift
    const j = api.jscodeshift;
    // get source code

    const root = j(file.source);
    // find a node

    return root.find(j.VariableDeclarator, {id: {name: 'list'}})
    .find(j.ArrayExpression)
    .forEach(p => p.get('elements').push(j.template.expression`x`))
    .toSource();
};
复制代码
// 最后输出的代码字符串风格保持单引号形式
j(file.source).toSource({quote: 'single'});

// 双引号形式
j(file.source).toSource({quote: 'double'});
复制代码

print=>最后输出打印

打印部分的代码相对来讲比较简单,直接利用 toSource 方法就能够完成。 有时候咱们还须要控制一些代码输出格式(如引号等),就能够借助 quote 等属性来处理。

测试

写codemod的代码,测试是十分必要的。因为涉及到文件的修改,借助测试能够大大简化咱们开发的工做。

在jscodeshift里,官方提供了一些测试工具函数,能够直接借助这些工具函数快速地编写咱们的测试代码。 首先,须要先创建两个目录:

  1. testfixtures: 该目录主要用来存放待修改的测试文件, input.js 结尾的文件表明待转换的文件,而 output.js 结尾的则表明指望转换后的文件。

  2. tests: 该目录用来存放全部的测试用例代码

const { defineTest } = require('jscodeshift/dist/testUtils');
const transform = require('../index');
const jscodeshift = require('jscodeshift');
const fs = require('fs');
const path = require('path');

jest.autoMockOff();

defineTest(__dirname, 'bff');

describe('config', function () {
  it('should work correctly', function () {
    const source = fs.readFileSync(
      path.resolve(__dirname, '../__testfixtures__/config.output.ts'),
      'utf8'
    );
    const dest = fs.readFileSync(
      path.resolve(__dirname, '../__testfixtures__/config.output.ts'),
      'utf8'
    );
    const result = transform.config({ source, path }, { jscodeshift });
    expect(result).toEqual(dest);
  });
});
复制代码
// 第二个参数用来指定做用范围,若是不指定的话,则全局生效
jscodeshift.registerMethods({
    log: function() {
        return this.forEach(path => console.log(path.node.name));
    }
}, jscodeshift.Identifier);

jscodeshift.registerMethods({
    log: function() {
        return this.forEach(path => console.log(path.node.name));
    }
});

// 以后就能够直接在语法树使用自定义方法了
jscodeshift(ast).log();
复制代码

extend 扩充

jscodeshift除了官方提供的一些基本接口外,还提供了扩展接口方便咱们用来自定义一些工具函数使用 registerMethods 这个方法就能够在jscodeshift命名空间上绑定咱们自定义的工具函数。

例子

  1. 代码重构工具: github.com/reactjs/rea… 这是react官方提供的代码迁移工具,能够大大减小对于大项目代码重构时的人力成本。

参考文档

AST解析器:

相关文章
相关标签/搜索