SOLIDJS速效课程 - 构建REST API客户端 - 第1部分
#javascript #网络开发人员 #typescript #solidjs

介绍

虽然反应,但在网络开发社区中已经闻名。 Solidjs是镇上的新酷框架。每个人都对此大肆宣传。在这一系列文章中,您将学习稳固JS及其反应性的来龙去脉。有很多概念要涵盖,所以扣紧!

如果您想观看视频,我已经有了您ð

设置项目

我们将使用pnpm安装软件包。您也可以使用NPM或纱线。您可以通过在终端中运行npm install -g pnpm来安装PNPM。

创建和服务

npx degit solidjs/templates/ts solid-rest-client-app
cd solid-rest-client-app
pnpm i # or `npm install` or `yarn`
pnpm run dev # Open the app on localhost:3000
# OR
# npm run dev
# yarn dev

运行上面的最后一个命令后,您应该可以看到该应用在http://localhost:3000

上运行

添加尾风CSS

按照SolidJS Official Docs或以下说明的说明。

使用以下命令安装软件包:

pnpm add --save-dev tailwindcss postcss autoprefixer

然后运行以下命令以生成尾风配置

npx tailwindcss init -p

替换content属性以定位tailwind.config.js文件中的solidjs文件:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

现在在src/index.css文件中添加以下CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Existing CSS Code */

添加Nunito字体

<head></head>标签内的index.html中添加nunito字体:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700&display=swap" rel="stylesheet">

最后,将nunito字体添加到index.css文件,如下:

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  margin: 0;
  font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

添加离子

index.html中的<head></head>标签中添加Ionic Fonts (Ionicons),如下:

<script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>

让我们现在尝试使用图标。更新app.tsx文件以添加Ionicon,如下所示:

const App: Component = () => {
  return (
    <div class={styles.App}>
      <header class={styles.header}>
        <img src={logo} class={styles.logo} alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          class={styles.link}
          href="https://github.com/solidjs/solid"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn Solid
        </a>
        <ion-icon name="accessibility-outline"></ion-icon> // <-- Here
      </header>
    </div>
  );
};

您会注意到Typescript目前根本不满意,并且可能显示出这样的内容:

IonIcons making TS Sad

这是因为solidjs和typeScript在一起不了解元素是什么。因为它在JSX中使用。让我们通过提供类型来解决它。在src文件夹中创建一个名为types的新文件夹。和一个名为solid-js.d.ts的新文件。然后在内部添加以下代码:

import "solid-js";

declare module "solid-js" {
  namespace JSX {
    interface IntrinsicElements {
      "ion-icon": any;
    }
  }
}

之后,您会看到app.tsx和tyspript现在很高兴。查看app,您应该能够在视图上看到可访问性图标。

添加eslint

编码很棒。但是编写好的代码更好。我们将在我们的项目中添加ESLINT,以确保我们的代码在应用程序的整个开发过程中保持不错。抱歉,我内心的软件架构师无法避免执行标准ð!

运行以下命令从终端设置ESLINT:

npx eslint --init

在整个安装中显示以下选项:

  • To check syntax, find problems, and enforce code style
  • JavaScript modules (import/export)
  • None of these(用于框架)
  • Yes(对于“您的项目使用Typescript”)
  • Browser(对于“您的代码运行何处”)
  • Standard: https://github.com/standard/eslint-config-standard-with-typescript(对于“您想遵循哪个样式指南”)
  • JavaScript(对于“您要在哪种格式中使用您的配置文件”)
  • JSON用于使用的文件类型
  • pnpm安装软件包

然后运行以下内容以安装所需的软件包:

pnpm add --save-dev eslint eslint-plugin-solid @typescript-eslint/parser 

完成后,更新.eslintrc文件以使用以下代码:

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "overrides": [],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "rules": {},
  "parser": "@typescript-eslint/parser",
  "plugins": ["solid"],
  "extends": [
    "eslint:recommended",
    "standard-with-typescript",
    "plugin:solid/typescript"
  ]
}

导航和路由

我们将开始创建一些页面,并将设置路由。要使用路线,我们将通过在终端中运行以下命令来安装solid-app-router软件包:

pnpm add --save solid-app-router

创建Navbar并添加基本路线

src文件夹中创建一个名为components的新文件夹,然后在src/components文件夹中创建一个名为Navbar.tsx的新文件。在文件中添加以下代码:

import { Component } from 'solid-js';
import { Link } from 'solid-app-router';

const Navbar: Component = () => {
  return (
    <header class="bg-purple-600 text-white py-2 px-8 h-16 flex items-center justify-between">
      <Link class="hover:opacity-50 hero" href='/'>REST in Peace</Link>
      <div class="flex items-center gap-4">
        <Link class="hover:opacity-50" href='/about'>About</Link>
      </div>
    </header>
  )
}

export default Navbar;

现在,在app.tsx文件中添加以下路由配置如下:

import type { Component } from 'solid-js';
import styles from './App.module.css';
import { hashIntegration, Route, Router, Routes } from 'solid-app-router';
import Navbar from './components/Navbar';

const App: Component = () => {
  return (
    <Router source={hashIntegration()}>
      <div class={styles.App}>
        <Navbar />
      </div>
      <Routes>
        <Route path="/" component={() => <div>Home Component</div>}></Route>
        <Route path="/about" component={() => <div>About Component</div>}></Route>
      </Routes>
    </Router>
  );
};

export default App;

如果您现在查看该应用程序,则应该能够看到带有徽标和链接的Navbar。您可以单击两者以导航到不同的路线。

Navbar with links

请注意,我们正在使用<Router>组件从solid-app-router包装所有路由配置。在Router内部,我们具有具有单独的<Route>元素的Navbar组件和<Routes>组件。每个<Route>元素都定义了路线的工作方式。以下是您可以提供的道具的可能组合:

export declare type RouteProps = {
    path: string | string[];
    children?: JSX.Element;
    data?: RouteDataFunc;
} & ({
    element?: never;
    component: Component;
} | {
    component?: never;
    element?: JSX.Element;
    preload?: () => void;
});

请注意,每条路线都需要path。所有其他属性都是可选的。但是我们不能在同一路线上同时提供elementcomponent。请注意,最后两个条件之间有一个或操作员:

({
    element?: never;
    component: Component;
} | {
    component?: never;
    element?: JSX.Element;
    preload?: () => void;
})

因此,目前,我们提供的硬编码功能将JSX作为两种路线的component支柱。我们将切换到本教程稍后使用element

创建家庭和关于组件:

让我们现在可以正确创建主页和关于大约页面。在src文件夹中创建一个名为pages的新文件夹。

家庭组件:

在名为Home.tsxpages文件夹中创建一个新文件,并在其中粘贴以下代码:

import { Outlet } from 'solid-app-router';
import { Component } from 'solid-js';

const Home: Component = () => {
  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
        <div class="flex justify-between py-4">
          <h1 class="text-sm ">Rest Requests</h1>
          <button class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8" onClick={() => alert('To be implemented')}>
            <div>+</div>
          </button>
        </div>
      </div>
      <div class="flex-1 min-h-full">
        <Outlet />
      </div>
    </div>
  )
}

export default Home;

关于组件:

pages文件夹中创建一个名为About.tsx的新文件。然后粘贴其中的以下代码:

import { Component } from 'solid-js';

const About: Component = () => {
  return (
    <div>
      <h2>This is About</h2>
    </div>
  )
}

export default About;

现在我们已经创建了两个文件,让我们更新App.tsx文件的<Router>元素以使用以下路线:

import type { Component } from 'solid-js';
import { hashIntegration, Route, Router, Routes } from 'solid-app-router';
import Navbar from './components/Navbar';
import About from './pages/About'; // <!-- new import
import Home from './pages/Home'; // <!-- new import

const App: Component = () => {
  return (
    <Router source={hashIntegration()}>
      <div class="flex flex-col h-full min-h-screen">
        <Navbar></Navbar>
        <main class="px-8 py-4 flex-1 flex flex-col h-full">
          <Routes>
            <Route path="/about" element={<About />} />
            <Route path="/" element={<Home />}>
              {/* <Route path="/" element={<RestClientIndex />} />
              <Route
                path="/:id"
                element={<RestClient />}
                data={fetchSelectedRequest}
              /> */}
            </Route>
          </Routes>
        </main>
      </div>
    </Router>
  );
};

export default App;

现在,如果您转到主页,您将看到以下内容:

Real Components

创建接口

我们正在使用TypeScript ...它非常酷。我们将在此应用中处理一些REST API请求。因此,我们将创建与它们合作所需的接口。
src文件夹中创建一个名为interfaces的新文件夹。然后创建一个名为rest.interfaces.ts的文件。
最后,在文件中添加以下代码:

interface IRequest {
  headers?: {
    [key: string]: string;
  }[];
  method: string;
  url: string;
  body?: any;
}
export interface IRestRequest {
  id: string;
  name: string;
  description: string;
  request: IRequest;
}

export interface IRestResponse {
  data: any;
  status: number;
  headers: any;
}

请注意,我们仅导出此文件中的两个接口。 IRestRequestIRestResponse接口。它们代表我们应用程序上下文中的请求和响应。请注意,IRestRequest内部使用IRequest属性的IRequest接口。此request属性是我们将在本教程的稍后阶段传递给axios的实际请求。

创建请求数据

让我们开始创建一些数据。我们将创建一些虚拟请求来使用。更新home.tsx以添加以下请求数组:

import { Outlet } from 'solid-app-router';
import { Component } from 'solid-js';
import { IRestRequest } from '../interfaces/rest.interfaces';

const Home: Component = () => {
  const requests: IRestRequest[] = [
    {
      id: "1",
      name: "Get Scores",
      description: "Getting scores from server",
      request: {
        method: "GET",
        url: "https://scorer-pro3.p.rapidapi.com/score/game123",
        headers: [
          {
            key: "X-RapidAPI-Host",
            value: "API_HOST_FROM_RAPID_API",
          },
          {
            key: "X-RapidAPI-Key",
            value: "API_KEY_FROM_RAPID_API",
          },
        ],
      },
    },
    {
      id: "2",
      name: "Add Score",
      description: "Adding scores to server",
      request: {
        method: "POST",
        url: "https://scorer-pro3.p.rapidapi.com/score",
        headers: [
          {
            key: "X-RapidAPI-Host",
            value: "API_HOST_FROM_RAPID_API",
          },
          {
            key: "X-RapidAPI-Key",
            value: "API_KEY_FROM_RAPID_API",
          },
        ],
        body: JSON.stringify({
          score: 100,
          gameId: "123",
          userId: "test123",
        }),
      },
    },
  ];

  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <!-- further code here -->
    </div>
  )
}

export default Home;

请注意,我们根据提供的接口获得数据。另请注意,每个请求都有此headers属性。我们不会在本教程中使用它。这只是为了让您了解如何将此应用程序扩展到与请求标头一起使用。或等待我的另一个教程ð!

创建请求元素

现在我们有了数据,让我们通过使用solid-js中的For元素在同一Home.tsx文件中使用它来循环循环请求阵列并呈现一些请求。
如下:
Home.tsx文件中更新模板

import { Link, Outlet } from "solid-app-router";
import { Component, For } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";

const Home: Component = () => {
  {/*  ... */}
  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
        <div class="flex justify-between py-4">
          <h1 class="text-sm ">Rest Requests</h1>
          <button
            class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8"
            onClick={() => alert("To be implemented")}
          >
            <div>+</div>
          </button>
        </div>
        {/*  👇 We've added this 👇 */}
        <div class="list">
          <For each={requests} fallback={<div>Loading...</div>}>
            {(item) => (
              <Link href={`/${item.id}`} class="relative list__item">
                <div
                  class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
                  classList={{
                    "list__item--active": Boolean(
                      location.pathname === `/${item.id}`
                    ),
                  }}
                >
                  <div>{item.name}</div>
                  <div class="text-xs break-all">
                    {item.request.method} {item.request.url}
                  </div>
                </div>
              </Link>
            )}
          </For>
        </div>
      </div>
      <div class="flex-1 min-h-full">
        <Outlet />
      </div>
    </div>
  );
};

export default Home;

Rendering requests list

您可以看到,渲染的每个项目都是solid-app-router<Link>元素。单击请求应将您带到请求的详细信息页面。而且我们尚未实施它。每个项目在视图上显示请求的名称,方法和URL。

使用信号创建一个反应性持久商店

由于我们希望我们的商店持久,因此我们将使用@solid-primitives/storage软件包保存所有请求中的所有请求。如果是空的,我们将使用硬编码/虚拟数据启动商店。让我们首先安装包裹:

pnpm add --save @solid-primitives/storage

现在,在src文件夹中创建一个名为store.ts的新文件,并粘贴以下代码:

import { IRestRequest } from "./interfaces/rest.interfaces";
import { createStorageSignal } from "@solid-primitives/storage";

export const [restRequests, setRestRequests] = createStorageSignal<
  IRestRequest[]
>(
  "requests",
  [
    {
      id: "1",
      name: "Get Scores",
      description: "Getting scores from server",
      request: {
        method: "GET",
        url: "https://scorer-pro3.p.rapidapi.com/score/game123",
        headers: [
          {
            key: "X-RapidAPI-Host",
            value: "API_HOST_FROM_RAPID_API",
          },
          {
            key: "X-RapidAPI-Key",
            value: "API_KEY_FROM_RAPID_API",
          },
        ],
      },
    },
    {
      id: "2",
      name: "Add Score",
      description: "Adding scores to server",
      request: {
        method: "POST",
        url: "https://scorer-pro3.p.rapidapi.com/score",
        headers: [
          {
            key: "X-RapidAPI-Host",
            value: "API_HOST_FROM_RAPID_API",
          },
          {
            key: "X-RapidAPI-Key",
            value: "API_KEY_FROM_RAPID_API",
          },
        ],
        body: JSON.stringify({
          score: 100,
          gameId: "123",
          userId: "test123",
        }),
      },
    },
  ],
  {
    deserializer: (val): IRestRequest[] => {
      if (val === null) {
        return [];
      }
      return JSON.parse(val);
    },
    serializer: (val) => {
      return JSON.stringify(val);
    },
  }
);

请注意,我们正在使用createStorageSignal方法与类型IRestRequest[]来创建存储信号来存储我们的请求数组。请注意,该方法的第一个参数具有"requests"的值,该值既设置了该商店的名称in App的会话,又是浏览器存储的键(localStorage)。第二个参数是商店的初始值,我们在这里提供了我们的硬编码/虚拟数据。
第三个参数是存储选项。我们正在传递deserializerserializer的方法,这些方法分别从存储和写入存储时,负责转换数据。
最后,我们将同时导出restRequests,这是一个可从信号访问值的固体js访问者,也是更新信号值的setRestRequests方法。

现在我们已经创建了存储空间,让我们在Home.tsx中使用它:

import { Link, Outlet } from "solid-app-router";
import { Component, For } from "solid-js";
import { restRequests } from "../store"; // <-- importing accessor

const Home: Component = () => {
  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
        <div class="flex justify-between py-4">
          <h1 class="text-sm ">Rest Requests</h1>
          <button
            class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8"
            onClick={() => alert("To be implemented")}
          >
            <div>+</div>
          </button>
        </div>
        <div class="list">
          {/*  Using the accessor here */}
          <For each={restRequests()} fallback={<div>Loading...</div>}>
            {(item) => (
              <Link href={`/${item.id}`} class="relative list__item">
                <div
                  class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
                  classList={{
                    "list__item--active": Boolean(
                      location.pathname === `/${item.id}`
                    ),
                  }}
                >
                  <div>{item.name}</div>
                  <div class="text-xs break-all">
                    {item.request.method} {item.request.url}
                  </div>
                </div>
              </Link>
            )}
          </For>
        </div>
      </div>
      <div class="flex-1 min-h-full">
        <Outlet />
      </div>
    </div>
  );
};

export default Home;

请注意,当我们将其移至store.ts文件时,我们已从文件中删除了Home.tsx的硬编码请求。现在的家庭组件现在看起来很干净ð«§您可以在localstorage中看到持续的请求,如Chrome调试器中的如下:

Requests Persistent Storage

创建添加请求模式

由于我们现在已经实施了存储,因此让我们创建一种添加/创建更多请求的方法。这就是乐趣开始的地方,因为我们现在开始动态地将内容添加到应用程序中。我们将创建一个模式以添加新请求。为此,在src/components文件夹中创建一个名为RequestModal.tsx的新文件。添加以下代码创建基本模式:

import { Component, ComponentProps, Show } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";

interface RequestModalProps extends ComponentProps<any> {
  show: boolean;
  onModalHide: (id: string | null) => void;
  request?: IRestRequest;
}

const RequestModal: Component<RequestModalProps> = (
  props: RequestModalProps
) => {
  return (
    <Show when={props.show}>
      <div class="fixed z-50 top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.75)]">
        <div
          class="relative max-h-[85%] overflow-y-auto top-20 bg-gray-200 max-w-md m-auto h- block p-8 pb-8 border-t-4 border-purple-600 rounded-sm shadow-xl"
        >
          <h5 class="text-4xl font-bold mb-4">
            {(props.request ? "Edit" : "Create") + " Request"}
          </h5>
          <span class="absolute bottom-9 right-8">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              class="w-10 h-10 text-purple-600"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="M13 10V3L4 14h7v7l9-11h-7z"
              />
            </svg>
          </span>
        </div>
      </div>
    </Show>
  );
};

export default RequestModal;

请注意,RequestModal使用固体JS的<Show>组件,该组件根据提供给when属性的条件,显示其中的模板/组件。我们依靠RequestModalshow支柱来告诉<Show>组件何时显示内容。这样我们就可以从组件外部控制RequestModal以使其可见或隐藏。
现在我们有了这个组件,让我们在Home.tsx中添加如下:

import { Link, Outlet, useLocation, useNavigate } from "solid-app-router";
import { Component, createSignal, For } from "solid-js";
import RequestModal from "../components/RequestModal";
import { restRequests } from "../store";

const Home: Component = () => {
  const [showModal, setShowModal] = createSignal(false);
  const navigate = useNavigate();
  const location = useLocation();
  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <div>
        <button onClick={() => setShowModal(!showModal())}>Click Me</button>
        <RequestModal
          show={showModal()}
          onModalHide={(id: string | null) => {
            setShowModal(!showModal());
          }}
        />
      </div>
      <div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
        <div class="flex justify-between py-4">
          <h1 class="text-sm ">Rest Requests</h1>
          <button
            class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8"
            onClick={() => alert("To be implemented")}
          >
            <div>+</div>
          </button>
        </div>

        <div class="list">
          <For each={restRequests()} fallback={<div>Loading...</div>}>
            {(item) => (
              <Link href={`/${item.id}`} class="relative list__item">
                <div
                  class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
                  classList={{
                    "list__item--active": Boolean(
                      location.pathname === `/${item.id}`
                    ),
                  }}
                >
                  <div>{item.name}</div>
                  <div class="text-xs break-all">
                    {item.request.method} {item.request.url}
                  </div>
                </div>
              </Link>
            )}
          </For>
        </div>
      </div>
      <div class="flex-1 min-h-full">
        <Outlet />
      </div>
    </div>
  );
};

export default Home;

让我们看一下我们在顶部进口的变化的几步之类的步骤。

const Home: Component = () => {
  const [showModal, setShowModal] = createSignal(false);
  const navigate = useNavigate();
  const location = useLocation();
  // more code
};

export default Home;

我们创建了一个信号showModal,我们将用于显示或隐藏模式。 IE。稍后,我们将作为RequestModalshow道具传递。我们还创建了两个常数。一次进行导航,一个用于访问位置。

<div>
  <button onClick={() => setShowModal(!showModal())}>Click Me</button>
  <RequestModal
    show={showModal()}
    onModalHide={(id: string | null) => {
      setShowModal(!showModal());
    }}
  />
</div>

在上面的代码中,我们有一个按钮可以切换showModal信号的值。并且我们使用的是RequestModal,该RequestModal具有分配给show Prop的showModal的值。您可以看到我们还为onModalHide Prop提供了功能。当RequestModal被隐藏时,这将被调用。我们还没有写过它的逻辑。

使用所有这些,当您单击左侧的Click Me文本时,您应该可以在应用中看到它。

Create Request Simple Modal

请注意,我们无法关闭模态。我们将稍作处理。

创建IconButton组件

我们将在整个应用程序中使用一些图标按钮。因此,创建可重复使用的IconButton组件是一个好主意。在名为IconButton.tsxsrc/components文件夹中创建一个新文件。然后向其添加以下代码:

import { Component, ComponentProps } from "solid-js";

interface IconButtonProps extends ComponentProps<any> {
  onClick: (event: MouseEvent) => void;
  label: string;
  icon: string;
  type?: "reset" | "submit" | "button";
}

const IconButton: Component<IconButtonProps> = ({
  onClick,
  label,
  icon,
  type,
}) => {
  return (
    <button
      onclick={onClick}
      role="button"
      type={type || "button"}
      title={label}
      class="w-6 h-6 flex transition-all ease-in-out duration-100 hover:scale-125 items-center justify-center text-white bg-purple-600 border border-purple-600 rounded-full hover:bg-purple-700 active:text-white focus:outline-none focus:ring"
    >
      <span class="sr-only">{label}</span>
      <ion-icon name={icon}></ion-icon>
    </button>
  );
};

export default IconButton;

此组件非常简单。我们正在此处造成一个按钮,并使用<ion-icon>元素显示目标图标。查看IconButtonProps接口,以查看我们可以将哪些可能的道具传递给此组件。

让我们用这个新的IconButton组件替换Home.tsx文件中的“添加请求”按钮。我们还将从模板中删除“单击我”按钮:

import { Link, Outlet, useLocation, useNavigate } from "solid-app-router";
import { Component, createSignal, For } from "solid-js";
import IconButton from "../components/IconButton";
import RequestModal from "../components/RequestModal";
import { restRequests } from "../store";

const Home: Component = () => {
  const [showModal, setShowModal] = createSignal(false);
  const navigate = useNavigate();
  const location = useLocation();
  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <div>
        <RequestModal
          show={showModal()}
          onModalHide={(id: string | null) => {
            setShowModal(!showModal());
          }}
        />
      </div>
      <div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
        <div class="flex justify-between py-4">
          <h1 class="text-sm ">Rest Requests</h1>
          {/* Replaced the Add Request Button with IconButton */}
          <IconButton
            onClick={() => setShowModal(true)}
            icon="add"
            label="Add Request"
          />
        </div>

        <div class="list">
          <For each={restRequests()} fallback={<div>Loading...</div>}>
            {(item) => (
              <Link href={`/${item.id}`} class="relative list__item">
                <div
                  class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
                  classList={{
                    "list__item--active": Boolean(
                      location.pathname === `/${item.id}`
                    ),
                  }}
                >
                  <div>{item.name}</div>
                  <div class="text-xs break-all">
                    {item.request.method} {item.request.url}
                  </div>
                </div>
              </Link>
            )}
          </For>
        </div>
      </div>
      <div class="flex-1 min-h-full">
        <Outlet />
      </div>
    </div>
  );
};

export default Home;

太好了!现在让我们迈向创建添加请求表格。

使用固体JS表单创建请求

我们将使用solid-forms软件包在本教程中使用表格。我看过多个表格库,但发现这个图书馆很不错。如下安装包:

pnpm add --save solid-forms

实施TextField组件

现在在src/components文件夹下创建一个新文件,并将其命名为TextField.tsx。只要我们要显示输入或文本股组件,就会使用此组件。将以下代码添加到创建的文件中:

import { IFormControl } from "solid-forms";
import { Component } from "solid-js";

export const TextField: Component<{
  control: IFormControl<string>;
  label: string;
  placeholder?: string;
  type?: string;
  rows?: number;
  id: string;
  class?: string;
  valueUpdated?: (val: any) => void;
}> = (props) => {
  const type = props.type || "text";
  const onInput = (e: { currentTarget: { value: string } }) => {
    props.control.markDirty(true);
    props.control.setValue(e.currentTarget.value);
  };

  const onBlur = () => {
    props.control.markTouched(true);
    if (props.valueUpdated) {
      props.valueUpdated(props.control.value);
    }
  };
  return (
    <>
      <label class="sr-only" for={props.id}>
        {props.label}
      </label>
      {type === "textarea" ? (
        <textarea
          value={props.control.value}
          rows={props.rows || 3}
          oninput={onInput}
          onblur={onBlur}
          placeholder={props.placeholder}
          required={props.control.isRequired}
          id={props.id}
          class={`w-full p-3 text-sm border-gray-200 rounded-lg ${props.class}`}
        />
      ) : (
        <input
          type="text"
          value={props.control.value}
          oninput={onInput}
          onblur={onBlur}
          placeholder={props.placeholder}
          required={props.control.isRequired}
          id={props.id}
          class={`w-full p-3 text-sm border-gray-200 rounded-lg ${props.class}`}
        />
      )}
    </>
  );
};

除了一些明显的道具外,要考虑的重要实现是模板中<input><textarea>元素上使用的onbluroninput方法。 TextField接受control Prop,该道具应该是固体形式的形式控制。每当<input><textarea>元素中有输入更改时,都会触发onInput方法,并通过props.control.setValue语句设置控件的值。
同样,我们让父组件知道使用valueUpdated Prop更改了该值。每当我们 blur 从输入或文本方面,如果提供了props.valueUpdated方法(因为它是可选的道具)。

创建RESTCLIENTFORM

此表单将用于创建和编辑请求。并且它将基于多个TextField组件。在src/components文件夹中创建一个名为RestClientForm.tsx的新文件。然后向其添加以下代码:

import { Component } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";

export const RestClientForm: Component<{
  request?: Partial<IRestRequest>;
  formSubmit: Function;
  formUpdate?: Function;
  actionBtnText: string;
}> = (props) => {
  return (
    <form
      action=""
      class="space-y-4"
      classList={{}}
      onSubmit={(e) => {
        e.preventDefault();
      }}
    >
      <div class="grid grid-cols-1 gap-4">
        <div>
          <label for="name" class="mb-4 block">
            Name
          </label>
          <input placeholder="name" />
        </div>
        <div>
          <label for="url" class="mb-4 block">
            URL
          </label>
          <input placeholder="url" />
        </div>

        <div>
          <label class="my-4 block">Method</label>
          <input placeholder="method" />
        </div>
      </div>
      <div>
        <label class="my-4 block">Body</label>
        <input placeholder="body" />
      </div>

      <div class="mt-4">
        <button
          disabled={false}
          type="submit"
          class="inline-flex items-center disabled:bg-gray-500 justify-center w-full px-5 py-3 text-white bg-purple-600 hover:bg-purple-700 rounded-lg sm:w-auto"
        >
          <span class="font-medium"> {props.actionBtnText} </span>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="w-5 h-5 ml-3"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M14 5l7 7m0 0l-7 7m7-7H3"
            />
          </svg>
        </button>
      </div>
    </form>
  );
};

该组件接受针对formSubmit Prop的功能,该功能将被调用...好吧...在表单上提交ð!另一个强制性道具是actionBtnText,因此我们可以在“提交”按钮上显示所需的标签。
如果您仔细查看上述代码,您会注意到我们尚未使用到目前为止的任何TextField组件。好吧,握住你的马。现在,让我们在RequestModal组件中使用此RestClientForm组件,看看一切外观。在RequestModal.tsx文件中添加RestClientForm如下:

import { Component, ComponentProps, Show } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { setRestRequests, restRequests } from "../store";
import { RestClientForm } from "./RestClientForm";

interface RequestModalProps extends ComponentProps<any> {
  show: boolean;
  onModalHide: (id: string | null) => void;
  request?: IRestRequest;
}

const RequestModal: Component<RequestModalProps> = (
  props: RequestModalProps
) => {
  return (
    <Show when={props.show}>
      <div class="fixed z-50 top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.75)]">
        <div class="relative max-h-[85%] overflow-y-auto top-20 bg-gray-200 max-w-md m-auto h- block p-8 pb-8 border-t-4 border-purple-600 rounded-sm shadow-xl">
          <h5 class="text-4xl font-bold mb-4">
            {(props.request ? "Edit" : "Create") + " Request"}
          </h5>
          <RestClientForm
            formSubmit={(request: IRestRequest) => {
              const id = self.crypto?.randomUUID() || Date.now().toString();
              setRestRequests([
                ...(restRequests() || []),
                {
                  ...request,
                  id,
                },
              ]);
              props.onModalHide(id);
            }}
            actionBtnText={"Save"}
          />
          <span class="absolute bottom-9 right-8">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              class="w-10 h-10 text-purple-600"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="M13 10V3L4 14h7v7l9-11h-7z"
              />
            </svg>
          </span>
        </div>
      </div>
    </Show>
  );
};

export default RequestModal;

RestClientForm with simple html elements

请注意,我们在此处传递针对formSubmit Prop的功能。如果将request传递给此功能以响应提交表单,则使用self.crypto?.randomUUID()(漂亮的Trick Trick TBH)为该请求创建一个唯一的ID,并使用setRestRequests Update功能将请求添加到请求列表中。请记住,这是信号的一部分,不仅是正常信号,而且是@solid-primitives/storage包装中的信号。因此,将在应用程序的当前状态和本地存储中更新。最后,您可以看到我们正在调用props.onModalHide(id)语句以在创建新请求并保存后关闭模式。

但是等等!如果您现在尝试一下,它将无法正常工作。因为我们还没有使用任何控件,也没有使用TextField组件。因此,让我们首先创建控件。

创建固体形式控件

我们现在将利用solid-forms控制工厂。这使我们能够创建一个表格组,我们可以轻松地设法处理表单输入并对其做出反应。
为此,请如下更新RestClientForm.tsx文件:

import { Component } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { createFormGroup, createFormControl } from 'solid-forms';

const controlFactory = () => {
  return createFormGroup({
    name: createFormControl<string>("New Request", {
      required: true,
      validators: (val: string) => {
        return !val.length ? {isMissing: true} : null;
      }
    }),
    request: createFormGroup({
      method: createFormControl<string>("GET"),
      body: createFormControl<string>(""),
      url: createFormControl<string>(""),
    }),
  });
};

// rest of the code

controlFactory方法从实心形式返回一组IFormGroup的形式。这意味着我们有一个形式组,现在可以在其中具有多个表单控件和表单组。在我们的情况下,我们将name作为表单控件,因为您可以看到它使用createFormControl方法。请注意,该方法的第一个参数是初始值,第二个参数是包含required: true和用于自定义验证的validators方法的配置。
我们表格组中的request属性是另一个表单组,其中包含三个表单控件,methodbodyurl

现在,我们将使用withControl方法将RestClientForm组件用固体表单包装。
controlFactory代码下方创建包装组件如下:

import { Component } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { createFormGroup, createFormControl, withControl } from "solid-forms";
import { TextField } from "./TextField";

const controlFactory = () => {
  return createFormGroup({
    name: createFormControl<string>("New Request", {
      required: true,
      validators: (val: string) => {
        return !val.length ? { isMissing: true } : null;
      },
    }),
    request: createFormGroup({
      method: createFormControl<string>("GET"),
      body: createFormControl<string>(""),
      url: createFormControl<string>(""),
    }),
  });
};

export const RestClientForm = withControl<
  {
    request?: Partial<IRestRequest>;
    formSubmit: Function;
    formUpdate?: Function;
    actionBtnText: string;
  },
  typeof controlFactory
>({
  controlFactory,
  component: (props) => {
    const controlGroup = () => props.control.controls;
    const requestControlGroup = () => controlGroup().request.controls;
    const request = () => props.request;

    return (
      <form
        action=""
        class="space-y-4"
        classList={{
          "is-valid": props.control.isValid,
          "is-invalid": !props.control.isValid,
          "is-touched": props.control.isTouched,
          "is-untouched": !props.control.isTouched,
          "is-dirty": props.control.isDirty,
          "is-clean": !props.control.isDirty,
        }}
        onSubmit={(e) => {
          e.preventDefault();
          const params = {
            ...props.control.value,
            request: {
              ...props.control.value.request,
            },
          };
          props.formSubmit(params);
        }}
      >
        <div class="grid grid-cols-1 gap-4">
          <div>
            <label for="name" class="mb-4 block">
              Name
            </label>
            <TextField
              placeholder="name"
              id="name"
              label="Name"
              control={controlGroup().name}
            />
          </div>
          <div>
            <label for="url" class="mb-4 block">
              URL
            </label>
            <TextField
              placeholder="url"
              id="url"
              label="Url"
              control={requestControlGroup().url}
            />
          </div>

          <div>
            <label class="my-4 block">Method</label>
            <TextField
              id="method"
              label="Method"
              placeholder="method"
              control={requestControlGroup().method}
            />
          </div>
        </div>
        <div>
          <label class="my-4 block">Body</label>
          <TextField
            id="body"
            type="textarea"
            label="Body"
            placeholder="body"
            control={requestControlGroup().body}
          />
        </div>

        <div class="mt-4">
          <button
            disabled={!props.control.isValid}
            type="submit"
            class="inline-flex items-center disabled:bg-gray-500 justify-center w-full px-5 py-3 text-white bg-purple-600 hover:bg-purple-700 rounded-lg sm:w-auto"
          >
            <span class="font-medium"> {props.actionBtnText} </span>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              class="w-5 h-5 ml-3"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="M14 5l7 7m0 0l-7 7m7-7H3"
              />
            </svg>
          </button>
        </div>
      </form>
    );
  },
});

在上面的代码中,您会注意到我们的RestClientForm组件现在使用withControl方法,并使用了我们之前创建的controlFactory。结果,我们获得了使用组件函数中的工厂创建的实体(表格组)。我们收到的表格组为props.control属性。这是一个实体JS信号。是的!这意味着这是反应性的。
我们将控件分配给两组。

  • 使用controlGroup访问者的主要组
  • 使用requestControlGroup访问者的request对照组

<form
  action=""
  class="space-y-4"
  classList={{
    "is-valid": props.control.isValid,
    "is-invalid": !props.control.isValid,
    "is-touched": props.control.isTouched,
    "is-untouched": !props.control.isTouched,
    "is-dirty": props.control.isDirty,
    "is-clean": !props.control.isDirty,
  }}
</form>

最后,我们将每个控件通过control Prop传递到相应的TextField组件。请注意如何通过调用登录器然后访问属性来访问控件。例如,requestControlGroup().url而不是requestControlGroup.url

注意:您会注意到我用于method的元素是TextField而不是<select>组件。这是故意的,不要使教程更复杂。随意自行调整代码ð

如果您现在查看该应用,则应如下:

RestClientForm with TextField components

看起来很酷ð,对吗?尝试立即创建请求。您应该能够看到将其添加到UI以及localStorage中。这是因为当我们提交表单时,我们从RestClientForm调用props.formSubmit方法。这反过

固体JS指令(您的第一个指令)

我们将立即实施指令。指令是像组件这样的实现,但通常没有模板,并且在大多数情况下纯粹与DOM合作。就我们的情况而言,我们将在单击外部时实现关闭RequestModal的指令。
在名为directivessrc文件夹中创建一个新文件夹,并在其内部创建一个名为click-outside.directive.ts的文件。然后向其添加以下代码:

import { Accessor, onCleanup } from "solid-js";

export default function clickOutside(el: Element, accessor: Accessor<any>) {
  const onClick = (e: Event) =>
    !el.contains(e.target as Node) && accessor()?.();
  document.body.addEventListener("click", onClick);

  onCleanup(() => document.body.removeEventListener("click", onClick));
}

您可以看到代码很小。但是它做什么? ðÖ
指令本身是具有两个参数的函数。 SolidJS提供了第一个参数,因为该指令被应用于指令。第二个论点是我们提供给指令的价值。在这种情况下,它将是关闭模式的函数。
请注意,我们在document.body元素上注册了click事件,该事件应检查我们是否在指令内部或外部单击。而且,如果我们单击室外,它将调用accessor()以获取要调用的函数,然后通过accessor()?.();语句调用它。似乎有点混乱。只是克服它ð

我们还使用固体JS的onCleanup钩子去除事件侦听器,当该指令所应用的元素不再在DOM上。在我们的情况下,当我们打开RequestModal时,该指令将栩栩如生,活动听众将被注册。当模态关闭时,指令将被销毁并将删除事件听众。

让我们现在使用RequestModal.tsx中的指令如下:

import { Component, ComponentProps, Show } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { setRestRequests, restRequests } from "../store";
import { RestClientForm } from "./RestClientForm";
import outsideDirective from "../directives/click-outside.directive"; // <-- new import

// https://github.com/solidjs/solid/discussions/845
const clickOutside = outsideDirective; // <-- remapping to `clickOutside` variable

interface RequestModalProps extends ComponentProps<any> {
  show: boolean;
  onModalHide: (id: string | null) => void;
  request?: IRestRequest;
}

const RequestModal: Component<RequestModalProps> = (
  props: RequestModalProps
) => {
  return (
    <Show when={props.show}>
      <div class="fixed z-50 top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.75)]">
        <div
          class="relative max-h-[85%] overflow-y-auto top-20 bg-gray-200 max-w-md m-auto h- block p-8 pb-8 border-t-4 border-purple-600 rounded-sm shadow-xl"
          use:clickOutside={() => {  {/** Using the directive here */}
            props.onModalHide(null);
          }}
        >
          <h5 class="text-4xl font-bold mb-4">
            {(props.request ? "Edit" : "Create") + " Request"}
          </h5>
          <RestClientForm
            ...
          />
          <span class="absolute bottom-9 right-8">
            ...
          </span>
        </div>
      </div>
    </Show>
  );
};

export default RequestModal;

您会注意到Typescript再次不满意,因为它不了解clickOutside是指令。

directive making TS sad

让我们让打字稿开心。如下:
,更新types文件夹中的solid-js.d.ts文件

import "solid-js";

declare module "solid-js" {
  namespace JSX {
    interface IntrinsicElements {
      "ion-icon": any;
    }
    interface Directives {
      clickOutside?: () => void;
    }
  }
}

打字稿现在应该很开心!您可能还注意到,我们正在重新映射从outsideDirectiveclickOutside的变量。这是因为我在代码中链接的打字稿 +固体JS问题。
如果立即打开请求模式,然后单击其外部,您将自动看到模态关闭。是的! ð

删除请求

现在我们可以添加请求,在单击外部时自动关闭模式并持续保存请求,让我们处理删除请求。
要删除请求,我们将在左侧栏中的每个列表项目上添加一个删除按钮。为此,请如下更新Home.tsx

// existing imports
import { restRequests, setRestRequests } from "../store";

const Home: Component = () => {
  const [showModal, setShowModal] = createSignal(false);
  const navigate = useNavigate();
  const location = useLocation();
  return (
    <div class="flex flex-col md:flex-row gap-4 h-full flex-1">
      <div>
        <RequestModal
          show={showModal()}
          onModalHide={(id: string | null) => {
            setShowModal(!showModal());
          }}
        />
      </div>
      <div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
        <div class="flex justify-between py-4">
          <h1 class="text-sm ">Rest Requests</h1>
          {/* Replaced the Add Request Button with IconButton */}
          <IconButton
            onClick={() => setShowModal(true)}
            icon="add"
            label="Add Request"
          />
        </div>

        <div class="list">
          <For each={restRequests()} fallback={<div>Loading...</div>}>
            {(item) => (
              <Link href={`/${item.id}`} class="relative list__item">
                <div
                  class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
                  classList={{
                    "list__item--active": Boolean(
                      location.pathname === `/${item.id}`
                    ),
                  }}
                >
                  <div>{item.name}</div>
                  <div class="text-xs break-all">
                    {item.request.method} {item.request.url}
                  </div>
                </div>
                {/* Delete Request Button */}
                <button
                  onclick={(e: MouseEvent) => {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    if (restRequests()?.length) {
                      const requests = restRequests() || [];
                      setRestRequests(requests.filter((i) => i.id !== item.id));
                      if (location.pathname === `/${item.id}`) {
                        navigate("/");
                      }
                    }
                  }}
                  class="absolute text-xl hover:scale-125 transition-all ease-in-out duration-100 hover:text-red-700 text-red-600 right-2 top-0 bottom-0 m-auto"
                >
                  <ion-icon name="trash"></ion-icon>
                </button>
              </Link>
            )}
          </For>
        </div>
      </div>
      <div class="flex-1 min-h-full">
        <Outlet />
      </div>
    </div>
  );
};

export default Home;

如果您在此更改后查看该应用程序,则将在每个请求上查看一个删除按钮。我们将更改它以显示仅在悬停的项目上。
查看删除按钮的onclick处理程序,请注意,我们使用requests.filter方法使用请求的id从数组中删除了请求。然后,我们使用setRestRequests更新程序功能来持续使用它们。尝试删除按钮删除一些请求。

让我们添加一些CSS,以使删除按钮仅在请求项目的悬停上可见。创建一个名为Home.css的新文件,并在其中添加以下代码:

.list .list__item ion-icon {
  display: none;
}

.list .list__item:hover ion-icon {
  display: flex;
}

.list .list__item:hover .list__item--active + ion-icon {
  @apply text-white;
}

.list .list__item--active {
  @apply bg-purple-600 text-white;
}

最后,在Home.tsx文件中导入Home.css文件如下:

// existing imports
import { restRequests, setRestRequests } from "../store";
import "./Home.css";

如果您现在查看该应用程序,我们仅在悬停请求项目上显示删除按钮。

Delete btn only on hover

真棒,我们现在能够创建请求,删除请求,保存然后持续保存,并为应用程序提供非常好的UI/UX。随意修改它的外观并使其成为您自己的ð

结论

在本教程中,我们在固体JS方面学到了一些概念。我们学会了:

  • 从固体JS
  • 开始
  • 信号
  • 将数据持续到存储
  • 使用solid-app-router使用路线
  • 使用solid-forms处理表格
  • 创建指令

我喜欢写这篇文章,希望您从中学到了很多东西。如果您这样做,请对此帖子做出反应并为其添加书签。本教程也可以在YouTube上找到,因此您可以在那里查看Part1Part2

如果您想连接,以下是我社交的链接:

TwitterYouTubeLinkedInGitHub