QQQQQCY

QQQQQCY 查看完整档案

填写现居城市  |  填写毕业院校百度  |  前端开发 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

QQQQQCY 发布了文章 · 9月1日

「完全理解」如何统一项目中的代码风格

image

对代码进行格式化,是程序员的日常。格式化很爽,但是配置相关的规则、安装对应插件却很烦。与其每次碰到问题再慢慢排查,不如一次性融会贯通。

一、代码风格是什么(Code Conventions)

我们可以按照修改后是否会影响程序运行结果来把风格分为两个部分。例如下面的代码

var a= 1;
if (a=='1') console.log(true )

1. Format 格式 -> format 格式化(首字母大小写以便区分)

格式,简单来说就是变量后面是否需要空格,语句后面是否需要分号等,这一类无论如何改动,都不会影响代码运行结果的部分。对格式进行改变叫格式化

使用如下规则对代码进行 format:

  • 语句之后必须接分号
  • 括号头尾需要 1 个空格
  • 运算符前后需要 1 个空格
var a = 1;
if ( a == '1' ) console.log( true );

2. Source 源码 -> (source action 源动作)

源码,与格式相对的,是被修改之后会切实影响代码运行结果的部分。对源码进行改变叫源动作

使用如下规则对代码进行 source action:

  • 未发生改变的变量必须使用 const 定义
  • 必须使用 ===,禁止使用 ==
const a = 1;
if ( a === '1' ) console.log( true );

格式加源码,构成了代码风格

定义来源:vscode 任意选中一段代码之后右键

image.png

二、风格工具

风格当然是有各种各样的,有的工具可以设定自己的风格,有的工具强制使用特定风格。为了便于理解,本文只介绍业界最主流的工具(也很少有人用其他的)

Prettier

image.png

Prettier 专注于 Format,基本支持了前端项目相关的所有文件类型,包括但不限于

  • JavaScript
  • JSX
  • Vue
  • TypeScript
  • CSS、Less、SCSS
  • HTML
  • JSON
  • Markdown

Prettier 最大的特点是独裁,在自身规定的几百上千条 Format 规则中,可配置的仅有 20 条。这也导致其他工具与 Prettier 并存时,都需要迁就 Prettier 的规则

ESLint

image.png

ESLint 是目前所有 Lint 工具中使用最广泛的(前端范围内)。

计算机科学中,lint 是一种工具程序的名称,它用来标记源代码中,某些可疑的、不具结构性(可能造成bug)的段落。它是一种静态程序分析工具,最早适用于C语言,在UNIX平台上开发出来。后来它成为通用术语,可用于描述在任何一种计算机程序语言中,用来标记源代码中有疑义段落的工具。
—— wiki

ESlint 可配置的范围,包括 Format 和 Source,但是仅限于可以被转换成 AST 的代码,对于 CSS、HTML 等都无能为力(这也是为什么需要同时使用 ESLint 和 Prettier 的原因)

stylelint

image.png

前端开发基本等同于 HTML、JavaScript 和 CSS 开发。通过 Prettier + ESLint,对于风格中的 Format 已经完全统一,JavaScript 中的 Source 也被统一。所以现在还剩下基本可以忽略的 HTML 和 CSS 中的 Source 部分

我们选择使用 stylelint 来处理 CSS 相关 Source

这样基本可以限定前端开发过程中的所有风格

HTMLJavaScriptCSS
Formatprettierprettierprettier
SourceESlintstylelint

三、使用说明

通常项目中使用风格工具,有三种途径

  • A - 编译器引入相关插件
  • B - webpack 引入相关 loader、plugin
  • C - 命令行直接执行

先模拟一下项目中开发需求的流程

  1. 拉取项目代码,安装依赖,初始化项目配置风格设置,命令行格式化所有代码(C)
  2. 使用 webpack 编译项目

    1. 窗口中及时提示风格问题并自动修复(A)
    2. 控制台及时提示风格问题,不修正情况下打包失败(B)
  3. 使用 git 提交,触发钩子,检查风格通过之后才允许提交(C)

任何流程没有控制住,都有可能造成设定以外的代码风格

无论是通过函数、命令行或者插件来使用,风格化的过程基本都可以抽象为如下公式。

Result = format(sourceAction(src, config)) = Tool(src, config)
// 最终风格 = 格式化(源动作(源码,配置)) = 工具(源码,配置)

源码为常量。只要保证工具版本统一,配置相同,则不同途径均也可以得出相同的结果。规则和工具的选择从来不是难点,难点在于保证工具、配置的统一

接下来介绍如何保证 A、B、C 三种途径的统一

A - 编译器引入相关插件

工具

ESlint: The extension uses the ESLint library installed in the opened workspace folder. If the folder doesn't provide one the extension looks for a global install version.
Prettier: This extension will use prettier from your project's local dependencies (recommended)...
stylelint: The extension uses the stylelint library installed in the opened workspace folder...

根据各个工具的官方插件文档说明,插件会优先使用项目中的对应库 node_modules  ,如果不存在则使用全局库。

配置

ESLint

ESLint 插件目前可以指定配置文件,如果不指定会读取 package.json 同级的配置文件

eslint.options: options to configure how ESLint is started using the ESLint CLI Engine API. Defaults to an empty option bag. An example to point to a custom .eslintrc.json file is:
{
  "eslint.options": { "configFile": "C:/mydirectory/.eslintrc.json" }
}

Prettier

Prettier 的配置可以来源于三处,按优先级排列

  1. Prettier configuration file
  2. .editorconfig
  3. Visual Studio Code Settings (Ignored if any other configuration is present)

实际使用中,使用第一个  package.json 同级的配置文件即可

stylelint

与 Prettier 类似

B - webpack 引入相关 loader、plugin

const StyleLintPluginfrom = require('stylelint-webpack-plugin');
//...  
{
    test: /\.(j|t)sx?$/,
        use: [
          {
            loader: 'babel-loader',
          },
          {
            loader: 'eslint-loader',
            options: {
                            configFile: getWorkSpacePath('.eslintrc.js'),
            }
          },
        ],
  }
//...
plugins: [
    new StyleLintPluginfrom({
      configFile: getWorkSpacePath('.stylelintrc.js'),
    })
]
//...

工具

所有库都需要安装在项目的 node_modules 中, eslint-loader 同时依赖于 eslint 库(stylelint-webpack-plugin 类似)

配置

所有工具基本都可以在 option 中指定配置文件

C - 命令行直接执行

工具

首先需要在 node_modules 中安装有对应的库,然后在 package.json 中设置对应脚本

{
  //...
    "scripts": {
        "eslint": "eslint",
        "prettier": "prettier",
        "stylelint": "stylelint",
    },
  //...
}

之后可以在命令行中使用

yarn eslint --fix
-c, --config path::String      Use this configuration, overriding .eslintrc.* config options if present

yarn prettier --write
--config ./my/.prettierrc

yarn stylelint --fix
--config ./my/.stylelintrc

配置

所有工具在使用均可以指定配置文件

所以

  • 工具统一等价于把相关依赖全部安装到项目 **node_modules**,保证不使用全局依赖
  • 配置统一等价于使用统一的配置文件。由于部分插件不能指定路径,只会默认读取 node_modules 同级,所以建议所以配置文件都放在这一层

四、实战

首先引入 Prettier,我们所有的 format 都使用 Prettier 来完成。

在项目中接入 Prettier

yarn add prettier -D

1. 安装插件

安装官方推荐的编译器插件 Prettier - Code formatter(此处只考虑 VSCode),设置工作区的 .vscode/setting.json

切记一定要关闭其他带有格式化功能的插件,否则会发生冲突。特别是使用 VUE 框架时,记得关闭 vetur 的格式化。同时记得检查一下全局范围的 setting.json,注释掉所有 format 相关的配置

{ 
    // 保存时自动 Format
  "editor.formatOnSave": true,
    // 关闭 vetur 格式化
  "vetur.format.enable": false,
}

2. 推荐插件

为了告诉其他协同开发的同事,安装正确的 Prettier 插件,添加插件名在 .vscode/extensions.json 中,同时把其他容易干扰格式化的插件设置为不推荐。

这样其他人在首次安装项目的同时也会收到对应的提示了。

{
  "recommendations": [
    "esbenp.prettier-vscode",
  ],
  "unwantedRecommendations": ["hookyqr.beautify"]
}

 

3. 配置文件

package.json 同级创建 Prettier 的配置文件

// prettier.config.js or .prettierrc.js
module.exports = {
  trailingComma: "es5",
  tabWidth: 4,
  semi: false,
  singleQuote: true,
};

git 提交本次修改(尤其是 .vscode 文件夹)。之后任何人开发这个项目,只要安装了正确的插件,写出来的代码风格肯定都是统一的了

4. 限制提交

假设有一个同事头铁,就是不正确安装插件和配置编译器。他提交代码的风格就无法得到统一,所以需要再增加一层保险,在 git commit 时候需要校验所有改动过的文件,对这些文件使用 prettier --write 。

git commit 时触发、校验所有改动过的文件。这两个功能分别是两个插件

  • husky 

它能运行Githooks诸如 Pre-commit、pre-receive 和 post-receive。

用以在各个阶段触发不同校验,一般配合各种 lint 使用

  • lint-staged 

当项目文件比较多的时候,如果每次改动都对所有文件进行校验。势必导致等待时间变长。 解决上面的痛点就需要使用 lint-staged。它只会校验你提交或者说你修改的部分内容。

yarn add husky lint-staged -D

package.json 中添加字段

 "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    // src 目录下任意文件有改动,都使用 prettier --write 格式化之后再提交
    "src/**/*.*": [
      "prettier --write",
      "git add"
    ]
  }

这样基本就能保证项目的万无一失了(一失在于开发者删除 husky、lint-stage 配置🤦‍♂️)

在项目中接入 ESLint、stylelint

Prettier 只提供了风格中 Format 的部分,对于 Source,还需要接入其他工具。

  • ESLint:处理 JavaScript 相关(TypeScript、JSX ...)的 Source action
  • stylelint:处理 CSS 相关 (Less、SCSS) 的 Source action

流程基本和接入 Prettier 一样

1. 安装、推荐插件

安装 ESLint stylelint,并修改 setting.json

yarn add eslint stylelint -D

注意 formatOnSavecodeActionsOnSave 两者的区别。之前说过,由 Prettier 进行所有的 format,然后再针对不同类型的文件使用各种的 Soruce action

相关讨论:https://github.com/microsoft/vscode/issues/87096#issuecomment-567750791
{ 
  // 保存时自动 Format
  "editor.formatOnSave": true,
    
  // 保存时自动 Source action
  "editor.codeActionsOnSave": {
    "source.fixAll": true, // 开启所有工具的 source action
    //"source.fixAll.eslint": true, // 开启 ESLint 的 source action
    //"source.fixAll.stylelint": true, // 开启 stylelint 的 source action
  },
  "vetur.format.enable": false,
}

修改 extensions.json

{
  "recommendations": [
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "stylelint.vscode-stylelint"
  ],
  "unwantedRecommendations": ["hookyqr.beautify"]
}

2. 配置文件

package.json 同级创建 ESlint、stylelint 的配置文件,具体如何设置此处就不说明了。要注意的就是一定要给 Prettier 的设置让位
**
给 ESLint、stylelint 安装兼容 prettier 的插件 

yarn add eslint-config-prettier stylelint-config-prettier -D
// .eslintrc.js
module.exports = {
  //...
  env: {
    browser: true,
    es6: true,
  },
  
  extends: ["eslint:recommended", "prettier"],
  plugins: [],
  rules: {},
  //...
};
// .stylelintrc.js
module.exports = {
  //...
  extends: ["stylelint-config-prettier"],
  plugins: [],
  rules: {},
  //...
};

3. 编译校验

很多 Source 问题会要求用户手动更改,所以需要及时提示。插件虽然会提示,但不会影响代码编译结果,所以需要在编译过程中校验

详见之前的 B - webpack 引入相关 loader、plugin 段落

4. 限制提交

修改 package.json

 "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    // src 目录下任意文件有改动,都使用 prettier --write 格式化之后再提交
    "src/**/*.js": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ],
    "src/**/*.css":[
      "stylelint --fix",
      "prettier --write",
      "git add"
    ]
  }

至此,风格统一相关的所有流程都已经覆盖到了!

附录

查看原文

赞 0 收藏 0 评论 0

QQQQQCY 发布了文章 · 8月28日

「完全理解」如何配置项目中的 Babel

image.png

Babel,已经是每个项目都必不可少的依赖了。不过大多数同学可能并没有自己配置 Babel 的经验和机会

其实 Babel 配置并没有很难,只要了解了配置文件中的几个参数和所需依赖的作用,基本就等于完全掌握了 Babel 的使用

一、启动 Babel


Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中

简单说就是源码通过 Babel 处理后会得到向后兼容的代码。我们可以按照这个逻辑写出最简单的 Babel 用例

// 安装 babel 核心库
yarn add @babel/core
const babel = require('@babel/core')

babel.transform(`
        const fn=()=>console.log(1);
        fn();
    `, {}, (err, result) => {
    console.log(result.code);
});

使用 Node.js 执行这个 js 文件之后得到的输出如下

const fn = () => console.log(1);
fn();

可以发现待转化的源码没有任何变化。因为 @babel/core 的作用只是用来解析源码、把 js 代码分析成 ast,方便各个插件分析语法进行相应处理

而且我们实际使用过程中也不会直接这么用,一般会基于构建工具做一层封装,例如使用 webpack 时的 babel-loader

为了后续调用 babel 时更加直观,我们使用相应的 cli 库 @babel/cli

yarn add @bable/cli

之后随便新建个 js 文件

// src/index.js

const fn = () => console.log(1);
fn();

然后在根目录里执行命令即可

./node_modules/.bin/babel src --out-dir lib

参数的含义是把 src 里的代码用 @babel/core 处理并输出到 lib 文件夹中,所以执行成功我们可以看到输出后的代码

当然这样的执行方式太丑陋了,我们选择在 package.json 里添加 scripts

{
  ...
  "scripts": {
      "babel":"babel src --out-dir lib"
   },
  ...
}
yarn babel

由于只是换了一种执行方式,所以输出的 js 当然还是和之前一样,不会有任何转换

// lib/index.js

const fn = () => console.log(1);
fn();

二、@babel/preset-env


2.1 Plugins 插件

@babel/core 我们已经测试过,不会转化任何源码。所以为了能正确转化,我们必须使用对应的 plugins

Babel 推崇功能的单一性,就是每个插件的功能尽可能的单一。比如想使用 ES6 的箭头函数,需要对应的转化插件

yarn add @babel/plugin-transform-arrow-functions

再执行 babel

yarn babel --plugins=@babel/plugin-transform-arrow-functions

终于得到了转化后的代码

// lib/index.js

const fn = function () {
  return console.log(1);
};

fn();

不过这么写太丑了,我们直接在根目录新建一个 babel.config.js

// babel.config.js

module.exports = {
  plugins: [
    "@babel/plugin-transform-arrow-functions"
  ]
};

执行命令,Babel 会自动找到根目录中的 config 文件并使用。得到的转化文件和之前一样

yarn babel

2.2 Presets 预设

转化箭头函数需要一个 Plugin,而 const 需要另外的。我们不可能一个个的设置所有的 Plugin

这时候就需要 Presets,可以简单理解为它是一堆 Plugin 的组合。常见的 Preset 如下:(前两个已弃用,了解一下即可)

2.2.1 @babel/preset-stage-xxx

@babel/preset-stage-xxx 是 ES 在不同阶段语法提案的转码规则而产生的预设,随着被批准为 ES 新版本的组成部分而进行相应的改变(例如 ES6/ES2015)

提案分为以下几个阶段:

  • stage-0 - 设想(Strawman):只是一个想法,可能有 Babel 插件,stage-0 的功能范围最广大,包含stage-1 , stage-2 以及 stage-3 的所有功能
  • stage-1 - 建议(Proposal):这是值得跟进的
  • stage-2 - 草案(Draft):初始规范
  • stage-3 - 候选(Candidate):完成规范并在浏览器上初步实现
  • stage-4 - 完成(Finished):将添加到下一个年度版本发布中

2.2.2 @babel/preset-es2015

preset-es2015 是仅包含 ES6 功能的 Babel 预设

实际上在 Babel7 出来后上面提到的这些预设 stage-x,preset-es2015 都可以废弃了,因为 @babel/preset-env 出来一统江湖了

2.2.3 @babel/preset-env

前面两个预设是从 ES 标准的维度来确定转码规则的,而 @babel/preset-env 是根据浏览器的不同版本中缺失的功能确定代码转换规则的,在配置的时候我们只用配置需要支持的浏览器版本就好了,@babel/preset-env 会根据目标浏览器生成对应的插件列表然后进行编译:

// babel.config.js

module.exports = {
 presets: [
   ["@babel/preset-env", {
     targets: {
       browsers: ["last 10 versions", "ie >= 9"]
     }
   }],
 ],
  // plugins: ["@babel/plugin-transform-arrow-functions"]
};

执行命令之后,会得到箭头函数和 const 都被转化过的代码

//lib/index.js

"use strict";

var fn = function fn() {
  return console.log(1);
};

fn();

2.2.4 @babel/preset-react

用来转化 jsx https://babeljs.io/docs/en/babel-preset-react

2.2.5 @babel/preset-typescript

用来转化 ts https://babeljs.io/docs/en/babel-preset-typescript

2.3 执行顺序

插件的排列顺序很重要。

如果两个转换插件都将处理源码的某个代码片段时,转化将根据 Plugins 或 Presets 的排列顺序依次执行

  • Plugins 在 Presets 前运行
  • Plugin 顺序从前往后排列
  • Preset 顺序是颠倒的(从后往前)

三、@babel/polyfill

看起来我们的 babel 配置好像已经比较完善了。我们在源码上再加两行代码并转化试试

// src/index.js

const fn = () => console.log(1);
fn();
+ const pro = new Promise()
+ const isIncludes = [1, 2, 3].includes(2)
//lib/index.js

"use strict";

var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);

可以看到 Promise 和 includes 均没有被转化

有理由怀疑可能是 targets 设置过大,不过实际上就算调整到 ie 6,新加的两行代码也不会被转化

3.1 syntax 与 api

这是由于 Babel 把 Javascript 语法 分为 syntax 和 api

syntax 句法

类似箭头函数、let、const、class 等在 JavaScript 运行时无法重写的部分,就是 syntax 句法

api 方法

类似 Promise、includes 等可以通过函数重新覆盖的语法都可以归类为 api 方法。而且方法本身还分为两类

  • 方法 - Promise,Object.entries 等可以直接调用的
  • 实例方法 - [1, 2, 3].includes 等绑定在实例上的

而之前我们的源码部分只转化了 syntax 部分。 api 部分则需要借助另外的库

3.2 core-js 与 regenerator-runtime/runtime

转化 api 的思路很简单。在全局上下文下添加一个同名 api 即可

这些 api 都在 @babel/polyfill 中,所以先安装

yarn add @babel/polyfill

修改配置如下并转化源码(配置后续再说明)

// babel.config.js

module.exports = {
  "presets": [
    ["@babel/preset-env", {
+      useBuiltIns: "usage",
+      corejs: 3,
      targets: {
        browsers: ["last 10 versions", "ie >= 9"]
      }
    }],
  ],
  // plugins: ["@babel/plugin-transform-arrow-functions"]
};

可以发现我们的代码已经引入了 promise 和 includes 相关的垫片了

// lib/index.js

"use strict";

require("core-js/modules/es.array.includes");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

// src/index.js
var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);

如果使用 await/async 语法

// src/index.js

const asyncFn = async () => { }

会发现在转化句法的同时还引入了 regenerator-runtime 库

// dist/index.js

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("regenerator-runtime/runtime");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

// src/index.js
var asyncFn = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

  return function asyncFn() {
    return _ref.apply(this, arguments);
  };
}();

其实 @babel/polyfill 本质就是集成了这两个库

core-js

core-js 是用于 JavaScript 的组合式标准化库,它包含 es5 (e.g: object.freeze), es6 的 promise,symbols, collections, iterators, typed arrays, es7+提案等等的 polyfills 实现。也就是说,它几乎包含了所有 JavaScript 最新标准的垫片

// 比如,只不过需要单个引用
require('core-js/array/reduce');
require('core-js/object/values');

regenerator-runtime/runtime

它是来自于 facebook 的一个库,链接。主要就是实现了 generator/yeild, async/await。
所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。

3.3 Options

// babel.config.js

module.exports = {
  "presets": [
    ["@babel/preset-env", {
      useBuiltIns: "usage",
      corejs: 3,
      targets: {
        browsers: ["last 10 versions", "ie >= 9"]
      }
    }],
  ],
  // plugins: ["@babel/plugin-transform-arrow-functions"]
};

关于 preset-env 的几个常用配置如下

corejs

指定 core-js 库的版本。除非是有历史遗留的项目,否则使用官方推荐的就行,目前是 3

useBuiltIns

最重要的一个配置项,一共有 3 个值 false、usage 和 entry

  • false - 默认值。不转化 api,只转化 syntax
  • usage - 转化源码里用到的 api
  • entry - 转化所有的 api

看起来好像 usage 吊打 entry,其实不然。

真实开发中,我们通常会引用各种第三方库,并且为了编译速度,一般都配置了规则,不会让 babel 处理这些库。

但是第三方库质量良莠不齐,保不齐哪个库里的代码就没做好兼容性转化。一旦特定浏览器运行到这部分代码,轻则报 error,重则页面白屏

所以要是对第三方库没有完善的管理机制,还是使用 entry 更保险

使用 entry 时必须先手动引入 core-js 和 regenerator-runtime/runtime(在以前引入 @babel/polyfill 这个集成库即可,最新版 babel 弃用了这个设定,必须直接引入两个库)

// src/index.js

import 'core-js'
import 'regenerator-runtime/runtime'

const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)
// dist/index.js

"use strict";

require("core-js/modules/es.symbol");

require("core-js/modules/es.symbol.description");
// ... 省略 500+ 行 require
require('regenerator-runtime/runtime');

var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);

targets

当然直接引入也有很大缺点,由于需要转化的 api 过多,会极大增加打包后的代码体积。因此一定要配合 targets 参数使用,这样 babel 只会转化指定浏览器版本所需要的 api

modules

引入 api 的方式,默认值为 auto。设置为 false 的话 api 引入方式会变成 import

其他参数见官方文档:https://babeljs.io/docs/en/babel-preset-env#shippedproposals

四、@babel/runtime


现在想象我们开发的不是一个普通项目,而是一个工具库。

有一天,一个同学引用了我们的工具库,本来这个同学的项目经过改写原 api,每次调用 promise 方法时都会发送一个埋点

可是在使用我们的工具库之后,由于工具库里全局引入了 core-js,导致各个 api 都被 core-js 里提供的方法全局覆盖以至于污染了。因此埋点方法彻底失效了!

显然这是不合理的,因此对于类库、工具库中的 api 转化,我们可以选择全局覆盖之外的另一种方法:api 替换

先安装所需要的相关依赖

yarn add @babel/runtime @babel/plugin-transform-runtime @babel/runtime-corejs3

再修改配置(修改的是 plugins 而不是 presets)

// babel.config.js

module.exports = {
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
      },
    ]
  ]
};

源码

// src/index.js

const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)

转化后代码

// dis/index.js

import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";

var _context;

// src/index.js
const fn = () => console.log(1);

fn();
const pro = new _Promise();

const isIncludes = _includesInstanceProperty(_context = [1, 2, 3]).call(_context, 2);

这样需要转化的 api 使用的就是 @babel/runtime-corejs3 提供的方法,且不会污染全局 api

当然转化后的代码还有几个其他问题,我们依次来解决

4.1 syntax

源码并没有转化句法,因此还是需要使用 @babel/preset-env 来转化句法。(记得设置 useBuiltIns 为 false,以保证不使用 polyfill 来转化 api,不过不设置的情况下也会优先使用 runtime)

// babel.config.js

module.exports = {
  "presets": [
    ["@babel/preset-env", {
      useBuiltIns: false,
      targets: {
        browsers: ["last 10 versions", "ie >= 9"]
      }
    }],
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,
      },
    ]
  ]
};

转化后代码

// dist/index.js

"use strict";

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

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var _context;

// src/index.js
var fn = function fn() {
  return console.log(1);
};

fn();
var pro = new _promise["default"]();
var isIncludes = (0, _includes["default"])(_context = [1, 2, 3]).call(_context, 2);

可以看到 syntax 也完全转化了

4.2 target

有个坑点是 @babel/plugin-transform-runtime 目前不支持设置 target,也就意味着所有能被转化的 api 都会被转化。不过好像有人正在帮官方增加这个配置

4.3 实例方法

在之前我们提到过 api 有两种,一种是像 Promise 这样可以直接使用的,一种是挂载在实例上的。在 corejs 2 时期,runtime 是无法转化实例方法的。

因为 JavaScript 实在太灵活了,以至于编译阶段 Babel 根本无法分析实例到底是什么

举个极端例子

// src/index.js

function IncludesMock() { }

IncludesMock.prototype.includes = function (val) {
    return 'faker includes: ' + val
}

const randomVal = Math.random(1)
// 只有在运行时才知道 dontKnow 到底是 Array 实例 or IncludesMock 实例
const dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock()

console.log(randomVal);
console.log(dontKnow.includes(1));

不过 corejs 3 的现在已经可以处理这个问题了

// dist/index.js

"use strict";

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

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

function includesMock() {}

includesMock.prototype.includes = function (val) {
  return 'faker includes: ' + val;
};

var randomVal = Math.random(1);
var dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new includesMock();
console.log(randomVal);
console.log((0, _includes["default"])(dontKnow).call(dontKnow, 1));

使用 Node.js 直接执行 lib/index.js 的两种输出

0.19043928592202097
true

0.7307718322750261
faker includes: 1

推测应该是只要获取到 'includes' 关键字就会触发转化的逻辑

4.4 特例

再变态一点

// src/index.js

function IncludesMock() { }

IncludesMock.prototype.includes = function (val) {
    return 'faker includes: ' + val
}
IncludesMock.prototype.pop = function () {
    return 'faker pop'
}

const randomVal = Math.random(1)
const dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock()

const randomVal2 = Math.random(1)
const randomApi = randomVal2 <= 0.5 ? 'includes' : 'pop'

console.log(randomVal);
console.log(dontKnow[randomApi](1));
// dist/index.js

"use strict";

function IncludesMock() {}

IncludesMock.prototype.includes = function (val) {
  return 'faker includes: ' + val;
};

IncludesMock.prototype.pop = function () {
  return 'faker pop';
};

var randomVal = Math.random(1);
var dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock();
var randomVal2 = Math.random(1);
var randomApi = randomVal2 <= 0.5 ? 'includes' : 'pop';
console.log(randomVal);
console.log(dontKnow[randomApi](1));

可以发现 runtime 已经无能为力了,动态语言就是这么无法预测。不过正常人应该不会这么写代码,所以提醒自己注意,使用 api 的时候不要太非主流即可

其他参数见官方文档:https://babeljs.io/docs/en/babel-plugin-transform-runtime#docsNav

到这里,你因该已经完全了解了该如何配置 Babel,可喜可贺~

参考:

查看原文

赞 4 收藏 3 评论 0

QQQQQCY 回答了问题 · 8月28日

解决babel presets配置useBuiltIns报错,找不到corejs

引入 core-js 和 regenerator-runtime/runtime(在以前引入 @babel/polyfill 这个集成库即可,最新版 babel 弃用了这个设定,必须直接引入两个库)

关注 3 回答 2

QQQQQCY 赞了文章 · 3月23日

从零开始实现一个React(二):实现组件功能

前言

在上一篇文章JSX和虚拟DOM中,我们实现了基础的JSX渲染功能,但是React的意义在于组件化。在这篇文章中,我们就要实现React的组件功能。

React定义组件的方式可以分为两种:函数和类,我们姑且将两种不同方式定义的组件称之为函数定义组件类定义组件

函数定义组件

函数定义组件相对简单,只需要用组件名称声明一个函数,并返回一段JSX即可。
例如我们定义一个Welcome组件:

function Welcome( props ) {
    return <h1>Hello, {props.name}</h1>;
}
注意组件名称要以大写字母开头

函数组件接受一个props参数,它是给组件传入的数据。

我们可以这样来使用它:

const element = <Welcome name="Sara" />;
ReactDOM.render(
    element,
    document.getElementById( 'root' )
);

让createElemen支持函数定义组件

回顾一下上一篇文章中我们对React.createElement的实现:

function createElement( tag, attrs, ...children ) {
    return {
        tag,
        attrs,
        children
    }
}

这种实现只能渲染原生DOM元素,而对于组件,createElement得到的参数略有不同:
如果JSX片段中的某个元素是组件,那么createElement的第一个参数tag将会是一个方法,而不是字符串。

区分组件和原生DOM的工作,是babel-plugin-transform-react-jsx帮我们做的

例如在处理<Welcome name="Sara" />时,createElement方法的第一个参数tag,实际上就是我们定义Welcome的方法:

function Welcome( props ) {
    return <h1>Hello, {props.name}</h1>;
}

所以我们需要修改一下createElement,让它能够渲染组件。

function createElement( tag, attrs, ...children ) {
    
    // 如果tag是一个方法,那么它是一个组件
    if ( typeof tag === 'function' ) {
        return tag( attrs || {} );
    }

    return {
        tag,
        attrs,
        children
    }
}

渲染函数定义组件

在简单的修改了createElement方法后,我们就可以用来渲染函数定义组件了。
渲染上文定义的Welcome组件:

const element = <Welcome name="Sara" />;
ReactDOM.render(
    element,
    document.getElementById( 'root' )
);

在浏览器中可以看到结果:

图片描述

试试更复杂的例子,将多个组件组合起来:

function App() {
    return (
        <div>
            <Welcome name="Sara" />
            <Welcome name="Cahal" />
            <Welcome name="Edite" />
        </div>
    );
}
ReactDOM.render(
    <App />,
    document.getElementById( 'root' )
);

在浏览器中可以看到结果:
图片描述

类定义组件

类定义组件相对麻烦一点,我们通过继承React.Component来定义一个组件:

class Welcome extends React.Component {
    render() {
        return <h1>Hello, {this.props.name}</h1>;
    }
}

Componet

为了实现类定义组件,我们需要定义一个Component类:

class Component {}

state & props

通过继承React.Component定义的组件有自己的私有状态state,可以通过this.state获取到。同时也能通过this.props来获取传入的数据。
所以在构造函数中,我们需要初始化stateprops

// React.Component
class Component {
    constructor( props = {} ) {
        this.isReactComponent = true;
        this.state = {};
        this.props = props;
    }
}

这里多了一个isReactComponent属性,我们后面会用到。

setState

组件内部的state和渲染结果相关,当state改变时通常会触发渲染,为了让React知道我们改变了state,我们只能通过setState方法去修改它。我们可以通过Object.assign来做一个简单的实现。
在每次更新state后,我们需要使用ReactDOM.render重新渲染。

import ReactDOM from '../react-dom'
class Component {
    constructor( props = {} ) {
        // ...
    }

    setState( stateChange ) {
        // 将修改合并到state
        Object.assign( this.state, stateChange );
        if ( this._container ) {
            ReactDOM.render( this, this._container );
        }
    }
}

你可能听说过React的setState是异步的,同时它有很多优化手段,这里我们暂时不去管它,在以后会有一篇文章专门来讲setState方法。

让createElemen支持类定义组件

在js中,class只是语法糖,它的本质仍然是一个函数。
所以第一步,我们需要在createElemen方法中区分当前的节点是函数定义还是类定义。
类定义组件必须有render方法,而通过class定义的类,它的方法都附加在prototype上。
所以只需要判断tag的prototype中是否有render方法,就能知道这个组件是函数定义还是类定义。
现在我们可以进一步修改React.createElement

function createElement( tag, attrs, ...children ) {

    // 类定义组件
    if ( tag.prototype &&  tag.prototype.render ) {
        return new tag( attrs );
    // 函数定义组件
    } else if ( typeof tag === 'function' ) {
        return tag( attrs || {} );
    }

    return {
        tag,
        attrs,
        children
    }
}

render

函数定义组件返回的是jsx,我们不需要做额外处理。但是类定义组件不同,它并不直接返回jsx。而是通过render方法来得到渲染结果。

所以我们需要修改ReactDOM.render方法。
修改之前我们先来回顾一下上一篇文章中我们对ReactDOM.render的实现:

function render( vnode, container ) {

    if ( vnode === undefined ) return;
    
    // 当vnode为字符串时,渲染结果是一段文本
    if ( typeof vnode === 'string' ) {
        const textNode = document.createTextNode( vnode );
        return container.appendChild( textNode );
    }

    const dom = document.createElement( vnode.tag );

    if ( vnode.attrs ) {
        Object.keys( vnode.attrs ).forEach( key => {
            if ( key === 'className' ) key = 'class';            // 当属性名为className时,改回class
            dom.setAttribute( key, vnode.attrs[ key ] )
        } );
    }

    vnode.children.forEach( child => render( child, dom ) );    // 递归渲染子节点

    return container.appendChild( dom );    // 将渲染结果挂载到真正的DOM上
}

在上文定义Component时,我们添加了一个isReactComponent属性,在这里我们需要用它来判断当前渲染的是否是一个组件:

function render( vnode, container ) {

    if ( vnode.isReactComponent ) {
        const component = vnode;
        component._container = container;   // 保存父容器信息,用于更新
        vnode = component.render();            //  render()返回的结果才是需要渲染的vnode
    }
    
    // 后面的代码不变...
}

现在我们的render方法就可以用来渲染组件了。

生命周期

上面的实现还差一个关键的部分:生命周期。

在React的组件中,我们可以通过定义生命周期方法在某个时间做一些事情,例如定义componentDidMount方法,在组件挂载时会执行它。

但是现在我们的实现非常简单,还没有对比虚拟DOM的变化,很多生命周期的状态没办法区分,所以我们暂时只添加componentWillMountcomponentWillUpdate两个方法,它们会在组件挂载之前和更新之前执行。

function render( vnode, container ) {

    if ( vnode.isReactComponent ) {
        const component = vnode;

        if ( component._container ) {
            if ( component.componentWillUpdate ) {
                component.componentWillUpdate();    // 更新
            }
        } else if ( component.componentWillMount ) {
            component.componentWillMount();          // 挂载
        }

        component._container = container;   // 保存父容器信息,用于更新

        vnode = component.render();
    }
    
    // 后面的代码不变...
}

渲染类定义组件

现在大部分工作已经完成,我们可以用它来渲染类定义组件了。
我们来试一试将刚才函数定义组件改成类定义:

class Welcome extends React.Component {
    render() {
        return <h1>Hello, {this.props.name}</h1>;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <Welcome name="Sara" />
                <Welcome name="Cahal" />
                <Welcome name="Edite" />
            </div>
        );
    }
}
ReactDOM.render(
    <App />,
    document.getElementById( 'root' )
);

运行起来结果和函数定义组件完全一致:
图片描述

再来尝试一个能体现出类定义组件区别的例子,实现一个计数器Counter,每点击一次就会加1。
并且组件中还增加了两个生命周期函数:

class Counter extends React.Component {
    constructor( props ) {
        super( props );
        this.state = {
            num: 0
        }
    }

    componentWillUpdate() {
        console.log( 'update' );
    }

    componentWillMount() {
        console.log( 'mount' );
    }

    onClick() {
        this.setState( { num: this.state.num + 1 } );
    }

    render() {
        return (
            <div onClick={ () => this.onClick() }>
                <h1>number: {this.state.num}</h1>
                <button>add</button>
            </div>
        );
    }
}

ReactDOM.render(
    <Counter />,
    document.getElementById( 'root' )
);

可以看到结果:
图片描述

mount只在挂载时输出了一次,后面每次更新时会输出update

后话

至此我们已经从API层面实现了React的核心功能。但是我们目前的做法是每次更新都重新渲染整个组件甚至是整个应用,这样的做法在页面复杂时将会暴露出性能上的问题,DOM操作非常昂贵,而为了减少DOM操作,React又做了哪些事?这就是我们下一篇文章的内容了。

这篇文章的代码:https://github.com/hujiulong/...

从零开始实现React系列

React是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。

整个系列大概会有六篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题需要探讨也请在github上回复我~

博客地址: https://github.com/hujiulong/blog
关注点star,订阅点watch

上一篇文章

从零开始实现React(一):JSX和虚拟DOM

查看原文

赞 10 收藏 20 评论 0

QQQQQCY 关注了专栏 · 3月20日

前端笔记

我的技术博客

关注 27

QQQQQCY 赞了文章 · 3月20日

从零开始实现一个React(一):JSX和虚拟DOM

前言

React是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。

提起React,总是免不了和Vue做一番对比

Vue的API设计非常简洁,但是其实现方式却让人感觉是“魔法”,开发者虽然能马上上手,但是为什么能实现功能却很难说清楚。

相比之下React的设计哲学非常简单,虽然经常有需要自己处理各种细节问题,但是却让人感觉它非常“真实”,能清楚地感觉到自己仍然是在写js。

关于jsx

在开始之前,我们有必要搞清楚一些概念。

我们来看一下这样一段代码:

const title = <h1 className="title">Hello, world!</h1>;

这段代码并不是合法的js代码,它是一种被称为jsx的语法扩展,通过它我们就可以很方便的在js代码中书写html片段。

本质上,jsx是语法糖,上面这段代码会被babel转换成如下代码

const title = React.createElement(
    'h1',
    { className: 'title' },
    'Hello, world!'
);

你可以在babel官网提供的在线转译测试jsx转换后的代码,这里有一个稍微复杂一点的例子

准备工作

为了集中精力编写逻辑,在代码打包工具上选择了最近火热的零配置打包工具parcel,需要先安装parcel:

npm install -g parcel-bundler

接下来新建index.jsindex.html,在index.html中引入index.js

当然,有一个更简单的方法,你可以直接下载这个仓库的代码:

https://github.com/hujiulong/...

注意一下babel的配置
.babelrc

{
    "presets": ["env"],
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

这个transform-react-jsx就是将jsx转换成js的babel插件,它有一个pragma项,可以定义jsx转换方法的名称,你也可以将它改成h(这是很多类React框架使用的名称)或别的。

准备工作完成后,我们可以用命令parcel index.html将它跑起来了,当然,现在它还什么都没有。

React.createElement和虚拟DOM

前文提到,jsx片段会被转译成用React.createElement方法包裹的代码。所以第一步,我们来实现这个React.createElement方法

从jsx转译结果来看,createElement方法的参数是这样:

createElement( tag, attrs, child1, child2, child3 );

第一个参数是DOM节点的标签名,它的值可能是divh1span等等
第二个参数是一个对象,里面包含了所有的属性,可能包含了classNameid等等
从第三个参数开始,就是它的子节点

我们对createElement的实现非常简单,只需要返回一个对象来保存它的信息就行了。

function createElement( tag, attrs, ...children ) {
    return {
        tag,
        attrs,
        children
    }
}

函数的参数 ...children使用了ES6的rest参数,它的作用是将后面child1,child2等参数合并成一个数组children。

现在我们来试试调用它

// 将上文定义的createElement方法放到对象React中
const React = {
    createElement
}

const element = (
    <div>
        hello<span>world!</span>
    </div>
);
console.log( element );

打开调试工具,我们可以看到输出的对象和我们预想的一致

图片描述

我们的createElement方法返回的对象记录了这个DOM节点所有的信息,换言之,通过它我们就可以生成真正的DOM,这个记录信息的对象我们称之为虚拟DOM

ReactDOM.render

接下来是ReactDOM.render方法,我们再来看这段代码

ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.getElementById('root')
);

经过转换,这段代码变成了这样

ReactDOM.render(
    React.createElement( 'h1', null, 'Hello, world!' ),
    document.getElementById('root')
);

所以render的第一个参数实际上接受的是createElement返回的对象,也就是虚拟DOM
而第二个参数则是挂载的目标DOM

总而言之,render方法的作用就是将虚拟DOM渲染成真实的DOM,下面是它的实现:

function render( vnode, container ) {
    
    // 当vnode为字符串时,渲染结果是一段文本
    if ( typeof vnode === 'string' ) {
        const textNode = document.createTextNode( vnode );
        return container.appendChild( textNode );
    }

    const dom = document.createElement( vnode.tag );

    if ( vnode.attrs ) {
        Object.keys( vnode.attrs ).forEach( key => {
            if ( key === 'className' ) key = 'class';            // 当属性名为className时,改回class
            dom.setAttribute( key, vnode.attrs[ key ] )
        } );
    }

    vnode.children.forEach( child => render( child, dom ) );    // 递归渲染子节点

    return container.appendChild( dom );    // 将渲染结果挂载到真正的DOM上
}

这里注意React为了避免类名class和js关键字class冲突,将类名改成了className,在渲染成真实DOM时,需要将其改回。

这里其实还有个小问题:当多次调用render函数时,不会清除原来的内容。所以我们将其附加到ReactDOM对象上时,先清除一下挂载目标DOM的内容:

const ReactDOM = {
    render: ( vnode, container ) => {
        container.innerHTML = '';
        return render( vnode, container );
    }
}

渲染和更新

到这里我们已经实现了React最为基础的功能,可以用它来做一些事了。

我们先在index.html中添加一个根节点

<div id="root"></div>

我们先来试试官方文档中的Hello,World

ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.getElementById('root')
);

可以看到结果:
图片描述

试试渲染一段动态的代码,这个例子也来自官方文档

function tick() {
    const element = (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {new Date().toLocaleTimeString()}.</h2>
        </div>
      );
    ReactDOM.render(
        element,
        document.getElementById( 'root' )
    );
}

setInterval( tick, 1000 );

可以看到结果:
图片描述

后话

这篇文章中,我们实现了React非常基础的功能,也了解了jsx和虚拟DOM,下一篇文章我们将实现非常重要的组件功能。

最后留下一个小问题
在定义React组件或者书写React相关代码,不管代码中有没有用到React这个对象,我们都必须将其import进来,这是为什么?

例如:

import React from 'react';    // 下面的代码没有用到React对象,为什么也要将其import进来
import ReactDOM from 'react-dom';

ReactDOM.render( <App />, document.getElementById( 'editor' ) );

不知道答案的同学再仔细看看这篇文章哦

从零开始实现React系列

React是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。

整个系列大概会有六篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题需要探讨也请在github上回复我~

博客地址: https://github.com/hujiulong/blog
关注点star,订阅点watch
查看原文

赞 25 收藏 47 评论 4

QQQQQCY 关注了用户 · 2018-02-07

hasaki @sosout

Zant/Zanm框架的作者!Zant/Zanm 是一套基于 Vue.js 的开源 UI 组件库,Zant 主要服务于 PC 界面的中后台产品,Zanm 主要服务于移动端界面的产品。地址:http://www.zantb.com/

关注 4168

QQQQQCY 关注了用户 · 2018-02-07

缘自世界 @birenyangguangcanlan

心态很重要,我始终相信没有不会做的,只有不想做的,在这个人人都聪明的今天,你不凭智慧,只需努力就能打败90%的对手,如果你再展现出你50%的智慧,我想没有什么问题可以难倒你。

关注 7723

QQQQQCY 关注了用户 · 2018-02-07

锦峰将将 @jinfengjiangjiang

关注 3662

QQQQQCY 关注了用户 · 2018-02-07

LieRabbit @lierabbit

有些梦虽然遥不可及,但并不是不可能实现,只要我足够的强

关注 2620

认证与成就

  • 获得 4 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-06-18
个人主页被 76 人浏览