babel6的新老AMD模块并存问题

近来又遇到了很恶心的事儿,那就是想要升级babel为6的时候,遇到了新老AMD模块并存的问题。

是的,又遇到了,尽管在babel7都WIP了,升级个6还是恶心的够呛,扫了一圈之后发现,圈里貌似还是没人去解决这个问题……

先列出引到的链接:

我开发的小插件: babel-plugin-transform-es2015-modules-existed-amd

问题症结

主要是新老模块并存导致的。

简而言之,babel6的插件babel-plugin-transform-es2015-modules-amd在处理的时候,由于面临着这样的使用状况(示例代码):

1
2
3
4
5
6
7
import _ from 'lodash';
import {add} from './mathlib';
// do sth
export let tested = true;
export default {
tested: false
};

可以看到export let了tested,default里面也有tested,那么应该是哪个?

所以babel6定义为:

1
2
exports.tested = true;
exports.default = {tested: false};

多了一个default,所以babel6翻译过的import 'a'都变成了require('a').default。然而它并不会处理旧的那些AMD的模块,而如果你“不幸”的在旧的模块中去require了新的es6的模块,就挂了……

关键点在于,babel6的做法没什么问题,只是不兼容旧的而已,而插件babel-plugin-transform-es2015-modules-simple-amd的解决方案是将babel6的AMD转换输出改成babel5的方式,去掉default,但是如果你用了export let,那就只能sorry了,搞不了。

扫了一些文章基本结论都是:ES6 module to AMD的转码逻辑变更并且暂时没有什么好的办法解决这个问题。

还有!

babel-plugin-transform-es2015-modules-amd在处理旧的AMDUMD,都会在外面给包一层define,醉了么?

硬着头皮处理

或者可以解决的方案:不修改babel6,而是针对旧AMD模块,然后采用插件的方式处理,将它的输出也加个default,require的部分也是一样。

先只处理简单的状况!!

下面都是使用了 preset es2015 和 babel-plugin-transform-es2015-modules-amd,所以代码会先被转换一次。

为啥要用?因为不想去处理 es2015模块啊,万一人家支持了,我们只要撤掉这个插件就好了。

列一下要面对的问题:

  1. 旧模块的定义
  2. 定位模块主体
  3. require的处理
  4. exports的处理

定义

限制为使用define来定义的模块,形如:

1
2
3
4
5
define(function(require) {
'use strict';
var a = require('a');
return {hello: 'world'};
});

模块主体部分

define一共有四种形式(变参):

1
2
3
4
define(factory);
define(id, factory);
define(dependencies, factory);
define(id, dependencies, factory);

无论是用哪种,主体都是factory,最后一个参数。

不过其实要处理的是babel-plugin-transform-es2015-modules-amd搞进来的define部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// after es2015 preset + transform-es2015-modules-amd
define([], function () {
'use strict';
define(function (require) {
'use strict';
var a = require('a');
return { hello: 'world' };
});
});
// babel插件的ast信息
// 'use strict',这个东西不算语句,而是被定义为directives里 如node.directives[0].value.value = 'use strict';
// 怎么找 define?
node.body.length === 1 // 只有一个调用
&& node.body[0].type === 'ExpressionStatement'
&& node.body[0].expression.type === 'CallExpression'
&& node.body[0].expression.callee.name === 'define';
node.body[0]; // 最外层的define
// 继续找里面的
node.body[0].expression.arguments[1]; // 找到了最外层的define的匿名函数参数
node.body[0].expression.arguments[1].body.body[0]; // ExpressionStatement
node.body[0].expression.arguments[1].body.body[0].expression.type; // CallExpression
node.body[0].expression.arguments[1].body.body[0].expression.callee.name; // define
// 又找到了里层的define 即node.body[0].expression.arguments[1].body.body[0]
// 那么这时候设置换成这个结果数据是什么呢
path.node.body = node.body[0].expression.arguments[1].body.body;
define(function (require) {
'use strict';
var a = require('a');
return { hello: 'world' };
});
// yes,又滚回来了
// 并且 node.body[0].expression.arguments[node.body[0].expression.arguments.length - 1] 就是主体

require的处理

我是个较懒的人,所以选择的方式是这样的。

找到对应的点,然后使用自带的 _ref.typesbabel-types 的方法callExpression来替换原节点……

在 visitor.Program.exit 中 path.traverse(amdVisitor, this); 来遍历

然后就直接处理了,区分了几个使用的场景,没效率,但是架不住简单。

因为是包了一层调用,所以还得加个函数进去,这个简单,ast node直接用babel-template搞就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function turnRequire(target) {
return t.callExpression(
t.identifier('_interopRequireDefault'),
[target]
);
}
var amdVisitor = {
CallExpression: function CallExpression(path) {
if (isValidRequireCall(path)) {
let parentKey = path.parentKey;
switch (parentKey) {
case 'arguments':
case 'elements':
path.parent[path.parentKey][path.key] = turnRequire(path.parent[path.parentKey][path.key]);
path.node = path.parent[path.parentKey][path.key];
break;
default:
path.parent[path.parentKey] = turnRequire(path.parent[path.parentKey]);
path.node = path.parent[path.parentKey];
break;
}
}
}
};

没有搞 Gloabl require,形如 require(['main']);,先手改吧。

至此的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
define(function(require) {
'use strict';
var a = require('a');
var c = require('b').c;
var d = require('e').f.g;
require('lodash');
require('wer')();
function t() {
require('sss').go();
}
require(['aaa']);
return {hello: 'world'};
});
// 被转换为
define(function (require) {
'use strict';
var a = _interopRequireDefault(require('a'));
var c = _interopRequireDefault(require('b')).c;
var d = _interopRequireDefault(require('e')).f.g;
_interopRequireDefault(require('lodash'));
_interopRequireDefault(require('wer'))();
function t() {
_interopRequireDefault(require('sss')).go();
}
require(['aaa']);
return {hello: 'world'};
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj.default : obj;
}
});
### export的处理
我想的是,直接就处理 Function 的 ReturnStatement 就好了……
还是从第二步定位的模块主体部分入手。
来看 `mainFunc.body.body` 就是所有的语句了,只看 `ReturnStatement`,如果没有,那可能是使用 exports 或 module.exports 定义的,我们没用这种,先无视。
将`ReturnStatement`的前面插一个`VariableDeclaration`,然后让`ReturnStatement`返回的是`{default:exports}`这种,大体思路如此。
```javascript
// 简单的实现
var mainFunc = getDefineFunction(path);
var returnStatement = getReturnStatement(mainFunc);
if (returnStatement && returnStatement.target) {
mainFunc.body.body.splice(
returnStatement.index, 0,
t.VariableDeclaration('var', [
t.VariableDeclarator(t.identifier('__esModuleAMDExport'), t.objectExpression(
[
t.objectProperty(t.identifier('default'), returnStatement.target.argument),
t.objectProperty(t.identifier('__esModule'), t.booleanLiteral(true))
]
))
])
);
returnStatement.target.argument = t.identifier('__esModuleAMDExport');
}

最后,要说的是:

如果遇到了这样的问题,只能改代码了,因为根据babel6的设计,这样本来就是不行的:

1
2
3
4
5
6
7
8
9
// 在文件a中
export let b = 1;
// 然后你非要在其他的文件中去
import a from './a';
a.b; // 这样是不行的
// 应该是
import {b} from './a';