玩轉自動化工具開發

在平常工做當中除了要實現業務功能外, 每每還會遇到一些須要進行自動化處理的場景。大多數狀況下咱們都會採用腳本的方式進行。那麼做爲前端工程師,若是你要用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解析器:

相關文章
相關標籤/搜索