Solid JavaScript 库入门

Avatar of Charlie Gerard
Charlie Gerard

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

Solid 是一个用于创建用户界面的反应式 JavaScript 库,无需虚拟 DOM。它将模板编译成真实的 DOM 节点,并将更新封装在细粒度的反应中,以便在状态更新时,只有相关的代码运行。

这样,编译器可以优化初始渲染,而运行时可以优化更新。这种对性能的关注使其成为 最受欢迎的 JavaScript 框架之一

我对它感到好奇,想尝试一下,所以我花了一些时间创建了一个小型待办事项应用程序,以探索该框架如何处理渲染组件、更新状态、设置存储等等。

如果您迫不及待地想查看最终代码和结果,以下是最终演示

入门

与大多数框架一样,我们可以从安装 npm 包开始。要将框架与 JSX 一起使用,请运行

npm install solid-js babel-preset-solid

然后,我们需要将 babel-preset-solid 添加到我们的 Babel、webpack 或 Rollup 配置文件中,使用

"presets": ["solid"]

或者,如果您想搭建一个小型应用程序,您也可以使用他们的模板之一

# Create a small app from a Solid template
npx degit solidjs/templates/js my-app
 
# Change directory to the project created
cd my-app
 
# Install dependencies
npm i # or yarn or pnpm
 
# Start the dev server
npm run dev

有 TypeScript 支持,因此如果您想启动一个 TypeScript 项目,请将第一个命令更改为 npx degit solidjs/templates/ts my-app

创建和渲染组件

要渲染组件,语法与 React.js 相似,因此可能看起来很熟悉

import { render } from "solid-js/web";
 
const HelloMessage = props => <div>Hello {props.name}</div>;
 
render(
 () => <HelloMessage name="Taylor" />,
 document.getElementById("hello-example")
);

我们需要从导入 render 函数开始,然后我们创建一个带有文本和属性的 div,并调用 render,传递组件和容器元素。

然后,此代码编译成真实的 DOM 表达式。例如,上面的代码示例,一旦由 Solid 编译,看起来就像这样

import { render, template, insert, createComponent } from "solid-js/web";
 
const _tmpl$ = template(`<div>Hello </div>`);
 
const HelloMessage = props => {
 const _el$ = _tmpl$.cloneNode(true);
 insert(_el$, () => props.name);
 return _el$;
};
 
render(
 () => createComponent(HelloMessage, { name: "Taylor" }),
 document.getElementById("hello-example")
);

Solid Playground 非常酷,它表明 Solid 具有不同的渲染方式,包括客户端、服务器端和客户端带水合。

使用信号跟踪变化的值

Solid 使用一个名为 createSignal 的钩子,它返回两个函数:一个 getter 和一个 setter。如果您习惯于使用 React.js 之类的框架,这可能看起来有点奇怪。您通常会期望第一个元素是值本身;但是,在 Solid 中,我们需要显式调用 getter 来拦截读取值的地址,以跟踪其更改。

例如,如果我们正在编写以下代码

const [todos, addTodos] = createSignal([]);

记录 todos 将不会返回该值,而是一个函数。如果我们要使用该值,我们需要调用该函数,如 todos()

对于一个小型的待办事项列表,这将是

import { createSignal } from "solid-js";
 
const TodoList = () => {
 let input;
 const [todos, addTodos] = createSignal([]);
 
 const addTodo = value => {
   return addTodos([...todos(), value]);
 };
 
 return (
   <section>
     <h1>To do list:</h1>
     <label for="todo-item">Todo item</label>
     <input type="text" ref={input} name="todo-item" id="todo-item" />
     <button onClick={() => addTodo(input.value)}>Add item</button>
     <ul>
       {todos().map(item => (
         <li>{item}</li>
       ))}
     </ul>
   </section>
 );
};

上面的代码示例将显示一个文本字段,并在单击“添加项目”按钮后,将使用新项目更新待办事项,并在列表中显示它。

这可能看起来与使用 useState 非常相似,那么使用 getter 有什么不同呢?请考虑以下代码示例

console.log("Create Signals");
const [firstName, setFirstName] = createSignal("Whitney");
const [lastName, setLastName] = createSignal("Houston");
const [displayFullName, setDisplayFullName] = createSignal(true);
 
const displayName = createMemo(() => {
 if (!displayFullName()) return firstName();
 return `${firstName()} ${lastName()}`;
});
 
createEffect(() => console.log("My name is", displayName()));
 
console.log("Set showFullName: false ");
setDisplayFullName(false);
 
console.log("Change lastName ");
setLastName("Boop");
 
console.log("Set showFullName: true ");
setDisplayFullName(true);

运行上面的代码将产生

Create Signals
 
My name is Whitney Houston
 
Set showFullName: false
 
My name is Whitney
 
Change lastName
 
Set showFullName: true
 
My name is Whitney Boop

需要注意的主要问题是,在设置新的姓氏后,我的名字是... 不会被记录。这是因为此时,没有任何东西在监听 lastName() 上的更改。只有在 displayFullName() 的值更改时才会设置 displayName() 的新值,这就是为什么当 setShowFullName 被设置为 true 时,我们可以看到新的姓氏显示的原因。

这为我们提供了一种更安全的方式来跟踪值的更新。

反应式原语

在最后一个代码示例中,我介绍了 createSignal,但也介绍了其他几个原语:createEffectcreateMemo

createEffect

createEffect 跟踪依赖项,并在每次依赖项更改的渲染后运行。

// Don't forget to import it first with 'import { createEffect } from "solid-js";'
const [count, setCount] = createSignal(0);
 
createEffect(() => {
 console.log("Count is at", count());
});

计数为... 每次 count() 的值更改时都会记录。

createMemo

createMemo 创建一个只读信号,该信号在执行代码的依赖项更新时重新计算其值。当您想要缓存一些值并在依赖项更改之前访问它们而无需重新评估它们时,可以使用它。

例如,如果我们想显示一个计数器 100 次,并在单击按钮时更新该值,使用 createMemo 将允许重新计算仅在每次单击时发生一次

function Counter() {
   const [count, setCount] = createSignal(0);
   // Calling `counter` without wrapping it in `createMemo` would result in calling it 100 times.
   // const counter = () => {
   //    return count();
   // }
 
   // Calling `counter` wrapped in `createMemo` results in calling it once per update.
// Don't forget to import it first with 'import { createMemo } from "solid-js";'
   const counter = createMemo(() => {
       return count()
   })
 
   return (
       <>
       <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
       <div>1. {counter()}</div>
       <div>2. {counter()}</div>
       <div>3. {counter()}</div>
       <div>4. {counter()}</div>
       <!-- 96 more times -->
       </>
   );
}

生命周期方法

Solid 公开了一些生命周期方法,例如 onMountonCleanuponError。如果我们想要在初始渲染后运行一些代码,我们需要使用 onMount

// Don't forget to import it first with 'import { onMount } from "solid-js";'
 
onMount(() => {
 console.log("I mounted!");
});

onCleanup 与 React 中的 componentDidUnmount 类似——它在反应式范围重新计算时运行。

onError 在最近的子级范围内发生错误时执行。例如,我们可以在获取数据失败时使用它。

存储

要创建用于数据的存储,Solid 公开了 createStore,其返回值是一个只读代理对象和一个 setter 函数。

例如,如果我们将我们的待办事项示例更改为使用存储而不是状态,它将看起来像这样

const [todos, addTodos] = createStore({ list: [] });
 
createEffect(() => {
 console.log(todos.list);
});
 
onMount(() => {
 addTodos("list", [
   ...todos.list,
   { item: "a new todo item", completed: false }
 ]);
});

上面的代码示例将首先记录一个带有空数组的代理对象,然后记录一个带有包含 {item: "一个新的待办事项", completed: false} 对象的数组的代理对象。

需要注意的一点是,在不访问其属性的情况下,无法跟踪顶层状态对象——这就是为什么我们记录 todos.list 而不是 todos 的原因。

如果我们只记录了 todo` 在 createEffect 中,我们将看到列表的初始值,但不会看到在 onMount 中更新后的值。

要更改存储中的值,我们可以使用在使用 createStore 时定义的设置函数更新它们。例如,如果我们想将待办事项列表项更新为“已完成”,我们可以通过这种方式更新存储

const [todos, setTodos] = createStore({
 list: [{ item: "new item", completed: false }]
});
 
const markAsComplete = text => {
 setTodos(
   "list",
   i => i.item === text,
   "completed",
   c => !c
 );
};
 
return (
 <button onClick={() => markAsComplete("new item")}>Mark as complete</button>
);

控制流

为了避免在使用 .map() 等方法时在每次更新时都浪费地重新创建所有 DOM 节点,Solid 允许我们使用模板助手。

其中一些可用,例如 For 用于循环遍历项目、Show 用于有条件地显示和隐藏元素、SwitchMatch 用于显示与特定条件匹配的元素,等等!

以下是一些显示如何使用它们的示例

<For each={todos.list} fallback={<div>Loading...</div>}>
 {(item) => <div>{item}</div>}
</For>
 
<Show when={todos.list[0].completed} fallback={<div>Loading...</div>}>
 <div>1st item completed</div>
</Show>
 
<Switch fallback={<div>No items</div>}>
 <Match when={todos.list[0].completed}>
   <CompletedList />
 </Match>
 <Match when={!todos.list[0].completed}>
   <TodosList />
 </Match>
</Switch>

演示项目

这是一个对 Solid 基础知识的快速介绍。如果您想尝试一下,我创建了一个入门项目,您可以自动部署到 Netlify 并通过单击下面的按钮将其克隆到您的 GitHub!

该项目包括 Solid 项目的默认设置,以及一个示例待办事项应用程序,其中包含我在本文中提到的基本概念,以帮助您入门!

这个框架远不止我在这里介绍的内容,所以请随时查看文档以获取更多信息!