如何安装svelte
A few days ago, I came upon this tutorial that explains how to build the GitHub file search functionality with React. If you don’t know what that is, try going to any github repo and press ‘t’ to start a file search.
几天前,我遇到了本教程 , 该教程说明了如何使用React构建GitHub文件搜索功能。 如果您不知道这是什么,请尝试转到任何github存储库 ,然后按“ t”开始文件搜索。
This is a nice component for training, not too simple, not too complicated. Here I’ll show you how to build it with Svelte + Typescript instead of React.
这是一个很好的培训组件,不太简单,也不太复杂。 在这里,我将向您展示如何使用Svelte + Typescript而非React构建它。
Here is a live version of the app we’re going to build. Press ‘t’ to try out the file search.
这是我们将要构建的应用程序的实时版本 。 按“ t”尝试搜索文件。
项目设置 (Project Setup)
Two options :
两种选择:
If you’d like to learn how to do it manually, follow my Svelte + Typescript + TailwindCSS + SCSS how-to article.
如果您想学习手动操作方法,请阅读我的Svelte + Typescript + TailwindCSS + SCSS操作方法文章 。
If you just want to get building the component right away, follow the instructions below
如果您只是想立即构建组件,请按照以下说明进行操作
npx degit https://github.com/skflowne/svelte-ts-scss-tailwind-template github-file-search-svelte
cd github-file-search-svelte
yarn
I’m using yarn
as package manager but you can use npm
if you like
我正在使用yarn
作为包管理器,但是如果您愿意,可以使用npm
You can now run yarn dev
to run the development server
现在您可以运行yarn dev
来运行开发服务器
API数据 (API data)
We need some data to render in our component, I’m using the same data as the original React tutorial.
我们需要一些数据来渲染我们的组件,我正在使用与原始React教程相同的数据。
Download the API data file from here
Create a new api
folder within your app’s src
folder and copy the file’s content into index.js
在应用程序的src
文件夹中创建一个新的api
文件夹,并将文件内容复制到index.js
Note that everytime I tell you to create a new folder in the rest of the article, it’s going to be within thesrc
folder
请注意,每当我告诉您在本文的其余部分中创建一个新文件夹时,该文件夹都将位于src
文件夹中
This is how our data looks like :
这就是我们的数据的样子:
We can see our files have an id
, a type which can be either file
or folder
, a name
, a commit comment
and the file’s last modified_time
我们可以看到我们的文件具有id
,可以是file
或folder
, name
,提交comment
以及文件的最后modified_time
Let’s create an interface
named File
to respresent our data
让我们创建一个名为File
的interface
来表示我们的数据
In your src
folder, create an interfaces
folder and a new file file.interface.ts
在src
文件夹中,创建一个interfaces
文件夹和一个新文件file.interface.ts
We now have a custom Typescript type to represent our files.
现在,我们有一个自定义的Typescript类型来表示我们的文件。
You might be thinking, why don’t we use a string
type for our type
property ?
您可能在想,为什么我们不将string
类型用作type
属性?
This is because we only want to accept either file
or folder
as values for this property and this will allow Typescript to warn us if we use an invalid value.
这是因为我们只希望将file
或folder
作为该属性的值,并且如果我们使用无效的值,这将使Typescript发出警告。
Typescript will also provide autocompletion with the valid values which is another benefit of using Typescript.
Typescript还将提供具有有效值的自动补全功能,这是使用Typescript的另一个好处。
This provides an easy way to know what our File
data structure should look like directly in our IDE. It will help other people on our project or our future self, who might not remember as much as our present self.
这提供了一种简单的方法,可以直接在IDE中了解我们的File
数据结构。 它将帮助我们的项目或未来的自我的其他人,他们可能不记得我们现在的自我。
通过Svelte商店访问我们的文件 (Accessing our files through a Svelte store)
Now that we have our data in api/index.js
and our custom type in interfaces/file.interface.ts
, let’s create a Svelte store to use in our application.
现在,我们将数据存储在api/index.js
并将自定义类型存储在interfaces/file.interface.ts
,让我们创建一个Svelte存储以在我们的应用程序中使用。
Since this is supposed to be data coming from an API, we would never write to it, therefore we will create a readonly store with readable
from svelte/store
由于这应该是来自API的数据,因此我们永远不会对其进行写操作,因此我们将创建一个只读存储,并从svelte/store
readable
Create a new folder store
and create a new files.ts
file inside.
创建一个新的文件夹store
并在其中创建一个新的files.ts
文件。
Notice that readable
is of the form readable<T>
which allows us to specify the type of our store.
请注意, readable
格式为readable<T>
,它使我们可以指定商店的类型。
We don’t set the type on the files
variable because files
is not a File[]
, it’s a store containing a File[]
, if you hover over it in VSCode you’ll see its actual type is Readable<File[]>
, a readable store containing a File
array.
我们没有在files
变量上设置类型,因为files
不是File[]
,它是一个包含File[]
的商店,如果将鼠标悬停在VSCode上,您会看到其实际类型为Readable<File[]>
,一个包含File
数组的可读存储。
readable
takes two arguments: start
and a setter function (set)=> void
readable
有两个参数: start
和setter函数(set)=> void
Here we provide an empty array as start
and provide the setter function which then simply sets our store’s value to the data contained in our /api/index.js
file
在这里,我们提供了一个空数组作为start
并提供了setter函数,该函数随后将商店的值简单地设置为/api/index.js
文件中包含的数据
While this store isn’t very useful since we’re using static data, keep in mind you could use the setter function to make an API call or setup a subscription to a Firebase collection for example.
尽管由于我们使用的是静态数据,所以此存储不是很有用,但是请记住,您可以使用setter函数进行API调用或设置Firebase集合的订阅。
It also helps separate our concerns by having all of our data logic in the store, we don’t have to clutter App.svelte
with an import to our File
type for example.
通过将所有数据逻辑存储在商店中,它也有助于分离我们的关注点,例如,我们不必将App.svelte
杂乱地导入到File
类型。
We will do more useful things with stores when we implement the actual file search.
当我们实现实际的文件搜索时,我们将对商店做更多有用的事情。
I’ll be using stores in a “liberal” way to demonstrate how they can make our life easier. However, when building such a component, which should be able to stand alone, you should avoid having dependencies to stores in the subcomponents.
我将以“自由”的方式使用商店来展示它们如何使我们的生活更轻松。 但是,在构建这样的组件(该组件应能够独立运行)时,应避免依赖于子组件中的存储。
Ideally, everything that is in file-search
should make up one component to which you can pass whatever data it might need through props, so that if you later decide to use it in another project it wouldn’t complain that the stores don’t exist.
理想情况下, file-search
所有内容都应该组成一个组件,您可以通过prop传递它可能需要的任何数据,这样,如果您以后决定在其他项目中使用它,就不会抱怨商店没有存在。
Another option might be to make the stores a part of that component, so that you could still enjoy the benefits without having unmet dependencies.
另一个选择可能是使商店成为该组件的一部分,以便您仍然可以享受好处而不会遇到未满足的依赖关系。
But this is a discussion for another time, I won’t be bothering with it here, just keep in mind that the organisation shown here is not best for reusability.
但这是另一次讨论,在这里我不会打扰,只是要记住,这里显示的组织并不是最佳的可重用性。
构建我们的FileItem组件 (Building our FileItem component)
Let’s import our files
store in our root App.svelte
component and start building our first component FileItem.svelte
让我们将files
存储导入到我们的根App.svelte
组件中,然后开始构建我们的第一个组件FileItem.svelte
First, you can clean up App.svelte
by removing the style tag entirely as we won’t need it thanks to TailwindCSS, and you can also remove the HTML inside our <main>
tag.
首先,您可以通过完全删除样式标签来清理App.svelte
,因为TailwindCSS不需要了它,您也可以删除它,并且还可以删除<main>
标签内HTML。
Create a new components
folder, inside it create a file-search
folder to hold all of our file search related components.
创建一个新的components
文件夹,在其中创建一个file-search
文件夹,以保存我们所有与文件搜索相关的组件。
In there, create our first component FileItem.svelte
在其中创建我们的第一个组件FileItem.svelte
Notice we’re doing import type
to import our File
interface because you’ll get an error if you just do import
, this is required because the Svelte + Typescript default configuration is using importsNotUsedAsValues
is set to “error”, you can read more about this syntax here.
请注意,我们正在执行import type
以导入我们的File
接口,因为如果您只执行import
,则会出现错误,这是必需的,因为Svelte + Typescript默认配置使用的是importsNotUsedAsValues
设置为“ error”,您可以阅读有关的更多信息这种语法在这里 。
I’m not sure why the configuration is setup like this, you can override it in your tsconfig.json
by adding a new compilerOptions
object and setting it’s importsNotUsedAsValues
to either preserve
or remove
. Both seem to work fine. If anyone knows why this is setup this way, let me know in the comments please. My best guess is that a type
only import will be completely removed and therefore might avoid unnecessary imports. But I would also guess that setting the configuration to remove would have the same effect while still allowing you to use the normal import
syntax.
我不知道为什么配置设置这样,你可以覆盖在你的tsconfig.json
通过添加新compilerOptions
对象和它的设置importsNotUsedAsValues
要么preserve
或remove
。 两者似乎都工作正常。 如果有人知道为什么这样设置,请在评论中告诉我。 我最好的猜测是只导入type
将被完全删除,因此可以避免不必要的导入。 但是我也猜想,将配置设置为remove将具有相同的效果,同时仍然允许您使用常规的import
语法。
However, since I don’t know why this is setup this way, I’ll leave the configuration as it is.
但是,由于我不知道为什么要这样设置,因此我将保持原样。
Also notice thelang="ts"
attribute set on our script tag, this is how we tell the compiler to treat our code as Typescript, you’ll get an error on any Typescript specific syntax if you forget it.
还要注意在script标记上设置的lang="ts"
属性,这是我们告诉编译器将代码视为Typescript的方式,如果您忘记了任何特定于Typescript的语法,就会出错。
We’re also defining a component prop of type File
with the export
keyword. This is how you tell Svelte to expect an external prop.
我们还使用export
关键字定义File
类型的组件prop。 这是您告诉Svelte期望使用外部道具的方式。
export let myProp: Type
Update our App.svelte
to display the first file
更新我们的App.svelte
以显示第一个文件
We import our FileItem.svelte
component and our files
store
我们导入FileItem.svelte
组件和files
存储
Then we render a FileItem
to which we pass our first file by using $files
to access the value contained in the store.
然后,我们呈现一个FileItem
,使用$files
访问存储中包含的值,将第一个文件传递给该FileItem
。
Adding the special $
in front of a store tells Svelte that we want to subscribe to the store and access the value inside.
在商店前面添加特殊的$
告诉Svelte我们要订阅商店并访问其中的值。
Without the $
in front, you’re accessing the store itself, with the $
you’re telling Svelte to setup an automatic subscription to that store and get back the actual value instead.
如果没有$
放在前面,您将访问商店本身,而$
告诉Svelte设置对该商店的自动订阅并取回实际值。
This is equivalent to doing
这等效于
Keep in mind that the code above is a simplification, normally you should also manually unsubscribe when your component unmounts.
请记住,上面的代码只是一种简化,通常在卸载组件时也应手动取消订阅。
Let’s display the rest of our file’s informations, display either a file icon or a folder icon depending on our File
‘s type
and style our component with TailwindCSS.
让我们显示文件的其余信息,根据File
的type
显示文件图标或文件夹图标,并使用TailwindCSS设置组件样式。
We’ll be using svelte-awesome
to display fontawesome icons and date-fns
to display our modified_time
as a distance such as “a month ago”.
我们将使用svelte-awesome
,以显示fontawesome图标和date-fns
显示我们modified_time
的距离,如“一个月前”。
In the original tutorial, they use moment
, I’m using date-fns
because it is a much lighter package and it has similar features.
在原始教程中,他们使用moment
,而我使用的是date-fns
因为它是一个轻巧得多的软件包,并且具有类似的功能。
At the end, we’ll do a quick comparison of bundle size to the React + Moment.js version.
最后,我们将对捆绑包大小与React + Moment.js版本进行快速比较。
yarn add -D svelte-awesome @fortawesome/free-regular-svg-icons date-fns
Notice we use -D
because Svelte is a compiler, it will take care of including the code it needs from the packages in our bundle and we don’t need them as dependencies
注意,我们使用-D
是因为Svelte是编译器,它将负责从捆绑包中包含所需的代码,而我们不需要将它们作为dependencies
Update our component to look like this
更新我们的组件,使其看起来像这样
We use TailwindCSS’s classes to style everything without the need to write any CSS. If you’d like to know what those classes do head over to TailwindCSS’s docs.
我们使用TailwindCSS的类来设置所有样式的样式,而无需编写任何CSS。 如果您想知道这些类的作用,请访问TailwindCSS的文档。
We import Icon
from svelte-awesome
which will allow us to display fontawesome icons provided by the @fortawesome/free-regular-svg-icons
by passing them into the data
attribute.
我们从svelte-awesome
导入Icon
,这将允许我们将@fortawesome/free-regular-svg-icons
提供的@fortawesome/free-regular-svg-icons
传递到data
属性中。
You’ll get an error in IDE on the data
attribute because svelte-awesome
doesn’t have Typescript types definitions but it will still work.
您会在IDE的data
属性上遇到错误,因为svelte-awesome
没有Typescript类型定义,但仍然可以使用。
const distance = formatDistanceToNow(parseJSON(file.modified_time))
Finally, we calculate the distance of time between now and our modified_time
by first converting our JSON date with parseJSON
and passing the resulting Date
to formatDistanceFromNow
, both methods provided by date-fns
最后,我们计算现在和我们之间的时间距离modified_time
首先将我们的JSON日期parseJSON
以及将得到的Date
,以formatDistanceFromNow
,提供两种方法date-fns
创建FileList组件 (Creating the FileList component)
Create a new file called FileList.svelte
inside the our components/file-search
在我们的components/file-search
内创建一个名为FileList.svelte
的新文件
We definie afiles
prop of type File[]
我们定义File[]
类型的files
道具
When this array is not empty, we loop over the files with an {#each as file (file.id)}
block which allows us to render a FileItem
component to which we pass our file as a prop. {file}
is a shorthand for file={file}
.
当此数组不为空时,我们使用{#each as file (file.id)}
块{#each as file (file.id)}
,这使我们可以呈现FileItem
组件,并将文件作为道具传递给该组件。 {file}
是file={file}
的简写。
(file.id)
takes care of providing a unique key for our component, in the same way that you would pass key={file.id}
if you were using React or key="file.id"
with Vue.js
(file.id)
负责为我们的组件提供唯一的密钥,就像在key={file.id}
中使用React或key="file.id"
时,您将传递key={file.id}
When our array is empty we display a “No matching files” message instead.
如果数组为空,则会显示“无匹配文件”消息。
Modifiy App.svelte
to display the FileList
component
修改App.svelte
以显示FileList
组件
使用Svelte商店实现文件搜索 (Implementing the file search using Svelte stores)
We’ll use a store to keep track of what our user enters in the search input and derive the search results from both the user’s input and our files data.
我们将使用商店来跟踪用户在搜索输入中输入的内容,并从用户输入和文件数据中得出搜索结果。
Create a fileSearch.ts
file in our store
folder
在我们的store
文件夹中创建一个fileSearch.ts
文件
We simply create a writable store, specify it will hold a string
, which will be our user’s input and initialize it to an empty string.
我们只需创建一个可写存储,指定它将包含一个string
,这将是我们用户的输入,并将其初始化为一个空字符串。
Create another file namedfileSearchResults.ts
in our store
folder for our search results.
在store
文件夹中为搜索结果创建另一个名为fileSearchResults.ts
文件。
We use a derived store to recalculate our search results every time either our fileSearch
store or our files
store updates.
每当我们的fileSearch
存储或files
存储更新时,我们都使用派生存储重新计算搜索结果。
derived
takes in a store or an array of stores and a callback function.
derived
需要一个商店或一系列商店以及一个回调函数。
The callback function will contain an array of our stores values and run everytime one of them changes.
回调函数将包含一个存储值数组,并在其中任何一个更改时运行。
In this case our files never change since we’re reading static data from a file, and therefore, the callback will be called everytime our fileSearch
‘s store gets updated.
在这种情况下,由于我们是从文件中读取静态数据,因此文件永远不会更改,因此,每次我们的fileSearch
的存储更新时,都会调用该回调。
Now, imagine that your files weren’t static and the app allowed users to add new files. If you were using Firebase to setup a subscription to a files
collection, whenever another user would add a new file, this would trigger the subscription, update our files
store and also rerun our callback and update our search results for the current user. This would allow our app’s state to always stay consistent even when other users make changes, without needing a page refresh.
现在,假设您的文件不是静态的,并且该应用程序允许用户添加新文件。 如果您使用Firebase设置对files
集合的订阅,则每当另一个用户添加新文件时,这将触发订阅,更新files
存储并重新运行回调并更新当前用户的搜索结果。 这样,即使其他用户进行了更改,我们的应用程序状态也始终保持一致,而无需刷新页面。
In file search mode, we want to display only files, this is done by first always filtering our files
array and only returning files with type
“file”.
在文件搜索模式下,我们只想显示文件,这是通过首先始终过滤files
数组并仅返回“文件” type
文件来完成的。
We check if our search is empty, in that case we want to return everything, otherwise, we want to return only the matching results.
我们检查搜索是否为空,在这种情况下,我们想返回所有内容,否则,我们只想返回匹配的结果。
We take care to make both the file’s name and our search input lowercase to allow for case insensitive search. Otherwise we could be searching for readme
and the file named README.md
would not match. We then return the file only if its name
includes our search
.
我们注意使文件名和搜索输入都小写,以允许不区分大小写的搜索。 否则,我们可能正在搜索readme
文件,并且名为README.md
的文件将不匹配。 然后,仅当文件name
包含我们的search
我们才返回文件。
FileSearch组件 (The FileSearch component)
Create a FileSearch
component inside components/file-search
在components/file-search
内创建一个FileSearch
组件
We import our fileSearch
store and setup a two-way binding on our input’s value with bind:value={$fileSearch}
.
我们导入fileSearch
存储,并使用bind:value={$fileSearch}
在输入值上设置双向绑定。
This way, our input’s value will always be set to our store’s value, and whenver the user types in, our store’s value will be automatically updated.
这样,我们的输入值将始终设置为商店的值,并且只要用户输入,我们的商店的值就会自动更新。
Again we bind on $fileSearch
and not on fileSearch
as we want to bind to the store’s value, not the store itself.
再次,我们绑定到$fileSearch
而不绑定到fileSearch
因为我们想绑定到商店的值,而不是商店本身。
In order for our input element to be focused immediately when the component appears, we first create an input
variable to hold a ref to our input element and we use bind:this={input}
on our input to bind it to our variable. You can think of it as “bind this element to theinput
variable”.
为了使输入元素在组件出现时立即集中,我们首先创建一个input
变量以保存对输入元素的引用,然后在输入上使用bind:this={input}
将其绑定到变量。 您可以将其视为“将此元素绑定到input
变量”。
We use Svelte’s reactive statements to focus our input whenever input
changes and exists. This will happen once our input element has actually rendered.
只要input
发生更改并存在,我们就使用Svelte的React式语句来集中输入。 一旦我们的输入元素实际呈现,就会发生这种情况。
Svelte’s reactive statements will update when a variable on the right side of the $:
changes.
当$:
右侧的变量更改时,Svelte的React式语句将更新。
$: if(input){
// ...runs when input changes and exists
}
When we enter search mode, our component will render, set input to the actualHTMLElement
in the DOM that now exists, this will trigger our focus code.
当我们进入搜索模式时,我们的组件将呈现,将输入设置为现在存在的DOM中的实际HTMLElement
,这将触发我们的焦点代码。
调试失败错误 (The debug failure error)
Sometimes, Rollup won’t be able to resolve your newly created files and error out :
有时,汇总将无法解析您新创建的文件并出错:
Debug Failure. False expression: Expected fileName to be present in command line
If you get this, just stop your development server and restart it with yarn dev
如果得到了,只需停止开发服务器,然后使用yarn dev
重新启动它即可
Also if some changes don’t seem to take effect in your browser, you might need to save the file you were working on again or save App.svelte
again.
另外,如果某些更改似乎无法在浏览器中生效,则可能需要再次保存正在处理的文件或再次保存App.svelte
。
This happened to me a few times, not sure what’s causing it.
这在我身上发生过几次,不确定是什么原因造成的。
设置键盘事件监听器以触发文件搜索模式 (Setting up keyboard event listeners to trigger the file search mode)
We will enter the file search mode when the user presses thet
key and allow them to exit when they press theEscape
key.
当用户按t
键时,我们将进入文件搜索模式,并在按Escape
键时允许他们退出。
Modifiy App.svelte
修改应用App.svelte
We use the onMount
lifecycle method, which we be called once when our component first mounts.
我们使用onMount
生命周期方法,该方法在组件首次装入时被调用一次。
We setup our event listener on the keyup
event and pass it our event handler which will be in charge of checking which key has been pressed and set the search mode status accordingly.
我们在keyup
事件上设置事件侦听器,并将其传递给事件处理程序,该事件处理程序将负责检查按下了哪个键并相应地设置搜索模式状态。
If it’s t
or T
we go in search mode by setting searching
to true
, if it was Escape
we set it to false
to exit search mode.
如果它的t
还是T
,我们在搜索模式去设置searching
到true
,如果它是Escape
,我们将它设置为false
退出搜索模式。
We then update the template to render our file search component only when we’re searching
with {#if searching}
然后,我们仅searching
使用{#if searching}
search {#if searching}
searching
,才更新模板以呈现文件搜索组件。
onMount
can also return a cleanup function, in this case we use it to make sure that when our component is unmounted, we also remove our event listener.
onMount
还可以返回清除函数,在这种情况下,我们将使用它来确保卸载组件时也删除事件监听器。
In this case, App
would never get unmounted unless we close the tab.
在这种情况下,除非关闭选项卡,否则App
永远不会卸载。
Now, imagine if the component that uses onMount
to register the event listener was part of a list where elements could change.
现在,假设使用onMount
来注册事件侦听器的组件是否是元素可以更改的列表的一部分。
When the list updates, the components that it renders will change, this means that new components will get mounted and those that are not needed anymore will get unmounted.
当列表更新时,它呈现的组件将更改,这意味着将挂载新组件,而不再需要的组件将被挂载。
Every time, new event listeners will get registered on mount and if you did cleanup, the old event listeners will be removed when the component is unmounted.
每次,新的事件侦听器都会在安装时注册,如果您进行了清理,则在卸载组件时,旧的事件侦听器也会被删除。
If you didn’t cleanup your event listeners however, new event listeners will be added everytime a new component is mounted, without removing the old ones which are now unnecessary.
但是,如果您不清理事件侦听器,则每次安装新组件时都会添加新的事件侦听器,而不会删除现在不再需要的旧组件。
This would create a memory leak because everytime we’ll get a keyup
event, all the registered listeners will run, even if the components that registered them are no longer present.
这将导致内存泄漏,因为每次我们遇到keyup
事件时,所有注册的侦听器都将运行,即使注册它们的组件不再存在。
From there, it’s only a matter of time for your app’s performance to suffer severely. So don’t forget to cleanup!
从那里开始,您的应用程序性能受到严重影响只是时间问题。 所以不要忘记清理!
显示搜索结果 (Displaying search results)
Right now, we’re always displaying our full files list no matter what. We need to change this so that when we’re searching we display our fileSearchResults
and otherwise display the full list.
现在,无论如何,我们始终会显示完整的文件列表。 我们需要更改它,以便在搜索时显示文件搜索fileSearchResults
,否则显示完整列表。
Since our search results are already derived from the search input through our fileSearchResults
store, we can simply swap the full list for the value of the fileSearchResults
store when we are in file search mode.
由于我们的搜索结果已经从通过fileSearchResults
存储库进行的搜索输入中得出,因此当我们处于文件搜索模式时,我们可以简单地将完整列表交换为fileSearchResults
存储库的值。
Now the search will work!
现在搜索将起作用!
关于组件卸载时的清理 (About clean up when a component unmounts)
This part is only for demonstration if you’re curious about what happens when you don’t cleanup. Remove the code below after you’ve tried it.
如果您对不清理时会发生的事情感到好奇,那么此部分仅用于演示。 试用后,请删除下面的代码。
Add this code to FileItem.svelte
to see the behavior of not cleaning up event listeners in action
将此代码添加到FileItem.svelte
以查看未清除事件监听器的行为
Open the devtools console and start the search with t
打开devtools控制台并使用t
开始搜索
Type something in the search and erase it to make our list update and mount/unmount components.
在搜索中输入内容并将其删除,以使我们的列表更新并安装/卸载组件。
Repeat this a few times, then clear your console and press a key.
重复几次,然后清除控制台并按一个键。
You’ll see one keyup event can now trigger more callback calls than we have components. This quickly gets to a point where one keyup
event triggers hundreds of callback calls even though we never have more than 13 FileItem
components.
您会看到一个keyup事件现在可以触发比我们拥有的组件更多的回调调用。 即使我们从来没有超过13个FileItem
组件,这很快就会达到一个keyup
事件触发数百个回调调用的地步。
Now add the cleanup code, and retry the above process
现在添加清理代码,然后重试以上过程
No more useless calls. Keep in mind, event listeners aren’t the only thing you should cleanup, a store subscription could be another example.
没有更多无用的电话了。 请记住,事件监听器不是您应该清除的唯一内容,商店订阅可能是另一个示例。
What’s nice about Svelte’s $
syntax for stores is it also handles unsubscribing from stores automatically.
Svelte为商店提供的$
语法的好处是,它还能自动处理从商店退订。
添加搜索词突出显示 (Adding search term highlighting)
We can modify fileSearchResults
to return the file names with HTML markup around the matched search term.
我们可以修改fileSearchResults
以返回带有匹配搜索词周围HTML标记的文件名。
We do this by wrapping the search term between <mark></mark>
tags
为此,我们将搜索字词包裹在<mark></mark>
标签之间
First, when our search is not empty, we create Regexp
that will match our $fileSearch
term. We pass gi
as second argument to the Regexp
, those are flags that will make our Regexp
match globally g
, and case-insensitive i
首先,当我们的搜索不为空时,我们将创建与$fileSearch
术语匹配的Regexp
。 我们将gi
作为第二个参数传递给Regexp
,这些标志将使我们的Regexp
全局匹配g
,并且不区分大小写i
After our filter which will return our search results, wemap
over each result, and return our original file but except with a new name where our search term will now be wrapped with between <mark></mark>
tags.
在将返回搜索结果的过滤器之后,我们会map
每个结果,并返回原始文件,但使用新名称除外,在新名称中搜索词现在将用<mark></mark>
标签包裹。
For example, searching for “readme” will replace the file name README.md
with <mark>README</mark>.md
例如,搜索“自述文件”会将文件名README.md
替换为<mark>README</mark>.md
If you try this now, you’ll see that there is no highlighting and just a bunch of <mark> tags inside our file names.
如果立即尝试,您会发现文件名中没有突出显示,只有一堆<mark>标签。
This is because Svelte, like most frontend frameworks, will escape HTML markup by default.
这是因为Svelte与大多数前端框架一样,默认情况下将转义HTML标记。
We need to modify our FileItem
component to render our file.name
as HTML, since it now possibly contains HTML markup.
我们需要修改FileItem
组件以将file.name
呈现为HTML,因为它现在可能包含HTML标记。
In Svelte, this is done with the @html
directive
在Svelte中,这是通过@html
指令完成的
Now we have highlighting because Svelte knows it needs to render file.name
as HTML.
现在我们重点介绍一下,因为Svelte知道需要将file.name
呈现为HTML。
添加键盘导航 (Adding keyboard navigation)
First, let’s add a nice message that will display the available keyboard controls once we’ve entered search mode.
首先,让我们添加一条不错的消息,该消息将在我们进入搜索模式后显示可用的键盘控件。
Create InfoMessage.svelte
inside components/file-search
在components/file-search
内部创建InfoMessage.svelte
Import it in App.svelte
so it displays on top of our search
将其导入App.svelte
,使其显示在搜索结果的顶部
Let’s implement navigation with the arrow keys, we’ll keep track of the currently selected file index with a store.
让我们使用箭头键实现导航,我们将通过商店来跟踪当前选定的文件索引。
First create a new store fileIndex.ts
首先创建一个新的商店fileIndex.ts
Simple writable store to hold our index as a number
, we initialize it to 0, so it points to the first file of the list.
简单的可写存储将索引保存为number
,我们将其初始化为0,因此它指向列表的第一个文件。
Now add the listeners for the arrow keys in App.svelte
现在在App.svelte
为箭头键添加侦听App.svelte
We import our fileIndex
store so we can update it in our listeners.
我们导入fileIndex
存储,以便可以在侦听器中对其进行更新。
We added the handleKeyDown
listener and registered it on the document on the keydown
event.
我们添加了handleKeyDown
侦听器,并在keydown
事件上将其注册在文档中。
We’re not using keyup
so that you don’t have to release the key to keep moving. keydown
will fire as long as the key is pressed.
我们不使用keyup
,这样你就不必松开按键,继续前进。 keydown
只要按下键就会触发。
When we get our keydown
event, handleKeyDown
checks whether the key pressed was ArrowUp
or ArrowDown
and updates the fileIndex
store accordingly.
当我们得到keydown
事件时, handleKeyDown
检查按下的键是ArrowUp
还是ArrowDown
并相应地更新fileIndex
存储。
On ArrowDown
, we increment our index and use the modulo %
operator so that when we reach the length of our search results we reset our index to 0, so when we reach the last file and press down, we go back to the top of the list.
在ArrowDown
,我们增加索引并使用%
模运算符,以便在达到搜索结果的长度时,将索引重置为0,因此,当到达最后一个文件并按下时,我们返回到顶部清单。
We do the same thing for ArrowUp
by checking if we get a negative value and then resetting our index to be the last file in our results.
我们通过检查我们是否得到负值,然后将索引重置为结果中的最后一个文件,对ArrowUp
做同样的事情。
This way the navigation will loop seamlessly and we won’t index out of bound errors.
这样,导航将无缝循环,并且我们不会索引超出范围的错误。
Note that we now also pass down searching
as a prop to FileList
because we only want to navigate between files when we’re in search mode.
请注意,我们现在还将向下searching
作为对FileList
的支持,因为我们只希望在处于搜索模式时在文件之间导航。
Let’s modify our FileList
to accept the searching
prop and pass an active
prop down to our FileItem
component.
让我们修改FileList
以接受searching
道具并将active
道具向下传递到FileItem
组件。
We import our fileIndex
store so we can read it’s value and add an index
parameter to our {#each file as file, index (file.id)}
block.
我们导入fileIndex
存储,以便我们可以读取它的值,并将index
参数添加到{#each file as file, index (file.id)}
块。
We pass a new active
prop to FileItem
that is true
when our fileIndex
equals the current index
and we are in search mode, given by the new searching
prop.
我们将一个新的active
道具传递给FileItem
,当我们的fileIndex
等于当前index
并且我们处于搜索模式时(由新的searching
道具给出),它为true
。
Now let’s use that prop to apply a light blue background to our selected FileItem
现在让我们使用该道具将浅蓝色背景应用于我们选择的FileItem
We added active
as a prop and use the class:className={condition}
syntax to apply a conditional class, this class will only be added when condition
is true.
我们添加了active
作为道具,并使用class:className={condition}
语法来应用条件类,只有在condition
为true时才添加此类。
In this case, this looks like this class:bg-blue-200={active}
在这种情况下, class:bg-blue-200={active}
类似于class:bg-blue-200={active}
Now the arrow keys navigation works!
现在,箭头键导航有效!
There’s just one small thing, if you search for something, then navigate, then change the search, our index won’t reset to 0.
只有一件小事,如果您搜索某事,然后浏览,然后更改搜索,我们的索引将不会重置为0。
To fix that, simply modify our fileSearchResults
store to reset the fileIndex
whenever the search changes
要解决此问题,只需修改我们的fileSearchResults
存储以在搜索更改时重置fileIndex
We’ve imported our fileIndex
store and added fileIndex.set(0)
since this will run everytime either $fileSearch
or $files
changes.
我们已经导入了fileIndex
存储并添加了fileIndex.set(0)
因为这将在$fileSearch
或$files
每次更改时运行。
更新postcss配置,使我们的图标不会在生产中消失 (Updating the postcss configuration so our icons don’t disappear in production)
If you build for production right now, you’ll notice that when you access the app, icons won’t appear anymore.
如果您现在为生产而构建,则会注意到,当您访问该应用程序时,将不再显示图标。
This is because we’re using purgecss
to remove unused classes from our CSS bundle.
这是因为我们正在使用purgecss
从CSS捆绑包中删除未使用的类。
However, svelte-awesome
generates an fa-icon
class that gets removed by purgecss
in our production bundle because it doesn’t appear in our code.
但是, svelte-awesome
生成一个fa-icon
类,该类将被我们的产品包中的purgecss
删除,因为它未出现在我们的代码中。
We need to whitelist this class so it doesn’t get removed from our CSS bundle
我们需要将此类列入白名单,以便不会将其从CSS包中删除
In postcss.config.js
, you’ll see a whitelistPatterns
array that only contains /svelte/
, you need to add /fa-icon/
in there as well.
在postcss.config.js
,您将看到仅包含/svelte/
的whitelistPatterns
数组,还需要在其中添加/fa-icon/
。
使用React + Moment.js版本比较包的大小和加载时间 (Comparing bundle size and loading times with the React + Moment.js version)
I want to emphasize that this comparison might not say much about the difference between Svelte and React.
我想强调的是,这种比较可能不会说太多Svelte和React之间的区别。
This is because we’re using date-fns
which is about 3 times lighter gzipped than moment
. So it’s unclear how much is due to this difference and how much is due to the Svelte/React difference.
这是因为我们使用的date-fns
压缩的时间比moment
轻约3倍。 因此,尚不清楚该差异是多少,而Svelte / React差异是多少。
I’ve made a few modifications to our app, since we’re not using the global.css
file, I deleted it and removed the import in public.html
我对我们的应用做了一些修改,因为我们没有使用global.css
文件,所以我删除了它,并删除了public.html
的导入。
I’ve aadded a Header
component that simply displays “Github File Search” to match the React app more exactly.
我添加了一个Header
组件,该组件仅显示“ Github File Search”以更准确地匹配React应用。
I’ll be doing this with the devtools, network tab with “disable cache” checked and incognito mode to ensure extensions are not impacting anything.
我将使用devtools,选中“禁用缓存”的网络标签和隐身模式来执行此操作,以确保扩展不会影响任何内容。
Let’s start with bundle size
让我们从捆绑包大小开始
The React version is shipping two javascript files, a main javascript file (1.9 kB) and a chunk (58.7 kB) and one main CSS file (878 B) and HTML file (1.1 kB) for a total of ~62.5 kB
React版本提供了两个javascript文件,一个主要的javascript文件(1.9 kB)和一个块(58.7 kB),一个主要CSS文件(878 B)和HTML文件(1.1 kB),总计〜62.5 kB
The Svelte version is shipping one file for javascript (12.3 kB) and one for CSS (1.3 kB) and a HTML file (343 B), for a total of ~14 kB
Svelte版本提供了一个用于javascript(12.3 kB)的文件,一个用于CSS(1.3 kB)和HTML文件(343 B)的文件,总计约14 kB
So our Svelte app ships 48.5 kB less and is 77,6% lighter in total
因此,我们的Svelte应用程序的出货量减少了48.5 kB,总重量减轻了77.6%
Let’s see how it affects loading times now
让我们看看它如何影响加载时间
I’ve taken 10 measures for each app through the network tab’s load value, I’ve also removed outliers as those happened when the hosting platform would take more time to respond then usual.
我已通过“网络”标签的负载值为每个应用程序采取了10种措施,还删除了离群值,因为当托管平台需要更多时间来响应通常的情况时,就会发生异常。
This would mean the React app usually loaded in between 100 to 200ms and the Svelte app in between 50 to 60ms.
这意味着React应用通常在100到200毫秒之间加载,而Svelte应用通常在50到60毫秒之间加载。
Average for the React app was 167ms
React应用的平均值为167ms
Average for the Svelte app was 55ms
Svelte应用的平均值为55毫秒
So the Svelte/date-fns app loads about 3 times faster than the React/moment one.
因此,Svelte / date-fns应用程序的加载速度比React / moment加载速度快3倍。
Those are pretty good results, but it’d be more interesting to compare this in a larger app with the same third party libraries.
这些都是相当不错的结果,但是将它与具有相同第三方库的更大应用程序进行比较会更有趣。
结论 (Conclusion)
You can find the completed tutorial code in this github repo
您可以在此github存储库中找到完整的教程代码
如何安装svelte