Я использую АСТ Дафа в реальном проекте!

JavaScript

Наконец-то пользуйтесь спросом на АСТ!

АСТ и я

Концепции, связанные с 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. Большой объем повторяющейся работы

АСТ это круто!