一个 JS 框架需要做什么-阿里云开发者社区

开发者社区> 行者武松> 正文

一个 JS 框架需要做什么

简介:
+关注继续查看

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

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

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

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

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)

组件式开发

“组件”这个词其实来源于很多框架,比如 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 只占很少一部分营收)。

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


来源:51CTO

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
easyUi 框架中的JS文件传递参数的区别
1.情景一 //JS文件 ajax的请求url : parent.baseUrl+"user/customer/findOne/" + id, //后台JAVA代码接收参数 @RequestMapping(value = "findOne/{id}") @ResponseBody ...
552 0
怎么设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程
6896 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4477 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
7746 0
Node.js Web 开发框架大全《路由篇》
  这篇文章与大家分享优秀的 Node.js 路由(Routers)模块。Node 是一个服务器端 JavaScript 解释器,它将改变服务器应该如何工作的概念。它的目标是帮助程序员构建高度可伸缩的应用程序,编写能够处理数万条同时连接到一个(只有一个)物理机的连接代码。
750 0
【自然框架】js版的QuickPager分页控件 V2.0
优点: 1、  通过更换模板可以控制各个分页元素(比如首页、末页,页号导航等)的位置和是否显示。 2、  通过更换css可以实现各种UI风格和效果。(附带24套css效果) 3、  Js的方式创建分页UI,不占用服务器资源。
982 0
+关注
行者武松
杀人者,打虎武松也。
14545
文章
2569
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载