[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...');

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容