본문으로 건너뛰기

ES Module

무료2017-09-03#JS#es6 module#es6 import#es module and cjs#es module current state#js模块

2 년을 기다린 Demo 가 드디어 실행될 수 있게 되었습니다

零.7 종의 모듈화 방식

1.섹션 주석

<!--html-->
<script>
    // module1 code
    // module2 code
</script>

수동으로 주석을 추가하여 모듈 범위를 표시. CSS 내의 섹션 주석과 유사:

/* -----------------
 * TOOLTIPS
 * ----------------- */

유일한 작용은 코드 열람을 쉽게 하는 것. 지정 모듈을 빠르게 찾음. 근본 원인은 단일 파일의 내용이 너무 길어서, 이미 유지보수의 麻烦에 직면했기 때문. 수동으로 몇 개의 앵커를 삽입하여 빠른 점프용

매우 원시적인 모듈화 방안. 실질적인 이점은 없음 (모듈 스코프, 의존 처리, 모듈 간 오류 격리 등)

2.복수 script 태그

<!--html-->
<script type="application/javascript" src="PATH/polyfill-vendor.js" ></script>
<script type="application/javascript" src="PATH/module1.js" ></script>
<script type="application/javascript" src="PATH/module2.js" ></script>
<script type="application/javascript" src="PATH/app.js" ></script>

각 모듈을 독립 파일로 분할. 3 가지 이점이 있음:

  • 리소스 로드 순서를 제어하여 모듈 의존을 처리

  • 모듈 간 오류 격리가 있음 (module1.js 초기화 실행 이상은 module2.jsapp.js 의 실행을 阻断하지 않음)

  • 각 모듈은 별도 파일에 위치. 실제로 유지보수 체험을 향상

그러나 2 가지 문제가 아직 존재:

  • 모듈 스코프가 없음

  • 리소스 요청 수량은 모듈화 입자와 관련. 성능과 모듈화 수익의 균형을 찾을 필요

3.IIFE

const myModule = (function (...deps){
   // JavaScript chunk
   return {hello : () => console.log('hello from myModule')};
})(dependencies);

패치로 사용 가능. 다른 방식과 배합하여 사용. 모듈 스코프를 제공

4.Asynchronous module definition (AMD)

RequireJS 예:

// polyfill-vendor.js
define(function () {
    // polyfills-vendor code
});

// module1.js
define(function () {
    //...
    return module1;
});
// module2.js
define(function () {
    //...
    return module2;
});

// app.js
define(['PATH/polyfill-vendor'] , function () {
    define(['PATH/module1', 'PATH/module2'] , function (module1, module2) {
        var APP = {};

        if (isModule1Needed) {
            APP.module1 = module1({param: 1});
        }
        APP.module2 = new module2({a: 42});
    });
});

비교적 完善한 모듈 정의 방안. 모듈 의존 문제를 해결. 모듈 스코프, 오류 격리/포획 등의 방안을 제공. 그러나 조금 冗長해 보임

P.S. 另外 SeaJS 도 있음 (공식 사이트는 이미 없음. 소개하지 않음). 커뮤니티 구현의 모듈화 패치는 모두 과도 산물. 현재看来, JS 는 드디어 모듈화 특성을 맞이할 것 같음

5.CommonJS

NodeJS 예:

// polyfill-vendor.js
    // polyfills-vendor code

// module1.js
    // module1 code
    module.exports= module1;
// module2.js
module.exports= module2;

// app.js
require('PATH/polyfill-vendor');

const module1 = require('PATH/module1');
const module2 = require('PATH/module2');

const APP = {};
if(isModule1Needed){
    APP.module1 = module1({param:1});
}
APP.module2 = new module2({a: 42});

NodeJS 는 CommonJS 규범을 따름. 파일 즉 모듈.同樣하게 비교적 完善한 방안. 그러나 브라우저 환경에는 적용하지 않음

6.UMD (Universal Module Dependency)

UMD 예:

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
    typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, function () {
    // JavaScript chunk
    return {
       hello : () => console.log('hello from myModule')
    }
});

同樣하게 패치. AMD 와 CommonJS 모듈 정의를 互換. 모듈 跨環境通用을 실현. UMD 가 출현한 근본 원인은 커뮤니티 모듈 정의 방식이 너무 많아서, 오픈소스 모듈 유지보수가 매우 麻烦 (각종 MD issue 가 출현. 어쩔 수 없이 UMD 로 교환). 따라서 표준화가 매우 迫切. ES6 는 이 사명을 짊어짐

P.S. 물론, 오픈소스 모듈의 유지보수 문제는 아직 존재 (ES Module 에 迎合하기 위해, 또 전용의 ES6 구축 버전을 추가). 그러나 加剧하지 않음. 畢竟 이미 표준화의 길 위에 있음

7.ES6 Module

기본 용법 예:

// myModule.js
export {fn1, fn2};

function fn1() {
    console.log('fn1');
}
function fn2() {
    console.log('fn2');
}

// app.js
import {fn1, fn2} from './myModule.js';
fn1();
fn2();

// index.html
<script type="module" src="app.js"></script>

주의:

  • script 태그는 반드시 type="module" 을 선언하여 ES Module 방식으로 내용을 해석하는 것을示す. 否则 실행되지 않음

  • import 모듈 파일정확한 경로(./), 파일확장자(.js) 및 대응의MIME 타입이 반드시 필요. 否则 도입 실패

현재各大主流 브라우저는 모두 ES Module 실험性功能을 제공:

  • Safari 10.1.

  • Chrome Canary 60 – behind the Experimental Web Platform flag in chrome:flags.

  • Firefox 54 – behind the dom.moduleScripts.enabled setting in about:config.

  • Edge 15 – behind the Experimental JavaScript Features setting in about:flags.

2 년을 기다린 Demo 가 드디어 실행될 수 있게 되었습니다: http://ayqy.net/temp/module/index.html

P.S. 一般에 ES Module 이라고 부름. Module 특성은 여러 버전이 존재하지 않기 때문. ES Module 은 ES6 가 도입한 Module 특성을 가리킴

一.구문

export

// 기본 구문
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var, function
export let name1 = …, name2 = …, …, nameN; // also var, const

// 디폴트 수출
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };

// 聚合 수출
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;

exportexport default 의 차이에 주의:

  • 각 모듈 (/파일) 은 1 개의 export default 만 가능. 여러 개의 export 가 가능

  • export default 뒤는 임의의 식을 연결 가능. export 구문은 3 종류만

예를 들어:

// 불합법. 구문 오류
export {
    a: 1
};
// 而应该用 export { name1, name2, …, nameN };
let a = 1;
export {
    a
};
// 또는 export let name1 = …, name2 = …, …, nameN; // also var, const
export let a = 1;

디폴트 수출

디폴트 수출은 일종의 특수한 수출 형태. 예를 들어:

// module.js
export {fn1, fn2};
function fn1() {
    console.log('fn1');
}
function fn2() {
    console.log('fn2');
}
export default {
    a: 1
};
let b = 2;
export {
    b
};
export let c = 3;

// app.js
import * as m from './module.js';
console.log(m);
// 출력 결과
Module {
    b: 2,
    c: 3,
    default: {
        a: 1
    },
    fn1: ?n1,
    fn2: ?n2
}

디폴트 수출은 Module 오브젝트의 default 속성에 격리됨. 다른 export 와 대우가 다름

聚合 수출

import + export 에 상당. 그러나 현재 모듈 스코프에 각 API 변수를 도입하지 않음 (도입 후 직접 수출. 인용 불가). API 聚合의 중전 작용만. 예를 들어:

// lib.js
let util = {name: 'util'};
let dialog = {name: 'core'};
let modal = {name: 'modal'};

export {
    util,
    dialog,
    modal
}

// module.js
console.log(`before export from lib: ${typeof dialog}`);
export * from './lib.js';
console.log(`after export from lib: ${typeof dialog}`);

전후 모두 undefined. 畢竟 중전만. 현재 모듈 스코프에 도입하지 않음. 而 import + export 는 먼저 도입. 현재 모듈에서 사용 가능

import

// default export 내용을 도입
import defaultMember from "module-name";
// 모든 export 내용을 도입. default 를 포함하며, name 이라는 오브젝트에 팩
import * as name from "module-name";
// 이름으로 지정 export 내용을 도입
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1, member2 } from "module-name";
import { member1, member2 as alias2 , [...] } from "module-name";
// default export 내용을 도입. 동시에 이름으로 지정 export 내용을 도입
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
// 모듈에 노출하는 것을 도입하지 않음. 해당 모듈 코드만 실행
import "module-name";

마지막一种是 재미있음. Import a module for its side effects only 라고 불림. 모듈 코드만 실행. 아무것도 새로운 것을 도입하지 않음 (외부 상태에 영향하는 부분만生效. 즉 부작용)

P.S. ES Module 구문에 대한 상세 정보는, [module_ES6 筆記 13](/articles/module-es6 筆記 13/), 또는 참고 자료 부분의 ES Module Spec 을 참조

P.S. NodeJS 도 ES Module 서포트를 검토. 그러나 CommonJS 모듈과 ES Module 을 어떻게 구분하는가라는 문제에 직면. 아직 토론 중. 상세 정보는 ES Module Detection in Node 를 참조

二.로드 메커니즘

ES6 모듈 로드 메커니즘示意图

즉:

  • type="module" 의 리소스는 defer 효과를自带 (HTML 문서 해석 완료 후에 실행)

  • async 는 여전히 유효 (리소스 로드 완료 후 즉시 실행. 실행 완료 후 HTML 문서 해석을 계속)

  • import 리소스 로드는 병행

自带 defer 효과. 裸 script 의 디폴트 동작 (리소스 로드 즉시 실행. かつ HTML 문서 해석을 블록) 과 다름. 另外, import 가 동급 리소스를 로드하는 것은 병행이지만, 下一级 의존을 찾는 프로세스는 不可避免地 순서串行. 这部分 성능은 무시할 수 없음. 브라우저가 原生으로 ES Module 을 서포트해도, 함부로 import 할 수 없음

CSS 중의 @import 규칙과 유사. 最佳 실천이 발전할 가능성. 모듈화와 로드 성능 사이에서 균형을 추구

三.특징

1.정적 메커니즘

if, try-catch 문, 함수 또는 eval 등의 곳에서 import 를 사용할 수 없음. 모듈 최외층에만 출현 가능

并且 import 에는提升(Hosting) 특성이 있음. 변수 선언이 현재 스코프顶部에 提升되는 것과 마찬가지로, 모듈에 선언된 import 는 모듈顶部에 提升됨

P.S. 정적 모듈 메커니즘은 해석/실행 최적화에 유리

2.새로운 script 타입

새로운 script 타입 속성 type="module" 이 필요. 해석기는 내용이 ES Module 인지 아닌지를 추측할 수 없음 (예를 들어 import, export 키워드가 없음. 엄격 모드에도 따름. 그렇다면 모듈로 간주하는가?)

另外, 내용으로 추측하는 것은 多次元 해석의 성능 손실이 존재

3.모듈 스코프

각 모듈은 자신의 스코프를 가짐. 모듈 하의 변수 선언은 글로벌에 노출하지 않음

4.디폴트로 엄격 모드를开启

thisglobal 을 가리키지 않고, undefined

5.Data URI 와 Blob URI 를 서포트

import grape from 'data:text/javascript,export default "grape"';

// 빈 ES 모듈을 생성
const scriptAsBlob = new Blob([''], {
    type: 'application/javascript'
});
const srcObjectURL = URL.createObjectURL(scriptAsBlob);
 // ES 모듈을 삽입하고, 이벤트를 리슨
const script = document.createElement('script');
script.type = 'module';
document.head.appendChild(script);
// 스크립트의 로드를 시작
script.src = srcObjectURL;

6.CORS 제한을 받음

跨域의 모듈 리소스는 import 도입할 수 없음. script 태그로 모듈 방식으로 로드할 수도 없음

7.HTTPS 리소스는 HTTP 리소스를 import 할 수 없음

HTTPS 페이지가 HTTP 리소스를 로드하는 것과 유사. block 됨

8.모듈은 단례

일반 script 와 다름. 도입되는 모듈은 단례 (1 회만 실행). import 로든 type="module"script 태그로 도입하든

9.모듈 리소스를 요청할 때 신분 증빙 (credentials) 을帯び지 않음

Fetch API 와脾气가 같음. 디폴트로 신분 증빙을帯び지 않음. script 태그에 crossorigin 속성을 추가할 필요

四.문제

1.import 오류

정확한 모듈 파일 경로를给出할 필요. 否则 모듈 내용을 실행하지 않음. 并且 Chrome 60 은 오류 보고도 없음

P.S. import 오류는 현재 각 브라우저에 아직 차이가 존재

2.모듈 간 오류 격리는 여전히 문제

리소스 로드 오류: 동적으로 script 를 삽입하여 모듈을 로드. onerror 로 로드 이상을 리슨

모듈 초기화 오류: window.onerror 글로벌 포획. 오류 정보를 통해 모듈 이름을 찾아보려 함. 모듈 초기화 실패를 기록

3.요청 수량 폭발

예를 들어 lodash demo. 600 개 이상의 파일을 로드할 필요가 있음

HTTP2 를 올리면碎파일의 문제를 완화할 수 있음. 그러나 근본에서 보면, 생산 환경에 적용하는 最佳 실천의一套가 필요. 모듈화의 입자를 규범

4.동적 import

현재 아직 실현하지 않음. import() API 가专门에 이 문제를 해결. 규범은 아직 초안 제 3 단계. 상세 정보는 Native ECMAScript modules: dynamic import() 를 참조

5.모듈 환경 검출

현재의 실행 환경이 모듈인지 아닌지를 체크:

const inModule = this === undefined;

그다지 신뢰할 수 없어 보임. 그러나 어쩔 수 없이 이렇게밖에 할 수 없음. document.currentScript 는 ES Module 에서 null 이기 때문. type 체크를 할 수 없음

五.다운그레이드 방안

1.특성 검출

특성 검출을一遍通함. 환경 검출 util 로부터 모듈을 도입. 매우 수고스럽고 성능을 손해. 예를 들어 malyw/es-modules-utils

typeof 는 통하지 않음. import, export 는 키워드이기 때문. type="module"script 태그를 삽입 가능. 빈 모듈을 로드 (Blob URI 또는 Data URI 를 사용 가능). onload 를 트리거하면 서포트를示す

另外一种의取巧한 방법이 있음:

<script type="module">
    window.__browserHasModules = true;
</script>

이러한 모듈을 도입하여 특성 검출을 수행. 그러나 ES Module 은自带 defer 효과이기 때문. 실행 순서를 보증하기 위해, 후속의 모든 JS 리소스는 defer 속성을 가질 필요가 있음 (다운그레이드용의 정상 버전을 포함)

2.nomodule

nomodule 속성. 작용은 noscript 태그와 유사. <script nomodule>console.log('ES Module 을 서포트하지 않는 환경에서만 실행')</script>

그러나 브라우저 서포트에 의존. 해당 속성을 서포트하지 않지만 ES Module 을 서포트하는 환경에서는 문제가 있음 (양쪽 모두 실행). 이미 HTML 규범에 추가됨. 그러나 현재호환성은 아직 매우 나쁨:

  • Firefox 최신 버전은 서포트

  • Edge 는 서포트하지 않음

  • Safari 10.1 은 서포트하지 않음. 그러나 방법 으로 해결 가능

  • Chrome 60 은 서포트

다운그레이드 방안에 대한 상세 정보는, Native ECMAScript modules: nomodule attribute for the migration 를 참조

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성