Помимо реализации бизнес-функций в повседневной работе часто возникают некоторые сценарии, которые необходимо автоматизировать. В большинстве случаев мы будем делать это по сценарию. Итак, если вы, как фронтенд-инженер, хотите автоматизировать работу с node.js, вам необходимо овладеть некоторыми навыками обработки текста. В следующей статье будут представлены некоторые методы, используемые при разработке инструмента автоматизации.
Обработка пользовательского ввода
Ввиду разницы в типах разрабатываемых инструментов командной строки у нас обычно есть два следующих метода обработки:
чистый инструмент командной строки
Сначала заполните экран приветствия:
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
Этот инструмент автоматически анализирует пользовательский ввод из командной строки:
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;
Меню для разбора команд, подобное следующему
./cli --inject-janus
Интерактивный инструмент командной строки
Для обработки пользовательского ввода инструментом командной строки Nodejs мы можем использоватьinquirer
Эта библиотека:
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
Этот пакет используется для вызова сценариев оболочки, тем самым расширяя возможности наших инструментов автоматизации.
const shell = require('shelljs');
shell.exec('git commit -am "Auto-commit"'
файл читать и писать
Проект будет в основном информацию о конфигурации на отдельном документе, то нам нужно помочь справиться с интерфейсами, связанными с файлами, которые будут обработаны. Обычно используемый интерфейс обработки файлов:
-
fs.access: доступ к файлу
-
fs.chmod: изменить права на чтение и запись файлов.
-
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.resolve
а такжеpath.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;
// }
-
__dirname: путь, по которому находится текущий файл
-
process.cwd: путь, по которому выполняется команда.
Следует отметить, что в процессе фактического написания инструмента вам необходимо различать путь к файлу, с которым вам нужно работать, и информацию об относительном пути текущей командной строки. Первый обычно является адресом пути к проекту, а второй — текущим рабочим путем.
обработка текста
Имея возможность читать и записывать файлы, в процессе разработки автоматизированных инструментов нам также необходимо заменять и изменять текст. В реальном процессе разработки при обработке текста обычно используются два метода: обычная замена и преобразование абстрактного синтаксического дерева.
Регулярная замена
Для простого текста мы обычно заменяем его обычным способом. Преимущество в том, что код относительно лаконичен, и его можно реализовать с помощью встроенного в язык интерфейса без необходимости использования дополнительных библиотек инструментов.
Наиболее часто используемый метод обработки интерфейса в JS — этоstring.replace
или с помощьюshelljs
выполнение модуляshell
сценарий. Первые в основном направлены на регулярную регулярную обработку, а вторые можно использовать с помощьюshell
Создание сценариев мощных инструментов обработки текста, таких какsed
,awk
Ждать.
Например, следующий код:
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 поможет нам не только разработать автоматизированные инструменты, но и реализовать следующие функции:
-
Проверка стиля синтаксиса кода JS (см. eslint)
-
Подсказка об ошибке, автодополнение, рефакторинг в IDE
-
Сжатие и обфускация кода, преобразование кода (см. prettier, babel)
Чтобы научиться использовать AST для преобразования текста, сначала нужно понять общую структуру абстрактных синтаксических деревьев. На самом деле это древовидная структура с информацией о языке программирования, а узлы, содержащиеся в ней, являются продуктами лексического анализа, такими как литералы, идентификаторы и методы, объявления вызовов и т. д. Ниже приведены некоторые часто используемые сведения о синтаксическом узле (токен):
-
Буквальный: буквальный
-
Идентификатор: идентификатор
-
CallExpression: вызов метода
-
VariableDeclaration: объявление переменной
Чтобы просмотреть абстрактную книгу по грамматике после разбора кода, вы можете использовать ASTEXplorer.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. Дно инкапсулированоrecast
GitHub.com/Этот Армани/Энтузиазм…эта библиотека.
Во всем потоке обработки этого файла принцип тот же, что и выше. Он также включает в себя такие шаги, как анализ синтаксического дерева, изменение синтаксического дерева и, наконец, генерация кода. и черезtransform函数
Преимущество предоставления интерфейса внешнему миру заключается в том, что интерфейс очень прост, а окончательный выходной код может сохранять стиль программирования исходного кода, поэтому он очень подходит для таких сценариев, как рефакторинг кода и изменение файла конфигурации.
Весь принцип работы показан на следующем рисунке:
AST == DOM-дерево AST-EXPLORER == браузер АОКОДЕШИФТ == Jquery
найти => найти операцию
Поиск узла является основным этапом работы AST. Обычно мы можем использовать платформу ast-explorer для визуализации информации об узле. Затем используйте оператор запроса, чтобы найти путь к нужному узлу.
Например, следующий код:
find(j.Property, {value: { type: 'literal', raw: 'xxx' } })
replace=>заменить операцию
Замена узлов также является очень часто используемой функцией в реальном процессе разработки, и метод построения вновь добавленных узлов должен соответствоватьast-types
GitHub.com/Этот Армани/AST…Определение типа:
node.replaceWith(j.literal('test')); // 替换成字符串节点
node.insertBefore(j.literal("test")); // 在该节点后插入新构造的ast
node.insertAfter(j.literal()); // 在该节点前插入新构造的ast
Вот небольшая хитрость, чтобы запомнить API: «Ищите вещи в верхнем регистре, создавайте узлы в нижнем регистре».
создать => создать узел
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 официально предоставлены некоторые функции инструментов тестирования, и мы можем напрямую использовать эти функции инструментов для быстрого написания нашего тестового кода. Во-первых, вам нужно создать два каталога:
-
testfixtures: Этот каталог в основном используется для хранения тестовых файлов, которые необходимо изменить.
input.js
Файл в конце представляет файл, который нужно преобразовать, аoutput.js
Один в конце представляет желаемый преобразованный файл. -
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();
продлевать
В дополнение к некоторым официально предоставленным базовым интерфейсам, jscodeshift также предоставляет расширенные интерфейсы, которые мы можем использовать для настройки некоторых функций инструмента.registerMethods
Этот метод может связать нашу пользовательскую функцию инструмента с пространством имен jscodeshift.
пример
- Инструменты рефакторинга кода:GitHub.com/реагировать JS/Горячие…Это официальный инструмент миграции кода, предоставляемый React, который может значительно снизить трудозатраты на рефакторинг кода для крупных проектов.
Справочная документация
Парсер АСТ: