添加服务器以分离应用程序和数据
这是关于从简单的 HTML 网站创建动态移动应用程序系列文章的第三部分。在 第二部分 中,我们将数据与其视觉表现形式分离,但数据仍然包含在应用程序中。在本篇文章中,我们将该数据从应用程序中完全移除,并从服务器提供,以最大限度地提高不同用户之间的可重用性。
我们将介绍使用 NodeJS 创建合适服务器所需的步骤。然后,我们将使应用程序作为该服务器的客户端运行。
如果你手头有上次的代码,那么你就可以开始了。确保你已经安装了 NodeJS。如果你没有设置以前的代码库,你可以按照 说明 加载本教程的任何阶段。使用 stage5 作为起点。
重构我们的代码
让我们快速看一下 第二部分中创建的代码。
在 app.onDeviceReady
方法中(参见 js/index.js
)会发生两件事 - 数据从 JSON 文件加载,并且在 <brick-deck id="plan-group">
上侦听触摸事件。数据加载后,app.renderData
解析 JSON 字符串,app.createUI
为每个计划分别创建 UI。
onDeviceReady
和 createUI
目前效率不高,所以让我们更新它们。让我们通过创建一个名为 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
类型为一个具有两个方法的对象 - createUI
和 selectTab
- 这些方法从现有方法和函数中复制而来。唯一的改变是与数据现在具有对象性质相关。我们使用 this.schedule
代替 plan.week
。this
始终与当前计划的范围相关。与在对象中存储参数相关的更改如下
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 的创建者。
关于 Chris Mills
Chris Mills 是 Mozilla 的高级技术作家,他撰写有关开放式 Web 应用程序、HTML/CSS/JavaScript、A11y、WebAssembly 等的文档和演示文稿。他喜欢摆弄 Web 技术,并在会议和大学偶尔做技术演讲。他曾在 Opera 和 W3C 工作,喜欢演奏重金属鼓和喝好啤酒。他和他的妻子和三个可爱的女儿住在英国曼彻斯特附近。
4 条评论