四、实际例子——看看 Svelte 和原始

在过去的几章中我们已经介绍了现代网络和可用的 api,现在我们将举一个实际的例子来说明这些 api 的作用。 web 框架已经有了相当多的发展,它们创建了一种与之相关的运行时。 这个运行时几乎都可以归因于虚拟 DOM(VDOM)和一个状态系统。 当这两件事相互联系时,我们就能够创造丰富的和反应性的前端。 这些框架的例子有 React、Vue 和 Angular。

但是,如果我们抛弃 VDOM 和运行时的概念,以某种方式将所有这些代码编译成普通的 JavaScript 和 web API 调用,会发生什么呢? 这就是 Svelte 框架的创建者所考虑的:利用我们在浏览器中拥有的东西,而不是创建我们自己的浏览器版本(这是一个明显的过度简化,但它并没有过分夸大事实)。 在本章中,我们将介绍一个 Svelte,以及它是如何实现这种魔力的,以及一些在这个框架中编写的应用的例子。 这将有助于我们更好地理解现有的 Svelte 和无运行时框架,以及它们如何潜在地提高我们的应用运行时速度。

本章涉及的主题如下:

  • 一个纯粹的速度框架
  • 构建基础——一个 Todo 应用
  • 越来越花哨-基本的天气应用

技术要求

本章要求如下:

一个纯粹的速度框架

Svelte 框架决定将重点从基于运行时的系统转移到基于编译器的系统。 这可以在他们的网站上看到,网址是https://svelte.dev。 在他们的首页上,甚至写着:

Svelte compiles your code to tiny, framework-less vanilla JS – your app starts fast and stays fast.

通过将这些步骤从运行时转移到初始编译,我们能够创建下载和运行速度很快的应用。 但是,在我们开始看这个编译器之前,我们需要把它放到我们的机器上。 下面的步骤应该能够让我们开始为 Svelte 编写代码(直接从https://svelte.dev/blog/the-easiest-way-to-get-started中获取):

> npx degit sveltejs/template todo
> cd todo
> npm install
> npm run dev

有了这些命令,我们现在有了一个位于localhost:5000的正在运行的 Svelte 应用。 让我们看看里面有什么package.json,让我们起来,并运行得如此之快。 首先,我们会注意到我们有一堆基于 rollup 的依赖项。 Rollup 是一个 JavaScript 模块绑定器,它也有一组丰富的工具来完成许多其他任务。 它类似于 webpack 或 Parcel,但它是 Svelte 决定依赖的工具。 我们将在第 12 章构建和部署一个完整的 Web 应用中更深入地了解 Rollup。 只要知道它正在为我们编译和绑定我们的代码。

似乎我们还得到了一个叫做sirv的(可以在package.json文件中看到)。 如果我们查找sirv``npm内,我们将看到它是一个静态资产服务器,但是,而不是寻找直接在文件系统上的文件(这是一个相当昂贵的操作),它在内存中缓存请求头和响应了一段时间。 这允许它为可能已经快速服务的资产提供服务,因为它只需要查看自己的内存,而不需要执行 I/O 操作来查找资产。 命令行界面(CLI)允许我们快速设置服务器。

最后,我们以 dev 模式启动应用。 如果我们查看package.json文件的scripts部分,我们将看到它运行以下命令: run-p命令表示并行运行所有后续命令。 start:dev命令表示在 dev 模式下启动我们的sirv服务器,autobuild命令告诉 Rollup 编译并观察我们的代码。 这意味着无论何时我们对文件进行更改,它都会自动为我们构建。 让我们快速地看看它是如何运作的。 让我们进入src文件夹,并对App.svelte文件进行更改。 添加以下:

//inside of the script tag
export let name;
export let counter;

function clicker() {
   counter += 1;
}

//add to the template
<span>We have been clicked {counter} times</span>
<button on:click={clicker}>Click me!</button>

我们会注意到我们的网页已经自动更新,我们现在有一个基于事件的反应网页! 当我们处于开发模式时,这真的很好,因为我们不需要一直触发编译器。

The editor of choice in these examples is VS Code. If we head to the extensions section of VS Code, there is a nice plugin for Svelte. We can utilize this plugin for syntax highlighting and some alerts when we are doing something wrong. If the preferred editor does not have a Svelte plugin, try to at least get the HTML highlighting enabled for the editor.

好了:这个简单的例子已经给了我们很多东西可以看。 首先,App.svelte文件提供了与 Vue 文件相似的语法。 我们有一个用于 JavaScript 的部分,一个用于样式化的部分,还有一个用于增强的 HTML 的部分。 我们导出了两个变量:namecounter。 我们还在按钮的点击处理程序中使用了一个函数。 我们还为我们的h1元素启用了样式。

看起来花括号添加了我们期望从这些响应式框架中得到的单向数据绑定。 它看起来也像我们在一个简单的on:<event>绑定中附加事件,而不是利用内置的on<event>系统。

如果我们现在进入main.js文件,我们将看到我们正在导入刚才看到的 Svelte 文件。 然后我们创建一个新的应用(它看起来应该与其他响应式框架相似),我们的应用的目标是文档的主体。 在此基础上,我们设置了一些属性,即我们之前导出的namecounter变量。 然后将其导出为该文件的默认导出。

所有这些看起来都与前一章非常相似,当时我们研究了内置在浏览器中的类和模块系统。 Svelte 只是利用了这些类似的概念来编写编译器。 现在,我们应该看看编译过程的输出。 我们将注意到我们有一个bundle.css和一个bundle.js文件。 如果我们首先看一下生成的bundle.css文件,我们将看到如下内容:

h1.svelte-i7qo5m{color:purple}

本质上,Svelte 是模仿web 组件,将它们放在一个独特的名称空间下,在本例中是svelte-i7qo5m。 这非常简单,使用过其他系统的人会注意到,许多框架就是这样创建作用域样式表的。

现在,如果我们进入bundle.js文件,我们将看到一个完全不同的故事。 首先,我们有一个立即调用函数表达式(IIFE),这是实时重新加载代码。 接下来,我们有另一个 IIFE,它将我们的应用分配给一个全局变量app。 然后里面的代码有一堆样板代码,比如nooprunblank_object。 我们还可以看到,Svelte 封装了许多内置方法,比如 DOM 的appendChildcreateElementapi。 这可以在以下代码中看到:

function append(target, node) {
    target.appendChild(node);
}
function insert(target, node, anchor) {
    target.insertBefore(node, anchor || null);
}
function detach(node) {
    node.parentNode.removeChild(node);
}
function element(name) {
    return document.createElement(name);
}
function text(data) {
    return document.createTextNode(data);
}
function space() {
    return text(' ');
}

他们甚至用自己的形式包装了addEventListener系统,这样他们就可以控制回调和生命周期事件。 这可以通过下面的代码看到:

function listen(node, event, handler, options) {
    node.addEventListener(event, handler, options);
    return () => node.removeEventListener(event, handler, options);
}

然后,它们有一组数组,它们利用这些数组作为各种事件的队列。 它们通过它们循环,并在事件出现时弹出并运行。 这可以从他们列出的冲洗方法中看出。 一个有趣的注意是,他们有seen_callbacks集。 这是为了通过计数可能导致无限循环的方法/事件来停止无限循环。 例如,组件A获取一个更新,该更新随后发送一个更新到组件B,后者再发送一个更新到组件AWeakSet在这里可能是一个更好的选择,但他们选择使用常规Set,因为一旦 flush 方法完成,它将被转储。

最后一个值得关注的函数是create_fragment方法。 我们将注意到它返回一个对象,该对象有一个名为c的创建函数。 正如我们所看到的,这将创建我们在 Svelte 文件中拥有的 HTML 元素。 然后我们将看到一个m属性,它是将 DOM 元素添加到实际文档中的 mount 函数。 p属性更新我们已经绑定到这个 Svelte 组件的属性(在本例中是namecounter属性)。 最后,我们有d属性,它与destroy方法相关,并删除所有的 DOM 元素和 DOM 事件。

通过这段代码中,我们可以看到,Svelte 的利用很多的概念,我们会使用如果我们从头开始构建 UI 和利用 DOM API,但是他们刚刚裹成一堆方便包装和聪明的代码行。

理解库的一个很好的方法是阅读源代码或查看它的输出。 通过这样做,我们可以找到魔法通常存在的地方。 虽然这可能不会马上带来好处,但它可以帮助我们为框架编写代码,甚至利用我们在自己的代码库中看到的一些代码技巧。 学习的一种方法是模仿别人。

在所有这些中,我们可以看到 Svelte 是如何声明没有运行时的。 它们利用了 DOM 在一些方便的包装器中提供的基本元素。 它们也为我们编写代码提供了一种很好的文件格式。 尽管这看起来像是一些基本的代码,但我们可以用这种风格编写复杂的应用。

我们将编写的第一个应用是一个简单的 Todo 应用。 我们将添加一些我们自己的想法,但它将是一个传统的 Todo 应用开始。

构建基础——一个 Todo 应用

为了启动我们的 Todo 应用,让我们继续使用我们已经拥有的模板。 现在,在大多数的 Todo 应用中,我们希望能够做以下事情:

  • 添加
  • 删除/马克完成
  • 更新

因此,我们拥有的是一个没有任何服务器操作的基本 CRUD 应用。 让我们继续为这个应用编写我们期望的 Svelte HTML:

<script>
    import { createEventDispatcher } from 'svelte';
    export let completed;
    export let num;
    export let description;

    const dispatch = createEventDispatcher();
</script>
<style>
    .completed {
        text-decoration: line-through;
    }
</style>
<li class:completed>
    Task {num}: {description}
    <input type="checkbox" bind:checked={completed} />
    <button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>

我们将 Todo 应用分成 Todo 组件和通用应用。 Todo 元素将包含完成和删除元素的所有逻辑。 从上面的例子中我们可以看到,我们正在做以下事情:

  • 我们公开这个任务的编号和描述。
  • 我们有一个对主应用隐藏的完整属性。
  • 我们有一个用于对完成的项目进行样式化的类。
  • 带有 completion 变量的 list 元素绑定到 complete 类。
  • numdescription属性与信息相关联。
  • 当我们完成一个项目时,会添加一个复选框。
  • 有一个按钮会告诉应用我们想要删除什么。

这有点难理解,但当我们把它们放在一起时,我们会看到这包含了单个 Todo 项目的大部分逻辑。 现在,我们需要添加应用的所有逻辑。 它看起来应该如下所示:

<script>
    import Todo from './Todo.svelte';

    let newTodoText = '';
    const Todos = new Set();

    function addTodo() {
        const todo = new Todo({
            target: document.querySelector('#main'),
            props: {
                num : Todos.size,
                description : newTodoText
            }
        });
        newTodoText = '';
        todo.$on('remove', () => {
            Todos.delete(todo);
            todo.$destroy();
        });
        Todos.add(todo);
    }
</script>
<style></style>
<h1>Todo Application!</h1>
<ul id="main">
</ul>
<button on:click={addTodo}>Add Todo</button>
<input type="text" bind:value={newTodoText} />

我们首先导入之前创建的Todo。 然后我们将newTodoText作为属性绑定到输入文本。 然后,我们创建一个集合来存储所有的Todos。 接下来,我们创建一个将绑定到 Add Todo 按钮的click事件的addTodo方法。 这将创建一个新的Todo,将元素绑定到我们的无序列表,并将属性分别设置为我们的设置大小和输入文本。 我们重置了Todo文本,并添加了一个删除监听器来销毁Todo,并从我们的设置中删除它。 最后,我们将它添加到集合中。

现在我们有了一个基本的 Todo 应用! 所有这些逻辑都应该相当简单明了。 让我们像前一章那样添加一些额外的特性。 我们将添加以下内容到我们的 Todo 应用,使其更健壮和有用:

  • 有与每个相关的截止日期Todo
  • 数一数所有的Todos
  • 创建基于过期、已完成和所有内容进行筛选的过滤器
  • 基于过滤器和添加每个Todo的过渡

首先,让我们向 Todo 应用添加一个到期日期。 我们将在我们的Todo.svelte文件中添加一个新的导出字段dueDate,并将其添加到我们的模板中,如下所示:

//inside of script tag
export let dueDate;

//part of the template
<li class:completed>
    Task {num}: {description} - Due on {dueDate}
    <input type="checkbox" bind:checked={completed} />
    <button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>

然后,在我们的App.svelte文件中,我们将添加一个日期控件,并确保当我们将Todo添加到列表中时,我们也确保将这个字段放回列表中。 这看起来应该如下所示:

//inside of the script tag
let newTodoDate = null;
function addTodo() {
    const todo = new Todo({
        target: document.querySelector('#main'),
        props: {
            num : Todos.size + 1,
            dueDate : newTodoDate,
            description : newTodoText
        }
    });
    newTodoText = '';
    newTodoDate = null;
    todo.$on('remove', () => {
        Todos.delete(todo);
        todo.$destroy();
    });
    Todos.add(todo);
}

//part of the template
<input type="date" bind:value={newTodoDate} />

我们现在有一个功能完善的到期日系统。 接下来,我们将当前的Todos添加到我们的应用中。 这就像将 span 中的一些文本绑定到 set 的大小一样简单,如下面的代码所示:

//inside of script tag
let currSize = 0;
function addTodo() {
    const todo = new Todo({
        // code removed for readability
    });
    todo.$on('remove', () => {
        Todos.delete(todo);
        currSize = Todos.size;
        todo.$destroy();
    });
    Todos.add(todo);
    currSize = Todos.size;
}

//part of the template
<h1>Todo Application! <span> Current number of Todos: {currSize}</span></h1>

好了,现在我们想要用所有的日期和完成状态来做一些事情。 让我们添加一些过滤器,这样我们就可以删除不符合我们标准的Todos。 我们将添加已完成和过期过滤器。 我们将创建这些复选框,因为一个项目可以同时过期和完成:

//inside of script tag
let completed = false;
let overdue = false;

//part of the template
<label><input type="checkbox" bind:checked={completed}
    on:change={handleFilter}/>Completed</label>
<label><input type="checkbox" bind:checked={overdue}
    on:change={handleFilter}/>Overdue</label>

我们的句柄过滤逻辑应该如下所示:

function handleHide(item) {
    const currDate = Date.now();
    if( completed && overdue ) {
        item.hidden = !item.completed || new Date(item.dueDate).getTime() < currDate;
        return;
    }
    if( completed ) {
        item.hidden = !item.completed;
        return;
    }
    if( overdue ) {
        item.hidden = new Date(item.dueDate).getTime() < currDate;
        return;
    }
    item.hidden = false;
}

function handleFilter() {
    for(const item of Todos) {
        handleHide(item);
    }
}

我们还需要确保我们对任何新的Todo项有相同的隐藏逻辑:

const todo = new Todo({
    target: document.querySelector('#main'),
    props: {
        num : Todos.size + 1,
        dueDate : newTodoDate,
        description : newTodoText
    }
});
handleHide(todo);

最后,我们的Todo.svelte组件应该如下所示:

<svelte:options accessors={true} />
<script>
    import { createEventDispatcher } from 'svelte';

    export let num;
    export let description;
    export let dueDate;
    export let hidden = false;
    export let completed = false;

    const dispatch = createEventDispatcher();
</script>
<style>
    .completed {
        text-decoration: line-through;
    }
    .hidden {
        display : none;
    }
</style>
<li class:completed class:hidden>
    Task {num}: {description} - Due on {dueDate}
    <input type="checkbox" bind:checked={completed} />
    <button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>

除了上面的部分,大部分看起来应该很熟悉。 有一些特殊的标签,我们可以添加到 Svelte 文件,允许我们访问某些属性,如以下:

  • <svelte:window>让我们可以访问窗口事件。
  • <svelte:body>让我们接触到身体活动。
  • 使我们可以看到文件的开头部分。
  • <svelte:component>让我们以 DOM 元素的形式访问我们自己。
  • <svelete:self>允许我们包含自己(对于递归结构,如树)。
  • <svelte:options>允许我们向组件中添加编译器选项。

在本例中,我们希望父组件能够通过 getter /setters 访问我们的属性,因此我们将accessors选项设置为true。 这就是我们如何改变App.svelte文件中的隐藏属性,并允许我们获得每个Todo上的属性。

最后,让我们添加一些渐入渐出的过渡。 当我们添加/删除元素时,Svelte 有一些漂亮的动画。 我们将要使用的是fade动画。 因此,我们的Todo.svelte文件现在将添加以下内容:

//inside of script tag
import { fade } form 'svelte/transition';

//part of template
{#if !hidden}
    <li in:fade out:fade class:completed>
        Task {num}: {description} - Due on {dueDate}
        <input type="checkbox" bind:checked={completed} />
        <button on:click="{() => dispatch('remove', null)}">Remove</button>
    </li>
{/if}

这个特殊的语法用于条件 DOM 的加法/减法。 与我们在 DOM API 中添加/删除子元素的方式相同,Svelte 也在做同样的事情。 接下来,我们可以看到我们向列表元素添加了in:fadeout:fade指令。 现在,当元素从 DOM 中添加或删除时,它将淡入和淡出。

现在我们有了一个功能相当强大的 Todo 应用。 我们有过滤逻辑,Todos与截止日期绑定,甚至还有一点动画。 下一步是稍微清理一下代码。 我们可以通过 Svelte 内置的商店做到这一点。

存储是一种共享状态的方式,而不必使用我们在应用中使用的一些技巧(我们在可能不应该使用的时候开放了访问器系统)。 我们的Todos和主应用之间的共享状态是过期和完成的过滤器。 每个Todo应该最有可能控制这个属性,但我们目前使用的是访问器选项,所有的过滤都是在主应用中完成的。 有了可写存储,我们就不再需要这样做了。

首先,我们像下面这样写一个stores.js文件:

import { writable } from 'svelte/store';

export const overdue = writable(false);
export const completed = writable(false);

接下来,我们更新我们的App.svelte文件,使其不再针对Todos中的hidden属性,并将复选框输入的checked属性绑定到存储中,如下所示:

//inside of script tag
import { completed, overdue } from './stores.js';

//part of the template
<label><input type="checkbox" bind:checked={$completed} />Completed</label>
<label><input type="checkbox" bind:checked={$overdue} />Overdue</label>

商店前面的美元符号表示这些是商店,而不是脚本中的变量。 它允许我们更新和订阅商店,而不必取消从他们销毁。 最后,我们可以更新我们的Todo.svelte文件,如下所示:

<script>
    import { overdue, completed } from './stores.js';
    import { createEventDispatcher, onDestroy } from 'svelte';
    import { fade } from 'svelte/transition';

    export let num;
    export let description;
    export let dueDate;
    let _completed = false;

    const dispatch = createEventDispatcher();
</script>
<style>
    .completed {
        text-decoration: line-through;
    }
</style>
{#if
    !(
         ($completed && !_completed) ||
         ($overdue && new Date(dueDate).getTime() >= Date.now())
     )
}
    <li in:fade out:fade class:_completed>
        Task {num}: {description} - Due on {dueDate}
        <input type="checkbox" bind:checked={_completed} />
        <button on:click="{() => dispatch('remove', null)}">Remove</button>
    </li>
{/if}

我们已经将过期和完成的存储添加到我们的系统中。 您可能已经注意到,我们去掉了文件顶部的编译器选项。 然后我们将我们的#if条件与这些商店联系起来。 我们现在把基于过滤器的Todos隐藏的责任放在Todos本身上,同时也删除了相当多的代码。 显而易见的是,我们有很多方法可以在 Svelte 中构建应用,并对应用保持相当多的控制。

在进入下一个应用之前,请继续研究绑定的 JavaScript 和 CSS 以及向应用添加新特性。 接下来,我们将构建一个天气应用,并从服务器获取该信息的数据。

一个基本的天气应用

很明显,Svelte 已经构建了可以与大多数现代 ECMAScript 标准一起工作的编译器。 它们不提供任何包装器的一个领域是获取数据。 添加这个并查看效果的一个好方法是构建一个基本的天气应用。

天气应用的核心是,需要能够接收邮政编码或城市,并输出有关该地区当前天气的信息。 我们还可以根据这个位置获得天气预报。 最后,我们还可以在浏览器中保存这些选项,以便在返回应用时使用它们。

我们将从https://openweathermap.org/api获取天气数据。 在这里,免费服务将允许我们获得当前的天气。 在此之上,我们将需要一个输入系统,将接受以下内容:

  • 城市/国家
  • 邮政编码(如果没有给出国家,我们将假定美国,因为这是 API 的默认值)

当我们输入正确的值时,我们将它存储在LocalStorage中。 在本章的后面,我们将更深入地了解LocalStorageAPI,但请注意,它是浏览器中的键值存储机制。 当我们为输入输入一个值时,我们将得到一个下拉列表,显示所有以前的搜索结果。 我们还将添加从列表中删除任何一个结果的功能。

首先,我们需要获取一个 API 密钥。 要做到这一点,请遵循以下步骤:

  1. 进入https://openweathermap.org/api,按照说明获取 API 密钥。
  2. 一旦我们创建了一个帐户并验证它,我们就可以添加 API 密钥了。
  3. 登录后,应该有一个标签,说API 键。 如果我们这样做,我们应该收到一个无 api 键消息。
  4. 我们可以创建一个键并添加一个名称,如果我们想(我们可以叫它default)。
  5. 有了这个密钥,我们现在就可以开始调用他们的服务器了。

让我们来设置一个测试调用。 下面的代码应该可以工作:

let api_key = "<your_api_key>";
fetch(`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${api_key}`)
    .then((res) => res.json())
    .then((final) => console.log(final));

如果我们将其放入代码段中,我们应该返回一个 JSON 对象,其中包含一组数据。 现在我们可以使用这个 API 来创建一个不错的天气应用。

让我们以与设置 Todo 应用相同的方式来设置应用。 执行如下命令:

> cd ..
> npx degit sveltejs/template weather
> cd weather
> npm install
> npm run dev

现在我们已经启动了环境,让我们创建一个具有一些基本样式的样板应用。 在global.css文件中,将以下行添加到正文:

display: flex;
flex-direction : column;
align-items : center;

这将确保我们的元素都是基于列的,它们将从中心开始并向外扩展。 这将为我们的应用提供一个漂亮的外观。 接下来,我们将创建两个 Svelte 组件,一个WeatherInput和一个WeatherOutput组件。 接下来,我们将关注输入。

我们将需要有以下项目,以便我们可以从我们的用户获得正确的输入:

  • 输入邮政编码或城市
  • 输入国家代码
  • 提交按钮

我们还将向应用添加一些条件逻辑。 我们不是试图解析输入,而是根据输入左侧的复选框有条件地呈现文本或数字输入。 有了这些想法,我们的WeatherInput.svelte文件应该如下所示:

<script>
    import { zipcode } from './stores.js';
    const api_key = '<your_api_key>'

    let city = null;
    let zip = null;
    let country_code = null;

    const submitData = function() {
        fetch(`https://api.openweathermap.org/data/2.5/weather?q=${zipcode 
         ? zip : city},${country_code}&appid=${api_key}`)
            .then(res => res.json())
            .then(final => console.log(final));
    }
</script>
<style>
    input:valid {
        border: 1px solid #333;
    }
    input:invalid {
        border: 1px solid #c71e19;
    }
</style>
<div>
    <input type="checkbox" bind:checked={$zipcode} />
    {#if zipcode}
        <input type="number" bind:value={zip} minLength="6" maxLength="10" 
         require />
    {:else}
        <input type="text" bind:value={city} required />
    {/if}
    <input type="text" bind:value={country_code} minLength="2" 
     maxLength="2" required />
    <button on:click={submitData}>Check</button>
</div>

这样,我们就有了输入的基本模板。 首先,我们创建一个zipcode存储,以便有条件地显示数字或文本输入。 然后,创建两个本地变量,将它们绑定到输入值。 一旦我们准备好得到某种类型的响应,submitData函数将提交所有内容。 目前,我们只是将输出记录到开发人员控制台。

对于样式,我们只是为有效输入和无效输入添加了一些基本样式。 我们的模板提供了一个复选框来打开zipcode功能或关闭它。 然后我们有条件地显示zipcode或城市文本框。 每个文本框都添加了内置的验证。 接下来,我们添加了另一个文本字段,以从用户那里获取国家代码。 最后,我们添加了一个按钮,它将显示并检查数据。

The brackets are heavily utilized in Svelte. One feature of input validation is regex based. The field is called a pattern. If we try to utilize brackets in here, it will cause the Svelte compiler to fail. Just be aware of this.

在输出之前,让我们继续向输入添加一些标签,以便用户更容易使用。 以下几点应该可以做到:

//in the style tag
input {
    margin-left: 10px;
}
label {
    display: inline-block;
}
#cc input {
    width: 3em;
}

对于每个input元素,我们将它们包裹在label中,如下所示:

<label id="cc">Country Code<input type="text" bind:value={country_code} minLength="2" maxLength="2" required /></label>

这样,我们就有了input元素的基本用户界面。 现在,我们需要让fetch调用实际输出到一些东西,一旦我们做了它,WeatherOutput元素就可以使用它。 我们不只是将数据作为道具传递出去,而是创建一个实现gather方法的自定义存储。 在stores.js内部,我们应该有如下内容:

function createWeather() {
    const { subscribe, update } = writable({});
    const api_key = '<your_api_key>';
    return {
        subscribe,
        gather: (cc, _z, zip=null, city=null) => {
            fetch(`https://api.openweathermap.org/data/2.5/weather?=${_z ? 
             zip : city},${cc}&appid=${api_key})
                .then(res => res.json())
                .then(final => update(() => { return {...final} }));
        }
    }
}

现在,我们已经将获取数据的逻辑转移到一个存储中,我们现在可以订阅这个存储来更新自己。 这将意味着我们可以让WeatherOutput组件订阅这个函数以获得一些基本输出。 WeatherOtuput.svelte应输入以下代码:

<script>
    import { weather } from './stores.js';
</script>
<style>
</style>
<p>{JSON.stringify($weather)}</p>

我们现在所做的就是将天气的输出放到一个段落元素中,并对其进行严格化,这样我们就可以在不查看控制台的情况下读取输出。 我们还需要更新我们的App.svelte文件,并导入WeatherOutput组件,如下所示:

//inside the script tag
import WeatherOutput from './WeatherOutput.svelte'

//part of the template
<WeatherOutput></WeatherOutput>

如果我们现在测试我们的应用,我们应该得到一些难看的 JSON,但我们现在已经通过商店绑定了我们的两个组件! 现在,我们需要做的就是美化输出,这样我们就有了一个功能完整的天气应用! 将WeatherOutput.svelte的样式和模板更改为如下:

<div>
    {#if $weather.error}
        <p>There was an error getting your data!</p>
    {:else if $weather.data}
        <dl>
            <dt>Conditions</dt>
            <dd>{$weather.weather}</dd>
            <dt>Temperature</dt>
            <dd>{$weather.temperature.current}</dd>
            <dd>{$weather.temperature.min}</dd>
            <dd>{$weather.temperature.max}</dd>
            <dt>Humidity</dt>
            <dd>{$weather.humidity}</dd>
            <dt>Sunrise</dt>
            <dd>{$weather.sunrise}</dd>
            <dt>Sunset</dt>
            <dd>{$weather.sunset}</dd>
            <dt>Windspeed</dt>
            <dd>{$weather.windspeed}</dd>
            <dt>Direction</dt>
            <dd>{$weather.direction}</dd>
        </dl>
    {:else}
        <p>No city or zipcode has been submitted!</p>
    {/if}
</div>

最后,我们应该添加一个新的控件,以便用户可以为输出选择公制或英制单位。 在WeatherInput.svelte中添加以下内容:

<label>Metric?<input type="checkbox" bind:checked={$metric}</label>

我们还将使用一个新的metric存储到默认为falsestores.js文件。 有了所有这些,我们现在应该有一个功能良好的天气应用了! 我们唯一剩下的就是添加LocalStorage功能。

有两种类型的存储可以做类似的事情。 它们是LocalStorageSessionStorage。 主要的区别在于缓存的时间。 在用户删除缓存或应用开发人员决定删除缓存之前,都将保留LocalStorageSessionStorage在页面的生命周期内保持在缓存中。 一旦用户决定离开页面,SessionStorage将被清除。 离开页面意味着关闭标签或导航离开; 这并不意味着重新加载页面或 Chrome 崩溃和用户恢复页面。 使用哪一个由设计者决定。

利用LocalStorage是很容易的。 在我们的例子中,对象被保存在窗口中(如果我们在一个 worker 中,它将被保存在全局对象中)。 要记住的一件事是,当我们使用LocalStorage时,它将所有的值转换为字符串,所以如果我们想存储复杂对象,我们需要转换它们。

要更改应用,让我们专门为下拉列表创建一个新组件。 我们称它为Dropdown。 首先,创建一个Dropdown.svelte文件。 接下来,将以下代码添加到文件中:

<script>
    import { weather } from './stores.js';
    import { onDestroy, onMount } from 'svelte';

    export let type = "text";
    export let name = "DEFAULT";
    export let value = null;
    export let required = true;
    export let minLength = 0;
    export let maxLength = 100000;
    let active = false;
    let inputs = [];
    let el;

    const unsubscribe = weather.subscribe(() => {
        if(!inputs.includes(value) ) {
            inputs = [...inputs, value];
            localStorage.setItem(name, inputs);
        }
        value = '';
    });
    const active = function() {
        active = true;
    }
    const deactivate = function(ev) {
        if(!ev.path.includes(el) ) 
            active = false;
    }
    const add = function(ev) {
        value = ev.target.innerText;
        active = false;
    }
    const remove = function(ev) {
        const text = ev.target.parentNode.querySelector('span').innerText;
        const data = localStorage.getItem(name).split(',');
        data.splice(data.indexOf(text));
        inputs = [...data];
        localStorage.setItem(name, inputs);
    }
    onMount(() => {
        const data = localStorage.getItem(name);
        if( data === "" ) { inputs = []; }
        else { inputs = [...data.split(',')]; }
    });
    onDestroy(() => {
        unsubscribe();
    });
</script>
<style>
    input:valid {
        border 1px solid #333;
    }
    input:invalid {
        border 1px solid #c71e19;
    }
    div {
        position : relative;
    }
    ul {
        position : absolute;
        top : 100%;
        list-style-type : none;
        background : white;
        display : none;
    }
    li {
        cursor : hand;
        border-bottom : 1px solid black;
    }
    ul.active {
        display : inline-block;
    }
</style>
<svelte:window on:mousedown={deactivate} />
<div>
    {#if type === "text"}
        <input on:focus={activate} type="text" bind:value={value} 
         {minLength} {maxLength} {required} />
    {:else}
        <input on:focus={activate} type="number" bind:value={value} 
         {minLength} {maxLength} {required} />
    {/if}
    <ul class:active bind:this={el}>
        {#each inputs as input }
            <li><span on:click={add}>{input}</span> <button 
             on:click={remove}>&times;</button></li>
        {/each}
    </ul>
</div>

这是相当多的代码,所以让我们分析一下我们刚才做了什么。 首先,我们将输入更改为dropdown组件。 我们也在内化这个组件的许多状态。 我们为用户打开各种字段,以便用户能够自定义字段本身。 我们需要确保设置的主字段是name。 这就是我们用来存储搜索结果的LocalStorage键。

接下来,我们订阅weather商店。 我们不使用实际的数据,但我们获得了事件,所以如果选择是唯一的,我们可以将它添加到存储中(这里可以使用集合而不是数组)。 我们添加了一些基本的逻辑,如果我们想激活下拉菜单,如果我们被聚焦,或者如果我们点击了下拉菜单之外。 我们还为列表元素的点击事件添加了一些逻辑(实际上我们将其添加到列表元素的子元素中),以便将文本放入下拉菜单中或从LocalStorage中删除。 最后,我们向组件的onMountonDestroy添加行为。 onMount将从localStorage中提取并将其添加到我们的输入列表中。 onDestroy只是删除了我们的订阅,所以我们没有内存泄漏。

除了无序列表系统中的bind:this之外,其余样式和模板看起来应该很熟悉。 这允许我们将变量绑定到元素本身。 这允许我们在元素不在事件路径中的元素列表中时禁用下拉列表。

通过这个,对WeatherInput.svelte进行以下更新:

//inside the script tag
import Dropdown from './Dropdown.svelte';

//part of the template
{#if $zipcode}
    <label>Zip<Dropdown name="zip" type="number" bind:value={zip} 
     minLength="6" maxLength="10"></Dropdown></label>
{:else}
    <label>City<Dropdown name="city" bind:value={city}></Dropdown></label>
{/if}
<label>Country Code<Dropdown name="cc" bind:value={country_code} 
 minLength="2" maxLength="2"></Dropdown></label>

我们现在已经创建了一个半可重用的dropdown组件(我们确实依赖于天气存储,所以它实际上只适用于我们的应用),并创建了一些看起来像单个组件的东西。

总结

Svelte 是一个有趣的框架,我们可以在其中将代码编译成原始 JavaScript。 它利用了诸如模块、模板和作用域样式等现代思想。 我们还能够以简单的方式创建可重用组件。 虽然我们还可以对已经构建的应用进行更多的优化,但我们可以看到它们的实际速度有多快。 虽然 Svelte 可能不会成为应用开发的主流选择,但它是一个很好的框架,可以看到我们在前几章中探索的许多概念。

接下来,我们将暂时离开浏览器,看看如何利用 Node.js 在服务器上利用 JavaScript。 我们在这里看到的许多想法将在那里得到应用。 我们还将看到编写应用的新方法,以及如何在整个网络生态系统中使用一种语言。