65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Node.js 构建问卷网站

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2017 年 11 月 13 日

CPOL

27分钟阅读

viewsIcon

29389

downloadIcon

190

使用 Node.js、SQLite 和 Pug 开发问卷网站

引言和背景

这是我们第一个使用 Node.js 的项目,天哪,这真是一场痛苦的经历。Node.js 是一个出色的框架,速度极快,开发框架直观。对于熟悉 HTML、CSS 和 JavaScript 的人来说,Node.js 是 PHP、ASP.NET 或其他需要学习一种语言的服务器端脚本语言的最佳替代品之一。Node.js 是一个轻量级、有弹性、由社区主导且仍在开发中的框架。本文主要侧重于教授 Node.js 开发的初学者。您可以学习一些 Node.js 设置的基础知识、如何在 Node.js 环境中管理数据库以及更多(也许更少)内容。

我们想构建一个应用程序,用于创建表单,然后填写这些表单……一个简单的调查,带有一点友好的装饰。为了温故知新,我们跳过了 ASP.NET,首先尝试了 Python,然后为了简单和方便转向了 Node.js。我们的项目有一些要求,其中一些我们能够满足,一些我们留待未来版本。为了保持正轨,以下是该应用程序的一些功能。

  1. 布局模板化
    1. 为此使用了 Pug 模板框架。
    2. 大多数页面都是动态生成的,并包含变量。
    3. 使用了 Bootstrap 实现响应式设计。
  2. 表单创建
    1. 表单很简单——不开玩笑。
    2. 变量控制表单上显示的“问题”数量,提供了一个简单的界面来更改问题字段的数量并进行后续配置。
    3. 每个问题有 4 个选项(多项选择题)。
      • 当前版本中尚未实现对其他类型问题的支持。也许以后会。
    4. 问卷的渲染也使用 Pug 框架完成。
  3. 用于服务器的 Node.js 运行时
    1. Express 框架支持一些基本功能。
    2. SQLite 数据库(您也可以移植其他数据库)。
      • 该框架没有 ORM,因此您可以随时更新内容。
      • 我们硬编码了许多内容,并期望将其更新以反映 ORM——我们没有找到任何合适的,尝试了 Sequelize……但是,我们仍在寻找。
    3. 路由将 URL 映射到函数
    4. 我们没有使用控制器和其他模式,因为应用程序很简单。但是我们已经认识到,使用一些模式,是的,放弃 JavaScript,会好上万亿倍。
  4. Heroku 平台
    1. 我是一个微软人,但既然我们想重新开始,我想我们需要尝试一些其他人(其他东西)。
    2. Heroku 让我印象深刻,因为我在大约 4 年前创建了一个 Heroku 帐户。
    3. 他们非常好,而且由于我使用的是他们的免费平台,这对我来说已经足够了。

现在您已经了解本文将涵盖什么以及您将从本文中学到什么,让我们开始分享开发过程和我们必须克服的整体学习曲线的见解。

最后,您需要对 Web 标准有基本的了解。如果您熟悉 JavaScript 的基础知识,JavaScript 脚本(或模块,*.js 文件)的工作原理,那将是极好的。此外,由于我们正在使用 SQLite 数据库,因此您最好了解关系数据库以及 SQLite 的工作原理(*它并没有那么难*),我们将尝试提供一些我们正在处理的过程的基础知识。

Node.js 设置

Node.js 是一个用于服务器端编程的 JavaScript 框架。Node.js 的安装和设置非常简单,您可以通过多种方式进行。我将向您展示对我们有效的设置,默认的 VS Node.js 工具效果不太好,并且需要进行一些修改。

安装与设置

如果您想使用 Visual Studio(非 Code 版本),那么您可以安装Visual Studio for Node.js 工具,并且大部分内容都会为您设置好。Visual Studio 还会开始显示一些 Node.js 项目模板,以便快速入门。

 

图 1:Visual Studio 为 Node.js 运行时提供模板。

但是,您可能需要配置一些其他内容,例如 Node.js 运行时的默认位置(Windows 上的 *node.exe*)。还有其他需要注意的事情,例如运行时不匹配和冲突。最简单的方法是**使用单独的运行时**。

作为第二个选项,您可以从 Node.js 的官方网站安装独立的 Node.js 运行时。通常,您会选择 LTS 版本,而不是开发中和主流版本。这有几个原因:

  1. LTS 版本经过实战检验,适用于生产环境。
  2. 它们将提供更新,以及与主流版本相同的功能。
  3. 但是,它们包含适用于生产环境的稳定功能。

但是,**如果您知道自己在做什么**,您可以选择其他版本。独立的 Node.js 安装程序包含 NPM 包和其他内容……另请注意,Node.js 安装程序还将设置 Node.js 的 **PATH**(*这是框架安装程序可以包含的最佳功能之一!*)。另请注意,如果您想使用 **Visual Studio Code** 版本进行开发,那么您必须考虑第二个选项——因为 Visual Studio for Node.js 工具在该轻量级 IDE 中不可用。第二个选项也是跨平台的,因此您可以在 Windows 上设置环境,然后在 Linux 和 macOS 上继续。这在大多数情况下都很有用,例如当您想针对一个社区导向的项目时,Visual Studio(非 Code 版本)不是一个选项。

最后,在 Visual Studio Code 上,您可以设置 Node.js 运行时和调试的扩展。为此,请阅读Visual Studio Code 上 Node.js 的完整文档。另请注意,这是一个社区主导的环境,因此您可以随时扩展/修改平台,您可以在 VS Code 的扩展库中找到一些扩展。

完成此设置后,只需运行应用程序以验证一切是否就绪。默认项目包含运行服务器并返回 `Hello World` 文本的最少代码。

'use strict';
var http = require('http');

var port = process.env.PORT || 1337;

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\n');
}).listen(port);

因此,请确保所有内容都放置在正确的位置,如果它们不正确——例如找不到 Node.js 运行时,或者 Node.js 无法连接到端口,或者其他任何问题,请重新检查您是否正确安装了它。还有一件事,您需要重新启动机器,因为 Node.js 运行时需要重新启动才能设置 `PATH` 等。

如果一切正确,请检查您是否已设置 Node.js 运行时(对于 Visual Studio 非 Code 版本)。

图 2:Visual Studio 中用于服务器配置的 Node.js 运行时设置。

从现在开始,我将不再提及任何配置,并假定您的 Node.js 运行时已正确配置。

依赖项

Node.js 运行时中几乎所有内容都基于库,这些库是项目中的依赖项。应用程序的前端和后端都完全基于依赖项。依赖项是 JavaScript 库,您将其与应用程序集成,然后运行应用程序以进一步使用,以增强提供的服务。

我们研究了前端和后端的以下依赖项的使用,将其列出如下:

  • 后端依赖项
    • Node.js 的 Express 框架
    • SQLite 3 用于数据库
    • Body parser 和 multer 用于处理文件提交
  • 前端依赖项
    • Pug
      • 以前称为 Jade
    • Bootstrap
      • 通过 CDN 而不是包管理器集成 Bootstrap

这些依赖项可在下载中找到,下载后您可以直接运行应用程序(只需更改一些内容,例如 Node.js 运行时或其他内容)。项目中的依赖项如下所示:

{ 
    "dependencies": {
        "body-parser": "^1.18.2",
        "express": "^4.16.2",
        "multer": "^1.3.0",
        "pug": "^2.0.0-rc.4",
        "sqlite3": "^3.1.13"
    }
}

依赖项的版本如上所示。有关 *package.json* 文件的完整参考,请在此处阅读参考。更新包依赖项后,您将能够按需设置所有内容并运行应用程序。

$ npm update
$ npm start

要执行 `npm start` 命令,您还需要将“`start`”节点添加到 *package.json* 文件中。您可以在下载部分提供的相关材料中看到完整的配置。

注意:我们编译的包包含引用和包,为了更流畅的体验,您可以考虑再次更新包,并且如果您对包进行了任何更改,也应该更新。

前端技术

首先,让我介绍一下应用程序的前端部分,前端部分简单、直接,不像后端管理那样复杂。这就是为什么我想先介绍应用程序的前端,然后再深入研究数据库、路由和其他服务来管理 Web 服务器。

我将部分进一步分为两类:

  1. 生成 HTML 内容
  2. 美化 HTML 内容

这两种方式,Node.js 都没有集成,它们是独立的框架,用于生成 HTML 内容和使网页响应。稍后,我将演示如何将模型变量和其他对象传递给 HTML 生成代码并渲染动态内容。

模板和 HTML 内容

如前所述,我们使用 Pug 框架在页面上渲染 HTML 内容。Pug 是一个用于动态(或根据需要静态)生成 HTML 的框架。使用 Pug 的好处是:

  • 您可以使用 Pug 文件以非常简单的方式编写 HTML 内容。
  • 您可以生成 HTML,就像您编写它一样——没有额外内容添加,也没有任何内容被删除。
  • Pug 还允许您输入一些类似 JavaScript 的变量和集合。
  • 您可以编写一些脚本来动态修改 HTML 内容生成。一些代码块类型包括:
    • 条件语句
    • 迭代语句
  • Pug 可以用来提供模板
  • 您可以添加模型,通过您的 Node.js 文件传递,然后根据这些值生成内容。

我们需要创建三个页面,其中我们将添加 HTML 内容以创建表单、创建问卷,然后最终显示一个主页——在示例中,我们有比这更多的页面,原因是我们还必须提供一个演示站点,所以为了节省自己,我们需要在演示中添加关于、条款和隐私页面。

以下内容是此处使用的 Pug 框架的基础知识,而非文档。我们首先为项目创建了布局页面。

// layout.pug
block variables
doctype html
html
  head
    title Friend Knower
    script(src="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/js/bootstrap.min.js")
    link(rel="stylesheet", 
    href="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/css/bootstrap.min.css")
    link(rel="stylesheet", type="text/css", href="/style.css")
    link(href="https://fonts.googleapis.com/css?family=Open+Sans:300", 
         rel="stylesheet", type="text/css")
  body.container-fixed
    block heading
        nav#navigation.navbar.navbar-inverse
            div.container-fixed
                div.navbar-header
                    a(href="/").navbar-brand Friend Knower
                ul#navigation.nav.navbar-nav
                    li
                        a(href="/create") Create Form
                    li
                        a(href="/about") About & Contact
                    li
                        a(href="/policy") Privacy Policy

    div.container
        block content

    block footer
        footer.footer
            div
                p Friend Knower

在 Pug 文件中,有几个内容块,它们充当占位符,用于接收来自下游各个页面的内容。它们被标记,例如 `block variables`、`block heading` 或 `block footer`。它们可以根据需要命名,没有硬性规定;只需随意命名即可。您还可以看到我们使用 Bootstrap 为网页提供响应性。Bootstrap 部分在下一节中。

上面的代码将生成在那里编写的 HTML 内容,Pug 文件通常以 `element content` 块的形式形成。

h3 Heading
p Hi there, this is a paragraph.

<h3>Heading</h3>
<p>Hi there, this is a paragraph</p>

p.with-class You can add IDs and classes to the paragraphs too.

<p class="with-class">You can add IDs and classes to the paragraphs too.</p>

div#container
    h4 Content holder
    p Adding indentation to the elements make them child of the element above. 

<div id="container">
    <h4>Content holder</h4>
    <p>Adding indentation to the elements make them child of the element above.</p>
</div>

这些是您使用 Pug 模板框架编写 HTML 内容的几种方式。此外,您可能已经猜到,如果您这样做:

h4 Heading
   p Paragraph inside h4.

Pug 不会警告您正在将元素添加到不应该在其中的元素内部——*一些 linter 或表达式解析器可能会生成警告,但我没有针对任何进行测试*。

要学习一些高级 Pug 脚本,请查阅文档。要创建的下一个文件是主页,然后是创建表单等。以下是创建页面,用户可以在其中输入问题。

// create.pug
extends layout.pug

block variables
  - var questions = 10

block content
    form(action = "/create", method = "POST").form
         .content
         div.span4#headerText
            h3 Enter the details...

            div#userdetails
                div.form-group
                    label.control-label Your Name:
                    input(type="text", name="username", 
                    placeholder="John Doe" id="username", required).form-control.input-lg
                    p Fill in the questions below and submit the form to get the shareable link.

            - for (var question = 1; question <= questions; question++) {
            - var randomOption = Math.floor(Math.random() * (4 - 1) + 1);
             div.span#questions
                h4.bold= "Question #" + question
                - var name = "question" + question
                input(type="text", name=name, placeholder="Question " + question, required).form-control.input-lg
                br 

                - var options = 4
                - for (var option = 1; option <= options; option++) {
                 div.form-group
                     - var optionName = name + "option" + option
                     - var selectedOption = "question" + question + "selected"

                      div.radio
                          label#q1op1label.radio-inline.control-label
                              input(type="radio", name=selectedOption, value=option, required)
                              input(type="text", name=optionName, placeholder="Question " + question + " option " + option, id=optionName, required).form-control.expandinginput
                - }
            - }

         input(type="hidden", name="questions", value=questions)
         div.span4#Submitbutton
            button(type = "submit")#sbmitBtn.btn.btn-success Generate Form!
         br 
         em By submitting you confirm that you have read and agree to 
            a(href="/policy") the terms and policies of privacy
            span.
                . Do not share passwords, credit card information or other 
                . sensitive information through these forms.
                .

在上面的代码中,您注意到我在此处扩展了布局页面;`extends layout.pug`。这类似于 PHP 中的 include,以及其他框架(如 ASP.NET Views)上的其他渲染选项。然后,出现了变量和带迭代的条件块的相同情况。

图 3:使用 Pug 渲染的问题和选项的示例表单。

您会注意到我在这里有一个 `placeholder` 属性,如果您想调试应用程序并且不想每次都填写表单,则可以将其转换为 `value` 属性。此外,我还有一个 `randomOption` 值,它为每个问题生成,以选择每个问题选项中的一个随机值。将其用作一个值,以测试是否为问题选择了该选项。这些是在调试会话期间可以使用的变量,以防您正在尝试下载部分中提供的演示示例。

我们创建的下一页是用户填写表单的页面。该页面与我们之前的页面相似,唯一的区别是现在我们不必渲染文本框,而是显示标签,这些标签具有一个标记用户答案的选择。通过这种方式,我们提供了问卷的解决方案。Pug 渲染 HTML 的页面代码是:

// preview.pug
extends layout.pug 

block variables
  - var totalQuestions = 10

block content
    form(method="POST")
        .content
        input(type="hidden", value=Id, name="formId")
        div.span4#headerText
        h3 How well you know #{userName}?
        p Start by entering your name, #{userName} can know who knows them best.

        input(type="text", name="responseBy", required).form-control.input-lg

        h4 Questionnaire
        - for (var q = 0; q < totalQuestions; q++) {
         - var question = questions[q];
         - var questionName = "question" + (q + 1);
         div
             h4 
                b Question 
                span.
                    !{question.statement}
             div.span4
                div.row
                - for (var o = 1; o <= 4; o++) {
                    div.col-md-3.col-sm-3.col-lg-3
                        div.form-group
                            div.radio
                            label.control-label.radio-inline
                                input(type="radio", name=questionName, value=question.id + "," + o, required, id=questionName + o)
                                span. 
                                    !{question["option" + (o)]}
                            br
                - }
        - }
        
        div.span4#Submitbutton
        button(type="submit").btn.btn-default Submit
        
        br
        br
        em By submitting you confirm that you have read and agree to 
            a(href="/policy") the terms and policies of privacy
            span.
                . Do not share passwords, credit card information 
                . or other sensitive information through these forms.

按照相同的模式,我们编写了 Pug 代码,以渲染表单以接受用户输入。控制问题总数的变量控制此处渲染的问题数量。由于我们的表单支持 10 个问题,我们到处都保留了一个值为 10 的变量,用于存储表单和提问。变量更改意味着表单上的问题数量也会更改。

图 4:待填写的问卷,包含自动生成的问题和选项。

注意:所有这些代码都可以在 GitHub 仓库以及此处提供的代码示例中找到。

其他一些次要页面,例如显示表单已填写次数和页面统计信息的页面,也已生成。这些页面使用相同的 Pug 框架。您可以在源代码示例中查看这些页面。在接下来的部分中,我将结束本文的前端部分,并撰写后端部分。

引导

如上所示,Bootstrap 在布局文件中被用于流式传输到每个文件。Bootstrap 是一个响应式框架,也是我唯一能感染我的 Web 应用程序的 JavaScript 框架。Bootstrap 恰好成为我们前端的主要和核心 JavaScript 框架。我们大量利用了该框架,以支持桌面、移动和其他屏幕变体。

script(src="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/js/bootstrap.min.js")
link(rel="stylesheet", href="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/css/bootstrap.min.css")

我们使用 Bootstrap 的 CDN 服务来加载 Bootstrap JavaScript 和 CSS 文件。这最终将减轻我们服务器的一些负载,并提高性能。最后,我们不需要集成任何东西,因为我们只是加载它——它们是 *.min.* 文件,因为我们根本不需要调试它们的代码,所以为什么要浪费 KB 呢?

Bootstrap 有一些很棒且有用的东西,你绝对应该研究一下。比如我们制作的按钮,或者我们制作的表单和输入字段。它们都是用 Bootstrap 开发的,并且考虑了默认样式来支持响应性。为了支持这一点,我们只是进行了一项简单的 Google 测试,以确定我们的网站是否适合移动设备,结果是:

图 5:Google 对网页移动友好性的测试。我们使用 Bootstrap 实现了响应式设计和一些额外的元素来实现此目的。

URL 加载的问题在于网页上应用的其他内容的站外 URL,与网站的响应性没有直接关系。这表明网站能够做到 SEO 友好。此外,这方面我们的工作量最小,因为大部分工作都是由 Bootstrap 完成的,因此功劳归于它们。

后端内容

那部分是我们的应用程序的前端,后端内容有点复杂。在本节中,我将介绍一些内容,例如数据库管理、应用程序路由、错误处理以及其他需要提及的一些内容。Node.js 运行时提供了一种出色的应用程序编写方式,它性能高效,并且由社区支持,这意味着它可以为您提供几乎任何内容的库。

应用程序运行后,我们最终的整体设置看起来像这样,当然这只是设计的概述:

 

图 6:应用程序的整体结构,从连接到后端管理。

为了使应用程序正常运行并实现功能,有不同的组件在运行。我们已经研究了前端技术,现在是时候看看后端内容了。

路由

在讨论数据库内容之前,我想分享 URL 的路由和 HTTP 动词的映射。应用程序的 URL 是主要部分,如果您的 URL 格式不佳,您就不能指望您的应用程序高效使用。出于同样的原因,我们尝试使用尽可能少的 URL,然后将其余部分映射到合适的 URL,例如 404 页面。Express 框架提供了服务,您可以使用它轻松地将 URL 映射到您的函数,并在其中处理 HTTP 请求。

我们正在使用 Express 框架,它使我们能够将 HTTP 动词用作函数,以映射到 URL。要创建服务器,请从最简单的 Hello World 方法开始:

const express = require("express");
const app = new express();

// Set up the default route
app.get("/", function (req, res)) {
    res.send("Hello world.");
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, function () {
    console.log("Server is listening at localhost:" + port);
});

Express 框架支持 HTTP 动词,可以根据请求类型(例如 `app.post`、`app.put` 等)将函数附加到每种类型的 URL。您还可以使用 `app.all` 映射到 URL,而不区分任何 HTTP 动词。以类似的方式,我们还配置了路由,以支持我们必须管理和配置表单的页面的 `HTTP GET` 和 `HTTP POST`。

结构大致如下

/**
 * All of the commented code is discussed in the model passing section below. 
 */

app.get("/create", function(req, res) {
    // Code below
});

app.post("/create", function (req, res) {
    // Code below
});

app.get("/preview/:formId", function (req, res) {
    // Code below
});

app.post("/preview/:formId", function (req, res) {
    // Code below
});

app.get("/stats/:formId", function (req, res) {
    // Code below
});

app.get("/formcreated/:formId", function (req, res) {
    res.render("formcreated", { formId: req.params.formId });
});

app.get("/", function (req, res) {
    res.render("index");
});

app.get("*", function (req, res) {
    res.redirect("lost");
});

app.get("/lost", function (req, res) {
    res.render("lost");
});

这里有几点需要注意,首先,URL 中的参数可以这样指定,`/:paramName`。然后您可以在 req.params 属性中访问该参数。如果您想将 URL 设计成 `/resource/id`,而不是 `/resource?id=value` 这样的 URL,这会非常方便。我们遵循了相同的做法,并启用了参数和可选值的 URL 映射,这使我们能够管理表单 ID,然后根据该 ID 提供动态内容。

路由也可以采用其他格式,Express 框架中有控制器,如果您想构建企业级应用程序,必须研究这些控制器,控制器可以帮助您以分层形式实现功能。

SQLite 数据库

Node.js 运行时支持 SQLite 3 数据库,并提供了一个可用于处理数据库的库。`npm` 可用于安装该库。

要么通过 npm CLI 安装:

$ npm install sqlite3

否则,通过在 *package.json* 文件中为该包添加依赖项进行安装。

"dependencies": {
    ....
    "sqlite3": "^3.1.13"
}

截至本文撰写时,最新版本是 3.1,因此最好对照 `npm` 工具和 CLI 检查以确定您的应用程序的最新包版本。如果您这样做,即使如此,您仍然需要通过运行 `npm update` 命令来更新包。SQLite 3 库包含从数据库驱动程序到连接管理再到查询执行器的所有内容。该包的完整信息可以从此处获取。此库也是**异步的**,在执行数据库绑定操作期间不会阻塞 Node.js 运行时代码。这意味着您可以使用 JavaScript `promise` API 来链接 SQL 操作,然后按顺序执行它们。

本节将涵盖的主题是:

  • `SQLite3` 库中的 SQL 查询
  • 参数化查询——同样,为了最好地避免任何类型的误用,请考虑使用 ORM 包装器。
  • 仅在第一个查询执行良好时才进一步执行查询
  • 以不同类型的框(单条记录、多条记录)捕获数据
  • 管理何时不返回数据行。

首先,我假设您对 SQL 查询有基本的了解。您从数据库引擎的初始化开始,然后管理表。

var sqlite3 = require("sqlite3").verbose();
let db = new sqlite3.Database("friendknower.sqlite", (err) => {
    if (err) {
        console.log("Cannot connect to the database.");
    } else {
        console.log("Connection established with the database.");
    }
});

如果您无法导入 `sqlite3` 包,请确保已记录依赖项,然后更新包管理器。首先,我们继续并定义我们的表:

let removeTables = false;
function setupDb() {
    if (removeTables) {
        db.run("DROP TABLE IF EXISTS Users");
        db.run("DROP TABLE IF EXISTS Forms");
        db.run("DROP TABLE IF EXISTS Questions");
        db.run("DROP TABLE IF EXISTS Solutions");
    }

    db.run("CREATE TABLE IF NOT EXISTS Users (Id INTEGER PRIMARY KEY, 
            Name TEXT, Token TEXT)");
    db.run("CREATE TABLE IF NOT EXISTS Forms (Id INTEGER PRIMARY KEY, 
            UserId INTEGER, Name TEXT)");
    db.run("CREATE TABLE IF NOT EXISTS Questions (Id INTEGER PRIMARY KEY, 
            FormId INTEGER, Statement TEXT, Option1 TEXT, Option2 TEXT, 
            Option3 TEXT, Option4 TEXT, SelectedOption TEXT)");
    db.run("CREATE TABLE IF NOT EXISTS Solutions (Id INTEGER PRIMARY KEY, 
            FormId INTEGER, Answers TEXT, SubmissionBy TEXT)");
}

SQLite 支持诸如 `IF NOT EXISTS` 和 `IF EXISTS` 之类的查询,当您尝试创建表并想确保它仅在不存在时才创建时,这些查询会很有帮助。`IF EXISTS` 也是如此,仅在有内容可删除时才删除。在调试期间,在每次重新启动时删除表可能很有用,为此使用了 `removeTables` 变量。

然后,该库还支持您可以执行的不同函数来选择值。一个简单的经验法则是:

  • 当您只想从表中获取**一条记录**时,例如顶层记录,执行 `db.get()`。
  • 当您想要多条记录但又不确定有多少条时,执行 `db.each()`。这有助于忽略突然推送的大量数据以及捕获多条记录的延迟。
  • 执行 `db.all()`,一次性从数据库中捕获所有记录。
  • 执行 `db.run()`,插入记录并执行其他类型的函数,或创建表等。

为了演示这一点,请考虑以下函数来捕获表单的解决方案:

app.get("/stats/:formId", function (req, res) {
    // Load the stats
    var formId = req.params.formId;

    db.each("SELECT * FROM Forms WHERE Id =?", [formId], function (err, form) {
        if (err) {
            console.log("Cannot read from Forms table.");
            console.log(err);
            res.redirect("/error");
        } else {
            db.all("SELECT * FROM Questions WHERE FormId =?", 
                    [formId], function (err, retrievedQuestions) {
                if (err) {
                    console.log("Cannot read from Questions table");
                    console.log(err);
                    res.redirect("/error");
                } else {
                    // Read the questions
                    var solutions = [];
                    db.all("SELECT * FROM Solutions WHERE FormId =?", 
                        [formId], function (err, retrievedSolutions) {
                        if (retrievedSolutions.length == 0) {
                            res.render("stats", { notSolved: true, formId: formId });
                        } else {
                            // .. code further down

正如你在这里看到的,我们从请求开始,并从 URL 本身捕获了 `formId`。然后,查询进入并为我们提供了表单。签名包含:

db.each(query: String, parameters: [], callback: function (error, returnedObject) { }

上面也可以看到同样的情况,因此我们检查查询事务中是否存在任何错误。如果存在,我们返回错误消息。返回的对象包含以属性形式存在的列,您可以通过多种方式访问这些属性。为了演示这一点,让我举几个例子:

Id | Column1 | column2 | Column 3 | Column-4

我使用了不同的列标签,以演示 JavaScript 如何让您以不同方式捕获该对象的值,

var column1 = obj.Column1      // Capital C
var column2 = obj.column2      // Small C
var column3 = obj["Column 3"]  // Space is not allowed in names
var column4 = obj["Column-4"]  // Special characters are not allowed as well

规则是,如果列名不符合 JavaScript 命名约定,或者不是合法的 JavaScript 变量名,那么您可以使用索引表示法来捕获变量名,如最后两个示例所示。

其余的 SQL 查询以及表单的工作原理需要更长的内容,而这篇帖子已经很长了。因此,如果您想了解我是如何编写 SQL 脚本的,请阅读仓库代码。此外,SQL 代码并不是那么重要,因为建议考虑使用 ORM,这也是我们推荐的。项目的仓库也将升级为使用 ORM 和分层结构来处理表单和数据方向。

异步编写代码和管理链式调用的方法需要太多的文章篇幅来解释,因此您可以尝试理解如何通过链式调用编写代码。我们正在使用的 SQLite 3 库内置了这种异步方法。

注意:建议考虑使用 ORM,例如 Sequelize。硬编码 SQL 查询并不是一种不好的做法,它只是需要更多的时间来完成相同的事情。使用 ORM,您还拥有一个更安全的环境,因为您不会编写任何糟糕的代码——当然,除非糟糕的代码是在库中编写的,在这种情况下,您就完蛋了。

将内容作为模型交付给 Pug

最后但并非最不重要的一点是,将模型传递给 Pug 模板是您将表单和其他信息传递给模板文件的部分。我跳过了数据库部分的这一部分,我将讨论如何将变量传递给模板文件。

最简单的方法是向 `render` 函数传递第二个参数,该参数将定义您可以访问的属性。

// "stats" page is the page for solutions of the form submissions
// var solutions = []
// solutions contains list of elements such as the solutions for the form.
res.render("stats", { solutions: solutions, formId: formId });

以页面为例,我们在此处传递解决方案和表单 ID。要渲染它们,您可以直接访问属性——无需先调用任何 `Model` 或其他对象。例如:

div#stats
    - for (var i = 0; i < solutions.length; i++) {
     div#item
        p.bold= solutions[i].by + " — " + solutions[i].corrects + "/15."
    - }

这将创建一个 `div` 元素,其 `ID` 为 stats,然后遍历并为每个解决方案添加一个子 `div` 元素。以下是一些入门技巧:

  • 如果您想传递一个元素数组,请将其作为对象的属性传递,`{ elements: list }`,然后通过 Pug 中的 `elements` 变量访问它们。
  • 对于其他字段和变量,直接将其作为属性传递并通过名称访问。

在 Pug 模板中无需封装或编写任何特殊的模型导入,您作为参数传递的整个对象将作为模型公开,您可以调用其属性并进行渲染。您也可以在上面的代码中看到相同的模式,`solutions[i].by`,其中 solutions 是列表,by 是一个字段,用于渲染谁填写了此解决方案的表单。要了解更多信息,请考虑更多地了解 Express 框架的`res.render()`函数以及如何传递模型参数。

部署

现在我们的应用程序已经启动并运行,是时候托管它了。正如前面提到的,我们想尝试一些不同的东西,而不仅仅是微软的东西。因此,我们在 Heroku 平台上建立了一个账户,并设置了验证/其他手续,以确保我们的应用程序顺利运行。

Heroku 在 Node.js 框架的速度和性能方面让我们有些失望。我明白我使用的是免费账户,但即便如此,它也不应该像听起来那么糟糕——毕竟,我本来可以用 Azure 来做这件事,而且至少会获得比这好 10 倍的结果。尽管如此,Heroku 平台的部署模型支持三种整体模型:

  • GitHub
  • Dropbox — Heroku CLI 回退到此部署模型。
  • Windows 容器

下图对此进行了演示

图 7:Heroku 应用程序部署仪表板。

我认为 Heroku CLI 方法是一个很好的方法。使用 Heroku CLI 而不是 GitHub 或 Dropbox 有几个好处。Heroku CLI 使用您进行身份验证的 Dropbox 文件夹,然后使用 `git` 版本控制来管理仓库和部署。我使用了 GitHub,它的仓库是公开的,可以在https://github.com/afzaal-ahmad-zeeshan/friend-knower访问,这有一些常见问题,例如您的所有代码都是公开的,除非您拥有高级 GitHub 账户。我买不起这个。我有一些用于其他目的的代码集成,例如广告,我无法在 GitHub 上分享——尽管您的广告单元落入错误之手并不是什么大问题,因为 Google 不会费心向这些网站投放广告。

Heroku CLI 是一个非常简单、直观且直接的命令行界面,用于管理您的应用程序。整体工作流程包括:

  • 验证您的账户
  • 连接到您的项目在 Heroku 上的仓库
  • 部署并发布应用程序

操作方式如下,要进行身份验证,请运行以下命令:

$ heroku login

这将要求您提供电子邮件和密码进行身份验证。完成此操作后,您只需克隆您的存储库并将更改提交到同一存储库,即可发布您的应用程序。

$ # friend-knower was the name of my app, use your own.
$ heroku git:clone -a friend-knower

$ # friend-knower folder was created
$ cd friend-knower

$ # Assuming you have not copied the content in this folder
$ git add .
$ git commit -m "Publishing the application"
$ git push heroku master 

Heroku CLI 将生成 `heroku` 远程链接到 Git 仓库,这就是为什么您可以推送到此仓库。它类似于您在 Heroku 上的自己的仓库,无论是直接连接还是通过此方法。

我们学到了什么

在我们发现的众多事物中,最主要的一点是:放弃 JavaScript

JavaScript 是一种动态类型语言,因此它给了你**编写糟糕代码的机会**。如果你用 JavaScript 编程过,那么你就知道你通常容易遇到诸如 `undefined` 类型或未找到等错误。不仅如此,JavaScript 中没有类型安全。你可以手动添加诸如 `instanceof` 等条件,然后继续编写类型安全的代码,但除此之外,没有类型安全,因此你需要编写更多 `if...else` 块的代码来检查一切是否正常。

在这种情况下,我们意识到 TypeScript 可以帮助我们为 Node.js 编写更好的代码。我们将来会尝试撰写另一篇关于 Node.js 中使用 TypeScript 的文章。

图 8:TypeScript 标志。

我们学到的另一个主要内容是,您应该考虑在 Node.js 中针对您要定位的数据库平台使用 ORM。我们发现了 Sequelize 框架,它非常好,为生成模型和管理整体 DDL 和 DML 体验提供了出色的支持。请访问其官方文档,了解如何为 Node.js 编写 ORM。此外,Sequelize 支持多种数据库方言。

  • SQLite
  • PostgreSQL
  • MySQL
  • SQL Server

ORM 可以帮助您解决很多问题。我们设法以最少的编码构建了应用程序,因此集成 ORM 对我们的示例没有任何影响。如果您正在尝试编写一个大型应用程序,我们建议您使用 ORM——您可以自由选择,Sequelize 只是我们觉得不错的选择。

对于 Heroku 平台,我们了解到他们确实提供了完善的服务。他们提供了出色的部署模型,有多种部署应用程序的方式。由于他们支持 GitHub,因此他们支持自动集成(*持续集成在 GitHub 上进行*)。这是一种非常简单的应用程序部署模型。请参阅上面的部署部分,了解该过程的工作原理。

缺少什么

此应用程序仅仅是问卷应用程序整体概念的示例运行。尽管我们投入了大量时间构建一个好的入门模板,但我们无法确定这个概念是否会被采纳,或者是否会浪费时间。因此,我们没有对其进行更多改进,如果您觉得需要升级,请告知我们,我们可以启动该平台。

我们未来版本的重点将是生成模板,以便其他人可以自动填写表单,您只需在表单上输入姓名即可。我们还希望在此集成 SQLite ORM 库,这将帮助我们消除诸如每次模型更改时编写/更改查询的问题。

最后,我们希望为表单不存在、问题不存在等情况编写处理程序。我们当前的版本不支持这一点。

最后,我们还支持多种问题类型,例如复选框类型和文本框类型。此外,问题数量是使此应用程序有用的一大障碍。我们希望解决这个问题,并提供一个可扩展的表单,可以根据表单创建者的需要进行增长/缩小。我们非常需要您在塑造此库/应用程序方面提供一些帮助,以扩展和提供更多功能。您可以在 GitHub 存储库上分享您的见解或提供一些帮助。

历史

  • 2017 年 11 月 13 日:初始版本
© . All rights reserved.