28 | //或者
29 |
Hello world
30 |
31 | )
32 | }
33 | });
34 | ```
35 |
36 | 可以看出,React 的 style 属性接收的也是一个 JavaScript 对象。
37 |
38 | ## Styled Component
39 |
40 | # Class
41 |
42 | 你可以根据这个策略为每个组件创建 CSS 文件,可以让组件名和 CSS 中的 class 使用一个命名空间,来避免一个组件中的一些 class 干扰到另外一些组件的 class。
43 |
44 | _app/components/MyComponent.css_
45 |
46 | ```css
47 | .MyComponent-wrapper {
48 | background-color: #eee;
49 | }
50 | ```
51 |
52 | _app/components/MyComponent.jsx_
53 |
54 | ```js
55 | import "./MyComponent.css";
56 | import React from "react";
57 |
58 | export default React.createClass({
59 | render: function () {
60 | return (
61 |
62 |
Hello world
63 |
64 |
65 | );
66 | },
67 | });
68 | ```
69 |
70 | ## Multiple Class
71 |
72 | 上文中提及的利用 className 方式赋值,如果在存在多个类名的情况下:
73 |
74 | ```js
75 | render: function() {
76 | var cx = React.addons.classSet;
77 | var classes = cx({
78 | 'message': true,
79 | 'message-important': this.props.isImportant,
80 | 'message-read': this.props.isRead
81 | });
82 | // same final string, but much cleaner
83 | return
Great, I'll be there.
;
84 | }
85 | ```
86 |
87 | 这里的 classSet 只是起到了一个帮助进行类名合成的功能,React 官方已经弃用了,改为了[这个](https://github.com/JedWatson/classnames)。
88 |
89 | # SCSS
90 |
--------------------------------------------------------------------------------
/04~工程实践/类 React 库/Preact.md:
--------------------------------------------------------------------------------
1 | # Preact
2 |
3 | Preact,它是 React 的 3KB 轻量替代方案,拥有同样的 ES6 API。高性能,轻量,即时生产是 Preact 关注的核心。基于这些主题,Preact 关注于 React 的核心功能,实现了一套简单可预测的 diff 算法使它成为最快的虚拟 DOM 框架之一,同时 preact-compat 为兼容性提供了保证,使得 Preact 可以无缝对接 React 生态中的大量组件,同时也补充了很多 Preact 没有实现的功能。
4 |
5 | 
6 |
7 | ## 工作流程
8 |
9 | 简单介绍了 Preact 的前生今世以后,接下来说下 Preact 的工作流程,主要包含五个模块:component、h 函数、render、diff 算法、回收机制。首先是我们定义好的组件,在渲染开始的时候,首先会进入 h 函数生成对应的 virtual node(如果是 JSX 编写,之前还需要一步转码)。每一个 vnode 中包含自身节点的信息,以及子节点的信息,由此而连结成为一棵 virtual dom 树。基于生成的 vnode,render 模块会结合当前 dom 树的情况进行流程控制,并为后续的 diff 操作做一些准备工作。
10 |
11 | 
12 |
13 | Preact 的 diff 算法实现有别于 react 基于双 virtual dom 树的思路,Preact 只维持一棵新的 virtual dom 树,diff 过程中会基于 dom 树还原出旧的 virtual dom 树,再将两者进行比较,并在比较过程中实时对 dom 树进行 patch 操作,最终生成新的 dom 树。与此同时,diff 过程中被卸载的组件和节点不会被直接删除,而是被分别放入回收池中缓存,当再次有同类型的组件或节点被构建时,可以在回收池中找到同名元素进行改造,避免从零构建的开销。
14 |
15 | # 快速开始
16 |
17 | ```js
18 | import { h, render, Component } from "preact";
19 |
20 | class Clock extends Component {
21 | state = { time: Date.now() };
22 |
23 | // Called whenever our component is created
24 | componentDidMount() {
25 | // update time every second
26 | this.timer = setInterval(() => {
27 | this.setState({ time: Date.now() });
28 | }, 1000);
29 | }
30 |
31 | // Called just before our component will be destroyed
32 | componentWillUnmount() {
33 | // stop when not renderable
34 | clearInterval(this.timer);
35 | }
36 |
37 | render() {
38 | let time = new Date(this.state.time).toLocaleTimeString();
39 | return
{time};
40 | }
41 | }
42 |
43 | render(
, document.body);
44 | ```
45 |
46 | # Links
47 |
48 | — https://www.axihe.com/react/preact/home.html#linkstate Preact 学习笔记
49 |
--------------------------------------------------------------------------------
/04~工程实践/组件测试/Jest.md:
--------------------------------------------------------------------------------
1 | # 基于 Jest 的 React 组件测试
2 |
3 | # JSX
4 |
5 | TypeScript 具有三种 JSX 模式:preserve,react 和 react-native。这些模式只在代码生成阶段起作用,类型检查并不受影响。
6 |
7 | - 在 preserve 模式下生成代码中会保留 JSX 以供后续的转换操作使用(比如:Babel)。另外,输出文件会带有.jsx 扩展名。
8 |
9 | - react 模式会生成 React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。
10 |
11 | - react-native 相当于 preserve,它也保留了所有的 JSX,但是输出文件的扩展名是.js。
12 |
13 | 假设有这样一个 JSX 表达式`
`,expr 可能引用环境自带的某些东西(比如,在 DOM 环境里的 div 或 span)或者是你自定义的组件。这是非常重要的,原因有如下两点:
14 |
15 | - 对于 React,固有元素会生成字符串`(React.createElement("div"))`,然而由你自定义的组件却不会生成(React.createElement(MyComponent))。
16 | - 传入 JSX 元素里的属性类型的查找方式不同。固有元素属性本身就支持,然而自定义的组件会自己去指定它们具有哪个属性。
17 |
18 | TypeScript 使用与 React 相同的规范 来区别它们。固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。
19 |
20 | # 固有元素
21 |
22 | 固有元素使用特殊的接口 JSX.IntrinsicElements 来查找。默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。然而,如果这个接口存在,那么固有元素的名字需要在 JSX.IntrinsicElements 接口的属性里查找。例如:
23 |
24 | ```jsx
25 | declare namespace JSX {
26 | interface IntrinsicElements {
27 | foo: any
28 | }
29 | }
30 |
31 |
; // 正确
32 |
; // 错误
33 | ```
34 |
35 | 在上例中,`
` 没有问题,但是 `
` 会报错,因为它没在 JSX.IntrinsicElements 里指定。
36 |
37 | ## 基于值的元素
38 |
39 | 基于值的元素会简单的在它所在的作用域里按标识符查找。
40 |
41 | ```jsx
42 | import MyComponent from "./myComponent";
43 |
44 |
; // 正确
45 |
; // 错误
46 | ```
47 |
48 | ## 工厂函数
49 |
50 | `jsx: react`编译选项使用的工厂函数是可以配置的。可以使用 jsxFactory 命令行选项,或内联的@jsx 注释指令在每个文件上设置。比如,给 createElement 设置 jsxFactory,`
` 会使用 `createElement("div")` 来生成,而不是 React.createElement("div")。
51 |
52 | 注释指令可以像下面这样使用(在 TypeScript 2.8 里):
53 |
54 | ```js
55 | import preact = require("preact");
56 | /* @jsx preact.h */
57 | const x =
;
58 | ```
59 |
60 | 生成:
61 |
62 | ```js
63 | const preact = require("preact");
64 | const x = preact.h("div", null);
65 | ```
66 |
67 | 工厂函数的选择同样会影响 JSX 命名空间的查找(类型检查)。如果工厂函数使用 React.createElement 定义(默认),编译器会先检查 React.JSX,之后才检查全局的 JSX。如果工厂函数定义为 h,那么在检查全局的 JSX 之前先检查 h.JSX。
68 |
--------------------------------------------------------------------------------
/04~工程实践/服务端渲染/搭建渲染服务器.md:
--------------------------------------------------------------------------------
1 | # 基于 Express 的渲染服务器
2 |
3 | ## renderToString
4 |
5 | React 提供了两个方法 `renderToString` 和 `renderToStaticMarkup` 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。这两个方法被包含在了 react-dom 仓库中,可以通过如下方式引入与使用:
6 |
7 | ```js
8 | import ReactDOMServer from "react-dom/server";
9 |
10 | var ReactDOMServer = require("react-dom/server");
11 |
12 | ReactDOMServer.renderToString(element);
13 | ```
14 |
15 | 我们可以在服务端即使用`renderToString`将组件转化为 HTML 标签然后传递给客户端,这里 React 会自动为标签进行校验和计算;这样我们在客户端调用 `ReactDOM.render()` 渲染某个组件时,如果 React 发现已经存在了服务端渲染好的标签,则会直接使用这些标签来节约渲染时间。ReactDOMServer 中提供的另一个渲染函数是`renderToStaticMarkup`,其很类似于`renderToString`,不过其忽略了额外的譬如`data-reactid`这样的 React 内部使用的非 HTML 标准属性;如果你只想把 React 作为简单的静态网页生成器,那么推荐使用这种方式,会帮你避免额外的带宽消耗。
16 |
17 | 服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:
18 |
19 | - 前后端可以共享状态
20 |
21 | - 前后端路由可以统一处理
22 |
23 | ## 状态传递
24 |
25 | ## 路由权限控制
26 |
27 | ```js
28 | import { renderToString } from "react-dom/server";
29 | import { match, RouterContext } from "react-router";
30 | import routes from "./routes";
31 |
32 | serve((req, res) => {
33 | // Note that req.url here should be the full URL path from
34 | // the original request, including the query string.
35 | match(
36 | { routes, location: req.url },
37 | (error, redirectLocation, renderProps) => {
38 | if (error) {
39 | res.status(500).send(error.message);
40 | } else if (redirectLocation) {
41 | res.redirect(302, redirectLocation.pathname + redirectLocation.search);
42 | } else if (renderProps) {
43 | // You can also check renderProps.components or renderProps.routes for
44 | // your "not found" component or route respectively, and send a 404 as
45 | // below, if you're using a catch-all route.
46 | res
47 | .status(200)
48 | .send(renderToString(
));
49 | } else {
50 | res.status(404).send("Not found");
51 | }
52 | }
53 | );
54 | });
55 | ```
56 |
57 | # 基于 Next.js 快速搭建渲染服务器
58 |
--------------------------------------------------------------------------------
/04~工程实践/组件测试/99~参考资料/README.md:
--------------------------------------------------------------------------------
1 | # 参考资料
2 |
3 | # Test | 测试
4 |
5 | - [2017~Testing React Applications #Series#](https://blog.logrocket.com/testing-react-applications-part-1-of-3-ebd8397917f3):With React and the ecosystem of testing tools that have emerged around it, it’s finally possible to build robust, scalable tests that provide strong guarantees on code correctness.
6 |
7 | - [Snapshot Testing React Components with Jest](https://semaphoreci.com/community/tutorials/snapshot-testing-react-components-with-jest)
8 |
9 | - [Testing in React: best practices, tips and tricks](https://parg.co/bsP)
10 |
11 | - [2017~Testing React components with Jest and Enzyme](https://hackernoon.com/testing-react-components-with-jest-and-enzyme-41d592c174f#.yfpuy4eip)
12 |
13 | - [Testing a React & Redux Codebase](http://silvenon.com/testing-react-and-redux/)
14 |
15 | - [Testing in React: Getting Off The Ground](https://medium.com/javascript-inside/testing-in-react-getting-off-the-ground-5f569f3088a#.6ip96uul5)
16 |
17 | - [a-step-by-step-tdd-approach-on-testing-react-components-using-enzyme](http://thereignn.ghost.io/a-step-by-step-tdd-approach-on-testing-react-components-using-enzyme/)
18 |
19 | - [enzyme-javascript-testing-utilities-for-react](https://medium.com/airbnb-engineering/enzyme-javascript-testing-utilities-for-react-a417e5e5090f#.huj3rtv24)
20 |
21 | - [Testing React Apps With Jest](https://facebook.github.io/jest/docs/tutorial-react.html)
22 |
23 | - [2017~Front-end (React) Snapshot Testing with Jest: What is it for?](https://parg.co/bRQ)
24 |
25 | - [2017~Jest Testing patterns in React-Redux applications](https://parg.co/U1G): Jest provides a complete ecosystem for testing. There is no need of extra libraries - Mocha, Sinon, Istanbul, Chai, proxyquire etc. as all are present in Jest itself.
26 |
27 | ## Component Test | 组件测试
28 |
29 | ## E2E Test | 端到端测试
30 |
31 | - [2018~End-to-end testing React apps with Puppeteer and Jest](https://blog.logrocket.com/end-to-end-testing-react-apps-with-puppeteer-and-jest-ce2f414b4fd7): In this tutorial, we’ll see how to write tests for a React app using Jest and Puppeteer.
32 |
--------------------------------------------------------------------------------
/03~状态管理/Recoiljs/README.md:
--------------------------------------------------------------------------------
1 | # Rcoiljs
2 |
3 | Rcoiljs 是 Facebook 针对 React 推出的新的状态管理框架,它更小,更加地 Reactive,不会破坏 Code Splitting。
4 |
5 | # Atoms 和 Selectors
6 |
7 | Atoms 跟名字一样,就是原子化,提供一个 state,如下,设置唯一的 key 和 默认值:
8 |
9 | ```js
10 | const todoListState = atom({
11 | key: "todoListState",
12 | default: [],
13 | });
14 | ```
15 |
16 | 当我们在 app 里面使用的时候,从官网的 todo list 项目来看,有三种使用方式
17 |
18 | - 单纯去使用它的值 `const todoList = useRecoilValue(todoListState);`, 如下
19 |
20 | ```js
21 | {todoList.map((todoItem) => (
22 |
23 | ))}
24 | ```
25 |
26 | - 单纯想去更新值 `const setTodoList = useSetRecoilState(todoListState);`, 如下
27 |
28 | ```js
29 | const addItem = () => {
30 | setTodoList((oldTodoList) => [
31 | ...oldTodoList,
32 | {
33 | id: getId(),
34 | text: inputValue,
35 | isComplete: false,
36 | },
37 | ]);
38 | };
39 | ```
40 |
41 | - 想同时获取值和可以更新值 `const [todoList, setTodoList] = useRecoilState(todoListState);`,类似 react useState,其中 todolist 是 state 值,这个没什么好说,setTodoList 也是直接把值设置进去,注意跟上面 useSetRecoilState 产出的 setTodoList 的区别,
42 |
43 | # Selectors
44 |
45 | ```js
46 | const todoListFilterState = atom({
47 | key: "todoListFilterState",
48 | default: "Show All",
49 | });
50 |
51 | const filteredTodoListState = selector({
52 | key: "filteredTodoListState",
53 | get: ({ get }) => {
54 | const filter = get(todoListFilterState);
55 | const list = get(todoListState);
56 |
57 | switch (filter) {
58 | case "Show Completed":
59 | return list.filter((item) => item.isComplete);
60 | case "Show Uncompleted":
61 | return list.filter((item) => !item.isComplete);
62 | default:
63 | return list;
64 | }
65 | },
66 | });
67 | ```
68 |
69 | 同时 selector 也支持 set 操作,类似官网对华氏度和摄氏度的转化, 当我们对摄氏度的 selector 进行赋值的时候,也会更新华氏度 tempFahrenheit 的值:
70 |
71 | ```js
72 | const tempFahrenheit = atom({
73 | key: 'tempFahrenheit',
74 | default: 32,
75 | });
76 |
77 | const tempCelcius = selector({
78 | key: 'tempCelcius',
79 | get: ({get}) => ((get(tempFahrenheit) - 32) 5) / 9,
80 | set: ({set}, newValue) => set(tempFahrenheit, (newValue 9) / 5 + 32),
81 | });
82 | ```
83 |
--------------------------------------------------------------------------------
/02~组件基础/04~React Router/框架集成.md:
--------------------------------------------------------------------------------
1 | # 框架集成
2 |
3 | # Redux
4 |
5 | - 安装方式:
6 |
7 | ```jsx
8 | npm install --save react-router-redux
9 | ```
10 |
11 | ## 简单示例
12 |
13 | ```jsx
14 | import React from "react";
15 | import ReactDOM from "react-dom";
16 | import { createStore, combineReducers, applyMiddleware } from "redux";
17 | import { Provider } from "react-redux";
18 | import { Router, Route, browserHistory } from "react-router";
19 | import { syncHistoryWithStore, routerReducer } from "react-router-redux";
20 |
21 | import reducers from "
/reducers";
22 |
23 | // Add the reducer to your store on the `routing` key
24 | const store = createStore(
25 | combineReducers({
26 | ...reducers,
27 | routing: routerReducer,
28 | })
29 | );
30 |
31 | // Create an enhanced history that syncs navigation events with the store
32 | const history = syncHistoryWithStore(browserHistory, store);
33 |
34 | ReactDOM.render(
35 |
36 | {/* Tell the Router to use our enhanced history */}
37 |
38 |
39 |
40 |
41 |
42 |
43 | ,
44 | document.getElementById("mount")
45 | );
46 | ```
47 |
48 | ## Params | Router 的参数
49 |
50 | 在 React Router 中可以通过本身组件的 Props 来传递路由参数,而在 React-Redux 中因为是采用了`connect()`方法将 State 映射到了 Props 中,因此需要采用`mapStateToProps`中的第二个参数进行路由映射:
51 |
52 | ```js
53 | function mapStateToProps(state, ownProps) {
54 | return {
55 | id: ownProps.params.id,
56 | filter: ownProps.location.query.filter,
57 | };
58 | }
59 | ```
60 |
61 | ## History
62 |
63 | 如果有时候需要对于你的路由的历史进行监控的话,可以采用如下的方案:
64 |
65 | ```js
66 | const history = syncHistoryWithStore(browserHistory, store);
67 |
68 | history.listen((location) => analyticsService.track(location.pathname));
69 | ```
70 |
71 | ## Navigation Control
72 |
73 | - issue navigation events via Redux actions
74 |
75 | ```js
76 | import { routerMiddleware, push } from "react-router-redux";
77 |
78 | // Apply the middleware to the store
79 | const middleware = routerMiddleware(browserHistory);
80 | const store = createStore(reducers, applyMiddleware(middleware));
81 |
82 | // Dispatch from anywhere like normal.
83 | store.dispatch(push("/foo"));
84 | ```
85 |
--------------------------------------------------------------------------------
/03~状态管理/Redux/redux-form.md:
--------------------------------------------------------------------------------
1 | # redux-form
2 |
3 | ## Simple Form
4 |
5 | 如果在 Redux Form 中需要手动地设置值,应该在 Field 的 `onChange` 方法中进行修改,譬如:
6 |
7 | ```jsx
8 |
21 | ```
22 |
23 | 这一特性常常用于在自定义组件中进行值设置,
24 |
25 | ## Initial Form Values
26 |
27 | ```jsx
28 | import { React, Component } from "react";
29 | import { bindActionCreators } from "redux";
30 | import { connect } from "react-redux";
31 |
32 | import { reduxForm } from "redux-form";
33 | import { registerPerson } from "actions/coolStuff";
34 |
35 | @connect(null, (dispatch) => ({
36 | registerPerson: bindActionCreators(registerPerson, dispatch),
37 | }))
38 | export default class ExampleComponent extends Component {
39 | render() {
40 | const myInitialValues = {
41 | initialValues: {
42 | name: "John Doe",
43 | age: 42,
44 | fruitPreference: "apples",
45 | },
46 | };
47 | return (
48 |
49 |
Check out my cool form!
50 | {
53 | this.props.registerPerson(fields);
54 | }}
55 | />
56 |
57 | );
58 | }
59 | }
60 |
61 | @reduxForm({
62 | form: "exampleForm",
63 | fields: ["name", "age", "fruitPreference"],
64 | })
65 | class CoolForm extends Component {
66 | render() {
67 | const {
68 | fields: { name, age, fruitPreference },
69 | handleSubmit,
70 | } = this.props;
71 | return (
72 |
86 | );
87 | }
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/02~组件基础/02~组件数据流/Hooks/基础元语/useState & useRef.md:
--------------------------------------------------------------------------------
1 | # 组件内状态
2 |
3 | # useState
4 |
5 | useState 接收一个初始值,返回一个数组,数组里面分别是当前值和修改这个值的方法(类似 state 和 setState)。useState 接收一个函数,返回一个数组。setCount 可以接收新值,也可以接收一个返回新值的函数。
6 |
7 | ```ts
8 | const [count1, setCount1] = useState(0);
9 | const [count2, setCount2] = useState(() => 0);
10 | setCount1(1); // 修改 state
11 | ```
12 |
13 | 函数式状态的粒度会比类中状态更细,函数式状态保存的是快照,类状态保存的是最新值。引用类型的情况下,类状态不需要传入新的引用,而函数式状态必须保证是个新的引用。
14 |
15 | ## 快照(闭包)与最新值(引用)
16 |
17 | 在函数式组件中,我们有时候会发现所谓闭包冻结的现象,譬如在如下代码中:
18 |
19 | ```ts
20 | function App() {
21 | const [count, setCount] = useState(0);
22 | const inc = React.useCallback(() => {
23 | setTimeout(() => {
24 | setCount(count + 1);
25 | });
26 | }, []);
27 |
28 | return {count}
;
29 | }
30 | ```
31 |
32 | 类组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。在函数式组件里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了 1 次。
33 |
34 | 想要解决这个问题,那就涉及到另一个新的 Hook 方法 useRef。useRef 是一个对象,它拥有一个 current 属性,并且不管函数组件执行多少次,而 useRef 返回的对象永远都是原来那一个。
35 |
36 | ```ts
37 | export default function App() {
38 | const [count, setCount] = React.useState(0);
39 | const ref = useRef(0);
40 |
41 | const inc = React.useCallback(() => {
42 | setTimeout(() => {
43 | setCount((ref.current += 1));
44 | });
45 | }, []);
46 |
47 | return (
48 |
49 | {count},{ref.current}
50 |
51 | );
52 | }
53 | ```
54 |
55 | # useRef
56 |
57 | ```js
58 | export function useRef(initialValue: T): { current: T } {
59 | currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
60 | workInProgressHook = createWorkInProgressHook();
61 | let ref;
62 |
63 | if (workInProgressHook.memoizedState === null) {
64 | ref = { current: initialValue };
65 | // ...
66 | workInProgressHook.memoizedState = ref;
67 | } else {
68 | ref = workInProgressHook.memoizedState;
69 | }
70 | return ref;
71 | }
72 | ```
73 |
74 | 对于函数式组件,如果我们需要获取该组件子元素的 Ref,可以使用 forwardRef 来进行 Ref 转发:
75 |
76 | ```js
77 | const FancyButton = React.forwardRef((props, ref) => (
78 |
81 | ));
82 |
83 | // You can now get a ref directly to the DOM button:
84 | const ref = React.createRef();
85 | Click me!;
86 | ```
87 |
--------------------------------------------------------------------------------
/02~组件基础/01~组件声明/类组件/DOM 操作.md:
--------------------------------------------------------------------------------
1 | # React 组件中 DOM 操作
2 |
3 | `React.findDOMNode()`方法能够帮我们根据`refs`获取某个子组件的 DOM 对象,不过需要注意的是组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM 上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。但是,有时需要从组件获取真实 DOM 的节点,这时就要用到 React.findDOMNode 方法。
4 |
5 | ```js
6 | var MyComponent = React.createClass({
7 | handleClick: function() {
8 | React.findDOMNode(this.refs.myTextInput).focus();
9 | },
10 | render: function() {
11 | return (
12 |
13 |
14 |
19 |
20 | );
21 | }
22 | });
23 |
24 | React.render(, document.getElementById("example"));
25 | ```
26 |
27 | 需要注意的是,由于 React.findDOMNode 方法获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个方法,否则会返回 null 。上面代码中,通过为组件指定 Click 事件的回调函数,确保了只有等到真实 DOM 发生 Click 事件之后,才会调用 React.findDOMNode 方法。
28 |
29 | # 组件渲染到 DOM
30 |
31 | React 的初衷是构建相对独立的界面开发库,
32 |
33 | 在源代码中,组件定义相关代码与渲染相关代码是相分离的。当我们声明了某个组件之后,可以通过`ReactDOM`的`render`函数将 React 组件渲染到 DOM 元素中:
34 |
35 | ```js
36 | const RootElement = (
37 |
38 |
The world is yours
39 |
Say hello to my little friend
40 |
41 | )
42 |
43 | ReactDOM.render(RootElement, document.getElementById('app'))
44 | ```
45 |
46 | # Refs
47 |
48 | # 整合非 React 类库
49 |
50 | ## jQuery Integration
51 |
52 | 目前,我们项目中不可避免的还会存在大量的基于 jQuery 的插件,这些插件也确实非常的好用呦,通常我们会采取将 jQuery 插件封装成一个 React 组件的方式来进行调用,譬如我们需要调用一个用于播放的 jQuery 插件 JPlayer,那么可以以如下方式使用:
53 |
54 | ```js
55 | // JPlayer component
56 | class JPlayer extends React.Component {
57 | static propTypes = {
58 | sources: React.PropTypes.array.isRequired
59 | };
60 | componentDidMount() {
61 | $(this.refs.jplayer).jPlayer({
62 | ready: () => {
63 | $(this.refs.jplayer).jPlayer("setMedia", this.props.sources);
64 | },
65 | swfPath: "/js",
66 | supplied: _.keys(this.props.sources)
67 | });
68 | }
69 | componentWillUmount() {
70 | // I don't know jPlayer API but if you need to destroy it do it here.
71 | }
72 | render() {
73 | return ;
74 | }
75 | }
76 |
77 | // Use it in another component...
78 | ;
79 | ```
80 |
--------------------------------------------------------------------------------
/02~组件基础/06~组件库/组件范式/表单组件.md:
--------------------------------------------------------------------------------
1 | # 表单组件
2 |
3 | # 受控组件与非受控组件
4 |
5 | # 常用组件
6 |
7 | ## Text
8 |
9 | ## Select
10 |
11 | # 表单验证
12 |
13 | 在我们真实的表单组件开发中,我们不可避免地需要对于用户输入的内容进行验证并且根据验证结果给予用户相关反馈提示。实际开发中,我们会使用受控组件,这样所有的表单数据都会存放于组件状态中(暂时不考虑状态存放于组件外),我们也可以很方便地根据内部状态进行计算。譬如产品需求是唯有用户在输入有效的邮箱地址时才允许点击注册按钮,否则注册按钮处于不可点击状态,图示如下:
14 |
15 | 我们可以使用简单的表达式来推导是否应该允许点击按钮,即仅当邮箱长度大于 0 并且密码长度大于 0 的时候才允许点击。
16 |
17 | ```js
18 | const { email, password } = this.state;
19 | const isEnabled = email.length > 0 && password.length > 0;
20 |
21 | ;
22 | ```
23 |
24 | 到这里我们已经完成了最简单基础的表单验证,不过在真实工程开发中我们会遇到的最头痛的问题反而不是验证本身,而在于如何进行合适的错误反馈。常见的输入框错误反馈模式有如下几种:
25 |
26 | 不同的产品经理、不同的产品对于这些错误反馈有不同的喜好,不过从工程的角度上我们希望尽可能地将逻辑与界面表示相分离,并且能够根据产品经理的要求迅速改变错误反馈模式与具体的样式。首先,我们需要考虑下组件内的存储错误信息的可独立于界面层的数据结构。基本的数据结构如下所示:
27 |
28 | ```
29 | errors: {
30 | name: false,
31 | email: true,
32 | }
33 | ```
34 |
35 | 这里的`false`代表某个域是验证通过的,而`true`则代表某个域是有问题的。在构建了存储错误信息的数据结构之后,我们要接着讲验证的过程独立于渲染函数以使其符合单一职责原则:
36 |
37 | ```
38 | function validate(email, password) {
39 | // true means invalid, so our conditions got reversed
40 | return {
41 | email: email.length === 0,
42 | password: password.length === 0,
43 | };
44 | }
45 | ```
46 |
47 | 这里的`validate`函数是典型的纯函数,我们可以方便地对其进行单元测试或者重构。下面我们需要在渲染函数中调用该验证函数:
48 |
49 | ```js
50 | const errors = validate(this.state.email, this.state.password);
51 | ```
52 |
53 | 在获取了错误信息后,我们还需要在按钮的属性上引用错误信息,完整的注册表单代码为:
54 |
55 | ```jsx
56 |
57 | class SignUpForm extends React.Component {
58 | constructor() {
59 | super();
60 | this.state = {
61 | email: '',
62 | password: '',
63 | touched: {
64 | email: false,
65 | password: false,
66 | },
67 | };
68 | }
69 |
70 | // ...
71 |
72 | handleBlur = (field) => (evt) => {
73 | this.setState({
74 | touched: { ...this.state.touched, [field]: true },
75 | });
76 | }
77 |
78 | render()
79 | const shouldMarkError = (field) => {
80 | const hasError = errors[field];
81 | const shouldShow = this.state.touched[field];
82 |
83 | return hasError ? shouldShow : false;
84 | };}
85 |
86 | // ...
87 | /**
88 |
89 |
98 |
99 | */
100 | }
101 |
102 | ```
103 |
--------------------------------------------------------------------------------
/02~组件基础/04~React Router/Hooks Api.md:
--------------------------------------------------------------------------------
1 | # Hooks Api
2 |
3 | React Router 附带了一些 Hooks Api,可让您访问路由器的状态并从组件内部执行导航。
4 |
5 | # useHistory
6 |
7 | useHistory Hook 使您可以访问可用于导航的历史记录实例。
8 |
9 | ```js
10 | import { useHistory } from "react-router";
11 |
12 | function HomeButton() {
13 | let history = useHistory();
14 |
15 | function handleClick() {
16 | history.push("/home");
17 | }
18 |
19 | return (
20 |
23 | );
24 | }
25 | ```
26 |
27 | # useLocation
28 |
29 | useLocation Hook 返回代表当前 URL 的位置对象。您可以将其想像为 useState,它会在 URL 发生更改时返回一个新位置。在您希望每次加载新页面时都使用 Web 分析工具触发新的“页面浏览”事件的情况下,如以下示例所示:
30 |
31 | ```js
32 | import React from "react";
33 | import ReactDOM from "react-dom";
34 | import { BrowserRouter as Router, Switch, useLocation } from "react-router";
35 |
36 | function usePageViews() {
37 | let location = useLocation();
38 | React.useEffect(() => {
39 | ga.send(["pageview", location.pathname]);
40 | }, [location]);
41 | }
42 |
43 | function App() {
44 | usePageViews();
45 | return ...;
46 | }
47 |
48 | ReactDOM.render(
49 |
50 |
51 | ,
52 | node
53 | );
54 | ```
55 |
56 | # useParams
57 |
58 | useParams 返回 URL 参数的键/值对的对象。使用它来访问当前 `` 的 match.params。
59 |
60 | ```js
61 | import React from "react";
62 | import ReactDOM from "react-dom";
63 | import {
64 | BrowserRouter as Router,
65 | Switch,
66 | Route,
67 | useParams
68 | } from "react-router";
69 |
70 | function BlogPost() {
71 | let { slug } = useParams();
72 | return Now showing post {slug}
;
73 | }
74 |
75 | ReactDOM.render(
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | ,
86 | node
87 | );
88 | ```
89 |
90 | # useRouteMatch
91 |
92 | useRouteMatch Hook 尝试以与 `` 相同的方式匹配当前 URL。在无需实际呈现`` 的情况下访问匹配数据最有用。
93 |
94 | ```js
95 | function BlogPost() {
96 | return (
97 | {
100 | // Do whatever you want with the match ...
101 | return ;
102 | }}
103 | />
104 | );
105 | }
106 |
107 | function BlogPost() {
108 | let match = useMatch("/blog/:slug");
109 | // Do whatever you want with the match...
110 | }
111 | ```
112 |
--------------------------------------------------------------------------------
/03~状态管理/XState/README.md:
--------------------------------------------------------------------------------
1 | # xstate
2 |
3 | XState 是一个状态管理(State Management)的 Library,负责储存及描述各种状态与各种状态间的转换,有点类似于 Redux、Flux,不同的地方在于 XState 整个核心都源自于 Statecharts,也就是我们需要定义好整个应用,程序会有哪些状态,和每个状态下能转换到哪些状态(顺序性)以及他们之间如何转换。
4 |
5 | ## Statecharts
6 |
7 | 其实 Statecharts 并不是什麽新技术或新概念,早在 1984 年 David HAREL 的论文就提出了 Statechart,是由早期的状态图(state diagrams)所拓展而来的,在该篇论文中对状态图加入了三个元素分别处理了层级(hierarchy)、併发(concurrency)和通讯(communication)。让原先的状态图变的高度结构化且能更有效地描述各种状态。
8 |
9 | 
10 |
11 | # 为何需要 xstate?
12 |
13 | 其实用过 Angular.js 的开发者都会知道状态管理的重要性,当应用,程序状态分散在不同地方时,就会使得状态难以管理,并且容易出现 Bug,直到 Flux 出现提出了单一资料源(Single Source of Truth)及单向资料流(unidirectional data flow)等概念后,状态管理的问题才得到了缓解。而 Redux 的出现利用 Funtional Programming 的手法大幅度的降低原先 Flux 的複杂度以及学习成本,如果我们依照 Redux 的架构已经可以把因为状态複杂度而陡然上升的维护成本控制得很好,那如今为什麽我们还需要一个新的状态管理工具呢?
14 |
15 | ## 缺乏清晰的状态描述
16 |
17 | 不管是使用 Redux 或其他相关的 Library 都会有状态难以清晰描述的问题,最主要原因有两个,第一个是我们完全混合了状态(state)跟资料(context),所有东西都直接往 Reducer 裡面塞导致我们无法分清楚哪些是资料、哪些是状态。这裡的资料(context)指的是显示在页面上的内容,通常这些资料会存储在后端并透过 API 取得,在 XState 称之为 context,在 UML State Mechine 裡面称为 Extended states;而状态(state)则是指应用,程序当前的状态,比如说是否已登入或者 Menu 是否展开等等状态。
18 |
19 | 另一个因素是我们通常都使用 flag 来表达某个小状态,再由多个 flags 来表达某个状态,当这种 flag 越来越多时,我们的,程序就会很容易出现 Bug,,程序码会长的像下面这样:
20 |
21 | ```js
22 | if (isLogin && isYYY && isXXX)
23 | ```
24 |
25 | 这样的,程序码其实就是所谓的 bottom-up code,通常是我们先有一个小状态比如说 isLogin 然后后面又加了其他各种状态,当我们这种小状态一多,就会让,程序容出现难以察觉的 Bug。
26 |
27 | ## 过于自由的状态转换
28 |
29 | 如上面我们提到的,过去我们的状态是由多个 flags 所组成,这导致了我们无法明确的定义各种状态之间的关係,最后就会变成我们无法确定状态之间的切换是否正确,比如说 isAdmin 为 true 时 isLogin 应该必定为 true。像这样用 flag 储存小状态就会有可能出现状态转换出错的情况,比如说 isAdmin 设定成 true 了,却忘记把 isLogin 也设定为 true;而实际上状态的複杂度会比这裡举的例子複杂许多,这样的,程序码大到一定程度就会变成我们再也无法真正理解,程序有哪些状态,以及哪些可能的状态应该被处理(除非你再从头跟 PM 及 Designer 完整的过一次流程与画面,但如果专案够大很有可能他们也不会很清楚)。
30 |
31 | ## 难以与工程师之外的人讨论
32 |
33 | 同样的当我们今天用各种 flags 的方式去描述整个应用,程序的状态时,其实是很难跟工程师之外的人沟通或讨论的,就算是工程师也要追 Code 花时间理解当前的,程序到底是如何运作并且在哪个状态下出现的 Bug,这会让我们很难快速地发现问题也很难跟 PM 讨论需求设计是否存在逻辑上的矛盾,或是有未处理的状态。
34 |
35 | # XState 有什麽优势?
36 |
37 | ## ,程序码即 UI Spec
38 |
39 | 当我们今天用 XState 定义好各种状态之后,就可以直接利用 XState 提供的图像化工具(Visualizer)把,程序码转换成图片,如下:
40 |
41 | 
42 |
43 | 当我们有这张图之后,就可以把这个当作 UI Spec 跟 PM 及设计师讨论哪方面流程有问题,或是还有哪些没有明确订定的状态。
44 |
45 | ## 写更少的测试
46 |
47 | 由于我们已经明确定义出各个状态以及每个状态之间的关係,这让我们可以更轻鬆的撰写测试,也不需要测试那些根本不可能出现的状态,并透过 Model-based Testing 我们只需要写各个状态下的断言(assertion)就可以自动把各种状态切换的路径都测试完!XState 在这方面也提供了 xstate-test 有完整的范例跟教学。
48 |
49 | ## 更快速的路径优化
50 |
51 | 当我们完成一个应用,程序时,最需要做的通常就是使用者体验(User Experience)的优化,我们常常需要利用各种服务来收集各个页面间的转化率或是哪些状态让使用者最快跳过等等的数据。透过这些数据来优化我们应用,程序的流程,让使用者体验进一步的提升。而如果使用了 XState 我们就可以在各个状态转换之间送 log 到数据收集的服务(如 GA, MIXpanel 等等),就可以进一步分析哪些状态可能是不必要的,来优化我们的 User Flow。
52 |
--------------------------------------------------------------------------------
/03~状态管理/Zustand/01.状态派生与 Selector.md:
--------------------------------------------------------------------------------
1 | # Computed State
2 |
3 | ```ts
4 | import computed from "zustand-computed";
5 |
6 | type Store = {
7 | count: number;
8 | inc: () => void;
9 | dec: () => void;
10 | };
11 |
12 | type ComputedStore = {
13 | countSq: number;
14 | };
15 |
16 | const computeState = (state: Store): ComputedStore => ({
17 | countSq: state.count ** 2,
18 | });
19 |
20 | // use curried create
21 | const useStore = create()(
22 | computed(
23 | (set) => ({
24 | count: 1,
25 | inc: () => set((state) => ({ count: state.count + 1 })),
26 | dec: () => set((state) => ({ count: state.count - 1 })),
27 | }),
28 | computeState
29 | )
30 | );
31 |
32 | const useStore = create()(
33 | devtools(
34 | computed(
35 | immer((set) => ({
36 | count: 1,
37 | inc: () =>
38 | set((state) => {
39 | // example with Immer middleware
40 | state.count += 1;
41 | }),
42 | dec: () => set((state) => ({ count: state.count - 1 })),
43 | })),
44 | computeState
45 | )
46 | )
47 | );
48 | ```
49 |
50 | # 自动生成 Selector
51 |
52 | 我们建议在使用 store 的属性或动作时使用选择器。你可以像这样访问 store 里的值:
53 |
54 | ```ts
55 | const bears = useBearStore((state) => state.bears);
56 | ```
57 |
58 | 然而,写这些东西可能很乏味。如果你是这种情况,你可以自动生成你的选择器。
59 |
60 | ```ts
61 | import { StoreApi, UseBoundStore } from "zustand";
62 |
63 | type WithSelectors = S extends { getState: () => infer T }
64 | ? S & { use: { [K in keyof T]: () => T[K] } }
65 | : never;
66 |
67 | const createSelectors = >>(
68 | _store: S
69 | ) => {
70 | let store = _store as WithSelectors;
71 | store.use = {};
72 | for (let k of Object.keys(store.getState())) {
73 | (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
74 | }
75 |
76 | return store;
77 | };
78 | ```
79 |
80 | 如果有如下的 Store:
81 |
82 | ```ts
83 | interface BearState {
84 | bears: number;
85 | increase: (by: number) => void;
86 | increment: () => void;
87 | }
88 |
89 | const useBearStoreBase = create()((set) => ({
90 | bears: 0,
91 | increase: (by) => set((state) => ({ bears: state.bears + by })),
92 | increment: () => set((state) => ({ bears: state.bears + 1 })),
93 | }));
94 | ```
95 |
96 | 即可生成如下可直接访问的 store:
97 |
98 | ```ts
99 | const useBearStore = createSelectors(useBearStoreBase);
100 |
101 | // get the property
102 | const bears = useBearStore.use.bears();
103 |
104 | // get the action
105 | const increase = useBearStore.use.increment();
106 | ```
107 |
--------------------------------------------------------------------------------
/03~状态管理/Redux/State 结构设计.md:
--------------------------------------------------------------------------------
1 | # State 结构设计
2 |
3 | 接下来我们一起讨论下 State 的结构设计问题,在复杂组件中,我们可能需要在但单组件内维持复杂的具有项目依赖关系的状态数据,譬如在经典的 TODOList 组件中,我们首先需要保存当前全部的待做事项数据:
4 |
5 | ```js
6 | const initialState = [
7 | { id: 1, text: "laundry" },
8 | { id: 2, text: "shopping" }, // ...
9 | ];
10 |
11 | class List extends React.Component {
12 | state = {
13 | todos: initialState,
14 | };
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
21 | {this.state.todos.map((todo) => (
22 | - {todo.text}
23 | ))}
24 |
25 |
26 | );
27 | }
28 | }
29 | ```
30 |
31 | 当你以为你大功告成的时候,产品经理让你添加一个搜索框,可以根据用户输入的内容动态地过滤列表显示内容。估计很多开发者会根据直觉写出如下代码:
32 |
33 | ```js
34 | class List extends React.Component {
35 | state = {
36 | todos: initialState,
37 | filteredTodos: null,
38 | };
39 |
40 | search(searchText) {
41 | const filteredTodos = this.state.todos.filter(
42 | (todo) => todo.text.indexOf(searchText) > 0
43 | );
44 | this.setState({ filteredTodos: filteredTodos });
45 | }
46 |
47 | render() {
48 | // get todos from state
49 | const { filteredTodos, todos } = this.state; // if there are filtered todos use them
50 |
51 | const list = filteredTodos === null ? todos : filteredTodos;
52 |
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 | {list.map((todo) => (
61 | - {todo.text}
62 | ))}
63 |
64 |
65 |
66 | );
67 | }
68 | }
69 | ```
70 |
71 | 从功能上来说,这段代码没有问题,但是其将 Todos 数据保存到了两个不同的列表中,这就导致了相同的数据被保存到了两个地方,不仅造成了数据存储的冗余,还会为我们未来的修改造成不便。譬如用户可能需要在过滤之后修改某个 Todo 项目的属性,那么此时便需要同时改变`todos`与`filteredTodos`这两个属性的数据方可。在 State 的结构设计时,我们应该遵循尽可能地保证其最小化原则,在此考虑下我们的组件可以修改为:
72 |
73 | ```js
74 | class List extends React.Component {
75 | state = {
76 | todos: initialState,
77 | searchText: null,
78 | };
79 |
80 | search(searchText) {
81 | this.setState({ searchText: searchText });
82 | }
83 |
84 | filter(todos) {
85 | if (!this.state.searchText) {
86 | return todos;
87 | }
88 |
89 | return todos.filter((todo) => todo.text.indexOf(this.state.searchText) > 0);
90 | }
91 |
92 | render() {
93 | const { todos } = this.state;
94 |
95 | return (
96 |
97 |
98 |
99 |
100 |
101 |
102 | {this.filter(todos).map((todo) => (
103 | - {todo.text}
104 | ))}
105 |
106 |
107 |
108 | );
109 | }
110 | }
111 | ```
112 |
--------------------------------------------------------------------------------
/02~组件基础/01~组件声明/事件系统/合成事件.md:
--------------------------------------------------------------------------------
1 | # SyntheticEvent | 合成事件详解
2 |
3 | # Event pooling
4 |
5 | 如上图所示,在 JavaScript 中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在 div 中触发了 click 事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在 DOM 的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。
6 |
7 | React 会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。并且 React 并没有使用原生的浏览器事件,而是在基于 Virtual DOM 的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是 SyntheticEvent 的实例。SyntheticEvent 完全符合 W3C 的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过 stopPropagation()和 preventDefault() 相应的中断。如果需要访问当原生的事件对象,可以通过引用 nativeEvent 获得。
8 |
9 | 
10 |
11 | 
12 |
13 | 上图为大致的 React 事件机制的流程图,React 中的事件机制分为两个阶段:事件注册和事件触发:
14 |
15 | - 事件注册:React 在组件加载(mount)和更新(update)时,其中的 ReactDOMComponent 会对传入的事件属性进行处理,对相关事件进行注册和存储。document 中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter 作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由 EventPluginHub 来统一管理,根据事件的类型(type)和组件标识(`_rootNodeID`)为 key 唯一标识事件并进行存储。
16 |
17 | - 事件执行:事件执行时,document 上绑定事件 ReactEventListener.dispatchEvent 会对事件进行分发,根据之前存储的类型(type)和组件标识(`_rootNodeID`)找到触发事件的组件。ReactEventEmitter 利用 EventPluginHub 中注入(inject)的 plugins(例如:SimpleEventPlugin、EnterLeaveEventPlugin)会将原生的 DOM 事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个 listener 创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React 在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。
18 |
19 | # 外部事件触发
20 |
21 | ## 外部触发关闭
22 |
23 | 点击事件是 Web 开发中常见的事件之一,我们在上文中介绍的基本事件绑定也是以点击事件为例。这里我们想讨论下另一个常见的与点击相关的需求,即点击组件外部分触发事件处理。典型的用例譬如在弹出窗中,我们希望点击弹出窗外的部分自动关闭弹出窗,或者某个下拉菜单打开状态下,点击其他部分自动关闭该菜单。这种需求有一种解决思路是为组件设置一个全局浮层,这样可以将组件外的点击事件绑定到浮层上,直接监听浮层的点击即可。不过很多产品经理在提需求的时候是不喜欢这种方式的,因此我们可以使用另一种方法,直接在 React 根容器中监听点击事件:
24 |
25 | ```js
26 | window.__myapp_container = document.getElementById("app");
27 |
28 | React.render(, window.__myapp_container);
29 | ```
30 |
31 | 然后在组件中我们动态的设置对于根元素的监听:
32 |
33 | ```js
34 | export default class ClickListener extends Component {
35 | static propTypes = {
36 | children: PropTypes.node.isRequired,
37 | onClickOutside: PropTypes.func.isRequired
38 | };
39 |
40 | componentDidMount() {
41 | window.__myapp_container.addEventListener(
42 | "click",
43 | this.handleDocumentClick
44 | );
45 | }
46 |
47 | componentWillUnmount() {
48 | window.__myapp_container.removeEventListener(
49 | "click",
50 | this.handleDocumentClick
51 | );
52 | } /* using fat arrow to bind to instance */
53 |
54 | handleDocumentClick = evt => {
55 | const area = ReactDOM.findDOMNode(this.refs.area);
56 |
57 | if (!area.contains(evt.target)) {
58 | this.props.onClickOutside(evt);
59 | }
60 | };
61 |
62 | render() {
63 | return (
64 |
65 | {this.props.children}
66 |
67 |
68 | );
69 | }
70 | }
71 | ```
72 |
73 | ## 自定义事件分发
74 |
--------------------------------------------------------------------------------
/03~状态管理/Zustand/04.状态重置.md:
--------------------------------------------------------------------------------
1 | # 状态重置
2 |
3 | 以下模式可用于将状态重置为其初始值。
4 |
5 | ```ts
6 | import { create } from "zustand";
7 |
8 | // define types for state values and actions separately
9 | type State = {
10 | salmon: number;
11 | tuna: number;
12 | };
13 |
14 | type Actions = {
15 | addSalmon: (qty: number) => void;
16 | addTuna: (qty: number) => void;
17 | reset: () => void;
18 | };
19 |
20 | // define the initial state
21 | const initialState: State = {
22 | salmon: 0,
23 | tuna: 0,
24 | };
25 |
26 | // create store
27 | const useSlice = create()((set, get) => ({
28 | ...initialState,
29 |
30 | addSalmon: (qty: number) => {
31 | set({ salmon: get().salmon + qty });
32 | },
33 |
34 | addTuna: (qty: number) => {
35 | set({ tuna: get().tuna + qty });
36 | },
37 |
38 | reset: () => {
39 | set(initialState);
40 | },
41 | }));
42 | ```
43 |
44 | 一次重置多个 store:
45 |
46 | ```ts
47 | import { create: _create, StateCreator } from 'zustand'
48 |
49 | const resetters: (() => void)[] = []
50 |
51 | export const create = ((f: StateCreator | undefined) => {
52 | if (f === undefined) return create
53 | const store = _create(f)
54 | const initialState = store.getState()
55 | resetters.push(() => {
56 | store.setState(initialState, true)
57 | })
58 | return store
59 | }) as typeof _create
60 |
61 | export const resetAllStores = () => {
62 | for (const resetter of resetters) {
63 | resetter()
64 | }
65 | }
66 | ```
67 |
68 | 使用切片模式重置绑定的 store:
69 |
70 | ```ts
71 | import create, { StateCreator } from "zustand";
72 |
73 | const resetters: (() => void)[] = [];
74 |
75 | const initialBearState = { bears: 0 };
76 |
77 | interface BearSlice {
78 | bears: number;
79 | addBear: () => void;
80 | eatFish: () => void;
81 | }
82 |
83 | const createBearSlice: StateCreator<
84 | BearSlice & FishSlice,
85 | [],
86 | [],
87 | BearSlice
88 | > = (set) => {
89 | resetters.push(() => set(initialBearState));
90 | return {
91 | ...initialBearState,
92 | addBear: () => set((state) => ({ bears: state.bears + 1 })),
93 | eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
94 | };
95 | };
96 |
97 | const initialStateFish = { fishes: 0 };
98 |
99 | interface FishSlice {
100 | fishes: number;
101 | addFish: () => void;
102 | }
103 |
104 | const createFishSlice: StateCreator<
105 | BearSlice & FishSlice,
106 | [],
107 | [],
108 | FishSlice
109 | > = (set) => {
110 | resetters.push(() => set(initialStateFish));
111 | return {
112 | ...initialStateFish,
113 | addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
114 | };
115 | };
116 |
117 | const useBoundStore = create()((...a) => ({
118 | ...createBearSlice(...a),
119 | ...createFishSlice(...a),
120 | }));
121 |
122 | export const resetAllSlices = () => resetters.forEach((resetter) => resetter());
123 |
124 | export default useBoundStore;
125 | ```
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Contributors][contributors-shield]][contributors-url]
2 | [![Forks][forks-shield]][forks-url]
3 | [![Stargazers][stars-shield]][stars-url]
4 | [![Issues][issues-shield]][issues-url]
5 | [][license-url]
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 在线阅读 >>
16 |
17 |
18 | 代码案例
19 | ·
20 | 参考资料
21 |
22 |
23 |
24 |
25 | # React Series - React 入门与工程实践篇
26 |
27 | 本篇归属于 [Web Series,Web 开发入门与工程实践](https://github.com/wx-chevalier/Web-Notes),其主要以 React 为核心的技术体系为主线,为读者构建完整的前端技术知识体系,探讨前端工程化的思想,并且能使不同技术水准的读者都有所得。传统上,Web 应用的 UI 是使用模板或 HTML 构建的。这些模板就是你可以用来构建 UI 的全套抽象。React 将用户界面分解为各个组件,发展出了构建 UI 的全新途径。构建可以管理自己状态的封装组件,然后将它们组合起来制作成复杂的 UI。
28 |
29 | 
30 |
31 | # Nav | 关联导航
32 |
33 | - 如果你是颇有经验的开发者,想要了解前端工程化与架构方面的知识,那么建议阅读[《Web-Notes](https://github.com/wx-chevalier/Web-Notes)》一文。
34 |
35 | - 如果你希望了解 Node.js 全栈开发,可以参阅 [《Node-Notes](https://github.com/wx-chevalier/Node-Notes)》。
36 |
37 | # About
38 |
39 | ## Copyright & More | 延伸阅读
40 |
41 |  
42 |
43 | 笔者所有文章遵循[知识共享 署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表:
44 |
45 | [](https://ng-tech.icu/books-gallery/)
46 |
47 |
48 |
49 |
50 | [contributors-shield]: https://img.shields.io/github/contributors/wx-chevalier/repo.svg?style=flat-square
51 | [contributors-url]: https://github.com/wx-chevalier/repo/graphs/contributors
52 | [forks-shield]: https://img.shields.io/github/forks/wx-chevalier/repo.svg?style=flat-square
53 | [forks-url]: https://github.com/wx-chevalier/repo/network/members
54 | [stars-shield]: https://img.shields.io/github/stars/wx-chevalier/repo.svg?style=flat-square
55 | [stars-url]: https://github.com/wx-chevalier/repo/stargazers
56 | [issues-shield]: https://img.shields.io/github/issues/wx-chevalier/repo.svg?style=flat-square
57 | [issues-url]: https://github.com/wx-chevalier/repo/issues
58 | [license-shield]: https://img.shields.io/github/license/wx-chevalier/repo.svg?style=flat-square
59 | [license-url]: https://github.com/wx-chevalier/repo/blob/master/LICENSE.txt
60 |
--------------------------------------------------------------------------------
/03~状态管理/Context/Context.md:
--------------------------------------------------------------------------------
1 | # Context 用于状态管理
2 |
3 | 如前文所述,React 单组件中允许使用 setState 进行状态管理,而对于组件树,我们可以使用 Props 逐层传递状态值与回调函数。不过这种方式无形会导致多层组件间的强耦合,并且会导致大量的冗余代码。像 Redux, MobX 这样的状态管理框架,它们的作用之一就是将状态代码独立于组件,并未多层组件提供直接的数据流通通道;实际上我们也可以利用 Context API 进行跨层组件间的数据传递,来构建简单的状态管理工具。[Unstated](https://github.com/jamiebuilds/unstated) 就是对于 Context API 进行简要封装形成的状态管理库,它并不需要开发者学习额外的 API 或者库用法,而只需要使用普通的 React 组件中的 setState 方法操作 state,并利用 Context 将其传递到子组件中。Unstated 中主要包含了 Container,Subscribe 以及 Provider 三个组件,其中 Provider 负责存放所有的内部状态实例,类似于 Redux 或者 Apollo 中的 Provider:
4 |
5 | ```js
6 | const App = () => (
7 |
8 |
9 |
10 | );
11 | ```
12 |
13 | Unstated 会在内部创建 Context 对象,并在 Provider 中包裹 Context.Provider 对象:
14 |
15 | ```js
16 | const StateContext = createReactContext(null);
17 |
18 | ...
19 |
20 | export function Provider(props: ProviderProps) {
21 | return (
22 | // 集成父组件中的 Provider
23 |
24 | {
25 | ...
26 | return (
27 |
28 | {props.children}
29 |
30 | );
31 | }}
32 |
33 | );
34 | }
35 | ```
36 |
37 | Container 是朴素的拥有 setState 方法的 JavaScript 类,其仅负责进行状态操作,其用法如下:
38 |
39 | ```js
40 | // BookContainer.js
41 | import { Container } from "unstated";
42 | class BookContainer extends Container {
43 | state = {
44 | books: [],
45 | booksVisible: false
46 | };
47 | addBook = book => {
48 | const books = [...this.state.books];
49 | books.push(book);
50 | this.setState({ books });
51 | };
52 | toggleVisibility = () => {
53 | this.setState({
54 | booksVisible: !this.state.booksVisible
55 | });
56 | };
57 | }
58 | export { BookContainer };
59 | ```
60 |
61 | 参考 Container 的源代码,可以发现其主要是对 setState 进行了复写:
62 |
63 | ```js
64 | // ...
65 | setState(state: $Shape) {
66 | this.state = Object.assign({}, this.state, state);
67 | this._listeners.forEach(fn => fn());
68 | }
69 | // ...
70 | ```
71 |
72 | Subscribe 组件则提供了将 Container 实例传递给自定义组件的媒介,当状态变化时,组件会进行自动渲染:
73 |
74 | ```js
75 |
76 | {(bookStore, counterStore) => {
77 | const { state: { books, booksVisible } } = bookStore
78 | {
79 | booksVisible && books.map((book, index) => (
80 |
81 |
{book.name}
82 |
{book.author}
83 |
84 | )
85 | }
86 | }}
87 |
88 | ```
89 |
90 | Subscribe 在组件内提供了 Context.Consumer 包裹,并且自动创建 Container/Store 实例:
91 |
92 | ```js
93 | instance = new Container();
94 | safeMap.set(Container, instance);
95 |
96 | ...
97 |
98 | render() {
99 | return (
100 |
101 | {map =>
102 | this.props.children.apply(
103 | null,
104 | this._createInstances(map, this.props.to)
105 | )
106 | }
107 |
108 | );
109 | }
110 | ```
111 |
--------------------------------------------------------------------------------
/04~工程实践/服务端组件/README.md:
--------------------------------------------------------------------------------
1 | # React Server Components
2 |
3 | 服务端组件允许开发人员构建跨越服务器和客户端的应用程序,将客户端应用程序的丰富交互性与传统服务器渲染的改进性能相结合。
4 |
5 | - 服务端组件只在服务器上运行,对捆绑包大小没有影响。它们的代码永远不会下载到客户端,有助于减少捆绑包大小,改善启动时间。
6 | - 服务端组件可以访问服务器端的数据源,如数据库、文件系统或(微)服务。
7 | - 服务端组件可与客户端组件(即传统的 React 组件)无缝集成。服务端组件可以在服务器上加载数据,并将其作为 Props 传递给客户端组件,允许客户端处理渲染页面的交互部分。
8 | - 服务端组件可以动态地选择要渲染的客户端组件,允许客户端只下载渲染页面所需的最少代码。
9 | - 服务端组件在重新加载时保留客户端状态。这意味着当服务端组件树被重新加载时,客户端的状态、焦点、甚至正在进行的动画都不会被中断或重置。
10 | - 服务端组件是以渐进式和增量式的方式将 UI 的渲染单元流向客户端。结合 Suspense,这使得开发人员能够制作有意的加载状态,并在等待页面剩余部分加载时快速显示重要内容。
11 | - 开发人员还可以在服务器和客户端之间共享代码,允许用一个组件在一个路由上渲染服务器上某些内容的静态版本,并在不同的路由上渲染客户端上该内容的可编辑版本。
12 |
13 | # 基础示例
14 |
15 | 这个例子渲染了一个带有标题和正文的简单 Note。它使用服务器组件渲染 Note 的不可编辑视图,并使用客户端组件(传统的 React 组件)选择性地渲染 Note 的编辑器。首先,我们在服务器上渲染 Note。我们的工作惯例是用.server.js 后缀(或.server.jsx、.server.tsx 等)来命名服务器组件。
16 |
17 | ```js
18 | // Note.server.js - Server Component
19 |
20 | import db from "db.server";
21 | // (A1) We import from NoteEditor.client.js - a Client Component.
22 | import NoteEditor from "NoteEditor.client";
23 |
24 | function Note(props) {
25 | const { id, isEditing } = props;
26 | // (B) Can directly access server data sources during render, e.g. databases
27 | const note = db.posts.get(id);
28 |
29 | return (
30 |
31 |
{note.title}
32 |
33 | {/* (A2) Dynamically render the editor only if necessary */}
34 | {isEditing ? : null}
35 |
36 | );
37 | }
38 | ```
39 |
40 | 这个例子说明了几个关键点。
41 |
42 | - 这 "只是" 一个 React 组件:它接收道具并渲染一个视图。服务器组件有一些限制--例如,它们不能使用状态或效果--但总的来说,它们的工作与你所期望的一样。更多的细节在下面的 Capabilities & Constraints of Server and Client Components 中提供。
43 | - 服务器组件可以直接访问服务器数据源,如数据库,如(B)所示。这是通过一个通用机制实现的,允许社区创建兼容的 API,与各种数据源一起工作。
44 | - 服务器组件可以通过导入和渲染一个 "客户端" 组件将渲染工作交给客户端,如(A1)和(A2)中分别说明。客户端组件使用 .client.js 后缀(或 .client.jsx、.client.tsx 等)。捆绑程序会将这些导入与其他动态导入进行类似的处理,有可能根据各种启发式方法将它们拆分到另一个捆绑程序中。在这个例子中,只有当 props.isEditing 为真时,NodeEditor.client.js 才会被加载到客户端。
45 |
46 | 相比之下,客户端组件是你已经习惯的典型组件。它们可以访问 React 的全部功能:状态、效果、访问 DOM 等。"客户端组件" 这个名字并没有任何新的含义,它只是为了将这些组件与服务器组件区分开来。继续我们的例子,下面是我们如何实现 Note 编辑器。
47 |
48 | ```js
49 | // NodeEditor.client.js - Client Component
50 |
51 | export default function NoteEditor(props) {
52 | const note = props.note;
53 | const [title, setTitle] = useState(note.title);
54 | const [body, setBody] = useState(note.body);
55 | const updateTitle = (event) => {
56 | setTitle(event.target.value);
57 | };
58 | const updateBody = (event) => {
59 | setTitle(event.target.value);
60 | };
61 | const submit = () => {
62 | // ...save note...
63 | };
64 | return (
65 |
71 | );
72 | }
73 | ```
74 |
75 | 这看起来像一个普通的 React 组件,因为它就是。客户端组件只是普通的组件。一个重要的考虑因素是,当 React 在客户端上渲染 Server Components 的结果时,它保留了之前可能已经渲染的 Client Components 的状态。具体来说,React 会将从服务器传递过来的新道具合并到现有的 Client Components 中,维护这些组件的状态(和 DOM),以保留焦点、状态和任何正在进行的动画。
76 |
--------------------------------------------------------------------------------
/02~组件基础/06~组件库/Antd/文件上传.md:
--------------------------------------------------------------------------------
1 | # 文件上传
2 |
3 | # Upload
4 |
5 | ## 图片上传
6 |
7 | ```js
8 | class UploadThumb extends PureComponent {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | loading: false,
13 | imageUrl: ""
14 | };
15 | }
16 |
17 | handleChange(info) {
18 | if (info.file.status === "uploading") {
19 | this.setState({ loading: true });
20 | return;
21 | }
22 | if (info.file.status === "done") {
23 | // Get this url from response in real world.
24 | this.getBase64(info.file.originFileObj, imageUrl =>
25 | this.setState({ imageUrl, loading: false })
26 | );
27 | }
28 | }
29 |
30 | getBase64(img, callback) {
31 | const reader = new FileReader();
32 | reader.addEventListener("load", () => callback(reader.result));
33 | reader.readAsDataURL(img);
34 | }
35 |
36 | beforeUpload(file) {
37 | const isJPG = file.type === "image/jpeg";
38 | if (!isJPG) {
39 | message.error("You can only upload JPG file!");
40 | }
41 | const isLt2M = file.size / 1024 / 1024 < 2;
42 | if (!isLt2M) {
43 | message.error("Image must smaller than 2MB!");
44 | }
45 | return isJPG && isLt2M;
46 | }
47 |
48 | render() {
49 | const uploadButton = (
50 |
54 | );
55 |
56 | return (
57 |
66 | {this.state.imageUrl ? (
67 |
68 | ) : (
69 | uploadButton
70 | )}
71 |
72 | );
73 | }
74 | }
75 | ```
76 |
77 | ## 文件转换
78 |
79 | ```js
80 | const props = {
81 | action: "https://www.mocky.io/v2/5cc8019d300000980a055e76",
82 | transformFile(file) {
83 | return new Promise(resolve => {
84 | const reader = new FileReader();
85 | reader.readAsDataURL(file);
86 | reader.onload = () => {
87 | const canvas = document.createElement("canvas");
88 | const img = document.createElement("img");
89 | img.src = reader.result;
90 | img.onload = () => {
91 | const ctx = canvas.getContext("2d");
92 | ctx.drawImage(img, 0, 0);
93 | ctx.fillStyle = "red";
94 | ctx.textBaseline = "middle";
95 | ctx.fillText("Ant Design", 20, 20);
96 | canvas.toBlob(resolve);
97 | };
98 | };
99 | });
100 | }
101 | };
102 |
103 | ReactDOM.render(
104 |
105 |
106 |
109 |
110 |
,
111 | mountNode
112 | );
113 | ```
114 |
--------------------------------------------------------------------------------
/04~工程实践/02~数据加载/Suspense.md:
--------------------------------------------------------------------------------
1 | # Suspense
2 |
3 | React Suspense 全部涉及处理具有异步数据需求的视图之间的转换:
4 |
5 | ```js
6 | import { createCache } from "react-cache";
7 | import { createResource } from "react-cache";
8 |
9 | export let cache = createCache();
10 |
11 | export let InvoiceResource = createResource((id) => {
12 | return fetch(`/invoices/${id}`).then((response) => {
13 | return response.json();
14 | });
15 | });
16 | ```
17 |
18 | ```js
19 | import cache from "./cache";
20 | import InvoiceResource from "./InvoiceResource";
21 |
22 | let Invoice = ({ invoiceId }) => {
23 | let invoice = InvoiceResource.read(cache, invoiceId);
24 | return {invoice.number}
;
25 | };
26 | ```
27 |
28 | React 开始渲染(在内存中)。它打到了 InvoicesResource.read() 调用。该键的缓存(id 为键)将为空,因此它将调用我们提供给 createResource 的函数,从而触发异步获取。然后缓存将抛出我们返回的承诺(是的,我也从未考虑过抛出任何错误,但也有错误,但是如果需要可以抛出窗口。)抛出之后,不再执行任何代码。React 等待承诺解决。诺言解决。React 尝试再次渲染发票(在内存中)。它再次点击 InvoicesResource.read() 。这次数据位于缓存中,因此可以从 ApiResource.read() 同步返回我们的数据。React 将页面呈现到 DOM:
29 |
30 | ```js
31 | // the store and reducer
32 | import { createStore } from "redux";
33 | import { connect } from "react-redux";
34 |
35 | let reducer = (state, action) => {
36 | if (action.type === "LOADED_INVOICE") {
37 | return {
38 | ...state,
39 | invoice: action.data,
40 | };
41 | }
42 | return state;
43 | };
44 |
45 | let store = createStore(reducer);
46 |
47 | /////////////////////////////////////////////
48 | // the action
49 | function fetchInvoice(dispatch, id) {
50 | fetch(`/invoices/${id}`).then((response) => {
51 | dispatch({
52 | type: "LOADED_INVOICE",
53 | data: response.json(),
54 | });
55 | });
56 | }
57 |
58 | /////////////////////////////////////////////
59 | // the component, all connected up
60 | class Invoice extends React.Component {
61 | componentDidMount() {
62 | fetchInvoice(this.props.dispatch, this.props.invoiceId);
63 | }
64 |
65 | componentDidUpdate(prevProps) {
66 | if (prevProps.invoiceId !== this.props.invoiceId) {
67 | fetchInvoice(this.props.dispatch, this.props.invoiceId);
68 | }
69 | }
70 |
71 | render() {
72 | if (!this.props.invoice) {
73 | return null;
74 | }
75 | return {invoice.number}
;
76 | }
77 | }
78 |
79 | export default connect((state) => {
80 | return { invoices: state.invoices };
81 | })(Invoices);
82 | ```
83 |
84 | ```js
85 | import React, { Suspense, Fragment, memo } from "react";
86 | import { unstable_createResource } from "react-cache";
87 |
88 | const Fetcher = unstable_createResource(() =>
89 | fetch("https://jsonplaceholder.typicode.com/todos").then((r) => r.json())
90 | );
91 |
92 | const List = () => {
93 | const data = Fetcher.read();
94 | return (
95 |
96 | {data.map((item) => (
97 | -
98 | {item.title}
99 |
100 | ))}
101 |
102 | );
103 | };
104 |
105 | const App = () => (
106 |
107 | {`React: ${React.version} Demo`}
108 | Loading... }>
109 |