babel-preset-env升级迁移完全指北

发表于 2017-11-13
更新于 2024-05-23
分类于 技术专栏
阅读量 7796
字数统计 25565

前言

在9月份Babel宣布ES2015/ES2016/ES2017等等ES20xx时代的presets通通被废弃,取而代之的是babel-preset-env,并且承诺它将成为“未来不会过时的(future-proof)”解决方案。所以当我们再重新安装这些es20xx包的时候通通会抛出这样的提示:

npm WARN deprecated babel-preset-es2015@6.24.1: Thanks for using Babel: we recommend using babel-preset-env now: please read babeljs.io/env to update!

预示着如果我们以后想要使用那些最新的特性的话,就不再需要安装各种preset-esxx,只需要一个包就可以搞定一切。对于我们追求新特性的前端,这种升级势在必行,于是我就开始尝了个鲜,开始我的升级迁移之路。

升级之前,我们先来了解这个包的一些新的特性。

1. babel-preset-env的实现原理

查看官网的介绍,该preset可以通过你配置的目标浏览器或者实时运行环境来自动地选择Babel插件和polyfills来完成ES2015+代码的编译。那么我们可以通过源码中的package.json来查看该preset支持了多少种插件(在源码的 available-plugins.js有对这些插件的引用):

"babel-plugin-check-es2015-constants": "7.0.0-beta.2",
"babel-plugin-syntax-async-generators": "7.0.0-beta.0",
"babel-plugin-syntax-object-rest-spread": "7.0.0-beta.0",
"babel-plugin-syntax-optional-catch-binding": "7.0.0-beta.0",
"babel-plugin-syntax-trailing-function-commas": "7.0.0-beta.0",
"babel-plugin-transform-async-to-generator": "7.0.0-beta.2",
"babel-plugin-transform-async-generator-functions": "7.0.0-beta.2",
"babel-plugin-transform-es2015-arrow-functions": "7.0.0-beta.2",
"babel-plugin-transform-es2015-block-scoped-functions": "7.0.0-beta.2",
"babel-plugin-transform-es2015-block-scoping": "7.0.0-beta.2",
"babel-plugin-transform-es2015-classes": "7.0.0-beta.2",
"babel-plugin-transform-es2015-computed-properties": "7.0.0-beta.2",
"babel-plugin-transform-es2015-destructuring": "7.0.0-beta.2",
"babel-plugin-transform-es2015-duplicate-keys": "7.0.0-beta.2",
"babel-plugin-transform-es2015-for-of": "7.0.0-beta.2",
"babel-plugin-transform-es2015-function-name": "7.0.0-beta.2",
"babel-plugin-transform-es2015-literals": "7.0.0-beta.2",
"babel-plugin-transform-es2015-modules-amd": "7.0.0-beta.2",
"babel-plugin-transform-es2015-modules-commonjs": "7.0.0-beta.2",
"babel-plugin-transform-es2015-modules-systemjs": "7.0.0-beta.2",
"babel-plugin-transform-es2015-modules-umd": "7.0.0-beta.2",
"babel-plugin-transform-es2015-object-super": "7.0.0-beta.2",
"babel-plugin-transform-es2015-parameters": "7.0.0-beta.2",
"babel-plugin-transform-es2015-shorthand-properties": "7.0.0-beta.2",
"babel-plugin-transform-es2015-spread": "7.0.0-beta.2",
"babel-plugin-transform-es2015-sticky-regex": "7.0.0-beta.2",
"babel-plugin-transform-es2015-template-literals": "7.0.0-beta.2",
"babel-plugin-transform-es2015-typeof-symbol": "7.0.0-beta.2",
"babel-plugin-transform-es2015-unicode-regex": "7.0.0-beta.2",
"babel-plugin-transform-exponentiation-operator": "7.0.0-beta.2",
"babel-plugin-transform-new-target": "7.0.0-beta.2",
"babel-plugin-transform-regenerator": "7.0.0-beta.2",
"babel-plugin-transform-object-rest-spread": "7.0.0-beta.2",
"babel-plugin-transform-optional-catch-binding": "7.0.0-beta.2",
"babel-plugin-transform-unicode-property-regex": "^2.0.5",

除了这些插件,还有一些内建的定义,包括Array的新方法等。在源码的src/built-in-definitions.js中有对应的引用。但是有可能不是很完整。比如里面就没有Array.includes()方法的包。

那么babel-preset-env是如何通过我们设置的target去自动加载对应的插件和内建方法的呢?

通过阅读代码可以发现在/data/目录下维护了所有插件和内建方法的支持情况,比如在/data/plugin.json中:

"transform-es2015-arrow-functions": {
    "chrome": "47",
    "edge": "13",
    "firefox": "45",
    "safari": "10",
    "node": "6",
    "ios": "10",
    "opera": "34",
    "electron": "0.36"
  }

列举了箭头函数的支持情况,从Chrome47版本就开始支持,别的浏览器也可以一目了然。然后依托于这些插件的支持情况,在/src/index.js中这个函数去判断当前设置的target是否需要加载该插件:

export const isPluginRequired = (
  supportedEnvironments: Targets,
  plugin: Targets,
): boolean => {
  ......
}

由此我们便有了指定环境下的插件列表或内建方法列表。

弄清楚babel-preset-env的实现原理之后我们在后面的迁移升级过程中便有一定的把握,而不是处于试错的状态。

至于代码中的另外那些实现,比如如何解析target,如何装载内建方法的问题,大家有时间可以读读源码,说不定你下次就有可能需要实现这么一个类似的功能呢?

2. babel-preset-env的升级迁移

为了让我们有效果可以对比,我们会拿出在迁移之前的一份配置以及迁移之后的一份配置,以方便我们做对比,毕竟迁移之前的版本肯定是稳定版本。同时我们会以前端js和nodejs分别来做对应的迁移。接下去先给出迁移前前端(移动端)的.babelrc和nodejs端的.babelrc文件的配置:

前端:

{
  "presets": [
    ["es2015", {"modules": false}],
    "react",
    "stage-1"
  ],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

nodejs端:

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

首先我们先说说前端的。

2.1. 前端的迁移和升级

首先我们知道babel-preset-es2015有哪些对应的插件,加上配置的额外插件,大致的插件列表参考下面的表一。

ok,旧的配置大致的插件列表我们得到了,现在我们使用babel-preset-env获取另外一份插件列表。

根据官方文档的讲解,只需要将.babelrc文件中的preset选项中关于es2015/es2016/es2017/latest替换成env即可。于是我们使用这份.babelrc文件,因为是移动端,所以我们配置target的时候就以系统为标识。

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["Android >= 4.0", "ios >= 6"]
      },
      "debug": true,
      "include": [],
      "useBuiltIns": false
    }],
    "react",
    "stage-1"
  ],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

执行一下babel命令,因为是debug=true的,所以可以看到对应的打印是:

Using targets:
{
  "android": "4",
  "ios": "6"
}

Modules transform: false

Using plugins:
  check-es2015-constants {"android":"4","ios":"6"}
  transform-es2015-arrow-functions {"android":"4","ios":"6"}
  transform-es2015-block-scoped-functions {"android":"4","ios":"6"}
  transform-es2015-block-scoping {"android":"4","ios":"6"}
  transform-es2015-classes {"android":"4","ios":"6"}
  transform-es2015-computed-properties {"android":"4","ios":"6"}
  transform-es2015-destructuring {"android":"4","ios":"6"}
  transform-es2015-duplicate-keys {"android":"4","ios":"6"}
  transform-es2015-for-of {"android":"4","ios":"6"}
  transform-es2015-function-name {"android":"4","ios":"6"}
  transform-es2015-literals {"android":"4","ios":"6"}
  transform-es2015-object-super {"android":"4","ios":"6"}
  transform-es2015-parameters {"android":"4","ios":"6"}
  transform-es2015-shorthand-properties {"android":"4","ios":"6"}
  transform-es2015-spread {"android":"4","ios":"6"}
  transform-es2015-sticky-regex {"android":"4","ios":"6"}
  transform-es2015-template-literals {"android":"4","ios":"6"}
  transform-es2015-typeof-symbol {"android":"4","ios":"6"}
  transform-es2015-unicode-regex {"android":"4","ios":"6"}
  transform-regenerator {"android":"4","ios":"6"}
  transform-exponentiation-operator {"android":"4","ios":"6"}
  transform-async-to-generator {"android":"4","ios":"6"}
  syntax-trailing-function-commas {"android":"4","ios":"6"}

外加额外配置的插件,得到的插件列表和之前的配置得到的插件列表对比一下:

表一: babel-preset-es2015 vs babel-preset-env

插件旧配置新配置范畴作用
check-es2015-constantsYYES2015用来校验const定义的变量是否是常量
transform-es2015-arrow-functionsYYES2015转译箭头函数为ES5的函数定义
transform-es2015-block-scoped-functionsYYES2015确保块级别的函数声明是在块级作用域内的
transform-es2015-block-scopingYYES2015转译块级作用域内的const/let为ES5的语法
transform-es2015-classesYYES2015转译ES2015的类为ES5的语法
transform-es2015-computed-propertiesYYES2015转译ES2015的可计算属性(computed properties)为ES5的语法
transform-es2015-destructuringYYES2015转译ES2015的解构语法为ES5的语法
transform-es2015-duplicate-keysYYES2015转译带有重复key的对象为有效严格的ES5语法
transform-es2015-for-ofYYES2015转译ES2015的for...of语法为ES5语法
transform-es2015-function-nameYYES2015转译ES2015的funtion.name语法为ES5语法
transform-es2015-literalsYYES2015转移ES2015的整型和unicode字面量为ES5语法
transform-es2015-modules-commonjsYY(打印这行说明的: Modules transform: commonjs)ES2015转译ES2015的模块声明语法为Commonjs语法
transform-es2015-object-superYYES2015转译ES2015的对象super语法为ES5语法
transform-es2015-parametersYYES2015转译ES2015的默认参数和剩余(rest)参数语法为ES5语法
transform-es2015-shorthand-propertiesYYES2015转译ES2015的属性和方法简写语法为ES5语法
transform-es2015-spreadYYES2015转译ES2015的spread语法为ES5语法
transform-es2015-sticky-regexYYES2015转译ES2015的严格正则表达式为ES5的RegExp构造器
transform-es2015-template-literalsYYES2015转译ES2015的模板字面量为ES5的语法
transform-es2015-typeof-symbolYYES2015使用一个复制本地行为的方法来封装所有typeof的表达式
transform-es2015-unicode-regexYYES2015转译ES2015的unicode正则表达式为ES5语法
transform-regeneratorYYES2015该插件使用regenerator模块来转译async和generator函数,它不包含regeneratorRuntime,你需要同时包含Babel polyfill或者regenerator runtime
transform-exponentiation-operatorNYstage-3可以通过**这个符号来进行幂操作,想当于Math.pow(a,b)
syntax-trailing-function-commasNYstage-2它不是对ES6功能的增加,而是为了增强代码的可读性和可修改性而提出的
transform-async-to-generatorNYstage-3转译async语法为generator语法

明显我们的这个配置比旧配置更加完善。

除此之外我们还有配置stage-x的,从上面的表格来看,babel-preset-env包含的插件有些已经涉及到stage-x的范畴了。再贴出一张stage-x包含的插件表,可以看得更加清楚:

表二: stage-x的插件列表

stage插件(stage-X向下包含)作用
stage-0transform-do-expressions为了方便在 jsx写if/else表达式而提出的
transform-function-bind就是提供过 :: 这个操作符来方便快速切换上下文
stage-1transform-class-constructor-call (Deprecated)
transform-export-extensions转译额外增加的export..from语法到ES5语法
stage-2syntax-dynamic-import语法允许babel解析动态import()
transform-class-properties转译类属性语法
transform-decorators – disabled pending proposal update
stage-3transform-object-rest-spread它是对 ES6中解构赋值的一个扩展,因为ES6只支持对数组的解构赋值,对对象是不支持的
transform-async-generator-functions转变async generator函数和for-await声明为ES2015的generators
stage-4syntax-trailing-function-commas它不是对ES6功能的增加,而是为了增强代码的可读性和可修改性而提出的
transform-async-to-generator转变async方法为ES2015的generators
transform-exponentiation-operator这个插件算是一个语法糖,可以通过**这个符号来进行幂操作,想当于Math.pow(a,b)

这个时候熟悉这个插件的童鞋估计会有疑问:为什么这些插件不配置在preset里面的那个include字段里面?因为根据官方文档的解释,include字段放置的插件必须在这个列表中存在plugin-features.js,否则会报错。

至此,我们前端迁移完毕。接下去我们想要做一些升级,升级些什么呢?

在平时的代码编写中,很多内置方法已经都在使用了,但往往有时候会出现这种糟糕的情况:

在某些比较低版本的手机上某个页面报错,或者甚至打不开页面,仅仅是因为我们在组件的willMount阶段使用了includes/find等内置方法。但是因为我们之前的那份配置并没有配置去转译这些内置方法,所以这些方法都不会被编译到。

使用babel-polyfill在老的配置中可以实现这些方法的编译,但是因为不够灵活导致我们将整个包都引进来,造成无谓的一些开销。但是在新配置中,我们仅仅需要打开一个开关,即可以实现: useBuiltIns=true. 根据官方文档介绍:

该字段给babel-preset-env提供了一种方式去polyfill(但也是要通过babel-polyfill)。只是做法变成了根据当前配置的环境去加载对应的一系列插件译替换直接引用babel-polyfill。比如我们更新下面的配置:

{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["Android >= 4.0", "ios >= 6"]
      },
      "debug": true,
      "include": [],
      "useBuiltIns": true
    }],
    "react",
    "stage-1"

  ],
  "plugins": [
    "transform-decorators-legacy",
    "transform-object-rest-spread"
  ]
}

那么调试打印如下:

Using polyfills:
  es6.typed.array-buffer {"android":"4","ios":"6"}
  es6.typed.int8-array {"android":"4","ios":"6"}
  es6.typed.uint8-array {"android":"4","ios":"6"}
  es6.typed.uint8-clamped-array {"android":"4","ios":"6"}
  es6.typed.int16-array {"android":"4","ios":"6"}
  es6.typed.uint16-array {"android":"4","ios":"6"}
  es6.typed.int32-array {"android":"4","ios":"6"}
  es6.typed.uint32-array {"android":"4","ios":"6"}
  es6.typed.float32-array {"android":"4","ios":"6"}
  es6.typed.float64-array {"android":"4","ios":"6"}
  es6.map {"android":"4","ios":"6"}
  es6.set {"android":"4","ios":"6"}
  es6.weak-map {"android":"4","ios":"6"}
  es6.weak-set {"android":"4","ios":"6"}
  es6.reflect.apply {"android":"4","ios":"6"}
  es6.reflect.construct {"android":"4","ios":"6"}
  es6.reflect.define-property {"android":"4","ios":"6"}
  es6.reflect.delete-property {"android":"4","ios":"6"}
  es6.reflect.get {"android":"4","ios":"6"}
  es6.reflect.get-own-property-descriptor {"android":"4","ios":"6"}
  es6.reflect.get-prototype-of {"android":"4","ios":"6"}
  es6.reflect.has {"android":"4","ios":"6"}
  es6.reflect.is-extensible {"android":"4","ios":"6"}
  es6.reflect.own-keys {"android":"4","ios":"6"}
  es6.reflect.prevent-extensions {"android":"4","ios":"6"}
  es6.reflect.set {"android":"4","ios":"6"}
  es6.reflect.set-prototype-of {"android":"4","ios":"6"}
  es6.promise {"android":"4","ios":"6"}
  es6.symbol {"android":"4","ios":"6"}
  es6.object.freeze {"android":"4","ios":"6"}
  es6.object.seal {"android":"4","ios":"6"}
  es6.object.prevent-extensions {"android":"4","ios":"6"}
  es6.object.is-frozen {"android":"4","ios":"6"}
  es6.object.is-sealed {"android":"4","ios":"6"}
  es6.object.is-extensible {"android":"4","ios":"6"}
  es6.object.get-own-property-descriptor {"android":"4","ios":"6"}
  es6.object.get-prototype-of {"android":"4","ios":"6"}
  es6.object.keys {"android":"4","ios":"6"}
  es6.object.get-own-property-names {"android":"4","ios":"6"}
  es6.object.assign {"android":"4","ios":"6"}
  es6.object.is {"android":"4","ios":"6"}
  es6.object.set-prototype-of {"android":"4","ios":"6"}
  es6.function.name {"android":"4","ios":"6"}
  es6.string.raw {"android":"4","ios":"6"}
  es6.string.from-code-point {"android":"4","ios":"6"}
  es6.string.code-point-at {"android":"4","ios":"6"}
  es6.string.repeat {"android":"4","ios":"6"}
  es6.string.starts-with {"android":"4","ios":"6"}
  es6.string.ends-with {"android":"4","ios":"6"}
  es6.string.includes {"android":"4","ios":"6"}
  es6.regexp.flags {"android":"4","ios":"6"}
  es6.regexp.match {"android":"4","ios":"6"}
  es6.regexp.replace {"android":"4","ios":"6"}
  es6.regexp.split {"android":"4","ios":"6"}
  es6.regexp.search {"android":"4","ios":"6"}
  es6.array.from {"android":"4","ios":"6"}
  es6.array.of {"android":"4","ios":"6"}
  es6.array.copy-within {"android":"4","ios":"6"}
  es6.array.find {"android":"4","ios":"6"}
  es6.array.find-index {"android":"4","ios":"6"}
  es6.array.fill {"android":"4","ios":"6"}
  es6.array.iterator {"android":"4","ios":"6"}
  es6.number.is-finite {"android":"4","ios":"6"}
  es6.number.is-integer {"android":"4","ios":"6"}
  es6.number.is-safe-integer {"android":"4","ios":"6"}
  es6.number.is-nan {"android":"4","ios":"6"}
  es6.number.epsilon {"android":"4","ios":"6"}
  es6.number.min-safe-integer {"android":"4","ios":"6"}
  es6.number.max-safe-integer {"android":"4","ios":"6"}
  es6.math.acosh {"android":"4","ios":"6"}
  es6.math.asinh {"android":"4","ios":"6"}
  es6.math.atanh {"android":"4","ios":"6"}
  es6.math.cbrt {"android":"4","ios":"6"}
  es6.math.clz32 {"android":"4","ios":"6"}
  es6.math.cosh {"android":"4","ios":"6"}
  es6.math.expm1 {"android":"4","ios":"6"}
  es6.math.fround {"android":"4","ios":"6"}
  es6.math.hypot {"android":"4","ios":"6"}
  es6.math.imul {"android":"4","ios":"6"}
  es6.math.log1p {"android":"4","ios":"6"}
  es6.math.log10 {"android":"4","ios":"6"}
  es6.math.log2 {"android":"4","ios":"6"}
  es6.math.sign {"android":"4","ios":"6"}
  es6.math.sinh {"android":"4","ios":"6"}
  es6.math.tanh {"android":"4","ios":"6"}
  es6.math.trunc {"android":"4","ios":"6"}
  es7.array.includes {"android":"4","ios":"6"}
  es7.object.values {"android":"4","ios":"6"}
  es7.object.entries {"android":"4","ios":"6"}
  es7.object.get-own-property-descriptors {"android":"4","ios":"6"}
  es7.string.pad-start {"android":"4","ios":"6"}
  es7.string.pad-end {"android":"4","ios":"6"}
  web.timers {"android":"4","ios":"6"}
  web.immediate {"android":"4","ios":"6"}
  web.dom.iterable {"android":"4","ios":"6"}

粗略瞅了一下,加了很多的内建方法,如果你想要使用的方法在上面还是没有,那就只能配置到plugins字段,比如我想使用includes方法,那么只能是这样的配置:

"plugins": [
  "array-includes"
]

可是上面的内建方法已经包含了includes了哦:

  es7.array.includes {"android":"4","ios":"6"}

至此前端算是迁移升级完毕,对应的配置以及demo大家可以在这里找到: babel-preset-env-demo

2.2. nodejs端的迁移升级

按照之前前端的迁移升级思路,我们不难得到下面这份新的配置:(假设我们的Nodejs版本是6.4)

{
  "presets": [
    ["env", {
      "targets": {
        "node": "6.4"
      },
      "debug": true,
      "useBuiltIns": true,
      "include": [
      ]
    }],
    "react",
    "stage-0"
  ],
  "plugins": [
    "transform-decorators-legacy",
  ]
}

如果觉得这样就算迁移升级完了的话,那你真的是too young too simple了。这一份配置的一个很大的不同点是我们的target变化了,那么可想而知,对应应用的插件和内置方法肯定也发生了变化,打印如下:

Using targets:
{
  "node": "6.4"
}

Modules transform: commonjs

Using plugins:
  transform-es2015-destructuring {"node":"6.4"}
  transform-es2015-for-of {"node":"6.4"}
  transform-es2015-function-name {"node":"6.4"}
  transform-exponentiation-operator {"node":"6.4"}
  transform-async-to-generator {"node":"6.4"}
  syntax-trailing-function-commas {"node":"6.4"}

Using polyfills:
  es6.typed.array-buffer {"node":"6.4"}
  es6.typed.int8-array {"node":"6.4"}
  es6.typed.uint8-array {"node":"6.4"}
  es6.typed.uint8-clamped-array {"node":"6.4"}
  es6.typed.int16-array {"node":"6.4"}
  es6.typed.uint16-array {"node":"6.4"}
  es6.typed.int32-array {"node":"6.4"}
  es6.typed.uint32-array {"node":"6.4"}
  es6.typed.float32-array {"node":"6.4"}
  es6.typed.float64-array {"node":"6.4"}
  es6.map {"node":"6.4"}
  es6.set {"node":"6.4"}
  es6.weak-map {"node":"6.4"}
  es6.weak-set {"node":"6.4"}
  es6.promise {"node":"6.4"}
  es6.symbol {"node":"6.4"}
  es6.function.name {"node":"6.4"}
  es6.array.from {"node":"6.4"}
  es7.object.values {"node":"6.4"}
  es7.object.entries {"node":"6.4"}
  es7.object.get-own-property-descriptors {"node":"6.4"}
  es7.string.pad-start {"node":"6.4"}
  es7.string.pad-end {"node":"6.4"}

可以发现比前端页面的插件使用减少了很多,毕竟是node6呀!如果是Node8的话肯定是更少的了。本来这些插件肯定是足够满足我们的平时使用,但偏偏我在项目中的一种用法直接导致报错,这就让我很无语。下面是demo:

1// module.js 2module.exports = { 3 error(message){ 4 this.name = 'Dianwoda'; 5 this.message = message || '系统异常,请稍后再试'; 6 this.type = 'customeErrorType' 7 this.stack = (new Error()).stack; 8 } 9} 10 11// main.js 12const mod = require('./module.js') 13(() => { 14 throw new mod.error('这里抛自定义的错误') 15})

聪明厉害的童鞋们是否看出这段代码的问题来了?会报错吗?报的是什么错误呢?

5

4

3

2

1

在当前的babel配置下,代码运行会报错,报错的内容肯定是mod.error is not a constructor!为什么呢?

答案是在这份配置下,babel并没有帮你转译ES6的新语法:类方法简写。依然保留着最原始的代码,那么这样做有什么问题呢?这就涉及到函数定义的三种方式的区别:

  1. 正常定义
  2. 简写式定义
  3. 箭头函数

请看下面的demo:

const example = {
  normal: function() { console.log(this); },
  arrow: () => { console.log(this); },
  shorthand() { console.log(this); }
};
new example.fn();        // fn {}
new example.arrow();     // Uncaught TypeError: example.arrow is not a constructor
new example.shorthand(); // Uncaught TypeError: example.shorthand is not a constructor

Method definitions中有这么一句话:

Method definitions are not constructable

明确指出所有的第三种方式定义的函数都是不可构造的。我们在Chrome中打印出三者的结构就可以一目了然了:

所以在当前的babel配置下报这种错误是理所当然的。那么我们接下去修改配置,增加对应的插件:

...
      "include": [
        "transform-es2015-shorthand-properties"
      ]
...

这样就可以将ES6的shorthand properties转译为正常的函数声明了,于是这个问题就解决了。

这样的配置基本满足了我们在项目的使用,关于该配置的demo可以参考: babel-preset-env-demo

参考

  1. Method definitions
  2. FunctionCreate
  3. TypeError: "x" is not a constructor
  4. babel-preset-env
  5. babel-preset-es2015
  6. Node ES2015兼容性

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 babel-preset-env升级迁移完全指北 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github