來寫第一行 js 宏

from http://jlongster.com/Writing-Your-First-Sweet.js-Macrophp

你將學會html

  • 寫第一行宏
  • 基礎的模式匹配
  • 如何用 sjs 編譯器
  • 使用 sourcemaps 來調試。

全部的教程和可用的編譯 sweet.js 宏環境都躺在 repo https://github.com/jlongster/sweet.js-tutorials 中。下一個教程是遞歸的宏以及模式匹配 http://jlongster.com/Sweet.js-Tutorial--2--Recursive-Macros-and-Custom-Pattern-Classesnode

若是你徹底不瞭解 sweet.js 和 js 宏,我推薦先閱讀 http://jlongster.com/Stop-Writing-JavaScript-Compilers--Make-Macros-Instead。我儘早把這些表達出來,雖然要花些時間。你能夠訂閱 rss http://feeds.feedburner.com/jlongster 或者 follow 個人 twitter https://twitter.com/jlongstergit

來見見咱們的宏編輯器

整個教程都圍繞他進行,你能夠交互式的左邊寫代碼,右邊會自動 expand 展開你的代碼,而且展現結果 (ps:咱們這裏可能要上面看代碼 (//註釋分隔) 下面看結果了)es6

macro foo {
  rule { $x } => { $x }
}

foo "Hi there!";
foo "Another string";
//
'Hi there!';
'Another string';

若是有報錯 error 會有個大紅條兒出如今底部,也能夠單步調試。這在後面的教程中尤爲有用哦~github

下面有些連接,點擊就會改變左邊框中的代碼,好比點擊這裏,看看變化,點擊 revert 恢復原始代碼。express

macro bar {
  rule { $x } => { $x }
}

bar "This text is different!";
bar "The macro name changed too";
//
'This text is different!';
'The macro name changed too';

當你想要寫更大變化的代碼,直接用在線編輯器 http://sweetjs.org/browser/editor.html 會更好啦npm

注意:本教程用的 sweet.js 提交在 ba9b6771678cb26af58dfa6b5b99d5b7eac75e2c 3月9號, sweet.js 仍然是早期階段,因此你可能碰到 bugs.gulp

讓咱們開始吧

宏用關鍵詞 macro 建立。咱們提供一個名字,以及一個模式匹配列表數組

macro foo {
  rule { $x } => { $x + 'rule1' }
}

foo 5;
foo bar;
//
5 + 'rule1';
bar + 'rule1';

宏名字能夠包含任何關鍵字,標識符或者標點。一個 punctuator 加標點者 是一個符號,好比:+ - * & ^ $ # 等等。只有 {} 這幾個被看成分隔符的符號無效。

這裏宏的惟一規則就是綁定 bind 第一個元素到 $x。它執行宏名字後面的模式匹配語法。當你給一個標識符加前綴 $ ,宏就會捕獲匹配的元素,你能夠在模板中輸出匹配的值,不然,宏會照字面意思原樣匹配口令就像 { 或 } 等任何你扔到代碼裏面的東西。

沒有 $ ,宏會照字面意思原樣匹配標識符。來看看,咱們把上面宏裏的 $x 改成 x

macro foo {
  rule { x } => { $x + 'rule1' }
}

foo 5;
foo bar;
//
SyntaxError: [macro] Macro `foo` could not be matched with `5 ; foo bar ;...` 5: foo 5; ^

咱們看到,這觸發了一個錯誤。若是你想重置代碼例子,只須要點擊 「恢復 revert」,而後它又活過來啦。

咱們堅持只用 x 他也會工做

macro foo {
  rule { x } => { 'rule1' }
}

foo x;
//
'rule1';

可是若是咱們調用任何非 x 的數據,都會觸發宏的失敗。它如今只是徹底照字面意思原樣的匹配 x 除非咱們用 $x 來使得他成爲一個,模式變量

來玩玩幾個宏輕鬆下:

  • ^ - 用標點當宏
  • var - 簡單到愚蠢的重構咱們的 var
macro ^ {
  rule { { $x } } => { wrapped($x) }
}

^{x};
foo(x, y, ^{z})
//
wrapped(x);
foo(x, y, wrapped(z));
let var = macro {
  rule { [$x, $y] = $arr } => {
    var $x = $arr[0];
    var $y = $arr[1];
  }
}

var [foo, bar] = arr;
//
var foo = arr[0];
var bar = arr[1];

多行模式

下面,咱們來試試添加更多的規則和模式:

macro foo {
  rule { => $x } => { $x + 'rule1' }
  rule { [$x] } => { $x + 'rule2' }
  rule { $x } => { $x + 'rule3' }
}

foo => 5;
foo 6;
foo [bar];
//
5 + 'rule1';
6 + 'rule3';
bar + 'rule2';

如今更好玩了。咱們用不一樣的代碼觸發不一樣的匹配模式。第一個規則匹配 => 口令,而後把這個口令後面跟的任何東西一塊兒綁定到 $x。 foo => 5 正確的匹配到 5 + 'rule1'。第二個規則剝去 [] 而後捕獲到剩下的東西

=> 被視做一個單獨的口令,由於es6 fat arrow functions http://wiki.ecmascript.org/doku.php?id=harmony:arrow_function_syntax 這麼用啦。只要整個口令集都匹配,任何口令集都能正常工做,因此 =*> 也是能夠滴~

macro foo {
  rule { =*> $x } => { $x + 'rule1' }
  rule { [$x] } => { $x + 'rule2' }
  rule { $x } => { $x + 'rule3' }
}

foo =*> baller;
foo 6;
foo [bar];
//
baller + 'rule1';
6 + 'rule3';
bar + 'rule2';

規則的順序是嚴格的。這是咱們要學習的模式匹配的一個基本的原則。他是從頂至底匹配的,因此更細的模式應在包容性更強的模式上面。例如,[$x] 是比 $x 更細粒度的,若是你切換他們的順序,foo [bar]將匹配到更包容的模式,也就是說有些就匹配不到了。好比:

macro foo {
  rule { => $x } => { $x + 'rule1' }
  rule { $x } => { $x + 'rule3' }
  rule { [$x] } => { $x + 'rule2' }
}

foo => 5;
foo 6;
foo [bar];
//
5 + 'rule1';
6 + 'rule3';
[bar] + 'rule3';

模式類

當你用一個模式變量 $x,他匹配任何在其位置表明的東西,不管是字符串,數組表達式。。。。若是你要限定他匹配的類型咋辦呢?

能夠指定一個特殊的解析類 parse class。例如: $x:ident 只匹配標識符。有3個可用解析類:

  • expr 表達式
  • ident 一個標識符
  • lit 一段文字

你可能會想,什麼是 expr 呢?由於 expr 幾乎能夠是任何東西。默認狀況下,sweet.js 是不貪婪的。他會找到最小匹配的語法。若是你嘗試匹配 $x 爲 bar(),$x 將僅僅被綁定到標識符 bar。若是你強制他爲表達式,$x:expr 才能匹配到整個 bar() 表達式。

使用模式解析類,當出問題的時候,你會獲得很好的匹配錯誤警告。

macro foo {
  rule { $x:lit } => { $x + 'lit' }
  rule { $x:ident } => { $x + 'ident' }
  rule { $x:expr } => { $x + 'expr' }
}

foo 3;
foo "string";
foo bar;
foo [1, 2, 3];
foo baz();
//
3 + 'lit';
'string' + 'lit';
bar + 'ident';
[
    1,
    2,
    3
] + 'expr';
baz + 'ident'();

3 和 "string" 匹配 lit,bar 匹配 ident。一個標識符是任意的 js 變量名,一個文本是任何的數字或者字符串常數。

數組以表達式來匹配,這沒錯。可是,baz()呢?前面提到,sweet.js是不貪婪的,他只匹配 baz。特別在這個狀況下,因咱們有了一個 $x:ident 其優先級高於 $x:expr,因而匹配了標識符baz。就算咱們不用模式類,咱們也有這個問題:

macro foo {
  rule { $x } => { $x + 'any' }
  rule { $x:expr } => { $x + 'expr' }
}

foo baz();
//
baz + 'any'();

若是咱們真的要匹配整個表達式呢,咱們須要用 $x:expr 而且要把他放到其餘規則之上。

macro foo {
  rule { $x:expr } => { $x + 'expr' }
  rule { $x } => { $x + 'any' }
}

foo baz();
//
baz() + 'expr';

至少如今看不出來任何規則在他之下,由於 $x:expr 將匹配表達式和文本。

若是真的想匹配整個表達式,應一直在模式變量上用 expr 類

遞歸宏 和 let

宏展開是遞歸的,意思是在你的宏運行後 sweet.js 將再次展開代碼。你會發現這個行爲在更高級的宏(用數個可展開的步驟來轉換代碼)中很是好用。當你匹配一個規則,而後釋放代碼來再次運行宏來匹配另外一個規則。在更進一步的教程中,你將看到它們。

macro foo {
  rule { { $expr:expr } } => {
    foo ($expr + 3)
  }

  rule { ($expr:expr) } => {
    "expression: " + $expr
  }
}

foo { 1 + 2 }
//
'expression: ' + 1 + 2 + 3;

一個很好用的功能是重寫關鍵字。例如你能夠實現一個 var 宏,重寫內置的var,並加上 es6 非結構化 http://wiki.ecmascript.org/doku.php?id=harmony:destructuring。固然,若是非結構化沒有被使用,你只須要簡單的擴展到原生的 var 聲明上。

這形成一個問題:你須要釋放一個不遞歸展開宏的代碼。這時候可能用 let 定義一個你本身的宏: let foo = macro { ... }。如今 foo 在你的展開代碼中不會引用任何你的宏外面的 foo ,因此他不會被展開。若是你製做一個 var 宏,任何 var 你釋放的都成爲內置的 var。

來改變上面的例子到一個 let 宏:

let foo = macro {
  rule { { $expr:expr } } => {
    foo ($expr + 3)
  }

  rule { ($expr:expr) } => {
    "expression: " + $expr
  }
}

foo { 1 + 2 }
//
foo(1 + 2 + 3);

重複的模式

重複模式容許你一次捕獲多重的模式實例。只要匹配就會這樣。你能夠用 ... 匹配重複的模式。

一個基礎的例子: rule { ($name ...) } => { $name ... }。它匹配全部有括號的語法,而後在輸出中剝去括號

一個重複模式匹配 0 到屢次,因此他不能用來強制一個模式的存在,他會一直匹配,就算匹配的是空

macro basic {
  rule { { $x (,) ... } } => {
    wrapped($x (,) ...);
  }
}

basic {} // expands to wrapped()
basic { x, y, z } // expands to wrapped(x, y, z)

爲了讓重複模式更有用, sweet.js 容許你定製一個分隔符,用來分開模式們。例如, rule { $name (,) ... } => { $name (,) ... } 說的是在匹配之間應有一個逗號,而且這個逗號同重復的 names 一同釋放。你能夠在調用代碼中丟掉這個逗號(只是 $name ...) 那麼他將只釋放 names 的集合,忽略逗號。在 js 中用逗號分隔元素很是廣泛 (想一想 參數 args, 數組元素,等等)

最後,爲了定製一個複雜的模式,你須要用到模式組。由於 ... 只是提早在一個簡單的匹配變量上起做用,當你須要 "組" 一個模式,到一個簡單的元素。你該這麼作: rule { $($name = $init) (,) ... } => { $name ... }。注意這個額外的 $() 他建立了一個模式組。這個宏將匹配像 x=5, y=6, z=7 並釋放 x y z 。你能可選的來用模式組和分隔符,當匹配和釋放代碼的時候,這容許你轉換重複模式到任何其餘想要的重複模式。

但願多玩玩 editor 能多感覺一下。這裏是一些給你瞎搞的宏~~:

  • base 一個簡單的重複模式
  • function 一個簡單的函數追蹤
  • var var 帶着一些簡單的結構化
  • nested 混合的重複
macro basic {
  rule { { $x (,) ... } } => {
    wrapped($x (,) ...);
  }
}

basic {}
basic { x, y, z }
//
wrapped();
wrapped(x, y, z);
let function = macro {
  rule { $name ($args (,) ...) { $body ... } } => {
    function $name($args (,) ...) {
      console.log("called");
      $body ...
    }
  }
}

function bar() {
  var x = 2, y = 5;
  console.log('hello');
}
//
function bar() {
    console.log('called');
    var x = 2, y = 5;
    console.log('hello');
}
let var = macro {
  rule { $([$name] = $expr:expr) (,) ... } => {
    $(var $name = $expr[0]) ...
  }
}

var [x] = arr, [y] = bar();
//
var x = arr[0];
var y = bar()[0];
macro foo {
  rule { [$([$name ...] -> $init) (,) ...] } => {
    $($(var $name = $init;) ...) ...
  }
}

foo [[x y z] -> 3, [bar baz] -> 10]
//
var x = 3;
var y = 3;
var z = 3;
var bar = 10;
var baz = 10;

全部的這些例子都是故意簡化的來方便簡單的玩玩。var 宏,好比破壞了 var 由於你再也不能正常的用內建的 var 啦。將來的教程中咱們會深刻更多的複雜的宏,將會解釋這類事情。

重複模式是好用的,可是若是你想要更多的控制匹配的模式,通常用遞歸的宏來代替。它讓你作一些了匹配 1 或者更多,捕獲多個不一樣的模式,等等其餘。一樣的,更多的將會在之後的教程中!

==衛生==

這裏咱們不能談衛生,即時他一般不影響你如何寫宏。事實上,衛生的關鍵點是:它都是自動生效的,因此你不須要擔憂名字衝突。

全部在 sweet.js 中的宏都是衛生的,意思是標識符老是引用了正確的東西。若是一個宏建立了一個新的變量(好比用了 var ),他會只在宏的自己中有效。他不會和調用宏的代碼中的另外的其餘同名變量衝突。默認狀況下,你不能引入一個新變量到一個無心識的做用域中。你能作一些工做來故意作到這一點,咱們都把他放到將來的教程中

macro foo {
  rule { $id = $init } => {
    var $id = $init
  }
  rule { $init } => { var x = $init }
}

foo 5;
var x = 6;

foo y = 10;
var y = 11;
//
var x = 5;
var x$2 = 6;
var y = 10;
var y = 11;

上面的例子中,前2個例子建立了2個不一樣的變量,由於宏本身建立了一個 x ,和咱們用 var 產生的是不一樣的。在後面的2個例子中,都引用了一樣的 y ,由於我把他傳給了宏

衛生更名是你看到 foo 更名到了 'foo$1234' 的緣由。 sweet.js 確保每一個變量都引用了正確的東西。他必須重命名全部的變量,爲了這篇文章 http://disnetdev.com/blog/2013/09/27/hygiene-in-sweet.js/ 裏面描述的全部的緣由。不幸的是那意味着全部你的標識符,會變得醜一點,可是若是你傳入 -r 參數 或者 "readable names"選項給 sweet.js 編譯器,他會清理下,而且大部分狀況都會從新變好看點。因爲這僅僅在 es5 代碼中生效,因此還不是默認開啓的。

用 sweet.js 編譯器

首篇教程僅需學會如何用在本身的項目上
npm install -g sweet.js 安裝。如今 sjs 命令生效了,運行它用這些參數:

  • -o 輸出文件名,不然就打印到標準輸出中了
  • -c 生成一個 source map 方便 debug
  • -m 導入一組逗號分隔的模塊
  • -r 用更適合閱讀的變量名重命名變量 去除後面醜陋的 $1234 這種後綴,如今他只在 es5 下工做

若是你想編譯一個使用宏的 foo.js 而且用上 source map,你須要 sjs -c -o foo.built.js foo.js 你能用一個不一樣的擴展名 例如 .sjs

我推薦使用 https://github.com/evanw/node-source-map-support 這個node模塊,他自動處理錯誤信息,你只須要 require('source-map-support').install() 在文件頂部就能夠啦;

Grunt

最後,你會想要一個 Grunt 任務,這更容易,甚至自動添加了 source-map 支持(需配置)。

npm install grunt-sweet.js --save-dev 而後寫 Gruntfile.js 模仿個人示例項目 https://gist.github.com/jlongster/8838950

Gulp/Makefile/etc

https://github.com/sindresorhus/gulp-sweetjs gulp插件

第一部分教程結束

覆蓋了用 sweet.js 寫宏的基礎,接下來的教程我會更深刻,which I will release in a week or two. Follow my blog to see when that comes out. I am also speaking at MountainWestJS about macros March 17-18th so let me know if you'll be there!

(Thanks to Tim Disney, Nate Faubion, Dave Herman, and other who reviewed this)

相關文章
相關標籤/搜索