Save 37% off PRO during our Black Friday Sale! »

捗るかもしれないフロントエンド開発環境

45daf58c77e9dbbab5a1c8a5afc7ac5c?s=47 koba04
October 21, 2013

 捗るかもしれないフロントエンド開発環境

LiveReload
connect + proxy + easymock
testem
mocha + expect + sinon
assemble、foreman....
などを試してみた話です

45daf58c77e9dbbab5a1c8a5afc7ac5c?s=128

koba04

October 21, 2013
Tweet

Transcript

  1. ḿΔ͔΋͠Εͳ͍ϑϩϯτΤϯυ։ൃ؀ڥ @koba04 (2012/10/21) 13೥10݄23೔ਫ༵೔

  2. ໨త • ͳΔ΂ࣗ͘ಈԽ • ςετΛॻ͘ • αʔόʔͱϑϩϯτͷ։ൃΛૄ݁߹ʹ • ָ͘͠։ൃ 13೥10݄23೔ਫ༵೔

  3. ૝ఆ • Backbone.js + Handlebars΍Angular.jsΛ࢖ͬͯɺΫϥΠΞϯτଆͰDOMΛ૊ Έཱ͍ͯͯ͘ΞϓϦΛ૝ఆ • αʔόʔଆ͸APIΛJSONͳͲͰฦ͚ͩ͢ 13೥10݄23೔ਫ༵೔

  4. gruntͷpluginΛ࢖͏ 13೥10݄23೔ਫ༵೔

  5. gruntͷϓϥάΠϯΛ࢖͏ • Πϯετʔϧ͍ͨ͠ϓϥάΠϯΛnpm install xxx --save-dev ͢Δ • Gruntfile.coffee(js) ͷதͰgrunt.loadTasksͯ͠ϓϥάΠϯͷઃఆΛॻ͚ͩ͘

    % npm install grunt-contrib-coffee --save-dev % cat Gruntfile.coffee module.exports = (grunt) -> grunt.loadNpmTasks "grunt-contrib-coffee" grunt.initConfig coffee: compile: files: “public/js/app.js”: [ “coffee/lib.coffee” “coffee/app.coffee” ] % grunt coffee 13೥10݄23೔ਫ༵೔
  6. livereload + watch + connectͰF5ΛࣗಈԽ 13೥10݄23೔ਫ༵೔

  7. LiveReloadͱ͸ • ϑΝΠϧΛมߋͨ͠Βϒϥ΢βΛࣗಈతʹ࠶ಡΈࠐΈͯ͘͠ΕΔ LiveReload Server ϒϥ΢β֦ுΛ࢖͏͔ɺ LiveReload༻ͷScriptΛ ຒΊࠐΜͰ͓͘ 13೥10݄23೔ਫ༵೔

  8. LiveReloadͱ͸ • ϑΝΠϧΛมߋͨ͠Βϒϥ΢βΛࣗಈతʹ࠶ಡΈࠐΈͯ͘͠ΕΔ LiveReload Server ϒϥ΢β֦ுΛ࢖͏͔ɺ LiveReload༻ͷScriptΛ ຒΊࠐΜͰ͓͘ ϑΝΠϧߋ৽ 13೥10݄23೔ਫ༵೔

  9. LiveReloadͱ͸ • ϑΝΠϧΛมߋͨ͠Βϒϥ΢βΛࣗಈతʹ࠶ಡΈࠐΈͯ͘͠ΕΔ ϑΝΠϧͷ؂ࢹͯ͠ มߋ͕͋Ε͹௨஌ʂ LiveReload Server ϒϥ΢β֦ுΛ࢖͏͔ɺ LiveReload༻ͷScriptΛ ຒΊࠐΜͰ͓͘

    ϑΝΠϧߋ৽ 13೥10݄23೔ਫ༵೔
  10. LiveReloadͱ͸ • ϑΝΠϧΛมߋͨ͠Βϒϥ΢βΛࣗಈతʹ࠶ಡΈࠐΈͯ͘͠ΕΔ ϑΝΠϧͷ؂ࢹͯ͠ มߋ͕͋Ε͹௨஌ʂ LiveReload Server ϒϥ΢β֦ுΛ࢖͏͔ɺ LiveReload༻ͷScriptΛ ຒΊࠐΜͰ͓͘

    ࠶ಡΈࠐΈ͢ΔΑ͏ ϒϥ΢βʹ௨஌ʂ ϑΝΠϧߋ৽ 13೥10݄23೔ਫ༵೔
  11. LiveReloadͱ͸ • ϑΝΠϧΛมߋͨ͠Βϒϥ΢βΛࣗಈతʹ࠶ಡΈࠐΈͯ͘͠ΕΔ ϑΝΠϧͷ؂ࢹͯ͠ มߋ͕͋Ε͹௨஌ʂ LiveReload Server ϒϥ΢β֦ுΛ࢖͏͔ɺ LiveReload༻ͷScriptΛ ຒΊࠐΜͰ͓͘

    ࠶ಡΈࠐΈ͢ΔΑ͏ ϒϥ΢βʹ௨஌ʂ ࠶ಡΈࠐΈʂ ϑΝΠϧߋ৽ 13೥10݄23೔ਫ༵೔
  12. ඞཁͳ΋ͷ • grunt-contrib-watch • ϑΝΠϧͷ؂ࢹͱLiveReload Server (tiny-lr) • grunt-contrib-connect •

    LiveReloadͷͨΊͷScriptΛHTMLʹຒΊࠐΜͰ഑৴ͯ͘͠ΕΔ 13೥10݄23೔ਫ༵೔
  13. grunt-contrib-watch • ؂ࢹ͢ΔϑΝΠϧͷઃఆͱLiveReloadͷ༗ޮԽ watch: options: livereload: true coffee: files: "coffee/**/*.coffee"

    tasks: ["coffee2js"] handlebars: files: "template/**/*.hbs" tasks: ["handlebars"] scss: files: "scss/**/*.scss" tasks: ["compass"] assemble: files: "assemble/**/*" tasks: ["assemble"] 13೥10݄23೔ਫ༵೔
  14. grunt-contrib-connect • ੩తϑΝΠϧ഑৴αʔόʔͷઃఆ connect: server: options: livereload: true port: 9000

    base: "public" 13೥10݄23೔ਫ༵೔
  15. connect + proxy + easymock Ͱ αʔόʔ͔Β੾Γ཭͢ 13೥10݄23೔ਫ༵೔

  16. connect + proxy + easymock • connectͰαʔόʔΛཱͯͯɺproxyڬΜͰ೚ҙʹAPIΛࢦఆग़དྷΔΑ͏ʹͨ͠ ΓɺmockͰఆٛͨ͠JSONฦ͢Α͏ʹ͢Δ • αʔόʔଆͷ։ൃͷ͜ͱΛؾʹͤͣɺAPIͷ࢓༷ΛܾΊ͓͚ͯ͹ϑϩϯτΤϯ

    υͷ։ൃΛਐΊΔ͜ͱ͕ग़དྷΔ • gruntͷΦϓγϣϯͰAPIͷ઀ଓઌΛࢦఆग़དྷΔΑ͏ʹ͓ͯ͘͜͠ͱͰɺAPIͱ ͷ݁߹ςετ΋؆୯ʹग़དྷΔ 13೥10݄23೔ਫ༵೔
  17. connect + proxy + easymock Ͱͷߏ੒ HTTP /api/hoge proxy server

    A͞Μͷ։ൃ؀ڥ ઃఆʹΑͬͯ઀ଓઌΛ ม͑Δ stage؀ڥ easymockʹΑΔ mock؀ڥ ※Access-Control-Allow-Origin Headerͷઃఆ͕ඞཁ 13೥10݄23೔ਫ༵೔
  18. grunt-contrib-connect, grunt-connect-proxy • proxyͷઃఆ proxyHost = if grunt.option 'proxy_host' ?

    then grunt.option 'proxy_host' else null : connect: server: options: livereload: true port: 9000 base: "public" middleware: (connect, options) -> [ connect.static(options.base) require("grunt-connect-proxy/lib/utils").proxyRequest ] proxies: [ context: [ "/users/" "/items/" ] host: if proxyHost? then proxyHost else "localhost" port: if proxyHost? then 80 else 3000 changeOrigin: true ] % grunt --proxy_host example.com ͷΑ͏ʹ઀ଓઌΛࢦఆग़དྷΔΑ͏ʹ 13೥10݄23೔ਫ༵೔
  19. easymock (npm install -g easymock) • JSONϑΝΠϧΛஔ͚ͩ͘Ͱ؆୯ʹAPIͷϞοΫΛ࡞Δ͜ͱ͕ग़དྷΔ • proxyઌͷαʔόʔ΋ࢦఆͰ͖ΔͷͰɺ࣮૷ग़དྷ͍ͯͳ͍API͚ͩϞοΫ͢Δ ͜ͱ΋Մೳ

    • APIͷυΩϡϝϯτϖʔδ΋࡞ͬͯ͘ΕΔͷͰ࢓༷ͱͯ͠΋࢖͑ͦ͏ 13೥10݄23೔ਫ༵೔
  20. easymock (config.json) • config.jsonʹઃఆΛॻ͘ { "proxy": { "server": "http://example.com", "calls":

    { "/items/": { "get": true } } }, "routes": [ "/users/:id" ] } proxy͍ͤͨ͞αʔόʔ͕͋Ε͹ઃఆ callsͰserverʹ౉͢ϦΫΤετΛఆٛ /user/1, user/2 ͷΑ͏ͳ ύεʹ஋͕෇͍ͯ͘ΔϧʔςΟϯάΛఆٛ 13೥10݄23೔ਫ༵೔
  21. easymock (response) • APIߏ଄ʹԊͬͨσΟϨΫτϦߏ଄Λ࡞Δ % tree . !"" users #

    $"" _get.json ← GET /users/ ʹର͢ΔϨεϙϯε # !"" id_get.json ← GET /users/:id ʹର͢ΔϨεϙϯε !"" ... % cat users/id_get.json { "id": #{id}, ← /user/:id ͷidΛม਺ͱͯ͠࢖༻Ͱ͖Δ "name": "Sasuke", "age": 15, "message": "I'm Ninja" } • ϨεϙϯεͷJSONΛ഑ஔ 13೥10݄23೔ਫ༵೔
  22. easymock (document) 13೥10݄23೔ਫ༵೔

  23. testem + phantomjs + browser Ͱշదςετ 13೥10݄23೔ਫ༵೔

  24. JavaScriptͷςετ • ༷ʑͳϨΠϠʔɺϥΠϒϥϦ͕͋ͬͯΑ͘Θ͔Βͳ͍... • TestRunner ... testem, karma • ࣮ߦ؀ڥ

    ... browser, phantomjs • Testing Framework ... mocha, Jasmine, Qunit • assertion library ... chai, expect, power assert 13೥10݄23೔ਫ༵೔
  25. JavaScriptͷςετ • ༷ʑͳϨΠϠʔɺϥΠϒϥϦ͕͋ͬͯΑ͘Θ͔Βͳ͍... • TestRunner ... testem, karma • ࣮ߦ؀ڥ

    ... browser, phantomjs • Testing Framework ... mocha, Jasmine, Qunit • assertion library ... chai, expect, power assert ͱΓ͋͑ͣ࢖ͬͯΈΔͱΘ͔ͬͯ͘ΔͷͰ testem + phantonjs & browser + mocha + expect Λࢼͯ͠ΈΔ 13೥10݄23೔ਫ༵೔
  26. testem • JavaScriptͷςετͷ࣮ߦΛ؅ཧͯ͘͠ΕΔ • ࣮ߦ͢Δϒϥ΢βͷࢦఆɺมߋΛ؂ࢹͯ͠ͷࣗಈ࣮ߦɺςετϑϨʔϜϫ ʔΫͷαϙʔτ 13೥10݄23೔ਫ༵೔

  27. grunt-contrib-testem • grunt-contrib-testemೖΕͯɺGruntfileॻ͚ͩ͘ͰOK testem: app: src: [ "bower_components/expect/expect.js" "bower_components/sinon/index.js" "public/js/vendor.js"

    "public/js/template.js" "public/js/app.js" "spec/**/*_spec.coffee" ] options: test_page: "spec/runner.mustache" #parallel: 4 launch_in_dev: ["PhantomJS", "Chrome", "Safari"] launch_in_ci: ["PhantomJS", "Firefox"] ϥΠϒϥϦɺΞϓϦͷίʔυɺςετΛࢦఆ ࣮ߦ͢Δϒϥ΢β ςετΛ࣮ߦ͢ΔHTML mustacheͰ΋OK 13೥10݄23೔ਫ༵೔
  28. grunt-contrib-testem(test_page) <!doctype html> <html> <head> <title>Test'em</title> <link rel="stylesheet" href="/testem/mocha.css"> <script

    src="/testem/mocha.js"></script> <script src="/testem.js"></script> <script> mocha.setup('bdd') </script> </head> <body> <div id="mocha"></div> <!-- for unit test --> <article style="display: none;"> <section id="mainview"></section> <section id="subview"></section> </article> {{#serve_files}} <script src="{{src}}"></script> {{/serve_files}} <script> mocha.run() </script> </body> </html> testemʹؚ·Ε͍ͯΔmochaͷϑΝΠϧͱ testem.jsΛಡΈࠐΉ GruntfileͰsrcʹॻ͍͓͍ͯͨ ϑΝΠϧΛsrcࢦఆ mochaͰ࣮ߦ mochaͷsetup DOMͷ४උ 13೥10݄23೔ਫ༵೔
  29. mocha + expect + sinon Ͱָ͘͠ςετ 13೥10݄23೔ਫ༵೔

  30. mocha + expect + sinon Ͱςετ • mocha͸࠷௿ݶͷςετϑϨʔϜϫʔΫͳͷͰɺassertion library΍mockతͳ ΋ͷ͸ࣗ෼ͰબͿඞཁ͕͋Δ

    (Jasmine͸શ෦ೖͬͯΔ) • ࠓճassertͷछྨ͕ଟ͔ͬͨͷͰexpect.jsΛ࢖༻ɻpower-assert΋γϯϓϧͰ Θ͔Γ΍͍݁͢ՌΛग़ྗͯ͘͠ΕΔͷͰؾʹͳ͍ͬͯΔ • stubɺmockϥΠϒϥϦ͸sinon.jsΛ࢖༻ • HTTP Request΍࣌ؒͷϞοΫɺ͜ͷؔ਺͕Ͳ͏͍͏Ҿ਺ͰԿճݺ͹Εͨͷ ͔(spy)ͳͲɺ୯ମςετʹ͸ඞਢ 13೥10݄23೔ਫ༵೔
  31. mochaͰͷςετ݁Ռͷग़ྗ (browser) • ςετ݁Ռ͕៉ྷʹग़ྗ͞ΕΔ͜ͱ͸ॏཁ 13೥10݄23೔ਫ༵೔

  32. mochaͰͷςετ describe("model.Base", -> describe "override Model.sync", -> user = null

    server = null User = myapp.model.Base.extend urlRoot: "/users/" initialize: (attrs) -> @setStorage "model:user:#{attrs.id}" before -> server = sinon.fakeServer.create() server.respondWith "GET", /\/users\//, [ 200, {}, JSON.stringify id: 1, name: "jim", age: 21 ] after -> server.restore() beforeEach -> user = new User id: 1, name: "jim", age: 20 it "set storage fetched data", -> spy = sinon.spy user.storage, 'set' user.fetch() server.respond() expect(spy.calledOnce).to.be.ok() expect(spy.args[0]).to.be.eql [ id:1, name:"jim", age:21 "read" ] spy.reset() ) coffeescriptͰॻ͚Δ serverͷmock࡞ͬͯ GET /users/ʹର͢Δ ϨεϙϯεΛఆٛ before͸࠷ॳʹ1ճ after͸࠷ޙʹ1ճ (serverΛ໭ͯ͠Δ) beforeEach͸ ຖճ࠷ॳʹ1ճ (afterEach΋͋Δ) spyͨؔ͠਺ͷ ঢ়ଶΛςετ 13೥10݄23೔ਫ༵೔
  33. Travis-CIͰ͞ΒͳΔຬ଍ײΛ 13೥10݄23೔ਫ༵೔

  34. Travis-CI • GitHubͰެ։ͯ͠ΔͳΒઃఆ͓ͯ͘͠΂͖ʢGreenʹͳΔ͜ͱͷຬ଍ײʣ • PhantomJSͱFirefoxͰςετ͕ग़དྷΔ 13೥10݄23೔ਫ༵೔

  35. Travis-CI • travis.yml language: node_js node_js: - "0.10" before_script: -

    npm install -g grunt-cli bower - bower install - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" FirefoxͰςετΛ͢ΔͨΊʹඞཁ • package.json • Gruntfile.coffee "scripts": { "test": "grunt testem:ci:app" }, launch_in_ci: ["PhantomJS", "Firefox"] npm test Ͱ࣮ߦͰ͖ΔΑ͏ʹ͢Δ PhantomJSͱFIrefoxΛࢦఆ 13೥10݄23೔ਫ༵೔
  36. assembleͰޮ཰HTML؅ཧ 13೥10݄23೔ਫ༵೔

  37. assembleͱ͸ʁͦͯ͠࢖͏ཧ༝ʁ • assembleͱ͸ɺ੩తαΠτΛ࡞ΔͨΊͷgruntϓϥάΠϯɻ(like jekyll) • grunt-xxxxͰͳ͍͚ͲGruntͷϓϥάΠϯ • YAML(JSON)Λఆٛͨ͠΋ͷΛHandlebarsΛ࢖ͬͯHTMLʹు͖ग़͢͜ͱ͕ग़ དྷΔ •

    ن໿ͱ͔ϔϧϓͱ͔ΛYAMLͰ؅ཧ͍ͨ͠ • αʔόʔαΠυͷςϯϓϨʔτΤϯδϯΛ࢖Θͳ͍͚ͲɺimgɺjsɺcssͳͲΛ ຊ൪ͱ։ൃͰ෼͚ͨΓόʔδϣϯΛ෇Ճ͍ͨ͠ 13೥10݄23೔ਫ༵೔
  38. assemble (Gruntfile) assemble: term: options: ext: ".hbs" data: "assemble/data/term.yaml" layout:

    "assemble/layout/term.hbs" files: [ expand: true cwd: "assemble/template/term" src: "**/*.hbs" dest: "template/term" ] publicHTMLDevelop: options: product: false jsVersion: 1 cssVersion: 1 files: [ src: "assemble/template/public_html/index.hbs" dest: "public/index.html" ] publicHTMLProduct: options: product: true jsVersion: 1 cssVersion: 1 staticHost: staticHost files: [ src: "assemble/template/public_html/index.hbs" dest: "public/index.product.html" ] ن໿ΛYAMLΛݩʹlayout/term.hbsΛݩʹɺ term/ҎԼͷhbsϑΝΠϧΛ࢖ͬͯ࡞੒͠ɺ ͜͜Ͱ͸͞ΒʹhbsϑΝΠϧͱͯ͠࢖༻ (BackboneͷςϯϓϨʔτͱͯ͠࢖͍͍ͨͨΊ) ຊ൪ͱ։ൃ༻ͷHTMLɻ productม਺ʹΑͬͯ index.hbs಺Ͱग़͠෼͚Δ 13೥10݄23೔ਫ༵೔
  39. assemble (public/index.html) <!DOCTYPE html> <!-- generated by assemble --> <html

    lang="ja"> <head> <title>backbone and handlebars sample</title> <meta http-equiv="Cache-Control" content="no-cache"> <meta http-equiv="Pragma" content="no-cache"> <meta charset="utf8" /> {{#if product}} <link rel="stylesheet" href="{{staticHost}}/css/product/app.css?v={{cssVersion}}"> {{/if}} {{#unless product}} <link rel="stylesheet" href="/css/app.css?v={{cssVersion}}"> {{/unless}} </head> <body> : {{#if product}} <script src="{{staticHost}}/js/vendor.js?v={{jsVersion}}"></script> <script src="{{staticHost}}/js/template.product.js?v={{jsVersion}}"></script> <script src="{{staticHost}}/js/app.product.js?v={{jsVersion}}"></script> {{/if}} {{#unless product}} <script src="/js/vendor.js?v={{jsVersion}}"></script> <script src="/js/template.js?v={{jsVersion}}"></script> <script src="/js/app.js?v={{jsVersion}}"></script> {{/unless}} </body> </html> productม਺ʹΑͬͯ index.hbs಺Ͱग़͠෼͚Δ 13೥10݄23೔ਫ༵೔
  40. assemble (term) assemble/data/term.yaml term1: title: term1 content: this is term1

    content term2: title: term2 content: this is term2 content assemble/layout/term.hbs <div> <!-- generated by assemble --> {{> body}} </div> assemble/template/term/term1.hbs <h1>{{term.term1.title}}</h1> <p>{{term.term1.content}}</p> assemble/template/term/term2.hbs <h1>{{term.term2.title}}</h1> <p>{{term.term2.content}}</p> template/term/term1.hbs <div> <!-- generated by assemble --> <h1>term1</h1> <p>this is term1 content</p> </div> template/term/term2.hbs <div> <!-- generated by assemble --> <h1>term2</h1> <p>this is term2 content</p> </div> YAMLܗࣜͷ σʔλ ϨΠΞ΢τ ςϯϓϨʔτ ֤ϖʔδͷ ςϯϓϨʔτ ੜ੒͞Εͨ ςϯϓϨʔτ 13೥10݄23೔ਫ༵೔
  41. foremanͰ·ͱΊͯϓϩηε؅ཧ 13೥10݄23೔ਫ༵೔

  42. foreman • ϓϩηε؅ཧͷRubyGems • easymockͱgruntΛ·ͱΊͯىಈ͢Δͷʹ࢖༻ • gruntͷtaskͰ΋ग़དྷΔ͚Ͳɺޙʑ૿͑Δ͜ͱ΋ߟ͑Δͱforemanͷํָ͕ • Procfileʹ࣮ߦίϚϯυॻ͚ͩ͘ %

    cat Procfile grunt: grunt easymock: sh easymock.sh 13೥10݄23೔ਫ༵೔
  43. foreman 13೥10݄23೔ਫ༵೔

  44. grunt-notifyͰ௨஌ 13೥10݄23೔ਫ༵೔

  45. grunt-notify • ۭؾͷΑ͏ʹcoffeeͱ͔scssͷίϯύΠϧͯ͘͠ΕΔ͚Ͳɺࣦഊͨ͠ͱ͖͸ڭ ͑ͯཉ͍͠ • grunt-notifyΛ࢖ͬͯgrowl ͔௨஌ηϯλʔͰ௨஌͢Δ • Mac OS

    X 10.7ҎԼͷ৔߹͸ɺGrowl͚ͩͰͳ͘GrowlNotify΋Πϯετʔϧ͢ Δඞཁ͕͋Δ • http://growl.info/downloads#generaldownloads • Mac OS X 10.8Ҏ্ͷ৔߹͸ɺ௨஌ηϯλʔͰ௨஌ͯ͘͠ΕΔͷͰԿ΋ඞཁͳ ͍ 13೥10݄23೔ਫ༵೔
  46. ։ൃͷ༷ࢠ 13೥10݄23೔ਫ༵೔

  47. ։ൃͷ༷ࢠ gruntͱeasymock ͷ֬ೝ ςετ݁Ռ ͷ֬ೝ 13೥10݄23೔ਫ༵೔

  48. ։ൃͷ༷ࢠ gruntͱeasymock ͷ֬ೝ ςετ݁Ռ ͷ֬ೝ ίʔυΛॻ͍ͯΔ ͚ͩͰࣗಈ࣮ߦ͞Εͯ ݁ՌΛ֬ೝग़དྷΔʂ 13೥10݄23೔ਫ༵೔

  49. ͓·͚ 13೥10݄23೔ਫ༵೔

  50. grunt-remove-logging • console.logΛফͯ͘͠ΕΔͷͰσόοά༻ʹΨϯΨϯconsole.logΛೖΕ͓ͯ ͚Δ • product༻ʹugilify͢Δલʹ࢖ͬͯΔ removelogging: product: src: "public/js/app.js"

    dest: "public/js/app.product.js" 13೥10݄23೔ਫ༵೔
  51. matchdep • grunt.loadNpmTasksΛશ෦͢Δͷ͕໘౗Ͱ࢖ͬͯΔ require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks) grunt.loadNpmTasks "assemble" # grunt-*ͳ໊લͰ͸ͳ͍ͷͰ.... 13೥10݄23೔ਫ༵೔

  52. grunt-contrib-handlebarsͰCDNରԠ • processContentΛ࢖͏͜ͱͰɺίϯύΠϧ࣌ʹॲཧΛڬΊΔͷͰsrcଐੑͷ஋ ΛFQDNʹஔ׵͢Δͱ͔ग़དྷΔ staticHost = "http://example.com" grunt.initConfig : :

    handlebars: options: processContent: (content) -> content.replace /src="(.*)?"/gm, "src=\"#{staticHost}$1\"" 13೥10݄23೔ਫ༵೔
  53. ͓͠·͍ https://github.com/koba04/backbone-boilerplate 13೥10݄23೔ਫ༵೔