我真的需要 SPA 框架吗?

2025-05-25

我真的需要 SPA 框架吗?

如果你从事前端工作,除非你并非与世隔绝,否则你可能听说过 SPA、React、Angular 甚至 Vue 等术语。你知道它是什么吗?什么时候应该使用 SPA 框架?我们常常很容易陷入“我必须找个借口使用 X 框架”的陷阱。我们很少问自己这个问题:它是适合这项工作的工具吗?

在本文中,我们将讨论什么是 SPA 以及何时使用它。我们还将共同构建一个微型 SPA 框架,并意识到只需很少的代码,我们就能构建出一个运行良好的应用。因此,让我们也来思考这个问题:

我是否需要一个包含所有电池的 SPA 框架,或者这个微型 SPA 框架是否足以应对大多数情况?

我知道你很可能会使用 SPA 框架,不管你对这个问题的回答是什么,但至少要知道你是否真的需要一个框架。

以下是包含完整解决方案的 repo 链接:

回购

什么是 SPA 框架

SPA 是单页应用的缩写,意思是你的应用只存在于一个页面上。

这似乎不太有用,我的大多数应用程序都是多页的?

我并没有说你不能有多个页面,只是你永远不能离开那个页面

你在说谜语,请解释一下

好的,事情是这样的。你停留在这个页面上,但我们仍然可以切换该页面上的部分内容,给人一种你正在从一个页面切换到下一个页面的感觉。所以页面上会有静态部分,比如页眉页脚,但中间部分会根据例如选择菜单选项而变化。

好的,这很有道理。但是浏览器中的 URL 看起来不一样吗?

实际上,我们正在改变的是一种叫做哈希的东西,#因此您的路线不是从 开始home.html to products.html,而是从 转变someBaseUrl#/homesomeBaseUrl#/products

但我确信即使人们使用 SPA 应用程序时我也见过正常的路线?

是的,大多数 SPA 框架都有一种方法可以使用重写 URL history.pushState,并且还可以使用 catch-all 路由来确保您可以进行编写someBaseUrl/products

好的,足够公平。

 为什么要使用 SPA 框架

就像科技和生活中的其他一切一样,使用合适的工具来完成工作。虽然使用 SPA 框架来处理所有前端工作很诱人,但这并不总是正确的方法。

那么它解决了什么问题呢?闪烁和迟缓的 UI 正是 SPA 的用武之地。在没有 SPA 框架的时代,应用程序在从一个页面切换到下一个页面时会完全重新加载页面。这导致它感觉不像客户端应用程序那样快速流畅。因此,有了 SPA 框架,我们突然拥有了类似客户端的Web 应用程序。

然而,这也带来了一个缺点,那就是搜索引擎收录效果不佳,因为大多数页面都是动态的,无法被抓取。大多数主流 SPA 框架已经并正在解决这个问题,解决方案通常是从应用生成静态页面。不过,并非所有应用都需要考虑这个问题。对于生产力应用来说,这其实无关紧要,但对于电商网站来说,SEO 排名的高低可能会决定你的公司成败。

因此一定要使用 SPA 框架,您将构建快速的应用程序,但也要了解其缺点并确保找到解决这些缺点的解决方案。

 构建微型 SPA 框架

构建SPA框架,你确定你吃药了吗?:)

没关系,我们只构建了一小部分,以便理解那些最初的关键部分,并且在此过程中,我们希望能够展示它从“我可以用一个丑陋的黑客来做到这一点”到“我可能需要一个框架/库”的过程。

我们的计划如下:

  • 实现路由,路由对于任何 SPA 应用程序都至关重要,我们需要能够定义页面的静态部分以及可以轻松替换的动态部分
  • 定义模板并渲染数据。并非所有 SPA 都使用模板,但相当一部分 SPA 会使用模板,例如 Vue.js、AngularJS、Angular 和 Svelte。不过,我会在以后的文章中介绍 React 的方法 :) 我们想要实现的是能够在需要的地方准确地渲染数据,并且应该能够执行诸如渲染数据列表、有条件地渲染数据等操作。

实现路由

让我们首先创建两个文件:

app.js
index.html
Enter fullscreen mode Exit fullscreen mode

正如我们在本文前面所说,SPA 中的路由与哈希#符号及其更改时间有关。好消息是,我们可以使用以下代码监听该更改:

// app.js

async function hashHandler() {
  console.log('The hash has changed!', location.hash);
}

window.addEventListener('hashchange', hashHandler, false);
Enter fullscreen mode Exit fullscreen mode

好的,现在怎么办?

好吧,我们只需要将不同的路线映射到不同的动作,如下所示:

// app.js 

const appEl = document.getElementById('app');

const routes = {
  '#/': () => {
    return 'default page'
  }, 
  '#/products':() => {
    return 'Products'
  }
}

async function hashHandler() {
  console.log('The hash has changed!', location.hash);
  const hash = !location.hash ? '#/' : location.hash;
  appEl.innerHTML = await routes[hash]();
}
Enter fullscreen mode Exit fullscreen mode

然后我们可以将我们的更新index.html如下:

<!-- index.html -->
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet" />
  </head>
  <body>
    <div class="menu">
      <div class="item"><a href="#/">Home</a></div>
      <div class="item"><a href="#/products">Products</a></div>
    </div>
    <div class="app" id="app">
    </div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

模板

上面的代码不太好用,因为我们只能根据路由变化来渲染字符串。我们有一个路由器,但我们想要更多。

如果我们能够以某种方式定义一个模板和一些数据,然后能够将两者合并并显示出来,那就太好了,我们可以这样做吗?

可以,有很多模板库,但我们会选择 Handlebars。

您可以获取它的 CDN 链接或通过 NPM 下载它

npm install handlebars --save
Enter fullscreen mode Exit fullscreen mode

现在怎么办?

现在我们做两件事:

  1. 定义模板
  2. 在路线改变时渲染模板

定义模板

我们可以将模板定义为外部文件或scriptDOM 树中的元素,我们将采用后者以保持简单:

<script id="hello" type="text/x-handlebars-template">
  <div>
    {{title}}
  </div>
  <div>
    {{description}}
  </div>
</script>
Enter fullscreen mode Exit fullscreen mode

请注意,我们为模板添加了一个idhello,并将类型设置为text/x-handlebars-template。这样就可以handlebars找到这个模板。

渲染模板

渲染模板就像调用以下代码一样简单:

var template = $('#hello').html();

// Compile the template data into a function
var templateScript = Handlebars.compile(template);
var html = templateScript({ title: 'some title', description: 'some description' });
Enter fullscreen mode Exit fullscreen mode

此时,我们的变量html包含一段可以附加到 DOM 树的 HTML。让我们将这段代码嵌入到我们的应用中,如下所示:

// app.js 

const appEl = document.getElementById('app');

function buildTemplate(tmpId, context) {
  var template = $('#' + tmpId).html();

  // Compile the template data into a function
  var templateScript = Handlebars.compile(template);
  var html = templateScript(context);
  return html;
}

const routes = {
  '#/': () => {
    return buildTemplate('hello', { title: 'my title', description: 'my description' })
  }, 
  '#/products':() => {
    return 'Products'
  }
}

async function hashHandler() {
  console.log('The hash has changed!', location.hash);
  const hash = !location.hash ? '#/' : location.hash;
  appEl.innerHTML = await routes[hash]();
}
Enter fullscreen mode Exit fullscreen mode

好的,我们已经有了一些基本的模板,那么列表呢?handlebars 解决这个问题的方法是在模板中使用以下语法:

<script id="cats-list" type="text/x-handlebars-template">
  <div class="products">
  {{#each products}}
    <div class="product">
    {{title}} {{description}}
    </div>
  {{/each}}
  </div>
</script>
Enter fullscreen mode Exit fullscreen mode

让我们放大{{#each products}}并查看结束标签{{/each}},这样我们就可以渲染一个列表。现在来app.js更新我们的/products路线:

// app.js 

const appEl = document.getElementById('app');

function buildTemplate(tmpId, context) {
  var template = $('#' + tmpId).html();

  // Compile the template data into a function
  var templateScript = Handlebars.compile(template);
  var html = templateScript(context);
  return html;
}

const routes = {
  '#/': () => {
    return buildTemplate('hello', { title: 'my title', description: 'my description' })
  }, 
  '#/products':() => {
    return buildTemplate('products', { products: [{ id:1, title: 'IT', scary book }, { id:2, title: 'The Shining', 'not a fan of old houses' }] })
  }
}

async function hashHandler() {
  console.log('The hash has changed!', location.hash);
  const hash = !location.hash ? '#/' : location.hash;
  appEl.innerHTML = await routes[hash]();
}
Enter fullscreen mode Exit fullscreen mode

它还能handlebars为我们做更多的事情,比如条件逻辑、内置指令以及自定义指令。完整参考请见此处:

https://handlebarsjs.com/

事件处理

那么事件呢?

嗯,它是纯 JavaScript,因此只需将您拥有的任何事件与处理程序连接起来,如下所示:

<script id="cats-list" type="text/x-handlebars-template">
  <div class="products">
  {{#each products}}
    <div class="product">
    {{title}} {{description}}
    </div>
    <button onclick="buy({{id}})">Buy</button>
  {{/each}}
  </div>
</script>
Enter fullscreen mode Exit fullscreen mode

我们app.js只需要一个方法buy(),就像这样:

function buy(id) {
  console.log('should call an endpoint', id);
}
Enter fullscreen mode Exit fullscreen mode

异步数据

好的,我们如何处理后端,简单来说,通过fetch(),像这样:


'#/products': async() => {
    const res = await fetch('http://localhost:3000/products')
    const json = await res.json();
    return buildTemplate('products', { products: json })
  }
Enter fullscreen mode Exit fullscreen mode

概括

那么,你需要 SPA 吗?这取决于你是否只想渲染列表,并时不时地添加一些条件逻辑。我认为你不需要。不过,SPA 还附带很多其他功能,比如优化渲染。我敢打赌,这种方法在渲染几百个元素时就显得力不从心了。SPA 通常附带状态管理之类的功能,这些功能可以轻松地与 SPA 本身挂钩,你几乎毫不费力就能获得服务器端渲染和渐进式 Web 应用之类的功能。所以听起来我好像在提倡 YAGNI(你不需要它)?但众所周知,你周五做的那个小改动两年后就成了关键业务系统的一部分,所以你应该选择 React、Angular、Vue.js 或 Svelte 等等。

我希望至少我已经向你展示了如何在 30 分钟内实现许多类似 SPA 的行为。我想表达的重点是——要知道什么时候需要 SPA 方法,并且在某些情况下,采用完整的框架可能会有些过度,只是说说而已 ;)

文章来源:https://dev.to/itnext/do-i-really-need-a-spa-framework-3occ
PREV
如何为 Node.js 构建自己的 Web 框架
NEXT
5 件可能会让 JavaScript 初学者/OO 开发人员感到惊讶的事情