也就是说一个组件需要封装的就是关联的html、

作者: 前端  发布:2019-11-07

谈笑风生组件化

2016/02/28 · 基础技术 · 组件化

原文出处: 木的树   

  在当今的前端开发领域,大红大紫的组件化开发如万人空巷,前端技术圈中关于组件化讨论的文章亦如汗牛充栋。然而别人的理解终归是别人的,作为一个胸存小志的开发者,我还是希望能够根据自己的理解和实际工作,总结自己对组件和组件化开发的认知。

在我第一次接触组件化概念时,一时迷迷糊糊,如坠云雾深处。组件是什么?组件化开发是什么?为什么大牛们知道这么多而我不知道?这应该并不是我个人的疑问,每一个除此接触概念的新手,都会有此疑惑。

 

为什么大牛们知道这么多而我不知道?

我曾经无数次为类似的问题而烦恼,也曾感到急躁难耐。回答这个问题,我们需要有一个基本认知:任何一个新概念都是在无数前辈先贤的实践、总结中继承发展而来的。组件化开发也不例外。这个问题涉及认知学,能够引出很多值得探讨的问题,但这并不是本文的重点。关于前端组件化的发展过程,我推荐xufei大神的这篇文章:Web应用的组件化(一)——基本思路。

组件化开发是什么?

原来架构设计比较多关注的是横向的分层,即数据层,逻辑层和UI层。而组件化架构必须同时关注纵向的隔离和解耦。在分层和分模块后,每一个业务组件由三层各自存在的部署包组成,包本身是一个包含了技术组件和服务组件的一个结合体。由数据层,逻辑层,界面层三层的三个业务包可以构成一个完整的具备独立功能的业务组件。【人月神话的博客】

这个解释很正确,但太概念化,我理解的组件化开发是将复杂并混乱的页面逻辑,分割成一个个独立的业务单元。

组件是什么?

根据上面的回答,我们基本可以确定,组件就是页面中一个个独立的逻辑单元。这个结论是放之四海而皆准的,然每一个高高在上的理论都要落地,根据具体情况具体回答。组件放到前端就要有一个符合前端技术的回答:前端组件就是模板、样式、代码逻辑相结合的、独立的、可复用的业务逻辑单元,其中模板由html承担、样式由css负责、代码逻辑由JavaScript编写。

由张云龙大神的这张图,可以看出组件化的基本意图以及组件的基本组成。

9159.com 1

任何一种新的开发方式,都不能靠只读几篇文章就能明白,必须要实际动手并在工作中有所总结,才能彻底掌握。所以我并不奢望靠上文的几段文字就能让读者完全明白组件与组件化开发。

  接下来我将根据自己实际的开发经验,与大家分享一下我对组件的认知的和经验总结。

 

组件的基本修养

任何一个华丽的思想都有一套朴实的代码实现。上面我们从抽象的层次上谈了谈组件的概念,放到实际的代码世界,该如何去实现呢?众所周知,JavaScript是一门面向对象语言,面向对象语言最重要的特性就是——抽象。放到实际开发中就是定义一个基类,应用的我们现在的场景,我们需要一个组件基类——Component。由这个基类来提供组件的基础功能。具体都应该有什么方面的基础功能呢?别急,这个问题先放一放。

组件的管理

先看一下上面的这张图,我们会发现,整个页面都是由不同的功能的业务组件组成的。这就引出了另一个问题,当一个页面的组件非常多时,我们需要一套统一管理的仓库——CRepository。每一个组件都要将自身id向仓库注册,仓库提供管理功能,如增删改查。具体的方法由实际应用而异,但几个通用的方法可以参考:

count: Number.//整个app中组件的数量 add: function(component){....} //将一个组件添加到仓库中 remove: function(id){....} //将一个组件从仓库中移除 byId: function(id){....} //根据id从仓库中查找组件

1
2
3
4
5
6
7
count: Number.//整个app中组件的数量
 
add: function(component){....} //将一个组件添加到仓库中
 
remove: function(id){....} //将一个组件从仓库中移除
 
byId: function(id){....} //根据id从仓库中查找组件

了解完仓库之后,我们便可以将主要精力放回到Component上了。

 

组件的生命周期

生命周期这个概念最早在软件工程中接触到,可惜我对那些枯燥的理论没有什么兴趣,上起课来云里雾里,早就还给教授了。那我就举一个大家都有体会的例子。组件如人,人的生命有尽头,组件的生命必然有。将组件的生命周期分割成不同的几个阶段来处理不同的逻辑,就如同人的一生不同阶段要面对不同的烦恼一样。

constructor:function(){} //构造函数,处理外部参数 mixinProperties:function(){} //在这个阶段,混入必要的属性 parseTemplate:function(){}//在这个阶段解析模板,将模板由字符串转化成dom节点 postCreate:function(){}//在这个阶段,模板解析完毕,可以访问component的根节点cRoot。此时可以对组件的dom树进行访问或绑定事件。但此时组件还未加到页面dom树中。 startup:function(){}//此时组件以加入dom树中,这里可以在组件加入页面dom后做一些初始化工作。对于嵌套组件,需要处理子组件的startup destroy:function(){}//组件生命结束,进入销毁阶段,从组件仓库中注销

1
2
3
4
5
6
7
8
9
10
11
constructor:function(){} //构造函数,处理外部参数
 
mixinProperties:function(){} //在这个阶段,混入必要的属性
 
parseTemplate:function(){}//在这个阶段解析模板,将模板由字符串转化成dom节点
 
postCreate:function(){}//在这个阶段,模板解析完毕,可以访问component的根节点cRoot。此时可以对组件的dom树进行访问或绑定事件。但此时组件还未加到页面dom树中。
 
startup:function(){}//此时组件以加入dom树中,这里可以在组件加入页面dom后做一些初始化工作。对于嵌套组件,需要处理子组件的startup
 
destroy:function(){}//组件生命结束,进入销毁阶段,从组件仓库中注销

凡是比喻就一定有失真的地方,组件的生命当然不可能与人相比,但我却发现上面的生命周期与婴儿从被怀孕与诞生的过程极其相似。

constructor:function(){} //受精卵状态 mixinProperties:function(){} //染色体重组 parseTemplate:function(){}//婴儿在母体内的生长发育过程 postCreate:function(){}//婴儿在母体内生长发育完成,母亲即将临盆 startup:function(){}//婴儿出生,被社会认可 destroy:function(){}//个体消亡,取消社会户籍等等

1
2
3
4
5
6
7
8
9
10
11
constructor:function(){} //受精卵状态
 
mixinProperties:function(){} //染色体重组
 
parseTemplate:function(){}//婴儿在母体内的生长发育过程
 
postCreate:function(){}//婴儿在母体内生长发育完成,母亲即将临盆
 
startup:function(){}//婴儿出生,被社会认可
 
destroy:function(){}//个体消亡,取消社会户籍等等

组件的属性访问器

对于组件内部数据的访问,应当对外提供统一的访问渠道,通常来讲这部分内容就是属性的取值器与赋值器(get和set)。

set(prop, value)//为组件的某个属性赋值 get(prop)//为从组件中取得某个属性值

1
2
3
set(prop, value)//为组件的某个属性赋值
 
get(prop)//为从组件中取得某个属性值

要明确的一点是,这里的set与get不仅仅像点语法一样单纯的赋值与取值,否则就是画蛇添足。使用过C#的兄台知道,C#中存在“属性的Get与Set”,它们能够避免直接对字段进行访问,这里提到组件的get与set应当具有同样的功能,具体的实现方式敬请关注后续文章。

 

组件的模板解析

遇到模板通常会遇到数据绑定的需求,可能是双向绑定也可能是单向绑定。双向绑定如众多的MVVM框架,模板解析过程中可能会读取组件内数据来渲染dom元素,亦或者组件dom树生成后,dom元素的变动即可作用于组件内部数据。单向绑定常出现在MVC框架中,如dojo,只是将dom元素与组件内部某个属性绑定,或者将交互事件与组件内部方法绑定。

JavaScript中没有注解特性,所以众多绑定功能都是在template中添加自定义特性,并在解析过程中处理自定义特性。

说到事件的绑定,事件带来的内存泄露问题不容忽视。这就要在组件销毁时,一并处理组件内部绑定的事件。包括在模板中绑定的事件与组件内部手动绑定的事件。

 

组件关系

当一个页面变得越来越复杂时,组件之间必然会出现嵌套。嵌套意味会出现父子关系、兄弟关系等。嵌套的管理可以参照DOM中的层级关系,提供相应的处理方法。但通常来讲,只需要管理好父子关系即可,兄弟关系的管理往往太复杂,而且通常情况下,一个getChildren,然后根据索引便能满足需求。所以大部分类库中组件关系的管理,往往只需要两个方法:

getParent:function(){}//获取组件的父组件 getChildren:function(){}//获取组件内部所有子组件

1
2
3
getParent:function(){}//获取组件的父组件
 
getChildren:function(){}//获取组件内部所有子组件

 

组件通信

组件变得复杂增多时,另组件之间如何通信的问题便被应当被提上议事日程。JavaScript本身便适用于消息驱动,处理组件间的通信当然要就地取材,事件机制便是最佳方案,所以前端组件应当在事件机制(往往是语义事件)的基础 提供通信功能。组件应当既可以接收事件也可以发送事件,于是应当分别提供方法:

on:function(component, eventName, handler) //用于绑定组件事件 emit:function(eventName, event) //组件对外发送事件

1
2
3
on:function(component, eventName, handler) //用于绑定组件事件
 
emit:function(eventName, event) //组件对外发送事件

 

  组件的销毁

组件的销毁属于组件生命周期的一部分,当组件功能变得复杂,组件正确合理的销毁就变得尤为重要。组件的销毁通常要考虑以下几个方面:

  • 组件内部事件的解绑
  • 组件dom的销毁
  • 组件内部属性的销毁
  • 子组件的销毁
  • 组件注销

 

组件的解析

如果所有的组件都要通过new class的方式去手动初始化,这本无可厚非,然而在现今标签化语言盛行的时代,是否能够有一种更为方便的开发方式,将自定义组件也能够以标签化的方式来书写。答案是肯定的,主流的类库对此一般有两种做法:一种是完全的自定义标签方式,如angular2;一种是以自定义标签特性的方式,如dojo等。所有的这些都需要基础库能够提供组件解析的功能。

通常的思路是以深度优先搜索的方式,扫描整个DOM树,解析自定义标签、自定义特性,将其实例化成自定义组件。有意思的是,因为组件嵌套关系的存在,自定义组件之间就像DOM树一样也是一个倒长的树形结构。

 

 

感谢读到这里的兄台,有的兄台可能会说,这篇文章大谈特谈了一堆组件、组件化开发,但都是理论性的东西。说好听了叫方法论,说不好听了是扯淡。若不来点实际东西,那便是虚与委蛇之气溢于朱墨之表,扑人眉宇。那么接下面的几篇文章,我将与大家一起根据本文的理论,一步步实现一套基础的组件类库。

 

参考文章:

Web应用的组件化(一)——基本思路

Web应用的组件化(二)——管控平台

2015前端组件化框架之路

前端开发的模块化和组件化的定义,以及两者的关系?

对组件化架构的再思考

1 赞 5 收藏 评论

9159.com 2

react

  • react支持jsx的语法,可以html和js混着写,而不像模板引擎,需要去另外学习一套模板的语法。
  • 有了jsx,可以直接用
 ReactDOM.render( <MyComponent values="xxx"></MyComponent>, document.getElementById("container") )

通过解析jsx来初始化,而不需要手动去new一个组件对象。

  • react提供了从model到view的单向的绑定,state发生了变化,就会去render
  • react也提供了完善的生命周期函数供开发者在组件创建、更新、销毁前后进扩展一些功能。而且提供了componentWillReceiveProps和shouldComponentUpdate两个用于优化性能的生命周期函数。

componentWillReceiveProps是在组件接收到新的props,还没有render之前调用,在这里去调用setState更新状态,不会触发额外的render。shouldComponentUpdate是在state或props变化之后调用的,根据返回的结果决定是不是调用render, 可以和Immutable.js结合,来避免state的深层比较带来的性能损耗。。

  • react 有虚拟dom这一层,并且会通过优化到的o的diff算法来进行虚拟dom的对比。
  • react是reconsiler,react-dom是renderer。react 16使用了fiber这个新的调度算法。使得大计算量被拆解,提高了应用的可访问性和性能。
  • react-native提供了可以渲染成安卓、ios组件的renderer,同时提供了原生的api供js调用。
  • 可以结合redux来做状态管理

局部CSS

<body>

    <div>I'm other div!! my color is not red!!</div>

    <script src="../dist/nuclear.js"></script>

    <script type="text/javascript">
        var ScopedCSSDemo = Nuclear.create({
            clickHandler: function () {
                alert("my color is red!");
            },
            render: function () {
                return '<div onclick="clickHandler()">my color is red!</div>'
            },
            style: function () {
                return 'div { cursor:pointer; color:red }';
            }
        })
        //第三个参数true代表 增量(increment)到body里,而非替换(replace)body里的
        new ScopedCSSDemo ({ seen: true }, "body" ,true);

    </script>

</body>

组件外的div不会被组件内的CSS污染。

其他

9159.com,关于组件的想象空间还有很大。未来可能会能够渲染到所有的端,渲染过程中的每一个环节,每一个痛点都有相应的优化方案。性能、功能都可以不断地提升。只要我们不要停止思考、停止敲代码的双手。

我们从jquery插件出发,思考了很多我们想要的组件化框架的样子,回到现实,我们看一下现在主流的组件化的框架有哪些,他们各自都有哪些特性。

条件判断

var ConditionDemo = Nuclear.create({
    render: function () {
        return '{{#seen}}
                    <div>
                        you can see me
                    </div>
                {{/seen}}
                {{^seen}}
                    <div>
                        yan can not see me
                    </div>
                {{/seen}}'
    }
})

var cd = new ConditionDemo({ seen: true }, "body");

setTimeout(function () {
    cd.option.seen = false;
}, 2000);

2秒后改变seen,dom会自动变更。

现在的组件化的方案已经在那个时代的基础上前进了很大一步。

怎么优雅绑定事件?只能定义在window下?

如果HTML绑定的事件是局部作用域那就再好不过了!我真的见过模版代码里出现下面的代码:

<div onclick="xxx()"></div>

然后在js里找到了下面的代码:

<script>
    window.xxx = function(){

    }
</script>

要绑定的事件一多,得污染多少全局变量啊。所以还有的工程师这么干:

<div onclick="ns.xxx()"></div>
<div onclick="ns.xxxx()"></div>

然后在js里找到了下面的代码:

<script>
    window.ns = {};

    ns.xx = function(){

    }

    ns.xxx = function(){

    }
</script>

这里貌似比不设定namespace好很多,但是还是妥协的结果。一般希望能封装成组件,组件的HTML里绑定的事件就是组件内定义的事件,内聚内聚!!
通过js动态绑定事件的坏处我以前专门写了一篇文章来阐述,主要是lazy bind会导致用户看到了页面,但是页面确无法响应用户的交互,这里不再阐述。

一些常见的逻辑,我们会把他们封装成函数或者类,比如BaseXxx、XxxUtils,牵扯到ui的组件复用的不只是逻辑,还有模板和样式。也就是说一个组件需要封装的就是关联的html、css、js。

Hello,Nuclear!

var HelloNuclear = Nuclear.create({
    render: function () {
        return '<div>Hello , {{name}} !</div>';
    }
})

new HelloNuclear({ name: "Nuclear" }, "body");

内置了mustache.js无逻辑模板。

我们可以先想想如果我们自己去做一个组件化的框架,我们会怎么做。

讨厌反斜杠?

讨厌反斜杠可以使用 ES20XX template literals、或者split to js、css和html文件然后通过构建组装使用。也可以用template标签或者textare存放模板。

<template id="myTemplate">
    <style>
        h3 {
            color: red;
        }

        button {
            color: green;
        }
    </style>

    <div>
        <div>
            <h3>TODO</h3>
            <ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>
            <form onsubmit="add(event)">
                <input nc-id="textBox" value="{{inputValue}}" type="text">
                <button>Add #{{items.length}}</button>
            </form>
        </div>
    </div>
</template>

<script>
    var TodoApp = Nuclear.create({
        install: function () {
            this.todoTpl = document.querySelector("#myTemplate").innerHTML;
        },
        add: function (evt) {
            evt.preventDefault();
            this.inputValue = "";
            this.option.items.push(this.textBox.value);
        },
        render: function () {
            return this.todoTpl;
        }
    });

    new TodoApp({ inputValue: "", items: [] }, "body");

</script>

通过模板解析的方式来初始化

我们组件用的时候,需要new一个组件的对象,传入需要的参数。比如:

 new Component({ template:"<div><h1>title</h1><p>content</p></div>", onXxx: function;

想一下,我们如果想不通过js来初始化,想通过下面这种方式来初始化该怎么做,

<Component template="xxxx" onXxx=""></Component>

我们之前自己实现了一个模板引擎,除了自定义指令的解析,当然也会把自定义组件的解析加进去。这样一棵组件树,我们只需要调用一次初始化方法,然后在解析组件树模板的过程中,把一个个组件初始化,组装好。这一些都是用户感知不到的,用户只需要写模板。

install Nuclear

npm install alloynuclear

Component-Native

之前为了减少不必要的渲染,我们加了个中间层-虚拟dom,除了可以带来性能的提示之外,我们可以有一些别的思考,比如我可不可以不只渲染成dom元素,渲染成安卓、ios原生的组件?经过思考,我们觉得这是可行的,逻辑依然用js来写,通过jscore来执行js,js需要调用的原生api由框架封装,提供给js。渲染部分,建立原生组件和和模板中组件的映射关系,渲染的时候生成对应的原生组件。逻辑的部分可以复用,除了渲染的是原生的组件,别的功能依然都有。

9159.com 3

思路是可行的,但是实现这些组件、提供供js调用的原生api,工作量肯定比较大,而且会有很多坑。

Nuclear如何做到同构的?

内置三条管线如下所示:

9159.com 4

比如一般前后端分离的开发方式,仅仅会走中间那条管线。而同构的管线如下所示:

9159.com 5

这里前后后端会共用option,所以不仅仅需要直出HTML,option也会一并支持给前端用来二次渲染初始一些东西。

其他组件化的框架

实现组件化的框架很多,比如Avalon、Ember、Konckout等等,都有各自的特点

组件化是一个趋势,现在有很多实现组件化的框架,W3C提出了web compoenents的标准:。这个标准主要由4种技术组成,html import、shadow dom、custom elment和html template。新的标准肯定会有兼容性的问题,goole推出了Polymer这个基于web components规范的组件化框架。

从最开始的jquery插件,到现在的各种组件化的框架、web components标准,组件化已经是一种必然的趋势,我们不仅要会去设计、封装组件,更要去了解组件的发展的前世今生,这样才不会在框架的海洋中迷失。

组件需要嵌套?只能复制粘贴原组件?

扁平无嵌套组件还是比较简单,对模板的字符串处理下,把绑定的事件全指向组件自身定义的方法,生命周期也好处理。在真正的业务里经常需要组件嵌套,这样也更利于复用。虽然大量模板引擎支持引用子模板、共享数据等,但是组件是有生命周期的,模板嵌套不能真正解决组件嵌套的问题。能支持组件嵌套并且声明式嵌套就那就再好不过了!

虚拟dom和diff算法

现在我们的组件渲染是直接渲染到dom元素,并且是全局的渲染。model改变不大的时候,也会全局重新渲染一次,会有很多不必要的dom操作,性能损耗。我们知道,计算机领域很多问题都可以加一个中间层来解决,这里也一样,我们可以不直接渲染到真实dom元素,用js对象来模拟真实dom元素,每次渲染渲染成这样的一颗虚拟dom元素组成的树。

 { name: 'a', props: { }, children: [ { name: 'a-1', props:{}, children:[] }, { name: 'a-2', props:{}, children:[] }, { name: 'a-3', props:{}, children:[] } ] }

这样可以把上一次的渲染结果保留,下次渲染的时候和上一次的渲染结果做对比,比较有没有变化,有变化的话找出变化的部分,局部增量的渲染改变的部分。这样能避免不必要的dom操作带来的性能开销。比较的过程我们可以叫他diff算法。引入了虚拟dom这一层,虽然会增大计算量和内存消耗,但是却减少了大量的dom操作。性能会有明显的提升。

数据变了?重新生成HTML替换一下?

怎么替换?先查找dom?什么?你还在查找dom?你还在背诵CSS选择器?替换一下?不能增量更新吗?或者diff一下吧?不要每次全部替换啊!

生命周期函数

我们把dom操作给封装了,也就是把dom元素的增删改给自动化了,组件对应的dom元素的创建和销毁或者是重新绘制更新dom的时候,想做一些操作,就不能做了,所以我们要在这些时刻暴露一些钩子,让开发者可以在这些时候去做一些操作。比如组件的dom初次渲染完的时候要去请求数据,比如组件销毁的时候要做一些资源释放的工作避免内存泄漏等。主要的生命周期钩子函数就这么四类,创建前后,挂载到dom前后,更新前后,从dom中移除前后。生命周期的名字可以叫beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed,也可以叫componentWillMountcomponentDidMountcomponentWillUpdatecomponentDidUpdatecomponentWillUnmountcomponentDidUnmount等。

组件嵌套

<script>
    var TodoList = Nuclear.create({
        render: function () {
            return '<ul> {{#items}} <li>{{.}}</li> {{/items}}</ul>';
        }
    });

</script>

<script>
    var TodoTitle = Nuclear.create({
        render: function () {
            return '<h3>{{title}}</h3>';
        }
    });
</script>

<script>

    var TodoApp = Nuclear.create({
        install: function () {
            //pass options to children
            this.childrenOptions = [{ title: "Todo" }, { items: [] }];
            this.length = 0;
        },
        add: function (evt) {
            evt.preventDefault();

            //this.nulcearChildren[1].option.items.push(this.textBox.value);
            //or
            this.list.option.items.push(this.textBox.value);

            this.length = this.list.option.items.length;
            this.textBox.value = "";
        },
        render: function () {
            //or  any_namespace.xx.xxx.TodoList 对应的 nc-constructor="any_namespace.xx.xxx.TodoList"
            return '<div>
                        <child nc-constructor="TodoTitle"></child>
                        <child nc-constructor="TodoList"  nc-name="list"></child>
                        <form onsubmit="add(event)" >
                          <input nc-id="textBox" value="{{inputValue}}" type="text"  />
                          <button>Add #'+ this.length + '</button>
                         </form>
                   </div>';
        }
    });

    new TodoApp({ inputValue: "" }, "body");
</script>

通过在父对象的install里设置this.childrenOptions来把option传给子节点。

Immutable

我们会在model变化以后去更新view,但是model有没有变化需要和之前的model做对比,model是一个对象,可能层次比较深,深层的比较是比较慢的,这里又会有性能的问题。针对这一问题,我们应该怎么去优化呢?我们都知道字符串是常量。jvm的内存空间分为堆、栈、方法区、静态域4个部分,方法区中有个字符串常量池,来存放字符串。也就是我们创建一个字符串,如果常量池中有的话,他会直接把引用返回给你,如果没有的话会创建一个字符串然后放入常量池中。对字符串的修改会创建一个新的字符串,而不是直接修改原字符串。编程语言基本都是这样处理字符串的,好处也是很明显的,设想一下,如果有一个长度为1000的字符串,要和另一个字符串做比较,那么如果字符串不是常量,那么完成比较就要要遍历字符串的每一个字符,复杂度为o。但如果我们把字符串设计为常量,比较时只需要比较两个字符串的内存地址,那么复杂度就降到了o。这种优化的思路是典型的空间换时间。

组件的model我们也可以实现为不可变(immutable)的,这样比较的时候只需要比较两个model的引用就可以了,会使性能又有一个大的提高。

首屏太慢?以前抽象的组件没法复用?

什么?首屏太慢?改成直出(服务器渲染)?以前代码没法复用?要推翻重写?什么?怎么搞?排期?产品不给排期?需求没变为什么要给排期?

下面来看下Nuclear怎么解决上面问题。

9159.com 6

angular2

  • 支持模板的语法,指令、过滤器、插值表达式
  • decorator的方式来声明组件
  • 支持IOC
  • 支持组件化
  • 支持双向绑定MVVM
  • 创建、更新、销毁前后的生命周期函数
  • 和typescript结合紧密

需求变更?找不到在哪改代码?

大型项目如游戏什么的为啥都是面向对象式的写法?如果一个组件刚好又能是一个Class那就再好不过,Class base可以更方便地抽象现实世界的物体及其属性或者逻辑算法,所以甚至有些编程语言都是面向对象的(这里逆向逻辑),如JAVA、C#...整体过程式的代码对于大型项目几乎没法维护(如基于jQuery就能容易写出整体都是过程式的组织结构),整体OO,局部过程式是可以接受的。

vue

  • vue提供了内置的专用的模板引擎,有指令、过滤器、插值表达式等功能,有内置的指令过滤器,也可以注册自己扩展的指令过滤器。而且提供了render函数,可以结合babel来实现jsx的编译。
  • vue提供了双向绑定MVVM
  • vue有完善的生命周期函数,包括create前后,mount前后,update前后和destory前后
  • vue2.x加入了虚拟dom,可以减少不必要的渲染
  • vue社区有weex这个做原生渲染的框架
  • vue可以结合vuex来做全局状态管理

目前来看,团队内部前端项目已全面实施组件化开发。组件化的好处太多,如:按需加载、可复用、易维护、可扩展、少挖坑、不改组件代码直接切成服务器端渲染(如Nuclear组件化可以做到,大家叫同构)...
怎么做到这么强大的优势,来回忆下以前见过的坑,或者现有项目里的坑。

但是这种组件的方案或者说jquery本身就有很多问题:

Nuclear Github

模板引擎

现在我们把需要把数据填充到模板需要用拼接字符串的方式,这样的代码写起来很是繁琐,针对这个问题,已经有了成熟的解决方案,我们可以选用某一个模板引擎,像ejs,jsmart,jade之类的。但是我们需要的是一个能和我们的组件结合紧密的一个模板引擎,我们需要自己实现一个,这样,我们可以直接直接取组件中的数据,调用组件的某个方法,甚至自己扩展一些模板的功能。比如,我们如果想实现这样一个模板引擎,

 <table> <my:forEach items="goodsList" var="goods"> <td>${goods.name}</td> <td>${goods.price}</td> <td>${goods.amount}</td> </my:forEach> </table>

看上去是不是比较像jsp的语法,其实jsp就是一个专用的模板引擎,他有page,session,application,request,response等隐式对象,可以直接取几个域中的数据,而且也可以支持自定义标签和自定义el函数。想想该怎么实现。一种思路是通过xml的解析,xml解析方式有dom和sax两种,就是分析出有什么标签有什么属性。然后对应的属性做什么操作。属性和对应操作我们给封装起来,叫做指令。开发者可以自己去注册一些自定义的指令。模板在解析的时候解析出对应的属性就会执行对应的操作。

Nuclear优势

1.节约流量
2.提升用户体验
3.加载更加灵活
4.Dom查找几乎绝迹
5.搭积木一样写页面
6.提升代码复用性
7.可插拔的模板引擎
8.Lazy CSS首屏更轻松
9.Nuclear文件大小6KB (gzip)
10.零行代码修改无缝切到同构直出
...
...

  • 浏览器端效率最低的就是dom操作,因为会触发reflow,repaint,jquery是操作dom的一个库,基于jquery封装的插件当然也避免不了频繁的操作dom,所以这样的的插件如果代码写的时候不注意,效率很可能会比较低。
  • jquery只是一个库,而不是决定代码组织方式的框架,没有固定的代码规范,每个人都会有自己的编码风格,虽然可以规定一些规范,但毕竟不是强制的。如果团队成员,项目规模比较小的时候还好,随着项目、团队规模的扩大,这样的代码会越来越难以维护和复用。

服务器端渲染

function todo(Nuclear,server) {
    var Todo = Nuclear.create({
        add: function (evt) {
            evt.preventDefault();
            this.option.items.push(this.textBox.value);
        },
        render: function () {
            return `<div>
                      <h3>TODO</h3>
                      <ul> {{#items}} <li>{{.}}</li> {{/items}}</ul>
                      <form onsubmit="add(event)" >
                          <input nc-id="textBox" type="text"  value="" />
                          <button>Add #{{items.length}}</button>
                      </form>
                    </div>`;
        },
        style: function () {
            return `h3 { color:red; }
                   button{ color:green;}`;
        }
    },{
        server:server
    });
    return Todo;
}

if ( typeof module === "object" && typeof module.exports === "object" ) {
    module.exports =  todo ;
} else {
    this.todo = todo;
}

通过第二个参数server来决定是服务器端渲染还是客户端渲染。server使用的代码也很简单:

var koa = require('koa');
var serve = require('koa-static');
var router = require('koa-route');
var app = koa();
var jsdom = require('jsdom');
var Nuclear = require("alloynuclear")(jsdom.jsdom().defaultView);

var Todo = require('./component/todo')(Nuclear,true);



app.use(serve(__dirname + '/component'));

app.use(router.get('/todos', function *(){
    var  str = require('fs').readFileSync(__dirname + '/view/index.html', 'utf8');
    var todo = new Todo({ items: ["Nuclear2","koa",'ejs'] });
    this.body = Nuclear.Tpl.render(str, {
        todo:  todo.HTML
    }); 
    Nuclear.destroy(todo);
}));


app.listen(3000);

浏览器端使用的代码:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
 {{{todo}}}

 <script src="./nuclear.js"></script>
 <script src="./todo.js"></script>
 <script>
    var Todo= todo(Nuclear);
    new Todo('body');
 </script>
</body>
</html>

这样,组件的代码不需要任何变更就可以在server和client同时使用。

全局状态管理

组件之间可以通过传递参数来通信。如果只是父子组件通信比较简单,但是如果需要通信的两个组件之间间隔的层次比较多,或者是兄弟组件,那么之间互相通信就很麻烦了,需要多层的传递或者是通过父组件做中转。针对这个问题,有没有什么别的思路呢?其实可以引入一个中介者来解决,就像婚姻中介,如果男方自己去找女方,或者女方自己去找男方都不太方便,这时候可以找一个中介,男方和女方分别在那里注册自己的信息,然后等中介有消息的时候通知自己。这样男方和女方就不需要相互联系,只要和婚姻中介联系就可以了。

9159.com 7

类似的,我们可以创建一个store来存储全局的信息,组件在store那里注册,当一个组件向store发送消息的时候,监听store的组件就能收到消息,从store中取出变化后的数据。

9159.com 8

CSS层叠样式?保佑不要污染别的HTML!

在web前端,一般一个组件必须要有骨架HTML和装饰的CSS以及JS逻辑。而CSS要是可以是局部作用域那就再好不过了!就不用写长长的前缀了,浪费带宽不说,而且费劲。

.ui-popup-arrow-xx-xxxxx-xxxx-container {

}

这回够长了吧,不会污染别的HTML了吧。真的太长了,没有办法,因为CSS不是局部的,怕污染其他的HTML,规划好长长的namespace、module是以前的最佳实践。

fiber

想一想我们的组件化框架还有哪里有问题。我们知道浏览器中每个页面是单线程的,渲染和js计算共用一个线程,会相互阻塞。model改变后要生成虚拟dom,生成虚拟dom、虚拟dom之间的diff可能会计算比较长的时间,如果这时候页面上有个动画在同时抢占着主线程,那么势必会导致动画的卡顿。每个痛点的解决,都能会带来性能的提升,为了追求极致的性能,这个问题我们也要想办法解决。

虚拟dom是一颗树形的结构,生成或比较一般都是递归的过程。我们知道所有的递归都可以改成循环的方式,只要我们可以一个队列来保存中间状态。把递归改成循环后,就可以异步化分段执行了。先执行一段计算,然后把执行状态保存,释放主线程去做渲染,渲染完之后再去做之后的计算。这样就完美的解决了浏览器环境下计算和渲染之间相互阻塞的问题了,性能有了进一步的提升。这种资源的竞争在计算机中随处可见,就像cpu的进程调度,每个进程的计算都要用到cpu,操作系统就需要用一种合理的方式来分配cpu资源。cpu调度策略有很多几种,比如分时,按照优先级等等,都是把一个大的计算量给分成多次来执行,暂停执行的时候把上下文信息保存下来,得到cpu的时候再恢复上下文继续执行。计算量分段,类似切菜,我们把这种调度策略叫fiber,即纤维化。没有fiber之前的虚拟dom计算是这样的

9159.com 9

fiber之后是这样的

9159.com 10

完美解决了浏览器的单线程下单次计算量过大会阻塞渲染的问题。

事件绑定

var EventDemo = Nuclear.create({
    clickHandler: function (evt, target, other1,other2) {
        //MouseEvent {isTrusted: true, screenX: 51, screenY: 87, clientX: 51, clientY: 21…}
        console.log(evt);
        //<div onclick="Nuclear.instances[0].clickHandler(event,this,'otherParameter1','otherParameter2')">Click Me!</div>
        console.log(target);
        //otherParameter1
        console.log(other1);
        //otherParameter2
        console.log(other2);

        alert("Hello Nuclear!");
    },
    render: function () {
        return '<div onclick="clickHandler(event,this,'otherParameter1','otherParameter2')">Click Me!</div>'
    }
})

new EventDemo({ seen: true }, "body");

双向绑定MVVM

现在我们的组件还是避免不了要大量的操作dom,这必定会有很多的性能问题。能不能把dom操作也给封装起来,开发者不需要再去操作dom,只需要管理好数据就可以了呢。想一下后端开发,最频繁的就是增删改查,这样的sql语句是经常要写的,于是后端有了orm框架,比如hibernate,映射好实体类和数据表,类的属性和字段的关系之后,只需要调用hibernate提供的Session类的增删改查的方法就好了,sql语句会自动生成,比如mybatis,映射好方法和写在xml中的sql语句的关系,之后只要调用对应的方法就可以了,不需要自己去写sql语句。数据库中的表和java的实体类建立了映射关系就能够做到开发时不需要写sql语句,那么我们建立好数据和dom,也就是model和view之间的关系是不是也就可以不写任何一句dom操作的代码,只去管理数据呢,然后view会自动同步呢。当然是可以的,从model到view的绑定,我们可以监听model的变化,变化的时候就去通知view中的Observer,然后那个Observer去操作dom,去更新视图。监听model的变化,很容易想到的是es5中的Object.defineProperty这个api,他可以定义set方法,拦截对对象属性的赋值操作。

 //观察者的队列 var observers = []; observers.push(new Observer; var obj = {}; var value = ""; Object.defineProperty(obj, 'name', { get: function() { return value; }, set: function { value = val; //数据改变,通知观察者,去更新view var target = this; observers.forEach(function(observer,index){ observer.notify; }); } });

当然es6提供的Proxy这个更高层次的封装类也可以。

 // 观察者的队列 var observers = []; observers.push(new Observer; let obj = {}; let proxy = new Proxy(obj, { get: function (target, key, receiver) { return Reflect.get(target, key, receiver); }, set: function (target, key, value, receiver) { Reflect.set(target, key, value, receiver); for(let observer in observers){ observer.notify; } }})

至于从view到model的绑定,其实就是监听用户输入的一些操作,监听表单的事件,然后去根据用户输入的数据和映射关系,去同步model。

循环

var LoopDemo = Nuclear.create({
    render: function () {
        return '<ul>{{#list}}<li>姓名:{{name}} 年龄:{{age}}</li>{{/list}}</ul>'
    }
})

var ld = new LoopDemo({
    list: [
        { name: "dntzhang", age: 18 },
        { name: "vorshen", age: 17 }
    ]
}, "body");

setTimeout(function () {
    //增加
    ld.option.list.push({ name: "lisi", age: 38 });
}, 1000);

setTimeout(function () {
    //修改
    ld.option.list[0].age = 19;
}, 2000);

setTimeout(function () {
    //移除
    ld.option.list.splice(0, 1);
}, 3000);

Array的变更也能监听到,能够自动触发Dom的变更。

很多年以前,我们写网页的时候都是这样的:根据设计稿写好一个页面的html和css,然后再去写js来做一些交互。如果遇到同样功能的代码,最简单粗暴的方式是复制粘贴,如果为了更好的复用性,就封装个jquery的插件,需要用的时候就引入插件,调用初始化的方法,传入参数,比如一个日历、一个轮播图。在那个web野蛮生长的年代,这样的插件产生了很多,那个时代的前端工程师必须会自定义jquery的插件。那时候也有一些组件库,比如extjs、bootstrap、jquery ui等。

模板,样式,交互逻辑

组件最基础的就是这三部分。样式我们可以不做封装,通过全局引入然后加个命名空间的方式来区分组件。模板可以挂载到dom树上通过选择器来取,或者直接传入一段模板字符串。交互逻辑的部分,我们会通过事件绑定调用组件上的一些方法。

 class Component{ constructor({el,template,onXxx}){ this.el = el; this.template = template; this.onXxx = onXxx; this.render(); this.bindEvents(); } render(){ var ele = document.querySelector; ele.innerHTML = this.template; } bindEvents(){ this.el.querySelector.addEventListener('click', this.onXxx) } }

现在我们的组件有了最初的模型,模板,逻辑,事件绑定,可以传参数来进行一些定制

本文由9159.com发布于前端,转载请注明出处:也就是说一个组件需要封装的就是关联的html、

关键词: