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 #1

Open
Kehao opened this issue Sep 10, 2019 · 0 comments
Open

关于Babel #1

Kehao opened this issue Sep 10, 2019 · 0 comments

Comments

@Kehao
Copy link
Owner

Kehao commented Sep 10, 2019

🐬 Babel编译转码的范围

Babel默认只转换新的JavaScript语法,而不转换新的API。 例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。 如果想使用这些新的对象和方法,则需要为当前环境提供一个垫片(polyfill)。

🐟 Babel的工作流:

输入字符串 -> @babel/parser parser -> AST -> transformer[s] -> AST -> @babel/generator -> 输出字符串

其中在 transformer[s] 阶段,就是使用plugins来转换代码的阶段。不同的plugin转换特定的代码,而preset是一组完成特定转换的plugins,比如babel-preset-es2017即包含syntax-trailing-function-commas | transform-async-to-generator两个plugin,用于支持ES2017的新特性。

plugin 分为 transform plugin(实际转换代码) 和 syntax-plugin(语法支持,即parse阶段),一般transform plugin包含对应的syntax plugin

以前常用的yearly presets(preset-es2015 | preset-es2016 | preset-es2017)在 Babel 7 中已经不推荐使用了,建议用 preset-env 代替。

🍥 主要的测试方法

// npm install @babel/core @babel/cli @babel/preset-env -D
// npm install @babel/polyfill -S
// npm install @babel/runtime-corejs2 -D

// 文件1:polyfill.js
// import 'babel-polyfill'
console.log([1, 2, 3].includes(2))
console.log(Object.assign({}, {a: 1}))
console.log(Array.from([1,2,3]))

//文件2: babel.config.js
const presets = [
    ["@babel/env", {
        targets: {
            node: '0.10.8',
            // node: 'current'
        },
        useBuiltIns: 'usage'
    }]
];

const plugins = [
    ["@babel/plugin-transform-runtime", {
        "corejs": 2, 
        "helpers": true,
        "regenerator": true,
        "useESModules": false
    }]
]


module.exports = { presets, plugins };
// npx babel ./polyfill.js -d dist 得到
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _from = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/from"));

var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));

// import 'babel-polyfill'
console.log([1, 2, 3].includes(2));
console.log((0, _assign.default)({}, {
  a: 1
}));
console.log((0, _from.default)([1, 2, 3]));

// node dist/polifill.js 得到
true
{ a: 1 }
[ 1, 2, 3 ]

🐠 babel编译代码的几种方式

  1. 直接执行es6代码
# babel-node 默认是加载了 babel-polyfill 的,所以各种新的API 都能用。
npm i -g babel-cli
#babel7的cli安装方式
npm install --save-dev @babel/core @babel/cli 
babel-node es6.js
  1. babel-register

Node中另一种直接执行ES6代码的方式是使用 babel-register,该库引入后会重写你的require加载器,让你的Node代码中require模块时自动进行ES6转码。例如在你的 index.js 中使用 babel-register:

// index.js
require('babel-register')
...
require('./abc.js') // abc.js可以用ES6语法编写,require时会自动使用babel编译
  1. babel命令
# 编译 example.js 输出到 compiled.js
babel example.js -o compiled.js

# 或 整个目录转码
# --out-dir 或 -d 参数指定输出目录
$ babel src --out-dir lib
# 或者
$ babel src -d lib
# -s 参数生成source map文件
$ babel src -d lib -s
  1. API调用babel实现源码编译
npm install babel-core --save-dev // 老版本的babel
npm install @babel/core --save-dev // babel7

var babel = require("@babel/core");
import { transform } from "@babel/core";
import * as babel from "@babel/core";

babel.transform("code();", options, function(err, result) {
  result.code;
  result.map;
  result.ast;
});
  1. 通过babel-loader调用babel

babel-loader 是无法独立存在运行的。在babel-loader的package.json里你会发现有个 peerDependencies,其中就定义了一个依赖项是webpack。peerDependencies依赖表示了一个模块所依赖的宿主运行环境(一般各种插件的包内会使用 peerDependencies 来表明自己的宿主依赖)。

{
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['es2015']
            }
          }
        ]
      }
    ]
  }
}

🐡 @babel/polyfill

babel-polyfill通过向全局对象和内置对象的prototype上添加方法来模拟完整的 ES2015+ 环境。

  • @babel/polyfill主要用于应用程序(如浏览器,babel-node), 而不是库/工具, 并且使用babel-node时,这个polyfill会自动加载。
  • @babel/polyfill包括 core-js2和regenerator runtime模块。
  • @babel/polyfill是一次性引入你的项目中的,并且同项目代码一起编译到生产环境。
  • 问题1: @babel/polyfill包含所有补丁,不管浏览器是否支持,也不管你的项目是否有用到,都全量引了, 在浏览器中,这些代码体积比较大。
  • 问题2: 会污染全局变量。像Map,Array.prototype.find这些就存在于全局空间中。
  • 新版本@babel/polyfill 加入了core-js3支持。

shim、polyfill所谓的垫片技术,是通过提前加载一个脚本,给宿主环境添加一些额外的功能。从而让宿主拥有更多的能力。例如可以基于JavaScript的原型能力,给Array.prototype增加额外的方法,就可以一定程度上让宿主环境拥有ES6的能力。除了对ES6+之外,我们还得根据项目情况,添加一些额外的shim或者polyfill。比如fetch、requestAnimationFrame 这种浏览器API,如果我们需要兼容IE8,还需要添加 ES5 shim来兼容更早的JS语法。

🦈 babel-runtime

babel转译代码的时候需要一些工具方法,这些方法默认都会加到用到他们的文件的开头,有时这些方法会很长。babel-runtime就是为了优化这种情况的,将这些工具方法抽离出来,不用每个文件都引入。

  • babel-runtime可以看作是一种库/工具,搭配babel-plugin-transform-runtime,一般在开发第三方类库时使用。
  • babel-runtime不会污染全局空间和内置对象原型。
  • 多次使用只会打包一次。
  • 弥补了babel-polyfill的缺点,达到了按需加载的效果。
  • 一般在开发第三方类库或工具时使用,一般放在'devDependencies'里。
  • babel-runtime的问题: 不能转码实例方法。
// 这只能通过 babel-polyfill来转译,因为 babel-polyfill 是直接在原型链上增加方法。
'!!!'.repeat(3);
'hello'.includes('h');

transform-runtime开启corejs的方案, preset-env的useBuiltIns设置为"usage", preset-env的useBuiltIns可以按需在全局进行polyfill,会require相应实例方法的垫片。

babel-plugin-transform-runtime的作用是分析我们的 ast,通过映射关系插入 babel-rumtime 中的垫片, runtime 编译器插件做了以下三件事:

  • 当你使用 generators/async 函数时,自动引入 babel-runtime/regenerator 。
  • 自动引入 babel-runtime/core-js 并映射 ES6 静态方法和内置插件。
  • 移除内联的 Babel helper 并使用模块 babel-runtime/helpers 代替。
console.log({ ...a });

// 编译后是:
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

console.log(_objectSpread({}, a));

// 然后我们会有很多文件,每个文件都引入一遍 helper 方法,会有很多冗余。所以我们通常会使用 @babel/plugin-transform-runtime 来复用这些 helper 方法。
// 在 .babelrc 里配置:
{
  "plugins": [
    "@babel/transform-runtime"
  ]
}
// 编译后是:
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));

console.log((0, _objectSpread2.default)({}, a));

🦐 补丁方案

// .babelrc的配置说明: 
{
  "presets": [
    // 预设, Babel 插件的组合套餐
    // 插件的执行顺序:
    // 插件在 Presets 前运行。
    // 插件顺序从前往后排列。
    // Preset 顺序是颠倒的(从后往前)
    "@babel/preset-react",
    // 支持react特有的一些语法,如jsx等
    ["@babel/preset-env", {
      "modules": false, 
       //该参数的含义是:启用将ES6模块语法转换为另一种模块类型/模块的导入导出方式。将该设置为false就不会转换模块。默认为 auto, 基本会是'commonjs'。
       //该值可以有如下:'amd' | 'umd' | 'systemjs' | 'commonjs' | auto | false
       //设置为false的原因: 以前我们需要使用babel来将ES6的模块语法转换为AMD, CommonJS,UMD之类的模块化标准语法,但是现在webpack都帮我做了这件事了,所以我们不需要babel来做,因此需要在babel配置项中设置modules为false,这里导成其他的模块类型也没问题,因为webpack的模块支持这些方式。
      "targets": {
        // tagets 指支持的运行环境
        // 运行环境: chrome, opera, edge, firefox, safari, ie, ios, android, node, electron
        // 如果不设置,就相当于声明了所有ES2015+的plugin
        "browsers": ["last 2 versions", "safari 7",  "chrome": 49], 
        // 支持每个浏览器最后两个版本和safari大于等于7版本所需的polyfill代码转换。
        // 支持chrome49所需的polyfill代码转换。
        "node": "current"
        // 如果通过Babel编译Node.js代码的话,可以设置 "target.node" 是 'current', 含义是 支持的是当前运行版本的nodejs。
      },
      "loose": false,
       // 含义是:允许它们为这个 preset 的任何插件启用"loose" 转换, 默认为false。
       // babel编译时,对class的属性采用赋值表达式,而不是Object.defineProperty(更简洁)
       // 默认情况下,当在 Babel 下使用模块(module)时,将导出(export)一个不可枚举的 __esModule 属性。 
      "useBuiltIns": "usage",
       // "usage" | "entry" | false, 默认是false
       // entry的含义是找到入口文件里引入的 @babel/polyfill,并替换为 targets 浏览器/环境需要的补丁列表。
       // usage指按需引入。
       // false指不动态添加polyfills,并且不转译 import "core-js" 和 import "@babel/polyfill",会引入全部的polyfills。
      "corejs": 2, 
       // 新版本的@babel/polyfill包含了core-js@2和core-js@3版本,所以需要声明版本,否则webpack运行时会报warning,此处使用core-js@2版本
      "debug": false,
      // 是否在转码时打印debug信息
      "include": [], // 总是启用的 plugins, 可以让babel加载指定名称的插件。
      "exclude": [],  // 强制不启用的 plugins,可以让babel去除指定名称的插件。
      "forceAllTransforms": false, // 强制使用所有的plugins,用于只能支持ES5的uglify可以正确压缩代码
    }]
  ],
  "plugins": [
    [ "transform-runtime", {
      // enables the re-use of Babel's injected helper code to save on codesize.
      "corejs": false, 
      // 默认false,或者数字:{ corejs: 2/3 },代表需要使用corejs的版本。
      // 如果是false则使用@babel/runtime,如果是2则使用@babel/runtime-corejs2,除了runtime中的helpers,另外含有Promise,Symbol等。
      "helpers": true, // 默认是true,是否替换helpers。
      "polyfill": false, // v7无该属性
      "regenerator": true, // 默认true,generator是否被转译成用regenerator runtime包装不污染全局作用域的代码。
      "useBuiltIns": false, // v7无该属性
      "useESModules": false, // 默认false,如果是true将不会用@babel/plugin-transform-modules-commonjs进行转译,这样会减小打包体积,因为不需要保持语义。
      "absoluteRuntime": false
    }],
    [ "import", { "libraryName": "antd", "style": true } ]
  ]
  "env": {
    "development":{
      "plugins": [ "dva-hmr" ]
    },
    "production": {}
  }
}
  • useESModules:如果是true将不会用@babel/plugin-transform-modules-commonjs进行转译,这样会减小打包体积,因为不需要保持语义。
//useESModules: false
exports.__esModule = true;

exports.default = function(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
};

//useESModules: true
export default function(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
  • useBuiltIns: 'usage':当每个文件里用到(需要polyfill的特性)时,在文件中添加特定的import语句。这可以保证每个polyfill的特性仅load一次。
/// input
var a = new Promise(); // a.js
var b = new Map(); // b.js
/// output
// a.js
import "core-js/modules/es6.promise";
var a = new Promise();
// b.js
import "core-js/modules/es6.map";
var b = new Map();
  • useBuiltIns: 'entry':替换import "@babel/polyfill" / require("@babel/polyfill")语句为独立的(根据环境)需要引入的polyfill特性的import语句。
// input
import "@babel/polyfill";
// output
import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
  • useBuiltIns: false,babel不会帮你处理任何polyfill的事,你必须手动处理。
  • 关于loose mode可参考Babel 6: loose模式

方案一: corejs2, @babel/preset-env + useBuiltins: entry + targets

  • babel-preset-env 能根据当前的运行环境,自动确定你需要的 plugins 和 polyfills。通过各个 es标准 feature 在不同浏览器以及 node 版本的支持情况,再去维护一个 feature 跟 plugins 之间的映射关系,最终确定需要的 plugins。
  • 用于替代原来的babel-preset-es20xx/latest(babel-preset-es2015/babel-preset-es2016/babel-preset-es2017)
// 1, 安装的需要的全部依赖。
// npm insall babel-loader@8 @babel/core @babel/preset-env -D
// npm install @babel/polyfill

// 2, 在js代码第一行import '@babel/polyfill',或在webpack的入口entry中写入模块@babel/polyfill。

// 3, 配置文件.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false, // 推荐
        "useBuiltIns": "entry", // 推荐
        "browsers": ["last 2 versions", "safari 7",  "chrome": 49]
        "corejs": 2, 
         // 新版本的@babel/polyfill包含了core-js@2和core-js@3版本,所以需要声明版本,否则webpack运行时会报warning,此处使用core-js@2版本
      }
    ]
  ],
  "plugins": []
}

方案二: corejs3

// 1, 安装的需要的全部依赖。
// npm insall babel-loader@8 @babel/core @babel/preset-env -D
// npm insall core-js regenerator-runtime

// 2, 在js代码第一行,或者入口文件中引入相应的polyfill
// import "core-js/stable"
// import "regenerator-runtime/runtime"

// 3, 配置文件.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "useBuiltIns": "entry", 
        "browsers": ["last 2 versions", "safari 7",  "chrome": 49]
        // https://babeljs.io/docs/en/babel-preset-env#usebuiltins
        // https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
        "corejs": {
          "version": 3, // 使用core-js@3
          "proposals": true,
        },
        "debug": false
      }
    ],
    "@babel/preset-react",
    "@babel/preset-flow"
  ],
  "plugins": [
    "dva-hmr",
    // 动态导入, 异步加载语法编译插件
    "@babel/plugin-syntax-dynamic-import",

    // 可以使用 export v from 'mod'
    "@babel/plugin-proposal-export-default-from",

    // 装饰器
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],

    // 类属性, 实例属性
    [
      "@babel/plugin-proposal-class-properties",
      {
        "loose": true
      }
    ],
    // antd支持
    [
      "import",
      {
        "libraryName": "antd",
        "style": true
      }
    ]
  ]
}

关于使用了@babel/preset-env后,是否还需要使用transform-runtime的问题

基本是不需要的,参考creeperyang/blog#25 里的评论。

🎏 bable7 理解

  • core-js(v2)这个库有两个核心的文件夹,分别是 library 和 modules。@babel/runtime-corejs2 使用 library 这个文件夹,@babel/polyfill 使用 modules 这个文件夹。

library 使用 helper 的方式,局部实现某个 api,不会污染全局变量;modules 以污染全局变量的方法来实现 api;

  • @babel/polyfill 和@babel/runtime-corejs2 都使用了 core-js(v2)这个库来进行 api 的处理。
  • @babel/polyfill: 引用两个包 core-jsregenerator-runtime
  • @babel/runtime: 引用一个包regenerator-runtime, 它的devDependencies引用@babel-helpers
  • @babel/runtime-corejs2: 引用两个包core-jsregenerator-runtime, devDependencies引用@babel/helpers
  • regenerator-runtime 用来实现generator和async/await
  • @babel-helpers 是babel需要的一些函数,默认是放在每个文件的头部的,现在将其抽出来放到一起供各个文件引用这些函数,可以减小代码体积。
  • runtime-corejs2 是polyfill和runtime的并集,所以只需要引入@babel/runtime-corejs2就可以替代@babel/polyfill和@babel/runtime了,然后要配置一下["@babel/plugin-transform-runtime", {"corejs": 2}]

🎣 如何为 Babel 创建插件

有用的资源

Babel中文文档

各个平台对ES6,ES7等的支持情况

Babel 手册

babel-polyfill VS babel-runtime VS babel-preset-env

Polyfill 方案的过去、现在和未来

强悍的Babel

了解 Babel 6 & 7 生态

@babel/polyfill 还是 @babel/plugin-transform-runtime

corejs3 的更新

@Kehao Kehao changed the title 关于Babel的一切 关于Babel Sep 20, 2019
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