在没有框架的情况下构建 JavaScript 单页应用程序

Build a JavaScript Single Page App Without a Framework - SitePoint

前端框架很棒。它们抽象出构建单页应用程序 (SPA) 的大部分复杂性,并帮助您随着项目的发展以一种可理解的方式组织代码。

然而,也有不利的一面:这些框架会带来一定程度的开销,并且可能会引入它们自己的复杂性。

这就是为什么在本教程中,我们将学习如何在不使用客户端 JavaScript 框架的情况下从头开始构建 SPA。这将帮助您评估这些框架实际上为您做了什么,以及在什么时候使用它们是有意义的。它还将让您了解构成典型 SPA 的各个部分以及它们是如何连接在一起的。

让我们开始吧 …

先决条件

对于本教程,您需要具备现代 JavaScriptjQuery的基础知识。一些使用HandlebarsExpressAxios的经验会派上用场,尽管这不是绝对必要的。您还需要在您的环境中进行以下设置:

您可以在我们的GitHub 存储库中找到已完成的项目。

构建项目

我们将构建一个简单的货币应用程序,它将提供以下功能:

  • 显示最新汇率
  • 从一种货币转换为另一种货币
  • 根据指定日期显示过去的货币汇率。

我们将使用以下免费的在线 REST API 来实现这些功能:

Fixer 是一个完善的 API,提供外汇和货币转换 JSON API。不幸的是,这是一项商业服务,免费计划不允许货币兑换。所以我们还需要使用免费货币转换器 API。转换 API 有一些限制,幸运的是不会影响我们应用程序的功能。无需 API 密钥即可直接访问。但是,Fixer 需要 API 密钥来执行任何请求。只需在他们的网站上注册即可获得免费计划的访问密钥

理想情况下,我们应该能够在客户端构建整个单页应用程序。但是,由于我们将处理敏感信息(我们的 API 密钥),因此无法将其存储在我们的客户端代码中。这样做会使我们的应用程序容易受到攻击,并且任何初级黑客都可以绕过该应用程序并直接从我们的 API 端点访问数据。为了保护此类敏感信息,我们需要将其放入服务器代码中。因此,我们将设置一个Express服务器来充当客户端代码和云服务之间的代理。通过使用代理,我们可以安全地访问此密钥,因为服务器代码永远不会暴露给浏览器。下图说明了我们完成的项目将如何工作。

记下每个环境将使用的 npm 包——即浏览器(客户端)和服务器。既然您知道我们将要构建什么,请转到下一部分开始创建项目。

项目目录和依赖项

前往您的工作区目录并创建文件夹single-page-application。在 VSCode 或您喜欢的编辑器中打开文件夹,然后使用终端创建以下文件和文件夹:

touch .env .gitignore README.md server.js
mkdir public lib
mkdir public/js
touch public/index.html
touch public/js/app.js

打开.gitignore并添加这些行:

node_modules
.env

打开README.md并添加这些行:

# Single Page Application

This is a project demo that uses Vanilla JS to build a Single Page Application.

接下来,package.json通过在终端中执行以下命令来创建文件:

npm init -y

您应该获得为您生成的以下内容:

{
  "name": "single-page-application",
  "version": "1.0.0",
  "description": "This is a project demo that uses Vanilla JS to build a Single Page Application.",
  "main": "server.js",
  "directories": {
    "lib": "lib"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

看看 npm 命令有多方便?内容是根据项目结构生成的。现在让我们安装项目所需的核心依赖项。在终端中执行以下命令:

npm install jquery semantic-ui-css handlebars vanilla-router express dotenv axios

软件包安装完成后,转到下一部分开始构建应用程序的基础。

应用基础

在我们开始编写前端代码之前,我们需要实现一个服务器-客户端基础来工作。这意味着从 Express 服务器提供的基本 HTML 视图。node_modules出于性能和可靠性的原因,我们将直接从文件夹中注入前端依赖项。我们必须以一种特殊的方式设置我们的 Express 服务器来完成这项工作。打开server.js并添加以下内容:

require('dotenv').config(); // read .env files
const express = require('express');

const app = express();
const port = process.env.PORT || 3000;

// Set public folder as root
app.use(express.static('public'));

// Allow front-end access to node_modules folder
app.use('/scripts', express.static(`${__dirname}/node_modules/`));

// Listen for HTTP requests on port 3000
app.listen(port, () => {
  console.log('listening on %d', port);
});

这为我们提供了一个基本的 Express 服务器。我已经评论了代码,所以希望这能让您对正在发生的事情有一个相当好的了解。接下来,打开public/index.html并输入:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="scripts/semantic-ui-css/semantic.min.css">
  <title>SPA Demo</title>
</head>
<body>
  <div class="ui container">
    <!-- Navigation Menu -->
    <div class="ui four item inverted orange menu">
      <div class="header item">
        <i class="money bill alternate outline icon"></i>
        Single Page App
      </div>
      <a class="item" href="/">
        Currency Rates
      </a>
      <a class="item" href="/exchange">
        Exchange Rates
      </a>
      <a class="item" href="/historical">
        Historical Rates
      </a>
    </div>

    <!-- Application Root -->
    <div id="app"></div>
  </div>

  <!-- JS Library Dependencies -->
  <script src="scripts/jquery/dist/jquery.min.js"></script>
  <script src="scripts/semantic-ui-css/semantic.min.js"></script>
  <script src="scripts/axios/dist/axios.min.js"></script>
  <script src="scripts/handlebars/dist/handlebars.min.js"></script>
  <script src="scripts/vanilla-router/dist/vanilla-router.min.js"></script>
  <script src="js/app.js"></script>
</body>
</html>

我们使用语义 UI 进行样式设置。请参阅语义 UI 菜单文档以了解用于我们导航栏的代码。转到您的终端并启动服务器:

npm start

在浏览器中打开localhost:3000 。您应该有一个空白页面,其中仅显示导航栏:

现在让我们为我们的应用程序编写一些视图模板。

前端骨架模板

我们将使用Handlebars来编写我们的模板。JavaScript 将用于根据当前 URL 呈现模板。我们将创建的第一个模板将用于显示错误消息,例如 404 或服务器错误。将此代码放在public/index.html导航部分之后:

<!-- Error Template -->
<script id="error-template" type="text/x-handlebars-template">
  <div class="ui {{color}} inverted segment" style="height:250px;">
    <br>
    <h2 class="ui center aligned icon header">
      <i class="exclamation triangle icon"></i>
      <div class="content">
        {{title}}
        <div class="sub header">{{message}}</div>
      </div>
    </h2>
  </div>
</script>

接下来,添加以下模板,这些模板将代表我们在导航栏中指定的每个 URL 路径的视图:

<!-- Currency Rates Template -->
<script id="rates-template" type="text/x-handlebars-template">
  <h1 class="ui header">Currency Rates</h1>
  <hr>
</script>

<!-- Exchange Conversion Template -->
<script id="exchange-template" type="text/x-handlebars-template">
  <h1 class="ui header">Exchange Conversion</h1>
  <hr>
</script>

<!-- Historical Rates Template -->
<script id="historical-template" type="text/x-handlebars-template">
  <h1 class="ui header">Historical Rates</h1>
  <hr>
</script>

接下来,让我们将所有这些模板编译到public/js/app.js. 编译后,我们将渲染rates-template并查看它的样子:

window.addEventListener('load', () => {
  const el = $('#app');

  // Compile Handlebar Templates
  const errorTemplate = Handlebars.compile($('#error-template').html());
  const ratesTemplate = Handlebars.compile($('#rates-template').html());
  const exchangeTemplate = Handlebars.compile($('#exchange-template').html());
  const historicalTemplate = Handlebars.compile($('#historical-template').html());

  const html = ratesTemplate();
  el.html(html);
});

请注意,我们将所有 JavaScript 客户端代码包装在一个load事件中。这只是为了确保所有依赖项都已加载并且 DOM 已完成加载。刷新页面,看看我们有什么:

我们正在取得进展。现在,如果您单击除Currency Rates之外的其他链接,浏览器将尝试获取新页面并最终显示如下消息Cannot GET /exchange

我们正在构建一个单页应用程序,这意味着所有操作都应该发生在一个页面中。我们需要一种方法来告诉浏览器在 URL 更改时停止获取新页面。

客户端路由

为了控制浏览器环境中的路由,我们需要实现客户端路由。有许多客户端路由库可以帮助解决这个问题。对于我们的项目,我们将使用vanilla router,这是一个非常易于使用的路由包。

如果你还记得,我们之前已经在index.html. 因此,我们可以立即调用Router该类。删除您添加的最后两个语句app.js并将它们替换为以下代码:

// Router Declaration
const router = new Router({
  mode: 'history',
  page404: (path) => {
    const html = errorTemplate({
      color: 'yellow',
      title: 'Error 404 - Page NOT Found!',
      message: `The path '/${path}' does not exist on this site`,
    });
    el.html(html);
  },
});

router.add('/', () => {
  let html = ratesTemplate();
  el.html(html);
});

router.add('/exchange', () => {
  let html = exchangeTemplate();
  el.html(html);
});

router.add('/historical', () => {
  let html = historicalTemplate();
  el.html(html);
});

// Navigate app to current url
router.navigateTo(window.location.pathname);

 // Highlight Active Menu on Refresh/Page Reload
const link = $(`a[href$='${window.location.pathname}']`);
link.addClass('active');

$('a').on('click', (event) => {
  // Block browser page load
  event.preventDefault();

  // Highlight Active Menu on Click
  const target = $(event.target);
  $('.item').removeClass('active');
  target.addClass('active');

  // Navigate to clicked url
  const href = target.attr('href');
  const path = href.substr(href.lastIndexOf('/'));
  router.navigateTo(path);
});

花一些时间浏览代码。我在各个部分添加了评论来解释正在发生的事情。您会注意到,在路由器的声明中,我们指定了page404使用错误模板的属性。现在让我们测试链接:

链接现在应该可以工作了。但是我们有一个问题。单击/exchangehistorical链接,然后刷新浏览器。我们得到与以前相同的错误 - Cannot GET /exchange。要解决此问题,请转到server.js并在监听代码之前添加此语句:

// Redirect all traffic to index.html
app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));

您必须使用Ctrl+C和执行重新启动服务器npm start。返回浏览器并尝试刷新。您现在应该可以看到页面正确呈现。现在,让我们尝试在 URL 中输入一个不存在的路径,例如/exchanges. 该应用程序应显示 404 错误消息:

我们现在已经实现了必要的代码来创建我们的单页应用程序框架。现在让我们开始列出最新的货币汇率。

最新货币汇率

对于这个任务,我们将使用Fixer Latest Rates Endpoint。打开.env文件并添加您的 API 密钥。我们还将指定超时期限和我们将在页面上列出的符号。如果您的互联网连接速度较慢,请随意增加超时值:

API_KEY=<paste key here>
PORT=3000
TIMEOUT=5000
SYMBOLS=EUR,USD,GBP,AUD,BTC,KES,JPY,CNY

接下来创建文件lib/fixer-service.js。在这里,我们将为我们的 Express 服务器编写帮助代码,以便轻松地从 Fixer 请求信息。复制以下代码:

require('dotenv').config();
const axios = require('axios');

const symbols = process.env.SYMBOLS || 'EUR,USD,GBP';

// Axios Client declaration
const api = axios.create({
  baseURL: 'http://data.fixer.io/api',
  params: {
    access_key: process.env.API_KEY,
  },
  timeout: process.env.TIMEOUT || 5000,
});

// Generic GET request function
const get = async (url) => {
  const response = await api.get(url);
  const { data } = response;
  if (data.success) {
    return data;
  }
  throw new Error(data.error.type);
};

module.exports = {
  getRates: () => get(`/latest&symbols=${symbols}&base=EUR`),
};

同样,花一些时间浏览代码以了解正在发生的事情。如果您不确定,您还可以查看dotenvaxios的文档并阅读模块导出。现在让我们进行快速测试以确认该getRates()功能是否正常工作。

打开server.js并添加以下代码:

const { getRates } = require('./lib/fixer-service');

...
// Place this block at the bottom
const test = async() => {
  const data = await getRates();
  console.log(data);
}

test();

运行npm startnode server。几秒钟后,您应该得到以下输出:

{
  success: true,
  timestamp: 1523871848,
  base: 'EUR',
  date: '2018-04-16',
  rates: {
    EUR: 1,
    USD: 1.23732,
    GBP: 0.865158,
    AUD: 1.59169,
    BTC: 0.000153,
    KES: 124.226892,
    JPY: 132.608498,
    CNY: 7.775567
  }
}

如果您得到与上述类似的内容,则表示代码正在运行。这些值当然会有所不同,因为费率每天都在变化。现在注释掉测试块并在将所有流量重定向到的语句之前插入此代码index.html

// Express Error handler
const errorHandler = (err, req, res) => {
  if (err.response) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx
    res.status(403).send({ title: 'Server responded with an error', message: err.message });
  } else if (err.request) {
    // The request was made but no response was received
    res.status(503).send({ title: 'Unable to communicate with server', message: err.message });
  } else {
    // Something happened in setting up the request that triggered an Error
    res.status(500).send({ title: 'An unexpected error occurred', message: err.message });
  }
};

// Fetch Latest Currency Rates
app.get('/api/rates', async (req, res) => {
  try {
    const data = await getRates();
    res.setHeader('Content-Type', 'application/json');
    res.send(data);
  } catch (error) {
    errorHandler(error, req, res);
  }
});

正如我们所看到的,有一个自定义错误处理函数,旨在处理可能在服务器代码执行期间发生的不同错误场景。发生错误时,会构造错误消息并将其发送回客户端。

让我们确认这段代码是否有效。重新启动 Express 服务器并将浏览器导航到此 URL:localhost:3000/api/rates。您应该会看到控制台中显示的相同 JSON 结果。我们现在可以实现一个视图,将这些信息显示在一个整洁、优雅的表格中。

打开public/index.htmlrates-template用以下代码替换:

<!-- Currency Rates Template -->
<script id="rates-template" type="text/x-handlebars-template">
  <h1 class="ui header">Currency Rates</h1>
  <hr>
  <div class="ui loading basic segment">
    <div class="ui horizontal list">
      <div class="item">
        <i class="calendar alternate outline icon"></i>
        <div class="content">
          <div class="ui sub header">Date</div>
          <span>{{date}}</span>
        </div>
      </div>
      <div class="item">
        <i class="money bill alternate outline icon"></i>
        <div class="content">
          <div class="ui sub header">Base</div>
          <span>{{base}}</span>
        </div>
      </div>
    </div>

    <table class="ui celled striped selectable inverted table">
      <thead>
        <tr>
          <th>Code</th>
          <th>Rate</th>
        </tr>
      </thead>
      <tbody>
        {{#each rates}}
        <tr>
          <td>{{@key}}</td>
          <td>{{this}}</td>
        </tr>
        {{/each}}
      </tbody>
    </table>
  </div>
</script>

请记住,我们使用语义 UI 为我们提供样式。我希望您密切关注Segment 加载组件。这将表明在应用程序获取数据时让用户知道正在发生某些事情。我们还使用Table UI来显示费率。如果您是 Semantic 的新手,请阅读链接文档。

现在让我们更新我们的代码public/js/app.js以使用这个新模板。用以下代码替换第一个route.add('/')函数:

// Instantiate api handler
const api = axios.create({
  baseURL: 'http://localhost:3000/api',
  timeout: 5000,
});

// Display Error Banner
const showError = (error) => {
  const { title, message } = error.response.data;
  const html = errorTemplate({ color: 'red', title, message });
  el.html(html);
};

// Display Latest Currency Rates
router.add('/', async () => {
  // Display loader first
  let html = ratesTemplate();
  el.html(html);
  try {
    // Load Currency Rates
    const response = await api.get('/rates');
    const { base, date, rates } = response.data;
    // Display Rates Table
    html = ratesTemplate({ base, date, rates });
    el.html(html);
  } catch (error) {
    showError(error);
  } finally {
    // Remove loader status
    $('.loading').removeClass('loading');
  }
});

第一个代码块实例化了一个 API 客户端,用于与我们的代理服务器进行通信。第二个块是用于处理错误的全局函数。它的工作只是在服务器端出现问题时显示错误横幅。第三个块是我们从localhost:3000/api/rates端点获取费率数据并将其传递rates-template给显示信息的地方。

只需刷新浏览器。您现在应该有以下视图:

接下来我们将构建一个用于转换货币的界面。

兑换兑换

对于货币转换,我们将使用两个端点:

我们需要符号端点来获取支持的货币代码列表。我们将使用这些数据来填充用户将用来选择要转换的货币的下拉列表。在函数后面打开lib/fixer-service.js并添加这一行getRates()

getSymbols: () => get('/symbols'),

创建另一个帮助文件 ,lib/free-currency-service.js并添加以下代码:

require('dotenv').config();
const axios = require('axios');

const api = axios.create({
  baseURL: 'https://free.currencyconverterapi.com/api/v5',
  timeout: process.env.TIMEOUT || 5000,
});

module.exports = {
  convertCurrency: async (from, to) => {
    const response = await api.get(`/convert?q=${from}_${to}&compact=y`);
    const key = Object.keys(response.data)[0];
    const { val } = response.data[key];
    return { rate: val };
  },
};

这将帮助我们免费获得从一种货币到另一种货币的兑换率。在客户端代码中,我们必须通过将金额乘以费率来计算转换金额。现在让我们将这两个服务方法添加到我们的 Express 服务器代码中。打开server.js并相应更新:

const { getRates, getSymbols, } = require('./lib/fixer-service');
const { convertCurrency } = require('./lib/free-currency-service');
...
// Insert right after get '/api/rates', just before the redirect statement

// Fetch Symbols
app.get('/api/symbols', async (req, res) => {
  try {
    const data = await getSymbols();
    res.setHeader('Content-Type', 'application/json');
    res.send(data);
  } catch (error) {
    errorHandler(error, req, res);
  }
});

// Convert Currency
app.post('/api/convert', async (req, res) => {
  try {
    const { from, to } = req.body;
    const data = await convertCurrency(from, to);
    res.setHeader('Content-Type', 'application/json');
    res.send(data);
  } catch (error) {
    errorHandler(error, req, res);
  }
});

现在我们的代理服务器应该能够获取符号和转换率。请注意,这/api/convert是一个 POST 方法。我们将在客户端使用一个表单来构建货币转换 UI。随意使用该test功能来确认两个端点都在工作。这是一个例子:

// Test Symbols Endpoint
const test = async() => {
  const data = await getSymbols();
  console.log(data);
}

// Test Currency Conversion Endpoint
const test = async() => {
  const data = await convertCurrency('USD', 'KES');
  console.log(data);
}

您必须为每次测试重新启动服务器。一旦您确认代码到目前为止工作正常,请记住注释掉测试。现在让我们处理我们的货币转换 UI。通过将现有代码替换为以下代码来打开public/index.html并更新:exchange-template

<script id="exchange-template" type="text/x-handlebars-template">
  <h1 class="ui header">Exchange Rate</h1>
  <hr>
  <div class="ui basic loading segment">
    <form class="ui form">
      <div class="three fields">
        <div class="field">
          <label>From</label>
          <select class="ui dropdown" name="from" id="from">
            <option value="">Select Currency</option>
            {{#each symbols}}
              <option value="{{@key}}">{{this}}</option>
            {{/each}}
          </select>
        </div>
        <div class="field">
          <label>To</label>
          <select class="ui dropdown" name="to" id="to">
            <option value="">Select Currency</option>
            {{#each symbols}}
              <option value="{{@key}}">{{this}}</option>
            {{/each}}
          </select>
        </div>
        <div class="field">
          <label>Amount</label>
          <input type="number" name="amount" id="amount" placeholder="Enter amount">
        </div>
      </div>
      <div class="ui primary submit button">Convert</div>
      <div class="ui error message"></div>
    </form>
    <br>
    <div id="result-segment" class="ui center aligned segment">
      <h2 id="result" class="ui header">
        0.00
      </h2>
    </div>
  </div>
</script>

花点时间阅读脚本并了解正在发生的事情。我们正在使用语义 UI 表单来构建界面。我们还使用 Handlebars 符号来填充下拉框。下面是 Fixer 的 Symbols 端点使用的 JSON 格式:

{
  "success": true,
  "symbols": {
    "AED": "United Arab Emirates Dirham",
    "AFN": "Afghan Afghani",
    "ALL": "Albanian Lek",
    "AMD": "Armenian Dram",
  }
}

请注意,符号数据是地图格式。这意味着信息被存储为键值{{@key}}{{this}}。现在让我们更新public/js/app.js并使其与新模板一起使用。打开文件并将现有的路由代码替换为/exchange以下内容:

// Perform POST request, calculate and display conversion results
const getConversionResults = async () => {
  // Extract form data
  const from = $('#from').val();
  const to = $('#to').val();
  const amount = $('#amount').val();
  // Send post data to Express(proxy) server
  try {
    const response = await api.post('/convert', { from, to });
    const { rate } = response.data;
    const result = rate * amount;
    $('#result').html(`${to} ${result}`);
  } catch (error) {
    showError(error);
  } finally {
    $('#result-segment').removeClass('loading');
  }
};

// Handle Convert Button Click Event
const convertRatesHandler = () => {
  if ($('.ui.form').form('is valid')) {
    // hide error message
    $('.ui.error.message').hide();
    // Post to Express server
    $('#result-segment').addClass('loading');
    getConversionResults();
    // Prevent page from submitting to server
    return false;
  }
  return true;
};

router.add('/exchange', async () => {
  // Display loader first
  let html = exchangeTemplate();
  el.html(html);
  try {
    // Load Symbols
    const response = await api.get('/symbols');
    const { symbols } = response.data;
    html = exchangeTemplate({ symbols });
    el.html(html);
    $('.loading').removeClass('loading');
    // Validate Form Inputs
    $('.ui.form').form({
      fields: {
        from: 'empty',
        to: 'empty',
        amount: 'decimal',
      },
    });
    // Specify Submit Handler
    $('.submit').click(convertRatesHandler);
  } catch (error) {
    showError(error);
  }
});

刷新页面。您现在应该有以下视图:

选择您选择的一些货币并输入金额。然后点击转换按钮:

哎呀!我们刚刚遇到了一个错误场景。至少我们知道我们的错误处理代码是有效的。要找出错误发生的原因,请返回服务器代码并查看/api/convert函数。具体来说,请查看 const { from, to } = req.body;.

Express 似乎无法从request对象中读取属性。为了解决这个问题,我们需要安装可以帮助解决这个问题的中间件:

npm install body-parser

接下来,更新服务器代码如下:

const bodyParser = require('body-parser');
...

/** Place this code right before the error handler function **/

// Parse POST data as URL encoded data
app.use(bodyParser.urlencoded({
  extended: true,
}));

// Parse POST data as JSON
app.use(bodyParser.json());

再次启动服务器并刷新浏览器。尝试进行另一次转换。它现在应该可以工作了。

现在让我们关注最后一点——历史货币汇率。让我们从视图开始。

历史货币汇率

实现此功能就像组合第一页和第二页中的任务。我们将构建一个小表单,用户需要在其中输入日期。当用户点击提交时,指定日期的汇率将以表格形式显示。我们将使用来自 Fixer API 的历史汇率端点来实现这一点。API 请求如下所示:

https://data.fixer.io/api/2013-12-24
    ? access_key = API_KEY
    & base = GBP
    & symbols = USD,CAD,EUR

响应将如下所示:

{
  "success": true,
  "historical": true,
  "date": "2013-12-24",
  "timestamp": 1387929599,
  "base": "GBP",
  "rates": {
    "USD": 1.636492,
    "EUR": 1.196476,
    "CAD": 1.739516
  }
}

像这样打开lib/fixer-service.js和历史汇率端点:

...
  /** Place right after getSymbols **/
  getHistoricalRate: date => get(`/${date}&symbols=${symbols}&base=EUR`),
...

打开server.js并添加以下代码:

...
const { getRates, getSymbols, getHistoricalRate } = require('./lib/fixer-service');
...
/** Place this after '/api/convert' post function **/

// Fetch Currency Rates by date
app.post('/api/historical', async (req, res) => {
  try {
    const { date } = req.body;
    const data = await getHistoricalRate(date);
    res.setHeader('Content-Type', 'application/json');
    res.send(data);
  } catch (error) {
    errorHandler(error, req, res);
  }
});
...

如果您对代码的排列方式有任何疑问,请参阅GitHub 上server.js的完整文件。随意编写一个快速测试来确认历史端点是否正常工作:

const test = async() => {
  const data = await getHistoricalRate('2012-07-14');
  console.log(data);
}

test();

确认一切正常后,请记住注释掉测试块。现在让我们处理客户端代码。

打开index.html. 删除historical-template我们用作占位符的现有内容,并将其替换为以下内容:

<script id="historical-template" type="text/x-handlebars-template">
  <h1 class="ui header">Historical Rates</h1>
  <hr>
  <form class="ui form">
    <div class="field">
      <label>Pick Date</label>
      <div class="ui calendar" id="calendar">
        <div class="ui input left icon">
          <i class="calendar icon"></i>
          <input type="text" placeholder="Date" id="date">
        </div>
      </div>
    </div>
    <div class="ui primary submit button">Fetch Rates</div>
    <div class="ui error message"></div>
  </form>

  <div class="ui basic segment">
    <div id="historical-table"></div>
  </div>
</script>

先看一下表格。我想指出的一件事是语义 UI 没有正式的日期输入。然而,感谢Michael de Hoog的贡献,我们可以使用Semantic-UI-Calendar模块。只需使用 npm 安装它:

npm install semantic-ui-calendar

返回public/index.html并将其包含在脚本部分中:

...
<script src="scripts/semantic-ui-css/semantic.min.js"></script>
<script src="scripts/semantic-ui-calendar/dist/calendar.min.js"></script>
....

为了显示历史汇率,我们将简单地重复使用rates-template. 接下来打开public/js/app.js并更新现有的路线代码/historical

const getHistoricalRates = async () => {
  const date = $('#date').val();
  try {
    const response = await api.post('/historical', { date });
    const { base, rates } = response.data;
    const html = ratesTemplate({ base, date, rates });
    $('#historical-table').html(html);
  } catch (error) {
    showError(error);
  } finally {
    $('.segment').removeClass('loading');
  }
};

const historicalRatesHandler = () => {
  if ($('.ui.form').form('is valid')) {
    // hide error message
    $('.ui.error.message').hide();
    // Indicate loading status
    $('.segment').addClass('loading');
    getHistoricalRates();
    // Prevent page from submitting to server
    return false;
  }
  return true;
};

router.add('/historical', () => {
  // Display form
  const html = historicalTemplate();
  el.html(html);
  // Activate Date Picker
  $('#calendar').calendar({
    type: 'date',
    formatter: { //format date to yyyy-mm-dd
      date: date => new Date(date).toISOString().split('T')[0],
    },
  });
  // Validate Date input
  $('.ui.form').form({
    fields: {
      date: 'empty',
    },
  });
  $('.submit').click(historicalRatesHandler);
});

再一次,花时间阅读评论并理解代码和它在做什么。然后重新启动服务器,刷新浏览器并导航到/historical路径。选择 1999 年之前的任何日期,然后单击Fetch Rates。你应该有这样的东西:

如果您选择 1999 年之前的日期或未来的日期,提交表单时将显示错误横幅。

概括

现在我们已经结束了本教程,您应该会看到,在不使用框架的情况下构建由 REST API 支持的单页应用程序并不难。但是有几点我们应该关注:

  • DOM 性能。在我们的客户端代码中,我们直接操作 DOM。随着项目的发展,这很快就会失控,导致 UI 变得迟缓。

  • 浏览器性能。我们已经将很多前端库作为脚本加载到 中index.html,这对于开发目的来说是可以的。对于生产部署,我们需要一个捆绑所有脚本的系统,以便浏览器使用单个请求来加载必要的 JavaScript 资源。

  • 单片代码。对于服务器代码,将代码分解为模块化部分更容易,因为它在 Node 环境中运行。然而,对于客户端代码,除非你使用像webpack这样的打包器,否则在模块中组织起来并不容易。

  • 测试。到目前为止,我们一直在进行手动测试。对于一个生产就绪的应用程序,我们需要建立一个像 Jasmine、Mocha 或 Chai 这样的测试框架来自动化这项工作。这将有助于防止重复出现错误。

当您在不使用框架的情况下进行项目开发时,这些只是您将面临的众多问题中的一小部分。使用诸如 Angular、React 或 Vue 之类的东西将帮助您减轻很多这些担忧。我希望本教程对您有所帮助,并能帮助您成为一名专业的 JavaScript 开发人员。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值