일.작용
Babel is a JavaScript compiler.
구조상 컴파일러에 속하며, JS 소스 코드를 입력하고, 출력도 JS 소스 코드이므로 (소위 source to source), transpiler(전역기) 라고도 불립니다
이.원리
You give Babel some JavaScript code, it modifies the code and generates the new code back out.
구체적으로, 소스 코드 변환 작업은 3 개의 단계로 나뉩니다:
parsing -> transforming -> generation
먼저 소스 코드가 가진 의미를 "이해"하고, 다음으로 의미 레벨에서의 변환을 수행하고, 마지막으로 의미 표현 형식에서 소스 코드 형식으로 매핑하여 되돌립니다
의미 표현 형식은, Babel 에서는 AST(추상 구문 트리) 를 가리킵니다:
How it modifies the code? Exactly! It builds AST, traverses it, modifies it based on plugins applied and then generate new code from modified AST.
따라서, 코드의 표현 형식에 관해서는, 중간 표현 형식 (AST) 을 도입하여 의미 변환을 수행합니다:
parsing transforming generation
String -------> AST ------------> modified AST ----------> String
전 과정에서, parsing 과 generation 은 고정 불변이며, 가장 중요한 것은 transforming 단계로, babel 플러그인을 통해 서포트되며, 이것이 그확장성의 열쇠입니다
P.S. 컴파일 원리 관련 개념에 대해서는, [다시 컴파일 원리 보기](/articles/다시 컴파일 원리 보기/) 참조
parsing
JS 소스 코드를 입력하고, AST 를 출력
parsing(해석) 은, 컴파일러의 어휘 분석 및 구문 분석 단계에 해당합니다. 입력된 소스 코드 문자열은 어휘 분석을 거쳐, 어휘적인 의미를 가진 token 열을 생성하고 (키워드, 수치, 구두점 등을 구분 가능), 다음으로 구문 분석을 거쳐, 구문적인 의미를 가진 AST 를 생성합니다 (문 블록, 주석, 변수 선언, 함수 파라미터 등을 구분 가능)
실제로는코드 문자열에 대해 의미 식별을 수행하는프로세스로, 코드 문자열을 입력하고, 그 구문 의미를 어떻게 식별하는가, 예를 들어:
var a = 'A variable.';
parsing 후, 생성되는 AST 는 다음과 같습니다:
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": "A variable.",
"raw": "'A variable.'"
}
}
],
"kind": "var"
}
이것은 말합니다: 이것은 var 타입의 변수 선언으로, 변수명은 a, 초기값은 리터럴로, 값은 "A variable."
맞습니다, AST 는 코드가 가진 구문 의미를 완전히 기술할 수 있고, 이 정보가 있으면, 컴파일러는 사람처럼 코드를 이해할 수 있으며, 이것이 의미 레벨 변환의 기초입니다
P.S. JS 코드에 대응하는 AST 구조는 AST Explorer 도구로 확인할 수 있습니다
transforming
AST 를 입력하고, 수정된 AST 를 출력
transforming(변환) 은, 컴파일러의 기계 비의존 코드 최적화 단계에 해당합니다 (조금 억지스럽지만, 양자의 작업 내용은 AST 를 수정하는 것), AST 에 몇 가지 수정을 가합니다. 예를 들어 변수명 a 를 input 으로 변경:
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "input"
},
"init": {
"type": "Literal",
"value": "A variable.",
"raw": "'A variable.'"
}
}
],
"kind": "var"
}
AST 노드 속성을 수정하기만 하면 되지만, 선언과 대입을 분할하려면, AST 노드를 신규 추가해야 합니다:
[{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "input"
},
"init": null
}
],
"kind": "var"
},
{
"type": "ExpressionStatement",
"expression": {
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "input"
},
"right": {
"type": "Literal",
"value": "A variable.",
"raw": "'A variable.'"
}
}
}]
이것은 말합니다: 첫 번째 문은 var 타입의 변수 선언으로, 변수명은 input, 초기값은 없습니다. 두 번째 문은 식문으로, 구체적으로는 대입식으로, 연산자는 =, 좌피연산자는 식별자 input, 우피연산자는 리터럴로, 값은 "A variable."
의미 레벨의 변환은 구체적으로는 AST 에 대해 증, 삭, 개 조작을 수행하는 것 으로, 수정后的 AST 는 다른 의미를 가질 가능성이 있고, 코드 문자열에 매핑하여 되돌린 것도 다릅니다
generation
AST 를 입력하고, JS 소스 코드를 출력
generation(생성) 은, 컴파일러의 코드 생성 단계에 해당하며, AST 를 코드 문자열에 매핑하여 되돌립니다. 예를 들어:
var input;
input = 'A variable.';
parsing 과 비교하여, generation 의 프로세스는 비교적 쉽고, 문자열을 결합하기만 합니다
삼.용법
관련 npm 패키지
4 개의 코어 패키지:
-
@babel/core: 프로그래밍 방식으로 Babel 사용 (CLI 방식이 아님)
-
@babel/parser: 입력 소스 코드를 해석하고, AST 생성
-
@babel/traverse: AST 를 주사 조작
-
@babel/generator: AST 를 JS 코드로 전환하여 되돌림
8 개의 툴 패키지:
-
@babel/cli: CLI 방식으로 Babel 사용, @babel/core 에 의존
-
@babel/types: AST 조작 툴 라이브러리, 판단, 어설션, 작성의 3 류 API 포함 (
isXXX,assertXXX와xxx, 예를 들어t.isArrayExpression(node, opts),t.assertArrayExpression(node, opts)와t.arrayExpression(elements)) -
@babel/polyfill: 몇 가지 언어 특성 패치 포함 (완전한 ES2015+ 환경 서포트), core-js 와 regenerator runtime 포함
-
@babel/runtime: Babel 변환으로 생성되는 툴 메서드 (
_classCallCheck등) 및 regenerator-runtime 의 1 부를 포함하며, @babel/plugin-transform-runtime 플러그인과 연동하여 사용 -
@babel/register: Node 환경에서 require 를 해킹하여
require되는 모든 파일을 자동 컴파일하는 목적을 달성하며, @babel/node 와 연동하여 실행 -
@babel/template: AST 를快速作成하기 위한 템플릿 구문으로, 플레이스홀더를 서포트
-
@babel/helpers: 일련의 사전 정의된 @babel/template 템플릿 메서드로, Babel 플러그인에서 사용 됨
-
@babel/code-frame: 소스 코드 행렬 관련의 오류 정보를 출력하기 위해 사용
P.S. Babel packages 에 대한 더 많은 정보는, babel/packages/README.md 참조
P.S. 패키지명이 모두 @babel/xxx 형식인 이유에 대해,一方面是명명충돌을피하기위해, 另一方面은공식 package 와 커뮤니티 package 를구분하기쉽게하기위해, 오해를피하기위해, 상세는 Renames:ScopedPackages( @babel/x) 참조
babylon 과 @babel/parser
@babel/parser 는 Babel 7 에서推出된것으로, 이전에는 Babylon 이라고불렸습니다
The Babel parser (previously Babylon) is a JavaScript parser used in Babel.
Babel 의 JS 해석기로, 몇 가지 특징:
-
기본적으로최신버전 ES(ES2017) 특성서포트를유효화
-
주석을유지 (comment attachment)
-
JSX, Flow, Typescript 를서포트
-
실험적인언어특성 (stage-0 및其它단계의후보특성) 을서포트
@babel/polyfill 과@babel/runtime
이 2 개는모두 ES 특성패치를제공하기위한것입니다. 예를들어 Promise, Set, Map 등:
The babel-polyfill and babel-runtime modules are used to serve the same function in two different ways. Both modules ultimately serve to emulate an ES6 environment.
차이점은:
-
@babel/polyfill: 글로벌작용역을오염함, App 이나커맨드라인도구에적합
-
@babel/runtime: 런타임의존으로패키지에組み込ま지며, 글로벌작용역을오염하지않고, 라이브러리에더적합
심플한예
상수명을대문자로변환합니다. 즉:
// 입력
const numberFive = 5;
// 요구출력
const NUMBER_FIVE = 5;
명확하게하기위해, 각각 @babel/parser, @babel/traverse 와 @babel/generator 를인용합니다 (@babel/core 가제공하는상위 API 를직접사용하지않음):
const parser = require(' @babel/parser');
const traverse = require(' @babel/traverse').default;
const generate = require(' @babel/generator').default;
let input = `
const number = 'number';
const numberFive = 5;
const numberSix = 6, numberSeven = numberSix + 1;
const XMLHttpRequest = window.XMLHttpRequest;
let aString = 'string';
var numberEight = numberSeven + 1;
function f() {
const numberEleven = numberSeven + 4;
return numberFive + numberEleven + numberEight;
}
`;
// 1. 해석
let ast = parser.parse(input);
// 2. 변환
function renameConst(name) {
return name.replace(/([a-z])([A-Z])/, '$1_$2').toUpperCase();
}
function renameConstBindings(path) {
let ownBindings = path.scope.bindings;
for (let name in ownBindings) {
if (ownBindings[name].kind === 'const') {
path.scope.rename(name, renameConst(name));
}
}
}
traverse(ast, {
Program: {
exit: renameConstBindings
},
Function: {
exit: renameConstBindings
}
});
// 3. 생성
let output = generate(ast);
// test
console.log(output.code);
출력:
const NUMBER = 'number';
const NUMBER_FIVE = 5;
const NUMBER_SIX = 6,
NUMBER_SEVEN = NUMBER_SIX + 1;
const XMLHTTP_REQUEST = window.XMLHttpRequest;
let aString = 'string';
var numberEight = NUMBER_SEVEN + 1;
function f() {
const NUMBER_ELEVEN = NUMBER_SEVEN + 4;
return NUMBER_FIVE + NUMBER_ELEVEN + numberEight;
}
순수한 스코프 조작 (상수를 찾고, 재명명하기만 함) 으로, scope 관련의 더 많은 API 는 babel/packages/babel-traverse/src/scope/index.js 참조
사.플러그인
정의
Babel 플러그인의 일반 형식은:
export default function(babel) {
return {
// 필수, traverse 와 연동하여 사용하는 visitor 오브젝트
visitor: {},
// 선택,其它플러그인을상속, 예를들어 JSX, async function 등의구문을식별
inherits: OtherPlugin,
// 선택, 플러그인실행전, 상태를초기화, 예를들어 cache
pre(state) {},
// 선택, 플러그인실행후, 마무리정리작업
post(state) {}
}
}
따라서, 위의상수명변환기능을 Babel 플러그인에패키지하는것은쉽고, 변환부분의 visitor 를가져오기만하면됩니다:
// babel-plugin-transform-const-name.js
export default function(babel) {
return {
visitor: {
Program: {
exit: renameConstBindings
},
Function: {
exit: renameConstBindings
}
}
}
}
P.S. Babel 설정옵션에서설정된플러그인파라미터는, state.opts 를통해읽을수있습니다. 상세는 Plugin Options 참조
컴파일
Babel 및플러그인이실행하는 Node 환경은 ES Module(export default) 을서포트하지않으므로, 플러그인자신은컴파일이필요 합니다. 여기서는 @babel/cli 를통해완성:
npx babel plugins --no-babelrc --presets= @babel/preset-env --out-dir lib
npm scripts 를통해수행할수도있습니다:
"scripts": {
"compile-plugins": "babel plugins --no-babelrc --presets= @babel/preset-env --out-dir lib"
}
./plugins/ 디렉토리하의플러그인소스코드를모두변환하여 ./lib/ 하에배치하고, 파일명은변경없음
설정
일반적으로 .babelrc 설정파일 (프로젝트루트디렉토리에배치) 을통해지정플러그인을적용:
{
"plugins": ['./lib/babel-plugin-transform-const-name.js']
}
주의, 여기서는컴파일후 (lib 디렉토리하) 의플러그인을사용하며,否则 export 키워드를서포트하지않는다고오류를보고:
SyntaxError: Unexpected token export
적용
然后 @babel/core 를통해플러그인을실행:
const babel = require(' @babel/core');
const input = require('fs').readFileSync('./const-rename-input.js', 'utf-8');
let output = babel.transform(input, {
filename: 'const-rename-input.js'
});
console.log(output.code);
주의, .babelrc 설정을통과하려면, 반드시 filename 을지정해야합니다. 상세는 babel.transform API is not using .babelrc 참조
.babelrc files are loaded relative to the file being compiled. If this option is omitted, Babel will behave as if babelrc: false has been set.
또는 .babelrc 를통과하지않고, 직접 CLI 를통해실행:
npx babel const-rename-input.js --no-babelrc --presets= @babel/preset-env --plugins=./lib/babel-plugin-transform-const-name.js
P.S. Babel ClI 의더많은용법은, Usage 참조
출력:
"use strict";
const NUMBER = 'number';
const NUMBER_FIVE = 5;
const NUMBER_SIX = 6,
NUMBER_SEVEN = NUMBER_SIX + 1;
const XMLHTTP_REQUEST = window.XMLHttpRequest;
let aString = 'string';
var numberEight = NUMBER_SEVEN + 1;
function f() {
const NUMBER_ELEVEN = NUMBER_SEVEN + 4;
return NUMBER_FIVE + NUMBER_ELEVEN + numberEight;
}
오.응용씬
디버그코드삭제
console.xxx, debugger 를삭제. 구체적구현은다음과같습니다:
function removeConsoleCall(path, {types: t}) {
if (path.node.name === 'console') {
let consoleCall = path.findParent(p => p.isCallExpression());
if (consoleCall) {
try {
consoleCall.remove();
} catch(ex) {
consoleCall.replaceWith(t.identifier('undefined'));
}
}
}
}
export default function(babel) {
return {
visitor: {
Identifier: {
enter(path) {
removeConsoleCall(path, babel);
}
},
DebuggerStatement: {
enter(path) {
path.remove();
}
}
}
}
}
하나의세부사항 에주의, 기본적으로 console.xxx 를삭제합니다 (consoleCall.remove();), 하지만몇가지상황에서는직접삭제할수없습니다. 예를들어피연산자로서연산에참여할때, 삭제하면구문오류를引发합니다. 여기서는 path 조작자대의검증을이용하여, 此类오류를캡처하고, undefined 로치환하여兜底
입력:
console.log(1);
window.console.log(2);
console.error('err');
let result = 2 > 1 ? console.log(3) : window.console.log(4);
if (true) debugger;
if (true) {
debugger;console.log(2);alert(3);
let three = 2 + (console.info('info'), 1);
}
출력:
"use strict";
var result = 2 > 1 ? undefined : undefined;
if (true) {}
if (true) {
var three = 2 + (1);
}
看起来不错하지만, 에일리어스등추적이어려운것에대해서는무력 입니다. 예를들어:
let log = console.log.bind(console);
log(4);
var c = window.console;
c.log(5);
// 존재誤傷
void function(c) {
c.log(6);
alert(7);
}(window.console);
출력:
var log;
log(4);
var c = window.console;
c.log(5); // 존재誤傷
void undefined;
상수컴파일치환
컴파일시, _GET_CONFIG('c3') 를대응하는설정정보로, 예를들어:
{
"c1": "#FFFFFF",
"c2": "#00FFFF",
"c3": "#FF00FF",
"c4": "#FFFF00"
}
플러그인내용은다음과같습니다:
const CONFIG_MAP = {
"c1": "#FFFFFF",
"c2": "#00FFFF",
"c3": "#FF00FF",
"c4": "#FFFF00"
};
export default function({types: t}) {
return {
inherits: require(" @babel/plugin-syntax-jsx").default,
visitor: {
CallExpression: {
enter(path) {
if (path.node.callee.name === '_GET_CONFIG') {
let args = path.node.arguments.map(v => v.value);
let configValue = CONFIG_MAP[args[0]] || '';
path.replaceWith(t.stringLiteral(configValue));
}
}
}
}
}
}
입력:
function render() {
return <div style={{color: _GET_CONFIG('c3')}}></div>
}
출력:
"use strict";
function render() {
return <div style={{
color: "#FF00FF"
}}></div>;
}
마찬가지로, 정적치환의씬에만대응할수있고, 에일리어스를서포트하지않으며, 변수도서포트하지않습니다:
let x = 'c3';
_GET_CONFIG(x);
let get = _GET_CONFIG;
get('c4');
출력:
var x = 'c3';
"";
var get = _GET_CONFIG;
get('c4');
其它씬
-
강제약을구현: 예를들어 babel 플러그인을사용하여眞의"私有" 속성을打造,
Symbol을私有속성의key로사용하고, 도덕규범을강제약으로변환 -
소스코드변환: 전용툴 facebook/jscodeshift 가있으며, 더편리한 API(예를들어
findVariableDeclarators('foo').renameTo('bar')) 를제공하며, 특히 API 업그레이드등의대규모리팩토링이필요한씬에적합합니다. 예를들어 reactjs/react-codemod -
포맷: 예를들어 Prettier, 의미등가의코드스타일변환을수행하며, 예를들어애로우함수파라미터에괄호를붙이는지여부,文末에세미콜론을붙이는지여부등
-
가시화: js2flowchart 는코드에기반하여플로우차트를출력할수있고, 소스코드를읽을때참고가되며, 祖傳로직을분석하는것에도사용가능
아직 댓글이 없습니다