蓝色的秋风

蓝色的秋风 查看完整档案

杭州编辑  |  填写毕业院校美团  |  web前端 编辑 qiufeng.blue 编辑
编辑

JavaScript开发爱好者。全栈工程师。

📬微信公众号:秋风的笔记
📘博客主页:https://qiufeng.blue

个人动态

蓝色的秋风 赞了文章 · 1月19日

原来地图导航结合WebAR技术还能这么玩

本文探索在Web前端实现AR导航效果的前沿技术和难点。

1. AR简介

增强现实(Augmented Reality,简称AR):是一种实时地计算摄影机影像的位置及角度并加上相应图像、视频、3D模型的技术,这种技术的目标是在屏幕上把虚拟世界套在现实世界并进行互动。

一般在web中实现AR效果的主要步骤如下:

  1. 获取视频源
  2. 识别marker
  3. 叠加虚拟物体
  4. 显示最终画面

AR导航比较特殊的地方是,它并非通过识别marker来确定虚拟物体的叠加位置,而是通过定位将虚拟和现实联系在一起,主要步骤如下:

  1. 获取视频源
  2. 坐标系转换:

    1. 获取设备和路径的绝对定位
    2. 计算路径中各标记点与设备间的相对定位
    3. 在设备坐标系中绘制标记点
  3. 3D图像与视频叠加
  4. 更新定位和设备方向,控制Three.js中的相机移动

2. 技术难点

如上文所述AR导航的主要步骤,其中难点在于:

  1. 兼容性问题
  2. WebGL三维作图
  3. 定位的精确度和轨迹优化
  4. 虚拟和现实单位尺度的映射

2.1 兼容性问题:

不同设备不同操作系统以及不同浏览器带来的兼容性问题主要体现在对获取视频流和获取设备陀螺仪信息的支持上。

2.1.1 获取视频流

  1. Navigator API兼容处理

    navigator.getUserMedia()已不推荐使用,目前新标准采用navigator.mediaDevices.getUserMedia()。可是不同浏览器对新方法的支持程度不同,需要进行判断和处理。同时,如果采用旧方法,在不同浏览器中方法名称也不尽相同,比如webkitGetUserMedia

//不支持mediaDevices属性
    if (navigator.mediaDevices === undefined) {
      navigator.mediaDevices = {};
    }

//不支持mediaDevices.getUserMedia
    if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = function(constraints) {
            var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

            if(!getUserMedia) {
                return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
            }

            return new Promise(function(resolve, reject) {
                getUserMedia.call(navigator, constraints, resolve, reject);
            });
        }
    }
  1. 参数兼容处理

    getUserMedia接收一个MediaStreamConstraints类型的参数,该参数包含两个成员videoaudio

var constraints = {
        audio: true,
        video: {
            width: { 
                min: 1024,
                ideal: 1280,
                max: 1920
            },
            height: 720,
            frameRate: {
                ideal: 10,
                max: 15
            },
            facingMode: "user" // user/environment,设置前后摄像头
        }
    }

在使用WebAR导航时,需要调取后置摄像头,然而facingMode参数目前只有Firefox和Chrome部分支持,对于其他浏览器(微信、手Q、QQ浏览器)需要另一个参数optional.sourceId,传入设备媒体源的id。经测试,该方法在不同设备不同版本号的微信和手Q上表现有差异。

if(MediaStreamTrack.getSources) {
        MediaStreamTrack.getSources(function (sourceInfos) {
            for (var i = 0; i != sourceInfos.length; ++i) {
                var sourceInfo = sourceInfos[i];
                //这里会遍历audio,video,所以要加以区分  
                if (sourceInfo.kind === 'video') {  
                    exArray.push(sourceInfo.id);  
                }  
            }
            constraints = { 
                video: {  
                    optional: [{  
                        sourceId: exArray[1] //0为前置摄像头,1为后置
                    }]  
                }
            };
        });
    } else {
        constraints = { 
            video: {
                facingMode: {
                    exact: 'environment'
                }
            }
        });
    }
  1. 操作系统的兼容性问题

    由于苹果的安全机制问题,iOS设备任何浏览器都不支持getUserMedia()。所以无法在iOS系统上实现WebAR导航。

  2. 协议

    出于安全考虑,Chrome47之后只支持HTTPS页面获取视频源。

2.1.2 获取设备转动角度

设备的转动角度代表了用户的视角,也是连接虚拟和现实的重要参数。HTML5提供DeviceOrientation API可以实时获取设备的旋转角度参数。通过监听deviceorientation事件,返回DeviceOrientationEvent对象。

{
    absolute: [boolean] 是否为绝对转动值
    alpha: [0-360]
    beta: [-180-180]
    gamma: [-90-90]
}

其中alpha、beta、gamma是我们想要获取的角度,它们各自的意义可以参照下图和参考文章:

image
陀螺仪的基本知识

然而iOS系统的webkit内核浏览器中,该对象还包括webkitCompassHeading成员,其值为设备与正北方向的偏离角度。同时iOS系统的浏览器中,alpha并非绝对角度,而是以开始监听事件时的角度为零点。

Android系统中,我们可以使用-alpha得到设备与正北方的角度,但是就目前的测试情况看来,该值并不稳定。所以在测试Demo中加入了手动校正alpha值的过程,在导航开始前将设备朝向正北方来获取绝对0度,虽然不严谨但效果还不错。

image

2.2 WebGL三维作图

WebGL是在浏览器中实现三维效果的一套规范,AR导航需要绘制出不同距离不同角度的标记点,就需要三维效果以适应真实场景视频流。然而WebGL原生的接口非常复杂,Three.js是一个基于WebGL的库,它对一些原生的方法进行了简化封装,使我们能够更方便地进行编程。

Three.js中有三个主要概念:

  1. 场景(scene):物体的容器,我们要绘制标记点就是在场景中添加指定坐标和大小的球体
  2. 相机(camera):模拟人的眼睛,决定了呈现哪个角度哪个部分的场景,在AR导航中,我们主要通过相机的移动和转动来模拟设备的移动和转动
  3. 渲染器(renderer):设置画布,将相机拍摄的场景呈现在web页面上

在AR导航的代码中,我对Three.js的创建过程进行了封装,只需传入DOM元素(一般为<div>,作为容器)和参数,自动创建三大组件,并提供了Three.addObjectThree.renderThree等接口方法用于在场景中添加/删除物体或更新渲染等。

function Three(cSelector, options) {
    var container = document.querySelector(cSelector);
    // 创建场景
    var scene = new THREE.Scene();
    // 创建相机
    var camera = new THREE.PerspectiveCamera(options.camera.fov, options.camera.aspect, options.camera.near, options.camera.far);
    // 创建渲染器
    var renderer = new THREE.WebGLRenderer({
        alpha: true
    });
    // 设置相机转动控制器
    var oriControls = new THREE.DeviceOrientationControls(camera);
    // 设置场景大小,并添加到页面中
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setClearColor(0xFFFFFF, 0.0);
    container.appendChild(renderer.domElement);

    // 暴露在外的成员
    this.main = {
        scene: scene,
        camera: camera,
        renderer: renderer,
        oriControls: oriControls,
    }
    this.objects = [];
    this.options = options;
}
Three.prototype.addObject = function(type, options) {...} // 向场景中添加物体,type支持sphere/cube/cone
Three.prototype.popObject = function() {...} // 删除场景中的物体
Three.prototype.setCameraPos = function(position) {...} // 设置相机位置
Three.prototype.renderThree = function(render) {...} // 渲染更新,render为回调函数
Three.prototype.setAlphaOffset = function(offset) {..} // 设置校正alpha的偏离角度

在控制相机的转动上,我使用了DeviceOrientationControls,它是Three.js官方提供的相机跟随设备转动的控制器,实现对deviceorientation的侦听和对DeviceOrientationEvent的欧拉角处理,并控制相机的转动角度。只需在渲染更新时调用一下update方法:

three.renderThree(function(objects, main) {
    animate();
    function animate() {
        window.requestAnimationFrame(animate);
        main.oriControls.update();
        main.renderer.render(main.scene, main.camera);
    }
});

2.3 定位的精确度和轨迹优化

我们的调研中目前有三种获取定位的方案:原生navigator.geolocation接口,腾讯前端定位组件,微信JS-SDK地理位置接口:

  1. 原生接口

    navigator.geolocation接口提供了getCurrentPositionwatchPosition两个方法用于获取当前定位和监听位置改变。经过测试,Android系统中watchPosition更新频率低,而iOS中更新频率高,但抖动严重。

  2. 前端定位组件

    使用前端定位组件需要引入JS模块(https://3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js),通过 qq.maps.Geolocation(key, referer)构造对象,也提供getLocationwatchPosition两个方法。经过测试,在X5内核的浏览器(包括微信、手Q)中,定位组件比原生接口定位更加准确,更新频率较高。

  3. 微信JS-SDK地理位置接口

    使用微信JS-SDK接口,我们可以调用室内定位达到更高的精度,但是需要绑定公众号,只能在微信中使用,仅提供getLocation方法,暂时不考虑。

    综上所述,我们主要考虑在X5内核浏览器中的实现,所以选用腾讯前端定位组件获取定位。但是在测试中仍然暴露出了定位不准确的问题:

    1. 定位不准导致虚拟物体与现实无法准确叠加
    2. 定位的抖动导致虚拟标记点跟随抖动,移动视觉效果不够平稳

针对该问题,我设计了优化轨迹的方法,进行定位去噪、确定初始中心点、根据路径吸附等操作,以实现移动时的变化效果更加平稳且准确。

2.3.1 定位去噪

我们通过getLocationwatchPosition方法获取到的定位数据包含如下信息:

{
    accuracy: 65,
    lat: 39.98333,
    lng: 116.30133
    ...
}

其中accuracy表示定位精度,该值越低表示定位越精确。假设定位精度在固定的设备上服从正态分布(准确来说应该是正偏态分布),统计整条轨迹点定位精度的均值mean和标准差stdev,将轨迹中定位精度大于mean + (1~2) * stdev的点过滤掉。或者采用箱型图的方法去除噪声点。

2.3.2 初始点确定

初始点非常重要,若初始点偏离,则路线不准确、虚拟现实无法重叠、无法获取到正确的移动路线。测试中我发现定位开始时获得的定位点大多不太准确,所以需要一段时间来确定初始点。

定位开始,设置N秒用以获取初始定位。N秒钟获取到的定位去噪之后形成一个序列track_denoise = [ loc0, loc1, loc2...],对该序列中的每一个点计算其到其他点的距离之和,并加上自身的定位精度,得到一个中心衡量值,然后取衡量值最小的点为起始点。

image

2.3.3 基于路线的定位校正

基于设备始终跟随规划路线进行移动的假设,可以将定位点吸附到规划路线上以防止3D图像的抖动。

如下图所示,以定位点到线段的映射点作为校正点。路线线段的选择依据如下:

  1. 初始状态:以起始点与第二路线点之间的线段为当前线段,cur = 0; P_cur = P[cur];
  2. 在第N条线段上移动时,若映射长度(映射点与线段起点的距离)为负,校正点取当前线段的起点,线路回退至上一线段,cur = N - 1; P_cur = P[cur];;若映射长度大于线段长度,则校正点取当前线段的终点,线路前进至下一线段,cur = N + 1; P_cur = P[cur];
  3. 若当前线段与下一线段的有效范围有重叠区域(如下图绿色阴影区),则需判断定位点到两条线段的距离,以较短的为准,确定校正点和线路选择。

image

2.4 虚拟和现实的单位长度映射

WebGL中的单位长度与现实世界的单位长度并没有确定的映射关系,暂时还无法准确进行映射。通过测试,暂且选择1(米):15(WebGL单位长度)。

3. demo演示

演示视频:WebAR技术探索-导航中的应用

对地图感兴趣的开发者,欢迎登录腾讯位置服务体验~

以下内容转载自多多洛爱学习的文章《WebAR技术探索-导航中的应用》
作者:多多洛爱学习
链接:https://juejin.im/post/5c24252b6fb9a049d975411a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
查看原文

赞 5 收藏 4 评论 0

蓝色的秋风 关注了用户 · 1月19日

腾讯位置服务 @tengxunweizhifuwu

立足生态,连接未来

关注 17

蓝色的秋风 发布了文章 · 1月14日

2020 全球 JS 调查报告新鲜出炉

完整报告地址: https://2020.stateofjs.com/zh-Hans/
润色/翻译: 蓝色的秋风(github/hua1995116)

千呼万唤的全球2020的JS报告终于出来了。顺便附上2020全球CSS报告地址 2020年度全球CSS报告新鲜出炉

image-20210114001259007

我们来看看这一个糟糕却又不平凡的一年,JS发生了什么样的变化。

image-20210114001739976

尽管2020年很糟糕,但 JavaScript 作为一个整体仍然设法向前发展。随着语言本身的不断改进,得益于诸如可选链操作符空值合并操作符并等新特性,TypeScript静态类型的普及更是将JS带到了一个全新的高度。

在框架方面,就在我们认为一切都已解决的时候,Svelte 横空出世以全新方式给前端注入新的血液。 在多年的webpack统治下,甚至构建工具也显示出新活动的迹象。

但是这次的区别是,相对而言,“老”后卫什么都没走。 Svelte和Snowpack很棒,但是React和webpack也很棒。 可以肯定的是,它们最终也会成为JavaScript大流氓的牺牲品,但是不会持续很多年。

所以,让我们享受我们所拥有的: 一个不断变得更好的伟大的生态系统!

访问对象统计

采样对象一共为 20744 位开发者。

image-20210114003014449

特性

虽然大多数受访者都知道调查中提到的大多数JavaScript特性,但很多人还没有真正使用它们。

这图表显示了按类别分组的所有特性的不同采用率。外圈的大小对应于了解某项功能的用户总数,而内圈则代表实际使用过该功能的用户。

image-20210114003419236

技术现状

2016年 - 2020年 趋势图

每条线从2016年到2020年(粗部为2020)。纵轴越高,表示一项技术被更多的人使用,横轴越大,表示有更多的用户想要学习,或者曾经使用过,还会再次使用。

image-20210114004041187

可以看出随着年限的的增长。webpack、Express、TypeScript、Jest、React 可以说是非常强势了。

风味(Flavors)

image-20210114011042569

可以看出 TypeScript 依旧独领风骚,其次就是 Elm ,但是 PureScript 也是一个值得关注的增强类型语言。

image-20210114004722545

对 TypeScript 的熟悉度一片叫好。

其他工具

image-20210114004757675

前端框架

image-20210114011217904

正如开头所说,svelte 的出现真的是对前端行业的冲击,原以为三大框架(React、Vue.js、Angular)包揽所有的时候,它出现了,一度成为了第四名(使用量),但是从兴趣度和满意度来看,它未来的潜力不可估量。

兴趣度

image-20210114011448248

满意度

image-20210114011511595

数据层

image-20210114011556857

使用排名比较高的状态管理依旧是Redux、Vuex、Mobx。 数据管理为 GraphQL 和 Apollo,并且 XState 横空出世。

其他工具

image-20210114005333559

后端框架

image-20210114011713944

Express 依旧是统治地位,而 Next 和 Nuxt 这些服务端渲染的框架也逐渐成为大家的所选的框架。

其他工具

image-20210114005400642

测试框架

image-20210114011830965

Jest和 Mocha 在使用量上依旧是统治地位,但是新增了 Testing Libray 很强劲。

以下是满意度排行。

image-20210114005427559

什么是 Testing Library ?用于 DOM 和 UI 组件测试的一系列工具,主要 API 包含 DOM 查询,更可以和其他测试工具(jest、cypress)配合,用于更多场景(react、vue、svelte)。而它是 React 的官方推荐。

我们推荐使用 React Testing Library,它使得针对组件编写测试用例就像终端用户在使用它一样方便。

----摘自 React 官网(https://zh-hans.reactjs.org/docs/test-utils.html)

打包工具

image-20210114011945951

虽然短时间内 webpack 使用量还处于霸主地位,这一年打包工具的发生了巨大的变化。

以下为满意度

image-20210114010039881

可以说这里发生了天翻地覆的变化。从 Parcel 到 Snowpack ,再到后来的 esbuild ,每一个都是打包的好手,至于 Vite 为什么没有在其中,我猜想,Vite 最开始只是为了解决 Vue 单个框架的方向,受众面不够广泛(现在它已经支持了多种框架的打包了)。

放张图来看看这些 bundleless 工具的速度吧。

image-20210114010649085

其他工具

image-20210114010412943

移动和桌面端

image-20210114012121186

Electron 依旧是桌面端的第一选择, Cordova 和 React Native 也是移动跨端的热门选择。但是新出的 Capacitor 值得关注。

其他工具

常用的工具函数库有?

image-20210114012305700

其他工具函数库

image-20210114012350566

JavaScript 运行时选择

image-20210114012435704

经常使用那(些)文字編輯器?

image-20210114012456227

常用用于开发的浏览器有哪些?

image-20210114012519560

资料

常用的 blog 和杂志?

image-20210114012556370

关注了哪些网站和课程?

image-20210114012616128

最后

如果我的文章有帮助到你,希望你也能帮助我,欢迎关注我的微信公众号 秋风的笔记,回复好友 二次,可加微信并且加入交流群,秋风的笔记 将一直陪伴你的左右。

image

查看原文

赞 10 收藏 3 评论 5

蓝色的秋风 发布了文章 · 2020-12-09

2020年度全球CSS报告新鲜出炉

介绍

CSS 从 1994 年 10 月首次被提出,到目前为止已经20余年,但是 CSS 早已发生了天翻地覆的变化,2020的CSS 又是如何的呢?

我们现在可以使用 CSS Grid 轻松制作动态或响应式的布局,以更少的代码来进行自适应布局。 CSS-in-JS 无需依赖全局样式表,我们可以将样式与组件写在一起去构建主题化的设计系统。

最重要的是,Tailwind CSS 突然出现,通过它的实用至上的 CSS 的类名使用,迫使我们重新考虑传统的语义类名称的设计。

本次调查一共统计了 10k+ 的人,由 Sacha Greif 设计、写作以及编码,Raphaël Benitte进行数据分析和数据可视化。还有包括Chen Hui-Jing, Philip Jägenstedt, Adam Argyke, Ahmad Shadeed, Robert Flack, Dominic Nguyen, Fantasai, and Kilian Valkhof. 等人的努力。

本次主要可以从6个方向(新特性、单位和选择器、CSS技术、CSS工具库、CSS使用环境和学习CSS渠道)进行了深度的报告CSS的使用学习情况,从本次调查,可以让你了解目前最流行的布局,最前沿的特性以及前沿的技术库等等~ (本文会举例个人觉得最值得讲的进行描述~,当然我觉得整个报告都非常的迷人,都非常值得看,但是因为篇幅原因,我对某些部分进行了删减,强烈建议去看完整版!!! https://2020.stateofcss.com/zh-Hans/)

先通过 5 张图来看看本次调查对象的样本构成。

采样人员分布

image-20201208121213392

人员的薪资分布

image-20201208121309948

工作年限

image-20201208121354726

工作岗位

image-20201208121423025

CSS 熟练程度

image-20201208121459800

新特性

近年来,CSS出现了大量的新特性,但是社区需要时间来吸收新特性,所以CSS的一些新特性的采用率速度有点慢。

以下图表显示了按类别分组的所有特性的不同采用率。

外圈的大小对应于了解某项功能的用户总数,而内圈则代表实际使用过该功能的用户。

image-20201208121618557

布局

The-State-of-CSS-2020-layout

也许 Grid 和 Flexbox 对你来说是最熟悉的,从上表也能看出来大部分的人使用了 flex,因为通过它,只要写很少的代码就能写出多样化的代码。但是 Grid 在今年的调查中可以说上升的趋势很快。

还有像 SubgridMulti-Column Layout 你可能不熟悉。但是相信如果看过 今年2020 web.dev live 的小伙伴一定记得 Ten modern layouts in one line of CSS ,里面就大量使用了 Subgrid 的特性,仅仅用一行代码实现现在流行的,自适应垂直居中、三列布局、圣杯布局和双飞翼布局等等布局。(也强烈建议看上面那篇文章,当我想翻译那篇文章的时候,发现掘金已经有人先翻译了,英文看着吃力的可以去搜中文版)

图形与图像

The-State-of-CSS-2020-img

还在烦恼图片的适配问题吗?也许你可以使用 object-fit试试。

交互

The-State-of-CSS-2020-jiaohu

还记得我在上一篇从破解某设计网站谈前端水印(详细教程)中讲的 pointer-events

排版

The-State-of-CSS-2020-paiban

需要多行... 的时候,line-clamp 是个好帮手。

动画与过度

The-State-of-CSS-2020-animation

TransitionsTransformsAnimations 依旧是当下主流的动画方式。

媒体查询

1607440199210

prefers-color-scheme 眼熟吗,利用好它我们就可以适配 mac 的深色模式~

其他特征

The-State-of-CSS-2020-qita

calc 帮助了我们计算单位,提前声明will-change 有助于我们处理动画时候提高性能。

单位和选择器

The-State-of-CSS-2020-selector

The-State-of-CSS-2020-单selector-2

px/%/em/rem肯定是老牌CSS 单位,但是vh,vw 的使用率也逐渐上升了~

CSS技术

CSS 生态系统正在经历各种更新,因为像 Bootstrap 这样的较老的主流现在必须适应 Tailwind CSS 等较新的参与者。 更不用说整个 CSS-in-JS 运动了,尽管它还没有成为 CSS 的主流,但是它是非常具有潜力的。

预/后处理

满意度、兴趣、使用和知晓率排名。

image-20201208205203721

SaSS 依旧是大哥大,这里可以提一下 libsass 已经弃用,已经使用了 dart-sass,社区各个正在对齐中,以后再也不用担心 node-sass 安装编译出错了。

CSS框架

满意度、兴趣、使用和知晓率排名。

image-20201208205332360

CSS 框架这里真的是神仙打架,又诞生了一些新的工具库,但是 Tailwind CSS 依旧处于不可撼动的地位 (想起了几年前还是 BootStrap 霸榜,不禁感叹自己真的老了老了。)

CSS 命名规范

满意度、兴趣、使用和知晓率排名。

image-20201208205647076

各个规范比较可以看 https://clubmate.fi/oocss-acss-bem-smacss-what-are-they-what-should-i-use/

规范是写组件库的时候尤其重要的一环。

CSS-in-JS

满意度、兴趣、使用和知晓率排名。

image-20201208205709411

随着 React 这样的库兴起,CSS-in-JS 写起来真的太爽了。著名的框架 Material UI(实现了 Google 的 Material Design)就是采用的这样的模式。

CSS工具库

1607432332485-1

1607432332485-2

CSS使用环境

image-20201208210249148

image-20201208210304523

CSS 已经越来越趋于多终端设备化了,不仅限于PC/Mobilede 。

学习CSS渠道

image-20201208210330009

image-20201208210404624

再推荐两个国内个人比较看好的CSS博客 一个是张鑫旭的博客(https://www.zhangxinxu.com/wordpress/)另一个是国服第一切图仔的博客(https://github.com/chokcoco/iCSS/issues)

最后

如果我的文章有帮助到你,希望你也能帮助我,欢迎关注我的微信公众号 秋风的笔记,回复好友 二次,可加微信并且加入交流群,秋风的笔记 将一直陪伴你的左右。

image

查看原文

赞 18 收藏 17 评论 0

蓝色的秋风 关注了用户 · 2020-12-08

若川 @lxchuan12

微信公众号:若川视野,欢迎关注呀~
某不那么知名的陶瓷大学毕业生,目前在杭州从事前端开发工作。常以若川为名混迹于江湖。

github blog
求个star^_^

微信号:ruochuan12,注明来源来者不拒

关注 498

蓝色的秋风 发布了文章 · 2020-12-03

从破解某设计网站谈前端水印(详细教程)

前言

最近在写公众号的时候,常常会自己做首图,并且慢慢地发现沉迷于制作首图,感觉扁平化的设计的真好好看。慢慢地萌生了一个做一个属于自己的首图生成器的想法。

haibai-shuiyin

制作呢,当然也不是拍拍脑袋就开始,在开始之前,就去研究了一下某在线设计网站(如果有人不知道的话,可以说一下,这是一个在线制作海报之类的网站 T T 像我们这种内容创作者用的比较多),毕竟人家已经做了很久了,我只是想做个方便个人使用的。毕竟以上用 PS 做着还是有一些废时间,由于组成的元素都很简单,做一个自动化生成的完全可以。

但是研究着研究着,就看到了某在线设计网站的水印,像这种技术支持的网站,最重要的防御措施就是水印了,水印能够很好的保护知识产权。

慢慢地路就走偏了,开始对它的水印感兴趣了。不禁发现之前只是大概知道水印的生成方法,但是从来没有仔细研究过,本文将以以下的路线进行讲解。以下所有代码示例均在

https://github.com/hua1995116/node-demo/tree/master/watermark

watermark-simple

明水印

水印(watermark)是一种容易识别、被夹于内,能够透过光线穿过从而显现出各种不同阴影的技术。

水印的类型有很多,有一些是整图覆盖在图层上的水印,还有一些是在角落。

image-20201128172843243

image-20201122184251938

那么这个水印怎么实现呢?熟悉 PS 的朋友,都知道 PS 有个叫做图层的概念。

image-20201123230350901

网页也是如此。我们可以通过绝对定位,来将水印覆盖到我们的页面之上。

image-20201123230659874

最终变成了这个样子。

1606144217031

等等,但是发现少了点什么。直接覆盖上去,就好像是一个蒙层,我都知道这样是无法触发底下图层的事件的,此时就要介绍一个css属性pointer-events

pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target

当它的被设置为 none 的时候,能让元素实体虚化,虽然存在这个元素,但是该元素不会触发鼠标事件。详情可以查看 CSS3 pointer-events:none应用举例及扩展 « 张鑫旭-鑫空间-鑫生活

这下理清了实现原理,等于成功了一半了!

72_c2ae29ca4f8c9769e1f8792146c8365c

明水印的生成

明水印的生成方式主要可以归为两类,一种是 纯 html 元素(纯div),另一种则为背景图(canvas/svg)。

下面我分别来介绍一下,两种方式。

div实现

我们首先来讲比较简单的 div 生成的方式。就按照我们刚才说的。

// 文本内容
<div class="app">
        <h1>秋风</h1>
        <p>hello</p>
</div> 

首先我们来生成一个水印块,就是上面的 一个个秋风的笔记。这里主要有一点就是设置一个透明度(为了让水印看起来不是那么明显,从而不遮挡我们的主要页面),另一个就是一个旋转,如果是正的水平会显得不是那么好看,最后一点就是使用 userSelect属性,让此时的文字无法被选中。

userSelect

CSS 属性 user-select 控制用户能否选中文本。除了文本框内,它对被载入为 chrome 的内容没有影响。
function cssHelper(el, prototype) {
  for (let i in prototype) {
    el.style[i] = prototype[i]
  }
}
const item = document.createElement('div')
item.innerHTML = '秋风的笔记'
cssHelper(item, {
  position: 'absolute',
  top: `50px`,
  left: `50px`,
  fontSize: `16px`,
  color: '#000',
  lineHeight: 1.5,
  opacity: 0.1,
  transform: `rotate(-15deg)`,
  transformOrigin: '0 0',
  userSelect: 'none',
  whiteSpace: 'nowrap',
  overflow: 'hidden',
}) 

有了一个水印片,我们就可以通过计算屏幕的宽高,以及水印的大小来计算我们需要生成的水印个数。

const waterHeight = 100;
const waterWidth = 180;
const { clientWidth, clientHeight } = document.documentElement || document.body;
const column = Math.ceil(clientWidth / waterWidth);
const rows = Math.ceil(clientHeight / waterHeight);
for (let i = 0; i < column * rows; i++) {
    const wrap = document.createElement('div');
    cssHelper(wrap, Object.create({
        position: 'relative',
        width: `${waterWidth}px`,
        height: `${waterHeight}px`,
        flex: `0 0 ${waterWidth}px`,
        overflow: 'hidden',
    }));
    wrap.appendChild(createItem());
    waterWrapper.appendChild(wrap)
}
document.body.appendChild(waterWrapper) 

这样子我们就完美地实现了上面我们给出的思路的样子啦。

image-20201130003407877

背景图实现

canvas

canvas的实现很简单,主要是利用canvas 绘制一个水印,然后将它转化为 base64 的图片,通过canvas.toDataURL() 来拿到文件流的 url ,关于文件流相关转化可以参考我之前写的文章一文带你层层解锁「文件下载」的奥秘, 然后将获取的 url 填充在一个元素的背景中,然后我们设置背景图片的属性为重复。

.watermark {
    position: fixed;
    top: 0px;
    right: 0px;
    bottom: 0px;
    left: 0px;
    pointer-events: none;
    background-repeat: repeat;
} 
function createWaterMark() {
  const angle = -20;
  const txt = '秋风的笔记'
  const canvas = document.createElement('canvas');
  canvas.width = 180;
  canvas.height = 100;
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, 180, 100);
  ctx.fillStyle = '#000';
  ctx.globalAlpha = 0.1;
  ctx.font = `16px serif`
  ctx.rotate(Math.PI / 180 * angle);
  ctx.fillText(txt, 0, 50);
  return canvas.toDataURL();
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr); 

svg

svg 和 canvas 类似,主要还是生成背景图片。

function createWaterMark() {
  const svgStr =
    `<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
      <text x="0px" y="30px" dy="16px"
      text-anchor="start"
      stroke="#000"
      stroke-opacity="0.1"
      fill="none"
      transform="rotate(-20)"
      font-weight="100"
      font-size="16"
      >
          秋风的笔记
      </text>
    </svg>`;
  return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr); 

明水印的破解一

以上就很快实现了水印的几种方案。但是对于有心之人来说,肯定会想着破解,以上破解也很简单。

打开了 Chrome Devtools 找到对应的元素,直接按 delete 即可删除。

image-20201128175505927

明水印的防御

这样子的水印对于大概知道控制台操作的小白就可以轻松破解,那么有什么办法能防御住这样的操作呢?

答案是肯定的,js 有一个方法叫做 MutationObserver,能够监控元素的改动。

MutationObserver 对现代浏览的兼容性还是不错的,MutationObserver是元素观察器,字面上就可以理解这是用来观察Node(节点)变化的。MutationObserver是在DOM4规范中定义的,它的前身是MutationEvent事件,最低支持版本为 ie9 ,目前已经被弃用。

img

在这里我们主要观察的有三点

  • 水印元素本身是否被移除
  • 水印元素属性是否被篡改(display: none ...)
  • 水印元素的子元素是否被移除和篡改 (element生成的方式 )

来通过 MDN 查看该方法的使用示例。

const targetNode = document.getElementById('some-id');

// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };

// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
    // Use traditional 'for loops' for IE 11
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type === 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode, config); 

MutationObserver主要是监听子元素的改动,因此我们的监听对象为 document.body, 一旦监听到我们的水印元素被删除,或者属性修改,我们就重新生成一个。通过以上示例,加上我们的思路,很快我们就写一个监听删除元素的示例。(监听属性修改也是类似就不一一展示了)

// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
  for (let mutation of mutationsList) {
    mutation.removedNodes.forEach(function (item) {
      if (item === watermakr) {
          document.body.appendChild(watermakr);
      }
    });
  }
};
// 监听元素
const targetNode = document.body;
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config); 

我们打开控制台来检验一下。

2020-11-28-21.11.25

这回完美了,能够完美抵御一些开发小白了。

那么这样就万无一失了吗?显然,道高一尺魔高一丈,毕竟前端的一切都是不安全的。

明水印的破解二

在一个高级前端工程师面前,一切都是纸老虎。接下来我就随便介绍三种破解的方式。

第一种

打开 Chrome Devtools,点击设置 - Debugger - Disabled JavaScript .

1606569631600

然后再打开页面,delete我们的水印元素。

image-20201128212007999

第二种

复制一个 body 元素,然后将原来 body 元素的删除。

image-20201128212148446

第三种

打开一个代理工具,例如 charles,将生成水印相关的代码删除。

破解实践

接下来我们实战一下,通过预先分析,我们看到某在线设计网站的内容是以 div 的方式实现的,所以可以利用这种方案。 打开 https://www.gaoding.com/design?id=33931419&simple=1 (仅供举例学习)

image-20201128212301927

打开控制台,Ctrl + F 搜索 watermark 相关字眼。(这一步是作为一个程序员的直觉,基本上你要找什么,搜索对应的英文就可以 ~)

image-20201128212425716

很快我们就找到了水印图。发现直接删除,没有办法删除水印元素,根据我们刚才学习的,肯定是利用了MutationObserver 方法。我们使用我们的第一个破解方法,将 JavaScript 禁用,再将元素删除。

image-20201128212612430

水印已经消失了。

但是这样真的就万事大吉了吗?

image-20201123233342701

不知道你有没有听过一种东西,看不见摸不着,但是它却真实存在,他的名字叫做暗水印,我们将时间倒流到 16 年间的月饼门事件,因为有员工将内网站点截图了,但是很快被定位出是谁截图了。

虽然你将一些可见的水印去除了,但是还会存在一些不可见的保护版权的水印。(这就是防止一些坏人拿去作另外的用途)

暗水印

暗水印是一种肉眼不可见的水印方式,可以保持图片美观的同时,保护你的资源版权。

暗水印的生成方式有很多,常见的为通过修改RGB 分量值的小量变动、DWT、DCT 和 FFT 等等方法。

通过介绍前端实现 RGB 分量值的小量变动 来揭秘其中的奥秘,主要参考 不能说的秘密——前端也能玩的图片隐写术 | AlloyTeam

我们都知道图片都是有一个个像素点构成的,每个像素点都是由 RGB 三种元素构成。当我们把其中的一个分量修改,人的肉眼是很难看出其中的变化,甚至是像素眼的设计师也很难分辨出。

image-20201128213551039

你能看出其中的差别吗?根据这个原理,我们就来实践吧。(女孩子可以掌握方法后可以拿以下图片进行试验测试)

qiufeng-super

首先拿到以上图片,我们先来讲解解码方式,解码其实很简单,我们需要创建一个规律,再通过我们的规律去解码。现在假设的规律为,我们将所有像素的 R 通道的值为奇数的时候我们创建的通道密码,举个简单的例子。

image-20201128220542389

例如我们把以上当做是一个图形,加入他要和一个中文的 "一" 放进图像,例如我们将 "一" 放入第二行。按照我们的算法,我们的图像会变成这个样子。

image-20201128220833657

解码的时候,我们拿到所有的奇数像素将它渲染出来,例如这里的 '5779' 是不是正好是一个 "一",下面就转化为实践。

解码过程

首先创建一个 canvas 标签。

 <canvas id="canvas" width="256" height="256"></canvas> 
var ctx = document.getElementById('canvas').getContext('2d');
var img = new Image();
var originalData;
img.onload = function () {
  // canvas像素信息
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  console.log()
  processData(ctx, originalData)
};
img.src = 'qiufeng-super.png'; 

我们打印出这个数组,会有一个非常大的数组,一共有 256 256 4 = 262144 个值。因为每个像素除了 RGB 外还有一个 alpha 通道,也就是我们常用的透明度。

image-20201128215615494

上面也说了,我们的 R 通道为奇数的时候 ,就我们的解密密码。因此我们只需要所有的像素点的 R 通道为奇数的时候,将它填填充,不为奇数的时候就不填充,很快我们就能得到我们的隐藏图像。

var processData = function (ctx, originalData) {
    var data = originalData.data;
    for (var i = 0; i < data.length; i++) {
        if (i % 4 == 0) {
            // R分量
            if (data[i] % 2 == 0) {
                data[i] = 0;
            } else {
                data[i] = 255;
            }
        } else if (i % 4 == 3) {
            // alpha通道不做处理
            continue;
        } else {
            // 关闭其他分量,不关闭也不影响答案
            data[i] = 0;
        }
    }
    // 将结果绘制到画布
    ctx.putImageData(originalData, 0, 0);
}
processData(ctx, originalData) 

解密完会出现类似于以下这个样子。

image-20201128220100175

那我们如何加密的,那就相反的方式就可以啦。(这里都用了 不能说的秘密——前端也能玩的图片隐写术中的例子,= = 我也能写出一个例子,但是觉得没必要,别人已经写得很好了,我们只是讲述这个方法,需要代码来举例而已)

编码过程

加密呢,首先我们需要获取加密的图像信息。

var textData;
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '30px Microsoft Yahei';
ctx.fillText('秋风的笔记', 60, 130);
textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data; 

然后提取加密信息在待加密的图片上进行处理。

var mergeData = function (ctx, newData, color, originalData) {
    var oData = originalData.data;
    var bit, offset;  // offset的作用是找到alpha通道值,这里需要大家自己动动脑筋

    switch (color) {
        case 'R':
            bit = 0;
            offset = 3;
            break;
        case 'G':
            bit = 1;
            offset = 2;
            break;
        case 'B':
            bit = 2;
            offset = 1;
            break;
    }

    for (var i = 0; i < oData.length; i++) {
        if (i % 4 == bit) {
            // 只处理目标通道
            if (newData[i + offset] === 0 && (oData[i] % 2 === 1)) {
                // 没有信息的像素,该通道最低位置0,但不要越界
                if (oData[i] === 255) {
                    oData[i]--;
                } else {
                    oData[i]++;
                }
            } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)) {
                // // 有信息的像素,该通道最低位置1,可以想想上面的斑点效果是怎么实现的
                oData[i]++;
            }
        }
    }
    ctx.putImageData(originalData, 0, 0);
} 

主要的思路还是我一开始所讲的,在有像素信息的点,将 R 偶数的通道+1。在没有像素点的地方将 R 通道转化成偶数,最后在 img.onload 调用 processData(ctx, originalData)

img.onload = function () {
  // 获取指定区域的canvas像素信息
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  console.log(originalData)
    processData(ctx, originalData)
}; 

以上方法就是一种比较简单的加密方式。以上代码都放到了仓库 watermark/demo/canvas-dark-watermark.html 路径下,方法都封装好了~。

但是实际过程需要更专业的加密方式,例如利用傅里叶变化公式,来进行频域制定数字盲水印,这里就不详细展开讲了,以后研究完再详细讲~

img

破解实践

听完上述的介绍,那么某在线设计网站是不是很有可能使用了暗水印呢?

当然啦,通过我对某在线设计网站的分析,我分析了以下几种情况,我们一一来进行测试。

暗水印2

我们先通过免费下载的图片来进行分析。打开 https://www.gaoding.com/design?id=13964513159025728&mode=user

image-20201128230510959

image-20201128230557383

通过实验(实验主要是去分析他各个场景下触发的请求),发现在下载免费图片的时候,发现它都会去向阿里云发送一个 POST 请求,这熟悉的请求域名以及熟悉的数据封装方式,这不就是 阿里云 OSS 客户端上传方式嘛。这就好办了,我们去查询一下阿里云是否有生成暗水印的相关方式,从而来看看某在线设计网站是否含有暗水印。很快我们就从官方文档搜索到了相关的文档,且对于低 QPS 是免费的。(这就是最好理解的连带效应,例如我们觉得耐克阿迪啥卖运动类服饰,你买了他的鞋子,可能还会想买他的衣服)

image-20201128231110192

const { RPCClient } = require("@alicloud/pop-core");
var client = new RPCClient({
  endpoint: "http://imm.cn-shenzhen.aliyuncs.com",
  accessKeyId: 'xxx',
  accessKeySecret: 'xxx',
  apiVersion: "2017-09-06",
});
(async () => {
  try {
        var params = {
          Project: "test-project",
          ImageUri: "oss://watermark-shenzheng/source/20201009-182331-fd5a.png",
            TargetUri: "oss://watermark-shenzheng/dist/20201009-182331-fd5a-out.jpg",
            Model: "DWT"
        };
        var result = await client.request("DecodeBlindWatermark", params);
        
        console.log(result);
      } catch (err) {
        console.log(err);
      }
})() 

我们写了一个demo进行了测试。由于阿里云含有多种暗水印加密方式,为啥我使用了 DWT 呢?因为其他几种都需要原图,而我们刚才的测试,他上传只会上传一个文件到 OSS ,因此大致上排除了需要原图的方案。

image-20201128231801100

但是我们的结果却没有发现任何加密的迹象。

为什么我们会去猜想阿里云的图片暗水印的方式?因为从上传的角度来考虑,我们上传的图片 key 的地址即是我们下载的图片,也就是现在存在两种情况,一就是通过阿里云的盲水印方案,另一种就是上传前进行了水印的植入。现在看来不是阿里云水印的方案,那么只是能是上传前就有了水印。

这个过程就有两种情况,一是生成的过程中加入的水印,前端加入的水印。二是物料图含有水印。

对于第一种情况,我们可以通过 dom-to-image 这个库,在前端直接进行下载,或者使用截图的方式。目前通过直接下载和通过站点内生成,发现元素略有不同。

image-20201128235427912

第一个为我通过 dom-to-image 的方式下载,第二种为站点内下载,明显大了一些。(有点怀疑他在图片生成中可能做了什么手脚)

但是感觉前端加密的方式比较容易破解,最坏的情况想到了对素材进行了加密,但是这样的话就无从破解了(但是查阅了一些资料,由于某在线设计网站站点素材大多是透明背景的,这种加密效果可能会弱一些,以后牛逼了再来补充)。目前这一块暂时还不清楚,探究止步于此了。

攻击实验

那如果一张图经过暗水印加密,他的抵抗攻击性又是如何呢?

1605680005172-out1

1605680005172-decode2

这是一张通过阿里云 DWT暗水印进行的加密,解密后的样子为"秋风"字样,我们分别来测试一下。

加一些元素

1605680005172-out-el

1605680005172-decode-out-el

结果: 识别效果不错

截图

1605680005172-out-cut1

1605680005172-decode-out-cut1

结果: 识别效果不错

大小变化

1605680005172-out-scale

1605680005172-out-decode-scale

结果:识别效果不错

加蒙层

1605680005172-out-bg

1605680005172-decode-out-bg

结果: 直接就拉胯了。

可见,暗水印的抵抗攻击性还是蛮强的,是一种比较好的抵御攻击的方式~

最后

以上仅仅为技术交流~ 大家不要在实际的场景盲目使用,商业项目违规使用后果自负 ~ 或者期待一下我接下来想搞的这个个人免费首图生成器~ 喜欢文章的小伙伴可以点个赞哦 ~ 欢迎关注公众号 秋风的笔记 ,学习前端不迷路。

参考

https://imm.console.aliyun.com/cn-shenzhen/project?accounttraceid=1280c6af416744a38e9acf63c4e0878cjdet

https://help.aliyun.com/document_detail/138800.html?spm=a2c4g.11186623.6.656.3bd46bb4oglhEr

https://oss.console.aliyun.com/bucket/oss-cn-shenzhen/watermark-shenzheng/object?path=dist%2F

https://juejin.cn/post/6844903650054111246

https://www.zhihu.com/question/50677827/answer/122388524

https://www.zhihu.com/question/50735753

最后

如果我的文章有帮助到你,希望你也能帮助我,欢迎关注我的微信公众号 秋风的笔记,回复好友 二次,可加微信并且加入交流群,秋风的笔记 将一直陪伴你的左右。

image

查看原文

赞 34 收藏 25 评论 4

蓝色的秋风 关注了用户 · 2020-11-17

gaoryrt @gaoryrt

Github 是 https://github.com/gaoryrt
生活博客在 https://gaoryrt.com
狗粮播客在 https://jungle.fm

关注 117

蓝色的秋风 发布了文章 · 2020-11-17

ES2021 我学不动了,这次只学这 3 个。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.逻辑赋值操作符

你有遇到过这样的情况吗?

function example(a) {
  // Default `a` to "foo"
  if (!a) {
    a = "foo";
  }
  // or
  a = a || "foo";
}

某些初始化的时候需要一些冗长的逻辑代码

function example(opts) {
  // Ok, but could trigger setter.
  opts.foo = opts.foo ?? "bar";

  // No setter, but 'feels wrong' to write.
  opts.baz ?? (opts.baz = "qux");
}

example({ foo: "foo" });

在这里插入图片描述
在这里插入图片描述

function example(opts) {
  // 旧的方式
  if (!a) {
    a = "foo";
  }
  // or
  a = a || "foo";
  // 新的方式
  a ||= "foo"
}

example({ foo: "foo" });
function example(opts) {
  // 旧的方式
  opts.foo = opts.foo ?? "bar";
  // 新的方式
  opts.foo ??= "bar";

  // 旧的方式
  opts.baz ?? (opts.baz = "qux");
  // 新的方式
  opts.baz ??= "qux";
}

example({ foo: "foo" });

在这里插入图片描述
1605350041175
在这里插入图片描述

a = a + b;
a += b;
a = a - b;
a -= b;

在这里插入图片描述

2.Promise.any

Promise.any。 从字面意思来看,相信聪明的你应该能大致猜出这个 API 的作用。Promise.any 接受一个 Promise 的数组。当其中任何一个 Promise 完成(fullfill)时,就返回那个已经有完成值的 Promise。如果所有的 Promise 都拒绝(reject),则返回一个拒绝的 Promise,该 Promise 的返回值是一个 AggregateError 对象。

Promise.any(promises).then(
  (first) => {
    // 任意一个Promise完成了
  },
  (error) => {
    // 所有Promise都被拒绝了
  }
);

在这里插入图片描述

Promise.any([
  fetch("https://v8.dev/").then(() => "home"),
  fetch("https://v8.dev/blog").then(() => "blog"),
  fetch("https://v8.dev/docs").then(() => "docs"),
])
  .then((first) => {
    // Any of the promises was fulfilled.
    console.log(first);
    // → 'home'
  })
  .catch((error) => {
    // All of the promises were rejected.
    console.log(error);
  });

例如一些播放平台,可以通过这个来测试当前延迟最低的线路是哪个,优先切换到对应的最快的线路。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
来,亮出祖传降级代码

function reverse(promise) {
  return new Promise((resolve, reject) =>
    Promise.resolve(promise).then(reject, resolve)
  );
}
function promiseAny(iterable) {
  return reverse(Promise.all([...iterable].map(reverse)));
}
// https://github.com/m0ppers/promise-any/blob/master/index.js

实现很简单,通过一个反转函数,利用 Promisea.all 的特性,只要一个 Promise 被拒绝了,就进入到 reject,因此反转 resolvereject 就能模拟出 Promise.any 了。
在这里插入图片描述
1605350041175在这里插入图片描述

3.数字分隔符

let fee = 1000000000;
let fee = 1_000_000_000;

这个模式不仅在十进制可以用,二进制,十六进制....甚至 BigInt,都可以使用。

// Binary Literals
let nibbles = 0b1010_0001_1000_0101;
// Hex Literal
let message = 0xa0_b0_c0;
// BigInt Literal
const max = 2n ** (64n - 1n) - 1n;
console.log(max === 9_223_372_036_854_775_807n);

以上特性均在最新版 chrome 支持,快打开控制台玩耍吧。

如果想要在实际项目中使用,请使用以下两个插件。

最后

image

查看原文

赞 12 收藏 6 评论 10

蓝色的秋风 发布了文章 · 2020-11-13

2020前端开发者11个必备的网站

网上有很多很棒的工具,让作为前端开发人员的我们生活的更加轻松。在这篇文章中,我将快速介绍一下我在开发工作中经常使用的11种工具。

Node.green

用来查询当前 Node 版本是否某些功能。例如,对象展开符( Rest/Spread Properties)

1582372545876.jpg

可以看到在 Node v8.3.0 以下是不支持的。分别在 Node v8.5.0v8.2.1 下运行以下代码片段

const a = { foo: 1};
console.log({...a, b: 2}); 

1582372779948.jpg

当你遇到以上错误,那大多就是 Node 版本问题啦。

在线地址: https://node.green/

CanIUse

当你想要确定某个 Web API 的兼容性的时候,这个在线工具将轻松搞定。

假设我们想知道哪些浏览器及其版本将支持 Web Share API:navigator.share(...

1_pq1UczjJ8dhTsO6hCPntyw.png

查看结果。浏览器和支持navigator.share(…)的版本都列出了。

在线地址: https://caniuse.com/

Minify

为了减少应用程序代码的包大小,我们在发布到到生产环境的时候,需要使它们最小化。 最小化消除了空格,无效代码等。这能够使应用程序包大小的显着减小,从而节省浏览器上的加载时间。(虽然在当下,有 webpack uglifyJS 等插件,但是当我在开发非打包的简单应用的时候,这个是一个不错的选择。 )

1582373652825.jpg

在线地址: https://www.minifier.org/

Bit.dev

Bit.dev是一个非常棒的组件中心。 可以用它来托管,记录和管理来自不同项目的可复用组件。 这是增加代码复用,加速开发并优化团队协作的好方法。

这也是从头开始构建设计系统的不错选择(因为它本质上具有设计系统所需的一切)。 Bit.devBit完美配合,Bit是处理组件隔离和发布的开源工具。

Bit.dev支持React,带有TypeScriptReactAngularVue等。

1_Nj2EzGOskF51B5AKuR-szw.gif

在线地址: https://bit.dev/

Unminify

免费的在线工具,用于最小化(解压,反混淆)JavaScript,CSS和HTML代码,使其可读性强,美观

1582375400913.jpg

在线地址: https://unminify.com/

Stackblitz

这是每个人都喜欢的工具。Stackblitz使我们能够使用世界上最流行和使用最多的IDE,即web上的Visual Studio代码。

只需单击一下,Stackblitz 即可快速提供AngularReactVueVanillaRxJSTypeScript项目的框架。

当你想从浏览器中尝试一段代码或任何当前JS框架中的功能时,Stackblitz非常有用。 假设你正在阅读Angular文章,并且遇到了想要尝试的代码。 您可以最小化您的浏览器并快速搭建一个新的Angular项目。

还有其他很棒的在线IDE,但是我相信Stackblitz的转折点是使用每个人都喜欢的 Visual Studio Code感觉和工具。 (ps: 本人使用体验,非常快速流畅, 附上图,比 sandbox 快很多)

1582374042909.jpg

在线地址: https://stackblitz.com/

JWT.io

如果您使用JSON Web令牌(JWT)保护应用程序安全,或者使用JWT允许用户访问后端的受保护资源。

决定是否应访问路线或资源的一种方法是检查令牌的到期时间。 有时候我们想要解码JWT以查看其有效 payload,jwt.io恰好提供了这一点。

这个在线工具使我们能够插入令牌以查看其有效 payload。 一旦我们粘贴了令牌,jwt.io便对该令牌进行解码并显示其有效payload

1582374387059.jpg

在线地址: https://jwt.io/

BundlePhobia

您是否曾经不确定过node_modules的大小,或者只是想知道将pakckage.json安装在您的计算机中的大小? BundlePhobia提供了答案

1582374462632.jpg

该工具使我们能够加载package.json文件,并显示将从package.json安装的依赖项的大小,也可以查询单包的体积。

在线地址: https://bundlephobia.com/

Babel REPL

Babel是一个免费的开放源代码JS转编译器,用于将现代ES代码转换为普通的 ES5 JavaScript。

该工具是Babeljs团队在网上建立的Web应用,可以将 ES6 +代码转换为ES5。

本人总结的两个比较方便的使用方式

  1. 方面面试时在线写高级语法。
  2. 可以快速查看某些 polyfill 是怎么写的。

1582374539633.jpg

在线地址: https://babeljs.io/en/repl

Prettier Playground

Prettier是一个自以为是的JS代码格式化程序。 它通过解析代码并使用JS最佳编码实践将其重新打印来实施一致的样式。

该工具已在我们的开发环境中广泛使用,但它也具有一个在线地址,你可以在其中美化您的代码。

1582375260418.jpg

在线地址: https://prettier.io/playground

postwoman

postwoman 是一款功能强大的网页调试和模拟发送HTTP请求的Chrome插件,支持几乎所有类型的HTTP请求,操作简单且方便。可用于接口测试,比如测试你用easy-mock生成的接口。

1582374841427.jpg

在线地址: https://postwoman.io/

本文翻译自 https://blog.bitsrc.io/12-use... 但是不仅仅是单纯地翻译,替换了原文中一些我觉得不太实用的并加入一些自己的总结。

最后

如果我的文章有帮助到你,希望你也能帮助我,欢迎关注我的微信公众号 秋风的笔记,回复好友 二次,可加微信并且加入交流群,秋风的笔记 将一直陪伴你的左右。

image

查看原文

赞 28 收藏 21 评论 0

蓝色的秋风 赞了文章 · 2020-11-12

【编译篇】AST实现函数错误的自动上报

前言

之前有身边有人问我在错误监控中,如何能实现自动为函数自动添加错误捕获。今天我们来聊一聊技术如何实现。先讲原理:在代码编译时,利用 babel 的 loader,劫持所有函数表达。然后利用 AST(抽象语法树) 修改函数节点,在函数外层包裹 try/catch。然后在 catch 中使用 sdk 将错误信息在运行时捕获上报。如果你对编译打包感兴趣,那么本文就是为你准备的。

本文涉及以下知识点:

  • [x] AST
  • [x] npm 包开发
  • [x] Babel
  • [x] Babel plugin
  • [x] Webpack loader

实现效果

Before 开发环境:

var fn = function(){
  console.log('hello');
}

After 线上环境:

var fn = function(){
+  try {
    console.log('hello');
+  } catch (error) {
+    // sdk 错误上报
+    ErrorCapture(error);
+  }
}

Babel 是什么?

Babel 是JS编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
简单说就是从一种源码到另一种源码的编辑器!下面列出的是 Babel 能为你做的事情:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
  • 源码转换 (codemods)
  • 其它

Babel 的运行主要分三个阶段,请牢记:解析->转换->生成,后面会用到。

本文我们将会写一个 Babel plugin 的 npm 包,用于编译时将代码进行改造。

babel-plugin 环境搭建

这里我们使用 yeomangenerator-babel-plugin 来构建插件的脚手架代码。安装:

$ npm i -g yo
$ npm i -g generator-babel-plugin

然后新建文件夹:

$ mkdir babel-plugin-function-try-actch
$ cd babel-plugin-function-try-actch

生成npm包的开发工程:

$ yo babel-plugin


此时项目结构为:

babel-plugin-function-try-catch
├─.babelrc
├─.gitignore
├─.npmignore
├─.travis.yml
├─README.md
├─package-lock.json
├─package.json
├─test
|  ├─index.js
|  ├─fixtures
|  |    ├─example
|  |    |    ├─.babelrc
|  |    |    ├─actual.js
|  |    |    └expected.js
├─src
|  └index.js
├─lib
|  └index.js

这就是我们的 Babel plugin,取名为 babel-loader-function-try-catch为方便文章阅读,以下我们统一简称为plugin)。

至此,npm 包环境搭建完毕,代码地址

调试 plugin 的 ast

开发工具

本文前面说过 Babel 的运行主要分三个阶段:解析->转换->生成,每个阶段 babel 官方提供了核心的 lib:

  • babel-core。Babel 的核心库,提供了将代码编译转化的能力。
  • babel-types。提供 AST 树节点的类型。
  • babel-template。可以将普通字符串转化成 AST,提供更便捷的使用

plugin 根目录安装需要用到的工具包:

npm i @babel/core @babel/parser babel-traverse @babel/template babel-types -S

打开 plugin 的 src/index.js 编辑:

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

// 先来定义一个简单的函数
let source = `var fn = function (n) {
  console.log(111)
}`;

// 解析为 ast
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 打印一下看看,是否正常
console.log(ast);

终端执行 node src/index.js 后将会打印如下结果:

这就是 fn 函数对应的 ast,第一步解析完成!

获取当前节点的 AST

然后我们使用 babel-traverse 去遍历对应的 AST 节点,我们想要寻找所有的 function 表达可以写在 FunctionExpression 中:

打开 plugin 的 src/index.js 编辑:

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

// mock 待改造的源码
let source = `var fn = function() {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
+ traverse(ast, {
+   FunctionExpression(path, state) { // Function 节点
+     // do some stuff
+   },
+ });

所有函数表达都会走到 FunctionExpression 中,然后我们可以在里面对其进行修改。
其中参数 path 用于访问到当前的节点信息 path.node,也可以像 DOM 树访问到父节点的方法 path.parent

修改当前节点的 AST

好了,接下来要做的是在 FunctionExpression 中去劫持函数的内部代码,然后将其放入 try 函数内,并且在 catch 内加入错误上报 sdk 的代码段。

获取函数体内部代码

上面定义的函数是

var fn = function() {
  console.log(111)
}

那么函数内部的代码块就是 console.log(111),可以使用 path 拿到这段代码的 AST 信息,如下:

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

// mock 待改造的源码
let source = `var fn = function(n) {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // 函数表达式会进入当前方法
+    // 获取函数当前节点信息
+    var node = path.node,
+        params = node.params,
+        blockStatement = node.body,
+        isGenerator = node.generator,
+        isAsync = node.async;

+    // 可以尝试打印看看结果
+    console.log(node, params, blockStatement);
  },
});

终端执行 node src/index.js,可以打印看到当前函数的 AST 节点信息。

创建 try/catch 节点(两步骤)

创建一个新的节点可能会稍微陌(fu)生(za)一点,不过我已经为大家总结了我个人的经验(仅供参考)。首先需要知道当前新增代码段它的声明是什么,然后使用 @babel-types 去创建即可。

第一步:

那么我们如何知道它的表达声明type是什么呢?这里我们可以 使用 astexplorer 查找它在 AST 中 type 的表达

如上截图得知,try/catch 在 AST 中的 type 就是 TryStatement

第二步:

然后去 @babel-types 官方文档查找对应方法,根据 API 文档来创建即可。

如文档所示,创建一个 try/catch 的方式使用 t.tryStatement(block, handler, finalizer)

创建新的ast节点一句话总结:使用 astexplorer 查找你要生成的代码的 type,再根据 type 在 @babel-types 文档查找对应的使用方法使用即可!

那么创建 try/catch 只需要使用 t.tryStatement(try代码块, catch代码块) 即可。

  • try代码块 表示 try 中的函数代码块,即原先函数 body 内的代码 console.log(111),可以直接用 path.node.body 获取;
  • catch代码块 表示 catch 代码块,即我们想要去改造进行错误收集上报的 sdk 的代码 ErrorCapture(error),可以使用 @babel/template 去生成。

代码如下所示:

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

// 0、定义一个待处理的函数(mock)
let source = `var fn = function() {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // Function 节点
    var node = path.node,
        params = node.params,
        blockStatement = node.body, // 函数function内部代码,将函数内部代码块放入 try 节点
        isGenerator = node.generator,
        isAsync = node.async;

+    // 创建 catch 节点中的代码
+    var catchStatement = template.statement(`ErrorCapture(error)`)();
+    var catchClause = t.catchClause(t.identifier('error'),
+          t.blockStatement(
+            [catchStatement] //  catchBody
+          )
+        );
+    // 创建 try/catch 的 ast
+    var tryStatement = t.tryStatement(blockStatement, catchClause);
  }
});

创建新函数节点,并将上面定义好的 try/catch 塞入函数体:

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

// 0、定义一个待处理的函数(mock)
let source = `var fn = function() {
  console.log(111)
}`;

// 1、解析
let ast = parser.parse(source, {
  sourceType: "module",
  plugins: ["dynamicImport"]
});

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // Function 节点
      var node = path.node,
          params = node.params,
          blockStatement = node.body, // 函数function内部代码,将函数内部代码块放入 try 节点
          isGenerator = node.generator,
          isAsync = node.async;

      // 创建 catch 节点中的代码
      var catchStatement = template.statement(`ErrorCapture(error)`)();
      var catchClause = t.catchClause(t.identifier('error'),
            t.blockStatement(
              [catchStatement] //  catchBody
            )
          );
      // 创建 try/catch 的 ast
      var tryStatement = t.tryStatement(blockStatement, catchClause);

+    // 创建新节点
+    var func = t.functionExpression(node.id, params, t.BlockStatement([tryStatement]), isGenerator, isAsync);
+    // 打印看看是否成功
+    console.log('当前节点是:', func);
+    console.log('当前节点下的自节点是:', func.body);
  }
});

此时将上述代码在终端执行 node src/index.js

可以看到此时我们在一个函数表达式 body 中创建了一个 try 函数(TryStatement)。
最后我们需要将原函数节点进行替换:

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

// 0、定义一个待处理的函数(mock)
let source = `var fn = function() {...

// 1、解析
let ast = parser.parse(source, {...

// 2、遍历
traverse(ast, {
  FunctionExpression(path, state) { // Function 节点
      var node = path.node,
          params = node.params,
          blockStatement = node.body, // 函数function内部代码,将函数内部代码块放入 try 节点
          isGenerator = node.generator,
          isAsync = node.async;

      // 创建 catch 节点中的代码
      var catchStatement = template.statement(`ErrorCapture(error)`)();
      var catchClause = t.catchClause(t.identifier('error'),...

      // 创建 try/catch 的 ast
      var tryStatement = t.tryStatement(blockStatement, catchClause);
      // 创建新节点
      var func = t.functionExpression(node.id, params, t.BlockStatement([tryStatement]), isGenerator, isAsync);
      
+    // 替换原节点
+    path.replaceWith(func);
  }
});

+ // 将新生成的 AST,转为 Source 源码:
+ return core.transformFromAstSync(ast, null, {
+  configFile: false // 屏蔽 babel.config.js,否则会注入 polyfill 使得调试变得困难
+ }).code;

“A loader is a node module exporting a function”,也就是说一个 loader 就是一个暴露出去的 node 模块,既然是一个node module,也就基本可以写成下面的样子:

module.exports = function() {
    //  ...
};

再编辑 src/index.js 为如下截图:

边界条件处理

我们并不需要为所有的函数都增加 try/catch,所有我们还得处理一些边界条件。

  • 1、如果有 try catch 包裹了
  • 2、防止 circle loops
  • 3、需要 try catch 的只能是语句,像 () => 0 这种的 body
  • 4、如果函数内容小于多少行数

满足以上条件就 return 掉!

代码如下:

if (blockStatement.body && t.isTryStatement(blockStatement.body[0])
  || !t.isBlockStatement(blockStatement) && !t.isExpressionStatement(blockStatement)
  || blockStatement.body && blockStatement.body.length <= LIMIT_LINE) {
  return;
}

最后我们发布到 npm 平台 使用。

由于篇幅过长不易阅读,本文特别的省略了本地调试过程,所以需要调试请移步 [【利用AST自动为函数增加错误上报-续集】有关 npm 包的本地开发和调试]()。

如何使用

npm install babel-plugin-function-try-catch

webpack 配置

rules: [{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
+   "babel-plugin-function-try-catch",
    "babel-loader",
  ]
}]

效果见如下图所示:

最后

有关 npm 包的本地调试见下篇: 有关 npm 包的本地开发和调试

更多 AST 相关请关注后面分享,谢谢。

Reference:

完整代码地址请点击

Babel 插件手册点击

查看原文

赞 17 收藏 9 评论 5

认证与成就

  • 获得 445 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • webchat

    Websocket project based on vue(基于vue2.0的实时聊天项目)

注册于 2017-11-12
个人主页被 4.3k 人浏览