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

前端框架很棒。 它们消除了构建单页应用程序(SPA)的许多复杂性,并随着项目的发展,以可理解的方式帮助您组织代码。

但是,还有一个缺点:这些框架具有一定程度的开销,并且可能会引入其自身的复杂性。

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

让我们开始吧 …

先决条件

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

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

建设项目

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

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

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

Fixer是构建良好的API,可提供外汇和货币转换JSON API。 不幸的是,这是一项商业服务,免费计划不允许货币兑换。 因此,我们还需要使用Free Currency Converter 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文件夹注入前端依赖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 。 您应该有一个空白页,仅导航栏显示:

导航栏

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

前端骨架模板

我们将使用把手来编写模板。 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已完成加载。 刷新页面,看看有什么:

货币汇率为空

我们正在进步。 现在,如果您单击除“ 货币汇率”以外的其他链接,浏览器将尝试获取一个新页面并最终显示如下消息: Cannot GET /exchange

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

客户端路由

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

回想一下,我们之前已经在index.html包含了我们需要的所有JavaScript库。 因此,我们可以立即调用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错误消息:

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.html并用以下代码替换rates-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为我们提供样式。 我希望您密切注意细分加载组件。 这将指示用户在应用程序获取数据时知道发生了某些事情。 我们还使用Table UI来显示费率。 如果您不熟悉语义,请仔细阅读链接文档。

现在,让我们更新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() 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和Historical Rates端点:

...
  /** 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年之前的任何日期,然后点击提取汇率 。 您应该具有以下内容:

待办事项历史汇率

如果您选择的日期是1999年之前的日期或将来的日期,则在提交表单时会显示错误标语。

待办事项历史汇率错误

摘要

现在,我们已经完成了本教程的结尾,您应该看到不使用框架构建由REST API驱动的单页应用程序并不困难。 但是,我们需要注意一些事项:

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

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

  • 整体代码 。 对于服务器代码,将代码分解为模块化部分会更容易,因为它在Node环境中运行。 但是,对于客户端代码,除非使用webpack之类的捆绑器,否则在模块中进行组织并不容易。

  • 测试 。 到目前为止,我们一直在进行手动测试。 对于可用于生产的应用程序,我们需要建立一个诸如Jasmine,Mocha或Chai的测试框架以使这项工作自动化。 这将有助于防止重复发生的错误。

这些只是在不使用框架的情况下进行项目开发时会遇到的许多问题中的几个。 使用诸如Angular,React或Vue之类的东西将帮助您减轻很多这些担忧。 希望本教程对您有所帮助,并且对您成为专业的JavaScript开发人员有帮助。

From: https://www.sitepoint.com/single-page-app-without-framework/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值