• 为何用 Web 模块的方式?


    本文讨论为何网站模块化很有用,以及模块化的各种实现方式的可行性。同时,还有个 独立页面 讨论 RequireJS 采用的函数封装的设计驱使。

    问题

    • 网站正在变成网络应用
    • 代码复杂度随着网站变大而增加
    • 代码组织变难了
    • 开发者希望 JS 文件模块化
    • 部署时又希望将代码优化进一到数次 HTTP 请求

    解决办法

    前端开发者需要一个满足以下条件的解决方案:

    • 类似 #includeimportrequire (译注:分别对应 C、Python、node.js)
    • 能够加载嵌套的依赖
    • 对开发者友好并且能提供帮助部署的优化工具

    脚本加载的 API

    首要任务是厘清加载脚本的 API。有以下备选方案:

    • Dojo:dojo.require('some.module')
    • LABjs:$LAB.script('some/module.js')
    • CommonJS: require('some/module')

    以上所有都将映射模块到 some/path/some/module.js。因为 CommonJS 的语法用者越来越广, 而我们希望代码复用,所以理想地,我们选择它。

    我们还想要某种语法,使得当前的 JavaScript 文件不经修改即可被加载 —— 开发者不需要因为想用脚本加载的方式而重写所有的 JavaScript 文件。

    但是,我们还需要这语法能够在浏览器中执行良好。CommonJS 里的 require() 是个同步的调用,意味着它得立即返回模块。 在浏览器里头,这可不太好使。

    异步对同步

    下例说明了浏览器(实现模块化)的基本问题。假设我们有个 Employee (职工)对象, 我们想要Manager(经理)继承自 Employee 对象。 以此为例 ,用我们的脚本加载 API,我们可能会把它写成这样:

    var Employee = require("types/Employee");
    
    function Manager () {
        this.reports = [];
    }
    
    // 如果 require 调用是异步的,这里就出错了
    Manager.prototype = new Employee();
    

    如上边注释所示,如果 require() 是异步的,这段代码就跑不起来。然而,在浏览器里同步加载脚本又严重影响性能。 那该怎么办呢?

    脚本加载:XHR

    用 XMLHttpRequest(XHR)加载脚本看起来很靠谱。如果用了 XHR,我们就可以调戏上例中的代码 —— 我们可以用正则表达式找出所有的 require() 调用,确保加载了这些脚本之后,再使用 eval() 或者动态创建script 节点将 XHR 获取的代码塞进去来执行上例代码。

    用 eval() 来执行模块代码很糟糕:

    • 开发者们已经被教育了 eval() 很糟糕
    • 有些环境不允许 eval()
    • 调试很困难。WebKit 的查看器,和 Firebug,都有个 //@ sourceURL=convention 来帮你给执行的文本命名, 但是这个特性并不是所有浏览器都有的
    • 不同浏览器中,eval() 的上下文也是不同的。你可以在 IE 里改用 execScript,但这意味着更多的不确定部分。

    用 script 节点插入模块代码来执行模块也不好:

    • 调试的时候,错误消息中的行号并不是指向到实际文件的

    XHR 还有个问题,它不支持跨域的请求。有些浏览器支持跨域 XHR 请求,但这个特性并不是所有浏览器都支持的, 而 IE 又创造了一个不同的专门用来处理跨域请求的 API 对象,叫做 XDomainRequest。 越多的不确定部分意味着事情越容易变糟。特别是,你要确保不发送任何非标准的 HTTP 头信息,不然会有个预请求,以确保是可以跨域访问的。

    Dojo 用的是基于 XHR 与 eval() 的加载器。尽管它管用,但成了开发者的沮丧之源。Dojo 有个跨域的加载器, 但是它需要模块在发布阶段改成函数包装的形式,从而可以用 script src="" 来加载模块。 此外,还有不少边界情况和不确定部分给开发者增加了额外的负担。

    如果我们要创造一个新的脚本加载器,我们可以做得更好。

    脚本加载:Web 工作线程

    Web 工作线程或许可以作为加载脚本的另一种方式,不过:

    • 它没有很好的跨浏览器支持
    • 它是个消息传递的 API,而脚本基本上都要与 DOM 交互的,所以这意味着工作线程直通用来获取脚本内容, 传送文本到主 window 然后使用 evalscript 来执行脚本。于是 XHR 会有的问题,它也都会有。

    脚本加载:document.write()

    document.write() 也可以用来加载脚本 —— 它可以从其他域名加载脚本,并且它的行为与浏览器正常加载脚本一致, 所以它是易于调试的。

    但是,在异步对同步的例子中,我们的例子是不能直接执行的。理想情况是,我们在执行脚本之前就知道require() 的依赖, 并保证这些依赖先被加载。但在此法中,我们不能在脚本被执行之前获取它的内容。

    并且,document.write() 在页面加载完毕之后不管用。而取得大幅性能提升的好办法之一就是按需加载, 因为用户在下一步操作时才需要它(译注:即在可能页面加载完毕之后才加载,因此 document.write() 不好使)。

    最后,通过 document.write() 加载脚本会阻滞页面渲染。当你专注于网站性能极限的时候,这种方法是不可容忍的。

    脚本加载:head.appendChild(script)

    我们可以按需创建节点,并插入到 head 中:

    var head = document.getElementsByTagName('head')[0],
        script = document.createElement('script');
    
    script.src = url;
    head.appendChild(script);
    

    这段小代码还是不够的,但它已经说明了基本的想法。这个方式比 document.write 的优势在于它不会阻碍页面渲染, 同时在页面加载完毕之后也是可用的。

    但是,它仍然有异步对同步里提到的问题:理想情况是,我们可以在执行脚本之前知道 require() 依赖,并确保它们先被加载好。

    函数封装

    所以我们需要知道依赖,并且保证在执行脚本之前先加载它们。最好的办法就是将我们的模块加载 API 用匿名函数封装起来。 像这样:

    define(
        // 模块名称
        "types/Manager",
    
        // 依赖数组
        ["types/Employee"],
    
        // 模块依赖加载完毕之后再执行的函数。
        // 这个函数的参数是依赖数组。
        function (Employee) {
            function Manager () {
                this.reports = [];
            }
    
            Manager.prototype = new Employee();
    
            // 返回 Manager 构造器,使其能为人所用
            return Manager;
        }
    );
    

    这就是 RequireJS 所采用的语法。还有一个简化的语法,方便你用来加载未使用 define 包装的 JavaScript 文件:

    require(["some/script.js"], function() {
        //This function is called after some/script.js has loaded.
    });
    

    选择这种语法的原因是它简洁,同时又允许加载器使用 head.appendChild(script) 这种加载方式。

    为了能够在浏览器里运行良好,它与 CommonJS 的语法的不同是必须的。坊间也有建议说常规的 CommonJS 语法也可以用来以 head.appendChild(script) 方式加载,只要服务端能够自动将模块代码以函数封装的形式包装起来。

    我相信有一点很重要,不能强制用户使用后端服务来改变代码:

    • 调试的时候会很怪,因为服务端插入了函数封装,行号会和源文件有偏差。
    • 增加了技术负担。前端开发应该只需静态文件就好。

    更多关于函数封装格式的设计驱使与用例的细节,称作异步模块定义(Asynchronous Module Definition,AMD), 可以在《为何 AMD?》页面找到。

  • 相关阅读:
    js18
    js17
    js16
    js15
    js14
    js13
    js12
    js11
    八月二十三的php
    八月二十二的php
  • 原文地址:https://www.cnblogs.com/zhepama/p/3092446.html
Copyright © 2020-2023  润新知