Наконец-то пользуйтесь спросом на АСТ!
АСТ и я
Концепции, связанные с AST, обсуждались в группе ранее, иrecast
библиотека для управления деревьями AST,
Вы можете прочитать эту статью здесь
Абстрактное синтаксическое дерево (AST)
Доля закончена в то время, было очень пустым, хотя некоторые из его основных концепций, чтобы понять, но также сделали небольшую демонстрацию, но все еще слишком много на поверхности, нет практического применения, бумага приходит чунь. Совсем недавно у меня была возможность потратить в два раза больше АСТ.
Очень крутой ТМД!
Конфликт между библиотекой enum и JSDOC
Когда члены команды разобрались с перечислением проекта и упаковали его в библиотеку, MR был отправлен.
Прежде чем проект был фрагментирован, перечисление единства поддерживалось частной библиотекой, больше не нужно было поддерживать копию каждого проекта.
但文档似乎有点多,好几十个 js 脚本。 тогда
Я: Должен быть еще один документ, чтобы информировать разработчиков, для чего нужны эти перечисления, чтобы разработчикам не нужно было искать код, а вызывать его напрямую.
Члены команды сказали да, затем они исследовали библиотеку JSDOC и обнаружили, что формат кода не соответствует требованиям JSDOC. Например:
Замечания, подобные первому, а не второму:
/** 这是JSDOC可识别的备注 */
/* 这是JSDOC不可识别的备注 */
Например:
Типы экспорта, подобные первому, а не второму:
const applyTypeObj = {
/** 普通投递 */
NORMAL_APPLY: 0,
/** 一键投递 */
ONE_CLICK_APPLY: 1,
/** 邀请投递 */
INVITE_APPLY: 2
}
export const applyTypeEnum = Object.freeze(applyTypeObj)
// 普通投递
export const NORMAL_APPLY = 0
// 一键投递
export const ONE_CLICK_APPLY = 1
// 邀请投递
export const INVITE_APPLY = 2
С 21 файлом и большим количеством перечислений в файлах для этого вы можете подсчитать трудозатраты, а процесс утомительный, утомительный и подверженный ошибкам.
Почему бы тебе не спросить у волшебного AST
С опытом, которым поделились в прошлый раз, на этот раз написать такой код преобразования должно быть легко.
Сначала подумайте об основном процессе
Рекурсивное чтение файла проекта -> Чтение файла -> Действия AST -> Запись файлов
recast
библиотека.
function recastFileName(path, fileName) {
fs.readFile(path, function(err, data) {
// 读取文件失败/错误
if (err) {
throw err
}
const code = data.toString()
console.log(code)
const ast = recast.parse(code)
let i = 0
// 要做的事情很简单,把所有 var 定义的 并且值是 Literal 整合起来
const maps = {}
// 各个字段的备注存在这
const markMap = {}
let markDown = ''
recast.visit(ast, {
visitExportNamedDeclaration: function(path) {
const init = path.node.declaration.declarations[0].init
const key = path.node.declaration.declarations[0].id.name
let value = init.value
const type = init.type
if (type === 'UnaryExpression') {
value = eval(`${init.operator}${init.argument.value}`)
}
if (type === 'Literal' || type === 'UnaryExpression') {
maps[key] = value
path.node.comments &&
path.node.comments.map(item => {
markDown = `/**
* Enum for ${fileName}
* ${item.value}
* @enum {number}
*/\n`
})
return null
}
return false
},
visitVariableDeclaration: function(path) {
if (
!path.value.declarations ||
!path.value.declarations[0] ||
!path.value.declarations[0].init.elements
) {
return false
}
console.log('定义')
console.log(path.value.declarations[0].init.elements)
path.value.declarations[0].init.elements.map(element => {
const key = element.properties[0].value.name
const value = element.properties[1].value.value
element.properties[0].value = memberExpression(id(`${fileName}Obj`), id(key))
markMap[key] = value
})
return false
}
})
if (!Object.keys(maps).length) {
console.log('无需转换')
return
}
let mapString = '{\n'
Object.keys(maps).map((key, index) => {
if (markMap[key]) mapString += ` /** ${markMap[key]} */\n`
if (index === Object.keys(maps).length - 1) {
mapString += ` "${key}": ${maps[key]}\n`
} else {
mapString += ` "${key}": ${maps[key]},\n`
}
})
mapString += '}'
const res = `const ${fileName}Obj = ${mapString}\nexport const ${fileName}Enum = Object.freeze(${fileName}Obj)\n`
const output = res + recast.print(ast).code
const finel = recast.print(recast.parse(output)).code
console.log(finel)
console.log(output)
fs.writeFile(path, `${markDown}\n${finel}`, {}, function() {
console.log(`wirte ${fileName} OK!`)
})
})
}
const map = []({
// 很容易看出来,这个方法是用来捕捉 export 语句的
visitExportNamedDeclaration: function(path) {
const init = path.node.declaration.declarations[0].init
const key = path.node.declaration.declarations[0].id.name
let value = init.value
const type = init.type
/* 将
export const NORMAL_APPLY = 0
有用的信息拿出来,存进对象里
maps: { NORMAL_APPLY: 0 }
*/
if (type === 'Literal' || type === 'UnaryExpression') {
maps[key] = value
}
return false
}
})
let mapString = '{\n'
Object.keys(maps).map((key, index) => {
if (markMap[key]) mapString += ` /** ${markMap[key]} */\n`
if (index === Object.keys(maps).length - 1) {
mapString += ` "${key}": ${maps[key]}\n`
} else {
mapString += ` "${key}": ${maps[key]},\n`
}
})
mapString += '}'
writeFile(mapString)
Это требование было выполнено достаточно хорошо.
еще одно требование
Изменение параметров мини-программы маршрутизации
Когда я потратил полдня, чтобы закончить перечисление библиотеки преобразования, сердце немного взволновалось, просто в текущей версии есть потребность и спрос, как указано выше.
Для проекта апплета переход маршрутизации выглядит следующим образом:
wx.navigateTo({
url: `/pages/resumeOptimize?jobId=${this.jobId}&resume_enhance_source=apply_work_success&workid=${this.jobId}&service_type=resume_optimization`
})
Длинные, уродливые и подверженные ошибкам добавления параметров позже.
Значит надо так писать.
wx.navigateTo({
url: `/pages/resumeOptimize?${qs.stringify({
jobId: this.jobId,
resume_enhance_source: 'apply_work_success',
workid: this.jobId,
service_type: 'resume_optimization'
})}`
})
Элегантный, с красивым отступом и простой в обслуживании.
Спросить у волшебного AST еще раз?
На этот раз явно сложнее, чем в прошлый раз. Во-первых, файл с кодом не js, а.wpy
.
Поскольку апплет использует структуру wepy, структуру, подобную vue.
<template></template>
<script></script>
<style></style>
Во-первых, отделить от этой структурыscript
вне. Разумеется, с мощным регуляром.
function getScript(code) {
let jsReg = /<script>[\s|\S]*?<\/script>/ig;
const scriptColletion = code.match(jsReg)[0].replace(/<script>/, '').replace(/<\/script>/, '');
return scriptColletion
}
легко получитьscript
Содержание.
Предположим, что операция завершена, код, который вы хотите сохранить в файле, останется прежним.
// 再把script设置回去
function setScript(code, script) {
let jsReg = /<script>[\s|\S]*?<\/script>/ig;
return code.replace(jsReg, `<script>\n${script}</script>`)
}
анализ спроса
Последний раздел просто реализовал доступ к коду, и вот, наконец, наступило главное событие. Сначала проанализируйте, что мы собираемся делать.
- код перехвата
wx.navigateTo
илиwepy.navigateTo
, эти два API одинаковы, и разработчики могут вызывать - Замените параметры этого API строкой шаблона на
qs.stringify
вызов метода - Если файл имеет вторую операцию, и заголовок файла не
import qs from 'qs'
, Вам нужно вручную добавить
Перехватить API
Вызов метода API, очевидно,ExpressionStatement
Поэтому операция за нами очень простая, чтобы перехватить его, и находится вvisitExpressionStatement
делается в обратном вызове.
recast.visit(ast, {
visitExpressionStatement: function(path) {
const callee = path.node.expression.callee
if(!callee || !callee.object) {
return false
}
const objName = callee.object.name
const fnName = callee.property.name
// 调用者是wx 或者 wepy
if(objName === 'wx' || objName === 'wepy') {
// 跳转
if(fnName === 'navigateTo') {
// 拦截到了
}
}
return false
},
}
замена строки шаблона
Происхождение мечты
Вот основная функция, она у меня ушла больше чем на полдня.
Мы находимся в точке, где приведенный выше код «перехватывается». Используйте devTool, чтобы найти состав синтаксического дерева wx.navigateTo.
Например 🌰:
wepy.navigateTo({
url: `/pages/detail/jobDetail?id=${e.id}&from=job_detail&num=${e.index}&uniqueKey=${uniqueKey}`
})
<pre>
if(fnName === 'navigateTo') {
const argument = path.node.expression.arguments[0]
let {expressions, quasis} = argument.properties[0].value
if(!expressions || !quasis || !expressions.length) {
return false
}
if(expressions.length < 2) {return false}
expressions = expressions.map((val) => {
const res = recast.print(val)
return res.code
})
let url = ''
quasis = quasis.map((val) => {
const path = val.original.value.cooked
if(/\?/.test(path)) {
// 把 url 存下来
url = path.split('?')[0]
return path.split('?')[1]
}
return path
})
}
</pre>
На самом деле ast разбивает эту строку кода на две группы, одну для выражений и одну для квази, и я печатаю их обе. Первое — это выражение, второе — строка. Именно длина выражения = длина строки - 1.
Это соответствует формату строки шаблона.
["e.id", "e.index", "uniqueKey"]
["id=", "&from=job_detail&num=", "&uniqueKey=", ""]
Мне нужно объединить два массива и поместить строку в кавычки.
<pre>
const express = assignArray(quasis, expressions).join('').split('&')
function assignArray(arr2, arr1) {
// 把 arr2里面的字符串加上引号
arr2 = arr2.map((val) => val.split('&').map((equel) => {
if(!equel || !equel.split('=')[1]) {
return equel
}
const value = '\'' + equel.split('=')[1] + '\''
return [equel.split('=')[0], value].join('=')
}).join('&'))
arr1.forEach((item, index) => {
arr2.splice(2 * (index + 1) - 1, 0, item)
})
return arr2
}
</pre>
В итоге это выглядит так:
["id=e.id", "from='job_detail'", "num=e.index", "uniqueKey=uniqueKey"]
Затем преобразуйте приведенный выше массив в параметры функции.
const results = giveQsString(express, url)
function giveQsString(expressArr, url) {
let str = `url: \`${url}?\${qs.stringify({\n`
expressArr.map((val, index) => {
const [key, value] = val.split('=')
if(index === expressArr.length - 1)
str += ` ${key}: ${value}\n`
else
str += ` ${key}: ${value},\n`
})
str += `})}\``
console.log(str)
return str
}
/*
url: `/pages/detail/jobDetail?${qs.stringify({
id: e.id,
from: 'job_detail',
num: e.index,
uniqueKey: uniqueKey
})}`
*/
Самый важный шаг, параметры, способ заполнения для навигации
path.node.expression.arguments[0].properties[0] = templateElement({
"cooked": results, "raw": results
}, false)
Таким образом, основной AST завершается.
плюс qs
Если файл выполнил описанные выше шаги, он должен импортировать qs. Здесь нужно оценить только ImportDeclaration. Если модуль qs не импортирован, скажите подчиненному добавить оператор импорта в заголовок файла.
visitImportDeclaration: function(path) {
// 如果模块引入了qs,则不需要导入
if(path.node.source.value === 'qs') {
needQs = false
}
return false
}
Суммировать
Подводя итог словами из моей еженедельной газеты
Оба требования на этой неделе использовали AST, и это был первый раз, когда AST использовался в реальном проекте.Эффекты этих двух требований также были разными.
Структура кода и изменения комментариев: формат файла кода этой библиотеки относительно однороден, с большим количеством файлов, что не подходит для ручной перезаписи один за другим Использование AST для этого экономит много времени, а сложность не высокий.
Параметры роутинга go qs: Формат кода этой библиотеки типа wepy, что относительно сложно сделать.На написание AST кода ушёл целый день,и наконец реализовал замену глобальной роутинга.После замены обнаружилось что нет было не так много файлов для замены. , так что это контрпример.
Поэтому, прежде чем принять решение об использовании AST, вы должны сначала выяснить, подходит ли он для использования.Я думаю, что должны быть выполнены следующие два условия:
1. Код AST легко написать.
2. Большой объем повторяющейся работы
АСТ это круто!