Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webp 的一些事儿 #8

Open
imfenghuang opened this issue Mar 15, 2018 · 1 comment
Open

webp 的一些事儿 #8

imfenghuang opened this issue Mar 15, 2018 · 1 comment

Comments

@imfenghuang
Copy link
Owner

imfenghuang commented Mar 15, 2018

webp的一些事儿

前言

webp 是 google 推出的一种支持无损、有损压缩的一种格式,详情可以看这儿 webp图片使用方案预研 ,这儿WebP 探寻之路 ,还有这儿 google官方文档 (需翻~墙)。

它的优点在于,一般情况下下,webp 格式的图片体积会比 pngjpg 格式的图片体积小,体积小就可以节省流量,提高加载速度,增强用户体验,体积小就是给公司节约 money。 目前,很多公司都已经启用了 webp,下面就看哈他们是怎么做的。以 h5 页面为例,app端的 webp 支持需要单独的解析库,这儿不做讨论。

京东

京东的首页(m.jd.com)没有启用 webp,但是在 京东超市 服装城 等页面,都启用了 webp

以京东超市为例:

京东超市首屏
京东超市首屏

在禁用 js 的情况下,可以看到,京东超市只加载了部分结构,对于图片,则通过 js 控制进行图片懒加载,通过查看网页源代码可以发现,首屏的图片懒加载由立即执行函数控制,如下图:

立即执行函数
立即执行函数

通过格式化该函数可以发现,该函数对 webp 进行了特性检测:

_webpDetect: function(a) {
    var b, c, d;
    a && (a && void 0 === a.feature && (a.feature = "lossy"), 
            void 0 !== window.iphone_webp_enabled ? "function" == typeof a.onSupport && a.onSupport() : (b = {lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA"},
            d = b[a.feature], c = new Image, c.onload = function() {
                d && "function" == typeof a.onSupport && c.width > 0 && c.height > 0 && a.onSupport()
            },
            c.onerror = function() {
                "function" == typeof a.onNotSupport && a.onNotSupport()
            },
            c.src = "data:image/webp;base64," + d)
        )
},

_webpFormat: function(a) {return d ? a : (this.isWebpSupported && this.imgForWebpRegex.test(a) && (a += ".webp"), a)}

run: function() {
    var a = this;
    a._scaleBodyContent(),
        this.scaleW = this.screenW / 1242,
        this._webpDetect({
            onSupport: function() {
                a.isWebpSupported = !0,
                    setTimeout(function() {
                            a._main()
                        },
                        0)
            },
            onNotSupport: function() {
                setTimeout(function() {
                        a._main()
                    },
                    0)
            },
            feature: "lossy"
        })
},


_main: function() {
    ...
    var h, i, j = this,
        k = 0,
        l = 0,
        m = j.imgs.length;
    m > 0 ? [].forEach.call(j.imgs, function(a) {
        if (a.offsetWidth <= 0 && a.offsetHeight <= 0) return void d();
        if (!j._isInViewPort(a)) return void d();
        var b = a.getAttribute("data-src");
        if (b) {
            b = j._resize(b),
                b = j._imgQuality(b),
                b = j._webpFormat(b);
            var e = new Image;
            e.onload = function() {
                    a.removeAttribute("data-src"),
                        a.className += " fadein",
                        a.setAttribute("src", b),
                        c(a)
                },
                e.onerror = function() {
                    d()
                },
                e.src = b
        } else d()
    }) : a()
}

通过执行 run() 函数,调用 _webpDetect() 检测用户浏览器对 webp 的支持情况,对于支持 webp 的,把对象的isWebpSupported 属性设置为真,之后执行 _main() 函数,而在 _main() 函数中,会通过执行 _webpFormat()函数,对支持 webp 格式的浏览器页面进行格式化,即把懒加载的 jpg png 图片地址加上 .webp,进而进行懒加载。

京东超市首屏加载
京东超市首屏图片懒加载

对于页面中下部,模板懒加载的部分,则会执行 main.js 文件中的 webp特征检测函数,执行逻辑则和前面的立即执行函数基本一致。

https://m.360buyimg.com/babel/s211x211_jfs/t2755/289/308242991/265446/5c33728e/570e3506N1b3ae24a.jpg!q50.jpg.webp 
https://m.360buyimg.com/babel/s746x219_jfs/t3097/182/8158408287/110514/dc2c6a32/58c0b759N39bc646f.jpg!q50.jpg.webp
https://m.360buyimg.com/babel/s2101x2101_jfs/t2755/289/308242991/265446/5c33728e/570e3506N1b3ae24a.jpg!q100.jpg.webp

从上面几张图片的 url 地址还可以发现,京东的图片支持分辨率和图片质量的控制,s2101x2101_jfs为图片分辨率, q100 为图片压缩质量。

美团

美团(i.meituan.com)首页有 webp 的特征检测,但是没有启用 webp 图片。但美团的一些具体页面,都启用了 webp 图片。

define(["util/cookie.js"], function(a) {
    var b = {
        checkWebp: function() {
            if (/(iPhone|iPad)/i.test(window.navigator.userAgent)) {
                var b = new Date;
                b.setDate(b.getDate() - 1), document.cookie = "webp=1; expires=" + b.toGMTString() + "; domain=.meituan.com; path=/"
            } else if (!a.get("webp")) {
                var c = new Image;
                c.onload = function() {
                    if (2 === c.height) {
                        var a = new Date;
                        a.setDate(a.getDate() + 365), document.cookie = "webp=1; expires=" + a.toGMTString() + "; domain=.meituan.com; path=/"
                    }
                }, c.src = "data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"
            }
        }
    };
    return b
});

美团首页 webp 特征检测。该代码会先检测浏览器UA,对于是 iPhone iPad等属于 iOS 系统的移动端浏览器,则直接将 cookie 中的 webp 记录设置比当前时间早一天的过期时间,即让该cookie 记录失效并删除该记录;对于非 iOS 的浏览器,且 cookie 中没有 webp记录的,则会执行一次特征检测,对支持 webp 的浏览器会设置一年过期时间的 webp 记录。这样可以方便其它页面进行判断。

美团周边游为例:

webp 特征检测代码:

!function() {
    "use strict";
    var e = function(e, t) {
        var i = t.isUC && t.isIphone
          , a = i;
        if (a)
            e.del("webp", {
                path: "/",
                domain: ".meituan.com"
            });
        else if (!e.get("webp")) {
            var o = new Image;
            o.onload = function() {
                if (2 === o.height) {
                    var t = new Date;
                    t.setDate(t.getDate() + 365),
                    e.set("webp", "1", {
                        path: "/",
                        domain: ".meituan.com",
                        expires: t.toGMTString()
                    })
                }
            }
            ,
            o.src = "data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"
        }
    };
    define(["touch/js/util/cookies", "touch/js/component/user_agent"], e)
}();

上述 webp 特征检测代码,对支持 webp 的浏览器,会在 cookie 中添加一个 webp=1 记录(还包含过期时间等)。

对比美团首页的特征检测,两者基本一样,只是在第一步判断 UA 上有点不同。至少安卓端上的UC浏览器(11.4.2.936)是支持 webp 的,而 iPadUA又被剔除了,比较奇怪。

美团周边游webp替换可以分成两个部分

1.前端检测与替换

! function() {
    "use strict";
    var i = function(i) {
            var t = {};
            if (i)
                for (var e = 0, a = i.length; e < a; ++e) {
                    var n = i[e];
                    t[n.poiid] = n.ct_poi
                }
            return t
        },
        t = function(i) {
            var t = {};
            if (i)
                for (var e = 0, a = i.length; e < a; ++e) {
                    var n = i[e];
                    t[n.dealid] = n.stid
                }
            return t
        },
        e = Math.PI / 180,
        a = function(i, t) {
            return i = Number(i),
                t = Number(t),
                function(a, n) {
                    var o, r, c = i * e,
                        u = a * e,
                        l = c - u,
                        g = t * e - n * e;
                    return c && u && l && g && (o = 12756274 * Math.asin(Math.sqrt(Math.pow(Math.sin(l / 2), 2) + Math.cos(c) * Math.cos(u) * Math.pow(Math.sin(g / 2), 2))),
                            r = Math.round(o),
                            r = r >= 100 ? Math.round(r / 100) / 10 : "0.1"),
                        r
                }
        },
        n = function(t, e, n, o, r, c, u, l, g) {
            if (e = i(e),
                "distance" === o || r) {
                r = r.split(",");
                var p = a(r[0], r[1])
            }
            for (var d = 0, m = t.length; d < m; ++d) {
                var s = t[d];
                if (s.ct_poi = e[s.poiid],
                    s.frontImg && (s.img = s.frontImg.replace("w.h", "100.0"),
                        s.imgHigh = s.frontImg.replace("w.h", "200.0"),
                        n && (s.img += ".webp",
                            s.imgHigh += ".webp")),
                    g && (5 === s.avgScore ? s.score = 50 : s.avgScore >= 4.5 ? s.score = 45 : s.avgScore >= 4 ? s.score = 40 : s.avgScore >= 3.5 ? s.score = 35 : s.score = 10 * Math.floor(s.avgScore)),
                    s.iconCount = 0,
                    s.poiTags)
                    for (var h = s.poiTags.split(","), f = 0; f < h.length; f++) {
                        var v = h[f];
                        if ("MP" == v)
                            s.MP = 1,
                            s.iconCount++;
                        else if ("ZTC" == v) {
                            if (g)
                                continue;
                            s.ZTC = 1,
                                s.iconCount++
                        } else if ("TC" == v) {
                            if (g)
                                continue;
                            s.TC = 1,
                                s.iconCount++,
                                s.iconCount++
                        } else if ("LINE" == v) {
                            if (g)
                                continue;
                            s.LINE = 1,
                                s.iconCount++
                        } else
                            "GROUP" == v ? (s.GROUP = 1,
                                s.iconCount++) : "MYY" == v ? s.MYY = 1 : "BTT" == v && (s.BTT = 1)
                    }
                if ("android" == u || "iphone" == u ? s.url = "imeituan://www.meituan.com/poi?channel=travel&id=" + s.poiid : "zhoubianyou" == l ? s.url = (g ? "//m.dianping.com/shop/" : "/awp/h5/lvyou/poi/detail/index.html?poiId=") + s.poiid + "&ct_poi=" + s.ct_poi : s.url = "/poi/" + s.poiid,
                    ("distance" === o || p) && s.lat && s.lng) {
                    var M = [];
                    M.push(p(s.lat, s.lng));
                    var w = Math.min.apply(Math, M);
                    w && "zhoubianyou" == l && (s.areaName = w + "km")
                }
            }
            return t
        },
        o = function(i, e, n, o, r, c, u, l) {
            if (e = t(e),
                "distance" === o || r) {
                r = r.split(",");
                var g = a(r[0], r[1])
            }
            for (var p = 0, d = i.length; p < d; ++p) {
                var m = i[p];
                if (m.stid = e[m.id],
                    m.image = m.squareimgurl || m.frontImg,
                    m.image && (m.img = m.image.replace("w.h", "100.0"),
                        m.imgHigh = m.image.replace("w.h", "200.0"),
                        n && (m.img += ".webp",
                            m.imgHigh += ".webp"),
                        delete m.image),
                    ("distance" === o || g) && m.mlls) {
                    var s = m.mlls.split(";");
                    if (s.length) {
                        for (var h = [], f = 0; f < s.length; f++) {
                            var v = s[f].split(",");
                            h.push(g(v[0], v[1]))
                        }
                        var M = Math.min.apply(Math, h);
                        M && (m.areaName = M + "km")
                    }
                }
                "android" == u || "iphone" == u ? m.url = "imeituan://www.meituan.com/deal?channel=travel&did=" + m.id : m.url = "/deal/" + m.id + ".html"
            }
            return i
        },
        r = function(i, t, e, a, r) {
            if (!i || !i.data || i.data && !i.data.length)
                return {};
            var c, u = {},
                l = !1;
            return e = e || {},
                a = a || "deal",
                r = r || "",
                l = "dp" == t.get("source"),
                u.isRecommend = i.isRecommend,
                "poi" == a && (i.count ? u.count = i.count : u.count = i.data.length,
                    u.tj = i.ct_pois,
                    c = n),
                "deal" == a && (i.paging && i.paging.count ? u.count = i.paging.count : u.count = i.data.length,
                    u.tj = i.stids,
                    c = o),
                u.list = c(i.data, u.tj, e.webp, t.get("sort"), t.get("mypos"), t.get("cateId"), t.get("utm_medium"), r, l),
                u
        };
    "object" == typeof exports ? module.exports = r : define(function() {
        return r
    })
}();

这一部分主要是在异步加载数据的部分。在结构懒加载时,会在支持 webp 的浏览器,添加 .webp 结尾。而首屏加载的页面结构,进行图片懒加载时,图片继续保留原格式,并不会替换成 webp

2.后台检测与数据返回
首屏加载的页面数据,会在第二次访问时发生改变。当用户第二次及之后访问美团周边游页面时,后台服务器会根据 cookie 中的 webp 记录,返回已经添加 .webp 结尾的页面数据。

美团周边游两次访问返回数据对比
美团周边游两次访问返回数据对比

天猫 淘宝

天猫和淘宝的 webp 也主要是利用特征检测,然后替换图片地址,这里就不展开叙述了。

其他

目前,对于启用 webp 图片比较普遍的做法都是通过前端进行特征检测,根据检测结果,替换图片链接地址。下面看一种别的方法: 利用 Service Workers 动态响应 webp

// Register the service worker
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
    // Registration was successful
    console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
    // registration failed :(
        console.log('ServiceWorker registration failed: ', err);
    });
}

// service-worker.js
"use strict";
// Listen to fetch events
self.addEventListener('fetch', function(event) {
    // Check if the image is a jpeg
    if (/\.jpg$|.png$/.test(event.request.url)) {
        // Inspect the accept header for WebP support
        var supportsWebp = false;
        if (event.request.headers.has('accept')) {
            supportsWebp = event.request.headers.get('accept').includes('webp');
        }
        // If we support WebP
        if (supportsWebp) {
            // Clone the request
            var req = event.request.clone();
            // Build the return URL
            var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp";
            event.respondWith(
                fetch(returnUrl, {
                    mode: 'no-cors'
                })
            );
        }
    }
});

先上代码再分析。上述代码先在页面上判断浏览器是否支持 service Worker,支持的,注册并安装 service-worker.js。在 service-worker.js 中,通过监听 fetch 事件,如果是 jpg png 的图片请求,则检测这些图片请求的请求头 accept 字段,是否包含 image/webp字符串。如果有该字符串,表示浏览器支持 webp 格式,进一步替换这些图片的 url 地址。

支持webp的浏览器图片请求
支持webp的浏览器图片请求

不支持webp的浏览器图片请求
不支持webp的浏览器图片请求

通过 service Worker 的方法看上去不错,但是目前 service Worker 的兼容性还比较差。因此,通过利用 Service Workers 动态响应 webp 的方法还难以用于生产环境。
service workers 兼容情况
service workers 兼容情况

总结

综上,目前行业内启用 webp 的做法普遍以前端特性检测+替换图片地址为主,辅之以后后端检测输出。

参考资料

欢迎批评指正 :)

PS:这是2017年4月做的一个 webp 小调研的总结

@iTaster
Copy link

iTaster commented Oct 11, 2018

强势围观

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants