二、Redux 演变过程 02:模块化过程

一、React+Redux 项目结构
二、Redux 演变过程

  1. 混乱结构 -> 组件化结构
  2. 插件引用 -> 模块化工程
  3. MVC -> MVVM
  4. 零散状态管理 -> 全局状态管理
  5. 双向数据流 -> 单向数据流

前言

       Web前端模块化的发展历程,也就是开发人员组织代码结构,提高项目可维护性的过程。JavaScript 语言因为出生过于随性,引发了非常多的历史遗留问题。而最大的缺憾就是,没有考虑语法层面的模块组织结构,致使其在很长一段时间内只能作为寄生于网站页面的脚本语言,发挥着玩具一般的简单动效。直到有一天,不堪忍受的开发者们膝盖纷纷中箭。

2.1 原始时代:冲突纷争

       模块化演变的历程中,是以 JavaScript 为核心,逐渐深化为 多种文件打包 ,不过这是后话。在原始时代(其实也没过多久),Web前端的开发者们通常将 JavaScript 代码直接写入 html 页面,常见的方式如:

<html>
    <head>
        <meta charset="utf-8">
        <title>Example</title>
    </head>
    <body>
        
        <div id="root"></div>
        
        <script type="text/javascript">
            
            // 此处省略一万行 JavaScript 代码...
            
        </script>
    </body>
</html>

       如同这样,简单的页面嵌入简单的代码片段,可能感受不到什么影响。但做个假设,这里的 JavaScript 代码真的有一万行,花了你2个月的时间进行开发和改善。你会发现,随着代码量增加,维护成本逐渐提高。甚至你自己都找不到一些逻辑究竟在写在了哪里。
       同时,这样的开发模式完全不适合多人协作。如果这里的一万行代码交给两个人来写,很明显的冲突就是:变量或函数命名冲突、重复处理的业务逻辑、业务处理的时序性混乱等。
       此时有的同学可能会站出来,将一些业务逻辑写入到独立的 js 文件之中,页面代码变成了这个样子:

<html>
    <head>
        <meta charset="utf-8">
        <title>Example</title>
    </head>
    <body>
        
        <div id="root"></div>
        <script type="text/javascript" src="biz01.js">/script>
        <script type="text/javascript" src="biz02.js">/script>
        <script type="text/javascript" src="biz03.js">/script>
    </body>
</html>

       如果在每一个 bizXX.js 之中,并没有专门去处理作用域冲突(闭包),那么以这样的方式构建的页面脚本,仅仅解决了不同的 <script> 标签加载时序不同,引发的一些声明没有提升的问题,而完全无法从根本上解决冲突。

2.2 插件时代:闭包

       随着20世纪末,至21世纪初,越来越多的人关注并且开始利用互联网获取信息,浏览器的使用率也越来越大。越来越多的公司或团体希望全世界能看到自己,纷纷开始建立自己的网站,用于展示企业或团队形象,以及展示产品和服务。
       横向对比,假设越来越多同质化的展示产品的网站涌现,就会令其价值稀释。因此一些团队希望让自己的网站显得与众不同。市场的需求也在进一步推动技术革新,在Web视觉层面,快速构建复杂的网站视图特效也成为了当务之急。
       很多团队提出了自己对于更快更好的构建Web前端工程的解决方案,其中最为著名的比如Sam Stephenson编写的类库 prototype.js 、John Resig发布的类库 jQuery.js。纵观这些优秀的解决方案,无一例外的使用着闭包特性解决着一些引用冲突问题。
       那么什么是闭包呢?详细了解请参考我的另一篇文章《JS 函数式编程思维简述(四):闭包(Closure)》。简而言之,闭包是 JavaScript 语言支持函数式编程的重要基石。闭包特性最主要解决两个问题:

  • 缓存闭包函数内部数据,对外导出;
  • 保证函数内部变量引用的准确性(就近引用原则);

jQuery 源码为例:

( function( global, factory ) {

    "use strict";

    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = global.document ?
            factory( global, true ) :
            function( w ) {
                if ( !w.document ) {
                    throw new Error( "jQuery requires a window with a document" );
                }
                return factory( w );
            };
    } else {
        factory( global );
    }

} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
    
    // 此处省略一万多行... 

    if ( !noGlobal ) {
        window.jQuery = window.$ = jQuery;
    }
    
    return jQuery;
} );

       无论 jQuery 现在的市场角色如何,其学习价值都是非常大的。通过闭包的方式,我们解耦了 js 代码片段之间的关系,只有在需要时才进行互相引用。以此作为基石, jQuery 团队又推出了自己的UI插件库 —— jQuery-ui,以及鼓励开发者们以 jQuery 为地基构建自己的功能性插件。著名的如瀑布流特效插件Masonry、以及 Bootstrap 团队提供的 UI 插件等。
       无论是以何种方式来构建插件,其基准原则都是:加强内聚、减少耦合、减少冲突。而诸多插件的引入,也形成了插件时代独特的代码组织结构风格。

2.3 模块时代(初期):大乱斗

       插件时代通过不同的闭包环境来构建整体应用,因此一个网页内部结构经常是这个样子的:

<html>
    <head>
        <meta charset="utf-8">
        <title>Example</title>
    </head>
    <body>
        
        <div id="root">
            <!-- 省略以前多行代码... -->
        </div>

        <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/jquery-unveil@1.3.2/jquery.unveil.min.js"></script>
        <script src="https://cdn.jsdelivr.net/gh/markgoodyear/scrollup@2.4.1/dist/jquery.scrollUp.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/@firstandthird/toc@1.4.1/dist/toc.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/jquery-match-height@0.7.2/dist/jquery.matchHeight-min.js"></script>
        ...

    </body>
</html>

       插件的独立引用为构建大型Web应用视图提供了可行性,但长期的实践过程中,我们发现这并不是一个最好的解决方案,主要暴露出的问题是:

  • 插件之间的引用次序边界模糊;
  • 插件之间构建和调用方式风格迥异;
  • 增加了页面额外文件的请求频次;
  • 团队协作时容易引发插件版本或其他引用问题;
  • ...

       有待解决的问题产生,就有解决问题的人出现,因此,Web前端工程的模块化的呼声也越来越响。TC39 (ECMA-262标准制定委员会)官方迟迟没有改进方案,那么只能由民间高手们自发制定模块化方案。
       模块化最主要的表现形式为:

  • 规范了独立功能的对外引用(导出)方式;
  • 规范了独立功能的内部引用(导入)方式;
  • 规范了独立功能的依赖加载次序;

最著名的Web前端模块化方案要数 AMD规范CMD规范,具体示例请参考我的另一篇文章《JS 函数式编程思维简述(四):闭包之流行的模块化方案》。以 Alibaba 的前辈玉伯所设计的 Sea.js 作为示例:

demo目录结构:

// 当前示例的目录结构
├─ js
│  ├─ sea.js
│  ├─ aa.js
│  ├─ bb.js
│  └─ cc.js
├─ index.html

模块引用方式:

<script src="js/sea.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
    
    // 通过 seajs 对象调用 use() 方法获取其他模块的引用
    seajs.use('./js/aa.js', function (aa) {
        aa.printBB();
        aa.printCC();
    });
    
    // 未阻塞的页面其他 js 语句
    console.log('page loaded...');
            
</script>

       无论是何种 模块规范,都秉持统一的一个理念:整体项目通过一个主入口进入,通过对其他模块的依赖来按需加载。熟悉 Java 的同学可能会想到 Java 项目中的 main() 方法,的确设计理念是一致的。
       这些模块方案解决了两个重要的问题:统一的模块依赖次序、统一的模块编码风格,为 JavaScript 模块化进程奠定了基础。

2.4 模块时代(中期):趋于一统

       天下大势,合久必分,分久必合。在 Web前端模块化趋于一统的进程中,出现了三个重要的技术影响,他们分别是node.jswebpackES6模块机制

       2.4.1 node.js 和 CommonJS规范

       node.js是一个非常神奇的产物,是融合了性能变态的Google V8引擎、C++底层,通过 JavaScript 编写可涉足于多个领域能力的运行平台,其研发的初衷是为了更快的创建异步的高性能的web应用服务。其本身扩展了诸多 JavaScript 能力,同时以 CommonJS 作为模块化规范,定义了项目中解构出的独立文件的依赖方式:

典型的 node.js 代码:

// 模块导入
let fs = require('fs');

// 定义一个读取文件返回 Promise 对象的异步函数
let readFile = (txtOrig) => new Promise((resolve, reject) => {
    // 使用导入的模块 fs
    fs.readFile(txtOrig, {encoding: 'utf8'}, (err, data) => {
        if(err) reject(err);
        resolve(data);
    });
});

// 模块导出
module.exports = {
    readFile
}

       在一段时间里,node.js 中使用的 CommonJS 模块通常被称之为是服务器端模块化方案,而 Web前端 所应用广泛的 AMDCMD 则通常被称之为是前端模块化方案。原本互不影响的两个应用领域,随着 webpack 技术的成熟,产生了命运的交汇。

       2.4.2 webpack

       webpack是一种文件打包合并技术,是 node.js 工程中常用的构建工具。原理是通过分析待打包的依赖文件关系,将数个文件合并成一个整体文件。引用 webpack 官方网站 的简单示例:

待合并的文件:

// ./src/bar.js

export default function bar() {
    console.log("bar...");
    // do something...
}
// ./src/index.js

import bar from './bar';

bar();

配置打包方式:

// ./webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
  }
};

最终生成合并后的文件:

// ./dist/bundle.js

!function(e) {
    var t = {};
    function r(n) {
        if (t[n]) return t[n].exports;
        var o = t[n] = {
            i: n,
            l: !1,
            exports: {}
        };
        return e[n].call(o.exports, o, o.exports, r),
        o.l = !0,
        o.exports
    }
    r.m = e,
    r.c = t,
    r.d = function(e, t, n) {
        r.o(e, t) || Object.defineProperty(e, t, {
            enumerable: !0,
            get: n
        })
    },
    r.r = function(e) {
        "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
            value: "Module"
        }),
        Object.defineProperty(e, "__esModule", {
            value: !0
        })
    },
    r.t = function(e, t) {
        if (1 & t && (e = r(e)), 8 & t) return e;
        if (4 & t && "object" == typeof e && e && e.__esModule) return e;
        var n = Object.create(null);
        if (r.r(n), Object.defineProperty(n, "default", {
            enumerable: !0,
            value: e
        }), 2 & t && "string" != typeof e) for (var o in e) r.d(n, o,
        function(t) {
            return e[t]
        }.bind(null, o));
        return n
    },
    r.n = function(e) {
        var t = e && e.__esModule ?
        function() {
            return e.
        default
        }:
        function() {
            return e
        };
        return r.d(t, "a", t),
        t
    },
    r.o = function(e, t) {
        return Object.prototype.hasOwnProperty.call(e, t)
    },
    r.p = "",
    r(r.s = 0)
} ([function(e, t, r) {
    "use strict";
    r.r(t),
    console.log("bar...")
}]);

此时,我们便可以在页面这样引用:

<!doctype html>
<html>
  <head>
    ...
  </head>
  <body>
    ...
    <script src="dist/bundle.js"></script>
  </body>
</html>

       即便源代码非常简洁,合并后的文件 bundle.js 也显得非常复杂。webpack 在打包的过程中会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。打包合并后的独立文件,是一个内部包含完整的模块引用及运作的闭包环境。

       早期的Web前端模块化方案是这样的:


image

       借助于 webpack 的 Web前端 工程方案是这样的:

image

       可以看到,之前的方案中对于模块的引用是采用按需动态引用,支持了模块化的方式进行开发,提高了工程的可维护性。但并未有效的降低依赖文件的请求次数,缓解页面请求压力。
       借助 webpack ,我们通常在开发阶段就把所有的模块合并成为了一个 js 文件。在项目上线运行的生产环境,通常只需要加载一个 bundle.js 文件即可,大大缩减了开发过程模块化对于运行过程的影响。 webpack 真的是帮我们做了很多事。时至今日,AMDCMD模块化支持的使用率正在逐步减少,因为 webpack 打包过程是基于 node.js 运行环境中的,直接使用 CommonJS 规范进行工程模块构建即可。

       2.4.3 ES6模块化定义:未来

       终于,TC39 在 ES6(也称ES2015)中定义了官方推荐的模块化方案,部分浏览器厂商也逐步开始支持。即使你的浏览器环境或者nodejs环境无法支撑ES6模块化语法,也可以通过诸如 BabelTypeScript等工具直接体验新版本的开发过程。关于ES6模块的简单示例,可参阅我的另一篇文章《JS 函数式编程思维简述(四):闭包之流行的模块化方案》
       CommonJS 规范 因为 node.js 而大肆盛行于Web前端和服务器端,但作为 ECMA 制定的官方模块化规范,相信 node.js 团队也会加快脚步,尽快完成对其支持。

结束语:模块化是构建复杂应用的基础,JavaScript 的模块化支持,是其从玩具语言转变成为功能强大的编程语言过程中的重要里程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 姓名:樊松松 学号:17021211234 转载自https://www.leiphone.com/news/20...
    远方_c2e0阅读 958评论 0 1
  • 【同读一本书.王纪云】2016-4-6-059:《影响力》 正文:69%“大众对品牌的疯狂偏好,其实也是关联好都道...
    AA王纪云阅读 279评论 0 0
  • 程序员如何克服焦虑焦虑,作为积极的个体都不可避免,如何克服? 以问题为中心,设立小目标 放弃all in,适时的放...
    Jeff阅读 88评论 0 0
  • 唯有对自己卓越的才能和独特的价值有坚定、不可动摇之确信的人才被称为骄傲。——叔本华 ​ 刚才听到了my sole ...
    栖惶阅读 171评论 0 0