咱們先來回顧一下 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
準備搭建組件庫之初,這估計是你們第一個會考慮到的問題:到底把組件庫的代碼放在一塊兒,仍是分散在各個倉庫?ios
調查發現 Antd 是將全部組件都寫入一個項目中,這樣方便組件統一管理,開發時不須要在多個倉庫之間切換,並且預覽效果只需運行跟項目,而不是爲每一個組件開啓一個端口進行預覽。其依賴的 react-components 組件庫中的組件以 rc 開頭,不過這個項目沒有進行集中管理。git
Material-UI、 React-UI 採用集中式管理等等。
可是集中管理有一些弊端。
分散維護的弊端更明顯,沒法在同一個項目中觀察全局,修改組件後引起的連帶風險沒法觀察,組件之間引用須要發佈或者 mock,不直觀,甚至組件之間的版本關聯、依賴分析都無法有效進行管理。
所以 Fit 組件庫在設計時,也經歷了一番醞釀,最後採用了二者結合的方案,分散部署+集中維護的折中方式,並且竟能結合了二者各自的優勢:
組件的依賴版本號須要統一,好比 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 中,核心代碼以下:
先收集每一個組件中的依賴, 若是在根目錄的 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…
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 後獲得的調試效果:
文件結構
DOM結構對應 className
(cloud.githubusercontent.com/assets/7970…)
直接從 dom 結構就能順藤摸瓜找到文件,上線時再將路徑 md5 處理。
這個插件會自動對當前目錄下的 scss或less 文件包一層目錄名,在 jsx
中,使用 className="_namespace"
,html-path-loader 會自動將 _namespace 替換爲與 css 一致的目錄名稱。
既然前端模塊化向後端看齊,強類型也成爲了無可阻擋的將來趨勢,咱們須要讓開發出的組件原生支持 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
這個模塊,所以引用了它。
webpack 提供了以下 api 拓展 require 行爲:
通常來講,咱們都在配置文件設置了對 js 文件的 loader,若是想引用源碼,正好能夠用 !! 打頭把全部 loaders 都幹掉,而後直接用 text-loader 引用,這樣咱們就獲得了一份純源碼以供展現。
defaultValue 屬性用於設置組件初始值,以後組件內部觸發的值的改變,不會受到這個屬性的影響,當父級組件觸發 render 後,組件的值應當從新被賦予 defaultValue。
value 是受控屬性,也用來設置值,但除了能夠設置初始值(優先級比 defaultValue 高)以外,還應知足只要設置了 value,組件內部就沒法修改狀態的要求,這個組件的狀態只能由父級授予並控制,因此叫受控屬性。
value 與 defaultValue 不該該同時存在,最好作一下檢查。
React 的宗旨是但願經過修改狀態來修改渲染內容,儘可能不要在 render 函數中編寫過多的業務邏輯和判斷語句,最好將能抽離成狀態的放在 state
中,在 componentWillReceiveProps
中改變它
若是你也使用 ES6 寫法,那麼最好注意使用 auto-bind 插件,將全部成員函數自動綁定 this,不然 .bind(this)
會返回一個新的函數,一來損耗性能,二來很是影響子組件的 shouldComponentUpdate
判斷!
對於同構模塊,React 組件的生命週期 componentWillMount
會在 node 環境中執行,而 componentDidMount
不會。
要避免在 willMount 中操做瀏覽器的 api,也要避免將無關緊要的邏輯寫在其中,致使後端服務器渲染吃力(目前 React 渲染是同步的),無關初始化邏輯應當放在 didMount 中,由客戶端均攤計算壓力。對於影響到頁面渲染的邏輯仍是要放在 willMount 中,否則後端渲染就沒有意義。
React 組件生命週期中 shouldComponentUpdate
方法是控制組件狀態改變時是否要觸發渲染的,但當同級組件量很是龐大時,即使在每一個組件作是否渲染的判斷都會花費幾百毫秒,這時咱們就要選擇更好的優化方式了。
新的優化方式仍是基於 shouldComponentUpdate
,只不過判斷條件很是苛刻,咱們設定爲只有 state 發生變化纔會觸發 render,其它任何狀況都不會觸發。這種方式排除了對複雜 props 條件的判斷,當 props 結構很是複雜時,對沒有使用 immutable 的代碼簡直是一場災難,咱們如今徹底忽略 props 的影響,組件變成爲了完徹底全封閉的王國,不會遵從任何人的指揮。
當咱們實在須要更新它時,全部的 props 都不起做用,可是能夠經過 key
的改變來繞過 shouldComponentUpdate
進行強制刷新,這樣組件的一舉一動徹底被咱們控制在手,最大化提高了渲染效率。
組件級 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
,這樣他們即使隔着千山萬水,也能暢快無阻的通訊了。
webpack&fis 最核心的功能能夠說就是對 npm 生態的支持了,社區是編譯工具的衣食父母,支持了生態纔會有將來。
爲了解決業務線可能遇到的各類 npm 環境問題,咱們要有刨根問底的精神,瞭解 npm 包加載原理。下面會一步一步介紹一個 npm 模塊是如何被解析加載的。
不管是 webpack、fis,仍是其它構建工具,都有文件查找的鉤子,當解析了相似 import '../index.js'
時,會優先查找相對路徑,但解析到了 import 'react'
便無從下手,由於這時構建工具還不知道這種模塊應該從哪查找,咱們就從這裏開始截斷,當出現沒法找到的模塊時,就優先從 node_modules 文件夾下進行查找(node_modules 下查找模塊放到後面講)。
因爲 npm 模塊打平&嵌套兩種方案可能並存,每次都遞歸查找的效率過低,所以咱們首先會把 node_modules 下全部模塊緩存起來,這裏分爲兩種方案:
將全部模塊存到 map 後,咱們直接就能 get
到想要的模塊,可是要注意版本問題:若是這個模塊是打平安裝的,那毫無疑問不會存在同模塊多版本號問題,npm@3.x 後即使是打平安裝,但遇到依賴模塊已經在根目錄存在,但版本號不一致,仍是會採用嵌套方式,而 npm@2.x 不管如何都會用嵌套的方式。
所以咱們的目的就明確了,不用區分 npm 的版本,若是這個當前文件位於非 node_modules 文件夾中,直接從根目錄引用它須要的模塊,若是這個當前位於 node_modules 中,優先從當前文件夾中的 node_modules 獲取,若是當前文件夾的 node_modules 不存在依賴文件,就從根目錄取。
找到了依賴在 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_ENV
爲 production
時會自動幹掉,若是咱們不對這種狀況作處理,上線後沒法達到模塊的最佳性能(甚至報錯,由於 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 請求。如今有兩種比較理想的方案:
這種方案依賴同構的請求庫,例如 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 併發請求至關友好。
使用了 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)
})
}複製代碼
這樣遍作到了對業務模塊的按需加載,並且業務模塊代碼很少,能夠忽略編譯時對性能的影響:
若是是同構的模塊,須要在 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 模塊。
編譯優化的最終目的是將大型第三方模塊拆開,在編譯時直接跳過對其的編譯,並直接在頁面中引用編譯好的腳本,所以第一步須要將全部不順眼的模塊所有打包到 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,執行結果以下:
產出了 dll 與 mainfest 兩種文件,dll 是打包後的文件,mainfest 是配置文件
發現配置了兩個重要屬性,一個是暴露在 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'))
})
],複製代碼
執行結果只有很小的大小:
再將全部文件引用到頁面中,這樣初始化構建時先執行 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 組件咱們一直在努力維護中,我也但願將編寫組件的經驗分享給更多人,讓更多人蔘與到構建組件生態的隊伍中,願組件社區這棵大樹枝繁葉茂。