一个 JS 框架需要做什么

开发 前端
那这篇文章要说的是什么呢?就是假设你现在什么都没学,就靠基本功,去完成一个静态页面,当然也有业务逻辑,包括数据的 CRUD、动画,怎么做?有个关于 VanillaJS 的梗不知道大家看过没,你一定会会心一笑的。

为什么这么说?不知道各位有没有发现,虽然前端发展快,但一些有名的框架至少会火热很长时间,比如 Backbone、React、Ember 。如果有心要学,肯定有足够的时间把它学会,毕竟事实摆在面前,很多公司的上线产品就是用 React 来写的,比如 Teambition 的简聊,貌似它是从 Backbone 重构过来的。然而,很多同学在接手新项目时,常常会不知所措,不知道用什么技术去做,或者说,只依赖于擅长的技术,就算在一些场景中它可能并不是最适合 的。

因为这些同学平时不够努力吗?不是吧。他们可能会看书到很晚,浏览很多博客,就是为了去了解 CORS 的应用,或者是想知道为什么 Angular 中的 scope 在某些时候不能双向绑定了。对,时间是花了,但遇到问题还是一头雾水。可能前端就是这么一份工作吧,怂恿你去学游泳,蛙泳、自由泳、蝶泳,海啸来了照样被 冲走……

那这篇文章要说的是什么呢?就是假设你现在什么都没学,就靠基本功,去完成一个静态页面,当然也有业务逻辑,包括数据的 CRUD、动画,怎么做?有个关于 VanillaJS 的梗不知道大家看过没,你一定会会心一笑的。

没有 jQuery 了,没有 Bootstrap 了,扔掉所有你引以为傲的武器,但大恶魔 IE 6 还在。具体的需求不给了,反正给了你们也不会照着去实现,真有心要做的话,可以做一个 todo app 吧。

[[153714]]

DOM 查询

在没有第三方框架可以用的时候,如果真的按照功能列表,从第一条实现到最后一条,每个模块用自执行匿名函数包起来,所有代码写在一个文件中,看上去十分合理,但真这么做的话,恐怕你会疯掉吧。哦,好处是你可以跟别人吹嘘今天写了三四百行代码,产量很高呢!

所以,不使用第三方框架,我们可以自己写,它的功能只要符合应用场景就可以了,不用去考虑各种不会发生的奇葩情况。

好,开始。我们最依赖的功能是通过 CSS 选择器获取相应的 DOM 元素,这里只使用兼容性最高的方式,就是 id 和元素名选择器。

 

  1. var idRegex         = /^#[\w\-]+/i, 
  2.     tagRegex        = /^[a-z]+/i; 
  3.       
  4. function query(selector, context) { 
  5.   context = context || document; 
  6.  
  7.   if (idRegex.test(selector)) { 
  8.     return document.getElementById(selector.substring(1)); 
  9.   } else if (tagRegex.test(selector)) { 
  10.     return context.getElementsByTagName(selector); 
  11.   } 
  12.  
  13.   return null

对了,我把所有 DOM 操作放在了 F.DOM 命名空间下,所以是这样使用 query 方法的:

F.DOM.query('#id');

的确比 jQuery 的 $('#id') 方式麻烦很多,但“子不嫌母丑,狗不嫌家贫”,自己写的代码,再烂也要用下去。

另外一些必须的操作就不把代码贴出来了,比如说 addClass、removeClass、hasClass 等。

DOM 事件

如果有同学参加过面试的话,我想“怎么去监听一个 DOM 事件?请尽可能考虑浏览器兼容性”这个问题是经常会问到吧。这儿写一个可行方案吧。

  1. / 监听 DOM 事件 
  2. function addEventListener(el, event, handler, useCapture) { 
  3.   if (el.addEventListener) { 
  4.     el.addEventListener(event, handler, useCapture); 
  5.   } else if (el.attachEvent) { 
  6.     el.attachEvent('on' + event, handler); 
  7.   } else { 
  8.     // not support 
  9.   } 
  10.  
  11. // 取消 DOM 事件 
  12. function removeEventListener(el, event, handler, useCapture) { 
  13.   if (el.removeEventListener) { 
  14.     el.removeEventListener(event, handler, useCapture); 
  15.   } else if (el.detachEvent) { 
  16.     el.detachEvent('on' + event, handler); 
  17.   } else { 
  18.     // not support 
  19.   } 

我知道大家可能有更好的,或者更完善的方案,但抱歉这里讨论的重点不是它。

关于 DOM 事件方面,还有一些有用的方法,比如 preventDefault 和 stopPropagation 也可以自己去封装一下。然后这儿想讨论一下 DOM 加载完成的事件。jQuery 中我们会这么用:

  1. $(function() { 
  2.   // ready 
  3. }); 

如果我们也想封装一个类似的方法,可能会这么写:

addEventListener('window', 'load', callback);

可是 load 事件是在什么情况下触发的呢?当页面上的所有资源,包括图片,加载完之后才触发!也就是说,如果图片很多,网速很慢,那触发 load 要花很长时间。在本地调试时不会有这种延迟的问题,所以往往会被忽略。

那怎么改正呢?第一,可以把 <script> 放到 <body> 中所有元素的下方,就不需要监听任何“加载完成”的事件了。第二,监听 DOMContentLoaded 事件,IE 9+ 支持。至于如何兼容低版本浏览器,可以看这篇文章 (addDOMLoadEvent)

#p#

组件式开发

“组件”这个词其实来源于很多框架,比如 Backbone 中的 View,React 就更不用说了,它为了组件化专门规定了 JSX(当然它有更宏伟的 goal) 。我们这里讨论的组件也是差不多的意思,就是按照功能,把页面上分成一个个独立的模块,模块之间通过消息(事件)进行沟通。关于模块耦合,JSX 是通过类似于 HTML 标签嵌套的方式来表现的,而我们自然没这么高级,就直接把依赖的模块注入到其他模块中,比如:

  1. /** 
  2. * 应用顶层,构造一些页面中用到的组件 
  3. */ 
  4. function App() { 
  5.   F.Component.call(this); 
  6.  
  7. App.prototype = new F.Component(); 
  8.  
  9. F.extend(App.prototype, { 
  10.   constructor: App, 
  11.   init: function() { 
  12.     this._blogPost = new BlogPost('#blog-post'); 
  13.     this._blogList = new BlogList('#blog-list'this); 
  14.     this._newsList = new NewsList('#news-wrapper'); 
  15.   } 
  16. }); 
  17.  
  18. new App(); 

其中,BlogPost 是发布日志的组件,BlogList 是日志列表。发布日志后必然会显示到列表中,所以在构造日志列表时,会把 BlogPost 注入到 BlogList 中。

每个组件可以提供一个 id 选择器,表示该组件需要绘制在哪个元素内。

消息传播机制

关于 BlogPost 和 BlogList,大家可以想象微博的主页,它上面是一个发布框,下面是微博列表,就是这样一个界面。

当微博发布之后,列表中需要增加新发布的内容,这个过程是谁给谁发消息?按照面向对象的思想,应该是类似于这样:

// 在 发布框组件 中调用 列表组件 的方法
blogList.add(item);

显然是 BlogPost 依赖于 BlogList 对吗?但貌似我们上面的代码不是这个逻辑,而是反过来。那么实际情况就成了这样:

// 发布框:BlogPost 中触发事件
this.emit('add', item);

// 列表:BlogList 中监听事件
this.listenTo(blogPost, 'add', handler);

// 由 handler 处理发布事件

嗯,代码变多了,看来得强行圆回来。

为什么我强烈建议使用后者?假设过了一段时间,某个充满创意的策划突然告诉你,当发布微博之后,可以显示到朋友圈(假设有这么个东西)。那么前者的方式会怎么做?是不是首先给这个发布框多注入一个依赖,即朋友圈,然后调用朋友圈的某个方法?

如果再过段时间,又有新创意了,是不是又得给发布框加依赖了?最后搞得发布框依赖于微博列表、依赖于朋友圈、依赖于其他 8 个组件,真不想用水性杨花来形容它。

这个问题很常见吧?如果用消息机制的方式就会好很多,只需要在新增加的组件中监听发布框的 'add' 事件就可以了。

如果你能接受这个方式,可能想知道怎么去简单地实现它。

 

  1. var Event = F.Event = function Event() { 
  2.  
  3.   // 该组件相关的所有的事件都保存在 _events 对象中 
  4.   // 格式 - {'eventName': [{handler, context}*]} 
  5.   this._events = {}; 
  6. }; 
  7.  
  8. F.extend(Event.prototype, { 
  9.  
  10.   // 监听事件 
  11.   on: function(event, handler, context) { 
  12.  
  13.     if (!this._events[event]) { 
  14.       this._events[event] = []; 
  15.     } 
  16.  
  17.     this._events[event].push({ 
  18.       handler: handler, 
  19.       context: context || this 
  20.     }); 
  21.   }, 
  22.     
  23.   // 触发事件 
  24.   emit: function(event) { 
  25.     var events    = this._events[event] || [], 
  26.         args      = []; 
  27.  
  28.     // 第一个参数为事件名,后面的参数需要传给处理该事件的方法,记录到 args 中 
  29.     if (arguments.length > 1) { 
  30.       args = slice.call(arguments, 1); 
  31.     } 
  32.  
  33.     // 回调时需要传入参数 
  34.     events.forEach(function(v) { 
  35.       v.handler.apply(v.context, args); 
  36.     }); 
  37.   } 

把重点部分贴了一下。第一,这个 Event 是所有组件的基类,所以每个组件都有 on 和 emit 方法。第二,F.extend 的作用就是把后面对象的方法和属性直接赋值给第一个,extend 的意思是“扩展”而不是“继承”,这点别混淆了。第三,通过改变上下文(就是 this),当一个组件的事件触发时,由另一个组件处理。

由于上面省略了很多代码,一般还要考虑的情况有,怎么取消监听,怎么实现例子中的 listenTo 等。

组件继承

关于继承,这篇文章略有提到(4.2 通过 prototype 实现继承)。

这里就写个 F.extend 技巧好了。一般来说,会在继承之后修改 prototype 的 constructor 属性,并在它上面定义很多方法,就变成了:

  1. A.prototype = new B(); 
  2. A.prototype.constructor = A; 
  3. A.prototype.f1 = function() {}; 
  4. A.prototype.f2 = function() {}; 

大家不妨去实现一个 extend 方法,让代码变成:

  1. A.prototype = new B(); 
  2. extend(A.prototype, { /* 原型上的方法和属性 */ }); 

DOM 事件代理

一个组件往往会对应一个页面区域,那在这个区域上会有单击按钮等一些 DOM 事件。由于在初始化组件时,这些元素还没有追加到 DOM 上去,所以就不能使用 addEventListener 这个方法来监听单击事件。那要怎么监听呢?

两种方法。一,在生成 HTML 片段时,设置元素的 onclick 属性,比如:

container.innerHTML = '<a href="#" onclick="delegate(' + id + ')">click</a>';

技巧在于,这个 delegate 方法是全局的,并且它能通过组件的 id 来找到对应的组件对象,再调用该组件的回调函数。

二,在子元素添加到 DOM 之前,父容器是存在了的,所以可以对父容器监听 click 事件,然后对 event.target 判断。

addEventListener(container, 'click', delegate)

无论是哪种方法,具体实现时肯定会碰到问题,这些都是预期范围内的,所以不用沮丧。

封装 AJAX

同样地,面试官极有可能问你“请用原生 JS 封装 AJAX 的 GET 请求”。你应该已经熟稔于心,或者至少有笔记记录了怎么写。

现在要讨论的是,如何利用“消息机制”去避免回调。jQuery 中的 ajax 方法需要一个 success 的回调,加上配置 url 等信息,导致完成一次请求所用到的代码非常复杂,很难阅读。ES 6 推出了 Promise,使得我们可以用同步的语法去做异步的事,阅读性得到了提升。

由于我们不能用 Promise,所以就发消息吧,也很优雅。

 

  1. F.extend(Request.prototype, { 
  2.   constructor: Request, 
  3.   get: function() { 
  4.     var xhr   = createXHR(), 
  5.     self  = this
  6.  
  7.     xhr.open('GET'this._api, true); 
  8.     xhr.onreadystatechange = function() { 
  9.       if (xhr.readyState === 4 && xhr.status === 200) { 
  10.         self.emit('success', xhr.responseText); 
  11.       } 
  12.     }; 
  13.     xhr.send(); 
  14.   } 
  15. }); 

代码并不全,说明一下,Request 继承自 Event,构造时需要传入一个 url,表示请求的地址。用法类似于:

// 在某个组件中,this 指向该对象的实例
var r = new Request('http://www.example.com/blogs');

r.get();
r.on('success', callback, this);

貌似有点像 Angular 中 new Resource(url); 的用法。

功能性兼容 (Polyfill)

这部分主要是为了兼容比如说 IE 6 不支持 HTML5 元素的样式、数组中的高级用法(forEach 和 map 等)、字符串的高级用法(trim)、Function 的 bind 等。

因为是临时编写的框架,所以业务逻辑的代码中需要什么,就补什么。

兼容 HTML5 元素你可以这么做,很简单:

document.createElement('header');

把所有用到的元素都 createElement 一遍就行了,这段代码必须放在 <head> 中。

至于兼容 forEach、map、bind 这一些,网上应该有一大堆吧,这儿只是为了提醒各位去考虑这些方面。然后,网上的兼容策略可能很复杂,没必要,大家完全可以尝试自己去写,“过早的优化是万恶之源”(这是个人最喜欢的名言了)。

浅谈模板语言

这个虽然不是必须的,并且在我目前写的代码中也没有考虑到,但经过一位高人提醒,就觉得,咦,很多听上去高大上的技术,从原理来讲都是柴米油盐这些基础知识。

如果各位之前对 underscore 中的 _.template 方法并不了解,看完这节应该会帮助你一些。

假设要生成一个用户名的链接,用模板可以这么写:

<a href="#">{{ name }}</a>

而用现在的方式是这么做的:

var html = '<a href="#">' + model.name + '</a>';

那么怎么通过模板的方式去做,不用费劲地拼接字符串呢?答案是正则。

  1. function parse(template, model) { 
  2.   return template.replace(/\{\{\s*(.+?)\s*\}\}/g, function(match, p1) { 
  3.     return model[p1] || match; 
  4.   }); 

替换时,match 表示由正则匹配到的字符串,这里是 '{{ name }}',p1 表示匹配到的字符串中第一个组的值,这里是 'name',问号 ? 是阻止贪婪匹配,最后由返回值替换 match,这里是 model.name 。

小结

文章中的代码可能只展示了一小部分,因为我主要是想说明一些值得考虑的点,并不是教程,至少大家可以用这些作为草稿去开始。

绕来绕去,JS 中的语法也屈指可数,那为什么在学习新技术的时候会很焦灼呢?基础是一个原因,没有基础就造不了任何建筑;知识面是另一个,解决问题时最怕的是不知道有某 个答案存在,使用 API 时最讨厌的就是不知道它已经提供这个功能了。所以平时应该多看一些文章,有想法就记下来,无论是笔记的方式还是博客的方式都行,写博客可以强迫你把想法表 达出来,这跟“看懂”是不一样的。

至于学习的性价比,我只能说,不要停!

你可能觉得,唉,React 是很好,但眼下又用不到,就算学了也没用,还不如把时间花在绩效上。自己写代码永远是个封闭的空间,包括因为遇到什么问题被动地去 google 也好,如果不是主动去看新鲜事物,能力的增长是十分缓慢的。为什么会不断有新技术产生?这个事情本身就在告诉我们,需要用新的角度去解决新的问题(或者旧 的)。举两个例子,IE 在 Windows 系统上是不会自动更新的,现在它死了(Windows 10 Edge);Adobe Flash 适应不了移动平台,而安全漏洞又频出,现在它马上要死了(Adobe 的高层表示并不 care,因为 Flash 只占很少一部分营收)。

互联网它不跟你讲人情的,适者生存。

 
责任编辑:王雪燕 来源: segmentfault
相关推荐

2020-10-29 18:36:02

DenoNode.jsJavascript

2011-02-25 14:26:18

ProFTPDMySQL

2011-11-15 10:32:27

2011-11-10 10:04:20

2023-06-29 00:16:45

2021-03-05 14:55:31

大数据面试跳槽

2021-10-28 22:51:24

比特币黄金货币

2011-11-21 09:40:38

产品经理

2016-03-09 09:42:15

App产品经理项目启动

2023-09-28 07:23:14

AB 实验体系算法

2021-04-09 09:45:33

GitOps环境应用程序

2012-08-24 10:46:12

程序员

2015-11-13 16:25:45

电商

2020-08-07 10:40:56

Node.jsexpress前端

2017-09-04 20:20:04

数据中心DCIM工具传感器

2021-09-17 13:49:34

数字化

2022-04-27 05:55:43

去QA化自动化测试开发

2017-11-14 11:12:50

Go语言编译器

2015-10-26 15:45:33

CIBN

2014-04-29 10:50:16

池建强
点赞
收藏

51CTO技术栈公众号