from http://oyanglul.us

Install Node

brew install node

node here is use for setup tools like grunt and mocha

npm install -g grunt

grunt is js build tool.

npm install -g bower

bower is package management for frontend js.

Install Dependencies

npm install

this will install all dependecies under package.json

  "devDependencies": {
    "grunt": "~0.4.2",
    "nunjucks": "~1.0.1",
    "grunt-nunjucks": "~0.1.0",
    "grunt-contrib-copy": "~0.5.0",
    "grunt-contrib-concat": "~0.3.0",
    "mocha": "~1.17.1",
    "grunt-contrib-uglify": "~0.3.2",
    "grunt-contrib-watch": "~0.5.3",
    "requirejs": "~2.1.11",
    "grunt-mocha": "~0.4.10"
}

Install Frontend Dependencies

bower install

same thing, bower install will install all dependencies of bower.json

 "devDependencies": {
 "bootswatch": "~3.1.1",
    "nunjucks": "~1.0.1",
    "chai": "~1.9.0",
    "sinon": "~1.8.2"
}

Run the mocha test

grunt mocha to run test with phantomjs

or just open tests/index.html with your browser.

chai is for assertion

nunjucks.render.calledOnce.should.be.true;

sinon for mock

sinon.stub(nunjucks,"render")

Precompile Template

we use nunjucks for template engine

nunjucks is very effective and powerful since it support precompile of templates, and python-like syntax.

grunt-nunjucks will compile the templates for us through grunt task

   nunjucks: {
      precompile: {
        src: 'src/templates/*',
        dest: 'javascripts/templates.js'
      }
    }

run grunt nunjucks will precompile all templates under src/templates/ into javascripts/tempalates.js

Generate Github API Token

open https://github.com/settings/applications and click generate token

copy the token. KEEP IT AS SAFE AS YOUR GITHUB PASSWORD

Run the app

simply run grunt will precomile nunjucks and copy javascripts from src into javascripts folder.

then python -m SimpleHTTPServer to start a server.

open localhost:8000

open console and put it

localStorage.setItem("access_token",<PASTE>); <RETURN>

refresh and you will see...

DONE

view raw 00 Setup Env.md hosted with ❤ by GitHub

TASK 1

get all gist list and render them into home page.

Check Points

  1. ajax get data.
  2. use nunjucks to render your data.
  3. mocha test your async code.

TASK 1 Walkthrough

Get Gist Data via API

function getGists(){
  return Q($.ajax({
    url:"https://api.github.com/users/"+login_user+"/gists?access_token=" + localStorage.getItem("access_token"),
    method:'get'
  }));
};

now we have the data, what we need to do is render the data into template, then put it on the page.

the data is wrapped with Q which will return a Promise object. with Promise, you can do

Q(do something aync).then(console.log)

Template

function renderGist(){
    getGists().then(function(body){
        $('.container .article').html(nunjucks.render("src/templates/gistlist.html",{gists:body}));
    }).then(bindEvent);
}

see, then nunjuncks.render will take 2 arguments, first the template address, then the data.

lets see what is in the template

<ul class="list-group">
    {% for gist in gists %}
    {% for name,file in gist.files%}
    <li class="list-group-item">
        <a href="#" data-id="{{gist.id}}" data-url="{{gist.html_url}}">
            {{name}}
        </a>
  </li>
    {% endfor%}
    {% endfor%}
</ul>

simple ehh, print the data value with {{}}

but how nunjucks find the template html.

a grunt task will precompile all html template file into javascript/templates.js

    nunjucks: {
      precompile: {
        src: 'src/templates/*',
        dest: 'javascripts/templates.js'
      }
    },

Test

how to test function with promise

    describe('article',function(){
        it('show all gist', function(done){
            var gists = [{
                html_url:"https://gist.github.com/2171fd506edc072ca80e"
            }];
            var gistsData = Q(gists);
            getGists = stub().returns(gistsData);
            renderGist();
            gistsData.should.be.fulfilled.then(function(){
                nunjucks.render.calledOnce.should.be.true;
                nunjucks.render.getCall(0).args[0].should.be.equal("src/templates/gistlist.html");
                nunjucks.render.getCall(0).args[1].should.be.deep.equal({gists:gists});
            }).should.notify(done);
        });
    });

the .should.be.fulfilled.then and notify is provide by chai promise plugin.

this test will check

  1. if the getGists is been called
  2. the Promise it returned should be fulfilled.
  3. nunjucks.render has been called
  4. nunjucks.render has been called with the right data.
  5. finally notify the done callback provide by mocha, which tell moche the test has been done.

BTW change the before and after to beforeEach, why

TASK 2: Play with Event

Bind event to items in the gists list, when I click, the content of the gist will be rendered.

Gists List

then detail page:

tip: use this api to get gist content https://gist.github.com/<YOURNAME>/<GIST ID>.json

example: https://gist.github.com/jcouyang/9362622.json

Check points

  1. use JSONP to cross origin ajax
  2. get data and render is async, make sure bind event after render.

TASK 2 Walkthrough

Cross Domain Ajax

$.getJSON($(this).data("url")+".json?callback=?"

use getJSON and add callback=? to get JSONP across origin

bind event

your code will may like this:

getGists().then(function(body){
        $('.container .article').html(nunjucks.render("src/templates/gistlist.html",{gists:body}));
}).then(bindEvent);

function bindEvent(){
    $(".article li a").click(function(){
        $.getJSON($(this).data("url")+".json?callback=?",function(data){
            $(".article").html(data.div);
        });
    });
}

renderHeader().then(renderGist);

bind event after render

now you'll find 2 problems here

  1. repeat code.
  2. how to go back from the gist detail page.

TASK 3

now your code should have repeat code

each time you want to add something new to the page, you'll probalily copy this code and make some minor change:

function renderGist(){
    getGists().then(function(body){
        $('.container .article').html(nunjucks.render("src/templates/gistlist.html",{gists:body}));
    }).then(bindEvent);
}

Once you have to copy and paste your code, that means you need refactory

look at the code and you'll see the common things: 1. get Data from backend 2. use the data to render template 3. bind event to the element after the template rendered.

So, TASK 3 would be:

extract the View out of the common code

Check Points

  1. remove repeat code and replace them with View
  2. View can be extend -- inheritance
  3. View should be easy to use like backbone

Hint:

this will explain how to implement simple javascript inheritance using underscore

function Mammal(name){ 
    this.name=name;
    this.offspring=[];
} 
Mammal.prototype.toString=function(){ 
    return '[Mammal "'+this.name+'"]';
}

Mammal.extend = function(props){
    var parent = this,child;
    child = function(){
        parent.apply(this, arguments)
    }
    var FixConstructor = function(){
        this.constructor = child;
    };
    FixConstructor.prototype = parent.prototype
    child.prototype = new FixConstructor();
    _.extend(child, parent);
    _.extend(child.prototype,props);
    return child;
}

var Cat = Mammal.extend({type:"Cat"})
var cat = new Cat("meow")

Walkthrough

prototype

 +----------------+ prototype+-----------------+
 |   Function     +----------+  function       |     
 +-------+--------+          +-----------------+     
           |                                               
           |Mammal instanceof Function === true            
           |.constructor                                           
   +-------+--------+ prototype+----------------+              
   | Mammal         +----------+  empty Object {}              
   +----------------+          +----------------+              

.. .. . . . . .Mammal.prototype.toString=function() .............

 +------------------+            +----------------------+
 |     Mammal       |.prototype  | {}.toString          |
 +------------------+            +-----------+----------+
 |      .extend     |                        ^ 
 +------------------+                        | 

.......^.......var mammal= new Mammal("dog") .|................... | |toString | Object.getPrototypeOf +-----------+-----------+ +---------------------------+ mammal | .constructor +-----------------------+

constructor

as you can see here, Mammal.constructor == Function

mammal.constructor == Mammal

so function Mammal() just like create new instance of Function

Function

  1. Function
new Function (arg1, arg2, ... argN, functionBody)
  1. function statment
function name([param[, param[, ... param]]]) {
   statements
   }
  1. function operater/expression
function [name]([param] [, param] [..., param]) {
   statements
}
  1. Function constructor vs. function declaration vs. function expression

Function constructor

var multiply = new Function("x", "y", "return x * y;");

function declaration

function multiply(x, y) {
   return x * y;
}

function expression

var multiply = function(x, y) {
   return x * y;
};

named function expression

var multiply = function func_name(x, y) {
   return x * y;
};

implement the inheritance of View,just like the mammal:

View.extend = function(props){
    var parent = this,child;
    child = function(){ 
        parent.apply(this, arguments);
    };                  
    child.parent=parent;  
    var FixConstructor = function(){
        this.constructor = child;
    };                  
    FixConstructor.prototype = parent.prototype;
    child.prototype = new FixConstructor();
    _.extend(child, parent);
    _.extend(child.prototype,props);
    return child;       
};                      
  1. the mighty basic View
_.extend(View.prototype, {
    el:$("body"),         
    templateEngine: nunjucks,
    template:"",          
    data:"",              
    events:"click body:", 
    initialize:function(){
    },                    
    getData:function(options){
        if (!this.data) return Q();
        var methodAndUrl = this.data.split('@');
        return Q($.ajax(_.extend({
            url:methodAndUrl[1],
            method:methodAndUrl[0]
        },options)));       
    },                    
    render:function(){    
        var self = this;    
        return this.getData(this.dataOptions).then(function(data){
            self.el.html(self.templateEngine.render(self.template, {data:data}));
        }).then(self.bindEvent.bind(self));
    },                    
    bindEvent:function(){ 
        var self = this;    
        var events = _.result(this, 'events');
        if (!events) return this;
        _.each(_(events).keys(),function(key){
            var match = key.match(delegateEventSplitter);
            var eventName = match[1], selector = match[2];
            if (selector==''){
                self.el.on(eventName, _.bind(self[events[key]],self));
            } else{           
                self.el.find(selector).on(eventName, _.bind(self[events[key]],self));
            }                 

        });                 
    }                     
});                     

Why Router

as you may notice that when go to the detail page, you can't navigate back to blog list.

so basically, router can let us navigate around in single page app.

TASK 4

here is how we gonno use the router.

we should able new a Router, and register location to an action.

var router = new Router();

router.get("/", function(){
    header.render().then(bloglist.render.bind(bloglist));
});

router.get("/:gistid",function(params){
    new BlogDetailView('https://gitst.github.com/'+ params.gistid +".json").render();
});

you'll gonna implement this Router

once we have Router, we can remove the useless event

'click li a':"renderDetail"

Hint

  1. there are a window event listener call hashchange

  2. you'll gonna like get help from regex

  3. popstate will help you manage history go back and forward.

Walkthrough

git checkout task.4

  1. regist what hash goes where
router.get("/", function(){
    console.log("homepage");
    header.render();
    bloglist.render();
});

this is define of a rout, when receive / call the function which render header and blogist.

  1. implement the register
Router.prototype.get = function(url,callback){
    var paramRegx = /:([^/.\\\\]+)/g;
    var param;
    var route = {
        paramNames:[],
        regex:'^'+ url +'$',
        params:{},
        callback:callback
    };
    while((param = paramRegx.exec(url))!==null){
        route.paramNames.push(param[1]);
        route.regex = route.regex.replace(param[0],"([^/.\\\\]+)").replace(":","");
    }
    this.routes.push(route);
};

we create a Router object which contain a list of routes, whenever get is called, regist the route as an route hash like

    var route = {
        paramNames:[],
        regex:'^'+ url +'$',
        params:{},
        callback:callback
    };

which contains a regex to match hash, paramname array, params dict, and a callback.

  1. then we need to listen to the hashchange, and find the route according to the hash.
$(window).on("hashchange",function(e){
findRoute(e);
}
  1. what the findRoute actually do
_.each(self.routes,function(route){
            var regex = new RegExp(route.regex,"g");
            if((values = regex.exec(hash))!==null){
                console.log(values);
                values.shift();
                route.callback(_.object(route.paramNames,values));
                return;
            }

findRoute need to iterate routes we registed before, find the one who match the hash loaction, then call it's callback.

view raw 04 We-Need-Router.md hosted with ❤ by GitHub

Currently we put all our job into View, like define where to fetch data, fetch and operate on data, which is not good and should be done by Model.

TASK 5

extract Model out of View.

Hint

better use Q.defer to define your data which need to wait.

view raw 05 Model.md hosted with ❤ by GitHub

oyanglulu
738 声望20 粉丝