如何像 Google 分析一样缩小 JavaScript?

How to minify JavaScript like Google Analytics?

你们中的大多数人可能都熟悉 Google Analytics 提供的这个小跟踪代码。

<script>
(
    function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    }
)(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-00000000-0', 'auto');
ga('send', 'pageview');
</script>

有趣的是,这个小摘录包含构成单词 isogram 的参数。该脚本还使用参数来声明变量,以减少最终文件大小的一些位。可以说,您在编写代码时不会使用这种模式 (?),所以我的问题是,Google 如何缩小其代码,这些技术是否也适用于凡人?

我在网上找到了关于高级优化的 example by Stephen Morley that includes a code that looks like something you'd write before minifying it. I took that code and run it through Google's very own Closure Compiler。正如预期的那样,生成的代码与 Google Analytics 使用的实际脚本完全不同。

(function(){window.b="ga";"ga"in window||(window.a=function(){window.a.q.push(arguments)},window.a.q=[]);window.a.c=(new Date).getTime();var c=document.createElement("script");c.src="//www.google-analytics.com/analytics.js";c.async=!0;var d=document.getElementsByTagName("script")[0];d.parentNode.insertBefore(c,d)})();

这一次,即使没有两个额外的命令,代码也不那么枯燥,而且更大。

所以澄清一下,我很好奇 Google 工程师是如何得出上述结果的(我不认为他们的代码实际上看起来像 Stephen 示例中的代码),并且即使你不是 Google 的一员?提前致谢!

编写此类脚本实际上相当简单且有趣。

这里有一个很长的例子,如何将常规函数变成这样的东西:

我会从一个虚构的脚本开始。我包含了一个异步加载 javascript 文件的 scriptLoader:

window.loadScript  = function(src){
    const scriptTag = document.createElement('script');
    scriptTag.async = true;
    scriptTag.src = src;

    const anyOtherScriptTag = document.getElementsByTagName('script')[0];
    anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}

当这样调用时:loadScript("/url.js") 它将在 DOM 中插入一个新的脚本标签(在第一个脚本标签之前),浏览器将下载脚本。

到目前为止一切顺利。假设我想在加载之前传递此脚本参数。在将要加载的脚本中,我访问了一个唯一的全局对象。我们称它为 window.myScriptArgs。所以理想情况下,一旦脚本加载,它就会读取 window.myScriptArgs 并相应地执行。

现在我可以做 window.myScriptArgs = [] 并收工了,但由于我的假设示例只会加载一个 单个 脚本文件,我将逻辑添加到 loadScript 函数还有。

window.loadScript  = function(src){
    window.myScriptArgs = window.myScriptArgs || [];
    const scriptTag = document.createElement('script');
    scriptTag.async = true;
    scriptTag.src = src;

    const anyOtherScriptTag = document.getElementsByTagName('script')[0];
    anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
loadScript("/my-script.js");

好的,所以我检查 myScriptArgs 是否已经存在,如果不存在,我将其设置为一个空数组。 现在我也知道 my-script.js 公开了一个全局的 myScript() 方法。所以我为它写了一个存根。此存根会将接收到的每个参数放入 myScriptArgs 数组中:

window.myScript = () => {
     window.myScriptArgs = window.myScriptArgs || [];
     window.myScriptArgs.push(arguments);
}

现在我可以调用 loadScript 并立即调用带有给定参数的 myScript() 。无需担心加载问题或诸如此类的问题。加载 "my-script.js" 后,它会读取 window.myScriptArgs 并按例外情况运行。代码如下所示:

window.myScript = () => {
    window.myScriptArgs = window.myScriptArgs || [];
    window.myScriptArgs.push(arguments);
}

window.loadScript  = function(src){
    window.myScriptArgs = window.myScriptArgs || [];
    const scriptTag = document.createElement('script');
    scriptTag.async = true;
    scriptTag.src = src;

    const anyOtherScriptTag = document.getElementsByTagName('script')[0];
    anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
loadScript("/my-script.js");
myScript('command', 'args', 'args1');
myScript('command2', 'args3', 'args4');

好的,按预期工作。让我们优化它。首先,我将 loadScriptmyScript 存根组合到一个名为 initMyScript():

的函数中
window.initMyScript = function(src){
    window.myScriptArgs = window.myScriptArgs || [];
    window.myScript = window.myScript || function(){
        window.myScriptArgs.push(arguments);
    }

    const scriptTag = document.createElement('script');
    scriptTag.async = true;
    scriptTag.src = src;

    const anyOtherScriptTag = document.getElementsByTagName('script')[0];
    anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
initMyScript("/my-script.js");
myScript('command', 'args', 'args1');
myScript('command2', 'args3', 'args4');

这台 atm 没什么特别的。现在,我将通过将 window 作为参数传递给 initMyScript 来摆脱多次 window. 调用。我还将使用 document 执行此操作。

脚本如下所示:

window.initMyScript = function(p, a, src){
    p.myScriptArgs = p.myScriptArgs || [];
    p.myScript = p.myScript || function(){
        p.myScriptArgs.push(arguments);
    }

    const scriptTag = a.createElement('script');
    scriptTag.async = true;
    scriptTag.src = src;

    const anyOtherScriptTag = a.getElementsByTagName('script')[0];
    anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
initMyScript(window, document, "/my-script.js");

现在让我们看看我在哪里重复自己以节省更多位。我使用字符串 script 两次,与 myScript:

相同
window.initMyScript = function(p, a, s, c, src){
    p.myScriptArgs = p.myScriptArgs || [];
    p[c] = p[c] || function(){
        p.myScriptArgs.push(arguments);
    }

    const scriptTag = a.createElement(s);
    scriptTag.async = true;
    scriptTag.src = src;

    const anyOtherScriptTag = a.getElementsByTagName(s)[0];
    anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
initMyScript(window, document, 'script', 'myScript', "/my-script.js");

我旅程的下一步是缩短变量。而且我还把这个函数变成了一个自执行函数来保存window.initMyScript定义:

(function(p, a, s, c, src){
    p.myScriptArgs = p.myScriptArgs || [];
    p[c] = p[c] || function(){
        p.myScriptArgs.push(arguments);
    }

    const q = a.createElement(s);
    q.async = true;
    q.src = src;

    const d = a.getElementsByTagName(s)[0];
    d.parentNode.insertBefore(q, d);
})(window, document, 'script', 'myScript', "/my-script.js");

关于我的最后一个谜:我编辑函数参数来混淆人们,并进一步缩小代码。您实际上可以使用逗号 ;).

在 javascript 中链接函数
(function(p, a, s, c, A, l, i){
    p["myScriptArgs"]=p["myScriptArgs"]||[],p[c] = p[c]||function(){
        p["myScriptArgs"].push(arguments)},
    l = a.createElement(s);l.async = true;l[A] = A;
    i = a.getElementsByTagName(s)[0];
    i.parentNode.insertBefore(l, i);
})(window, document, 'script', 'myScript', "/my-script.js");
myScript("arg1", "arg2");
myScript("arg2", "arg3");

请注意,我在函数中添加了两个额外的参数,那是因为我需要保存 createElement 返回的元素并且不想使用 var 语句 ;).

你可以更进一步,但你明白了。小功能,自己搞定也没问题。

此外,您可以使用像 UglifyJS 这样的缩小器,然后如果您真的很喜欢整个等值图,然后自己重​​命名变量...

注意:我没有测试任何这段代码。这里是龙。 虚构的代码是我在反混淆 google 示例中的错误尝试。 google-analytics 片段的工作原理与我的自定义片段几乎相同。 GA 优化了一点(例如将 true 变成 1),但你明白了。

详细了解我的示例中使用的内容: Immediately Invoked Function Expression Property accessors (especially Bracket notation)

和 javascript 具体的事情,比如将三个参数传递给一个需要 5 个参数的函数。

Google 很好,因为它们为我们提供了关于 https://developers.google.com

上很多事情的完整文档

您的许多回复都可以在 :

上找到

这是未缩小的 Analytics.js

(function(i, s, o, g, r, a, m){
  i['GoogleAnalyticsObject'] = r; // Acts as a pointer to support renaming.

  // Creates an initial ga() function.
  // The queued commands will be executed once analytics.js loads.
  i[r] = i[r] || function() {
    (i[r].q = i[r].q || []).push(arguments)
  },

  // Sets the time (as an integer) this tag was executed.
  // Used for timing hits.
  i[r].l = 1 * new Date();

  // Insert the script tag asynchronously.
  // Inserts above current tag to prevent blocking in addition to using the
  // async attribute.
  a = s.createElement(o),
  m = s.getElementsByTagName(o)[0];
  a.async = 1;
  a.src = g;
  m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

// Creates a default tracker with automatic cookie domain configuration.
ga('create', 'UA-XXXXX-Y', 'auto');

// Sends a pageview hit from the tracker just created.
ga('send', 'pageview');

这是他们提供的缩小版(漂亮版):

(function (i, s, o, g, r, a, m) {
    i['GoogleAnalyticsObject'] = r;
    i[r] = i[r] || function () {
            (i[r].q = i[r].q || []).push(arguments)
        }, i[r].l = 1 * new Date();
    a = s.createElement(o),
        m = s.getElementsByTagName(o)[0];
    a.async = 1;
    a.src = g;
    m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');

ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');

here带有闭包编译器工具的缩小版

(function (a, e, f, g, b, c, d) {
    a.GoogleAnalyticsObject = b;
    a[b] = a[b] || function () {(a[b].q = a[b].q || []).push(arguments)};
    a[b].l = 1 * new Date;
    c = e.createElement(f);
    d = e.getElementsByTagName(f)[0];
    c.async = 1;
    c.src = g;
    d.parentNode.insertBefore(c, d)
})(window, document, "script", "//www.google-analytics.com/analytics.js", "ga");
ga("create", "UA-XXXXX-Y", "auto");
ga("send", "pageview");

看起来是一样的。
您可以在 Github repository.

上找到有关该项目的更多详细信息

我喜欢认为编写代码有无限多种可能的方法。 (尽管这可能不是真的)将代码写入缩小并可能保存 space 的地方的一种方法是使用混淆。 例如下面的代码:

function NewObject(prefix)
{
    var count=0;
    this.SayHello=function(msg)
    {
          count++;
          alert(prefix+msg);
    }
    this.GetCount=function()
    {
          return count;
    }
}
var obj=new NewObject("Message : ");
obj.SayHello("You are welcome.");

可以混淆成这样:

var _0x3c28=["\x53\x61\x79\x48\x65\x6C\x6C\x6F","\x47\x65\x74\x43\x6F\x75\x6E\x74","\x4D\x65\x73\x73\x61\x67\x65\x20\x3A\x20","\x59\x6F\x75\x20\x61\x72\x65\x20\x77\x65\x6C\x63\x6F\x6D\x65\x2E"];function NewObject(_0x12c4x2){var _0x12c4x3=0;this[_0x3c28[0]]= function(_0x12c4x4){_0x12c4x3++;alert(_0x12c4x2+ _0x12c4x4)};this[_0x3c28[1]]= function(){return _0x12c4x3}}var obj= new NewObject(_0x3c28[2]);obj.SayHello(_0x3c28[3])

这是在 https://javascriptobfuscator.com/Javascript-Obfuscator.aspx 上使用免费混淆算法完成的。

我确定 google 当然有他们自己的代码处理方式 :)。

我感觉 "isogram" 这个词有点来自缩小此代码的 Google 员工的狡猾暗示。

由于等值线图是 a word with no repeating characters,它代表了缩小参数和其他必须彼此唯一的变量名称所必需的精确逻辑。

很可能,这个术语被嵌入到缩小器中,因此第一个缩小变量集合将表明他们对唯一字母排序的逻辑有所了解。

由于单词 isogram 本身就是一个等值图,创建缩小逻辑的人可以将其设置为检查参数或参数列表,以了解有 7 个 arguments/params 的情况,并且在这种情况下,只需将每个替换为单词 "isogram" 中的相应字母即可。这会增加一些开销,但这种情况很少见,Google 确实有很多服务器和网络工程师来优化他们的脚本。

图像有一段代码如下:

(function(){
  window.GoogleAnalyticsObject = 'ga';
  window.ga = window.ga || function(){
    (window.ga.q = window.ga.q || []).push(arguments)
  },
  window.ga.l =1 * new Date();
  var a = document.createElement('script'),
  var m = document.getElementsByTagName('script')[0];
  a.async = 1;
  a.src = '//www.google-analytics.com/analytics.js';
  m.parentNode.insertBefore(a, m)
})();

然后,修改您的代码以将您需要的所有对象作为参数传递:

(function(i, s, o, g, r, a, m){
  i['GoogleAnalyticsObject'] = r;
  i[r] = i[r] || function(){
    (i[r].q = i[r].q || []).push(arguments)
  },
  i[r].l =1 * new Date();
  a = s.createElement(o),
  m = s.getElementsByTagName(o)[0];
  a.async = 1;
  a.src = g;
  m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

去掉所有空格最后得到:

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

希望我说清楚了,再见。

已更新:你是在问他们为什么选择"isogram"这个词吗?它是 "known" 等角词之一,如果您需要更多参数,请参阅 Wikipedia

您可以使用 npm 和类似 gulp 的任务运行程序。 Gulp 有一个名为 uglify 的插件,它可以消除多余的空格,并将参数和变量减少到一个字母,以进一步减少代码中的字符总数。