2

在 ng2-bootstrap 的 datepicker 组件中,有这么一段代码(点击查看源码)

import * as moment from 'moment';

export class DateFormatter {
  public format(date:Date, format:string):string {
    return moment(date.getTime()).format(format); // <-- 这里 moment 当函数使用了
  }
}

刚看到这段代码的时候,我就感觉 moment 一定是个 object,但是下面却当函数调用了。感觉非常疑惑。

moment 模块的代码如下:

;(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    global.moment = factory()
}(this, (function () { 'use strict';

function hooks () {
    return hookCallback.apply(null, arguments);
}

// ...

return hooks;

})));

可以看到 moment 模块返回了 hooks 函数。

我在 babel.io 中 看了一下 babel 对 import * as moment from 'moment' 的转译:

'use strict';

var _moment = require('moment'); 
var moment = _interopRequireWildcard(_moment);

function _interopRequireWildcard(obj) {
    if (obj && obj.__esModule) {
        return obj;
    } else {
        var newObj = {};
        if (obj != null) {
            for (var key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
            }
        }
        newObj.default = obj;
        return newObj;
    }
}

babel 是先 for in 它上面的属性到新的对象中,并把 default 设为本身。得到的必定是个 object,moment.default 才是那个 hooks 函数。

我用 tsc 编译成 commonjs 的代码如下:

"use strict";
var moment = require('./moment');
console.log(typeof moment);

只是简单地用了 require,没有做其他事情,得到的就肯定是那个 hooks 函数。

两种不同的表现就导致 ts 与 js 混用时的尴尬。

比如一个用 es6 的 Angular2 项目,用的是 babel 编译,而刚好使用了 ng2-bootstrap 的 datepicker。每次运行到那里都会提示 moment is not a function

为什么会导致两种不同的表现,规范是怎么规定 import * as xx from xx 这种行为的 ?

1个回答

5

已采纳

import 是针对 export 的。

按 es6 的规范 import * as obj from "xxx" 会将 "xxx" 中所有 export 导出的内容组合成一个对象返回。如果都使用 es6 的规范,这个是很明确的。

但是现在问题来了,moment 不是一个符合 es6 规范的库。

不管是 AMD、CMD 还是 CommonJS,都是通过 module.exports 导出,AMD 和 CMD 还可以通过工厂函数 return 来导出。不过是 module.exports = ... 还是 function factory() { return ... } 哪种方式,都对默认提供的 module.exports 对象进行了替换,它可以是任意 JS 数据类型,函数就是一种很常用的数据类型。

TypeScript 对 ES6 export 的转译,是将所有 export 的东西,都作为 exports,即 module.exports 的属性,比如

// test.ts
export function hello() {}
const a = 0;
export default a;

用 tsc 按 AMD 模块转译

tsc test.ts --module AMD

得到

define(["require", "exports"], function (require, exports) {
    "use strict";
    function hello() { }
    exports.hello = hello;
    var a = 0;
    exports.__esModule = true;
    exports["default"] = a;
});

所以 TypeScript 对 import * as x from "./test.js" 的转译直接转译成

var x = require("./test");

是可以理解的。

Babel 也类似,同样的 es6 代码转译出来是

// by babel
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.hello = hello;
function hello() {}
var a = 0;
exports.default = a;

但是 Babel 想得比较复杂。既然不是 __esModule,说明不是 es6 定义的模块。那么按 es6 模块导出的方式,导入的肯定是一个对象,所以它就创建了一个新对象,把导出内容的所有属性拷贝过去,兼容 exports.xxx = xxxmodule.exports 是个一般对象的情况。但万一 module.exports 不是个普通对象呢,假设它是当作 export default 导出的,所以最后加了句 newObj["default"] = ....

在这一点上其实我更倾向 Babel 的作法,但是这个转译没有标准(如果标准那么好定,NodeJS 早就用上 es6 的模块语法了)。

问题在于不管谁的作法,关键是你需要有一个统一的标准来兼容 TypeScript 和 Babel,那么要不试试

// .ts
import * as _moment from "./moment";
const moment = (_moment as any).default || _moment;

(_moment as any).default 是为了 ts 编译不报错,|| _moment 是为了兼容有 default 和没有 default 的情况 (其实可以不做兼容处理,反正 Babel 处理过后一定会有 default)。

1

谢谢你的回答!

t1mer · 2017年02月11日

添加评论

撰写答案