阅读更多

2顶
2踩

Web前端

原创新闻 前端模块化发展简史

2017-06-02 16:29 by 副主编 jihong10102006 评论(4) 有22078人浏览
前端发展日新月异,短短不过 10 年已经从原始走向现代,甚至引领潮流。网站逐渐变成了互联网应用程序,代码量飞速增长,为了支撑这种需求和变化,同时兼顾代码质量、降低开发成本,接入模块化势在必行。伴随这一变化的是相对应的构建工具的快速成长,或是为了优化、或是为了转义,都离不开这类工具。

所谓温故而知新,本篇回顾总结下前端模块化的发展历程及辅助工具。在回顾中可以更清晰的看到当前我们用的方案所处的位置,为什么会发展到这一步,目前模块化方案带来的优势等。

1. 没有模块化的日子

最开始 JavaScript 承担的任务量并不多,表单验证基本上就是他的全部,最多就是简短的前端交互,这个时期 JavaScript 组织结构非常凌乱,大部分都是后端哥哥们顺手代劳,那时候还没有“前端”这一职位。 一般都是写到一个文件或者直接写到 jsp、asp 的后端模板页面上就完事了。这个阶段没啥可说的,跳过吧。。。

2. 传统模块化

随着 ajax 的流行,前端能做的东西一夜之间暴涨,代码量飞速增加,单文件维护代码已经太沉重,于是拆之,进而引入模块化,将负责不同功能的代码拆分成小粒度的模块,方便维护。

这里要说的模块化是抛开现在你所熟知的 require,amd,seajs 等,不借助任何的模式和工具,由 JavaScript 直接完成的代码结构化。JavaScript 天生没有模块化的概念(直到 ES6), 而不像后端语言源生自带模块功能, 比如 Java 的 import、C++的 include、Node 的 require(下文说到),所以需要通过其他的方式来实现模块化。

应用模块化开发的主要目的是为了复用代码、代码结构清晰、便于维护等,比如在开发过程中,我们往往会将一些重复用到的代码提取出来,封装到一个 function 里,然后在需要的地方调用,那么这可以看做是一种模块化。

我们看一段代码 例 – 1:
function show(element) { // 展示一个元素 }
function close(element) { // 隐藏一个元素 }

上面的代码非常直观,就是要显示、隐藏一个 dom 元素,往往这种方法需要大范围多次调用,一般我们可能会放到 util.js 这样的文件里,这是第一步。接下来在业务代码中引用,例 – 2
<body>
    <script src="lib/utils.js"></script>
    <script src="lib/page-1.js"></script>
    <script src="lib/page-2.js"></script>
</body>

2.1 存在的问题

以现在的经验来看,上面的写法会带来非常明显的问题,当然也是在这个模块化引入阶段逐步暴露的。
  • 全局变量冲突风险:如果编写 page-1 的同学不知道 utils 里面有一个 show/close 方法,然后他自个也写了一个,同时还添加了额外逻辑,自然就覆盖了原来的方法,那么 page-2 同学在不知道的情况下调用了这方法,自然会发生错误.
  • 人工维护依赖关系:因为存在依赖关系,所以必须先加载 util,然后才能加载 page-1/2,这里的例子非常简单,但在实际项目场景了,这样的依赖会非常多且复杂,维护非常困难,很难建立清晰的依赖关系。想想当时高大上的校内网,那种程度的页面得需要多少的模块去支撑。后续项目迭代往往会带来意料之外的问题.
2.2 尝试解决问题

针对问题 1,可以做下面这些改进:

2.2.1 代码提取

把这些方法放到一个 object 里面对外输出,例 - 3
var utils = {
    _name: ‘baotong.wang’,
    show: function(element) {},
    close: function(element) {}
}

但这样依然不能避免我们的 utils 被覆盖的可能性,孱弱的英语积累让我们想不出什么更高级的词来命名 utils,var fuzhufangfa = {}?。。。

不过这种写法同时还带来了暴露内部变量的问题,外部可以访问到 _name。

2.2.2 命名空间

然后部分开发者引入了命名空间,这个东西牛逼了,例 – 4:
var com.company.departure.team.utils = {}

代码模块通过严格的命名规则做了规范,可以按照实际情况具体到部门、team、类库。如果一个公司在代码规范上做了这样的约束,基本上可以避免变量名冲突的问题,但同时带来的需要输入过多单词的负担,目前还没有哪个 IDE 能支持 JavaScript 像 Java 一样可以一路点点点下去,这些都是需要打出来的。当然我们也可以不设计成这么复杂的命名空间,var Company.ProjectName.Module = {}; 同时结合局部变量减少输入的长度。

2.2.3 闭包封装

为了解决封装内部变量的问题,就该有请立即执行的函数登场了,这也是我们接触的最多的一种模块化方式,公司内部有点年纪的项目多少都能看到这样的写法,结合命名空间如下,例 – 5:
(function() { 
   var Company = Company || {};
   Company.Base = Company.Base || {};

   var _name = ‘baotong.wang’

   function show () {}
   function close () {}

   Company.Base.Util = {
     show: show,
     close: close
   }
})();

上述写法通过一个立即执行的函数表达式,赋予了模块的独立作用域,同时通过全局变量配置了我们的 module,从而达到模块化的目的。基本上到这一步,问题 1 就解决了。

2.2.4 关于依赖关系

针对问题 2,代码的组织依赖关系,这块我不是很了解,向司徒求证了一下。大概情况如下。

当时业界也是有不少方案的,比如百度的 Tangram 与 Qwrap,查了下他们的 github 地址,最后一次更新是在五年前。它解决依赖关系的方式是在类库中什么依赖,类似 depend=[“com.qunar.dujia.lib”, “”, …],然后通过配套的工具去解析。

同时也有一些后端大牛为了解决前端工程化的问题发明创造了各种方案,但当时的氛围并没有现在这么重视前端,前端从业人员的水平也没现在高;同时后端哥哥们往往对前端问题、痛点了解的不深入,所以开发出来的方案很难推广。比如搞 Java 和搞 Ruby 的后端做的方案基本不太会一样。 用司徒的话来讲叫生不逢时。

2.3 这个时代的工具

2.3.1 代码合并

例 – 2 中的代码引用方式相信肯定存在于一些站点上。虽然不会带来功能问题,但是却带来了很多不必要的 http 请求,特别是复杂页面需要引用很多独立 JavaScript 的时候,从而延长了页面的 ready 时间。所以这里需要合对文件进行合并处理,将可以合并的业务代码连接到一个文件里。

需要注意的是,合并并不是所有的文件合并为一个为好,比如公共文件 jquery 文件、功能公共方法可以单独引用,利用浏览器的缓存机制,减少多页面情况下总的下载量。如果站点一共就是一个 SPA,合并为一个为好。

2.3.2 代码混淆压缩

另外一个就是代码压缩,现在的同学对这个肯定非常熟悉了,但是即便现在找一个你熟悉的网站看一下,也不敢说一定做到了这一步。

走到这有了这两步,网站看起来就挺像那么回事了。

2.3.3 代表性工具

YUI compressor,出自雅虎,在那个时期雅虎可以说是网站优化的风向标,同样出自雅虎的前端优化 34 条(数量不同版本不一样)在业界也是鼎鼎大名,为前端做出了很大贡献。

这个时候适合模块化的通用工具并未出现,相信有实力的大公司都有内部的一条工具去做类似的事情,这里个人所知有限,没啥发言权,欢迎大家交流讨论。

3. Node 来了

2009 年,node 的发布给前端同学带来了无限可能,npm 生态的逐渐成熟给了我们更多选择,以往需要通过其他语言工具执行的编译过程也可以由前端一手接管。同时 node 也带来了 commonJS,给前端的模块化提供了新的思路,我们这里首先关注 node 实现的 commonJS 规范。

3.1 commonJS 概述

作为后端语言,没有模块化加载机制是运转不起来的,node 选择实现了 commonJS 作为它的模块加载方案,整体非常简单。注:commonJS 并不是 node 发明的,他只是按照该规范做了一套实现。

3.2 npm 生态

npm 生态让 node 有了自己的模块仓库,各种类库的不断支持让我们也有了更多选择。commonJS 一开始就提供了对 npm module 的支持,在路径查找的时候内部配置了对 node_modules 文件夹的查找支持。

3.3 说说 node

对前端来说幸运的是 node 的设计者 Ryan Dahl 选择了 JavaScript 作为他的支持语言,这也说明了 JavaScript 事件驱动的魅力所在。大批后端的加入丰富了作为一门后端语言的各种基本功能。

对于前端同学来说 node 有着天然的亲和力,让我们多了一个全新施展本领的领域;同时对于懂后端的同学来说视乎可以大干一场了。目前我们用的最多的有两部分,node 布置站点、数据接口集成维护,这个和本篇没啥关系,不展开说;另外一部分就是利用 node 开发工作工具,提高前端的工作效率,社区里解析 commonJS 的、构建工程工具不断喷涌而出, 具有代表性的有 grunt、gulp、browserify,webpack,前端模块化可以更进一步。

4. 模块化方案

4.1 commonJS

简单概括下 commonJS 的几个概念,还是非常简单的
  • 每个文件是一个模块,有自己的作用域。这里面定义到函数、变量、类都是私有的,对其他文件不可见;
  • 每个模块内部,module 变量代表当前模块,它是一个对象;
  • module 的 exports 属性(即 module.exports)是对外的接口;加载某个模块,其实是加载该模块的 module.exports 属性如果文件中没有 exports 属性,那么外部引用不到任何东西;
  • 使用 require 关键字加载对应的文件,也就是模块;
  • require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象,如果没有发现该模块,报错。
这里对上面的代码做了模块化的改进,文件内部设置的对外输出,下面的写法在 node 环境中源生支持。
// 设置文件输出
module.exports = { 
func: function() {},
field: "string"
}
// 添加单个 export
module.exports.show = function() {}

//这里引入几个模块、文件
require("modulepath"); 
var Base = require("../base.js");
var page = require("./file.js");

page.show();

4.2 其它模式

除了 commonJS,当前留下的模块化模式还有以 requireJS 为代表的 AMD 和以 seaJS 为代表 CMD, 在去哪儿网内部始终是 commonJS 占据主流,我个人也是喜欢 commonJS 更多一些,requireJS 可以做到在浏览器端执行动态异步模块加载,仅从首次代码下载量的角度讲,这种方案更好一些,但我们完全有其他办法在 commonJS 模式下解决这有个问题,所以本篇主要介绍 commonJS 和编译工具支持的一个思路

4.3 代码改造

基于 commonJS,回过头来再看下例 -5 中的代码应该怎么改造。
  • 首先,那层闭包可以不用加了,你没看到有谁在 node 里面加这个东西吧,这层其实还是需要有点,但是我们交给工具自动帮我们加上。
  • 其次,我们需要在模块内部写上对外输出的内容,module.exports = *;
  • 然后,在业务代码中添加对模块的引用,var module = require(“modulepath”), 有了这个之后就能引用 module export 出来的功能了
  • 最后,通过打包工具的编译,解析 commonJS,分析入口文件得到最终输出。
最终得到的代码如下:
// module.js
var _name = 'baotong.wang';

function show() { alert(_name); }
function close() {}

module.exports = {
    show,
    close
}

// page.js
var module = require('./module.js');
module.show();

4.4 浏览器端支持

commonJS 是服务器端的模块化方案,浏览器端是不支持的,单是 require 就没有,所以就需要辅助工具来替我们完成 commonJS 代码向浏览器代码的转换。

社区成熟的解析类库有 browserify,能够完美解析 commonJS;因为公司内部业务的特点需要,browserify 并不能满足实际需求,因此去哪儿网内部先后推出了 fekit、ykit 两款针对 commonJS 的前端工具,来执行代码的编译。前者是自己实现的一套解析 commonJS 的工具集,对一些规范的实现不是很规范,同时面向的是内部的 module 仓库,导致和主流 npm 环境脱节,于是有了 ykit;后者是基于 webpack 和公司业务特点封装的一个工具集,核心打包交给了 webpack,同时做了部分优化,具体前面发过一篇文章,介绍过实现机制。

在这我说下 fekit 的编译过程,介绍下这个工具处理 commonJS 的一般思路。

4.5 fekit 编译过程

fekit 是一个基于 node 的命令行工具集,在支持 commonJS 的过程中也做了一些修改和扩展,比如支持在 css 文件中通过 require 加载文件,做到和 JS 文件一样;增加对内部 module 仓库的支持,下图介绍了一次 pack 的具体执行过程。下面的流程适合模块解析相关的部分,其他业务构建部分在这里跳过。

module 处理模板代码
;(function(__context) {
    var module = {        
        id : "{{md5Key}}" ,        
        filename : "{{fileName}}" ,        
        exports : {}
    };    

   if( !__context.____MODULES ) { 
        __context.____MODULES = {}; 
    } 

   var r = (function( exports , module , global ) { 

       //----------原始文件代码----------
        {source}        
       //----------原始文件代码----------

    })( module.exports , module , __context );

    __context.____MODULES[ "{{md5Key}}" ] = module.exports;

})(this);

在业务代码中的 require 会变成,即通过一个 object 拿到 module.exports。 var module = context.__MODULES[“md5Key”]; 以上就是对一个 commonJS 文件的解析过程了。

4.6 ES6 的模块化方案

ES6 中给出了 import export 这样的方案,目前为止我们都是通过 babel 将 ES6 代码转为 ES5,import 转为了 require,export 转为了 module.exports,即 commonJS。

他的实现原理和 commonJS 这种引用即引用整个类不一样,它是用啥就引用啥,export 输出的也不是一个类,这里往下说就比较多了,阮一峰老师的 ES6 教程对这块也有比较详细的说明。限于篇幅,本篇不针对这个展开来说了。

5. 总结

本篇简单回顾了模块化的发展历程,介绍了以往存在的问题。然后到现代模块化方案的时候,讲解了 commonJS,同时介绍了解析 commonJS 的一种方法。通过一个例子串连,讲述了模块化带来的改变。部分知识点没展开来说,大家有兴趣可以深入学习一下。同时感谢司徒指点,希望本篇能对大家有所帮助,best regards。
引用
本文来自「Qunar 技术沙龙」,作者王宝同,2014 年加入 Qunar,在旅游度假事业部担任前端工程师。擅长代码结构设计与优化,喜欢研究构建工具,折腾 Node。

  • 大小: 115.5 KB
2
2
评论 共 4 条 请登录后发表评论
4 楼 softor 2017-06-07 13:52
fekit 软文?
3 楼 white_crucifix 2017-06-05 09:38
部分文字似乎拷贝了阮一峰的文章
2 楼 liangcoder 2017-06-03 09:13
good job.
1 楼 if(i!=我){} 2017-06-02 21:31
假大空,看似头头是道,实际啥也没说。

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 滴滴前端工程化思维

    纵观互联网的发展历史,没有哪一刻能像现在这样,存在如此丰富多彩的前端技术。前端技术的发展伴随着互联网的发展,逐步成为一个独立的技术世界,跨越了从BBS、门户、搜索、社交媒体平台、APP等互联网各个黄金发展...

  • 前端模块化详解(完整版)

    如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范...

  • 五分钟带你回顾前端模块化发展史

    CSS 早在 2.1 的版本就提出了 @import 来实现模块化,但是 JavaScript 直到 ES6 才出现官方的模块化方案 ES Module。...社区里的前辈们创建并实现了规范,这些规范便是前端模块化发展之路上智慧的结晶。

  • 详谈前端模块化这十四年的发展史:CommonJS、AMD、CMD、ES6

    十年之前,模块化还只是使用「闭包」简单的实现一个命名空间。转眼间模块化已经发展了有十余年了,不同的工具和轮子层出不穷,下面是最各大工具或框架的诞生时间

  • 软件架构之前后端分离与前端模块化发展史

    在现行的软件架构中,前端和后端是分离的,即前端只专注于页面渲染,而后台专注于业务逻辑,前端和后端是两个不同的工种,而前后端交互最常见的方式就是通过接口。 前后端分离架构 在正式说明前后台架构分离之前,...

  • 【Web前端】模块化开发

    模块化的写法历史 AMD规范 require.js用法 request.js的应用 一、JavaScript模块化的必要性 随着网站逐渐变成"互联网应用程序(WebApp)",嵌入网页的Javascript代码越来越庞大,越来越复杂。 网页越来越像桌面程序...

  • JavaScript 模块化发展史

    JavaScript 模块化发展史 {ignore} 第一阶段 在 JavaScript 语言刚刚诞生的时候,它仅仅用于实现页面中的一些小效果 那个时候,一个页面所用到的 JS 可能只有区区几百行的代码 在这种情况下,语言本身所存在的一些...

  • 前端发展简史

    前端发展简史 起源 1990 HTML 1990 年,Tim 以超文本语言 HTML 为基础在 NeXT 电脑上发明了最原始的 Web 浏览器。 1991 年,Tim 作为布道者在 Internet 上广泛推广 Web 的理念,与此同时,美国国家超算...

  • 前端模块化 (requireJS)

    改变this指向的两大方法 apply(传参方式为 数组 ) call(可直接传参)es6新增的 module 模块化语法。模块化概念:独立作用域【不污染环境】、封装、...发展历史:commonJS语法【nodeJS】模块化特性:高内聚低耦合。

  • 一文带你了解,前端模块化那些事儿

    该文章主要讲述了前端模块化的发展历史和各个阶段的技术方案,包括无模块化(IIFE)、CommonJS、AMD、CMD、ESModule、UMD。其中,无模块化时期的文件拆分是最基础的模块化,但也存在函数命名冲突的问题;IIFE 是现代...

  • 前端工程化详解——理解与实践前端工程化

    前端工程化一直是一个老生常谈的问题,不管是面试还是我们在公司做基建都会经常提到前端工程化,那么为什么经常会说到前端工程化,并没有听过后端工程化、Java工程化或者Python工程化呢?我们理解的前端工程化是不是...

  • web前端发展历程

    前端的地位也愈见明显,很多学校已经体系的传授前端课程,众多培训机构也将前端知识作为了主流课程,也有越来越多的同学加入到前端学习的行列中,作为前端工程师或者前端的学习者我们有必要去了解前端的发展史。...

  • 【前端模块化】-认识前端模块化

    浅谈前端模块化的历史与进程

  • 什么是前端模块化?

    前端模块化 传统开发遇到的问题 js变量命名冲突 文件依赖关系复杂 模块化解决方案 模块化,就是把单独的功能封装到一个文件当中,模块之间相互隔离,通过公开的接口通讯,成为其他模块的依赖 优势:方便代码...

  • 模块化技术

    AMD和CMD最大的问题是没有通过语法升级解决模块化(它们定义模块还是通过调用js的方式定义一个模块,它没有办法对模块进行规模化的引用)命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。...

  • 重学webpack系列(一) -- 前端模块化的演变历史

    原因归结于并没有想到现如今前端的快速发展与规模,现如今前端开发已经在web新时代占据了半壁江山,在前端演变的过程中,社区规范也在不断的完善,那在前端的发展过程中,前端模块化究竟经历了什么样的变革呢,这将...

  • setting.xml文件,修改Maven仓库指向至阿里仓

    setting.xml文件,修改Maven仓库指向至阿里仓

  • 基于java的玉安农副产品销售系统的开题报告.docx

    基于java的玉安农副产品销售系统的开题报告

  • dev-c++ 6.3版本

    dev-c++ 6.3版本

Global site tag (gtag.js) - Google Analytics