[Node.js基础]学习③0--在线电子表格

package.json

{
    "name": "view-koa",
    "version": "1.0.0",
    "description": "mini-excel example with vue",
    "main": "app.js",
    "scripts": {
        "start": "node --use_strict app.js"
    },
    "keywords": [
        "vue",
        "mvvm"
    ],
    "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",
        "mime": "1.3.4",
        "mz": "2.4.0"
    }
}

static

index.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>Mini Excel</title>
    <link rel="stylesheet" href="/static/css/bootstrap.css">
    <script src="/static/js/jquery.min.js"></script>
    <script src="/static/js/bootstrap.min.js"></script>
    <script src="/static/js/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/vue.resource/1.0.3/vue-resource.min.js"></script>
    <script src="/static/js/excel.js"></script>
    <style>

#sheet {
    table-layout: fixed;
    min-width: auto;
    margin-bottom: 0px;
}

#sheet tr>th {
    width: 150px;
    background-color: #e6e6e6;
}

#sheet tr>th:first-child {
    width: 50px;
    background-color: #e6e6e6;
}

#sheet tr>td {
    width: 150px !important;
    overflow: hidden;
    text-overflow: ellipsis;
    text-wrap: none;
    word-wrap: normal;
    white-space: nowrap;
}

#sheet tr>td:first-child {
    width: 50px;
}

    </style>
    <script>

var ID = 'S-001';
var COLUMNS = 10;

function createHeader() {
    var hdr = [{
        row: 0,
        col: 0,
        text: ''
    }];
    for (var i=1; i<=COLUMNS; i++) {
        hdr.push({
            row: 0,
            col: i,
            text: String.fromCharCode(64 + i)
        });
    }
    return hdr;
}

function createRow(rIndex) {
    var row = [{
        row: rIndex,
        col: 0,
        contentEditable: false,
        text: '' + rIndex,
        align: 'left'
    }];
    for (var i=1; i<=COLUMNS; i++) {
        row.push({
            row: rIndex,
            col: i,
            contentEditable: true,
            text: '',
            align: 'left'
        });
    }
    return row;
}

function createRows() {
    var rows = [];
    for (var i=1; i<=100; i++) {
        rows.push(createRow(i));
    }
    return rows;
}

$(function () {
    var vm = new Vue({
        el: '#sheet',
        data: {
            title: 'New Sheet',
            header: createHeader(),
            rows: createRows(),
            selectedRowIndex: 0,
            selectedColIndex: 0
        },
        methods: {
            open: function () {
                var that = this;
                that.$resource('/api/sheets/' + ID).get().then(function (resp) {
                    resp.json().then(function (result) {
                        that.title = result.title;
                        that.rows = result.rows;
                    });
                }, function (resp) {
                    alert('Failed to load.');
                });
            },
            save: function () {
                this.$resource('/api/sheets/' + ID).update({
                    title: this.title,
                    rows: this.rows
                }).then(function (resp) {
                    console.log('saved ok.');
                }, function (resp) {
                    alert('failed to save.');
                });
            },
            focus: function (cell) {
                this.selectedRowIndex = cell.row;
                this.selectedColIndex = cell.col;
            },
            change: function (e) {
                var
                    rowIndex = this.selectedRowIndex,
                    colIndex = this.selectedColIndex,
                    text;
                if (rowIndex > 0 && colIndex > 0) {
                    text = e.target.innerText;
                    this.rows[rowIndex - 1][colIndex].text = text;
                }
            }
        }
    });
    window.vm = vm;

    var setAlign = function (align) {
        var
            rowIndex = vm.selectedRowIndex,
            colIndex = vm.selectedColIndex,
            row, cell;
        if (rowIndex > 0 && colIndex > 0) {
            row = vm.rows[rowIndex - 1];
            cell = row[colIndex];
            cell.align = align;
        }
    };

    $('#cmd-open').click(function () {
        vm.open();
    });

    $('#cmd-save').click(function () {
        vm.save();
    });

    $('#cmd-left').click(function () {
        setAlign('left');
    });

    $('#cmd-center').click(function () {
        setAlign('center');
    });

    $('#cmd-right').click(function () {
        setAlign('right');
    });

    $('#cmd-download').click(function () {
        var
            fileName = vm.title + '.xls',
            a = document.createElement('a');
        a.setAttribute('href', 'data:text/xml,' + encodeURIComponent(makeXls(vm.rows)));
        a.setAttribute('download', fileName);
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    });

    $('#toolbar button').focus(function () {
        $(this).blur();
    });
});
    </script>
</head>

<body style="overflow:hidden">
    <header class="navbar navbar-static-top">
        <div class="container-fluid">
            <div class="navbar-header">
                <a href="#0" class="navbar-brand">Sheet</a>
            </div>
            <nav id="toolbar" class="collapse navbar-collapse">
                <div class="btn-group">
                    <button id="cmd-open" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-folder-open"></i> Open</button>
                    <button id="cmd-save" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
                    <button id="cmd-download" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-save"></i> Download</button>
                </div>
                <div class="btn-group">
                    <button id="cmd-left" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-align-left"></i></button>
                    <button id="cmd-center" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-align-center"></i></button>
                    <button id="cmd-right" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-align-right"></i></button>
                </div>
            </nav>
        </div>
    </header>

    <div id="important" style="position:absolute; margin:50px 0 35px 0; left: 0; right: 0; top: 0; bottom: 0; overflow:scroll;">
        <table id="sheet" class="table table-bordered">
            <thead>
                <tr>
                    <th v-for="cell in header" v-on:click="focus(cell)" v-text="cell.text"></th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="tr in rows">
                    <td v-for="cell in tr" v-on:click="focus(cell)" v-on:blur="change" v-bind:contenteditable="cell.contentEditable" v-bind:style="{ textAlign: cell.align }" v-text="cell.text"></td>
                </tr>
            </tbody>
        </table>
    </div>

    <footer class="navbar navbar-fixed-bottom" style="background-color:#e7e7e7; height:35px; min-height:35px; overflow:hidden;">
        <div class="container-fluid">
            <nav class="collapse navbar-collapse">
                <p class="text-right" style="padding-top:5px">
                    <a target="_blank" href="http://www.liaoxuefeng.com">Website</a> -
                    <a target="_blank" href="https://github.com/michaelliao/learn-javascript">GitHub</a> -
                    <a target="_blank" href="http://weibo.com/liaoxuefeng">Weibo</a>
                    This JavaScript course is created by <a target="_blank" href="http://weibo.com/liaoxuefeng">@廖雪峰</a>.
                    Code licensed <a target="_blank" href="https://github.com/michaelliao/learn-javascript/blob/master/LICENSE">Apache</a>.
                </p>
            </nav>
        </div>
    </footer>
</body>

</html>

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;

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();
            }
        };
    }
};

contriller.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();
};

controllers

api.js


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

const path = require('path');

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

const saved_dir = path.normalize(__dirname + path.sep + '..' + path.sep + 'saved-docs');

console.log(`documents will be saved in ${saved_dir}.`);

module.exports = {
    'GET /api/sheets/:id': async (ctx, next) => {
        var s, fp = path.join(saved_dir, '.' + ctx.params.id);
        console.log(`load from file ${fp}...`);
        s = await fs.readFile(fp, 'utf8');
        ctx.rest(JSON.parse(s));
    },
    'PUT /api/sheets/:id': async (ctx, next) => {
        var
            fp = path.join(saved_dir, '.' + ctx.params.id),
            title = ctx.request.body.title,
            rows = ctx.request.body.rows,
            data;
        if (!title) {
            throw new APIError('invalid_data', 'invalid title');
        }
        if (!Array.isArray(rows)){
            throw new APIError('invalid_data', 'invalid rows');
        }
        data = {
            title: title,
            rows: rows
        };
        await fs.writeFile(fp, JSON.stringify({
            title: title,
            rows: rows
        }), 'utf8');
        console.log(`wrote to file ${fp}.`);
        ctx.rest({
            id: ctx.params.id
        });
    }
}

app.js

const Koa = require('koa');

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

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

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

const app = new Koa();

const isProduction = process.env.NODE_ENV === 'production';

// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    var
        start = new Date().getTime(),
        execTime;
    await next();
    execTime = new Date().getTime() - start;
    ctx.response.set('X-Response-Time', `${execTime}ms`);
});

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

app.use(async (ctx, next) => {
    if (ctx.request.path === '/') {
        ctx.response.redirect('/static/index.html');
    } else {
        await next();
    }
});

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容