React 扩展 2
1.redux-saga
基本介绍
Redux Saga 是一个用于处理副作用(side effect)的库,特别适用于与 Redux 配合使用。副作用包括异步操作、访问浏览器缓存、调用 API、处理定时器等。Redux Saga 提供了一种优雅的方式来管理和协调这些副作用。
Redux Saga 使用 Generator 函数(ES6 中的一种特殊函数)来定义副作用的流程。Generator 函数可以通过暂停和恢复的方式控制流程的执行,与普通函数不同,它可以被中途暂停并在需要时恢复执行。
使用 Redux Saga,你可以创建叫做 "sagas" 的 Generator 函数,用于监听 Redux store 中的特定 action,并执行相应的副作用操作。这些 sagas 可以用于处理异步操作、捕获错误、派发新的 action、执行条件逻辑等。
以下是 Redux Saga 的一些关键概念:
- Effect:Effect 是一个简单的 JavaScript 对象,表示要在 saga 中执行的操作。Redux Saga 提供了各种内置的 Effect,例如
take
(监听 action)、put
(派发 action)、call
(调用函数)等。通过组合和嵌套这些 Effect,可以构建复杂的副作用流程。 - Saga:Saga 是一个 Generator 函数,用于定义副作用的处理逻辑。Saga 监听特定的 action,并在满足条件时执行相应的副作用操作。Saga 可以使用各种 Effect 来描述副作用的执行顺序和流程。
- Middleware:Redux Saga 使用中间件来集成到 Redux 中。中间件拦截发起的 action,并将其传递给 Saga 进行处理。Redux Saga 提供了一个
redux-saga
中间件,用于将 saga 注入到 Redux 应用程序中。
Redux Saga 的优点包括:
- 代码清晰易读:使用 Generator 函数来定义副作用的流程,使代码更加清晰和易于理解。
- 可测试性:Saga 的逻辑可以独立于组件进行测试,方便编写单元测试。
- 可组合性:Saga 可以轻松组合和重用,使得处理复杂的异步流程变得更加简单和可维护。
- 高度可控:通过取消(cancel)操作,可以控制和管理异步操作的生命周期。
- 错误处理:可以捕获和处理副作用操作中的错误,避免影响应用程序的整体流程。
总的来说,Redux Saga 是一个强大且灵活的副作用管理库,它能够与 Redux 无缝集成,帮助开发人员更好地处理异步操作和副作用,并提供一种可预测和可控的方式来管理应用程序的状态和流程。
理解:其实就是代替 redux-thunk 和 redux-promise 中间件对于异步 action 的解决方案,非侵入式地解决异步 action,让 action 始终都是一个朴素的对象,但依然可以实现异步 action!——> saga 适合大型项目,更加强大
图解
在 saga 中,全局监听器和接收器使用 Generator 函数和 saga 自身的一些辅助函数实现对整个流程的管控
前置知识:ES6 的 Generator 生成器函数
ES6 的 Generator 是一种特殊类型的函数,它具有暂停和恢复执行的能力。它使用 function*
声明语法定义,并使用 yield
关键字来控制函数的执行流程。
以下是 Generator 函数的一些关键概念和特性:
- 声明和执行:Generator 函数使用
function*
声明语法进行定义。与普通函数不同的是,在调用 Generator 函数时,并不会立即执行函数体,而是返回一个称为 Generator 对象的迭代器。 - 暂停和恢复:Generator 函数的执行过程可以通过
yield
关键字来暂停和恢复。**当yield
被执行时,函数会暂停,并将一个值返回给调用者。**在下一次调用next()
方法时,函数会从上次暂停的地方继续执行。 - 迭代器接口:Generator 对象实现了迭代器接口,可以使用
next()
方法逐步遍历函数体中的每个yield
表达式,并获取函数的返回值。 - 控制流程:在 Generator 函数内部,可以使用控制流程的结构,如条件语句(if-else)、循环语句(for、while)、异常处理等。这使得可以根据特定的条件决定是否暂停函数的执行或跳转到特定的代码块。
- 双向通信:**除了从 Generator 函数中获取值外,还可以通过
next()
方法向 Generator 函数内部发送值。**这种双向通信可以实现更灵活的控制流程和数据传递。
Generator 函数的主要优点是它提供了一种简洁而强大的方式来处理异步操作、迭代算法和状态机等复杂的控制流程。它能够将复杂的问题分解成多个步骤,并以可读性高的方式表达。
下面是一个简单的示例,展示了如何定义和使用 Generator 函数:
function* test() {
console.log("111111");
var input1 = yield "111-输出"; //注意:input1 是我们收到外面传的值,而 yield 后面的值 "111-输出" 是我们给外面的值!
console.log("22222", input1); //22222 aaaa
var input2 = yield "222-输出";
console.log("333333", input2); //333333 bbbb
var input3 = yield "333-输出";
console.log("444444", input3); //444444 ccccc
}
var kerwintest = test(); //第一次运行生成器,会创建迭代器,其他什么都不干
console.log(kerwintest); //Object [Generator] {}
//后面每当 调用next方法时 就会把next 的参数先传给当前所处的 yield,然后从当前位置开始向下寻找直到(再次)遇到 yield,就运行 yield 后面的函数,并返回yield的结果并在当前位置停下来 ——> 注意接收next参数的时机
var res1 = kerwintest.next();
console.log(res1); //返回一个对象{value:"111-输出", done:false}
var res2 = kerwintest.next("aaaa"); //可以传值,里面可以收到值
console.log(res2); //{value:"222-输出", done:false}
var res3 = kerwintest.next("bbbb");
console.log(res3); //{value:"333-输出", done:false}
var res4 = kerwintest.next("ccccc");
console.log(res4); //{value:undefined, done:true} //代表生成器没有内容了
在上述示例中,myGenerator()
是一个 Generator 函数,通过 yield
关键字定义了三个步骤。通过调用 next()
方法,我们可以逐步遍历 Generator 函数并获取每个 yield
表达式的值。
总的来说,ES6 的 Generator 函数是一种强大的功能,它提供了一种处理复杂控制流程和状态机的方式,并且在异步编程中具有重要的应用价值。
注意:ES7 中的 async 和 await 实际上就是对于 Generator 的一种升级!是它的语法糖!
——> 对于异步请求的处理来说,一般就是解决链式的 ajax(后一个请求需要前一个请求的返回值做参数)
一个最简单的不合理的链式异步请求示例:
function* test1() {
setTimeout(() => {
console.log("11111-success");
kerwintest1.next(); //在第一个成功的回调执行第二个
}, 1000);
yield; //遇到 yield 暂停
setTimeout(() => {
console.log("222222-success");
kerwintest1.next(); //在第二个成功的回调执行第三个
}, 1000);
yield;
setTimeout(() => {
console.log("3333-success");
}, 1000);
yield;
}
var kerwintest1 = test1();
kerwintest1.next();
缺点:返回值不好维护把控,内部调用外部变量:耦合性大,可读性可维护性不好
可执行生成器:
自动化地进行 ajax 链式调用,最终得到一个返回值
function getData1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("data1");
}, 1000);
});
}
function getData2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("data2");
}, 1000);
});
}
function getData3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("data3");
}, 1000);
});
}
//生成器函数
function* gen() {
var f1 = yield getData1(); //运行getData1返回 res 给外面,f1 在下一次 next 的时候通过参数传进来
console.log(f1);
var f2 = yield getData2(f1); //把上面接收到的f1 用在这里作为getData2请求的参数
console.log(f2);
var f3 = yield getData3(f2); //把接收到的f2 用在这里作为getData3请求的参数
console.log(f3);
}
//封装的链式 ajax 运行入口
function run(fn) {
var g = fn(); //创建迭代器
//构造一个递归函数执行 next 方法
function next(data) {
var result = g.next(data); //请求返回的res
if (result.done) {
//如果执行完了,就直接返回结果
return result.value;
}
result.value.then((res) => {
//如果没有执行完,那么把 res 给到 next 继续向下执行
next(res); //递归调用自己
});
}
next();
}
var res = run(gen);
其实就类似于 async 和 await 的效果了,我们用 async 和 await 的时候只是不用写 run 这个封装方法。
注意:
Async/Await 不是基于 Generator 函数实现的,它们是两种独立的语言特性。在底层实现上,Async/Await 基于 Promise,而 Generator 函数基于迭代器(Iterator)和生成器(Generator)协议。
saga 基本使用
npm i redux-saga
reducer.js
function reducer(
prevState = {
list1: [],
},
action = {}
) {
var newState = { ...prevState };
switch (action.type) {
case "change-list1":
newState.list1 = action.payload;
return newState;
default:
return prevState;
}
}
export default reducer;
store.js
import { legacy_createStore as createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
import createSagaMidlleWare from "redux-saga";
import watchSaga1 from "./saga1"; //导入 saga 任务
const SagaMidlleWare = createSagaMidlleWare(); //必须先生成 saga 对象
const store = createStore(reducer, applyMiddleware(SagaMidlleWare));
SagaMidlleWare.run(watchSaga1); //布置saga任务
export default store;
saga1.js ——> saga 任务
import { takeEvery, take, fork, put, call } from "redux-saga/effects";
function* watchSaga1() {
// 分开的写法:
// while(true){
// // take 监听 组件发来的action
// yield take("get-list1")
// // fork 非阻塞调用 的形式执行 fn
// yield fork(getList1) //实际上 next 方法的递归调用是被 fork 封装了!实现了 ajax 的链式调用,就类似于我们写的那个 run 方法!
// }
//这个写法等价于上面的:
yield takeEvery("get-list1", getList1); //相当于监听器
}
//相当于之前 异步action 内部的逻辑
function* getList1() {
// 异步处理函数
// call函数用来发送异步请求(参数必须是一个返回值为 promise 对象的函数),会阻塞地调用fn,等待函数执行完毕才会向下执行 ——> 相当于之前的执行异步函数
let res = yield call(getListAction1);
// put函数用来发出新的action,非阻塞式执行 ——> 相当于之前的调用同步 action,但是这里自动 dispatch 给 reducer 了!
yield put({
type: "change-list1",
payload: res,
});
}
//异步函数解耦出来
function getListAction1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(["1111", "2222", "3333"]);
}, 2000);
});
}
export default watchSaga1;
export { getList1 };
注意:这时异步 action 完全不需要写在 action.js 文件中了 ——> 符合 react 的模块化开发思想,异步和同步 action 分开管理,这也是 saga 的好处!
使用 redux:App.jsx
import React, { Component } from "react";
import store from "./redux/store";
export default class App extends Component {
render() {
return (
<div>
<button
onClick={() => {
if (store.getState().list1.length === 0) {
//dispatch
store.dispatch({
type: "get-list1",
});
} else {
console.log("缓存", store.getState().list1);
}
}}>
click-ajax-异步缓存111
</button>
</div>
);
}
}
增加一个 saga 任务,多个异步管理:
saga2.js
import { takeEvery, put, call } from "redux-saga/effects";
function* watchSaga2() {
yield takeEvery("get-list2", getList2);
}
function* getList2() {
// call函数发异步请求
let res1 = yield call(getListAction2_1); //阻塞的调用fn
let res2 = yield call(getListAction2_2, res1); //阻塞的调用fn,并把第一个请求的返回值 res1 作为参数
//非阻塞式执行fn
yield put({
type: "change-list2",
payload: res2,
});
}
function getListAction2_1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(["4444", "5555", "66666"]);
}, 2000);
});
}
function getListAction2_2(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([...data, "777", "888", "999"]);
}, 2000);
});
}
export default watchSaga2;
export { getList2 };
saga.js ——> 合并 saga 任务
import { all } from "redux-saga/effects";
import watchSaga1 from "./saga/saga1";
import watchSaga2 from "./saga/saga2";
function* watchSaga() {
yield all([watchSaga1(), watchSaga2()]); //聚合统一监听
}
export default watchSaga;
这时在 store 中引入 saga.js 即可
改良监听方案,统一文件进行监听:
saga-every.js ——> 统一 watch 监听
import { takeEvery } from "redux-saga/effects";
import { getList1 } from "./saga/saga1";
import { getList2 } from "./saga/saga2";
function* watchSaga() {
yield takeEvery("get-list1", getList1);
yield takeEvery("get-list2", getList2);
}
export default watchSaga;
注意:这样 saga1 中的 watchSaga1 和 saga2 中的 watchSaga2,以及 saga.js 文件都不需要了!
这时 store.js 的写法:
import watchSaga from "./saga-every";
SagaMidlleWare.run(watchSaga); //布置saga任务
2.Portal
Portals 提供了一个最好的在父组件包含的 DOM 结构层级外的 DOM 节点渲染组件的方法。
理解:将父组件中的子组件在父组件之外渲染出来真实 dom,类似传送门
应用:模态框(遮罩层),loading 框
一个典型的用法就是当父组件的 dom 元素有 overflow:hidden 或者 z-index 样式,而你又需要显示的子元素超出父元素的盒子,这时候应该用 Portal 实现。举例来说,如对话框,悬浮框,和小提示。
为什么一定要渲染在外边呢?
一般情况下可以用 fixed 布局结合 100%来实现遮罩层
反例:但是当 Dialog 作为子组件在父组件中时,会继承父组件的 z-index,自己本身的 z-index 再高也没有用,当其他组件的 z-index 比自己的父组件高时,就会盖住 Dialog,那么 Dialog 就不能全屏了!
也就是说 Dialog 有时会受到父和兄弟的影响,所以渲染在外面是最好的!
语法
ReactDOM.createPortal(child, container);
第一个参数 child 是可渲染的 react 子项,比如元素,字符串或者片段等。第二个参数 container 是一个 DOM 元素,用于接受 react 子项并渲染在这个 dom 里面。
实例
PortalDialog.jsx
import React, { Component } from "react";
import { createPortal } from "react-dom";
export default class Dialog extends Component {
render() {
return createPortal(
<div
style={{
width: "100%",
height: "100%",
position: "fixed",
left: 0,
top: 0,
background: "rgba(0,0,0,0.7)",
zIndex: "9999999",
color: "white",
}}>
Dialog-
<div>loading-正在加载中</div>
{this.props.children} {/*插槽,嵌入内容(比如加载圈)*/}
<button onClick={this.props.onClose}>close</button>
</div>,
document.body
); //在 body 里面渲染
}
}
使用:
<div className='right'>
<button
onClick={() => {
this.setState({
isShow: true,
});
}}>
ajax
</button>
{this.state.isShow && (
<PortalDialog
onClose={() => {
this.setState({
isShow: false,
});
}}>
<div>1111</div> {/*插入内容*/}
<div>加载圈</div>
</PortalDialog>
)}
</div>
protal 中的事件冒泡问题
虽然通过 portal 渲染的元素在父组件的盒子之外,但是渲染的 dom 节点仍在 React 的元素树上,在那个 dom 元素上的点击事件仍然能在 dom 树中监听到。
例子:
<div
className='box'
onClick={() => {
console.log("box身上监听的事件");
}}>
<div className='left'></div>
<div className='right'>
{this.state.isShow && (
<PortalDialog
onClose={() => {
this.setState({
isShow: false,
});
}}></PortalDialog>
)}
</div>
</div>
当 onClose 被触发的时候(是一个点击事件),自己的父节点 box 身上的 onClick 也会被触发!虽然 PortalDialog 没有被渲染在 box 内部!
因为 react 自己构建了一套事件流,事件注册都是放在 document 根节点上的(代理模式),React 会接管事件冒泡,在事件冒泡过程中,React 会根据虚拟 DOM 树的结构和事件的冒泡规则,将事件传递给相应的组件。
react 事件原理
React 事件系统的原理是基于合成事件(SyntheticEvent)的机制。React 将浏览器的原生事件封装成了合成事件,提供了一种跨浏览器一致的事件处理方式。
React 事件系统的工作流程如下:
- 事件绑定:在 JSX 中,我们可以通过
onEventName
的属性来绑定事件,例如onClick
、onChange
等。 - 事件注册:当组件渲染到页面上时,React 在组件的根元素上注册事件监听器,以便捕获和处理触发的事件。
- 合成事件生成:当事件在浏览器中触发时,React 会通过事件委派的方式,将原生事件封装成合成事件对象(SyntheticEvent)。合成事件对象包含了与原生事件相关的属性和方法。
- 事件处理:React 调用绑定的事件处理函数,并将合成事件对象作为参数传递给处理函数。在事件处理函数中,我们可以通过访问合成事件对象来获取事件的相关信息,如事件类型、目标元素、键盘状态等。
- 事件冒泡:在事件处理函数执行完成后,React 负责处理事件的冒泡过程。React 使用了一种虚拟的 DOM 结构(虚拟 DOM 树)来模拟实际的 DOM 结构,并通过比较虚拟 DOM 树的变化来更新实际的 DOM。在事件冒泡过程中,React 会根据虚拟 DOM 树的结构和事件的冒泡规则,将事件传递给相应的组件。
React 事件系统的优点和特性包括:
- 跨浏览器一致性:React 事件系统封装了浏览器的原生事件,使得我们无需关心不同浏览器之间的兼容性问题,提供了一种统一的事件处理方式。
- 委派机制:React 通过事件委派将事件处理函数绑定在组件的根元素上,而不是为每个子元素都注册单独的事件监听器。这样可以减少内存占用和事件注册的开销。
- 合成事件:React 使用合成事件对象来封装原生事件,提供了丰富的事件信息和方法。合成事件对象是一个轻量级的对象,可以有效地传递给事件处理函数,并进行高效的事件处理。
- 批量更新:React 会对连续的事件进行批量更新,通过合并多个更新操作,减少不必要的 DOM 操作,提高性能和渲染效率。
总的来说,React 事件系统通过封装原生事件、合成事件的生成和事件委派机制,提供了一种高效、统一且易用的事件处理方式,使我们能够方便地处理用户交互和响应各种事件。
react 事件注册的委派机制:
用一些伪代码来描述 React 的事件委派机制的工作原理。
假设我们有一个简单的 React 组件结构:
function App() {
return (
<div onClick={handleClick}>
<button>Click me</button>
<input onChange={handleChange} />
</div>
);
}
在这个例子中,我们在最外层的 <div>
元素上绑定了一个点击事件 onClick={handleClick}
,在 <input>
元素上绑定了一个输入变化事件 onChange={handleChange}
。
React 在组件渲染时,会在最外层的 <div>
元素上注册一个单一的事件监听器来处理所有子元素的事件。这个事件监听器会捕获所有触发的事件,而不是为每个子元素都注册独立的事件监听器。
伪代码示例:
function registerEventListener() {
//给 document 绑定,而不是给每个子元素绑定,靠虚拟 dom 的事件冒泡机制来得知事件的产生 ——> 这样只需要注册一次事件(代理模式)
document.addEventListener("click", handleEvent);
}
function handleEvent(event) {
const target = event.target; // 获取事件的目标元素
// 根据事件类型和目标元素来判断执行哪个事件处理函数
if (event.type === "click" && target.matches("div")) {
handleClick(event);
} else if (event.type === "change" && target.matches("input")) {
handleChange(event);
}
}
function handleClick(event) {
// 处理点击事件的逻辑
}
function handleChange(event) {
// 处理输入变化事件的逻辑
}
在上述伪代码中,registerEventListener
函数用于在组件渲染时注册事件监听器,这里假设使用了 document.addEventListener
来进行注册。然后,handleEvent
函数作为统一的事件处理函数,根据事件类型和目标元素来分发事件到相应的处理函数。
这样,无论是点击 <div>
元素还是改变 <input>
元素的值,都会经过统一的事件处理函数,并根据事件类型和目标元素来调用相应的事件处理函数。
需要注意的是,以上是一个简化的示例,**实际情况下 React 的事件委派机制更加复杂,并且通过虚拟 DOM 进行事件冒泡和处理。**这个示例只是用伪代码描述了事件委派的基本原理。
注意:虽然每个组件都有自己的根元素,但是 react 的事件绑定是通过 document.addEventListener
绑定到全局的 document
对象上的,而不是直接绑定到组件的根元素上。
3.GraphQL
基本介绍
GraphQL 是一种用于构建 API 的查询语言和运行时环境。它由 Facebook 开发并于 2015 年开源,现在由 GraphQL 基金会进行维护。相比于传统的 RESTful API,GraphQL 提供了更灵活、高效和强大的数据查询和操作能力,是 REST API 的替代品。
GraphQL 即是一种用于 API 的查询语言,也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获取它需要的数据,而且没有任何荣誉,也让 API 更容易地随着时间推移而演进。
在 GraphQL 中,客户端可以通过发送一个特定的查询来精确地指定需要获取的数据。这个查询由客户端定义,并且服务器会返回与查询相匹配的数据。这样客户端可以避免过度获取数据或多次请求来获取所需的信息,提高了网络效率。
以下是一些 GraphQL 的核心概念:
- Schema(模式):定义了 API 中可用的类型和操作。它描述了数据结构和数据之间的关系,包括对象、接口、枚举和标量等。
- 查询(Query):用于从服务器获取数据的操作。通过查询语句指定需要的数据字段,并从根类型开始执行查询。
- 变更(Mutation):用于修改服务器上的数据的操作。通过变更操作,客户端可以发送修改请求,如创建、更新或删除数据。
- 订阅(Subscription):用于实时获取数据的操作。通过订阅操作,客户端可以订阅特定的事件或数据更新,并在服务器端发生相应的变化时接收到实时的数据更新。
GraphQL 在前端和后端之间建立了一种有效的数据传输方式,并且可以与各种编程语言和框架进行集成。它提供了更好的灵活性和可扩展性,使开发人员能够更精确地定义和获取所需的数据,从而提升应用程序的性能和用户体验。
应用:
比如我们可能已经开发了一个非常完善的 pc 端,现在需要一个小程序,但是小程序中展示的内容不需要那么完善,所以要针对性地获取数据字段。让后端再去改接口的话,就会很麻烦,但是不改的话,会增加我们小程序的流量损耗。——> 我们希望可以描述我们想要哪些数据字段
理解:
相当于服务器的一个中间商,将接口数据做处理再给前端,但是这个处理过程是前端来实现。
特点:
请求需要的数据,不多不少
例如:account 中有 name,age,sex,department 等,可以只取得需要的字段。
获取多个资源,只用一个请求
例如:我们可以自行聚合这些请求,不需要请求多次
描述所有可能类型的系统。便于维护和扩展项目,根据需求平滑演进,添加或者隐藏相应字段。
- restful 一个接口只能返回一个资源,graphql 一次可以获取多个资源
- restful 用不同的 url 来区分资源,graphql 用类型区分资源
参数类型与传递
基本类型:String,Int,Float,Boolean 和 ID。可以在 shema 声明的时候直接使用。
[类型]代表数组,例如:[Int]代表整形数组
和 js 传递参数一样,小括号内定义形参,但是注意:参数需要定义类型
! 叹号代表参数不能为空,没加 ! 就是可以为空
jstype Query{ rollDice(numDice: Int!, numSides: Int): [Int] }
graphql 基本使用方法:
1.前端进行配置查询:使用 graphql 语言进行接口请求
2.后端必须要进行支持:构建 graphql 风格的接口
graphql 后端支持简单类型的查询:Query
后端我们这里基于 express 进行开发,安装以下版本的依赖:
"dependencies": {
"express": "^4.16.4",
"express-graphql": "^0.7.1",
"graphql": "^14.0.2",
"mongoose": "^5.13.14"
}
const express = require("express");
const { buildSchema } = require("graphql");
const graphqlHttp = require("express-graphql");
//定义 Schema 轮廓(相当于请求路径),type Query定义支持的查询处理项名字和对应的返回值类型
var Schema = buildSchema(`
type Query{
hello: String,
getName: String,
getAge :Int
}
`);
//处理器:轮廓对应的处理方案(相当于路径的函数体)
const root = {
hello: () => {
//这里实际上是通过数据库查
var str = "hello wolrd!";
return str;
},
getName: () => {
return "kerwin";
},
getAge: () => {
return 100;
},
};
var app = express();
//对比:
//传统的 restful api:
app.use("/home", function (req, res) {
res.send("home data2222");
});
app.use("/list", function (req, res) {
res.send("list data");
});
//graphql api:
//使用graphqlHttp连接器,传入轮廓和处理器,graphql: true表示开启调试
//当在网页访问/graphql路径的时候,会有一个接口文档!
app.use(
"/graphql",
graphqlHttp({
schema: Schema,
rootValue: root,
graphiql: true,
})
);
app.listen(3000);
使用这个 hello 和 getName 接口:
//简写:
{
hello,
getName
}
//或者:
query {
hello,
getName
}
所以说我们前端发起一个请求,可以同时获取两个接口即 hello 和 getName 的数据。
nodemon 或 node-dev 实现后端项目热更新
我们在运行后端 express 的时候,每次有代码更改都要重新手动运行启动,有没有自动化解决方案呢?
使用 nodemon,首先 npm i -g nodemon
然后 nodemon app.js 启动即可!
或者使用 node-dev 库,也可以实现一样的效果,npm i -g node-dev 安装,然后 node-dev app.js
这样就可以自动帮我们重启后端 express 项目
graphql 后端支持参数传递 与 复杂类型 的查询
const express = require("express");
const { buildSchema } = require("graphql");
const graphqlHttp = require("express-graphql");
var Scchema = buildSchema(`
//自定义类型,类似 ts 中的接口
type Account{
name: String,
age: Int,
location: String
}
//自定义类型,类似 ts 中的接口
type Film{
id: Int,
name: String,
poster: String,
price: Int
}
type Query{
hello: String,
getName: String,
getAge: Int,
getAllNames: [String],
getAllAges: [Int],
getAccountInfo: Account, //使用上面的自定义类型
getNowplayingList: [Film], //使用上面的自定义类型
geteFilmDetail(id:Int!): Film //根据 id 从列表中查询出一个数据
}
`);
var faskeDb = [
{
id: 1,
name: "1111",
poster: "http://1111",
price: 100,
},
{
id: 2,
name: "2222",
poster: "http://2222",
price: 200,
},
{
id: 3,
name: "3333",
poster: "http://333",
price: 300,
},
];
//处理器
const root = {
hello: () => {
//通过数据库查
var str = "hello wolrd1111";
return str;
},
getName: () => {
return "kerwin";
},
getAge: () => {
return 100;
},
getAllNames: () => {
return ["kerwin", "teichui", "xiaoming"];
},
//这里是对象属性的简写方法
getAllAges() {
return [19, 20, 200];
},
getAccountInfo() {
return {
name: "kerwin",
age: 100,
location: "dalian",
};
},
getNowplayingList() {
return faskeDb;
},
geteFilmDetail({ id }) {
//拿到的数据为一个对象{id:2},用{}来解构参数
console.log(id);
return faskeDb.filter((item) => item.id === id)[0]; //filter过滤出来的结果是一个数组,所以需要取第一个(根据 id 查询)
},
};
var app = express();
app.use(
"/graphql",
graphqlHttp({
schema: Scchema,
rootValue: root,
graphiql: true,
})
);
app.listen(3000);
使用接口:
query{
getAllAges,
getAllNames,
getAccountInfo{
name //这样就可以只查询出 Account 类型中的 name字段(只有是我们自定义类型的数据才可以指定字段!!!)
}
getNowplayingList{
name, //只要每个对象中的这两个字段,最终组成一个列表给我们!
price
}
getFilmDetail(id:2) //不写要哪些字段,默认就是所有字段
}
graphql 后端支持增删改:Mutation
mutation:修改
const express = require("express");
const { buildSchema } = require("graphql");
const graphqlHttp = require("express-graphql");
var Scchema = buildSchema(`
type Film{
id:Int,
name:String,
poster:String,
price:Int
}
input FilmInput{ //注意:给方法的参数类型如果是自定义的,必须使用 input 关键字,不能用 type 关键字
name:String,
poster:String,
price:Int
}
type Mutation{
createFilm(input:FilmInput):Film,
updateFilm(id:Int!,input:FilmInput):Film,
deleteFilm(id:Int!):Int
}
`);
var faskeDb = [
{
id: 1,
name: "1111",
poster: "http://1111",
price: 100,
},
{
id: 2,
name: "2222",
poster: "http://2222",
price: 200,
},
{
id: 3,
name: "3333",
poster: "http://333",
price: 300,
},
];
//处理器
const root = {
//新增
createFilm({ input }) {
var obj = { ...input, id: faskeDb.length + 1 };
faskeDb.push(obj);
return obj;
},
//修改
updateFilm({ id, input }) {
console.log(id, input);
var current = null;
faskeDb = faskeDb.map((item) => {
//不会影响原数组,所以要覆盖替换
if (item.id === id) {
current = { ...item, ...input };
return { ...item, ...input };
}
return item; //如果不等于这个id,那么就直接返回,原封不动
});
return current;
},
//删除
deleteFilm({ id }) {
faskeDb = faskeDb.filter((item) => item.id !== id); //不会影响原数组,所以要覆盖替换
return 1; //影响的行数
},
};
var app = express();
app.use(
"/graphql",
graphqlHttp({
schema: Scchema,
rootValue: root,
graphiql: true,
})
);
app.listen(3000);
使用接口:
mutation{
createFilm(input:{
name:"hao",
poster:"aaa",
price:400
}){
id,
name,
price
},
createFilm(input:{
name:"hao",
poster:"aaa",
price:400
}){
id,
name,
price
},
updateFilm(id:1,input:{
name:"111-修改",
poster:"111-poster-修改"
}){
id,
name
}
deleteFilm(id:1) //返回类型不是自定义类型,不要加{}再定义字段了!
}
graphql 后端结合 mongodb 数据库
const express = require("express");
const { buildSchema } = require("graphql");
const graphqlHttp = require("express-graphql");
//----------------------链接数据库服务------------------------
var mongoose = require("mongoose"); //使用 mongoose 连接 mogo数据库
mongoose.connect("mongodb://localhost:27017/maizuo", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
/*
mongoose的操作方法:
1. 创建模型:可以让增删改查都复用这个模型
2. 操作数据库
*/
//创建FilmModel模型:限制 数据库中这个名为 films的集合表 只能存name、poster、price这三个字段(默认会有_id为索引字段)
var FilmModel = mongoose.model(
"film", //这里是 film,集合表就会叫 films(没有这个表就会创建,有的话就不创建)
new mongoose.Schema({
name: String,
poster: String,
price: Number,
})
);
// 模型的四个操作方法:
// FilmModel.create
// filmModel.find
// FilmModel.update
// FimlModel.delete
//------------------------------------------------------------
var Scchema = buildSchema(`
type Film{
id:String, //id 的名字不需要写成_id,查询的时候会自动转换为 id 的,但是必须是 String 类型,因为 mongo 中的默认索引是字符串
name:String,
poster:String,
price:Int
}
input FilmInput{
name:String,
poster:String,
price:Int
}
type Query{
getNowplayingList(id:String!):[Film]
}
type Mutation{
createFilm(input: FilmInput):Film,
updateFilm(id:String!,input:FilmInput):Film,
deleteFilm(id:String!):Int
}
`);
//处理器
const root = {
//查询
getNowplayingList({ id }) {
if (!id) return FilmModel.find();
return FilmModel.find({ _id: id }); //根据 id 查询,要严格地写为_id(如果不传参数就是查询所有的) //注意:也可以findOne,就是查一个
},
//增加
createFilm({ input }) {
return FilmModel.create({
//增加用 create
...input, //把 input 这个对象展开即可
}); //注意:graphql 支持我们 return 一个 promise 对象,而FilmModel方法的结果正好就是一个 promise 对象,所以我们直接 return 即可
//如果不直接 return,那么就这样写:返回添加对象的个数
/*
FilmModel.create({
...input,
}).then((res)=>{
if(Object.prototype.toString.call(res) === '[object Object]'){
return 1;
}
})
*/
},
//更新
updateFilm({ id, input }) {
//更新一个用updateOne,更新多个用updateMany
return FilmModel.updateOne(
{
_id: id,
},
{
...input,
}
)
.then((res) => FilmModel.find({ _id: id })) //查询出我们所更新的那条数据
.then((res) => res[0]); //得到的是数组,取第一个即可(这样返回的还是 promise 对象)
},
//删除
deleteFilm({ id }) {
//deleteOne和deleteMany
return FilmModel.deleteOne({ _id: id }).then((res) => 1);
},
};
var app = express();
app.use(
"/graphql",
graphqlHttp({
schema: Scchema,
rootValue: root,
graphiql: true,
})
);
app.listen(3000);
使用接口:
mutation{
createFilm(input:{
name:"hao", //注意:这里不用给 id,mongo 会自动添加 id 的!
poster:"aaa",
price:40
}){
id, //会自动从_id转换为 id
name,
price
}
updateFilm(id:"adahdhcacsacs21312e1221312",input:{ //这里不用写_id,因为我们传的参数是叫 id 的
name:"111-修改",
poster:"111-poster-修改"
})
//注意:更新成功不会返回更新后的对象,而是返回一个{ok:1, nModified:0, n:1}的对象
//{
//id,
//name
//}
deleteFilm(id:"adahdhcacsacs21312e1221312")
}
graphql 前端 html 中纯 js 请求接口
只是在后端建立静态页面,不存在跨域问题,因为都是在一个服务端口的

app.js 需要配置静态资源目录:
app.use(
"/graphql",
graphqlHttp({
schema: Scchema,
rootValue: root,
graphiql: true,
})
);
//配置静态资源目录
app.use(express.static("public"));
app.listen(3000);
/public/home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>home</h1>
<button onclick="getData()">查询数据</button>
<button onclick="createData()">创建数据</button>
<button onclick="updateData()">更新数据</button>
<button onclick="deleteData()">删除数据</button>
<script>
function getData() {
//创建 graphql 的查询语言,用来匹配后端的轮廓表,以及指定我们想要的数据,跟我们在调试工具中写 gql 是一样的!——> 有种把后端接口当做数据库的感觉,前端在用 sql 查数据库获取数据!
const myquery = `
query {
getNowplayingList {
id,
name
}
}
`
//使用 fetch 发送 ajax 请求:会比 axios 麻烦一点
fetch("/graphql", { //graphql的接口只有一个请求路径,就是/graphql,再结合请求体 gql 的不同匹配不同的轮廓表
method: "POST", //必须是 POST 请求
headers: {
"Content-Type": "application/json", //传输 json 格式
"Accept": "application/json" //接收的是 json 格式
},
body: JSON.stringify({ //必须要用JSON.stringify包裹一下,不然传的是 js 对象!
query: myquery //这个 query 属性是固定的,graphql 需要的!
})
}).then(res => res.json()).then(res => { //fetch必须两次链式调用
console.log(res)
})
}
function createData() {
const myquery = `
mutation ($input:FilmInput){ //FilmInput是要对标的后端的那个自定义类型!限定了接收的变量的类型
//$input:FilmInput 和 input:$input 的意思就是接收一个 FilmInput类型 的 input变量($是ES6模版字符串中引用变量的方法!),作为实参 input 的值(对应后端的形参的名字input)
createFilm(input:$input) {
id,
name
}
}
`
fetch("/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
query: myquery,
variables: { //variables属性是传递的参数变量
input: { //这个名字和上面的$后面的名字input必须一样,是被接收的变量!会把上面的$input 替换为这个 input变量
name: "6666",
price: 60,
poster: "http://6666"
}
}
})
}).then(res => res.json()).then(res => {
console.log(res)
})
}
function updateData() {
const myquery = `
mutation ($id:String!,$input:FilmInput){ //这里必须也要加!,要和后端定义的参数接收规则保持一致,差一点都不行,否则也会匹配不上轮廓表 ——> 一个比较大的缺点,但是这本身就是相当于写restful的路径
updateFilm(id:$id,input:$input) {
id,
name
}
}
`
fetch("/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({ //传多个参数
query: myquery,
variables: {
id: "61e520b322f995d3f88f3faa",
input: {
name: "6666-修改",
price: 66,
poster: "http://6666-修改"
}
}
})
}).then(res => res.json()).then(res => {
console.log(res)
})
}
function deleteData() {
const myquery = `
mutation ($id:String!){
deleteFilm(id:$id)
}
`
fetch("/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
query: myquery,
variables: {
id: "61e520b322f995d3f88f3faa"
}
})
}).then(res => res.json()).then(res => {
console.log(res)
})
}
</script>
</body>
</html>
结合 react:实现 query
安装插件:4 个插件
npm i react-apollo apollo-boost graphql graphql-tag
1.react-apollo
是一个用于在 React 应用中集成 GraphQL 的第三方库,它提供了一组 React 组件和钩子,以便在 React 组件中进行 GraphQL 查询(queries)和变更(mutations)操作。
react-apollo
以 Apollo Client 作为底层,提供了与 React 无缝集成的功能,简化了在 React 应用中使用 GraphQL 的流程。
2.apollo-boost
是一个用于简化 Apollo Client 配置的工具包,它提供了一个预配置的 Apollo Client 实例,使得在使用 Apollo Client 进行 GraphQL 开发时更加便捷。
使用 apollo-boost
,你可以更快速地设置 Apollo Client,而无需手动配置和组合各个组件。
3.graphql-tag
是一个用于处理 GraphQL 查询和变异字符串的 JavaScript 库。它提供了一种方便的方式来定义和解析 GraphQL 查询和变异的模板字符串。
通过导入 gql
函数,你可以在 JavaScript 中使用模板字符串的形式定义 GraphQL 查询和变异,而无需手动处理字符串的拼接和转义。
配置跨域代理:setupProxy.js ——> 改完之后必须要重启服务
app.use(
"/graphql",
createProxyMiddleware({
target: "localhost:3000",
changeOrigin: true,
})
);
实现无参数查询:
import React, { Component } from "react";
import { ApolloProvider, Query } from "react-apollo";
import ApolloClient from "apollo-boost"; //最新版是"@apollo/client"
import gql from "graphql-tag";
//ApolloClient用于连接服务端
const client = new ApolloClient({
uri: "/graphql", //这里就没必要写 localhost:3000了,因为我们配置了代理!
});
export default class App extends Component {
render() {
return (
{/*生产者-消费者模式,给组件提供连接后端的服务*/}
<ApolloProvider client={client}>
<div>
{/*可以理解为一个展示列表数据的组件*/}
<KerwinQuery></KerwinQuery>
</div>
</ApolloProvider>
);
}
}
//需要查询内容的组件:
class KerwinQuery extends Component {
//gql模块 用于创建gql 语句
query = gql`
query {
getNowplayingList {
id
name
price
}
}
`;
render() {
return (
<Query query={this.query}> {/*查询方法是一个组件Query(高阶组件,组件内部封装了 ajax 请求,不用我们去管),传入gql语句即可 ——> 万物皆组件的思想*/}
{({ loading, data }) => { //会有两个返回值,loading(标志着是否请求完毕) 和 data
console.log(loading);
return loading ? (
<div>loading....</div>
) : (
<div>
{data.getNowplayingList.map((item) => ( //data 里面结果的名字就是上面查询方法的名字getNowplayingList
<div key={item.id}>
<div>名字:{item.name}</div>
<div>价格:{item.price}</div>
</div>
))}
</div>
);
}}
</Query>
);
}
}
实现有参数查询:
query = gql`
query getNowplayingListAll($id: String!) { //可以给 query操作 起一个名字叫getNowplayingListAll,也就是具名查询,也可以不起别名(匿名查询)——> 这个名字没什么实质性的作用
getNowplayingList(id: $id) { //这是具体的函数
id
name
price
}
}
`;
state = {
id: "61e66f60dd8ae3c99074ac53",
};
render() {
return (
<div>
<input
type="text"
onChange={(evt) => {
this.setState({
id: evt.target.value,
});
}}
/>
<Query query={this.query} variables={{ id: this.state.id }}> {/*传入 gql 语句对象,这里是直接调用了查询语句*/}
{({ loading, data }) => {
console.log(loading);
return loading ? (
<div>loading....</div>
) : (
<div>
{/*这里用操作里面的对应函数名getNowplayingListAll去取数据*/}
{data.getNowplayingListAll.map((item) => (
<div key={item.id}>
<div>名字:{item.name}</div>
<div>价格:{item.price}</div>
</div>
))}
</div>
);
}}
</Query>
...
结合 react:实现 mutation
添加功能:
import React, { Component } from "react";
import { ApolloProvider, Mutation } from "react-apollo";
import ApolloClient from "apollo-boost";
import gql from "graphql-tag";
const client = new ApolloClient({
uri: "/graphql",
});
export default class App extends Component {
render() {
return (
<ApolloProvider client={client}>
<div>
<KerwinCreate></KerwinCreate>
</div>
</ApolloProvider>
);
}
}
class KerwinCreate extends Component {
createFilm = gql`
mutation createFilm($input: FilmInput) { //给mutation操作起一个名字叫做createFilm
createFilm(input: $input) {
id
name
price
}
}
`;
render() {
return (
<div>
<Mutation mutation={this.createFilm}>
{" "}
{/*传入 gql 语句对象,并没有直接调用,在组件内部返回函数给下面的内容作为事件回调*/}
{(createFilm, { data }) => {
//这里接收gql 语句对象,是一个叫做createFilm的形参(其实叫啥都行,会默认把 gql语句对象即mutation操作 传进来的!)
console.log(data); //返回的 data 是我们添加的对象的内容
return (
<div>
<button
onClick={() => {
createFilm({
//使用这个函数
variables: {
//可以传多个函数需要的参数,都在这里传
input: {
name: "777",
poster: "http://777",
price: 70,
},
},
});
}}>
add
</button>
</div>
);
}}
</Mutation>
</div>
);
}
}
对于下面代码的解读:
createFilm = gql`
mutation createFilm($input: FilmInput) { //给方法起一个名字叫做createFilm
createFilm(input: $input) {
id
name
price
}
}
`;
变量createFilm
是一个 GraphQL 的 mutation
操作,用于创建电影的数据。mutation
后面的createFilm
是这个操作的名字,没有实质性的作用,只是增加代码的可读性而已。
注意:变量createFilm
和 mutation 后面的createFilm
之间没有本质的区别,它们是同一个 GraphQL mutation
操作的名称。
在这个 mutation
操作中,createFilm
表示一个可以执行的函数或方法,它接受一个名为 $input
的变量作为参数,并返回一个对象,其中包含 id
、name
和 price
字段。
这个 createFilm
函数代表了后端服务器中相应的处理逻辑。通过调用这个函数,并传递 $input
变量作为参数,可以在服务器上创建电影数据。在返回结果中,你可以获取新创建电影的 id
、name
和 price
等信息。
注意:gql`` 标签模板中只可以写入一个 GraphQL 查询(queries)或变更(mutations),但一个查询或者变更里面可以写多个函数
更新功能:
import { ApolloProvider, Mutation } from "react-apollo";
import ApolloClient from "apollo-boost";
import gql from "graphql-tag";
const client = new ApolloClient({
uri: "/graphql",
});
export default class App extends Component {
render() {
return (
<ApolloProvider client={client}>
<div>
<KerwinUpdate></KerwinUpdate>
</div>
</ApolloProvider>
);
}
}
class KerwinUpdate extends Component {
updateFilm = gql`
mutation updateFilm($id: String!, $input: FilmInput) {
updateFilm(id: $id, input: $input) {
id
name
price
}
}
`;
render() {
return (
<div>
<Mutation mutation={this.updateFilm}>
{(updateFilm, { data }) => {
console.log(data); //得到 ok 标志
return (
<div>
<button
onClick={() => {
updateFilm({
variables: {
id: "61e67c0031bf52b53c9245c7",
input: {
name: "777-修改",
poster: "http://777-修改",
price: 700,
},
},
});
}}>
update
</button>
</div>
);
}}
</Mutation>
</div>
);
}
}
删除功能:
import React, { Component } from "react";
import { ApolloProvider, Mutation } from "react-apollo";
import ApolloClient from "apollo-boost";
import gql from "graphql-tag";
const client = new ApolloClient({
uri: "/graphql",
});
export default class App extends Component {
render() {
return (
<ApolloProvider client={client}>
<div>
<KerwinDelete></KerwinDelete>
</div>
</ApolloProvider>
);
}
}
class KerwinDelete extends Component {
deleteFilm = gql`
mutation deleteFilm($id: String!) {
deleteFilm(id: $id)
}
`;
render() {
return (
<div>
<Mutation mutation={this.deleteFilm}>
{(deleteFilm, { data }) => {
console.log(data);
return (
<div>
<button
onClick={() => {
deleteFilm({
variables: {
id: "61e67c0031bf52b53c9245c7",
},
});
}}>
delete
</button>
</div>
);
}}
</Mutation>
</div>
);
}
}
react 结合 graphql 案例:todolist
kerwinQuery.js
import { Component } from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";
import KerwinDelete from "./KerwinDelete";
class KerwinQuery extends Component {
query = gql`
query getNowplayingList($id: String!) {
getNowplayingList(id: $id) {
id
name
price
}
}
`;
state = {
id: "",
};
render() {
return (
<div>
<Query query={this.query} variables={{ id: this.state.id }}>
{({ loading, data, refetch }) => {
// 第三个参数为重新发起请求的函数,可以传给其他组件调用,这里我们直接传给 delete 组件
// 通过接收函数进行子到父通信的方式把 fetch 传给外面,通过外面传给 create 组件(实现兄弟通信)
this.props.fetch(refetch); //这里调用 props 传入的函数,进行子到父的通信,把 refetch 传给外面!
return loading ? (
<div>loading....</div>
) : (
<div>
{data.getNowplayingList.map((item) => (
<div
key={item.id}
style={{ border: "1px solid black", padding: "20px" }}>
<div>名字:{item.name}</div>
<div>价格:{item.price}</div>
{/* 删除一项:这里就有一个 button 而已*/}
<KerwinDelete
id={item.id}
cb={() => {
refetch();
}}></KerwinDelete>
</div>
))}
</div>
);
}}
</Query>
</div>
);
}
}
export default KerwinQuery;
kerwinCreate.js
import gql from "graphql-tag";
import { Mutation } from "react-apollo";
import React, { Component } from "react";
class KerwinCreate extends Component {
createFilm = gql`
mutation createFilm($input: FilmInput) {
createFilm(input: $input) {
id
name
price
}
}
`;
nameRef = React.createRef();
posterRef = React.createRef();
priceRef = React.createRef();
render() {
return (
<div>
<Mutation mutation={this.createFilm}>
{(createFilm, { data }) => {
console.log(data);
return (
<div>
<p>
名字
<input type='text' ref={this.nameRef} />
</p>
<p>
海报
<input type='text' ref={this.posterRef} />
</p>
<p>
价格
<input type='number' ref={this.priceRef} />
</p>
<button
onClick={() => {
createFilm({
variables: {
input: {
name: this.nameRef.current.value,
poster: this.posterRef.current.value,
price: Number(this.priceRef.current.value), //必须转换一下,因为后端要的是 Int 类型!
},
},
}).then((res) => {
this.props.cb();
});
}}>
add
</button>
</div>
);
}}
</Mutation>
</div>
);
}
}
export default KerwinCreate;
kerwinDelete.js
import gql from "graphql-tag";
import { Mutation } from "react-apollo";
import React, { Component } from "react";
class KerwinDelete extends Component {
createFilm = gql`
mutation deleteFilm($id: String!) {
deleteFilm(id: $id)
}
`;
render() {
return (
<div>
<Mutation mutation={this.createFilm}>
{(deleteFilm, { data }) => {
console.log(data);
return (
<div>
<button
onClick={() => {
deleteFilm({
variables: {
id: this.props.id,
},
}).then((res) => {
this.props.cb();
});
}}>
delete
</button>
</div>
);
}}
</Mutation>
</div>
);
}
}
export default KerwinDelete;
App.js
import React, { Component } from "react";
import { ApolloProvider } from "react-apollo";
import ApolloClient from "apollo-boost";
import KerwinQuery from "./components/KerwinQuery";
import KerwinCreate from "./components/KerwinAdd";
const client = new ApolloClient({
uri: "/graphql",
});
export default class App extends Component {
refetch = null;
render() {
return (
<ApolloProvider client={client}>
{/* 添加一项:三个 input 和一个 button */}
<KerwinCreate
cb={() => {
this.refetch(); // 传给 create 组件 让kerwinquery 重新发起一次请求
}}
/>
{/* 展示所有项:列表展示 */}
<KerwinQuery
fetch={(refetch) => {
this.refetch = refetch; //进行父子通信,接收 refecth 函数
}}
/>
</ApolloProvider>
);
}
}
函数式组件使用 hook 实现增删改成
除了组件化的使用方法,React Apollo 还提供了函数式的使用方法。React Apollo 的函数式 API 允许你在函数组件中以 Hook 的形式使用 Apollo 客户端。
React Apollo 提供了一些用于在函数组件中使用 Apollo 客户端的 Hook:
useQuery
:用于执行 GraphQL 查询的 Hook。它接受一个查询的 GraphQL 文档和可选的配置选项,并返回查询结果。useMutation
:用于执行 GraphQL 变更(mutation)的 Hook。它接受一个变更的 GraphQL 文档,并返回一个函数,可用于触发该变更,并返回变更结果。useSubscription
:用于订阅 GraphQL 操作的 Hook。它接受一个订阅的 GraphQL 文档,并返回一个对象,包含当前订阅的数据。useLazyQuery
:用于在需要时执行延迟加载的查询的 Hook。它类似于useQuery
,但不会在组件挂载时自动执行查询,而是返回一个函数,用于手动触发查询。
当使用 React Apollo 的函数式 API 时,以下是每个 Hook 的具体使用方法:
useQuery
Hook:
import { useQuery } from "@apollo/client";
import { GET_USERS } from "./queries";
const UserList = () => {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>User List</h1>
<ul>
{data.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
在上面的示例中,我们使用useQuery
Hook 来执行名为 GET_USERS
的 GraphQL 查询。我们通过解构赋值从返回的对象中获取 loading
、error
和 data
属性。根据加载状态和错误状态,我们返回不同的 UI 内容。
useMutation
Hook:
import { useMutation } from "@apollo/client";
import { ADD_USER } from "./mutations";
const AddUserForm = () => {
const [addUser, { loading, error }] = useMutation(ADD_USER);
const handleSubmit = (event) => {
event.preventDefault();
addUser({ variables: { name: "John", age: 25 } });
};
return (
<form onSubmit={handleSubmit}>
<input type='text' name='name' placeholder='Name' />
<input type='number' name='age' placeholder='Age' />
<button type='submit'>Add User</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
</form>
);
};
在上面的示例中,我们使用useMutation
Hook 来执行名为 ADD_USER
的 GraphQL 变更操作。我们解构赋值得到 addUser
函数和 loading
、error
属性。在表单的提交处理函数中,我们调用 addUser
函数来触发变更操作,传递变量数据作为参数。
useSubscription
Hook:
import { useSubscription } from "@apollo/client";
import { USER_CREATED } from "./subscriptions";
const UserSubscription = () => {
const { data, loading, error } = useSubscription(USER_CREATED);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h2>New User Created</h2>
<p>Name: {data.userCreated.name}</p>
<p>Age: {data.userCreated.age}</p>
</div>
);
};
在上面的示例中,我们使用useSubscription
Hook 来订阅名为 USER_CREATED
的 GraphQL 操作。我们通过解构赋值从返回的对象中获取 data
、loading
和 error
属性。根据加载状态和错误状态,我们返回不同的 UI 内容。
注意:使用 useSubscription
可以订阅某个 GraphQL 操作(通常是订阅操作),并在结果发生变化时获取更新的数据。它适用于实时数据的场景,比如聊天应用程序中的实时消息、实时通知等。
当我们说订阅的数据发生变化时,是指服务器端的数据发生变化,然后将更新的数据推送给客户端进行处理。
后端需要配置实时数据传输机制:为了支持实时的数据传输和订阅,服务器需要使用适当的实时传输协议,如 WebSocket。配置服务器以支持 WebSocket 连接,并确保订阅操作可以通过 WebSocket 进行监听和推送数据。
useLazyQuery
Hook:类似 useMutation,不过是用来做查询的
import { useLazyQuery } from "@apollo/client";
import { SEARCH_USERS } from "./queries";
const UserSearch = () => {
const [searchUsers, { loading, data }] = useLazyQuery(SEARCH_USERS);
const handleSearch = (event) => {
event.preventDefault();
searchUsers({ variables: { keyword: "John" } });
};
return (
<div>
<form onSubmit={handleSearch}>
<input type='text' name='keyword' placeholder='Search' />
<button type='submit'>Search</button>
</form>
{loading && <p>Loading...</p>}
{data && (
<ul>
{data.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
};
在上面的示例中,我们使用useLazyQuery
Hook 来执行延迟加载的查询操作。我们解构赋值得到 searchUsers
函数和 loading
、data
属性。在搜索表单的提交处理函数中,我们调用 searchUsers
函数来触发查询操作,传递查询变量作为参数。
注意,上面我们使用 hook 的时候,gql 文件被我们单独解耦出来了:下面是示例
import { gql } from "@apollo/client";
export const CREATE_USER = gql`
mutation CreateUser($name: String!, $age: Int!) {
createUser(name: $name, age: $age) {
id
name
age
}
}
`;
4.DvaJS
介绍
dva.js 是一个基于 React 和 Redux 的轻量级前端框架,用于简化复杂应用程序的状态管理和数据流。它是由阿里巴巴前端团队开发和维护的,旨在提供一种简单且可扩展的开发模式。
dva.js 主要包含以下特点和概念:
- 基于 Redux:dva.js 建立在 Redux 的基础之上,提供了 Redux 的核心概念,如 reducers、actions 和 store。它通过封装 Redux 的复杂性,使开发人员能够更轻松地使用 Redux 进行状态管理。
- 集成 React-Router:dva.js 默认集成了 React-Router,使得开发单页面应用(SPA)的路由管理更加简单和高效。
- 集成 Redux-Saga:dva.js 默认集成了 Redux-Saga,一个用于管理副作用(例如异步请求和数据获取)的库。通过 Redux-Saga,可以在应用程序中以声明式和可测试的方式处理异步操作。
- 约定式的配置:dva.js 遵循约定大于配置的原则,提供了一套约定来简化应用程序的配置和开发流程。例如,它默认约定了 models、services、routes 等目录结构,并自动关联它们。
- 插件化的架构:dva.js 提供了插件化的架构,允许开发人员根据项目需求自由地扩展和定制功能。它拥有丰富的插件生态系统,可满足不同的需求。
通过以上特点,dva.js 提供了一种简化和规范化的开发模式,使得开发人员能够更快速地构建复杂的前端应用程序。它在 React 生态系统中得到了广泛的应用和认可,并被许多开发团队用于实际项目开发。如果你对基于 React 和 Redux 的应用程序开发感兴趣,可以考虑尝试使用 dva.js。
dva 首先是一个基于 redux 和 redux-saga 的数据流方案(数据管理),然后为了简化开发体验,dva 还额外内置了 react- router 和 fetch, 所以也可以理解为一个轻量级的应用框架。 ——> 可以理解为 cra 的代替品!
dva = React-Router + Redux + Redux-saga
umi 是可以集成 dva 的!
安装
npm install dva-cli -g
#查看版本号
dva -v
创建项目
dva new myapp-dva
注意:现在用 dva 创建项目已经被废弃了,官网推荐用 umi 创建项目并集成 dva!我们对于 dva 简单了解即可!
dva 应用的最简结构
index.js
import dva from "dva";
const App = () => <div>Hello dva</div>;
// 创建应用
const app = dva();
// 注册视图
app.router(() => <App />);
// 启动应用
app.start("#root");
数据流图
通过路由连接组件,组件通过 connect 与 数据仓库连接,通过 reducer 操作数据仓库(异步操作会经过 effect 再到 reducer,并且 effect 会于服务端进行请求交互)
index.js
import dva from "dva";
import "./index.css";
// 1. Initialize
const app = dva({
// history: require("history").createBrowserHistory()
history: require("history").createHashHistory(), //不写的话默认是 hash 模式
});
// 2. Plugins
// app.use({});
// 3. Model //用模型管理状态,模型与组件之间有对应的关系形成模块化!
app.model(require("./models/maizuo").default);
// app.model(require('./models/aaa').default); //还可以引入其他的 model
// app.model(require('./models/bbb').default);
// 4. Router //管理路由
app.router(require("./router").default);
// 5. Start
app.start("#root");
router.js
import React from "react";
import { Router, Route, Switch, Redirect } from "dva/router"; //dva 封装的 router
import App from "./routes/App"; //routes 里面就是所有页面
import Film from "./routes/Film";
import Cinema from "./routes/Cinema";
import Center from "./routes/Center";
import Detail from "./routes/Detail";
import Login from "./routes/Login";
function RouterConfig({ history }) {
return (
<Router history={history}>
<Switch>
{/* /login必须要放在/的前面 */}
<Route path='/login' component={Login} />
<Route
path='/'
render={() => (
//使用回调函数的写法,更加方便配置大组件路由
//根组件:App组件 里面必须要留一个插槽,否则跳转路由会把整个 App 替换掉的(这些路由也不能写在 App里面,因为所有的路由必须写在这个 router.js里面!这是规矩!)
<App>
<Switch>
<Route path='/film' component={Film} />
<Route path='/cinema' component={Cinema} />
{/* 这里加 render 可以继续配置孙子路由 或者 可以进行鉴权 (render 回调里面可以写任意的 jsx 结构,包括组件和路由等)*/}
<Route
path='/center'
render={() =>
localStorage.getItem("token") ? ( //并不是所有的数据都需要本地持久化,一些展示性的数据一般在 redux 中存放即可(不刷新就一直存在),而 token 或者标志变量类的数据可以直接放到 localStorage 中!
<Center />
) : (
<Redirect to='/login' />
)
}
/>
{/* 动态路由 */}
<Route path='/detail/:myid' component={Detail} />
<Redirect from='/' to='/film'></Redirect>
</Switch>
</App>
)}
/>
</Switch>
</Router>
);
}
export default RouterConfig;
根组件 App.jsx
import { connect } from "dva";
import React, { Component } from "react";
import Tabbar from "../components/Tabbar";
class App extends Component {
componentDidMount() {
// console.log(this.props)
}
render() {
return (
<div>
{this.props.children}
{this.props.isShow && <Tabbar />} {/*这里可以方便地配置 tabbar:导航栏,因为我们的 tabbar 不是任何时候都进行显示,所以并且必须放在这里,因为在组件里面才可以根据 flag 值进行 tabbar 的显示和隐藏,在路由配置文件router.js中做不到的!*/}
</div>
);
}
}
export default connect((state) => {
// 使用 redux 数据
// console.log(state)
return {
a: 1,
isShow: state.maizuo.isShow, //使用 state
};
})(App);
导航栏 Tabbar.jsx
import React, { Component } from "react";
import style from "./Tabbar.css"; //注意在 dva 里面必须这样引入,但是 css 前面不用加.module,也就是说 dva 自动进行了模块化
import { NavLink } from "dva/router";
export default class Tabbar extends Component {
render() {
return (
<footer>
<ul>
<li>
<NavLink to='/film' activeClassName={style.active}>
film
</NavLink>
</li>
<li>
<NavLink to='/cinema' activeClassName={style.active}>
cinema
</NavLink>
</li>
<li>
<NavLink to='/center' activeClassName={style.active}>
center
</NavLink>
</li>
</ul>
</footer>
);
}
}
./models/maizuo.js
model 的定义,与 saga 的使用:
import { getCinemaListService } from "../services/maizuo";
export default {
namespace: "maizuo", //直接定义命名空间即可,但是不需要到公共文件store.js中去集合,和之前不一样!
state: {
isShow: true,
list: [],
},
// 注意:model 省略了 action 的创建函数,在 dispatch 的时候直接给 type 和 data 就可以了!
reducers: {
hide(prevState, action) {
//type 就对应了 reducer 这个方法的名字,前面要加上这个 model 的模块名/为前缀
return { ...prevState, isShow: false };
},
show(prevState, action) {
return { ...prevState, isShow: true };
},
changeCinemaList(prevState, { payload }) {
return { ...prevState, list: payload };
},
},
subscriptions: {
//订阅函数
setup({ dispatch, history }) {
//在模型被注册时执行的钩子
// eslint-disable-line
console.log("初始化");
// 可以转发 action,可以跳转路由
},
},
//异步处理器:集成redux-saga(对于异步 action,需要经过一个中间处理器才可以到 reducer)
effects: {
*getCinemaList(action, { call, put }) {
//直接接收 action 与 saga中的方法 //type 就对应了 effects 这个方法的名字,前面要加上这个 model 的模块名/为前缀
var res = yield call(getCinemaListService);
console.log(res.data.data.cinemas);
yield put({
type: "changeCinemaList", //内部就不需要加模块名前缀了
payload: res.data.data.cinemas,
});
},
},
};
使用封装的 fetch 构建封装的 api 函数:./services/maizuo.js
import request from "../utils/request";
export function getCinemaListService() {
return request(
"https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=5386964",
{
headers: {
"X-Client-Info":
'{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
"X-Host": "mall.film-ticket.cinema.list",
},
}
);
}
export function login() {
return request("/users/login", {
method: "POST",
body: JSON.stringify({
username: this.username.current.value,
password: this.password.current.value,
}),
headers: {
"Content-Type": "application/json",
},
});
}
Detail.jsx 页面向 model 分发同步 action
import { connect } from "dva";
import React, { Component } from "react";
class Detail extends Component {
componentDidMount() {
console.log(
`接受上个页面传来的id,利用此id取数据`,
this.props.match.params.myid
);
this.props.dispatch({
type: "maizuo/hide",
});
}
componentWillUnmount() {
this.props.dispatch({
type: "maizuo/show",
});
}
render() {
return <div>Detail</div>;
}
}
export default connect()(Detail); //连接还是一样,但是这里不再给 action函数,因为 model 本身就省略了 action 的创建函数,会默认给 dispatch 方法到 props 中!
在 Cinema.js 中使用异步处理器请求数据
import { connect } from "dva";
import React, { Component } from "react";
class Cinema extends Component {
componentDidMount() {
if (this.props.list.length === 0) {
//如果没有数据就请求
//dispatch
this.props.dispatch({
type: "maizuo/getCinemaList",
});
} else {
//如果有数据就用 redux 的数据 ——> 实现了数据缓存,减少客户端流量损耗,减轻服务器端压力,增强用户体验!
console.log("缓存", this.props.list);
}
}
render() {
return (
<div>
<ul>
{this.props.list.map((item) => (
<li key={item.cinemaId}>{item.name}</li>
))}
</ul>
</div>
);
}
}
const mapStateToProps = (state) => ({
//要获取 state,需要自己进行定义!还是和之前一模一样!
list: state.maizuo.list,
});
export default connect(mapStateToProps)(Cinema);
配置反向代理,解决跨域:.webpackrc 文件
{
"proxy": {
"/api": {
"target": "https://i.maoyan.com",
"changeOrigin": true
}
}
}
mock
模拟假数据进行请求,普通 json 文件只能进行 get 模拟,而 mock 支持了 post 的模拟(并且可以在无跨域的情况下请求,因为是处在一服务上面的)
mock 文件:hao.js ——> 每个人写自己的 mock 文件
export default {
"GET /users": { name: "kerwin", age: 100, location: "dalian" }, //接口路径与返回值
"POST /users/login": (req, res) => {
//拿到请求数据
console.log(req.body);
if (req.body.username === "kerwin" && req.body.password === "123") {
res.send({
//返回值
ok: 1,
});
} else {
res.send({
ok: 0,
});
}
},
};
.roadhogrc.mock.js ——> 会把这个文件的接口当做后端服务来进行请求
const mockobj = require("./mock/hao");
export default {
...mockobj,
};
在组件中发请求,测试 mock:直接写/user 路径,即可 (不需要手动进行启动,.roadhogrc.mock.js 文件会自动启动的!)
request("/users").then((res) => {
console.log(res.data);
});
request("/users/login", {
method: "POST",
body: JSON.stringify({
username: this.username.current.value,
password: this.password.current.value,
}),
headers: {
//注意:这里必须要注明格式,因为 fetch 没有进行自动的格式判断,而 axios 会自动判断是 json格式,还是 html 表单编码格式!这样后端才能接收到 body!
"Content-Type": "application/json",
},
}).then((res) => {
console.log(res.data);
if (res.data.ok) {
localStorage.setItem("token", "dwadw23232");
this.props.history.push("/center");
} else {
alert("用户名密码不匹配");
}
});
5.UMI
基本介绍
注意:dva 现在很少单独去开发,都是在 umi 去集成 dva 去开发了!
UMI(可读作:you-mi)是一个基于 React 开发的可扩展企业级前端应用框架,由阿里巴巴的工程师团队开发和维护。它提供了一整套的开发规范和最佳实践,旨在帮助开发者构建可靠、可扩展和高效的 Web 应用程序。
以下是 UMI 的一些主要特点和功能:
- 插件化架构:UMI 采用插件化架构,可以根据项目需求选择并配置各种插件,以便于快速扩展功能,例如路由配置、状态管理、构建优化等。
- 路由管理:UMI 提供了强大的路由功能,支持配置式路由和约定式路由两种方式。你可以使用配置文件对路由进行定义,也可以根据约定的文件目录结构自动生成路由。
- 状态管理:UMI 内置了对常见状态管理工具的支持,包括 Redux、Dva 等,使得管理应用的状态变得更加简单和高效。
- 构建工具:UMI 提供了基于 webpack 的构建工具,内置了一系列的优化配置和插件,帮助你构建高性能的 Web 应用。
- 高度可配置:UMI 提供了丰富的配置选项,可以根据项目需求进行灵活的配置,包括代理配置、主题配置、构建配置等。
- 插件市场:UMI 拥有活跃的插件生态系统,你可以从插件市场中选择和使用各种插件,快速集成各类功能和工具。
- 测试支持:UMI 对单元测试、端到端测试等提供了良好的支持,帮助开发者编写可靠的测试代码。
UMI 是一个功能强大且灵活的前端应用框架,它的目标是提供一套完整的开发体验和最佳实践,帮助开发者提升开发效率和项目质量。无论是构建单页面应用还是多页面应用,UMI 都可以成为你的首选框架之一。
umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架。umi 是以路由为基础的,支持类 next.js 的约定式路由,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。umi 在约定式路由的功能层面会更像 nuxt.js 一些。
开箱即用,省去了搭框架的时间。
安装
mkdir myapp
cd myapp //空目录
npx @umijs/create-umi-app //创建 package.json及其他文件
npm i
npm start //启动
umi 默认集成了 ts,所以内容都是基于 ts 的
目录
mock 文件夹用于模拟后端接口,做前后端测试
src 主文件夹:
pages 文件夹下是所有的页面(构建约定式路由)
.umi 文件夹是自动生成的,由 umi 框架来进行管理
.umirc.ts 文件是最重要的,umi 核心配置文件

umi 会根据 pages 目录自动生成路由配置。需要注释.umirc.js、routes 相关,否则自动配置不生效。
配置式路由
export default defineConfig({
nodeModulesTransform: {
type: "none",
},
routes: [
//在这里配置即可,和vue-router基本一样
{ path: "/", component: "@/pages/index" },
],
fastRefresh: {},
history: {
type: "hash",
},
});
约定式路由
(1)基础路由
首先将配置文件的 routes 选项注释掉
然后直接在 pages 目录下创建页面文件即可(大小写不敏感,但一般页面文件的首字母大写),自动生成路由配置
注意:首页是必须为首字母小写的,index.tsx(对应的路径为/)——> 如果怕名字出错,那么就所有文件名字都首字母小写即可

访问/center、/cinema、/film 即可进入对应的页面(大小写不敏感)
(2)重定向
index.tsx 为首页 (固定的名字)——> 实际上相当于 Index.jsx(并不是 App.jsx)
import React from "react";
import { Redirect } from "umi";
export default function Index() {
return <Redirect to='/film' />;
}
(3)404 页面
在 pages 文件夹下直接创建 404.tsx(固定的名字)即可
import React from "react";
export default function NotFound() {
return <div>404 not found</div>;
}
(4)嵌套路由
构建如下目录结构

这样就有了/film/comingsoon 和/film/nowplaying 两个嵌套路径了
_layout.tsx 为默认名字的文件,指定父路由/film 的内容
import { Redirect, useLocation } from "umi"; //useHistory和useLocation钩子都被 umi 接管了(路由的内容基本都被接管)
export default function Film(props: any) {
const location = useLocation(); //这是 react 的钩子,用于获取 location 对象
// console.log(location)
// 进行重定向,指定默认跳转到的页面
if (location.pathname === "/film" || location.pathname === "/film/") {
//二级重定向
return <Redirect to='/film/nowplaying' />;
}
return (
<div>
<div style={{ height: "200px", background: "yellow" }}>大轮播</div>
{props.children} {/* 必须要留插槽,相当于router-view */}
</div>
);
}
comingsoon.tsx
import { useEffect } from "react";
export default function Comingsoon() {
useEffect(() => {
fetch(
"/api/mmdb/movie/v3/list/hot.json?ct=%E5%8C%97%E4%BA%AC&ci=1&channelId=4"
)
.then((res) => res.json())
.then((res) => {
console.log(res);
});
}, []);
return <div>Comingsoon</div>;
}
nowplaying.tsx
import { useEffect, useState } from "react";
import { useHistory } from "umi"; //useHistory和useLocation钩子都被 umi 接管了
export default function Nowplaying(props: any) {
const [list, setlist] = useState([]);
const history = useHistory();
useEffect(() => {
fetch(
"https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=1&k=7383801",
{
headers: {
"X-Client-Info":
'{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
"X-Host": "mall.film-ticket.film.list",
},
}
)
.then((res) => res.json())
.then((res) => {
console.log(res.data.films);
setlist(res.data.films);
});
}, []);
return (
<div>
{list.map((item: any) => (
<li
key={item.filmId}
onClick={() => {
history.push(`/detail/${item.filmId}`);
}}>
{item.name}
</li>
))}
</div>
);
}
(5)layouts 容器 与 声明式导航
可以在里面构建声明式导航 ——> 全局导航栏
layouts 文件夹,里面的 index.tsx 会包裹所有的组件,相当于之前的 App.jsx
./layouts/index.tsx
import { NavLink } from "umi";
import "./index.less"; //默认集成了 less,直接可以用
export default function IndexLayout(props: any) {
//设置动态地显示导航栏,如果是特定的路径,就只显示组件,不显示导航栏!
if (
props.location.pathname === "/city" ||
props.location.pathname.includes("/detail")
) {
return <div>{props.children}</div>;
}
return (
<div>
{props.children}
<ul>
<li>
<NavLink to='/film' activeClassName='active'>
film
</NavLink>
</li>
<li>
<NavLink to='/cinema' activeClassName='active'>
cinema
</NavLink>
</li>
<li>
<NavLink to='/center' activeClassName='active'>
center
</NavLink>
</li>
</ul>
</div>
);
}
(6)编程式导航 与 动态路由
const history = useHistory();
onClick={() => {
history.push(`/detail/${item.filmId}`); //实际上不一定用useHistory()钩子,直接用 props.里面也有 history 对象!
}}
构建如下目录结构:

相当于一个可以接收 id 参数的 detail.tsx 文件!
接收参数的文件的命名规则:[参数 1,参数 2].tsx 固定的规则
import { useParams } from "umi";
interface IParams {
id: string;
}
export default function Detail(props: any) {
// console.log(props) //实际上 props 里面就有match.params对象了
const params = useParams<IParams>(); //也可以用 hook 去获取(给函数可以传递一个类型,就可以获取自定义类型的 params)
console.log(params.id);
return <div>Detail</div>;
}
(7)路由拦截
需要添加包装器
在 wrappers 文件夹(固定名字)下定义规则文件
Auth.tsx(随意名字)——> 一个高阶组件,用于鉴权(渲染劫持)
import { Redirect } from "umi";
export default function Auth(props: any) {
if (localStorage.getItem("token")) {
return <div>{props.children}</div>;
}
return <Redirect to='/login' />;
}
Center.tsx ——> 需要被鉴权的组件
function Center() {
return <div>Center</div>;
}
Center.wrappers = ["@/wrappers/Auth"]; //设置包装器(即 Center 的父组件就是 Autn这个高阶组件,把 Center 作为 props 的 chileren 属性 传入)
export default Center;
(8)路由模式配置
在配置文件中:修改之后不需要重启服务器 ——> umi 的强大之处
export default defineConfig({
...
history:{
type:"hash" //改为 hash 模式,默认是 history 模式
},
...
});
mock 功能
./mock/api.js ——> 写了就行,不需要在进行其他配置(和 dva 的不同之处)
export default {
"GET /users": { name: "kerwin", age: 100 },
"POST /users/login": (req, res) => {
console.log(req.body);
if (req.body.username === "kerwin" && req.body.password === "123") {
res.send({
ok: 1,
});
} else {
res.send({
ok: 0,
});
}
},
};
使用:
onClick={() => {
fetch('/users/login', { //使用原生的 fetch,不需要引入
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
})
.then((res) => res.json())
.then((res) => {
console.log(res);
if (res.ok) {
localStorage.setItem('token', username);
history.push(`/center`);
} else {
alert('用户名密码不匹配');
}
});
}}
反向代理配置
import { defineConfig } from 'umi';
export default defineConfig({
...
proxy:{
"/api":{ //路径中有 /api 的,就转发到 https://i.maoyan.com
target:"https://i.maoyan.com",
changeOrigin:true
}
},
...
});
antd mobile 集成
注意:可以直接使用,不需要安装,已经默认集成了,但是版本并不一定是我们想要的(一般比较旧)
升级版本:自行安装 npm i --saveantd-mobile@next
,然后把默认的给禁用掉
export default defineConfig({
antd: {
mobile: false,
},
});
使用:
Cinema.tsx
import React from "react";
import { NavBar } from "antd-mobile";
import { SearchOutline } from "antd-mobile-icons";
export default function Cinema() {
return (
<div>
<NavBar
onBack={() => {
console.log("click");
}}
back='北京'
backArrow={false}
right={<SearchOutline />}>
标题
</NavBar>
Cinema
</div>
);
}
City.tsx
使用索引组件 IndexBar(有一个标题列表,每个标题对应一个城市列表)
import { useEffect, useState } from 'react';
import { IndexBar, List } from 'antd-mobile';
import { useHistory } from 'umi';
function City(props: any) {
const history = useHistory();
const [list, setlist] = useState<any>([]);
//进行对象数组的构建
const filterCity = (cities: any) => {
console.log(cities);
const letterArr: Array<string> = []; //所有首字母的数组
const newlist = [];
for (var i = 65; i < 91; i++) {
letterArr.push(String.fromCharCode(i)); //65-90 的 unicode 码就是对应的26 个英文字母
}
//遍历所有字母
for (var m in letterArr) {
var cityitems: any = cities.filter(
(item: any) =>
//有一个拼音字段,判断首字母是否为当前的字母,过滤出来一个数组
item.pinyin.substring(0, 1).toUpperCase() === letterArr[m],
);
//
cityitems.length &&
newlist.push({
title: letterArr[m], //字母标志
items: cityitems, //字母对应的城市数组
});
}
return newlist;
};
useEffect(() => {
fetch('https://m.maizuo.com/gateway?k=2145459', {
headers: {
'X-Client-Info':
'{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
'X-Host': 'mall.film-ticket.city.list',
},
})
.then((res) => res.json())
.then((res) => {
setlist(filterCity(res.data.cities)); //请求所有城市数据,进行处理构建后保存为 list对象数组,每一个对象的其中一个字段为一个数组,一个字段为字母标志
});
}, []);
return (
<div style={{ height: window.innerHeight }}>
<IndexBar>
{list.map((item: any) => {
const { title, items } = item;
return (
{/*每一个字母面板*/}
<IndexBar.Panel index={title} title={title} key={title}>
{/*城市列表数据*/}
<List>
{items.map((item: any, index: number) => (
<List.Item key={index}>
{item.name}
</List.Item>
))}
</List>
</IndexBar.Panel>
);
})}
</IndexBar>
</div>
);
}
export default City;
dva 集成
按目录约定注册 model,无需手动 app.model(在入口文件中注册)
文件名就作为了 namespace,可以省去 model 文件中的 namespace key(也可以写 namespace)
无需手写 router.js,交给 umi 处理,支持 model 和 component 的按需加载(默认可以按需加载)
内置 query-string 处理,无需再手动解码和编码
内置 dva-loading 和 dva-immer,其中 dva-immer 需通过配置开启(简化 reducer 编写)
注意:默认开启了 redux 调试工具,不需要额外安装和配置
在 models 文件夹(固定名字)下新建模型
Cinema.ts
export default {
namespace: "cinema", //可以不写
state: {
list: [],
},
reducers: {
clearList(prevState: any, action: any) {
//方法名字就是 type
return {
...prevState,
list: [],
};
},
changeList(prevState: any, action: any) {
return {
...prevState,
list: action.payload,
};
},
},
//异步处理器
effects: {
*getList(action: any, obj: any): any {
//方法名字就是 type
// console.log("getList",action,obj)
const { put, call } = obj;
var res = yield call(getListForCinema, action.payload.cityId);
yield put({
type: "changeList",
payload: res,
});
},
},
};
async function getListForCinema(cityId) {
// console.log(cityId)
var res = await fetch(
`https://m.maizuo.com/gateway?cityId=${cityId}&ticketFlag=1&k=6412143`,
{
headers: {
"X-Client-Info":
'{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529"}',
"X-Host": "mall.film-ticket.cinema.list",
},
}
).then((res) => res.json());
// console.log(res)
return res.data.cinemas;
}
CityModel.ts
export default {
namespace: "city", //命名空间,可以不写
state: {
cityName: "北京",
cityId: "110100",
},
reducers: {
changeCity(prevState: any, action: any) {
console.log(action);
return {
...prevState,
cityName: action.payload.cityName,
cityId: action.payload.cityId,
};
},
},
};
优化上面的 City.tsx 文件
import { connect } from "dva";
const changeCity = (item) => {
console.log(item.name, item.cityId);
// 选中城市之后,修改store state中的状态(props 里面就有 dispatch 方法)
props.dispatch({
type: "city/changeCity",
payload: {
cityName: item.name,
cityId: item.cityId,
},
});
history.push("/cinema"); //跳转到 cinema 页面(cinema 从 redux 中取出我们选择的城市数据进行应用,不采用路由传参,不一定好用)
};
<List.Item key={index} onClick={() => changeCity(item)}>
{item.name}
</List.Item>;
export default connect(() => ({}))(City);
Cinema.tsx
import { useEffect } from "react";
import { NavBar, DotLoading } from "antd-mobile";
import { SearchOutline } from "antd-mobile-icons";
import { connect } from "dva";
function Cinema(props: any) {
// console.log(props)
useEffect(() => {
if (props.list.length === 0) {
//获取列表数据
props.dispatch({
type: "cinema/getList",
payload: {
cityId: props.cityId,
},
});
} else {
console.log("缓存");
}
}, []);
return (
<div>
<NavBar
onBack={() => {
//清空cinema list数据(因为我们要换城市了,必须要清除,否则下一次就不会再请求新的了!)
props.dispatch({
type: "cinema/clearList",
});
props.history.push(`/city`); //跳转到 city,选择新的城市
}}
back={props.cityName} //显示被选择的城市名字
backArrow={false}
right={<SearchOutline />}>
标题
</NavBar>
{/*根据 loading 判断展示状态*/}
{props.loading && (
<div style={{ fontSize: 14, textAlign: "center" }}>
<DotLoading />
</div>
)}
<ul>
{props.list.map((item: any) => (
<li key={item.cinemaId}>{item.name}</li>
))}
</ul>
</div>
);
}
export default connect((state: any) => {
// console.log(state)
return {
a: 1, //自定义的键值
loading: state.loading.global, //默认集成了dva-loading,loading.global在有异步请求 effects 的时候就为 true,所有的 effects 都走完了就变为 false
cityName: state.city.cityName,
cityId: state.city.cityId,
list: state.cinema.list,
};
})(Cinema);