深入解析ES Module
不要使用 export default {a, b, c}
一个常见的错误如下
错误用法1
# lib.js
export default {
a: 1,
b: 2
}
# main.js
import { a,b } from './lib';
console.log('a:',a);
console.log('b:',b);
正确用法1
# lib.js
// 导出方式1
const a =1;
const b = 2;
export {
a, b
}
// 导出方式2
export const a = 1;
export const b = 2;
#main.js
// 导入方式1
import * as lib from './lib';
console.log(lib.a);
console.log(lib.b);
// 导入方式2
import { a,b} from './lib';
console.log(a);
console.log(b);
正确用法2
#lib.js
export default {
a:1,
b:2
}
# main.js
import lib from './lib';
console.log('a:',lib.a);
console.log('b:',lib.b);
const { a, b} = lib;
console.log('a:',a);
console.log('b:',b);
错误用法1这样的写法非常常见,然而该写法是严重的错误,按照esm的标准,a,b的打印结果应该是undefined,undefined,但是假如你使用babel5,你会得到打印结果是1,2,这就加剧了人们的认识,认为上述写法不存在任何问题,然而这种写法导致了非常多的问题。
造成这种错误的原因就在于 对象解构(object destruct)的语法和 命名导出(named export)的语法长得一模一样.虽然语法一模一样,但是由于两者使用的上下文不一样,import {a,b,c } from './xxx',这种情况下就是named export,不和import/export一起使用时才是对象解构。
babel 发现了babel5的这个问题,再babel6中已经进行了修复。上述代码在babel6中打印的结果就是undefined,undefined了。然而由于老代码的原因在迁移babel5到babel6的过程中,可以使用babel-plugin-add-module-exports插件,恢复babel5的功能。
既然有插件支持了,我们为什么不能一直用错误用法1呢?这比正确用法的写法简洁很多。原因就是如果要使用插件,就必须要使用babel将esm转换为cjs,这导致后面的打包工具难以对代码进行静态分析了。没有了静态分析,就没法做tree shaking了。更主要的原因一切非标准的写法,不同工具难以保证对齐支持方式一致,这会导致各种交互性问题。
正确使用ESM
esm支持两种导入方式和三种导出方式,如下所示
// 导出方式
export default 'hello world'; // default export
export const name = 'yj'; // named export
// 导入方式
import lib from './lib'; // default import
import * as lib from './lib'; //
import { method1, method2 } from './lib';
与之相比 cjs只有一种导入和导出方式,简单很多啊,(为啥esm的module设计的那么复杂呢。。。)
# lib.js 导出
module.exports = {
a: 1,
b: 2
}
// 和上面等价,算一种
exports.a = 1;
exports.b = 2;
//main.js 导入
const lib = require('./lib');
console.log('a:',lib.a);
console.log('b:',lib.b);
与之相关的还有 dynamic import,dynamic 只有import并且只有一种导入方式
# lib.js
export default{ a:1,b:2}
export const c = 3;
import('./lib').then(module => {
console.log(module.default.a);
console.log(module.default.b);
console.log(module.c);
});
这就导致了一个很尴尬的问题,esm和cjs如何交互呢。这分为如下几种情况
esm 导入cjs
cjs 导入esm
dynamic import 导入 esm
dynamic import 导入 cjs
随着因为esm的存在多种导入和导出方式,这就导致情况更加复杂。而且不同的平台的处理方式不同,不同工具生成的代码之间又如何处理导入和导出。
这进一步导致了不同平台生成的代码要如何交互,rollup, webpack, babel, typescript,浏览器,node这几种工具要怎么处理cjs和esm的交互性呢。简直一大深坑。
最佳实践
为了简化ESM和CJS的互操作性,和支持webpack tree shaking,以及老代码的兼容性我们对模块的导入和导出加如下限制。
禁止在前端代码使用commonjs
1.导出:对于单class,function,变量、及字面量的导出使用export default ,禁止对复合对象字面量进行导出操作包括数组和对象
export default 1; // ok
export default function name() {} // ok
export default class name {}; // ok
export default { a: 1, b: 2 } // not ok
2. 导入:对于export default的导出,使用import xxx from,对于named export的导出使用import * as lib from './lib' 和 import { a,b,c} from './lib'
import A from './lib';
import * as lib from './lib';
import { a, b} from './lib';
代码迁移
代码方案虽然已经定下来了,但是如何迁移老代码实际上是个问题,上百个组件,一个个修改也不太现实,所幸找到了个自动化迁移工具5to6-codemod,其可以先通过exports和cjs transform将module.exports和require转换为export和import,接着可以通过named-export-generation将export default 进行转换,转换方式如下
# 源格式 lib.js
export default { a,b c}
## 转换后的格式
const exported = { a,b ,c}
export default exported;
export const { a,b,c} = exported;
之所以进行上述转换是因为兼容两种import的写法
# 方式1
import Lib from './lib.js'
console.log(Lib.a,Lib.b)
// export default exported 为了兼容此写法
# 方式 2
import { a,b ,c} from './lib.js';
// export const { a,b,c} = exported; 为了兼容老代码中使用babel5导致的错误的写法
通过工具我们就自动完成了老代码的迁移,为了进一步防止新代码使用错误的方式,我们可以通过eslint进行禁止。通过eslint-plugin-import可以对模块的用法进行精细的控制,对应上面规则,我们开启如下规则
"import/no-anonymous-default-export": ["error", {
"allowArrowFunction": true,
"allowAnonymousClass": true,
"allowAnonymousFunction": true,
"allowLiteral": true,
"allowObject": false,
"allowArray": true
}]
Typescript 对于CJS和ESM的交互处理
对于初次尝试使用TS编写应用来说,碰到的第一个坑就是导入已有的库了,以React为例
# index.ts
import React from 'react';
console.log('react:', React);
对上述代码使用tsc进行编译会提示如下错误
Module '".../@types/react/index"' has no default export
错误提示很明显,React的库并没有提供default导出,而是整体导出。TS在2.7以前提供了allowSyntheticDefaultImports选项,设置为ture再次进行编译。不再报错,但是执行结果如下
react undefined
很明显,虽然我们在语法检查层面进行了转换,但是实际的代码导入和导出行为并没有进行转换。如何才能成功的导入React呢。
方案1: namespace import
# 方案1
// index.ts
import * as React from 'react';
console.log('react:',react);
方案2: default 导入 + allowSyntheticDefaultImports + add-module-exports
# 方案2
// index.ts
import React from 'react';
console.log('react:',react);
// .babelrc
{
...
plugins: ['add-module-exports']
...
}
// tsconfig.json
{
"allowSyntheticDefaultImports": true,
}
方案3:default导入 + esModuleInterop
// index.ts
import React from 'react';
console.log('react:', React);
// tsconfig.json
{
"esModuleInterop": true
}
方案4: React提供default导出
// react.js
module.exports = react;
module.exports.default = react;
方案1:最好,但是如果是js代码迁移为ts代码,则需要对已有的使用方式进行修改。
方案2:适用于已有的代码已经使用了add-module-exports插件,只有ts开启语法检查的支持即可
方案3:适用于已有代码并未使用add-module-exports插件,那么需要在代码生成时进行处理
方案4:需要第三方库提供对打包工具的支持,实际上有的库已经这样干了,
module.exports = require('./dist/index.js').default
module.exports.default = module.exports