从简单的 HTML 网站创建移动应用:第二部分

或:让我们的简单应用为其他人工作

本系列的第一部分(于去年年底开始)中,我们介绍了开发学校计划应用的过程。到目前为止(查看第一部分的最终代码),我们已经实现了同时显示多个学校计划,并且通过 Cordova 支持了 Web、iOS 和 Android 平台。

Stage4 Result Screenshot

让我们想象一下,其他人也看到了它的益处,想要使用我们的应用。通过简单地替换一个文件(我们将其命名为 www/app_data/plans.json)并进行一些其他调整,就可以创建适合他们的应用。这是我们将在本部分开头关注的内容。

我们最终的目标是创建一个应用来显示存储在服务器上的数据(学校计划),这样每个用户都可以从任何计算机查看自己的数据。在本教程中,为了避免分散对主要目标的注意力,服务器部分将被简化,计划数据库将写入 JSON 文件。如果您想将其扩展到 SaaS,请随意进行,并在下面的评论部分告诉我们。

将要构建的内容

一个移动应用程序,将

  1. 显示学校计划。
  2. 离线工作。
  3. 在多个平台上运行。
  4. 使用存储在服务器/JSON 文件中的学校计划(我们本系列接下来的部分的目标)。

先决条件

如果您还没有完成 第一篇文章,我们建议您现在就完成它。您至少应该完成 第一部分的先决条件,并确保您熟悉它们。

然后按照以下步骤操作,更新您从上一篇文章中获得的代码示例。如果您没有这个代码示例(即,如果您没有完成上一篇文章),请按照 说明加载本教程中的任何阶段。使用 stage4 作为起点。

您还应该确保您已经安装了 NodeJS

添加图标

在第一部分中,我省略了图标,因此 Cordova 添加了一个默认图标。让我们使用自定义图标使应用看起来更专业。我已经从 findicon.com 下载了一个背包图标,调整了大小并复制到了 www/img 文件夹。在应用目录(school-plan/ - 在运行 cordova create 后创建)中 - 编辑 config.xml 文件并添加以下内容

<icon src="www/img/backpack-128.png" />

如果您愿意,您可以添加更多特定信息 - 例如,为每个所需的平台添加更多图像大小。您可以在 Cordova 文档 中找到更多信息。以下是在 Firefox OS 中的特殊定义

<platform name="firefoxos">
	<icon width="128" height="128" src="www/img/backpack-128.png" />
	<icon width="64" height="64" src="www/img/backpack-64.png" />
	<icon width="32" height="32" src="www/img/backpack-32.png" />
</platform>

由于 Cordova 中的 Firefox OS 部分存在错误,因此请使用以下命令创建 www/icon 目录。

mkdir www/icon

修改数据代码

在本阶段,我们将修改学校计划应用,使其使用数据而不是纯 HTML(查看 GitHub 上完成的 stage 5 代码)。

之前我们将所有学校计划数据硬编码到 index.html 中。现在我们将数据与其可视化表示分离。为此,我们将从 www/index.html 中删除旧数据,只保留最小的结构。

清空 index.html 文件中的 <brick-tabbar><brick-deck> 元素,使它们看起来像这样

<brick-tabbar id="plan-group-menu" selected-index="0">
</brick-tabbar>
<brick-deck id="plan-group" selected-index="0">
</brick-deck>

JavaScript 项目中最常用的数据结构是 JSON;因此我们将以这种格式添加我们的数据。目前,将学校计划与应用一起分发是可以的(稍后我们将从服务器获取它,以获得更大的灵活性)。我们的 JSON 包含一个计划的 ArrayPlan 是一个包含 titleid、可选的 active 字段和 weekObject。后者本身是一个课程计划的 Array

注意:使用 jsHint 检查代码质量。

[
    {
      "title": "Name of the plan",
      "id": "id-of-the-plan",
      "active": 1,  // should the plan be active now?
      "week": [
        [
          "first hour of Monday",
          "second hour",
          "third",
          // ... and so on
        [],  // no activities on Tuesday
        ], [
          "",
          "",
          "Wednesday is starting on third hour",
          "fourth"
        ]
    }
]

您可以在 Github 上找到一个 示例文件。将其复制到您的项目中的 www/app_data/plans.json 文件夹。

现在我们需要从应用目录中读取此 JSON 文件。

由于卡片和标签在加载 JavaScript 文件时还不存在,因此我们现在应该从 www/js/index.js 中删除将它们链接在一起的部分。转到该文件,找到 assignTabs() 函数,并将其完全删除,以及其下面的调用。

没有数据,应用程序将不会显示任何内容。数据需要在应用准备就绪后立即加载 - 找到 onDeviceReady 方法,并在其内部顶部输入以下代码行

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

注意:之前我计划使用 Cordova 的 FileSystem 插件,但它只会使代码更复杂。

请求成功返回我们的 JSON 后,它将传递给 app.renderData(如下所示)。它将 JSON 格式的文本解析为 JavaScript,并将数据发送到 app.createUI,以便创建必要的 DOM 元素来形成 UI。

onDeviceReady 方法下方添加以下代码块

renderData: function() {
    var plans = JSON.parse(this.responseText);
    app.createUI(plans);
},

为了创建 UI,我们需要工作日名称。最佳选择是使用 Cordova 的 Globalization 插件。要将此插件添加到应用程序,只需在您的终端中运行以下命令,确保您位于根 school-plan 目录中

cordova plugin add org.apache.cordova.globalization

接下来,在 renderData() 方法下方添加我们之前提到的 createUI() 方法。它看起来像这样

createUI: function(plans) {
    var deck = document.getElementById('plan-group');
    var tabbar = document.getElementById('plan-group-menu');
    navigator.globalization.getDateNames(function(dayOfWeek) {
        // render UI in the callback
    }, function() {}, {type: 'narrow', item: 'days'});
},

工作日使用 navigator.globalization.getDateNames 方法检索。dayOfWeek 将保存一个工作日名称的 Array,例如(在我的例子中是波兰语) - ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd']。如果您想使用完整的日期名称,只需将 type: 'narrow' 更改为 type: 'wide'

现在我们需要为单个计划创建 DOM 元素。这一次,brick-tabbar-tabbrick-card 元素使用 JavaScript 创建。标签使用其 target 参数引用相应的卡片。它在每种情况下都与卡片的 id 相同。Brick 将解析此值并创建一个 tab.targetElement,它将链接到卡片元素。在 getDateNames 的回调中,输入以下代码(替换上面的“// 在回调中渲染 UI”注释)

for (var i = 0; i < plans.length; i++) {
    var plan = plans[i];

    // create card
    var card = document.createElement('brick-card');
    card.setAttribute('id', plan.id);
    deck.appendChild(card);

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

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

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

与编写纯 HTML 不同,我们现在将创建 table 的主体,然后是标题。这是因为 table.insertRow() 或者在内部创建一个新的 tbodytr,或者将一行添加到任何现有的 HTMLTableSectionElement(如果已经创建,则为 thead)。我们也可以调用 table.tBodies(0),但这会使代码更复杂。

我们不想显示没有课程的日子。让我们将只有非空日子的计划复制到新的 cleanPlan 数组中。将此代码放在创建 <table> 元素的行之后(参见上面的列表)

var numberOfDays = plan.week.length;
var cleanPlan = [];
for (j = 0; j < numberOfDays; j++) {
    if (plan.week[j].length > 0) {
        cleanPlan.push(plan.week[j]);
    }
}

在我们能够创建其他 DOM 元素(<tr><td>)之前,有一个问题需要解决 - 我们在 JSON 文件中以人类理解的方式表示计划 - 小时在日期内。不幸的是,HTML 中的表格是按行创建的(日期在小时内),这意味着表示计划的数组需要翻转。

例如,存储在 JSON 中的数组将如下所示:(dXhY 代表 X 天 Y 小时)

d1h1 d1h2 d1h3 ...
d2h1 d2h2 d2h3 ...
d3h1 d3h2 d3h3 ...
...

但我们的 <table> 结构将如下所示

d1h1 d2h1 d3h1 ...
d1h2 d2h2 d3h2 ...
d1h3 d2h3 d3h3 ...
...

在最后一个代码块之后添加以下代码块,开始执行此数据转换

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];
    }
}

上面最重要的行是 daysInHours[k][j] = cleanPlan[j][k];,其中索引反转 - 一个数组的 kj 元素成为另一个数组的 jk 元素。d3h2 取代了 d2h3,反之亦然。

daysInHours 数组现在应该保存为 UI 准备好的计划。现在我们可以遍历它,将计划渲染到 HTML 表格中。这里需要注意一个重要的事情 - table.insertRow 需要使用(否则为可选的)设置为 -1 的索引,因为默认情况下,Android 会将行插入到表格的顶部。

在最后一个代码块下方添加以下代码块

for (var j = 0; j < daysInHours.length; j++) {
    var tr = 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]));
        }
    }
}

我们遍历所有的小时(索引 j)。第一个 <tr> 在数组的底部创建,然后是包含该行号的 textNode<td>。之后,我们遍历小时内的日期(索引 k)并创建更多单元格 - 如果该小时和日期有计划,则创建一个 textNode

你可能会惊讶地看到这段代码使用 cleanPlan.length 而不是 daysInHours[j].length。这是因为我们需要在每一天创建一个单元格,即使没有安排课程,否则我们将最终得到像这样的损坏的表格结构。

short row issue

现在我们准备创建一个带有日期的标题。将以下代码块添加到上一个代码块的正下方。

var thead = table.createTHead();
var tr = thead.insertRow();
var th_empty = document.createElement('th');
tr.appendChild(th_empty);
var weekDayNumber;
for (var j = 0; j < numberOfDays; j++) {
    var weekDayNumber = (j + 1) % 7;
    if (plan.week[j].length > 0) {
        var th = document.createElement('th');
        th.appendChild(document.createTextNode(dayOfWeek.value[weekDayNumber]));
        tr.appendChild(th);
    }
}

首先,为该列创建一个空标题单元格,包含对应的一天的小时。然后,我们遍历日期并仅在当前日期有计划时(与之前 cleanPlan 的方式相同),创建一个带有日期名称的新 <th>

接下来,我们需要一行将创建的 <table> 放入 brick-card 元素中 - 将以下行添加到上一个代码块下方。

card.appendChild(table);

当所有选项卡都准备好后,我们希望应用程序加载时第一个选项卡被选中。为此,我们将使用以下代码块 - 将其添加到上一行的正下方。

if (plan.active) {
    selectTab(deck, tab);
}

selectTab 是在 app 对象之外创建的辅助函数。它使用 轮询activeTab.targetElement 上检测 Brick 是否已将选项卡与卡片链接,如下所示。将此代码块添加到 index.js 文件的底部。

function selectTab(deck, activeTab) {
    function selectActiveTab() {
        if (!activeTab.targetElement) {
            return window.setTimeout(selectActiveTab, 100);
        }
        deck.showCard(activeTab.targetElement);
    }
    selectActiveTab();
}

测试时间

此时,应用程序应该与第 1 部分第 4 阶段的应用程序完全相同。唯一的区别可能是计划标题中本地化日期名称的不同。

当你想测试你的应用程序时,在终端中输入以下命令(从你的 school-plan 应用程序的根目录)。

cordova prepare
cordova serve

这将使应用程序及其不同的平台在 localhost:8000 上对你可用。

如果你还想在 Firefox OS 上测试应用程序,你需要在 Firefox 中打开 WebIDE,转到打开应用程序 > 打开打包应用程序,然后从文件选择器中选择打开 school-plan/platforms/firefoxos/www 目录作为打包应用程序。从这里,你可以选择在模拟器或真实的 Firefox OS 设备上加载应用程序。有关更多详细信息,请参阅 MDN WebIDE 页面。

注意:如果你想为希望使用该应用程序的不同用户定制应用程序,你可以在此阶段通过用每个用户的不同信息替换 www/app_data/plans.json 文件来完成。

[编辑] 请参阅

关于 Piotr Zalewa

Piotr Zalewa 是 Mozilla 开发者生态系统团队的高级 Web 开发人员。从事 Web 应用程序开发工作。他是 JSFiddle 的创建者。

更多 Piotr Zalewa 的文章...

关于 Chris Mills

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

更多 Chris Mills 的文章...


2 条评论

  1. mario martinez

    请修复步骤 1 的链接

    2015 年 4 月 16 日 下午 4:13

    1. Chris Mills

      我假设你指的是全球化插件的链接?已修复;感谢你指出这一点。

      2015 年 4 月 17 日 上午 2:19

本文的评论已关闭。