前端工程与模块化框架

2,217 阅读6分钟
原文链接: github.com

@tm-roamer

你贴的那个项目可能有点问题,最好在那个repos的issue中留言问问作者具体用法,我这里不方便展开讨论了。

但针对fis的解决方案,我想多啰嗦几句,希望能耐心读完以下内容:

模块化打包和加载这件事,说起来有点麻烦,原因在当前这篇blog也提到一些,究其根本,要从模块化框架说起,对于模块化框架来说,一般有三个功能:

1. 模块接口导入导出

一般模块化框架或方案(requirejs/seajs/nodejs)遵循的是CommonJS的模块内部 上下文规范,也就是模块内上下文中的 require 函数和 exports 对象,以及 modules.exports 属性。

在ES6中,给出了 importexport 关键字,写法可以在网上查到,这里不举例了。

2. 模块定义

现阶段模块化框架(requirejs/seajs)为了在浏览器中实现模块定义,提供模块接口导入导出的方式,还须提供模块定义函数 define(id, deps, factory) ,模块代码就写在factory参数的作用域中,并能实现模块接口导入导出的管理。

同样,在ES6中,也给出了 module 关键字,用于定义模块。

3. 模块加载

在浏览器端,模块化框架为了解决依赖问题,需要实现按依赖异步加载模块资源,加载完成后才能执行对应的模块代码,这是在浏览器中实现模块化所必须面对的现实问题,其实模块加载和模块化的核心部分关系并不大,它可以被独立实现。seajs和requirejs在模块加载上实现并不一致,连接口名称都不同,由此可见模块加载的特殊性。

个人觉得,模块加载是前端工程化的重要组成部分

以上,就是模块化框架的全部内容了,总结为一幅图大概是:

js-modules

模块化核心部分(导入导出、定义)代码其实很少,写一个也就4、50行而已,半小时手起刀落应该就能搞定,真正重点是模块加载。

模块加载有这么几个痛点:

  1. 按需加载:就是依赖什么就加载什么,别搞aio(all-in-one)。
  2. 并行加载:不搞aio的打包,但也不能串行加载,有的框架要加载完才知道依赖,这样就串行请求了,工程上根本用不了。
  3. 请求合并:在HTTP1.1时代,虽然没有明显的证据表明请求合并比不合并能快多少,但是大家确实心里有阴影,不然也不会有all-in-one了,请求合并这件事还挺刚需的。

模块化框架做的好不好,加载是重点,以上三条,则是重点中的重点。

别人的方案我就不说了,这里就讲讲fis的思想,你可以考虑基于这样的思想来理解fis的一些设计:

fis就是盯着这三件事设计的,把能去掉的部分都去掉了,不留一点冗余,为的就是工程应用上真正的性能优化。

确切的说,fis希望使用者,能利用fis深耕模块化加载方案,用最简洁的方式表达自己的工程诉求,如果直接依赖requirejs和seajs,我觉得诸多冗余。

基于前面讲过的这些,模块化框架其实并不难实现,首先用50行代码解决核心的定义、导入、导出问题,具体代码可以参考 umd 超简单的。

然后就是加载部分了,fis的核心功能,其实不是什么压缩、校验这些基础功能,而是扫描所有项目代码,识别其中的依赖关系,并整理出一个 map.json 的资源依赖关系表,里面记录了资源的id、url和依赖关系,形如:

{
    "res": {
        "a.js": {
           "type": "js",
           "url": "/static/js/a-912cf3.js",
           "deps": [ "b.js", "a.css" ]
        },
        "a.css": {
           "type": "css",
           "url": "/static/js/a-c02a39.css",
           "deps": [ "e.css" ]
        },
        "b.js": {
           "type": "js",
           "url": "/static/js/b-b2ef91.js",
           "deps": [ "c.js", "b.css" ]
        },
        "b.css": {
           "type": "css",
           "url": "/static/js/b-fc01b2.css"
        },
        "c.js": {
           "type": "js",
           "url": "/static/js/c-103cf2.js"
        },
        "e.css": {
           "type": "css",
           "url": "/static/js/e-d0f135.css"
        },
    }
}

如果配置了fis的打包,fis合并文件后还会把文件的合并信息也写入到这个map.json中:

{
    "res": {
        "a.js": {
           "type": "js",
           "url": "/static/js/a-912cf3.js",
           "deps": [ "b.js", "a.css" ],
           "pkg": "p0"
        },
        "a.css": {
           "type": "css",
           "url": "/static/js/a-c02a39.css",
           "deps": [ "e.css" ],
           "pkg": "p1"
        },
        "b.js": {
           "type": "js",
           "url": "/static/js/b-b2ef91.js",
           "deps": [ "c.js", "b.css" ],
           "pkg": "p0"
        },
        "b.css": {
           "type": "css",
           "url": "/static/js/b-fc01b2.css",
           "pkg": "p1"
        },
        "c.js": {
           "type": "js",
           "url": "/static/js/c-103cf2.js",
           "pkg": "p0"
        },
        "e.css": {
           "type": "css",
           "url": "/static/js/e-d0f135.css",
           "pkg": "p1"
        },
    },
    "pkg": {
        "p0": {
            "url": "/static/pkg/p0-ac0f334.js",
            "type": "js",
            "has": [ "c.js", "b.js", "a.js" ]
        },
        "p1": {
            "url": "/static/pkg/p1-03d5ba.css",
            "type": "css",
            "has": [ "e.css", "b.css", "a.css" ]
        }
    }
}

好了,fis的想法是,团队花点时间写一个好一点的loader,利用这张表加载资源,肯定能同时解决前面说到的三个问题,顺便还能做一下静态资源localstorage缓存什么的,不是更妙?

比如源代码这么写:



    ...
    
    
        loader.registerMap(__FIS_MAP__);
        loader.fetch('a.js', function(a){
            console.log(a);
        });
    
    ...

通过写一个简单的fis插件,就可以把上述表的内容构建后生成到html中,替换那个我们跟构建工具约定好的 __FIS_MAP__ 变量,得到:



    ...
    
    
        loader.registerMap({
            "res": {
                "a.js": {
                    "type": "js",
                    "url": "/static/js/a-912cf3.js",
                    "deps": [ "b.js", "a.css" ],
                    "pkg": "p0"
                },
                "a.css": {
                    "type": "css",
                    "url": "/static/js/a-c02a39.css",
                    "deps": [ "e.css" ],
                    "pkg": "p1"
                },
                "b.js": {
                    "type": "js",
                    "url": "/static/js/b-b2ef91.js",
                    "deps": [ "c.js", "b.css" ],
                    "pkg": "p0"
                },
                "b.css": {
                    "type": "css",
                    "url": "/static/js/b-fc01b2.css",
                    "pkg": "p1"
                },
                "c.js": {
                    "type": "js",
                    "url": "/static/js/c-103cf2.js",
                    "pkg": "p0"
                },
                "e.css": {
                    "type": "css",
                    "url": "/static/js/e-d0f135.css",
                    "pkg": "p1"
                },
            },
            "pkg": {
                "p0": {
                    "url": "/static/pkg/p0-ac0f334.js",
                    "type": "js",
                    "has": [ "c.js", "b.js", "a.js" ]
                },
                "p1": {
                    "url": "/static/pkg/p1-03d5ba.css",
                    "type": "css",
                    "has": [ "e.css", "b.css", "a.css" ]
                }
            }
        });
        loader.fetch('a.js', function(a){
            console.log(a);
        });
    
    ...

仔细观察表就知道,我们想加载什么模块都能提前知道模块的依赖,并且知道模块被合并到哪个文件中了,这样我们就能通过loader框架按需的、并行的、合并请求的加载资源,进一步深耕loader,提升项目性能,把性能优化和开发分离开,实现工程层面的优化,这都将成为可能。

说了这么多,希望你能读到这里。

你的问题是fis与requirejs的结合,很抱歉,我还没有真正尝试花时间解决这个问题,fis和requirejs配合肯定能实现,如果给我1天左右的时间应该就能写出来fis的构建配置,然后把资源表传递给requirejs,并扩展requirejs的资源加载方式,改成查表加载,提升requirejs的加载性能。

但是我始终都没有这么搞,因为个人觉得这样做有点可笑。根据上述工程化方案介绍,这个loader能用到requirejs中现成的代码很有限,基本要深度接管requirejs资源加载的部分,将其重写,在这样种情况下,所谓的结合requirejs,等于仅仅用到了requirejs中的那50行的模块管理代码,其他将近2000行的代码都是冗余,我再额外添加几百行loader的代码实现高性能的资源加载,这不是很浪费么。。。基于这些考虑,fis团队才始终没有认真的写过fis与那些主流模块化框架的整合demo,感觉有点自欺欺人,糊弄小白用户。

总的来说,fis的模块化加载方案和requirejs的设计初衷有很大不同,requirejs为了不强依赖工具,写了大量代码解决无构建情况下的模块加载处理,但fis觉得,工程上应用时,为了高性能应该必然需要构建(包括开发阶段),无须多余考虑不构建的情况,这使得二者的设计方案有本质的区别。

真高兴你能读到这里!

当然,诚如你所讲,项目伊始,资源有限,想找个现成的方案,这个可以理解,fis是有这样现成的框架和方案的,请看这里: 基于FIS的纯前端模块化解决方案pure