Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端工程化下的组件设计原则漫谈 #5

Open
slashhuang opened this issue Oct 26, 2016 · 0 comments
Open

前端工程化下的组件设计原则漫谈 #5

slashhuang opened this issue Oct 26, 2016 · 0 comments

Comments

@slashhuang
Copy link
Owner

slashhuang commented Oct 26, 2016

前言

前端项目已经走向工程化,这个事实已经被大多数前端工程师所认识到。

在UI + Model+ Logic三块并重的前端领域,组件化 + 模块化 + 规范化 + 工程化是前端领域的四个现代化。
对于以上四个方面,业界已经有不少讨论,无论是react和vue等前端框架对组件化的实践、web-components提出的对组件设计规范的约束、webpack对amd、cmd、commonJS、umd模块规范的实现,还是phatomJS在腾讯、谷歌、fb等的同构JS架构实践。

业界的前端大咖们已经在不经意间,将前端的各种新理念推送给每一位前端开发工程师。

写一个组件具备的基本条件

形式上来看: 组件是由html+css+js组合而成的。

功能上来看: 组件管理着自己的内部逻辑、同时也能给其他模块提供数据调用接口。

如何写一个组件

借鉴react、vue框架的设计理念,我们来看下如何安排一个简单的html+css+js结构

  • html ==> 采用字符串或者模板引擎来处理【仅仅处理首次渲染】
  • js ==> 在html的基础上挂载事件及处理逻辑
  • css ==> 剥离在html和js之外【本文的讨论均不涵盖css】

JS如何代理model到view映射的职责

抽象前端的model

model一般而言可以分为UI层面和功能层面的数据模型,同时UI的数据来源必须只有一个。在这个共识基础上,我们可以设计一个类似mongo或者redis的前端运行时数据管理方案。

以一个hashMap前端数据结构为例,它一般会长如下的样子

       var model={
             state<HashMap>, //组件内部的数据集合static state
             props<HashMap> //组件动态的数据集合dynamic props
       }

然而如果model的数据结构按照如上设计后,UI会因为有state和props两种hash表而无法统一更新的数据来源。

同时更新策略也无法对比本次和上次model的不同而做出弹性处理,所以我们在这个基础上修正model的设计方案如下。

       var staticModel = {
             state<HashMap>, //组件内部的数据集合static state,不存在数据对比
       }
       var previousModel={
             props<HashMap> //组件动态的数据集合dynamic props,存储组件的上次model
       }
      var currentmodel={
             props<HashMap> //组件动态的数据集合dynamic props,存储组件的本次model
       }

如上的model基本满足我们对UI的处理方案了,接下来开始设计model的对外接口。

抽象model的接口

针对以上model的设计方式,我们设计setState(),updateProps(),getModel()来完成一个基本的model更新接口,比如基本的功能可以设计如下。

       var setState=function(obj){
               Object.assign(staticModel,obj)
       }
      var updateProps=function(obj){
               previousModel = currentModel;
               currentmodel = Object.assign({},currentmodel,obj);
             //这里只是伪代码,比如react-redux里面是采用的shallowequal来处理的
              if(previousModel!=currentmodel){ 
                     //执行controller逻辑
               }
       }
     var getModel=function(obj){
               return { staticModel,currentmodel,previousModel}
       } 

设计controller以便完成model到UI更新策略

所有的UI更新应该发生在model接口调用、数据更新完毕后,这时model和view的映射关系是如何建立的对于组件的性能提升至关重要。

为了快速的完成dom的更新。model到view的映射关系设计应该尽量的精确,同时dom的更新层级应该尽量的少。

借鉴主流的react前端框架

以虚拟dom框架的代表react为例,它提供的方案是在dom节点添加data-reactid来处理model和dom节点的映射关系。
同时由于JS速度快于dom,react对dom更新策略全都发生在JS运行时。具体更新策略按照如下几步执行。

  1. 以setState调用的component为起点,生成新的model
  2. 对比新生成的model和旧有的model,如果有不同,则执行生成新的虚拟dom策略
  3. 按照虚拟dom本身的attributes,innerHTML,children来比对数据
  4. 递归children调用patch diff对比操作,执行remove,update,move ,appendNode等操作,完成dom更新。

然而,以上策略对于一个简单的组件,尤其是jquery组件来说,实在太重了。

我们可以在借鉴部分思想的基础上将组件的controller设计的更加颗粒化。

设计组件的简单controller以完成model到UI的更新策略

对于dom的映射关系,如果不采用虚拟dom【react】或者遍历dom树添加reference【vue/angular】来建立model和view映射关系的话。

目前我能想到的就是手动添加data-model-id规则来标示这种关系了。

比如建立一个规则,在dom上标注data-model-id='color,height'就表明这个节点依赖color和height两个键值。

我们根据以上的一些讨论和启发,来写一个简单controller。

       var html= '<div data-model-id='color,height'  id='hello'>hello</div>';
       var controller={ 
           nodeList:$(['data-model-id']),
           changeNodeList:(key)=>{
                  return this.nodeList.filter((node,key)=>{
                            return     node.data('modelId').split(',').indexOf(key)>-1
                  ])
          },
          compileDom(dom){
                 //这里面写dom如何修正,比如mustache语法的数据绑定规则或者underscore之类的
           },
           model2UI:(key)=>{
                   let model = model.getModel();
                   let changeNodeList = this.changeNodeList(key);
                   this.compileDom(changeNodeList);
            } ,     
            events:()=>{
                    let self =this;
                   //可以和vue一样写成内联的形式,提取依赖对dom统一挂载事件
                    this.nodeList.addEventListener('click',(e)=>{
                                     model.setState({color:red});
                                     self.model2UI('color')
                     })
             }
       }

以上的controller实现技术已经包括了模板提取技术、dom更新策略、view和model映射关系的一个简单实现了。

事实上,对于日常写组件的需求,在使用jquery等工具的时候,完全可以抛弃view=>model映射关系的处理,而仅仅保留controller和events。

这样的组件也是MVC分层及其清晰的。

对外接口

一个组件不可能完全独立于其他模块,这个时候就需要提供对外接口了。

一个简单的方案就是_定义规范_,比如每个组件都必须实现prototype.subscribe(callback)方法,在每次数据更新的时候,就可以通知其他组件,完成基本的通信需求了。

结语

对于市面上的框架来说,对于组件层面的渲染而言,vue的dom输出非常干净清爽,没有任何框架的痕迹。

而对于react、angular来说,输出的dom是在太多框架的痕迹。然而对于这个,可以不认为是个问题。

挺好奇vue对于view<=>model的映射关系建立是如何做的,大致猜测下是vue component在ready的时候,就完成dom节点referernce的建立。

我觉得这对于model和dom节点分层的抽象来说,是个很大的技术难点。

回归主题。前端组件的设计采用MVC分层的方式,好处就是一目了然、便于维护、功能更加封装。

不管是用jquery还是框架,这种设计原则将会在未来几年内逐步普及。

完。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant