专注于WEB前端开发, 追求更好的用户体验, 更好的开发体验 (长沙前端QQ群:234746733)

开发

  • [译] 对比Redux和Relay

    / 分类: 开发 / No Comments

    原文: Comparing Redux and Relay, 作者: Mikhail Novikov (reindex CTO & Co-founder)

    注: 译文内容根据个人对Redux和Relay的使用经验而翻译, 如发现任何问题, 望指正(可留言或Email: kairyou@qq.com).

    开发React应用, 有时必须要解决管理客户端state的问题. 现代应用程序不应总是等服务器的响应, 切换页面就要重新请求, 而应该让页面展现尽可能地快. 应用的状态管理层(也可称为缓存层或模型层)正是负责处理这些逻辑.

    Redux和Relay在应用中就是负责这一层. 本文会用一些常见的示例代码来比较这两个库。

    架构概述

    Redux和Relay的灵感都源于Flux(一种架构模式用来设计应用程序, 和MVC属同类). Flux的基本思路是让数据从应用的数据存储中心(Stores)到组件(Components)始终单向流动(单向数据流). 组件调用Action创建函数(Action Creators), 将Actions发送给Store. Flux最初由Facebook提出, 但是他们并没有提供一个已集成好Flux的现成库. 之后, 开源社区很快出现了许多Flux实现, 还有更多自定义实现在闭源代码库中 :). Flux很适合作为React的数据模型, 因为它禁止数据从子组件向上传递到store, 数据的修改必须通过dispatch(action)完成.

    Redux

    Redux简化了Flux架构. 把Flux中多个Store的概念简化成只有一个Store.
    Store的数据可以通过转换方法传给组件(provider把store的数据传给connect, 让它把这些数据传给组件).
    Store可以通过reducers处理action. 最大的区别是, reducer是纯函数, 接收2个参数, 现有的state(previousState)和action, 并返回新的state.
    Action, 在Flux中通常是改变state的操作, 在Redux中是函数式转换(Flux里的action是函数的形式,但在redux里是普通的js对象). 任何数据都可以储存在Redux store.

    Redux是一个很小的库. Action可以通过中间件来拦截或改变, 开源社区中也有很多中间件可用. 通过中间件, 编写功能完善的Redux应用会更容易.

    Relay

    Relay在很多方面也受到Flux启发. 只有一个store,通过action去改变(在Relay中称为Mutations). 然而, Relay禁止开发人员直接控制Store的内容. 相反, Relay根据GraphQL查询语句去自动处理, 储存或修改服务端数据.
    组件通过编写GraphQL查询片段(fragments)来描述依赖的数据, Relay会根据当前组件树中所有组件的依赖数据描述去自动优化查询(把多个组件的请求合并为1次GraphQL请求).
    对Store的修改(写操作)可以通过mutation(变更)来实现, mutations在客户端和服务器端都修改数据, 保持数据一致. 不同于Redux, 在Relay中只能变更在服务端声明过的数据, 并且服务器必须有一个GraphQL服务.

    Relay提供了许多很赞的功能. Relay负责所有数据的获取, 并确保所需数据无异常. Relay有完善的分页支持, 很适合类瀑布流类的场景(无限滚动). Relay mutations可以做到乐观更新(Optimistic Update), 即: 页面UI先改变, 再以服务器返回结果为准更改页面UI, 如果出错会回滚.

    组件集成

    Redux

    Relay和Redux都可以很好地与与React集成. Redux并不依赖于React, 可以和其他框架搭配使用. Relay目前只能和React/React Native搭配使用. 然而, 为了支持除React外的其他框架, 抽取Relay组件层的工作已经展开.

    Redux提倡通过容器组件和展示组件分离的开发思想实现表现与数据逻辑分离. 即: 只在顶层组件用Redux, 仅用于展示的内部组件的数据都通过props传入.
    容器组件通常由Redux创建, 负责dispatch actions以及从Redux store读取state. 展示组件仅仅是普通的React组件. 容器组件通过定义映射store state到props的方法(mapStateToProps)把指定数据传递给展示组件. 复杂的应用,也有存在多个容器组件, 但层次结构应保持使用传递props传递数据.

    // 这个例子: VisibleTodoList是容器组件, TodoList是展示组件
    import { connect } from 'react-redux'
    
    const getVisibleTodos = (todos, filter) => {
      switch (filter) {
        case 'SHOW_ALL':
          return todos
        case 'SHOW_COMPLETED':
          return todos.filter(t => t.completed)
        case 'SHOW_ACTIVE':
          return todos.filter(t => !t.completed)
      }
    }
    
    const mapStateToProps = (state) => {
      return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
      }
    }
    
    const VisibleTodoList = connect(
      mapStateToProps,
    )(TodoList)
    
    export default VisibleTodoList
    

    通过react-redux提供的Provider作为最顶层组件, 来包住根组件 ,以此来让组件树中所有组件都能访问到store.

    import { Provider } from 'react-redux'
    const store = createStore(todoApp); // Redux store
    // ...
    render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )
    
    Relay

    Relay把React组件包裹进Relay容器中. Relay容器够自动检索子组件的数据依赖(根据GraphQ查询片段). Relay容器容器互相隔离, 并确保GraphQL查询片段在组件被渲染之前获取到数据, 查询数据作为props传递进UI组件.

    // 下面的todo会
    class Todo extends React.Component {
      render() {
        return (
          <div>{this.props.todo.id} {this.props.todo.text}</div>
        );
      }
    }
    
    const TodoContainer = Relay.createContainer(Todo, {
      fragments: {
        todo: () => Relay.QL`
          fragment on Todo {
            id,
            text
          }
        `,
      }
    })
    

    组件树里所有的查询片段会合并为1次查询, 已经获取过的数据查询将会自动排除, 如果所有数据都获取过将不会有查询.

    const TodoListContainer = Relay.createContainer(Todo, {
      fragments: {
        todoList: () => Relay.QL`
          fragment on TodoConnection {
            count,
            edges {
              node {
                ${TodoContainer.getFragment('todo')}
              }
            }
          }
        `,
      },
    })
    

    Relay有个顶级的组件RootContainer(相当于entry point). 依赖2个属性: Relay容器和Route.
    Route和路由没半点关系, 用于配置数据查询, 未来可能会改名为RelayQueryRoots或RelayQueryConfig.
    Relay.Route是必要的, 因为类似的组件可能需要不同的初始数据(比如<TodoList />组件, 既可以可以用来展示整个团队的任务, 也可以用于展示某个人的任务).
    Relay.Route还可使用参数, 用paramDefinitions标记预期的参数.

    class TodoRoute extends Relay.Route {
      static routeName = 'TodoRoute';
    
      static queries = {
        todoById: () => Relay.QL`
          query {
            todoById(id: $id)
          }
        `
      };
    
      static paramDefinitions = {
        id: { required: true }
      };
    }
    
    render(
      <Relay.RootContainer
        Component={SingleTodoContainer}
        route={new TodoRoute({id: 123})} />,
      document.getElementById('root')
    )
    

    Mutations(变更)

    客户端改变数据是一个常见需求. 我们通常希望交互更快, 做到乐观更新. 之后等待服务器返回结果后, 进行UI改变.

    Let’s make a mutation that changes the text of a TODO item, updates it on the server, and rolls back the change, if it fails.

    Redux

    我们将使用thunk 中间件实现异步操作. 首先触发乐观更新, 然后再回滚或应用变更.

    function todoChange(id, text, isOptimistic) {
      return {
        type: TODO_CHANGE,
        todo: { id, text, isOptimistic }
      };
    }
    
    function editTodo(id, text) {
      return (dispatch, getState) => {
        const oldTodo = getState().todos[id];
        // Perform optimistic update
        dispatch(todoChange(id, text, true))
        fetch(`/todo/${id}`, {
          method: 'POST'
        }).then((result) => {
          if (result.code === '200') {
            // Confirm update
            dispatch(todoChange(id, text, false))
          } else {
            dispatch(todoChange(oldTodo.id, oldTodo.text, false))
          }
        })
      }
    }
    
    // In the store handler
    case TODO_CHANGE:
      return {
        ...state,
        todos: {
          ...state.todos,
          [id]: {
            id,
            text,
            isOptimistic
          }
        }
      }
    )
    
    // Now we can dispatch it
    
    store.dispatch(editTodo(todo.id, todo.text));
    
    Relay

    Relay中没有自定义动作. 相反, 我们需要定义个Mutation(使用Relay mutation DSL).
    GraphQL mutations采用一种有趣的方式 - mutation首先是一个操作, 然后是一个查询. 因此, 我们可以通过mutations使用GraphQL查询, 以类似的方式来获取数据.
    这样, Relay根据变更的数据会自动处理UI变动.

    class ChangeTodoTextMutation extends Relay.Mutation {
      // Get the name of the mutation on server, so that we can call the server
      getMutation() {
        return Relay.QL`mutation{ updateTodo }`;
      }
    
      // Map the props passed to mutation to the server input object
      getVariables() {
        return {
          id: this.props.id,
          text: this.props.text,
        };
      }
    
      // Define a query on the resulting payload, with all the data that changed
      getFatQuery() {
        return Relay.QL`
          fragment on _TodoPayload {
            changedTodo {
              id
              text
            }
          }
        `;
      }
    
      // Define what exactly Relay should change in the store. In this case
      // we say that it should match the item from `changedTodo` element in the
      // result with an item in the store by id and then update the item in the
      // store
      getConfigs() {
        return [{
          type: 'FIELDS_CHANGE',
          fieldIDs: {
            changedTodo: this.props.id,
          },
        }];
      }
    
      // To make Relay make an optimistic update, we need to "fake" the response
      // from the server. Here it's pretty easy.
      getOptimisticResponse() {
        return {
          changedTodo: {
            id: this.props.id,
            text: this.props.text,
          },
        };
      }
    }
    

    现在这个mutation可以以类似action的方式被调用.

    Relay.Store.commitUpdate(
      new ChangeTodoTextMutation({
        id: this.props.todo.id,
        text: text,
      }),
    );
    

    我们还可以传递mutation成功或失败的回调函数给commitUpdate. 当然, 任何情况, 如果请求失败, 回滚的动作都是自动处理的.

    Relay.Store.commitUpdate(
      new ChangeTodoTextMutation({
        id: this.props.todo.id,
        text: text,
      }), {
       onFailure: () => {
         console.error('error!');
       },
       onSuccess: (response) => {
         console.log('success!')
       }
    });
    

    Mutation DSL被认为是Relay的一个弱点.
    有时, 不太容易弄清所有参数和Relay所需的精确匹配查询.
    应该指出的是Relay可以做一些相当复杂的事情,比如对一些分页数据的条目做变更.
    Relay维护人员目前正在开发一种新的底层API, 有望提高编写mutations的体验.

    处理分页数据

    Redux

    在Redux中可以有很多方式实现分页. 在Redux中实现分页, 需要创建一个reducer来跟踪当前获取的数据, 得到下一页的cursor_id或URL或页码参数.

    real-world示例应用中有个用Redux实现分页的例子, 分页数据来自GitHub API.

    Redux在这方面处于劣势, 因为缺乏标准的服务端API而不能自动实现分页. 然而, 这也意味着可以更加自由地实现手动分页.

    Relay

    Relay通过Connection模型来抽象列表数据.
    Relay GraphQL的所有connection都支持分页参数, connection的每条元素都有一个cursor属性作为指针标记翻页关系.
    此外, connection还提供了pageInfo字段标记是否有下一页.

    分页采用标准化API的好处是复杂的工作都交给了Relay.
    我们只需传递参数, Relay会去负责获取数据(那些未获取的数据).

    可以在Relay容器中传递变量来使用cursor.
    变量可以被传递到GraphQL查询片段中.

    下面尝试实现一个简单的PaginatedTodoList组件和容器.

    class PaginatedTodoList extends React.Component {
      nextPage() {
        const lastElement = this.props.user.todos.edges.length - 1;
        this.setVariables({
          after: this.props.user.todos.edges[lastElement].cursor;
        });
      }
    
      render() {
        return (
          <div>
            <TodoList todos={this.props.user.todos} />
            {this.props.user.todos.pageInfo.hasNextPage ?
            <button onClick={this.nextPage} :
            <div>Last page</div>}
          </div>
        );
      }
    }
    
    const PaginatedTodoListContainer = Relay.createContainer(PaginatedTodoList, {
      fragments: {
        user: () => Relay.QL`
          fragment on User {
            todos(first: 10, after: $after) {
              edges {
                cursor
              }
              # 声明子组件需要的数据,再把数据作为props传给子组件
              # `todos`是子组件`<TodoList>` 的 `fragments`里定义的 (https://git.io/voo3i)
              ${TodoList.getFragment('todos')}
              pageInfo {
                hasNextPage
              }
            }
          }
        `,
      },
      initialVariables: {
        after: null,
      },
    });
    

    当点击按钮加载更多时, 我们仅仅更新了after参数,Relay负责将获缺少的数据.

    总结

    Redux和Relay都是较成熟并经过行业验证的库.
    Relay提供了更多实用功能, 但正因为如此, 后端方面会更严格, 需要兼容GraphQL API.
    Redux非常灵活, 但是意味着你需要编写更多的代码.

    如果你想快速尝试下Relay, 可以尝试下Reindex, Relay兼容的GraphQL实现(Reindex是一个提供GraphQL后端服务的平台, 也是作者的公司).

    延伸阅读

    一些中文资料:

  • 解决新版npm乱码

    / 分类: 开发 / No Comments

    新版npm install的时候, 可能会看到类似这样的特殊字符:
    loadDep:glob-base ▌ ╢█████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╟

    应该是新版针对可显示特殊字符的环境故意为之, 如果希望看到下面这样的效果:
    loadDep:glob-base / |#############-------------------|

    OSX系统选了语言为英文: LC_CTYPE=en_US.UTF-8 npm install

    OSX系统选了语言为中文: LANG= npm install

    所以通用的解决办法: 在~/.zshrc~/.bashrc里增加一行
    alias npm="LC_CTYPE=en_US.UTF-8 LANG= npm"

    重新加载配置, 或者重启Terminal后, 发现npm install变正常了~

    选择正常, 还是非主流, 看个人喜好吧~

  • 免费静态资源CDN整理

    / 分类: 开发 / No Comments

    资源多, 比较靠谱的CDN:

    • www.bootcdn.cn:
      更新快, 用的又拍云
      例子: //cdn.bootcss.com/react/15.1.0/react.min.js
    • www.jsdelivr.com:
      支持请求合并, 貌似有中国节点: http://git.io/vrfDQ
      例子: //cdn.jsdelivr.net/g/react@15.1.0(react.min.js+react-dom.min.js)
    • cdnjs.com:
      国内访问较慢, 例子: //cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js
    • unpkg.com:
      也使用的cloudflare的cnd, 更新快, react官方文档cdn部分也写的这个.
    • www.cdnjs.net:
      更新快, 支持请求合并 (*目前已不提供免费使用)
      例子: //libs.cdnjs.net/react/15.1.0/??react.min.js,react-dom.min.js
    • tlo.xyz/cdnjs-china
      由ze3kr.com站长提供, 和bootcdn类似, 也是又拍云, 不知是否能长期提供.
      例子: //cdnjs-com.b0.upaiyun.com/ajax/libs/react/15.1.0/react.min.js

    资源少或更新慢大厂cdn:

    • google: https://developers.google.com/speed/libraries/ (国内项目没法用)
    • microsoft: http://www.asp.net/ajax/cdn
    • baidu: http://cdn.code.baidu.com/
    • sina: http://lib.sinaapp.com/
    • 360: libs.useso.com已经停止服务, 奇舞团增加了: https://cdn.baomitu.com
    • upai: http://jscdn.upai.com/
    • qiniu: http://www.staticfile.org/ (不支持https, 更新/处理问题都比较慢)

    如何选择靠谱的

    • 国内项目: 貌似只能选bootcdn了(未使用较新js库的项目也可以选择大厂cdn). 非常可悲, 同步更新cdnjs的国内CDN, 没一个大厂能提供
      国外项目: jsdelivr/unpkg.com/cdnjs.com
      测试靠谱程度, 可以用下面几个地址测试下:
      http://ce.cloud.360.cn/
      http://www.17ce.com/
      http://tool.chinaz.com/speedtest.aspx

    • 使用多个CDN(防止某个CND挂掉):

    // 拿jQuery举例(cdn1挂掉, 使用cdn2)
    <script src="//cdn1.com/jquery.min.js"></script>
    <script>window.jQuery || document.write('<script src=//cdn2.com/jquery.min.js><\/script>')</script>
    
  • 使用nodejs开发桌面客户端应用

    / 分类: 开发 / No Comments

    这里主要针对node-webkit和atom-shell, nodejs下开发桌面应用也有其他可以选择(大体都是基于Chromium + nodejs), 主要这两个相对比较流行, 源码也一直保持更新. 目前node-webkit文档/例子可能多一些, atom-shell相对少一些.
    详细区别什么的自己去搜吧, 自己使用中体验到的:

    • node-webkit入口是html, atom-shell入口是JS;
    • node-webkit功能相对多一些, 两者都可以把代码打包(一个是.nw,一个是.asar)放到应用里面;
    • 即使自己写了几KB的代码, 但最终生成的程序都至少几十MB, node-webkit生成的应用比atom-shell相对小一些;
    • 他们一些概念比较类似(可能方法不同), 熟悉一个后, 对熟悉另一个应该是有帮助的;

    性能什么肯定没有原生的好了, 但是用一种语言就可以生成cross-platform的软件, 这好处也是显而易见的; 总之开发一些简单的小应用, 还是非常适合的, 至少不需要再去学2-3门编程语言了.

    自己写了简单的例子, 源码放在: github.com/kairyou/create-desktop-app-with-nodejs
    里面的脚本, 在Mac下面可以直接运行程序, 或者可以直接生成/OSX/windows/Linux三个平台的程序.

    自己偏向atom-shell多一些, 不过目前两个还是都要熟悉下, 可能有些特殊的功能, 必须要使用其中一个才能满足~

    这两个的源码和官方文档:
    github.com/rogerwang/node-webkit
    github.com/atom/atom-shell

  • 升级IE11后 的一些问题

    / 分类: 开发 / 2 Comments

    主要针对开发人员使用的一些障碍

    1. F12开发人员工具
    "仿真"面板只有"文档模式"(可切换IE7-IE10), 去掉了"浏览器模式"(IE8-IE10), 主要的影响就是:
    彻底不支持IE的条件注释了(虽然IE10的标准模式就已经不支持, 但IE10下支持切换"浏览器模式").
    所以IE11下面无论怎么设置都不支持[if gte IE xx]这样的条件注释了, 如果条件注释里加载了css/js, 在IE11测试IE7-IE9就只能暂时把条件注释去掉了.
    测试用IETester v0.52(windows8.1), IE9会崩溃IE10不能用, IE7/8貌似是正常的.
    再就是安装虚拟机了(www.modern.ie/zh-cn/virtualization-tools有很多版本可以下载), 硬盘不够大的情况为了测试安装几个windows是很浪费的~
    当然这3中方法测试IE体验都不够好, 如果一个平台就能解决那是最好不过了, 谁有更好的解决方案可以共享下~
    另外, 切换IE11文档模式来测试css/js都是正常的(比如IE8模式的canvas是不支持的), 所以这部分应该是没问题的.

    2. 代理服务器
    依然不能用SOCKS 5的代理. 还是需要使用HTTP方式, 而且必须要取消勾选"选项-高级-启用增强保护模式"并重启浏览器才可以使用代理.