Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于Babel #38

Open
Luin-Li opened this issue Oct 31, 2019 · 0 comments
Open

关于Babel #38

Luin-Li opened this issue Oct 31, 2019 · 0 comments

Comments

@Luin-Li
Copy link
Contributor

Luin-Li commented Oct 31, 2019

关于Babel

  • Babel 编译的三个阶段
  • Babel使用
  • polyfill和runtime差别
  • Babel插件开发流程

Babel 编译的三个阶段

Babel 的编译过程可以分为三个阶段:

  • 解析(Parsing):将代码字符串解析成抽象语法树。
  • 转换(Transformation):对抽象语法树进行转换操作。
  • 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。
    编译过程

1. 解析(Parsing)

使用 babylon 解析器对输入的源代码字符串进行解析并生成初始 AST。整个解析过程分为两个步骤:1)词法分析(lexical analyzer或scanner):将整个代码字符串分割成语法单元数组在线分词工具。2)语法分析(syntax analyzer):它会将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。

babel 内部使用的解析类库叫做 babylon,并非 babel 自行开发。

简单来说语法分析是对语句和表达式识别,这是个递归过程,在解析中,Babel 会在解析每个语句和表达式的过程中设置一个暂存器,用来暂存当前读取到的语法单元,如果解析失败,就会返回之前的暂存点,再按照另一种方式进行解析,如果解析成功,则将暂存点销毁,不断重复以上操作,直到最后生成对应的语法树。

*可以通过astexplorer.net 网站在线生成抽象语法树AST*

2. 转换(Transformation)

babel中最核心的是 babel-core ,它向外暴露出 babel.transform 接口,遍历 AST 树并应用各 transformers(plugin)。

const babel = require('babel-core');
let result = babel.transform(code, {
    presets: ['env'],
    plugins: ['transform-runtime']
})

3. 生成(Code Generation)
利用 babel-generator 将 AST 树生成转码后的代码

Babel使用

将插件的名字增加到配置文件中 (根目录下创建.babelrc或者 package.json的babel中),然后npm install babel-plugin-xxx进行安装。
示例:

{
  "presets": [
    ["env", // 带了配置项,自己变成数组。第一个元素依然是名字
      {  // 第二个元素是对象,列出配置项
        "modules": false,
        "targets": {
          "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
        }
      }
    ],
    // 不带配置项,直接列出名字
    "stage-2"
  ],
  "plugins": [
    "transform-vue-jsx", 
    "transform-runtime",
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ],
  "env": {
    // 基于环境来配置 Babel 
    "test": {
      "presets": ["env", "stage-2"],
      "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
    }
  }
}

env:Babel 将根据当前环境来开启 env 下的配置。当前环境可以使用 process.env.BABEL_ENV 来获得。 如果 BABEL_ENV 不可用,将会替换成 NODE_ENV,并且如果后者也没有设置,那么缺省值是development

Presets

因为一套类似es2015的规范,包含了很多个转译插件。为了每次要开发者一个个添加并安装转译插件问题,babel提供了一组插件的集合。理解为单点和套餐的差别

preset分为以下几种:

  • 官方内容:目前包括 env, react, flow, minify 等。
  • Stage 0 - 稻草人: 只是一个想法,经过 TC39 成员提出即可。
  • Stage 1 - 提案: 初步尝试。
  • Stage 2 - 初稿: 完成初步规范。
  • Stage 3 - 候选: 完成规范和浏览器初步实现。
  • Stage 4 - 完成: 将被添加到下一年度发布。在下一年更新会直接放到env中,所以没有单独的 stage-4 可供使用。
env配置参数
  • 如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加。env包含的插件列表
  • targets:描述所需要支持的环境
{
  "presets": [
    ["env", {
      "targets": "> 0.25%, not dead"
    }]
  ]
}
{
  "presets": [
    ["env", {
      "targets": {
        "chrome": "58",
        "ie": "11"
      }
    }]
  ]
}
{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["last 2 versions", "safari >= 7"]
      }
    }]
  ]
}

语法可以参考browserslist

  • modules:它的取值可以是amd, umd, systemjs, commonjsfalse。转换es6模块语法到其他模块规范, false不会转换。默认为commonjs

执行顺序

  • Plugin 会运行在 Preset 之前。
  • Plugin 会从前到后顺序执行。
  • Preset 的顺序则从后向前。(preset的逆向顺序主要是为了保证向后兼容)

其他工具

其他相关工具

  • babel-register只会对 require 命令加载的文件转码,而不会对当前文件转码。由于它是实时转码,所以 只适合在开发环境使用。
  • babel-loader: 希望在 uglify 之前就加入 babel 处理,所以就有了babel 插入到构建工具内部这样的需求。
  • babel-plugin-component:按需引入需要的组件,以达到减小项目体积的目的。
{

  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui", "styleLibraryName": "theme-chalk"
      },
      { "libraryName": "mint-ui", "style": true }
    ]
  ]
}

babel-plugin-component部分源码:

var _options = options,
    _options$libDir = _options.libDir,//这是组件所在根目录下的路径element-ui/lib/
    libDir = _options$libDir === void 0 ? 'lib' : _options$libDir,
    _options$libraryName = _options.libraryName,//这是ui库的名字--elementui
    libraryName = _options$libraryName === void 0 ? defaultLibraryName : _options$libraryName,
    _options$style = _options.style,
    style = _options$style === void 0 ? true : _options$style,
    styleLibrary = _options.styleLibrary,//这是引入组件时,所需要引入对应组件样式的配置对象
    _options$root = _options.root,
    root = _options$root === void 0 ? '' : _options$root,
    _options$camel2Dash = _options.camel2Dash,
    camel2Dash = _options$camel2Dash === void 0 ? true : _options$camel2Dash;
    
    var styleLibraryName = options.styleLibraryName;//这是组件所需样式的路径(相对于上面的lib)
    var _root = root;
    var isBaseStyle = true;
    var modulePathTpl;
    var styleRoot;
    var mixin = false;
    var ext = options.ext || '.css';//这是加载样式的后缀,默认css

所以,如果在.babelrc文件配置过styleLibraryName属性的,就不需要在全局引入element的css样式。

polyfill和runtime差别

背景:
Babel 把 Javascript 语法 分为 syntax 和 api:

api指那些我们可以通过 函数重新覆盖的语法 ,类似 includes,map,includes,Promise,凡是我们能想到重写的都可以归属到 api

syntax像箭头函数,let,const,class, 依赖注入Decorators,等等这些,我们在 Javascript 在运行是无法重写的,想象下,在不支持的浏览器里不管怎么样,你都用不了 let 这个关键字

babel 默认只转换syntax层语法,而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。对于API, Babel 把这个放在了 单独放在了 polyfill 这个模块处理。

polyfill(垫片)

使用时,在所有代码运行之前增加 require('babel-polyfill')。或者更常规的操作是在 webpack.config.js 中将 babel-polyfill 作为第一个 entry。

babel-polyfill主要有两个缺点:

  • 使用babel-polyfill会导致打出来的包非常大,因为 babel-polyfill 是一个整体,把所有方法都加到原型链上。
  • babel-polyfill会污染全局变量,给很多类的原型链上都作了修改,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。

改进方案:
使用useBuiltIns设置:取值可以是usage(按需引入), entry(全部引入,会根据browserlist 过滤出需要的polyfill)和false。默认值为falseBabel 7该配置才会生效

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage"
      }
    ]
  ]
}

使用上面的配置,打包后如下:
polyfill打包

runtime

为了解决babel-polyfill会污染全局空间的问题,有了babel-plugin-transform-runtimebabel-plugin-transform-runtime的目的是为您的代码创建沙盒环境,不会污染全局空间和内置对象原型。它适用于开发供其他人使用的库,或者如果您无法准确控制代码运行的环境。

{
  "presets": [
    [
      "env"
    ]
  ],
  "plugins": ["transform-runtime"]
}

babel配置如上,打包后的结果:
runtime例子

例如,你的代码中用了Promise,babel-plugin-transform-runtime会自动重写你使用Promise的代码,转换为使用babel-runtime导出(export)的Promise-like对象。 所以plugin-transform-runtime一般用于开发(devDependencies),而runtime自身用于部署的代码(dependencies),两者配合来一起工作。

babel-runtime内部集成了:

  • core-js: 转换一些内置类 (Promise, Symbols等等) 和静态方法 (Array.from 等)。它几乎包含了所有 JavaScript 最新标准的垫片。
  • regenerator:作为 core-js 的拾遗补漏,主要是 generator/yield 和 async/await 两组的支持。
  • helpers: babel的辅助函数,例如 toArray函数, jsx转化函数。

babel-runtime缺点:
babel-plugin-transform-runtime不支持实例方法 (例如 [1,2,3].includes(1)),这时还是需要使用babel-polyfill

*总结:Babel 只是转换syntax层语法,所有需要 @babel/polyfill 来处理API兼容,又因为 polyfill 体积太大,所以通过 preset的 useBuiltIns 来实现按需加载,再接着为了满足 npm 组件开发的需要 出现了@babel/runtime 来做隔离。*

Babel插件开发流程

开发流程

Babel的插件模块需要你暴露一个function,function内返回visitor,visitor是对各类型的AST节点做处理的地方。类似这样:

module.export = function(babel){
  return {
    visitor: {
    //   ...你的插件代码
    }
  }
}

通过AST explorer知道要处理的AST节点,如下:
ast示例

基本流程:

  1. 定义需要转换的节点
BinaryExpression(path) {
  ...
}
  1. 在node节点上找到替换的节点(通过babel-types)
var t = require('babel-types');
if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
  ...
}
  1. 创建用来替换的节点(通过babel-types),replaceWith(替换)
var t = require('babel-types');
...
path.replaceWith(t.numericLiteral(result));

【参考】
前端工程师需要了解的 Babel 知识
一口(很长的)气了解 babel
Babel学习系列4-polyfill和runtime差别(必看)
@babel/polyfill 与 @babel/plugin-transform-runtime 详解
你真的会用 Babel 吗?
了解 Babel 6 & 7 生态
深入浅出的webpack构建工具---babel之配置文件.babelrc(三)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant