Code Monkey home page Code Monkey logo

technology-blog's Introduction

technology-blog's People

Contributors

airuikun avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

technology-blog's Issues

第 13 题:前后端分离的项目如何seo

  • 使用prerender。但是回答prerender,面试官肯定会问你,如果不用prerender,让你直接去实现,好的,请看下面的第二个答案。
  • 先去 https://www.baidu.com/robots.txt 找出常见的爬虫,然后在nginx上判断来访问页面用户的User-Agent是否是爬虫,如果是爬虫,就用nginx方向代理到我们自己用nodejs + puppeteer实现的爬虫服务器上,然后用你的爬虫服务器爬自己的前后端分离的前端项目页面,增加扒页面的接收延时,保证异步渲染的接口数据返回,最后得到了页面的数据,返还给来访问的爬虫即可。

十万条数据插入数据库,怎么去优化和处理高并发情况下的DB插入

这种题,你懂的,逼格高,亮瞎眼,大厂太爱考了。

不过装逼归装逼,有能力并且真真正正处理过这些高并发情况的FE,这题是他们一个很好的展现机会。

以前我的mentor,用nodejs实现了高并发的智能容灾,我至今记忆犹新,并且他也收获了那年的高绩效。

来玩一下?

小蝌蚪传记:前端实用技巧,通过babel精准操作js文件——暗恋

今天是暗恋她的第90天,但马上就要失恋了

因为组织架构变动,要换到一个离她很远的地方

暗恋她的90天里,一直在997

每天都在跟同类互相残杀

我厌倦了和一群老男人加班的日子

她是这段黑暗时间里,唯一的光

她曾是年会的女主持

万千男人暗恋的女神,而我只是个加班狗

她身边都是鲜花和掌声

我身边全是抠脚大汉和LSP

我配不上她,但却又被她深深的吸引

结婚7年来,我每天下班就回家

不抽烟不喝酒不近女色

腾讯男德第一人

居然对她产生了情愫

本以为我很专一,遇见她以后才明白

原来男人真的可以同时爱上好几个女人

离别之前,不知道要不要跟她表白

如果表白,她一定会说我是个好人

如果自己长的再好看点,是不是就不用自卑了

可惜没有如果,我是个完美的垃圾

就这样心烦意乱的上线了一版代码,配置文件还修改错了

直接崩出线上大bug

所有网页全部 404 not found

1000封报警邮件狂轰滥炸

boss委婉的发来1星绩效的暗示

都要哭了

配置文件这种东西,人工去修改,太容易受情绪影响而改错了

这些重复且易出错的操作,应该用工程化、自动化手段去解决

babel修改js配置文件实现原理

完整代码参考:github

像那些js配置文件,里面可能有很多的非配置代码,而且一次可能要修改好几个文件

比如我们在前端项目,要插入一个页面,需要修改router、menus等配置文件,还要手动拷贝页面模板等等

这些高重复机械化操作,人工修改非常容易出错

我们可以直接用babel来操作AST抽象语法树,通过工程化去精准修改。让babel去帮我们找到指定位置,并正确插入配置代码。我们在做工程化开发的时候,经常会用到babel去操作AST。

首先我们了解一下什么是AST

AST,抽象语法树(Abstract Syntax Tree)它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构。

我们使用babel来转化和操作AST,主要分为三个步骤:解析(parser)、转换(traverse)、生成(generator)

操作AST三大阶段

如下图,如果我们想通过babel,在配置文件里面插入一段配置代码,应该怎么实现呢

解析(parser)

第一步:读取配置文件代码,并生成AST抽象语法树

let configJsData = fs.readFileSync(configJsPath, "utf8");

然后将配置文件代码生成AST抽象语法树

const parser = require("@babel/parser");

let configJsTree = parser.parse(`${configJsData}`,{
    sourceType: "module",
    plugins: [
      "jsx",
      "flow",
    ],
  });

configJsTree就是我们的AST了

加上sourceType: "module"这个配置属性,是为了让babel支持解析export和import

转换(traverse)

转换(traverse)阶段,就是要遍历整个AST抽象语法树,找到指定的位置,然后插入对应的配置代码。

代码如下:

const traverse = require("@babel/traverse").default;

traverse(configJsTree, {
    ObjectProperty(path) {
      // 插入配置文件代码
    },
  });

我们使用@babel/traverse的traverse方法进行遍历整个AST

其中ObjectProperty的作用是在遍历AST过程中,识别出所有的Object对象,因为我们是要将配置代码插入一个Object对象,所以使用的是ObjectProperty。如果要将配置插入数组中,就使用ArrayExpression

然后我们开始进行配置代码的插入,将代码

{
  key: "testPath",
  icon: HomeOutlined,
  exact: true,
}

插入如下的位置

我们需要在traverseObjectProperty进行位置的查找和代码插入

首先我们要找到key: 'home'的位置

代码如下:

traverse(configJsTree, {
    ObjectProperty(path) {
      if ( path.node.key.name === "key" && path.node.value.value === "home" ) {
        // 这就是 key: 'home'的位置
      }
    },
  });

通过path.node.key.namepath.node.value.value找到对象属性为key并且对象值为home的Object对象

找到位置后开始插入代码

traverse(configJsTree, {
    ObjectProperty(path) {
      // 搜索并识别出配置文件里key: "home" 这个object对象位置
      if ( path.node.key.name === "key" && path.node.value.value === "home" ) {
        path.parent.properties.forEach(e=>{
          if ( e.key.name === "children" ) {
           // 找到children属性
          }
        })
      }
    },
  });

通过path.parent.properties找到对象的父级,然后遍历父级下的所有属性,找到children这个属性。这就是我们要插入的位置。

接下来我们要构造要插入的数据

{
   key: "testPath",
   icon: HomeOutlined,
   exact: true,
}

构造数据的代码如下:

const t = require("@babel/types");

const newObj = t.objectExpression([
    t.objectProperty(
      t.identifier("key"),
      t.stringLiteral("testPath")
    ),
    t.objectProperty(
      t.identifier("icon"),
      t.identifier("HomeOutlined")
    ),
    t.objectProperty(
      t.identifier("exact"),
      t.booleanLiteral(true)
    ),
  ]);

可以看到用dentifier来标识对象的属性,用stringLiteral标识字符串数据,booleanLiteral标识boolean值,这些类型都可以在@babel/types查询到。

最后一步,将构造好的数据插入:

e.value.elements.push(newObj)

完成~!

将所有 traverse 阶段代码汇总起来如下:

const traverse = require("@babel/traverse").default;
const t = require("@babel/types");

traverse(configJsTree, {
    ObjectProperty(path) {
      // 搜索并识别出配置文件里key: "home" 这个object对象位置
      if ( path.node.key.name === "key" && path.node.value.value === "home" ) {
        path.parent.properties.forEach(e=>{
          if ( e.key.name === "children" ) {
            const newObj = t.objectExpression([
              t.objectProperty(
                t.identifier("key"),
                t.stringLiteral("testPath")
              ),
              t.objectProperty(
                t.identifier("icon"),
                t.identifier("HomeOutlined")
              ),
              t.objectProperty(
                t.identifier("exact"),
                t.booleanLiteral(true)
              ),
            ]);
            e.value.elements.push(newObj)
          }
        })
      }
    },
  });

生成(generator)

这个阶段就是把AST抽象语法树反解,生成我们常规的代码

const generate = require("@babel/generator").default;

const result = generate(configJsTree, { jsescOption: { minimal: true } }, "").code;

fs.writeFileSync(resultPath, result);

通过@babel/generator将AST抽象代码语法树反解回原代码,jsescOption: { minimal: true }配置是为了解决中文为unicode乱码的问题。

至此咱们就完成了对js配置文件插入代码,最终结果如下:

以上就是通过babel操作AST,然后精准插入配置代码的全流程,完整代码参考:github

结尾

结尾

线上一切都稳定后

回头看了下女神

她正在照镜子涂口红

就静静坐着,都好喜欢好喜欢

她很爱笑、每周三都会去打篮球,早上10:30准时到公司、喜欢咖啡和奶茶

她的生活每天都是一首歌

而我只会敲代码,加班加到腿抽筋

据线人说,她已经有男朋友了,又高又帅又有钱

而我又老又丑、不仅穷还秃

听说她还很年轻,而我已经32

还有不到3年就35岁,我的时间不多了

怎么能留恋世间烟火呢

女人只会影响我敲代码的速度

哎。。。真该死

我还是默默地守护她、支持她、欣赏她吧

不打搅是屌丝最高级的告白

只是可惜的是

至始至终,我都不知道她叫什么

我们从没说过一句话

都是我一个人的独角戏

鲁迅说过:“我知道妳不是我的花,但能途经妳的盛放,我不胜荣幸”

。。。。。。

深夜空无一人的总部大楼

就这样默默坐在彼此的身边

就当我们也曾经在一起过吧

这次离别,可能就再也见不到了

不知道妳会不会想起我

也不知道我会不会爱上别的女孩

但是还是谢谢妳

惊艳了我每个加班的夜晚

goodbye my lover

————— yours 小蝌蚪

第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决

499对应的是 “client has closed connection”,客户端请求等待链接已经关闭,这很有可能是因为服务器端处理的时间过长,客户端等得“不耐烦”了。还有一种原因是两次提交post过快就会出现499。
解决方法:

  • 前端将timeout最大等待时间设置大一些
  • nginx上配置proxy_ignore_client_abort on;

如果你用nodejs去实现爬虫 在生产环境上去爬定时爬接口和一些大型网站 很容易出现499 一版面试官会试着问一下你懂不懂499 如果懂的话 就会为怎么出现的 你是做什么项目出现的 然后引导你去说爬虫 + nodejs 一条路问下去

第 8 题:手写代码,简单实现bind

Function.prototype.bind2 = function(context) {
    var _this = this;
    var argsParent = Array.prototype.slice.call(arguments, 1);
    return function() {
        var args = argsParent.concat(Array.prototype.slice.call(arguments)); //转化成数组
        _this.apply(context, args);
    };
}

第 3 题:讲解一下https对称加密和非对称加密。

对称加密:

发送方和接收方需要持有同一把密钥,发送消息和接收消息均使用该密钥。相对于非对称加密,对称加密具有更高的加解密速度,但双方都需要事先知道密钥,密钥在传输过程中可能会被窃取,因此安全性没有非对称加密高。

非对称加密:

接收方在发送消息前需要事先生成公钥和私钥,然后将公钥发送给发送方。发送放收到公钥后,将待发送数据用公钥加密,发送给接收方。接收到收到数据后,用私钥解密。
在这个过程中,公钥负责加密,私钥负责解密,数据在传输过程中即使被截获,攻击者由于没有私钥,因此也无法破解。
非对称加密算法的加解密速度低于对称加密算法,但是安全性更高。

几个名词要理清
  • RSA:非对称加密
  • AES:对称加密 生成一个随机字符串key 只有客户端和服务端有 他们两个通过这个key对数据加密和传输跟解密 这一个统称对称加密
  • CA:权威认证机构 服务器在建站的时候 去CA认证机构认证 得到对应的数字签名 相当于身份证号 客户端每次安装浏览器的时候 都会下载最新的CA列表 这个列表有对应的数字签名和服务器IP一一对应的列表 这就是为什么我们自己搭建的localhost没法发https的原因 因为没法进行CA认证
  • 数字证书:包含了数字签名跟RSA公钥
  • 数字签名:保证数字证书一定是服务器传给客户端的 相当于服务器的身份证ID
  • 对称密钥: 对数据进行加密的key
  • 非对称密钥: (k1, k2) k1加密的数据 只有k2能解开 k1位非对称公钥 k2为非对称私钥
  • 非对称公钥:RSA公钥 k1加密的数据 只有k2能解开
  • 非对称私钥:RSA私钥 k1加密的数据 只有k2能解开

小蝌蚪传记:200行代码实现前端无痕埋点——顶级渣男

灰色的天

妳的脸

说分手的语气斩钉截铁

小蝌蚪:“能不走吗”

女神:“不能”

小蝌蚪:“那个男人有什么好”

女神:“他说话好听,长得帅,还有钱”

小蝌蚪:“我没房没车没存款,但我有一颗爱妳的心”

高富帅出现:“我有房有车有存款,我也有一颗爱她的心”

小蝌蚪:“我能跑十公里去为她买宵夜”

高富帅:“我开兰博基尼去为她买宵夜”

小蝌蚪:“我一分钟能敲5000行代码”

高富帅:“你们公司的老板,是我爸”

小蝌蚪:“这。。。”

在金钱力量面前,一切言语都显得那么苍白无力

小蝌蚪跪在地上,望着高富帅远去的尾灯,消失在地平线

失恋第三十天,小蝌蚪上山拜佛

小蝌蚪:“伟大的佛,为何我感情如此失败”

佛曰:“因为你不够渣,一次只爱一个人,下次同时爱一百个试试?”

小蝌蚪若有所悟

小蝌蚪:“伟大的佛,那我如何才能成为江湖第一的渣男”

佛说:“想要成为顶级渣男,你要闯过三关”

佛:“第一关我们称之为<富婆>”

佛:“一位顶级渣男,他需要有雄厚的资金来源,才能浪迹天涯,而富婆是这资金流的关键”

第一关<富婆>

得到佛主的指点,小蝌蚪来到国际大酒店

楼顶正举行富婆八十大寿生日宴

富婆坐在轮椅上,望着舞池里的妹纸

满眼都是自己十八岁的样子

小蝌蚪出现:“女士,您好,我叫小蝌蚪”

富婆:“有何贵干”

小蝌蚪:“我想要得到妳的包养”

富婆的假牙差点从嘴里喷了出来

富婆:“我的包养,不是你想要,想要就能要”

小蝌蚪就地表演了一段舌头碎大石

富婆压制住内心的狂喜,说道:“这还不够,还差了点”

小蝌蚪:“我一分钟能敲5000行代码,手速奇快”

富婆大惊:“你就是我唯一的真爱~!”

第二关<渣女>

成功拿下富婆后,来到第二关

佛曰:“你需要撩到一个名叫 '夜魔' 的顶级渣女,通过她身上的《绝世婊技》,你才能悟出传说中的《渣男心经》”

夜魔常年混迹于夜店

斡旋在多名富二代之间

夜魔的座右铭:“肾走多了,才明白走心的可贵”

“渣男收割机”、“夜店王中王”、“叱咤级渣皇”

都是她曾用的小名

一位被她玩死的富二代

去世前曾留下遗言:

别爱上她,相信我

你只是寂寞的晚上

她想要缠绵的

小玩具

夜魔位列年度渣女榜榜首

小蝌蚪的任务,猎杀夜魔

凌晨一点,夜店

小蝌蚪:“小姐姐,您好,我叫小蝌蚪”

夜魔爱搭不理

鲁迅说过:“在金钱面前,一切渣女都是纸老虎”

小蝌蚪故意不经意间露出兰博基尼车钥匙(富婆八十大寿赠)

夜魔大喜:“哥哥,请坐!”

夜魔:“哥哥想喝点什么”

小蝌蚪:“想喝点妳的酒窝”

面对搭讪,夜魔故作脸红,假装羞涩

让你觉得她是一个清纯走心的小姐姐

夜魔:“我看哥哥挺有钱,哥哥职业是什么”

小蝌蚪:“我的职业是职业渣男”

一个低端富二代都是炫耀自己多有钱,爸爸多厉害

但一个顶级富二代,从来都是说自己是渣男

夜魔:“哥哥有喜欢的人吗”

小蝌蚪:“有,她在别人的床上”

小蝌蚪开始打感情牌,宣扬自己受过情伤

唤母爱,博同情

夜魔:“哥哥来找我什么事?”

小蝌蚪:“想借用妳的美色和媚术,帮我攻破一个男人”

夜魔:“谁?”

小蝌蚪:“我”

这是一个调情套路

面对渣女,你要表现的比她更渣

妳渣任妳渣,反正都没我渣

小蝌蚪不再周旋,直接强攻

小蝌蚪:“明人不说暗话,我想和妳结婚”

夜魔是远近闻名的渣女,男人们只想和她暧昧

面对突如其来的‘结婚’,开始手无足措

小蝌蚪抓住时机,放大招

谈笑间侧露价值180万的金表(富婆八十大寿赠)

夜魔大惊

菊花一紧,虎躯一震

无数道圣光冲击她的天灵盖

夜魔热泪盈眶

满意的点了点头

酒后三巡,意乱情迷之际

小蝌蚪带她去找了妈妈

深藏功与名

第三关:报复

最后一关

佛曰:“第三关,报仇。报复当初甩掉你的女神和高富帅”

“好的”,蝌蚪微笑,召唤出了夜魔

晚八点,高档餐厅

女神和高富帅在激情派对上亲亲我我

夜魔出现,上台拿起话筒:

“台下的小哥哥,请放下女朋友的手,你们被我包围了”

现场一片哗然

夜魔:“不是我针对谁,论美色,在座的各位,都是垃圾”

所有人被夜魔的顶配神颜惊呆

夜魔:“我会随机抽一个男人,明天和我一起起床”

鲁迅说过:“我想和妳睡觉,是耍流氓。我想和妳起床,是徐志摩”

男人们像疯狗一样欢呼和跪舔

夜魔锁定目标

径直走向高富帅:“小哥哥,你长得好像我下一任男朋友”

高富帅惊慌失措:“我我我。。。已经。。。”

夜魔强撩:“谈恋爱吗?二缺一”

高富帅捂住心脏:“糟糕,是心动的感觉”

一旁的女神暴怒:“我xx妳个xx,勾引我男人”

夜魔一副柔弱装纯的样子:“我只是把他当哥哥~”

女神继续:“我xx妳个xx”

夜魔无辜的看着高富帅:“都怪我,害你女朋友生气了”

高富帅沦陷:“不要理会那八婆”

女神:“我xx妳个xx”

夜魔:“她好凶,我好怕”

高富帅:“不要怕,我的小心心,紫薯于妳”

鲁迅说过:“渣女装纯,天下无敌”

高富帅沦为了夜魔的裆下亡魂

女神跪下,掩面痛哭

这一切,都是小蝌蚪的精心策划

佛主出现:“恭喜小蝌蚪,你成为了一位顶级渣男”

小蝌蚪:“佛心四大皆空,贫僧尘念已结”

佛曰:“我现赐予你法号——渣佛”

佛曰:“希望你今后,随老衲去夜店降妖除魔,还人间一片净土”

小蝌蚪:“哦咪陀佛”

小蝌蚪终于成为了江湖第一的渣男

手段虽然残忍

但我们不要怪渣男渣

因为每个渣男背后,都有一段刻骨铭心的虐恋

每一位渣男,都曾是折翼的天使

甩掉女神那天晚上

小蝌蚪的肩膀上靠着富婆

车里循环了一首歌:

i lost myself again

我又一次迷失了自己

but i still remember you

脑海中的妳依然那么深刻

don't come back

别回头看我,那些伤还未愈合

our love is six feet under

我们的爱已深埋殆尽

i can't help but wonder

不能自己的我很想知道

if our grave was watered by the rain

滂沱大雨后,埋葬我们爱的地方

would rose bloom

是否会有玫瑰,悄然绽放

————《six feet under》

作者:第一名的小蝌蚪

微信公众号:前端屌丝

github: https://github.com/airuikun/blog

《 蝌蚪传记:200行代码实现前端无痕埋点 》

背景

上次公开演讲结束后,很多小伙伴对无痕埋点很感兴趣

那这次就讲讲前端无痕埋点的原理与实现吧。

鲁迅说过:“一切不放源码的技术文章都是耍流氓”

所以无痕埋点源码:smart-tracker

什么是无痕埋点

简单来说,就是当引入无痕埋点的库以后

用户在浏览器里所有行为和操作都会被自动记录下来

并将信息发送到后端进行统计和分析

传统的埋点形式,都是手动埋点

在指定的元素上绑定事件

将用户行为信息发送到服务端进行统计

假设如果有一万个点需要前端狗去埋,惊喜不惊喜,意外不意外

我们为什么要做无痕埋点

提高工作效率,解放双手

屌丝的双手得到解放以后

就有更多的时间拿双手来取悦自己

嘻嘻

无痕埋点原理

原理很简单,这里只讲click的无痕埋点原理

当用户点击了页面上某一个元素

我们要把当前元素到body之间整个dom的路径记录下来,作为这个元素的唯一标识,我们称之为domPath

这个domPath不仅是这个元素唯一标识

还可以通过document.querySelector(domPath)去唯一选择和定位到这个元素

当用户点击一次这个元素,就会将埋点数据上传到服务器

服务器上这个domPath对应的统计数据加一

无痕埋点代码实现

    document.body.addEventListener('click',  (event) => {
        const eventFix = getEvent(event);
        if (!eventFix) {
            return;
        }
        this._handleEvent(eventFix);
    }, false)

首先在document的body上监听和绑定全局click事件,捕获用户所有的点击事件。

const getDomPath = (element, useClass = false) => {
    if (!(element instanceof HTMLElement)) {
        console.warn('input is not a HTML element!');
        return '';
    }
    let domPath = [];
    let elem = element;
    while (elem) {
        let domDesc = getDomDesc(elem, useClass);
        if (!domDesc) {
            break;
        }
        domPath.unshift(domDesc);
        if (querySelector(domPath.join('>')) === element || domDesc.indexOf('body') >= 0) {
            break;
        }
        domPath.shift();
        const children = elem.parentNode.children;
        if (children.length > 1) {
            for (let i = 0; i < children.length; i++) {
                if (children[i] === elem) {
                    domDesc += `:nth-child(${i + 1})`;
                    break;
                }
            }
        }
        domPath.unshift(domDesc);
        if (querySelector(domPath.join('>')) === element) {
            break;
        }
        elem = elem.parentNode;
    }
    return domPath.join('>');
}

这段代码是关键,获取元素唯一标识domPath

getDomPath函数传入的是用户点击事件的target对象: getDomPath(event.target)。

主要思路是找到当前元素event.target

然后不断的去循环找它的父节点parentNode

将父节点的tagName当做domPath路径上的节点

如果当前元素有id,那就取消所有路径的循环,直接讲id赋值给domPath

    const children = elem.parentNode.children;
    if (children.length > 1) {
        for (let i = 0; i < children.length; i++) {
            if (children[i] === elem) {
                domDesc += `:nth-child(${i + 1})`;
                break;
            }
        }
    }
    domPath.unshift(domDesc);

getDomPath函数中的这段代码

意思是在同一级上出现了多个相同tagName元素

那我们要定位到这个event.target这个元素在这一级里的第几个

假设这个div是同一级的第三个,那返回的就是div:nth-child(3)

这样就可以在document.querySelector(domPath)里唯一定位到这个元素

    _handleEvent(event) {
        const domPath = getDomPath(event.target);
        const rect = getBoundingClientRect(event.target);
        if (rect.width == 0 || rect.height == 0) {
            return;
        }
        let t = document.documentElement || document.body.parentNode;
        const scrollX = (t && typeof t.scrollLeft == 'number' ? t : document.body).scrollLeft;
        const scrollY = (t && typeof t.scrollTop == 'number' ? t : document.body).scrollTop;
        const pageX = event.pageX || event.clientX + scrollX;
        const pageY = event.pageY || event.clientY + scrollY;
        const data = {
            domPath: encodeURIComponent(domPath),
            trackingType: event.type,
            offsetX: ((pageX - rect.left - scrollX) / rect.width).toFixed(6),
            offsetY: ((pageY - rect.top - scrollY) / rect.height).toFixed(6),
        };
        this.send(data);
    }

这段代码就是得到用户点击某个元素的相对位置的横向位置和竖向位置比例

得到这个位置的值,就可以反向从埋点数据中得到用户点击元素的具体位置

因为是个比例值,所以在反向推导中还能自适应页面大小的改变

    send(data = {}) {
        const image = new Image(1, 1);
        image.onload = function () {
            image = null;
        };
        image.src = `/?${stringify(data)}`;
    }

得到了用户点击的位置信息和唯一标识domPath

就可以将数据发送到服务端进行统计了

用image的src,将数据进行传输

用image的src有个好处就是轻量,并且还支持跨域

打点基本上都用的这个方法进行发送数据

结尾

两个多月没写文章了,因为在忙着晋升

这次晋升最大的感悟就是,如果你只一心专注业务,是很难晋升成功的

需要在日常工作中做一些技术型需求

而无痕埋点,是一个不错的选择

但是这篇文章仅仅只是无痕埋点的一个简单实现

对整个无痕埋点体系来说,这些只是冰山一角

真正的无痕埋点,还需要做统计、分析、差量预测、标记策略、智能降噪、可视化无痕、无痕分桶、反向推导热力图、大数据中台等等
涉及到前端、后端、运维、DBA和算法

一起干下来,那你就是江湖顶级的前端渣男了

以上就是文章的全部了,谢谢你能全部看完

最后,祝你过上幸福快乐的生活

小蝌蚪传记:nodejs线上模块热部署原理与实现——富婆的爱

晚十二点,床上

富婆:这是给你的500元,辛苦了

小蝌蚪:不辛苦,主人

富婆:我老公要回来了,你走吧

小蝌蚪:是,主人

说完小蝌蚪从三楼别墅跳了下去

。。。

小蝌蚪是一名程序员,也是一个技师

白天敲代码,晚上捏脚

由于常年敲代码,所以指法高超

时常把客户按到上天日龙

。。。

富婆年轻、有钱,但是缺爱

因为嫁给了80岁老公

小蝌蚪听话活好不粘人

深得富婆的欢心

。。。

又一次见面

富婆:好无聊,赶紧讨好我

小蝌蚪:主人,我用手指在您腿上敲一段“Dijkstra最短路径算法”吧

富婆:这招你用过了

小蝌蚪:那我用胸毛在您背上默写《javascript高级程序设计》

富婆:不想

小蝌蚪:那我用舌头给您表演一段口技

富婆:舌头?口技?

小蝌蚪:不要多想,是正规表演

于是小蝌蚪表演了一段舌头碎大石

富婆大喜,掌声经久不衰

。。。

表演完毕,富婆面色泛红

富婆:我喜欢你

小蝌蚪:我也喜欢我自己

富婆:你爱我吗

小蝌蚪:我不能对客户动心

富婆:我要如何才能得到你的心

小蝌蚪:杀了我,把心掏出来,嘻嘻

富婆:嘻嘻

。。。

富婆:对了,下周来参加我的闺蜜派对

小蝌蚪:不带你老公去吗

富婆:他80岁了,带去丢脸

小蝌蚪:那我可要加钱噢

富婆:多少

小蝌蚪:200块钱两小时,物美价廉,服务一流

富婆:deal

。。。

闺蜜派对

晚八点,高级酒店,各种炫富

闺蜜A:我买了个包,才10万,好便宜哦~

闺蜜B:我包了三个小奶狗,让他们打了一晚上斗地主

闺蜜C:我老公养了好多小动物,比如宝马、路虎、捷豹

富婆默默上前:我在北京二环买了套四合院

其他闺蜜瞬间不说话了

。。。

小蝌蚪意识到

这不是一个普通的饭局

而是一场闺蜜之间的装逼盛宴

小蝌蚪:你需要我做什么

富婆:我需要你打败她们的男宠

。。。

酒后三巡

到了炫耀男宠的阶段

每个闺蜜都带来了自己包养的男宠

有肌肉男、高级鸭、男模等等

肌肉男上台就展示八块腹肌和沙包一样大的裤裆

男模直接亮出大长腿和帅到掉渣的脸

高级鸭把舌头伸出来,在台上发出“略略略略略...”的声音

男宠们各有所长

赢得了场下闺蜜们的掌声

。。。

轮到小蝌蚪上台

一身屎绿色格子衬衫

呆若木鸡

主持人问:辣鸡,你要展示什么特长

小蝌蚪:舌头碎大石

主持人:除了这个还有吗

小蝌蚪:胸毛碎大石

主持人暴怒:我怀疑你是来砸场的

小蝌蚪:是的,我就是来砸场的

主持人叫来20个保安

十秒钟后保安全部倒地抽搐

身上被小蝌蚪的胸毛刻满了神秘javascript代码

。。。

主持人:爸爸对不起,刚才是我冒犯了

主持人:爸爸,麻烦跟观众介绍一下自己

小蝌蚪从书包里掏出一千万美金(富婆送)

点燃后从98楼外抛出

燃烧的钱漫天飞舞

小蝌蚪介绍道:“我是颜色不一样的渣男”

台下所有女人狂轰乱叫

因过度兴奋而七窍流血

男宠们看到自己的主子疯狂跪舔小蝌蚪

深感自己被绿了

主持人:爸爸您还有什么温柔点的特长展示给大家吗

小蝌蚪:我要唱一首孙艳枝的《绿光》,送给在座的各位男辣鸡

《绿光》

爱就像一道绿光

洒在你头上

都怪嫂子太漂亮

。。。

不要怪爸爸无情

是你们太辣鸡

绿光呀~绿光

照亮你我他

男宠们:小蝌蚪你太过分了

小蝌蚪:各凭本事做人渣

男宠们:你到底想干什么

小蝌蚪:我想在你们坟头上蹦迪

男宠们心态爆炸,疯狂抓头

从98楼跳了下去

小蝌蚪赢得了比赛

成为了第一名男宠

。。。

酒店阳台,微风拂过

富婆:今晚谢谢你

小蝌蚪:为了钱,应该的

富婆:我喜欢你

小蝌蚪:我也喜欢我自己

富婆:你爱我吗

小蝌蚪:我不能对客户动心

富婆:我要如何才能得到你的心

小蝌蚪:杀了我,把心掏出来,嘻嘻

富婆:嘻嘻

。。。

黑帮大佬

屋内,捏脚

富婆:我要死了

小蝌蚪:怎么了

富婆:我老公在外面有女人了

富婆:他是个黑帮大佬,要杀我,跟小三在一起

小蝌蚪轻抚富婆的脸:傻瓜,在这之前,我会杀了他

小蝌蚪和富婆拥吻在了一起

两嘴之间拉出一条亮丝

吻毕,小蝌蚪消失在夜空中

。。。

刺杀大佬

深夜十二点

黑帮大佬酒店房间门外

塞入了一张小卡片

上面写着“包小姐”

和一张小蝌蚪穿泳裤的照片

。。。

黑帮大佬有着诡异的癖好

喜欢胸毛浓密的小奶狗

小蝌蚪满足了他对一切小奶狗的幻想

。。。

房间门果然打开了

小蝌蚪用一千根胸毛扎入了大佬体内

大佬:你。。。你是谁

小蝌蚪:我是你爸爸

大佬:为什么要杀我

小蝌蚪:因为我是你爸爸

大佬:能不能不提爸爸两个字

小蝌蚪:我和你老婆有一腿

黑帮大佬,猝,享年80

。。。

由于刺杀了黑帮大佬

当晚回去的路上

小蝌蚪被1000名杀手全城追杀

用尽了最后一根胸毛

小蝌蚪寡不敌众

倒在了血泊里,奄奄一息

将死之际,富婆从杀手中出现

她微笑的捅了小蝌蚪一刀

把心掏了出来

小蝌蚪惊恐的望着她

富婆:我终于得到了你的心,嘻嘻

(完)

作者:第一名的小蝌蚪

微信公众号:前端屌丝

github: https://github.com/airuikun/blog

《基于nodejs的云端热部署原理与实现》

背景

大家都知道,nodejs启的后端服务,如果有代码变动,要重启进程,代码才能生效。

nodejs的进程在重启的时候,爸爸们去访问服务,就会出现短暂的 502 bad gateway,爸爸们会不高兴

如果你的服务器加上了watch机制

当服务器上的代码频繁发生变动,或者短时间内发生高频变动,那就会一直 502 bad gateway

所谓“重启一时爽,一直重启一直爽”

近段时间在做云编译相关需求的时候,就出现了短时间内线上服务代码高频变动,代码功能模块高频更新,在不能重启服务的情况下,让更新的代码生效的场景。

这就涉及到一个热部署的概念,在不重启服务的情况下,让新部署的代码生效。

接下来我来给爸爸们讲解热部署的原理和实现方案

代码没法实时生效的原因

当我们通过require('xx/xx.js')去加载一个功能模块的时候,node会把require('xx/xx.js')得到的结果缓存在require.cache('xx/xx.js')

当我们多次调用require('xx/xx.js'),node就不再重新加载,而是直接从require.cache('xx/xx.js')读取缓存

所以当爸爸在服务器上修改xx/xx.js这个路径下的文件时,node只会去读取缓存,不会去加载爸爸的最新代码

源码地址和使用

为了实现这个热部署机制,在网上到处查资料,到处踩坑才弄好

以下代码是提炼出来、完整可运行的热部署基础原理代码,大家可以基于这个代码去自行拓展:smart-node-reload

注意最新版本12版本的node运行会报错,官方对require.cache做了调整,已经上报问题给官方,建议使用nodejs版本:v10.5.0

git clone下来以后,无需安装,直接运行

    npm start

这时候就开启了热部署变动监听

如何看到效果呢

爸爸请看/hots/hot.js文件

    const hot = 1
    module.exports = hot

将第一行代码改为const hot = 111

    const hot = 111
    module.exports = hot

这时候就能看到终端里监听到代码变动,然后动态加载你的最新代码并得到执行结果,输出为:

    热部署文件: hot.js ,执行结果: { 'hot.js': 111 }

热部署服务监听到代码变动,并重新加载了代码,爸爸们就可以实时拿到最新代码的执行结果了,整个过程都在线上环境运行,node进程也没有重启

源码解析

loadHandlers主函数

const handlerMap = {};// 缓存
const hotsPath = path.join(__dirname, "hots");

// 加载文件代码 并 监听指定文件夹目录文件内容变动
const loadHandlers = async () => {
  // 遍历出指定文件夹下的所有文件
  const files = await new Promise((resolve, reject) => {
    fs.readdir(hotsPath, (err, files) => {
      if (err) {
        reject(err);
      } else {
        resolve(files);
      }
    });
  });
  // 初始化加载所有文件 把每个文件结果缓存到handlerMap变量当中
  for (let f in files) {
    handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));
  }

  // 监听指定文件夹的文件内容变动
  await watchHandlers();
};

loadHandlers是整个热部署服务的主函数,我们指定了服务器根目录下的hots文件夹是用来监听变动和热部署的文件夹

fs.readdir扫描hots文件夹下的所有文件,通过loadHandler方法去加载和运行每一个扫描到的文件,将结果缓存到handlerMap

然后用watchHandlers方法开启文件变动监听

watchHandlers监听文件变动

// 监视指定文件夹下的文件变动
const watchHandlers = async () => {
  // 这里建议用chokidar的npm包代替文件夹监听
  fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {
    // 获取到每个文件的绝对路径 
    // 包一层require.resolve的原因,拼接好路径以后,它会主动去帮你判断这个路径下的文件是否存在
    const targetFile = require.resolve(path.join(hotsPath, filename));
    // 当你适应require加载一个模块后,模块的数据就会缓存到require.cache中,下次再加载相同模块,就会直接走require.cache
    // 所以我们热加载部署,首要做的就是清除require.cache中对应文件的缓存
    const cacheModule = require.cache[targetFile];
    // 去除掉在require.cache缓存中parent对当前模块的引用,否则会引起内存泄露,具体解释可以看下面的文章
	//《记录一次由一行代码引发的“血案”》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
	//《一行 delete require.cache 引发的内存泄漏血案》https://zhuanlan.zhihu.com/p/34702356
    if (cacheModule.parent) {    
        cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
    }
    // 清除指定路径对应模块的require.cache缓存
    require.cache[targetFile] = null;

    // 重新加载发生变动后的模块文件,实现热加载部署效果,并将重新加载后的结果,更新到handlerMap变量当中
    const code = await loadHandler(targetFile)
    handlerMap[filename] = code;
    console.log("热部署文件:", filename, ",执行结果:", handlerMap);
  });
};

watchHandlers函数是用来监听指定文件夹下的文件变动、清理缓存更新缓存用的。

fs.watch原生函数监听hots文件夹下文件变动,当文件发生变动,就算出文件的绝对路径targetFile

require.cache[targetFile]就是requiretargetFile原文件的缓存,清除缓存用require.cache[targetFile] = null;

坑爹的地方来了,仅仅只是将缓存置为null,会发生内存泄露,我们还需要清除缓存父级的引用require.cache[targetFile].parent,就是下面这段代码

    if (cacheModule.parent) {    
        cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
    }

loadHandler加载文件

// 加载指定文件的代码
const loadHandler = filename => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if (err) {
        resolve(null);
      } else {
        try {
          // 使用vm模块的Script方法来预编译发生变化后的文件代码,检查语法错误,提前发现是否存在语法错误等报错
          new vm.Script(data);
        } catch (e) {
          // 语法错误,编译失败
          reject(e);
          return;
        }
        // 编译通过后,重新require加载最新的代码
        resolve(require(filename));
      }
    });
  });
};

loadHandler函数的作用是加载指定文件,并校验新文件代码语法等。

通过fs.readFile读取文件内容

用node原生vm模块vm.Script方法去预编译发生变化后的文件代码,检查语法错误,提前发现是否存在语法错误等报错

检验通过后,通过resolve(require(filename))方法重新将文件require加载,并自动加入到require.cache缓存中

结尾:

以上就是热部署的所有内容了,代码地址是:smart-node-reload

小蝌蚪传记:端口转发——夜店传说

背景

2019年6月9号,星期天,晴,33度
今天的bug格外的难解
对面商店的小姐姐,依然是我不敢奢求的梦

进入商店
小蝌蚪:您好,我想买两包妹子
她:嗯?
小蝌蚪:错了,买两包烟
她:一共20块。
小蝌蚪:太贵了,23块行不行?
她:行,还有什么需要的吗?
小蝌蚪:有需要,我有一场恋爱需要和妳谈谈
话音刚落就被姑娘的拳击男友爆揍了一顿

小蝌蚪拖着受伤的身躯蹲在路边
微风吹过我的脸庞,我很迷茫
多希望有个富婆能看穿我的逞强
让我卸下所有伪装,走进她的心房
话音刚落就被包工头在电话里骂了一顿
因为线上出现了bug

端口转发

由于电脑不在身边,小蝌蚪直接进了间网吧,这个bug的问题出在公司内网数据库中,只要调整数据数据就能解。

可是,没有vpn,怎么连接到内网数据库呢?

小蝌蚪发出了诡异的笑声,为了应付紧急情况,小蝌蚪早就做好了准备,

通过隧道技术,在内网打穿了一个洞,击穿了内网。以便不时之需。

假设,小蝌蚪个人服务器叫server1,公司的对外网站服务器叫server2,公司内网的数据库叫MySQL.

公司的对外网站服务器server2能通过ssh连接到小蝌蚪的server1,

但server1不能通过ssh连接server2。

并且只有公司的对外网站服务器server2能连接数据库。

他们的关系图如下:

image

server1的端口是6666,数据库MySQL端口是3306,那么只需要在server2服务器上执行:

ssh -R 6666:MySQL:3306 server1

即可,这样小蝌蚪直接去连接server1的6666端口,就相当于连接了公司内网的数据库,完美的解决掉了bug。

这个就是利用了ssh的隧道去实现的内网击穿。

我们在平时开发的时候,还有可能会遇到一种场景

在内网环境要连接跳板机,然后再通过跳板机才能连接到数据库。

这样要想在本地开发的时候操作数据库,非常的麻烦。

那么我们如何解决这个问题呢?

假设小蝌蚪在公司的个人电脑叫server1,需要用跳板机才能连接的数据叫MySQL,关系图如下:

image

server1的端口是6666,数据库MySQL端口是3306,那么只需要在server1上执行:

ssh -L server1:6666:MySQL:3306 root@跳板机

即可,这样小蝌蚪直接连接本地电脑的6666,就相当于连接上了线上数据库MySQL,非常方便和简单。

以上的两个技能分别叫ssh的本地端口转发和远程端口转发。

结尾

解决完这个bug,已经是晚上十二点
楼下的夜店响起了战歌
站在物欲横流的街
小蝌蚪对其中一家夜店着了迷
因为门口站着一排黑丝大长腿
黑丝对程序员来说是一种圣物,同时也是个迷
黑丝套在头上,你就是劫匪
黑丝套在腿上,妳就是神明

就在这时,天边突然响起了师傅的佛音:

小蝌蚪,美色是你职业道路上的绊脚石
还在意女人,你就成为不了江湖第一的程序员


小蝌蚪狠狠扇了自己一巴掌
一个真正厉害的程序员应该是心无旁骛 ,只有代码
心中有码,到哪里都是比基尼

小蝌蚪的师傅是一个高级前端工程师
离至尊级程序员就差一步
在一次修炼中,为了突破到至尊级
孤身一人进入夜店
后来就再也没有回来

消失前一小时
他在微信群留下了两个字:“黑丝”

那天晚上师傅到底经历了什么
小蝌蚪决定进店探个究竟

深夜中的男男女女,在舞池**群魔乱舞
小蝌蚪身上穿着公司十周年发的屎黄色战服
在舞池里蹦起来就好像一个小儿麻痹

平时习惯了昏暗的办公室,见不得光
被夜店里的聚光灯一照,差点亮瞎了狗眼

随着音乐摇到一半
突然看到后排有个面黄肌瘦的男人
小蝌蚪:师傅!是你吗
师傅:小。。。小蝌蚪?你为什么会在这里
小蝌蚪:师傅~!终于找到你了。快跟我回去
师傅:走不了,我的灵魂中毒了。
小蝌蚪:中毒?
师傅:我要找到一个女孩,她是我唯一的解药
小蝌蚪:怎样才能找到她
师傅:她的代号叫“黑丝”,传说中的夜店女皇,她藏的很深,几乎没有人能找到她。
小蝌蚪:她是个什么样的女人
师傅:黑丝是一个极度危险的女人,所有被她撩过的男人,都会瞬间沉沦,然后日渐消瘦,思念至死。
师傅:三年前,她亲吻了我一下,然后、然后、然后。。。。。。黑丝、黑丝、黑丝、黑丝、黑丝、黑丝、黑丝、黑丝、黑丝、黑丝。。。
话还没说完,师傅就像毒瘾发作
疯狂乱抓自己头发
口里不断重复着“黑丝”两个字
。。。
。。。
小蝌蚪发誓一定要找到黑丝
于是闭上眼睛,气运丹田,
动用“心眼”去感受身边每个人的蠕动
“心眼”是程序员的一个高阶技巧
它能让程序员在短时间内找到万行代码中的bug所在

过了许久,最终锁定人群九点钟方向
那里坐着一个低着头的妹纸
女性在夜店都是尽可能穿得妖艳
可她只穿了一件简单白色短袖
与周围环境形成强烈反差
“心眼”感受到了她倾世容颜下一颗躁动的野心

显然,她只是看起来纯洁
一个真正的高手,她的外表看起来永远都不像一个高手
高富帅就偏偏喜欢这样清纯简单的女人
因为高富帅已经厌倦了庸脂俗粉和妖艳贱货

终于找到了黑丝,可是要怎么撩她呢
小蝌蚪急中生智,在屎黄色的公司战服上,
用圆珠笔画了一个“supreme”,逼格瞬间爆炸

进入蹦迪的舞池后,不会摇摆怎么办?
师傅曾说过:“如果你在夜店里不会摇摆,那就用脸在天上画一个<粪>字”

头在摇摆的时候,手应该怎么晃动呢?
师傅又曾经说过:“
如果在夜店里你的手不知道怎么晃,
就想象天花板是一个巨大的键盘,将手举过头顶
对着天空一顿盲敲
那样你就是夜店里最厉害的仔

为了让自己看起来更像一个夜店高手
小蝌蚪一面摇晃着脑袋
一面将程序员的加班战歌《no-sex(无性繁殖)》大声唱了出来
歌词大意如下:

药药 切克闹
哟哟 嘿V够
i don't need sex, the code 发可 me everyday
大声跟我一起念
i don't need sex, the code 发可 me everyday
发可!发可!
me!me!
eve!ry!Day!

小蝌蚪一边rap一边嘲讽旁边的年轻人,不懂得什么才是真正的music
才用不到半小时,小蝌蚪已经是整个夜店里最狂的仔

小蝌蚪的*操作,引起了黑丝的注意
这一切都在意料之中,就好像代码的运行流程,每一行都胸有成竹

黑丝主动坐到了小蝌蚪身边
桌上早已为她点了杯名叫“烈焰红唇”的鸡尾酒
杯壁上用口红写了首情诗


52度的白酒
37度的妳

黑丝品完酒,问到:37度是什么意思
小蝌蚪:是人类一见钟情时的体温
黑丝:呵呵,男人,这招不错

黑丝习惯了被屌丝舔狗围绕
从来没有男人敢这么对她说话
黑丝的兴趣被提了起来

黑丝:你是谁,我怎么没见过你
小蝌蚪:我叫小蝌蚪,是一个杀手

说自己是个杀手,明显是在撒谎
但这种显著的谎言,恰恰是一个调情手段

黑丝:噢?杀手来这里做什么
小蝌蚪:寻找我的猎物,然后玩弄女孩子的心
黑丝:呵呵,男人,那你的猎物找到了吗
小蝌蚪:找到了,她刚刚对我说了句“呵呵,男人”

这句话重击了黑丝的内心,黑丝开始动摇
她从来没见过这么*的男人

黑丝:你到底想要什么
小蝌蚪:我想要一场艳遇,一场精心设计的邂逅
黑丝:那需要我做什么吗
小蝌蚪:需要,我有一场恋爱,需要和妳谈谈

小蝌蚪不再周旋,开门见山,直奔主题
满足黑丝霸道的占有欲与虚荣心

黑丝:给你十分钟,给我一个不拒绝你的理由
小蝌蚪:好的,我去趟厕所,马上回来

时机已到,是时候给予黑丝致命一击
小蝌蚪赶紧跑到厕所,拿出电脑
将支付宝余额数目p得跟自己身份证号码一样长

image

从厕所回来后,黑丝看到余额
无比确信眼前这个男人就是她的真命天子
黑丝饥渴难耐,浴火焚身:“男人,我接受你了”
小蝌蚪突然狂笑:“这么容易就拿下妳了?辣鸡”
黑丝大惊失色
小蝌蚪继续嘲讽:“都是出来玩玩的,走心就是妳不对咯,辣鸡”
黑丝心里防线瞬间被击穿,溃不成军
小蝌蚪冷漠道:“玩弄感情者,恒被玩之,懂?再见,辣鸡”
黑丝跪下来抱着小蝌蚪狂哭,求小蝌蚪不要走
哭声惊天地、泣鬼神
黑丝从未受到如此大的打击
从那以后,她就好像灵魂被操控
口里不断重复着“小蝌蚪、小蝌蚪、小蝌蚪、小蝌蚪、小蝌蚪、小蝌蚪、。。。”
。。。
。。。
小蝌蚪很聪明,巧妙利用金钱的力量,智取女神,最终解救出师傅
将程序员处乱不惊、勇于创新的优良品质体现的淋漓尽致
此次一战,成为了程序员的一段佳话,小蝌蚪也成为了夜店传说。
。。。
那天晚上,海云天共一色,小蝌蚪没有回家,小蝌蚪去了哪?传说去找了妈妈
。。。
。。。
(完结)

第 20 题:git时光机问题

现在大厂,已经全部都是用git了,基本没人使用svn了

很多面试候选人对git只会commit、pull、push

但是有没有使用过reflog、cherry-pick等等,这些都很能体现出来你对代码管理的灵活程度和代码质量管理。

针对git时光机经典问题,我专门写了一个文章,轻松搞笑通俗易懂,大家可以看一下,放松放松,同时也能学到对git的时光机操作《git时光机--女神的侧颜》

第 2 题:讲解一下HTTPS的工作原理

HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,TLS/SSL中使用了非对称加密,对称加密以及HASH算法。握手过程的简单描述如下:

  • 浏览器将自己支持的一套加密规则发送给网站。

  • 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。

  • 获得网站证书之后浏览器要做以下工作:

    • a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。

    • 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。

    • 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。

  • 4.网站接收浏览器发来的数据之后要做以下的操作:

    • a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。

    • b) 使用密码加密一段握手消息,发送给浏览器。

  • 5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

现在有多个spa的项目,有angular的,有vue的和react的,如何将他们合并成一个大统一的spa项目

很多公司,都有很多的老项目,并且都是不同框架,很分散,很多上级的上级,很喜欢提这种多项目统一的需求。

这时候你就会面临不同框架的项目重构。

如果你遇到这些问题,打算重写项目,那是非常费力不讨好的。这些问题,其实是能框架层面上去解决的。

我提供个思路,比如在angular项目里融合react项目,可以从ReactDOM.render结合angular的路由入手。

再提供个思路,例如简单的vue和react项目,可以用ast等手法,将代码互转。但这个我曾经实践过,会面临很多的bad case。

剩下的一些思路,欢迎大家挑战。

第 16 题:(开放题)2万小球问题:在浏览器端,用js存储2万个小球的信息,包含小球的大小,位置,颜色等,如何做到对这2万条小球信息进行最优检索和存储

能否进大厂,就看你对开放题能答有多深和多广。

这题目考察你如何在浏览器端中进行大数据的存储优化和检索优化。

如果你仅仅只是答用数组对象存储了2万个小球信息,然后用for循环去遍历进行索引,那是远远不够的。

这题要往深一点走,用特殊的数据结构和算法进行存储和索引。

然后进行存储和速度的一个权衡和对比,最终给出你认为的最优解。

我提供几个思路:

  • 用ArrayBuffer实现极致存储
  • 哈夫曼编码 + 字典查询树实现更优索引
  • 用bit-map实现大数据筛查
  • 用hash索引实现简单快捷的检索
  • 用IndexedDB实现动态存储扩充浏览器端虚拟容量
  • 用iframe的漏洞实现浏览器端localStorage无限存储,实现2千万小球信息存储

最近找到几个思路

  • 用webassembly去加速计算
  • 用service worker进行多进程非阻塞计算
  • 最近还看了一篇文章 用skia 2d渲染引擎 去加速2w小球canvas渲染

这种开放题答案不唯一,也不会要你现场手敲代码去实现,但是思路一定要行得通,并且是能打动面试官的思路

小蝌蚪传记:前端菜鸟让接口提速60%的优化与原理

背景

好久没写文章了,沉寂了大半年

持续性萎靡不振,间歇性癫痫发作

天天来大姨爹,在迷茫、焦虑中度过每一天

不得不承认,其实自己就是个废物

作为一名低级前端工程师

最近处理了一个十几年的祖传老接口

它继承了一切至尊级复杂度逻辑

传说中调用一次就能让cpu负载飙升90%的日天服务

专治各种不服与老年痴呆

我们欣赏一下这个接口的耗时

平均调用时间在3s以上

导致页面出现严重的转菊花

经过各种深度剖析与专业人士答疑

最后得出结论是:放弃医疗

鲁迅在《狂人日记》里曾说过:“能打败我的,只有女人和酒精,而不是bug

每当身处黑暗之时

这句话总能让我看到光

所以这次要硬起来

我决定做一个node代理层

用下面三个方法进行优化:

  • 按需加载 -> graphQL

  • 数据缓存 -> redis

  • 轮询更新 -> schedule

代码地址:github

按需加载 -> graphQL

天秀老接口存在一个问题,我们每次请求1000条数据,返回的数组中,每一条数据都有上百个字段,其实我们前端只用到其中的10个字段而已。

如何从一百多个字段中,抽取任意n个字段,这就用到graphQL。

graphQL按需加载数据只需要三步:

  • 定义数据池 root
  • 描述数据池中数据结构 schema
  • 自定义查询数据 query

定义数据池

我们针对屌丝追求女神的场景,定义一个数据池,如下:

// 数据池
var root = {
    girls: [{
        id: 1,
        name: '女神一',
        iphone: 12345678910,
        weixin: 'xixixixi',
        height: 175,
        school: '剑桥大学',
        wheel: [{ name: '备胎1号', money: '24万元' }, { name: '备胎2号', money: '26万元' }]
    },
    {
        id: 2,
        name: '女神二',
        iphone: 12345678910,
        weixin: 'hahahahah',
        height: 168,
        school: '哈佛大学',
        wheel: [{ name: '备胎3号', money: '80万元' }, { name: '备胎4号', money: '200万元' }]
    }]
}

里面有两个女神的所有信息,包括女神的名字、手机、微信、身高、学校、备胎集合等信息。

接下来我们就要对这些数据结构进行描述。

描述数据池中数据结构

const { buildSchema } = require('graphql');

// 描述数据结构 schema
var schema = buildSchema(`
    type Wheel {
        name: String,
        money: String
    }
    type Info {
        id: Int
        name: String
        iphone: Int
        weixin: String
        height: Int
        school: String
        wheel: [Wheel]
    }
    type Query {
        girls: [Info]
    }
`);

上面这段代码就是女神信息的schema。

首先我们用type Query定义了一个对女神信息的查询,里面包含了很多女孩girls的信息Info,这些信息是一堆数组,所以是[Info]

我们在type Info中描述了一个女孩的所有信息的维度,包括了名字(name)、手机(iphone)、微信(weixin)、身高(height)、学校(school)、备胎集合(wheel)

定义查询规则

得到女神的信息描述(schema)后,就可以自定义获取女神的各种信息组合了。

比如我想和女神认识,只需要拿到她的名字(name)和微信号(weixin)。查询规则代码如下:

const { graphql } = require('graphql');

// 定义查询内容
const query = `
    { 
        girls {
            name
            weixin
        }
    }
`;

// 查询数据
const result = await graphql(schema, query, root)

筛选结果如下:

又比如我想进一步和女神发展,我需要拿到她备胎信息,查询一下她备胎们(wheel)的家产(money)分别是多少,分析一下自己能不能获取优先择偶权。查询规则代码如下:

const { graphql } = require('graphql');

// 定义查询内容
const query = `
    { 
        girls {
            name
            wheel {
            	money
            }
        }
    }
`;

// 查询数据
const result = await graphql(schema, query, root)

筛选结果如下:

我们通过女神的例子,展现了如何通过graphQL按需加载数据。

映射到我们业务具体场景中,天秀接口返回的每条数据都包含100个字段,我们配置schema,获取其中的10个字段,这样就避免了剩下90个不必要字段的传输。

graphQL还有另一个好处就是可以灵活配置,这个接口需要10个字段,另一个接口要5个字段,第n个接口需要另外x个字段

按照传统的做法我们要做出n个接口才能满足,现在只需要一个接口配置不同schema就能满足所有情况了。

感悟

在生活中,咱们舔狗真的很缺少graphQL按需加载的思维

渣男渣女,各取所需

你的真情在名媛面前不值一提

我们要学会投其所好

上来就亮车钥匙,没有车就秀才艺

今晚我有一条祖传的染色体想与您分享一下

行就行,不行就换下一个

直奔主题,简单粗暴

缓存 -> redis

第二个优化手段,使用redis缓存

天秀老接口内部调用了另外三个老接口,而且是串行调用,极其耗时耗资源,秀到你头皮发麻

我们用redis来缓存天秀接口的聚合数据,下次再调用天秀接口,直接从缓存中获取数据即可,避免高耗时的复杂调用,简化后代码如下:

const redis = require("redis");
const { promisify } = require("util");

// 链接redis服务
const client = redis.createClient(6379, '127.0.0.1');

// promise化redis方法,以便用async/await
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);

async function list() {
	// 先获取缓存中数据,没有缓存就去拉取天秀接口
	let result = await getAsync("缓存");
    if (!result) {
    	  // 拉接口
    	  const data = await 天秀接口();
          result = data;
          // 设置缓存数据
          await setAsync("缓存", data)
    }
   	return result;
}

list(); 

先通过getAsync来读取redis缓存中的数据,如果有数据,直接返回,绕过接口调用,如果没有数据,就会调用天秀接口,然后setAsync更新到缓存中,以便下次调用。因为redis存储的是字符串,所以在设置缓存的时候,需要加上JSON.stringify(data),为了便于大家理解,我就不加了,会把具体细节代码放在github中。

将数据放在redis缓存里有几个好处

可以实现多接口复用、多机共享缓存

这就是传说中的云备胎

追求一个女神的成功率是1%

同时追求100个女神,那你获取到一个女神的概率就是100%

鲁迅《狂人日记》里曾说过:“舔一个是舔狗,舔一百个你就是战狼

你是想当舔狗还是当战狼?

来吧,缓存用起来,redis用起来

轮询更新 -> schedule

最后一个优化手段:轮询更新 -> schedule

女神的备胎用久了,会定时换一批备胎,让新鲜血液进来,发现新的快乐

缓存也一样,需要定时更新,保持与数据源的一致性,代码如下:

const schedule = require('node-schedule');

// 每个小时更新一次缓存
schedule.scheduleJob('* * 0 * * *', async () => {
    const data = await 天秀接口();
    // 设置redis缓存数据
    await setAsync("缓存", data)
});

我们用node-schedule这个库来轮询更新缓存,* * 0 * * *这个的意思就是设置每个小时的第0分钟就开始执行缓存更新逻辑,将获取到的数据更新到缓存中,这样其他接口和机器在调用缓存的时候,就能获取到最新数据,这就是共享缓存和轮询更新的好处。

早年我在当舔狗的时候,就将轮询机制发挥到淋漓尽致

每天向白名单里的女神,定时轮询发消息

无限循环云跪舔三件套:

  • “啊宝贝,最近有没有想我”
  • “啊宝贝早安安”
  • “宝贝晚安,么么哒”

虽然女神依然看不上我

但仍然时刻准备着为女神服务!

结尾

经过以上三个方法优化后

接口请求耗时从3s降到了860ms

这些代码都是从业务中简化后的逻辑

真实的业务场景远比这要复杂:分段式数据存储、主从同步 读写分离、高并发同步策略等等

每一个模块都晦涩难懂

就好像每一个女神都高不可攀

屌丝战胜了所有bug,唯独战胜不了她的心

受伤了只能在深夜里独自买醉

但每当梦到女神打开我做的页面

被极致流畅的体验惊艳到

在精神高潮中享受灵魂升华

那一刻

我觉得我又行了

(完)

代码地址:github

小蝌蚪系列:面试中软性问题的套路与反套路(上)

前言

最近是跳槽季,发现有小伙伴在一些非技术的软性问题上答的不是很好。

众所周知,程序员情商偏低,而这些软性问题,恰恰都具有一定欺骗性和吹牛皮成分在里边,对于演技不好的直男癌,简直就是天生克星。

其实不用太担心,软性问题往往就那几个,稍加训练和准备,你就可以成为一位高端名猿。

题目

第 1 题:说一下你自己的缺点

演技考验:4星

这题处处暗藏杀鸡,很多小伙伴会推心置腹,诚实的将自己所有缺点说出来,比如:“我脾气不好”、“我学习东西慢”、“我贪财好.色”。

这种缺点,人人都有,但大可不必说出来。

很多小伙伴说,我就要做real man,你爱喜欢不喜欢。外面的那些人都是虚伪装哔的妖艳剑货,我就要做真实的自己。

其实,这根本不是真实的自己,这是情商低。不懂得逢场作戏,不懂得在特定的场合说正确的话。

所以,如何说一个缺点,但又不完全是缺点的缺点,还能让人觉得这是优点的缺点

你应该这么演:

唉声叹气,微笑、头部倾斜45度看向地面,说:我偶尔会因为专研技术问题,而搞到深夜,把自己弄得很累。今后我会多注意,把控好技术学习和工作状态的平衡。

这里几个细节注意以下

  • “头部倾斜45度看向地面”从行为学上让人感觉你没有架子,谦虚好相处。
  • 回答中多次用了“搞”、“弄”、“累”等动词,让人觉得你不做作,接地气。
  • 我说我的缺点是因为太爱学习,就好像是说,我的缺点是因为我太有钱,这根本不是缺点,而是优点,只是换了一种说法,直接上天日龙

这就是传说中的反套路学,程序猿和面试官之间的高端博弈,顶级拉扯

如果你有更好的答案或想法,欢迎在题目对应的github下留言:第一名的小蝌蚪

第 2 题:你对加班什么看法?

演技考验:3星

这题真的是炒鸡容易被问到。首先,没人有会喜欢加班,但是,但凡出现“敏捷开发”、“谈加班看法”的,大概率都是经常加班的公司。

我觉得只要不是业界那几个著名黑工厂,正常的加班,都是可以接受的

在大厂工作了8年,总结了一下导致我加班的几个原因:

  • 不爱思考
  • 能力不行
  • 贪玩好.色
  • 第五点不能说,懂的都懂

这8年里,我对加班的心态,从早期的抵触,到中期的忍耐,到现在的主动,我发现,你越不想加班,加班就越来找你,还不如主动进攻,主动学习技术,主动思考工作中优化点。

将重复性工作,通过开发一些工程化、自动化工具去代替,慢慢的你会发现,工作会事半功倍,不再那么被动,真的比被按住头逼你加班要好受很多。

撸迅曾说过:“工作就像强J,与其反抗,不如闭上眼睛好好享受”,很悲哀,但也是中年屌丝生存之道

所以,这道题有一个很不错的答案:
如果是工作需要我会义不容辞加班。但同时,我也会思考工作中的优化点,将重复性工作,通过开发一些工程化、自动化工具去代替,提高工作效率,减少不必要的加班

如果你有更好的答案或想法,欢迎在这题目对应的github下留言:第一名的小蝌蚪

第 3 题:你为什么从上家离职

演技考验:2星

有几个注意点,在说离职原因的时候,有几个大忌:

  • 不能说你跟上家leader闹掰
  • 不能说自己是被开除的
  • 不能说上家公司黄了,所以我跳槽了

反正千万记住,不要说任何上家公司和leader的坏话。

正确的表演应该是,不管上家对你怎么样,都要一副感恩戴德的态度去展示给面试官。是因为某种不可抗拒因素所以才导致你离职

有一个常规回答:上家公司平台趋于稳定,想到一家更大的平台去开阔视野,更好的展现自己的实力,让自己创造更大的价值

见过几个比较搞笑的回答:“我老婆要生了,想去一家能准点下班的公司”、“我失恋了,想去一个地方重新开始”,嘻嘻,但老铁还是别这么回答就好

如果你有更好的答案或想法,欢迎在这题目对应的github下留言:第一名的小蝌蚪

第 4 题:面对hr和面试官刁难,如何应对

演技考验:5星

这个情况是我真实遇到的,我职业生涯第二家公司(某知名大厂),就被当时的hr刁难了

她当时很高傲,还很藐视我,说过几句话让我印象深刻

  • 你背景很一般啊,上家公司是个小公司,我们一般只要有大厂经历的候选人
  • 我们都招985的研究生,你的综合素质没有我想象的好
  • 你简历里面写的那几个项目经历,都太一般了,没有任何知名度,你凭什么觉得我们会要你呢?

当时面完以后,我其实很生气,觉得那个hr人品有问题,但从头到尾都忍住了。

最后诡异的是。。。我还拿到了offer,拿到了期望的薪资和职级。。。。。

很久以后问过她为什么要这么面试候选人

她的回答,大意是:“hr一般这么刁难你,都是想压你的气势,让你受挫,让你觉得配不上我们,增加你接收offer的成功率,还能增加将来谈判薪资的主动权。”

这绝对是一种对人性心理的操控了。。。。

第二点,尤为重要“测试你的情商,那些随便刺激一下,就暴怒、就疯狂反驳、情绪失控的候选人,是绝对不能要的。能全程忍下来,且不卑不亢,保持自信的人,证明了你的抗压能力,确保你今后在工作中能处理好各种工作关系和极端情况”

她的回答令我醍醐灌顶,可能是代码敲多了,从来没有想过,原来人性也可以和代码一样,被度量,被证明

从那以后,对大厂的hr专业度,还是挺认可的

当然,这种刁难也可能是pua的早期萌芽,细思极恐。。。

所以针对候选人在面试过程中被刁难情况,咱们千万不能失态

我的建议是:保持彬彬有礼、谦逊低调、不卑不亢,表明我的来意是因为想到一家更大的平台去开阔视野,更好的展现自己的实力,让自己创造更大的价值,接触更厉害的大牛,同时提升自己

足以

如果你有更好的答案或想法,欢迎在这题目对应的github下留言:第一名的小蝌蚪

总结

以上4题,揭露了些面试中的套路和反套路,展示了逢场作戏和演技的技巧,很多人可能会觉得这样很虚伪

这里又要引用撸迅的一句名言:“当混浊变成一种常态,清白也会是一种罪行”

虚伪的人,有时候也是一种自我保护。

也许这并不虚伪,而是一种生存法则,因为不这么做,就会被这么做的人弄si。

希望大家针对上面提出的问题,和对应的答案,触发一些思考,总结自身,完善自己。不仅仅是面试,在工作也是要这样。

由于篇幅限制,下期会公布另外几道软性问题的答案:

  • 1.职场上,你的技术方案和同事不合,如何处理?
  • 2.如果你的方案和领导不合,如何处理?
  • 3.你未来五年的规划是什么
  • 4.你如何看待ppt文化
  • 5.你的入职,能给我们带来什么价值

第 17 题:(开放题)接上一题如何尽可能流畅的实现这2万小球在浏览器中,以直线运动的动效显示出来

这题考察对大数据的动画显示优化,当然方法有很多种。

但是你有没有用到浏览器的高级api?

你还有没有用到浏览器的专门针对动画的引擎?

或者你对3D的实践和优化,都可以给面试官展示出来。

提供几个思路:

  • 使用GPU硬件加速
  • 使用webGL
  • 使用assembly辅助计算,然后在浏览器端控制动画帧频
  • 用web worker实现javascript多线程,分块处理小球
  • 用单链表树算法和携程机制,实现任务动态分割和任务暂停、恢复、回滚,动态渲染和处理小球

请写一个正则,去除掉html标签字符串里的所有属性,并保留src和href两种属性

这题目简单的理解就是,写一个正则表达式,将字符串'正则'转化成'正则'。

当然,真正包含一个网页的html的字符串要比这个复杂。

而且,google里关于这个问题的前三篇文章答案,都存在严重的问题,随便写几个case都是满足不了的。

正则的问题,很多前端人员都停留在如何用正则去判断一个数字是不是手机号,一段字符串是不是邮箱,说实话,这都没用到正则知识体系的十分之一

在一些工程项目难题上,如果正则使用到位,真的是一行正则可以抵1000行代码。

建议有能力的小伙伴,可以玩一下这题。

小蝌蚪日记:通过console.log高仿FBI Warning

深夜,十二点

昏暗的房间,一卷餐巾纸

注定这是个不平凡的夜

。。。

小蝌蚪打开了珍藏在C盘深处的隐藏文件夹

里面罗列着许多晦涩难懂的文件名

"日理万机.mp4"、"操劳过度.mp4"

“多人运动.mp4”、“2v2敏捷开发.mp4”

别问这些是什么,问就是前端开发教学视频

。。。

小蝌蚪在现实生活中,是一个低端屌丝

受尽女神的冷嘲热讽

但今夜,我的世界我做主

小蝌蚪点开了"采蘑菇的小姑娘.mp4"

笑容逐渐癫狂

。。。

打开后,映入眼帘的是一副FBI Warning大图

众所周知,FBI Warning是东方神秘图腾

它象征着一场大战前,冲锋的号角

小蝌蚪起身

对自己的双手鞠躬:“对不起,今晚又要麻烦你们俩了”

作为一名低级前端工程师

小蝌蚪将用双手

展示如何通过console.log高仿FBI Warning大图

效果如下:

代码如下:

源码:fbi-warning

实现原理很简单

console.log里是可以解析css代码的

格式为:console.log("%c内容1 %c内容2", "css代码1","css代码2");

。。。

实现一个FBI Warning需要4个%c

第一个%c对应的css代码为

    background: #000; 
    font-size: 18px; 
    font-family: monospaces;

这是一个纯黑色背景

黑色象征神秘与性感

似乎为接下来的内容打下了铺垫

第二个%c对应的css代码为

    background: #f33; 
    font-size: 18px; 
    font-family: monospace;
    color: #eee; 
    text-shadow:0 0 1px #fff;

它为“FBI Warning”标题添加了红色背景

红色代表加强

警示每一位看客要遵纪守法

让心怀不轨的人产生羞耻感和罪恶感

第三个%c也是黑色背景

第四个%c对应的css为:

    background: #000; 
    font-size: 18px; 
    font-family: monospace; 
    color: #ddd; 
    text-shadow:0 0 2px #fff;

黑色背景配上白色的字

就好像一个落魄的屌丝

但是内心纯洁无瑕

。。。

以上就是整个高仿FBI Waring的实现了

我敲了6年代码

曾被3位女神抛弃

被2个渣女玩弄过

当过1年小三

再看这幅FBI Waring图 感慨万千

它其实是当代屌丝的一个缩影

描述了一个想爱但不敢爱

只能靠想象来和女神为爱鼓掌的落魄屌丝

卑微且虔诚

。。。

言归正传

在平时开发中console.log搭配一些工具

还能有一些好玩的功能

比如转化'FBI WARNING'成一个字符画

网址:http://patorjk.com/software/taag/#p=display&f=Soft&t=Type%20Something%20

。。。

console.log还能画流程图

网址:http://stable.ascii-flow.appspot.com/#Draw

。。。

console.log里添加emoji表情包

网址:http://getemoji.com/

。。。

把一张图片转成字符画,如下:

网址:https://www.degraeve.com/img2txt.php

。。。

最后一个技能

把js代码,转成漂亮的代码图片

网址:https://carbon.now.sh

以上就是所有console.log技能了

虽然没什么用

嘻嘻

。。。

。。。

。。。

深夜,凌晨二点

小蝌蚪看了一整晚带马赛克的葫芦娃

随着女蛇妖被爷爷打败

小蝌蚪打翻了牛奶

擦拭干净后

在床边点了根烟

强烈的孤独感袭卷背后

世间一切都变得索然无味

寂静的夜 又归于平静

作者:第一名的小蝌蚪

微信公众号:前端屌丝

前端人工智能:通过机器学习推导函数方程式

最近在阅读一些tensorflow.js相关的国外文档和文章,学习到了一些人工智能相关的知识,其中通过机器学习来拟合曲线,推测函数方程,这块觉得挺好玩的,想记录下来分享给大家。

图片描述

什么是tensorflow.js

tensorflow.js是一个能运行在浏览器和nodejs的一个机器学习、机器训练的javascript库,众所周知在浏览器上用javascript进行计算是很慢的,而tensorflow.js会基于WebGL通过gpu进行运算加速来对高性能的机器学习模块进行加速运算,从而能让我们前端开发人员能在浏览器中进行机器学习和训练神经网络。本文要讲解的项目代码,就是要根据一些规则模拟数据,然后通过机器学习和训练,根据这些数据去反向推测出生成这些数据的公式函数。

基本概念

接下来我们用五分钟过一下tensorflow的基本概念,这一部分主要介绍一些概念,笔者会用一些类比方式简单的讲述一些概念,旨在帮助大家快速理解,但是限于精力和篇幅,概念的具体详细定义读者们还是多去参照官方文档。

张量(tensors)

张量其实就是一个数组,可以一维或多维数组。张量在tensorflow.js里就是一个数据单元。

const tensors = tf.tensor([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
tensors.print();

在浏览器里将会输出:

图片描述

tensorflow还提供了语义化的张量创建函数:tf.scalar(创建零维度的张量), tf.tensor1d(创建一维度的张量), tf.tensor2d(创建二维度的张量), tf.tensor3d(创建三维度的张量)、tf.tensor4d(创建四维度的张量)以及 tf.ones(张量里的值全是1)或者tf.zeros(张量里的值全是0)。

变量(variable)

张量tensor是不可变的,而变量variable是可变的,变量是通过张量初始化而来,代码如下:

const initialValues = tf.zeros([5]);//[0, 0, 0, 0, 0]
const biases = tf.variable(initialValues); //通过张量初始化变量
biases.print(); //输出[0, 0, 0, 0, 0]

操作(operations)

张量可以通过操作符进行运算,比如add(加法)、sub(减法)、mul(乘法)、square(求平方)、mean(求平均值) 。

const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const e_plus_f = e.add(f);
e_plus_f.print();

上面的例子输出:
图片描述

内存管理(dispose和tf.tidy)

dispose和tf.tidy都是用来清空GPU缓存的,就相当于咱们在编写js代码的时候,通过给这个变量赋值null来清空缓存的意思。

var a = {num: 1};
a = null;//清除缓存

dispose

张量和变量都可以通过dispose来清空GPU缓存:

const x = tf.tensor2d([[0.0, 2.0], [4.0, 6.0]]);
const x_squared = x.square();

x.dispose();
x_squared.dispose();
tf.tidy

当有多个张量和变量的时候,如果挨个调用dispose就太麻烦了,所以有了tf.tidy,将张量或者变量操作放在tf.tidy函数中,就会自动给我们优化和清除缓存。

const average = tf.tidy(() => {
  const y = tf.tensor1d([4.0, 3.0, 2.0, 1.0]);
  const z = tf.ones([1]);
  return y.sub(z);
});

average.print() 

以上例子输出:
图片描述

模拟数据

首先,我们要模拟一组数据,根据 这个三次方程,以参数a=-0.8, b=-0.2, c=0.9, d=0.5生成[-1, 1]这个区间内一些有误差的数据,数据可视化后如下:
图片描述

假设我们并不知道a、b、c、d这四个参数的值,我们要通过这一堆数据,用机器学习和机器训练去反向推导出这个多项式函数方程的和它的a、b、c、d这四个参数值。

设置变量(Set up variables)

因为我们要反向推导出多项式方程的a、b、c、d这四个参数值,所以首先我们要先定义这四个变量,并给他们赋上一些随机数当做初始值。

const a = tf.variable(tf.scalar(Math.random()));
const b = tf.variable(tf.scalar(Math.random()));
const c = tf.variable(tf.scalar(Math.random()));
const d = tf.variable(tf.scalar(Math.random()));

上面这四行代码,tf.scalar就是创建了一个零维度的张量,tf.variable就是将我们的张量转化并初始化成一个变量variable,如果通俗的用我们平时编写javascript去理解,上面四行代码就相当于:

let a = Math.random();
let b = Math.random();
let c = Math.random();
let d = Math.random();

当我们给a、b、c、d这四个参数值赋上了初始随机值以后,a=0.513, b=0.261, c=0.259, d=0.504,我们将这些参数放入方程式后得到的曲线图如下:
图片描述

我们可以看到,根据随机生成的a、b、c、d这四个参数并入到多项式后生成的数据跟真正的数据模拟的曲线差别很大,这就是我们接下来要做的,通过机器学习和训练,不断的调整a、b、c、d这四个参数来将这根曲线尽可能的无限接近实际的数据曲线。

创建优化器(Create an optimizer)

const learningRate = 0.5;
const optimizer = tf.train.sgd(learningRate);

learningRate这个变量是定义学习率,在进行每一次机器训练的时候,会根据学习率的大小去进行计算的偏移量调整幅度,学习率越低,最后预测到的值就会越精准,但是响应的会增加程序的运行时间和计算量。高学习率会加快学习过程,但是由于偏移量幅度太大,容易造成在正确值的周边上下摆动导致运算出的结果没有那么准确。

tf.train.sgd是我们选用了tensorflow.js里帮我们封装好的SGD优化器,即随机梯度下降法。在机器学习算法的时候,通常采用梯度下降法来对我们的算法进行机器训练,梯度下降法常用有三种形式BGD、SGD以及MBGD。

我们使用的是SGD这个批梯度下降法,因为每当梯度下降而要更新一个训练参数的时候,机器训练的速度会随着样本的数量增加而变得非常缓慢。随机梯度下降正是为了解决这个办法而提出的。假设一般线性回归函数的函数为:
图片描述

SGD它是利用每个样本的损失函数对θ求偏导得到对应的梯度,来更新θ:
图片描述

随机梯度下降是通过每个样本来迭代更新一次,对比上面的批量梯度下降,迭代一次需要用到所有训练样本,SGD迭代的次数较多,在解空间的搜索过程看起来很盲目。但是大体上是往着最优值方向移动。随机梯度下降收敛图如下:
图片描述

预期函数模型(training process functions)
编写预期函数模型,其实就是用一些列的operations操作去描述我们的函数模型

function predict(x) {
  // y = a * x ^ 3 + b * x ^ 2 + c * x + d
  return tf.tidy(() => {
    return a.mul(x.pow(tf.scalar(3, 'int32')))
      .add(b.mul(x.square()))
      .add(c.mul(x))
      .add(d);
  });
}

a.mul(x.pow(tf.scalar(3, 'int32')))就是描述了a*x^3(a乘以x的三次方),b.mul(x.square()))描述了b * x ^ 2(b乘以x的平方),c.mul(x)这些同理。注意,在predict函数return的时候,用tf.tidy包了起来,这是为了方便内存管理和优化机器训练过程的内存。

定义损失函数(loss)

接下来我们要定义一个损失函数,使用的是MSE(均方误差,mean squared error)。数理统计中均方误差是指参数估计值与参数真值之差平方的期望值,记为MSE。MSE是衡量“平均误差”的一种较方便的方法,MSE可以评价数据的变化程度,MSE的值越小,说明预测模型描述实验数据具有更好的精确度。MSE的计算非常简单,就是先根据给定的x得到实际的y值与预测得到的y值之差 的平方,然后在对这些差的平方求平均数即可。 
图片描述

根据如上所述,我们的损失函数代码如下:

function loss(prediction, labels) {
  const error = prediction.sub(labels).square().mean();
  return error;
}

预期值prediction减去实际值labels,然后平方后求平均数即可。

机器训练(training)

好了,上面说了这么多,做了这么多的铺垫和准备,终于到了最关键的步骤,下面这段代码和函数就是真正的根据数据然后通过机器学习和训练计算出我们想要的结果最重要的步骤。我们已经定义好了基于SGD随机梯度下降的优化器optimizer,然后也定义了基于MSE均方误差的损失函数,我们应该怎么结合他们两个装备去进行机器训练和机器学习呢,看下面的代码。

const numIterations = 75;

async function train(xs, ys, numIterations) {
  for (let iter = 0; iter < numIterations; iter++) {
    //优化器:SGD随机梯度下降
    optimizer.minimize(() => {
      const pred = predict(xs);
      //损失函数:MSE均方误差
      return loss(pred, ys);
    });
    //防止阻塞浏览器
    await tf.nextFrame();
  }
}

我们在外层定义了一个numIterations = 75,意思是我们要进行75次机器训练。在每一次循环中我们都调用了optimizer.minimize这个函数,它会不断地调用SGD随机梯度下降法来不断地更新和修正我们的a、b、c、d这四个参数,并且每一次return的时候都会调用我们的基于MSE均方误差loss损失函数来减小损失。经过这75次的机器训练和机器学习,加上SGD随机梯度下降优化器和loss损失函数进行校准,最后就会得到非常接近正确数值的a、b、c、d四个参数。

我们注意到这个函数最后有一行tf.nextFrame(),这个函数是为了解决在机器训练和机器学习的过程中会进行大量的机器运算,会阻塞浏览器,导致ui没法更新的问题。

我们调用这个机器训练的函数train:

  import {generateData} from './data';//这个文件在git仓库里
  const trainingData = generateData(100, {a: -.8, b: -.2, c: .9, d: .5});
  await train(trainingData.xs, trainingData.ys, 75);

调用了train函数后,我们就可以拿到a、b、c、d四个参数了。

  console.log('a', a.dataSync()[0]);
  console.log('b', b.dataSync()[0]);
  console.log('c', c.dataSync()[0]);
  console.log('d', d.dataSync()[0]);

最后得到的值是a=-0.564, b=-0.207, c=0.824, d=0.590,和原先我们定义的实际值a=-0.8, b=-0.2, c=0.9, d=0.5非常的接近了,对比图如下:
图片描述

项目运行和安装

本文涉及到的代码安装和运行步骤如下:

git clone https://github.com/tensorflow/tfjs-examples
cd tfjs-examples/polynomial-regression-core
yarn
yarn watch

tensorflow.js的官方example里有很多个项目,其中polynomial-regression-core(多项式方程回归复原)这个例子就是我们本文重点讲解的代码,我在安装的过程中并不太顺利,每一次运行都会报缺少模块的error,读者只需要根据报错,把缺少的模块挨个安装上,然后根据error提示信息上google去搜索相应的解决方法,最后都能运行起来。

结语

bb了这么多,本来不想写结语的,但是想想,还是想表达一下本人内心的一个搞笑荒谬的想法。我为什么会对这个人工智能的例子感兴趣呢,是因为,在我广西老家(一个偏远的山村),那边比较封建迷信,经常拿一个人的生辰八字就去计算并说出这个人一生的命运,balabala说一堆,本人对这些风气一贯都是嗤之以鼻。但是,但是,但是。。。。荒谬的东西来了,我老丈人十早年前因为车祸而断了一条腿,几年前带媳妇和老丈人回老家见亲戚,老丈人觉得南方人这些封建迷信很好玩,就拿他自己的生辰八字去给乡下的老者算了一下,结果那个老人说了很多,并说出了我老丈人出车祸的那一天的准确的日期,精确到那一天的下午大致时间。。。。。这。。。。这就好玩了。。。当年整个空气突然安静的场景至今历历在目,这件事一直记在心里,毕竟我从来不相信这些鬼鬼乖乖的东西,一直信奉科学是至高无上带我们飞的唯一真理,但是。。。真的因为这件事,让我菊紧蛋疼不知道怎么去评价。。。。

咦?这跟人工智能有什么关系?我只是在思考,是不是我们每个人的生辰八字,就是笛卡尔平面坐标系上的维度,或者说生辰八字就是多项式函数的a、b、c、d、e系数,是不是真的有一个多项式函数方程能把这些生辰八字系数串联起来得到一个公式,这个公式可以描述你的一生,记录你的过去,并预测你的将来。。。。。。我们能不能找到自己对应的维度和发生过的事情联系起来,然后用人工智能去机器学习并训练出一个属于我们自己一生命运轨迹的函数。。。。行 不说了 ,各位读者能看到这里我也是觉得对不起你们,好好读书并忘掉我说的话。

上述观点纯属个人意淫,该搬砖搬砖,该带娃带娃,祝各位早日登上前端最强王者的段位。!^_^!

作者:第一名的小蝌蚪

github: 文章会第一时间分享在前端屌丝心路历程,欢迎star或者watch,感恩

交流

欢迎关注我的微信公众号,讲述了一个前端屌丝逆袭的心路历程,共勉。

image

结结语

最后,TNFE团队为前端开发人员整理出了小程序以及web前端技术领域的最新优质内容,每周更新✨,欢迎star,github地址:https://github.com/Tnfe/TNFE-Weekly

第 9 题: 简单实现项目代码按需加载,例如import { Button } from 'antd',打包的时候只打包button

原理很简单,就是将

import { Select, Pagination, Button } from 'xxx-ui';

通过babel转化成

import Button from `xxx-ui/src/components/ui-base/Button/Button`;
import Pagination from `xxx-ui/src/components/ui-base/Pagination/Pagination`;
import Select from `xxx-ui/src/components/ui-base/Select/Select`;

自定义拓展一个babel插件,代码如下:

visitor: {
	ImportDeclaration (path, { opts }) {
	    const specifiers = path.node.specifiers;
	    const source = path.node.source;

            // 判断传入的配置参数是否是数组形式
	    if (Array.isArray(opts)) {
	        opts.forEach(opt => {
	            assert(opt.libraryName, 'libraryName should be provided');
	        });
	        if (!opts.find(opt => opt.libraryName === source.value)) return;
	    } else {
	        assert(opts.libraryName, 'libraryName should be provided');
	        if (opts.libraryName !== source.value) return;
	    }

	    const opt = Array.isArray(opts) ? opts.find(opt => opt.libraryName === source.value) : opts;
	    opt.camel2UnderlineComponentName = typeof opt.camel2UnderlineComponentName === 'undefined'
	        ? false
	        : opt.camel2UnderlineComponentName;
	    opt.camel2DashComponentName = typeof opt.camel2DashComponentName === 'undefined'
	        ? false
	        : opt.camel2DashComponentName;

	    if (!types.isImportDefaultSpecifier(specifiers[0]) && !types.isImportNamespaceSpecifier(specifiers[0])) {
	        // 遍历specifiers生成转换后的ImportDeclaration节点数组
    		const declarations = specifiers.map((specifier) => {
	            // 转换组件名称
                    const transformedSourceName = opt.camel2UnderlineComponentName
                	? camel2Underline(specifier.imported.name)
                	: opt.camel2DashComponentName
            		    ? camel2Dash(specifier.imported.name)
            		    : specifier.imported.name;
    		    // 利用自定义的customSourceFunc生成绝对路径,然后创建新的ImportDeclaration节点
                    return types.ImportDeclaration([types.ImportDefaultSpecifier(specifier.local)],
                	types.StringLiteral(opt.customSourceFunc(transformedSourceName)));
                });
                // 将当前节点替换成新建的ImportDeclaration节点组
    		path.replaceWithMultiple(declarations);
    	}
    }
}

KOA2框架原码解析和实现

什么是koa框架?

       koa是一个基于node实现的一个新的web框架,它是由express框架的原班人马打造的。它的特点是优雅、简洁、表达力强、自由度高。它更express相比,它是一个更轻量的node框架,因为它所有功能都通过插件实现,这种插拔式的架构设计模式,很符合unix哲学。

       koa框架现在更新到了2.x版本,本文从零开始,循序渐进,讲解koa2的框架源码结构和实现原理,展示和详解koa2框架源码中的几个最重要的概念,然后手把手教大家亲自实现一个简易的koa2框架,帮助大家学习和更深层次的理解koa2,看完本文以后,再去对照koa2的源码进行查看,相信你的思路将会非常的顺畅。

       本文所用的框架是koa2,它跟koa1不同,koa1使用的是generator+co.js的执行方式,而koa2中使用了async/await,因此本文的代码和demo需要运行在node 8版本及其以上,如果读者的node版本较低,建议升级或者安装babel-cli,用其中的babel-node来运行本文涉及到的代码。

       本文实现的轻量版koa的完整代码gitlab地址为:article_koa2

koa源码结构

image

       上图是koa2的源码目录结构的lib文件夹,lib文件夹下放着四个koa2的核心文件:application.js、context.js、request.js、response.js。

application.js

        application.js是koa的入口文件,它向外导出了创建class实例的构造函数,它继承了events,这样就会赋予框架事件监听和事件触发的能力。application还暴露了一些常用的api,比如toJSON、listen、use等等。

        listen的实现原理其实就是对http.createServer进行了一个封装,重点是这个函数中传入的callback,它里面包含了中间件的合并,上下文的处理,对res的特殊处理。

        use是收集中间件,将多个中间件放入一个缓存队列中,然后通过koa-compose这个插件进行递归组合调用这一些列的中间件。

context.js

        这部分就是koa的应用上下文ctx,其实就一个简单的对象暴露,里面的重点在delegate,这个就是代理,这个就是为了开发者方便而设计的,比如我们要访问ctx.repsponse.status但是我们通过delegate,可以直接访问ctx.status访问到它。

request.js、response.js

        这两部分就是对原生的res、req的一些操作了,大量使用es6的get和set的一些语法,去取headers或者设置headers、还有设置body等等,这些就不详细介绍了,有兴趣的读者可以自行看源码。

实现koa2的四大模块

        上文简述了koa2源码的大体框架结构,接下来我们来实现一个koa2的框架,笔者认为理解和实现一个koa框架需要实现四个大模块,分别是:

  • 封装node http server、创建Koa类构造函数
  • 构造request、response、context对象
  • 中间件机制和剥洋葱模型的实现
  • 错误捕获和错误处理
  • 下面我们就逐一分析和实现。

模块一:封装node http server和创建Koa类构造函数

        阅读koa2的源码得知,实现koa的服务器应用和端口监听,其实就是基于node的原生代码进行了封装,如下图的代码就是通过node原生代码实现的服务器监听。

let http = require('http');
let server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world');
});
server.listen(3000, () => {    
    console.log('listenning on 3000');
});

        我们需要将上面的node原生代码封装实现成koa的模式:

const http = require('http');
const Koa = require('koa');
const app = new Koa();
app.listen(3000);

        实现koa的第一步就是对以上的这个过程进行封装,为此我们需要创建application.js实现一个Application类的构造函数

let http = require('http');
class Application {    
    constructor() {        
        this.callbackFunc;
    }
    listen(port) {        
        let server = http.createServer(this.callback());
        server.listen(port);
    }
    use(fn) {
        this.callbackFunc = fn;
    }
    callback() {
        return (req, res) => {
            this.callbackFunc(req, res);
        };
    }
}
module.exports = Application;

        然后创建example.js,引入application.js,运行服务器实例启动监听代码:

let Koa = require('./application');
let app = new Koa();
app.use((req, res) => {
    res.writeHead(200);
    res.end('hello world');
});
app.listen(3000, () => {
    console.log('listening on 3000');
});

        现在在浏览器输入localhost:3000即可看到浏览器里显示“hello world”。现在第一步我们已经完成了,对http server进行了简单的封装和创建了一个可以生成koa实例的类class,这个类里还实现了app.use用来注册中间件和注册回调函数,app.listen用来开启服务器实例并传入callback回调函数,第一模块主要是实现典型的koa风格和搭好了一个koa的简单的架子。接下来我们开始编写和讲解第二模块。

模块二:构造request、response、context对象

        阅读koa2的源码得知,其中context.js、request.js、response.js三个文件分别是request、response、context三个模块的代码文件。context就是我们平时写koa代码时的ctx,它相当于一个全局的koa实例上下文this,它连接了request、response两个功能模块,并且暴露给koa的实例和中间件等回调函数的参数中,起到承上启下的作用。

        request、response两个功能模块分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象。我们基于这个原理简单实现一下request.js、response.js,首先创建request.js文件,然后写入以下代码:

let url = require('url');
module.exports = {
    get query() {
        return url.parse(this.req.url, true).query;
    }
};

        这样当你在koa实例里使用ctx.query的时候,就会返回url.parse(this.req.url, true).query的值。看源码可知,基于getter和setter,在request.js里还封装了header、url、origin、path等方法,都是对原生的request上用getter和setter进行了封装,笔者不再这里一一实现。

        接下来我们实现response.js文件代码模块,它和request原理一样,也是基于getter和setter对原生response进行了封装,那我们接下来通过对常用的ctx.body和ctx.status这个两个语句当做例子简述一下如果实现koa的response的模块,我们首先创建好response.js文件,然后输入下面的代码:

module.exports = {
    get body() {
        return this._body;
    },
    set body(data) {
        this._body = data;
    },
    get status() {
        return this.res.statusCode;
    },
    set status(statusCode) {
        if (typeof statusCode !== 'number') {
            throw new Error('something wrong!');
        }
        this.res.statusCode = statusCode;
    }
};

        以上代码实现了对koa的status的读取和设置,读取的时候返回的是基于原生的response对象的statusCode属性,而body的读取则是对this._body进行读写和操作。这里对body进行操作并没有使用原生的this.res.end,因为在我们编写koa代码的时候,会对body进行多次的读取和修改,所以真正返回浏览器信息的操作是在application.js里进行封装和操作。

        现在我们已经实现了request.js、response.js,获取到了request、response对象和他们的封装的方法,然后我们开始实现context.js,context的作用就是将request、response对象挂载到ctx的上面,让koa实例和代码能方便的使用到request、response对象中的方法。现在我们创建context.js文件,输入如下代码:

let proto = {};

function delegateSet(property, name) {
    proto.__defineSetter__(name, function (val) {
        this[property][name] = val;
    });
}

function delegateGet(property, name) {
    proto.__defineGetter__(name, function () {
        return this[property][name];
    });
}

let requestSet = [];
let requestGet = ['query'];

let responseSet = ['body', 'status'];
let responseGet = responseSet;

requestSet.forEach(ele => {
    delegateSet('request', ele);
});

requestGet.forEach(ele => {
    delegateGet('request', ele);
});

responseSet.forEach(ele => {
    delegateSet('response', ele);
});

responseGet.forEach(ele => {
    delegateGet('response', ele);
});

module.exports = proto;

        context.js文件主要是对常用的request和response方法进行挂载和代理,通过context.query直接代理了context.request.query,context.body和context.status代理了context.response.body与context.response.status。而context.request,context.response则会在application.js中挂载。

        本来可以用简单的setter和getter去设置每一个方法,但是由于context对象定义方法比较简单和规范,在koa源码里可以看到,koa源码用的是__defineSetter__和__defineSetter__来代替setter/getter每一个属性的读取设置,这样做主要是方便拓展和精简了写法,当我们需要代理更多的res和req的方法的时候,可以向context.js文件里面的数组对象里面添加对应的方法名和属性名即可。

        目前为止,我们已经得到了request、response、context三个模块对象了,接下来就是将request、response所有方法挂载到context下,让context实现它的承上启下的作用,修改application.js文件,添加如下代码:

let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');

createContext(req, res) {       
   let ctx = Object.create(this.context);
   ctx.request = Object.create(this.request);
   ctx.response = Object.create(this.response);
   ctx.req = ctx.request.req = req;
   ctx.res = ctx.response.res = res; 
   return ctx;
}

        可以看到,我们添加了createContext这个方法,这个方法是关键,它通过Object.create创建了ctx,并将request和response挂载到了ctx上面,将原生的req和res挂载到了ctx的子属性上,往回看一下context/request/response.js文件,就能知道当时使用的this.res或者this.response之类的是从哪里来的了,原来是在这个createContext方法中挂载到了对应的实例上,构建了运行时上下文ctx之后,我们的app.use回调函数参数就都基于ctx了。

模块三:中间件机制和剥洋葱模型的实现

        目前为止我们已经成功实现了上下文context对象、 请求request对象和响应response对象模块,还差一个最重要的模块,就是koa的中间件模块,koa的中间件机制是一个剥洋葱式的模型,多个中间件通过use放进一个数组队列然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分,这就是剥洋葱模型,koa的中间件机制。

        koa的剥洋葱模型在koa1中使用的是generator + co.js去实现的,koa2则使用了async/await + Promise去实现的,接下来我们基于async/await + Promise去实现koa2中的中间件机制。首先,假设当koa的中间件机制已经做好了,那么它是能成功运行下面代码的:

let Koa = require('../src/application');

let app = new Koa();

app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(6);
});

app.use(async (ctx, next) => {
    console.log(2);
    await next();
    console.log(5);
});

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});

app.listen(3000, () => {
    console.log('listenning on 3000');
});
```        
        运行成功后会在终端输出123456,那就能验证我们的koa的剥洋葱模型是正确的。接下来我们开始实现,修改application.js文件,添加如下代码:      
```javascript
compose() {
        return async ctx => {
            function createNext(middleware, oldNext) {
                return async () => {
                    await middleware(ctx, oldNext);
                }
            }
            let len = this.middlewares.length;
            let next = async () => {
                return Promise.resolve();
            };
            for (let i = len - 1; i >= 0; i--) {
                let currentMiddleware = this.middlewares[i];
                next = createNext(currentMiddleware, next);
            }
            await next();
        };
    }

    callback() {
        return (req, res) => {
            let ctx = this.createContext(req, res);
            let respond = () => this.responseBody(ctx);
            let onerror = (err) => this.onerror(err, ctx);
            let fn = this.compose();
            return fn(ctx);
        };
    }
```        
        koa通过use函数,把所有的中间件push到一个内部数组队列this.middlewares中,剥洋葱模型能让所有的中间件依次执行,每次执行完一个中间件,遇到next()就会将控制权传递到下一个中间件,下一个中间件的next参数,剥洋葱模型的最关键代码是compose这个函数:      
```javascript
compose() {
        return async ctx => {
            function createNext(middleware, oldNext) {
                return async () => {
                    await middleware(ctx, oldNext);
                }
            }
            let len = this.middlewares.length;
            let next = async () => {
                return Promise.resolve();
            };
            for (let i = len - 1; i >= 0; i--) {
                let currentMiddleware = this.middlewares[i];
                next = createNext(currentMiddleware, next);
            }
            await next();
        };
    }

        createNext函数的作用就是将上一个中间件的next当做参数传给下一个中间件,并且将上下文ctx绑定当前中间件,当中间件执行完,调用next()的时候,其实就是去执行下一个中间件。

for (let i = len - 1; i >= 0; i--) {
        let currentMiddleware = this.middlewares[i];
        next = createNext(currentMiddleware, next);
 }

        上面这段代码其实就是一个链式反向递归模型的实现,i是从最大数开始循环的,将中间件从最后一个开始封装,每一次都是将自己的执行函数封装成next当做上一个中间件的next参数,这样当循环到第一个中间件的时候,只需要执行一次next(),就能链式的递归调用所有中间件,这个就是koa剥洋葱的核心代码机制。

        到这里我们总结一下上面所有剥洋葱模型代码的流程,通过use传进来的中间件是一个回调函数,回调函数的参数是ctx上下文和next,next其实就是控制权的交接棒,next的作用是停止运行当前中间件,将控制权交给下一个中间件,执行下一个中间件的next()之前的代码,当下一个中间件运行的代码遇到了next(),又会将代码执行权交给下下个中间件,当执行到最后一个中间件的时候,控制权发生反转,开始回头去执行之前所有中间件中剩下未执行的代码,这整个流程有点像一个伪递归,当最终所有中间件全部执行完后,会返回一个Promise对象,因为我们的compose函数返回的是一个async的函数,async函数执行完后会返回一个Promise,这样我们就能将所有的中间件异步执行同步化,通过then就可以执行响应函数和错误处理函数。

        当中间件机制代码写好了以后,运行我们的上面的例子,已经能输出123456了,至此,我们的koa的基本框架已经基本做好了,不过一个框架不能只实现功能,为了框架和服务器实例的健壮,还需要加上错误处理机制。

模块四:错误捕获和错误处理

        要实现一个基础框架,错误处理和捕获必不可少,一个健壮的框架,必须保证在发生错误的时候,能够捕获到错误和抛出的异常,并反馈出来,将错误信息发送到监控系统上进行反馈,目前我们实现的简易koa框架还没有能实现这一点,我们接下加上错误处理和捕获的机制。

throw new Error('oooops');

        基于现在的框架,如果中间件代码中出现如上错误异常抛出,是捕获不到错误的,这时候我们看一下application.js中的callback函数的return返回代码,如下:

return fn(ctx).then(respond);

        可以看到,fn是中间件的执行函数,每一个中间件代码都是由async包裹着的,而且中间件的执行函数compose返回的也是一个async函数,我们根据es7的规范知道,async返回的是一个promise的对象实例,我们如果想要捕获promise的错误,只需要使用promise的catch方法,就可以把所有的中间件的异常全部捕获到,修改后callback的返回代码如下:

return fn(ctx).then(respond).catch(onerror);

        现在我们已经实现了中间件的错误异常捕获,但是我们还缺少框架层发生错误的捕获机制,我们希望我们的服务器实例能有错误事件的监听机制,通过on的监听函数就能订阅和监听框架层面上的错误,实现这个机制不难,使用nodejs原生events模块即可,events模块给我们提供了事件监听on函数和事件触发emit行为函数,一个发射事件,一个负责接收事件,我们只需要将koa的构造函数继承events模块即可,构造后的伪代码如下:

let EventEmitter = require('events');
class Application extends EventEmitter {}

        继承了events模块后,当我们创建koa实例的时候,加上on监听函数,代码如下:

let app = new Koa();

app.on('error', err => {
    console.log('error happends: ', err.stack);
});

        这样我们就实现了框架层面上的错误的捕获和监听机制了。总结一下,错误处理和捕获,分中间件的错误处理捕获和框架层的错误处理捕获,中间件的错误处理用promise的catch,框架层面的错误处理用nodejs的原生模块events,这样我们就可以把一个服务器实例上的所有的错误异常全部捕获到了。至此,我们就完整实现了一个轻量版的koa框架了。

结尾

目前为止,我们已经实现了一个轻量版的koa框架了,我们实现了封装node http server、创建Koa类构造函数、构造request、response、context对象、中间件机制和剥洋葱模型的实现、错误捕获和错误处理这四个大模块,理解了这个轻量版koa的实现原理,再去看koa2的源码,你就会发现一切都豁然开朗,koa2的源码无非就是在这个轻量版基础上加了很多工具函数和细节的处理,限于篇幅笔者就不再一一介绍了。

本文实现的轻量版koa的完整代码gitlab地址为:article_koa2   

第 12 题:前端如何进行seo优化

  • 合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。
  • 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页
  • 重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取
  • 重要内容不要用js输出:爬虫不会执行js获取内容
  • 少用iframe:搜索引擎不会抓取iframe中的内容
  • 非装饰性图片必须加alt
  • 提高网站速度:网站速度是搜索引擎排序的一个重要指标
  • 前后端分离的项目使用服务端同构渲染,既提高了访问速度,同时重要关键内容首次渲染不通过 js 输出
  • 友情链接,好的友情链接可以快速的提高你的网站权重
  • 外链,高质量的外链,会给你的网站提高源源不断的权重提升
  • 向各大搜索引擎登陆入口提交尚未收录站点

小蝌蚪传记:PNG图片压缩原理--屌丝的眼泪

背景

今天凌晨一点,突然有个人加我的qq,一看竟然是十年前被我删掉的初恋。。。。

因为之前在qq空间有太多的互动,所以qq推荐好友里面经常推荐我俩互相认识。。。。谜之尴尬

同意好友申请以后,仔细看了她这十年间所有的qq动态和照片。
她变美了,会打扮了,以前瘦瘦的身材配上现在的装扮和妆容,已经是超越我认知的女神了。

而我依然碌碌无为,逐渐臃肿的身体加上日益上扬的发际线,每天为生活操劳和奔波,还穷。

用一句话形容现在的感受就是:
“妳已经登上更高的巅峰 而我只能望着妳远去的背影”。

默默点了根烟,把她长得好看的照片都保存了下来。
咦?发现每一张照片都是.png的图片格式。
png??png的图片我们每天都在用,可是png到底是什么,它的压缩原理是什么?
很好,接下来我将会给大家一一阐述。

什么是PNG

PNG的全称叫便携式网络图型(Portable Network Graphics)是目前最流行的网络传输和展示的图片格式,原因有如下几点:

  • 无损压缩:PNG图片采取了基于LZ77派生算法对文件进行压缩,使得它压缩比率更高,生成的文件体积更小,并且不损失数据。

  • 体积小:它利用特殊的编码方法标记重复出现的数据,使得同样格式的图片,PNG图片文件的体积更小。网络通讯中因受带宽制约,在保证图片清晰、逼真的前提下,优先选择PNG格式的图片。

  • 支持透明效果:PNG支持对原图像定义256个透明层次,使得图像的边缘能与任何背景平滑融合,这种功能是GIF和JPEG没有的。

PNG类型

PNG图片主要有三个类型,分别为 PNG 8/ PNG 24 / PNG 32。

  • PNG 8:PNG 8中的8,其实指的是8bits,相当于用2^8(2的8次方)大小来存储一张图片的颜色种类,2^8等于256,也就是说PNG 8能存储256种颜色,一张图片如果颜色种类很少,将它设置成PNG 8得图片类型是非常适合的。

  • PNG 24:PNG 24中的24,相当于3乘以8 等于 24,就是用三个8bits分别去表示 R(红)、G(绿)、B(蓝)。R(0255),G(0255),B(0~255),可以表达256乘以256乘以256=16777216种颜色的图片,这样PNG 24就能比PNG 8表示色彩更丰富的图片。但是所占用的空间相对就更大了。

  • PNG 32:PNG 32中的32,相当于PNG 24 加上 8bits的透明颜色通道,就相当于R(红)、G(绿)、B(蓝)、A(透明)。R(0255),G(0255),B(0255),A(0255)。比PNG 24多了一个A(透明),也就是说PNG 32能表示跟PNG 24一样多的色彩,并且还支持256种透明的颜色,能表示更加丰富的图片颜色类型。

怎么说呢,总的来说,PNG 8/ PNG 24 / PNG 32就相当于我们屌丝心中,把女神分为三类:

  • 一类女神 = PNG 8:屌丝舔狗们见到第一类女神,顿时会觉得心情愉悦、笑逐颜开,屌丝发黑的印堂逐渐舒展,确认过眼神,是心动的感觉。

  • 二类女神 = PNG 24:第二类女神开始厉害了,会给屌丝们一种菊花一紧、振聋发聩的心弦震撼,接触多了第二类女神能让屌丝每天精神抖擞,延年益寿。

  • 三类女神 = PNG 32:在第三类女神面前,所有的语言都显得苍白无力。那是一种看了让屌丝上下通透、手眼通天的至尊级存在。超凡脱俗、天神下凡都不足以描摹她美色的二分之一。我曾经只有在梦里才见到过。

哎。。。我的初恋,看着她现在的照片,应该是触及PNG 24这一等级了。

PNG图片数据结构

PNG图片的数据结构其实跟http请求的结构很像,都是一个数据头,后面跟着很多的数据块,如下图所示:

image

如果你用vim的查看编码模式打开一张png图片,会是下面这个样子:

image

握草,第一眼看到这一坨坨十六进制编码是不是感觉和女神的心思一样晦涩难懂?

老弟 莫慌,讲实话,如果撩妹纸有那一坨坨乱码那么简单,哥哥我早就妻妾成群啦。
接下来我就一一讲解这一堆十六进制编码的含义。

8950 4e47 0d0a 1a0a:这个是PNG图片的头,所有的PNG图片的头都是这一串编码,图片软件通过这串编码判定这个文件是不是PNG格式的图片。

0000 000d:是iHDR数据块的长度,为13。

4948 4452:是数据块的type,为IHDR,之后紧跟着是data。

0000 02bc:是图片的宽度。

0000 03a5:是高度。

以此类推,每一段十六进制编码就代表着一个特定的含义。下面其他的就不一一分析了,太多了,小伙伴们自己去查吧。

什么样的PNG图片更适合压缩

常规的png图片,颜色越单一,颜色值越少,压缩率就越大,比如下面这张图:

image

它仅仅由红色和绿色构成,如果用0代表红色,用1代表绿色,那用数字表示这张图就是下面这个样子:

00000000000000000

00000000000000000

00000000000000000

1111111111111111111111111

1111111111111111111111111

1111111111111111111111111

我们可以看到,这张图片是用了大量重复的数字,我们可以将重复的数字去掉,直接用数组形式的[0, 1]就可以直接表示出这张图片了,仅仅用两个数字,就能表示出一张很大的图片,这样就极大的压缩了一张png图片。

所以!颜色越单一,颜色值越少,颜色差异越小的png图片,压缩率就越大,体积就越小。

PNG的压缩

PNG图片的压缩,分两个阶段:

  • 预解析(Prediction):这个阶段就是对png图片进行一个预处理,处理后让它更方便后续的压缩。说白了,就是一个女神,在化妆前,会先打底,先涂乳液和精华,方便后续上妆、美白、眼影、打光等等。

  • 压缩(Compression):执行Deflate压缩,该算法结合了 LZ77 算法和 Huffman 算法对图片进行编码。

预解析(Prediction)

png图片用差分编码(Delta encoding)对图片进行预处理,处理每一个的像素点中每条通道的值,差分编码主要有几种:

  • 不过滤
  • X-A
  • X-B
  • X-(A+B)/2(又称平均值)
  • Paeth推断(这种比较复杂)

假设,一张png图片如下:

image

这张图片是一个红色逐渐增强的渐变色图,它的红色从左到右逐渐加强,映射成数组的值为[1,2,3,4,5,6,7,8],使用X-A的差分编码的话,那就是:

[2-1=1, 3-2=1, 4-3=1, 5-4=1, 6-5=1, 7-6=1, 8-7=1]

得到的结果为

[1,1,1,1,1,1,1]

最后的[1,1,1,1,1,1,1]这个结果出现了大量的重复数字,这样就非常适合进行压缩。

这就是为什么渐变色图片、颜色值变化不大并且颜色单一的图片更容易压缩的原理。

差分编码的目的,就是尽可能的将png图片数据值转换成一组重复的、低的值,这样的值更容易被压缩。

最后还要注意的是,差分编码处理的是每一个的像素点中每条颜色通道的值,R(红)、G(绿)、B(蓝)、A(透明)四个颜色通道的值分别进行处理。

压缩(Compression)

压缩阶段会将预处理阶段得到的结果进行Deflate压缩,它由 Huffman 编码 和 LZ77压缩构成。

如前面所说,Deflate压缩会标记图片所有的重复数据,并记录数据特征和结构,会得到一个压缩比最大的png图片 编码数据。

Deflate是一种压缩数据流的算法. 任何需要流式压缩的地方都可以用。

还有就是我们前面说过,一个png图片,是由很多的数据块构成的,但是数据块里面的一些信息其实是没有用的,比如用Photoshop保存了一张png图片,图片里就会有一个区块记录“这张图片是由photshop创建的”,很多类似这些信息都是无用的,如果用photoshop的“导出web格式”就能去掉这些无用信息。导出web格式前后对比效果如下图所示:

image

可以看到,导出web格式,去除了很多无用信息后,图片明显小了很多。

结语

以上就是我对png的理解了,写的不好,就像一个支离破碎的中老年,杂乱无章。

想起那年跟初恋分手的原因 是因为怕影响到学习。。。可是分开后成绩也还是很烂,不仅错过了女神,而且到现在也依然一事无成。

如今中年已至,身上背负着巨大的房贷,家里还有嗷嗷待哺的孩子,看着身旁呼噜声轰天熟睡中的妻子,突然也就想开了。

就像鲁迅说的:

“爱情就像在海滩上捡贝壳,不要捡最大的, 也不要捡最漂亮的,要捡就捡自己最喜欢的, 最重要的是捡到了自己喜欢的 就永远不要再去海边了。”

。。。。。。

凌晨四点写完文章 不知不觉睡着了

梦回到十年前的那个夏天 我们都笑的很甜

看着你哭泣的脸 微笑着对我说再见

再见

第 5 题:new操作符都做了什么

四大步骤:

1、创建一个空对象,并且 this 变量引用该对象,// lat target = {};

2、继承了函数的原型。// target.proto = func.prototype;

3、属性和方法被加入到 this 引用的对象中。并执行了该函数func// func.call(target);

4、新创建的对象由 this 所引用,并且最后隐式的返回 this 。// 如果func.call(target)返回的res是个对象或者function 就返回它

function new(func) {
	lat target = {};
	target.__proto__ = func.prototype;
	let res = func.call(target);
	if (typeof(res) == "object" || typeof(res) == "function") {
		return res;
	}
	return target;
}

小蝌蚪传记:git时光穿梭机--女神的侧颜

背景

狗蛋年近三十,被老母亲逼着跟隔壁村大花成亲
狗蛋厌倦了种田,觉得自己的人生要自己决定
于是在某大型婚恋平台上约了个妹纸
狗蛋感觉有诈 ,于是叫我今晚陪他一起去面基

到了约定的饭店后 我们都震惊了
见到妹纸的一瞬间 我们俩全部都沦陷
宇宙创世的光芒 冲击着我们的天盖骨

由于狗蛋从小到大都在敲代码 没见女人
狗蛋的额头冒出了豆大般的汗珠子
腿就像烧柴油的马达 在桌子下狂抖
女神问:“嗯?地震了?”
我急忙用牙签插进了狗蛋的腿
微笑说:“女神您好 那是我见了您后的心跳声 嘻嘻”
说完我看向狗蛋 他双手紧握狗腿
额头上冒出了土豆般大的汗珠子

稳定住局面后 我快速对女神进行多维度剖析:
通过音色得知她是南方人
身高168cm 净重96斤
C杯 不能再多
gc包包 + lv皮带 + Burberry围巾
根据她腮红的渐变色和眼影描边的手法 得出结论
她是杀手 。。。
我们的心理防线再一次沦陷

没想到今天会有这样的艳遇
后悔自己穿拖鞋来了
应该把家里的阿迪王运动鞋拿出来
我是个高傲的屌丝 气质上既然不能赢
那至少要做到分庭抗礼、势均力敌

恢复意识以后 叫了服务员过来点菜
女神优雅娴熟地说:
“一份意大利进口菲力牛排 7分熟
一杯卡布奇诺咖啡 微糖”
服务员问:“两位先生想要点些什么”
狗蛋因为从小吃惯了食堂大锅菜 于是脱口而出:
“给俺俩来一盆醋溜土豆丝 再加两斤米饭”
看着服务员脸上扭曲的表情
现场一度陷入死寂

好吧 局面既然失控 那我就破罐破摔吧
我把手伸到桌子下 用手势暗语告诉他
现在到了相亲的第二阶段 可以开始撩妹了
出招吧狗蛋~

狗蛋接到指示后 整理了一下衣角
露出了邪魅的笑容说:“女神 您有狐臭吗?”
女神停止了正在咀嚼的嘴 说:“没有”
狗蛋继续邪魅的说:“既然妳没有狐臭 那为什么妳像狐狸一样迷人?”
。。。。
听到这句话以后 我天盖骨就好像被雷劈中一样
这尼玛到底是去哪学的土味撩妹情话啊
整个现场近乎崩坏 优雅的氛围分崩离析
邻桌的顾客全部离场

狗蛋看到我脸上的表情 也很惭愧 无助 不知道自己说错了什么
我绝望的把手伸到桌子下 用手势暗语告诉他:
“狗蛋 你还是回家敲代码吧 谈什么激八恋爱 坑死爹啊”
狗蛋惭愧的用手势回复:
“小蝌蚪 我决定了 还是回村里娶大花吧
我回公司加班了 这里收场就交给你了 对不起 老哥”
我用手势安慰他:“么么 抱抱”

女神从惊慌失措中回过神来 问我:“小蝌蚪 狗蛋他为什么走了”
我随意敷衍说:“妳太美 他觉得自己不配”
女神低下头 委屈的说:“哦。。。对不起”
我:“今后你可以打扮的丑一点 要不然会吓跑我们这些中低端屌丝”
女神叹了口气 说:“没想到太美也是一种错”
。。。。。
说实话 要是别人说出这句话 我可能会上去劈他
但女神说出这句话 完全看不出来她在装逼 非常的自然而真实

我调整了一下自己的心绪 严肃的和她说:“既然今天相亲失败了 咱们好聚好散 吃完我回去加班了”
女神疑惑的问:“这么赶回去上班 小蝌蚪你是做什么的 ”
我:“我是一名低级前端切菜工程尸”
女神:“你每个月收入多少”
我:“加上我老婆的工资 每个月收入是负200块钱”
女神:“你开的什么车”
我指了指窗外那辆被抹去二维码的ofo单车
女神:“在北京有房吗”
我:“我在北京六环外租了个8平米的小单间”

说着说着连我自己都心酸了
一把年纪了 自己仍是一个在底层打工的低配屌丝

女神俏皮可爱的说:“小蝌蚪 不瞒您说 其实我也是一个前端工程师”
我菊紧了一下:“那我考考妳 前端安全需要注意哪几方面问题?”
女神说:“xss、csrf、arp、xff、中间人攻击、运营商劫持、防暴刷”
握草,女神可以啊,xff这么冷门的安全问题都能知道,不简单。

女神:“小蝌蚪 那我出个题考考您”
我这钢铁直男的战斗欲立马就起来了。
你要说长相,我比不过别人,但是你要聊技术,
凭借爸爸多年在大厂的搬砖工经验
小妹妹,接受爸爸的惩罚吧~

女神:“那我问了啊,一段字符串'i love you',用sha256不可逆算法加密,将得到的值传入傅立叶变化函数,最后再用<拉格朗日定理>和<夹逼定理>进行一次求导。”
女神突然加强声音:“那么请问,小蝌蚪,你的手机号码和微信号码分别是多少?”
问完后,女神脸色泛红,娇羞的低下了头。

我一脸懵逼,全程只听到了“拉格朗日”和“夹逼定理”这两个词,这都是什么鬼。
我偷偷用手机查了一下“夹逼定理”
image
这是什么鬼?太难了吧,这题老子不会啊
当了三十年的屌丝
被人骂被人打我都没觉得什么,
但这一次,钢铁直男的自尊心受到了戳伤。
我陷入了深深的苦恼。

女神很失望的看着我 说:“这么简单的题目,不会?”
握草,听到女神说这“拉格朗日 + 夹逼定理”的题目简单,
等于是在伤口上撒盐巴,简直是残暴的鞭尸行为。

女神看到我一脸沮丧的样子 变得越发的嫌弃和浮躁 说:“小蝌蚪 看不出来 你果然很屌丝啊 之前我以为你有一颗不一样的灵魂 看来你是真屌丝”
我继续沉浸在沮丧和自责中
女神又说:“屌丝蝌蚪 我最后给你一次机会 问你一个简单的技术问题 好好回答 给我一个看得起你的理由”

这次装逼不成被反杀,真的很耻辱
没想到女神又给了一次机会
我瞬间挺直腰杆,破涕为笑:“女神 请出题”

git reflog 时光穿梭机

问题描述

女神说:“我们公司新来了一个前端小白,她对git不熟悉,辛辛苦苦加班一星期 翘的代码没了。”
我:“噢?怎么没了”
女神:“在终端输入git log,列出所有的commit信息,如下图:”
image
女神:“commit的信息很简单,就是做了6个功能,每个功能对应一个commit的提交,分别是feature-1 到 feature-6”
我:“好的 然后呢”
女神:“然后前端小白坑爹了,执行了强制回滚,如下:”

git reset --hard 2216d4e

女神:“小白回滚到了feature-1上,并且回滚的时候加了--hard,导致之前feature-2 到 feature-6的所有代码全部弄丢了,现在git log的显示如下:”
image
女神:“现在feature-2 到 feature-6的代码没了”
女神:“小白还在这个基础上新添加了一个commit提交,信息叫feature-7,如下图:”
image
女神:“现在feature-2 到 feature-6全没了,还多了一个feature-7”
女神:“那么小蝌蚪 请问 如何把丢失的代码feature-2 到 feature-6全部恢复回来,并且feature-7的代码也要保留”
女神:“屌丝蝌蚪,开始你的表演”
我的笑容逐渐猖狂:“啊哈哈哈!这题我会!让爸爸教你”

解答

这个问题是一个很经典很经典的git问题,基本上,每次部门有人来面试前端,只要他在简历上写“精通git”,我都会问这个问题,基本上90%的人答不出来。
其实用git reflog和git cherry-pick就能解决。
基本上掌握了git reflog和git cherry-pick,你的git命令行操作就算是成功入门了。
来,接下来爸爸就一一讲解如何操作。
你只需要在终端里输入:

git reflog

然后就会展示出所有你之前git操作,你以前所有的操作都被git记录了下来,如下图:
image
这时候要记好两个值:4c97ff3和cd52afc,他们分别是feature-7和feature-6的hash码。然后执行回滚,回到feature-6上:

git reset --hard cd52afc

现在我们回到了feature-6上,如下图:
image
好的,我们回到了feature-6上,但是feature-7没了,如何加上来呢?这个时候就用上了git cherry-pick,刚刚我们知道了feature-7的hash码为4c97ff3,操作如下:

git cherry-pick 4c97ff3

输入好了以后,你的feature-7的代码就回来了。期间可能会有一些冲突,按照提示解决就好。最后的结果如下图:
image
是不是很简单,feature-1 到 feature-7的代码就合并到了一起,以前的代码也都回来了。
说到这里,我看到女神脸上露出了满意的笑容。

结局

给女神讲解完git的操作技巧 一转眼已经晚上十一点多
我说:“女神 天色不早了 ”
女神妩媚的说:“小蝌蚪 有一些技术原理我还没有很明白 今晚你能否到我家来 继续探讨一下?”
女神又说:“如果你来我家的话 我就告诉你<拉格朗日>和<夹逼定理>的最终奥义”

我顿时陷入了沉思
其实 根据今晚女神的表现 眼神和肢体上的交流
我已经非常明白女神的用意了

用鲁迅说的一句话来描述就是:
“妳懂我的故作矜持
我懂妳的图谋不轨
我们互相之间不拆穿
这种感觉 最美”

不得不承认 暧昧上头那几秒 像极了爱情
但是
如果和女神上楼 对不起老婆
不去 对不起自己

这时突然想起家里熟睡的妻子
她虽然对我很凶 睡觉还打呼噜
但是她陪我一起在北京打拼 不离不弃
想到这里我已经热泪盈眶
于是转向女神,坚定不移、斩钉截铁的对女神说:“好呀好呀 我和妳回家”

到了女神家 她褪去了外衣
昏暗的灯光下 她的发丝蔓延到我的全身
侵蚀着心智

鲁迅说的对:“抵御诱惑最好的方法就是向诱惑屈服”

随着信仰的破碎 我放弃了抵抗
全身处于一种酥麻的状态
至此赌徒已经是在和魔鬼交易 出卖的是自己的灵魂

就在所有人以为要进入主题的时候
女神突然坐直 认真严肃地说:“小蝌蚪 事已至此 我需要跟你坦白一件事”

坦白???难道女神要跟我坦白她是个魔鬼??
女神:“对不起 其实我不是什么女神 我是xxx公司的高级猎头”
我虎躯一震:“what?”

女神:“小蝌蚪,最近您有跳槽的需求吗?我这里有阿里xx、头x、蚂蚁xx的高级前端岗急缺人才 经过今晚对您的测试 我觉得你可以试一下”

我一脸懵逼 心里的热血瞬间冻结 听到了内心深处崩裂的声音
猎头继续挑逗的说:“如果你主动 我们今晚就会有故事”
说实话 当一个屌丝对妳失去了信任 长得再美 再诱人的挑逗 都只是对牛弹琴
我很冷漠地说:“没有意愿 再见”

原来今晚的这一切都骗局只是个骗局
也许看到我这张猥琐脸 女神内心已经呕吐了无数次
却还要假装一副喜欢的样子
妳可以玩弄我的感情 但是暴虐我的灵魂 这就是妳的不对了

当了三十年的舔狗 女神对我来说只是海市蜃楼
屌丝跪舔的背后只是一颗枯竭的内心
我输了
结婚十年来第一次感觉到失恋
但还是谢谢妳 给了我一个不一样的夜晚

凌晨一点半 踩着被抹去二维码得ofo小黄车
独自骑向北六环
在空无一人的街道上
放着周杰伦的《一路向北》:

街景一直在后退
崩溃在窗外零碎
我一路向北,离开有妳的季节
我好累 已无法再爱上谁

完。

第 7 题:手写代码,简单实现apply

Function.prototype.apply2 = function(context, arr) {
    var context = context || window; //因为传进来的context有可能是null
    context.fn = this;
    var args = [];
    var params = arr || [];
    for (var i = 0; i < params.length; i++) {
        args.push("params[" + i + "]"); //不这么做的话 字符串的引号会被自动去掉 变成了变量 导致报错
    }
    args = args.join(",");

    var result = eval("context.fn(" + args + ")"); //相当于执行了context.fn(arguments[1], arguments[2]);

    delete context.fn;
    return result; //因为有可能this函数会有返回值return
}

第 15 题:1000-div问题

  • 一次性插入1000个div,如何优化插入的性能
    • 使用Fragment
    var fragment = document.createDocumentFragment();
    fragment.appendChild(elem);
  • 向1000个并排的div元素中,插入一个平级的div元素,如何优化插入的性能
    • 先display:none 然后插入 再display:block
    • 赋予key,然后使用virtual-dom,先render,然后diff,最后patch
    • 脱离文档流,用GPU去渲染,开启硬件加速

第 11 题:如何劫持https的请求,提供思路

很多人在google上搜索“前端面试 + https详解”,把答案倒背如流,但是问到如何劫持https请求的时候就一脸懵逼,是因为还是停留在https理论性阶段。

想告诉大家的是,就算是https,也不是绝对的安全,以下提供一个本地劫持https请求的简单思路。

模拟中间人攻击,以百度为例

  • 先用OpenSSL查看下证书,直接调用openssl库识别目标服务器支持的SSL/TLS cipher suite
    openssl s_client -connect www.baidu.com:443
  • 用sslcan识别ssl配置错误,过期协议,过时cipher suite和hash算法
    sslscan -tlsall www.baidu.com:443
  • 分析证书详细数据
    sslscan -show-certificate --no-ciphersuites www.baidu.com:443
  • 生成一个证书
    openssl req -new -x509 -days 1096 -key ca.key -out ca.crt
  • 开启路由功能
    sysctl -w net.ipv4.ip_forward=1
  • 写转发规则,将80、443端口进行转发给8080和8443端口
    iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 
    iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443
  • 最后使用arpspoof进行arp欺骗

English please

What is the problem with Chinese README's

Firstly, we congratulate you for getting so much star by sharing this repository with humanity.

But it is very disappointing for non-Chinese speakers when one couldn't understand what a trending repository is about.

When we see such a repo on trending, our minds are blurring like Gollum's.

Gollum Image

There is a way you can help to solve this disappointment which I believe is experienced by many people who want to know more about your valuable work and appreciate it.

What we want:

  • Please add English translation of your README so you are sharing your work and knowledge with more people.

How this will help you:

  • More feedback to fix and improve your project.
  • New ideas about your project.
  • Greater fame.
  • SungerBob Image

“Sharing knowledge is the most fundamental act of friendship. Because it is a way you can give something without loosing something.”

— Richard Stallman

Thank you!

This issue created by us/english-please script. Please report on any error. Thank you!

第 4 题:如何遍历一个dom树

function traversal(node) {
    //对node的处理
    if (node && node.nodeType === 1) {
        console.log(node.tagName);
    }
    var i = 0,
        childNodes = node.childNodes,
        item;
    for (; i < childNodes.length; i++) {
        item = childNodes[i];
        if (item.nodeType === 1) {
            //递归先序遍历子节点
            traversal(item);
        }
    }
}

第 18 题:(开放题)100亿排序问题:内存不足,一次只允许你装载和操作1亿条数据,如何对100亿条数据进行排序

这题是考察算法和实际问题结合的一个问题

很多场景的数据量都是百亿甚至千亿级别。

那么如何对这些数据进行高效的操作呢,可以通过这题考察出来。

以前老听说很多人问,前端学算法没有用,考算法都是垃圾,面不出候选人的能力

其实。。。老哥实话告诉你,当你在做前端需要用到crc32、并查集、字典树、哈夫曼编码、LZ77之类东西的时候

已经是涉及到框架实现和极致优化层面了

那时你就已经到了另外一个前端高阶境界了

所以不要抵触算法,可能只是我们目前的眼界和能力,还没触及到那个层级

我前面已经公布了两道开放题的答案了,相信大家已经有所参悟。我觉得在思考开放题的过程中,会有很多意想不到的成长,所以我建议这道题大家可以尝试自己思考一下。
本题答案会在周五公布

第 10 题:简单手写实现promise

       // 简易版本的promise 
        // 第一步: 列出三大块  this.then   resolve/reject   fn(resolve,reject)
        // 第二步: this.then负责注册所有的函数   resolve/reject负责执行所有的函数 
        // 第三步: 在resolve/reject里面要加上setTimeout  防止还没进行then注册 就直接执行resolve了
        // 第四步: resolve/reject里面要返回this  这样就可以链式调用了
        // 第五步: 三个状态的管理 pending fulfilled rejected
     
        // *****promise的链式调用 在then里面return一个promise 这样才能then里面加上异步函数
        // 加上了catch
        function PromiseM(fn) {
            var value = null;
            var callbacks = [];
            //加入状态 为了解决在Promise异步操作成功之后调用的then注册的回调不会执行的问题
            var state = 'pending';
            var _this = this;

            //注册所有的回调函数
            this.then = function (fulfilled, rejected) {
                //如果想链式promise 那就要在这边return一个new Promise
                return new PromiseM(function (resolv, rejec) {
                    //异常处理
                    try {
                        if (state == 'pending') {
                            callbacks.push(fulfilled);
                            //实现链式调用
                            return;
                        }
                        if (state == 'fulfilled') {
                            var data = fulfilled(value);
                            //为了能让两个promise连接起来
                            resolv(data);
                            return;
                        }
                        if (state == 'rejected') {
                            var data = rejected(value);
                            //为了能让两个promise连接起来
                            resolv(data);
                            return;
                        }
                    } catch (e) {
                        _this.catch(e);
                    }
                });
            }

            //执行所有的回调函数
            function resolve(valueNew) {
                value = valueNew;
                state = 'fulfilled';
                execute();
            }

            //执行所有的回调函数
            function reject(valueNew) {
                value = valueNew;
                state = 'rejected';
                execute();
            }

            function execute() {
                //加入延时机制 防止promise里面有同步函数 导致resolve先执行 then还没注册上函数
                setTimeout(function () {
                    callbacks.forEach(function (cb) {
                        value = cb(value);
                    });
                }, 0);
            }

            this.catch = function (e) {
                console.log(JSON.stringify(e));
            }

            //经典 实现异步回调
            fn(resolve, reject);
        }

第 19 题:(开放题)a.b.c.d和a['b']['c']['d'],哪个性能更高

别看这题,题目上每个字都能看懂,但是里面涉及到的知识,暗藏杀鸡

这题要往深处走,会涉及ast抽象语法树、编译原理、v8内核对原生js实现问题

我觉得这个题是这篇文章里最难的一道题,所以我放在了开放题中的最后一题

大家多多思考,这题的答案也会在周五公

第 6 题:手写代码,简单实现call

Function.prototype.call2 = function(context) {
    var context = context || window; //因为传进来的context有可能是null
    context.fn = this;
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i + "]"); //不这么做的话 字符串的引号会被自动去掉 变成了变量 导致报错
    }
    args = args.join(",");

    var result = eval("context.fn(" + args + ")"); //相当于执行了context.fn(arguments[1], arguments[2]);

    delete context.fn;
    return result; //因为有可能this函数会有返回值return
}

webpack工程化打包原理解析与实现

本文主要讲解了目前最流行的前端 webpack 工程化打包工具的内部打包原理和如何实现各个模块之间的依赖分析和代码注入,然后手把手教大家亲自实现一个简易的 webpack 打包工具,帮助大家学习和更深层次的理解 webpack 和前端界的工程化工具打包实现原理。

背景

       随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的grunt,gulp。到后来的 webpack 和 Parcel 。但是目前很多脚手架工具,比如 vue-cli 已经帮我们集成了一些构建工具的使用。有的时候我们可能并不知道其内部的实现原理。其实了解这些工具的工作方式可以帮助我们更好理解和使用这些工具,也方便我们在项目开发中应用。

       弄清楚打包工具的背后原理,有利于我们实现各种神奇的自动化、工程化东西,比如自创 JavaScript 语法,又如蚂蚁金服 ant 中大名鼎鼎的 import 插件,甚至是前端文件自动扫描载入等,能够极大的提升我们工作效率。

一些基础知识

AST抽象语法树

       什么是 AST 抽象语法树?在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个细节。例如下面这行代码:

const answer = 8 * 9;

转化成 AST 抽象语法树后,是如下这个样子的:

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "answer"
                    },
                    "init": {
                        "type": "BinaryExpression",
                        "operator": "*",
                        "left": {
                            "type": "Literal",
                            "value": 8,
                            "raw": "8"
                        },
                        "right": {
                            "type": "Literal",
                            "value": 9,
                            "raw": "9"
                        }
                    }
                }
            ],
            "kind": "const"
        }
    ],
    "sourceType": "script"
}

       大家可以通过Esprima 这个网站来将代码转化成 ast。首先一段代码转化成的抽象语法树是一个对象,该对象会有一个顶级的 type 属性 Program ,第二个属性是 body 是一个数组。body 数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息

type:描述该语句的类型 --变量声明语句
kind:变量声明的关键字 -- const
declaration: 声明的内容数组,里面的每一项也是一个对象
    type: 描述该语句的类型 
    id: 描述变量名称的对象
        type:定义
        name: 是变量的名字
        init: 初始化变量值得对象
        type: 类型
        value:  "is tree" 不带引号
        row: "\"is tree"\" 带引号

打包原理的代码实现

       有了上面这些基础的知识,我们开始亲自实现一个简易版本的 webpack 打包工具,代码仓库地址放在里gitlab上.

首先我们定义3个文件:

// entry.js
import message from './message.js';
console.log(message);

// message.js
import {name} from './name.js';
export default `hello ${name}!`;

// name.js
export const name = 'world';

当我们实现了简易版本的 webpack 后,运行 entry.js 会输出 “hello world” 。

接下来我们参照笔者放在gitlab上实现的简易版源码,进行一一解析。我们创建 /src/minipack.js 文件,在顶部插入如下代码:

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');

这是实现打包原理所需要的核心依赖,我们先考虑一下一个基础的打包编译工具可以做什么?

转换 ES6 语法成 ES5
处理模块加载依赖
生成一个可以在浏览器加载执行的 js 文件
       第一个问题,转换语法,其实我们可以通过babel来做。核心步骤就是通过 babylon 生成 AST ,通过 babel-core 的 transformFromAst 方法将 AST 重新生成源码,traverse 的作用就是帮助开发者遍历 AST 抽象语法树,帮助我们获取树节点上的需要的信息和属性。接下来看:

let ID = 0;
       全局的自增 id,记录每一个载入的模块的 id,我们将所有的模块都用唯一标识符进行标示,因此自增 id 是最有效也是最直观的,有多少个模块,一统计就出来了。

然后创建createAsset函数:

function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });
  const dependencies = [];
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  });
  const id = ID++;
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });
  const customCode = loader(filename, code)
  return {
    id,
    filename,
    dependencies,
    code,
  };
}

       我们对每一个文件进行处理。因为这只是一个简单版本的 bundler,因此,我们并不考虑如何去解析 css、md、txt 等等之类的格式,我们专心处理好 js 文件的打包,因为对于其他文件而言,处理起来过程不太一样,用文件后缀很容易将他们区分进行不同的处理,在这个版本,我们还是专注 js代码,createAsset 函数第一行:

const content = fs.readFileSync(filename, 'utf-8');

函数注入一个 filename 顾名思义,就是文件名,读取其的文件文本内容。

const ast = babylon.parse(content, {
    sourceType: 'module',
  });

       首先,我们使用 babylon 的 parse 方法去转换我们的原始代码,通过转换以后,我们的代码变成了抽象语法树( AST ),你可以通过 https://astexplorer.net/ 这个可视化的网站,看看 AST 生成的是什么。

  const dependencies = [];
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  });

       当我们解析完以后,我们就可以提取当前文件中的 dependencies,dependencies 翻译为依赖,也就是我们文件中所有的 import xxxx from xxxx,我们将这些依赖都放在 dependencies 的数组里面,之后统一进行导出,traverse 函数是一个遍历 AST 的方法,由 babel-traverse 提供,他的遍历模式是经典的 visitor 模式,visitor 模式就是定义一系列的 visitor ,当碰到 AST 的 type === visitor 名字时,就会进入这个 visitor 的函数,类型为 ImportDeclaration 的 AST 节点,其实就是我们的 import xxx from xxxx,其中 path.node.source.value 的值,就是我们 import from xxxx 中的地址,将地址 push 到 dependencies 中。

  const id = ID++;
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });

       当我们完成依赖的收集以后,我们就可以把我们的代码从 AST 转换成 CommenJS 的代码,这样子兼容性更高更好,并且将 ID 自增。

const customCode = loader(filename, code)
function loader(filename, code) {
  if (/entry/.test(filename)) {
    console.log('this is loader ')
  }
  return code
} 

       还记得我们的 webpack-loader 系统吗?具体实现就是在这里可以实现,通过将文件名和代码都传入 loader 中,进行判断,甚至用户定义行为再进行转换,就可以实现 loader 的机制,当然,我们在这里,就做一个弱智版的 loader 就可以了,parcel 在这里的优化技巧是很有意思的,在 webpack 中,我们每一个 loader 之间传递的是转换好的代码,而不是 AST,那么我们必须要在每一个 loader 进行 code -> AST 的转换,这样时非常耗时的,parcel 的做法其实就是将 AST 直接传递,而不是转换好的代码,这样,速度就快起来了。
       接下来,我们对模块进行更高级的处理。我们之前已经写了一个 createAsset 函数,那么现在我们要来写一个 createGraph 函数,我们将所有文件模块组成的集合叫做 queue ,用于描述我们这个项目的所有的依赖关系,createGraph 从 entry (入口) 出发,一直到打包完所有的文件为止。createGraph 函数如下:

function createGraph(entry) {
  const mainAsset = createAsset(entry);
  const queue = [mainAsset];
  for (const asset of queue) {
    asset.mapping = {};
    const dirname = path.dirname(asset.filename);
    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      queue.push(child);
    });
  }
  return queue;
}

先看前面两行代码:

const mainAsset = createAsset(entry);
const queue = [mainAsset];

从 entry 出发,首先收集 entry 文件的依赖,queue 其实是一个数组,我们将最开始的入口模块放在最开头。

for (const asset of queue) {
    asset.mapping = {};
    //从asset中获取文件对应的文件夹
    const dirname = path.dirname(asset.filename);
    //... ...
  }

       在这里我们使用 for of 循环而不是 foreach ,原因是因为我们在循环之中会不断的向queue 中,push 进东西,queue 会不断增加,用 for of 会一直持续这个循环直到 queue 不会再被推进去东西,这就意味着,所有的依赖已经解析完毕,queue 数组数量不会继续增加,但是用 foreach 是不行的,只会遍历一次, asset 代表解析好的模块,里面有 filename,code,dependencies 等东西,asset.mapping 是一个不太好理解的概念,我们每一个文件都会进行 import 操作,import 操作在之后会被转换成 require ,每一个文件中的 require 的 path 其实会对应一个数字自增 id ,这个自增 id 其实就是我们一开始的时候设置的 id ,我们通过将 path-id 利用键值对,对应起来,之后我们在文件中 require 就能够轻松的找到文件的代码,解释这么啰嗦的原因是往往模块之间的引用是错中复杂的,这恰巧是这个概念难以解释的原因。

    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      queue.push(child);
    });

       每个文件都会被 parse 出一个 dependencise,他是一个数组,在之前的函数中已经讲到,因此,我们要遍历这个数组,将有用的信息全部取出来,值得关注的是 asset.mapping[dependencyPath] = denpendencyAsset.id 操作。absolutePath 获取文件中模块的绝对路径,比如 import ABC from './world',会转换成 /User/xxxx/desktop/xproject/world 这样的形式。

asset.mapping[relativePath] = child.id;

       这里是重要的点,我们解析每解析一个模块,我们就将他记录在这个文件模块 asset 下的 mapping 中,之后我们 require 的时候,能够通过这个 id 值,找到这个模块对应的代码,并进行运行,将解析的模块推入 queue 中去。最后我们得到的 queue 是如下这个样子的:

  [ 
    { id: 0,
      filename: './example/entry.js',
      dependencies: [ './message.js' ],
      code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
      mapping: { './message.js': 1 } },
  
    { id: 1,
      filename: 'example/message.js',
      dependencies: [ './name.js' ],
      code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "hello " + _name.name + "!";',
      mapping: { './name.js': 2 } },
  
    { id: 2,
      filename: 'example/name.js',
      dependencies: [],
      code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
      mapping: {} } 
    
  ]

       接下来我们创建 bundle 函数,我们通过 createGraph 完成了 queue 的收集,那么就到我们真正的代码打包了,这个 bundle 函数使用了大量的字符串处理,你们不要觉得奇怪,为什么代码和字符串可以混起来写,如果你跳出写代码的范畴,看我们的代码,实际上,代码就是字符串,只不过他通过特殊的语言形式组织起来而已,对于脚本语言 JS 来说,字符串拼接成代码,然后跑起来,这种操作在前端非常的常见,我认为,这种思维的转换,是拥有自动化、工程化的第一步。

function bundle(graph) {
  let modules = '';
  graph.forEach(mod => {
    modules += `${mod.id}: [
      function (require, module, exports) {
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });
  // ... ...
  return result;
}

       我们的 modules 就是一个字符串,我们将 graph 中所有的 asset 取出来,然后使用 node.js 制造模块的方法来将一份代码包起来。我们将转换好的源码,放进一个 function(require,module,exports){} 函数中,这个函数的参数就是我们随处可用的 require,module,以及 exports,这就是为什么我们可以随处使用这三个玩意的原因,因为我们每一个文件的代码终将被这样一个函数包裹起来,不过这段代码中比较奇怪的是,我们将代码封装成了 1:[...],2:[...]的形式,我们在最后导入模块的时候,会为这个字符串加上一个 {},变成 {1:[...],2:[...]},你没看错,这是一个对象,这个对象里用数字作为 key ,一个二维元组作为值,[0] 第一个就是我们被包裹的代码,[1] 第二个就是我们的 mapping 。

function bundle(graph) {
  //... ...
  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports : {} };

        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `;
  return result;
}

       这一段代码实际上才是模块引入的核心逻辑,我们制造一个顶层的 require 函数,这个函数接收一个 id 作为值,并且返回一个全新的 module 对象,我们倒入我们刚刚制作好的模块,给他加上 {},使其成为 {1:[...],2:[...]} 这样一个完整的形式,然后塞入我们的立即执行函数中(function(modules) {...})(),在 (function(modules) {...})() 中,我们先调用 require(0),理由很简单,因为我们的主模块永远是排在第一位的,紧接着,在我们的 require 函数中,我们拿到外部传进来的 modules,利用我们一直在说的全局数字 id 获取我们的模块,每个模块获取出来的就是一个二维元组,然后,我们要制造一个 子require,这么做的原因是我们在文件中使用 require 时,我们一般 require 的是地址,而顶层的 require 函数参数时 id,不要担心,我们之前的 mapping 在这里就用上了,通过用户 require 进来的地址,在 mapping 中找到 id,然后递归调用 require(id),就能够实现模块的自动倒入了,接下来制造一个 const newModule = {exports: {}};,运行我们的函数 fn(childRequire, newModule, newModule.exports);,将应该丢进去的丢进去,最后 return newModule.exports 这个模块的 exports 对象。

const graph = createGraph('./example/entry.js');
const result = bundle(graph);
console.log(result);

       最后,运行我们开头的demo例子三个文件的代码,node src/minipack.js,就能看到所有编译好的代码,将代码复制出来放到chrome浏览器的console里,就能看到"hello world"的输出。

结尾

       目前为止,我们已经实现了一个简易版本的 webpack 了,我们将 es6 的代码通过 entry.js 入口文件开始编译,然后根据循环递归调用把所有文件所依赖的文件都解析和加载了一遍,理解了这个简易版本的 webpack 实现原理,再去看看其他的打包工具原理,大同小异,你就会发现一切都豁然开朗,无非就是在这个简易版本的 webpack 基础上加了很多工具函数和细节的处理,限于篇幅笔者就不再一一介绍了。

        本文实现的简易版本的 webpack 的完整代码gitlab地址为:minipack

第 14 题:简单实现async/await中的async函数

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里

function spawn(genF) {
    return new Promise(function(resolve, reject) {
        const gen = genF();
        function step(nextF) {
            let next;
            try {
                next = nextF();
            } catch (e) {
                return reject(e);
            }
            if (next.done) {
                return resolve(next.value);
            }
            Promise.resolve(next.value).then(
                function(v) {
                    step(function() {
                        return gen.next(v);
                    });
                },
                function(e) {
                    step(function() {
                        return gen.throw(e);
                    });
                }
            );
        }
        step(function() {
            return gen.next(undefined);
        });
    });
}

小蝌蚪传记:js、css、html压缩与混淆汇总 --> 变弯记

《小蝌蚪传记》

小蝌蚪,男,低级前端工程师

单身三十年,热爱用双手解决问题

白天用双手敲代码

晚上用双手做一些正能量的事情

双手终于不堪重负,于是去医院检查

医生问:你有女朋友吗

小蝌蚪伸出双手:这就是

医生叹气说:你需要找到一个真正的女朋友,减少不必要的重复劳动,解放双手

小蝌蚪恍然大悟

于是下载某同城交友软件

约了两个妹纸见面

第一个妹纸

网名叫:水冰月

个人简介:“洳淉芣嬡莪,僦芣婹傷嗐莪”

传说她是葬爱家族大公爵前女友

受了男人的伤,从此封心不再爱

如今想找个平凡人,度过平凡的下半生

她的头像是一位绝美的忧郁女神

无敌侧颜,万千少男的梦想

可是见面的时候,小蝌蚪被她的巨颜童乳惊呆

300多斤的水冰月,大象腿

一副被沙枪打过的脸蛋

穿着美少女战士的jk装

半露着肥大的肚皮

小蝌蚪:您是“水冰月”?

水冰月:正是

小蝌蚪:妳是美少女壮士吧

水冰月:小哥哥好幽默

小蝌蚪点开她的头像,反复确认:阿姨,您别闹好么

水冰月打开美颜相机,调成十级美颜:看!多美啊,这就是我,葬爱家族第一女皇,唯一的真神

小蝌蚪万念俱灰,双眼迷离

小蝌蚪强忍镇静:妳当初为何离开葬爱家族

水冰月:因为家族大公爵秃顶了,头发象征着权利,我的信仰崩塌了

小蝌蚪:那你接下来什么打算

水冰月:我要找到一个男士,和他一起传承我优秀的基因

然后邪魅的看向小蝌蚪

小蝌蚪感觉不妙,想要逃

被水冰月蠕动的肚皮,si亡缠绕

慌乱之中小蝌蚪露出了胸前小猪佩奇纹身

水冰月大惊失色,小蝌蚪跳窗逃亡

小蝌蚪心态大崩,找个女朋友,竟如此凶险

放弃之际,禅师托梦鼓励到:每当你低迷的时候,记住,你曾是第一名的小蝌蚪

小蝌蚪振作精神,又约了一个妹纸

网名叫:“初恋”

见面时,小蝌蚪被她的绝世神颜惊呆。。。

初恋背着lv包包,手戴百达翡丽表

0.618黄金比例身材,发型大波浪

人类高质量女性

所到之处,无数男人侧目

小蝌蚪见状,赶紧涂抹大宝SOD蜜

口服六味地黄丸

五菱红光车钥匙摆桌上

珍藏版红星尔克男鞋擦的铮亮

开启超级舔狗模式

初恋坐下,深情款款看着小蝌蚪:我最近受了心伤,想找个老实人接盘,请问你是老实人吗

小蝌蚪:国家一级老实人、后厂村天才舔狗、昌平区第一备胎,就是我

初恋:我有100个前男友,脚踏100只船,你还会爱我吗

小蝌蚪:我会将他们一一打败,因为我是第一名的小蝌蚪

初恋:我爱好出轨,喜欢给男朋友戴绿色的帽子

小蝌蚪:宝贝怕我冷,所以P腿给我戴帽子、送温暖,更加爱妳了

初恋:人家今天心情不好,你能哄我开心吗

小蝌蚪表演了一段舌头碎大石

然后用100根胸毛,开启多线程,在桌子上刻了一版react源码

初恋终于笑了:欢迎你走上这条爱我的不归路

小蝌蚪:请妳现在、立刻、马上玩弄我的感情

小蝌蚪完全迷失了心智,被热恋冲晕头脑

就在这时,一位开着兰博基尼的高富帅路过

初恋看了一眼,回头对小蝌蚪说:我们分手吧

小蝌蚪:啊?

初恋:我爱上了别人,再见

小蝌蚪一脸懵逼

初恋起身勾搭高富帅:我最近受了点心伤,请问你是老实人吗

高富帅:请上我的车,我给您展示我是如何老实的

看着兰博基尼远去的尾灯

小蝌蚪在风中凌乱

回到公司,小蝌蚪每天以泪洗面,双目无神、行尸走肉

一位男同事,为了帮助他走出阴影

每天给小蝌蚪端茶倒水吃零食

每晚陪小蝌蚪搬砖加班到深夜

一起讨论react源码、js高级程序设计

一起手牵手,结对编程

人类最高级的精神共鸣,往往只会发生在两个男人不期而遇的怦然心动

不知不觉,小蝌蚪靠在了他的肩

一瞬间,小蝌蚪想通了

找女朋友,是为了寻求心灵的慰藉和生理上的满足

可是女朋友为什么一定要是女的呢

人类的祖先

喜欢大长腿,是因为有利于奔跑

喜欢大粗臀,是因为有利于孕育

喜欢那个大,是因为有利于哺乳

抛开这一切生物本能,而投向男性宽广的怀抱

才是最完美无瑕的爱恋

才是最高级的灵魂契合

小蝌蚪和男同事紧紧相拥

爱抚着他的胸毛

享受着超凡脱俗的灵魂快感

那天晚上,小蝌蚪带他去找了妈妈

他们从此过上了幸福快乐的生活

《js、css、html压缩与混淆汇总》

背景

前段时间针对某件大事件,我们用之前的一个老原生html项目,涂涂改改快速做了一个h5,由于时间紧迫直接上线了

结果没想到。。。。第二天就被某大公司抄袭了。。。调了点颜色、改了些文案就直接抄袭并上线了。。。连我们变量名都没改。。。

后来我们快速对项目代码进行了压缩和混淆,才避免了后续迭代没有被抄袭。。。

整件事说起来又气又搞笑。。。

经过这件事,我汇总了一下js、css、html压缩与混淆,增加页面加载速度的同时,还能防止页面被抄袭

js混淆

js混淆,其实就是将你的js代码弄的晦涩难懂,达到了防抄袭的效果

业界比较常用的是javascript-obfuscator这个库

const JO = require("javascript-obfuscator");
const code = `
        function add(first, second) { return first + second; }; 
        var v = add(1,2); 
        console.log(v);
`;
const result = JO.obfuscate(code,
    {
      compact: false,
      controlFlowFlattening: true,
      controlFlowFlatteningThreshold: 1,
      numbersToExpressions: true,
      simplify: true,
      shuffleStringArray: true,
      splitStrings: true,
      stringArrayThreshold: 1,
    }
  );
console.log("混淆结果:", result.getObfuscatedCode())

这段代码就是将一段简单的加法运算代码进行了混淆,最后结果是

分析一下你会发现,其实多了一个字典,所有方法变量,都有存在字典中,调用时先调用字典还原方法名变量再执行

js压缩

压缩以前用的最多的是uglifyjs,现在用的比较多的是terser

const { minify } = require("terser");
const code = `
        function add(first, second) { return first + second; }; 
        var v = add(1,2); 
        console.log(v);
`;
const result = await minify(code);
console.log("压缩结果:", result.code)

压缩后结果如下:

function add(d,n){return d+n}var v=add(1,2);console.log(v);

将所有参数都变成了一个字符,所有能缩减的空间都去掉了,转化成一行代码,最大限度节省代码体积

css压缩

css压缩,我用的clean-css,当然业界也有很多优秀的css处理插件比如PostCSS,但这里我简单介绍一下clean-css的用法

    const CleanCSS = require('clean-css');
    const input = `
        a { 
            font-weight:bold; 
        }
        .vb {
            border: 1px silid red;
        }
    `;
    const options = { /* options */ };
    const output = new CleanCSS(options).minify(code);
    console.log("压缩结果:", output.styles)

压缩后的结果如下:

a{font-weight:700}.vb{border:1px silid red}

html压缩

现在业界最常用的html压缩插件是html-minifier,功能很强大,还能压缩html中的js和css,直接上代码

我们压缩一段有html、js、css的代码

执行压缩的代码如下:

const htmlMinify = require("html-minifier").minify
const result = htmlMinify(htmlCode, {
        minifyCSS: true,// 压缩css
        minifyJS: true,// 压缩js
        collapseWhitespace: true,// 删除html里的空格 达到html的压缩
        removeAttributeQuotes: true,// 尽可能删除html标签里的双引号 达到html的压缩
        removeComments: true, //删除html中的注释
        removeCommentsFromCDATA: true, //从脚本和样式删除的注释
    });
console.log("压缩结果:", result)

压缩结果如下:

<html><head><style>a{font-weight:700}.vb{border:1px silid red}</style></head><body><div class=foreword>小蝌蚪,嘻嘻</div><script type=text/javascript>function add(d,n){return d+n}var v=add(1,2);console.log(v)</script></body></html>

通过设置对应的配置项minifyCSS和minifyJS,我们直接把html、js、css一起进行了压缩,非常方便

唯一遗憾的是,好像html-minifier不支持js的混淆,所以js的混淆我直接抽出来单独做了

结尾

通过抄袭的这件事,反射出了我们团队上线流程的不规范

但对于被其他团队抄袭这种行为,也只能在这里道德谴责一下,发发牢*

就当是吃一堑长一智吧,嘻嘻

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.