Эй, хватит писать так много try/catch для асинхронных функций

JavaScript Webpack

предисловие

В процессе разработки вы часто пишете логику try/catch в асинхронных функциях для надежности системы или для отлова асинхронных ошибок?

async function func() {
    try {
        let res = await asyncFunc()
    } catch (e) {
      //......
    }
}

однажды я был«28 навыков JavaScript, которыми должен овладеть квалифицированный интерфейсный инженер среднего уровня»Упоминается в изящном способе обработки async/await

Таким образом, мы можем обернуть эту асинхронную функцию вспомогательной функцией для перехвата ошибок.

async function func() {
    let [err, res] = await errorCaptured(asyncFunc)
    if (err) {
        //... 错误捕获
    }
    //...
}

Но в этом есть недостаток, то есть вспомогательная функция errorCaptured должна быть введена каждый раз, когда она используется.Есть ли "ленивый" метод?

Ответ определенно да.Я выдвинул новую идею после этого блога, который может автоматически вставлять код try/catch через загрузчик веб-пакетов.Конечный результат, надеюсь, будет таким

// development
async function func() {
   let res = await asyncFunc()
    //...其他逻辑
}

// release
async function func() {
    try {
        let res = await asyncFunc()
    } catch (e) {
      //......
    }
    //...其他逻辑
}

Разве это не здорово? Нет необходимости в каком-либо дополнительном коде в среде разработки, пусть веб-пакет автоматически внедряет логику захвата ошибок в код в рабочей среде, затем давайте реализуем этот загрузчик шаг за шагом.

принцип погрузчика

Прежде чем реализовать этот загрузчик WebPack, давайте кратко представим принцип загрузчика. Один из наших загрузчиков, определенных в WebPack, по сути, функция, и определите загрузчик, определит свойство Test, WebPack будет проходить все имена модулей. При сопоставлении режима, определенного Свойство Test, модуль заносится в Загрузчик как исходный параметр.

{
    test: /\.vue$/,
    use: "vue-loader",
}

При совпадении имени файла, оканчивающегося на .vue, файл будет передан vue-loader в качестве исходного параметра.За атрибутом использования может следовать строка или путь.Если это строка, она будет рассматриваться как nodejs и перейти к node_modules по умолчанию.

Эти файлы на самом деле являются строками (изображения и видео являются объектами буфера).В качестве примера возьмем vue-loader.Когда загрузчик получает файл, он будет разделен на три части путем сопоставления строк, а строка шаблона будет vue-loader. Скомпилировавшись в функцию рендера, скриптовая часть будет передана в babel-loader, а стилевая часть будет передана в css-loader, при этом загрузчик работает по единому принципу, то есть загрузчик выполняет только одно, чтобы несколько загрузчиков можно было гибко комбинировать, не мешая друг другу.

Реализовать идеи

Поскольку загрузчик может прочитать соответствующий файл и преобразовать его в желаемый результат после обработки, мы можем реализовать загрузчик самостоятельно, принимать файлы js и обертывать код слоем try/catch, когда встречается ключевое слово await.

Итак, как мы можем обернуть try/catch именно для await и последующих выражений? Здесь требуются знания об абстрактном синтаксическом дереве (AST).

AST

Абстрактное синтаксическое деревоисходный кодграмматикаАбстрактное представление структуры. это начинается сдревовидныйформа исполненияЯзык программированияграмматическая структура, каждый узел дерева представляет собой структуру в исходном коде

С помощью AST можно реализовать множество очень полезных функций, таких как преобразование кода после ES6 в ES5, проверка eslint, улучшение кода и даже механизмы js, реализованные с помощью AST. Препроцессоры CSS, такие как scss и менее, не только ограничиваются преобразованием между js, но и преобразуются в код CSS, распознаваемый браузером через AST.Давайте рассмотрим пример.

let a = 1
let b = a + 5

После преобразования в абстрактное синтаксическое дерево это выглядит так

Преобразование строки в дерево AST требует двух шагов лексического анализа и анализа синтаксиса.

Лексический анализ преобразует фрагменты кода в токены (лексические единицы) и убирает пробельные комментарии.Например, первая строка преобразует в токены let, a, =, 1. Токен — это объект, описывающий фрагмент кода во всем. код и некоторая информация для записи текущего значения

Анализ синтаксиса преобразует токен в узел (узел) в сочетании с синтаксисом текущего языка (JS), а узел содержит атрибут типа для записи текущего типа.Например, пусть представляет ключевое слово объявления переменной в JS, поэтому его тип является VariableDeclaration, а a = 1 будет использоваться в качестве описания объявления let, его тип — VariableDeclarator, а описание объявления зависит от объявления переменной, поэтому это иерархическая связь вверх и вниз.

Кроме того, может оказаться, что токен не соответствует узлу, и вокруг знака равенства должны быть значения для формирования оператора, иначе будет выдано предупреждение, что является основным принципом eslint. Наконец, все узлы объединяются вместе, чтобы сформировать синтаксическое дерево AST.

Порекомендуйте очень практичный инструмент для просмотра AST,AST explorerИ более интуитивно понятно, как код в абстрактное синтаксическое дерево

Вернемся к реализации кода, нам нужно только найти выражение ожидания через дерево AST и обернуть ожидание вне узла узла try/catch.

async function func() {
   await asyncFunc()
}

Соответствующее дерево AST:

async function func() {
    try {
        await asyncFunc()
    } catch (e) {
        console.log(e)
    }
}

Соответствующее дерево AST:

разработка загрузчика

С конкретными идеями начинаем писать загрузчик.Когда наш загрузчик получит исходный файл, он пройдет@babel/parser
Этот пакет может преобразовать файл в абстрактное синтаксическое дерево AST, так как же найти соответствующее выражение ожидания?

Для этого требуется другой пакет babel@babel/traverse,пройти через@babel/traverseВы можете передать дерево AST и некоторые функции-ловушки, а затем глубоко пройти по входящему дереву AST.Когда пройденный узел имеет то же имя, что и функция-ловушка, будет выполнен соответствующий обратный вызов.

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            //... 
        }
    })
    //...
}

пройти через@babel/traverseМы можем легко найти узел Node, соответствующий выражению await, следующим шагом будет создание узла Node типа TryStatement и, наконец, добавление в него await. Здесь есть еще один пакет, от которого нужно зависеть.@babel/types, которую можно понимать как babel-версию библиотеки loadsh, которая предоставляет множество вспомогательных функций, связанных с узлом Node AST, нам нужно использоватьtryStatementметод, который создает узел узла TryStatement

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            let tryCatchAst = t.tryStatement(
                //...
            )
            //...
        }
    })
}

tryStatementПринимая 3 параметра, первое — это предложение try, второе — предложение catch, а третье — предложение finally Узел Node, соответствующий завершенному оператору try/catch, выглядит следующим образом.

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            let tryCatchAst = t.tryStatement(
                // try 子句(必需项)
                t.blockStatement([
                    t.expressionStatement(path.node)
                ]),
                // catch 子句
                t.catchClause(
                    //...
                )
            )
            path.replaceWithMultiple([
                tryCatchAst
            ])
        }
    })
      //...
}

использоватьblockStatement,expressionStatementметод создает область блока и узел узла, в котором размещается выражение ожидания,@babel/traverseКаждой функции-ловушке передается параметр пути, который содержит некоторую информацию о текущем обходе, такую ​​как текущий узел, последний пройденный объект пути и соответствующий узел.Самое главное, что есть некоторая информация, которую можноМетоды управления узлами Node, нам нужно использоватьreplaceWithMultipleЭтот метод заменяет текущий узел узла узлом узла оператора try/catch.

Кроме того, мы должны учитывать, что выражение ожидания может использоваться как оператор объявления.

 let res = await asyncFunc()

Это также может быть оператор присваивания

 res = await asyncFunc()

Также возможно, что это простое выражение

 await asyncFunc()

AST, соответствующие этим трем случаям, также различны, поэтому их нужно рассматривать отдельно.@bable/typesПредоставляет множество функций оценки.В функции ловушки AwaitExpression нам нужно только оценить, какой тип узла Node является вышестоящим узлом.AST explorerЧтобы просмотреть структуру окончательного дерева AST, которое необходимо сгенерировать

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            if (t.isVariableDeclarator(path.parent)) { // 变量声明
                let variableDeclarationPath = path.parentPath.parentPath
                let tryCatchAst = t.tryStatement(
                    t.blockStatement([
                        variableDeclarationPath.node // Ast
                    ]),
                    t.catchClause(
                        //...
                    )
                )
                variableDeclarationPath.replaceWithMultiple([
                    tryCatchAst
                ])
            } else if (t.isAssignmentExpression(path.parent)) { // 赋值表达式
                let expressionStatementPath = path.parentPath.parentPath
                let tryCatchAst = t.tryStatement(
                    t.blockStatement([
                        expressionStatementPath.node
                    ]),
                    t.catchClause(
                        //...
                    )
                )
                expressionStatementPath.replaceWithMultiple([
                    tryCatchAst
                ])
            } else { // await 表达式
                let tryCatchAst = t.tryStatement(
                    t.blockStatement([
                        t.expressionStatement(path.node)
                    ]),
                    t.catchClause(
                        //...
                    )
                )
                path.replaceWithMultiple([
                    tryCatchAst
                ])
            }
        }
    })
      //...
}

После получения замененного дерева AST используйте@babel/coreв упаковкеtransformFromAstSyncМетод преобразует дерево AST в соответствующую строку кода и возвращает его.

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")
const core = require("@babel/core")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
              // 同上
        }
    })
    return core.transformFromAstSync(ast).code
}

На этом основании некоторые элементы конфигурации загрузчика также выставлены для упрощения использования. Например, если оператор ожидания был обернут с помощью try/catch, он не будет внедрен снова. Этот принцип также основан на AST, используя путь параметр.findParentМетод проходит все родительские узлы вверх, чтобы определить, обернут ли он узлом try/catch.

traverse(ast, {
    AwaitExpression(path) {
        if (path.findParent((path) => t.isTryStatement(path.node))) return
        // 处理逻辑
    }
})

Кроме того, фрагменты кода в предложении catch также поддерживают настройку, поэтому все ошибки обрабатываются с помощью единой логики.Принцип заключается в преобразовании фрагментов кода, настроенных пользователем, в AST и передаче их в узел TryStatement в качестве параметра, когда он создан узел Catch

дальнейшее улучшение

После сообщения в области комментариев я добавлю try/catch к каждому оператору ожидания по умолчанию и изменю его, чтобы обернуть try/catch для всей асинхронной функции.Принцип заключается в том, чтобы сначала найти оператор ожидания, а затем рекурсивно траверс вверх.

Когда асинхронная функция найдена, создайте узел узла try/catch и используйте код исходной асинхронной функции в качестве дочернего узла узла узла и замените тело функции асинхронной функции.

Когда встречается try/catch, это означает, что он был обернут try/catch, отменил внедрение и вышел из обхода напрямую, так что, когда у пользователя есть собственный код захвата ошибок, логика захвата по умолчанию загрузчика не будет быть казненным.

Соответствующее дерево AST:

Соответствующее дерево AST:

Это всего лишь узел node самого простого объявления асинхронной функции, а также функциональные выражения, стрелочные функции, методы в виде объектов и т. д. Когда одно из этих условий выполняется, вводится блок кода try/catch.

// 函数表达式
const func = async function () {
    await asyncFunc()
}

// 箭头函数
const func2 = async () => {
    await asyncFunc()
}

// 方法
const vueComponent = {
    methods: {
        async func() {
            await asyncFunc()
        }
    }
}

Суммировать

Эта статья бросает кирпич, в повседневном процессе разработки вы можете комбинировать свою бизнес-направление, разрабатывать более подходящие для себя, например, технический стек - это старый проект jQuery, вы можете сопоставить$.ajaxУзел узла, единообразное внедрение логики обработки ошибок и даже настройка некоторых новых синтаксисов, которых нет в ECMA.

Извините, я понимаю принцип компиляции, я действительно могу делать все, что захочу

Разработав этот загрузчик, вы сможете не только узнать, как работает загрузчик webpack, но и многое узнать об AST, понять принцип работы babel, а также можно просмотреть другие методы.Официальная документация Babelиливавилонский почерк

Об этом загрузчике я опубликовал его на npm, и заинтересованные друзья могут позвонить ему напрямую.npm install async-catch-loader -DУстановка и исследование, используйте тот же метод, что и обычный загрузчик, не забудьте поместить его за загрузчиком babel для приоритетного выполнения и продолжайте передавать внедренный результат в babel для побега.

{
    test: /\.js$/,
    use: [
        "babel-loader?cacheDirectory=true",
        'async-catch-loader'
    ]
}

Более подробную информацию и исходный код можно посмотреть на github, и если эта статья будет вам полезна, я надеюсь, что вы сможете нажать на звездочку, большое спасибо~

async-catch-loader