美文网首页
深入解析ES Module

深入解析ES Module

作者: 乐宝呗 | 来源:发表于2021-10-13 15:28 被阅读0次

不要使用 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

摘抄自 https://zhuanlan.zhihu.com/p/40733281

相关文章

网友评论

      本文标题:深入解析ES Module

      本文链接:https://www.haomeiwen.com/subject/msosoltx.html