tuobaye0711

tuobaye0711 查看完整档案

南京编辑南京邮电大学  |  通信工程 编辑字节跳动  |  前端开发 编辑 tuobaye.com 编辑
编辑

hello segmentfault

个人动态

tuobaye0711 收藏了文章 · 2020-09-07

前端性能优化:当页面渲染遇上边缘计算

简介: 当前几种常见的前端性能优化方案仍然不可避免地会存在一些缺点。本文在 ESI (Edge Side Include) 的基础上,提出了一种新的优化思路:边缘流式渲染方案(ESR),即借助 CDN 的边缘计算能力,将静态内容与动态内容以流式的方式,先后返回给用户。(文末福利:下载《覆盖全端业务的大前端技术》电子书)

image.png

背景

对于 web 页面来说,首跳场景(例如 SEO、付费引流)的性能普遍比二跳场景下要差。原因有多种,主要是首跳用户在连接复用,和本地资源缓存利用方面,有很大的劣势。首跳场景下,很多在端上的优化手段(预加载,预执行,预渲染等)无法实施。

在客户端缓存能力无法利用的情况下,利用 cdn 距离用户近的特性,可以结合缓存做一些性能优化。

思路

思路 1:SSR

为了性能优化考虑,我们一般都会通过服务端渲染(SSR) ,将首屏动态内容直接服务端输出。

image.png

这种方式的优点是一次 html 返回即可包含页面主体内容,不需要浏览器二次请求接口后再用 js 渲染。但这种方式的缺点也比较明显,对于距离服务端远,或者服务端处理时间较长的场景,用户会看到较长时间的白屏。而且即使 html 返回完成了,用户并不会立即看到内容,页面还需要加载前置的 js,css 等资源后,才能看到内容。

思路 2:CSR + CDN

为了减少白屏时间,考虑利用 CDN 的边缘缓存能力,可以把页面 html 直接缓存在 cdn 节点上。但对于大部分场景来说,页面的主体内容都是动态,或者个性化的,把全部 html 内容缓存在 cdn 上对于业务影响较大,很有少场景能接受。那么换个思路,只把 html 静态部分缓存在 cdn 上呢?其实这个思路也是一个很常见的操作,即把 html 的静态框架部分缓存在 cdn 上,让用户能快速看到部分内容,然后再在客户端发起异步请求,获取动态内容并且渲染(CSR)。CSR + CDN 模式下的渲染时序图如下:

image.png

这种方式的优点是页面静态框架缓存在 cdn 上,用户可以快速看到页面框架内容,减少白屏等待焦虑。缺点是完整的页面内容需要再执行 js ,拉取异步接口回来后再进行渲染。最终有意义的动态内容展示出来的时间,比 SSR 更晚。

思路 3:ESI

CSR + CDN 的方式,很好地解决了白屏时间问题,但带来了动态内容展示的延时。之所以有这个问题,是因为我们把页面的动态内容和静态内容分割到了两个阶段中,并且是串行的,而且串行过程中还穿插了 js 的下载和执行。有什么办法把动态内容和静态内容在 CDN 上整合起来呢?

ESI (Edge Side Include) 给了我们一个很好的思路启发,ESI 最初也是 CDN 服务商们提出的规范,可通过 html 标签里加特定的动态标签,可让页面的静态内容缓存在 cdn 上,动态内容可以自由组装。ESI 的渲染时序图如下:

image.png

这个方案看起来很美好,可以把静态的部分缓存在 CDN 上了,动态部分在用户请求时会动态请求和拼接。但最关键的问题在于,ESI 模式下,最终返回给用户的首字节,还是要等到所有动态内容在 CDN 上都获取和拼接完成。也就是并没有减少白屏时间,只是减少了 CDN 和服务器之间内容传输的体积,带来的性能优化收益很小。最终效果上与 SSR 区别不大。

虽然 ESI 的效果不符合我们预期,但给了我们很好的思考方向。如果能把 ESI 改造成可先返回静态内容,动态内容在 CDN 节点获取到之后,再返回给页面,就可以保证白屏时间短并且动态内容返回不推迟。如果要实现类似于流式 ESI 的效果,要求在 CDN 上能对请求进行细粒度的操作,以及流式的返回。CDN 节点上支持这么复杂的操作吗?答案是肯定的:边缘计算。我们可以在 CDN 上做类似于浏览器的 service worker 的操作,可对请求和响应做灵活的编程。

基于边缘计算的能力,我们有了一种新的选择:边缘流式渲染方案(ESR)。方案详情如下。

渲染流程

方案的核心思想是,借助边缘计算的能力,将静态内容与动态内容以流式的方式,先后返回给用户。cdn 节点相比于 server,距离用户更近,有着更短的网络延时。在 cdn 节点上,将可缓存的页面静态部分,先快速返回给用户,同时在 cdn 节点上发起动态部分内容请求,并将动态内容在静态部分的响应流后,继续返回给用户。最终页面渲染的时序图如下:

image.png

从上图可以看出,cdn 边缘节点可以很快地返回首字节和页面静态部分内容,然后动态内容由 cdn 发起向 server 起并流式返回给用户。方案有以下特点:

  • 首屏 ttfb 会很短,静态内容(例如页面 Header 、基本结构、骨骼图)可以很快看到。
  • 动态内容是由 cdn 发起,相比于传统浏览器渲染,发起时间更早,且不依赖浏览器上下载和执行 js。理论上,最终 reponse 完结时间,与直接访问服务器获取完整动态页面时间一致。
  • 在静态内容返回后,已经可以开始部分 html 的解析,以及 js, css 的下载和执行。把一些阻塞页面的操作提前进行,等完整动态内容流式返回后,可以更快地展示动态内容。
  • 边缘节点与服务端之间的网络,相比于客户端与服务端之间的网络,更有优化空间。例如通过动态加速,以及 edge 与 server 之间的连接复用,能为动态请求减少 tcp 建连和网络传输开销。以做到最终动态内容的返回时间,比 client 直接访问 server 更快。

demo 对比

目前在 alicdn 上对主搜页面做了一个 demo (https://edge-routine.m.alibaba.com/), 下面是在不同网络(通过 charles 的 network throttle 配置限速)情况下,与原始页面的加载对比:

不限速(wifi)
image.png

限速 4G
image.png

限速 3g
image.png

从上面结果可以看出,在网速越慢的情况下,通过 cdn 流式渲染的最终主要元素出来的时间比原始 ssr 的方式出来得越早。这与实际推论也符合,因为网络越慢,静态资源加载时间越慢,对应的浏览器提前加载静态资源带来的效果也越明显。另外,不管在什么网络情况下,cdn 流式渲染方式的白屏时间要短很多。

整体架构

架构图

image.png

边缘流式渲染

1 模板

模板就是一个类似于包含 ESI 区块的语法,基于模板,会将需要动态请求的内容提取出来,把可以静态返回的内容分离出来并缓存起来。所以模板本质上定义了页面动态内容和静态内容。

在流式渲染过程中,会从上到下解析页面模板,如果是静态内容,直接返回给用户,如果遇到动态内容,会执行动态内容的 fetch 逻辑。整个过程中可能有静态和动态内容交替出现。

设计有以下几种类型的模板。

1)原始 HTML

这种模板对现有业务的侵入性最小,只需要在现有的 SSR 页面内容里加上一定的标签,即可把页面中动态部分申明出来:

<html>
  <head>
    <linkrel="stylesheet"type="text/css"href="index.css">
    <scriptdata-original="index.js"></script><metaname="esr-version"content="0.0.1"/>
  </head>
  <body>
    <div>staic content....</div>
    <scripttype="esr/snippet/start"esr-id="111"content="SLICE"></script>
    <div>dynamic content1....</div>
    <scripttype="esr/snippet/end"></script>
    <div>staic content....</div>
    <scripttype="esr/snippet/start"esr-id="222"content="https://test.alibaba.com/snippet/222"></script>
    <divid="222">        
      dynamic content2....
    </div>
    <scripttype="esr/snippet/end"></script>
  </body>
</html>

2)静态模板(暂时没有关联的实际场景)

这种模板需要单独把模板发到 cdn 上(未来如果渲染层接入了 FASS 网关和 SSR ,在这块可以和他们共用模板内容,并且在工作流中发布模板时自动同步到 cdn 上一份,同时清空 cdn 上缓存)。动态的内容有两种渲染方式。一种是利用后端 SSR 出来的动态 html 片断,另一种是后端提供动态数据,由边缘节进行动态html片断渲染。

使用 SSR 动态 html 片断的好处是,不需要在边缘上做 html 模板渲染,并且不需要开发者写两套模板逻辑。缺点是需要后端有 SSR 能力,并且动态内容传输体积较大。

使用边缘节点渲染动态 html 内容的好处是,后端只需要提供动态数据,不需要 SSR 能力(但前端要有 CSR 的能力做降级兜底),并且传输的动态内容体积小。切点是边缘节点上无法流式透传动态内容,需要等完整下载到边缘节点上,处理后再返回给用户。

<html>
  <head>
    <linkrel="stylesheet"type="text/css"href="index.css">
    <scriptdata-original="index.js"></script>
  </head>
  <body>
    <div>staic content....</div>
    <scripttype="esr/block"esr-id="111"content="https://test.alibaba.com/snippet/111"></script>
    <div>staic content....</div>
    <scripttype="esr/template"esr-id="222"content="https://test.alibaba.com/api/data">
      <div>
        {$data.name}
      </div>
    </script>
  </body>
</html>

2 静态内容展现

静态内容来自于模板。对于不同模板类型,获取静态内容的方式不一样。对于 “原始 HTML” 类型的模板,静态内容会从首次动态请求返回的完整 HTML 中,根据 html 注释标记提取出来,并存储到 edge 缓存上。对于 “静态模板”,会通过拉取 CDN 的的模板文件 ,并存储到 edge 缓存上。静态内容有缓存过期时间和版本号。

模板一开始的静态内容会在响应时直接返回给用户。后续的静态内容(例如 html 和 body 的闭合标签)有两种方式:

一种是等待动态内容返回后,再写到响应流中。这种方式对 SEO 比较友好,但缺点是动态内容会阻塞住后续静态内容,并且如果有多个动态内容区块的话,无法实现先返回的动态模板先展示,只能依次展示。

另一种方式是先把静态内容完全返回,然后动态内容以类 bigpipe 的方式,通过脚本把内容插入到对应的坑位。这种方式的优点是静态内容可以一开始就完整展示,且多个动态内容可以先到先展示。缺点是对 SEO 不友好(因为动态内容是能进 js 插进去的)。

3 动态内容

动态内容是在渲染过程中,解析到需要动态获取的区域,会在 edge 上发起动态内容请求。动态内容支持以动态加速的形式到达服务端(源站)。连续节点与后端的动态的内容交互,分为三种方式:

  • 第一种是后端动态内容返回的是全量的页面,需要通过注释标记来从内容中提取。这种方式的优点是对现有业务侵入较小,缺点是动态内容传输体积大,并且需要下载完整 html 后再截取动态内容。
  • 第二种是后端动态内容只返回动态区块的内容,这种方式的优点是可以将动态响应流式返回给用户,缺点时需要页面单独对外提供一个只返回动态区块内容的 url。
  • 第三种是后端动态内容只返回数据,配合静态模板中的动态渲染模板,在边缘节点上渲染出动态 html 后返回给用户。优点是与后端传输数据量小,且不需要后端有 SSR 能力。缺点是需要开发者多维护一套模板逻辑,并且在边缘节点上做复杂的模板渲染可能会有 cpu 开销和限制。

用户和边缘节点的动态内容交互,分为两种形式:

  • 瀑布流式(对应路由配置里的 WATER_FALL ): 动态内容以瀑布流的形式依次返回。虽然在边缘节点上多个动态内容加载的操作是并行的,但对于用户来说,会从上到下依次展示页面内容。这种方式优点是对 SEO 友好,并且不影响页面模块的加载顺序。缺点是多个动态模块时,无法看到整体页面的框架,首个动态块的内容会阻塞后续动态块内容的展示,且页面底部的 js css 资源无法提前加载和执行。
  • 嵌入式(对应路由配置里的 ASYNC_INSERT ):静态内容一次性全部返回,其中动态部分内容会先占一些坑位。后续动态内容会以 innerHTML 的形式,插入到先前占的坑中。这种方式优点是页面底部的 js css 资源无法提前加载和执行,并且页面可以先看到一个全貌。缺点是对 SEO 不友好,且页面模块的执行顺序会根据动态块返回速度有所变化,需要在浏览器端页面逻辑里做一些判断和兼容。

边缘路由

路由配置:
https://g.alicdn.com/edgerender/config.json

{
  version: '0.0.1'//配置版本号
  origin: 'us-proxy.alibaba.com',
  host: 'edge.alibaba.com'
  pages: [
  {
    pageName: 'seo', //页面名称标识
    match: '/abc/efg/.*', //页面path匹配正则字符串
    renderConf: { 
      //渲染配置
      renderType: 'ESR', //边缘渲染
      templateType: 'FULL_HTML', //模板类型:将SSR出的完整html作为模板
      dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 动态内容append返回方式:瀑布流返回|异步填坑(innerHTML)
      templateUrl: ''// 模板url      
     }
   },
   {
     pageName: 'seo',
     match: '/abc/efg/.*',
     renderConf: { 
        renderType: 'ESR', 
        templateType: 'STATIC', // 静态模板,可通过cdn url获取
        dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 动态内容append返回方式:瀑布流返回|异步填坑(innerHTML)
        templateUrl: 'https://g.alicdn.com/@g/xxx.html'      
    }
   },
   {
     pageName: 'jump',
     match: '/jump/.*',
     renderConf: {
        renderType: 'REDIRECT_302', // 302跳转
        rewriteUrl: 'https://jump'      
       }
     },
    {
      pageName: 'proxy',
      match: '/proxy/.*',
      renderConf: {
         renderType: 'PROXY_PASS', // 301跳转
         rewriteUrl: 'https://proxypassurl'      
       }
    }
  ]
}

路由可以认为是边缘计算的一个入口,只有在路由配置中的页面,才会走对应的渲染流程。否则页面会直接走回源,获取页面完整内容。上面的 json 是目前设计的路由配置文件。配置文件最终会在一个静态资源的方式,走覆盖式发布发到 assets cdn 上。同时,为了支持配置发布灰度,线上会存在灰度版本和全量版本的两个配置,在路由代码里配置固定比例,加载灰度或者全量版本的配置。

目前在路由里设计了三种渲染模式,分别是流式渲染、重定向和反向代理。重定向和反向代理的配置比较简单,与 nginx 配置类似,只需要提目标 url 即可。

稳定性

影响范围控制

  • CDN 开关:域名按区域、按比例切流,同时可随时从 cdn 上把流量切回统一接入。
  • 边缘计算 SCOPE 开关:cdn 上配置边缘计算覆盖路径,控制边缘计算只运行在部分路径下。
  • 边缘计算路由开关:边缘计算中通过读取路由配置,控制只有部分页面走流式渲染,否则请求直接走动态加速获取完整页面内容。

异常处理

  • dns 开关,如出现 cdn 严重问题,直接 dns 回切到统一接入。
  • 如果边缘计算基础功能出现异常,在 cdn 配置平台上关闭所有路径的边缘计算,走默认的动态加速。
  • 如果在进了边缘渲染,在没有返回任何响应内容给客户端前,就出现了错误,捕获错误并降级到获取完整页面内容。
  • 如果进了边缘渲染,已经返回了静态部分的响应给客户端,然后在边缘节点了加载动态内容出了问题(超时、http 错误码、与静态内容版本号不匹配),返回一个 location.reload() 的 script 标签,并结束响应,让页面强制刷新。刷新时可带上 bypass 边缘计算的 query 参数以保证刷新时不走边缘渲染。

灰度

1)边缘计算代码灰度

本身平台支持灰度发布边缘计算代码。

2)路由配置灰度

在边缘计算代码里,根据固定比例,加载灰度版本和正式版本的两个配置 url。灰度发布时只发布灰度配置,全量发布时发布全量配置。发布的同时清空 cdn 缓存。

3)页面内容灰度

给灰度页面一个特殊的模板版本号,遇到这个版本号的话,就不走边缘渲染。

平滑发布

前后端分离的发模式下,有一个普遍存在的问题:平滑发布。当页面的静态资源(js,css )的发布,不是与后端一起发布时,可能引起后端返回的 HTML 内容与前端的 js,css 内容不匹配的问题。如果两者之间的不匹配没做兼容处理,可能会出现样式错乱或者 document 选择器找不到元素的问题。

解决平滑发布的一种方式是,在做前后端同时变更的需求时,在代码上做兼容。这样先后发布就不影响页面可用性。

另一种方式是通过版本号。在后端页面上手动配置版本号。当有不兼容发布时,先发前端资源,然后后端手动修改版号,保证只有发布成功的后端机器, HTML 里引用的才是新版本的静态资源。

平滑发布的问题其实在分批发布和 Beta 发布的场景一直存在。只是在 ESR 的场景,我们把静态部分缓存在 cdn 上,会使前后端不一致的可能性更大。为了解决这个问题,需要对应业务的开发者进行发布时的风险识别。如果已经做了兼容,可以不用做特殊处理。但如果没有兼容,需要在修改页面模板的版本号,新版本的动态内容,在遇到版本号不匹配的静态内容时,会放弃本次流式渲染,保证页面不出动态内容和静态内容的兼容问题。

边缘 cdn 服务商

目前各大 cdn 服务商对边缘计算的支持情况如下:

alicdn

  • 支持类 service worker 环境的边缘计算,功能满足需求。
  • 海外节点目前还有限,部分区域性能可与akamai 对标甚至超过,但有些域名性能因节点少的原因还是比 akamai 稍差。
    akamai
  • 只支持简单的请求改写计算,不满足边缘渲染的需求。
  • ESI 可以组装动态和静态内容,但不支持流式,动态内容会阻塞首屏。
  • 海外节点多,在一些地区下相比于 alicdn 有性能优势。

cloudfare

  • 支持类 service worker 环境的边缘计算,功能满足需求。
  • 没有使用经验,如果要用的话可能流程比较复杂。

落地计划

我们会在一个典型的首跳场景进行实验。目前已经在灰度上线,通过 webpagetest 在印尼测试进方案和不进方案的对比,可以看出优化效果:
1.ttfb 减少 1s
2.白屏时间减少 1s
3.核心内容展示时间减少 500ms

webpagetest 对比结果:
https://www.webpagetest.org/video/view.php?id=191202_24218f13985a6403b911beb7f94d6d1a1940fc13

参考

[1]cloudfare edge worker
(https://blog.cloudflare.com/introducing-cloudflare-workers/))
[2]2016 - the year of web streams
(https://jakearchibald.com/2016/streams-ftw/))
[3]ESI(https://www.w3.org/TR/esi-lang/))
[4]Async Fragments: Rediscovering Progressive HTML Rendering with Marko(https://tech.ebayinc.com/engineering/async-fragments-rediscovering-progressive-html-rendering-with-marko/))
[5]The Lost Art of Progressive HTML Rendering
(https://blog.codinghorror.com/the-lost-art-of-progressive-html-rendering/))

福利来了 | 免费下载《覆盖全端业务的大前端技术》

优酷前端业务场景多、技术栈繁杂,对前端工程能力的要求越来越高。阿里文娱将团队遇到的技术挑战以及解决过程做详细的展开,希望由解决方案的推演抽丝剥茧,一探优酷前端团队在支撑业务过程中的技术思考和沉淀,为读者带来一些启发。

点击文末“阅读原文”立即下载!

查看原文

tuobaye0711 赞了文章 · 2019-11-07

Node环境变量 process.env 的那些事儿

前言

这两天在和运维GG搞部署项目的事儿。

碰到一个问题就是,咱们的dev,uat,product环境的问题。

因为是前后端分离,所以在开发和部署的过程中会有对后端接口的域名的切换问题。折腾了一下午,查询了各种资料这才把这Node环境变量process.env给弄明白。

下面这就做个问题解决的记录。希望能对这个不明白的人有所帮助。

Node环境变量

首先,咱们在做reactvue的单页应用开发的时候,相信大家对配置文件里的process.env并不眼生。
想不起来? 黑人问号.jpg。

就是下面这些玩意儿。

clipboard.png

从字面上看,就是这个 env属性,在 developmentproduction不同环境上,配置会有些不同。

行,那下面我们开始看看这个所谓的 process到底是个什么东西。

文档:http://nodejs.cn/api/process....
官方解释process 对象是一个 global (全局变量),提供有关信息,控制当前 Node.js 进程。作为一个对象,它对于 Node.js 应用程序始终是可用的,故无需使用 require()

process(进程)其实就是存在nodejs中的一个全局变量。
然后呢,咱们可以通过这个所谓的进程能拿到一些有意思的东西。

不过我们今天主要是讲讲 process.env

process.env

这是啥?

官方: process.env属性返回一个包含用户环境信息的对象。

文档:http://nodejs.cn/api/process....

噢噢噢,原来着个属性能返回项目运行所在环境的一些信息。

有啥用呢?

很明显的一个使用场景,依靠这个我们就可以给服务器上打上一个标签。这样的话,我们就能根据不同的环境,做一些配置上的处理。比如开启 sourceMap,后端接口的域名切换等等。

你是 dev 环境
他是 uat 环境
她是 product 环境。

如何配置环境变量

下面讲讲如何配置各个环境的环境变量。

Windows配置

临时配置

直接在cmd环境配置即可,查看环境变量,添加环境变量,删除环境变量。

#node中常用的到的环境变量是NODE_ENV,首先查看是否存在 
set NODE_ENV 
#如果不存在则添加环境变量 
set NODE_ENV=production 
#环境变量追加值 set 变量名=%变量名%;变量内容 
set path=%path%;C:\web;C:\Tools 
#某些时候需要删除环境变量 
set NODE_ENV=

永久配置

右键(此电脑) -> 属性(R) -> 高级系统设置 -> 环境变量(N)...

Linux配置

临时

查看环境变量,添加环境变量,删除环境变量

#node中常用的到的环境变量是NODE_ENV,首先查看是否存在
echo $NODE_ENV
#如果不存在则添加环境变量
export NODE_ENV=production
#环境变量追加值
export path=$path:/home/download:/usr/local/
#某些时候需要删除环境变量
unset NODE_ENV
#某些时候需要显示所有的环境变量
env

永久

打开配置文件所在

# 所有用户都生效
vim /etc/profile
# 当前用户生效
vim ~/.bash_profile

在文件末尾添加类似如下语句进行环境变量的设置或修改

# 在文件末尾添加如下格式的环境变量
export path=$path:/home/download:/usr/local/
export NODE_ENV = product

最后修改完成后需要运行如下语句令系统重新加载

# 修改/etc/profile文件后
source /etc/profile
# 修改~/.bash_profile文件后
source ~/.bash_profile

解决环境导致后端接口变换问题

搞清楚这个问题后,我们就可以在不同环境的机器上设置不同的 NODE_ENV,当然这个字段也不一定。
你也可以换成其他的NODE_ENV_NIZUISHUAI等等,反正是自定义的。

解决步骤

1.修改代码里的后端地址配置

很简单,就是利用 process.env.NODE_ENV这个字段来判断。(processnode全局属性,直接用就行了)

clipboard.png

2.在linux上设置环境变量

export NODE_ENV=dev

然后你就可以去愉快的启动项目玩了。

说在最后

因为我现在这个项目 React 服务端渲染。所以后端的请求转发就没交给nginx进行处理。
像平常的纯单页应用,一般是用nginx进行请求转发的。

本篇成文比较快,哈哈,如果文内有任何的纰漏,还请指点,我也就学习学习啦。

另外,如果这篇小小的文章对你带来帮助,不妨给我点个赞吧,这将是我继续下的一大动力。

谢谢~~

参考文献

环境变量-JasperXu的博客 :http://sorex.cnblogs.com/p/62...
如何查看并设置NODE_ENV的值? :http://cnodejs.org/topic/587d...
Node.js的process模块 : http://www.css88.com/archives...

注:【如何配置环境变量】这一块的内容,是直接引用的 JasperXu的博客 的环境变量。

感谢上面三位的好文。

查看原文

赞 139 收藏 122 评论 5

tuobaye0711 赞了文章 · 2019-06-24

JS 中可以提升幸福度的小技巧

本文主要介绍一些JS中用到的小技巧,可以在日常Coding中提升幸福度,将不定期更新~

1. 类型强制转换

1.1 string强制转换为数字

可以用*1来转化为数字(实际上是调用.valueOf方法)
然后使用Number.isNaN来判断是否为NaN,或者使用 a !== a 来判断是否为NaN,因为 NaN !== NaN

'32' * 1            // 32
'ds' * 1            // NaN
null * 1            // 0
undefined * 1    // NaN
1  * { valueOf: ()=>'3' }        // 3

常用:也可以使用+来转化字符串为数字

+ '123'            // 123
+ 'ds'               // NaN
+ ''                    // 0
+ null              // 0
+ undefined    // NaN
+ { valueOf: ()=>'3' }    // 3

1.2 object强制转化为string

可以使用 字符串+Object 的方式来转化对象为字符串(实际上是调用 .toString() 方法)

'the Math object:' + Math                // "the Math object:[object Math]"
'the JSON object:' + JSON              // "the JSON object:[object JSON]"

当然也可以覆盖对象的toStringvalueOf方法来自定义对象的类型转换:

2  * { valueOf: ()=>'3' }                // 6
'J' + { toString: ()=>'S' }                // "JS"
《Effective JavaScript》P11:当+用在连接字符串时,当一个对象既有toString方法又有valueOf方法时候,JS通过盲目使用valueOf方法来解决这种含糊。
对象通过valueOf方法强制转换为数字,通过toString方法强制转换为字符串
'' + {toString:()=>'S',valueOf:()=>'J'}                // J

1.3 使用Boolean过滤数组中的所有假值

我们知道JS中有一些假值:falsenull0""undefinedNaN,怎样把数组中的假值快速过滤呢,可以使用Boolean构造函数来进行一次转换

const compact = arr => arr.filter(Boolean)
compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34])             // [ 1, 2, 3, 'a', 's', 34 ]

1.4 双位运算符 ~~

可以使用双位操作符来替代正数的 Math.floor( ),替代负数的Math.ceil( )。双否定位操作符的优势在于它执行相同的操作运行速度更快。

Math.floor(4.9) === 4      //true
// 简写为:
~~4.9 === 4      //true

不过要注意,对正数来说 ~~ 运算结果与 Math.floor( ) 运算结果相同,而对于负数来说与Math.ceil( )的运算结果相同:

~~4.5                // 4
Math.floor(4.5)      // 4
Math.ceil(4.5)       // 5

~~-4.5                // -4
Math.floor(-4.5)     // -5
Math.ceil(-4.5)      // -4

1.5 短路运算符

我们知道逻辑与&&与逻辑或||是短路运算符,短路运算符就是从左到右的运算中前者满足要求,就不再执行后者了;
可以理解为:

  • &&为取假运算,从左到右依次判断,如果遇到一个假值,就返回假值,以后不再执行,否则返回最后一个真值
  • ||为取真运算,从左到右依次判断,如果遇到一个真值,就返回真值,以后不再执行,否则返回最后一个假值
let param1 = expr1 && expr2
let param2 = expr1 || expr2
运算符示例说明
&&expr1&&expr2如果expr1 能转换成false则返回expr1,否则返回expr2. 因此, 在Boolean环境中使用时, 两个操作结果都为true时返回true,否则返回false.
||expr1||expr2如果expr1能转换成true则返回expr1,否则返回expr2. 因此,在boolean环境(在if的条件判断中)中使用时, 二者操作结果中只要有一个为true,返回true;二者操作结果都为false时返回false.
!!expr如果单个表达式能转换为true的话返回false,否则返回true.

因此可以用来做很多有意思的事,比如给变量赋初值:

let variable1
let variable2 = variable1  || 'foo'

如果variable1是真值就直接返回了,后面短路就不会被返回了,如果为假值,则会返回后面的foo

也可以用来进行简单的判断,取代冗长的if语句:

let variable = param && param.prop

如果param如果为真值则返回param.prop属性,否则返回param这个假值,这样在某些地方防止paramundefined的时候还取其属性造成报错。

1.6 取整 | 0

对一个数字| 0可以取整,负数也同样适用,num | 0

1.3 | 0         // 1
-1.9 | 0        // -1

1.7 判断奇偶数 & 1

对一个数字& 1可以判断奇偶数,负数也同样适用,num & 1

const num=3;
!!(num & 1)                    // true
!!(num % 2)                    // true

2. 函数

2.1 函数默认值

func = (l, m = 3, n = 4 ) => (l * m * n);
func(2)             //output: 24

注意,传入参数为undefined或者不传入的时候会使用默认参数,但是传入null还是会覆盖默认参数。

2.2 强制参数

默认情况下,如果不向函数参数传值,那么JS 会将函数参数设置为undefined。其它一些语言则会发出警告或错误。要执行参数分配,可以使用if语句抛出未定义的错误,或者可以利用强制参数

mandatory = ( ) => {
  throw new Error('Missing parameter!');
}
foo = (bar = mandatory( )) => {     // 这里如果不传入参数,就会执行manadatory函数报出错误
  return bar;
}

2.3 隐式返回值

返回值是我们通常用来返回函数最终结果的关键字。只有一个语句的箭头函数,可以隐式返回结果(函数必须省略大括号{ },以便省略返回关键字)。

要返回多行语句(例如对象文本),需要使用( )而不是{ }来包裹函数体。这样可以确保代码以单个语句的形式进行求值。

function calcCircumference(diameter) {
  return Math.PI * diameter
}
// 简写为:
calcCircumference = diameter => (
  Math.PI * diameter;
)

2.4 惰性载入函数

在某个场景下我们的函数中有判断语句,这个判断依据在整个项目运行期间一般不会变化,所以判断分支在整个项目运行期间只会运行某个特定分支,那么就可以考虑惰性载入函数

function foo(){
    if(a !== b){
        console.log('aaa')
    }else{
        console.log('bbb')
    }
}
 
// 优化后
function foo(){
    if(a != b){
        foo = function(){
            console.log('aaa')
        }
    }else{
        foo = function(){
            console.log('bbb')
        }
    }
    return foo();
}

那么第一次运行之后就会覆写这个方法,下一次再运行的时候就不会执行判断了。当然现在只有一个判断,如果判断很多,分支比较复杂,那么节约的资源还是可观的。

2.5 一次性函数

跟上面的惰性载入函数同理,可以在函数体里覆写当前函数,那么可以创建一个一次性的函数,重新赋值之前的代码相当于只运行了一次,适用于运行一些只需要执行一次的初始化代码

var sca = function() {
    console.log('msg')
    sca = function() {
        console.log('foo')
    }
}
sca()        // msg
sca()        // foo
sca()        // foo

3. 字符串

3.1 字符串比较时间先后

比较时间先后顺序可以使用字符串:

var a = "2014-08-08";
var b = "2014-09-09";
 
console.log(a>b, a<b); // false true
console.log("21:00"<"09:10");  // false
console.log("21:00"<"9:10");   // true   时间形式注意补0

因为字符串比较大小是按照字符串从左到右每个字符的charCode来的,但所以特别要注意时间形式注意补0

4. 数字

4.1 不同进制表示法

ES6中新增了不同进制的书写格式,在后台传参的时候要注意这一点。

29            // 10进制
035            // 8进制29      原来的方式
0o35            // 8进制29      ES6的方式
0x1d            // 16进制29
0b11101            // 2进制29

4.2 精确到指定位数的小数

将数字四舍五入到指定的小数位数。使用 Math.round() 和模板字面量将数字四舍五入为指定的小数位数。 省略第二个参数 decimals ,数字将被四舍五入到一个整数。

const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`)
round(1.345, 2)                 // 1.35
round(1.345, 1)                 // 1.3

4.3 数字补0操作

感谢网友 @JserWang @vczhan 提供 这个小技巧
有时候比如显示时间的时候有时候会需要把一位数字显示成两位,这时候就需要补0操作,可以使用slice和string的padStart方法

const addZero1 = (num, len = 2) => (`0${num}`).slice(-len)
const addZero2 = (num, len = 2) => (`${num}`).padStart( len   , '0')
addZero1(3) // 03
 
addZero2(32,4)  // 0032

5. 数组

5.1 reduce方法同时实现map和filter

假设现在有一个数列,你希望更新它的每一项(map的功能)然后筛选出一部分(filter的功能)。如果是先使用map然后filter的话,你需要遍历这个数组两次。
在下面的代码中,我们将数列中的值翻倍,然后挑选出那些大于50的数。

const numbers = [10, 20, 30, 40];
const doubledOver50 = numbers.reduce((finalList, num) => {
  num = num * 2;
  if (num > 50) {
    finalList.push(num);
  }
  return finalList;
}, []);
doubledOver50;            // [60, 80]

5.2 统计数组中相同项的个数

很多时候,你希望统计数组中重复出现项的个数然后用一个对象表示。那么你可以使用reduce方法处理这个数组。

下面的代码将统计每一种车的数目然后把总数用一个对象表示。

var cars = ['BMW','Benz', 'Benz', 'Tesla', 'BMW', 'Toyota'];
var carsObj = cars.reduce(function (obj, name) {
  obj[name] = obj[name] ? ++obj[name] : 1;
  return obj;
}, {});
carsObj; // => { BMW: 2, Benz: 2, Tesla: 1, Toyota: 1 }

5.3 使用解构来交换参数数值

有时候你会将函数返回的多个值放在一个数组里。我们可以使用数组解构来获取其中每一个值。

let param1 = 1;
let param2 = 2;
[param1, param2] = [param2, param1];
console.log(param1) // 2
console.log(param2) // 1

当然我们关于交换数值有不少其他办法:

var temp = a; a = b; b = temp            
b = [a, a = b][0]                     
a = a + b; b = a - b; a = a - b        

5.4 接收函数返回的多个结果

在下面的代码中,我们从/post中获取一个帖子,然后在/comments中获取相关评论。由于我们使用的是async/await,函数把返回值放在一个数组中。而我们使用数组解构后就可以把返回值直接赋给相应的变量。

async function getFullPost(){
  return await Promise.all([
     fetch('/post'),
     fetch('/comments')
  ]);
}
const [post, comments] = getFullPost();

5.5 将数组平铺到指定深度

使用递归,为每个深度级别 depth 递减 1 。 使用 Array.reduce()Array.concat() 来合并元素或数组。 基本情况下,depth 等于 1 停止递归。 省略第二个参数,depth 只能平铺到 1 (单层平铺) 的深度。

const flatten = (arr, depth = 1) =>
  depth != 1
    ? arr.reduce((a, v) => a.concat(Array.isArray(v) ? flatten(v, depth - 1) : v), [])
    : arr.reduce((a, v) => a.concat(v), []);
flatten([1, [2], 3, 4]);                             // [1, 2, 3, 4]
flatten([1, [2, [3, [4, 5], 6], 7], 8], 2);           // [1, 2, 3, [4, 5], 6, 7, 8]

5.6 数组的对象解构

数组也可以对象解构,可以方便的获取数组的第n个值

const csvFileLine = '1997,John Doe,US,john@doe.com,New York';
const { 2: country, 4: state } = csvFileLine.split(',');
 
country            // US
state            // New Yourk

6. 对象

6.1 使用解构删除不必要属性

有时候你不希望保留某些对象属性,也许是因为它们包含敏感信息或仅仅是太大了(just too big)。你可能会枚举整个对象然后删除它们,但实际上只需要简单的将这些无用属性赋值给变量,然后把想要保留的有用部分作为剩余参数就可以了。

下面的代码里,我们希望删除_internal和tooBig参数。我们可以把它们赋值给internal和tooBig变量,然后在cleanObject中存储剩下的属性以备后用。

let {_internal, tooBig, ...cleanObject} = {el1: '1', _internal:"secret", tooBig:{}, el2: '2', el3: '3'};
 
console.log(cleanObject);                         // {el1: '1', el2: '2', el3: '3'}

6.2 在函数参数中解构嵌套对象

在下面的代码中,engine是对象car中嵌套的一个对象。如果我们对engine的vin属性感兴趣,使用解构赋值可以很轻松地得到它。

var car = {
  model: 'bmw 2018',
  engine: {
    v6: true,
    turbo: true,
    vin: 12345
  }
}
const modelAndVIN = ({model, engine: {vin}}) => {
  console.log(`model: ${model} vin: ${vin}`);
}
modelAndVIN(car); // => model: bmw 2018  vin: 12345

7. 代码复用

7.1 Object [key]

虽然将 foo.bar 写成 foo ['bar'] 是一种常见的做法,但是这种做法构成了编写可重用代码的基础。许多框架使用了这种方法,比如element的表单验证

请考虑下面这个验证函数的简化示例:

function validate(values) {
  if(!values.first)
    return false;
  if(!values.last)
    return false;
  return true;
}
console.log(validate({first:'Bruce',last:'Wayne'})); // true

上面的函数完美的完成验证工作。但是当有很多表单,则需要应用验证,此时会有不同的字段和规则。如果可以构建一个在运行时配置的通用验证函数,会是一个好选择。

// object validation rules
const schema = {
  first: {
    required:true
  },
  last: {
    required:true
  }
}
 
// universal validation function
const validate = (schema, values) => {
  for(field in schema) {
    if(schema[field].required) {
      if(!values[field]) {
        return false;
      }
    }
  }
  return true;
}
console.log(validate(schema, {first:'Bruce'})); // false
console.log(validate(schema, {first:'Bruce',last:'Wayne'})); // true

现在有了这个验证函数,我们就可以在所有窗体中重用,而无需为每个窗体编写自定义验证函数。


  1. 添加网友 网友 @JserWang @vczhan 提供的 数字补0操作
  2. 添加3.1,字符串比较时间先后
  3. 感谢网友 @Pandaaa 提醒,~~在负数时表现与Math.ceil( )相同

网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

参考:

  1. JavaScript 开发人员需要知道的简写技巧
  2. 《Effective Javascript》
  3. 不得不知的ES6小技巧
  4. js运算符的一些特殊应用
  5. JS高级技巧(简洁版)
  6. 小议 js 下字符串比较大小

推介阅读:
ES6演示小网站

PS:欢迎大家关注我的公众号【前端下午茶】,一起加油吧~

另外可以加入「前端下午茶交流群」微信群,长按识别下面二维码即可加我好友,备注加群,我拉你入群~

查看原文

赞 33 收藏 33 评论 4

tuobaye0711 赞了回答 · 2019-05-06

解决react ant design 中如何在表头中加个Icon,悬浮icon又触发Tooltip?

Table.Columntitle 传值可以传入一个 ReactNode

所以,可以写成这样

const title = (
  <span>
    目的地
    <Tooltip title="这是提示">
      <Icon style={{ marginLeft: '0.25em' }} type="question-circle" />
    </Tooltip>
  </span>
);

<Table.Column title={title} dataIndex='destination' key='destination'></Table.Column>

关注 3 回答 1

tuobaye0711 回答了问题 · 2019-03-06

大家觉得node.js怎么样?

js本身就是一门依赖于异步的语言,同样基于ECMA语法的node使用这种语法并不奇怪吧?

关注 3 回答 4

tuobaye0711 发布了文章 · 2019-03-05

从零开始开发一个Node交互式命令行应用

导言:对于大多数前端开发者而言,谈到命令行工具,大家肯定都用过。但是谈到开发命令行工具,估计就没几人有了解了。本文旨在用最短的时间内,帮您开发一个实用(斜眼笑)的图片爬虫命令行应用。

想追求更好的阅读体验,请移步拓跋的前端客栈。同时把项目地址放在显眼的位置

Puppeteer 简介

什么是 Puppeteer?

Puppeteer 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具。Chrome 作为浏览器市场的领头羊,Chrome Headless  将成为 web 应用   自动化测试   的行业标杆。所以我们很有必要来了解一下它。

puppeteer

Puppeteer 可以做什么?

Puppeteer 可以做的事情有很多,包括但不限于:

  • 利用网页生成 PDF、图片
  • 可以从网站抓取内容
  • 自动化表单提交、UI 测试、键盘输入等
  • 帮你创建一个最新的自动化测试环境(chrome),可以直接在此运行测试用例
  • 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题

Puppeteer 有什么优势?

  • 相对于真实浏览器而言,少了加载 css,js 以及渲染页面的工作。无头浏览器要比真实浏览器快的多。
  • 可以在无界面的服务器或 CI 上运行,减少了外界的干扰,更稳定。
  • 在一台机器上可以模拟运行多个无头浏览器,方便进行并发运行。

如何安装 Puppeteer?

安装  Puppeteer  很简单,如下:
npm i --save puppeteer
or
yarn add puppeteer

需要注意的是,由于用到了 ES7 的  async/await  语法 ,node  版本最好是 v7.6.0 或以上。

如何使用 Puppeteer?

由于本文不是专门讲 Puppeteer 的文章,故这部分暂且略过,大家可以去看下面的链接学习。

Puppeteer Github

Puppeteer Api Doc

Puppeteer 中文 Api Doc

说了这么多,Puppeteer 与我们要开发的命令行应用有什么关系呢?我们准备制作一个抓图命令行工具,不使用传统的请求式爬虫,我们使用 Puppeteer 这种无头浏览器,从 DOM 里抓图,这样能有效规避部分爬虫防御手段。

Puppeteer 简单应用

case 1. 屏幕截图

直接上代码,很好理解:

const puppeteer = require("puppeteer");

const getScreenShot = async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto("https://baidu.com");
  await page.screenshot({ path: "baidu.png" });

  await browser.close();
};

getScreenShot();

这段代码的意思就是以 headless(无头)模式打开浏览器,然后打开一个新标签页,跳转到百度网址, 并且进行屏幕截图,保存为 baidu.png 为名的图片,最后关闭浏览器。

执行结果如下。

baidu

case 2. 抓取网站信息

接下来学习如何用 Puppeteer 抓取网站信息了。

这次我们来抓取 jd 书单信息。

// book info spider
const puppeteer = require("puppeteer");
const fs = require("fs");

const spider = async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto("https://search.jd.com/Search?keyword=javascript");

  const result = await page.evaluate(() => {
    let elements = document.querySelectorAll(".gl-item");

    const data = [...elements].map(i => {
      return {
        name: i.querySelector(".p-name em").innerText,
        description: i.querySelector(".p-name i").innerText,
        price: i.querySelector(".p-price").innerText,
        shop: i.querySelector(".p-shopnum").innerText,
        url: i.querySelector(".p-img a").href
      };
    });
    return data; // 返回数据
  });

  browser.close();
  return result;
};

spider().then(value => {
  fs.writeFile(`${__dirname}/javascript.json`, JSON.stringify(value), err => {
    if (err) {
      throw err;
    }
    console.log("file saved!");
  });
  console.log(value); // Success!
});

我们做的就是跳转到关键字是 javascript 的页面,然后对页面的 dom 结构进行分析,找到图书列表所对应的书名、描述、价格、出版社、网页链接信息,然后把数据写入到 javascript.json 文件里去,方便我们保存浏览。

逻辑很简单。这已经是一个爬虫的雏形了,最后得到如下图所示的 json 文件,非常给力。

javascript.json

case 3. 图片爬虫

图片爬虫,这就是我们要做的命令行应用的主题了。

一个最基本的思路是这样的:

打开浏览器 —> 跳转到百度图片 —> 获取 input 框的焦点 —> 输入 keywords —> 点击搜索按钮 —> 跳转至结果列表页 —> 下拉到底部 —> 操作 dom,获取所有图片的 src 备用 —> 根据 src 将对应图片保存到本地 —> 关闭浏览器

代码实现之:

首先是浏览器操作部分

const browser = await puppeteer.launch(); // 打开浏览器
const page = await browser.newPage(); // 打开新tab页
await page.goto("https://image.baidu.com"); // 跳转到百度图片
console.log("go to https://image.baidu.com"); // 获取input框的焦点

await page.focus("#kw"); // 把焦点定位到搜索input框
await page.keyboard.sendCharacter("猫咪"); // 输入关键字
await page.click(".s_search"); // 点击搜索按钮
console.log("go to search list"); // 提示跳转到搜索列表页

然后是图片处理部分

page.on("load", async () => {
  await autoScroll(page); // 向下滚动加载图片
  console.log("page loading done, start fetch...");
  const srcs = await page.evaluate(() => {
    const images = document.querySelectorAll("img.main_img");
    return Array.prototype.map.call(images, img => img.src);
  }); // 获取所有img的src
  console.log(`get ${srcs.length} images, start download`);
  for (let i = 0; i < srcs.length; i++) {
    await convert2Img(srcs[i], target);
    console.log(`finished ${i + 1}/${srcs.length} images`);
  } // 保存图片
  console.log(`job finished!`);
  await browser.close();
});

因为百度图片是往下滚动就可以继续懒加载。因此,我们想要加载更多图片,可以先往下滚动一会儿。然后通过分析 dom 结构来获取列表里所有图片的 src,最后进行下载。

执行以下,就能得到一系列猫咪的图片:

cat

图片下载的地方只写了主函数,更详细的代码可以去参见github.

至此,我们用 Node 和 Puppeteer 开发出了一个最基本的图片爬虫工具。

如何优化?

这个图片爬虫工具目前还有点 low 啊,我们的目标是要开发一个交互式的命令行应用,肯定不能止于此。有哪些可以进一步优化的点呢?经过简单的思考,我列了一下:

  • 下载图片的内容可以自定义
  • 可以支持用户选择图片下载张数
  • 支持命令行传参
  • 支持命令行交互
  • 交互界面美观
  • 支持双击直接运行
  • 支持全局命令行调用

使用 commander.js 支持命令行传参

Commander 是一款重量轻,表现力和强大的命令行框架。提供了用户命令行输入和参数解析强大功能。

const program = require("commander");

program
  .version("0.0.1")
  .description("a test cli program")
  .option("-n, --name <name>", "your name", "zhl")
  .option("-a, --age <age>", "your age", "22")
  .option("-e, --enjoy [enjoy]")
  .action(option => {
    console.log('name: ', option.name);
    console.log('age: ', option.age);
    console.log('enjoy: ', option.enjoy);
  });

program.parse(process.argv);

Commander十分容易上手,上面这一段代码仅用了寥寥数行,就定义了一个命令行的输入与输出。其中:

  • version 定义版本号
  • description 定义描述信息
  • option 定义输入选项,传3个参数,如option("-n, --name <name>", "your name", "GK"),第一项是传参的值,-n是简写形式,--name是全称形式, <name>表示输入的参数,<>是必填项,如果是[],则是选填项。第二项“your name"是求助help时的提示信息,告诉用户应该输入的内容,最后一项"GK"是默认值。
  • action 定义执行的操作,是一个回调函数,入参是前文输入的option选项,如果没有输入option,则使用定义的默认值。

要查询更详细的api,请参考Commander Api文档

执行一下上述脚本,可以得到:

commander.png

这样命令行就可以做到简单的交互效果了。但是有没有觉得不够好看呢,别急,继续往下看。

使用inquirer制作可交互命令行应用

inquirer可以为Node制作可嵌入式的美观的命令行界面。

可以提供问答式的命令输入:

inquirer1

可以提供多种形式的选择界面:

inquirer2
inquirer3

可以对输入信息进行校验:
inquirer4

最后可以对输入信息进行处理:
inquirer5

上面的例子是inquirer的官方例子,可以参考pizza.js

inquirer的文档可以查看inquirer documents

有了inquirer,我们就可以制作更为精美的交互式命令行工具了。

使用 chalk.js来让交互界面更美观

chalk.js

chalk的语法非常简单:

const chalk = require('chalk');
const log = console.log;

// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!'));
// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
// Nest styles
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
// Nest styles of the same type even (color, underline, background)
log(chalk.green(
  'I am a green line ' +
  chalk.blue.underline.bold('with a blue substring') +
  ' that becomes green again!'
));

可以输出如下信息,一看便懂:

chalk2

再让我们做点有意思的事情...

想必有人看到过下面知乎的控制台效果,既然要做点有意思的事情,今天我们不妨也把这种效果加到命令行程序里面,提升一下逼格。

zhihu

首先我们准备一副ASCII码用来打印,各位可以自行搜索text转ASCII,网上的转化方案不要太多。我们准备制作的命令行image spider就制作一个IMG SPD的ASCII码字符串吧~

经过挑选,效果如图:

imgspd

这种复杂的字符串怎么打印出来呢?直接保存为string一定是不行的,格式会乱的一塌糊涂。

想要能完整的打印出格式来,有一个取巧的方法,以注释的形式打印出来。什么能保存注释呢?~~ function。

所以事情就简单到了打印一个function。但是直接打印函数还是不行的,这时候就用到了可以怼天怼地的toString()方法,我们只需要把注释中间的部分用正则匹配出来就行了,easy~

imgspd2

最后看一看效果,铛铛铛铛~

imgspd3

支持双击运行

这里使用一种叫做Shebang的技术。

Shebang(也称为 Hashbang )是一个由#和!构成的字符序列 #! ,其出现在文本文件的第一行的前两个字符。 在文件中存在 Shebang 的情况下,类 Unix 操作系统的程序加载器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,并将载有 Shebang 的文件路径作为该解释器的参数。

node下我们使用#! /usr/bin/env node即可

这时候我们便可以取消文件的扩展名.js了。

加入环境变量,支持全局调用

package.json里面配置

"bin": {
  "img-spd": "app"
},

执行npm link,它将会把img-spd这个字段复制到npm的全局模块安装文件夹node_modules内,并创建符号链接(symbolic link,软链接),也就是将 app 的路径加入环境变量 PATH。

这时,在任意目录下,直接命令行输入img-spd即可运行此命令行

尾声

至此,要改进的地方已经全部修改完毕,快来看看我们的成品吧~

imgspd4

看着一整个文件夹的gakki,感觉满满的幸福要溢出来了

gakki

最后用动图来展示一下:

img-spd

附录

项目地址

项目地址

Install

npm install -g img-spd

Usage

img-spd

or

Usage: img-spd [options]

img-spd is a spider get images from image.baidu.com

Options:
  -v --version               output the version number
  -k, --key [key]            input the image keywords to download
  -i, --interval [interval]  input the operation interval(ms,default 200)
  -n, --number [number]      input the operation interval(ms,default 200)
  -m, --headless [headless]  choose whether the program is running in headless mode
  -h, --help                 output usage information
查看原文

赞 95 收藏 64 评论 3

tuobaye0711 关注了专栏 · 2019-02-25

前端早早聊

技术经用得上、听得懂、抄得走,加微 codingdreamer 围观

关注 599

tuobaye0711 评论了文章 · 2019-02-20

程序员30岁前月薪达不到30K,该何去何从

作者:志佳老师

本文首发微信公众号:jingchengyideng
欢迎关注,每天都给你推送新鲜的前端技术文章


​ 前端自08年左右兴盛已来,无数年轻人投入其中,但是这2年不知道身为前端的你,有没有感觉到求一份靠谱的前端的工作变的很难,同样企业求一名靠谱的前端也觉得很难,其实并不是因为求职者太过挑剔,也不是企业给不起工资,而是因为这两年大前端的发展无论从技术深度和广度都跟以前大不一样,前端不再是页面仔、组件仔,而是指能够使用JavaScript和相关H5技术开发iOS、Android、AR、VR、IoT、PC、Server、WebApp、3D甚至AI的综合性技术人才。很多同学的计算机基础相对薄弱,导致了知识直接断层。才会出现咫尺天涯的这种怪象。

​ 也正是由于这样综合性的前端一人难求,造成了前端的工资水涨船高。今天看到这篇文章的老铁,不论你是计算机科班还是”野路子”前端,如果你在一线城市30岁前依旧月薪没到30K,你真的要反思一下自己了。到底是技术、情商、智商、努力程度还是哪里出现了问题让你没能斩获一份这样的薪水。鄙人不才,前端开发界一名小学生,有人叫我老袁,因为身高170体重170,也有人叫我圆老师,正经土生土长东北爷们。几年前侥幸进入百度和腾讯,后来在阿里offer和创业之间选择了创业,更侥幸在27岁那年拿到了月薪30K+,说这些并不是为了装或者鄙视谁,我何德何能,更何况程序员何必diss程序员,我只是想把我的一些想法写出来与各位同仁共勉。您看着舒坦您就看,您看着厌恶就关了页面,别当键盘侠,100多斤的人了还是得长点心的。乖~


我总结了以下这6点。

1. 非科班出身

​ 如果你是非科班出身,草草入了前端这行,而且入门学习的知识和工作只是HTML、CSS、JavaScrpt(给按钮绑绑事件、发发请求 ),没有任何后端语言的背景,那么这将直接制约你的职业生涯。因为前端如火如荼的大势之下,其实是把大部分后端思想在前移比如经典的DI、IOC、AOP、MVVM(起源于 SilverLight)等等,这些思想什么三大框架中运用的淋淋尽致,综述好好学一门后端语言不但对于理解思想有帮助还能更好的让你去使用ES9和TS。

2. 避免天天用cli搬砖

​ 每天只是抱着vue-cli或其他cli的同学,这类同学往往每天n都是pm install & npm start & npm test,一顿操作猛如虎,一问工资两千五,感觉自己很溜,但是让你配个完整的vue-cli一脸茫然,这更是一个危险的信号。但是呢这真的是大部分老铁的工作常态,多往Webpack和成熟的打包工具上用用心吧。这里也要推荐一下百度的FIS,并不是让你一定要使用FIS而是理解整个FIS的思想,站在巨人的肩膀去实践更好的前端工程化。

3. X平台的小程序

​ 这个写给天天写微信小程序、支付宝小程序、头条小程序等XX小程序这一类的老铁,我并不是说写小程序你技术弱,而是写着写着你的技术会学跑偏。因为它是某种生态下的技术,并不通用,严格意义上来讲其实并不算技术。而且很多文档确实不健全,是典型的程序员坑程序员的大众技术典范。有些老铁可能就不服了,我会Taro啊~嗯,那你棒棒的,祝你幸福。

4. 走出自己的安逸区

​ 以上这几点从我自己的感觉来看确实是前景堪忧的。

​ 为什么说这类童鞋前景堪忧呢?如果天天就干上面说的这些活,真心是很难突破15k的,15k是很多同学职业生涯的第一道坎。老袁我来北京7年,所以对北京了解的多一些。目前来看在北京社招的同学月薪30k还是非常普遍的,因为很多刚毕业的本科学生(学校还可以的)校招在北京起薪就已经15k了,如果老铁你天天写些微信小程序之类的,或者长年堆在业务写着你自己都厌恶的API的时候,真的得想办法突破自己,走出自己的安逸区。

​ 走出自己的安逸区,首先学历是个门槛(以北京举例哈),你应该看到过关于读书无用类似的帖子,但是这个还真别信,在北京很多企业没有学历根本过不了HR这关,更别说你的简历能到面试官的手里。如果你学历不好技术也不行说实话很难混老铁,现在市场招聘,先不说顶级BAT、TMD,就是二线互联网企业都必须要求全日制本科,不是不给你机会,而是你都没有资格。2018年下半年这个现象更加突出,我承认学历不是很好的同学能力不一定差,但是老铁这块敲门砖你没有,根本就没有机会把自己优秀的一面展示在企业面前。如果你是大专,就更得狠下心来去学习吧,先奔着25K努力。

​ 你会问,既然企业都不给我机会了,我还努力啥。错!这个大错特错,你要让自己慢慢变得优秀,先让你的技术在现有的公司可以独挡一面,各方面全盘掌控,重要事情你都可以顶上。创业公司最重要的是积累人脉,你周围的同事或者Leader可能是从某某知名大厂出来的,如果你具备独挡一面的能力,周围的资源及外界的机遇都会助你进入更高平台,那句话说的没错机会总是留给有准备的人。所以看到这里的专科老铁,学历不好使,就是让技术过硬,加班是提升技术的最好手段,但不是加无意义的班,无意义是指重复的工作,你会的,这里说的有意义的加班指比如你的团队哪天要搭SPA、MPA、CI、BDD或者有图形学的项目需要你等等,让你参与核心开发部分。青春就那么几年,如果每天加无意义的班,就没必要继续在这家公司待下去了。那么就有童鞋说,我去外包吧,对学历和技术的要求低些。我告诉有这类想法的童鞋,外包公司是万万不能待的,只是浪费青春,四点现实原因:重复性工作 、各种脏乱差的活 、你的工资大部分根本到不了你的手 、你的工牌的颜色跟其他同事也不一样!不一样!

5. 提升自己的技术能力

​ 接下来,就是提高自己的技术了。当然,上面说了学历还算不错的话,咱就更得让自个的技术独挡一面了。那么如何提高自己的前端技术呢?这就应了那句老话,小孩没娘说来话长。但是我讲下我的一些建议:

​ 首先要了解最新的前端趋势比如Micro Frontends、omi、Houdini、CSS Scroll Snap Points、React Suspense、Hooks、quicklink、Workbox、Angular8 Lvy等等。同时还要熟练掌握现在流行的前端技术如Webpack、React、Vue、ES9、Angular、KOA、TS、Three等等。但如果你是普通公司,你就每天实打实,脑子里不要考虑做轮子哥(原因——轮子太多了Parcel、Moon、hyperapp.js、Fastify…)但是一定要理解内部机制是怎么样的,要理解后面的东西,不是天天一顿摆API。因为现在前端的面试基本都是原理!原理!原理!即便现在行情是面试造火箭,入职钉钉子。还有建议大家一定要把Node.Js要研究透彻,这才是真材实料的硬货。

​ 老子学不动?生死看淡,不服就干。再来几点:

  • 1.要学会C|C++(Webkit、Libuv等等后面也会用到)。天下武功出少林,天下语言爹是C。
  • 2.最早讨论的经典思想—OOP到AOP再到Functional,个人建议先学思想再学框架。还有NodeJs中三个特别有意思的框架 Nest(NodeJs版本的Spring)、Nuxt和Next(Vue&React的SSR),有没有也傻傻分不清楚。
  • 3.在强调一次至少会一门正统的语言,比如:JAVA、NET等
  • 4.项目,一定要做有质量的,有些技术没有条件创建条件也要用。只有把技术用到项目中去,才能让你醍醐灌顶,光学不干等于耍流氓。
  • 5.多读书,一本好书就像高级武功秘籍一样。哪怕是从里面领悟一招半式,功力提升起来都是惊人的。
  • 6.最后举个能跟得上时代的小栗子:比如CSS中的变量、JavaScript的类、依赖注入的代码,这些代码应该在你的项目里到处跑了。

clipboard.png

6. 给不同群体的建议

​ 另外,我来说下根据群体的划分来注意前端道路上的注意事项。

​ 上学群体:计算机基础必须好好学(校招必问)———数据结构和算法、计算机组成原理、编译原理、操作系统,这些课千万别逃。

​ 工作群体:学历可以且工作也可以的,要注重人脉的培养。原因呢我上面已经讲过,就不多赘述了。

​ 如果你是计算机科班生,一定要好好学专业课,这不是鸡汤,是现实。校招面临的压力不比社招低,校招上来就问Webpack的原理是什么,在工作中体验过什么是工程化吗。大家会说,天天上学,我怎么有项目,怎么体验工程化,但问题是就有人会,没办法。校招没人问学校教不教,只问企业用的你会不会,会就招,不会就拜拜。有些大学老师的授课内容已经和社会严重脱节,什么叫SPA,什么叫MPA,MPA怎么转换成SPA,老师都不懂,这个就得靠你在学校里多多去接触外面的资源了。但是上面提到的这些大学的专业课你必须得听,校招大部分都在考你数据结构与算法,社招也一样,如果你是纯野路子(非科班)更得补,有人说前端用不到算法,Dom Diff、投影算法、Trie树、KOA原理里的recursion、JS执行堆栈了解一下?想学明白上面说的框架原理,编译原理你躲不过(Webpack里的complier、AST分分钟体现出作者的专业性),数据结构与算法没学好,React和Vue的源码压根玩不溜,算法已经成为了大部分公司的一面,如果这些功课的基本功到位了,框架原理就很easy了。这个社会优秀的人太多了,所以在职场拼的就是基础瓷不瓷实,所以如果你还在上学,珍惜时光吧。如果你已经在职场,问问这些东西你懂不懂?


​ 最后,我想说前端路漫漫,以上只是我的个人一些感触,北漂7年,冷暖自知。月薪3万不是终点,是起点。在北京3万工资虽不高,但是你可以稍微更好的照顾你爱的人,可以更好地照顾父母,也可以在兄弟有难的时候更好的挺他们一下。写这篇文章也是想给同路人一点点建议,不喜勿喷,还有很多细节总结的也不是很全,见谅。以下是我总结的前端技术图谱, 江湖路远,有缘再见。

clipboard.png

作者 志佳老师
2019 年 02月 10日

本文首发微信公众号:jingchengyideng

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章



查看原文

tuobaye0711 赞了回答 · 2018-12-12

解决关于 sort() 的奇怪问题

arr.sort(compare)判断的依据compare(a,b)的返回值

  • compare( a, b) < 0 则 a 的排序在 b 之前
  • compare( a, b) = 0 则排序不变(某些老版本的浏览器不遵守这一条)
  • compare( a, b) > 0 则 b 在 a 前面

你的问题应该是在chrome上出现的,当使用node环境的时候,依然是你的第一个代码,却可以正确排序,其中的区别就是,node用的是冒泡排序而chrome用的是插入排序,具体你可以用下面的代码验证一下,分别在chromenode上测试下,查看输出的变化

var a = ['aa', 'vv', 'bb', 'ff', 'dd']
a.sort((a, b) => {
  console.log(a > b, a, b)
  return a > b
})

如果对排序算法有疑问可以看下面这个文章
十大经典排序算法总结(JavaScript描述)

如果对各个浏览器的排序算法有兴趣可以看看下面这个文章
深入浅出 JavaScript 的 Array.prototype.sort 排序算法

当然最好的记忆就是自己亲自经历过的,自己在不同平台上做下测试,会记忆的更深一些.

最后官方的文档
MDN Array.prototype.sort()

关注 3 回答 2

tuobaye0711 评论了文章 · 2018-12-10

你可能不知道的14个JavaScript调试技巧

了解你的工具可以在完成任务的过程中发挥重大作用。尽管传言 JavaScript 难以调试,但是如果你掌握了一些调试技巧,那么你将会花费更少的时间来解决这些错误。

我们已经列出了14个你可能不知道的调试技巧,但可能要记住,这样下次你需要调试 JavaScript 代码时就可以马上使用了!

现在就马上开始。

1. ‘debugger;’

除了 console.log , debugger; 是我们最喜欢、快速且肮脏的调试工具。一旦执行到这行代码,Chrome 会在执行时自动停止。 你甚至可以使用条件语句加上判断,这样可以只在你需要的时候运行。愚人码头注:本人实在觉得这种调试方面很不好,因为后续的调试步骤和断点调试没什么区别。而且调试完成后,还要记住删掉这行代码。确实有点肮脏。

JavaScript 代码:

if (thisThing) {
debugger;
}

2. 将 objects 显示为表格

有时,你有一个复杂的对象要查看。你可以用 console.log 查看并滚动浏览该对象,或者使用console.table展开,更容易看到正在处理的内容!

JavaScript 代码:

var animals = [
{ animal: 'Horse', name: 'Henry', age: 43 },
{ animal: 'Dog', name: 'Fred', age: 13 },
{ animal: 'Cat', name: 'Frodo', age: 18 }
];
console.table(animals);

输出:

3. 尝试所有的屏幕尺寸

虽然在桌面设备上安装不同移动设备模拟器非常棒,但在现实世界中并不可行。 应该是调整你的可视窗口,而不是替换移动设备? Chrome为你提供所需的一切。 进入Chrome 开发者调试工具,然后点击 ‘toggle device mode(切换设备模式)’ 按钮。 实时观察窗口变化即可!

4. 如何快速找到DOM元素

在 Elements(元素) 面板中标记 DOM 元素,并可以在 console(控制台) 中使用它。Chrome 检测器会保留其历史记录中的最后 5 个元素,以便最终标记的元素显示 $0 ,倒数第二个标记元素 $1 ,依此类推。

如果你按照“item-4”,“item-3”,“item-2”,“item-1”,“item-0”的顺序标记下列项,则可以在控制台中像这样访问DOM节点:

5. 使用 console.time() 和 console.timeEnd() 来标记循环耗时

要确切地知道某段代码需要执行多长时间,尤其是在调试慢循环时,可能会非常有用。您甚至可以通过为该方法分配标签来设置多个定时器。让我们看看它是如何工作的:

JavaScript 代码:

console.time('Timer1');
var items = [];
for(var i = 0; i < 100000; i++){
items.push({index: i});
}
console.timeEnd('Timer1');

运行产生了如下结果:

6. 获取函数的堆栈跟踪信息

你可能知道JavaScript框架,会引入大量代码。

它创建视图触发事件,而且你最终会想知道函数调用是怎么发生的。

因为 JavaScript 不是一个很结构化的语言,有时候很难完整的了解到底 发生了什么 以及 什么时候发生 的。 使用 console.trace((仅仅只是在控制台中跟踪) 可以方便地调试JavaScript 。

假设你现在想看 car 实例在第24行调用 funcZ 函数的完整堆栈轨迹信息:

JavaScript 代码:

var car; 
var func1 = function() {
func2();
} 
var func2 = function() {
func4();
}
var func3 = function() {
} 
var func4 = function() {
car = new Car();
car.funcX();
}
var Car = function() {
this.brand = ‘volvo’;
this.color = ‘red’;
this.funcX = function() {
this.funcY();
}
this.funcY = function() {
this.funcZ();
}
this.funcZ = function() {

24行将输出:

现在我们可以看到 func1 调用 func2, func2 调用 func4。 Func4 创建了一个 Car 的实例,然后调用函数 car.funcX,依此类推。

即使你认为非常了解自己的代码,这种分析仍然可以让你感到很方便。假如你想改进你的代码。获取跟踪信息和所有涉及的函数名单,每一项都可以点击,你可以在他们之间来回切换。这就像一个特地为你准备的菜单。

7. 美化代码使调试 JavaScript 变得简单

有时你可能在生产环境中遇到问题,但是你的source maps没有部署在服务器上。 不要害怕 。Chrome 可以将你的 Javascript 文件美化为更易阅读的格式。虽然代码不会像你的真实代码那样有用 – 但至少你可以看到发生了什么。点击检查器中源代码查看器下方的 {} 美化按钮即可。

8. 快速查找要调试的函数

假设你想在一个函数中设置一个断点。

最常见的两种方法是:

1.在源代码查看器查找到相应的行,并添加一个断点
2.在代码中添加debugger

在这两个解决方案中,您必须在文件中单击以调试特定行。

使用控制台打断点可能不太常见。在控制台中使用 debug(funcName),当到达传入的函数时,代码将停止。

这个调试方法很快, 但缺点是不适用于私有函数或匿名函数。但除了私有和匿名函数, 这可能是找到调试函数的最快方法。(注意:这个函数和console.debug 函数是不同的东西。)

JavaScript 代码:

var func1 = function() {
func2();
};
var Car = function() {
this.funcX = function() {
this.funcY();
}
this.funcY = function() {
this.funcZ();
}
}
var car = new Car();

在控制台中输入 debug(car.funcY) ,当调用 car.funcY 时,脚本将以调试模式停止:

9. 屏蔽不相关的代码

现在,我们经常在应用中引入多个库或框架。其中大多数都经过良好的测试且相对没有陷阱。 但是,调试器仍然会进入与调试任务无关的文件。解决方案是屏蔽不需要调试的脚本。当然也可以包括你自己的脚本。

10. 在复杂的调试过程中寻找重点

在更复杂的调试中,我们有时希望输出很多行。你可以做的事情就是使用更多控制台函数来保持良好的输出结构,例如, console.log, console.debug, console.warn, console.info, console.error等等。然后,可以在控制台中快速浏览。但有时候,某些 JavaScrip 调试信息并不是你需要的。现在,可以自己美化调试信息了。在调试 JavaScript 时,可以使用 CSS 并自定义控制台信息:

JavaScript 代码:

console.todo = function(msg) {
console.log(‘ % c % s % s % s‘, ‘color: yellow; background - color: black;’, ‘–‘, msg, ‘–‘);
}
console.important = function(msg) {
console.log(‘ % c % s % s % s’, ‘color: brown; font - weight: bold; text - decoration: underline;’, ‘–‘, msg, ‘–‘);
}
console.todo(“This is something that’ s need to be fixed”);
console.important(‘This is an important message’);

输出:

例如:

在 console.log() 中, 可以用 %s 设置字符串,%i 设置数字,%c 设置自定义样式等等,还有很多更好的 console.log() 使用方法。 如果使用的是单页应用框架,可以为视图(view)消息创建一个样式,为模型(models),集合(collections),控制器(controllers)等创建另一个样式。也许还可以像 wlog,clog 和 mlog 一样发挥想象力!

11. 观察特定函数的调用及其参数

在 Chrome 控制台中,您可以关注特定的函数。 每次调用该函数时,都会对传入的参数值进行记录。

JavaScript 代码:

var func1 = function(x, y, z) {
//....
};

输出:

这是查看哪些参数传递给函数的好方法。 但是我必须说,如果控制台可以告诉我们需要多少参数,那将是一件好事。 在上面的例子中,func1 期望 3个参数,但是只有 2 个参数被传入。如果在代码中没有处理这个参数,它可能导致一个可能的 bug 。

12. 在控制台中快速访问元素

在控制台中执行 querySelector 一种更快的方法是使用美元符。$('css-selector') 将会返回CSS选择器的第一个匹配项。$$('css-selector') 将会返回所有匹配项。如果多次使用一个元素,可以把它保存为一个变量。

13. Postman 很棒(但Firefox更快)

许多开发人员使用 Postman 查看ajax请求。Postman真的很优秀。但打开一个新的浏览器窗口,新写一个请求对象来测试,这确实显得很麻烦。

有时使用浏览器更容易。

当你使用浏览器查看时,如果请求一个密码验证页面,你不需要担心身份验证的cookie。下面看,在Firefox中如何编辑并重新发送请求。

打开检查员并转到网络选项卡。 右键单击所需的请求,然后选择编辑并重新发送。 现在你可以改变任何你想要的。 更改标题并编辑您的参数并点击重新发送。

下面我用不同的属性提出两次请求:

14. 节点变化时中断

DOM 是一个有趣的东西。 有时候它会被修改,但是你并不知道为什么。 但是,当您需要调试 JavaScript 时,Chrome会让您在DOM元素发生更改时暂停。 你甚至可以监视它的属性。 在Chrome 检查器中,右键单击该元素,然后在设置中选择一个中断就可以了:

这里推荐一下我的前端学习交流群:784783012,自己整理了一份2018最全面前端学习资料,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到移动端HTML5的项目实战的学习资料都有整理,送给每一位前端小伙伴
查看原文

认证与成就

  • 获得 216 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-25
个人主页被 1.2k 人浏览