Hooks
的 函数式组件
SCSS
, 而不是 Sass
REST API
作为 CRUD 后端🦄 node --version
v18.14.0
🦄 yarn --version
3.6.0
yarn create react-app keeptrack --template typescript
cd keeptrack
code .
查看默认的项目结构
package.json
public/index.html
页面模版src/index.tsx
JavaScript 入口点yarn start
做出一些改变并查看更新
src\App.tsx
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
- Learn React
+ Learn React!!!
</a>
</header>
</div>
);
}
保存后即可立即查看浏览器页面更新
停止项目运行:
Ctrl + C
参考:
注意:
node-sass
已被遗弃较新的版本不需要配置
sass-loader
等一系列插件,安装即用。Sass 有两种语法! SCSS 语法 (
.scss
) 最常使用。 它是 CSS 的超集,这意味着所有有效的 CSS 也是有效的 SCSS。 缩进语法 (.sass
) 更不寻常:它使用缩进而不是大括号来嵌套语句,并使用换行符而不是分号来分隔它们。
yarn add sass
安装
mini.css
包, Mini.css is a 最小的, 响应式的, 与样式无关的 CSS 框架.Mini.css
类似于Bootstrap
, 但更轻, and 需要的 CSS 类更少 因此你可以 专注于 学习React
但仍然可以获得 专业的外观.
yarn add mini.css@3.0.1
应用 CSS
打开并删除 App.css 的内容
更新 App.tsx
src\App.tsx
import React from 'react';
- import logo from './logo.svg';
import './App.css';
function App() {
return (
- <div className="App">
- <header className="App-header">
- <img src={logo} className="App-logo" alt="logo" />
- <p>
- Edit <code>src/App.tsx</code> and save to reload.
- </p>
- <a
- className="App-link"
- href="https://reactjs.org"
- target="_blank"
- rel="noopener noreferrer"
- >
- Learn React!!
- </a>
- </header>
- </div>
+ <blockquote cite="Benjamin Franklin">
+ Tell me and I forget, teach me and I may remember, involve me and I learn.
+ </blockquote>
);
}
export default App;
打开
src\index.css
并删除其中内容更新
src\index.css
/* 导入安装的包中 CSS */
@import '../node_modules/mini.css/dist/mini-default.min.css';
src\projects
src\projects\ProjectsPage.tsx
在 VSCode 中,可以使用扩展 VS Code ES7 React/Redux/React-Native/JS snippets , 安装启用后可以使用快捷键
rfce
然后tab
src\projects\ProjectsPage.tsx
import React from 'react';
function ProjectsPage() {
return <h1>Projects</h1>;
}
export default ProjectsPage;
import React from 'react';
在最新版本的 React 中不是必需的,因为它使用了一个新的JSX Transform
React 17
RC and higher supports the new JSX Transform, and they’ve also released React 16.14.0
, React 15.7.0
, and React 0.14.10
for people who are still on the older major versions).src\index.js
in a Create React App. Note that the code still works if you include the import in other files but it is no longer required.src\App.tsx
- import React from 'react';
- import './App.css';
+ import ProjectsPage from './projects/ProjectsPage';
function App() {
- return (
- <blockquote cite="Benjamin Franklin">
- Tell me and I forget, teach me and I may remember, involve me and I learn.
- </blockquote>
- );
+ return (
+ <div className="container">
+ <ProjectsPage />
+ </div>
+ );
}
assets
其中文件到 keeptrack/public
文件夹src\projects\Project.ts
, src\projects\MockProjects.ts
src\projects\Project.ts
export class Project {
id: number | undefined;
name: string = '';
description: string = '';
imageUrl: string = '';
contractTypeId: number | undefined;
contractSignedOn: Date = new Date();
budget: number = 0;
isActive: boolean = false;
get isNew(): boolean {
return this.id === undefined;
}
constructor(initializer?: any) {
if (!initializer) return;
if (initializer.id) this.id = initializer.id;
if (initializer.name) this.name = initializer.name;
if (initializer.description) this.description = initializer.description;
if (initializer.imageUrl) this.imageUrl = initializer.imageUrl;
if (initializer.contractTypeId)
this.contractTypeId = initializer.contractTypeId;
if (initializer.contractSignedOn)
this.contractSignedOn = new Date(initializer.contractSignedOn);
if (initializer.budget) this.budget = initializer.budget;
if (initializer.isActive) this.isActive = initializer.isActive;
}
}
src\projects\MockProjects.ts
import { Project } from './Project';
export const MOCK_PROJECTS = [
new Project({
id: 1,
name: 'Johnson - Kutch',
description:
'Fully-configurable intermediate framework. Ullam occaecati libero laudantium nihil voluptas omnis.',
imageUrl: '/assets/placeimg_500_300_arch4.jpg',
contractTypeId: 3,
contractSignedOn: '2013-08-04T22:39:41.473Z',
budget: 54637,
isActive: false,
}),
new Project({
id: 2,
name: 'Wisozk Group',
description:
'Centralized interactive application. Exercitationem nulla ut ipsam vero quasi enim quos doloribus voluptatibus.',
imageUrl: '/assets/placeimg_500_300_arch1.jpg',
contractTypeId: 4,
contractSignedOn: '2012-08-06T21:21:31.419Z',
budget: 91638,
isActive: true,
}),
new Project({
id: 3,
name: 'Denesik LLC',
description:
'Re-contextualized dynamic moratorium. Aut nulla soluta numquam qui dolor architecto et facere dolores.',
imageUrl: '/assets/placeimg_500_300_arch12.jpg',
contractTypeId: 6,
contractSignedOn: '2016-06-26T18:24:01.706Z',
budget: 29729,
isActive: true,
}),
new Project({
id: 4,
name: 'Purdy, Keeling and Smitham',
description:
'Innovative 6th generation model. Perferendis libero qui iusto et ullam cum sint molestias vel.',
imageUrl: '/assets/placeimg_500_300_arch5.jpg',
contractTypeId: 4,
contractSignedOn: '2013-05-26T01:10:42.344Z',
budget: 45660,
isActive: true,
}),
new Project({
id: 5,
name: 'Kreiger - Waelchi',
description:
'Managed logistical migration. Qui quod praesentium accusamus eos hic non error modi et.',
imageUrl: '/assets/placeimg_500_300_arch12.jpg',
contractTypeId: 2,
contractSignedOn: '2009-12-18T21:46:47.944Z',
budget: 81188,
isActive: true,
}),
new Project({
id: 6,
name: 'Lesch - Waelchi',
description:
'Profound mobile project. Rem consequatur laborum explicabo sint odit et illo voluptas expedita.',
imageUrl: '/assets/placeimg_500_300_arch1.jpg',
contractTypeId: 3,
contractSignedOn: '2016-09-23T21:27:25.035Z',
budget: 53407,
isActive: false,
}),
];
展示数据
使用
JSON.stringify()
输出来自MockProjects.ts
的MOCK_PROJECTS
数组
<></>
包装 <h1>
和 <pre>
标签
{}
JSON.stringify(MOCK_PROJECTS, null, ' ')
src\projects\ProjectsPage.tsx
+ import { MOCK_PROJECTS } from './MockProjects';
function ProjectsPage() {
- return <h1>Projects</h1>
+ return (
+ <>
+ <h1>Projects</h1>
+ <pre>{JSON.stringify(MOCK_PROJECTS, null, ' ')}</pre>
+ </>
+ );
}
export default ProjectsPage;
创建一个可重用的列表组件
src\projects\ProjectList.tsx
import React from 'react';
import { Project } from './Project';
interface ProjectListProps {
projects: Project[];
}
function ProjectList({ projects }: ProjectListProps) {
return <pre>{JSON.stringify(projects, null, ' ')}</pre>;
}
export default ProjectList;
传递数据到组件属性
src\projects\ProjectsPage.tsx
以便渲染 ProjectList
组件,并且传递 MOCK_PROJECTS
src\projects\ProjectsPage.tsx
import React from 'react';
import { MOCK_PROJECTS } from './MockProjects';
+ import ProjectList from './ProjectList';
function ProjectsPage() {
return (
<>
<h1>Projects</h1>
- <pre>{JSON.stringify(MOCK_PROJECTS, null, ' ')}</pre>
+ <ProjectList projects={MOCK_PROJECTS} />
</>
);
}
export default ProjectsPage;
验证界面
src\projects\ProjectList.tsx
...
function ProjectList({ projects }: ProjectListProps) {
- return <pre>{JSON.stringify(projects, null, ' ')}</pre>;
+ return (
+ <ul className="row">
+ {projects.map((project) => (
+ <li key={project.id}>{project.name}</li>
+ ))}
+ </ul>
+ );
}
export default ProjectList;
Visual Studio Code
扩展: HTML to JSX
src\projects\ProjectList.tsx
...
function ProjectList({ projects }: ProjectListProps) {
- return (
- <ul className="row">
- {projects.map((project) => (
- <li key={project.id}>{project.name}</li>
- ))}
- </ul>
- );
}
export default ProjectList;
...
function ProjectList({ projects }: ProjectListProps) {
+ return (
+ <div className="row">
+ {projects.map((project) => (
+ <div key={project.id} className="cols-sm">
+ <div className="card">
+ <img src={project.imageUrl} alt={project.name} />
+ <section className="section dark">
+ <h5 className="strong">
+ <strong>{project.name}</strong>
+ </h5>
+ <p>{project.description}</p>
+ <p>Budget : {project.budget.toLocaleString()}</p>
+ </section>
+ </div>
+ </div>
+ ))}
+ </div>
+ );
}
export default ProjectList;
src\projects\ProjectCard.tsx
import { Project } from './Project';
import React from 'react';
function formatDescription(description: string): string {
return description.substring(0, 60) + '...';
}
interface ProjectCardProps {
project: Project;
}
function ProjectCard(props: ProjectCardProps) {
const { project } = props;
return (
<div className="card">
<img src={project.imageUrl} alt={project.name} />
<section className="section dark">
<h5 className="strong">
<strong>{project.name}</strong>
</h5>
<p>{formatDescription(project.description)}</p>
<p>Budget : {project.budget.toLocaleString()}</p>
</section>
</div>
);
}
export default ProjectCard;
src\projects\ProjectList.tsx
import React from 'react';
import { Project } from './Project';
+ import ProjectCard from './ProjectCard';
interface ProjectListProps {
projects: Project[];
}
function ProjectList ({ projects }: ProjectListProps) {
const items = projects.map(project => (
<div key={project.id} className="cols-sm">
- <div className="card">
- <img src={project.imageUrl} alt={project.name} />
- <section className="section dark">
- <h5 className="strong">
- <strong>{project.name}</strong>
- </h5>
- <p>{project.description}</p>
- <p>Budget : {project.budget.toLocaleString()}</p>
- </section>
- </div>
+ <ProjectCard project={project}></ProjectCard>
</div>
));
return <div className="row">{items}</div>;
}
export default ProjectList;
src\projects\ProjectCard.tsx
...
<p>Budget...</p>
<button className=" bordered">
<span className="icon-edit "></span>
Edit
</button>
handleEditClick
事件处理程序添加到 ProjectCard
,该处理程序将 project
作为参数并将其记录到 console
。src\projects\ProjectCard.tsx
function ProjectCard(props: ProjectCardProps) {
const { project } = props;
+ const handleEditClick = (projectBeingEdited: Project) => {
+ console.log(projectBeingEdited);
+ };
return (
<div className="card">
<img src={project.imageUrl} alt={project.name} />
<section className="section dark">
<h5 className="strong">
<strong>{project.name}</strong>
</h5>
<p>{project.description}</p>
<p>Budget : {project.budget.toLocaleString()}</p>
<button className=" bordered">
<span className="icon-edit "></span>
Edit
</button>
</section>
</div>
);
}
handleEditClick
事件处理程序。src\projects\ProjectCard.tsx
function ProjectCard(props: ProjectCardProps) {
const { project } = props;
const handleEditClick = (projectBeingEdited: Project) => {
console.log(projectBeingEdited);
};
return (
<div className="card">
<img src={project.imageUrl} alt={project.name} />
<section className="section dark">
<h5 className="strong">
<strong>{project.name}</strong>
</h5>
<p>{project.description}</p>
<p>Budget : {project.budget.toLocaleString()}</p>
<button
className=" bordered"
+ onClick={() => {
+ handleEditClick(project);
+ }}
>
<span className="icon-edit "></span>
Edit
</button>
</section>
</div>
);
}
添加以下 CSS 样式以设置表单的宽度。
将所有
css
改为scss
, 相关import
路径也要更新
src\index.scss
form {
min-width: 300px;
}
src\projects\ProjectForm.tsx
import React from "react";
function ProjectForm() {
return (
<form className="input-group vertical">
<label htmlFor="name">Project Name</label>
<input type="text" name="name" placeholder="enter name" />
<label htmlFor="description">Project Description</label>
<textarea name="description" placeholder="enter description" />
<label htmlFor="budget">Project Budget</label>
<input type="number" name="budget" placeholder="enter budget" />
<label htmlFor="isActive">Active?</label>
<input type="checkbox" name="isActive" />
<div className="input-group">
<button className="primary bordered medium">Save</button>
<span />
<button type="button" className="bordered medium">
cancel
</button>
</div>
</form>
);
}
export default ProjectForm;
src\projects\ProjectList.tsx
...
+ import ProjectForm from './ProjectForm';
...
function ProjectList ({ projects }: ProjectListProps) {
const items = projects.map(project => (
<div key={project.id} className="cols-sm">
<ProjectCard project={project}></ProjectCard>
+ <ProjectForm />
</div>
));
return <div className="row">{items}</div>;
}
...
src\projects\ProjectCard.tsx
...
interface ProjectCardProps {
project: Project;
+ onEdit: (project: Project) => void;
}
...
更新
handleEditClick
事件将调用传递到onEdit
props
中的函数并删除console.log
语句。
src\projects\ProjectCard.tsx
function ProjectCard(props: ProjectCardProps) {
const { project,
+ onEdit
} = props;
const handleEditClick = (projectBeingEdited: Project) => {
+ onEdit(projectBeingEdited);
- console.log(projectBeingEdited);
};
...
}
在 VS Code 中,代码段
nfn
可以帮助创建handleEdit
事件处理程序。
src\projects\ProjectList.tsx
function ProjectList ({ projects }: ProjectListProps) {
+ const handleEdit = (project: Project) => {
+ console.log(project);
+ };
const items = projects.map(project => (
<div key={project.id} className="cols-sm">
<ProjectCard
project={project}
+ onEdit={handleEdit}
></ProjectCard>
<ProjectForm></ProjectForm>
</div>
));
return <div className="row">{items}</div>;
}
测试, 点击
Edit
添加状态变量
projectBeingEdited
以保存当前正在编辑的项目。 并更新handleEdit
以设置projectBeingEdited
变量。
src\projects\ProjectList.tsx
- import React from 'react';
+ import React, { useState } from 'react';
import { Project } from './Project';
import ProjectCard from './ProjectCard';
import ProjectForm from './ProjectForm';
interface ProjectListProps {
projects: Project[];
}
function ProjectList({ projects }: ProjectListProps) {
+ const [projectBeingEdited, setProjectBeingEdited] = useState({});
const handleEdit = (project: Project) => {
- console.log(project);
+ setProjectBeingEdited(project);
};
return (
...
);
}
export default ProjectList;
src\projects\ProjectList.tsx
...
function ProjectList({ projects }: ProjectListProps) {
const [projectBeingEdited, setProjectBeingEdited] = useState({});
const handleEdit = (project: Project) => {
setProjectBeingEdited(project);
};
return (
<div className="row">
{projects.map((project) => (
<div key={project.id} className="cols-sm">
- <ProjectCard project={project} onEdit={handleEdit} />
- <ProjectForm />
+ {project === projectBeingEdited ? (
+ <ProjectForm />
+ ) : (
+ <ProjectCard project={project} onEdit={handleEdit} />
+ )}
</div>
))}
</div>
);
}
export default ProjectList;
测试
src\projects\ProjectForm.tsx
import React from 'react';
+ interface ProjectFormProps {
+ onCancel: () => void;
+ }
- function ProjectForm() {
+ function ProjectForm({ onCancel }: ProjectFormProps) {
return (
<form className="input-group vertical">
<label htmlFor="name">Project Name</label>
<input type="text" name="name" placeholder="enter name" />
<label htmlFor="description">Project Description</label>
<textarea name="description" placeholder="enter description" />
<label htmlFor="budget">Project Budget</label>
<input type="number" name="budget" placeholder="enter budget" />
<label htmlFor="isActive">Active?</label>
<input type="checkbox" name="isActive" />
<div className="input-group">
<button className="primary bordered medium">Save</button>
<span />
<button type="button" className="bordered medium"
+ onClick={onCancel}
>
cancel
</button>
</div>
</form>
);
}
export default ProjectForm;
src\projects\ProjectList.tsx
...
function ProjectList({ projects }: ProjectListProps) {
const [projectBeingEdited, setProjectBeingEdited] = useState({});
const handleEdit = (project: Project) => {
setProjectBeingEdited(project);
};
+ const cancelEditing = () => {
+ setProjectBeingEdited({});
+ };
return (
<div className="row">
{projects.map((project) => (
<div key={project.id} className="cols-sm">
{project === projectBeingEdited ? (
<ProjectForm
+ onCancel={cancelEditing}
/>
) : (
<ProjectCard project={project} onEdit={handleEdit} />
)}
</div>
))}
</div>
);
}
export default ProjectList;
测试
src\projects\ProjectForm.tsx
...
+ import { Project } from './Project';
interface ProjectFormProps {
+ onSave: (project: Project) => void;
onCancel: () => void;
}
...
创建一个事件处理程序函数
handleSubmit
来处理表单的提交。该函数应防止浏览器的默认行为发布到后端,然后调用传递到
onSave
prop
中的函数, 并传递当前创建的新Project
。
src\projects\ProjectForm.tsx
+ import React, { SyntheticEvent } from 'react';
...
- function ProjectForm({ onCancel }: ProjectFormProps) {
+ function ProjectForm({ onSave, onCancel }: ProjectFormProps) {
+ const handleSubmit = (event: SyntheticEvent) => {
+ event.preventDefault();
+ onSave(new Project({ name: 'Updated Project' }));
+ };
return (
<form className="input-group vertical"
+ onSubmit={handleSubmit}
>
<label htmlFor="name">Project Name</label>
<input type="text" name="name" placeholder="enter name" />
<label htmlFor="description">Project Description</label>
<textarea name="description" placeholder="enter description" />
<label htmlFor="budget">Project Budget</label>
<input type="number" name="budget" placeholder="enter budget" />
<label htmlFor="isActive">Active?</label>
<input type="checkbox" name="isActive" />
<div className="input-group">
<button className="primary bordered medium">Save</button>
<span />
<button type="button" className="bordered medium" onClick={onCancel}>
cancel
</button>
</div>
</form>
);
}
export default ProjectForm;
src\projects\ProjectList.tsx
interface ProjectListProps {
projects: Project[];
+ onSave: (project: Project) => void;
}
src\projects\ProjectList.tsx
interface ProjectListProps {
projects: Project[];
onSave: (project: Project) => void;
}
- function ProjectList({ projects }: ProjectListProps) {
+ function ProjectList({ projects, onSave }: ProjectListProps) {
const [projectBeingEdited, setProjectBeingEdited] = useState({});
const handleEdit = (project: Project) => {
setProjectBeingEdited(project);
};
const cancelEditing = () => {
setProjectBeingEdited({});
};
return (
<div className="row">
{projects.map((project) => (
<div key={project.id} className="cols-sm">
{project === projectBeingEdited ? (
<ProjectForm
+ onSave={onSave}
onCancel={cancelEditing} />
) : (
<ProjectCard project={project} onEdit={handleEdit} />
)}
</div>
))}
</div>
);
}
export default ProjectList;
src\projects\ProjectsPage.tsx
import React, { Fragment } from 'react';
import { MOCK_PROJECTS } from './MockProjects';
import ProjectList from './ProjectList';
+ import { Project } from './Project';
function ProjectsPage() {
+ const saveProject = (project: Project) => {
+ console.log('Saving project: ', project);
+ };
return (
<Fragment>
<h1>Projects</h1>
<ProjectList
+ onSave={saveProject}
projects={MOCK_PROJECTS} />
</Fragment>
);
}
export default ProjectsPage;
测试
src\projects\ProjectForm.tsx
interface ProjectFormProps {
+ project: Project;
onSave: (project: Project) => void;
onCancel: () => void;
}
src\projects\ProjectForm.tsx
- import React, { SyntheticEvent } from 'react';
+ import React, { SyntheticEvent, useState } from 'react';
function ProjectForm({
+ project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
+ const [project, setProject] = useState(initialProject);
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
onSave(new Project({ name: 'Updated Project' }));
};
...
}
src\projects\ProjectForm.tsx
...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
const [project, setProject] = useState(initialProject);
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
onSave(new Project({ name: 'Updated Project' }));
};
+ const handleChange = (event: any) => {
+ const { type, name, value, checked } = event.target;
+ // if input type is checkbox use checked
+ // otherwise it's type is text, number etc. so use value
+ let updatedValue = type === 'checkbox' ? checked : value;
+
+ //if input type is number convert the updatedValue string to a +number
+ if (type === 'number') {
+ updatedValue = Number(updatedValue);
+ }
+ const change = {
+ [name]: updatedValue,
+ };
+
+ let updatedProject: Project;
+ // need to do functional update b/c
+ // the new project state is based on the previous project state
+ // so we can keep the project properties that aren't being edited +like project.id
+ // the spread operator (...) is used to
+ // spread the previous project properties and the new change
+ setProject((p) => {
+ updatedProject = new Project({ ...p, ...change });
+ return updatedProject;
+ });
+ };
return (
<form className="input-group vertical" onSubmit={handleSubmit}>
<label htmlFor="name">Project Name</label>
<input
type="text"
name="name"
placeholder="enter name"
+ value={project.name}
+ onChange={handleChange}
/>
<label htmlFor="description">Project Description</label>
<textarea
name="description"
placeholder="enter description"
+ value={project.description}
+ onChange={handleChange}
/>
<label htmlFor="budget">Project Budget</label>
<input
type="number"
name="budget"
placeholder="enter budget"
+ value={project.budget}
+ onChange={handleChange}
/>
<label htmlFor="isActive">Active?</label>
<input
type="checkbox"
name="isActive"
+ checked={project.isActive}
+ onChange={handleChange}
/>
<div className="input-group">
<button className="primary bordered medium">Save</button>
<span />
<button type="button" className="bordered medium" onClick={onCancel}>
cancel
</button>
</div>
</form>
);
}
export default ProjectForm;
src\projects\ProjectForm.tsx
...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
const [project, setProject] = useState(initialProject);
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
- onSave(new Project({ name: 'Updated Project' }));
+ onSave(project);
};
...
}
export default ProjectForm;
src\projects\ProjecList.tsx
...
function ProjectList({ projects, onSave }: ProjectListProps) {
const [projectBeingEdited, setProjectBeingEdited] = useState({});
const handleEdit = (project: Project) => {
setProjectBeingEdited(project);
};
const cancelEditing = () => {
setProjectBeingEdited({});
};
return (
<div className="row">
{projects.map((project) => (
<div key={project.id} className="cols-sm">
{project === projectBeingEdited ? (
<ProjectForm
+ project={project}
onSave={onSave}
onCancel={cancelEditing}
/>
) : (
<ProjectCard project={project} onEdit={handleEdit} />
)}
</div>
))}
</div>
);
}
export default ProjectList;
src\projects\ProjectsPage.tsx
import React, { Fragment,
+ useState } from 'react';
import { MOCK_PROJECTS } from './MockProjects';
import ProjectList from './ProjectList';
+ import { Project } from './Project';
function ProjectsPage() {
+ const [projects, setProjects] = useState<Project[]>(MOCK_PROJECTS);
const saveProject = (project: Project) => {
- console.log('Saving project: ', project);
+ let updatedProjects = projects.map((p: Project) => {
+ return p.id === project.id ? project : p;
+ });
+ setProjects(updatedProjects);
};
return (
<Fragment>
<h1>Projects</h1>
- <ProjectList onSave={saveProject} projects={MOCK_PROJECTS} />
+ <ProjectList onSave={saveProject} projects={projects} />
</Fragment>
);
}
export default ProjectsPage;
测试
src\project\ProjectForm.tsx
errors
对象初始化为状态变量为 {name: '', description: '', budget: ''}
,以便我们可以在组件的 state
中保存表单错误。src\projects\ProjectForm.tsx
...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
const [project, setProject] = useState(initialProject);
+ const [errors, setErrors] = useState({
+ name: '',
+ description: '',
+ budget: '',
+ });
...
}
export default ProjectForm;
ProjectForm
组件中实现 validate
函数。ProjectForm
组件中实现一个 isValid
函数,用于检查是否存在任何验证错误。src\projects\ProjectForm.tsx
...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
const [project, setProject] = useState(initialProject);
const [errors, setErrors] = useState({
name: '',
description: '',
budget: '',
});
const handleChange = (event: any) => {
...
};
+ function validate(project: Project) {
+ let errors: any = { name: '', description: '', budget: '' };
+ if (project.name.length === 0) {
+ errors.name = 'Name is required';
+ }
+ if (project.name.length > 0 && project.name.length < 3) {
+ errors.name = 'Name needs to be at least 3 characters.';
+ }
+ if (project.description.length === 0) {
+ errors.description = 'Description is required.';
+ }
+ if (project.budget === 0) {
+ errors.budget = 'Budget must be more than $0.';
+ }
+ return errors;
+ }
+ function isValid() {
+ return (
+ errors.name.length === 0 &&
+ errors.description.length === 0 &&
+ errors.budget.length === 0
+ );
+ }
return (
...
);
}
export default ProjectForm;
handleChange
中的 validate
函数以确定是否存在任何错误,然后将它们设置为 errors
状态变量。...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
...
const handleChange = (event: any) => {
const { type, name, value, checked } = event.target;
// if input type is checkbox use checked
// otherwise it's type is text, number etc. so use value
let updatedValue = type === 'checkbox' ? checked : value;
//if input type is number convert the updatedValue string to a number
if (type === 'number') {
updatedValue = Number(updatedValue);
}
const change = {
[name]: updatedValue,
};
let updatedProject: Project;
// need to do functional update b/c
// the new project state is based on the previous project state
// so we can keep the project properties that aren't being edited like project.id
// the spread operator (...) is used to
// spread the previous project properties and the new change
setProject((p) => {
updatedProject = new Project({ ...p, ...change });
return updatedProject;
});
+ setErrors(() => validate(updatedProject));
};
return (
...
);
}
export default ProjectForm;
isValid
函数,如果表单无效,则在保存更改之前返回该函数。src\projects\ProjectForm.tsx
...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
...
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
+ if (!isValid()) return;
onSave(project);
};
...
return (
...
);
}
export default ProjectForm;
HTML
模板显示验证消息。src\projects\ProjectForm.tsx
...
function ProjectForm({
project: initialProject,
onSave,
onCancel,
}: ProjectFormProps) {
const [project, setProject] = useState(initialProject);
const [errors, setErrors] = useState({
name: '',
description: '',
budget: '',
});
...
return (
<form className="input-group vertical" onSubmit={handleSubmit}>
<label htmlFor="name">Project Name</label>
<input
type="text"
name="name"
placeholder="enter name"
value={project.name}
onChange={handleChange}
/>
+ {errors.name.length > 0 && (
+ <div className="card error">
+ <p>{errors.name}</p>
+ </div>
+ )}
<label htmlFor="description">Project Description</label>
<textarea
name="description"
placeholder="enter description"
value={project.description}
onChange={handleChange}
/>
+ {errors.description.length > 0 && (
+ <div className="card error">
+ <p>{errors.description}</p>
+ </div>
+ )}
<label htmlFor="budget">Project Budget</label>
<input
type="number"
name="budget"
placeholder="enter budget"
value={project.budget}
onChange={handleChange}
/>
+ {errors.budget.length > 0 && (
+ <div className="card error">
+ <p>{errors.budget}</p>
+ </div>
+ )}
<label htmlFor="isActive">Active?</label>
<input
type="checkbox"
name="isActive"
checked={project.isActive}
onChange={handleChange}
/>
<div className="input-group">
<button className="primary bordered medium">Save</button>
<span />
<button type="button" className="bordered medium" onClick={onCancel}>
cancel
</button>
</div>
</form>
);
}
export default ProjectForm;
测试
yarn add json-server@0.16.2
package.json
{
"name": "keeptrack",
...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
+ "api": "json-server api/db.json --port 4000"
},
}
将之前下载的
react-starter-files-main
中api
文件夹复制到keeptrack
根目录
yarn api
若使用 npm
,则为下方
npm run api
预览 http://localhost:4000/ , 发现空白页,正常, 只有标题
React App
src\projects\projectAPI.ts
import { Project } from './Project';
const baseUrl = 'http://localhost:4000';
const url = `${baseUrl}/projects`;
function translateStatusToErrorMessage(status: number) {
switch (status) {
case 401:
return 'Please login again.';
case 403:
return 'You do not have permission to view the project(s).';
default:
return 'There was an error retrieving the project(s). Please try again.';
}
}
function checkStatus(response: any) {
if (response.ok) {
return response;
} else {
const httpErrorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
};
console.log(`log server http error: ${JSON.stringify(httpErrorInfo)}`);
let errorMessage = translateStatusToErrorMessage(httpErrorInfo.status);
throw new Error(errorMessage);
}
}
function parseJSON(response: Response) {
return response.json();
}
// eslint-disable-next-line
function delay(ms: number) {
return function (x: any): Promise<any> {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
}
function convertToProjectModels(data: any[]): Project[] {
let projects: Project[] = data.map(convertToProjectModel);
return projects;
}
function convertToProjectModel(item: any): Project {
return new Project(item);
}
const projectAPI = {
get(page = 1, limit = 20) {
return fetch(`${url}?_page=${page}&_limit=${limit}&_sort=name`)
.then(delay(600))
.then(checkStatus)
.then(parseJSON)
.then(convertToProjectModels)
.catch((error: TypeError) => {
console.log('log client error ' + error);
throw new Error(
'There was an error retrieving the projects. Please try again.'
);
});
},
};
export { projectAPI };
使用
useState
函数创建两个附加状态变量loading
和error
。
src\projects\ProjectsPage.tsx
...
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>(MOCK_PROJECTS);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | undefined>(undefined);
...
}
将
projects
状态更改为空数组[]
(请务必删除模拟数据)。
src\projects\ProjectsPage.tsx
- import { MOCK_PROJECTS } from './MockProjects';
...
function ProjectsPage() {
- const [projects, setProjects] = useState<Project[]>(MOCK_PROJECTS);
+ const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
...
}
在初始组件呈现在
useEffect
钩子中后实现从 API 加载数据。 请遵循这些规范。
loading
设置为 true
projectAPI.get(1)
data
设置为组件 projects
状态变量,并将 loading
状态变量设置为 false
error.message
设置为组件 error
状态,将 loading
设置为 false
src\projects\ProjectsPage.tsx
import React, { Fragment, useState,
+ useEffect } from 'react';
+ import { projectAPI } from './projectAPI';
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
//#region 方法1: 使用 promise then
// useEffect(() => {
// setLoading(true);
// projectAPI
// .get(1)
// .then((data) => {
// setError('');
// setLoading(false);
// setProjects(data);
// })
// .catch((e) => {
// setLoading(false);
// setError(e.message);
// if (e instanceof Error) {
// setError(e.message);
// }
// });
// }, []);
//#endregion
+ //#region 方法2: 使用 async/await
+ useEffect(() => {
+ async function loadProjects() {
+ setLoading(true);
+ try {
+ const data = await projectAPI.get(1);
+ setError('');
+ setProjects(data);
+ }
+ catch (e) {
+ if (e instanceof Error) {
+ setError(e.message);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+ loadProjects();
+ }, []);
+ //#endregion
...
}
在 <ProjectList />
下方显示加载指示器。仅在 loading=true
.
<></>
等价于<Fragment></Fragment>
src\projects\ProjectsPage.tsx
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
...
return (
<Fragment>
<h1>Projects</h1>
<ProjectList onSave={saveProject} projects={projects} />
+ {loading && (
+ <div className="center-page">
+ <span className="spinner primary"></span>
+ <p>Loading...</p>
+ </div>
+ )}
</Fragment>
);
}
export default ProjectsPage;
添加这
CSS
个样式以使加载指示器在页面上居中。
src\index.scss
... //add below existing styles
html,
body,
#root,
.container,
.center-page {
height: 100%;
}
.center-page {
display: flex;
justify-content: center;
align-items: center;
}
src\projects\ProjectsPage.tsx
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
...
return (
<>
<h1>Projects</h1>
+ {error && (
+ <div className="row">
+ <div className="card large error">
+ <section>
+ <p>
+ <span className="icon-alert inverse "></span>
+ {error}
+ </p>
+ </section>
+ </div>
+ </div>
+ )}
<ProjectList onSave={saveProject} projects={projects} />
{loading && (
<div className="center-page">
<span className="spinner primary"></span>
<p>Loading...</p>
</div>
)}
</>
);
}
export default ProjectsPage;
刷新页面, 短时间内会出现一个加载器
会发现一个 API 请求
我们在
projectAPI.get()
中使用delay
函数来延迟数据的返回,以便更容易看到加载指示器。 此时可以删除delay
。
src\projects\projectAPI.ts
return fetch(`${url}?_page=${page}&_limit=${limit}&_sort=name`)
- .then(delay(600))
.then(checkStatus)
.then(parseJSON);
更改 URL,以便无法访问 API 终结点。
src\projects\projectAPI.ts
const baseUrl = 'http://localhost:4000';
- const url = `${baseUrl}/projects`;
+ const url = `${baseUrl}/fail`;
...
此时再次测试, 会发现提示错误
修复后端 API 的 URL
src\projects\projectAPI.tsx
...
const baseUrl = 'http://localhost:4000';
+ const url = `${baseUrl}/projects`;
- const url = `${baseUrl}/fail`;
...
使用
useState
函数创建附加状态变量currentPage
src\projects\ProjectsPage.tsx
...
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
+ const [currentPage, setCurrentPage] = useState(1);
...
}
更新
useEffect
方法以使currentPage
成为依赖项,并在获取数据时使用它
src\projects\ProjectsPage.tsx
...
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
async function loadProjects() {
setLoading(true);
try {
- const data = await projectAPI.get(1);
+ const data = await projectAPI.get(currentPage);
- setProjects(data);
+ if (currentPage === 1) {
+ setProjects(data);
+ } else {
+ setProjects((projects) => [...projects, ...data]);
+ }
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
} finally {
setLoading(false);
}
}
loadProjects();
- }, []);
+ }, [currentPage]);
...
}
实现
handleMoreClick
事件处理程序,并通过递增页面然后调用loadProjects
来实现它
src\projects\ProjectsPage.tsx
...
function ProjectsPage() {
...
const [currentPage, setCurrentPage] = useState(1);
...
+ const handleMoreClick = () => {
+ setCurrentPage((currentPage) => currentPage + 1);
+ };
...
}
在
<ProjectList />
下方添加一个More...
按钮。 仅当不是loading
且没有error
时才显示More...
按钮, 并处理More...
按钮的click
事件并调用handleMoreClick
。
src\projects\ProjectsPage.tsx
...
function ProjectsPage() {
...
return (
<Fragment>
<h1>Projects</h1>
{error && (
<div className="row">
<div className="card large error">
<section>
<p>
<span className="icon-alert inverse "></span>
{error}
</p>
</section>
</div>
</div>
)}
<ProjectList onSave={saveProject} projects={projects} />
+ {!loading && !error && (
+ <div className="row">
+ <div className="col-sm-12">
+ <div className="button-group fluid">
+ <button className="button default" onClick={handleMoreClick}>
+ More...
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
{loading && (
<div className="center-page">
<span className="spinner primary"></span>
<p>Loading...</p>
</div>
)}
</Fragment>
);
}
export default ProjectsPage;
测试
More...
按钮More...
按钮在 API 对象中实现一个方法来执行 PUT(更新)
src\projects\projectAPI.ts
const projectAPI = {
...
+ put(project: Project) {
+ return fetch(`${url}/${project.id}`, {
+ method: 'PUT',
+ body: JSON.stringify(project),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(checkStatus)
+ .then(parseJSON)
+ .catch((error: TypeError) => {
+ console.log('log client error ' + error);
+ throw new Error(
+ 'There was an error updating the project. Please try again.'
+ );
+ });
+ },
};
src\projects\ProjectsPage.tsx
import { Project } from './Project';
...
function ProjectsPage() {
...
const saveProject = (project: Project) => {
- let updatedProjects = projects.map((p: Project) => {
- return p.id === project.id ? project : p;
- });
- setProjects(updatedProjects);
+ projectAPI
+ .put(project)
+ .then((updatedProject) => {
+ let updatedProjects = projects.map((p: Project) => {
+ return p.id === project.id ? new Project(updatedProject) : p;
+ });
+ setProjects(updatedProjects);
+ })
+ .catch((e) => {
+ if (e instanceof Error) {
+ setError(e.message);
+ }
+ });
};
return (
...
);
}
export default ProjectsPage;
测试
db.json
保存更新并不会重新排序: 于是按照 db.json
中顺序, 而保存更新后, 会保存到 db.json
最后 (×)更新后, 对应保存更新到
keeptrack/api/db.json
文件
创建 HomePage
组件。
src\home\HomePage.tsx
import React from 'react';
function HomePage() {
return <h2>Home</h2>;
}
export default HomePage;
运行以下命令以安装
React Router
:
yarn add react-router-dom@6.3
PS:
npm install react-router-dom@6.3
配置路由
src/App.tsx
import React from 'react';
import './App.css';
import ProjectsPage from './projects/ProjectsPage';
+ import { BrowserRouter as Router, Routes, Route, NavLink} from 'react-router-dom';
+ import HomePage from './home/HomePage';
function App() {
- return (
- <div className="container">
- <ProjectsPage />
- </div>
- );
+ return (
+ <Router>
+ <div className="container">
+ <Routes>
+ <Route path="/" element={<HomePage />} />
+ <Route path="/projects" element={<ProjectsPage />} />
+ </Routes>
+ </div>
+ </Router>
+ );
};
export default App;
修改 CSS 样式以包含导航菜单的一些自定义项。
src/App.scss
header {
height: 5.1875rem;
}
a.button.active {
border: 1px solid var(--fore-color);
}
...
添加两个
<NavLink>
组件(由 React 路由器提供)并将它们设置为访问配置的路由。
src/App.tsx
function App() {
return (
<Router>
+ <header className="sticky">
+ <span className="logo">
+ <img src="/assets/logo-3.svg" alt="logo" width="49" height="99" />
+ </span>
+ <NavLink to="/" className="button rounded">
+ <span className="icon-home"></span>
+ Home
+ </NavLink>
+ <NavLink to="/projects" className="button rounded">
+ Projects
+ </NavLink>
+ </header>
<div className="container">
...
</div>
</Router>
);
};
...
可以将任何
<a>
标记改为<NavLink>
,并添加to
属性以设置href
。测试 通过以下步骤验证路由是否正常工作:
http://localhost:3000/
并在浏览器中刷新页面Projects
/projects
路由和 ProjectsPage
显示Home
/
路由和 HomePage
显示将
find
方法添加到projectAPI
以返回单个Project
xid
src\projects\projectAPI.ts
const projectAPI = {
...
+ find(id: number) {
+ return fetch(`${url}/${id}`)
+ .then(checkStatus)
+ .then(parseJSON)
+ .then(convertToProjectModel);
+ },
+
...
};
创建下面的文件,并为我们将在本实验中使用的这些预构建组件添加代码。 花点时间查看其中的代码。
src\projects\ProjectDetail.tsx
import React from 'react';
import { Project } from './Project';
interface ProjectDetailProps {
project: Project;
}
export default function ProjectDetail({ project }: ProjectDetailProps) {
return (
<div className="row">
<div className="col-sm-6">
<div className="card large">
<img
className="rounded"
src={project.imageUrl}
alt={project.name}
/>
<section className="section dark">
<h3 className="strong">
<strong>{project.name}</strong>
</h3>
<p>{project.description}</p>
<p>Budget : {project.budget}</p>
<p>Signed: {project.contractSignedOn.toLocaleDateString()}</p>
<p>
<mark className="active">
{' '}
{project.isActive ? 'active' : 'inactive'}
</mark>
</p>
</section>
</div>
</div>
</div>
);
}
注意: 是
ProjectPage
,少了一个s
,是新文件, 代表单个项目 Project
src\projects\ProjectPage.tsx
import React, { useEffect, useState } from 'react';
import { projectAPI } from './projectAPI';
import ProjectDetail from './ProjectDetail';
import { Project } from './Project';
import { useParams } from 'react-router-dom';
function ProjectPage(props: any) {
const [loading, setLoading] = useState(false);
const [project, setProject] = useState<Project | null>(null);
const [error, setError] = useState<string | null>(null);
const params = useParams();
const id = Number(params.id);
useEffect(() => {
setLoading(true);
projectAPI
.find(id)
.then((data) => {
setProject(data);
setLoading(false);
})
.catch((e) => {
setError(e);
setLoading(false);
});
}, [id]);
return (
<div>
<>
<h1>Project Detail</h1>
{loading && (
<div className="center-page">
<span className="spinner primary"></span>
<p>Loading...</p>
</div>
)}
{error && (
<div className="row">
<div className="card large error">
<section>
<p>
<span className="icon-alert inverse "></span> {error}
</p>
</section>
</div>
</div>
)}
{project && <ProjectDetail project={project} />}
</>
</div>
);
}
export default ProjectPage;
添加一个路由以显示
ProjectPage
(请注意,我们现在有一个ProjectPage
和一个ProjectsPage
,所以请注意你在正确的文件中)。
src\App.tsx
import ProjectsPage from './projects/ProjectsPage';
+ import ProjectPage from './projects/ProjectPage';
function App() {
return (
<Router>
<header className="sticky">
<span className="logo">
<img src="/assets/logo-3.svg" alt="logo" width="49" height="99" />
</span>
<NavLink to="/" className="button rounded">
<span className="icon-home"></span>
Home
</NavLink>
<NavLink to="/projects/" className="button rounded">
Projects
</NavLink>
</header>
<div className="container">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage /> } />
+ <Route path="/projects/:id" element={<ProjectPage />} />
</Routes>
</div>
</Router>
);
}
通过在名称和描述周围添加
<Link />
组件,使它们可单击。
src\projects\ProjectCard.tsx
+ import { Link } from 'react-router-dom';
...
<section className="section dark">
+ <Link to={'/projects/' + project.id}>
<h5 className="strong">
<strong>{project.name}</strong>
</h5>
<p>{formatDescription(project.description)}</p>
<p>Budget : {project.budget.toLocaleString()}</p>
+ </Link>
<button
type="button"
className=" bordered"
onClick={() => {
handleEditClick(project);
}}
>
<span className="icon-edit "></span>
Edit
</button>
</section>
...
测试 通过以下步骤验证新路由是否正常工作:
http://localhost:3000/
并在浏览器中刷新页面Projects
/projects
路由和 ProjectsPage
显示/projects/1
路由,并且 ProjectPage
显示 ProjectDetail
组件运行以下命令以安装名为
serve
的 Node.js Web 服务器:
# yarn 全局安装 serve
yarn global add serve
# npm 全局安装 serve
npm install -g serve
构建
yarn build
build 完成后,验证是否已创建
keeptrack\build
目录
运行以下命令以启动 Web 服务器并提供在上一步中创建的
build
目录的内容
serve build
假设你想要提供单页应用程序或仅提供静态文件(无论是在你的设备上还是在本地网络上), 包
serve
是提供静态内容的 Web 服务器。
测试
PROJECTS
, 导航过去, 并再次点击其中一个项目,发现一切正常显示404
/
, 其它刷新页面(或通过 url 直接访问),都将会 404
,而通过路由导航的方式就正常Ctrl+C
停止 Web 服务器-s
标志serve -s build
http://localhost:5000/
404
yarn add redux react-redux redux-devtools-extension redux-thunk
yarn add --dev @types/react-redux
PS:
# npm
npm install redux react-redux redux-devtools-extension redux-thunk
npm install --save-dev @types/react-redux
完成后, 打开
package.json
文件, 验证dependencies
和devDependencies
src\state.ts
import { createStore, applyMiddleware } from "redux";
import ReduxThunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import { combineReducers } from "redux";
const reducer = combineReducers({});
export default function configureStore(preloadedState: any) {
const middlewares = [ReduxThunk];
const middlewareEnhancer = applyMiddleware(...middlewares);
//Thunk is middleware
//DevTools is an enhancer (actually changes Redux)
//applyMiddleware wraps middleware and returns an enhancer
// to use only thunk middleware
// const enhancer = compose(middlewareEnhancer);
//to use thunk & devTools
const enhancer = composeWithDevTools(middlewareEnhancer);
const store = createStore(reducer, preloadedState, enhancer);
return store;
}
export interface AppState {}
export const initialAppState: AppState = {};
export const store = configureStore(initialAppState);
测试编译是否成功
定义项目
actions types
、action interfaces
和state
src\projects\state\projectTypes.ts
import { Project } from '../Project';
//action types
export const LOAD_PROJECTS_REQUEST = 'LOAD_PROJECTS_REQUEST';
export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS';
export const LOAD_PROJECTS_FAILURE = 'LOAD_PROJECTS_FAILURE';
export const SAVE_PROJECT_REQUEST = 'SAVE_PROJECT_REQUEST';
export const SAVE_PROJECT_SUCCESS = 'SAVE_PROJECT_SUCCESS';
export const SAVE_PROJECT_FAILURE = 'SAVE_PROJECT_FAILURE';
export const DELETE_PROJECT_REQUEST = 'DELETE_PROJECT_REQUEST';
export const DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS';
export const DELETE_PROJECT_FAILURE = 'DELETE_PROJECT_FAILURE';
interface LoadProjectsRequest {
type: typeof LOAD_PROJECTS_REQUEST;
}
interface LoadProjectsSuccess {
type: typeof LOAD_PROJECTS_SUCCESS;
payload: { projects: Project[]; page: number };
}
interface LoadProjectsFailure {
type: typeof LOAD_PROJECTS_FAILURE;
payload: { message: string };
}
interface SaveProjectRequest {
type: typeof SAVE_PROJECT_REQUEST;
}
interface SaveProjectSuccess {
type: typeof SAVE_PROJECT_SUCCESS;
payload: Project;
}
interface SaveProjectFailure {
type: typeof SAVE_PROJECT_FAILURE;
payload: { message: string };
}
interface DeleteProjectRequest {
type: typeof DELETE_PROJECT_REQUEST;
}
interface DeleteProjectSuccess {
type: typeof DELETE_PROJECT_SUCCESS;
payload: Project;
}
interface DeleteProjectFailure {
type: typeof DELETE_PROJECT_FAILURE;
payload: { message: string };
}
export type ProjectActionTypes =
| LoadProjectsRequest
| LoadProjectsSuccess
| LoadProjectsFailure
| SaveProjectRequest
| SaveProjectSuccess
| SaveProjectFailure
| DeleteProjectRequest
| DeleteProjectSuccess
| DeleteProjectFailure;
export interface ProjectState {
loading: boolean;
projects: Project[];
error: string | undefined;
page: number;
}
定义操作创建器函数并返回
ThunkAction
(function
) 而不仅仅是Action
(object
) 来处理 HTTP 调用的异步性质。
src\projects\state\projectActions.ts
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { projectAPI } from '../projectAPI';
import { Project } from '../Project';
import {
LOAD_PROJECTS_REQUEST,
LOAD_PROJECTS_SUCCESS,
LOAD_PROJECTS_FAILURE,
SAVE_PROJECT_REQUEST,
SAVE_PROJECT_SUCCESS,
SAVE_PROJECT_FAILURE,
ProjectState,
} from './projectTypes';
//action creators
export function loadProjects(
page: number
): ThunkAction<void, ProjectState, null, Action<string>> {
return (dispatch: any) => {
dispatch({ type: LOAD_PROJECTS_REQUEST });
return projectAPI
.get(page)
.then((data) => {
dispatch({
type: LOAD_PROJECTS_SUCCESS,
payload: { projects: data, page },
});
})
.catch((error) => {
dispatch({ type: LOAD_PROJECTS_FAILURE, payload: error });
});
};
}
export function saveProject(
project: Project
): ThunkAction<void, ProjectState, null, Action<string>> {
return (dispatch: any) => {
dispatch({ type: SAVE_PROJECT_REQUEST });
return projectAPI
.put(project)
.then((data) => {
dispatch({ type: SAVE_PROJECT_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: SAVE_PROJECT_FAILURE, payload: error });
});
};
}
src\projects\state\projectReducer.ts
import {
ProjectActionTypes,
LOAD_PROJECTS_REQUEST,
LOAD_PROJECTS_SUCCESS,
LOAD_PROJECTS_FAILURE,
DELETE_PROJECT_REQUEST,
DELETE_PROJECT_SUCCESS,
DELETE_PROJECT_FAILURE,
SAVE_PROJECT_REQUEST,
SAVE_PROJECT_SUCCESS,
SAVE_PROJECT_FAILURE,
ProjectState,
} from './projectTypes';
import { Project } from '../Project';
export const initialProjectState: ProjectState = {
projects: [],
loading: false,
error: undefined,
page: 1,
};
export function projectReducer(
state = initialProjectState,
action: ProjectActionTypes
) {
switch (action.type) {
case LOAD_PROJECTS_REQUEST:
return { ...state, loading: true, error: '' };
case LOAD_PROJECTS_SUCCESS:
let projects: Project[];
const { page } = action.payload;
if (page === 1) {
projects = action.payload.projects;
} else {
projects = [...state.projects, ...action.payload.projects];
}
return {
...state,
loading: false,
page,
projects,
error: '',
};
case LOAD_PROJECTS_FAILURE:
return { ...state, loading: false, error: action.payload.message };
case SAVE_PROJECT_REQUEST:
return { ...state };
case SAVE_PROJECT_SUCCESS:
if (action.payload.isNew) {
return {
...state,
projects: [...state.projects, action.payload],
};
} else {
return {
...state,
projects: state.projects.map((project: Project) => {
return project.id === action.payload.id
? Object.assign({}, project, action.payload)
: project;
}),
};
}
case SAVE_PROJECT_FAILURE:
return { ...state, error: action.payload.message };
case DELETE_PROJECT_REQUEST:
return { ...state };
case DELETE_PROJECT_SUCCESS:
return {
...state,
projects: state.projects.filter(
(project: Project) => project.id !== action.payload.id
),
};
case DELETE_PROJECT_FAILURE:
return { ...state, error: action.payload.message };
default:
return state;
}
}
配置
projectReducer
和ProjectState
src\state.ts
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { combineReducers } from 'redux';
+ import { ProjectState } from './projects/state/projectTypes';
+ import { initialProjectState } from './projects/state/projectReducer';
+ import { projectReducer } from './projects/state/projectReducer';
const reducer = combineReducers({
+ projectState: projectReducer
});
...
export interface AppState {
+ projectState: ProjectState;
}
export const initialAppState: AppState = {
+ projectState: initialProjectState
};
export const store = configureStore(initialAppState);
测试 验证应用程序是否已编译成功
dispatch an action
)删除页面(容器)组件的本地状态,并使用 useSelector 替换为 Redux 状态。 此外,使用 useDispatch 获取对 store 的调度函数的引用,以便我们可以调度操作。
确保你在 ProjectsPage.tsx 而不是 ProjectPage.tsx 中
src\projects\ProjectsPage.tsx
- import React, { useState, useEffect } from 'react';
+ import React, { useEffect } from 'react';
import ProjectList from './ProjectList';
import { Project } from './Project';
+ import { useSelector, useDispatch } from 'react-redux';
+ import { AppState } from '../state';
function ProjectsPage() {
- const [projects, setProjects] = useState<Project[]>([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | undefined>(undefined);
- const [currentPage, setCurrentPage] = useState(1);
+ const loading = useSelector(
+ (appState: AppState) => appState.projectState.loading
+ );
+ const projects = useSelector(
+ (appState: AppState) => appState.projectState.projects
+ );
+ const error = useSelector(
+ (appState: AppState) => appState.projectState.error
+ );
+ const currentPage = useSelector(
+ (appState: AppState) => appState.projectState.page
+ );
+ const dispatch = useDispatch<ThunkDispatch<ProjectState, any, AnyAction>>();
...
}
上面写完后, 会有一些报错, 先无视
将状态 setter 函数调用和 API 调用替换为调度传递操作创建者的调用。 此外,删除
onSave
函数并停止将其作为 props 传递给<ProjectList/>
组件。
src\projects\ProjectsPage.tsx
...
- import { Project } from './Project';
- import { projectAPI } from './projectAPI';
+ import { loadProjects } from './state/projectActions';
+ import { AnyAction } from 'redux';
+ import { ThunkDispatch } from 'redux-thunk';
+ import { ProjectState } from './state/projectTypes';
function ProjectsPage() {
...
+ const dispatch = useDispatch<ThunkDispatch<ProjectState, any, AnyAction>>();
- useEffect(() => {
- setLoading(true);
- projectAPI
- .get(currentPage)
- .then((data) => {
- setLoading(false);
- if (currentPage === 1) {
- setProjects(data);
- } else {
- setProjects((projects) => [...projects, ...data]);
- }
- })
- .catch((e) => {
- setLoading(false);
- if (e instanceof Error) {
- setError(e.message);
- }
- });
- }, [currentPage]);
+ useEffect(() => {
+ dispatch(loadProjects(1));
+ }, [dispatch]);
const handleMoreClick = () => {
- setCurrentPage((currentPage) => currentPage + 1);
+ dispatch(loadProjects(currentPage + 1));
};
- const saveProject = (project: Project) => {
- projectAPI
- .put(project)
- .then((updatedProject) => {
- let updatedProjects = projects.map((p: Project) => {
- return p.id === project.id ? project : p;
- });
- setProjects(updatedProjects);
- })
- .catch((e) => {
- if (e instanceof Error) {
- setError(e.message);
- }
- });
- };
return (
<Fragment>
...
- <ProjectList onSave={saveProject} projects={projects} />
+ <ProjectList projects={projects} />
...
</Fragment>
);
}
...
改完后, 会有报错:
Property 'onSave' is missing in type
, 先无视提供 store
src\App.tsx
import ProjectPage from './projects/ProjectPage';
+ import { Provider } from 'react-redux';
+ import { store } from './state';
function App() {
return (
+ <Provider store={store}>
<Router>
<header className="sticky">
<span className="logo">
<img src="/assets/logo-3.svg" alt="logo" width="49" height="99" />
</span>
<NavLink to="/" className="button rounded">
<span className="icon-home"></span>
Home
</NavLink>
<NavLink to="/projects/" className="button rounded">
Projects
</NavLink>
</header>
<div className="container">
<Routes>
<Route path="/" component={HomePage} />
<Route path="/projects" component={ProjectsPage} />
<Route path="/projects/:id" component={ProjectPage} />
</Routes>
</div>
</Router>
+ </Provider>
);
};
export default App;
重构 Form 组件,使其调度
saveProject
操作,而不是将函数作为 prop 接收。请务必在下一个代码块中包含用于调用 useDispatch 挂钩的行,如下所示
const dispatch = useDispatch();
。 如果不这样做,编辑器可能会实现一个名为dispatch
的占位符方法,该方法会引发错误。
src\projects\ProjectForm.tsx
import React, { SyntheticEvent, useState } from 'react';
import { Project } from './Project';
+ import { useDispatch } from 'react-redux';
+ import { saveProject } from './state/projectActions';
+ import { ThunkDispatch } from 'redux-thunk';
+ import { ProjectState } from './state/projectTypes';
+ import { AnyAction } from 'redux';
interface ProjectFormProps {
project: Project;
- onSave: (project: Project) => void;
onCancel: () => void;
}
function ProjectForm({
project: initialProject,
- onSave,
onCancel,
}: ProjectFormProps) {
const [project, setProject] = useState(initialProject);
const [errors, setErrors] = useState({
name: '',
description: '',
budget: '',
});
+ const dispatch = useDispatch<ThunkDispatch<ProjectState, any, AnyAction>>();
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
if (!isValid()) return;
- onSave(project);
+ dispatch(saveProject(project));
};
const handleChange = (event: any) => {
...
};
function validate(project: Project) {
...
}
function isValid() {
...
}
return (
<form className="input-group vertical" onSubmit={handleSubmit}>
...
</form>
);
}
export default ProjectForm;
提供 store
src\App.tsx
中完成,因为它继承自父页面组件:页面 =>List=>Form在
ProjectList
组件中, 删除ProjectListProps
接口中的onSave
并将组件更新为不传递onSave
到<ProjectForm>
, 因为它现在在导入此操作后自行调度此操作。
src\Projects\ProjectList.tsx
import React, { useState } from 'react';
import { Project } from './Project';
import ProjectCard from './ProjectCard';
import ProjectForm from './ProjectForm';
interface ProjectListProps {
projects: Project[];
- onSave: (project: Project) => void;
}
- function ProjectList({ projects, onSave }: ProjectListProps) {
+ function ProjectList({ projects }: ProjectListProps) {
const [projectBeingEdited, setProjectBeingEdited] = useState({});
const handleEdit = (project: Project) => {
setProjectBeingEdited(project);
};
const cancelEditing = () => {
setProjectBeingEdited({});
};
return (
<div className="row">
{projects.map((project) => (
<div key={project.id} className="cols-sm">
{project === projectBeingEdited ? (
<ProjectForm project={project}
- onSave={onSave}
onCancel={cancelEditing} />
) : (
<ProjectCard project={project} onEdit={handleEdit} />
)}
</div>
))}
</div>
);
}
export default ProjectList;
测试 验证应用程序是否仍然有效,包括加载和更新项目
React Testing Library
yarn test
npm
npm test
npm run test
按
a
运行所有测试
按
w
查看更多, 按q
退出
src/App.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
- test('renders learn react link', () => {
- const { getByText } = render(<App />);
- const linkElement = getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
+ test('should render without crashing', () => {
+ render(<App />);
+ });
验证测试现在是否通过
yarn test
创建文件
src\home\HomePage.test.tsx
添加测试以验证组件浅层渲染而不会崩溃
src\home\HomePage.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import HomePage from './HomePage';
test('renders home heading', () => {
render(<HomePage />);
expect(screen.getByRole('heading')).toHaveTextContent('Home');
});
验证测试是否通过
yarn test
yarn add --dev react-test-renderer
yarn add --dev @types/react-test-renderer
npm
npm i react-test-renderer --save-dev
npm i @types/react-test-renderer --save-dev
为组件添加快照测试。在套件中组织测试(描述函数)
src\home\HomePage.test.tsx
import React from 'react';
import HomePage from './HomePage';
+ import renderer from 'react-test-renderer';
+ describe('<HomePage />', () => {
test('should render without crashing', () => {
render(<HomePage />);
});
+ test('snapshot', () => {
+ const tree = renderer.create(<HomePage />).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ });
验证快照是否已创建
yarn test
打开
src\home\__snapshots__\HomePage.test.tsx.snap
并查看内容
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<HomePage /> snapshot 1`] = `
<h2>
Home
</h2>
`;
创建目录
src\projects\__tests__
创建文件
src\projects\__tests__\ProjectCard-test.tsx
添加下面的设置代码以测试组件
src\projects\__tests__\ProjectCard-test.tsx
import { render, screen } from "@testing-library/react";
import React from "react";
import { Project } from "../Project";
import ProjectCard from "../ProjectCard";
describe("<ProjectCard />", () => {
let project: Project;
let handleEdit: jest.Mock;
beforeEach(() => {
project = new Project({
id: 1,
name: "Mission Impossible",
description: "This is really difficult",
budget: 100,
});
handleEdit = jest.fn();
});
it("should initially render", () => {
render(<ProjectCard project={project} onEdit={handleEdit} />);
});
});
验证测试是否失败
yarn test
将组件包装在
MemoryRouter
中
import { render, screen } from '@testing-library/react';
import React from 'react';
+ import { MemoryRouter } from 'react-router-dom';
import { Project } from '../Project';
import ProjectCard from '../ProjectCard';
describe('<ProjectCard />', () => {
let project: Project;
let handleEdit: jest.Mock;
beforeEach(() => {
project = new Project({
id: 1,
name: 'Mission Impossible',
description: 'This is really difficult',
budget: 100,
});
handleEdit = jest.fn();
});
it('should initially render', () => {
render(
+ <MemoryRouter>
<ProjectCard project={project} onEdit={handleEdit} />
+ </MemoryRouter>
);
});
});
<MemoryRouter>
- 是一个<Router>
,用于将“URL”的历史记录保存在内存中(不读取或写入地址栏)。 在测试和非浏览器环境(如 React Native)中很有用。验证初始测试现在是否通过
yarn test
PS: 可以不退出
yarn test
, 保持在后台运行,会自动监控代码更新,重新运行
打开命令提示符或终端并运行以下命令,从 React 测试库后面的核心测试库中安装
user-event
。
yarn remove @testing-library/user-event
yarn add --dev @testing-library/user-event @testing-library/dom
测试项目属性是否正确呈现。
src\projects\__tests__\ProjectCard-test.tsx
...
describe('<ProjectCard />', () => {
let project: Project;
let handleEdit: jest.Mock;
beforeEach(() => {
project = new Project({
id: 1,
name: 'Mission Impossible',
description: 'This is really difficult',
budget: 100,
});
handleEdit = jest.fn();
});
it('should initially render', () => {
render(
<MemoryRouter>
<ProjectCard project={project} onEdit={handleEdit} />
</MemoryRouter>
);
});
+ it('renders project properly', () => {
+ render(
+ <MemoryRouter>
+ <ProjectCard project={project} onEdit={handleEdit} />
+ </MemoryRouter>
+ );
+ expect(screen.getByRole('heading')).toHaveTextContent(project.name);
+ // screen.debug(document);
+ screen.getByText(/this is really difficult\.\.\./i);
+ screen.getByText(/budget : 100/i);
+ });
});
验证测试是否通过
src\projects\__tests__\ProjectCard-test.tsx
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Project } from '../Project';
import ProjectCard from '../ProjectCard';
+ import userEvent from '@testing-library/user-event';
describe('<ProjectCard />', () => {
let project: Project;
let handleEdit: jest.Mock;
beforeEach(() => {
project = new Project({
id: 1,
name: 'Mission Impossible',
description: 'This is really difficult',
budget: 100,
});
handleEdit = jest.fn();
});
it('should initially render', () => {
render(
<MemoryRouter>
<ProjectCard project={project} onEdit={handleEdit} />
</MemoryRouter>
);
});
it('renders project properly', () => {
render(
<MemoryRouter>
<ProjectCard project={project} onEdit={handleEdit} />
</MemoryRouter>
);
expect(screen.getByRole('heading')).toHaveTextContent(project.name);
// screen.debug(document);
screen.getByText(/this is really difficult\.\.\./i);
screen.getByText(/budget : 100/i);
});
+ it('handler called when edit clicked', async () => {
+ render(
+ <MemoryRouter>
+ <ProjectCard project={project} onEdit={handleEdit} />
+ </MemoryRouter>
+ );
+ // this query works screen.getByText(/edit/i)
+ // but using role is better
+ const user = userEvent.setup();
+ await user.click(screen.getByRole('button', { name: /edit/i }));
+ expect(handleEdit).toBeCalledTimes(1);
+ expect(handleEdit).toBeCalledWith(project);
+ });
});
验证测试是否通过
重构
ProjectCard-test.tsx
以使用setup
函数在每个测试开始时呈现组件。
import { render, screen } from "@testing-library/react";
import React from "react";
import { MemoryRouter } from "react-router-dom";
import { Project } from "../Project";
import ProjectCard from "../ProjectCard";
import userEvent from "@testing-library/user-event";
import renderer from "react-test-renderer";
describe("<ProjectCard />", () => {
let project: Project;
let handleEdit: jest.Mock;
const setup = () =>
render(
<MemoryRouter>
<ProjectCard project={project} onEdit={handleEdit} />
</MemoryRouter>
);
beforeEach(() => {
project = new Project({
id: 1,
name: "Mission Impossible",
description: "This is really difficult",
budget: 100,
});
handleEdit = jest.fn();
});
it("should initially render", () => {
setup();
});
it("renders project properly", () => {
setup();
expect(screen.getByRole("heading")).toHaveTextContent(project.name);
// screen.debug(document);
screen.getByText(/this is really difficult\.\.\./i);
screen.getByText(/budget : 100/i);
});
it("handler called when edit clicked", async () => {
setup();
// this query works screen.getByText(/edit/i)
// but using role is better
// eslint-disable-next-line testing-library/render-result-naming-convention
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /edit/i }));
expect(handleEdit).toBeCalledTimes(1);
expect(handleEdit).toBeCalledWith(project);
});
test("snapshot", () => {
const tree = renderer
.create(
<MemoryRouter>
<ProjectCard project={project} onEdit={handleEdit} />
</MemoryRouter>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
为什么不直接将设置代码放在
beforeEach
中?请参阅 此 ESLint 规则 以获取 react-testing-library。
拍摄组件的快照。
src\projects\__tests__\ProjectCard-test.tsx
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Project } from '../Project';
import ProjectCard from '../ProjectCard';
import userEvent from '@testing-library/user-event';
+ import renderer from 'react-test-renderer';
describe('<ProjectCard />', () => {
let project: Project;
let handleEdit: jest.Mock;
beforeEach(() => {
...
});
...
+ test('snapshot', () => {
+ const tree = renderer
+ .create(
+ <MemoryRouter>
+ <ProjectCard project={project} onEdit={handleEdit} />
+ </MemoryRouter>
+ )
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
});
验证是否已拍摄快照。
src\projects\__tests__\__snapshots__\ProjectCard-test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ProjectCard /> snapshot 1`] = `
<div
className="card"
>
<img
alt="Mission Impossible"
src=""
/>
<section
className="section dark"
>
<a
href="/projects/1"
onClick={[Function]}
>
<h5
className="strong"
>
<strong>
Mission Impossible
</strong>
</h5>
<p>
This is really difficult...
</p>
<p>
Budget :
100
</p>
</a>
<button
className="bordered"
onClick={[Function]}
>
<span
className="icon-edit"
/>
Edit
</button>
</section>
</div>
`;
https://handsonreact.com/docs/labs/ts/T4-NestedComponents
App.test.tsx
: Property 'toBeInTheDocument' does not exist on type 'JestMatchers<HTMLElement>'.
参考:
yarn start
ERROR in src/App.test.tsx:8:23
TS2339: Property 'toBeInTheDocument' does not exist on type 'JestMatchers<HTMLElement>'.
6 | render(<App />);
7 | const linkElement = screen.getByText(/learn react/i);
> 8 | expect(linkElement).toBeInTheDocument();
| ^^^^^^^^^^^^^^^^^
9 | });
10 |
yarn remove @testing-library/jest-dom
yarn add -D @testing-library/jest-dom@^4.2.4
经过测试, 上方没有解决, 只能暂时关闭提示/注释
默认为
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
},
最终解决, 安装下方版本即可
yarn add --dev @testing-library/jest-dom@^5.16.4
package.json
: devDependencies
测试库非应用 build 后所需,仅开发环境需要建议放入 devDependencies
中
yarn add --dev @testing-library/react
# 等同于 下方
yarn add -D @testing-library/react
# npm
npm install --save-dev @testing-library/react
Usage Error: The 'yarn global' commands have been removed in 2.x
参考:
🦄 yarn global add serve
Usage Error: The 'yarn global' commands have been removed in 2.x - consider using 'yarn dlx' or a third-party plugin instead
$ yarn run [--inspect] [--inspect-brk] [-T,--top-level] [-B,--binaries-only] <scriptName> ...
'createStore' is deprecated
参考:
'createStore' is deprecated.ts(6385)
index.d.ts(375, 4): The declaration was marked as deprecated here.
(alias) function createStore<S, A extends Action<any>, Ext, StateExt>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<Ext, StateExt>): Store<S & StateExt, A> & Ext (+1 overload)
import createStore
@deprecated
We recommend using the configureStore
method of the @reduxjs/toolkit
package, which replaces createStore
.
Redux Toolkit is our recommended approach for writing Redux logic today, including store setup, reducers, data fetching, and more.
For more details, please read this Redux docs page: https://redux.js.org/introduction/why-rtk-is-redux-today
configureStore
from Redux Toolkit is an improved version of createStore
that simplifies setup and helps avoid common bugs.
You should not be using the redux
core package by itself today, except for learning purposes. The createStore
method from the core redux
package will not be removed, but we encourage all users to migrate to using Redux Toolkit for all Redux code.
If you want to use createStore
without this visual deprecation warning, use the legacy_createStore
import instead:
import { legacy_createStore as createStore} from 'redux'
thunk
?参考:
其实
thunk
是函数编程届的一个专有名词,主要用于 calculation delay,也就是延迟计算。
用代码演示如下:
function wrapper(arg) {
// 内部返回的函数就叫`thunk`
return function thunk() {
console.log('thunk running, arg: ', arg)
}
}
// 我们通过调用wrapper来获得thunk
const thunk = wrapper('wrapper arg')
// 然后在需要的地方再去执行thunk
thunk()
yarn
参考:
Node.js >=16.10 默认提供 npm 包管理器,Corepack 为您提供 Yarn 和 pnpm,而无需安装它们。
corepack enable
corepack prepare yarn@stable --activate
🦄 corepack enable
Internal Error: EPERM: operation not permitted, open 'D:\Program Files\nodejs\pnpm'
Error: EPERM: operation not permitted, open 'D:\Program Files\nodejs\pnpm'
解决, 用管理员权限执行
create-react-app
重写 webpack
配置参考:
参考:
项目级安装 prettier
yarn add --dev --exact prettier
创建一个空的配置文件, 以便让编辑器和其它工具知道你使用 prettier
echo {}> .prettierrc.json
创建一个 .prettierignore 文件,让 Prettier CLI 和编辑器知道哪些文件不格式化。 下面是一个示例:
.prettierignore
# Ignore artifacts:
build
coverage
使用
Prettier CLI
格式化文件
yarn prettier
运行本地安装的 Prettier 版本prettier --write .
格式化所有内容prettier --write app/
格式化 app/
目录yarn prettier --write .
如果您有 CI 设置,请运行以下命令作为其中的一部分,以确保每个人都运行更漂亮。这避免了合并冲突和其他协作问题!
--check
类似于--write
,但仅检查文件是否已格式化,而不是覆盖它们。prettier --write
和prettier --check
是运行 Prettier 的最常见方法。
npx prettier --check .
注意: 不要跳过常规的本地(项目级)安装! 编辑器插件将选取你的本地版本 Prettier,确保您在每个项目中使用正确的版本。 (你不希望编辑器意外地引起大量更改,因为当没有本地安装 prettier 时, 就会使用编辑器扩展自带的 prettier) 能够从命令行运行 Prettier 仍然是一个很好的后备,并且是 CI/CD 所必需的。
参考:
Visual Studio Code
感谢帮助!
React Router