前端模块化发展简史

前端发展日新月异,短短不过 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。

时间: 2024-10-25 22:03:11

前端模块化发展简史的相关文章

【webpack学习笔记(一)】流行的前端模块化工具webpack初探

从开发文件到生产文件   有一天我突然意识到一个问题,在使用react框架搭建应用时,我使用到了sass/less,JSX模版以及ES6的语法在编辑器下进行开发,使用这些写法是可以提高开发的效率.可是浏览器它本身是并不能够"理解"这些语法的呀.就像下面这张图: 在开发代码文件 --> 生产代码文件的转换过程中,我们到底需要做些什么呢?没错,这一切都和webpack(或gulp)有关: 转一张webpack官网的图,webpack能把less/sass文件,json文件,乃至css

Web前端发展简史

Web前端发展简史 有人说“前端开发”是IT界最容易被误解的岗位,这不是空穴来风.如果你还认为前端只是从美工那里拿到切图, JS和CSS一番乱炖,难搞的功能就去网上信手拈来,CtrlC + Ctrl V的话,那就正中了这份误解的下怀.经过十几年的发展,web前端早已脱离了原来边缘化的形态,扮演了移动互联网开发链条中最关键的角色,是应用或产品能否打动用户的踹门砖.那么什么是前端开发,其又包含了哪些内容? 前端开发的定义 从狭义的定义来看,“前端开发”是指围绕HTML.JavaScript.CSS这

前端模块化的发展

这一篇是读后感,总结一下 前端模块化:https://www.cnblogs.com/dolphinX/p/4381855.html web端JavaScript,因为没有类(class)和模块(moudle)的概念,极其简单的代码组织规范不足以驾驭日益庞大的前端项目... (写文章太累了...先用图片代替) http://processon.com/chart_image/5d71f778e4b04c14c4daac87.png 原文地址:https://www.cnblogs.com/wuy

前端模块化

前端模块化 前端模块化 在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可:如今CPU.浏览器性能得到了极大的提升,很多页面逻辑迁移到了客 户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀 这时候JavaScript作为嵌入式的脚本语言的定位动摇了,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,JavaScript极其简单的代码组织规范不足以驾

前端模块化开发的价值

本文发表在<程序员>杂志 2013 年 3 月刊,推荐购买. 前端模块化开发的价值 随着互联网的飞速发展,前端开发越来越复杂.本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及如何使用 Sea.js 进行前端的模块化开发. 恼人的命名冲突 我们从一个简单的习惯出发.我做项目时,常常会将一些通用的.底层的功能抽象出来,独立成一个个函数,比如 function each(arr) { // 实现代码 } function log(str) { // 实现代码 } 并像模像样地把这些函

Intel CPU 发展简史

Intel CPU 发展简史 1971年11月15日:4004 1971年11月15日,Intel公司的工程师霍夫发明了世界上第一个商用微处理器-4004.这款4位微处理器集成了2250个晶体管,晶体管之间的距离是10微米,能够处理4bit的数据,每秒运算6万次,频率为108KHz,前端总线为0.74MHz (4bit).原为日本Busicom公司的计数器开发. 编号为4004,第一个"4"代表此芯片是客户订购的产品编号,后一个"4"代表此芯片是英特尔公司制作的第四

前端模块化(CommonJs,AMD和CMD)

前端模块规范有三种:CommonJs,AMD和CMD. CommonJs用在服务器端,AMD和CMD用在浏览器环境AMD 是 RequireJS 在推广过程中对模块定义的规范化产出.CMD 是 SeaJS 在推广过程中对模块定义的规范化产出.AMD:提前执行(异步加载:依赖先执行)+延迟执行CMD:延迟执行(运行到需加载,根据顺序执行) 模块 函数写法 function f1(){ //... } function f2(){ //... } 模块就是实现特定功能的文件,把几个函数放在一个文件里

web前端的发展态势

以前 作为一个java程序员写的代码主要还是后台的代码,虽然开始的时候前后端都写,但是也是用别人造好的轮子来用,学学html,css,js,jquery,再找一个前端ui框架学学,上手之后我们就可以写界面,写后台了,当然这只是企业的后台管理项目,界面要求不是特别高,对于网站项目还是需要前端童鞋来设计,布局,写好html给到我们,我们转成jsp,或者各种模板引擎文件.这种模式对前端童鞋要求不是特别高,会设计,切图,就差不多了,什么前端组件化,模块化,自动化这些概念都没有,可能是小公司没有接触到,大

前端模块化开发学习之gulp&amp;browserify篇

 随着web应用的发展,前端的比重占得越来越多,编写代码从而也越来越复杂.而通常我们需要将不同功能或者不同模块的代码分开写,最后在html中一起加载,这样做是可以的,但是当你需要进行维护或者是二次开发的时候,你会觉得十分费劲,因为你不知道文件之间复杂的关系,所以我们需要利用一些插件来配合进行模块化的开发. 所谓模块化的开发,写过nodejs的人都知道,文件之间的依赖可以用require()实现,但是浏览器端是不支持这样的依赖形式的,而browserify却可以解决这个问题,再加上gulp这个强大