[Node.js基礎]學習②⑤--開發(fā)REST API

http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/0014735944539193ab2edd2740f44a79efb438a05e83727000

package.json

{
    "name": "rest-koa",
    "version": "1.0.0",
    "description": "koa 2 example with rest api",
    "main": "app.js",
    "scripts": {
        "start": "node --use_strict app.js"
    },
    "keywords": [
        "koa",
        "rest",
        "api"
    ],
    "author": "Michael Liao",
    "license": "Apache-2.0",
    "repository": {
        "type": "git",
        "url": "https://github.com/michaelliao/learn-javascript.git"
    },
    "dependencies": {
        "koa": "2.0.0",
        "koa-bodyparser": "3.2.0",
        "koa-router": "7.0.0",
        "nunjucks": "2.4.2",
        "mime": "1.3.4",
        "mz": "2.4.0"
    }
}

views

base.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="description" content="learn javascript by www.liaoxuefeng.com">
    <title>{{ title }}</title>
    <link rel="stylesheet" href="/static/css/bootstrap.css">
    <script src="/static/js/jquery.min.js"></script>
    <script src="/static/js/vue.min.js"></script>
    <script src="/static/js/bootstrap.js"></script>
</head>

<body>
    <header class="navbar navbar-static-top">
        <div class="container">
            <div class="navbar-header">
                <a href="/" class="navbar-brand">Learn JavaScript</a>
            </div>
            <nav class="collapse navbar-collapse" id="bs-navbar">
                <ul class="nav navbar-nav">
                    <li><a target="_blank" >Get Courses</a></li>
                    <li><a target="_blank" >Source Code</a></li>
                    <li><a target="_blank" >Resource</a></li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/signout">Sign Out</a></li>
                </ul>
            </nav>
        </div>
    </header>
    <div id="important" style="color:#cdbfe3; background-color:#6f5499; padding:30px 0; margin:-20px 0 20px 0;">
        <div class="container">
            <h1 style="color:#fff; font-size:60px">Getting started with REST API!</h1>
            <p style="font-size:24px; line-height:48px">Learn JavaScript, Node.js, REST, WebSocket, npm, koa2, nunjucks, babel, etc. at liaoxuefeng.com.</p>
        </div>
    </div>
    {% block main %} {% endblock %}
    <footer style="background-color:#ddd; padding: 20px 0;">
        <div class="container">
            <p>
                <a target="_blank" >Website</a> - 
                <a target="_blank" >GitHub</a> - 
                <a target="_blank" >Weibo</a>
            </p>
            <p>This JavaScript course is created by <a target="_blank" >@廖雪峰</a>.</p>
            <p>Code licensed <a target="_blank" >Apache</a>.</p>
        </div>
    </footer>
</body>

</html>

index.html

{% extends "base.html" %} {% block main %}

<script>

$(function () {
    var vm = new Vue({
        el: '#product-list',
        data: {
            products: []
        },
        methods: {
            deleteProduct: function (id) {
                var that = this;
                // AJAX提交JSON:
                $.ajax({
                    type: 'delete',
                    dataType: 'json',
                    url: '/api/products/' + id
                }).done(function (r) {
                    console.log(r);
                    var i;
                    for (i=0; i<that.products.length; i++) {
                        if (that.products[i].id === r.id) {
                            that.products.splice(i, 1);
                            return;
                        }
                    }
                }).fail(function (jqXHR, textStatus) {
                    // Not 200:
                    alert('Error: ' + jqXHR.status);
                });
            }
        }
    });

    $.getJSON('/api/products').done(function (data) {
        vm.products = data.products;
    }).fail(function (jqXHR, textStatus) {
        alert('Error: ' + jqXHR.status);
    });

    $('#product-form').submit(function (e) {
        e.preventDefault();
        var
            product = {
                name: $(this).find('input[name=name]').val(),
                manufacturer: $(this).find('input[name=manufacturer]').val(),
                price: parseFloat($(this).find('input[name=price]').val())
            };
        // AJAX提交JSON:
        $.ajax({
            type: 'post',
            dataType: 'json',
            contentType: 'application/json',
            url: '/api/products',
            data: JSON.stringify(product)
        }).done(function (r) {
            vm.products.push(r);
        }).fail(function (jqXHR, textStatus) {
            // Not 200:
            alert('Error: ' + jqXHR.status);
        });
    });
});
</script>

<div class="container">
    <div class="row">
        <div class="col-md-8">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><span class="glyphicon glyphicon-th-list"></span> Products</h3>
                </div>
                <div class="panel-body">
                    <table id="product-list" class="table table-hover">
                        <thead>
                            <tr>
                                <th style="width:50px"></th>
                                <th>Product</th>
                                <th style="width:150px">Price</th>
                                <th style="width:50px"></th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="p in products">
                                <td>
                                    ![](/static/images/icon.png)
                                </td>
                                <td>
                                    <h4 class="media-heading" v-text="p.name"></h4>
                                    <p><span v-text="p.manufacturer"></span> <span><a v-on:click="deleteProduct(p.id)" href="#0">delete</a></span></p>
                                </td>
                                <td>
                                    <p style="font-size:2em">¥ <span v-text="p.price"></span></p>
                                </td>
                                <td><a target="_blank" v-bind:href="'http://search.jd.com/Search?enc=utf-8&keyword=' + encodeURIComponent(p.name)">Buy</a>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><span class="glyphicon glyphicon-user"></span> Create New Product</h3>
                </div>
                <div class="panel-body">
                    <form id="product-form">
                        <div class="form-group">
                            <label>Name</label>
                            <input type="text" name="name" class="form-control" placeholder="Product name" value="">
                            <p class="help-block">Product name</p>
                        </div>
                        <div class="form-group">
                            <label>Manufacturer</label>
                            <input type="text" name="manufacturer" class="form-control" placeholder="Manufacturer name" value="">
                            <p class="help-block">Manufacturer name</p>
                        </div>
                        <div class="form-group">
                            <label>Price</label>
                            <input type="number" name="price" class="form-control" placeholder="Product price" value="">
                            <p class="help-block">Product price</p>
                        </div>
                        <div class="form-group">
                            <button type="submit" class="btn btn-primary">Create</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <h1>Get more courses...</h1>
        </div>
    </div>
    <div class="row">
        <div class="col-md-4">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">JavaScript</h3>
                </div>
                <div class="panel-body">
                    <p>full-stack JavaScript course</p>
                    <p><a target="_blank" >Read more</a></p>
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">Python</h3>
                </div>
                <div class="panel-body">
                    <p>the latest Python course</p>
                    <p><a target="_blank" >Read more</a></p>
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">git</h3>
                </div>
                <div class="panel-body">
                    <p>A course about git version control</p>
                    <p><a target="_blank" >Read more</a></p>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

static-files.js

const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');

function staticFiles(url, dir) {
    return async (ctx, next) => {
        let rpath = ctx.request.path;
        if (rpath.startsWith(url)) {
            let fp = path.join(dir, rpath.substring(url.length));
            if (await fs.exists(fp)) {
                ctx.response.type = mime.lookup(rpath);
                ctx.response.body = await fs.readFile(fp);
            } else {
                ctx.response.status = 404;
            }
        } else {
            await next();
        }
    };
}

module.exports = staticFiles;

templating.js

const nunjucks = require('nunjucks');

function createEnv(path, opts) {
    var
        autoescape = opts.autoescape === undefined ? true : opts.autoescape,
        noCache = opts.noCache || false,
        watch = opts.watch || false,
        throwOnUndefined = opts.throwOnUndefined || false,
        env = new nunjucks.Environment(
            new nunjucks.FileSystemLoader(path, {
                noCache: noCache,
                watch: watch,
            }), {
                autoescape: autoescape,
                throwOnUndefined: throwOnUndefined
            });
    if (opts.filters) {
        for (var f in opts.filters) {
            env.addFilter(f, opts.filters[f]);
        }
    }
    return env;
}

function templating(path, opts) {
    var env = createEnv(path, opts);
    return async (ctx, next) => {
        ctx.render = function (view, model) {
            ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
            ctx.response.type = 'text/html';
        };
        await next();
    };
}

module.exports = templating;

product.js

// store products as database:

var id = 0;

function nextId() {
    id++;
    return 'p' + id;
}

function Product(name, manufacturer, price) {
    this.id = nextId();
    this.name = name;
    this.manufacturer = manufacturer;
    this.price = price;
}

var products = [
    new Product('iPhone 7', 'Apple', 6800),
    new Product('ThinkPad T440', 'Lenovo', 5999),
    new Product('LBP2900', 'Canon', 1099)
];

module.exports = {
    getProducts: () => {
        return products;
    },

    getProduct: (id) => {
        var i;
        for (i = 0; i < products.length; i++) {
            if (products[i].id === id) {
                return products[i];
            }
        }
        return null;
    },

    createProduct: (name, manufacturer, price) => {
        var p = new Product(name, manufacturer, price);
        products.push(p);
        return p;
    },

    deleteProduct: (id) => {
        var
            index = -1,
            i;
        for (i = 0; i < products.length; i++) {
            if (products[i].id === id) {
                index = i;
                break;
            }
        }
        if (index >= 0) {
            // remove products[index]:
            return products.splice(index, 1)[0];
        }
        return null;
    }
};

controller.js


const fs = require('fs');

// add url-route in /controllers:

function addMapping(router, mapping) {
    for (var url in mapping) {
        if (url.startsWith('GET ')) {
            var path = url.substring(4);
            router.get(path, mapping[url]);
            console.log(`register URL mapping: GET ${path}`);
        } else if (url.startsWith('POST ')) {
            var path = url.substring(5);
            router.post(path, mapping[url]);
            console.log(`register URL mapping: POST ${path}`);
        } else if (url.startsWith('PUT ')) {
            var path = url.substring(4);
            router.put(path, mapping[url]);
            console.log(`register URL mapping: PUT ${path}`);
        } else if (url.startsWith('DELETE ')) {
            var path = url.substring(7);
            router.del(path, mapping[url]);
            console.log(`register URL mapping: DELETE ${path}`);
        } else {
            console.log(`invalid URL: ${url}`);
        }
    }
}

function addControllers(router, dir) {
    fs.readdirSync(__dirname + '/' + dir).filter((f) => {
        return f.endsWith('.js');
    }).forEach((f) => {
        console.log(`process controller: ${f}...`);
        let mapping = require(__dirname + '/' + dir + '/' + f);
        addMapping(router, mapping);
    });
}

module.exports = function (dir) {
    let
        controllers_dir = dir || 'controllers',
        router = require('koa-router')();
    addControllers(router, controllers_dir);
    return router.routes();
};

api.js

const products = require('../products');

const APIError = require('../rest').APIError;

module.exports = {
    'GET /api/products': async (ctx, next) => {
        ctx.rest({
            products: products.getProducts()
        });
    },

    'POST /api/products': async (ctx, next) => {
        var p = products.createProduct(ctx.request.body.name, ctx.request.body.manufacturer, parseFloat(ctx.request.body.price));
        ctx.rest(p);
    },

    'DELETE /api/products/:id': async (ctx, next) => {
        console.log(`delete product ${ctx.params.id}...`);
        var p = products.deleteProduct(ctx.params.id);
        if (p) {
            ctx.rest(p);
        } else {
            throw new APIError('product:not_found', 'product not found by id.');
        }
    }
};

index.js


module.exports = {
    'GET /': async (ctx, next) => {
        ctx.render('index.html');
    }
};

rest.js

module.exports = {
    APIError: function (code, message) {
        this.code = code || 'internal:unknown_error';
        this.message = message || '';
    },
    restify: (pathPrefix) => {
        pathPrefix = pathPrefix || '/api/';
        return async (ctx, next) => {
            if (ctx.request.path.startsWith(pathPrefix)) {
                console.log(`Process API ${ctx.request.method} ${ctx.request.url}...`);
                ctx.rest = (data) => {
                    ctx.response.type = 'application/json';
                    ctx.response.body = data;
                }
                try {
                    await next();
                } catch (e) {
                    console.log('Process API error...');
                    ctx.response.status = 400;
                    ctx.response.type = 'application/json';
                    ctx.response.body = {
                        code: e.code || 'internal:unknown_error',
                        message: e.message || ''
                    };
                }
            } else {
                await next();
            }
        };
    }
};

app.js

const Koa = require('koa');

const bodyParser = require('koa-bodyparser');

const controller = require('./controller');

const templating = require('./templating');

const rest = require('./rest');

const app = new Koa();

// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    await next();
});

// static file support:
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));

// parse request body:
app.use(bodyParser());

// add nunjucks as view:
app.use(templating('views', {
    noCache: true,
    watch: true
}));

// bind .rest() for ctx:
app.use(rest.restify());

// add controllers:
app.use(controller());

app.listen(3000);
console.log('app started at port 3000...');

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末伟骨,一起剝皮案震驚了整個濱河市醒叁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件例隆,死亡現(xiàn)場離奇詭異,居然都是意外死亡床未,警方通過查閱死者的電腦和手機医舆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門俘侠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔬将,你說我怎么就攤上這事爷速。” “怎么了霞怀?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵惫东,是天一觀的道長。 經常有香客問我毙石,道長廉沮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任徐矩,我火速辦了婚禮滞时,結果婚禮上,老公的妹妹穿的比我還像新娘丧蘸。我一直安慰自己漂洋,他們只是感情好遥皂,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著刽漂,像睡著了一般演训。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贝咙,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天样悟,我揣著相機與錄音,去河邊找鬼庭猩。 笑死窟她,一個胖子當著我的面吹牛,可吹牛的內容都是我干的蔼水。 我是一名探鬼主播震糖,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼趴腋!你這毒婦竟也來了吊说?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤优炬,失蹤者是張志新(化名)和其女友劉穎颁井,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蠢护,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡雅宾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了葵硕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眉抬。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖贬芥,靈堂內的尸體忽然破棺而出吐辙,到底是詐尸還是另有隱情,我是刑警寧澤蘸劈,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布昏苏,位于F島的核電站,受9級特大地震影響威沫,放射性物質發(fā)生泄漏贤惯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一棒掠、第九天 我趴在偏房一處隱蔽的房頂上張望孵构。 院中可真熱鬧,春花似錦烟很、人聲如沸颈墅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恤筛。三九已至官还,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毒坛,已是汗流浹背望伦。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留煎殷,地道東北人屯伞。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像豪直,于是被迫代替她去往敵國和親劣摇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容