精讀《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 許可證

相關文章
相關標籤/搜索