精读《React Conf 2019 - Day1》

1 引言

React Conf 2019 在今年 10 月份举办,内容质量仍是一如既往的高,若是想进一步学习前端或者 React,这个大会必定不能错过。php

但愿前端精读成为你学习成长路上的布道者,因此本期精读就介绍 React Conf 2019 - Day1 的相关内容。css

总的来看,React Conf 今年的内容视野更广了,不只仅有技术内容,还有宣扬公益、拓展到移动端、后端,最后还有对 web 发展的总结与展望。html

前端世界正变得愈来愈复杂,能够看到你们对将来都充满了但愿,永不停歇的探索精神是这场大会的主旋律。前端

2 概述 & 精读

本期大会思想、设计上的内容较多,具体实现层内容较少,由于行业领导者须要引领规范,而真正技术价值在于思惟模型与算法,理解了解题思路,实现它其实并不难。react

开发者体验与用户体验

  • 开发者体验:DX(develop experience)
  • 用户体验:UX(user experience)

技术人解决的问题老是围绕 DX 与 UX,而通常来讲,优化了 DX 每每会带来 UX 的提高,这是由于一个解决开发者体验的技术创新每每也会带来用户体验的升级,至少也能让开发者有更好的心情、更充足的时间作出好产品。webpack

如何优化开发者体验呢?ios

易上手git

React 确实致力于解决这个问题,由于 React 其实是一个开发者桥梁,不管你开发 web、ios 仍是单片机,均可以经过一套统一的语法去实现。React 是一个协议标准(读到 reactReconciler 章节会更有体感),React 像 HTML,但 React 不止能构建 HTML 应用,React 但愿构建一切。github

高效开发web

React 解决调试、工具问题,让开发者更高效的完成工做,这也是开发者体验重要组成部分。

弹性

React 编写的程序拥有良好可维护性,包括数据驱动、模块化等等特征都是为了更好服务于不一样规模的团队。

对于 UX 问题,React 也有 Concurrent mode、Suspense 等方案。

虽然 React 还不完美,但 React 致力于解决 DX 与 UX 的目标和效果都是咱们有目共睹的,更好的 DX、UX 必定是前端技术将来发展的大趋势。

样式方案

Facebook 使用 css-in-js,而今年的 React conf 给出了一种技术方案,将 413 kb 的样式文件体积下降到 74kb!

一步步了解这个方案,从用法开始:

const styles = stylex.create({
  blue: { color: "blue" },
  red: { color: "red" }
});

function MyComponent(props) {
  return <span className={styles("blue", "red")}>I'm red now!</span>;
}
复制代码

如上是这个方案的写法,经过 stylex.create 建立样式,经过 styles() 使用样式。

主题方案

若是使用 CSS 变量定义主题,那么换肤就能够由最外层 class 轻松决定了:

.old-school-theme {
  --link-text: blue;
}

.text-link {
  color: var(--link-text);
}
复制代码

字体颜色具体的值由外层 class 决定,所以外层的 class 就能够控制全部子元素的样式:

<div class="old-school-theme">
  <a class="text-link" href="...">
    I'm blue!
  </a>
</div>
复制代码

将其封装成 React 组件,也不须要用 context 等 JS 能力,而是包裹一层 class 便可。

function ThemeProvider({ children, theme }) {
  return <div className={themes[theme]}>{children}</div>;
}
复制代码

图标方案

下面是设计师给出的 svg 代码:

<svg viewBox="0 0 100 100">
  <path d="M9 25C8 25 8..." />
</svg>
复制代码

将其包装为 React 组件:

function SettingsIcon(props) {
  return (
    <SVGIcon viewBox="0 0 100 100" {...props}>
      <path d="M9 25C8 25 8..." />
    </SVGIcon>
  );
}
复制代码

结合上面提到的主题方案,就能够控制 svg 的主题颜色。

const styles = stylex.create({
  primary: { fill: "var(--primary-icon)" },
  gighlight: { fill: "var(--highlight-icon)" }
});

function SVGIcon(color, ...props) {
  return (
    <svg>
      {...props}
      className={styles({
        primary: color === "primary",
        highlight: color === "highlight"
      })}
      {children}
    </svg>
  );
}
复制代码

减小样式大小的秘密

const styles = stylex.create({
  blue: { color: "blue" },
  default: { color: "red", fontSize: 16 }
});

function MyComponent(props) {
  return <span className={styles("default", props.isBlue && "blue")} />;
}
复制代码

对于上述样式文件代码,最终会编译成 c1c2c3 三个 class

.c1 {
  color: blue;
}
.c2 {
  color: red;
}
.c3 {
  font-size: 16px;
}
复制代码

出乎意料的是,并无根据 bluedefault 生成对应的 class,而是根据实际样式值生成 class,这样作有什么好处呢?

首先是加载顺序,class 生效的顺序与加载顺序有关,而按照样式值生成的 class 能够精确控制样式加载顺序,使其与书写顺序对应:

// 效果多是 blue 而不是 red
<div className="blue red" />

// 效果必定是 red,由于 css-in-js 在最终编排 class 时,虽然两种样式都存在,但书写顺序致使最后一个优先级最高,
// 合并的时候就会舍弃失效的那个 class
<div className={styles('blue', 'red')} />
复制代码

这么作永远不会出现头疼的样式覆盖问题。

更重要的是,随着样式文件的增多,class 总量会减小。这是由于新增的 class 涵盖的属性可能已经被其余 class 写到并生成了,此时会直接复用对应属性生成的 class 而不会生成新的:

<Component1 className=".class1"/>
<Component2 className=".class2"/>
复制代码
.class1 {
  background-color: mediumseagreen;
  cursor: default;
  margin-left: 0px;
}
.class2 {
  background-color: thistle;
  cursor: default;
  justify-self: flex-start;
  margin-left: 0px;
}
复制代码

正如这个 Demo 所示,正常状况的 class1class2 存在许多重复定义的属性,但换成 css-in-js 的方案,编译后的效果等价于将 class 复用并拆解了:

<Component1 classNames=".classA .classC .classD">

<Component2 classNames=".classA .classN .classD .classE">
复制代码
.classA {
  cursor: default;
}
.classB {
  background-color: mediumseagreen;
}
.classC {
  background-color: thistle;
}
.classD {
  margin-left: 0px;
}
.classE {
  justify-self: flex-start;
}
复制代码

这种方式不只节省空间、还能自动计算样式优先级避免冲突,并将 413 kb 的样式文件体积下降到 74kb。

字体大小方案

rem 的好处是相对的字体大小,使用 rem 做为单位能够很方便实现网页字体大小的切换。

但问题是如今工业设计都习惯了以 px 做为单位,因此一种全新的编译方案产生了:在编译阶段将 px 自动转换成 rem

这等于让以 px 为单位的字体大小能够跟随根节点字体大小随意缩放。

代码检测

静态检测类型错误、拼写错误、浏览器兼容问题。

在线检测 dom 节点元素问题,好比是否有可访问性,好比替代文案 aria-label。

提高加载速度

普通网页的加载流程是这样的:

先加载代码,而后会渲染页面,在渲染的同时发取数请求,等取数完成后才能渲染出真实数据。

那么如何改善这个状况呢?首先是预取数,提早解析出请求并在脚本加载的同时取数,能够节省大量时间:

那么下载的代码能够再拆分吗?注意到并非全部代码都做用于 UI 渲染,咱们能够将模块分为 ImportForDisplayimportForAfterDisplay

这样就能够优先加载与 UI 相关的代码,其他逻辑代码在页面展现出以后再加载:

这样能够实现源码分段加载,并分段渲染:

对取数来讲也是如此,并非全部取数都是初始化渲染阶段必须用上的。能够经过 relay 的特性 @defer 标记出能够延迟加载的数据:

fragment ProfileData on User {
  classNameprofile_picture { ... }

  ...AdditionalData @defer
}
复制代码

这下取数也能够分段了,首屏的数据会优先加载:

利用 relay 还能够以数据驱动方式结合代码拆分:

... on Post {
  ... on PhotoPost {
    @module('PhotoComponent.js')
    photo_data
  }

  ... on VideoPost {
    @module('VideoComponent.js')
    video_data
  }

  ... on SongPost {
    @module('SongComponent.js')
    song_data
  }
}
复制代码

这样首屏数据中也只会按需加载用到的部分,请求时间能够再次缩短:

能够看到,与 relay 结合能够进一步优化加载性能。

加载体验

能够 React.SuspenseReact.lazy 动态加载组件。经过 fallback 指定元素的占位图能够提高加载体验:

<React.Suspense fallback={<MyPlaceholder />}>
  <Post>
    <Header />
    <Body />
    <Reactions />
    <Comments />
  </Post>
</React.Suspense>
复制代码

Suspense 能够被嵌套,资源会按嵌套顺序加载,保证一个天然的视觉连贯性。

智能文档

经过解析 Markdown 自动生成文档你们已经很熟悉了,也有不少现成的工具能够用,但此次分享的文档系统有意思之处在于,能够动态修改源码并实时生效。

不只如此,还利用了 Typescript + MonacoEditor 在网页上作语法检测与 API 自动提示,这种文档体验上升了一个档次。

虽然没有透露技术实现细节,但从热更新的操做来看像是把编译工做放在了浏览器 web worker 中,若是是这种实现方式,原理与 CodeSandbox 实现原理 相似。

GraphQL and Stuff

这一段在安利利用接口自动生成 Typescript 代码提高先后端联调效率的工具,好比 go2dts。

咱们团队也开源了基于 swagger 的 Typescript 接口自动生成工具 pont,欢迎使用。

React Reconciler

这是知识密度最大的一节,介绍了如何使用 React Reconclier。

React Reconclier 能够建立基于任何平台的 React 渲染器,也能够理解为经过 React Reconclier 能够建立自定义的 ReactDOM。

好比下面的例子,咱们尝试用自定义函数 ReactDOMMini 渲染 React 组件:

import React from "react";
import logo from "./logo.svg";
import ReactDOMMini from "./react-dom-mini";
import "./App.css";

function App() {
  const [showLogo, setShowLogo] = React.useState(true);

  let [color, setColor] = React.useState("red");
  React.useEffect(() => {
    let colors = ["red", "green", "blue"];
    let i = 0;
    let interval = setInterval(() => {
      i++;
      setColor(colors[i % 3]);
    }, 1000);

    return () => clearInterval(interval);
  });

  return (
    <div className="App" onClick={() => { setShowLogo(show => !show); }} > <header className="App-header"> {showLogo && <img src={logo} className="App-logo" alt="logo /" />} // 自创语法 <p bgColor={color}> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React{" "} </a> </header> </div> ); } ReactDOMMini.render(<App />, codument.getElementById("root")); 复制代码

ReactDOMMini 是利用 ReactReconciler 生成的自定义组件渲染函数,下面是完整的代码:

import ReactReconciler from "react-reconciler";

const reconciler = ReactReconciler({
  createInstance(
    type,
    props,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    const el = document.createElement(type);

    ["alt", "className", "href", "rel", "src", "target"].forEach(key => {
      if (props[key]) {
        el[key] = props[key];
      }
    });

    // React 事件代理
    if (props.onClick) {
      el.addEventListener("click", props.onClick);
    }

    // 自创 api bgColor
    if (props.bgColor) {
      el.style.backgroundColor = props.bgColor;
    }

    return el;
  },

  createTextInstance(
    text,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    return document.createTextNode(text);
  },

  appendChildToContainer(container, child) {
    container.appendChild(child);
  },
  appendChild(parent, child) {
    parent.appendChild(child);
  },
  appendInitialChild(parent, child) {
    parent.appendChild(child);
  },

  removeChildFromContainer(container, child) {
    container.removeChild(child);
  },
  removeChild(parent, child) {
    parent.removeChild(child);
  },
  insertInContainerBefore(container, child, before) {
    container.insertBefore(child, before);
  },
  insertBefore(parent, child, before) {
    parent.insertBefore(child, before);
  },

  prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext
  ) {
    let payload;
    if (oldProps.bgColor !== newProps.bgColor) {
      payload = { newBgCOlor: newProps.bgColor };
    }
    return payload;
  },
  commitUpdate(
    instance,
    updatePayload,
    type,
    oldProps,
    newProps,
    finishedWork
  ) {
    if (updatePayload.newBgColor) {
      instance.style.backgroundColor = updatePayload.newBgColor;
    }
  }
});

const ReactDOMMini = {
  render(wahtToRender, div) {
    const container = reconciler.createContainer(div, false, false);
    reconciler.updateContainer(whatToRender, container, null, null);
  }
};

export default ReactDOMMini;
复制代码

笔者拆解一下说明:

React 之因此具有跨平台特性,是由于其渲染函数 ReactReconciler 只关心如何组织组件与组件间关系,而不关心具体实现,因此会暴露出一系列回调函数。

建立实例

因为 React 组件本质是一个描述,即 tag + 属性,因此 Reconciler 不关心元素是如何建立的,须要经过 createInstance 拿到组件基本属性,在 Web 平台利用 DOM API 实现:

createInstance(
    type,
    props,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    const el = document.createElement(type);

    ["alt", "className", "href", "rel", "src", "target"].forEach(key => {
      if (props[key]) {
        el[key] = props[key];
      }
    });

    // React 事件代理
    if (props.onClick) {
      el.addEventListener("click", props.onClick);
    }

    // 自创 api bgColor
    if (props.bgColor) {
      el.style.backgroundColor = props.bgColor;
    }

    return el;
  }
复制代码

之因此说 React 对 DOM 事件都作了一层代理,是由于 JSX 的全部函数都没有真正透传给 DOM,而是经过相似 el.addEventListener("click", props.onClick) 的方式代理实现的。

而自定义这个函数,咱们甚至能建立例如 bgColor 这种特殊语法,只要解析引擎实现了这个语法的 Handler。

除此以外,还有 建立、删除实例 的回调函数,咱们都要利用 DOM 平台的 API 从新实现一遍,这样不只能够实现对浏览器 API 的兼容,还能够对接到好比 react-native 等非 WEB 平台。

更新组件

实现了 prepareUpdatecommitUpdate 才能完成组件更新。

prepareUpdate 返回的 payloadcommitUpdate 函数接收到,并根据接收到的信息决定如何更新实例节点。这个实例节点就是 createInstance 回调函数返回的对象,因此若是在 WEB 环境返回的 instance 就是 DOMInstance,后续全部操做都使用 DOMAPI。

总结一下:react 主要用平台无关的语法生成具备业务含义的 AST,而利用 react-reconciler 生成的渲染函数能够解析这个 AST,并提供了一系列回调函数实现完整的 UI 渲染功能,react-dom 如今也是基于 react-reconciler 写的。

图标体积优化

Facebook 团队经过优化,将图标大小从 4046.05KB 下降到了 132.95kb,体积减小了惊人的 96.7%,减小体积占总包体积的 19.6%!

实现方式很简单,下面是原始图标使用的代码:

<FontAwesomeIcon icon="coffee" />
<Icon icon={["fab", "twitter"]} />
<Button leftIcon="user" />
<FeatureGroup.Item icon="info" />
<FeatureGroup.Item icon={["fail", "info"]} />
复制代码

在编译期间经过 AST 分析,将全部字符串引用换成了图标实例的引用,利用 webpack 的 tree-shaking 功能实现按需加载,从而删除了没有使用到的图标。

import {faCoffee,faInfo,faUser} from "@fontawesome/free-solid-svg-icons"
import {faTwitter} from '@fontawesome/free-brands-svg-icons'
import {faInfo as faInfoFal} from '@fontawesome/pro-light-svg-icons'

<FontAwesomeIcon icon={faCoffee} />
<Icon icon={faTwitter} />
<Button leftIcon={faUser} />
<FeatureGroup.Item icon={faInfo} />
<FeatureGroup.Item icon={faInfoFal} />
复制代码

替换工具 的连接放出来了,感兴趣的同窗能够点进去了解更多。

这也从某种意义上说明了 iconFont 注定被淘汰,由于字体文件目前没法按需加载,只有所有使用 SVG 图标的项目才能使用这种优化。

Git & Github

这一节介绍了基本 Git 知识以及 Github 用法,笔者略过比较水的部分,直接列出两个可能你不知道的点:

干预 Github 项目主要语言检测

若是你提交的代码包含许多自动生成的文件,可能你实际使用的语言不会被 Github 解析为主要语言,这时候能够经过 .gitattributes 文件忽略指定文件夹的检测:

static/* linguist-vendored
复制代码

这样语言文件占比统计就会忽略 static/ 文件夹。

Git hooks 的技巧

如下是几个比较具备启发的点,咱们能够利用 Git hooks 作点什么:

  • 阻止提交到 master。
  • 在 commit 以前执行 prettier/eslint/jest 检测。
  • 检测代码规范、合并冲突、检测是否有大文件。
  • commit 成功后给出提示或记录到日志。

但 Git hooks 仍然有局限性:

  • 容易被绕过:--no-verifuy --no-merge --no-checkout ---force。
  • 本地 hooks 没法提交,致使项目开发规则可能不尽相同。
  • 没法替代 CI、服务端分支保护、Code Review。

能够畅想一下,在 WebIDE 环境能够经过自定义 git 命令禁止检测绕过,天然解决第二条环境不一致的问题。

GraphQL + Typescript

GraphQL 是没有类型支持的,若是要手动建立一遍类型文件是很是痛苦的:

interface GetArticleData {
  getArticle: {
    id: number;
    title: string;
  };
}

const query = graphql(gql` query getArticle { article { id title } } `);

apolloClient.query<GetArticleData>(query);
复制代码

一样的代码分散在两处维护必定会带来问题,咱们能够利用好比 typed-graphqlify 这种库解决类型问题:

import { params, types, query } from "typed-graphqlify";

const getArticleQuery = {
  article: params({
    id: types.number,
    title: types.string
  })
};

const gqlString = query("getUser", getUserQuery);
复制代码

只要一遍定义就能够自动生成 GQLString,而且拿到 Typescript 类型。

React 文档国际化

即使是谷歌翻译也不是很靠谱,国际化文档仍是要靠人肉,Nat Alison 利用 Github 充分发动各国人民的力量,共同打造了一个个 reactjs group 下的国际化仓库。

国际化仓库命名规则是 reactjs/xx.reactjs.org,好比简体中文的国际化仓库是:github.com/reactjs/zh-…

从仓库的 readme 能够看到维护规则是这样的:

  • 请 fork 这个仓库。
  • 基于 fork 后的仓库中 master 分支拉取一个新的分支(名字自取)。
  • 翻译(校对)你所选择的文章,提交到新的分支。
  • 此时提交 Pull Request 到该仓库。
  • 会有专人 Review 该 Pull Request,当两人以上经过该 Pull Request 时,你的翻译将被合并到仓库中。
  • 删除你所建立的分支(如继续参与,参考同步流程)。

以后按期从 React 官方文档项目拉取最新代码便可保持文档的同步更新。

你须要 redux 吗?

关于数据流的话题目前没有什么新意,但此次 React Conf 关于数据流总结的算是比较真诚的,总结了如下几个点:

  1. 全局数据流如今不是必须的,好比 Redux,但也不能说彻底不能用,至少在全局状态较为复杂时有必要使用。
  2. 不要只使用一种数据流方案,根据状态的做用域肯定方案比较好。
  3. 工程技术与科学不一样,工程世界没有最好的方案,只有更好的方案。
  4. 就算有了完美方案也不要中止学习的步伐,总会有新知识产生。

web 历史

很精彩的演讲,不过新鲜内容并很少,比较有感触一点是:之前的网页地址对应到的是服务器磁盘的某个具体文件,好比早期 php 应用,如今后端再也不是文件化而是服务化了,这层抽象让服务端摆脱了对文件结构的依赖,能够构建更多复杂动态逻辑,也支持了先后端分离的技术方案。

3 总结

这届 React Conf 让咱们看到前端更多的可能性,咱们不只要关注技术实现细节,更要关注行业标准以及团队愿景。

React 团队的愿景是让 React 一应俱全,提高全球开发者的开发体验、提高全球产品的用户体验,基于这个目标,React Conf 天然不能只包含 DOM Diff、Reconciler 等等技术细节,更须要展现 React 如何帮助全球开发者,如何让这些开发者帮助到用户,如何推进行业标准的演进,如何让 React 打破国界、语言的壁垒。

相比其余前端大会很是多的干货来讲,React Conf 虽然显得主题比较杂,但这正是人文情怀的体现,我相信只有带着更高的使命愿景,真诚帮助他人的技术团队才能够走得更远。

讨论地址是:精读《React Conf 2019 - Day1》 · Issue #214 · dt-fe/weekly

若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

相关文章
相关标签/搜索