Skip to content

Latest commit

 

History

History
1344 lines (1046 loc) · 59.3 KB

File metadata and controls

1344 lines (1046 loc) · 59.3 KB

七、从头开始创建应用

现在是时候应用你的知识了!由于 WebAssembly 的主要设计目标之一是在现有的 web 平台中执行并与其很好地集成,因此构建一个 web 应用来测试它是有意义的。尽管 WebAssembly 当前的功能集相当有限,但我们可以在基本层面上利用这项技术。在本章中,我们将从头开始构建一个单页应用,在核心规范的上下文中使用 Wasm 模块。

本章结束时,您将知道如何:

  • 用 C 编写执行简单计算的函数
  • 用 Vue 构建一个基本的 JavaScript 应用
  • 将 Wasm 集成到您的 JavaScript 应用中
  • 确定当前形式的 WebAssembly 的功能和限制
  • 使用browser-sync运行并测试一个 JavaScript 应用

烹饪书籍——让 WebAssembly 变得负责任

如前所述,WebAssembly 当前的功能集相当有限。我们可以使用 Emscripten 来极大地扩展 web 应用的功能,但是这将带来不符合官方规范和添加粘合代码的代价。今天,我们仍然可以有效地使用 WebAssembly,这将带我们进入本章将要构建的应用。在本节中,我们将回顾我们将用来构建应用的库和工具,以及它的功能的简要概述。

概述和功能

在 WebAssembly 的当前形式中,我们可以相对容易地在 Wasm 模块和 JavaScript 代码之间传递数字。就现实世界的适用性而言,会计应用似乎是一个合乎逻辑的选择。我对会计软件的唯一争议是它有点无聊(无意冒犯)。我们将通过建立一些不道德的会计惯例来增加一点趣味。该应用名为做假账,一个与会计欺诈相关的术语。投资媒体对做账的定义如下:

"Cook the Books is an idiom describing fraudulent activities performed by corporations in order to falsify their financial statements. Typically, cooking the books involves augmenting financial data to yield previously nonexistent earnings. Examples of techniques used to cook the books involve accelerating revenues, delaying expenses, manipulating pension plans, and implementing synthetic leases."

https://www.investopedia.com/terms/c/cookthebooks.asp 的投资媒体页面提供了烹饪书籍的详细例子。我们将对我们的应用采取一种简单的方法。我们将允许用户输入生熟金额的交易。原始金额代表实际存入或提取的金额,而熟金额是其他人都会看到的。该应用将生成饼图,按类别显示生交易或熟交易的费用和收入。用户将能够在两个视图之间轻松切换。该应用由以下组件组成:

  • 用于在事务和图表之间切换的选项卡
  • 显示交易记录的表
  • 允许用户添加、编辑或删除交易的按钮
  • 用于添加/更新事务的模式对话框
  • 按类别显示收入/支出的饼图

使用的 JavaScript 库

应用的 JavaScript 部分将使用 CDN 提供的几个库。它还将使用一个本地安装的库来观察代码的变化。以下部分将描述每个库及其在应用中的用途。

某视频剪辑软件

Vue 是一个 JavaScript 框架,它允许您将一个应用拆分成单独的组件,以便于开发和调试。我们使用它来避免一个完整的 JavaScript 文件包含所有的应用逻辑,而另一个完整的 HTML 文件包含整个用户界面。之所以选择 Vue,是因为它不需要增加构建系统的复杂性,并且允许我们使用 HTML、CSS 和 JavaScript,而无需进行任何传输。官网是https://vuejs.org

UIkit

UIkit 是我们将用于为应用添加样式和布局的前端框架。有几十种选择,像 Bootstrap 或布尔玛,提供类似的组件和功能。但是我选择了 UIkit,因为它提供了有用的实用程序类,并增加了 JavaScript 功能。您可以在https://getuikit.com查看文档。

洛拉斯

Lodash 是一个优秀的实用程序库,它提供了在 JavaScript 中执行常见操作的方法,这些方法还没有内置到语言中。我们将使用它来执行计算和操作交易数据。文件和安装说明可以在https://lodash.com找到。

数据驱动文档

数据驱动文档 ( D3 )是一个多面库,允许您将数据转换为令人印象深刻的可视化。D3 的应用编程接口由几个模块组成,从数组操作到图表和转换。我们将主要使用 D3 来创建饼图,但我们也将利用它提供的一些实用方法。你可以在https://d3js.org找到更多信息。

其他库

为了以正确的格式显示货币值并确保用户输入有效的美元金额,我们将利用accounting . js(http://openexchangerates.github.io/accounting.js)和vue-numeric(https://kevinongko.github.io/vue-numeric)库。为了简化开发,我们将设置一个基本的npm项目,并使用浏览器同步(https://www . browser sync . io)立即查看运行的应用中反映的代码更改。

c 和构建过程

应用使用 C 语言,因为我们用基本代数进行简单的计算。在这种情况下使用 C++ 是没有意义的。这将引入额外的步骤,确保我们需要从 JavaScript 调用的函数被包装在一个extern块中。我们将在一个 C 文件中编写计算函数,并将其编译成一个 Wasm 模块。我们可以继续使用 VS Code 的 Tasks 功能来执行构建,但是参数需要更新,因为我们将只编译一个文件。让我们继续项目配置。

设置项目

WebAssembly 还没有出现足够长的时间来建立关于文件夹结构、文件命名约定等方面的最佳实践。如果您要为 C/C++ 或 JavaScript 项目寻找最佳实践,您会遇到大量相互矛盾的建议和强烈的意见。考虑到这一点,让我们用这一节用所需的配置文件来设置我们的项目。

这个项目的代码位于learn-webassembly存储库中的/chapter-07-cook-the-books文件夹中。当我们进入应用的 JavaScript 部分时,您必须有这些代码。我不会提供书中所有 Vue 组件的源代码,所以您需要从存储库中复制它们。

为 Node.js 配置

为了保持应用尽可能的简单,我们将避免使用像 Webpack 或 Rollup.js 这样的构建/捆绑工具。这允许我们减少所需依赖项的数量,并确保您遇到的任何问题都不是由构建依赖项的突然变化引起的。

我们将创建一个 Node.js 项目,因为它允许我们运行脚本并在本地安装一个依赖项用于开发目的。到目前为止,我们已经使用了/book-examples文件夹,但是我们将在/book-examples之外创建一个新的项目文件夹,以便在 VS Code 中配置一个不同的默认构建任务。打开终端,cd进入所需文件夹,并输入以下命令:

// Create a new directory and cd into it:
mkdir cook-the-books
cd cook-the-books

// Create a package.json file with default values
npm init -y

-y命令放弃提示并用合理的默认值填充package.json文件。完成后,运行以下命令安装browser-sync:

npm install -D browser-sync@^2.24.4

-D是可选的,表示库是开发依赖项。如果您正在构建和分发应用,您将使用-D标志,因此我将其包括在内是为了遵守惯例。我建议安装该特定版本,以确保start脚本运行没有任何问题。在browser-sync安装后,在package.json文件的scripts条目中添加以下条目:

...
"scripts": {
 ...
 "start": "browser-sync start --server \"src\" --files \"src/**\" --single --no-open --port 4000"
},
…

If you run npm init with the -y flag, there should be an existing script named test, which I omitted for clarity. If you didn't run it with the -y flag, you may need to create the scripts entry.

如果需要,您可以填充"description""author"键。该文件最终看起来应该如下所示:

{
  "name": "cook-the-books",
  "version": "1.0.0",
  "description": "Example application for Learn WebAssembly",
  "main": "src/index.js",
  "scripts": {
    "start": "browser-sync start --server \"src\" --files \"src/**\" --single --no-open --port 4000",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Mike Rourke",
  "license": "MIT",
  "devDependencies": {
    "browser-sync": "^2.24.4"
  }
}

If you omit the --no-open flag from the start script, the browser will open automatically. The flag was included to prevent issues with users running in a headless environment.

添加文件和文件夹

在根文件夹中创建两个新文件夹:/lib/src。JavaScript、HTML、CSS 和 Wasm 文件将位于/src文件夹中,而 C 文件将位于/lib中。我只想包括在/src的网络应用使用的文件。我们永远不会直接从应用中使用 C 文件,只会使用编译后的输出。

/book-examples项目中的/.vscode文件夹复制到根文件夹中。这将确保您使用现有的 C/C++ 设置,并为构建任务提供一个良好的起点。

If you're using macOS or Linux, you'll have to use the terminal to copy the folder; you can accomplish this by running the cp -r command.

配置构建步骤

我们需要修改/.vscode/tasks.json文件中的默认构建步骤,以适应我们更新的工作流程。我们在/book-examples项目中使用的构建步骤的参数允许我们编译编辑器中当前活动的任何文件。它还将.wasm文件输出到与 C 源文件相同的文件夹中。但是,这种配置对于这个项目没有意义。我们总是将输出到编译后的.wasm文件的同一个 C 文件编译到一个特定的文件夹中。为此,用以下内容更新/.vscode/tasks.jsonBuild任务中的args数组:

"args": [
  "${workspaceFolder}/lib/main.c",
  "-Os",
  "-s", "WASM=1",
  "-s", "SIDE_MODULE=1",
  "-s", "BINARYEN_ASYNC_COMPILATION=0",
  "-o", "${workspaceFolder}/src/img/main.wasm"
],

我们更改了输入和输出路径,这是args数组中的第一个和最后一个元素。现在两者都是静态路径,无论哪个文件在活动编辑器中打开,它们总是编译并输出相同的文件。

设置模拟应用编程接口

我们需要一些模拟数据和保持任何更新的方法。如果您将数据本地存储在 JSON 文件中,一旦刷新页面,您对事务所做的任何更改都将丢失。我们可以用像 Express 这样的库建立一个本地服务器,模拟一个数据库,编写路由,等等。相反,我们将利用在线提供的优秀开发工具。online too jsonstore.io 允许您为小型项目存储 JSON 数据,并提供开箱即用的端点。采取以下步骤启动并运行模拟应用编程接口:

  1. 导航至https://www.jsonstore.io/,按复制按钮将端点复制到剪贴板;这是您将向其发出 HTTP 请求的端点。
  2. 转到https://jsfiddle.net/mikerourke/cta0km6d的 js 小提琴,将您的 jsonstore.io 端点粘贴到输入中,并按下填充数据按钮。
  3. 打开一个新的标签,在地址栏中粘贴你的 jsonstore.io 端点,并在 URL 的末尾添加/transactions,然后按进入。如果您在浏览器中看到 JSON 文件的内容,则表明 API 设置成功。

将 jsonstore.io 端点放在手边——当我们构建应用的 JavaScript 部分时,您会需要它。

下载 C 标准库 Wasm

我们需要 C 的标准库中的malloc()free()函数来实现 C 代码中的功能。WebAssembly 没有内置这些功能,所以我们需要提供自己的实现。

幸运的是,有人已经为我们建造了它;我们只需要下载该模块并将其包含在实例化步骤中。该模块可从盖伊·贝德福德位于https://github.com/guybedford/wasm-stdlib-hackwasm-stdlib-hack GitHub 资源库下载。你需要/dist文件夹中的memory.wasm文件。文件下载后,在项目的/src文件夹中创建一个名为/assets的文件夹,并将memory.wasm文件复制到那里。

You can copy the memory.wasm file from the /chapter-07-cook-the-books/src/assets folder of the learn-webassembly repository instead of downloading it from GitHub.

最终结果

执行这些步骤后,您的项目应该如下所示:

├── /.vscode
│    ├── tasks.json
│    └── c_cpp_properties.json
├── /lib
├── /src
│    └── /assets
│         └── memory.wasm
├── package.json
└── package-lock.json

构建 C 部分

应用的 C 部分将汇总交易和类别金额。我们在 C 语言中执行的计算在 JavaScript 中同样容易完成,但是 WebAssembly 非常适合计算。我们将在第 8 章中更深入地探讨 C/C++ 更复杂的用法。用 Emscripten 移植一个游戏,但是目前我们试图将我们的范围限制在核心规范的范围内。在本节中,我们将编写一些 C 代码来演示如何在不使用 Emscripten 的情况下将 WebAssembly 与 web 应用集成。

概观

我们将编写一些 C 函数来计算总计以及生交易和熟交易的期末余额。除了计算总计,我们还需要计算每个类别的总计,以显示在饼图中。所有这些计算都将在一个 C 文件中执行,并编译成一个 Wasm 文件,当应用加载时,该文件将被实例化。对于不熟悉的人来说,c 可能有点令人生畏,所以为了清晰起见,我们的代码会牺牲一些效率。我想花一点时间向读这本书的 C/C++ 程序员道歉;你不会喜欢你 c

为了动态执行计算,我们需要在添加和删除事务时分配和释放内存。为此,我们将使用双链表。双向链表是一种数据结构,允许我们删除列表中的项目或节点,并根据需要添加和编辑节点。使用malloc()添加节点,使用free()移除节点,这两个都是由您在上一节下载的memory.wasm模块提供的。

关于工作流程的说明

从开发的角度来看,操作的顺序并不反映您通常如何构建使用 WebAssembly 的应用。工作流程将包括在 C/C++ 和 JavaScript 之间跳转,以达到预期的结果。在这种情况下,我们从 JavaScript 卸载到 WebAssembly 中的功能是已知的,所以我们将提前编写 C 代码。

c 文件内容

让我们浏览一下 C 文件的每个部分。在名为main.c/lib文件夹中创建一个文件,并在每个部分中填充以下内容。如果我们把 C 文件分成更小的块,就会更容易理解它发生了什么。让我们从声明部分开始。

声明

第一部分包含我们将用来创建和遍历双向链表的声明,如下所示:

#include <stdlib.h>

struct Node {
  int id;
  int categoryId;
  float rawAmount;
  float cookedAmount;
  struct Node *next;
  struct Node *prev;
};

typedef enum {
  RAW = 1,
  COOKED = 2
} AmountType;

struct Node *transactionsHead = NULL;
struct Node *categoriesHead = NULL;

Node结构用于表示事务或类别。transactionsHeadcategoriesHead节点实例代表我们将使用的每个链表中的第一个节点(一个用于事务,一个用于类别)。AmountType``enum不是必需的,但是我们将讨论当我们到达使用它的代码部分时它是如何有用的。

链表操作

第二部分包含用于在链表中添加和删除节点的两个函数:

void deleteNode(struct Node **headNode, struct Node *delNode) {
    // Base case:
    if (*headNode == NULL || delNode == NULL) return;

    // If node to be deleted is head node:
    if (*headNode == delNode) *headNode = delNode->next;

    // Change next only if node to be deleted is NOT the last node:
    if (delNode->next != NULL) delNode->next->prev = delNode->prev;

    // Change prev only if node to be deleted is NOT the first node:
    if (delNode->prev != NULL) delNode->prev->next = delNode->next;

    // Finally, free the memory occupied by delNode:
    free(delNode);
}

void appendNode(struct Node **headNode, int id, int categoryId,
                float rawAmount, float cookedAmount) {
    // 1\. Allocate node:
    struct Node *newNode = (struct Node *) malloc(sizeof(struct Node));
    struct Node *last = *headNode; // Used in Step 5

    // 2\. Populate with data:
    newNode->id = id;
    newNode->categoryId = categoryId;
    newNode->rawAmount = rawAmount;
    newNode->cookedAmount = cookedAmount;

    // 3\. This new node is going to be the last node, so make next NULL:
    newNode->next = NULL;

    // 4\. If the linked list is empty, then make the new node as head:
    if (*headNode == NULL) {
        newNode->prev = NULL;
        *headNode = newNode;
        return;
    }

    // 5\. Otherwise, traverse till the last node:
    while (last->next != NULL) {
        last = last->next;
    }

    // 6\. Change the next of last node:
    last->next = newNode;

    // 7\. Make last node as previous of new node:
    newNode->prev = last;
}

代码中的注释描述了每一步发生的事情。当我们需要向列表中添加一个节点时,我们必须使用malloc()分配struct Node占用的内存,并将其追加到链表中的最后一个节点。如果我们需要删除一个节点,我们必须从链表中删除它,并通过调用free()函数来释放该节点正在使用的内存。

交易操作

第三部分包含从transactions链表中添加、编辑和删除事务的功能,如下所示:

struct Node *findNodeById(int id, struct Node *withinNode) {
    struct Node *node = withinNode;
    while (node != NULL) {
        if (node->id == id) return node;
        node = node->next;
    }
    return NULL;
}

void addTransaction(int id, int categoryId, float rawAmount,
                    float cookedAmount) {
    appendNode(&transactionsHead, id, categoryId, rawAmount, cookedAmount);
}

void editTransaction(int id, int categoryId, float rawAmount,
                     float cookedAmount) {
    struct Node *foundNode = findNodeById(id, transactionsHead);
    if (foundNode != NULL) {
        foundNode->categoryId = categoryId;
        foundNode->rawAmount = rawAmount;
        foundNode->cookedAmount = cookedAmount;
    }
}

void removeTransaction(int id) {
    struct Node *foundNode = findNodeById(id, transactionsHead);
    if (foundNode != NULL) deleteNode(&transactionsHead, foundNode);
}

我们在前一节中回顾的appendNode()deleteNode()函数并不打算从 JavaScript 代码中调用。相反,调用addTransaction()editTransaction()removeTransaction()来更新本地链表。addTransaction()函数调用appendNode()函数将作为参数传入的数据添加到本地链表中的新节点。removeTransaction()调用deleteNode()功能删除对应的交易节点。findNodeById()功能用于根据指定的标识确定链表中哪个节点需要更新或删除。

交易计算

第四部分包含计算生熟transactions总计和最终余额的函数,如下所示:

void calculateGrandTotals(float *totalRaw, float *totalCooked) {
    struct Node *node = transactionsHead;
    while (node != NULL) {
        *totalRaw += node->rawAmount;
        *totalCooked += node->cookedAmount;
        node = node->next;
    }
}

float getGrandTotalForType(AmountType type) {
    float totalRaw = 0;
    float totalCooked = 0;
    calculateGrandTotals(&totalRaw, &totalCooked);

    if (type == RAW) return totalRaw;
    if (type == COOKED) return totalCooked;
    return 0;
}

float getFinalBalanceForType(AmountType type, float initialBalance) {
    float totalForType = getGrandTotalForType(type);
    return initialBalance + totalForType;
}

我们在声明部分声明的AmountType enum在这里用来避免幻数。很容易记住1代表原始交易,2代表熟交易。生熟交易的总计都是在calculateGrandTotals()函数中计算的,尽管我们在getGrandTotalForType()中只要求一种类型。由于我们只能从一个 Wasm 函数中返回一个值,所以当我们为原始和熟事务调用getGrandTotalForType()时,我们最终会在所有事务中循环两次。交易量相对较少,计算简单,这不会带来任何问题。getFinalBalanceForType()返回总计加上指定的initialBalance。当我们在 web 应用中添加更改初始余额的功能时,您会看到这一点。

类别计算

第五部分也是最后一部分包含按类别计算总数的函数,我们将在饼图中使用这些函数,如下所示:

void upsertCategoryNode(int categoryId, float transactionRaw,
                        float transactionCooked) {
    struct Node *foundNode = findNodeById(categoryId, categoriesHead);
    if (foundNode != NULL) {
        foundNode->rawAmount += transactionRaw;
        foundNode->cookedAmount += transactionCooked;
    } else {
        appendNode(&categoriesHead, categoryId, categoryId, transactionRaw,
                   transactionCooked);
    }
}

void buildValuesByCategoryList() {
    struct Node *node = transactionsHead;
    while (node != NULL) {
        upsertCategoryNode(node->categoryId, node->rawAmount,
                           node->cookedAmount);
        node = node->next;
    }
}

void recalculateForCategories() {
    categoriesHead = NULL;
    buildValuesByCategoryList();
}

float getCategoryTotal(AmountType type, int categoryId) {
    // Ensure the category totals have been calculated:
    if (categoriesHead == NULL) buildValuesByCategoryList();

    struct Node *categoryNode = findNodeById(categoryId, categoriesHead);
    if (categoryNode == NULL) return 0;

    if (type == RAW) return categoryNode->rawAmount;
    if (type == COOKED) return categoryNode->cookedAmount;
    return 0;
}

每当调用recalculateForCategories()getCategoryTotal()函数时,都会调用buildValuesByCategoryList()函数。该功能循环遍历transactions链表中的所有交易,并在单独的链表中为每个对应的类别创建一个节点,其中包含合计的原始金额和总金额。upsertCategoryNode()功能在categories链表中查找对应于categoryId的节点。如果找到它,原始和熟交易金额被添加到该节点上的现有金额,否则为所述类别创建新节点。调用recalculateForCategories()函数以确保类别总数随着任何交易的变化而更新。

编译到 Wasm

填充文件后,我们需要将其编译成 Wasm,以便在应用的 JavaScript 部分使用。通过选择任务|运行生成任务来运行生成任务...从菜单或使用键盘快捷键Cmd/Ctrl+Shift+B。如果构建成功,您将在/src/assets文件夹中看到一个名为main.wasm的文件。如果出现错误,终端应该提供如何解决的细节。

如果您没有使用 VS 代码,请在/cook-the-books文件夹中打开一个终端实例并运行以下命令:

emcc lib/main.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o src/img/main.wasm

C 代码到此为止。让我们继续讨论 JavaScript 部分。

构建 JavaScript 部分

应用的 JavaScript 部分向用户呈现事务数据,并允许他们轻松添加、编辑和删除事务。该应用被分割成几个文件,以简化开发过程,并使用本章中描述的库。在本节中,我们将逐步构建应用,从 API 和全局状态交互层开始。我们将编写函数来实例化和交互我们的 Wasm 模块,并查看构建用户界面所需的 Vue 组件。

概观

应用被分解成多个上下文,以简化开发过程。我们将自下而上地构建应用,以确保在编写代码时不必在不同的上下文之间来回切换。我们将从 Wasm 交互代码开始,然后进入全局存储和 API 交互。我将描述每个 Vue 组件的用途,但源代码将只提供给选定的几个。如果您正在跟进并希望在本地运行应用,您需要将learn-webassembly存储库中的/chapter-07-cook-the-books文件夹中的/src/components文件夹复制到项目的/src文件夹中。

关于浏览器兼容性的说明

在我们开始编写任何代码之前,您必须确保您的浏览器支持我们将在应用中使用的较新的 JavaScript 功能。您的浏览器必须支持专家系统模块(importexport)、提取应用编程接口和async / await。你至少需要 61 版的谷歌 Chrome 或者 60 版的火狐。您可以通过从菜单栏中选择关于 Chrome 或关于火狐来检查您当前使用的版本。我目前正在运行 Chrome 版本 67 和火狐版本 61 的应用,没有任何问题。

在 initializeWasm.js 中创建 Wasm 实例

您应该在项目的/src/assets文件夹中有两个已编译的 Wasm 文件:main.wasmmemory.wasm。由于我们需要利用从main.wasm代码中的memory.wasm导出的malloc()free()功能,我们的加载代码看起来将与前面的示例不同。在名为initializeWasm.js/src/store文件夹中创建一个文件,并用以下内容填充:

/**
 * Returns an array of compiled (not instantiated!) Wasm modules.
 * We need the main.wasm file we created, as well as the memory.wasm file
 * that allows us to use C functions like malloc() and free().
 */
const fetchAndCompileModules = () =>
  Promise.all(
    ['../img/main.wasm', '../img/memory.wasm'].map(fileName =>
      fetch(fileName)
        .then(response => {
          if (response.ok) return response.arrayBuffer();
          throw new Error(`Unable to fetch WebAssembly file: ${fileName}`);
        })
        .then(bytes => WebAssembly.compile(bytes))
    )
  );

/**
 * Returns an instance of the compiled "main.wasm" file.
 */
const instantiateMain = (compiledMain, memoryInstance, wasmMemory) => {
  const memoryMethods = memoryInstance.exports;
  return WebAssembly.instantiate(compiledMain, {
    env: {
      memoryBase: 0,
      tableBase: 0,
      memory: wasmMemory,
      table: new WebAssembly.Table({ initial: 16, element: 'anyfunc' }),
      abort: console.log,
      _consoleLog: value => console.log(value),
      _malloc: memoryMethods.malloc,
      _free: memoryMethods.free
    }
  });
};

/**
 * Compiles and instantiates the "memory.wasm" and "main.wasm" files and
 * returns the `exports` property from main's `instance`.
 */
export default async function initializeWasm() {
  const wasmMemory = new WebAssembly.Memory({ initial: 1024 });
  const [compiledMain, compiledMemory] = await fetchAndCompileModules();

  const memoryInstance = await WebAssembly.instantiate(compiledMemory, {
    env: {
      memory: wasmMemory
    }
  });

  const mainInstance = await instantiateMain(
    compiledMain,
    memoryInstance,
    wasmMemory
  );

  return mainInstance.exports;
}

文件的默认export功能initializeWasm()执行以下步骤:

  1. 创建新的WebAssembly.Memory实例(wasmMemory)。
  2. 调用fetchAndCompileModules()函数获取memory.wasm ( compiledMemory)和main.wasm ( compiledMain)的WebAssembly.Module实例。
  3. 实例化compiledMemory ( memoryInstance)并将wasmMemory传入importObj
  4. compiledMainmemoryInstancewasmMemory传入instantiateMain()功能。
  5. 实例化compiledMain并将memoryInstancewasmMemory导出的malloc()free()功能传入importObj
  6. 归还从instantiateMain ( mainInstance)归还的Instanceexports财产。

如您所见,当您在 Wasm 模块中有依赖关系时,这个过程会更加复杂。

You may have noticed that the malloc and free methods on the memoryInstance exports property weren't prefixed with an underscore. This is because the memory.wasm file was compiled using LLVM without Emscripten, which doesn't add the _.

在 WasmTransactions.js 中与 Wasm 交互

我们将使用 JavaScript 的class语法来创建一个封装 Wasm 交互函数的包装器。这使得我们可以快速地对 C 代码进行修改,而不必搜索整个应用来寻找 Wasm 函数被调用的地方。如果你在 C 文件中重命名一个方法,你只需要重命名一个地方。在名为WasmTransactions.js/src/store文件夹中创建新文件,并使用以下内容填充该文件:

import initializeWasm from './initializeWasm.js';

/**
 * Class used to wrap the functionality from the Wasm module (rather
 * than access it directly from the Vue components or store).
 * @class
 */
export default class WasmTransactions {
  constructor() {
    this.instance = null;
    this.categories = [];
  }

  async initialize() {
    this.instance = await initializeWasm();
    return this;
  }

  getCategoryId(category) {
    return this.categories.indexOf(category);
  }

  // Ensures the raw and cooked amounts have the proper sign (withdrawals
  // are negative and deposits are positive).
  getValidAmounts(transaction) {
    const { rawAmount, cookedAmount, type } = transaction;
    const getAmount = amount =>
      type === 'Withdrawal' ? -Math.abs(amount) : amount;
    return {
      validRaw: getAmount(rawAmount),
      validCooked: getAmount(cookedAmount)
    };
  }

  // Adds the specified transaction to the linked list in the Wasm module.
  addToWasm(transaction) {
    const { id, category } = transaction;
    const { validRaw, validCooked } = this.getValidAmounts(transaction);
    const categoryId = this.getCategoryId(category);
    this.instance._addTransaction(id, categoryId, validRaw, validCooked);
  }

  // Updates the transaction node in the Wasm module:
  editInWasm(transaction) {
    const { id, category } = transaction;
    const { validRaw, validCooked } = this.getValidAmounts(transaction);
    const categoryId = this.getCategoryId(category);
    this.instance._editTransaction(id, categoryId, validRaw, validCooked);
  }

  // Removes the transaction node from the linked list in the Wasm module:
  removeFromWasm(transactionId) {
    this.instance._removeTransaction(transactionId);
  }

  // Populates the linked list in the Wasm module. The categories are
  // needed to set the categoryId in the Wasm module.
  populateInWasm(transactions, categories) {
    this.categories = categories;
    transactions.forEach(transaction => this.addToWasm(transaction));
  }

  // Returns the balance for raw and cooked transactions based on the
  // specified initial balances.
  getCurrentBalances(initialRaw, initialCooked) {
    const currentRaw = this.instance._getFinalBalanceForType(
      AMOUNT_TYPE.raw,
      initialRaw
    );
    const currentCooked = this.instance._getFinalBalanceForType(
      AMOUNT_TYPE.cooked,
      initialCooked
    );
    return { currentRaw, currentCooked };
  }

  // Returns an object that has category totals for all income (deposit)
  // and expense (withdrawal) transactions.
  getCategoryTotals() {
    // This is done to ensure the totals reflect the most recent
    // transactions:
    this.instance._recalculateForCategories();
    const categoryTotals = this.categories.map((category, idx) => ({
      category,
      id: idx,
      rawTotal: this.instance._getCategoryTotal(AMOUNT_TYPE.raw, idx),
      cookedTotal: this.instance._getCategoryTotal(AMOUNT_TYPE.cooked, idx)
    }));

    const totalsByGroup = { income: [], expenses: [] };
    categoryTotals.forEach(categoryTotal => {
      if (categoryTotal.rawTotal < 0) {
        totalsByGroup.expenses.push(categoryTotal);
      } else {
        totalsByGroup.income.push(categoryTotal);
      }
    });
    return totalsByGroup;
  }
}

当在类的实例上调用initialize()函数时,initializeWasm()函数的返回值被分配给类的instance属性。class方法从this.instance调用函数,如果适用,返回所需的结果。注意getCurrentBalances()getCategoryTotals()功能中引用的AMOUNT_TYPE对象。这对应于我们 C 文件中的AmountType enumAMOUNT_TYPE对象在加载应用的/src/main.js文件中全局声明。现在我们已经编写了 Wasm 交互代码,让我们继续讨论 API 交互代码。

利用 api.js 中的 API

该应用编程接口以在提取调用中定义的 HTTP 方法的形式提供了添加、编辑、删除和查询事务的方法。为了简化执行这些动作的过程,我们将编写一些 API wrapper函数。在名为api.js/src/store文件夹中创建一个文件,并用以下内容填充:

// Paste your jsonstore.io endpoint here (no ending slash):
const API_URL = '[JSONSTORE.IO ENDPOINT]';

/**
 * Wrapper for performing API calls. We don't want to call response.json()
 * each time we make a fetch call.
 * @param {string} endpoint Endpoint (e.g. "/transactions" to make API call to
 * @param {Object} init Fetch options object containing any custom settings
 * @returns {Promise<*>}
 * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
 */
const performApiFetch = (endpoint = '', init = {}) =>
  fetch(`${API_URL}${endpoint}`, {
    headers: {
      'Content-type': 'application/json'
    },
    ...init
  }).then(response => response.json());

export const apiFetchTransactions = () =>
  performApiFetch('/transactions').then(({ result }) =>
    /*
     * The response object looks like this:
     * {
     *   "result": {
     *     "1": {
     *       "category": "Sales Revenue",
     *       ...
     *     },
     *     "2": {
     *       "category": "Hotels",
     *       ...
     *     },
     *     ...
     *   }
     * }
     * We need the "1" and "2" values for deleting or editing existing
     * records, so we store that in the transaction record as "apiId".
     */
    Object.keys(result).map(apiId => ({
      ...result[apiId],
      apiId
    }))
  );

export const apiEditTransaction = transaction =>
  performApiFetch(`/transactions/${transaction.apiId}`, {
    method: 'POST',
    body: JSON.stringify(transaction)
  });

export const apiRemoveTransaction = transaction =>
  performApiFetch(`/transactions/${transaction.apiId}`, {
    method: 'DELETE'
  });

export const apiAddTransaction = transaction =>
  performApiFetch(`/transactions/${transaction.apiId}`, {
    method: 'POST',
    body: JSON.stringify(transaction)
  });

您将需要在设置项目部分创建的 jsonstore.io 端点,以便与应用编程接口交互。将[JSONSTORE.IO ENDPOINT]替换为您的 jsonstore.io 端点。确保端点不以正斜杠或单词 transactions 结尾。

管理存储中的全局状态

管理应用中全局状态的文件有许多移动部分。因此,我们将把代码分解成更小的块,并逐个遍历每个部分。在名为store.js/src/store文件夹中创建一个文件,并用以下每个部分的内容填充它。

导入和存储声明

第一部分包含import语句以及导出的store对象的wasmstate属性,如下所示:

import {
  apiFetchTransactions,
  apiAddTransaction,
  apiEditTransaction,
  apiRemoveTransaction
} from './api.js';
import WasmTransactions from './WasmTransactions.js';

export const store = {
  wasm: null,
  state: {
    transactions: [],
    activeTransactionId: 0,
    balances: {
      initialRaw: 0,
      currentRaw: 0,
      initialCooked: 0,
      currentCooked: 0
    }
  },
  ...

所有 API 交互仅限于store.js文件。因为我们需要操作、添加和搜索事务,所以从api.js导出的所有功能都是导入的。store对象在wasm属性中持有WasmTransactions实例,在state属性中持有初始状态。state中的值在整个应用中的多个位置被引用。当应用加载时,store对象将被添加到全局window对象中,因此所有组件都可以访问全局状态。

交易操作

第二部分包含管理 Wasm 实例(通过WasmTransactions实例)和 API 中事务的函数,如下所示:

...
  getCategories() {
    const categories = this.state.transactions.map(
      ({ category }) => category
    );
    // Remove duplicate categories and sort the names in ascending order:
    return _.uniq(categories).sort();
  },

  // Populate global state with the transactions from the API response:
  populateTransactions(transactions) {
    const sortedTransactions = _.sortBy(transactions, [
      'transactionDate',
      'id'
    ]);
    this.state.transactions = sortedTransactions;
    store.wasm.populateInWasm(sortedTransactions, this.getCategories());
    this.recalculateBalances();
  },

  addTransaction(newTransaction) {
    // We need to assign a new ID to the transaction, so this just adds
    // 1 to the current maximum transaction ID:
    newTransaction.id = _.maxBy(this.state.transactions, 'id').id + 1;
    store.wasm.addToWasm(newTransaction);
    apiAddTransaction(newTransaction).then(() => {
      this.state.transactions.push(newTransaction);
      this.hideTransactionModal();
    });
  },

  editTransaction(editedTransaction) {
    store.wasm.editInWasm(editedTransaction);
    apiEditTransaction(editedTransaction).then(() => {
      this.state.transactions = this.state.transactions.map(
        transaction => {
          if (transaction.id === editedTransaction.id) {
            return editedTransaction;
          }
          return transaction;
        }
      );
      this.hideTransactionModal();
    });
  },

  removeTransaction(transaction) {
    const transactionId = transaction.id;
    store.wasm.removeFromWasm(transactionId);

    // We're passing the whole transaction record into the API call
    // for the sake of consistency:
    apiRemoveTransaction(transaction).then(() => {
      this.state.transactions = this.state.transactions.filter(
        ({ id }) => id !== transactionId
      );
      this.hideTransactionModal();
    });
  },
...

populateTransactions()函数从应用编程接口获取所有事务,并将它们加载到全局状态和 Wasm 实例中。类别名称是从getCategories()函数中的transactions数组推断出来的。当调用store.wasm.populateInWasm()时,结果被传递给WasmTransactions实例。

addTransaction()editTransaction()removeTransaction()功能执行与其名称相对应的动作。这三个函数都操作 Wasm 实例,并通过一个提取调用更新应用编程接口上的数据。每个函数都调用this.hideTransactionModal(),因为对事务的更改只能通过TransactionModal组件进行。一旦更改成功,模式应该关闭。接下来看看TransactionModal管理代码。

交易模式管理

第三部分包含管理TransactionModal组件(位于/src/components/TransactionsTab/TransactionModal.js中)的可见性和内容的功能,如下所示:

...
  showTransactionModal(transactionId) {
    this.state.activeTransactionId = transactionId || 0;
    const transactModal = document.querySelector('#transactionModal');
    UIkit.modal(transactModal).show();
  },

  hideTransactionModal() {
    this.state.activeTransactionId = 0;
    const transactModal = document.querySelector('#transactionModal');
    UIkit.modal(transactModal).hide();
  },

  getActiveTransaction() {
    const { transactions, activeTransactionId } = this.state;
    const foundTransaction = transactions.find(transaction =>
      transaction.id === activeTransactionId);
    return foundTransaction || { id: 0 };
  },
...

showTransactionModal()hideTransactionModal()功能应该是不言自明的。在表示TransactionModal的 DOM 元素上调用UIkit.modal()hide()show()方法。getActiveTransaction()功能返回全局状态下与activeTransactionId值相关联的交易记录。

余额计算

第四部分包含计算和更新全局状态下的余额对象的函数:

...
  updateInitialBalance(amount, fieldName) {
    this.state.balances[fieldName] = amount;
  },

  // Update the "balances" object in global state based on the current
  // initial balances:
  recalculateBalances() {
    const { initialRaw, initialCooked } = this.state.balances;
    const { currentRaw, currentCooked } = this.wasm.getCurrentBalances(
      initialRaw,
      initialCooked
    );
    this.state.balances = {
      initialRaw,
      currentRaw,
      initialCooked,
      currentCooked
    };
  }
};

updateInitialBalance()函数根据amountfieldName参数设置全局状态下balances对象的属性值。recalculateBalances()功能更新balances对象上的所有字段,以反映对初始余额或交易所做的任何更改。

存储初始化

文件中的最后一段代码初始化存储:

/**
 * This function instantiates the Wasm module, fetches the transactions
 * from the API endpoint, and loads them into state and the Wasm
 * instance.
 */
export const initializeStore = async () => {
  const wasmTransactions = new WasmTransactions();
  store.wasm = await wasmTransactions.initialize();
  const transactions = await apiFetchTransactions();
  store.populateTransactions(transactions);
};

initializeStore()函数实例化 Wasm 模块,从 API 获取所有事务,并填充状态的内容。这个函数是从/src/main.js中的应用加载代码中调用的,我们将在下一节中介绍。

在 main.js 中加载应用

我们需要一个入口点来加载我们的应用。在名为main.js/src文件夹中创建一个文件,并用以下内容填充:

import App from './components/App.js';
import { store, initializeStore } from './store/store.js';

// This allows us to use the <vue-numeric> component globally:
Vue.use(VueNumeric.default);

// Create a globally accessible store (without having to pass it down
// as props):
window.$store = store;

// Since we can only pass numbers into a Wasm function, these flags
// represent the amount type we're trying to calculate:
window.AMOUNT_TYPE = {
  raw: 1,
  cooked: 2
};

// After fetching the transactions and initializing the Wasm module,
// render the app.
initializeStore()
  .then(() => {
    new Vue({ render: h => h(App), el: '#app' });
  })
  .catch(err => {
    console.error(err);
  });

/src/index.html中从 cdn 中提取并加载库后,加载该文件。我们使用全局Vue对象来指定我们想要使用VueNumeric组件。我们将从/store/store.js导出的store对象添加到window作为$store。这不是最可靠的解决方案,但考虑到应用范围,这已经足够了。如果您正在创建一个生产应用,您将使用像 VuexRedux 这样的库来进行全局状态管理。为了保持简单,我们将放弃这种方法。

我们还将AMOUNT_TYPE添加到了window对象中。这样做是为了确保整个应用可以引用AMOUNT_TYPE值,而不是指定一个神奇的数字。给window赋值后,调用initializeStore()函数。如果成功触发initializeStore()功能,将创建一个新的Vue实例来呈现应用。接下来,让我们添加网络资产,然后进入 Vue 组件。

添加网络资产

在我们开始向应用添加 Vue 组件之前,让我们创建包含标记和样式的 HTML 和 CSS 文件。在名为index.html/src文件夹中创建一个文件,并用以下内容填充:

<!doctype html>
<html lang="en-us">
<head>
  <title>Cook the Books</title>
  <link
    rel="stylesheet"
    type="text/css"
    href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.6/css/uikit.min.css"
  />
  <link rel="stylesheet" type="text/css" href="styles.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.6/js/uikit.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.6/js/uikit-icons.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/accounting.umd.js"></script>
  <script src="https://unpkg.com/[email protected]/lodash.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/d3.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/vue-numeric.min.js"></script>
  <script src="main.js" type="module"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

我们只使用 HTML 文件从 cdn 中获取库,指定一个 Vue 可以渲染到的<div>,并加载main.js来启动应用。注意最后一个<script>元素的type="module"属性。这允许我们在整个应用中使用专家系统模块。现在让我们添加 CSS 文件。在名为styles.css/src文件夹中创建一个文件,并用以下内容填充:

@import url("https://fonts.googleapis.com/css?family=Quicksand");

:root {
  --blue: #2889ed;
}

* {
  font-family: "Quicksand", Helvetica, Arial, sans-serif !important;
}

#app {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.addTransactionButton {
  color: white;
  height: 64px;
  width: 64px;
  background: var(--blue);
  position: fixed;
  bottom: 24px;
  right: 24px;
}

.addTransactionButton:hover {
  color: white;
  background-color: var(--blue);
  opacity: .6;
}

.errorText {
  color: white;
  font-size: 36px;
}

.appHeader {
  height: 80px;
  margin: 0;
}

.balanceEntry {
  font-size: 2rem;
}

.tableAmount {
  white-space: pre;
}

这个文件只有几个类,因为大多数样式都是在组件级别处理的。在下一节中,我们将回顾组成我们应用的 Vue 组件。

创建 Vue 组件

使用 Vue,我们可以创建封装自己功能的独立组件,然后组合这些组件来构建应用。这使得调试、可扩展性和变更管理比将应用存储在单个整体文件中容易得多。

该应用使用每个文件一个组件的开发方法。在我们开始查看组件文件之前,让我们看看成品。下面是选择了 TRANSACTIONS 选项卡的应用的屏幕截图:

Running the application with TRANSACTIONS tab visible

下面是选择了 CHARTS 选项卡的应用的屏幕截图:

Running the application with the CHARTS tab visible

Vue 组件的结构

Vue 组件只是一个带有导出对象的文件,该对象包含定义该组件外观和行为的属性。属性的名称必须符合 Vue 应用编程接口。你可以在https://vuejs.org/v2/api阅读到这些属性和 Vue API 的其他方面。下面的代码表示一个包含此应用中使用的 Vue API 元素的示例组件:

import SomeComponent from './SomeComponent.js';

export default {
  name: 'dummy-component',

  // Props passed from other components:
  props: {
    label: String,
  },

  // Other Vue components to render within the template:
  components: {
    SomeComponent
  },

  // Used to store local data/state:
  data() {
    return {
      amount: 0
    }
  },

  // Used to store complex logic that outside of the `template`:
  computed: {
    negativeClass() {
      return {
        'negative': this.amount < 0
      };
    }
  },

  // Methods that can be performed within the component:
  methods: {
    addOne() {
      this.amount += 1;
    }
  },

  // Perform actions if the local data changes:
  watch: {
    amount(val, oldVal) {
      console.log(`New: ${val} | Old: ${oldVal}`);
    }
  },

  // Contains the HTML to render the component:
  template: `
    <div>
      <some-component></some-component>
      <label for="someAmount">{{ label }}</label>
      <input
        id="someAmount"
        :class="negativeClass"
        v-model="amount"
        type="number"
      />
      <button @click="addOne">Add One</button>
    </div>
  `
};

每个属性上面的注释描述了它的用途,尽管级别很高。让我们通过查看App组件来看看 Vue 的运行情况。

应用组件

App组件是渲染应用中所有子组件的基础组件。我们将简要回顾一下App组件的代码,以便更好地理解 Vue。接下来,我们将描述每个剩余组件所扮演的角色,但仅回顾相应代码的部分。位于/src/components/App.jsApp组件文件的内容如下所示:

import BalancesBar from './BalancesBar/BalancesBar.js';
import ChartsTab from './ChartsTab/ChartsTab.js';
import TransactionsTab from './TransactionsTab/TransactionsTab.js';

/**
 * This component is the entry point for the application. It contains the
 * header, tabs, and content.
 */
export default {
  name: 'app',
  components: {
    BalancesBar,
    ChartsTab,
    TransactionsTab
  },
  data() {
    return {
      balances: $store.state.balances,
      activeTab: 0
    };
  },
  methods: {
    // Any time a transaction is added, edited, or removed, we need to
    // ensure the balance is updated:
    onTransactionChange() {
      $store.recalculateBalances();
      this.balances = $store.state.balances;
    },

    // When the "Charts" tab is activated, this ensures that the charts
    // get automatically updated:
    onTabClick(event) {
      this.activeTab = +event.target.dataset.tab;
    }
  },
  template: `
    <div>
      <div class="appHeader uk-background-primary uk-flex uk-flex-middle">
        <h2 class="uk-light uk-margin-remove-bottom uk-margin-left">
          Cook the Books
        </h2>
      </div>
      <div class="uk-position-relative">
        <ul uk-tab class="uk-margin-small-bottom uk-margin-top">
          <li class="uk-margin-small-left">
            <a href="#" data-tab="0" @click="onTabClick">Transactions</a>
          </li>
          <li>
            <a href="#" data-tab="1" @click="onTabClick">Charts</a>
          </li>
        </ul>
        <balances-bar
          :balances="balances"
          :onTransactionChange="onTransactionChange">
        </balances-bar>
        <ul class="uk-switcher">
          <li>
            <transactions-tab :onTransactionChange="onTransactionChange">
            </transactions-tab>
          </li>
          <li>
            <charts-tab :isActive="this.activeTab === 1"></charts-tab>
          </li>
        </ul>
      </div>
    </div>
  `
};

我们使用components属性来指定我们将在template中为App组件渲染的其他 Vue 组件。返回本地状态的data()功能用于跟踪余额和哪个选项卡处于活动状态(TRANSACTIONS 或 CHARTS)。methods属性包含两个功能:onTransactionChange()onTabClick()。如果交易记录发生变化,onTransactionChange()函数调用$store.recalculateBalances()并在本地状态下更新balancesonTabClick()功能将本地状态下activeTab的值更改为被点击标签的data-tab属性。最后,template属性包含用于呈现组件的标记。

If you're not using single file components in Vue (.vue extension), you need to convert the component name to kebab case in the template property. For example, in the App component shown earlier, BalancesBar was changed to <balances-bar> in the template.

平衡杆

/components/BalancesBar文件夹包含两个组成文件:BalanceCard.jsBalancesBar.jsBalancesBar组件跨“交易”和“图表”选项卡存在,并直接位于选项卡控件下。它包含四种BalanceCard成分,每种平衡类型一种:初始生、当前生、初始熟和当前熟。代表初始余额的第一张和第三张卡包含输入,因此可以更改余额。代表当前余额的第二张和第四张卡在 Wasm 模块中动态计算(使用getFinalBalanceForType()功能)。以下片段取自BalancesBar组件,演示了 Vue 的绑定语法:

<balance-card
  title="Initial Raw Balance"
  :value="balances.initialRaw"
  :onChange="amount => onBalanceChange(amount, 'initialRaw')">
</balance-card>

valueonChange属性前面的:表示这些属性绑定到 Vue 组件。如果balances.initialRaw的值发生变化,BalanceCard中显示的值也会更新。该卡的onBalanceChange()功能在全局状态下更新balances.initialRaw的值。

TransactionsTab

/components/TransactionsTab文件夹包含以下四个组成文件:

  • ConfirmationModal.js
  • TransactionModal.js
  • TransactionsTab.js
  • TransactionsTable.js

TransactionsTab组件包含TransactionsTableTransactionsModal组件,以及用于添加新交易的按钮。通过TransactionModal组件进行更改和添加。TransactionsTable包含所有当前交易,每行都有按钮,用于编辑或删除交易。如果用户按下删除按钮,ConfirmationModal组件出现并提示用户继续。如果用户按“是”,交易将被删除。以下片段取自TransactionsTable组件中的methods属性,演示了显示值的格式:

getFormattedTransactions() {
  const getDisplayAmount = (type, amount) => {
    if (amount === 0) return accounting.formatMoney(amount);
    return accounting.formatMoney(amount, {
      format: { pos: '%s %v', neg: '%s (%v)' }
    });
  };

  const getDisplayDate = transactionDate => {
    if (!transactionDate) return '';
    const parsedTime = d3.timeParse('%Y-%m-%d')(transactionDate);
    return d3.timeFormat('%m/%d/%Y')(parsedTime);
  };

  return $store.state.transactions.map(
    ({
      type,
      rawAmount,
      cookedAmount,
      transactionDate,
      ...transaction
    }) => ({
      ...transaction,
      type,
      rawAmount: getDisplayAmount(type, rawAmount),
      cookedAmount: getDisplayAmount(type, cookedAmount),
      transactionDate: getDisplayDate(transactionDate)
    })
  );
}

前面显示的getFormattedTransactions()功能对每个transaction记录中的rawAmountcookedAmounttransactionDate字段应用格式。这样做是为了确保显示的值包括一个美元符号(金额),并以用户友好的格式呈现。

图表

/components/ChartsTab文件夹包含两个组成文件:ChartsTab.jsPieChart.jsChartsTab组件包含两个PieChart组件实例,一个用于收入,一个用于支出。每个PieChart组件按类别显示生的或熟的百分比。用户可以通过图表正上方的按钮在生视图或熟视图之间切换。PieChart.js中的drawChart()方法使用 D3 渲染饼图和图例。加载时,它使用 D3 的内置动画制作饼图的每个部分:

arc
  .append('path')
  .attr('fill', d => colorScale(d.data.category))
  .transition()
  .delay((d, i) => i * 100)
  .duration(500)
  .attrTween('d', d => {
    const i = d3.interpolate(d.startAngle + 0.1, d.endAngle);
    return t => {
      d.endAngle = i(t);
      return arcPath(d);
    };
  });

前面的片段取自PieChart.js中的drawChart(),仅用几行代码定义了饼图的动画。如果您有兴趣了解更多关于 D3 的功能,请查看https://bl.ocks.org的一些示例。组件评审到此为止;让我们尝试运行应用。

运行应用

您已经编写并编译了 C 代码,并添加了前端逻辑。是时候启动应用并与之交互了。在这一部分中,我们将验证您的应用的/src文件夹,运行应用,并测试特性以确保一切正常工作。

正在验证/src 文件夹

在启动应用之前,请参考以下结构,以确保您的/src文件夹结构正确,并且包含以下内容:

├── /assets
│    ├── main.wasm
│    └── memory.wasm
├── /components
│    ├── /BalancesBar
│    │    ├── BalanceCard.js
│    │    └── BalancesBar.js
│    ├── /ChartsTab
│    │    ├── ChartsTab.js
│    │    └── PieChart.js
│    ├── /TransactionsTab
│    │    ├── ConfirmationModal.js
│    |    ├── TransactionModal.js
│    |    ├── TransactionsTab.js
│    |    └── TransactionsTable.js
│    └── App.js
├── /store
│    ├── api.js
│    ├── initializeWasm.js
│    ├── store.js
│    └── WasmTransactions.js
├── index.html
├── main.js
└── styles.css

如果一切都符合,你就可以继续了。

启动它!

要启动应用,请在/cook-the-books文件夹中打开一个终端,并运行以下命令:

npm start

browser-sync我们在本章第一节中安装的开发依赖项充当本地服务器(类似于serve库)。它使应用可以从package.json文件中指定的端口(在本例中为4000)在浏览器中访问。如果您在浏览器中导航到http://localhost:4000/index.html,您应该会看到:

Application on initial load We're using browser-sync instead of serve because it watches for changes in your files and automatically reloads the application if you make a change. To see this in action, try changing the contents of the title bar in App.js from Cook the Books to Broil the Books. The browser will refresh and you'll see the updated text in the title bar.

测试一下

为了确保一切正常,让我们测试一下应用。以下各节描述了应用特定功能的操作和预期行为。跟着看你是否得到了预期的结果。如果遇到问题,您可以随时回到learn-webassembly存储库中的/chapter-07-cook-the-books文件夹。

更改初始余额

尝试更改初始生平衡和初始熟平衡BalanceCard组件上的输入值。当前原始余额和当前熟余额卡值应更新以反映您的更改。

创建新交易

记下当前的生天平和熟天平,然后按窗口右下角的蓝色“添加”按钮。它应该加载TransactionModal组件。填写输入,记下您输入的类型生量熟量,然后按保存按钮。

余额应该已经更新,以反映新的数额。如果选择了类型的提取,余额应该会减少,否则会增加(对于存款),如下图所示:

TransactionModal when adding a new transaction

删除现有交易

TransactionsTable组件中选择一行,记下金额,然后按下看起来像垃圾桶的按钮记录。ConfirmationModal组件应该出现。当您按下按钮时,交易记录不应再出现在表格中,当前余额应更新以反映与已删除交易相关的金额,如下图所示:

Confirmation modal shown after delete button is pressed

编辑现有交易

除了更改现有金额之外,请遵循与创建新交易相同的步骤。检查当前余额,确保它们反映更新的交易金额。

测试图表选项卡

选择图表选项卡加载ChartsTab组件。按下每个PieChart组件中的按钮,在生视图和熟视图之间切换。饼图应使用更新后的值重新呈现:

Contents of CHARTS tab with different amount types selected

包裹

恭喜,您刚刚构建了一个使用 WebAssembly 的应用!告诉你的朋友!现在您已经了解了 WebAssembly 的功能和局限性,是时候扩展我们的视野并使用 Emscripten 提供的一些优秀功能了。

摘要

在本章中,我们从头开始构建了一个使用 WebAssembly 的会计应用,没有 Emscripten 提供的任何额外功能。通过遵守核心规范,我们展示了当前形式的 WebAssembly 的局限性。然而,我们能够通过使用 Wasm 模块快速执行计算,这非常适合会计。我们使用 Vue 将我们的应用分割成组件,使用 UIkit 进行设计和布局,使用 D3 根据我们的交易数据创建饼图。在第 8 章用 Emscripten 移植游戏中,我们将充分利用 Emscripten 将现有的 C++ 代码库移植到 WebAssembly 中。

问题

  1. 为什么我们在这个应用中使用 Vue(而不是 React 或 Angular)?
  2. 为什么我们在这个项目中使用 C 而不是 C++ 呢?
  3. 为什么我们需要使用 jsonstore.io 建立一个模拟 API,而不是将数据本地存储在一个 JSON 文件中?
  4. 我们用来管理 C 文件中事务的数据结构的名称是什么?
  5. 我们需要memory.wasm文件中的哪些功能,它们是用来做什么的?
  6. 为什么我们要围绕 Wasm 模块创建一个包装类?
  7. 为什么我们要将$store对象全局化?
  8. 您可以在生产应用中使用哪些库来管理全局状态?
  9. 为什么我们用browser-sync而不是serve来运行应用?

进一步阅读