这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
前言
在老项目中遇到了一个 babel import 语法编译报错的问题,通过最终分析定位到了问题所在,下面记录一下解决这个问题的思路。
配置环境
先看下配置文件,非重点内容已经被过滤。
// .babelrc
{
presets: ['react-native']
}
// package.json
{
dependencies: {
"react-native": "0.44.0"
},
devDependencies: {
"babel-core": "6.26.3",
"babel-preset-react-native": "4.0.0"
}
}
最小重现demo
import styles from './selectFormItemStyles'
class SelectFormItem {
// 改为a(styles) {} 后 编译会正常
a(styles = {}) {}
render() {
// 该styles未被正确编译
styles.dxz
}
}
对于上述代码的编译结果如下:
// ignore nouse code ...
var _selectFormItemStyles = require('./selectFormItemStyles');
var _selectFormItemStyles2 = _interopRequireDefault(_selectFormItemStyles);
var SelectFormItem = function () {
_createClass(SelectFormItem, [{
key: 'a',
value: function a() {
var styles = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
}
}, {
key: 'render',
value: function render() {
styles.dxz;
}
}]);
return SelectFormItem;
}();
可以看到编译后, render 函数中的 styles.dxz 并没有被修改为 _selectFormItemStyles2.dxz
原因分析
styles.dxz 编译错误的原因可能是: a函数中的 形参默认值 导致编译 import 语法时没有正确的替换 render 中对 styles 的引用。由于只使用到了 babel-preset-react-native": "@4.0.0" 插件,而该插件是 facebook 封装的一系列的 babel 插件合集,所以考虑将当前编译错误环境实际使用到的插件剥离出,最终定位到编译错误的插件。
剥离后的demo :
const babel = require("babel-core")
const { code: res } = babel.transform(code, {
plugins: [
'transform-es2015-block-scoping',
'transform-es2015-parameters',
[
'transform-es2015-modules-commonjs',
{ strict: false, allowTopLevelThis: true }
],
'transform-es2015-classes',
]
})
上述四个插件在当前demo中的作用:
transform-es2015-block-scoping
将 let/const 转换为 var
transform-es2015-parameters
将函数的默认形参 转换为 函数内的arguments
transform-es2015-modules-commonjs
将import语法 转换为 require
transform-es2015-classes
将class语法 转换为 protopype
测试后发现,当只开启 transform-es2015-modules-commonjs 插件进行编译,编译结果正常。
此时查看 transform-es2015-modules-commonjs 是如何对import语法进行编译的。
// ignore nouse code
exports.default = function() {
return {
Program: {
// 在其他插件均编译完毕后 再进行import语法编译
exit(path){
const imports = {}
const body = path.get('body')
for (let i = 0; i < body.length; i++) {
const _path = body[i]
// 查找import语法
if (_path.isImportDeclaration()) {
const key = _path.node.source.value
// 保存该import信息到 imports
imports[key] = path.node /* general meaning */
// 移除该import语法
_path.remove()
}
}
// 遍历收集到的imports节点,替换成require语法
for (let source in imports) {
buildRequire(t.stringLiteral(source))
}
// 遍历所有节点中引用到import的资源name,并进行命名的替换
path.traverse({
AssignmentExpression(path) {
const left = path.get('left')
if (left.isIdentifier()) {
const name = left.node.name
// 调用 `babel-traverse` 的getBinding判断 当前的资源名是否和import的资源名是同一个作用域
if (this.scope.getBinding(name) !== path.scope.getBinding(name))
return
} else {
// ignore other branches analysis
}
}
})
}
}
}
}
汇总之前的结论及该插件源码发现
- 单独使用
transform-es2015-modules-commonjs编译并无错误 - 代码中引用了import声明的资源名是通过
babel-traverse的getBinding进行判断的 - 该插件的执行顺序在最后
此时提出猜想,另外的三个插件导致 babel-traverse 的 getBinding (获取当前变量绑定的作用域)判断发生错误。
由于 babel-traverse 代码量较多,考虑先从三个插件入手,查找可能会导致作用域变化的代码。查看代码后发现,在 transform-es2015-block-scoping 插件中有对作用域进行提升的操作:
// ignore unuse code
function convertBlockScopedToVar(path, node, parent, scope) {
// 是否需要移动作用域到父级
const moveBindingsToParent = arguments.length > 4 && ...
if (moveBindingsToParent) {
// 获取父级函数的作用域
var parentScope = scope.getFunctionParent();
// 获取需要替换引用资源名的变量
var ids = path.getBindingIdentifiers();
for (var name in ids) {
// 获取自身的绑定
var binding = scope.getOwnBinding(name);
if (binding) binding.kind = "var";
// 移动当前变量的作用域到父级
scope.moveBindingTo(name, parentScope);
}
}
}
通过上述代码可知:
- 对引用了和
import相同资源名的变量执行babel-traverse中的moveBindingTo方法来提升该变量的作用域
通过在 moveBindingTo 前添加日志发现,编译错误的 styles.dxz 的 binging 为undefined,由此猜测:另外两个插件的转换操作 该变量的 OwnBinding 发生丢失。
分析后猜想 当 OwnBingding 不存在时,不进行 moveBingingTo 提升作用域的操作,可解决此问题。
解决方案
// babel-plugin-transform-es2015-block-scoping@6.26.0/lib/index.js#L132
for (var name in ids) {
var binding = scope.getOwnBinding(name);
if (binding) binding.kind = "var";
+ if (binding) {
scope.moveBindingTo(name, parentScope);
+ }
}
修改后发现 styles.dxz 被正确的转换为 _selectFormItemStyles2.default.dxz
总结
分析过程
上述分析没有完整的查看 babel-traverse 及其他插件的代码,有错误的地方欢迎指正。
其他方案
由于该项目很久没有维护,使用的仍然是6.x版本的 babel,测试后发现 babel-preset-react-native 升级到5.x版本提示 babel 需要7.x版本,升级babel后发现此问题消失。