将简单的 HTML 网站转化为移动应用:第三部分

添加服务器以分离应用程序和数据

这是关于从简单的 HTML 网站创建动态移动应用程序系列文章的第三部分。在 第二部分 中,我们将数据与其视觉表现形式分离,但数据仍然包含在应用程序中。在本篇文章中,我们将该数据从应用程序中完全移除,并从服务器提供,以最大限度地提高不同用户之间的可重用性。

我们将介绍使用 NodeJS 创建合适服务器所需的步骤。然后,我们将使应用程序作为该服务器的客户端运行。

如果你手头有上次的代码,那么你就可以开始了。确保你已经安装了 NodeJS。如果你没有设置以前的代码库,你可以按照 说明 加载本教程的任何阶段。使用 stage5 作为起点。

重构我们的代码

让我们快速看一下 第二部分中创建的代码

app.onDeviceReady 方法中(参见 js/index.js)会发生两件事 - 数据从 JSON 文件加载,并且在 <brick-deck id="plan-group"> 上侦听触摸事件。数据加载后,app.renderData 解析 JSON 字符串,app.createUI 为每个计划分别创建 UI。

onDeviceReadycreateUI 目前效率不高,所以让我们更新它们。让我们通过创建一个名为 activateFingerSwipe 的新方法来分割 onDeviceReady,该方法将包含 request.send() 行之后的所有代码,并简单地调用它,不带参数。更新你的代码,如下所示

onDeviceReady: function() {
     var request = new XMLHttpRequest();
     request.onload = app.renderData;
     request.open("get", "app_data/plans.json", true);
     request.send();

     app.activateFingerSwipe();
},

activateFingerSwipe: function() {
    // Switching from one tab to another is done automatically
    // We just need to link it backwards - change menu if slides
    // changed without touching the menu
    app.planGroupMenu = document.getElementById('plan-group-menu');
    
    // Implementing one finger swipe to change deck card
    app.planGroup = document.getElementById('plan-group');

    var startX = null;
    var slideThreshold = 100;

    // ...

现在,让我们继续更新 createUI。我们不会将为每个单独的计划创建 UI 的代码放在 createUI 中,而是将创建一个名为 Plan 的新的用户定义对象类型,并在循环中实例化它。更新 app.renderData 使其看起来像下面的代码块

renderData: function() {
    var plans = JSON.parse(this.responseText);
    var deck = document.getElementById('plan-group');
    var tabbar = document.getElementById('plan-group-menu');
    navigator.globalization.getDateNames(function(dayOfWeek){
      for (var i = 0; i < plans.length; i++) {
          var plan = new Plan(plans[i]);
          plan.createUI(deck, tabbar, dayOfWeek);
      }
    }, function() {}, {type: 'narrow', item: 'days'});
},

我们将定义 Plan 类型为一个具有两个方法的对象 - createUIselectTab - 这些方法从现有方法和函数中复制而来。唯一的改变是与数据现在具有对象性质相关。我们使用 this.schedule 代替 plan.weekthis 始终与当前计划的范围相关。与在对象中存储参数相关的更改如下

  • plan.title -> this.title
  • plan.week -> this.schedule
  • plan.id -> this.id
  • plan.active -> this.active
  • var tab -> this.tab
  • var card -> this.card
  • var table -> this.table
  • selectTab(deck, tab) -> this.selectTab(deck)

在你的 index.js 文件的顶部,添加以下代码

function Plan(plan) {
    this.schedule = plan.week;
    this.title = plan.title;
    this.id = plan.id;
    this.active = plan.active;
    this.tab = null;
    this.card = null;
    this.table = null;
};

Plan.prototype.selectTab = function(deck) {
    var self = this;
    function selectActiveTab() {
        if (!self.tab.targetElement) {
            return window.setTimeout(selectActiveTab, 100);
        }
        deck.showCard(self.tab.targetElement);
    }
    selectActiveTab();
}

Plan.prototype.createUI = function(deck, tabbar, dayOfWeek) {
    // create card
    this.card = document.createElement('brick-card');
    this.card.setAttribute('id', this.id);
    deck.appendChild(this.card);

    //create tab
    this.tab = document.createElement('brick-tabbar-tab');
    this.tab.setAttribute('target', this.id);
    this.tab.appendChild(document.createTextNode(this.title));
    tabbar.appendChild(this.tab);

    // link card and tab DOM Elements
    this.card.tabElement = this.tab;
    this.card.addEventListener('show', function() {
        this.tabElement.select();
    });

    // create plan table
    this.table = document.createElement('table');

    var numberOfDays = this.schedule.length;
    var cleanPlan = [];
    for (var j = 0; j  0) {
            cleanPlan.push(this.schedule[j]);
        }
    }

    var daysInHours = [];
    for (j = 0; j < cleanPlan.length; j++) {
        for (var k = 0; k < cleanPlan[j].length; k++) {
            if (!daysInHours[k]) {
                daysInHours[k] = [];
            }
            daysInHours[k][j] = cleanPlan[j][k];
        }
    }

    for (var j = 0; j < daysInHours.length; j++) {
        var tr = this.table.insertRow(-1);
        var td = tr.insertCell(-1);
        td.appendChild(document.createTextNode(j + 1));
        for (var k = 0; k < cleanPlan.length; k++) {
            var td = tr.insertCell(-1);
            if (daysInHours[j][k]) {
                td.appendChild(document.createTextNode(daysInHours[j][k]));
            }
        }
    }

    var thead = this.table.createTHead();
    var tr = thead.insertRow();
    var th_empty = document.createElement('th');
    tr.appendChild(th_empty);
    var weekDayNumber;
     for (var j = 0; j  0) {
            var th = document.createElement('th');
            th.appendChild(document.createTextNode(dayOfWeek.value[weekDayNumber]));
            tr.appendChild(th);
        }
    }

    this.card.appendChild(this.table);

    if (this.active) {
      this.selectTab(deck);
    }
}

你可以将你的工作与 github 中的完整重构代码源 对比。

你可能注意到 Plan.prototype.selectTab 中的 var self = this;deck.showCard(self.tab.targetElement);selectActiveTab 范围内的 this 将表示与它外部不同的值。

如果你想了解更多信息,请阅读有关 new 运算符this 关键字的 MDN 文档

构建服务器

使用 NodeJS 构建服务器相当简单 - http 是唯一需要的模块,尽管我们也会包含 fs(因为我们需要从磁盘加载文件)和 sys(用于显示日志)。

我们将把服务器的代码放到一个单独的目录中(在相关的 Github 项目中命名为 stage6-server/),因为它不是应用程序的一部分。

首先,在与 school-plan 相同级别创建一个名为 server 的新目录。将 plans.json 文件从 school-plan/www/app-data 目录移动到 server 目录,然后删除 school-plan/www/app-data。你可以使用以下终端命令从 school-plan 的父目录中执行这些操作

mkdir server
mv school-plan/www/app_data/plans.json server/
rm -rf school-plan/www/app_data

现在开始 NodeJS 服务器代码。在 server 目录中创建一个名为 server.js 的新文件,并用以下内容填充它 - 这只是简单地从磁盘读取文件

var fs = require('fs'),
    sys = require("sys");

fs.readFile(__dirname + "/plans.json", function (err, data) {
    sys.puts(data);
});

使用终端命令 node server.js 运行它 - 你应该在终端中看到 plans.json 文件的内容。

现在我们将使用 http 模块提供此文件。修改你的 NodeJS 代码,如下所示

var fs = require('fs'),
    sys = require("sys"),
    http = require('http');

http.createServer(function (request, response) {
  fs.readFile(__dirname + "/plans.json", function (err, data) {
    response.writeHead(200, {"Content-Type": "application/json"});
    response.end(data);
    sys.puts("accessed");
  });
}).listen(8080);
sys.puts("Server Running on http://127.0.0.1:8080");

首先,我们使用 http.createServer 方法创建了服务器,并命令它监听端口 8080。对于每个请求,都会读取 plans.json 文件,并将其作为 HTTP 响应返回。然后,我们简单地将 accessed 记录到终端。

再次测试你的文件,如前所述。

使用浏览器导航到 http://127.0.0.1:8080,你应该在浏览器中看到文件的内容。(我使用 JSON View 附加组件使代码看起来更美观 - 值得一试。)

因此,我们能够从我们的机器提供文件 - 现在我们只需要在应用程序中读取它。

注意:服务器提供给任何地址 - 它在 http://localhost:8080 和你的本地网络地址(通常为 http://192.168.xx.xx:8080)上都工作。此地址应该在设备上测试应用程序时使用。

从服务器读取数据

我们需要更改应用程序以从服务器读取数据,而不是从应用程序中分发的文件中读取数据。让我们更改 request.open 调用(在 deviceReady 方法中)以从 URL 读取数据,而不是从本地文件读取数据。将 app_data/plans.json 替换为 http://127.0.0.1:8080,使其看起来像这样

request.open("get", "http://127.0.0.1:8080", true);

现在进行测试。在终端中运行 cordova prepare 命令,并在 WebIDE 中重新加载应用程序。

你将在浏览器控制台中收到错误:跨域请求被阻止:同源策略不允许读取 http://127.0.0.1:8080/ 上的远程资源。这可以通过将资源移动到同一域或启用 CORS 来解决。

CORS 和跨域修复

此错误是由于 CORS 安全策略阻止我们从不同域加载脚本。

有两种方法可以从不同域加载脚本。在 Cordova 中将其列入白名单是其中一种方法,但由于我们可以控制服务器,我们可以简单地允许跨站点 HTTP 请求。这通过在请求中设置一个名为 Access-Control-Allow-Origin 的标头调用来完成。它的值是可以从服务器请求内容的域名。由于我们的应用程序将在手机上使用,因此所有域都应该有权访问。所需的标头如下所示

"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept"

我们将这些标头添加到 server.js 脚本中指定的现有 Content-Type 标头中 - 更新 response.writeHead() 调用,如下所示

response.writeHead(200, {
  "Content-Type": "application/json",
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept"});

保存你的 NodeJs 代码,停止(Control + C)在终端中运行的服务器,然后再次运行它。应用程序现在应该能够按预期从服务器读取数据。

如果你在让应用程序工作时遇到问题,请检查 应用程序服务器 的代码。

提供和加载用户计划

目前,我们加载了 plans.json 中可用的所有计划。由于我们希望能够为任何用户提供计划,因此我们需要识别和提供一组计划。

我们需要识别一个计划。为了在我们的“数据库”(JSON 文件)中找到一个计划,我们将更改计划结构,从数组更改为对象。例如

{
  "somehashtag": {
  "title": "A",
  "id": "a",
  "active": 1,
  "week": [
    // ...
  ]
}, {
  "otherhashtag": {
  "title": "B",
  "id": "b",
  "week": [
    // ...
  ]
}

我们还需要找到一种存储主题标签的方法。大多数用户将拥有不同的主题标签列表。这些主题标签应该只存储在设备上,还是存储在服务器上?考虑到许多用户经常更换手机,最好将主题标签列表保存在服务器上。在生产中,我们将有单独的用户帐户,但为了本教程的简洁性和服务器代码的简单性,没有提供任何安全性。

我们还需要修改 JSON 文件,以允许不同的用户和单独的计划

{
  "users": {
    "userA": {
      "name": "John Doe",
      "plans": ["somehashtag", "otherhashtag"]
    },
    // ...  
  },
  "plans": {
    "somehashtag": {
    "title": "A",
    "id": "a",
    "active": 1,
    "week": [
      // ...
    ]
  }, {
    "otherhashtag": {
    "title": "B",
    "id": "b",
    "week": [
      // ...
    ]
  }
}

从现在起,data.plans 将代表所有计划,data.users 将代表所有用户。

此时,请从 Github 下载更新的 plans.json 文件,并用新文件替换旧文件。

我将访问用户计划的 URL 定义如下

http://{address}/plans/{userid}

使用 url 模块很容易读取 URL - 现在你应该将其添加到加载的模块中

var fs = require('fs'),
    sys = require("sys"),
    http = require('http'),
    url = require('url');

现在移除当前的 response.end(); 和 sys.puts() 调用,用以下代码替换它们

var args = url.parse(request.url).pathname.split('/');
var command = args[1];
var data = JSON.parse(data);

if (command === 'plans') {
    // get user from data.users
    var userid = args[2];
    var user = data.users[userid];
    var plans = [];
    var i, hashtag;
    for (i = 0; i < user.plans.length; i++) {
       hashtag = user.plans[i];
       // copy each user's plan to plans
       plans.push(data.plans[hashtag]);
    }
    response.end(JSON.stringify(plans));
    sys.puts("accessed user - " + userid);
}

应用程序中唯一更改的用法是,我们可以使用新的 URL 方案加载不同用户的數據

http://127.0.0.1:8080/plans/someuser

转到你的 index.js 文件,更新以下代码

request.open("get", "http://127.0.0.1:8080/", true);

request.open("get", "http://127.0.0.1:8080/plans/johndoe", true);

结论

我们现在提供了一种为不同用户显示不同计划的方法。但是,我们仍然在每次使用应用程序时下载计划,这对离线体验来说不是很好,而且目前我们无法更改显示的计划,除非我们自己编辑代码中的 XHR 调用。我们将在下一篇文章中解决这些问题。

现在,查看 应用程序服务器 的最终代码。请关注 5 月份发布的第四部分!

关于 Piotr Zalewa

Piotr Zalewa 是 Mozilla Dev Ecosystem 团队的高级网页开发者。致力于开发网页应用程序。他是 JSFiddle 的创建者。

更多 Piotr Zalewa 的文章……

关于 Chris Mills

Chris Mills 是 Mozilla 的高级技术作家,他撰写有关开放式 Web 应用程序、HTML/CSS/JavaScript、A11y、WebAssembly 等的文档和演示文稿。他喜欢摆弄 Web 技术,并在会议和大学偶尔做技术演讲。他曾在 Opera 和 W3C 工作,喜欢演奏重金属鼓和喝好啤酒。他和他的妻子和三个可爱的女儿住在英国曼彻斯特附近。

更多 Chris Mills 的文章…


4 条评论

  1. jens

    第 2 部分链接已损坏

    2015 年 4 月 30 日 03:00

    1. Chris Mills

      哎呀,href 链接不知怎么坏了。感谢您的提醒 - 已修复。

      2015 年 4 月 30 日 03:19

  2. Daniel

    第 2 部分在哪里?不在 Cordova 标签中

    2015 年 5 月 5 日 14:00

    1. Piotr Zalewa

      感谢您的提醒 - 已加标签。文章开头也有一个链接。

      2015 年 5 月 5 日 14:26

本文评论已关闭。