前端

Axios 的基本使用

fetch 虽然在现代浏览器中已经支持很好了,但是 API 还是没有那么好用。比如说增加 GET 参数必须使用 URLSearchParm 这个类。相比之下,Axios 的 API 还是更接近直觉一些。

import axios from 'axios'

axios(url, [config]);
axios(config);
axios.get(url, [config])
axios.delete(url, [config])
axios.put(url, data, [config])
axios.post(url, data, [config])

// 请求中 config 的属性
const config = {
    headers: {}, // Headers
    params: {},  // GET 参数
    data: {},  // 默认情况下 {} 会按照 JSON 发送
    timeout: 0, // 默认是没有 timeout 的
}

// 如果要发送传统的 POST 表单
const config = {
    data: new FormData()
}

// headers 会根据 data 是 json 还是表单自动设置,非常贴心。

// 响应的属性
let res = await axios.get("api")
res = {
    data: {},  // axios 会自动 JSON.parse
    status: 200,
    statusText: "OK",
    headers: {},
    config: {},
    request: {}
}
// 和 fetch 不同的是,res.data 可以直接使用,而 fetch 还需要 await res.json()

// 如果要添加一些默认的设置,使用
axios.defaults.headers.common["X-HEADER"] = "value"
axios.defaults.timeout = 3000
// 具体参见这里: https://github.com/axios/axios/blob/master/lib/defaults.js#L28

重定向与其他错误状态码

对于 3XX 重定向代码,axios 只能跟中,这是浏览器决定的,不是 axios 可以改变的。
对于 4XX 和 5XX 状态码,axios 会抛出异常,可以 catch 住。

上传文件

如果一个表单中包含的文件字段,那么传统的方法是把这个字段做为表单的一部分上传。然而现代的 API 大多是 json 接口,并没有 POST 表单这种格式。
这时候可以单独把上传作为一个接口,通过 POST 表单的方式上传,然后返回上传后的路径。在对应的 API 的接口中附件的字段填上这个路径就好了。

参考

  1. https://stackoverflow.com/questions/4083702/posting-a-file-and-associated-data-to-a-restful-webservice-preferably-as-json

React 中的表单

表单元素和所有其他元素的不同之处在于:表单元素是输入组件,它是有内部状态的,所有的其他元素都是输出元素。原生的 HTML 表单在 React 中本身也是能用的,但是一般情况下,我们可能会交给一个函数来处理提交表单这个事情,因为需要表单验证等等,这时候我们就可以用”受控组件”了。

什么是受控组件

受控组件就是数据不保存在 DOM 中,而是始终保存在 JS 中,随时通过 value/onChange 读取验证。非受控组件数据依然保存在 DOM 树中,只在 submit 时候读取。

function MyForm(props) {
  value, setValue = useState("");

  function handleChange(e) {
    setValue(e.target.value)
  }

  function handleSubmit(e) {
    console.log("A name was submitted: " + value);
    e.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" value={value} onChange={onChange} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

使用原生表单

完全可以使用原生组件,而不要直接使用受控组件。

function form2kv(form) {
  const fd = new FormData(form);
  const ret = {};
  for (let key of fd.keys()) {
    ret[key] = fd.get(key);
  }
  return ret;
}

function MyForm(props) {

  // 也可以直接通过读取 events.target 的值来获取
  function handleSubmit(event) {
    event.preventDefault();
    console.log(event.target.elements.username.value) // from elements property
    console.log(event.target.username.value)          // or directly
  }

  function handleSubmit(e) {
    e.preventDefault();
    const data = form2kv(e.target);
    axios.post("/api/myresource", data);
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Enter username</label>
      <input type="text" id="username" name="username">
      <label htmlFor="password">Enter password</label>
      <input type="password" id="password" name="password">
      <label htmlFor="birthdate">Enter your birth date</label>
      <input type="date" id="birthdate" name="birthdate">
      <button type="submit">Send data!</button>
    </form>
  )
}

在上面的例子中,我们使用了浏览器自带的 Formdata 对象,通过传入 DOM 中的 Form 元素来读取其中的数据。另外需要注意的一点是,我们使用了 onSubmit 事件,而不是 Submit Button 的 onClick 时间,这样保留了浏览器的原生处理方式,也就是可以使用回车键提交。

处理数据

如果当我们需要变换数据格式时呢?

  • 用户输入的数据是 MM/DD/YYYY, 服务端要求是 YYYY-MM-DD
  • 用户名应该全部大写

这时候可以使用另一个小技巧,data-x 属性。

const inputParsers = {
  date(input) {
    const [month, day, year] = input.split('/');
    return `${year}-${month}-${day}`;
  },
  uppercase(input) {
    return input.toUpperCase();
  },
  number(input) {
    return parseFloat(input);
  },
};

function form2kv(form, parsers = {}) {
  const fd = new FormData(form);
  const ret = {};
  for (let name of data.keys()) {
    const input = form.elements[name];
    const parseName = input.dataset.parse;

    if (parseName) {
      const parser = inputParsers[parseName];
      const parsedValue = parser(data.get(name));
      ret[name] = parsedValue;
    } else {
      ret[name] = data.get(name);
    }
  }
  return ret;
}

function MyForm(props) {
  function handleSubmit(e) {
    e.preventDefault();
    const data = form2kv(e.target);
    fetch("api/myresource", method="POST", body=JSON.stringify(data));
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Enter username</label>
      <input type="text" id="username" name="username" data-parse="uppercase">
      <label htmlFor="password">Enter password</label>
      <input type="password" id="password" name="password">
      <label htmlFor="birthdate">Enter your birth date</label>
      <input type="date" id="birthdate" name="birthdate" data-parse="date">
      <button type="submit">Send data!</button>
    </form>
  )
}

在上面的例子中,我们通过使用 data-x 属性指定了应该使用的处理函数。通过表单的 elements 属性,我们可以按照 input 的 name 访问元素。而节点的 data-x 属性可以在 dataset[x] 属性中访问。

验证表单

HTML 的原生 API 就支持了很多的验证属性,其中最最好用的就是 pattern 属性了,他可以指定一个正则来验证表单。而且我们也可以通过 type 属性来制定 email 等等特殊的 text 类型。HTML 支持的验证属性有:

  • required, 必填字段,不能为空
  • pattern, 通过一个正则来验证输入
  • minlength/maxlength, 输入文本的长度
  • min/max 数字的区间
  • type, email 等等类型

在 JS 中可以调用 form.checkValidity() 函数获得检查结果。还需要在 form 中添加 novalidate 属性,否则浏览器就会在校验失败的时候直接不触发 submit 事件了。

当表单验证不通过的时候,会获得一个 :invalid 的伪类。但是比较坑爹的是,当表单还没有填写的时候,验证就已经生效了,这样对用户来说非常不友好,不过也很好克服,我们只要设定一个类,比如说 display-errors, 只有当用户尝试提交表单之后再应用这个类就好啦~

displayErrors, setDisplayErrors = useState(false);

function handleSubmit(e) {
  if (!e.target.checkValidity()) {
    setDisplayError(true);
  }
}

return (
  <form
    onSubmit={handleSubmit}
    noValidate  // 即使校验失败也要触发
    className={displayErrors ? 'displayErrors' : ''}
  >
    {/* ... */}
  </form>
)
.displayErrors input:invalid {
  border-color: red;
}

select 组件

原生的 select 组件

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

在受控的组件中,可以在 select 组件中使用 value 属性指定选项。读取可以使用 e.target.value

<select value={state.value} onChange={handleChange}>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

如果需要多选的话,使用 <select multiple={true} value={['B', 'C']}>

file 组件

file 组件是只读的,所以只能使用

textarea 组件

react-hook-form

对于更复杂的逻辑,推荐使用 react-hook-form

react-hook-form 的 v7 中使用的是未受控的组件,这样性能更好。最近一直在使用 tailwind UI,对于原生组件来说写样式更方便一点。

当使用 RHF 的时候,可以配合使用 Yup 来做表单验证。

使用 antd 表单

表单的基本元素是 <Form/>, Form 中嵌套 Form.Item, 其中再嵌套 Input/TextArea 等组件。

表单的数据都在 form 元素中,使用 const form = Form.useForm() 来获取,并通过 form={form} 传递给 Form 控件。

设置了 name 属性的 Form.Item 会被 form 接管,也就是:

  1. 不需要通过 onChange 来更新属性值。当然依然可以通过 onChange 监听
  2. 不能使用 value 和 defaultValue,只能使用 form 的 initialValues/setFieldsValue 设置

总体来说,antd 的表单还是封装过度了,非常难以定制化。

参考

  1. https://reactjs.org/docs/uncontrolled-components.html
  2. Controlled and uncontrolled form inputs in React don’t have to be complicated
  3. How to handle forms with just React
  4. https://reactjs.org/docs/forms.html
  5. https://jsfiddle.net/everdimension/5ry2wdaa/
  6. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
  7. https://www.cnblogs.com/wonyun/p/6023363.html
  8. https://stackoverflow.com/questions/23427384/get-form-data-in-reactjs
  9. https://github.com/jquense/yup

放弃 Next.js, 拥抱 react-router

Next.js 是一个好库,设计上很优雅,实现上也没有什么大的问题。然而,考虑再三我还是决定暂时移除 next.js 了。不是我不想要服务端渲染,而是整个 JS 的生态圈大部分的库都没有考虑服务端渲染,这就导致我在学习和使用的过程中时不时要自己考虑如何处理服务端渲染的情形。本身我就是个初学者,连教程都看不太懂,再考虑服务端渲染,就一个头两个大了。另外一个原因就是组里另一个项目使用了 react-router, 没必要两个都搞了。这里姑且记录下移除 next.js, 添加 react-router 的过程,以便以后参考。

删除 nextjs

yarn remove next

更改 package.json scripts 部分的脚本:

"scripts": {
    "start": "react-scripts start",
    "dev": "react-scripts dev",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

还好之前 create-react-app 创建的 index.js 和 App.js 还没删掉,直接就能用了。

样式

nextjs 中规定了只能用 module css 或者 scoped css, 而在 react 中没有硬性的规定。如果使用原生 CSS 的话自然最简单,但是容易名字冲突。鉴于另一个项目使用了 sass, 这里也用 sass 以统一下开发体验。

页面

暂时先保留 pages, components, layouts 三个文件夹,但是需要使用 react-router 路由。

yarn add react-router react-router-dom

在 index.js 中使用 router, 去掉 <App/>

Link

更改所有的 Link. 从 import Link from 'next/link' 改成 import {Link} from 'react-router-dom', 其中需要把 href 改为 to.

更改所有的 useRouter 的跳转,需要使用 useHistory.

获取数据

页面里的 getServerSideProps 显然是不能用了,需要改用 redux 的 thunk 来获取数据,所以需要以下几步:

  1. 设置对后端 API 的代理,在 package.json 中添加 "proxy": "http://localhost:4000", 即可
  2. 引入 redux, 设计 store 等
  3. 调整请求接口到 redux 中

一般来说,我们把相应的 getServerSideProps 函数的逻辑转移到对应的 Page 组件的 useEffect(fn, []) 钩子中就可以了。

代理的问题

在 next.js 中,需要在两个地方指定代理,一个是后端在 server 预加载数据的时候,需要指定上游 API 的地址,另一方面,在浏览器中发送 ajax 请求的时候需要设定代理访问上游 API, 否则会有跨域的问题。

使用 react-router 之后问题就简单了,所有数据都是从前端加载的,所以只需要指定代理的地址就好了。但是也要考虑到几种不同的环境:

  1. 开发阶段的配置
  2. 部署阶段的配置
  3. 如果有多个后端如何处理
  4. 用户鉴权放在哪里

综合考虑后,采用以下几点:

  1. 用户鉴权放在前端的 server, 也就是 express 中,这样就避免了上游 API 再添加复杂的逻辑,但是用户列表可以放在后端中。
  2. 开发环境和部署环境统一使用 express 代理多个后端,这样在开发环境也能保证和生产环境一样的效果,方便 debug.

参考

  1. https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually
  2. https://stackoverflow.com/questions/50260684/bundle-react-express-app-for-production
  3. https://dev.to/nburgess/creating-a-react-app-with-react-router-and-an-express-backend-33l3

Nextjs 中遇到的一些坑

nextjs 的 Link 无法自定义 escape

nextjs 中的 Link 的 href 对象如果传的是字典,直接调用的是 nodejs 的 URL 库,不能自定义 escape, 比如说空格会被强制格式化成加好,而不是 %20. 而且好像它使用的这个 API 在 11.0 已经 deprecated 了,所以需要啥 url 的话,还是自己格式化吧~

不支持 loading spinner

Nextjs 不支持在页面跳转的时候触发 Loading Spinner, 也就是转动的小圆圈,所以需要自己实现一下,可以用 nprogress

在 _app.js 中:

import Router from 'next/router';
import NProgress from 'nprogress'; //nprogress module
import 'nprogress/nprogress.css'; //styles of nprogress

//Binding events. 
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());

function MyApp({ Component, pageProps }) {
    return <Component {...pageProps} />
}
export default MyApp;

代理后端 API 服务器

在 next.config.js 中配置重定向:

module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/proxy/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
      },
    ]
  },
}

参考

  1. https://levelup.gitconnected.com/improve-ux-of-your-next-js-app-in-3-minutes-with-page-loading-indicator-3a422113304d
  2. https://github.com/vercel/next.js/discussions/14057
  3. https://nextjs.org/docs/api-reference/next.config.js/rewrites

React Hooks

使用 useState hook

useState 可以用来管理一个组件比较简单的一两个状态,如果状态多了不适合使用 useState 管理,可以使用 useReducer.

import React, {useState} from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> Click me </button>
    </div>
  );
}

export default Counter;

在上面的简单例子中,我们直接使用值作为 setState 的参数,但是在部分更新 state 的时候,最好使用一个函数来作为参数:

const [state, setState] = useState({"showText": true, "showShadow": true})

<button onClick={() => setState((s) => {...s, showText: false})}>Hide Text</button>

使用 useEffect hook

useEffect hook 用来实现一些副作用,一般可以用作页面首次加载时读取数据。

import React, {useState, useEffect} from 'react';

function App() {
  const [isOn, setIsOn] = useState(false);

  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(() => console.log('tick'), 1000);
    }
    return () => clearInterval(interval);
  }, [isOn]);
  ...
}

export default App;

在 useEffect 中返回的函数会被用来做垃圾清理。另外需要注意的是,初始化的时候总会触发一次 useEffect.

默认情况下,每次 state 有改变的时候,都会调用 useEffect 函数。如果需要更改触发的时机,那么需要使用 useEffect 的第二个参数来指定监听的事件或者说状态。当第二个参数只使用一个空数组 [] 的时候就只会在组件加载的时候调用。数组中有哪些变量,表示在这些变量变化的时候调用。

一般建议把不依赖 props 和 state 的函数提到你的组件外面,并且把那些仅被 effect 使用的函数放到 effect 里面。

使用 useContext hook

Context 用来向所有包含的元素广播状态,或者说事件,而不需要通过组件树层层传递。

  1. 首先需要通过 React.createContext 定义一个高层次 Context,
  2. 然后在最外层使用 <MyContext.Provider /> 来包裹需要接受这个 context 的所有组件。
  3. 在需要使用状态的元素中调用 const ctx = useContext(MyContext), 然后使用 ctx 访问 Context 中的值

在基于类的 React 组件中,需要使用 <MyContext.Consumer /> 来实现读取值,现在我们都用 useContext 钩子了。

const themes = {
  light: { foreground: "#000000", background: "#eeeeee" },
  dark: { foreground: "#ffffff", background: "#222222" }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div> <ThemedButton /> </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

使用 useReducer hook

useReducer 和 useState 的用途基本是一样的,但是当需要的状态比较复杂的时候,最好使用 useReducer. 有了 useReducer 钩子,基本上可以不使用 redux 了。

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 通常放在管理一组状态的根元素这个层级,比如说一个页面。dispatch 函数触发事件,reducer 函数用来处理事件,更新 state.

需要注意的是,在 useReducer 的 reducer 函数中,和 redux 不同的是,不需要 state=initialState 这个参数。默认参数在调用 useReducer 的时候已经给出了。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useReducer + useContext = (Better) Redux

可以组合使用 useReducer 和 useContext 来实现 Redux 的功能。对于某个组件自身的数据,我们只使用 useReducer 就可以很好地管理了。然而对于多个组件都需要使用的全局数据,需要是用 useContext 来广播给所有需要该数据的组件。

两者的组合使用很简单,使用 Context.Provider 把 state 和 dispatch 这两个变量广播给 root 下的所有元素,这样在需要使用 state 和 dispatch 的地方直接 useContext(Context) 就好了。

useMemo/useCallback 钩子

用来避免重复计算或者重复生成函数。

useRef 钩子

https://stackoverflow.com/questions/56455887/react-usestate-or-useref

自定义钩子

通过灵活组合 useState, 和 useEffect, 我们完全可以创建自己的钩子。

import React from 'react';

function useOffline() {
  const [isOffline, setIsOffline] = React.useState(false);

  function onOffline() {
    setIsOffline(true);
  }

  function onOnline() {
    setIsOffline(false);
  }

  React.useEffect(() => {
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);

    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []);

  return isOffline;
}

function App() {
  const isOffline = useOffline();

  if (isOffline) {
    return <div>Sorry, you are offline ...</div>;
  }

  return <div>You are online!</div>;
}

export default App;

参考

  1. https://www.robinwieruch.de/react-hooks
  2. https://www.robinwieruch.de/react-hooks-fetch-data
  3. https://medium.com/@nazrhan.mohcine/react-hooks-work-with-usestate-and-usereducer-effectively-471646cdf925
  4. https://swizec.com/blog/usereducer-usecontext-for-easy-global-state-without-libraries
  5. https://medium.com/@wisecobbler/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1
  6. https://stackoverflow.com/questions/56615931/react-hook-setstate-arguments
  7. https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

Nextjs 教程

当我们要写一个稍微复杂的 React 应用的时候,就需要路由功能了,比较流行的路由是 react router. 这是一个很好的库,但是当我们已经用到路由的时候,下一步就该考虑如何做服务端渲染了,所以直接上 next.js 吧。

鉴于我已经使用 create-react-app 创建了 react 应用,需要手工安装一下 next.js

yarn add next

把 package.json 中的 scripts 替换掉

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

核心概念

next 的核心概念是页面,没啥可解释的吧。按照约定,放在 /pages 文件夹中的每一个组件都是一个页面,比较恶心的是每个组件需要使用 export default 导出。

当然,pages/index.js 对应的自然是首页了。

function HomePage() {
  return <div>Welcome to Next.js!</div>
}

export default HomePage

然后就可以看到首页啦!啊啊啊

next.js 中,完全按照文件的物理路径来确定路由,比如如果你需要 post/1 这种路径,直接定义 pages/post/[id].js, 也是够直接了。

读取 URL 参数

需要使用 router 来手动读取

import {useRouter} from 'next/router'

function MyPage() {
  const router = useRouter();
  const {keyword} = router.query;
}

获取数据

在 nextjs 中,鼓励的方式是在服务端编译或者渲染的时候获取数据,而不是由客户端渲染数据。这里我们先不看 SSG 了,看现在最需要的 SSR.

在一个页面中,export 一个 async 函数 getServerSideProps 就可以实现获取服务端的数据。

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

context 中比较重要的几个属性:

  • params 路径中的参数,比如 {id: xxx}
  • req/res 请求响应
  • query query_string

在这个函数中,应该直接读取数据库或者外部 API.

除此之外,另一种方式自然是传统的在客户端获取数据了,可以使用 useSWR 库。

样式

next.js 中默认不让导入全局的 CSS, 所以你必须在 pages/_app.js 中导入全局的 css.

import '../styles.css'

// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

对于每一个组件,把它们的样式文件放到 [name].module.css 中就好啦。然后需要导入

import styles from 'Button.module.css';

export default function Button() {
  return <div className={styles.button}>Button</div>
}

另一种方式是使用 styled-jsx, 也就是把 CSS-in-JS 的方式,我个人还是喜欢这种方式一些。但是这种不好在 VSCode 中直接显示调色板。

<style jsx>{`
  h1 {
    color: red;
  }
`}</style>

静态文件

也很简单,直接放到 /public 目录,然后就能在根路径访问了。

路由

使用 [xxx] 放在路径中作为参数就好了。

nextjs 中的链接是这样的:

<Link href="/blog/[slug]" as={`/blog/${post.slug}`}>
    <a>{post.title}</a>
</Link>

外部接口

错误页面

next.js 可以自定义 404 和 500 错误页面

参考

  1. https://haodong.io/render-client-side-only-component-in-next-js
  2. https://github.com/vercel/next.js/blob/canary/examples/progressive-render/pages/index.js

JavaScript 可视化库调研

核心关注指标:

  1. 好看吗?
  2. 支持多少数据,性能如何,内存占用如何?
  3. 开发活跃度
  4. 能否交互
  5. 是否支持 react
  6. 渲染后端是什么,基于 SVG 还是 canvas 还是 HTML?
  7. License,GPL 的不能要
  8. 支持绘制图形的种类
  9. API 是否足够简单,友好。

其中标注 * 的是我个人比较喜欢的

综合

  1. Vega Vega 很全面,几乎包括了所有的图形样式。
  2. nivo 基于 react 和 d3。支持的图形不少。我自己试了下,太复杂了,上手难度有点大。
  3. echarts 百度出品,国内用的比较多,但是感觉有点丑。据说 bug 也比较多。
  4. [d3.js] 这个可以说是绘图界的祖师爷了,功能过于强大,后面提到的好多库都是基于 d3 的。但是因为比较底层,像我这种拿来党还是暂时用不上。

统计常用图

绝大多数的图还是画折线图这些的,大部分的库也是做这个的。

  1. recharts 基于 React 和 D3.js。使用 SVG,只支持 line chart,bar chart 这些比较常见的。API 感觉有些复杂,不是很直观。
  2. reactviz 基于 react,Uber 出品,也是常见的统计图
  3. chartist 亮点是有动画,没有依赖,体积特别小。支持的图比较少
  4. nvd3 这个看起来确实不错,支持的图表类型一般,基于 d3.js。
  5. chart.js 支持的数量也比较少,主要是 line chart 和 bar chart. 这个可能是标星最多的了。
  6. xkcd 风格的图表

以下为不推荐的库:

  1. apexchart 似乎是 fusion chart 的一个开源版本。
  2. uvcharts.js 开发很不活跃,才 200 个星星
  3. victory 没看出有什么特别吸引人的。
  4. chartbuilder。好几年没有更新了。而且比较丑。
  5. c3.js。基于 D3, 貌似图比较少。
  6. toast。韩国的一个东西,还包含了日历。

图(Graph)

这里的图指的是计算机科学上的图,也就是由节点和边构成的结构。

  1. sigma 用于绘制 graph 的。
  2. cytoscape

时序数据

  1. *dc.js。特点是支持 crossfilter。特别好看,不过支持的图不多。
  2. Lightweight Charts。比较小巧,适合绘制金融数据。
  3. dygraphs。这个貌似只是画线条图的
  4. *uplot。特点是非常小,不支持任何交互。主要是画时间序列的。
  5. rickshaw Prometheus 使用过的绘图引擎。
  6. [flot] 这个貌似是 grafana 的绘图引擎。

其他

  1. plotly, plotly 是一个 Python 和 JavaScript 的绘图库。
  2. g2plot
  3. G2

uplot

综合以上的总结,决定开始使用 uplot.js

yarn add uplot
import React, {useEffect, useRef} from "react";
import uPlot from "uplot";

import "uplot/dist/uPlot.min.css";

function uPlotGraph({data, options}) {
    const plotRef = useRef();

    useEffect(function() {
        return new uPlot(options, data, plotRef);
    }, [options, data])

    return (
        <div ref={plotRef}></div>
    )
}

export default uPlotGraph;

uplot 的数据也很简单,就是 [X, Y1, Y2] 的形式,其中 X, Y 都是一个数组,X 中必须是单调递增的数字,默认是解析为秒级的时间戳。

参考

  1. https://codesandbox.io/s/black-sun-md3js9-mq7m3

从消息队列的角度来理解 Redux

在 React 中,组件之间是不能互相通信的,数据只能自上而下流动。所以必须把状态放在最高层的组件中。复杂一点的页面肯定会导致状态越提越高,所以最终还是需要一个单独的状态存储——redux。

Redux 的文档是非常差的。它只写了 What 和 How, 而没有写 Why. 他只说了自己是个状态存储,却不说为什么需要其他的东西,这就让初学者感到非常地迷惑,实际上它只要提到一下 “Event Bus” 这个词就非常容易理解了,非要上来就讲什么 Action/Store/Reducer/SingleSourceOfTruth 之类的。根据最高指示:代码是用来读的,不是用来装逼的,俗称 “码读不装”, 那么我们这里先来补上 redux 的 Why.

Why Redux?

Redux 本质上来说有很大一部分功能就是一个 Event Bus, 或者 Message Queue, 又或者 Pub/Sub. React 组件之间需要通信,如果让他们之间互相访问,那么就是一个 O(N^2) 的复杂度,而如果让他们通过一个中间组件,也就是 Redux 通信,那么复杂度就大大降低了,更重要的是,代码更清晰了。当然,Redux 不只是负责他们之间的通信,而且是把状态存储了下来。这两篇 文章 写得非常好了。

What is Redux?

Redux 里的概念特别多,不过知道了 EventBus, 就都非常好理解了,这里先列出来:

  • store, 存放状态的唯一容器,类似于消息队列的 broker.
  • reducer, 就是用来处理消息的函数,其实一般叫做 handler, 起个 reducer 的名字好像就函数式了,瞬间高大上加逼格高了起来。这个函数签名是 function(state, action) -> new_state.
  • action 和 dispatch, Action 其实就是消息总线里面的消息或者说事件,分发 Action, 其实不就是生产者么?
  • actionCreator, actionCreator 就是用来创建 Action 的。Action 不过就是一个 {type, value} 的字典罢了。function xxxCreator() -> action 就叫做一个 actionCreator
  • Middleware, 其实就是字面意思,会处理你的状态的一些中间件。
import {createStore} from "redux";

function reducer(state = {}, action) {
  switch (action.type) {
    case "XXX":
      return {
        ...state,
        XXX: action.value
      };
    case "YYY":
      return {

      }
    default:        
      return state;
  }
}

let store = createStore(reducer);

store.dispatch({type: "XXX", value: "XXX"})

console.log(store.getState())

以上就是一个非常基本的 redux 应用啦,如果要有多个 reducer 分开处理不同事件怎么办呢?使用 combineReducers. 需要注意的是,这样的话,每个 reducer 只会获得其中一部分数据了,比如说 userReducer 只会获得 user 部分,itemsReducer 只会处理 items 部分。

import {createStore, combineReducers} from "redux";

let reducers = combineReducers({
  user: userReducer, items: itemsReducer
})

let store = createStore(reducers)

React-Redux

在上面的例子中,实际上完全没有涉及到 react 的相关内容,redux 本身就是可以独立存在的一个库。但是,99.99% 的情况下,你还是为了管理 react 的状态采用 redux 的。要在 react 中使用 redux , 需要使用 react-redux 库。

这里需要注意的是,都 2020 年了,我们自然要用 hooks 了,所以网上好多还在讲 connect 的教程可以不用看了。几个常用的钩子:

  • useSelector, 顾名思义,用来从 store 中选取需要的状态,相当于消费者 consume
  • useDispatch, 用来 dispatch action, 相当于生产者 produce
  • useStore, 虽然有这个 hook, 但是文档里明确说了不推荐用
// yarn add react-redux
import {useSelector, useDispatch} from "react-redux";

// 这里需要注意的是,state 是整个状态树。
const result = useSelector((state) => selectedState)
// const result = useSelector((state) => state.counter)

const dispatch = useDispatch()
// 这里需要用一个匿名函数
<button onClick={() => dispatch({ type: 'increment-counter' })}>
  Increment counter
</button>

不使用 hooks 的方式

在没有 hooks 之前,需要使用 react-redux 库提供的 connect/mapState/mapDispatch 几个函数来实现 redux 和 react 组件之间的交互。

  • useSelector 对应以前的 mapState 函数
  • useDispatch 对应以前的 mapDispatch 函数

设计 redux state tree

我们知道 redux 的 state 是作为一个树存在的,设计这个树的形态是 redux 使用的重中之重了。

第一个问题,是否需要把所有的状态放进 redux 呢?不一定,对于一些组件内部的 UI 状态,比如是否隐藏某个按钮,是可以使用 useState 放在内部的。重点在于:你这个状态其他组件关心吗?一般来说,表单的状态时不需要放进 redux 的。

第二个问题,对于多个页面,比如说:一个列表页,一个详情页,一个用户页,要把所有的状态放在一个 store 中么?我倾向于把同一个组的页面的数据放在同一个 State 里面,这里的组大概就相当于 Django 的 App 或者是 Flask 的 Blueprint 的概念。当然,对于小型项目来说,用一个 store 就够了。

总的来说,分以下几步:

  1. 确定全局状态,比如用户登录状态,放在 redux 最顶级。
  2. 设计每个页面分区可视化树。根据页面布局,确定组件,以及每个组件的状态。
  3. 设计 reducers, 同样是一棵树状的组织。
  4. 实现 actions, 其中也包括加载数据的 action.
  5. 实现展示层,也就是组件们。

页面的布局图和状态树:


使用 normalized data

当我们使用 useSelector 的时候,需要从 state 出发,一层层地获取数据,所以数据的层级最好不要太多,避免出现 state.posts[0].comments[0] 这种冗长的表达式。另一方面,设计不好的状态树会导致好多状态重复存储,或者不好查询。为了解决这个问题,我们可以使用 Redux 官方推荐的解决方案。

不好的方案:

const blogPosts = [
  {
    id: 'post1',
    author: { username: 'user1', name: 'User 1' },
    body: '......',
    comments: [
      {
        id: 'comment1',
        author: { username: 'user2', name: 'User 2' },
        comment: '.....'
      },
      {
        id: 'comment2',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      }
    ]
  },
  {
    id: 'post2',
    author: { username: 'user2', name: 'User 2' },
    body: '......',
    comments: [
      {
        id: 'comment3',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      },
      {
        id: 'comment4',
        author: { username: 'user1', name: 'User 1' },
        comment: '.....'
      },
      {
        id: 'comment5',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      }
    ]
  }
  // and repeat many times
]

更好的组织方式

{
    posts : {
        byId : {
            "post1" : {
                id : "post1",
                author : "user1",
                body : "......",
                comments : ["comment1", "comment2"]
            },
            "post2" : {
                id : "post2",
                author : "user2",
                body : "......",
                comments : ["comment3", "comment4", "comment5"]
            }
        },
        allIds : ["post1", "post2"]
    },
    comments : {
        byId : {
            "comment1" : {
                id : "comment1",
                author : "user2",
                comment : ".....",
            },
            "comment2" : {
                id : "comment2",
                author : "user3",
                comment : ".....",
            },
            "comment3" : {
                id : "comment3",
                author : "user3",
                comment : ".....",
            },
            "comment4" : {
                id : "comment4",
                author : "user1",
                comment : ".....",
            },
            "comment5" : {
                id : "comment5",
                author : "user3",
                comment : ".....",
            },
        },
        allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
    },
    users : {
        byId : {
            "user1" : {
                username : "user1",
                name : "User 1",
            },
            "user2" : {
                username : "user2",
                name : "User 2",
            },
            "user3" : {
                username : "user3",
                name : "User 3",
            }
        },
        allIds : ["user1", "user2", "user3"]
    }
}

而这些数据要放在 entities 中:

{
    simpleDomainData1: {....},
    simpleDomainData2: {....},
    entities : {
        entityType1 : {....},
        entityType2 : {....}
    },
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}

何时发送数据请求?

通过 side effects 和 thunk 与服务器通信

从上面的脚本我们可以看出,redux 完全是一个本地的消息处理,然而当我们在本地做出更改的时候,肯定需要在放到服务器啊,这种操作在 redux 中被称作 side effects(副作用), 可以使用 redux-thunk 来实现。

// 添加 thunk 支持,一般在 store.js 中
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

如果是一个组件的内部数据,那么没必要在 redux 中保存状态,也就是使用 useState 就好了。获取数据直接在 useEffect 中 fetch 数据,然后 setState 就可以了。

在 useEffect 中获取数据之后触发 action, 还是通过触发一个 action 来获取数据?这个和私有状态是一样的考虑,如果还有其他的组件会触发这个操作更新数据,那么就使用 action 最好,如果这个数据是组件私有的,那么在 useEffect 中直接获取就好。比如说,对于页面组件,可能直接在组件中使用 useEffect(fn, []) 加载数据就好了,可以直接调用 fetch, 也可以触发一个 action.

对于需要获取数据的操作,一般需要三个 Action, 分别是 FETCH_XXX_BEGIN/SUCCESS/FAILURE. 如果获取数据成功,那么 SUCCESS 的 action 中就会包含数据。另外,还要有一个 RESET_XXX_DATA 的 action, 用来清除和重置。

一个典型的加载数据的 action:

export function fetchSearchData(args) {  
    return async (dispatch) => {    
    // Initiate loading state    
    dispatch({      
      type: FETCH_SEARCH_BEGIN    
    });
    try {      
      // Call the API      
      const result = await fetch(
        "api/search/?q=xxx",
        args.pageCount, 
        args.itemsPerPage
      );           
      // Update payload in reducer on success     
      dispatch({        
        type: FETCH_SEARCH_SUCCESS,        
        payload: result,        
        currentPage: args.pageCount      
      });    
    } catch (err) {     
      // Update error in reducer on failure           
      dispatch({        
        type: FETCH_SEARCH_FAILURE,        
        error: err      
      });    
    }  
  };
}

对应的 Reducer 可以这样:

const initialState = {  payload: [],  isLoading: false,  error: {}};

export function searchReducer( state=initialState, action ) {    
  switch(action.type) {    
    case FETCH_SEARCH_BEGIN:      
      return {        
        ...state,        
        isLoading: true    
      };        
    case FETCH_SEARCH_SUCCESS:      
      return {        
        ...state,        
        payload: action.payload,        
        isLoading: false      
      };        
    case FETCH_SEARCH_FAILURE:      
      return {        
        ...state,        
        error: action.error,        
        isLoading: false            
      };
    case RESET_SEARCH_DATA:      
      return {
        ...state,
        ...initialState
      };        
    default:
      return state;
  }
}

在 class-based React 中,一般是在 ComponentWill/DidMount 周期中调用加载数据的逻辑,我们现在自然是使用 useEffect 的这个钩子来实现。

redux 项目目录结构

只要分着放 actions 和 reducers 两个目录就可以了,其实没多大要求。我一般是这样放的:

App.js
state/
  actions/
  reducers/
  actionTypes.js
  rootReducer.js
  store.js

在 index.js 中还是需要

import {Provider} from 'react-redux';
import store from 'state/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

和 React-router 一起使用

当我们和 react-router 一起使用的时候,会遇到一个问题,组件是从 URL 中读取状态,还是从 store 中读取状态呢?这两个状态之间怎么同步呢?redux 的作者 Dan 给出了答案:

不要在 redux 中保存 URL 中的状态,直接从 URL 中读取。可以把 URL 想象成另外一个单独的小的 Store.

遗留问题

  • 页面跳转/组件卸载时,是否要删除上一个页面的数据

参考

  1. Redux 的本质,Event bus
  2. https://stackoverflow.com/questions/50703862/react-redux-state-shape-for-multiple-pages
  3. https://stackoverflow.com/questions/37288070/designing-redux-state-tree
  4. https://stackoverflow.com/questions/33619775/redux-multiple-stores-why-not
  5. Redux 中文基础教程
  6. https://www.pluralsight.com/guides/how-to-structure-redux-components-and-containers
  7. https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase
  8. 使用 hooks 编写 redux
  9. https://medium.com/@gaurav5430/async-await-with-redux-thunk-fff59d7be093
  10. https://medium.com/fullstack-academy/thunks-in-redux-the-basics-85e538a3fe60
  11. The best way to architect your Redux app
  12. Where and When to Fetch Data With Redux
  13. https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount/
  14. How to sync Redux state and url query params
  15. https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

如何加载数据

  1. https://stackoverflow.com/questions/51113369/how-to-dispatch-fetched-data-from-api
  2. https://stackoverflow.com/questions/39419237/what-is-mapdispatchtoprops
  3. You have to include dispatch in useEffect because of lint rules
  4. https://stackoverflow.com/questions/38206477/react-router-redux-fetch-resources-if-needed-on-child-routes
  5. https://www.freecodecamp.org/news/loading-data-in-react-redux-thunk-redux-saga-suspense-hooks-666b21da1569/
  6. https://stackoverflow.com/questions/57097390/react-hooks-fetch-data-inside-useeffect-warning
  7. https://stackoverflow.com/questions/57925027/useeffect-goes-in-infinite-loop-when-combined-usedispatch-useselector-of-redux
  8. https://stackoverflow.com/questions/62167174/need-clarification-for-react-react-redux-hooks-middleware-thunk-fetching-ap

其他的状态管理

  1. https://github.com/jamiebuilds/unstated-next
  2. https://imweb.io/topic/5a453691a192c3b460fce36e
  3. https://news.ycombinator.com/item?id=12371248
  4. https://github.com/mobxjs/mobx/issues/199
  5. https://blog.rocketinsights.com/redux-vs-mobx/

Python 和 JavaScript 语法对比

命名

  1. 注意使用驼峰变量名,不要使用下划线变量名

字符串

格式化

JavaScript:

`hello ${name}`

Python:

f"hello {name}"

文件

打开文件:

JavaScript:

const fs = require(fs).promises;  // 使用 async/await 版本的 fs 模块

await fs.writeFile(filename, data);  // 写入文件

// 文件是否存在
try {
  await fs.stat(filename)
  exitst = true
} catch (e) {
  exists = false
}

Python:

with open(filename, w) as f:
    f.write(data)

os.path.exists(filename)  # 文件是否存在

数组

切片:

JavaScript:

const arr1 = arr2.slice(3, 5);

Python:

arr1 = arr2[3:5]

学习 React

当我第一次接触前端的时候,那时候流行的是后端 MVC 模式。过去写界面的方法是,把所有的结构 (html),动作 (js),样式 (css) 分开,好处是非侵入,离了谁都能工作,缺点是无法模块化。在 js 无足轻重,甚至有 noscript 这种插件的过去,显然 MVC 是最佳实践,但是到了 js 大行其道的今天,把 html/js/css 打包在一起的模块化又被提出来了。

react 中也没有模板中的 {% block xxx %} 这个概念,直接使用 props。

Hello World

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

Jsx 中可以使用大括号插值。对于 html 中不能自闭合的标签,都可以自闭合。JSX 最终会被编译成 JavaScript.

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

// 相当于
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

一个函数就可以是一个组件。在 React 16 时代,就不要再用 class 了,统一用函数式组件就好了。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 相当于
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

props 只有向下传递一种方式。所有的函数都必须是纯函数。使用函数作为组件的一个缺点是没有办法保存状态。

在 class 组件中,需要使用使用 setState 更新状态。不能使用 += 类似的操作符,setState 是异步的,因此要传递回调函数。

事件

react 的事件和 HTML 的不同。所有属性是 camelCase 的;不能通过 return false 来阻止事件,必须调用 e.preventDefault

<button onClick={activateLasers}>
  Activate Lasers
</button>

一般情况下,在 React 中是不需要调用 addEventListener

条件渲染

React 是 functional 的。所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。state 是组件内部的状态。

key 是一个很关键的概念,有点像是 html 中的 ID, 用来唯一标示一个元素,因为 react 会尽可能复用元素。但是 key 不需要是全局唯一的,只需要在兄弟元素之间唯一即可。

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];

ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

组合

在 React 中不要使用继承来组织组件,而要使用组合,这也是近几年来面向对象领域的趋势。在 React 中,可以通过读取 props.children 来获取传递进来的子组件。

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

如果需要对子组件布局的话,可以使用命名的方式:

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane left={ <Contacts /> } right={ <Chat /> } />
  );
}

另一种方式是特化,也就是类似函数的 partial

Thinking in React

React 设计哲学:https://zh-hans.reactjs.org/docs/thinking-in-react.html


  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

已经过期的一些知识

由于 JS 的 this 的坑,需要使用 public class fields。如果要向回调函数中使用参数需要这样:onClick={(e) => this.deleteRow(id, e)}

class LoggingButton extends React.Component {
  // This syntax ensures `this` is bound within handleClick.
  // Warning: this is *experimental* syntax.
  handleClick = () => {
    console.log('this is:', this);
  }
render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

如果需要传递参数的话:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>

参考资料

  1. https://medium.com/@Zwenza/functional-vs-class-components-in-react-231e3fbd7108
  2. https://segmentfault.com/a/1190000011474522
  3. https://stackoverflow.com/questions/22876978/loop-inside-react-jsx