$ cnpm install @uyun/udap-sync-job
在本教程中,您将创建一个待办事项应用程序。 您的应用程序将需要显示任务、添加新任务、将任务标记为完成以及删除任务。 这些操作将涉及 CRUD(创建、读取、更新和删除)应用程序的四个方面。
这种类型的项目通常使用 Class 组件来完成,但是这个应用程序将集成 React Hooks。 React Hooks 允许功能组件具有状态并使用生命周期方法,允许您避免使用 Class 组件并拥有更多模块化和可读性的代码。
首先, 您需要创建一个组件包,在您的终端窗口中, 导航到您希望新应用程序所在的位置,然后键入:
yarn create:package react-todo
接下来执行 yarn install
将我们的包软连接到程序所在的位置的 node_modules
中
yarn install
然后,运行项目:
实时监听组件包 packages
的变化
yarn start
使用 cypress
查看组件包运行结果
yarn cypress:open-ct
使用 storybook
查看组件案例运行结果
yarn storybook
将组件发布到 npm
仓库中, 在这里我们使用 verdaccio 搭建一个私有 npm 仓库测试
在控制台使用 verdaccio 启动私有仓库
verdaccio
接下来使用 npm
注册账号
npm adduser --registry http://localhost:4873
然后在控制台中使用 commitizen
lerna
发布组件
git add .
使用 commitizen 代替 git commit
yarn commit
发布组件包
yarn lerna:publish
最终会得到如下代码
import React from "react";
import "./App.css";
function Todo({ todo, index, completeTodo, removeTodo }) {
return (
<div
className="todo"
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
<button onClick={() => removeTodo(index)}>x</button>
</div>
</div>
);
}
function TodoForm({ addTodo }) {
const [value, setValue] = React.useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
className="input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>
);
}
function App() {
const [todos, setTodos] = React.useState([
{
text: "Learn about React",
isCompleted: false,
},
{
text: "Meet friend for lunch",
isCompleted: false,
},
{
text: "Build really cool todo app",
isCompleted: false,
},
]);
const addTodo = (text) => {
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
const completeTodo = (index) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo = (index) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
completeTodo={completeTodo}
removeTodo={removeTodo}
/>
))}
<TodoForm addTodo={addTodo} />
</div>
</div>
);
}
export default App;
.app {
background: #209cee;
height: 100vh;
padding: 30px;
}
.todo-list {
background: #e8e8e8;
border-radius: 4px;
max-width: 400px;
padding: 5px;
}
.todo {
align-items: center;
background: #fff;
border-radius: 3px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15);
display: flex;
font-size: 12px;
justify-content: space-between;
margin-bottom: 6px;
padding: 3px 10px;
}
使用 Typescript 将如上代码整理到文件夹中
packages/react-todo/src/components/ReactTodo.tsx
import React, { useState } from "react";
import styles from "../styles.module.less";
import { TodoForm, TodoFormProps } from "./TodoForm";
import { Todo, TodoProps, TodoItem } from "./Todo";
export function ReactTodo() {
const [todos, setTodos] = useState<TodoItem[]>([
{
text: "Learn about React",
isCompleted: false,
},
{
text: "Meet friend for lunch",
isCompleted: false,
},
{
text: "Build really cool todo app",
isCompleted: false,
},
]);
const addTodo: TodoFormProps["addTodo"] = (text) => {
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
const completeTodo: TodoProps["completeTodo"] = (index) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo: TodoProps["removeTodo"] = (index) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return (
<div className={styles.app}>
<div className={styles["app-list"]}>
{todos.map((todo, index) => (
<Todo
key={index}
index={index}
todo={todo}
completeTodo={completeTodo}
removeTodo={removeTodo}
/>
))}
<TodoForm addTodo={addTodo} />
</div>
</div>
);
}
packages/react-todo/src/components/TodoForm.tsx
import React from "react";
export interface TodoFormProps {
addTodo: (value: string) => void;
}
export function TodoForm({ addTodo }: TodoFormProps) {
const [value, setValue] = React.useState("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
}}
>
<input
type="text"
className="input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>
);
}
packages/react-todo/src/components/Todo.tsx
import React from "react";
import styles from "../styles.module.less";
export interface TodoItem {
text: string;
isCompleted?: boolean;
}
export interface TodoProps {
todo: TodoItem;
index: number;
completeTodo: (index: number) => void;
removeTodo: (index: number) => void;
}
export function Todo({ todo, index, completeTodo, removeTodo }: TodoProps) {
return (
<div
className={styles.todo}
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
<button onClick={() => removeTodo(index)}>x</button>
</div>
</div>
);
}
修改测试文件 stories/__test__/ReactTodo.spec.tsx
/// <reference types="cypress" />
import * as React from "react";
import { mount } from "@cypress/react";
import { ReactTodo } from "@uyun/react-todo";
import "@uyun/components/dist/index.css";
import "@uyun/react-todo/dist/index.css";
it("渲染", () => {
mount(<ReactTodo />);
});
it("添加", () => {
mount(<ReactTodo />);
cy.get(".input").type("测试").should("have.value", "测试").type("{enter}");
});
it("完成", () => {
mount(<ReactTodo />);
cy.get(":nth-child(3) > div > :nth-child(2)").click();
});
it("删除", () => {
mount(<ReactTodo />);
cy.get(":nth-child(3) > div > :nth-child(2)").click();
});
packages/react-todo/src/hooks/useTodoImpl.ts
import { useState } from "react";
export interface TodoItem {
text: string;
isCompleted?: boolean;
}
export function useTodoImpl() {
const [todos, setTodos] = useState<TodoItem[]>([
{
text: "Learn about React",
isCompleted: false,
},
{
text: "Meet friend for lunch",
isCompleted: false,
},
{
text: "Build really cool todo app",
isCompleted: false,
},
]);
const addTodo = (text: string) => {
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
const completeTodo = (index: number) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo = (index: number) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return {
todos,
addTodo,
completeTodo,
removeTodo,
};
}
packages/react-todo/src/components/ReactTodo.tsx
import React from "react";
import styles from "../styles.module.less";
import { TodoForm } from "./TodoForm";
import { Todo } from "./Todo";
import { TodoProvider } from "../hooks";
import { useTodoImpl } from "../hooks/useTodoImpl";
export function ReactTodo({ useTodo = useTodoImpl }) {
const value = useTodo();
return (
<TodoProvider value={value}>
<div className={styles.app}>
<div className={styles["app-list"]}>
{value.todos.map((todo, index) => (
<Todo key={index} index={index} todo={todo} />
))}
<TodoForm />
</div>
</div>
</TodoProvider>
);
}
packages/react-todo/src/components/TodoForm.tsx
import React from "react";
import { useTodo } from "../hooks";
export function TodoForm() {
const [value, setValue] = React.useState("");
const { addTodo } = useTodo();
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
}}
>
<input
type="text"
className="input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>
);
}
packages/react-todo/src/components/Todo.tsx
import React from "react";
import styles from "../styles.module.less";
import { useTodo } from "../hooks";
import type { TodoItem } from "./../hooks/useTodoImpl";
export interface TodoProps {
todo: TodoItem;
index: number;
}
export function Todo({ todo, index }: TodoProps) {
const { completeTodo, removeTodo } = useTodo();
return (
<div
className={styles.todo}
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
<button onClick={() => removeTodo(index)}>x</button>
</div>
</div>
);
}
修改测试文件
stories/react-todo/MyReactTodo.tsx
import { useState } from "react";
import { ReactTodo } from "@uyun/react-todo";
import React from "react";
export interface TodoItem {
text: string;
isCompleted?: boolean;
}
function useTodoImpl() {
const [todos, setTodos] = useState<TodoItem[]>([
{
text: "12313",
isCompleted: false,
},
{
text: "55555",
isCompleted: false,
},
{
text: "66666",
isCompleted: false,
},
]);
const addTodo = (text: string) => {
const newTodos = [...todos, { text: text + "的加强王权" }];
setTodos(newTodos);
};
const completeTodo = (index: number) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo = (index: number) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return {
todos,
addTodo,
completeTodo,
removeTodo,
};
}
export function MyReactTodo() {
return <ReactTodo useTodo={useTodoImpl} />;
}
stories/__test__/ReactTodo.spec.tsx
/// <reference types="cypress" />
import * as React from "react";
import { mount } from "@cypress/react";
import { ReactTodo } from "@uyun/react-todo";
import { MyReactTodo } from "../react-todo/MyReactTodo";
import "@uyun/components/dist/index.css";
import "@uyun/react-todo/dist/index.css";
it("渲染", () => {
mount(<ReactTodo />);
});
it("添加", () => {
mount(<ReactTodo />);
cy.get(".input").type("测试").should("have.value", "测试").type("{enter}");
});
it("完成", () => {
mount(<ReactTodo />);
cy.get(":nth-child(3) > div > :nth-child(2)").click();
});
it("删除", () => {
mount(<ReactTodo />);
cy.get(":nth-child(3) > div > :nth-child(2)").click();
});
describe("自定义 Hooks 来控制 todo-list 中的 Actions", () => {
it("渲染", () => {
mount(<MyReactTodo />);
});
it("添加", () => {
mount(<MyReactTodo />);
cy.get(".input").type("测试").should("have.value", "测试").type("{enter}");
});
it("完成", () => {
mount(<MyReactTodo />);
cy.get(":nth-child(3) > div > :nth-child(2)").click();
});
it("删除", () => {
mount(<MyReactTodo />);
cy.get(":nth-child(3) > div > :nth-child(2)").click();
});
});
stories/react-todo/MyReactTodo.tsx
import { useState } from "react";
import { ReactTodo } from "@uyun/react-todo";
import React from "react";
export interface TodoItem {
text: string;
isCompleted?: boolean;
}
function useTodoImpl() {
const [todos, setTodos] = useState<TodoItem[]>([
{
text: "12313",
isCompleted: false,
},
{
text: "55555",
isCompleted: false,
},
{
text: "66666",
isCompleted: false,
},
]);
const addTodo = (text: string) => {
const newTodos = [...todos, { text: text + "的加强王权" }];
setTodos(newTodos);
};
const completeTodo = (index: number) => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo = (index: number) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return {
todos,
addTodo,
completeTodo,
removeTodo,
};
}
export function MyReactTodo() {
return <ReactTodo useTodo={useTodoImpl} />;
}
Copyright 2013 - present © cnpmjs.org