winner王

winner王 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

winner王 提出了问题 · 2月18日

执行npm install命令为什么报错了?

如图,新下载了项目,安装依赖npm install后就报错了,后来发现只要带npm install 的命令执行都报这个错,大佬知道怎么解决吗???
image.png

关注 1 回答 1

winner王 赞了文章 · 2020-12-07

我如何用最简单的前端技术揭示那些灰色产业背后的原理

这篇文章讲述的一些很通用简单的前端技术,但却是实现某些灰色产业的关键技术,当然我只实现一些很简单的思路和方法,尽量写得简单明了,希望能举一反三,一起探索前端技术的各种无限可能。我在几年前也写过几篇这方面的文章,后面由于某些关系删减了不少,有兴趣的可以去找回看看 😀

相信我们经常看直播或者电视剧都会留意到有很多的弹幕,有时候弹幕比电视剧或者直播的游戏更有意思,细心的同学会发现有时候弹幕会出现一致的相同,然后不断的循环,有时候还是出自同一个用户发送的,那么这个用户怎么做到在短时间内循环不断的发送同一个消息或者不同的消息的呢。

26

其实很简单,以网页版的花椒直播作为例子,任何的评论都肯定有输入框,如果人为的输入文字再发送实在是太低效了,作为灰产专业户,肯定不会自己手动输入的。

那么最直接暴力的方式是把任务交给开发者去自动实现,如果你是前端开发者,你可以使用选择器获取到该输入框的节点。

25

那节点非常容易,直接对着输入框右键查看即可,我们可以看到它本质是下面代码实现的。

<input name="message" class="tt-type-msg" placeholder="说点什么" />

那么你拿这个节点方式就能多种多样了,只要保证拿到是唯一的节点即可:

24

下面三行其实都可以:

document.querySelector('[name="message"]');
document.querySelector(".tt-type-msg");
document.querySelector('[placeholder="说点什么"]');

拿到节点我们下一步应该就是模拟我们的用户操作,输入弹幕的文字,可以看到我们在浏览器的控制台输入以下代码的敲回车运行的时候,我们的 666牛逼 自动的写入到输入框,懂前端的同学一看就明白这代码的意思:

23

无非就是改写输入框的 value,然后赋上输入的值。

document.querySelector('[placeholder="说点什么"]').value = "Wscats 666";

当然输入的值这样是写死的,我们还可以使用词典去生成随机的弹幕或者评论,具体复杂的评论可以线上找字典生成,你如果不想麻烦,可以使用一个简单的数组,把你想要发送的话定制好,然后配合随机函数来发送。

const offset = parseInt(Math.random() * 7);
const word = ["厉害", "牛逼", "666", "佩服", "真实", "下饭", "菜"];
document.querySelector('[placeholder="说点什么"]').value = word[offset];

自动输入是完成了,我们还差最后一步,自动点击发送,其实模拟就是鼠标点击发送按钮或者键盘敲回车键,那这都好办,我们还是刚才那个理念,模拟操作那个页面位置,就要寻找那个位置背后的代码节点,我们同样把鼠标放到红色的发送按钮右键查看,在控制台中就可以看到这个节点的信息。

22

由于这个节点没什么很多属性,我们直接根据 class 做唯一标记检索即可。

document.querySelector(".tt-type-submit").click();

同样按照上面的方法把代码输入到控制台敲回车,就可以看到输入框的文字被自动发送出去。

21

但是这里还差一点不完美,我们整个代码只是模拟了一次操作,那么怎么实现循环发呢,不要忘了,浏览器拥有定时器和延时器这种接口,所以我们可以去利用 setInterval 函数去实现按规定的时间评率发送消息,那么我们就可以解放双手安心去欣赏美女主播,哦,不,是游戏主播精彩的对局了是吧,游戏主播还不时线上截屏抽奖,挂着脚本在上面白嫖这不是美滋滋。

setInterval(() => {
    document.querySelector('[placeholder="说点什么"]').value = "一楼牛逼";
    document.querySelector(".tt-type-submit").click();
}, 2000);

这是我们实现的效果,当然这个只是最基本最入门的基础,我们可以还可以改变很多玩法,举个例子,如果我是一个主播,我会安排机器人帮我自动刷礼物,自动帮我回复用户,自动帮我点赞。

20

当然这里篇幅有限,我就简单写点,比如我们可以接入第三方给我们实现的图灵机器人接口,key 是我自己注册的,每天有使用次数限制,想测试的同学最好去官网自己注册一个哈:

http://www.tuling123.com/openapi/api?key=c75ba576f50ddaa5fd2a87615d144ecf&info=你好

这个接口只要改变 info 的参数,去请求图灵机器人就可以得到机器人的回复,比如你可以跟他说,讲个笑话我听,张学友是谁等等,我们自己就不花时间去实现这种 AI 机器人了,直接调用接口即可,具体你还可以去官网定制。

我们可以获取最后回复用户的内容,然后回复他的内容,当然如果你足够无聊,你还可以实现两个机器人互相聊天的功能,在直播室瞬间热闹起来,只要水军够多,直播间就可以有多热闹,在直播带货中尤为有用。

但这里还是使用花椒直播作为例子,欣赏下美女动人的歌声嘛。还是之前的思路,你如果想回复最后一个弹幕,你就需要获取这个弹幕背后所在的节点:

19

console.log("最后的一条评论为:" + $(
    $(".tt-msg-content-h5.tt-msg-content-h5-chat")[
        $(".tt-msg-content-h5.tt-msg-content-h5-chat").length - 1
    ]
)
.text()
.replace(/(^\s*)|(\s*$)/g, ""));

这段代码不复杂,其实就是做了几件事,把直播室聊天记录的最后一条内容的纯文本获取出来,然后删除里面的空格,具体你还可以做一下其他的处理,我这里就不弄复杂了。

18

拿到最后的回复,只要交给图灵接口去发送即可,当然很多直播网站都会防一手脚本刷的,首先如果网站是 http协议,图灵接口注意就使用 http 协议,如果是 https 的话,就直接改成 https 即可,不然可能会报协议错误。

image

这里直接放代码,就不写原生了,用花椒直播做例子试了下是支持 jQuery,那么直接上 $ 符号,我们发送一个 ajax 请求,请求的服务端地址就是图灵接口,请求的 info 参数就是最后一个用户回复的内容。

$.ajax({
    type: "GET",
    url:
        "https://www.tuling123.com/openapi/api?key=c75ba576f50ddaa5fd2a87615d144ecf&info=" +
        $(
            $(".tt-msg-content-h5.tt-msg-content-h5-chat")[
                $(".tt-msg-content-h5.tt-msg-content-h5-chat").length - 1
            ]
        )
            .text()
            .replace(/(^\s*)|(\s*$)/g, ""),
    success: function (data) {
        console.log(data);
        $(".tt-type-msg").val(data.text);
        $(".tt-type-submit").click();
    },
});

当我们在控制台输入以下代码敲回车的时候,发现请求是发送成功的,但是花椒这里做了些信息的判断,估计是防止小白刷留言,但是这怎么能难道我呢,想把我们卡在控制台,不能操作,这还是太嫩了。

我们只需要做一下骚操作即可,学过前端都知道断点是怎么打的,直接展开源码,加一个条件断点,输入 false 把这个错误忽略掉吧,让它顺利往下执行。

17

现在我们就顺利调通了机器自动回复最后一个用户,控制台出现的红色报错不影响我们的自动回复,当然这里我就不浪费时间继续写下去了,懂得同学都懂,只要加一些其他的处理即可,比如加定时器即可在上面看着机器人自言自语,加 id 判断即可只回复其他用户不回复自己的话,都可以发散思路去实现。

16

在一些灰色行业,这种技术是大行其道的,比如微博点赞,热搜控评,淘宝优惠券批量获取,商品秒杀等等,一切即可交给各种机器人水军去完成,当然这些一般不会在客户端实现,会把他们搬到服务器上,有后台专门去处理...

说了那么多,我们还是纯粹点用在技术领域吧,毕竟其他领域我不感兴趣,之前在一些技术简历上看到有些人的掘金,Github,博客等粉丝,等级,提交数和阅读数甚多,这里也是可以做手脚的,这里还是强调只针对技术,我就不用掘金做例子了,用 Github 这些作为例子吧,比如 Github 我们的提交量其实可以任意更改时间段,范围和数量。

15

比如这种经典的提交记录,看上去很有意思,有的开发者是自己慢慢用真实提交数叠的,但是可以轻易人为的,其实实现起来思路也是很简单,直接交给前端技术吧,思路跟刚才上面的代码类似。

首先每个 commit 其实就像刚才的直播发送评论,由于刚才是纯客户端的前端技术实现,现在我们换个口味用纯服务端的前端技术实现吧,扩展下思路。

我们一般在 Github 的仓库里面提交代码都是使用这种命令方式的,这也是 Github 官网提供的方式:

git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/wscats/test.git
git push -u origin main

这种提交方式会以当前本地端的时间为准,所以如果不想改动任何代码,你可以简单粗暴的更改系统时间,然后再执行上面的命令,这种方式适合新手,错过提交,想补回一些提交去画对应的图案:

14

但是作为具备一定前端技术的开发者,肯定不希望用这种方式,实在太低效了,每次都得在可视化界面操作一次时间,这怎么能忍。

那我们可以借助 node 来实现以下,为了能在服务端批量执行 git 命令,我们可以使用 child_process 的接口,熟悉 shell 脚本的同学,其实可以直接用 shell 来实现,我们使用 exec 方法,他可以使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。

const exec = require("child_process").exec;
module.exports = (cmd) => {
    return new Promise((resolve, reject) => {
        exec(cmd, function (error, stdout, stderr) {
            if (error) {
                console.log(error);
                reject(stderr);
            } else {
                resolve(stdout);
            }
        });
    });
};

我们再暴露一个写入代码的接口,我们可以新建一个 message.txt 文件,记录每一个代码片段,最简单粗暴就是直接生成一个随机数或者日期写入进去,每次写入代表着每一个 commit,后期你更可以改良成把一份完整的代码按 AST 语法树等,切成有规律的代码块,然后附上真实的提交记录,来进行每一次代码提交。

const fs = require("fs");
module.exports = (message) => {
    return new Promise((resolve, reject) => {
        fs.appendFile("message.txt", `${message}\n`, (err) => {
            err ? reject() : resolve();
        });
    });
};

最后就是核心的代码了,这里最关键的其实 git commit -m "${commitTime}" --no-edit --date="${commitTime}" 这一段,--date 参数可以改成其他的时间(没记错的一般最早是 1970 的 1 月 1 号到 2038 年的 1 月 19 号),具体原因是因为这是时间戳的范围,我也没去试过这么前和这么后的时间了,有兴趣的同学可以自己试试。

const cmd = require("./cmd");
const file = require("./file");
let day = 10;
const random = (lower, upper) => {
    return Math.floor(Math.random() * (upper - lower + 1)) + lower;
};
const commit = async () => {
    const today = new Date();
    today.setTime(
        today.getTime() - 0 * 24 * 60 * 60 * 1000 - day * 24 * 60 * 60 * 1000
    );
    let commitTime = `${today.getFullYear()}.${
        today.getMonth() + 1
    }.${today.getDate()}`;
    if (today.getFullYear() > 2019) {
        return;
    }
    let commitNumber = random(1, 10);
    let dayNumber = random(1, 3);
    while (commitNumber) {
        await file(commitTime);
        await cmd("git status");
        await cmd("git add .");
        await cmd(`git commit -m "${commitTime}" --no-edit --date="${commitTime}"`);
        commitNumber--;
    }
    if (day >= 10) {
        day -= dayNumber;
        commit();
    } else {
        // await cmd('git push origin master');
    }
};
commit();

上面的代码执行成功后,就会自动提交,然后帮你铺满 Contributions 的时间表,这远远比你的手动更改面板效率高多了。

13

为了观赏性,你可以用随机函数去制造每天的 commit 量,也可以随机制造出隔天提交的操作,当然也可以随机提交 commit 的信息,比如接入上面提到的图灵机器人就可以实现提交信息的自言自语,当然我自己牺牲一下,拿我自己的 Github 做一次小白鼠吧,各位同学就慎重娱乐了,万一最后只学会打满格子,不会删除格子就尴尬了,至于如何删除这些格子,其实多补点 Git 知识就懂了,就不展开说了,放一下实现后的效果,具体可以去我 Github 看看。

12

那么这里代码实现可以告诉我们,很多时候看技术文章不要只看表面的量化数据,诸如一些阅读量,点赞量和提交量都是我们参考的一部分而已,更多的是看这位作者呈现内容的质量。

我搜了下 Github,还是有不少这种骚操作的,具体都有很多开源代码的实现,大家都可以参考下,作为程序员日常可以拿来娱乐下,切记就千万不要拿来糊弄人了。

11

上面这个例子就点到即止,它的应用面其实很广,比如各种排行榜上的名次,在很多的榜单上面一定要斟酌细看,一般用户可能会难以发现使用了这种技术,时间还有不少,我就再续点吧,还是照样,我们拿技术社区做实验,我们用微软 VSCode 的插件排行榜上做实验,本来想用微博或者掘金做实验,但想想这可能要负后果啊,要讲武德,我们还是使用技术社区纯粹点,没太多的功利性。

10

这里都是为我们开发者提供便利的插件,这种榜单本质上是流行榜单,具有一定的参考意义,但是我们更多是要理性的去分析,因为进榜可能分分钟背后是一种"技术"在推动。

我们这次前端技术客户端和服务端都试一下,是否可行,我们尝试帮我们把一款插件的下载量提升上来,我们发现网页端其实是有下载按钮,那么根据上面介绍的技术就已经足够实现了,思路很简单。

9

找到该按钮背后的节点代码,然后按频率触发点击下载即可,这个下载完全模拟了人在客户端的基本操作,那么这个下载会成功发出请求,并被完全记录到后台,然后最后更新呈现到下载量里面。

8

当然这种方法简单粗暴可以快速达到目的,但是有没有比这种方式更迅速有效呢?答案肯定是有的,并且还有很多很多的方案,具体参考这个插件。

7

执行后直接在一天内进入下载量第一梯队了,当然可以直接把它拉满,进入下载量第一,但是这样娱乐娱乐就够了,如果只是为了处处争第一,太过了就违背技术的初衷了,提供一个插件的本质是为更多的开发者提供服务,年轻人还是要讲武德的,我实验了部分插件,发现这里是没有任何限制的,据我观察大部分靠前的插件都是官方插件,其中有些下载量是有点微妙的,这里我也希望微软在后续能修复这种漏洞,就像国产的 Hbuilder 那样,插件是账号绑定才能下载的。

这里我顺便讲下原理,因为插件除了在网页下载也支持在客户端下载,所以同样我们只要监听客户端的下载,就可以模拟整个下载流程,然后循环执行即可,有点像小号刷淘宝的好评一样,但是一般商业化项目这些入口都会有防范,没那么容易给你执行这些脚本。

6

这里可以非常灵活,既可以使用前端技术去动态执行下载,也可以劫持下载的请求去循环发送达到欺骗服务器的效果,就是我们可以在 node 端去伪造这个请求的头部和请求体,然后发送过去服务器。

这种方案稍微做改变可能用于暴力破解(wifi,服务器密码),中间人攻击等。

6.1

在网络世界其实每天都有这类人跑着这些脚本去扫描漏洞的,然后伪造请求去达到目的,我的服务器就经常被这些人光顾,难受啊。

当然除了 node,能实现这种能力的工具还有很多,这里就不一一列举,我们只围绕前端范围去分析。

5

所以我们为什么有时候登录注册需要这么多重的验证,还不是开发者为了保护我们跟恶势力抵抗嘛,对于一些榜单,比如热搜和热评,我们可作为一个参考,不能过度迷信。

这里顺便一提的不管手机还是电脑,其实应用和插件监听系统的方式有很多,就如上面的插件,尽量还是使有用稳定代码源的比较稳妥,因为他们其实可以轻易在本地对系统进行操作,比如使用简单的代码去监听你的键盘输入,轻易去读写你的本地文件等等。这些代码有时候甚至很简单,它可以离我们很远,也可以离我们很近。

4

就如下面这种简单的代码,在不合适的场景它将会是一段可怕的代码,配合一些手段可以悄悄进入你的系统,然后记录你输入的点点滴滴,包括输入的密码,输入习惯,语法习惯,虚词实词分布,常用字词和语言组织能力等,配合一些隐蔽的上报,能做到对你用户精确的分析,只要用户输入的足够多,它对你就越了解,因为每个人都是生长在一个独一无二的环境和时代,这种环境对用户语言的影响是非常的深远,就如曹雪晴和高鹗两位红楼梦的作者都不在了,但是留下的珍贵文学,通过数据分析,足够证明这本书拥有前后不一样的风格。

const inputs = document.querySelectorAll("input");
[].forEach.call(inputs, (input) => {
    input.addEventListener("input", (e) => {
        console.log(e.target);
    });
});

这些恶意代码可能会伴随着插件等方式植入到你的电脑,方式多种多样,比如套壳,"颜色"网站引导,魔改的山寨系统或者浏览器等...

3

技术是一把双刃剑,本质应该是向善的,小时候背三字经,印象最深刻的一句话是人之初,性本善,同样我也认为,技术之初,本质也是善的,换句话来说,技术的初衷是为用户带来价值,而非损失。

2

我非常乐此不疲的挖掘和探索技术的价值和乐趣,最近我就喜欢在微信中写点简单的脚本去自动回复下朋友家人,工作在外可能一句早安和晚安都是温暖的。

定时提醒下老人家吃饭睡觉,准点给朋友发送生日快乐,对方还是蛮开心的,这是里面其中一些关键的代码,当然你自己可以赋予它更多的功能。

setInterval(function () {
    $(".edit_area").html("微信需要发送的文字");
    $(".edit_area").trigger($.Event("keydown", { keyCode: 13, ctrlKey: true }));
    $(".btn_send").click();
}, 3000);

当然作为程序员也得有生活追求嘛,打工人的生活都是相似的,抽空去看看美丽的世界,帮我刷刷优惠券哈哈,让浏览器帮我自动完成些事情吧,程序员的世界可能注定这么平平无奇 😁

1

let y = 0;
let num = 0;
let imgArr = [];
setInterval(() => {
    let imgs = document.querySelectorAll("img");
    let length = imgs.length;
    if (num !== length) {
        num = length;
        imgArr = imgs;
        console.log(length, imgArr);
    }
    y = y + 1;
    scrollTo(0, y);
}, 1);

希望自己以后能有更多的时间去贡献更多有意思的代码,开源不易,且行且珍惜。

最后

上面所有的代码其实本质都很简单,单纯的分享和交流,仅供大家娱乐,如有不足请多多包涵。

实际很多场景还有很多的门槛等待着我们去解决的,比如验证码的识别,用户行为和调用链分析,木马植入等,以后如果有机会再找时间写写,作为开发者我们也应当有义务和意识,在用户操作的每个入口去把好关和提醒用户。

0

当然技术的乐趣在于探索,而非作恶,最后附上我心爱的小钢琴,与君共乐:🎹 https://github.com/Wscats/piano

往期的文章可以移步这里:🔖 https://github.com/Wscats/articles

你的支持(Star 和 Fork)是我前进的最大动力~

感谢音乐和编程的陪伴!也致敬各位奋斗于 996/007 的代码家,音乐不曾辜负任何人,正如 Leehom Wang 歌曲中唱到:

如果世界太危险,只有音乐最安全,带着我进梦里面,让歌词都实现! —— 《我们的歌》
查看原文

赞 16 收藏 7 评论 1

winner王 赞了文章 · 2020-12-04

你必须知道的webpack插件原理分析

本文在github做了收录 github.com/Michael-lzg…

demo源码地址 github.com/Michael-lzg…

在 webpack 中,专注于处理 webpack 在编译过程中的某个特定的任务的功能模块,可以称为插件。它和 loader 有以下区别:

  1. loader 是一个转换器,将 A 文件进行编译成 B 文件,比如:将 A.less 转换为 A.css,单纯的文件转换过程。webpack 自身只支持 js 和 json 这两种格式的文件,对于其他文件需要通过 loader 将其转换为 commonJS 规范的文件后,webpack 才能解析到。
  2. plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

plugin 的特征

webpack 插件有以下特征

  • 是一个独立的模块。
  • 模块对外暴露一个 js 函数。
  • 函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply 方法。
  • apply 函数中需要有通过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到当前编译的 compilation 对象,如果是异步编译插件的话可以拿到回调 callback。
  • 完成自定义子编译流程并处理 complition 对象的内部数据。
  • 如果异步编译插件的话,数据处理完成后执行 callback 回调。
class HelloPlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options) {}
  // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    })
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,
    compiler.plugin('emit', function (compilation, callback) {
      // 处理完毕后执行 callback 以通知 Webpack
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行
      callback()
    })
  }
}

module.exports = HelloPlugin
  1. webpack 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例。
  2. 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  3. 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin (事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

事件流机制

webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable

Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条 webapck 机制中,去改变 webapck 的运作,使得整个系统扩展性良好。

Tapable 也是一个小型的 library,是 Webpack 的一个核心工具。类似于 node 中的 events 库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。方法如下:

//  广播事件
compiler.apply('event-name', params)
compilation.apply('event-name', params)

// 监听事件
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})

我们来看下 Tapable

function Tapable() {
  this._plugins = {}
}
//发布name消息
Tapable.prototype.applyPlugins = function applyPlugins(name) {
  if (!this._plugins[name]) return
  var args = Array.prototype.slice.call(arguments, 1)
  var plugins = this._plugins[name]
  for (var i = 0; i < plugins.length; i++) {
    plugins[i].apply(this, args)
  }
}
// fn订阅name消息
Tapable.prototype.plugin = function plugin(name, fn) {
  if (!this._plugins[name]) {
    this._plugins[name] = [fn]
  } else {
    this._plugins[name].push(fn)
  }
}
//给定一个插件数组,对其中的每一个插件调用插件自身的apply方法注册插件
Tapable.prototype.apply = function apply() {
  for (var i = 0; i < arguments.length; i++) {
    arguments[i].apply(this)
  }
}

Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks,在 Tapable 源码中可以看到,他们是:

exports.SyncHook = require('./SyncHook')
exports.SyncBailHook = require('./SyncBailHook')
exports.SyncWaterfallHook = require('./SyncWaterfallHook')
exports.SyncLoopHook = require('./SyncLoopHook')
exports.AsyncParallelHook = require('./AsyncParallelHook')
exports.AsyncParallelBailHook = require('./AsyncParallelBailHook')
exports.AsyncSeriesHook = require('./AsyncSeriesHook')
exports.AsyncSeriesBailHook = require('./AsyncSeriesBailHook')
exports.AsyncSeriesLoopHook = require('./AsyncSeriesLoopHook')
exports.AsyncSeriesWaterfallHook = require('./AsyncSeriesWaterfallHook')

Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步钩子和异步钩子。
  • tapAsync:回调方式注册异步钩子。
  • tapPromise:Promise 方式注册异步钩子。

webpack 里的几个非常重要的对象,Compiler, CompilationJavascriptParser 都继承了 Tapable 类,它们身上挂着丰富的钩子。

编写一个插件

一个 webpack 插件由以下组成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

下面实现一个最简单的插件

class WebpackPlugin1 {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    compiler.hooks.done.tap('MYWebpackPlugin', () => {
      console.log(this.options)
    })
  }
}

module.exports = WebpackPlugin1

然后在 webpack 的配置中注册使用就行,只需要在 webpack.config.js 里引入并实例化就可以了:

const WebpackPlugin1 = require('./src/plugin/plugin1')

module.exports = {
  entry: {
    index: path.join(__dirname, '/src/main.js'),
  },
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'index.js',
  },
  plugins: [new WebpackPlugin1({ msg: 'hello world' })],
}

此时我们执行一下 npm run build 就能看到效果了

image

Compiler 对象 (负责编译)

Compiler 对象包含了当前运行 Webpack 的配置,包括 entryoutputloaders 等配置,这个对象在启动 Webpack 时被实例化,而且是全局唯一的。Plugin 可以通过该对象获取到 Webpack 的配置信息进行处理。

compiler 上暴露的一些常用的钩子:

image

下面来举个例子

class WebpackPlugin2 {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    compiler.hooks.run.tap('run', () => {
      console.log('开始编译...')
    })

    compiler.hooks.compile.tap('compile', () => {
      console.log('compile')
    })

    compiler.hooks.done.tap('compilation', () => {
      console.log('compilation')
    })
  }
}

module.exports = WebpackPlugin2

此时我们执行一下 npm run build 就能看到效果了

image

有一些编译插件中的步骤是异步的,这样就需要额外传入一个 callback 回调函数,并且在插件运行结束时执行这个回调函数

class WebpackPlugin2 {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    compiler.hooks.beforeCompile.tapAsync('compilation', (compilation, cb) => {
      setTimeout(() => {
        console.log('编译中...')
        cb()
      }, 1000)
    })
  }
}

module.exports = WebpackPlugin2

Compilation 对象

Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。

简单来说,Compilation 的职责就是构建模块和 Chunk,并利用插件优化构建过程。

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,compilation 就会被重新创建。

Compilation 上暴露的一些常用的钩子:

image

CompilerCompilation 的区别

  • Compiler 代表了整个 Webpack 从启动到关闭的生命周期
  • Compilation 只是代表了一次新的编译,只要文件有改动,compilation 就会被重新创建。

手写插件 1:文件清单

在每次 webpack 打包之后,自动产生一个一个 markdown 文件清单,记录打包之后的文件夹 dist 里所有的文件的一些信息。

思路:

  1. 通过 compiler.hooks.emit.tapAsync() 来触发生成资源到 output 目录之前的钩子
  2. 通过 compilation.assets 获取文件数量
  3. 定义 markdown 文件的内容,将文件信息写入 markdown 文件内
  4. 给 dist 文件夹里添加一个资源名称为 fileListName 的变量
  5. 写入资源的内容和文件大小
  6. 执行回调,让 webpack 继续执行
class FileListPlugin {
  constructor(options) {
    // 获取插件配置项
    this.filename = options && options.filename ? options.filename : 'FILELIST.md'
  }

  apply(compiler) {
    // 注册 compiler 上的 emit 钩子
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
      // 通过 compilation.assets 获取文件数量
      let len = Object.keys(compilation.assets).length

      // 添加统计信息
      let content = `# ${len} file${len > 1 ? 's' : ''} emitted by webpacknn`

      // 通过 compilation.assets 获取文件名列表
      for (let filename in compilation.assets) {
        content += `- ${filename}n`
      }

      // 往 compilation.assets 中添加清单文件
      compilation.assets[this.filename] = {
        // 写入新文件的内容
        source: function () {
          return content
        },
        // 新文件大小(给 webapck 输出展示用)
        size: function () {
          return content.length
        },
      }

      // 执行回调,让 webpack 继续执行
      cb()
    })
  }
}

module.exports = FileListPlugin

手写插件 2:去除注释

开发一个插件能够去除打包后代码的注释,这样我们的 bundle.js 将更容易阅读

思路:

  1. 通过 compiler.hooks.emit.tap() 来触发生成文件后的钩子
  2. 通过 compilation.assets 拿到生产后的文件,然后去遍历各个文件
  3. 通过 .source() 获取构建产物的文本,然后用正则去 replace 调注释的代码
  4. 更新构建产物对象
  5. 执行回调,让 webpack 继续执行
class RemoveCommentPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    // 去除注释正则
    const reg = /("([^"]*(.)?)*")|('([^']*(.)?)*')|(/{2,}.*?(r|n))|(/*(n|.)*?*/)|(/******/)/g

    compiler.hooks.emit.tap('RemoveComment', (compilation) => {
      // 遍历构建产物,.assets中包含构建产物的文件名
      Object.keys(compilation.assets).forEach((item) => {
        // .source()是获取构建产物的文本
        let content = compilation.assets[item].source()
        content = content.replace(reg, function (word) {
          // 去除注释后的文本
          return /^/{2,}/.test(word) || /^/*!/.test(word) || /^/*{3,}//.test(word) ? '' : word
        })
        // 更新构建产物对象
        compilation.assets[item] = {
          source: () => content,
          size: () => content.length,
        }
      })
    })
  }
}

module.exports = RemoveCommentPlugin

推荐文章

webpack的异步加载原理及分包策略
总结18个webpack插件,总会有你想要的!
搭建一个 vue-cli4+webpack 移动端框架(开箱即用)
从零构建到优化一个类似vue-cli的脚手架
封装一个toast和dialog组件并发布到npm
从零开始构建一个webpack项目
总结几个webpack打包优化的方法
总结vue知识体系之高级应用篇
总结vue知识体系之实用技巧
总结vue知识体系之基础入门篇
总结移动端H5开发常用技巧(干货满满哦!)

查看原文

赞 18 收藏 13 评论 0

winner王 赞了文章 · 2020-11-25

双十一SSR优化实践:秒开率提升新高度

前言

会场是每年双十一的主角之一,会场的用户体验自然也是每年最关注的点。在日趋复杂的业务需求下,如何保障我们的用户体验不劣化甚至能更优化是永恒的命题。

今年(2020)我们在不改变现有架构,不改变业务的前提下,在会场上使用了 SSR 技术,将秒开率提高到了新的高度(82.6%);也观察到在用户体验得到优化的同时,业务指标如 UV 点击率等也有小幅的增长(视不同业务场景有不同的提升,最大可达 5%),带来了不错的业务价值。

本文将从服务端、前端两个角度介绍我们在 SSR 上的方案与经验

  1. 前端在解决工程化、业务效果评估上的具体实践与方法论
  2. 服务端在解决前端模块代码于服务端执行、隔离和性能优化上的具体实践与方法论

(更多干货欢迎关注【淘系技术】公众号)

页面体验性能的核心指标

在正文开始前我们先介绍一下衡量的相关指标,从多年前雅虎 yslow 定义出了相对完整的体验性能评估指标,到后来的谷歌的 Lighthouse 等新工具的出现,体验性能的评估标准逐渐的统一且更加被大家认同。

会场的评估体系

基于 Web.Dev 以及其他的一些参考,我们定义了自己的简化评估体系

image.png

TTFB(Time to First Byte): 第一个字节的时间 - 从点击链接到收到第一个字节内容的时间

FP(First Paint): 第一次绘制 - 用户第一次看到任何像素内容的时间

FCP(First Contentful Paint): 第一次内容绘制 - 用户看到第一次有效内容的时间

FSP(First Screen Paint,首屏可视时间): 第一屏内容绘制 - 用户看到第一屏内容的时间

LCP(Largest Contentful Paint): 第一次最大内容绘制 - 用户看到最大内容的时间

TTI(Time To Interactive): 可交互时间 - 页面变为可交互的时间(比如可响应事件等)

大体上来说 FSP 约等于 FCP 或 LCP

会场的现状

我们的会场页面是使用基于低代码方案的页面搭建平台产出的,一个由搭建平台产出的会场页面简单而言由两部分组成:页面框架(layout)和楼层模块。

image.png

页面框架有一份单独的构建产物(即页面的 layout html 以及基础公共的 js、css 等 assets 资源)。每个楼层模块也有单独的一份构建产物(模块的 js、css 等 assets 资源,基础公共 js 的依赖版本信息等)。

页面框架的任务比较繁杂,负责页面的 layout、根据页面的搭投数据加载具体哪些楼层模块并组织分屏渲染模块。

会场原有的 CSR 渲染架构如下图,可以分成三部分:

  1. 客户端,包括手机淘宝等阿里系 App
  2. 文档服务,用于响应页面的主文档 HTML 清求
  3. 数据服务,用于响应页面的数据请求

image.png

原有的CSR渲染流程如下图

image.png

针对会场的性能,除了基础的大家都知道的前端优化手段之外,还结合客户端能力做过很多优化方案,比较具有代表性的有两个:

  1. 客户端主文档/Assets 缓存
在客户端内,我们利用了端侧提供的静态资源缓存能力,将 HTML 和基础公共的 JS 等资源,推送下发至用户侧客户端缓存。当客户端的 WebView 请求资源时,端侧可根据规则来匹配已下发的缓存包,在匹配成功后直接从本地缓存中读取对应的 HTML 和 JS 资源,而无需每次都请求网络、大大缩短了页面的初始化时间
  1. 数据预加载
从用户点击跳转链接到页面开始加载数据,中间还要经过客户端动画、WebView初始化、主文档 HTML 请求以及基础公共 js 的加载和执行这些过渡阶段,加起来有 几百ms 的时间被浪费掉。通过客户端提供的数据预加载能力,在用户点击后就可以立即由 native 开始页面的数据加载,等页面的基础公共 js 执行完需要使用页面数据时,直接调用 jsbridge 接口即可从 native 获取已经预先加载好的数据

在这些优化工作的基础上会场的体验性能已经可以达到不错的水准。

随着时间的推移,基于我们 CSR 渲染体系下的优化存在一些瓶颈:

  • 在线上复杂网络环境下(低网速、虚假的 WiFi)、Android 中低端机上的页面体验还是不尽人如意,特别是模块的加载和执行时间比较长,且这部分用户的占比有增长趋势
  • 作为拉新的一个重要手段,外部唤起淘宝或者天猫客户端因为需要时间来初始化一些功能组件,比如网络库等,页面的体验从体感上不能追平端内的会场
  • 会场是营销活动性质的业务,页面的复访率相对较低,且页面内容全面个性化。离线的 HTML 快照等用户侧缓存手段会因为缓存的数据过期导致出现重复渲染(打开更慢)、页面元素跳动(渲染闪烁、重排)等伤害体验的问题

还有没有优化手段呢?以一个 2020 年双十一会场页面,使用 PC 上的 Chrome DevTools 的 performance 离线分析结果为例,我们看一下重点的问题

image.png

可以看到页面从 FP 到 FCP 这段过渡的时间较长且只有背景色。FCP 到 LCP 这段时间处于等待图片加载的时间,优化空间较小,且难以衡量。

离线分析尚且如此,线上更有着复杂的网络环境/差异化的手机机型等,这样的“背景色”时间对用户的体验有很大的伤害,可能会让用户更加容易跳失。

我们的 CSR 渲染体系依赖前端+客户端的能力,从工作机制上已经很难再有比较大的提升。怎么才能让会场页面的体验更上一层楼呢,我们想到了服务端渲染(SSR), 针对 FP 到 FCP 这段时间进行攻坚优化

image.png

SSR 的线下测试结果,FP 到 FCP 从 825ms -> 408ms

SSR 要怎么做?

大的方向

SSR 本身意为服务端渲染,这个服务端可以在 任何地方 ,在 CDN 的边缘节点、在云上的中心机房或者就在你家的路由上。

实现一个 SSR 的 demo,熟悉的人应该都知道套路:

搞一个 Rax Server Renderer,传入一个 Rax Component,renderToString,完事了

业界也已经有很多实践的案例,但就像“把大象装进冰箱里”一样,看似简单的事情在双十一所要求的复杂场景稳定性下,需要有稳妥可实施的执行方案。

如何在现有的这套模块化、成熟的渲染架构之上使用SSR呢,一开始我们往常规的思路去想,直接在文档 HTML 响应中返回服务端渲染完成的 HTML,看下来存在几个问题:

  • 改造成本高,对现有的服务端架构改动比较大(CDN 缓存失效,文档服务的要求更高)
  • 无法复用现有的客户端性能优化能力,比如客户端主文档/Assets 缓存和数据预加载能力,会劣化完全可交互时间
  • CDN 缓存无法利用,TTFB 的时间增加,带来了新的 “完全白屏阶段”
  • SSR 服务不稳定因素较多,自动降级为CSR的方案复杂,无法保证 100% 能够降级
  • 主文档 HTML 的安全防护能力较弱,难以抵御黑产的恶意抓取

基于以上的问题,我们考虑是否还有其他的方案可以 低风险 、 低成本 地实现SSR呢?经过短暂且激烈的讨论,我们设计了「数据 SSR」架构方案,分享给大家。

数据 SSR 渲染架构如下,文档服务返回的内容保持静态化不变,数据服务新增调用一个独立的 SSR FaaS 函数,因为数据里有这张页面包含的模块列表和模块需要的数据,SSR FaaS 函数可以直接根据这些内容动态加载模块代码并渲染出 HTML。

image.png

这套方案在客户端内的场景下可以很好的将 前端 + 客户端 + 服务端三者的能力结合到一起。

有人可能会问,为什么这个方案会带来性能提升呢?不就是把浏览器的工作移到了服务端吗?我们举个例子(数据仅为定性分析,不代表真实值)。在正常 CSR 渲染流程下,每段消耗的时间如下,首屏可视时间总共耗时1500ms。

image.png

在SSR渲染流程下,在「调用加载基础js」之前的耗时都是一样的,由于下面两个原因,在服务端渲染的耗时会比客户端低几个数量级。

  • 服务端加载模块文件比在客户端快很多,而且服务端模块资源的缓存是公用的,只要有一次访问,后续所有用户的访问都使用这份缓存。
  • 服务端的机器性能比用户手机的性能高出几个数量级,所以在服务端渲染模块的耗时很小。根据线上实际耗时统计,服务端单纯渲染耗时平均 40ms 左右。

由于 HTML 被放到了数据响应中,gzip 后典型值增加 10KB 左右,相应的网络耗时会增加 30~100ms不等。最终 SSR 的渲染流程及耗时如下,可以看到 SSR 首屏的可视时间耗时为660ms,比CSR提升了800ms。

image.png

总而言之,「数据 SSR」的方案核心哲学是:将首屏内容的计算转移到算力更强的服务端

核心问题

大方向确定了,我们再来看看 SSR 应用到生产中还存在哪些核心问题

  1. 如何做到 CSR/SSR 的平滑切换
  2. 开发者如何开发出“能 SSR”的代码
  3. 开发者面向前端编写的代码在服务端运行的不可控风险
  4. 低代码搭建场景下,在服务端解决楼层模块代码加载的问题
  5. 服务端性能
  6. 怎么衡量优化的价值

别急,我们一个一个的来看解法

如何做到 CSR/SSR 的平滑切换?

在我们的页面渲染方案中,有两个分支:

  1. 页面未开启数据SSR,则与原有的 CSR 渲染流程一样,根据数据中的模块列表加载模块并渲染
  2. 页面开启了数据SSR并且返回的数据中有 SSR HTML,则使用 SSR 的 HTML 塞入到 root container 中,然后根据数据中的模块列表加载模块最终 hyrdate。

image.png

优点很明显

  • 风险低,能够无缝降级到CSR,只需要判断数据接口的响应中是否成功返回 HTML 即可。如果 SSR 失败或者超时(未返回 HTML),通过设置合理的服务端超时时间(例如 80ms),不会影响到用户的最终体验
  • 能够利用端上成熟的性能优化能力,比如客户端缓存能力,数据预加载能力。有客户端缓存能力,页面的白屏时间与原CSR一致;有了数据预加载能力,能够在页面加载之前就开始请求数据服务

在线上服务时,我们可以通过 HASH 分桶的方式对流量进行划分,将线上的流量缓慢的切换到 SSR 技术方案,既能保证稳定性,同时还可以方便的进行业务效果的进一步评估。

比较好的字符串转换为数字的 HASH 方法有 DJBHash,验证下来分桶效果较为稳定

开发者如何开发出“能 SSR”的代码?

很多做 SSR “demo”分享的往往会忽略一个重要点:开发者

在双十一的场景下,我们有百+的开发者,三百+的楼层模块,如何能推动这些存量代码升级,降低开发者的改造适配成本是我们的一个核心方向。

我们原有的楼层模块构建产物分为 PC/H5/Weex 三个,业界通用的是针对 SSR,单独构建一个 target 为 node 的构建产物。在实际 POC 验证过程中,我们发现其实绝大部分的模块并不需要改造就可以直接适配 SSR,而新增构建产物会牵扯到更多的开发者,于是想找寻别的解决方案。

复用现有 Web 构建产物的一个问题是,Webpack 4 默认会注入一些 Node 环境相关变量,会导致常用的组件库中的类似 const isNode = typeof process !== 'undefined' && process && process.env 的判断异常。不过还好这个是可以关闭的,开发环境下其他的类似 devServer 等的注入也是可以关闭的,这给了我们一点慰藉,最终复用了 Web 的构建产物。像更新的 Webpack 5 中把 target 的差异给弱化了,也可以更好的定制,让我们未来有了更好的社区化方向可以继续靠拢。

解决完构建产物的问题,在本地开发阶段,Rax 团队提供了 VSCode SSR 开发插件,集成了一些 best practice 以及 lint 规则,写代码的时候就可以发现 SSR 的相关问题,及时规避和修复。

image.png

同时我们模拟真实线上的环境,在本地提供了 Webpack 的 SSR 预览调试插件,直接 dev 就可以看到 SSR 的渲染结果。

针对开发者会在代码中直接访问 windowlocation 等变量的场景,我们一方面开发了统一的类库封装调用抹平差异,另一方面在服务端我们也模拟了部分常用的浏览器宿主变量,比如 windowlocationnavigatordocument  等等,再加上与 Web 共用构建产物,所以大部分模块无需改造即可在服务端执行。

接下来的模块发布阶段,我们在工程平台上增加了发布卡口,若在代码静态检查时发现了影响 SSR 的代码问题就阻止发布并提示修复。

image.png

由于实际的业务模块量较大,为了进一步缩小改造的范围,测试团队联合提供了模块的批量测试解决方案。具体的原理是构造一个待改造模块的 mock 页面,通过比较页面 SSR 渲染后的截图与 CSR 渲染后的截图是否一致,来检测SSR 的渲染结果是否符合预期

image.png

开发者面向前端编写的代码在服务端运行的不可控风险

尽管我们在开发阶段通过静态代码检查等方法极力规避问题,实际上仍然存在一些针刺痛着我们的心

  1. 开发者把全局变量当缓存用造成内存泄露
  2. 错误的条件结束语句导致死循环
  3. 未知情况页面上存在不支持 SSR 的模块

这些疑难点从 SSR 的机制上其实很难解决,需要有完善的自动降级方案避免对用户的体验造成影响。

在说更详细的方案前要先感谢我们自己,前端已经提前做到了 CSR/SSR 的平滑切换,让服务端能每天不活在恐惧里 = =

对于机制上的问题,可以引申阅读到之前分享过的 在 Node.js 中 ”相对可靠” 的高效执行可信三方的代码。我们这里主要聚焦在如何快速止血与恢复。

image.png

FaaS 给服务端降低了非常大的运维成本,“一个函数做一件事”的设计哲学也让 SSR 的不稳定性局限在了一块很小的部分,不给我们带来额外的运维负担。

低代码搭建场景下,在服务端解决楼层模块代码加载的问题

业界分享的一些 SSR 场景基本都是整页或者 SPA 类型的,即 SSR 所使用的 bundle 是将整页完整的代码构建后暴露出一个 Root Component,交由 Renderer 渲染的。而我们的低代码搭建场景,由于整个可选的模块池规模较大,页面的楼层模块是动态选择、排序和加载的。这在前端 CSR 情况下很方便,只要有个模块加载器就可以了,但是在服务端问题就比较复杂。

image.png

还好我们的模块规范遵守的是特殊的 CMD 规范,有显式的依赖关系声明,可以让我们在获取到页面的楼层组织信息之后一次性的把页面首屏的全部 Assets 依赖关系计算出来。

image

在服务端加载到代码后,我们就可以拼装出一个 Root Component 交给 Renderer 渲染了。

服务端性能

性能上主要是有几个方面的问题

  1. 机制问题
  2. 代码问题
机制问题

由于楼层模块很多,在实际执行的过程中发现存在一些机制上的性能问题

  1. 代码的 parse 时间较长且不稳定
  2. 流量较低情况下难以触发 JIT

优化方案的话比较 tricky

  1. 缓存 vm.Script  实例,避免重复 parse
  2. 期望一致性 HASH 或自动扩缩容(本次未实现)

巡检的时候还观测到存在小范围的 RT 抖动问题,分析后定位是同步的 renderToString 调用在微观上存在排队执行的问题

image.png

在这种情况下会造成部分渲染任务的 RT 为多个排队任务的渲染 RT 叠加,影响单个请求的 RT(但不影响吞吐量)。这种问题要求我们需要更精确的评估备容的资源。机制上有效的解法推测可以让 renderToString 以 fiber 的方式执行,缓解微观排队造成的不公平的问题。

代码问题

性能问题的分析当然免不了 CPU Profile,拿出最爱的 alinode 进行分析,很快的可以找到热点进行针对性优化。

image.png

上图中标蓝的方法为 CMD 加载器计算依赖的热点方法,对计算结果进行缓存后热点消除,整体性能提升了 80% 》.》

怎么衡量优化的价值

这么多的投入当然需要完善的评价体系来进行评价,我们从体验性能和业务收益两个分别评估。

体验性能

基于兼容性较好的 PerformanceTiming (将被 PerformanceNavigationTiming 替代),我们可以获取到前端范畴下的一些关键的时间

  • navigationStart
  • firstPaint

其中 navigationStart 将会作为我们的前端起点时间所使用。在前端之外,对用户的交互路径而言真正的起点是在客户端的点击跳转时间 jumpTime ,我们也联合客户端进行了全链路埋点,将客户端 native 的时间与前端的时间串联了起来,纳入到我们的评价体系中。

在最开始的核心指标中,我们看到有 FCP、TTI 这几个指标。目前的 Web 实现中,还未有兼容性较好的可以线上衡量的方案(线下可以使用 DevTools 或者 Lighthouse 等工具),因此我们通过其他的方式来做近似代替

image
线上取到的数据通过 tracker 的方式进行无采样上报,最终我们可以通过多个维度进行分析

  • 机型
  • 网络条件
  • 是否命中 SSR
  • 是否命中其他前端优化

主要的衡量指标有

  • 从用户点击到 FCP 的时间(FCP - jumpTime)
  • 从 NavigationStart 到 FCP 的时间(FCP - NavigationStart)
业务收益

这部分很忐忑,体验的优化是否会带来真金白银的收益呢?我们直接通过 AA 和 AB 实验进行业务数据的分析。

基于之前的切流分桶,我们可以通过类似 hash 值 % 10 的方式将流量分为 0~9 号十个桶,首先通过 AA 实验验证分桶是否均匀

image

统计指标举例

这一步是保证分桶的逻辑本身不会有数据的倾斜影响置信度。接下来我们再进行 AB 实验,逐步增加实验桶验证业务数据的变化。

最终的效果

搞了这么多,得看看产出了。在这次双十一会场中,我们切流了多个核心的页面,拿到的第一手数据分享给大家。

image

循环.gif

小米5 骁龙 820 处理器

可以看到,在 Android 碎片化的生态的下,带来的提升甚至超出了预期,这也给了我们未来更大的动力,将前端 + 客户端 + 服务端的能力更有效的结合到一起,带给用户更好的体验,给业务创造更大的价值。

未来的渲染架构还会更复杂吗?

为了更好的用户体验,当然会了!我们可以简单的看看短期和长期的一些事情

电商体验指标的统一定义

长期以来,业务在用户侧的实现有 Web、Native、Hybrid 混合开发等多种选择,每个体系都有着自己的封闭体验衡量标准,这就造成了一些“鸡同鸭讲”的问题。而 Web.dev 中所定义的 FCP、LCP 通用评价体系也并不适合电商场景,能展示出核心的商品/店铺其实对一张页面来说就完成了它的使命。

后续我们可以将体验指标评估标准对齐,将起点时间、绘制完成时间等在多个体系对齐概念与实现,达到互相之间可以横向比较良性竞争的状态。

工程上还有更多的事情要做...

在 Webpack 5 的 Release Note 中,我们可以看到 Webpack 正在弱化 target 的一些特殊处理,将 Web 描述为了 browserlike 的环境。同时还提供了自定义 browserlist 的能力,可以给予开发者更方便处理跨端的兼容性问题的能力。这一变化将推动我们更快的拥抱社区,获得更好的开发体验。

现有的 SSR 静态代码检查方案会有一些漏网之鱼,还有没有更完善的方案能从工程上前置解决代码风险(性能、安全)问题也是未来的一个方向。

ServiceWorker Cache 等离线缓存快照

复访率高,变化不太大的页面可以利用 ServiceWorker Cache 等方案,将之前的渲染结果缓存下来,命中缓存直接用,未命中缓存 SSR。降低服务端压力的同时可以让体验更好。

SSR 的性能优化与安全

现阶段的 Node.js 或者说 V8,对于动态加载代码的情况支持并没有特别的完善,缺失了安全相关的保护逻辑。并且从性能上来说,SSR 属于 CPU 密集型的 workload,纯异步的优势并不明显,也可能需要一些特殊的解决方案来配合。

外部投放场景的覆盖

「数据 SSR」的方案是端内的最佳方案,却是外投场景的最劣方案。外投场景下由于用户是在第三方 App 中打开页面,相应的缺失了客户端的定制化优化能力,SSR 调用会造成数据服务的 RT 增加,反而推后了 FCP。

这时候古老的 HTML 直出方案又可以再捞回来了。

image.png

核心在于

  1. 利用 CDN 的边缘计算能力,可以较好的做到“动静分离”以及容灾
  2. 使用中心化的 SSR 函数,可以将 SSR 的不稳定性与 CDN 的可靠性分离,保证近端链路的可靠,避免出现近端直接不可用导致的无法恢复

近端的流式方案经常被提及,但是在实际的使用中会遇到当流式输出遇到错误时,用户侧无法有效容灾的问题(HTML 损毁,无法补救)。通过“动静分离”可以将页面分为

image.png

仅将 Root Container 进行动态化,进而在享用流式输出带来的 TTFB 提前的好处的同时又能兼顾容灾 SSR 的不稳定。和业务团队更可以一起探讨下如何将页面更好的从业务上做到“动静分离”,而不是仅从技术的角度出发。

总结

渲染架构的不断改进实质上是我们在有限且变化的环境下(终端性能、复杂网络和多变业务)自发做的适应,也许有那么一天,环境不再是问题,性能优化的课题将会消失。我们项目组有时候还开玩笑,等明年手机叒换代了,5G 100% 普及了,是不是这些优化都可以下线了😝

但是!现在看理想还有点远,在 2020 的双十一会场我们走进了一个新的深水区,期待未来技术与业务结合能带给广大用户更棒的体验!

查看原文

赞 14 收藏 13 评论 1

winner王 关注了用户 · 2020-09-28

京东科技开发者 @jdcdevloper

京东科技开发者(JD Tech Developer)是京东科技集团旗下为AI、云计算、IoT等相关领域开发者提供技术分享交流的平台。
拥抱技术,与开发者携手预见未来!

官方微信:京东科技开发者(JDT_Developer)

关注 1023

winner王 赞了文章 · 2020-09-28

诚不欺我,只有程序员能过的节日,十一前还有它

每天,数以万计努力“搬砖”的程序猿们

表面,一副认真模样敲代码

心里,跪地呐喊我的妈

“离国庆假期居然还有 20 天?”

“1024 什么时候能到?”

“哪怕一次,提前让我们过个节,可好?”

可!!!

9月12日(今天)

是2020年的第256天

同样也是【国际·程序员节】,又称“溢出节”

起初,它只是一个俄罗斯官方节日

后来,包括中国,印度,英国,德国,美国等

诸多国家也在同一时间为程序员们庆祝这个节日

之所以选择每年的第256天,

是取其一字节(8位)溢出的意思

(从0开始,最大到255)

正如1024一样,256最能表示程序员所熟知的意义

程序员们就像是一个个1024、256一样

以最踏实、核心的功能模块改变世界

虽然,程序员节并不意味着【放假】

但小编深感“程序猿”之不易

今天,以京东智联云开发者社区之名

给大家放个(误...)

咳咳,给大家盘点一些灰常实用的

干货(干活?)内容

小编最朴实的愿望,还是希望大家今后能

搬地容易,搬地轻松,挣得多!

IT 领域千变万化,每天都有新东西、新技术出现。因此,一个合格的程序员想保持竞争力,就要不断学习,去适应和跟上快速变迁的技术趋势,京小云精选出几篇程序员必备技能,帮助大家在技术的世界中保持战斗力!

简易代码的秘密 — 开发人员如何产生10倍的价值

成为一名有10倍价值的开发人员有捷径吗?如果有一个神奇的秘密,能为您打开一道通向全新、高效的软件开发世界的大门,您愿意聆听吗?总是有怀疑者认为:"没有捷径可走!每个人都需要练习才能变得更好!" 的确如此,但是有没有一个可以令开发过程产生巨大变化的关键细节?有!在本文中,作者就使用了一些示例代码,逐步介绍了应用这个方法的全过程。

点击阅读

《敲出简易代码的秘密 — 开发人员如何产生10倍的价值》               

2020年TOP7的编程语言和框架,它们至少还能风靡全球5年以上

在本文中,我们将看一看由作者推荐的编程语言和框架列表。作者始终相信在接下来的5年至10年中,对于开发者来说,这份列表中的语言和框架仍然会有用。同时,为了应对日新月异的程序发展,在记下想法时,本文在对编程范式和创新是如何随着时间演变,进行了思考。

点击阅读

《2020年TOP7的编程语言和框架,它们至少还能风靡全球5年以上》

[](https://mp.weixin.qq.com/s?__...

程序员必看:如何充分利用代码审查提升你的代码质量?

作为软件工程师,工作上保持代码审查的习惯十分重要。 而审查其实就像编写代码一样,必须不断练习并提供反馈,这一点至关重要。 本文是VTS工程师制订的代码审查目标及一些改善建议。

点击阅读

《程序员必看:如何充分利用代码审查提升你的代码质量?》

[](https://mp.weixin.qq.com/s?__...

作为软件开发人员进行开发:7种方法可以更快地掌握代码

学习风格是一件因人而异的事情。即便如此,本文作者的许多导师和前辈们都曾强烈向他推荐过一些非常有效的方法和工具来学习编程 。在文中,作者是这样描述这些方法的:“我和我的同学有着不同的学习风格和专业背景,唯一的共通点大概就是我们都是用这些方法和工具顺利毕业了吧。”本文的分享都是作者发自内心,希望在学习如何编程的第一天就知道的方法。值得一读。

点击阅读

《作为软件开发人员进行开发:7种方法可以更快地掌握代码》

2020年,一场肆虐全国的新型冠状病毒肺炎疫情,让许多企业陷入经营困难危机之中。就职机会紧张,薪资缩减,变相裁员,加之随着年龄的增长,很多开发者有家有室不敢轻易放弃当前的工作,去搏一个不确定的创业机会。职场不稳定的风险怎么破?京小云挑选的这几篇文章,希望能在下半年,陪你一起扛过,继续加油。

编程中寻找成就感:哪个编程岗位适合您?

通常,开发者会发现自己所处的岗位十分普通,这导致我们跳槽、转行以及感受到不满的恶性循环。如果您想转行(或开始新的职业),那么您会发现那里有各种各样的编程岗位。做好规划、计划再求职,您不仅是找一份工作,而是一份适合自己的职业。本文就将向你讲述涉及到编程技能的最常见标题的分类及其通常包含的内容。

点击阅读

《编程中寻找成就感:哪个编程岗位适合您?》

程序员该怎样和老板谈论升职加薪

程序员可能是现在为数不多的,在职场上会考核你的专业硬技能的职业之一了。在全球化的当下,这其实是一件好事。尤其是房价高的城市,即使是在同一家公司里,你也应该为自己协商一个更好的薪酬。只要有可能,你就应该为自己获得更好的薪酬去谈判,理由有两个:一个是为了工作与生活的平衡,而另一个是为了尊重。那么,什么时候谈?如何谈?希望本文能够给你解答。

点击阅读

《程序员该怎样和老板谈论升职加薪》

开发人员如何做到生活和工作的平衡

很多人都有一种生活和工作该如何平衡的困扰。时间都被大量的工作占据,没有时间兼顾到家庭和孩子,更别提要有自己的悠闲时光。特别是开发人员,普遍工作压力都很大,总有项目上线,可能没有整块的时间放松、健身。这种不平衡,也让大家承受了极大的压力,对生活造成很大的困扰。本文所给出的方法,希望能带给大家一点启示。

点击阅读

《开发人员如何做到生活和工作的平衡》

周末畅谈 | 我是如何在硅谷获得年薪30万美金Offer的?

在一周的时间里,本文作者拿到了Google、Facebook、Amazon、Apple、LinkedIn以及Yelp这些硅谷TOP级的互联网公司软件工程师的offer,本文将向大家分享这位硅谷软件工程师的面试经验。

点击阅读

《周末畅谈 | 我是如何在硅谷获得年薪30万美金Offer的?》

作为一个聚焦开发者人群的社区,我们一直努力带给大家最前沿、最实用的技术干货。在 912 这个特殊的节日里,京小云为大家献上以下4 个技术专题,凝结了京东智联云多年来在云计算、云原生、微服务、AI等各个领域的实践经验,内容全部由京东各个一线团队技术大佬倾力打造,希望帮助大家解决技术方面的困惑与挑战。

《618 Tech Talk》专题

在今年第 17 个京东 618 大促中,京东智联云作为京东 618 的技术基石,起到了极其重要的保障作用。618大促作为京东创立的的电商活动,经历过这些年的发展,已经变成整个电商领域的盛大节日。我们也在多次的大促备战工作中,积累了丰富的经验,京小云特邀多位京东技术专家从备战、存储、数据库、安全等角度复盘本次大促的保障工作,帮助大家应对大规模活动下的技战。

点击阅读

#618 Tech Talk

《微服务》专题

微服务架构随着互联网的发展变得越来越重要,许多公司都开始或者已经转到了微服务架构,但仍然有大量公司对微服务抱着观望态度,还在考虑是否需要采用微服务架构。专题从微服务扫盲、企业微服务化转型以及微服务架构实践等内容逐次展开,帮助大家熟悉掌握微服务,早日实现架构转型。

点击阅读

#微服务

《云原生在京东》专题

京东目前运营着全球最大规模的 Docker 集群、Kubernetes 集群,以及最复杂的 Vitess 集群之一,基本实现了“All in Containers”,是目前全球容器化最彻底的互联网企业之一。专题通过 7 篇文章全面起底京东云原生的技术探索与实践之路。

点击阅读

#云原生在京东

《AI 论文解读》

人工智能战胜世界围棋冠军、人工智能战胜游戏高手、人工智能医生看病会诊……不断进步的科技正推动着人工智能从一个无法实现的幻想,不断突破人类的想象,完成一个又一个挑战。我们精选出多篇京东 AI 研究院发表的 NLP、文本生成和自动文摘论文相关,帮助大家解读其背后的技术与应用实践。

点击阅读

#论文解读

京东智联云开发者(JD Cloud & AI Developer)是京东集团旗下为 AI、云计算、IoT 等相关领域开发者,提供技术分享和交流的平台。从 2019 年 2 月 14 日「京东智联云开发者」推送了第一篇社区内容开始,我们已为开发者们提供了超 500+ 篇技术干货文章,100+ 场技术沙龙课程和公开课。

_「京东智联云开发者」_秉承着“拥抱技术,与开发者携手创造未来!”的初心,面向社区开发者们提供专栏鼓励,并发掘有潜力的技术倡导者、布道师们和我们共同助力技术推广。在这里你可以免费获得基于实践案例与业务场景的技术干货、动手实操文档,实时了解技术圈的每周热点事件,还可以参与社区各种活动,与大咖进行面对面交流。

更多实用、实践级干货内容,尽在京东智联云开发者,快动动小手关注我们~

查看原文

赞 0 收藏 0 评论 0

winner王 提出了问题 · 2020-09-28

vue动态路由权限控制如何实现

系统要进行权限控制,根据不同的角色访问不同的菜单,现在前端要对路由进行权限控制,根据后台返回的菜单路径识别哪些路由访问,现在有个问题是后台返回的只是菜单路由,像编辑页面这些子路由无法获取。因为角色权限是用户自由配置的,不是固定的角色所以不能对应生成固定的菜单,所以请教下像这种动态的路由权限控制如何实现?烦请各位大神指导~

关注 1 回答 1

winner王 提出了问题 · 2020-09-25

使用vue-router的addRoutes动态添加路由不生效??

前端实现路由权限控制,根据不同的角色使用不同的路由,我现在在main.js里使用addRoutes的方法添加角色对应的路由
`new Vue({

el: '#app',

router,

store,

template: '<App/>',

components: {App},

created() {

const newrouter = [

{

path: '/',

name: 'home首页',

component: resolve => require(['@/components/home'], resolve),

redirect: '/payro/manage',

children: [

{

path: '/payro/manage',

component: resolve => require(['views/index'], resolve),

name: '首页'

},

{

path: 'test',

component: require('views/test').default,

name: '测试'

}]

}

];

console.log('刷新');

this.$router.options.routes = [];

this.$router.addRoutes(newrouter);

this.$router.options.routes = newrouter;

console.log(this.$router);

}

});`
通过console.log打印,this.$routes.options.routes里面已经是newrouter的内容了,但是页面依然能跳转之前路由,所以想问下既然已经替换更新了路由为什么还能访问之前的路由呢?

关注 2 回答 1

winner王 关注了问题 · 2020-09-11

mac查看vue--version显示错误

mac查看vue--version显示“vue: command not found”应该如何解决啊?是在已经安装过的项目查看的。

关注 5 回答 4

winner王 赞了回答 · 2020-09-11

router.beforeEach对性能有影响吗

?为什么不在 login 的过程中就获取全部的菜单权限,然后格式化菜单的显示
beforeEach 会在每次变更路由的时候调用 ... 影响不大的样子,
当然如果你请求接口当我没说 ...

关注 2 回答 1

认证与成就

  • 获得 5 次点赞
  • 获得 38 枚徽章 获得 0 枚金徽章, 获得 9 枚银徽章, 获得 29 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-10-25
个人主页被 664 人浏览