六、Babel的深入解析

2021-04-27  本文已影响0人  AShuiCoder

1 为什么需要babel?

事实上,在开发中我们很少直接去接触babel,但是babel对于前端开发来说,目前是不可缺少的一部分。原因如下:

那么,Babel到底是什么呢?
Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript。包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等。

2 Babel命令行使用

babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配置来单独使用。如果我们希望在命令行尝试使用babel,需要安装如下库:

yarn add @babel/cli @babel/core -D

使用babel来处理我们的源代码:

npx babel src --out-dir dist

src:源文件的目录。
--out-dir:指定要输出的文件夹dist。

2.1 插件的使用

比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件

yarn add  @babel/plugin-transform-arrow-functions -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions

const转成 var:

yarn add  @babel/plugin-transform-block-scoping -D 
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping
,@babel/plugin-transform-arrow-functions

2.3 Babel的预设preset

但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset),后面我们再具体来讲预设代表的含义。

yarn add @babel/preset-env -D
npx babel src --out-dir dist --presets=@babel/preset-env

3 Babel的底层原理

babel是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?
从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
就是编译器,事实上我们可以将babel看成就是一个编译器。Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码。

3.1 babel编译器执行原理

Babel的执行阶段:


image.png

当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:


image.png

以下面代码为例:

const name = "coderwhy";
const foo = (name) => console.log(name);
foo(name);

首先,babel通过此法分析生成tokens数组,生成结构如下:

[
  {
      "type": "Keyword",
      "value": "const"
  },
  {
      "type": "Identifier",
      "value": "foo"
  },
  {
      "type": "Punctuator",
      "value": "="
  },
  {
      "type": "Punctuator",
      "value": "("
  },
  {
      "type": "Identifier",
      "value": "name"
  },
  {
      "type": "Punctuator",
      "value": ")"
  },
  {
      "type": "Punctuator",
      "value": "=>"
  },
  {
      "type": "Identifier",
      "value": "console"
  },
  {
      "type": "Punctuator",
      "value": "."
  },
  {
      "type": "Identifier",
      "value": "log"
  },
  {
      "type": "Punctuator",
      "value": "("
  },
  {
      "type": "Identifier",
      "value": "name"
  },
  {
      "type": "Punctuator",
      "value": ")"
  },
  {
      "type": "Punctuator",
      "value": ";"
  },
  {
      "type": "Identifier",
      "value": "foo"
  },
  {
      "type": "Punctuator",
      "value": "("
  },
  {
      "type": "String",
      "value": "\"coderwhy\""
  },
  {
      "type": "Punctuator",
      "value": ")"
  },
  {
      "type": "Punctuator",
      "value": ";"
  }
]

其次,babel通过语法分析(parsing)生成AST(抽象语法树):

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "foo"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "id": null,
            "params": [
              {
                "type": "Identifier",
                "name": "name"
              }
            ],
            "body": {
              "type": "CallExpression",
              "callee": {
                "type": "MemberExpression",
                "computed": false,
                "object": {
                  "type": "Identifier",
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "name": "log"
                }
              },
              "arguments": [
                {
                  "type": "Identifier",
                  "name": "name"
                }
              ]
            },
            "generator": false,
            "expression": true,
            "async": false
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "foo"
        },
        "arguments": [
          {
            "type": "Literal",
            "value": "coderwhy",
            "raw": "\"coderwhy\""
          }
        ]
      }
    }
  ],
  "sourceType": "script"
}

接着,babel遍历AST后进行访问操作,并通过相应的插件转成新的AST:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "foo"
          },
          "init": {
            "type": "FunctionExpression",
            "id": {
              "type": "Identifier",
              "name": "foo"
            },
            "params": [
              {
                "type": "Identifier",
                "name": "name"
              }
            ],
            "body": {
              "type": "BlockStatement",
              "body": [
                {
                  "type": "ReturnStatement",
                  "argument": {
                    "type": "CallExpression",
                    "callee": {
                      "type": "MemberExpression",
                      "computed": false,
                      "object": {
                        "type": "Identifier",
                        "name": "console"
                      },
                      "property": {
                        "type": "Identifier",
                        "name": "log"
                      }
                    },
                    "arguments": [
                      {
                        "type": "Identifier",
                        "name": "name"
                      }
                    ]
                  }
                }
              ]
            },
            "generator": false,
            "expression": false,
            "async": false
          }
        }
      ],
      "kind": "var"
    },
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "foo"
        },
        "arguments": [
          {
            "type": "Literal",
            "value": "coderwhy",
            "raw": "\"coderwhy\""
          }
        ]
      }
    }
  ],
  "sourceType": "script"
}

最后生成目标代码啦:

var name = "coderwhy";

var foo = function foo(name) {
  return console.log(name);
};

foo(name);

4. webpack使用babel

4.1 babel-loader

在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用的,比如在webpack中。
那么我们就需要去安装相关的依赖:

yarn add  babel-loader @babel/core -D

我们可以设置一个规则,在加载js文件时,使用我们的babel:
webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  }
}

4.2 指定使用的插件

我们必须指定使用的插件才会生效:
webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [
              '@babel/plugin-transform-arrow-functions',
              '@babel/plugin-transform-block-scoping'
            ]
          }
        }
      }
    ]
  }
}

4.3 babel-preset

如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。
安装preset-env:

yarn add @babel/preset-env -D

配置:
webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env'
            ]
          }
        }
      }
    ]
  }
}

4.4 设置目标浏览器 browserslist

我们最终打包的JavaScript代码,是需要跑在目标浏览器上的,那么如何告知babel我们的目标浏览器呢?

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: 'chrome 87'
              }]
            ]
          }
        }
      }
    ]
  }
}

target的优先级高于browserslist,也就是说,当设置target后,browserlist的配置会失效。但我们开发中推荐在browserlist里配置,这样能和postcss同步。

4.5 Stage-X的preset

要了解Stage-X,我们需要先了解一下TC39的组织:

TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage:

4.1.1 Babel的Stage-X设置

在babel7之前(比如babel6中),我们会经常看到这种设置方式:

image.png

5 Babel的配置文件

像之前一样,我们可以将babel的配置信息放到一个独立的文件中,babel给我们提供了两种配置文件的编写:

它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等);

在项目根目录新建babel.config.js文件

module.exports = {
  presets: [
    ['@babel/preset-env']
  ]
}

然后移除webpack.config.js的babel配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  }
}

6 认识polyfill

Polyfill是什么呢?

为什么时候会用到polyfill呢?

6.1 如何使用polyfill?

babel7.4.0之前,可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了。


image.png

babel7.4.0之后,可以通过单独引入core-js和regenerator-runtime来完成polyfill的使用:

 yarn add core-js regenerator-runtime --save

webpack.configf.js中排除node_modules的文件。因为很多第三方包内部已经实现了polyfill了,会引起冲突。

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  }
}

配置babel.config.js
我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性:

useBuiltIns属性设置
useBuiltIns属性有三个常见的值

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3 // 默认值2版本,我们安装的是3
    }]
  ]
}
module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'entry',
      corejs: 3
    }]
  ]
}

main.js

import 'core-js/stable'; 
import 'regenerator-runtime/runtime'
new Promise (resolve => {
  resolve(2)
})

6.2 认识Plugin-transform-runtime(了解)

在前面我们使用的polyfill,默认情况是添加的所有特性都是全局的

image.png

安装

yarn add install @babel/plugin-transform-runtime -D

使用plugins来配置babel.config.js:
注意:因为我们使用了corejs3,所以我们需要安装对应的库:

image.png
module.exports = {
  presets: [
    ['@babel/preset-env', {
      // useBuiltIns: 'entry',
      // corejs: 3
    }]
  ],
  plugins: [
    ['@babel/plugin-transform-runtime', {
      corejs: 3
    }]
  ]
}

7 React的jsx支持

在我们编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的。
对react jsx代码进行处理需要如下的插件:

但是开发中,我们并不需要一个个去安装这些插件,我们依然可以使用preset来配置:

yarn add @babel/preset-react -D

babel.config.js

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'entry',
      corejs: 3
    }],
    ['@babel/preset-react']
  ],
}

8 TypeScript

在项目开发中,我们会使用TypeScript来开发,那么TypeScript代码是需要转换成JavaScript代码。

8.1 TypeScript的命令行使用

可以通过TypeScript的compiler来转换成JavaScript:

npm install typescript -g

另外TypeScript的编译配置信息我们通常会编写一个tsconfig.json文件:

tsc --init

之后我们可以运行 npx tsc来编译自己的ts代码

tsc ./index.ts

8.2 使用ts-loader

如果我们希望在webpack中使用TypeScript,那么我们可以使用ts-loader来处理ts文件:

yarn add ts-loader -D

配置ts-loader:
webpack.config.js

module.exports = {
  module: {
    rules: [
        test: /\.ts$/,
        exclude: /node_modules/,
        use: 'ts-loader'
      }
    ]
  }
}

8.3 使用babel-loader

除了可以使用TypeScript Compiler来编译TypeScript之外,我们也可以使用Babel:

我们来安装@babel/preset-typescript:

yarn add @babel/preset-typescript -D

webpack.config.js

module.exports = {
  entry: './src/main.ts', // ts入口文件
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  }
}

babel.config.js

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3
    }],
    ['@babel/preset-typescript']
  ],
}

8.4 ts-loader和babel-loader选择

那么我们在开发中应该选择ts-loader还是babel-loader呢?
使用ts-loader(TypeScript Compiler)

使用babel-loader(Babel)

那么在开发中,我们如何可以同时保证两个情况都没有问题呢?

8.5 编译TypeScript最佳实践

事实上TypeScript官方文档有对其进行说明:


image.png

也就是说我们使用Babel来完成代码的转换,使用tsc来进行类型的检查。
但是,如何可以使用tsc来进行类型的检查呢?

package.json

{
  "scripts": {
    "build": "webpack --config wk.config.js",
    "type-check": "tsc --noEmit",
    "type-check-watch": "tsc --noEmit --watch"
  }
}

*注意:必须有tsconfig.json文件

9 es-lint

ESLint是一个静态代码分析工具(Static program analysis,在没有任何程序执行的情况下,对代码进行分析),ESLint可以帮助我们在项目中建立统一的团队代码规范,保持正确、统一的代码风格,提高代码的可读性、可维护性;并且ESLint的规则是可配置的,我们可以自定义属于自己的规则;早期还有一些其他的工具,比如JSLint、JSHint、JSCS等,目前使用最多的是ESLint。

9.1 使用ESLint

安装

yarn add eslint -D

创建ESLint的配置文件:

npx eslint --init

执行检测命令:

npx eslint ./src/main.js

9.2 ESLint的配置文件解析

默认创建的环境如下:

module.exports = {
  env: {
    browser: true,
    commonjs: true,
    es2021: true,
  },
  extends: ['plugin:vue/essential', 'airbnb-base'],
  parserOptions: {
    ecmaVersion: 12,
    parser: '@typescript-eslint/parser',
  },
  plugins: ['vue', '@typescript-eslint'],
  rules: {
    // 0 => off
    // 1 => warn
    // 2 => error
    'no-unused-vars': 0,
    quotes: ['warn', 'single'],
    'no-console': 0,
    'import/no-extraneous-dependencies': 0,
  },
};

9.3 eslint-loader

安装

yarn add eslint-loader -D

配置
webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: ['babel-loader', 'eslint-loader'],
      }
    ],
  },
};

配置完成后,执行yarn build的时候如果有eslint错误就会抛出,阻止打包。

9.4 vscode插件eslint

但是如果每次校验时,都需要执行一次npm run eslint就有点麻烦了,所以我们可以使用一个VSCode的插件:ESLint


image.png

9.5 VSCode的Prettier插件

ESLint会帮助我们提示错误(或者警告),但是不会帮助我们自动修复,在开发中我们希望文件在保存时,可以自动修复这些问题;我们可以选择使用另外一个工具:prettier


image.png
image.png

10 加载Vue文件

安装vue

yarn add vue

安装相关依赖

yarn add vue-loader  vue-template-compiler -D

配置
webpack.config.js

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2
            }
          },
          "postcss-loader",
          "less-loader"
        ]
      },
      {
        test: /\.vue$/,
        use: "vue-loader"
      }
    ],
  },

  plugins: [
    new VueLoaderPlugin()
  ],
};

上一篇 下一篇

猜你喜欢

热点阅读