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