blog icon indicating copy to clipboard operation
blog copied to clipboard

给 angularjs-requirejs-rjs-md5 加上 cdn 前缀的功能

Open lmk123 opened this issue 10 years ago • 0 comments

在接触 AngularJS 之前,我已经深入了解并使用了 RequireJS。我非常喜欢 AMD 的加载方式,因为它做到了模块化与按需加载,促进了代码之间的松耦合。

在接触到 AngularJS 之后,我就开始寻求一种方式——一种将 AngularJS 与 RequireJS 结合起来的方式,使 AngularJS 能借助 RequireJS 做到模块化和按需加载。经过一段时间的努力之后,我成功做到了这一点,并将成果发布到 lmk123/angularjs-requirejs-rjs-md5

顾名思义,在此基础上,我还做到了文件合并与 md5 签名,这样就可以将文件安全的放在 CDN 上。

但是还有一点没有做到——给文件路径加上 CDN 的前缀。

按照我那个项目的结构,只有 app/index.html 是不能缓存的,而其它文件:脚本、样式、模板都可以放在 CDN 上。于是我有了这么一个设想:将 AngularJS 应用放在 lmk123.github.io ,然后架设一个 CDN 服务器(比如七牛)dn-lmk123.qbox.me,然后将源镜像地址设为 lmk123.github.io,然后给 AngularJS 应用的所有加载路径加上 cdn 服务器前缀 dn-lmk123.qiniucdn.com。

我不知道我有没有表述清楚,简而言之,用户只有 app/index.html 是访问的 lmk123.github.io 上的,而其它所有文件都是访问的 cdn 服务器上的;当 cdn 服务器找不到对应的文件的时候就会到 lmk123.github.io 抓取并保存下来。

要想做到这一点,我就需要在请求文件的地方加上 cdn 的前缀。虽然 gulp-rev-all 里面有一个 prefix 设置,但它只对以 / 开头的绝对路径才有效,而我的项目里全都是相对路径,所以我只能自己想办法。

思考了一下,我那个项目有三个加载场景:

  1. *.html 里通过浏览器直接加载
  2. 由 RequireJS 加载的文件
  3. 由 AngularJS 通过 $templateRequest 服务加载的模板

那现在来逐个击破!

  1. 用 gulp-rev-all 里的 transformPath 函数能轻松的加上前缀
  2. RequireJS 就很简单了,data-main的引用地址指向 cdn 后,baseUrl 会自动被设置成 cdn 地址,所以都不需要对文件做特殊处理;如果不是用 data-main 启动应用,就需要自己写代码更新 baseUrl
  3. 可以使用 $provide.decorator 拦截 $templateRequest 服务,给每个 templateUrl 加上 cdn 前缀,但在实际实施过程中发现这么做无效,可能跟它本身就是一个函数有关,所以还是只能用 $http 拦截器;然而我觉得加一个拦截器有性能问题,还是用 transformPath 写入 cdn 吧。

动手完成

根据上面的分析,其实就是用 transformPath 设置项手动添加前缀。听起来很简单的,于是我试了试:

// CDN_PREFIX 即前缀,例如 http://my.cdn/ ;
// 此项目里使用的是 https://dn-lmk123.qbox.me/angularjs-requirejs-rjs-md5/cdn/
function transformPath( rev , source , file ) {
    if ( CDN_PREFIX ) {
        return CDN_PREFIX + rev;
    } else {
        return rev;
    }
}

上面这段代码成功的给场景一与场景三加上了前缀,但是同时也把通过 RequireJS 加载的文件路径给加上了,即:require( 'path/to/file' ) 变成了 require( 'http://my.cdn/path/to/file' ),于是 cdn 返回了一个 404。

也就是说,我必须有一种方法来区分_用 requirejs 加载的文件_和_其他文件_。

在折腾了很久之后,我发现真的没有太好的办法能使用代码区分这两种文件;然后我还发现,使用 requirejs 加载的 css 文件并不受影响,因为为了让 gulp-rev-all 能正确更新 css 文件的引用,我在写代码时手动带上了 .css 的扩展名,所以变成绝对路径之后仍能正常加载(也就是说,如果我在写代码时手动加上 .js 扩展名,那么就不需要这么麻烦了——但是这种牺牲编码方式的解决方案的兼容性太差了,并且每个路径前都加上前缀的话,或多或少会增加文件的大小)。

考虑到受到影响的实际上只有 js 文件,所以我手动维护了一个列表,列出了所有不是由 requirejs 加载的 js 文件:

paths.jsNotLoadByRequireJS = [ 'bootstrap.js' , 'vendor/require/require.js' ];

require.js 是在 index.html 里加载的,所以要列在里面;bootstrap.js 虽然是由 requirejs 加载的,但是因为是 data-main 入口文件,这个文件的路径会影响到 baseUrl 的值,所以为了后面的文件都能从正确的地方加载,这里也必须把它算作“不是由 requirejs 加载的 js 文件”。

最终的(或者说目前的) transformPath 函数是这样的:

function transformPath( rev , source , file ) {
            if ( CDN_PREFIX ) {
                if ( '.js' === file.revFilenameExtOriginal ) {
                    if ( paths.jsNotLoadByRequireJS.indexOf( file.revPathOriginal.slice( file.base.length ).replace( /\\/g , '/' ) ) < 0 ) {
                        return rev;
                    } else {
                        return CDN_PREFIX + rev;
                    }
                } else {
                    return CDN_PREFIX + rev;
                }
            } else {
                return rev;
            }
        }

确实绕的比较头晕 - -

细心的人可能还会发现一个问题:上面在判断一个文件是否在 paths.jsNotLoadByRequireJS 里的时候,没有用 source 变量,而是麻烦的用了 file.revPathOriginal.slice( file.base.length ).replace( /\\/g , '/' )

这是因为相对路径如 require( '../../directives/index-des' ) 会匹配不上这个列表,所以必须得找到文件真正的相对路径,所以麻烦了点。

总算是告一段落了。

现在我已经把这个功能加到我的 lmk123/angularjs-requirejs-rjs-md5 项目中了。你可以把 cdn/index.html 这个文件下载到本地,然后直接双击打开(即通过 file:// 协议打开),程序仍然能正常运行,因为所有其他的静态文件都托管在 https://dn-lmk123.qbox.me/angualrjs-requirejs-rjs-md5/ 上 :)

备用解决方案

前面提到了 gulp-rev-allprefix 设置,其实可以在代码里把文件引用路径全都写成以 / 开头的绝对路径,就不需要这么麻烦了,只是要确保程序是运行在网站根目录下的。为此我们在开发时会需要配置一个本地服务器(例如使用 AveVlad/gulp-connect

如果你有更好的解决方案,欢迎一起探讨 :)

lmk123 avatar Jun 19 '15 06:06 lmk123