React & Npm 組件庫維護經驗

咱們先來回顧一下 React ,Facebook 是這麼描述的:javascript

A JavaScript library for building user interfacescss

官方定義其爲 UI 庫,這名字彷佛過低調了些。從 React-Native 的發展就能看出來其野心勃勃,但官方的定義反而使其成爲了開發者的寵兒 —— "爲何要用React?" "它只是個UI庫"。html

從 jQuery 開始,前端組件遍地花開,有jQuery官方提供的成套組件,也有活躍社區提供的第三方組件,從最簡單的文本截斷功能,到複雜的拖拽排序都應有盡有。業務使用的時候,常常會給 window 掛上 $,代碼中組織也很是靈活,在須要複雜 dom 操做時,jQuery 總能幫忙輕鬆完成。前端

React 這個 UI 庫進入你們的視野後,咱們猛然發現『萬物皆組件』,就連最不加修飾的業務代碼也能夠做爲組件被其它模塊所引用,這極大的激發了你們的熱情。寫代碼的時候感受在造輪子,在寫導航欄、稍微通用點兒的功能時都自覺的將其拆了出來,剛要把標題寫死,猛然想到 "若是這裏用傳參變量,UI加個參數配置,這不就成通用組件了嗎!"。最先、最完全的把後端模塊思惟引入到前端,因此 React 組件生態迅速壯大。java

應該說 React 的出現加快了前端發展的進程,拉近了前端與後端開發的距離,以後各個框架便紛紛效仿,逐漸青睞對 Commonjs 規範的支持。業務開發中,將組件化思想完全貫徹其中,許多人都火燒眉毛的但願發佈本身平時積累的組件,下面就來談談如何從零開始構建組件庫。node

如何從零構建組件庫

組件庫的教程不僅對 React 適用,其中提到的思想,對大多數通用組件編寫都有效。react

本篇介紹的所有構建腳本代碼均可以在 github.com/fex-team/fi… 找到。webpack

分散維護 VS 集中維護

準備搭建組件庫之初,這估計是你們第一個會考慮到的問題:到底把組件庫的代碼放在一塊兒,仍是分散在各個倉庫?ios

調查發現 Antd 是將全部組件都寫入一個項目中,這樣方便組件統一管理,開發時不須要在多個倉庫之間切換,並且預覽效果只需運行跟項目,而不是爲每一個組件開啓一個端口進行預覽。其依賴的 react-components 組件庫中的組件以 rc 開頭,不過這個項目沒有進行集中管理。git

Material-UI、 React-UI 採用集中式管理等等。

可是集中管理有一些弊端。

  • 引用默認是載入所有,雖然能夠經過配置方式避免,(Antd 還提供了 webpack 插件作這個事情),但安裝時必須全量。
  • 沒法對每一個組件作更細粒度的版本控制。
  • 協做開發困難,每一個人都要搭建一套全環境,提 pr 也具備很多難度。

分散維護的弊端更明顯,沒法在同一個項目中觀察全局,修改組件後引起的連帶風險沒法觀察,組件之間引用須要發佈或者 mock,不直觀,甚至組件之間的版本關聯、依賴分析都無法有效進行管理。

所以 Fit 組件庫在設計時,也經歷了一番醞釀,最後採用了二者結合的方案,分散部署+集中維護的折中方式,並且竟能結合了二者各自的優勢:

  • 創建根項目 Root,用來作總體容器,順便還能夠當對外網站
  • 創建 Group,並在其中創建多個組件倉庫
  • 開發時只要用到項目 Root,根據依賴文件編寫腳本自動拉取每一個倉庫中的內容
  • 主要負責人拉取所有子項目倉庫,子組件維護者只須要下載對應組件
  • 發佈時獨立發佈每一個組件
  • 管理時,統一管理全部組件

package 版本統一

組件的依賴版本號須要統一,好比 fit-input ,fit-checkbox,fit-auto-complete 都依賴了 lodash,但由於前後開發時隔久遠,安裝時分別依賴了 2.x 3.x 4.x,當別人一塊兒使用你最新版的時候,就會無辜的額外增長了兩個 lodash 文件大小。

更可怕的是,連 React 的版本都不可靠,以前就遇到過一半組件留在 0.14.x ,一半新組件裝了 15.x 的狀況,直接致使了線上編譯後項目出錯,由於多個 React 組件不能同時兼容,這只是不能並存的其中一個例子。

由於項目開發時組件在一塊兒,使統一版本號成爲可能。咱們將全部依賴到的組件都安裝在 Root 項目中,每一個組件的 package.json 由腳本自動生成,這個腳本須要靜態掃描每一個組件的 Import 或 require 語法,分析到依賴的模塊後,使用根目錄的版本號,填寫在組件的 package.json 中,核心代碼以下:

ca505894-6bf9-47bd-8c5d-3a5fe39c3144

先收集每一個組件中的依賴, 若是在根目錄的 package.json 中找到了,就使用根目錄的版本號。

完整代碼倉庫:github.com/fex-team/fi…

依賴聯動

依賴聯動是指,fit-button 更新了代碼,若是 fit-table 依賴了 fit-button,那麼其也要發佈一個版本,更新 fit-button 依賴的版本號。

除了依賴第三方模塊,組件之間可能也有依賴,若是將模塊分散維護,想更新一下依賴模塊都須要發佈+下載,很是消耗時間,並且依賴聯動根本無法作。集中維護使用 webpack 的 alias 方案,在 typescript 找不到引用,總之不想找麻煩就不能寫 hack 的代碼。

回到 Fit 組件庫結構,由於全部組件都被下載到了 Root 倉庫下,所以組件之間的引用也天然而然的使用了相對路徑,這樣組件更新麻煩的問題迎刃而解,惟一須要注意的是,發佈後,將全部引入非本組件目錄的引用,替換成爲 npm 名稱,例如:

// 源碼的內容
import Button from '../../../button'
// 發佈時,經過編譯腳本替換爲
import Button from 'fit-button'複製代碼

依賴聯動,須要在發佈時,掃描全部組件,找出全部有更新的組件,並生成一項依賴配置,最後將全部更新、或者被依賴的組件統一升級版本號加入發佈隊列。

完整代碼倉庫:github.com/fex-team/fi…

inline Or ClassName?

React 組件使用 inline-style 仍是 className 是個一直有爭論的話題,在此我把本身的觀點擺出:className 比 inline-style 更具備拓展性。

首先 className 更符合 css 使用習慣,inline-style 無疑是一種退步,既拋棄了 sass less post-css 等強大預編譯工具的支持,也大大減弱了對內部樣式的控制能力,它讓 css 退化到了沒有優先級,沒有強大選擇器的荒蠻時代。

其次沒有預編譯工具的支持,別忘了許多 css 的實驗屬性都須要加上瀏覽器前綴,除非用庫把強大的 autoprefixer 再實現一遍。

使用 className 能夠很好的加上前綴,在追查文件時能獲得清晰的定位,下面是咱們對 CSS 命名空間的一種實現 ——html-path-loader css-path-loader 插件 配合 webpack 後獲得的調試效果:

文件結構

023fa032-ee32-4295-8d58-a5b29580eec3

DOM結構對應 className

(cloud.githubusercontent.com/assets/7970…)

直接從 dom 結構就能順藤摸瓜找到文件,上線時再將路徑 md5 處理。

這個插件會自動對當前目錄下的 scss或less 文件包一層目錄名,在 jsx 中,使用 className="_namespace" ,html-path-loader 會自動將 _namespace 替換爲與 css 一致的目錄名稱。

typescript 支持

既然前端模塊化向後端看齊,強類型也成爲了無可阻擋的將來趨勢,咱們須要讓開發出的組件原生支持 typescript 的項目,獲得更好的開發體驗,同時對 js 項目也能優雅降級。

因爲如今 typescript 已原生支持 npm 生態,若是組件自己使用 typescript 開發,咱們只須要使用 tsc -d 命令在目錄下生成對應的 d.ts 定義文件,當業務項目使用 typescript 的時候,會自動解析 d.ts 做爲組件的定義。

再給 package.json 再上 typings 定義指向入口文件的 d.ts ,那麼總體工做基本就完成了。

最後,對於某些沒有定義文件的第三方模塊,咱們在根項目 Root 中寫上定義文件後, 導入時將文件拷貝一份到組件目錄內,並修正相對引用的位置,保證組件獨立發佈後還能夠找到依賴文件。

完整代碼倉庫:github.com/fex-team/fi…

更強的拓展性

React 組件的拓展性彷佛永遠也爭論不休,不管你怎樣作組件,都會有人給你抱怨:要是這裏支持 xxx 參數就行了。

畢竟使用了組件,就必定不如本身定製的拓展性更強,節省了勞動力,就要付出被約束的代價,Fit 做爲一個大量被業務線使用的組件庫,使用了透傳方式儘量的加強組件拓展性。

咱們寫了一個很簡單的透傳組件:fit-transmit-transparently,使用方法以下:

import {others} from 'fit-transmit-transparently'
const _others = others(new Component.defaultProps, this.props)
// ... <div {..._others}/>複製代碼

它會將 this.props 中,除了 defaultProps 定義了的字段抽到 _others 中,直接透傳給外圍組件,由於 defaultProps 中定義了的字段默認是有含義的,所以不會對其進行操做,避免屢次定義產生的風險。

如今 fit-input 就將 props 透傳到了原生 Input 組件上,所以雖然我沒有處理各種事件,但依然能夠響應任意的 onKeyDown onKeyUp onChange onClick 等事件,也能夠定義 style 來覆蓋樣式等等。

fit-number 繼承了 fit-input,所以依然支持全部原生事件,fit-auto-complete 也繼承了 fit-input,對其添加的例如 onBlur 等事件依然會被透傳到 input 框中。

組件的 dom 結構要儘可能精簡,透傳屬性通常放置在最外層,但對於 input 這種重要標籤,透傳屬性最好放置與其之上,由於用戶的第一印象是 onChange 應該被 input 觸發。

同構模塊引用技巧

當依賴的模塊不支持 node 環境,但還必須加載它的時候,咱們但願在後端忽略掉它,而在前端加載它;當依賴模塊只處理了後端邏輯,在前端不必加載時,咱們但願前端忽略它,後端加載它,下面是實現的例子:

// 前端加載 & 後端不加載
if (process.browser) {
    require ('module-only-support-in-browser');
}
// 後端加載 & 前端不加載
require ('module-only' + '-support-in-node')複製代碼

前端加載&後端不加載的原理是,前端靜態掃描到了這個模塊,所以無條件加載了它(前端引用是靜態掃描),後端會由於判斷語句而忽略掉這個引用(後端引用是運行時)。

後端加載&前端不加載的原理是,將模塊引用拆成非字面量,前端靜態掃描發現,這是什麼鬼?忽略掉吧,而 node 會老老實實的把模塊拼湊起來,發現還真有 module-only-support-in-node 這個模塊,所以引用了它。

一份代碼 Demo & 源碼顯示

webpack 提供了以下 api 拓展 require 行爲:

0cb8955e-8050-4c56-a5ee-00e09c920336

7ab38b0f-2d22-45b2-be5f-6a86d2e25665

  • ! 打頭的,忽略配置文件的 preLoaders 設置
  • !!打頭的,忽略全部配置文件的設置
  • -! 打頭的,忽略 preLoaders 和 loaders ,但 postLoaders 依然有效

通常來講,咱們都在配置文件設置了對 js 文件的 loader,若是想引用源碼,正好能夠用 !! 打頭把全部 loaders 都幹掉,而後直接用 text-loader 引用,這樣咱們就獲得了一份純源碼以供展現。

組件編寫一些注意點

理解 value 與 defaultValue

defaultValue 屬性用於設置組件初始值,以後組件內部觸發的值的改變,不會受到這個屬性的影響,當父級組件觸發 render 後,組件的值應當從新被賦予 defaultValue。

value 是受控屬性,也用來設置值,但除了能夠設置初始值(優先級比 defaultValue 高)以外,還應知足只要設置了 value,組件內部就沒法修改狀態的要求,這個組件的狀態只能由父級授予並控制,因此叫受控屬性。

value 與 defaultValue 不該該同時存在,最好作一下檢查。

render 函數中最小化代碼邏輯

React 的宗旨是但願經過修改狀態來修改渲染內容,儘可能不要在 render 函數中編寫過多的業務邏輯和判斷語句,最好將能抽離成狀態的放在 state 中,在 componentWillReceiveProps 中改變它

使用 auto-bind

若是你也使用 ES6 寫法,那麼最好注意使用 auto-bind 插件,將全部成員函數自動綁定 this,不然 .bind(this) 會返回一個新的函數,一來損耗性能,二來很是影響子組件的 shouldComponentUpdate 判斷!

慎用 componentWillMount

對於同構模塊,React 組件的生命週期 componentWillMount 會在 node 環境中執行,而 componentDidMount 不會。

要避免在 willMount 中操做瀏覽器的 api,也要避免將無關緊要的邏輯寫在其中,致使後端服務器渲染吃力(目前 React 渲染是同步的),無關初始化邏輯應當放在 didMount 中,由客戶端均攤計算壓力。對於影響到頁面渲染的邏輯仍是要放在 willMount 中,否則後端渲染就沒有意義。

巧用 key 作性能優化

React 組件生命週期中 shouldComponentUpdate 方法是控制組件狀態改變時是否要觸發渲染的,但當同級組件量很是龐大時,即使在每一個組件作是否渲染的判斷都會花費幾百毫秒,這時咱們就要選擇更好的優化方式了。

新的優化方式仍是基於 shouldComponentUpdate ,只不過判斷條件很是苛刻,咱們設定爲只有 state 發生變化纔會觸發 render,其它任何狀況都不會觸發。這種方式排除了對複雜 props 條件的判斷,當 props 結構很是複雜時,對沒有使用 immutable 的代碼簡直是一場災難,咱們如今徹底忽略 props 的影響,組件變成爲了完徹底全封閉的王國,不會遵從任何人的指揮。

當咱們實在須要更新它時,全部的 props 都不起做用,可是能夠經過 key 的改變來繞過 shouldComponentUpdate 進行強制刷新,這樣組件的一舉一動徹底被咱們控制在手,最大化提高了渲染效率。

組件級 Redux 如何應用

組件級 Redux 使用場景主要在於組件邏輯很是複雜、或使用時,父子 dom 強依賴,但可能不會被用於直接父子級的場景,例如 fit-scroll-listen 組件,用來作滾動監聽:

import { ScrollListenBox, ScrollListenNail , ScrollListen, createStore } from 'fit-scroll-listen'
const store = createStore()

export default class Demo extends React.Component {
    render() {
        return (
            <div> <ScrollListenBox store={store}> <ScrollListenNail store={store} title="第一位置">第一個位置</ScrollListenNail> 內容 </ScrollListenBox> <ScrollListen store={store}/> </div> ) } }複製代碼

ScrollListenBox 是須要監聽滾動的區域,ScrollListenNail 是滾動區域中須要被標記的節點,ScrollListen 是顯示滾動監聽狀態的 dom 結構。

因爲業務需求,這三個節點極可能沒法知足直接父級子關係,並且上圖應用中,ScrollListen 就與 ScrollListenBox 是同級關係,二者也無辦法通訊,所以須要使用 Redux 做數據通訊。

咱們從 createStore 實例化了一個 store,並傳遞給每個 fit-scroll-listen,這樣他們即使隔着千山萬水,也能暢快無阻的通訊了。

npm 資源加載簡析

webpack&fis 最核心的功能能夠說就是對 npm 生態的支持了,社區是編譯工具的衣食父母,支持了生態纔會有將來。

爲了解決業務線可能遇到的各類 npm 環境問題,咱們要有刨根問底的精神,瞭解 npm 包加載原理。下面會一步一步介紹一個 npm 模塊是如何被解析加載的。

文件查找

不管是 webpack、fis,仍是其它構建工具,都有文件查找的鉤子,當解析了相似 import '../index.js' 時,會優先查找相對路徑,但解析到了 import 'react' 便無從下手,由於這時構建工具還不知道這種模塊應該從哪查找,咱們就從這裏開始截斷,當出現沒法找到的模塊時,就優先從 node_modules 文件夾下進行查找(node_modules 下查找模塊放到後面講)。

因爲 npm 模塊打平&嵌套兩種方案可能並存,每次都遞歸查找的效率過低,所以咱們首先會把 node_modules 下全部模塊緩存起來,這裏分爲兩種方案:

  1. 根據node_modules 下文件夾遍歷讀取,優勢是掃描全面,缺點是效率低。
  2. 根據 package.json 中 deps(能夠設置忽略devDeps)進行掃描,優先是效率高,缺點是忘記 --save 模塊會被忽略。

將全部模塊存到 map 後,咱們直接就能 get 到想要的模塊,可是要注意版本問題:若是這個模塊是打平安裝的,那毫無疑問不會存在同模塊多版本號問題,npm@3.x 後即使是打平安裝,但遇到依賴模塊已經在根目錄存在,但版本號不一致,仍是會採用嵌套方式,而 npm@2.x 不管如何都會用嵌套的方式。

所以咱們的目的就明確了,不用區分 npm 的版本,若是這個當前文件位於非 node_modules 文件夾中,直接從根目錄引用它須要的模塊,若是這個當前位於 node_modules 中,優先從當前文件夾中的 node_modules 獲取,若是當前文件夾的 node_modules 不存在依賴文件,就從根目錄取。

解讀 package.json

找到了依賴在 node_modules 裏的根目錄,咱們就要解析 package.json 進行引用了,main 這個屬性是咱們的指明燈,告訴咱們在複雜的包結構中,哪一個文件纔是真正的入口文件。

咱們還要注意 package.json 裏設置了 browser 屬性的模塊,因爲咱們作的是前端文件加載,因此這個屬性對咱們有效,將依賴模塊的路徑用 browser 作修正便可,通常都是同構模塊使用它,特地將前端實現重寫了一遍。因此當 browser 屬性爲字符串時咱們就放棄對 main 信任,轉而使用 browser 屬性來代替入口路徑。

當 browser 屬性爲對象時,狀況複雜一些,由於此時 browser 指代的含義不是入口文件的相對路徑,而是對這個模塊內部使用的包引用的重定向,此時咱們還不能信任 main 對入口的引導,初始化時將 browser 對象保存,總體查找順序是:優先查找當前模塊的 browser 設置,替換 require 路徑,找到模塊後,若是 browser 是字符串,優先用其路徑,不然使用 main 的路徑。

環境變量

npm 生態很是慣着用戶,咱們但願直接在模塊中使用 Buffer process.env.NODE_ENV 等變量,並且一般會根據當前傳入的變量環境作判斷,可能開發過程當中載入了很多影響性能,但方便調試的插件,當NODE_ENVproduction 時會自動幹掉,若是咱們不對這種狀況作處理,上線後沒法達到模塊的最佳性能(甚至報錯,由於 process 沒有定義)。

編譯腳本要根據用戶的設置,好比 CLI 使用了 NODE_ENV=production ,或者在插件中申明,就將代碼中 process.env.NODE_ENV 替換爲對應的字符串,對與 Buffer 這類模塊也要單獨拎出來替換成 require。

模塊加載

爲了讓瀏覽器識別 module.exports (es6 的 export 語法交給 babel 或者 typescript 轉換爲 module.exports)、define、require,須要給模塊包一層 Define,同時把模塊名緩存到 map 中,能夠根據文件路徑起名字,也可使用 hash,最後 require 就從這裏取便可。

因爲是簡析,不作更深刻的分析,剩下的工做基本上是優化緩存、對更多功能語法的支持。

同構方案

爲了保證傳統的首屏體驗,同時維持單頁應用的優點,替代方案走了很多彎路。從單獨寫一份給爬蟲看的頁面,到使用 phantomjs 抓取靜態頁面信息,如今已經步入了後端渲染階段,因爲其可維護性與用戶體驗二者兼顧,因此才快速壯大起來。

後端渲染

不管何種後端渲染方案,其本質都是在後端使用 nodejs 運行前端的 js 代碼,有的庫使用同步渲染,也有異步,React 目前官方實現屬於同步渲染,關於同步渲染遇到的問題與解決方案,會在 "同構請求" 這一節說明。

使用 React 進行後端渲染代碼以下:

import {renderToString} from 'react-dom/server'
const componentHTML = renderToString(React.createElement('div'))複製代碼

稍稍改造,將其與 Redux 結合,只須要將 Provider 做爲組件傳入,並傳入 store 來存儲頁面數據,最後得到的 initialState 就是頁面的初始數據:

import {Provider} from 'react-redux'
import configureStore from '../client/store'
const store = configureStore()
const InitialView = React.createElement(Provider, {store}, React.createElement('div'))
const componentHTML = renderToString(InitialView)
// Redux 後端渲染後的數據初始狀態
const initialState = store.getState()複製代碼

這樣,將頁面初始數據打在 window 全局變量中,前端 Redux 初始化直接用後端傳來的初始數據,就能夠將頁面狀態與後端渲染銜接上。

對於 Redux,是項目數據結構的抽象,最好按照 state 樹結構拆分文件夾,將 Redux 數據流與頁面、組件徹底解耦。

同構請求

同構請求是對後端渲染的進一步處理,使後端渲染不只僅能生成靜態頁面數據,還能夠首屏展示依賴網絡請求數據所渲染出的 dom 結構。

同構請求的優化主要體如今後端處理,由於前端沒有選擇,只能體如今 Http 請求。如今有兩種比較理想的方案:

http 請求

這種方案依賴同構的請求庫,例如 axios,在後端渲染時,能和前端同樣發出請求並獲取數據。主要注意一下,若是使用的是同步渲染的框架,例如 React,咱們須要將請求寫在生命週期以外,在其運行以前抽出來使用 Promise 調用,待請求 Ready 以後再執行一遍渲染便可。

這種方案修改爲本中等,須要把全部同構請求從組件實例中抽離出來,可能獲取某些依賴組件實例的數據源比較困難,不過能夠知足大部分簡單數據請求。

這種方案稍加改造,能夠產生一套修改爲本幾乎爲零的方案,缺點是須要渲染兩遍。第一遍渲染,將全部組件實例中的請求實例抽取出來,第二步相似使用 Promise.all 等數據獲取完畢,最後再執行一遍渲染便可,缺點是渲染兩遍,並且網絡請求耗費 IO,訪問外網數據速度很慢,和直接調用函數的速度徹底不在一個數量級。因此咱們在想,能不能將前端的 http 請求在後端轉換爲直接調用函數?

直接命中函數

這個方案基於上一套方案優化而來,惟一的缺點是渲染了兩遍,對項目改動極小,後端請求效率最大化。

但願後端直接命中函數,須要對總體項目框架進行改造,由於咱們要提早收集所有的後端方法存儲在 Map 中,當後端請求執行時,改成從 Map 中抽取方法並直接調用。

後端響應請求的方法,咱們採用裝飾器定義路由與收集到 Map:

import {initService, routerDecorator} from 'fit-isomorphic-redux-tools/lib/service'
class Service {
    @routerDecorator('/api/simple-get-function', 'get')
    simpleGet(options:any) {
        return `got get: ${options.name}`
    }
}
new Service()複製代碼

fit-isomorphic-redux-tools 組件導出的 routerDecorator 方法作了兩件事,第一件是綁定路由,第二件是將收集到的函數塞到 Map 中,key 就是 url,用於同構請求在後端定位查找。

前端代碼中,action 中調用 fit-isomorphic-redux-tools 提供的 fetch 方法,這個方法也作了兩件事,第一件是前端模塊根據配置發請求,第二件在後端環境下,經過 url 查找上一段代碼在 routerDecorator 註冊的函數,若是命中了,會直接執行該函數。

import fetch from 'fit-isomorphic-redux-tools/lib/fetch'
export const SIMPLE_GET_FUNCTION = 'SIMPLE_GET_FUNCTION'
export const simpleGet = ()=> {
    return fetch({
        type: SIMPLE_GET_FUNCTION,
        url: '/api/simple-get-function',
        params: {
            name: 'huangziyi'
        },
        method: 'get'
    })
}複製代碼

上面的 fetch 方法內部封裝了對瀏覽器與node環境的判斷,若是是瀏覽器環境則直接發送請求,node環境則直接調用 promise。在先後端都通過 redux 處理,爲了讓 reducer 拿到 promise 後的數據,咱們封裝一個 redux 中間件:

export default (store:any) => (next:any) => (action:any) => {
    const {promise, type} = action
    // 沒有 promise 字段不處理
    if (!promise) return next(action)

    const BEFORE = type + '_PROMISE_BEFORE'
    const DONE = type + '_PROMISE_DONE'
    next({type: BEFORE, ...action})
    if (process.browser) {
        // 前端必定是 promise
        return promise.then(req => {
            next({type: DONE, req, ...action})
        })
        //.catch...
    } else {
        const result = promise(action.data, action.req)
        if (typeof result.then === 'function') {
            // 處理 promise 狀況 (好比 async)
            return result.then((data: any) => {
                next({data, ...action})
                return true
            })
            //.catch
        } else {
            // 處理非 promise 狀況
            return next({type: DONE, ...action})
        }
    }
}複製代碼

上述代碼對全部包含 promise 的 action 起做用,在前端會在 promise 執行完畢後觸發 [actionName]_PROMISE_DONE ,在 reducer 裏監聽這個字符串便可。後端會直接調用方法,由於方法多是同步也多是異步的,好比下面就是異步的:

export const test = async (req:any, res:any) => {
    return 'test';
}複製代碼

因此作了兩套處理,async 最終返回一個 promise,若是不用 async 包裹住則沒有,所以 result.then === 'function' 即是判斷這個方法是不是 async 的。

給出一套上述理論的完整實現,有興趣的同窗能夠安裝體驗下:github.com/ascoders/is…

編譯優化

爲了不模塊太大致使的加載變慢問題,咱們經過 require.ensure 動態加載模塊,這也對 HTTP2.0 併發請求至關友好。

webpack&Fis 按需加載

使用了 require.ensure 的模塊,webpack&fis會將其拆分後單獨打包,並在引用時轉換爲 amd 方式加載,下面是與 react-router 結合的例子:

<IndexRoute getComponent={getHome}/>複製代碼
const getHome = (nextState: any, callback: any)=> {
    require.ensure([], function (require: any) {
        callback(null, require('./routes/home').default)
    })
}複製代碼

這樣遍作到了對業務模塊的按需加載,並且業務模塊代碼很少,能夠忽略編譯時對性能的影響:

7ab38b0f-2d22-45b2-be5f-6a86d2e25665

若是是同構的模塊,須要在 node 端對 require.ensure 作 mock 處理,由於 nodejs 可不知道 require.ensure 是什麼!

if (typeof(require.ensure) !== 'function') {
    require.ensure = function (modules: Array<string>, callback: Function) {
        callback(require)
    }
}複製代碼

如今訪問 /home 這個 url,前端模塊會先加載基礎庫文件,再動態請求 Home 這個組件,獲取到組件後再執行其中代碼,渲染到頁面,但對於後端渲染,但願直接獲取到動態加載的組件,並根據組件設置頁面標題就變得困難,所以上面代碼中, callback(require) 將 require.ensure 在後端改成的同步加載,所以能夠直接獲取到組件中靜態成員變量,咱們能夠將例如頁面標題寫在頁面級組件的靜態成員變量中,例如:

export default class Home extends React.Component <Props, States> {
    public static title: string = 'auth by ascoders'
}複製代碼

在 node 端這樣處理:

// 找到最深層組件的 title
const title = renderProps.components[renderProps.components.length-1].title複製代碼

並將獲取到的 title 插入到模板的 title 中,讓頁面初始化時標題就是動態組件加載後就要設置的,並且更利於搜索引擎對頁面初始狀態的抓取,實現了前端對後端的控制反轉。

相比業務代碼,npm 生態的模塊比起來真是龐然大物,動輒 2000+ 細文件的引用,雖然開啓了增量 build,但文件的整合打包依然很是影響開發體驗,所以有必要在開發時忽略 npm 模塊。

webpack 的編譯優化

編譯優化的最終目的是將大型第三方模塊拆開,在編譯時直接跳過對其的編譯,並直接在頁面中引用編譯好的腳本,所以第一步須要將全部不順眼的模塊所有打包到 vendor.js 文件中:

// webpack 配置截取
entry: {
    react: ['react', 'react-dom', 'react-router'],
    fit: ['fit-input', 'fit-isomorphic-redux-tools']
},

output: {
    filename: '[name].dll.js',
    path    : path.join(process.cwd(), 'output/dll'),
    library : '[name]'
},

plugins: [
    new webpack.optimize.CommonsChunkPlugin('common.js'),
    new webpack.DllPlugin({
        path: path.join(process.cwd(), 'output/dll', '[name]-mainfest.json'),
        name: '[name]'
    })
]複製代碼

entry 定義了哪些文件須要抽出,output 中,library 定義了暴露在 window 的 namespace, plugins 注意將 name 設置爲與 library 相同,由於引用時參考的是這個名字。

咱們執行 webpack,執行結果以下:

b983a356-0415-4785-8191-e48d94a3ac71

產出了 dll 與 mainfest 兩種文件,dll 是打包後的文件,mainfest 是配置文件

c95659f8-7758-47b1-926e-724f19a71241

發現配置了兩個重要屬性,一個是暴露在 window 的 namespace ,另外一個是全部相對路徑引用的模塊名,webpack 打包後會轉化爲數字進行查找,防止路徑過長在 windows 下報錯。

下面開始配置開發配置 webpack.config.js:

plugins: [
    new webpack.DllReferencePlugin({
        context : path.join(__dirname, '../output/dll'),
        manifest: require(path.join(process.cwd(), 'output/dll/react-mainfest.json'))
    }),
    new webpack.DllReferencePlugin({
        scope   : 'fit',
        manifest: require(path.join(process.cwd(), 'output/dll/fit-mainfest.json'))
    }),
    new webpack.DllReferencePlugin({
        scope   : 'common',
        manifest: require(path.join(process.cwd(), 'output/dll/common-mainfest.json'))
    })
],複製代碼

執行結果只有很小的大小:

b3250046-4bf8-4730-820a-70f996357226

再將全部文件引用到頁面中,這樣初始化構建時先執行 dll 腳本,生成打包文件後再僅對當前項目打包&監聽,這就解決了開發時體驗問題。

可視化拖拽平臺組件

最後分享一下咱們的終極解決方案 fit-gaea,它是一個組件,是可視化拖拽平臺,安裝方式以下:

npm install fit-gaea複製代碼
import {Gaea, Preview} from 'fit-gaea'複製代碼

Gaea 是編輯器自己,它主要負責拖拽視圖,並生成對應 json 配置。 Preview 是部署組件,將 Gaea 生成的 json 配置傳入,能夠自動生成與拖拽編輯時如出一轍的頁面。

最大特點在於組件自定義,右側菜單欄羅列了可供拖拽的組件,咱們也能夠本身編寫 React 組件在 Gaea 初始化時傳入自定義組件,自由設置這個組件能夠編輯的字段,而且在組件中使用它。

對於粗粒度的運營招聘頁,甚至能夠將整個頁面做爲一個自定義組件傳入,由於每一個頁面很是雷同,只須要定義幾處文字修改便可,生成一個新頁面,只須要將自定義組件拖拽出來實例化,而且簡單修改本身字段便可。

同時 fit-gaea 也提供了不少細粒度的通用組件,例如 按鈕、段落、輸入框、佈局組件 等等,咱們也能夠本身編寫一些細粒度組件,經過任意嵌套組合的方式,生成更加複雜的組合,平臺也支持將任意組合成組,打成一個組件保存在工具欄,咱們能夠經過嵌套組合的方式生成新的組件。

這個平臺本質就是一個組件,業務線不須要花費大量精力重複編寫很是複雜的拖拽平臺,只須要將精力關注在編寫與業務緊密結合的定製組件,再傳入 fit-gaea,就可讓寫的組件變得能夠拖拽編輯。

fit-gaea api 文檔地址:fit.baidu.com/components/…
fit-gaea demo 體驗地址: fit.baidu.com/designer

總結

分享進入了尾聲,對以上經驗作一個總結。經過對組件庫靈活分散的管理,同時透傳暴露更多 api 提升組件可用性,提供從組件,到同構方案,最後到開發體驗優化與打包性能優化,能夠說提供了一套完整的開發方案。

同時經過 React-Native 方案提升三端開發的效率,開發出 web、native 通用的組件,經過 fit-gaea 可視化編輯組件的支持,讓編輯器生成橫跨三端的頁面,而且不受發版、前端人力資源限制,運營&產品均可以快速建立任何定製化頁面。

最後,Fit 組件咱們一直在努力維護中,我也但願將編寫組件的經驗分享給更多人,讓更多人蔘與到構建組件生態的隊伍中,願組件社區這棵大樹枝繁葉茂。

相關文章
相關標籤/搜索