本文内容为学习http://www.alloyteam.com/2015/11/we-will-be-componentized-web-long-text/ 博文的总结
模块化的不足
为了提高项目开发效率和便于后期维护,我们倾向于使用模块化的方式来互相协作,现在大部分稍微大型一点的项目,都会使用requirejs或者seajs来实现JS的模块化,多人分工合作开发,各自定义自己模块的依赖和暴露接口,这些模块-----通过独立拆分且通用的代码单元,组成一个个的页面。
传统模块化代码示例如下:
以上是具体某个页面的主js,已经封装了功能模块,但页面的主逻辑依旧是”面向过程“的代码结构,即根据页面的渲染过程来编写代码结构。像init -> getData -> processData -> bindevent -> report -> xxx 方法之间线性跳转。面向过过程的代码随着页面逻辑越来越复杂,这条”过程线“也会越来越长,并且越来越绕。加之缺少规范约束,其他项目成员根据各自需要,在”过程线“加插各自逻辑,最终这个页面的逻辑变得难以维护。
require([
'Tmpl!../tmpl/list.html','lib/qqapi','module/position','module/refresh','module/page','module/net'
], function(listTmpl, QQapi, Position, Refresh, Page, NET){
var foo = '',
bar = [];
QQapi.report();
Position.getLocaiton(function(data){
//...
});
var init = function(){
bind();
NET.get('/cgi-bin/xxx/xxx',function(data){
renderA(data.banner);
renderB(data.list);
});
};
var processData = function(){
};
var bind = function(){
};
var renderA = function(){
};
var renderB = function(data){
listTmpl.render('#listContent',processData(data));
};
var refresh = function(){
Page.refresh();
};
// app start
init();
});
以上是具体某个页面的主js,已经封装了功能模块,但页面的主逻辑依旧是”面向过程“的代码结构,即根据页面的渲染过程来编写代码结构。像init -> getData -> processData -> bindevent -> report -> xxx 方法之间线性跳转。面向过过程的代码随着页面逻辑越来越复杂,这条”过程线“也会越来越长,并且越来越绕。加之缺少规范约束,其他项目成员根据各自需要,在”过程线“加插各自逻辑,最终这个页面的逻辑变得难以维护。
页面结构模块化
面对传统模块的面向过程问题,很多公司给出了解决方案,以腾讯为例:
假设页面划分为tabContainer,listContainer和imgsContainer三个模块,则上文介绍的传统模块化是“将所有模块糅合在一起的面向过程的代码”变为“各模块分别顺序执行自己流程的多个小面向过程代码”,如下图所示:
代码示例:
require([
'Tmpl!../tmpl/list.html','Tmpl!../tmpl/imgs.html','lib/qqapi','module/refresh','module/page'
], function(listTmpl, imgsTmpl, QQapi, Refresh, Page ){
var tabContainer = new RenderModel({
renderContainer: '#tabWrap',
data: {},
renderTmpl: "<li soda-repeat='item in data.tabs'>{{item}}</li>",
event: function(){
// tab's event
}
});
var listContainer = new ScrollModel({
scrollEl: $.os.ios ? $('#Page') : window,
renderContainer: '#listWrap',
renderTmpl: listTmpl,
cgiName: '/cgi-bin/index-list?num=1',
processData: function(data) {
//...
},
event: function(){
// listElement's event
},
error: function(data) {
Page.show('数据返回异常[' + data.retcode + ']');
}
});
var imgsContainer = new renderModel({
renderContainer: '#imgsWrap',
renderTmpl: listTmpl,
cgiName: '/cgi-bin/getPics',
processData: function(data) {
//...
},
event: function(){
// imgsElement's event
},
complete: function(data) {
QQapi.report();
}
});
var page = new PageModel();
page.add([tabContainer,listContainer,imgsContainer]);
page.rock();
});
这样我们就我们把这些常用的请求,对返回数据的处理,事件绑定,上报,容错处理等一系列逻辑方法,以页面块为单位封装成一个Model模块。我们可以清晰地看到该页面块,请求的CGI是什么,绑定了什么事件,做了什么上报,出错怎么处理。新增的代码就应该放置在相应的模块上相应的状态方法(preload,process,event,complete…),杜绝了以往的无规则乱增代码的行文。
现在基于Model的页面结构开发,已经带有一点”组件化“的味道
WebComponents 标准
现阶段的“组件”基本上只能达到是某个功能单元上的集合。他的资源都是松散地分散在三种资源文件中,而且组件作用域暴露在全局作用域下,很容易与其他组件产生冲突,如最简单的css命名冲突。对于这种“组件”,还不如最开始介绍的页面结构模块化。于是W3C按耐不住了,制定一个WebComponents标准,为组件化的未来指引了明路。
WebComponents标准有一下四点:
<template>模板能力
<template id="datapcikerTmpl">
<div>我是原生的模板</div>
</template>
这样,我们想要使用该模板时候,就可以通过
innerHTML= document.querySelector('#datapcikerTmpl').content;
来调用模板
ShadowDom 封装组件独立的内部结构
var wrap = document.querySelector('#wrap');
var shadow = wrap.createShadowRoot();
shadow.innerHTML = '<p>you can not see me </p>'
ShadowDom可以理解为一份有独立作用域的html片段。在具体dom节点上使用createShadowRoot方法即可生成其ShadowDom。就像在整份Html的屋子里面,新建了一个shadow的房间。房间外的人都不知道房间内有什么,保持shadowDom的独立性。
自定义原生标签
imports解决组件间的依赖
<link rel="import" href="datapciker.html">
即html导入功能
总结来说WebCompoents有以下四部分功能:
- <template>定义组件的HTML模板能力
- Shadow Dom封装组件的内部结构,并且保持其独立性
- Custom Element 对外提供组件的标签,实现自定义标签
- import解决组件结合和依赖加载
组件化实践方案
我们总结一下一份真正成熟可靠的组件化方案,需要具备的能力:
- “资源高内聚”—— 组件资源由自身加载控制
- “作用域独立”—— 内部结构密封,不与全局或其他组件产生影响
- “自定义标签”
- “可相互组合”—— 组件正在强大的地方,组件间组装整合
- “接口规范化”—— 组件接口有统一规范,或者是生命周期的管理、
现阶段WebComponent的支持度还不成熟,不能作为方案的手段。而以高性能虚拟Dom为切入点的组件框架React,在facebook的造势下,社区得到了大力发展。另外一名主角Webpack,负责解决组件资源内聚,同时跟React极度切合形成互补。
所以【Webpack】+【React】将会是这套方案的核心技术。
组件生命周期
React天生就是强制性组件化的,所以可以从根本性上解决面向过程代码所带来的麻烦。React组件自身有生命周期方法,能够满足“接口规范化”能力点。另外react的jsx自带模板功能,把html页面片直接写在render方法内,组件内聚性更加紧密。
react的生命周期有以下三个状态:
- Mount: 插入Dom
- Update: 更新Dom
- Unmount: 拔出Dom
组件状态就是: 插入-> 更新 ->拔出。
然后每个状态会有一至两个处理函数,will函数和did函数。
- componentWillMount() 准备插入前
- componentDidlMount() 插入后
- componentWillUpdate() 准备更新前
- componentDidUpdate() 更新后
- componentWillUnmount() 准备拔出前
另外React另外一个核心:数据模型props和state,对应着也有自个状态方法
- getInitialState() 获取初始化state。
- getDefaultProps() 获取默认props。对于那些没有父组件传递的props,通过该方法设置默认的props
- componentWillReceiveProps() 已插入的组件收到新的props时调用
- 还有一个特殊状态的处理函数,用于优化处理
- shouldComponentUpdate():判断组件是否需要update调用
上文我们提到的面向过程代码,在react中,通过生命周期变成了下图所示
组件的状态方法流中,有两点需要特殊说明:
二次渲染:
由于React的虚拟Dom特性,组件的render函数不需自己触发,根据props和state的改变自个通过差异算法,得出最优的渲染。
请求CGI一般都是异步,所以数据被取回后,必定会通过props和state的改变带来二次渲染。只是空数据渲染的时候,有可能会被React优化掉。当数据回来,通过setState,触发二次render
请求CGI一般都是异步,所以数据被取回后,必定会通过props和state的改变带来二次渲染。只是空数据渲染的时候,有可能会被React优化掉。当数据回来,通过setState,触发二次render
componentWiillMount与componentDidMount的差别
ajax请求建议在WillMount的方法内执行,而不是组件初始化成功之后的DidMount。这样能在“空数据渲染”阶段之前请求数据,尽早地减少二次渲染的时间。
willMount只会执行一次,非常适合做init的事情。
didMount也只会执行一次,并且这时候真实的Dom已经形成,非常适合事件绑定和complete类的逻辑
willMount只会执行一次,非常适合做init的事情。
didMount也只会执行一次,并且这时候真实的Dom已经形成,非常适合事件绑定和complete类的逻辑
JSX很丑,但是组件内聚的关键!
虽然jsx很丑,但是也为我们提供了很多组件化独有的遍历
1.之前的“逻辑视图分离”模式,我们需要去找相应的js文件,相应的event函数体内,找到td-info的class所绑定的事件。
而JSX的高度内聚,所有事件逻辑就是在本身jsx文件内,绑定的就是自身的showInfo方法。组件化的特性能立马体现出来。
2.或许JSX内部有负责繁琐的逻辑样式,可JSX的自定义标签能力,组件的黑盒性立马能体验出来
而JSX的高度内聚,所有事件逻辑就是在本身jsx文件内,绑定的就是自身的showInfo方法。组件化的特性能立马体现出来。
<p className="td-info" onClick={this.showInfo}>{obj.info}</p>
2.或许JSX内部有负责繁琐的逻辑样式,可JSX的自定义标签能力,组件的黑盒性立马能体验出来
render: function(){
return (
<div>
<Menus bannerNums={this.state.list.length}></Menus>
<TableList data={this.state.list}></TableList>
</div>
);
}
现在让我们看看在react帮助下,我们组件化能力点的完成情况
“ 资源高内聚”—— (33%) html与js内聚
“ 作用域独立”—— (50%) js的作用域独立
“ 自定义标签”—— (100%)jsx
“ 可相互组合”—— (50%) 可组合,但缺乏有效的加载方式
“ 接口规范化”—— (100%)组件生命周期方法
Webpack 资源组件化
根据webpack的设计理念,所有资源都是“模块”,webpack内部实现了一套资源加载机制,可以把css,图片等资源等有依赖关系的“模块”加载,不只是加载js模块。这跟requirejs大大不同。这套加载机制,通过一个个loader来实现。
组件一体输出
// 加载组件自身css
require('./slider.css');
// 加载组件依赖的模块
var React = require('react');
var Clip = require('../ui/clipitem.jsx');
// 加载图片资源
var spinnerImg = require('./loading.png');
var Slider = React.createClass({
getInitialState: function() {
// ...
},
componentDidMount: function(){
// ...
},
render: function() {
return (
<div>
<Clip data={this.props.imgs} />
<img className="loading" src={spinnerImg} />
</div>
);
}
});
module.exports = Slider;
如果说,react使到html和js合为一体。那么加上webpack,两者结合一起的话。js,css,png(base64),html 所有web资源都能合成一个JS文件。这正是这套方案的核心所在: 组件独立一体化。如果要引用一个组件,仅仅require('./slider.js') 即可完成。
加入webpack的模块加载器之后,我们组件的加载问题,内聚问题也都成功地解决掉
“资源高内聚”—— (100%) 所有资源可以一js输出
“可相互组合”—— (100%) 可组合可依赖加载
“可相互组合”—— (100%) 可组合可依赖加载