箭头函数转换插件
先来看看 ES 标准下的箭头函数 和 普通函数的 AST 区别
const sum = (a, b) => a + b
const sum = function(a, b) { return a + b }
我们需要把 ArrowFunctionExpression 节点换成 FunctionExpression 节点,从图中我们可以看到,我们需要修改节点的 type 、body 、range 等属性,比较繁琐。
由于 babel 的生态十分丰富且坚挺,我们使用 babel 的工具包 [@babel/core](https://babel.docschina.org/docs/en/babel-core/)
和[@babel/types](https://babel.docschina.org/docs/en/babel-types/)
来简化我们的操作。
@babel/core
提供了JavaScript 转 AST,AST 转 JavaScript 的功能@babel/types
提供了判断节点类型,创建节点的功能
const babel = require('@babel/core')
const t = require('@babel/types')
const source = `
const good = () => {}
let sum = (a, b) => { return a + b }
`
// 箭头函数转换插件
const ArrowTransformPlugin = {
visitor: {
// path 是树的路径
ArrowFunctionExpression: function (path) {
const node = path.node
let blockStatement = node.body
// 如果不是代码块,就改成代码块
if (!t.isBlockStatement(blockStatement)) {
const returnStatement = t.returnStatement(node.body)
blockStatement = t.blockStatement([returnStatement])
}
const fnExpression = t.functionExpression(null, node.params, blockStatement)
path.replaceWith(fnExpression)
}
}
}
const result = babel.transform(source, {
plugins: [
ArrowTransformPlugin
]
})
console.log(result.code)
class 类转换插件
简单模拟 babel 的类声明转换为函数声明,待完善的功能
- 类的继承
- super 方法 ```javascript const babel = require(‘@babel/core’) const t = require(‘@babel/types’)
const source = class Father {
constuctor(name) {
this.name = name
}
getName() {
console.log(this.name)
}
}
const ClassTransformPlugin = { visitor: { ClassDeclaration: function (path) { const { node } = path const classMethodNodeList = node.body.body const className = node.id let fnDeclaration = t.functionDeclaration(className, [], t.blockStatement([])) const prototypeMethods = [] classMethodNodeList.forEach(nodeItem => { if (nodeItem.key.name === ‘constuctor’) { // 添加构造函数的方法 fnDeclaration = t.functionDeclaration(className, nodeItem.params, nodeItem.body) } else { // 添加原型的方法 let prototype = t.memberExpression(className, t.identifier(‘prototype’)) const left = t.memberExpression(prototype, t.identifier(nodeItem.key.name)) t.identifier(nodeItem.key.name) const right = t.functionExpression(null, [], nodeItem.body) // // 创建赋值表达式,添加函数原型方法 const prototypeMethod = t.assignmentExpression(‘=’, left, right) prototypeMethods.push(prototypeMethod) } }) if (!prototypeMethods.length) { path.replaceWith(fnDeclaration) } else { prototypeMethods.push(fnDeclaration) path.replaceWithMultiple(prototypeMethods) } } } }
const result = babel.transform(source, { plugins: [ ClassTransformPlugin ] })
console.log(result.code)
<a name="Dq9qs"></a>
## JSX 转换插件
babel 转换 JSX 语法的工具包是 `@babel/preset-react`这个预设,它包含了以下插件
- [@babel/plugin-syntax-jsx](https://www.babeljs.cn/docs/babel-plugin-syntax-jsx):让 @babel/core 能识别 JSX 语法,正确构建 babel 的 AST 标准树
- [@babel/plugin-transform-react-jsx](https://www.babeljs.cn/docs/babel-plugin-transform-react-jsx):将 JSX 语法转换为 react 创建元素的语法
- [@babel/plugin-transform-react-display-name](https://www.babeljs.cn/docs/babel-plugin-transform-react-display-name)
如果开启了 `development`参数,还将包含以下插件
- [@babel/plugin-transform-react-jsx-self](https://www.babeljs.cn/docs/babel-plugin-transform-react-jsx-self)
- [@babel/plugin-transform-react-jsx-source](https://www.babeljs.cn/docs/babel-plugin-transform-react-jsx-source)
下面我们试着实现将含 JSX 语法的 JS 代码转换成 babel 标准的 AST 的功能
1. 词法分析:JS 代码 转换为 tokens
2. 语法分析:tokens 转换为 AST
```javascript
const babel = require('@babel/core')
const t = require('@babel/types')
const source = `
let element = <p>Konsoue</p>
`
/**
* 词法分析:先将 JS 代码转成 tokens
* [
* { type: 'KeyWord', value: 'let' },
* { type: 'WhiteSpace', value: ' ' },
* { type: 'Identifier', value: 'element'}
* { type: 'WhiteSpace', value: ' ' },
* { type: 'Equal', value: '=' },
* { type: 'WhiteSpace', value: ' ' },
* { type: 'JSXElement', value: '<p>Konsoue</p>' },
* ]
*/
function JSXToTokens(source) {
const tokens = []
let token;
const isWord = /[a-zA-Z]/
const isWhiteSpace = /\s/
const isEqual = /=/
for (let i = 0; i < source.length; i++) {
let char = source.charAt(i)
if (isWord.test(char)) {
token = { type: 'Identifier', value: char }
while (i++ < source.length) {
char = source.charAt(i)
if (isWord.test(char)) {
token.value += char
} else {
if (['let', 'const', 'var'].includes(token.value)) token.type = 'KeyWord'
i--
break
}
}
} else if (isWhiteSpace.test(char)) {
token = { type: 'WhiteSpace', value: ' ' }
while (i++ < source.length) {
char = source.charAt(i)
if (isWhiteSpace.test(char)) {
token.value += char
} else {
i--
break
}
}
} else if (isEqual.test(char)) {
token = { type: 'Equal', value: char }
} else if (char === '<') {
token = { type: 'JSXElement', value: char }
let isClose = false
while (i++ < source.length) {
char = source.charAt(i)
token.value += char
if (char === '/') {
isClose = true
} else if (char === '>' && isClose) {
break
}
}
} else {
token = { type: 'String', value: char }
while (i++ < source.length) {
char = source.charAt(i)
token.value += char
}
if (!Number.isNaN(Number(token.value))) token = { type: 'Numeric', value: Number(token.value) }
else {
let str = '', start = false
// 去掉字符串外层的双引号 ""
const top = token.value[0] // 获取第一个引号,用于判断是单引号还是双引号
for (let j = token.value.length - 1; j > 0; j--) {
if (start) str = token.value[j] + str
else {
if (token.value[j] === top) start = true
}
}
token.value = str
}
}
tokens.push(token)
}
return tokens
}
/**
* 语法分析:将 tokens 转成 AST
* @param {*} tokens
*/
const parser = (tokens) => {
const AST = {
type: 'Program',
body: [],
sourceType: 'module'
}
let i = 0
let currentToken
while (currentToken = tokens[i]) {
if (currentToken.type === 'KeyWord') {
const VariableDeclaration = { type: 'VariableDeclaration', declarations: [], kind: currentToken.value }
i += 2
currentToken = tokens[i]
const VariableDeclarator = {
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: currentToken.value
}
}
i += 2
while (['WhiteSpace', 'Equal'].includes(tokens[i].type)) i++
currentToken = tokens[i]
if (['String', 'Numeric'].includes(currentToken.type)) {
VariableDeclarator.init = {
type: currentToken.type + 'Literal',
value: currentToken.value
}
} else if (currentToken.type === 'JSXElement') {
const value = currentToken.value
const reg = /<([^>]+)>([^<]+)<\/([^>]+)>/
const [, openTagName, childrenText, closeTagName] = value.match(reg)
VariableDeclarator.init = {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {
type: 'JSXIdentifier',
name: openTagName
},
},
closeElement: {
type: 'JSXOpeningElement',
name: {
type: 'JSXIdentifier',
name: closeTagName
}
},
children: {
type: 'JSXText',
value: childrenText
}
}
}
VariableDeclaration.declarations.push(VariableDeclarator)
AST.body.push(VariableDeclaration)
}
i++
}
return AST
}
const tokens = JSXToTokens(source)
const AST = parser(tokens)
console.log(JSON.stringify(AST))