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 组件咱们一直在努力维护中,我也但愿将编写组件的经验分享给更多人,让更多人参与到构建组件生态的队伍中,愿组件社区这棵大树枝繁叶茂。

相关文章
相关标签/搜索