前端组件化开发实践,致我们终将组件化的Web

前端组件化开发实践

2015/07/12 · CSS, HTML5, JavaScript · 组件化

原文出处: 美团技术博客 - spring   

致我们终将组件化的Web

2015/11/25 · HTML5 · 1 评论 · 组件化

原文出处: AlloyTeam   

这篇文章将从两年前的一次技术争论开始。争论的聚焦就是下图的两个目录分层结构。我说按模块划分好,他说你傻逼啊,当然是按资源划分。

图片 1 《=》图片 2

”按模块划分“目录结构,把当前模块下的所有逻辑和资源都放一起了,这对于多人独自开发和维护个人模块不是很好吗?当然了,那争论的结果是我乖乖地改回主流的”按资源划分“的目录结构。因为,没有做到JS模块化和资源模块化,仅仅物理位置上的模块划分是没有意义的,只会增加构建的成本而已。

虽然他说得好有道理我无言以对,但是我心不甘,等待他日前端组件化成熟了,再来一战!

而今天就是我重申正义的日子!只是当年那个跟你撕逼的人不在。

模块化的不足

模块一般指能够独立拆分且通用的代码单元。由于JavaScript语言本身没有内置的模块机制(ES6有了!!),我们一般会使用CMD或ADM建立起模块机制。现在大部分稍微大型一点的项目,都会使用requirejs或者seajs来实现JS的模块化。多人分工合作开发,其各自定义依赖和暴露接口,维护功能模块间独立性,对于项目的开发效率和项目后期扩展和维护,都是是有很大的帮助作用。

但,麻烦大家稍微略读一下下面的代码

JavaScript

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(); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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,已经封装了像Position,NET,Refresh等功能模块,但页面的主逻辑依旧是”面向过程“的代码结构。所谓面向过程,是指根据页面的渲染过程来编写代码结构。像:init -> getData -> processData -> bindevent -> report -> xxx 。 方法之间线性跳转,你大概也能感受这样代码弊端。随着页面逻辑越来越复杂,这条”过程线“也会越来越长,并且越来越绕。加之缺少规范约束,其他项目成员根据各自需要,在”过程线“加插各自逻辑,最终这个页面的逻辑变得难以维护。

图片 3

开发需要小心翼翼,生怕影响“过程线”后面正常逻辑。并且每一次加插或修改都是bug泛滥,无不令产品相关人员个个提心吊胆。

 页面结构模块化

基于上面的面向过程的问题,行业内也有不少解决方案,而我们团队也总结出一套成熟的解决方案:Abstractjs,页面结构模块化。我们可以把我们的页面想象为一个乐高机器人,需要不同零件组装,如下图,假设页面划分为tabContainer,listContainer和imgsContainer三个模块。最终把这些模块add到最终的pageModel里面,最终使用rock方法让页面启动起来。

图片 4
(原过程线示例图)

图片 5
(页面结构化示例图)

下面是伪代码的实现

JavaScript

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(); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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();
 
});

我们把这些常用的请求CGI,处理数据,事件绑定,上报,容错处理等一系列逻辑方法,以页面块为单位封装成一个Model模块。

这样的一个抽象层Model,我们可以清晰地看到该页面块,请求的CGI是什么,绑定了什么事件,做了什么上报,出错怎么处理。新增的代码就应该放置在相应的模块上相应的状态方法(preload,process,event,complete…),杜绝了以往的无规则乱增代码的行文。并且,根据不同业务逻辑封装不同类型的Model,如列表滚动的ScrollModel,滑块功能的SliderModel等等,可以进行高度封装,集中优化。

现在基于Model的页面结构开发,已经带有一点”组件化“的味道。每个Model都带有各自的数据,模板,逻辑。已经算是一个完整的功能单元。但距离真正的WebComponent还是有一段距离,至少满足不了我的”理想目录结构“。

 WebComponents 标准

我们回顾一下使用一个datapicker的jquery的插件,所需要的步奏:

  1. 引入插件js

  2. 引入插件所需的css(如果有)

  3. copy 组件的所需的html片段

  4. 添加代码触发组件启动

现阶段的“组件”基本上只能达到是某个功能单元上的集合。他的资源都是松散地分散在三种资源文件中,而且组件作用域暴露在全局作用域下,缺乏内聚性很容易就会跟其他组件产生冲突,如最简单的css命名冲突。对于这种“组件”,还不如上面的页面结构模块化。

于是W3C按耐不住了,制定一个WebComponents标准,为组件化的未来指引了明路。

下面以较为简洁的方式介绍这份标准,力求大家能够快速了解实现组件化的内容。(对这部分了解的同学,可以跳过这一小节)

1. <template>模板能力

模板这东西大家最熟悉不过了,前些年见的较多的模板性能大战artTemplate,juicer,tmpl,underscoretemplate等等。而现在又有mustachejs无逻辑模板引擎等新入选手。可是大家有没有想过,这么基础的能力,原生HTML5是不支持的(T_T)。

而今天WebComponent将要提供原生的模板能力

XHTML

<template id="datapcikerTmpl"> <div>我是原生的模板</div> </template>

1
2
3
<template id="datapcikerTmpl">
<div>我是原生的模板</div>
</template>

template标签内定义了myTmpl的模板,需要使用的时候就要innerHTML= document.querySelector('#myTmpl').content;可以看出这个原生的模板够原始,模板占位符等功能都没有,对于动态数据渲染模板能力只能自力更新。

2. ShadowDom 封装组件独立的内部结构

ShadowDom可以理解为一份有独立作用域的html片段。这些html片段的CSS环境和主文档隔离的,各自保持内部的独立性。也正是ShadowDom的独立特性,使得组件化成为了可能。

JavaScript

var wrap = document.querySelector('#wrap'); var shadow = wrap.createShadowRoot(); shadow.innerHTML = '<p>you can not see me </p>'

1
2
3
var wrap = document.querySelector('#wrap');
var shadow = wrap.createShadowRoot();
shadow.innerHTML = '<p>you can not see me </p>'

在具体dom节点上使用createShadowRoot方法即可生成其ShadowDom。就像在整份Html的屋子里面,新建了一个shadow的房间。房间外的人都不知道房间内有什么,保持shadowDom的独立性。

3. 自定义原生标签

初次接触Angularjs的directive指令功能,设定好组件的逻辑后,一个<Datepicker />就能引入整个组件。如此狂炫酷炸碉堡天的功能,实在令人拍手称快,跃地三尺。

JavaScript

var tmpl = document.querySelector('#datapickerTmpl'); var datapickerProto = Object.create(HTMLElement.prototype); // 设置把我们模板内容我们的shadowDom datapickerProto.createdCallback = function() { var root = this.createShadowRoot(); root.appendChild(document.importNode(tmpl.content, true)); }; var datapicker = docuemnt.registerElement('datapicker',{ prototype: datapickerProto });

1
2
3
4
5
6
7
8
9
10
11
12
var tmpl = document.querySelector('#datapickerTmpl');
var datapickerProto = Object.create(HTMLElement.prototype);
 
// 设置把我们模板内容我们的shadowDom
datapickerProto.createdCallback = function() {
    var root = this.createShadowRoot();
    root.appendChild(document.importNode(tmpl.content, true));
};
 
var datapicker = docuemnt.registerElement('datapicker',{
    prototype: datapickerProto
});

Object.create方式继承HTMLElement.prototype,得到一个新的prototype。当解析器发现我们在文档中标记它将检查是否一个名为createdCallback的方法。如果找到这个方法它将立即运行它,所以我们把克隆模板的内容来创建的ShadowDom。

最后,registerElement的方法传递我们的prototype来注册自定义标签。

上面的代码开始略显复杂了,把前面两个能力“模板”“shadowDom”结合,形成组件的内部逻辑。最后通过registerElement的方式注册组件。之后可以愉快地<datapicker></datapicker>的使用。

4. imports解决组件间的依赖

XHTML

<link rel="import" href="datapciker.html">

1
<link rel="import" href="datapciker.html">

这个类php最常用的html导入功能,HTML原生也能支持了。

WebComponents标准内容大概到这里,是的,我这里没有什么Demo,也没有实践经验分享。由于webComponents新特性,基本上除了高版本的Chrome支持外,其他浏览器的支持度甚少。虽然有polymer帮忙推动webcompoents的库存在,但是polymer自身的要求版本也是非常高(IE10 )。所以今天的主角并不是他。

我们简单来回顾一下WebCompoents的四部分功能:

1 .<template>定义组件的HTML模板能力

  1. Shadow Dom封装组件的内部结构,并且保持其独立性

  2. Custom Element 对外提供组件的标签,实现自定义标签

  3. import解决组件结合和依赖加载

 组件化实践方案

官方的标准看完了,我们思考一下。一份真正成熟可靠的组件化方案,需要具备的能力。

“资源高内聚”—— 组件资源内部高内聚,组件资源由自身加载控制

“作用域独立”—— 内部结构密封,不与全局或其他组件产生影响

“自定义标签”—— 定义组件的使用方式

“可相互组合”—— 组件正在强大的地方,组件间组装整合

“接口规范化”—— 组件接口有统一规范,或者是生命周期的管理

个人认为,模板能力是基础能力,跟是否组件化没有强联系,所以没有提出一个大点。

既然是实践,现阶段WebComponent的支持度还不成熟,不能作为方案的手段。而另外一套以高性能虚拟Dom为切入点的组件框架React,在facebook的造势下,社区得到了大力发展。另外一名主角Webpack,负责解决组件资源内聚,同时跟React极度切合形成互补。

所以【Webpack】 【React】将会是这套方案的核心技术。

不知道你现在是“又是react webpack”感到失望图片 6,还是“太好了是react webpack”不用再学一次新框架的高兴图片 7。无论如何下面的内容不会让你失望的。

一,组件生命周期

图片 8

React天生就是强制性组件化的,所以可以从根本性上解决面向过程代码所带来的麻烦。React组件自身有生命周期方法,能够满足“接口规范化”能力点。并且跟“页面结构模块化”的所封装抽离的几个方法能一一对应。另外react的jsx自带模板功能,把html页面片直接写在render方法内,组件内聚性更加紧密。

由于React编写的JSX是会先生成虚拟Dom的,需要时机才真正插入到Dom树。使用React必须要清楚组件的生命周期,其生命周期三个状态:

Mount: 插入Dom

Update: 更新Dom

Unmount: 拔出Dom

mount这单词翻译增加,嵌入等。我倒是建议“插入”更好理解。插入!拔出!插入!拔出!默念三次,懂了没?别少看黄段子的力量,

图片 9

组件状态就是: 插入-> 更新 ->拔出。

然后每个组件状态会有两种处理函数,一前一后,will函数和did函数。

componentWillMount()  准备插入前

componentDidlMount()  插入后

componentWillUpdate() 准备更新前

componentDidUpdate()  更新后

componentWillUnmount() 准备拔出前

因为拔出后基本都是贤者形态(我说的是组件),所以没有DidUnmount这个方法。

另外React另外一个核心:数据模型props和state,对应着也有自个状态方法

getInitialState()     获取初始化state。

getDefaultProps() 获取默认props。对于那些没有父组件传递的props,通过该方法设置默认的props

componentWillReceiveProps()  已插入的组件收到新的props时调用

还有一个特殊状态的处理函数,用于优化处理

shouldComponentUpdate():判断组件是否需要update调用

加上最重要的render方法,React自身带的方法刚刚好10个。对于初学者来说是比较难以消化。但其实getInitialStatecomponentDidMountrender三个状态方法都能完成大部分组件,不必望而却步。

回到组件化的主题。

一个页面结构模块化的组件,能独立封装整个组件的过程线

图片 10

我们换算成React生命周期方法:

图片 11

 

组件的状态方法流中,有两点需要特殊说明:

1,二次渲染:

由于React的虚拟Dom特性,组件的render函数不需自己触发,根据props和state的改变自个通过差异算法,得出最优的渲染。

请求CGI一般都是异步,所以必定带来二次渲染。只是空数据渲染的时候,有可能会被React优化掉。当数据回来,通过setState,触发二次render

 

2,componentWiillMount与componentDidMount的差别

和大多数React的教程文章不一样,ajax请求我建议在WillMount的方法内执行,而不是组件初始化成功之后的DidMount。这样能在“空数据渲染”阶段之前请求数据,尽早地减少二次渲染的时间。

willMount只会执行一次,非常适合做init的事情。

didMount也只会执行一次,并且这时候真实的Dom已经形成,非常适合事件绑定和complete类的逻辑。

 

 二,JSX很丑,但是组件内聚的关键!

WebComponents的标准之一,需要模板能力。本是以为是我们熟悉的模板能力,但React中的JSX这样的怪胎还是令人议论纷纷。React还没有火起来的时候,大家就已经在微博上狠狠地吐槽了“JSX写的代码这TM的丑”。这其实只是Demo阶段JSX,等到实战的大型项目中的JSX,包含多状态多数据多事件的时候,你会发现………….JSX写的代码还是很丑。

图片 12
(即使用sublime-babel等插件高亮,逻辑和渲染耦合一起,阅读性还是略差)

为什么我们会觉得丑?因为我们早已经对“视图-样式-逻辑”分离的做法潜移默化。

基于维护性和可读性,甚至性能,我们都不建议直接在Dom上面绑定事件或者直接写style属性。我们会在JS写事件代理,在CSS上写上classname,html上的就是清晰的Dom结构。我们很好地维护着MVC的设计模式,一切安好。直到JSX把他们都糅合在一起,所守护的技术栈受到侵略,难免有所抵制。

 

但是从组件化的目的来看,这种高内聚的做法未尝不可。

下面的代码,之前的“逻辑视图分离”模式,我们需要去找相应的js文件,相应的event函数体内,找到td-info的class所绑定的事件。

对比起JSX的高度内聚,所有事件逻辑就是在本身jsx文件内,绑定的就是自身的showInfo方法。组件化的特性能立马体现出来。

(注意:虽然写法上我们好像是HTML的内联事件处理器,但是在React底层并没有实际赋值类似onClick属性,内层还是使用类似事件代理的方式,高效地维护着事件处理器)

再来看一段style的jsx。其实jsx没有对样式有硬性规定,我们完全可遵循之前的定义class的逻辑。任何一段样式都应该用class来定义。在jsx你也完全可以这样做。但是出于组件的独立性,我建议一些只有“一次性”的样式直接使用style赋值更好。减少冗余的class。

XHTML

<div className="list" style={{background: "#ddd"}}> {list_html} </div>

1
2
3
<div className="list" style={{background: "#ddd"}}>
   {list_html}
</div>

或许JSX内部有负责繁琐的逻辑样式,可JSX的自定义标签能力,组件的黑盒性立马能体验出来,是不是瞬间美好了很多。

JavaScript

render: function(){ return ( <div> <Menus bannerNums={this.state.list.length}></Menus> <TableList data={this.state.list}></TableList> </div> ); }

1
2
3
4
5
6
7
8
render: function(){
    return (
      <div>
         <Menus bannerNums={this.state.list.length}></Menus>
         <TableList data={this.state.list}></TableList>
      </div>
   );
}

虽然JSX本质上是为了虚拟Dom而准备的,但这种逻辑和视图高度合一对于组件化未尝不是一件好事。

 

学习完React这个组件化框架后,看看组件化能力点的完成情况

“资源高内聚”—— (33%)  html与js内聚

“作用域独立”—— (50%)  js的作用域独立

“自定义标签”—— (100%)jsx

“可相互组合”—— (50%)  可组合,但缺乏有效的加载方式

“接口规范化”—— (100%)组件生命周期方法

 

Webpack 资源组件化

对于组件化的资源独立性,一般的模块加载工具和构建流程视乎变得吃力。组件化的构建工程化,不再是之前我们常见的,css合二,js合三,而是体验在组件间的依赖于加载关系。webpack正好符合需求点,一方面填补组件化能力点,另一方帮助我们完善组件化的整体构建环境。

首先要申明一点是,webpack是一个模块加载打包工具,用于管理你的模块资源依赖打包问题。这跟我们熟悉的requirejs模块加载工具,和grunt/gulp构建工具的概念,多多少少有些出入又有些雷同。

图片 13

首先webpak对于CommonJS与AMD同时支持,满足我们模块/组件的加载方式。

JavaScript

require("module"); require("../file.js"); exports.doStuff = function() {}; module.exports = someValue;

1
2
3
4
require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;

JavaScript

define("mymodule", ["dep1", "dep2"], function(d1, d2) { return someExportedValue; });

1
2
3
define("mymodule", ["dep1", "dep2"], function(d1, d2) {
    return someExportedValue;
});

当然最强大的,最突出的,当然是模块打包功能。这正是这一功能,补充了组件化资源依赖,以及整体工程化的能力

根据webpack的设计理念,所有资源都是“模块”,webpack内部实现了一套资源加载机制,可以把想css,图片等资源等有依赖关系的“模块”加载。这跟我们使用requirejs这种仅仅处理js大大不同。而这套加载机制,通过一个个loader来实现。

 

JavaScript

// webpack.config.js module.exports = { entry: { entry: './index.jsx', }, output: { path: __dirname, filename: '[name].min.js' }, module: { loaders: [ {test: /.css$/, loader: 'style!css' }, {test: /.(jsx|js)?$/, loader: 'jsx?harmony', exclude: /node_modules/}, {test: /.(png|jpg|jpeg)$/, loader: 'url-loader?limit=10240'} ] } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js
module.exports = {
    entry: {
     entry: './index.jsx',
    },
    output: {
        path: __dirname,
        filename: '[name].min.js'
    },
    module: {
        loaders: [
            {test: /.css$/, loader: 'style!css' },
            {test: /.(jsx|js)?$/, loader: 'jsx?harmony', exclude: /node_modules/},
            {test: /.(png|jpg|jpeg)$/, loader: 'url-loader?limit=10240'}
        ]
    }
};

上面一份简单的webpack配置文件,留意loaders的配置,数组内一个object配置为一种模块资源的加载机制。test的正则为匹配文件规则,loader的为匹配到文件将由什么加载器处理,多个处理器之间用分隔,处理顺序从右到左。

 

style!css,css文件通过css-loader(处理css),再到style-loader(inline到html)的加工处理流。

jsx文件通过jsx-loader编译,‘?’开启加载参数,harmony支持ES6的语法。

图片资源通过url-loader加载器,配置参数limit,控制少于10KB的图片将会base64化。

 资源文件如何被require?

JavaScript

// 加载组件自身css require('./slider.css'); // 加载组件依赖的模块 var Clip = require('./clipitem.js'); // 加载图片资源 var spinnerImg = require('./loading.png');

1
2
3
4
5
6
// 加载组件自身css
require('./slider.css');
// 加载组件依赖的模块
var Clip = require('./clipitem.js');
// 加载图片资源
var spinnerImg = require('./loading.png');

在webpack的js文件中我们除了require我们正常的js文件,css和png等静态文件也可以被require进来。我们通过webpack命令,编译之后,看看输出结果如何:

JavaScript

webpackJsonp([0], { /* 0 */ /***/ function(module, exports, __webpack_require__) { // 加载组件自身css __webpack_require__(1); // 加载组件依赖的模块 var Clip = __webpack_require__(5); // 加载图片资源 var spinnerImg = __webpack_require__(6); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { /***/ }, /* 2 */ /***/ function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(3)(); exports.push([module.id, ".slider-wrap{rn position: relative;rn width: 100%;rn margin: 50px;rn background: #fff;rn}rnrn.slider-wrap li{rn text-align: center;rn line-height: 20px;rn}", ""]); /***/ }, /* 3 */ /***/ function(module, exports) { /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) { /***/ }, /* 5 */ /***/ function(module, exports) { console.log('hello, here is clipitem.js') ; /***/ }, /* 6 */ /***/ function(module, exports) { module.exports = "data:image/png;base64,iVBORw0KGg......" /***/ } ]);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
webpackJsonp([0], {
/* 0 */
/***/ function(module, exports, __webpack_require__) {
          // 加载组件自身css
          __webpack_require__(1);
          // 加载组件依赖的模块
          var Clip = __webpack_require__(5);
          // 加载图片资源
          var spinnerImg = __webpack_require__(6);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
 
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
          exports = module.exports = __webpack_require__(3)();
          exports.push([module.id, ".slider-wrap{rn position: relative;rn width: 100%;rn margin: 50px;rn background: #fff;rn}rnrn.slider-wrap li{rn text-align: center;rn line-height: 20px;rn}", ""]);
 
/***/ },
/* 3 */
/***/ function(module, exports) {
 
/***/ },
 
/* 4 */
/***/ function(module, exports, __webpack_require__) {
/***/ },
 
/* 5 */
/***/ function(module, exports) {
          console.log('hello, here is clipitem.js') ;
/***/ },
/* 6 */
/***/ function(module, exports) {
          module.exports = "data:image/png;base64,iVBORw0KGg......"
/***/ }
]);

webpack编译之后,输出文件视乎乱糟糟的,但其实每一个资源都被封装在一个函数体内,并且以编号的形式标记(注释)。这些模块,由webpack的__webpack_require__内部方法加载。入口文件为编号0的函数index.js,可以看到__webpack_require__加载其他编号的模块。

css文件在编号1,由于使用css-loader和style-loader,编号1-4都是处理css。其中编号2我们可以看我们的css的string体。最终会以内联的方式插入到html中。

图片文件在编号6,可以看出exports出base64化的图片。

 组件一体输出

JavaScript

// 加载组件自身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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 加载组件自身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%)  可组合可依赖加载

 

 CSS模块化实践

很高兴,你能阅读到这里。目前我们的组件完成度非常的高,资源内聚,易于组合,作用域独立互不污染。。。。等等图片 14,视乎CSS模块的完成度有欠缺。

那么目前组件完成度来看,CSS作用域其实是全局性的,并非组件内部独立。下一步,我们要做得就是如何让我们组件内部的CSS作用域独立。

这时可能有人立马跳出,大喊一句“德玛西亚!”,哦不,应该是“用sass啊傻逼!”。可是项目组件化之后,组件的内部封装已经很好了,其内部dom结构和css趋向简单,独立,甚至是破碎的。LESS和SASS的一体式样式框架的设计,他的嵌套,变量,include,函数等丰富的功能对于整体大型项目的样式管理非常有效。但对于一个功能单一组件内部样式,视乎就变的有点格格不入。“不能为了框架而框架,合适才是最好的”。视乎原生的css能力已经满足组件的样式需求,唯独就是上面的css作用域问题。

 

这里我给出思考的方案: classname随便写,保持原生的方式。编译阶段,根据组件在项目路径的唯一性,由【组件classname 组件唯一路径】打成md5,生成全局唯一性classname。正当我要写一个loader实现我的想法的时候,发现歪果仁已经早在先走一步了。。。。

这里具体方案参考我之前博客的译文:

之前我们讨论过JS的模块。现在通过Webpack被加载的CSS资源叫做“CSS模块”?我觉得还是有问题的。现在style-loader插件的实现本质上只是创建link[rel=stylesheet]元素插入到document中。这种行为和通常引入JS模块非常不同。引入另一个JS模块是调用它所提供的接口,但引入一个CSS却并不“调用”CSS。所以引入CSS本身对于JS程序来说并不存在“模块化”意义,纯粹只是表达了一种资源依赖——即该组件所要完成的功能还需要某些asset。

因此,那位歪果仁还扩展了“CSS模块化”的概念,除了上面的我们需要局部作用域外,还有很多功能,这里不详述。具体参考原文 

非常赞的一点,就是cssmodules已经被css-loader收纳。所以我们不需要依赖额外的loader,基本的css-loader开启参数modules即可

JavaScript

//webpack.config.js ... module: { loaders: [ {test: /.css$/, loader: 'style!css?modules&localIdentName=[local]__[name]_[hash:base64:5]' }, ] } ....

1
2
3
4
5
6
7
8
//webpack.config.js
...  
    module: {
        loaders: [
            {test: /.css$/, loader: 'style!css?modules&localIdentName=[local]__[name]_[hash:base64:5]' },
        ]  
    }
....

modules参数代表开启css-modules功能,loaclIdentName为设置我们编译后的css名字,为了方便debug,我们把classname(local)和组件名字(name)输出。当然可以在最后输出的版本为了节省提交,仅仅使用hash值即可。另外在react中的用法大概如下。

JavaScript

var styles = require('./banner.css'); var Banner = new React.createClass({ ... render: function(){ return ( <div> <div className={styles.classA}></div> </div> ) } });

1
2
3
4
5
6
7
8
9
10
11
var styles = require('./banner.css');
var Banner = new React.createClass({
    ...
    render: function(){
        return (
            <div>
                <div className={styles.classA}></div>
            </div>
        )
    }
});

最后这里关于出于对CSS一些思考,

关于css-modules的其它功能,我并不打算使用。在内部分享【我们竭尽所能地让CSS变得复杂】中提及:

我们项目中大部分的CSS都不会像boostrap那样需要变量来设置,身为一线开发者的我们大概能够感受到:设计师们改版UI,绝对不是简单的换个色或改个间距,而是面目全非的全新UI,这绝对不是一个变量所能解决的”维护性“。

反而项目实战过程中,真正要解决的是:在版本迭代过程中那些淘汰掉的过期CSS,大量地堆积在项目当中。我们像极了家中的欧巴酱不舍得丢掉没用的东西,因为这可是我们使用sass或less编写出具有高度的可维护性的,肯定有复用的一天。

这些堆积的过期CSS(or sass)之间又有部分依赖,一部分过期没用了,一部分又被新的样式复用了,导致没人敢动那些历史样式。结果现网项目迭代还带着大量两年前没用的样式文件。

组件化之后,css的格局同样被革新了。可能postcss才是你现在手上最适合的工具,而不在是sass。

 

到这里,我们终于把组件化最后一个问题也解决了。

“作用域独立”—— (100%) 如同shadowDom作用域独立

 

到这里,我们可以开一瓶82年的雪碧,好好庆祝一下。不是吗?

图片 15

 

 组件化之路还在继续

webpack和react还有很多新非常重要的特性和功能,介于本文仅仅围绕着组件化的为核心,没有一一阐述。另外,配搭gulp/grunt补充webpack构建能力,webpack的codeSplitting,react的组件通信问题,开发与生产环境配置等等,都是整套大型项目方案的所必须的,限于篇幅问题。可以等等我更新下篇,或大家可以自行查阅。

但是,不得不再安利一下react-hotloader神器。热加载的开发模式绝对是下一代前端开发必备。严格说,如果没有了热加载,我会很果断地放弃这套方案,即使这套方案再怎么优秀,我都讨厌react需要5~6s的编译时间。但是hotloader可以在我不刷新页面的情况下,动态修改代码,而且不单单是样式,连逻辑也是即时生效。

图片 16

如上在form表单内。使用热加载,表单不需要重新填写,修改submit的逻辑立刻生效。这样的开发效率真不是提高仅仅一个档次。必须安利一下。

 

或许你发现,使用组件化方案之后,整个技术栈都被更新了一番。学习成本也不少,并且可以预知到,基于组件化的前端还会很多不足的问题,例如性能优化方案需要重新思考,甚至最基本的组件可复用性不一定高。后面很长一段时间,需要我们不断磨练与优化,探求最优的前端组件化之道。

至少我们可以想象,不再担心自己写的代码跟某个谁谁冲突,不再为找某段逻辑在多个文件和方法间穿梭,不再copy一片片逻辑然后改改。我们每次编写都是可重用,可组合,独立且内聚的组件。而每个页面将会由一个个嵌套组合的组件,相互独立却相互作用。

 

对于这样的前端未来,有所期待,不是很好吗

至此,感谢你的阅读。

1 赞 6 收藏 1 评论

图片 17

摘要


  • 后端懒人一枚,临时需要搞个管理端,以前在学校有用jquery bootstrap做过类似的前端项目,然而如今回首,旧代码如屎一坨,更别提维护了。有了之前的教训,碰到这个需求就决定认真做做。
  • 对前端还是略小白,写得不好,还望多多指教。
  • 已完成的项目使用的组合是react webpack redux-form(支持ie8)

本文介绍seajs和hanlebars,并实现前端控件组件化.

前言

一位计算机前辈曾说过:

Controlling complexity is the essence of computer programming

1
Controlling complexity is the essence of computer programming

随着前端开发复杂度的日益提升,组件化开发应运而生,并随着 FIS、React 等优秀框架的出现遍地开花。这一过程同样发生在美团,面临业务规模的快速发展和工程师团队的不断扩张,我们历经引入组件化解决资源整合问题、逐步增强组件功能促进开发效率、重新打造新一代组件化方案适应全栈开发和共享共建等阶段,努力“controlling complexity”。本文将介绍我们组件化开发的实践过程。

背景与问题


  • Header,Banner,Footer等前端布局的公共部分,重复编写代码或借助php等后端语言进行渲染,会造成前后端代码高耦合。
  • 放任全局变量的存在,给项目扩展带来未知问题。
  • 随着项目开发不断进行,项目也会变得臃肿,糟糕的项目结构会使维护工作越发困难。复杂的页面需要控制更多状态,并根据状态的变化,执行更多相应的逻辑。
  • 针对管理端,进行ie8 的兼容。
  • 使用的第三方库较多,前端发送的静态文件(js,css)请求较多,导致网页加载速度较慢。
  • 管理平台的样式多有雷同,若样式定制开发,会产生不必要的工作量。

我想实现的是把组件单独一个文件夹,js和css,html都放在文件夹中,要使用组件的页面利用seajs引入组件,并直接调用组件的方法或者事件。这个方案是自己的思考,欢迎大家拍砖。

组件化 1.0:资源重组

在美团早期,前端资源是按照页面或者类似业务页面集合的形式进行组织的。例如 order.js 对应订单相关页面的交互,account.css 对应账户相关页面的样式。这种方式在过去的较长一段时间内,持续支撑了整个项目的正常推进,功勋卓著。

图片 18

随着业务规模的增加和开发团队的扩张,这套机制逐渐显示出它的一些不足:

  • 资源冗余页面的逐渐增加,交互的逐渐复杂化,导致对应的 css 和 js 都有大幅度增长,进而出现为了依赖某个 js 中的一个函数,需要加载整个模块,或者为了使用某个 css 中的部分样式依赖整个 css,冗余资源较多
  • 对应关系不直观没有显而易见的对应规则,导致的一个问题是修改某个业务模块的 css 或者 js 时,几乎只能依靠 grep。靠人来维护页面模块 html、css 和 js 之间的依赖关系,容易犯错,常常出现内容已经删除但是 css 或 js 还存在的问题
  • 难于单元测试以页面为最小粒度进行资源整合,不同功能的业务模块相互影响,复杂度太高,自动化测试难以推进

2013 年开始,在调研了 FIS、BEM 等方案之后,结合美团开发框架的实际,我们初步实现了一套轻量级的组件化开发方案。主要的改进是:

  • 以页面功能组件为单位聚合前端资源
  • 自动加载符合约定的 css、js 资源
  • 将业务数据到渲染数据的转换过程独立出来

图片 19

举例来说,美团顶部的搜索框就被实现为一个组件。

图片 20

代码构成:

www/component/smart-box/ ├── smart-box.js # 交互 ├── smart-box.php # 渲染数据生产、组件配置 ├── smart-box.scss # 样式 ├── smart-box.tpl # 内容 └── test ├── default.js # 自动化测试 └── default.php # 单测页面

1
2
3
4
5
6
7
8
www/component/smart-box/
├── smart-box.js    # 交互
├── smart-box.php   # 渲染数据生产、组件配置
├── smart-box.scss  # 样式
├── smart-box.tpl   # 内容
└── test
    ├── default.js  # 自动化测试
    └── default.php # 单测页面

调用组件变得十足简单:

JavaScript

echo View::useComponent('smart-box', [ 'keyword' => $keyword ]);

1
2
3
echo View::useComponent('smart-box', [
    'keyword' => $keyword
]);

对比之前,可以看到组件化的一些特点:

  • 按需加载只加载必要的前端资源
  • 对应关系非常清晰组件所需要的前端资源都在同一目录,职责明确且唯一,对应关系显著
  • 易于测试组件是具备独立展现和交互的最小单元,可利用 Phantom 等工具自动化测试

此外,由于前端资源集中进行调度,组件化也为高阶性能优化提供了空间。例如实现组件级别的 BigRender、通过数据分析进行资源的合并加载等等。

关键词


根据之前对背景和问题的分析,这次做前端的主要就是做到以下3个关键词。

  • 模块化
  • 组件化
  • 工程化

主要演员 seajs 和 handlebars

组件化 2.0:趋于成熟

组件化 1.0 上线后,由于简单易用,很快得到工程师的认可,并开始在各项业务中应用起来。新的需求接踵而来,一直持续到 2014 年底,这个阶段我们称之为组件化 2.0。下面介绍下主要的几个改进。

问题及对应解决方案


seajs是一个前端模块化js库,作用类似于requirejs,都用来前端js的模块化和按需依赖加载的工作。功能都差不多,seajs是淘宝玉伯开发的,国产,所以国内使用者比较多,requirejs国外的,存在时间比seajs要久许多,国外用的比较多。 两者最大的区别在于定义模块的语法不同,一个是AMD规范,一个是CMD规范。seajs是CMD规范,所以他的语法更像nodejs,写起来会比requirejs优雅一些。不过缺点也很多,比如很多第三方库都是按照AMD规范写的,会有兼容性问题,就需要自己改写模块或者使用spm工具处理。

Lifecycle

组件在高内聚的同时,往往需要暴露一些接口供外界调用,从而能够适应复杂的页面需求,例如提交订单页面需要在支付密码组件启动完成后绑定提交时的检查。Web Components、React 等都选择了生命周期事件/方法,我们也是一样。

组件的生命周期:

图片 21一个组件的完整生命周期包括:

  • init,初始化组件根节点和配置
  • fetch,加载 css 和 js 资源
  • render,内容渲染,默认的渲染内容方式是 BigRender
  • ready,进行数据绑定等操作
  • update,数据更新
  • destroy,解除所有事件监听,删除所有组件节点

组件提供 pause、resume 方法以方便进行生命周期控制。各个阶段使用 Promise 串行进行,异步的管理更清晰。使用自定义语义事件,在修改默认行为、组件间通信上充分利用了 YUI 强大的自定义事件体系,有效降低了开发维护成本。

举个例子,页面初始化时组件的启动过程实际也是借助生命周期实现的:

JavaScript

var afterLoadList = []; Y.all('[data-component]').each(function (node) { var component = new Y.mt.Component(node); // 绑定 init 生命周期事件,在 init 默认行为完成后执行回调 component.after('init', function (e) { // 如果配置了延迟启动 if (e.config.afterLoad) { // 暂停组件生命周期 e.component.pause(); // 压入延迟启动数组 afterLoadList.push(e.component); } }); // 开始进入生命周期 component.start(); }); Y.on('load', function () { // 在页面 load 事件发生时恢复组件生命周期 afterLoadList.forEach(function (component) { component.resume(); }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var afterLoadList = [];
Y.all('[data-component]').each(function (node) {
    var component = new Y.mt.Component(node);
    // 绑定 init 生命周期事件,在 init 默认行为完成后执行回调
    component.after('init', function (e) {
        // 如果配置了延迟启动
        if (e.config.afterLoad) {
            // 暂停组件生命周期
            e.component.pause();
            // 压入延迟启动数组
            afterLoadList.push(e.component);
        }
    });
    // 开始进入生命周期
    component.start();
});
 
Y.on('load', function () {
    // 在页面 load 事件发生时恢复组件生命周期
    afterLoadList.forEach(function (component) {
        component.resume();
    });
});

回过头来看,引入生命周期除了带来扩展性外,更重要的是理顺了组件的各个阶段,有助于更好的理解和运用。

问题:前后端代码高耦合 全局变量

handlebars是一个前端js模板引擎 我在之前的文章介绍过handlebars及简单的用法,大家可以去看下点我

Data Binding

数据绑定是我们期盼已久的功能,将 View 和 ViewModel 之间的交互自动化无疑会节省工程师的大量时间。在组件化减少关注点和降低复杂度后,实现数据绑定变得更加可能。

我们最终实现的数据绑定方案主要参考了 Angular,通过在 html 节点上添加特定的属性声明绑定逻辑,js 扫描这些内容并进行相应的渲染和事件绑定。当数据发生变化时,对应的内容全部重新渲染。

XHTML

<ul class="addressList"> <li mt-bind-repeat="addr in addrList" mt-bind-html="addr.text" > </li> </ul> <script> Y.use(['mt-bind', 'mt-scope'], function () { Y.mt.bind.init(document.body); var scope = Y.one('.addressList').getScope(); // 将 scope.addrList 设置为一个数组,DOM 上将自动渲染其内容 scope.$set('addrList', [ { text: "first address" }, { text: "second address" } ]); }); </script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ul class="addressList">
    <li
        mt-bind-repeat="addr in addrList"
        mt-bind-html="addr.text"
    >
    </li>
</ul>
 
<script>
Y.use(['mt-bind', 'mt-scope'], function () {
    Y.mt.bind.init(document.body);
    var scope = Y.one('.addressList').getScope();
    // 将 scope.addrList 设置为一个数组,DOM 上将自动渲染其内容  
    scope.$set('addrList', [
        { text: "first address" },
        { text: "second address" }
    ]);
});
</script>

使用属性声明绑定逻辑的好处是可以同时支持后端渲染,这对于美团团购这样的偏展现型业务是非常必要的,用户可以很快看到页面内容。

方案:模块化


首先,将前端的公共部分提取出来,进行模块化编程;
然后,划定作用域,不同模块间作用域不共享,这样就可以做到,公共代码模块化,避免了全局变量的困扰。

目前,流行的模块化编程的工具有RequireJS, SeaJS, Webpack,其中SeaJS是由阿里的团队开发的,在中国有活跃的社区。Webpack由德国人编写,体现了更强的项目工程化特点。
其中,RequireJS和SeaJS分别是AMD和CMD两种规范的代表。AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块;CMD推崇异步依赖加载的,只有在用到某个模块的时候再去require。

RequireJS(AMD写法)

SeaJS(CMD写法)

而Webpack则属于集大成者,不仅兼容两者书写规范,而且因采用异步IO及多级缓存,使Webpack在增量编译上更快。但实际上,Webpack在实际开发中更多的是充当编译者和打包者的角色,它结合React同样能够实现模块化开发,这个我们在后面展示。

任务1,页面框架配置,seajs初始化

Flux

实现数据绑定后,我们不得不面对另外一个问题:如何协同多个组件间的数据。因为某个组件的数据变化,很有可能引起其他组件的变化。例如当修改购买数量,总金额会变化,而总金额超过 500 后,还需要展示大额消费提醒。

为了解决这个问题,我们引入了 Flux,使用全局消息总线的思路进行跨组件交互。

例如因为交互复杂而一直让我们非常头疼的项目购买页,在应用组件 Flux 重构后,各模块之间的互动更加清晰:

图片 22

其他方面的改进还有很多,包括引入模板引擎 LightnCandy 约束模板逻辑、支持组件任意嵌套、支持异步加载并自动初始化等。

随着组件化 2.0 的逐步完善,基本已经可以从容应对日常开发,在效率和质量方面都上了一个台阶。

问题:糟糕的项目结构,控制更多的状态

新建html页面,引入需要的类库

组件化 3.0:重启征程

时间的车轮滚滚前行,2014 年底,我们遇到一些新的机遇和挑战:

  • 基于 Node 的全栈开发模式开始应用,前后端渲染有了更多的可能性
  • YUI 停止维护,需要一套新的资源管理方案
  • 新业务不断增加,需要找到一种组件共享的方式,避免重复造轮子

结合之前的实践,以及在这一过程中逐渐积累的对业内方案的认知,我们提出了新的组件化方案:

  • 基于 React 开发页面组件,使用 NPM 进行分发,方便共建共享
  • 基于 Browserify 二次开发,建设资源打包工具 Reduce,方便浏览器加载
  • 建设适应组件化开发模式的工程化开发方案 Turbo,方便工程师将组件应用于业务开发中

方案:组件化


组件化建立在模块化的基础上,模块化是对资源的管理,组件化是对业务逻辑的管理,各自要有不同的发展方向。经调研,NPM,Webpack(或Browserify)和React的组合会便于我们对项目结构有一个整体的认识。在这个基础上,我们便能非常方便地创建一个结构更为清晰的项目。

而针对不同组件所拥有的不断变化的状态,通过对Flux和Redux的学习和使用,可以方便我们对于复杂组件状态处理的机制,有进一步的理解。(但因为本人毕竟不是前端,且较懒,关于redux,未做过多学习,直接上手redux-form是为了快速开发管理端表单。)这里要提一下,redux-form非常强大,官方已给出很多例子,这里不再熬述。

Flux

Redux

seajs用于做js模块管理,seajs-text可以用于加载handlebars的模板内容

React

在组件化 2.0 的过程中,我们发现很多功能和 React 重合,例如 Data Binding、Lifecycle、前后端渲染,甚至直接借鉴的 Flux。除此之外,React 的函数式编程思想、增量更新、兼容性良好的事件体系也让我们非常向往。借着前端全栈开发的契机,我们开始考虑基于 React 进行组件化 3.0 的建设。

问题:IE8兼容

配置seajs

NPM Reduce

NPM Reduce 构成了我们新的资源管理方案,其中:

  • NPM 负责组件的发布和安装。可以认为是“分”的过程,粒度越小,重用的可能性越大
  • Reduce 负责将页面资源进行打包。可以认为是“合”的过程,让浏览器更快地加载

一个典型的组件包:

smart-box/ ├── package.json # 组件包元信息 ├── smart-box.jsx # React Component ├── smart-box.scss # 样式 └── test └── main.js # 测试

1
2
3
4
5
6
smart-box/
├── package.json    # 组件包元信息
├── smart-box.jsx   # React Component
├── smart-box.scss  # 样式
└── test
    └── main.js     # 测试

NPM 默认只支持 js 文件的管理,我们对 NPM 中的 package.json 进行了扩展,增加了 style 字段,以使打包工具 Reduce 也能够对 css 和 css 中引用的 image、font 进行识别和处理:

XHTML

{ "style": "./smart-box.scss" }

1
2
3
{
    "style": "./smart-box.scss"
}

只要在页面中 require 了 smart-box,经过 Reduce 打包后,js、css 甚至图片、字体,都会出现在浏览器中。

JavaScript

var SmartBox = require('@mtfe/smart-box'); // 页面 var IndexPage = React.createClass({ render: function () { return ( <Header> <SmartBox keyword={ this.props.keyword } /> </Header> ... ); } }); module.exports = IndexPage;

1
2
3
4
5
6
7
8
9
10
11
12
13
var SmartBox = require('@mtfe/smart-box');
// 页面
var IndexPage = React.createClass({
    render: function () {
        return (
            <Header>
                <SmartBox keyword={ this.props.keyword } />
            </Header>
            ...
        );
    }
});
module.exports = IndexPage;

整体思路和组件化 1.0 如出一辙,却又那么不同。

方案:React(0.14.x)


最一开始调研的时候,因为ie兼容的顾虑,所以,调研了比较流行的React,Angular,Vue以及小众的Avalon。其中后三个都是MVVM框架(核心是数据双向绑定)。关于兼容,Angular早在1.3版本就抛弃了对ie8的支持,Vue就没打算支持ie8,可面对中国1/5的ie8用户,只好在Avalon和React做取舍。

MVVM

Avalon是由去哪儿前端架构师“司徒正美”开发的一款基于虚拟DOM与属性劫持的迷你、易用、高性能的前端MVVM框架,适用于各种场景,兼容各种古老刁钻浏览器,吸收最新的技术成果,能迅速堆砌组件与应用。它竟然支持到IE6 。

avalon2

兼容性和性能好外,缺点就是除了文档和较小的论坛,你搜不到过多的资料,这对后期学习,或者是他人维护,是很不方便的。而且作者重写了不少js的核心方法,这导致如果出错,除了找作者和自己改源码外,很可能无处查询。

最后选择React(0.14.x),除了兼容外,还有诸如社区活跃,资源多,最重要的是有Facebook做支持。当然,它的ie8支持需要进行多插件配置和支持,在我github上关于react学习的相关实践里有相关配置文件。其中,package.json和webpack.config.js需要好好看下,其次就是注意项目结构(因为用了react-router,项目结构相对清晰),其他兼容相关可以通过下面链接进行学习。
https://github.com/xcatliu/react-ie8
http://www.aliued.com/?p=3240

//basePath由服务端配置域名varbasePath=" seajs 的简单配置seajs.config({base:basePath,alias:{"jquery":"../bower_components/jquery/jquery.seejs.min.js","handlebars":"../bower_components/handlebars/handlebars.seajs.min.js"}});// 加载入口模块seajs.use("./scripts/seajs handlebars.js");````注意,jquery默认是不支持AMD标准,也不支持seajs,所以这里用的````jquery.seejs.min.js和handlebars.seajs.min.js````都是自己手动改过的,jquery修改的方法就是在源码中级加上几行代码,如下:````jsdefine(function(){//jquery源代码写在中间return$.noConflict();});

Turbo

单单解决分发和打包的问题还不够,业务开发过程如果变得繁琐、难以 Debug、性能低下的话,恐怕不会受到工程师欢迎。

为了解决这些问题,我们在 Node 框架的基础上,提供了一系列中间件和开发工具,逐步构建对组件友好的前端工程化方案 Turbo。主要有:

  • 支持前后端同构渲染,让用户更早看到内容
  • 简化 Flux 流程,数据流更加清晰易维护
  • 引入 ImmutableJS,保证 Store 以外的数据不可变
  • 采用 cursor 机制,保证数据修改/获取同步
  • 支持 Hot Module Replacement,改进开发流自动化

通过这些改进,一线工程师可以方便的使用各种组件,专注在业务本身上。开发框架层面的支持也反过来促进了组件化的发展,大家更乐于使用一系列组件来构建页面功能。

问题:前端发送的静态文件(js,css)请求多

handlebars修改比jquery少了那句return $.noConflict();就可以了。

小结

发现痛点、分析调研、应用改进的解决问题思路在组件化开发实践中不断运用。历经三个大版本的演进,组件化开发模式有效缓解了业务发展带来的复杂度提升的压力,并培养工程师具备小而美的工程思想,形成共建共享的良好氛围。毫无疑问,组件化这种“分而治之”的思想将会长久地影响和促进前端开发模式。我们现在已经准备好,迎接新的机遇和挑战,用技术的不断革新提升工程师的幸福感。

1 赞 5 收藏 评论

图片 23

方案:Webpack


Webpack支持转译,打包,压缩等功能。转译可以将react的jsx语言转成js,es6写法转成es5(大部分浏览器兼容)等,同时,将众多文件的转译结果及依赖关系等,打包压缩到一个文件中,只需一次请求,便相当于加载了多个文件及其关系,极大地提升了前端页面加载速度。

basePath可以根据实际环境设置。使用alias设置别名后,就可以在sea中使用require()的方法获取到源码

问题:样式要求不高,但重复性高

seajs.use("./scripts/seajs handlebars.js");设置了页面的js入口

方案:Ace Admin(ie8)


自己主要还是后端开发,管理端样式不是自己研究的重点,故决定选择模板进行开发。通过调研,Ace Admin和AdminLTE都是github上比较受欢迎的前端样式模板,其中Ace支持ie8,故选取前者用于实际开发。但AdminLTE相对好看,提供的样式选择较多,大家也可以使用。

QQ截图20161027190718.jpg

入口seajs handlebars.js

第三方库


React的思想是通过操作虚拟dom完成指定任务。然而,在实际开发中,其实有很多方便的轮子并没有React化,仍然需要通过外部引入的方式借助第三方库进行开发。
在这里,React组件的生命周期为引入三方库,提供了componentDidMount等方法,方便对第三方库进行加载。
这里顺便安利几个库,如uploadify(flash版上传文件)、ZeroClipboard(点击即可复制)、dataTables(列表前端组件)

// 所有模块都通过 define 来定义

代码相关


项目代码不太好开源,但之前自己写的几个react相关的学习例子与项目契合度较高。
这里贴出react学习的例子链接,感兴趣的朋友可以自行下载。欢迎各位相互交流。

define(function(require, exports, module) {

$(function(){

var Handlebars = require('handlebars');

var tpl = require("./data.tpl");

var demoTplc = Handlebars.compile(tpl);

$("body").html( demoTplc("hello world"));

});

});

data.tpl

{ {this} }

加载Handlebars和tpl模板,然后直接调用Handlebars.compile进行模板编译后渲染dom。执行后bady的内容就变成了hello world。大家可以下载demo,打开seajs handlebars.html看下效果

组件化方案

之前的代码可以完成用seajs动态加载handlebars模板并渲染的功能,但并没有实现组件化。现在我们来实现组件的封装。首先建立一个文件夹,包括

components

- Boxes

-index.js  //js代码处理数据和事件

-index.css //boxes样式

-boxes.tpl //boxes html模板

里面的内容作为demo,我写的简单一些。

boxes.tpl

this is a boxes!

{ {this} }

index.css

.c-boxes{background-color:red;}.c-boxesh1{color:blue;}

index.js

define(function(require,exports,module){varHandlebars=require('handlebars');varbox={init:function(){returnbox;},clicked:function(){},render:function($dom,data){vartpl=require('./boxes.tpl');vartplc=Handlebars.compile(tpl);// var _clicked = clicked;$dom.html(tplc(data));box.$dom=$dom;$dom.click(function(){box.clicked&&box.clicked();});}};module.exports=box;});

index.js稍微复杂一些,封装了一个对象,并定义了对象的点击事件的外部接口。rander方法使用了前面相似的方式,渲染了bandlerbars模板,并注入点击事件

这样一个组件就已经封装好了。

组件的使用

前面定义了一个boxes组件,现在我们来使用他.html页面和之前的页面是一样的,唯一的区别是入口js换成了componentization.js

componentization.js

define(function(require,exports,module){// 通过 require 引入依赖var$=require('jquery');varBoxes=require('boxes');$(function(){//实例化组件varbox=Boxes.init();box.render($("body"),"hello world!");box.clicked=function(){console.log("clicked");};});});

box定义好后,使用起来非常简单,直接调用box.render()方法就可以了,也顺带个box绑定了点击事件,效果可以看demo中的 componentization.html 页面

组件化其他的思考

组件的模板和js都通过seajs加载和封装了,唯一遗憾的是css需要单独引入。 seajs也有css引入的插件sea-css,但是试了一下没成功。 所以我的解决方案是使用gulp完成,遍历component下的所有css文件,组合成并压缩成一个css,然后再使用的页面中统一引用这个合并的css

具体gulp脚本

//css components 组合 构建任务

gulp.task('css-concat', function () {

gulp.src(css_components_Src)

.pipe(concat('allComponent.css'))//合并后的文件名

.pipe(gulp.dest(cssDst));

});

demo

本文示例demo见demo-web

文本的demo在文件夹Handlebars中

使用方式

Handlebars目录下执行http-server -p 8080

usage.html =》 handlerbars的使用及模板预编译

componentization.html =》 前端开发框架 - seajs handlebars模块化开发

错误处理

如果本地没有http-server命令,请安装nodejs环境,并通过npm安装 http-server 命令:npm install -g http-server

本文由星彩网app下载发布于前端技术,转载请注明出处:前端组件化开发实践,致我们终将组件化的Web

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。