理解 Svelte 中的上下文(从 React 上下文进行转换)

14
我有一个使用ContextAPI来管理身份验证的React应用程序,现在我正在尝试在Svelte中实现类似的功能。
在Authenticate.js中,我有以下代码:
import React, { useContext, useState, useEffect } from "react"
import { auth } from "../firebase"

const AuthCt = React.createContext()

export function Auth() {
  return useContext(AuthCt)
}

export function AuthComp({ children }) {
  const [currentUser, setCurrentUser] = useState()
  const [loading, setLoading] = useState(true)

  function login(email, password) {
    return auth.signInWithEmailAndPassword(email, password)
  }

  function logout() {
    return auth.signOut()
  }

  useEffect(() => {
    const unmount = auth.onAuthStateChanged(user => {
      setCurrentUser(user)
      setLoading(false)
    })

    return unmount
  }, [])

  const value = {
    currentUser,
    login,
    signup
  }

  return (
    <AuthCt.Provider value={value}>
      {!loading && children}
    </AuthCt.Provider>
  )
}

这个上下文在其他Login.js组件中像这样使用:

import { Auth } from "./Authenticate"

const Login = () => {
  const { currentUser, login } = Auth()

而在 App.js 中我有:

import { AuthComp } from "./Authenticate";

function App() {
  return (
          <AuthComp>
               <div> All others go here </div>
          </AuthComp>
  );
}

我该如何在Svelte中实现这个,特别是Authenticate上下文?

由于不知道如何继续,我在Svelte中还没有做太多事情。到目前为止,我有一个AuthComp.svelte文件。我不确定我是否做对了。

<script>
    import { getContext, setContext } from 'svelte';
    import  { auth } from '../firebase';
    import { writable } from 'svelte/store';

    let Auth = getContext('AuthCt')
    setContext('Auth', Auth)

    let currentUser;
    let loading = true;

    
     const unmount = auth.onAuthStateChanged(user => {
        currentUser = user;
        loading = false
     });


    function login(email, password) {
        return auth.signInWithEmailandPassWord(email,password)
    }
    
    function logout() {
       return auth.signOut()
    }
    
    const value = { currentUser, login, signUp }
    
</script>

<slot value={value}></slot>
2个回答

78

从React Context迁移到Svelte

Svelte和React中的上下文可能看起来相似,但它们实际上使用方式不同。因为在核心上,Svelte的上下文要更加有限。但这没关系。实际上,这将使您的代码更简单易懂。

在Svelte中,您拥有更多的工具来传递数据(并保持同步)到应用程序中,而不仅仅是上下文。每个工具基本上只做一件事(使所有内容可预测),而且它们做得很好。其中包括:

  • 上下文
  • 存储
  • 属性

作为最近从React切换到Svelte的人,我认为我可以帮助解释它们之间的一些区别,并帮助您避免一些我所犯的概念错误。我还将介绍一些生命周期方法的差异,因为如果您曾经使用过useEffect,那么由于Svelte没有等效的API,您可能会感到非常迷惑。但在Svelte中将所有内容组合在一起将使一切变得简单。

上下文

Svelte中的上下文只做一件事:将数据从父组件传递到任何子组件(不一定是直接的子组件)。与React不同,上下文并非是响应式的。它在组件挂载时设置一次,然后不会再次更新。我们将在下面介绍“响应式上下文”。

<!-- parent.svelte -->

<script>
  import { setContext } from 'svelte'

  setContext('myContext', true)
</script>

<!-- child.svelte -->

<script>
  import { getContext } from 'svelte'

  const myContext = getContext('myContext')
</script>

注意,上下文涉及两个要素,即键和值。上下文设置为特定的键,然后可以使用该键检索值。与React不同,您不需要导出函数以检索上下文。上下文的键和值都可以是任何东西。如果您可以将其保存到变量中,则可以将其设置为上下文。您甚至可以将对象用作键!

存储

如果您有需要在应用程序中多个位置保持同步的数据,则存储是正确的选择。存储是反应性的,这意味着它们可以在创建后进行更新。与React或Svelte中的上下文不同,存储不仅仅是将数据传递给其子代。您的应用程序的任何部分都可以创建存储,并且应用程序的任何部分都可以读取存储。您甚至可以在单独的JavaScript文件中的Svelte组件之外创建存储。

// mystore.ts
import { writable } from 'svelte/store'

// 0 is the initial value
const writableStore = writable(0)

// set the new value to 1
writableStore.set(1)

// use `update` to set a new value based on the previous value
writableStore.update((oldValue) => oldValue + 1)

export { writableStore }

然后在组件内部,您可以订阅store。

<script>
  import { writableStore } from './mystore'

</script>

{$writableStore}

美元符号订阅了该存储。现在,每当存储更新时,组件将自动重新呈现。

使用context与存储

既然我们有了存储和上下文,我们可以创建“反应性上下文”(这是我刚刚想出来的一个术语,但它可行)。存储非常好,因为它们是反应性的,而上下文很适合向子组件传递数据。但事实上,我们可以通过上下文传递存储。这使得上下文具有反应性,并且存储被限定范围。

<!-- parent.svelte -->

<script>
  import { setContext } from 'svelte'
  import { writable } from 'svelte/store'

  const writableStore = writable(0)
  setContext('myContext', writableStore)
</script>

<!-- child.svelte -->

<script>
  import { getContext } from 'svelte'

  const myContext = getContext('myContext')
</script>

{$myContext}

现在,每当父组件中的store更新时,子组件也会更新。当然,store可以做得比这更多,但如果您想要复制React context,这是在Svelte中最接近的方式。而且它也少了很多样板代码!

Svelte没有与useEffect等效的功能。相反,Svelte有响应式语句。文档/教程中有很多关于这些内容的信息,所以我将保持简短。

// doubled will always be twice of single. If single updates, doubled will run again.
$: doubled = single * 2

// equivalent to this

let single = 0
const [doubled, setDoubled] = useState(single * 2)

useEffect(() => {
  setDoubled(single * 2)
}, [single])

Svelte智能地找出依赖关系,并仅在需要时运行每个响应式语句。如果您创建了依赖循环,编译器会警告您。

这意味着您可以使用响应式语句更新存储(从而更新上下文)。在这里,valueStore将在每次按键输入时更新。由于此存储通过上下文传递,任何子元素都可以获取当前输入的值。

<script>
  import { setContext } from 'svelte'
  import { writable } from 'svelte/store'

  // this value is bound to the input's value. When the user types, this variable will always update
  let value

  const valueStore = writable(value)

  setContext('inputContext', valueStore)

  $: valueStore.set(value)

</script>

<input type='text' bind:value />

属性 (Props)

在大部分情况下,在React和Svelte中,属性的功能都是完全相同的。但也有一些区别,因为Svelte的属性可以利用双向绑定(不是必需的,但是有可能)。然而这是一个不同的话题,而且教程非常好地讲解了如何使用带有属性的双向绑定。

Svelte中的验证

现在,在了解所有这些之后,让我们看一下如何创建一个身份验证包装器组件。

  • 创建一个认证存储 (auth store)
  • 通过上下文传递认证存储
  • 使用Firebase的onAuthStateChanged监听认证状态的变化
  • 在子组件中订阅认证存储
  • 当父组件被销毁时取消订阅onAuthStateChanged以防止内存泄漏
<!-- parent.svelte -->
<script>
  import { writable } from 'svelte/store'
  import { onDestroy, setContext } from 'svelte'

  import { auth } from '../firebase'

  const userStore = writable(null)

  const firebaseUnsubscribe = auth.onAuthStateChanged((user) => {
    userStore.set(user)
  })

  const login = (email, password) => auth.signInWithEmailandPassWord(email,password)

  const logout = () => auth.signOut()

  setContext('authContext', { user: userStore, login, logout })

  onDestroy(() => firebaseUnsubscribe())

</script>

<slot />

<!-- child.svelte -->
<script>
  import { getContext } from 'svelte'

  const { login, logout, user } = getContext('authContext')
</script>

{$user?.displayName}

12
非常好的回答! - Kay
2
除非您正在进行服务器端渲染,否则我认为不会有显着差异。在onMount之外调用它将更早地运行它,因为onMount等待组件出现在屏幕上。在服务器端渲染中,我一直在检查typeof window!== 'undefined'(因为onAuthStateChanged需要在浏览器中运行),而不是大多数情况下等待onMount。不确定这是否是最好的方法,但对我而言已经起作用了。 - Nick
2
保持存储范围的目的是什么?如果您可以将存储导入父级和子级,则不需要上下文。这样做是否有优势,还是这是一种反模式?顺便说一句,这种比较对我帮助很大。 - Ben Sewards
3
你说得对,使用 stores 不需要上下文也是可以的。这并不是一种反模式。当以这种方式使用 store 时,需要注意的主要事项是您需要在单独的文件中初始化它——您无法从组件内部导出 stores(据我所知)。如果想要限定 store 的范围或者不想处理上下文的连接,则使用上下文与 stores 是很好的选择。一个例子是 <List> 和 <ListItem> 组件。ListItems 应该始终位于列表中,并且可能有多个列表。在这种情况下,限定上下文的范围是惯用的。这样有帮助吗? - Nick
3
天啊!你真的做得很好! - K-Sato
显示剩余2条评论

1
在Svelte中,父组件使用setContext(key, value)设置上下文,子组件可以使用getContext(key)访问value对象。详见文档
在您的情况下,上下文将如下使用:
<script>
    import { getContext, setContext } from 'svelte';
    import  { auth } from '../firebase';
    import { writable } from 'svelte/store';

    // you can initialize this to something else if you want
    let currentUser = writable(null)
    let loading = true
    
    // maybe you're looking for `onMount` or `onDestroy`?
    const unmount = auth.onAuthStateChanged(user => {
        currentUser.set(user)
        loading = false
    });


    function login(email, password) {
        return auth.signInWithEmailandPassWord(email,password)
    }
    
    function logout() {
       return auth.signOut()
    }
    
    const value = { currentUser, login, signUp }

    setContext('Auth', value) 
    
</script>

{#if !loading}
    <slot></slot>
{/if}

在这里,currentUserloginsignup(不确定它来自哪里?)被设置为上下文,并使用setContext()。要使用此上下文,您可能会有类似以下的内容:
<!-- App -->
<AuthComp>
    <!-- Some content here -->
    <Component />
</AuthComp>

<!-- Component.svelte -->
<script>
    import { getContext } from 'svelte'

    const { currentUser, login, signup } = getContext('Auth')
    // you can subscribe to currentUser with $currentUser
</script>
<div>some content</div>

正如文档中所述,上下文是非反应性的,因此currentUser首先转换为 store,以便在子级中订阅它。至于useEffect,Svelte有生命周期函数,您可以使用它们在不同的点运行代码,例如onMountonDestroy
如果您是Svelte的新手,则他们的教程非常全面,具有丰富的示例供您参考。
希望这可以帮到您!

这太棒了。关于你的问题 - 也许你正在寻找onMount或onDestroy? - 这个想法是复制useEffect,以在身份验证状态更改时更改currentUserloading值。我猜我需要的是afterUpdate,但我不知道那是否会给我与useEffect相同的东西。我不确定卸载函数是否能帮我实现这一点。 - Kay
@Kay 如果我不了解 auth.onAuthStateChanged 函数的更多信息,我认为我无法帮助你(也许这是一个不同的问题,因为这个问题主要是关于上下文的)。有可能你根本不需要任何生命周期钩子,可以直接调用它。 - person_v1.32
onAuthStateChanged 为用户的登录状态添加观察者,当用户登录或退出时触发。 - Kay
我猜你正在使用Firebase身份验证。我也用过它,我所做的就是在App.svelteonMount中订阅onAuthStateChanged。然后,根据user参数,我设置了一些响应式值,用于确定要呈现什么。 - Christian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接