tn babel实战篇

前景回顾

客户端视角下的babel(理论篇)介绍了AST(抽象语法树)的概念,以及Babel作为JavaScript编译器的运作原理。我们深入讲解了Babel的各个阶段,详细介绍了它们所具备的基础概念及相应的功能。最后,我们阐述了Babel插件如何对AST节点进行操作和转换的。与上篇理论相比,本文将围绕TN项目中的一些具体案例,重点介绍Babel在实践应用中的功能和用途。

总览

在正式讲述具体项目前,我们还是按照惯例先提几个问题(带着问题感触更深:)

  1. TN从Source Code到AST经历了哪些阶段?

  2. 这些阶段TN处理了哪些事情,遇到了哪些难题,并如何解决的?

问题1:

image.png

TN会在编译期间通过Babel将Source Code转成AST。其中核心的两个阶段:parse、transform

  • parse:对Source Code进行词法分析语法分析生成AST

  • transform:对parse阶段生成的AST进行遍历。在此过程中对节点进行添加、更新及移除等操作

细节的内容这边就不一一展开了,客户端视角下的babel(理论篇)讲解的非常详细了

问题2:

TN任务的处理主要在transform阶段。在该阶段我们具体处理了哪些事项呢?

  1. 支持高级语法:比如Enum(JavaScript中是没有枚举类型的)

  2. 支持语法降级:比如foreach降级到for

  3. 移除换行符多出来的JSXText节点

  4. 多文件依赖导入

  5. 变量名冲突

  6. 函数声明提升到作用域的顶部

  7. ...

image.png

针对不同种类的事项,我们可以创建不同的业务插件,来对节点进行添加、更新、移除等操作,从而达到我们的诉求。

当然,期间也遇到一些难点:比如多文件依赖如何有序导入、变量名冲突如何解决;对于换行符多出来的JSXText节点,我们如何处理,并保证处理的结果和SolidJS是一致的等等。

下面我们将讲述具体的几个事项,来帮助我们更好的了解TN的处理机制,以及难点问题的解决方式。

实践

前景声明

实践项目中会以SolidJS的表现作为依据去处理节点。为何是SolidJS而非React等其他前端框架呢?归根结底取决于TN的愿景:极致的渲染速度+开发体验效率。

下面会简单说明为何使用SolidJS作为首选的前端开发框架,更加详细的内容可参考

https://graffersid.com/solidjs-vs-react-which-is-better/

开发体验和效率

逻辑表达式 VS React

语法与React相似,实现与Composition API相似

useState -> createSignal
useMemo -> createMemo
useEffect -> createEffect
useLayoutEffect -> createRenderEffect

PS:Composition API 是 Vue 3 新增的 API,旨在解决 Vue 2 中大型组件的可读性、代码重用和类型安全等问题。Composition API 的核心思想是将组件的逻辑按照功能划分为不同的逻辑部分(composition),然后通过组合这些逻辑部分来创建复杂的组件逻辑。

依赖追踪 VS Vue3

Solid和Vue3相似,定义响应式数据,添加副作用函数,只不过Solid是读写分离的。

Solid 是一款基于 JavaScript 的 UI 库,它的读写分离是指对 UI 组件状态进行分离,将其分为可读状态和可写状态。具体来说,Solid 将各个 UI 组件的状态分解为两部分:

  • 可读状态:指 UI 组件的非响应式数据和方法,即只能被访问而不能进行修改的状态。这些状态的访问不会导致 UI 重新渲染。Solid 通过将这些状态管理在普通 JavaScript 对象中,来实现它们的可访问性。
  • 可写状态:指 UI 组件的响应式数据和方法,即在修改状态时会导致 UI 重新渲染的状态。Solid 通过将这些状态管理在响应式数据流中,来实现它们的响应式。

基于编译的运行时优化

在React与Vue中存在一层「虚拟DOM」(React中叫Fiber树)。

每当发生更新,「虚拟DOM」会进行比较(Diff算法),比较的结果会执行不同的DOM操作(增、删、改)。

而SolidJS在发生更新时,可以直接调用编译好的DOM操作方法,省去了「虚拟DOM比较」这一步所消耗的时间。

enum转换

enum 是 TypeScript 中的一个特性,它是一种枚举类型,用于定义具有固定值的常量集合。而在 JavaScript 中,目前并没有原生的 enum 类型。

当你定义了一个枚举

enum Fruit {
    Apple = 0,
    Banner = 1
}

其被编译成 JavaScript 后如下所示:

var Fruit = /*#__PURE__*/function (Fruit) {
  Fruit[Fruit["Apple"] = 0] = "Apple";
  Fruit[Fruit["Banner"] = 1] = "Banner";
  return Fruit;
}(Fruit || {});

对应的AST如下:

{
    "type":"VariableDeclaration",
    "kind":"var",
    "declarations":[
        {
            "type":"VariableDeclarator",
            "id":{
                ...
                "name":"Fruit"
            },
            "init":{
                "type":"CallExpression",
                "callee":{
                    "type":"FunctionExpression",
                    ...
                    "body":{
                        "type":"BlockStatement",
                        "body":[
                            {
                                "type":"ExpressionStatement",
                                "expression":{
                                    "type":"AssignmentExpression",
                                    "operator":"=",
                                    "left":{
                                        "type":"MemberExpression",
                                        ...
                                        "property":{
                                            "type":"AssignmentExpression",
                                            "operator":"=",
                                            "left":{
                                                "type":"MemberExpression",
                                                ...
                                                "property":{
                                                    "type":"StringLiteral",
                                                    "value":"Apple"
                                                },
                                            },
                                            "right":{
                                                "type":"NumericLiteral",
                                                "value":0
                                            }
                                        },
                                    }
                                }
                            },
                            {
                                "type":"ExpressionStatement",
                                "expression":{
                                    "type":"AssignmentExpression",
                                    "operator":"=",
                                    "left":{
                                        "type":"MemberExpression",
                                        ...
                                        },
                                        "property":{
                                            "type":"AssignmentExpression",
                                            "operator":"=",
                                            "left":{
                                                "type":"MemberExpression",
                                                ...
                                                "property":{
                                                    "type":"StringLiteral",
                                                    "value":"Banner"
                                                },
                                            },
                                            "right":{
                                                "type":"NumericLiteral",
                                                "value":1
                                            }
                                        },
                                    }
                                }
                            },
                            ...
                        ]
                    },
                    ...
                },
               ...
            }
        }
    ],
    ...
}

拿到上述AST后,我们发现一个简单的枚举被paser后变得极其复杂,而且可读性极差。那么是否有办法可以将其处理的精简并且可读化呢?答案是肯定的:通过构建TSEnumDeclaration的转换插件就可以完成目标

visitor: {
      VariableDeclaration(path) {
        if (path.node.type === 'VariableDeclaration' && (path.node.kind === 'var' || path.node.kind === 'let') && path.node.declarations.length === 1) {
          const declarationNode = path.node.declarations[0];
          if (declarationNode.type === 'VariableDeclarator' && declarationNode.init?.type === 'CallExpression' && declarationNode.init?.callee?.type === 'FunctionExpression') {
            const enumName = declarationNode.id.name;
            const entries = [];
            declarationNode.init.callee?.body?.body.filter(function(propertyNode) {
              return propertyNode.type === 'ExpressionStatement';
            }).forEach(propertyNode => {
              // enum Fruit {
              //   Apple = "apple",
              //   Banana = "banana",
              //   Orange = "orange"
              // }

              // enum Fruit {
              //   Apple,
              //   Banana,
              //   Orange
              // }

              // enum构建
              const memberName = propertyNode.expression.left.property.left?.property.value || propertyNode.expression.left.property.value;
              // initial value 处理
              const rightValue = propertyNode.expression.right.value;
              const leftValue = propertyNode.expression.left.property.right?.value;
              const memberValue = typeof leftValue === 'undefined' || leftValue === null || leftValue === '' ? rightValue : leftValue;
              entries.push(
                tsEnumMember(identifier(memberName), typeof memberValue === 'string' ? stringLiteral(memberValue) : numericLiteral(memberValue))
              );
            });

            // TSEnumDeclaration
            const tsEnumDeclWithComment = tsEnumDeclaration(identifier(enumName), entries);
            ...
          }
        }
      }
    }

PS: 节点类型的判断可以根据type值判断,也可以根据@babel/types定义的isX进行判断,两者等价

上述AST节点经转换后,我们就得到了预期的结果:

{
  type: 'TSEnumDeclaration',
  id: { type: 'Identifier', name: 'Fruit' },
  members: [
    { type: 'TSEnumMember', id: [Object], initializer: [Object] },
    { type: 'TSEnumMember', id: [Object], initializer: [Object] }
  ]
}

{"type":"TSEnumDeclaration","id":{"type":"Identifier","name":"Fruit"},"members":[{"type":"TSEnumMember","id":{"type":"Identifier","name":"Apple"},"initializer":{"type":"NumericLiteral","value":0}},{"type":"TSEnumMember","id":{"type":"Identifier","name":"Banner"},"initializer":{"type":"NumericLiteral","value":1}}]}

换行符处理

我们通过几个例子看下Babel对于换行符是如何处理的(不添加插件默认输出的结果)

  • Case1:文本标签换行,换行符会包含在文本内容中(eg:文本数据前后包含上下的换行符及前置的空格符)
<text>
  文本数据
</text>
"children": [
  {
    "type": "JSXText",
    "value": "\n  文本数据\n",
    "raw": "\n  文本数据\n"
  }
]
  • Case2:标签换行会生产多余的JSXText节点(eg:view标签换行)
<view>
    <text>
    文本数据1
    </text>
</view>
"children": [
  {
    "type": "JSXText",
    "value": "\n\t",
    "raw": "\n\t"
  },
  {
    ...
    "children": [
      {
        "type": "JSXText",
        "value": "\n  \t\t文本数据1\n\t",
        "raw": "\n  \t\t文本数据1\n\t"
      }
    ]
  },
  {
    "type": "JSXText",
    "value": "\n",
    "raw": "\n"
  }
],

通过上述case,我们大概发现了一个结论:如果在原始的JSX代码中有多余的换行符,则Babel将这些换行符解释为一个JSXText元素,这样可能会导致最终渲染出来的DOM树中多出一些空行节点。

那我们看下React是如何处理空行符的?

image.png

通过上述demo我们非常清晰看到,首先标签换行并不会产生多余的换行符;文本内容如果需要换行则需要{'\n'}表示,否则换行效果只是多一个空格;同一行文本内部有多个空格,则显示多个空格,同一行文本的首尾空格不会显示。

看完React,我们了解下SolidJS是怎么样的一个现象

image.png

SolidJS和React处理类似,区别点在于:两个文本间多个空格符会合并成单个;换行符会转换成单空格。

基于SolidJS对空行符的处理机制,处理JSX空行符的业务插件具体实现就很明确了

visitor: {
  JSXText(path) {
    // 获取当前节点在父节点中的前一个相邻节点
    const prevSibling = path.getSibling(path.key - 1)
    // 获取当前节点在父节点中的下一个相邻节点
    const nextSibling = path.getSibling(path.key + 1)

    // 判断该节点是否只包含空白字符或换行符
    if (path.node.value.trim() === '') {
      if (!prevSibling.node) {
        path.remove()
      } else if (prevSibling.node.type === "JSXElement") {
        path.remove()
      } else if (!nextSibling.node) { // 末节点
        path.remove()
      } else if (prevSibling.node.type === 'JSXExpressionContainer') { // {count()}
        const text = path.node.value.replace(/\s+/g, " ") // 前置后置中间空格>=1替换为一个空格
        path.node.value = text
      } 
    } else { // 文本中存在多个空格、换行,变成单个空格
      var text = path.node.value
      if (!prevSibling.node && !nextSibling.node) { // 单一节点
        text = text.trim() // 去除首尾空格
        text = text.replace(/\s+/g, " ") // 前置后置中间空格>=1替换为一个空格
      } else if (!prevSibling.node) { // 首节点
        text = text.trimStart() // 去除前置空格
        text = text.replace(/\s+/g, " ") // 前置后置中间空格>=1替换为一个空格
      } else if (!nextSibling.node) { // 末节点
        text = text.replace(/\s+/g, " ") // 前置后置中间空格>=1替换为一个空格
        text = text.trimEnd() // 去除后置空格
      } else {
        text = text.replace(/\s+/g, " ") // 前置后置中间空格>=1替换为一个空格
      }
      path.node.value = text
    }
  }
}

基于上述插件,Case1的输出结果:

{
  "type":"JSXElement",
  ...
  "openingElement":Object{...},
  "closingElement":Object{...},
    "children":[
    {
      "type":"JSXText",
      ...
      "value":"文本数据"
    }]
}

Case2的输出结果:

{
  "type":"JSXElement",
  ...
  "openingElement":Object{...},
  "closingElement":Object{...},
  "children":[
    {
      "type":"JSXElement",
      ...
      "openingElement":Object{...},
        "closingElement":Object{...},
      "children":[
        {
          "type":"JSXText",
          ...
          "value":"文本数据1"
        }
      ]
    }]
}

多import导入解决变量名冲突

TN会将所有模块打包成一个单一的Bundle文件,然后通过包管理下发到Native侧。所以如何将import到的外部依赖文件导入进来,并处理好变量名冲突问题,就成了关键。

说到打包,我们肯定会想到webpack。那么webpack是如何导入并解决变量名冲突问题的呢?

input:

// index.js
import {targetVersionCompare} from './util.js'
targetVersionCompare('11')

// util.js
import { testValue} from "./utilA.js";
const isEmptyString = (str) =>
    typeof str === 'undefined' || str == null || str === '';

const a = "1111"
export const targetVersionCompare = (target) => {
    console.log(testValue)
    if (isEmptyString(target)) {
        return false
    }
    console.log(a)
    return false
}

// utilA.js
export const testValue = 'A'
const a = "xxxx"
console.log(a)

webpack dist:

/******/ (() => { // webpackBootstrap
/******/    "use strict";
var __webpack_exports__ = {};

;// CONCATENATED MODULE: ./utilA.js
const testValue = 'A'
const a = "xxxx"
console.log(a)
;// CONCATENATED MODULE: ./util.js

const isEmptyString = (str) =>
    typeof str === 'undefined' || str == null || str === '';

const util_a = "1111"
const targetVersionCompare = (target) => {
    console.log(testValue)
    if (isEmptyString(target)) {
        return false
    }
    console.log(util_a)
    return false
}

;// CONCATENATED MODULE: ./index.jsx

targetVersionCompare('11')
/******/ })()
;

从上述源码输入到最后的产物对比,我们大概了解了webpack处理多文件导入的机制。

  1. 如果导入的文件某个变量名和当前文件变量名冲突,则添加前缀:当前文件名变量名序列

  2. 变量名a冲突:a->util_a

  3. 变量名a冲突,存在变量名util_a:a->util_a_0

  4. 变量名a冲突,存在变量名util_a,util_a_0:a->util_a_1

  5. export导出标识会被remove

  6. 加载依赖文件的顺序:递归导入。当A依赖B、A依赖C,B依赖D,则导入的顺序依次为D-B-C-A

方案设计

webpack的处理机制我们大概了解了,整体的大致流程是怎么样的呢?

简要流程图

image.png

流程概述

  1. 创建remove-export-name-plugin:移除导出标识

  2. 创建import-declaration-plugin

  3. 递归读取依赖的文件

  4. 生成唯一标识 By 完整文件路径

  5. 链表记录前后依赖&AST、export数据绑定

  6. import导入数据处理

  7. 反转链表

  8. diff

  9. 重命名冲突处理

  10. 节点合并

方案实现

移除export
visitor: {
  ExportNamedDeclaration(path) {
    const { node } = path
    const { declaration, specifiers } = node
    if (declaration) {
      path.replaceWith(declaration);
    }
  },
    ExportDefaultDeclaration(path) {    
      const { node } = path
      const { declaration } = node

      if (declaration) {
        path.replaceWith(declaration);
      } else {
        path.remove();
      }
    }
}
import-declaration-plugin
  • 递归读取依赖的文件

当我们输入import { testValue} from "./utilA.js"语句的时候,AST会生成节点:ImportDeclaration,其导入的文件相对路径也会在source字段下展示。如下所示:

image.png

那么如何根据import的相对路径去构建完成的文件路径呢?

首先我们需要明白一点:导入文件的相对路径只有对于当前文件是透明的,而文件导入依赖是递归的,所以我们需要做的就是用一个栈记录当前import file路径依赖,导入文件的时候压栈,离开文件的时候出栈。

PS:另外JS在导入文件的时候,可以选择性的不写文件后缀名,需要单独处理

const dependFilePathStack = [] // 栈记录当前import file路径依赖

const constructFile = (importModulePath) => {
  const curFilePath = dependFilePathStack.slice(-1)[0] || source // 获取栈顶元素
  var filePath = path.resolve(path.dirname(curFilePath), importModulePath)
  const dirPath = path.resolve(filePath, '..'); // 获取上一级目录

  if (path.extname(filePath)) { // 当前文件路径存在后缀
    return filePath
  }

  // 获取目录下所有文件
  const fileNames = fs.readdirSync(dirPath)
  // 获取文件名
  const basename = path.basename(filePath);

  // 查找指定文件名的后缀名(兼容.d.ts声明文件)
  const targetFileName = fileNames.find(fullFileName => {
    return path.basename(fullFileName, path.extname(fullFileName)) === basename || path.basename(fullFileName, '.d.ts') === basename;
  })
  // 组合文件路径和后缀名
  return targetFileName ? `${dirPath}/${targetFileName}` : ''
}
  • 生成唯一标识 By 完整文件路径

tn处理重命名冲突规则和webpack稍微有点差异。差异点:webpack可以理解为在"运行时"去处理重命名冲突,即后置判断;而tn为了简化逻辑,会在“编译时”即前置阶段直接生成一个唯一标识,当命名冲突的时候,直接使用唯一标识去替换。

  • 链表记录前后依赖&AST、export数据绑定

import文件导入是递归读取的,我们可以使用链表来存储文件之间的依赖关系。节点的定义如下:next 表示当前文件导入的下一个依赖文件,nameSpace 表示当前文件的唯一标识,nodes 表示当前文件的 AST节点集合,exported 表示当前文件导出的集合。

class ImportNode {
    constructor(next, nameSpace, nodes, exported) {
        this.next = next
        this.nameSpace = nameSpace
        this.nodes = nodes
        this.exported = exported
    }
}

递归读取文件的同时,记录对应ImportNode的AST及exported相关数据

// 记录import依赖
const lasted = findLastedNode(path)
if (!lasted) {
  // 绑定Head Node
  const headNode = new ImportNode(null, nameSpace, null, null)
  importHeadNode = headNode
} else {
  lasted.next = new ImportNode(null, nameSpace, null, null)
}

// 获取ast节点集合
const importModuleAst = processedFilePaths[fullFilePath] || generateToAst(fullFilePath)
processedFilePaths[fullFilePath] = importModuleAst

const astBody = importModuleAst.program.body

// 绑定nodes
const target = findTargetNode(nameSpace)
target.nodes = astBody

// 绑定exported
target.exported = collectExportAs(astBody)

import-declaration-plugin组件大致实现如下:

var importHeadNode = null
const dependFilePathStack = [] // 栈记录当前import file路径依赖
...

visitor: {
  ImportDeclaration(path) {
    // 1.获取当前import module path
    // 2.获取导入模块的代码并解析成 AST
    const importModulePath = path.get('source').node.value
    const fullFilePath = constructFile(importModulePath)

    if (!fullFilePath || processedFilePaths[fullFilePath]) {
      return;
    }

    // 构建namespace
    const nameSpace = constructNameSpace(fullFilePath)
    // 记录import依赖
    const lasted = findLastedNode(path)
    if (!lasted) {
      // 绑定Head Node
      const headNode = new ImportNode(null, nameSpace, null, null)
      importHeadNode = headNode
      ...
    } else {
      lasted.next = new ImportNode(null, nameSpace, null, null)
    }
    dependFilePathStack.push(fullFilePath)

    // traverseImportHead(importHeadNode)
    const importModuleAst = processedFilePaths[fullFilePath] || generateToAst(fullFilePath)
    processedFilePaths[fullFilePath] = importModuleAst
    dependFilePathStack.pop()

    // 导入文件的body
    const astBody = importModuleAst.program.body

    // 绑定nodes
    const target = findTargetNode(nameSpace)
    target.nodes = astBody

    // 绑定exported
    target.exported = collectExportAs(astBody)
  }
}
import导入数据处理
  • 反转链表
  1. tn和webpack对于依赖的文件导入顺序是一致的,比如A依赖B、A依赖C,B依赖D,则导入的顺序依次为D-B-C-A。因此我们需要反转链表,从链表的尾部节点倒序读入
const reverseLinkList = (importHeadNode) => {
    const reverseList = []
    var head = importHeadNode
    while (head) {
        reverseList.push(head)
        head = head.next
    }
    return reverseList.reverse()
}
  • diff

diff是重命名冲突的核心函数。当import文件导入时,有可能导入的Identifier与当前文件存在冲突。大致处理的规则如下:

  1. 获取当前import文件内容中声明的所有names

  2. 当前node与导入的前几个文件diff

  3. 读取下一个节点,重复1,2两个步骤

// 1\. 获取当前import文件内容中声明的所有names 2\. 当前node与导入的前几个文件diff
const dealImportDependies = (reverseImportList) => {
    var importNodes = []
    for (let index = 0; index < reverseImportList.length; index++) {
        const node = reverseImportList[index]
        // diff前 (原始声明)
        const beforeDiffDeclarationNames = collectNodeDeclarationName(node)
        // diff
        diffImportFileDeclaration(importDeclarationNames(), node)
        importNodes = [...importNodes, ...node.nodes]
        // console.log(JSON.stringify(node))

        // diff后(替换后的声明):部分声明diff后会重新替换
        const afterDeclarationNames = collectNodeDeclarationName(node)

        // 根据前后diff生成对照表
        const nameUnion = declarationNameSpaceUnion(beforeDiffDeclarationNames, afterDeclarationNames)
        declarationNameTable[node.nameSpace] = nameUnion

        // 绑定export as
        exportedAsDeclarationTable[node.nameSpace] = isNonEmptyObj(node.exported) ? node.exported : null
    }
    return importNodes
}

后续的node与前面import过的声明集合进行diff

/**
 * dfs 节点下的叶子节点声明
 * @param {*} input node
 * @param {*} baseUnion 前置导入的file declaration name 集合
 * @param {*} nameSpace  当前文件的namespace
 * @param {*} importDeclarationNames 当前文件导入的import声明 import {A,B} from 'xxxx'
 * @returns 
 */
function dfsNodeDeclaration(input, baseUnion, nameSpace, importDeclarationNames) {
    if(!input){
        return
    }

    if(Array.isArray(input)){
        for(let i = 0; i < input.length; i++){
            dfsNodeDeclaration(input[i], baseUnion, nameSpace, importDeclarationNames)
        }
    }

    if(input.type === 'FunctionDeclaration'){ // done
        dfsNodeDeclaration(input.id, baseUnion, nameSpace, importDeclarationNames)
        dfsNodeDeclaration(input.body, baseUnion, nameSpace, importDeclarationNames)
    }
    ...
    else if(input.type === 'Identifier' || input.type === 'JSXIdentifier'){
        dealIdentifier(input, baseUnion, nameSpace, importDeclarationNames)
    } else if (input.type === 'JSXExpressionContainer')  {
        dfsNodeDeclaration(input.expression, baseUnion, nameSpace, importDeclarationNames)
    } 
    ...
    else {
        return
    }
}
  • 重命名冲突处理

首先我们梳理下可能存在的几种情况。

比如文件

// utilB
// utilB:utilB_Test
function utilB() {
}
export { utilB as utilB_Test }

// util
// utilB_Test:testB
import { utilB_Test as testB} from "./utilB.js";
testB()

// 最终映射链
testB()->testB->utilB_Test->utilB->declarationNameTable查找
为何还需declarationNameTable查找,因为utilB可能因为命名冲突已改成了utilB_utilB
/**
 * 重名的变量使用内部的还是外部的
 * 具体规则:如果命中重命名冲突,1.使用的Identifier在import中是否声明,若声明则使用该声明的 2.使用当前文件的namespace去更换,避免重命名
 * @param {*} node 
 * @param {*} baseUnion 
 * @param {*} namespace 
 * @param {*} importDeclarationNames
 */
const dealIdentifier = (node, baseUnion, nameSpace, importDeclarationNames) => {
    const name = node.name
    // find -> replace
    var declarationName = findImportedDeclarationName(importDeclarationNames, name)
    if (declarationName) { // 使用外部声明

        // find export as别名处理
        const exportedAsName = findExportedAsDeclarationName(name)
        if (exportedAsName) {
            declarationName = exportedAsName
        }

        const updatedDeclarationName = exportDeclarationName(declarationName)
        node.name = updatedDeclarationName
        return
    }

    if (baseUnion.includes(name)) { // 其他文件声明与当前的node冲突
        // 内部声明
        node.name = nameSpace+'_'+name
    }
}

总结

通过上述TN的一些实践项目,想必你对Babel的理解更加深刻了吧

总的来说,Babel对于现代前端开发来说是一个很重要的工具。通过Babel,我们可以使用最新的JavaScript特性,同时保证代码能够在较老的浏览器或运行环境中运行。在实践中,Babel的应用范围非常广泛,包括但不限于:

  • 在Node.js和浏览器环境中使用;

  • 在Webpack等打包工具中使用,将ES6/ES7代码转换为ES5代码;

  • 在React.js和Vue.js等框架中使用,通过相应的插件和预设来优化编译结果;

  • 实现一码多投,让应用程序能够在多个平台上运行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335