注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

浏览器检测之趣事

web
1 那段历史在开发过程中,我们通常用用户代理字符串—浏览器端 window.navigator.userAgent或者服务器端header携带的user-agent —来用于检测当前浏览器是否为移动端, 比如:if(isMobile()) { // 移动端逻...
继续阅读 »

1 那段历史

在开发过程中,我们通常用用户代理字符串—浏览器端 window.navigator.userAgent或者服务器端header携带的user-agent —来用于检测当前浏览器是否为移动端, 比如:

if(isMobile()) {
// 移动端逻辑...
}

function isMobile () {
  const versions = (function () {
      const u = window.navigator.userAgent // 服务器端:req.header('user-agent')
      return {
        trident: u.indexOf('Trident') > -1, // IE内核
        presto: u.indexOf('Presto') > -1, // opera内核
        webKit: u.indexOf('AppleWebKit') > -1, // 苹果、谷歌内核
        gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') === -1, // 火狐内核
        mobile: !!u.match(/AppleWebKit.*Mobile.*/), // 是否为移动终端
        ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
        android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, // android终端或者uc浏览器
        iPhone: u.indexOf('iPhone') > -1, // 是否为iPhone或者QQHD浏览器
        iPad: u.indexOf('iPad') > -1, // 是否iPad
        webApp: u.indexOf('Safari') === -1
      }
  }())
  return versions.mobile || versions.ios || versions.android || versions.iPhone || versions.iPad
}

我在使用时心里一直有疑问,一个移动端,为什么要做那么多判断呢?

目前我的 Chrome 浏览器:


看到这么一长串字符串,我表示更懵逼, Mozilla不是firefox的厂商么?这是 Chrome 浏览器,又怎么会有 “Safari” 的关键字?那个 “like Gecko” 又是什么鬼?

于是抱着这些疑问, 我打算好好深入了解一下浏览器检测这部分,没想到在学习过程中发现了挺有意思的事情,待我慢慢道来,大家也听个乐呵。

首先始于客户端与服务器端通信,要求携带名称与版本信息,于是服务器端与客户端协定好在每个HTTP请求的头部加上用户代理字符串(userAgent),方便服务器端进行检测,检测通过之后再进行后续操作。

早期的用户代理字符串(userAgent)很简单, 就 "产品名称/产品版本号",比如:"Mosaic/0.9"。93年之后,网景公司发布的Netscape Navigator 系列浏览器渐渐成为了当时最受欢迎的浏览器,于是它拥有了规则制定权,说从此以后我的用户代理字符串就为:


这时肯定有人会问,"Mozilla" 是网景公司为 Netscape 浏览器定义的代号,既然站在“食物链”顶端,那当然得用自己的命名,这能理解。可为啥直到现在,大部分主流浏览器的用户代理字符串(userAgent),第一个名称也是 “Mozilla” 呢?

这就是我即将要讲的, 第一根搅屎棍——微软。

96年,微软推出了 IE3, 而当时 Netscape Navigator3 的市场占有率太高,微软说,为了兼容 Netscape Navigator3, IE的用户代理字符串从此就为:


看到没有, 第一个名称还是 “Mozilla”,这个误导信息可以直接骗过服务器检测,而真正的 IE 版本放到后面去了。

大概意思就是初出茅庐的IE小同学怕自己知名度太低,万一服务端检测不到自己,用户流失了怎么办?隔壁老大哥家大业大,那就干脆去蹭波流量吧。关键是蹭流量就蹭流量吧,还嘴硬说我这可是Mozilla/2.0哦,不是Mozilla/3.0哦,跟那个Netscape Navigator3 不能说没有关系,只能说毫不相干。于是,IE成功地将自己伪装成了 Netscape Navigator。

这在当时来说是有争议,但不得不说, 微软这波操作相当精准。精准到直到97年 IE4 发布时,IE 的市场份额大幅增加,有了点话语权,也不藏着掖着了, 就跟 Netscape 同时将版本升级到了 Mozilla/4.0, 之后就一直保持同步了。

看到 IE 这波操作,场外观众有点坐不住了,更多的浏览器厂商沿着IE的老路,蹭着 Netscape 的流量,在此基础上依葫芦画瓢地设定自己的用户代理字符串(userAgent)。直到 Gecko 渲染引擎 (firefox的核心) 开始大流行,用户代理字符串(userAgent)基本已经形成了一个比较标准格式,服务端检测也能识别到 “Mozilla”、“Geoko” 等关键字,与之前字符串相比, 还增加了引擎、语言信息等等。


接下来我要说第二根搅屎棍——苹果。

2003年,苹果发布了 Safari, 它说,我的浏览器用户代理字符串是这样的:


Safari 用的渲染引擎是WebKit, 不是Gecko,它的核心是在渲染引擎KHTML基础上进行开发的,但是当时大部分浏览器的用户代理字符串(userAgent)都包含了 “Mozilla”、“Gecko”等关键字供服务器端检测。

苹果昂着脸,维持着表面的高傲,表示我的 WebKit 天下无敌、傲视群雄, 心里却颤颤发抖,小心翼翼地在用户代理字符串里加了个“like Gecko”,假装我是Gecko ?!

这波操作可谓是又当又立的典范!

我想可能心理阴影最大的要属 Netscape 了,本来 IE 来白嫖一波也就算了,你Safari 也要来,而且本身苹果的影响力就不容小觑,你再进来插一脚,让我以后怎么生存?但苹果说:“Safari 与 Mozilla 兼容,不能让网站以为用户使用了不受支持的浏览器而把 Safari 排斥在外。”大概意思是,我就是要白嫖, 怎么样?可以说是相当不要脸了。

不过至少苹果还有点藏着掖着, 而 Chrome 就有点不讲武德,它说,成年人的世界不做选择, 我想要的我都要:


Chrome 的渲染引擎是 Blink , Javascript引擎是 V8, 但它的用户代理字符串(userAgent)中, 不仅包含了“Mozilla”、“like Gecko”,还包含了 “WebKit” 的引擎信息, 几乎把能嫖的都嫖了, 只多了一个 “Chrome” 名称和版本号,甚至都没有一个 “Blink” 关键字,节操碎了一地,简直触目惊心,令人叹为观止。

到这里就不得不提一嘴高冷的Opera,直到Opera 8,用户代理字符串(userAgent)一直都是 “Opera/Version (OS-or-CPU; Encryption [; language])”

Opera 一直给人一种世人皆醉我独清、出淤泥而不染的气概。到直到 Opera9 画风突然变了, 估计也是看到几个大厂商各种骚操作,有点绷不住了,也跑去蹭流量。心态虽然崩但高冷人设不能崩,我就是不走寻常路,于是秀了一波玄学操作,它搞了两套用户代理字符串(userAgent):


场外观众表示有点看不懂, 蹭完 Firefox 又去蹭 IE,还得分开蹭,这哪是秀操作, 这可是秀智商啊!纵观浏览器发展的这几十年,大概就是长江后浪推前浪,后浪还没把前浪踩死在沙滩上,后后浪又踩过来的一段历史吧。就在这历史的溪流中,用户代理字符串(userAgent)也已经形成了一个比较标准的格式。

目前,各个浏览器的用户代理字符串(userAgent)始终包含着以下信息:


至于后来移动端的 IOS 和 Andriod 基本的格式就成了:


这里的Mobile可能是 “iphone”、“ipad”、“BlackBerry”等等,Andriod设备的OS-or-CPU通常都是“Andriod”或“Linux”。所以,回到开头的isMobile检测函数内部,一大堆的检测判断条件, 简直就是一粒粒历史尘埃的堆叠。

同时,本地Chrome浏览器输出:


我也可以翻译一下,大概意思就是,白嫖的Mozilla/5.0 + Macintosh平台 + Mac OS操作系统 × 10_15_7版本白嫖的AppleWebKit引擎/537.36引擎版本号 (KHTML内核, like Gecko 假装我是Gecko) Chrome浏览器/浏览器版本号99.0.4844.84 白嫖的Safari/Sarari版本号537.36。

本人表示很精彩, 一个用户代理字符串犹如看了一场轰轰烈烈(巨不要脸)、你挣我夺(你蹭我蹭)的大戏!

2 第三方插件

接下来, 为懒人推荐几款用于浏览器检测的省事的第三方插件。

1、如果只是检测设备是否为手机端, 可以用 isMobile ,它支持在node端或浏览器端使用。

地址:https://github.com/kaimallea/isMobile

2、如果要检测设备的类型、版本、CPU等信息,可以用 UAParser ,它支持在node端或浏览器端使用。

地址:https://github.com/faisalman/ua-parser-js

3、vue插件,vue-browser-detect-plugin

地址:https://github.com/ICJIA/vue-browser-detect-plugin

4、react插件,react-device-detect

地址:https://github.com/duskload/react-device-detect

5、在不同平台,要在Html中设置对应平台的CSS,可以用 current-device

地址:https://github.com/matthewhudson/current-device

需要注意的是, 第三方插件虽好用, 但也要注意安全问题哦,之前 UAParser 就被曝出被遭遇恶意投毒,所以只是简单的检测尽量手写。

3 移动端与PC端分流

移动端与PC端分流,可以用 nginx 来操作, nginx 可以通过 $http_user_agent 直接拿到用户代理信息:

http { 
server {
    listen 80;
      server_name localhost;
      location / {
          root /usr/share/nginx/pc; #pc端代码目录
          if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
          root /usr/share/nginx/mobile; #移动端代码目录
          }
      index index.html;
      }
}
}

来源:八戒技术团队

收起阅读 »

前端 PDF 水印方案

web
场景:前端下载 pdf 文件的时候,需要加上水印,再反给用户下载 用到的库:pdf-lib (文档) @pdf-lib/fontkit 字体:github 方案目标:logo图 + 中文 + 英文 + 数字 => 透明水印首先安装 pdf-lib: 它是...
继续阅读 »

场景:前端下载 pdf 文件的时候,需要加上水印,再反给用户下载
用到的库pdf-lib (文档) @pdf-lib/fontkit
字体github
方案目标:logo图 + 中文 + 英文 + 数字 => 透明水印


首先安装 pdf-lib: 它是前端创建和修改 PDF 文档的一个工具(默认不支持中文,需要加载自定义字体文件)

npm install --save pdf-lib

安装 @pdf-lib/fontkit:为 pdf-lib 加载自定义字体的工具

npm install --save @pdf-lib/fontkit

没有使用pdf.js的原因是因为:

  1. 会将 PDF 转成图片,无法选中

  2. 操作后 PDF 会变模糊

  3. 文档体积会变得异常大


实现:

首先我们的目标是在 PDF 文档中,加上一个带 logo 的,同时包含中文、英文、数字字符的透明水印,所以我们先来尝试着从本地加载一个文件,一步步搭建。

1. 获取 PDF 文件

本地:

// <input type="file" name="pdf" id="pdf-input">

let input = document.querySelector('#pdf-input');
input.onchange = onFileUpload;

// 上传文件
function onFileUpload(e) {
let event = window.event || e;

let file = event.target.files[0];
}

除了本地上传文件之外,我们也可以通过网络请求一个 PDF 回来,注意响应格式为 blob
网络:

var x = new XMLHttpRequest();
x.open("GET", url, true);
x.responseType = 'blob';
x.onload = function (e) {
let file = x.response;
}
x.send();

// 获取直接转成 pdf-lib 需要的 arrayBuffer
// const fileBytes = await fetch(url).then(res => res.arrayBuffer())

2. 文字水印

在获取到 PDF 文件数据之后,我们通过 pdf-lib 提供的接口来对文档做修改。

// 修改文档
async function modifyPdf(file) {
const pdfDoc = await PDFDocument.load(await file.arrayBuffer());

// 加载内置字体
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Courier);

// 获取文档所有页
const pages = pdfDoc.getPages();

// 文字渲染配置
const drawTextParams = {
  lineHeight: 50,
  font: helveticaFont,
  size: 12,
  color: rgb(0.08, 0.08, 0.2),
  rotate: degrees(15),
  opacity: 0.5,
};

for (let i = 0; i < pages.length; i++) {
  const page = pages[i];

  // 获取当前页宽高
  const { width, height } = page.getSize();

  // 要渲染的文字内容
  let text = "water 121314";

  for (let ix = 1; ix < width; ix += 230) { // 水印横向间隔
    let lineNum = 0;
    for (let iy = 50; iy <= height; iy += 110) { // 水印纵向间隔
      lineNum++;
       
      page.drawText(text, {
        x: lineNum & 1 ? ix : ix + 70,
        y: iy,
        ...drawTextParams,
      });
    }
  }
}

来看一下现在的效果

3. 加载本地 logo

在加载图片这块,我们最终想要的其实是图片的 Blob 数据,获取网图的话,这里就不做介绍了,下边主要着重介绍一下,如何通过 js 从本地加载一张图。
先贴上代码:

//  加载 logo blob 数据
~(function loadImg() {
let img = new Image();
img.src = "./water-logo.png";

let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");

img.crossOrigin = "";
img.onload = function () {
  canvas.width = this.width;
  canvas.height = this.height;

  ctx.fillStyle = "rgba(255, 255, 255, 1)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.drawImage(this, 0, 0, this.width, this.height);
  canvas.toBlob(
    function (blob) {
      imgBytes = blob; // 保存数据到 imgBytes 中
    },
    "image/jpeg",
    1
  ); // 参数为输出质量
};
})();

首先通过一个自执行函数,在初期就自动加载 logo 数据,当然我们也可以根据实际情况做相应的优化。
整体的思路就是,首先通过 image 元素来加载本地资源,再将 img 渲染到 canvas 中,再通过 canvas 的 toBlob 来得到我们想要的数据。

在这块我们需要注意两行代码:

ctx.fillStyle = "rgba(255, 255, 255, 1)"; 
ctx.fillRect(0, 0, canvas.width, canvas.height);

如果我们不加这两行代码的话,同时本地图片还是透明图,最后我们得到的数据将会是一个黑色的方块。所以我们需要在 drawImage 之前,用白色填充一下 canvas 。

4. 渲染 logo

在渲染 logo 图片到 PDF 文档上之前,我们还需要和加载字体类似的,把图片数据也挂载到 pdf-lib 创建的文档对象上(pdfDoc),其中 imgBytes 是我们已经加载好的图片数据。

let _img = await pdfDoc.embedJpg(await imgBytes.arrayBuffer());

挂载完之后,做一些个性化的配置

page.drawImage(_img, {
x: lineNum & 1 ? ix - 18 : ix + 70 - 18, // 奇偶行的坐标
y: iy - 8,
width: 15,
height: 15,
opacity: 0.5,
});

5. 查看文档

这一步的思路就是先通过 pdf-lib 提供的 save 方法,得到最后的文档数据,将数据转成 Blob,最后通过 a 标签打开查看。

// 保存文档 Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();

let blobData = new Blob([pdfBytes], { type: "application/pdf;Base64" });

// 新标签页预览
let a = document.createElement("a");
a.target = "_blank";
a.href = window.URL.createObjectURL(blobData);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

到目前的效果

6. 中文字体

由于默认的 pdf-lib 是不支持渲染中文的
Uncaught (in promise) Error: WinAnsi cannot encode "水" (0x6c34)

所以我们需要加载自定义字体,但是常规的字体文件都会很大,为了使用,需要将字体文件压缩一下,压缩好的字体在文档头部,包含空格和基础的3500字符。
压缩字体用到的是 gulp-fontmin 命令行工具,不是客户端。具体压缩方法,可自行搜索。

在拿到字体之后(ttf文件),将字体文件上传到网上,再拿到其 arrayBuffer 数据。之后再结合 pdf-lib 的文档对象,对字体进行注册和挂载。同时记得将文字渲染的字体配置改过来。

// 加载自定义字体
const url = 'https://xxx.xxx/xxxx';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

// 自定义字体挂载
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)

// 文字渲染配置
const drawTextParams = {
  lineHeight: 50,
  font: customFont, // 改字体配置
  size: 12,
  color: rgb(0.08, 0.08, 0.2),
  rotate: degrees(15),
  opacity: 0.5,
};

所以到现在的效果

7. 完整代码

import { PDFDocument, StandardFonts, rgb, degrees } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";

let input = document.querySelector("#pdf-input");
let imgBytes;

input.onchange = onFileUpload;

// 上传文件
function onFileUpload(e) {
let event = window.event || e;

let file = event.target.files[0];
console.log(file);
if (file.size) {
  modifyPdf(file);
}
}

// 修改文档
async function modifyPdf(file) {
const pdfDoc = await PDFDocument.load(await file.arrayBuffer());

// 加载内置字体
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Courier);

// 加载自定义字体
const url = 'pttps://xxx.xxx/xxx';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

// 自定义字体挂载
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)

// 获取文档所有页
const pages = pdfDoc.getPages();

// 文字渲染配置
const drawTextParams = {
  lineHeight: 50,
  font: customFont,
  size: 12,
  color: rgb(0.08, 0.08, 0.2),
  rotate: degrees(15),
  opacity: 0.5,
};

let _img = await pdfDoc.embedJpg(await imgBytes.arrayBuffer());

for (let i = 0; i < pages.length; i++) {
  const page = pages[i];

  // 获取当前页宽高
  const { width, height } = page.getSize();

  // 要渲染的文字内容
  let text = "水印 water 121314";

  for (let ix = 1; ix < width; ix += 230) { // 水印横向间隔
    let lineNum = 0;
    for (let iy = 50; iy <= height; iy += 110) { // 水印纵向间隔
      lineNum++;
      page.drawImage(_img, {
        x: lineNum & 1 ? ix - 18 : ix + 70 - 18,
        y: iy - 8,
        width: 15,
        height: 15,
        opacity: 0.7,
      });
      page.drawText(text, {
        x: lineNum & 1 ? ix : ix + 70,
        y: iy,
        ...drawTextParams,
      });
    }
  }
}

// 保存文档 Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();

let blobData = new Blob([pdfBytes], { type: "application/pdf;Base64" });

// 新标签页预览
let a = document.createElement("a");
a.target = "_blank";
a.href = window.URL.createObjectURL(blobData);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}

// 加载 logo blob 数据
~(function loadImg() {
let img = new Image();
img.src = "./water-logo.png";

let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");

img.crossOrigin = "";
img.onload = function () {
  canvas.width = this.width;
  canvas.height = this.height;

  ctx.fillStyle = "rgba(255, 255, 255, 1)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.drawImage(this, 0, 0, this.width, this.height);
  canvas.toBlob(
    function (blob) {
      imgBytes = blob;
    },
    "image/jpeg",
    1
  ); // 参数为输出质量
};
})();

8. 不完美的地方

当前方案虽然可以实现在前端为 PDF 加水印,但是由于时间关系,有些瑕疵还需要再进一步探索解决 💪:

  1. 水印是浮在原文本之上的,可以被选中

  2. logo 的背景虽然不注意看不到,但是实际上还未完全透明 🤔

来源:http://www.cnblogs.com/iamzhiyudong/p/14990528.html

收起阅读 »

V8系列第二篇:从执行上下文的角度看JavaScript到底是怎么运行的

1.前言 先来说一说V8引擎和浏览器 V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空...
继续阅读 »


1.前言


先来说一说V8引擎和浏览器


V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空间和栈空间,而栈空间就是来管理执行上下文的。而执行上下文就可以说是我们平常写的JavaScript代码的运行环境。


好了简单理解一下,那接下来本篇就重点来学习一下V8引擎中的执行上下文


2.执行上下文概述



首先从宏观的角度来说: JavaScript代码要想能够被执行,就必须先被V8引擎编译,编译完成之后才会进入到执行阶段,总结为六个字:先编译再执行



在V8引擎编译的过程中,同时会生成执行上下文。最开始执行代码的时候通常会生成全局执行上下文、执行一个函数时会生成该函数的执行上下文、当执行一个代码块时也会生成代码块的可执行上下文。所以一段代码可以说成是先编译再执行,那么整个过程就是无数个先编译再执行构成的(通常编译发生在执行代码前的几微秒,甚至更短的时间)。


我们再来理解一下上面说到的执行上下文, 在JavaScript 高级程序设计(第四版)中大概是这样描述的:



执行上下文的概念在JavaScript中是非常重要的。变量或者函数的执行上下文决定了它们可以访问哪些数据,以及他们拥有哪些行为(可以执行哪些方法吧)。每个执行上下文都有一个关联的变量对象,而这个执行上下文中定义的所有变量和函数都存在于这个对象上。这个对象我们在代码中是无法访问的。



执行上下文可以说是执行一段JavaScript代码的运行环境,可以算作是一个抽象的概念。


简单的理解一下概念(下文如果再需要的时候你可以返回顶部再次理解查看)后,我们就来看看JavaScript是怎么将一个变量和函数运行起来的。


3.准备测试代码


这里为了更直观的查看代码的运行效果,我特意新建了一个xxxx.html文件,文件所有代码如下所示:
这里突然发现html文件中只有script标签和js代码也是可以执行的,不清楚以前是不是也是可以,还是说JavaScript引擎在后期做了优化处理。


<script>
a_Function()
var a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>


特别强调一个点,我上面声明变量使用的var关键字



运行后的执行结果


image.png


4.调试var声明的变量


相信通过运行结果,你心中应该有了自己的代码执行过程了。我们接着往下操作,在第2行代码(下文截图中的位置)打个调试断点,如下图所示


image.png


此时代码已经准备开始要执行a_Function函数了。脑补一下,我们就以此为分割点(按正常来说这肯定是不合理的,因为代码已经开始执行了,不过你可以暂且这样尝试去理解一下),就是运行到第2行代码之前的时间段或者状态,我们就称它为编译阶段,这之后代码就开始运行了,我们称它为执行阶段


1、通过截图可以发现,作用域下的全局 已经有了一个a_Function函数,以及一个a_variable变量其值为 undefined,这里可以看到许许多多的其他变量、函数,这其实就是全局window对象。


2、使用过JavaScript的人都清楚,JavaScript是按照顺序执行代码的,但是通过截图去看,好像又不太对劲,所以执行前的编译阶段,JavaScript引擎还是处理了不少事情的,它做了什么事情呢?


V8引擎编译这段代码的时候,同时会生成一个全局执行上下文,在截图的第二行代码发现是一个函数,便会在代码中查找到该函数的定义,并将该函数体放到全局执行上下文词法环境中。该函数体里的代码还未执行,所以不会去编译,继续第三行代码,发现是var声明的一个变量,便会将该变量放到全局执行上文变量环境中,同时给该变量赋值为undefined。


具体如下模拟代码


function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
var a_variable = undefined

这段代码主要在编译代码阶段做了变量提升,会将var声明的变量存放到变量环境中(let和const声明的变量存放到词法环境中),而函数的声明会被存放到词法环境中。
词法环境变量环境是存在于执行上下文的,变量的默认值会被设置为undefined,函数的执行体会被带到词法环境
然后还会生成可执行代码,其实编译生成的是字节码,下面的代码算是模拟代码:


a_Function()
a_variable = 'aehyok'
console.log(a_variable)

  • 执行阶段
    接下来开始按照顺序执行上面生成的可执行代码,其实在执行阶段已经变成了机器码

a_Function()
a_variable = 'aehyok'
console.log(a_variable)

第一行模拟代码:先调用a_Function,此时会开始生成该函数的函数执行上下文, 执行a_Function中的代码,函数a_Function执行了 undefined,因为此时的a_variable还没给予赋值操作


第二行模拟代码:对a_variable变量进行赋值字符串"aehyok",此时变量环境中的a_variable值变为"aehyok"


第三行模拟代码:打印已经赋值为aehyok的变量。


5.调试let声明的变量


5.1主要是将上面的测试代码中:声明变量的关键字var改为let


<script>
a_Function()
let a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行代码以后发现直接报错了,报错内容如下图所示


image.png


5.2打断点调试代码


image.png
代码断点打到如截图中第2行位置,可以看到let声明的变量,存在于单独的Script作用域中,并且赋值为undefined。


5.3分析5.1和5.2的代码


  • 通过varlet两种方式代码运行比对情况来看,let声明变量的方式不存在变量提升的情况。
  • 通过3.2截图可以发现,let声明变量的方式,在作用域中的已经创建,并赋值为undefined,但通过查阅资料发现:


let声明的变量,主要是因为V8虚拟机做了限制,虽然a_variable已经在内存中并且赋值为undefined,但是当你在let a_variable 之前访问a_variable时,根据ECMAScript定义,虚拟机会阻止的访问!也可以说成是形成了暂时性的死区,这是语法规定出来的。所以就会报错。



6.调试let声明的变量继续执行


主要添加了一个let声明的变量,以及为其进行了赋值操作,代码如下所示


<script>
a_Function()
var a_variable = 'a_aehyok'
let aa_variable = 'aa_aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行后情况截图如下


image.png


可以发现通过var声明的变量和let(也可以使用const)声明的变量被储存在了不同的位置,之前上面说过通过var声明的变量被存放到了变量环境中了。那么现在我再告诉你,通过let(也可以是const)声明的变量被存放到了词法环境中了。


  • var声明的变量存放在变量环境
  • let和const声明的变量存放在词法环境
  • 函数的声明存放在词法环境
  • 变量环境词法环境都存在于执行上下文

7.总结三种执行上下文


在上面的一小段代码中,我们已经使用过了两种执行上下文,全局执行上下文函数执行上下文


  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。


var声明的变量会在全局window对象上,而let和const声明的变量是不会在全局window对象上的。而全局函数时会在全局window对象上。




  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。



  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。



8.总结



  • 1、通过这篇简单的文章,我想我自己理清楚了,原来JavaScript代码是先编译再执行的。



  • 2、然后代码在编译的时候就生成了执行上下文,也就是代码运行的环境。



  • 3、var声明的变量存在变量提升,并且在编译阶段存放到了变量环境中,变量环境其实也是一个词法环境



  • 4、通过变量提升发现,代码会先生成执行上下文,然后再生成可执行的代码



  • 5、const和let声明的变量不存在变量提升,并且再编译阶段被存放到了词法环境中。



  • 6、所有var定义的全局变量和全局定义的函数,都会在window对象上。



  • 7、所有let和const定义的全局变量不会定义在全局上下文中,但是在作用域链的解析效果上是一样的(跟var定义的)。

 
收起阅读 »

V8开篇:V8是如何运行JavaScript(let a = 1)代码的?

我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言: 编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需...
继续阅读 »


我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:



编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果。




解释型语言:需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。



Java 和 C++ 等语言都是编译型语言,而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。V8 是众多JavaScript引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。


1.运行的整体过程


未命名文件 (4).png


2.英译汉翻译的过程


比如我们看到了google V8官网的一篇英文文章 v8.dev/blog/faster…,在阅读的过程中,可以就是要对每一个单词进行解析翻译成中文,然后多个单词进行语法的解析,再通过对整句话进行整个语句进行解析,那么这句话就翻译结束了。


下面我们就举例一句英文的翻译过程:I am a programmer。


  • 1、首先对输入的字符串I am a programmer。进行拆分便会拆分成 I am a programmer


相当于词法分析




  • 2、I 是一个主语, am 是一个谓语, a是一个形容词, programmer是个名词, 标点符号。



  • 3、I的意思, am的意思, a一个的意思, programmer程序员的意思, 句号的意思。




2和3一起相当于语法分析



  • 4、对3中的语法分析进行拼接处理:我是一个程序员。当然这是非常简单的一个英译汉,一篇文章的话,就会复杂一些了。


相当于语义分析



3.V8运行的整个过程


3.1.准备一段JavaScript源代码


let a = 10

3.2.词法分析:


一段源代码,就是一段字符串。编译器识别源代码的第一步就是要进行分词,将源代码拆解成一个个的token。所谓的token,就是不可再分的单个字符或者字符串。


3.3.token


通过 esprima.org/demo/parse.… 可以查看生成的tokens,也就是上面那段源代码生成的所有token。


Token类别: 关键字、标识符、字面量、操作符、数据类型(String、Numeric)等


image.png


3.4.语法分析


将上一步生成的 token 数据,根据语法规则转为 AST。通过astexplorer.net 可以查看生成AST抽象语法树。


3.5.AST


生成的AST如下图所示,生成过程就是先分词(词法分析),再解析(语法分析)


image.png
当然你也可以查看生成的AST的JSON结构


{
"type": "Program",
"start": 0,
"end": 9,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 9,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}

同样我在本地下载了v8,直接用v8来查看AST


v8-debug  --print-ast hello.js

image.png


3.6.解释器


解释器会将AST生成字节码,生成字节码的过程也就是对AST抽象语法树进行遍历循环,并进行语义分析


3.7.字节码


在最开始的V8引擎中是没有字节码,是直接将AST转换生成为机器码。这种架构存在的问题就是内存消耗特别大,尤其是在移动设备上,编译出来的机器码占了整个chorme浏览器的三分之一,这样为代码运行时留下的内存就更小了。
于是后来在V8中加入了Ignition 解释器,引入字节码,主要就是为了减少内存消耗。
本地可以使用V8命令行查看生成的字节码


v8-debug  --print-bytecode hello.js

image.png


3.8.热点代码


首先判断字节码是否为热点代码。通常第一次执行的字节码,Ignition 解释器会逐条解释执行。在执行的过程中,如果发现是热点代码,比如for 循环中的代码被执行了多次,这种就称之为热点代码。那么后台的TurboFan就会把该段热点代码编译为高效的机器码,然后再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了, 这样就大大提升了代码的执行效率。


3.9.编译器


TurboFan编译器也可以说是JIT的即时编译器,也可以说是优化编译器。



Ignition 解释器: 可以将AST生成字节码,还可以解释执行字节码。



4、总结


  • 了解V8整个的运行机制
  • 学习JavaScript到底是怎么运行的
  • 对日后编写JavaScript代码有非常多的好处
  • 看完学习了,能提升我们的技术水平
  • 对于日后遇到问题,能够从底层去思考问题出在那里,更快速的定位和解决问题
  • 真的非常熟悉了,可以自己开发一门新的语言
 
收起阅读 »

清除 useEffect 副作用

web
在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。import { useState, useEffect } f...
继续阅读 »

在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   const id = setInterval(async () => {
     const data = await fetchData();
     setList(list => list.concat(data));
  }, 2000);
   return () => clearInterval(id);
}, [fetchData]);

 return list;
}

🐚 问题

该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。

所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   async function getList() {
     const data = await fetchData();
     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => clearTimeout(id);
}, [fetchData]);

 return list;
}

不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。

在线示例:CodeSandbox


可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。

🌟如何解决

🐋 Promise Effect

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let getListPromise;
   async function getList() {
     const data = await fetchData();
     setList((list) => list.concat(data));
     return setTimeout(() => {
       getListPromise = getList();
    }, 2000);
  }

   getListPromise = getList();
   return () => {
     getListPromise.then((id) => clearTimeout(id));
  };
}, [fetchData]);
 return list;
}

🐳 AbortController

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。

清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
 if (signal.aborted) {
   return Promise.reject("aborted");
}
 return new Promise((resolve, reject) => {
   fetchData().then(resolve, reject);
   signal.addEventListener("aborted", () => {
     reject("aborted");
  });
});
}
function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   const controller = new AbortController();
   async function getList() {
     try {
       const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
       setList(list => list.concat(data));
       id = setTimeout(getList, 2000);
    } catch(e) {
       console.error(e);
    }
  }
   getList();
   return () => {
     clearTimeout(id);
     controller.abort();
  };
}, [fetchData]);

 return list;
}

🐬 状态标记

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。

定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   let unmounted;
   async function getList() {
     const data = await fetchData();
     if(unmounted) {
       return;
    }

     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => {
     unmounted = true;
     clearTimeout(id);
  }
}, [fetchData]);

 return list;
}

🎃 后记

问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。

这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。

只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。


再加上一般异步请求都比较快,所以大家也不会注意到这个问题。

所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~


作者:lizheming
链接:juejin.cn/post/7057897311187238919

收起阅读 »

如何用一个插件解决 Serverless 灰度发布难题?

web
Serverless 灰度发布什么是 Serverless ?Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serv...
继续阅读 »

Serverless 灰度发布

什么是 Serverless ?

Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serverless 平台上;同时享受 Serverless 按需付费,高弹性,低运维成本,事件驱动,降本提效等优势。

什么是 Serverless 灰度发布?

灰度发布又称为金丝雀发布( Canary Deployment )。过去,矿工们下矿井前,会先放一只金丝雀到井内,如果金丝雀在矿井内没有因缺氧、气体中毒而死亡后,矿工们才会下井工作,可以说金丝雀保护了工人们的生命。

与此类似,在软件开发过程中,也有一只金丝雀,也就是灰度发布(Gray release):开发者会先将新开发的功能对部分用户开放,当新功能在这部分用户中能够平稳运行并且反馈正面后,才会把新功能开放给所有用户。金丝雀发布就是从不发布,然后逐渐过渡到正式发布的一个过程。

那么对于部署在 Serverless 平台上的函数应该怎么进行灰度发布呢?

下文将以阿里云函数计算 FC 为例,为各位展开介绍。

灰度发布有一个流程,两种方式。

一个流程

Serverless 灰度发布是通过配置别名来实现的,别名可以配置灰度版本和主版本的流量比例,在调用函数时使用配置好的别名即可将流量按比例发送到相应版本。


配置灰度发布的流程如下:

  1. Service 中发布一个新版本。

  2. 创建或更新别名,配置别名关联新版本和稳定版本,新版本即为灰度版本。

  3. 将触发器 ( Trigger ) 关联到别名。

  4. 将自定义域名 ( Custom Domain ) 关联到别名。

  5. 在调用函数中使用别名,流量会按配置比例发送到新版本和稳定版本。

传统做法的两种方式

1、阿里云控制台 web 界面:

a.发布版本


b.创建别名


c.关联触发器


d.关联自定义域名


2、使用 Serverless Devs cli

a.发布版本

s cli fc version publish --region cn-hangzhou --service-name fc-deploy-service --description "test publish version"

b.创建别名并设置灰度

s cli fc alias publish --region cn-hangzhou --service-name fc-deploy-service --alias-name pre --version-id 1 --gversion 3 --weight 20

c.关联触发器

需要到控制台配置

d.关联自定义域名

需要到控制台配置可以看到,使用控制台或 Serverless Devs 进行灰度发布流程中的每一步,都需要用户亲自操作。并且由于配置繁多,极易出错。除了这些弊端以外,客户困扰的另一个问题是使用灰度发布策略非常不方便。

常见的灰度发布策略有 5 种:

    1. CanaryStep: 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

    2. LinearStep:分批发布,每批固定流量,间隔指定时间后再开始下一个批次。

    3. CanaryPlans:自定义灰度批次,每批次设定灰度流量和间隔时间,间隔指定时间后按照设定的流量进行灰度。

    4. CanaryWeight:手动灰度,直接对灰度版本设置对应的权重。

    5. FullWeight: 全量发布,全量发布到某一版本。

这些灰度策略中,前三项都需要配置间隔时间,而用户在控制台或者使用 Serverless Devs 工具去配置灰度都没有办法通过自动程序来配置间隔时间,不得不通过闹钟等方式提醒用户手动进行下一步灰度流程,这个体验是非常不友好的。下面我们介绍个能够帮您一键灰度发布函数的插件:FC-Canary 。

基于 Serverless Devs 插件 FC-Canary 的灰度发布

为了应对以上问题,基于 Serverless Devs 的插件 FC-Canary 应运而生,该插件可以帮助您通过 Serverless-Devs 工具和 FC 组件实现函数的灰度发布能力,有效解决灰度发布时参数配置繁杂、需要开发人员亲自操作以及可用策略少等问题。


(内容配置及注意事项-部分截图)

详细流程请见:github.com/devsapp/fc-…

FC-Canary 的优势

1、FC-Canary 支持超简配置

用户最短只需在 s.yaml 中增加 5 行代码代码即可开启灰度发布功能。


2、FC-Canary 配置指引简单清晰:


3、FC-Canary 支持多种灰度策略

  • 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

此时流量变化为:20%流量到新版本,10 分钟后 100%流量到新版本


  • 手动灰度,指定时直接将灰度版本设置对应的权重。

此时为 10%流量到新版本,90%到稳定版本


  • 自定义灰度,以数组的方式配置灰度变化。

此时流量变化为:10%到新版本 -> (5 分钟后) 30% 流量到新版本 -> (10 分钟后) 100% 流量到新版本


  • 分批发布,不断累加灰度比例直到 100%流量到新版本。

流量变化:40%到新版本 -> (10 分钟后) 80%流量到新版本 -> (再 10 分钟后) 100%流量到新版本


  • 全量发布,100%流量发到新版本


FC-Canary 插件支持上述 5 种灰度策略,用户选择所需策略并进行简单配置,即可体验一键灰度发布。

4、FC-Canary 灰度阶段提示清晰


插件对每一个里程碑都会以 log 的方式展现出来,给开发者足够的信心。

5、FC-Canary 支持钉钉群组机器人提醒


配置钉钉机器人即可在群中收到相关提醒,例如:


FC-Canary 最佳实践

使用 FC-Canary 插件灰度发布 nodejs 12 的函数。

代码仓库:

github.com/devsapp/fc-…

初始化配置

  • 代码配置


  • yaml 配置


我们采用 canaryWeight 的灰度策略:灰度发布后,50%的流量到新版本,50%的流量到旧版本。

进行第一次发布

  1. 执行发布

在 terminal 中输入: s deploy --use-local

  1. 查看结果

命令行输出的 log 中可以看到:


由于是第一次发布,项目中不存在历史版本,所以即使配置了灰度发布策略 ,FC-Canary 插件也会进行全量发布,即流量都发送到版本 1。

修改代码,第二次发布

  1. 在第二次发布前,我们修改一下代码,让代码抛出错误。


  1. 执行发布

在terminal中输入: s deploy --use-local

  1. 结果

命令行输出 log 中可以看到:


第二次发布,应用了灰度发布策略,即 50%流量发送到版本 1, 50%的流量发送到版本 2。

测试

获取 log 中输出的 domain,访问 domain 100 次后查看控制台监控大盘。


可以看到调用了函数 100 次,错误的函数有 49 次,正确的函数有 100 - 49 = 51 次,正确和错误的函数都约占总调用数的 50%。

分析:函数版本 1 为正确函数,函数版本 2 为错误函数,我们的灰度配置为流量 50% 到版本 1,50% 到版本 2,所以调用过程中,错误函数和正确函数应该各占 50%,图中结果符合我们的假设,这证明我们的灰度策略是成功的。

总结

我们可以发现相比使用控制台进行灰度发布,使用 FC-Canary 插件免去了用户手动创建版本、发布别名、关联触发器和管理自定义域名的麻烦,使用起来非常方便。

引申阅读

Serverless Devs 组件和插件的关系

  • 组件是什么?

根据 Serverless Devs Model v0.0.1 中说明, 组件 Component: 是由 Package developer 开发并发布的符合 Serverless Package Model 规范的一段代码,通常这段代码会在应用中被引用,并在 Serverless Devs 开发者工具中被加载,并按照预定的规则进行执行某些动作。例如,将用户的代码部署到 Serverless 平台;将 Serverless 应用进行构建和打包;对 Serverless 应用进行调试等。

举个例子:

如果想要使用 Serverless Devs 管理阿里云函数计算的函数计算资源,则需要在 yaml 配置文件中声明阿里云 FC 组件,之后便可以使用阿里云 FC 组件的能力。

FC 组件可以提供管理阿里云函数计算资源的能力,包括:管理服务、函数、版本、别名 等功能。组件地址:github.com/devsapp/fc

  • 插件是什么?

插件作为组件的补充,提供组件的原子性功能。

举个例子:

  1. 使用 FC 组件 deploy 的功能部署函数,可以在部署结束后采用 FC-Canary 插件对部署的函数进行灰度发布。

  2. 使用 FC 组件 deploy 的功能部署函数,可以在部署开始前采用 layer-fc 插件来降低部署过程中上传代码的耗时:即 layer-fc 可以让函数直接使用公共依赖库(远程)中的依赖,从而在部署时不再需要上传这些远程存在的依赖。

  • 组件和插件的关系?


  • 在 Serverless Devs Model 中,组件是占据核心地位,插件是辅助地位,也就是说,插件的目的是提升组件能力,提供给组件一些可选的原子性功能。

  • Serverless Devs 管理组件和插件的生命周期,如果是 pre 插件,则会让其在组件执行前执行,反之,post 插件则会在组件后完成一些收尾工作。

  • 一个组件可以同时使用多个插件, 其中组件插件的执行顺序是:

    1. 插件按照 yaml 顺序执行, 前一个插件的执行结果为后一个插件的入参

    2. 最后一个 pre 插件的输出作为组件的入参

    3. 组件的输出作为第一个 post 插件的入参

相关概念

  • FC 函数 (Function) 是系统调度和运行的单位,由函数代码和函数配置构成。FC 函数必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作,请参见 管理函数。函数计算支持事件函数和 HTTP 函数两种函数类型,关于二者的区别,请参见 函数类型。

  • 服务 (Service) 可以和微服务对标 ( 有版本和别名 ),多个函数可以共同组成服务单元。创建函数前必须先创建服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。

  • 触发器 (Trigger) 的作用是触发函数执行的。函数计算提供了一种事件驱动的计算模型。函数的执行可以通过函数计算控制台或 SDK 触发,也可以由其他一些事件源来触发。您可以在指定函数中创建触发器,该触发器描述了一组规则,当某个事件满足这些规则,事件源就会触发关联的函数。

  • 自定义域名(Custom Domain) 是函数计算提供为 Web 应用绑定域名的能力。

  • 版本 (Version) 是服务的快照,包括服务的配置、服务内的函数代码及函数配置,不包括触发器,当发布版本时,函数计算会为服务生成快照,并自动分配一个版本号与其关联,以供后续使用。

  • 别名 (Alias) 结合版本,帮助函数计算实现软件开发生命周期中的持续集成和发布。

最后,欢迎大家一起来贡献更多的开源插件!

参考链接:

Serverless Devs:

github.com/Serverless-…

FC 组件地址:

github.com/devsapp/fc

FC-Canary 插件具体信息及其使用请参考:

github.com/devsapp/fc-…

FC 函数管理:

help.aliyun.com/document_de…

FC 函数类型:

help.aliyun.com/document_de…


作者:长淇
来源:https://juejin.cn/post/7116556273662820382

收起阅读 »

生成二维码或条形码JavaScript脚本库

web
二维码或条形码在日常生活中现在应用已经非常普遍了,文章分享生成条形码和二维码的JavaScript库。条形码条形码是日常生活中比较常见的,主要用于商品。通俗的理解就是一串字符串的集合(含字母、数字及其它ASCII字符的集合应用),用来常用来标识一个货品的唯一性...
继续阅读 »


二维码或条形码在日常生活中现在应用已经非常普遍了,文章分享生成条形码和二维码的JavaScript库。

条形码

条形码是日常生活中比较常见的,主要用于商品。通俗的理解就是一串字符串的集合(含字母、数字及其它ASCII字符的集合应用),用来常用来标识一个货品的唯一性,当然还有更多更深入与广泛的应用,像超市的商品、衣服、微信、支付宝、小程序等到处都有条形码的广泛应用;

安装依赖:

npm install jsbarcode --save-dev

在 HTML 页面上加入以下代码:

<svg id="barcode"
jsbarcode-value="123456789012"
jsbarcode-format="code128"></svg>

接下来看下 JavaScript 代码,如下:

import jsbarcode from 'jsbarcode';
const createBarcode = (value, elemTarget) => {
  jsbarcode(elemTarget, "value");
};
createBarcode("#barcode", "devpoint");

运行成功的效果如下:


二维码

相比条形码,二维码的使用场景也越来也多,支付码、场所码、小程序等等。二维码的长相经常是在一个正方形的框中填充各种点点或无规则小图形块而构成的图形,这种称之为二维码,他与一维码最大的区别就是存储容量大很多,而且保密性好。二维码本质上表现给大家的就是一个静态图片,其实是包含特字加密算法的图形,里面存储的是一串字符串(即字母、数字、ASCII码等),这说明二维码不仅存储量大,而且存储的内容很广泛,数字、字母、汉字等都可以被存储。

安装依赖:

npm install qrcode --save-dev

HTML:

<canvas id="qrcode"></canvas>

JavaScript:

import QRCode from "qrcode";
const createQrcode = (value, elemTarget) => {
  QRCode.toCanvas(document.querySelector(elemTarget), value);
};
createQrcode("#qrcode", "devpoint");

效果如下:


来源:juejin.cn/post/7116156434605146126

收起阅读 »

前端取消请求与取消重复请求

一、前言 大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清...
继续阅读 »





一、前言


大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清除掉原来的内容,然后替换上新的内容,这个时候,如果我们的数据是通过服务从后端获取的,就会存在一个问题,由于获取数据是需要一定的时间的,就会存在当我们切换tab栏到新的tab页时,原来的tab页的服务还在响应中,这时新的tab页的数据服务已经响应完成了,且页面已经显示了新的tab页的内容,但是,这个时候旧的tab页的数据也成功了并返回了数据,并将新的tab页的内容覆盖了。。。所以为了避免这种情况的发生,我们就需要在切换tab栏发送新的请求之前,将原来的的请求取消掉,至于如何取消请求,这便是今天我要讲的内容。


二、项目准备


在正式学习之前,我们先搭建一个项目,并还原刚刚所说的场景,为了节省时间,我们使用脚手架搭建了一个前端vue+TS+vite项目,简单的做了几个Demo,页面如下,上面是我们现实内容的区域,点击tab1按钮时获取并展示tab1的内容,点击tab2按钮时获取并展示tab2的内容,以此类推,内容比较简单,这里就不放具体代码了。


image.png


然后我们需要搭建一个本地服务器,这里我们新建一个app.ts文件,使用express以及cors解决跨域问题去搭建一个简单的服务器,具体代码如下:

 
// app.ts
const express = require('express')
const app = express()

const cors = require('cors')
app.use(cors())

app.get('/tab1', (req, res) => {
res.send('这是tab1的内容...')
})

app.get('/tab2', (req, res) => {
setTimeout(() => {
res.send('这是tab2的内容...')
}, 3000)
})

app.get('/tab3', (req, res) => {
res.send('这是tab3的内容...')
})

app.listen('3000', () => {
console.log('server running at 3000 port...')
})



上面代码,我们新建了一个服务器并让他运行在本地的3000端口,同时在获取tab2的内容时,我们设置了3秒的延迟,以便实现我们想要的场景,然后我们使用node app.ts启动服务器,当终端打印了server running at 3000 port...就说明服务器启动成功了。


然后我们使用axios去发送请求,安装axios,然后我们在项目中src下面新建utils文件夹,然后新建request.ts文件,具体代码如下:


作者:还是那个大斌啊
链接:https://juejin.cn/post/7108359238598000671
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


import axios, { AxiosRequestConfig } from 'axios'

// 新建一个axios实例
const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

export function request(Args: AxiosRequestConfig) {
return ins.request(Args)
}



这里我们新建了一个axios实例,并配置了baseURL和超时时间,并做了一个简单的封装然后导出,需要注意的是,axios请求方法的别名有很多种,如下图这里就不做过多介绍了,大家想了解的可以去看官网,我们这里使用request方法。


image.png


最后,我们在页面上引入并绑定请求:

// bar.vue
<script setup lang="ts">
import { ref } from 'vue'
import { request } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



为了方便理解,将template部分代码也附上:

// bar.vue
<template>
<div class="container">
<div class="context">{{ context }}</div>
<div class="btns">
<el-button type="primary" @click="getTab1Context">tab1</el-button>
<el-button type="primary" @click="getTab2Context">tab2</el-button>
<el-button type="primary" @click="getTab3Context">tab3</el-button>
</div>
</div>
</template>



到这里,我们的项目准备工作就好了,看下效果图


取消请求1.gif


然后看下我们前面提到的问题:


取消请求2.gif
注意看,在我点击了tab2之后立马点击tab3,盒子中会先显示tab3的内容,然后又被tab2的内容覆盖了。




三、原生方法


项目准备好之后,我们就可以进入正题了,其实,关于取消请求的方法,axios官方就已经有了,所以我们先来了解下使用axios原生的方法如何取消请求:
先看下官方的代码:


可以使用 CancelToken.source 工厂方法创建 cancel token 像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else { /* 处理错误 */ }
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
});

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');



同时还可以通过传递一个executor函数到CancelToken的构造函数来创建 cancel token :

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();



这是官方提供的两种方法,我们将他们用到我们的项目上,因为都差不多,所以我们这里就只演示一种,选择通过传递函数的方式来取消请求;


进入项目utils文件夹下的request.ts文件,修改代码如下:

 
// request.ts

import axios, { AxiosRequestConfig } from 'axios'
const CancelToken = axios.CancelToken

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}
export function request(Args: AxiosRequestConfig) {
// 在请求配置中增加取消请求的Token
Args.cancelToken = new CancelToken(function (cancel) {
cancelFn = cancel
})
return ins.request(Args)
}



然后我们就可以在想要取消请求的地方调用cancelFn函数就可以了,我们给tab1tab3按钮都加上取消请求功能:

// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



这样取消请求的功能就完成了,看下效果:


取消请求3.gif


四、promise


除了官网的方式之外,其实我们也可以借助Promise对象,我们都知道,Promise对象的状态一旦确定就不能再改变的,基于这个原理,我们可以使用Promise封装下我们的请求,然后通过手动改变Promise的状态去阻止请求的响应,看下面代码:

 
// request.ts

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}

export function request(Args: AxiosRequestConfig): Promise<AxiosResponse> {
return new Promise((resolve, reject) => {
ins.request(Args).then((res: AxiosResponse) => {
resolve(res)
})
cancelFn = (msg) => {
reject(msg)
}
})
}



效果也是一样的


取消请求4.gif


需要注意的是,虽然效果是一样的,但是使用Promise的方式,我们只是手动修改了Promise的状态为reject,但是请求还是一样发送并响应了,没有取消,这个是和使用Axios原生方法的不同之处。


五、借助Promise.race


讲完了取消请求,其实还有一种场景也很常见,那就是取消重复请求,如果是要取消重复请求,我们又该怎么实现呢?其实我们可以借助Promise.racePromise.race的作用就是将多个Promise对象包装成一个,即它接受一个数组,每一个数组成员都是一个Promise对象,只要这些成员中有一个状态改变,Promise.race的状态就随之改变,基于这个原理,我们可以实现取消重复请求请求的目的。


基本思路就是,我们给每一个请求身边都放一个Promise对象,这个对象就是一颗炸弹,将他们一起放到Promise.race里面,当我们需要取消请求的时候就可以点燃这颗炸药。


还是上面的例子,我们针对按钮tab2做一个取消重复请求的功能,我们先声明一个类,在里面做取消重复请求的功能,在utils下新建cancelClass.ts文件:

 
// cancelClass.ts

import { AxiosResponse } from 'axios'
export class CancelablePromise {
pendingPromise: any
reject: any
constructor() {
this.pendingPromise = null
this.reject = null
}

handleRequest(requestFn: any): Promise<AxiosResponse> {
if (this.pendingPromise) {
this.cancel('取消了上一个请求。。。')
}
const promise = new Promise((resolve, reject) => (this.reject = reject))
this.pendingPromise = Promise.race([requestFn(), promise])
return this.pendingPromise
}

cancel(reason: string) {
this.reject(reason)
this.pendingPromise = null
}
}

上面代码中,我们声明了一个类,然后在类中声明了两个属性pendingPromisereject,一个request请求方法用来封装请求并判断上一个请求是否还在响应中,如果还未响应则手动取消上一次的请求,同时声明了一个promise对象,并将他的reject方法保存在类的reject属性中,然后用promise.race包装了请求函数和刚刚声明的promise对象。最后声明了一个cancel方法,在cancel方法中触发reject函数,来触发promise对象的状态改变,这样就无法获取到reuestFn的响应数据了。从而达到了取消请求的目的;


因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;

 

因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;


// request.ts

export function request(Args: AxiosRequestConfig) {
return () => ins.request(Args)
}

最后在页面中引入并使用:


// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'
import { CancelablePromise } from '@/utils/cancelClass'

...
const cancelablePromise = new CancelablePromise()
...
const getTab2Context = async () => {
const { data } = await cancelablePromise.handleRequest(
request({
url: '/tab2',
})
)

context.value = data
}
</script>

最后看下效果


取消请求5.gif


六、总结


到这里,我们前端取消请求和取消重复请求的方法就学习完了,需要注意的是,即使是使用官方的方法,也仅仅是取消服务器还没接收到的请求,如果请求已经发送到了服务端是取消不了的,只能让后端同时去处理了,使用promise的方法,仅仅只是通过改变promise的状态来阻止响应结果的接收,服务还是照常发送的。今天的分享就到这里了,如果对你有帮助的,请给我一个赞吧!

 










 


收起阅读 »

关于 Axios 的再再再封装,总是会有所不一样

特性 class 封装 可以多次实例化默认全局可以共用一个实例对象可以实例化多个对象,实例化时可以配置该实例特有的 headers根据各个接口的要求不同,也可以针对该接口进行配置设置请求拦截和响应拦截,这个都是标配了拦截处理系统响应状态码对应的提示语 拦截器 ...
继续阅读 »


特性


  • class 封装 可以多次实例化
  • 默认全局可以共用一个实例对象
  • 可以实例化多个对象,实例化时可以配置该实例特有的 headers
  • 根据各个接口的要求不同,也可以针对该接口进行配置
  • 设置请求拦截和响应拦截,这个都是标配了
  • 拦截处理系统响应状态码对应的提示语

拦截器


首先为防止多次执行响应拦截,这里我们将拦截器设置在类外部,如下:

import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}



封装主体


这里为了方便起见,实例化对象处理的其实就是传入的配置文件,而封装的方法还是按照 axios 原生的方法处理的。为了方便做校验在接口上都统一增加了客户端发起请求的时间,以方便服务端做校验。配置参数可参照文档 axios 配置文档

// 构造函数
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
// 全局使用
this.init = axios;
this.config = defaultConfig;
}



get 方法的配置

// Get 请求
get(url, params = {}, headers = {}) {

params.time = Date.now();

// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



post 请求

// POST 请求
post(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



PUT 请求

// PUT 请求
put(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



Delete 请求

// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



>>使用


完整的代码的代码在文末会贴出来,这里简单说下如何使用

// @/api/index.js
import Http,{Axios} from '@/api/http'; // Axios 数据请求方法

// ① 可以使用文件中实例化的公共对象 Axios


// ②也可以单独实例化使用
const XHttp = new Http({
headers: {
'x-token': 'xxx'
}
});


export const getArticles = (params={}) => {
return XHttp.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}

export const getArticle = (params={}) => {
return Axios.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}



在页面中使用

// @/views/home.vue
import { getArticles,getArticle } from '@/api/index.js'

// 两个方法名差一个字母 's'
getArticle({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})

getArticles({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})



完整代码

// @/api/http.js
/**
* 说明:
* 1.多实例化,可以根据不同的配置进行实例化,满足不同场景的需求
* 2.多实例化情况下,可共用公共配置
* 3.请求拦截,响应拦截 对http错误提示进行二次处理
* 4.接口可单独配置 header 满足单一接口的特殊需求
* body 直传字符串参数,需要设置 headers: {"Content-Type": "text/plain"}, 传参:System.licenseImport('"'+this.code+'"');
* import Http,{Axios} from '../http'; // Http 类 和 Axios 数据请求方法 如无特殊需求 就使用实例化的 Axios 方法进行配置 有特殊需求再进行单独实例化
*
*
*/
import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}

class Http {
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
this.init = axios;
this.config = defaultConfig;
}

// Get 请求
get(url, params = {}, headers = {}) {
// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// POST 请求
post(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// PUT 请求
put(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}


// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}
}

export default Http;

// 无特殊需求的只需使用这个一个对象即可 公共 header 可在此配置, 如需多个实例 可按照此方式创建多个进行导出
export const Axios = new Http({
baseURL:'https://docs.ycsnews.com',
headers: {
'x-http-token': 'xxx'
}
});











收起阅读 »

不要滥用effect哦

你或你的同事在使用useEffect时有没有发生过以下场景:当你希望状态a变化后发起请求,于是你使用了useEffect:useEffect(() => { fetch(xxx); }, [a])这段代码运行符合预期,上线后也没问题。随着需求不断迭代...
继续阅读 »

你或你的同事在使用useEffect时有没有发生过以下场景:

当你希望状态a变化后发起请求,于是你使用了useEffect

useEffect(() => {
fetch(xxx);
}, [a])

这段代码运行符合预期,上线后也没问题。

随着需求不断迭代,其他地方也会修改状态a。但是在那个需求中,并不需要状态a改变后发起请求。

你不想动之前的代码,又得修复这个bug,于是你增加了判断条件:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a])

某一天,需求又变化了!现在请求还需要b字段。

这很简单,你顺手就将b作为useEffect的依赖加了进去:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a, b])

随着时间推移,你逐渐发现:

  • 是否发送请求if条件相关
  • 是否发送请求还与a、b等依赖项相关
  • a、b等依赖项又与很多需求相关

根本分不清到底什么时候会发送请求,真是头大...

如果以上场景似曾相识,那么React新文档里已经明确提供了解决办法。

欢迎加入人类高质量前端框架群,带飞

一些理论知识

新文档中这一节名为Synchronizing with Effects,当前还处于草稿状态。

但是其中提到的一些概念,所有React开发者都应该清楚。

首先,effect这一节隶属于Escape Hatches(逃生舱)这一章。


从命名就能看出,开发者并不一定需要使用effect,这仅仅是特殊情况下的逃生舱。

React中有两个重要的概念:

  • Rendering code(渲染代码)
  • Event handlers(事件处理器)

Rendering code开发者编写的组件渲染逻辑,最终会返回一段JSX

比如,如下组件内部就是Rendering code

function App() {
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

Rendering code的特点是:他应该是不带副作用的纯函数

如下Rendering code包含副作用(count变化),就是不推荐的写法:

let count = 0;

function App() {
count++;
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

处理副作用

Event handlers组件内部包含的函数,用于执行用户操作,可以包含副作用

下面这些操作都属于Event handlers

  • 更新input输入框
  • 提交表单
  • 导航到其他页面

如下例子中组件内部的changeName方法就属于Event handlers

function App() {
const [name, update] = useState('KaSong');

const changeName = () => {
update('KaKaSong');
}

return <div onClick={changeName}>Hello {name}</div>;
}

但是,并不是所有副作用都能在Event handlers中解决。

比如,在一个聊天室中,发送消息是用户触发的,应该交给Event handlers处理。

除此之外,聊天室需要随时保持和服务端的长连接,保持长连接的行为属于副作用,但并不是用户行为触发的。

对于这种:在视图渲染后触发的副作用,就属于effect,应该交给useEffect处理。

回到开篇的例子:

当你希望状态a变化后发起请求,首先应该明确,你的需求是:

状态a变化,接下来需要发起请求

还是

某个用户行为需要发起请求,请求依赖状态a作为参数

如果是后者,这是用户行为触发的副作用,那么相关逻辑应该放在Event handlers中。

假设之前的代码逻辑是:

  1. 点击按钮,触发状态a变化
  2. useEffect执行,发送请求

应该修改为:

  1. 点击按钮,在事件回调中获取状态a的值
  2. 在事件回调中发送请求

经过这样修改,状态a变化发送请求之间不再有因果关系,后续对状态a的修改不会再有无意间触发请求的顾虑。

总结

当我们编写组件时,应该尽量将组件编写为纯函数。

对于组件中的副作用,首先应该明确:

用户行为触发的还是视图渲染后主动触发的

对于前者,将逻辑放在Event handlers中处理。

对于后者,使用useEffect处理。

这也是为什么useEffect所在章节在新文档中叫做Escape Hatches —— 大部分情况下,你不会用到useEffect,这只是其他情况都不适应时的逃生舱。

原文:https://segmentfault.com/a/1190000041942007

收起阅读 »

web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)

文章目录 简介函数的创建1 用构造函数创建2 用函数声明创建3 用函数表达式创建 函数的参数 参数特性1 调用函数时解析器不会检查实参的类型2 调用函数时解析器不会检查实参的数量3 当形参和实参过多,可以用一个对象封装 函数的返回值...
继续阅读 »


文章目录

  • 简介
  • 函数的创建

  • 函数的参数






  • 函数的返回值
  • 立即执行函数
  • 方法
  • 函数作用域
  • 补充:JavaScript中的作用域相关概念
  •  

    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建




    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:





    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)





    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。




    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。













    web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)







    苏凉.py

    已于 2022-06-16 00:40:01 修改

    596



    收藏

    88


















    🐚作者简介:苏凉(专注于网络爬虫,数据分析,正在学习前端的路上)
    🐳博客主页:苏凉.py的博客
    🌐系列专栏:web前端基础教程
    👑名言警句:海阔凭鱼跃,天高任鸟飞。
    📰要是觉得博主文章写的不错的话,还望大家三连支持一下呀!!!
    👉关注✨点赞👍收藏📂






    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建


    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:



    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)



    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    在这里插入图片描述


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。


    在这里插入图片描述


    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。


    在这里插入图片描述


    函数的参数


    • 可以在函数的()中来指定一个或多个形参(形式参数)。
    • 多个形参之间使用,隔开,声明形参就相当于在函数内部声明了对应的变量但是并不赋值。
    • 调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数中对应的形参。



    参数特性


    1 调用函数时解析器不会检查实参的类型


    函数的实参可以时任意数据类型,在调用函数时传递的实参解析器并不会检查实参的类型,因此需要注意,是否有可能接收到非法的参数,如果有可能则需要对参数进行类型的检查。




    2 调用函数时解析器不会检查实参的数量


    在调用函数传入实参时,解析器不会检查实参的数量,当实参数大于形参数时,多余实参不会被赋值




    当实参数小于形参数时,没有被赋值的形参为undefined。


    3 当形参和实参过多,可以用一个对象封装


    当形参和实参数量过多时,我们很容易将其顺序搞乱或者传递参数时出错,此时我们可以将数据封装在一个对象中,在进行实参传递时,传入该对象即可。




    函数的返回值


    可以使用return来设置函数的返回值



    语法:return 值


     


  • return后的值将会作为函数的执行结果返回
  • 可以定义一个变量,来接收该结果。
  • 在return后的语句都不会执行。

  • 若return后不跟任何值或者不写return,函数的返回值都是undefined。


    另外,在函数体中return返回的是什么,变量接受的就是什么。


    立即执行函数


    • 函数定义完,立即被调用,这种函数叫做立即执行函数
    • 立即执行函数往往只会执行一次
    • 通常为匿名函数的调用。


    语法:(function(形参...){语句...})(实参...);





    方法


    对象的属性值可以时任意的数据类型,当属性值为一个函数时,在对象中调用该函数,就叫做调用该对象的方法。




    函数作用域




  • 调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
  • 每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的
    在函数作用域中可以访问到全局作用域

  • 的变量,在全局作用域中无法访问到函数作用域的变量


    当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错ReferenceError




    补充:JavaScript中的作用域相关概念







    在全局作用域中有一个全局对象window它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用

  • 作用域指一个变量的作用范围
  • 在JavaScript中有两种作用域1.全局作用域 2.函数作用域
  • 直接编写在script标签中的JS代码,都在全局作用域
  • 全局作用域在页面打开时创建,在页面关闭时销毁

  • 简而言之我们创建的全局变量都作为一个属性保存在window这个对象中。


    而在函数中创建局部变量时,必须使用var关键字创建,否则为全局变量。



    收起阅读 »

    JavaScript映射与集合(Map、Set)数据类型基础知识介绍与使用

    文章目录 映射与集合(Map、Set)映射(Map)Map常用的方法不要使用map[key]访问属性对象作为Map的键Map的遍历与迭代默认的迭代方式forEach() 从数组、对象创建Map从数组、Map创建对象 集合(Set)集合迭代 ...
    继续阅读 »







    映射与集合(Map、Set)

    前文的学习过程中,我们已经了解了非常多的数据类型,包括基础类型、复杂的对象、顺序存储的数组等。为了更好的应对现实生产中的情况,我们还需要学习更多的数据类型:映射(Map)和集合(Set)。
    映射(Map)

    Map是一个键值对构成的集合,和对象非常相似,都是由一个名称对应一个值组成的。Map和对象区别在于,Map的键可以采用任何类型的数据,而对象只能使用字符串作为属性名称
    Map常用的方法

    new Map()——创建Map对象;
    map.set(key, val)——添加一个键值对;
    map.get(key)——通过键找到val值,如果不存在key,返回undefined
    map.has(key)——判断map是否存在键key,存在返回true,不存在返回false
    map.delete(key)——删除指定键;
    map.clear()——清空map中所有的内容;
    map.size——map中键值对的数量;

    举个例子:

    let map = new Map()//创建一个空的Map
    map.set('name','xiaoming') //字符串作为键
    map.set(3120181049,'ID') //数字作为键
    map.set(true,'Bool') //bool作为键

    console.log(map.get('name'))//xiaoming
    console.log(map.has(true)) //true
    console.log(map.delete(true))//删除true键
    console.log(map.size) //2
    console.log(map.clear()) //清空
    console.log(map.size) //0


    代码执行结果:



    map.set(key, val)方法返回map本身。


    不要使用map[key]访问属性


    虽然map[key]方式同样可以访问映射的键值对,但是不推荐使用这种方式,因为它会造成歧义。我们可以看下面的案例:



    let map = new Map()
    map[123] = 123 //创建一个键值对
    console.log(map[123])//123
    console.log(map['123'])


    这里就出现了一个奇怪的结果:


    image-20220610213719690


    不仅使用键123还可以使用'123'访问数据。


    甚至,如果我们使用map.set()map[]混用的方式,会引起程序错误。


    JavaScript中,如果我们对映射使用了map[key]=val的方式,引擎就会把map视为plain object,它暗含了对应所有相应的限制(仅支持StringSymbol键)。


    所以,我们不要使用map[key]的方式访问Map的属性!!


    对象作为Map的键




    由于Map对键的类型不做任何限制,我们还可以把对象当作键值使用:
    let clazz = {className:'9年1班'}
    let school = new Map()
    school.set(clazz,{stu1:'xiaoming',stu2:'xiaohong'})
    console.log(school.get(clazz))



    代码执行结果:


    image-20220610215432261


    在对象中,对象是不能作为属性名称存在的,如果我们把对象作为属性名,也会发生奇怪的事:

    let obj = {}
    let objKey = {key:'key'}
    obj[objKey] = 'haihaihai'
    console.log(obj['[object Object]'])


    代码执行结果:


    image-20220610215731673


    发生这种现象的原因也非常简单,对象会把非字符串、Symbol类型的属性名转为字符串类型,对象相应的就转为'[object Object]'了,于是对象中就出现了一个名为'[object Object]'的属性。





    Map键值比较方法


    Map使用SameValueZero算法比较键值是否相等,和===差不多,但是NaNNaN是相等的,所以NaN也可以作为键使用!





    链式调用


    由于map.set返回值是map本身,我们可以使用如下调用方式:


    map.set(1,1)
    .set(2,2)
    .set(3,3)




    Map的遍历与迭代


    我们可以在以下三个函数的帮助下完成映射的迭代:


    1. map.keys()——返回map所有键的可迭代对象;
    2. map.values()——返回map所有值的可迭代对象;
    3. map.entries()——返回map所有键值对的可迭代对象;

    举个栗子:



    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])

    //遍历所有的键
    for(let key of map.keys()){
    console.log(key)
    }

    //遍历所有的值
    for(let val of map.values()){
    console.log(val)
    }

    //遍历所有的键值对
    for(let ky of map.entries()){
    console.log(ky)
    }


    代码执行结果:


    image-20220611202407661



    遍历的顺序


    遍历的顺序和元素插入顺序是相同的,这是和对象的区别之一。





    默认的迭代方式


    实际上,我们很少使用map.entries()方法遍历Map中的键值对,因为map.entries()map的默认遍历方式,我们可以直接使用如下代码:


    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])
    for(let kv of map){
    console.log(kv)
    }





    码执行结果:


    image-20220611203140858


    forEach()


    我们还可以通过Map内置的forEach()方法,为每个元素设置一个遍历方法,就像遍历数组一样。


    举例如下:




    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])
    map.forEach((val,key,map)=>{
    console.log(`${key}-${val}`)
    })



    代码执行结果:


    image-20220611203643650


    从数组、对象创建Map


    可能童鞋们已经发现了,在上面的案例中,我们使用了一种独特的初始化方式(没有使用set方法)




    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])




    我们通过向new Map()传入一个数组,完成了快速的映射创建。


    我们还可以通过Object.entires(obj)方法将对象转为数组,该数组的格式和Map需要的格式完全相同。


    举个例子:




    let obj = {
    xiaoming:'heiheihei',
    xiaohong:'hahahahah'
    }
    let map = new Map(Object.entries(obj))
    console.log(map)



    代码执行结果:


    image-20220611205622630


    Object.entries(obj)会返回obj对应的数组:[['xiaoming':'heiheihei'],['xiaoming':'hahahahah']]




    从数组、Map创建对象


    Object.fromEntries()Object.entries()功能相反,可以把数组和Map转为对象。


    数组转对象:




    let obj = Object.fromEntries([
    ['key1','val1'],
    ['key2','val2'],
    ['key3','val3'],
    ])
    console.log(obj)



    代码执行结果:


    image-20220611210835380


    Map转对象:




    let map = new Map()
    map.set('key1','val1')
    .set('key2','val2')
    .set('key3','val3')
    let obj = Object.fromEntries(map)
    console.log(obj)



    代码执行结果:


    image-20220611211125496


    map.entries()会返回映射对应的键值对数组,我们也可以使用一种稍微麻烦的方式:

    let obj = Object.fromEntries(map.entries())





    1. new Set([iter])——创建一个集合,如果传入了一个可迭代变量(例如数组),就使用这个变量初始化集合
    2. set.add(val)——向集合中添加一个元素val
    3. set.delete(val)——删除集合中的val




    4. set.has(val)——判断集合中是否存在val,存在返回true,否则返回false
    5. set.clear()——清空集合中所有的元素
    6. set.size——返回集合中元素的数量


    集合使用案例:


    let set = new Set()
    let xiaoming = {name:'xiaoming'}
    let xiaohong = {name:'xiaohong'}
    let xiaojunn = {name:'xiaojunn'}



    set.add(xiaoming)
    set.add(xiaohong)
    set.add(xiaojunn)
    console.log(set)



    代码执行结果:


    image-20220611212417105





    虽然Set的功能很大程度上可以使用Array代替,但是如果使用arr.find判断元素是否重复,就会造成巨大的性能开销。


    所以我们需要在合适的场景使用合适的数据结构,从而保证程序的效率。



    集合迭代




    集合的迭代非常简单,我们可以使用for...offorEach两种方式:

    let set = new Set(['xiaoming','xiaohong','xiaoli'])//使用数组初始化集合
    for(let val of set){
    console.log(val)
    }
    set.forEach((val,valAgain,set)=>{
    console.log(val)
    })


    代码执行结果:


    image-20220611212802952




    注意,使用forEach遍历集合时,和map一样有三个参数,而且第一个和第二个参数完全相同。这么做的目的是兼容Map,我们可以方便的使用集合替换Map而程序不会出错。


    Map中使用的方法,Set同样适用:


    1. set.keys()——返回一个包含所有值的可迭代对象
    2. set.values()——返回值和set.keys()完全相同
    3. set.entries()——返回[val,val]可迭代对象



    看起啦这些方法有些功能上的重复,很奇怪。实际上,和forEach一样,都是为了和Map兼容。


    总结


    Map 是一个带键的数据项的集合。




    常用方法:





    1. map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
    2. map.has(key) —— 如果 key 存在则返回 true,否则返回 false
    3. new Map([iter]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化;
    4. map.set(key, val) —— 根据键存储值,返回 map 自身,可用于链式插入元素;




    5. map.delete(key) —— 删除指定键对应的值,如果在调用时 key 存在,则返回 true,否则返回 false
    6. map.clear() —— 清空 map中所有键值对 ;
    7. map.size —— 返回键值对个数

    与普通对象 Object 的不同点主要是任何类型都可以作为键,包括对象、NaN


    Set —— 是一组值的集合。


    常用方法和属性:
















    MapSet 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。


















  • new Set([iter]) —— 创建 set,可选择带有 iterable(例如数组)来进行初始化。
  • set.add(value) —— 添加一个值(如果 value 存在则不做任何修改),返回 set 本身。
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 元素的个数。
  • 收起阅读 »

    在浏览器输入URL到页面展示发生了什么

    查询缓存其实从填写上url按下回车后,我们就进入了第一步就是 DNS 解析过程,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录缓存的查找记录为:浏览器缓存=》系统缓存=》路由 器缓存缓存中没有则查找...
    继续阅读 »

    查询缓存


    其实从填写上url按下回车后,我们就进入了第一步就是 DNS 解析过程,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录缓存的查找记录为:浏览器缓存=》系统缓存=》路由 器缓存缓存中没有则查找系统的 hosts 文件中是否有记录,

    DNS服务器


    如果没有缓存则查询 DNS 服务器,得到服务器的 ip 地址后,浏览器根据这个 ip 以及相应的端口号发送连接请求;当然如果DNS服务器中没有解析成功,他会向上一步获得的顶级DNS服务器发送解析请求。


    TCP三次握手


    客户端和服务端都需要直到各自可收发,因此需要三次握手。

    从图片可以得到三次握手可以简化为:

    1、浏览器发送连接请求;
    2、服务器允许连接后并发送ACK报文给浏览器;
    2、浏览器接受ACK后并向后端发送一个ACK,TCP连接建立成功
    HTTP协议包

    构造一个 http 请求,这个请求报文会包括这次请求的信息,主要是请求方法,请求说明和请求附带的数据,并将这个 http 请求封装在一个 tcp 包中;这个 tcp 包也就是会依次经过传输层,网络层, 数据链路层,物理层到达服务器,服务器解析这个请求来作出响应;返回相应的 html 给浏览器;
    浏览器处理HTML文档

    因为 html 是一个树形结构,浏览器根据这个 html 来构建 DOM 树,在 dom 树的构建过程中如果遇到 JS 脚本和外部 JS 连接,则会停止构建 DOM 树来执行和下载相应的代码,这会造成阻塞,这就是为什么推荐 JS 代码应该放在 html 代码的后面;

    渲染树
    之后根据外部样式,内部样式,内联样式构建一个 CSS 对象模型树 CSSOM 树,构建完成后和 DOM 树合并为渲染树,在排除非视觉节点,比如 script,meta 标签和排除 display 为 none 的节点,之后进行布局,布局主要是确定各个元素的位置和尺寸,之后是渲染页面,因为 html 文件中会含有图片,视频,音频等资源,在解析 DOM 的过 程中,遇到这些都会进行并行下载,浏览器对每个域的并行下载数量有一定的限制,一 般是 4-6 个,当然在这些所有的请求中我们还需要关注的就是缓存,缓存一般通过 Cache-Control、Last-Modify、Expires 等首部字段控制。

    Cache-Control 和 Expires 的区别
    在于 Cache-Control 使用相对时间,Expires 使用的是基于服务器 端的绝对时间,因为存 在时差问题,一般采用 Cache-Control,在请求这些有设置了缓存的数据时,会先 查看 是否过期,如果没有过期则直接使用本地缓存,过期则请求并在服务器校验文件是否修 改,如果上一次 响应设置了 ETag 值会在这次请求的时候作为 If-None-Match 的值交给 服务器校验,如果一致,继续校验 Last-Modified,没有设置 ETag 则直接验证 Last-Modified,再决定是否返回 304

    到这里就结束了么?其实按照标题所说的到渲染页面我们确实到此就说明完了,但是严格意义上其实我们后面还会有TCP的四次挥手断开连接,这个我们就放到后面单独出一篇为大家介绍吧!
    TCP 和 UDP 的区别

    1、TCP 是面向连接的,udp 是无连接的即发送数据前不需要先建立链接。
    2、TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失, 不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付。 并且因为 tcp 可靠, 面向连接,不会丢失数据因此适合大数据量的交换。
    3、TCP 是面向字节流,UDP 面向报文,并且网络出现拥塞不会使得发送速率降低(因 此会出现丢包,对实时的应用比如 IP 电话和视频会议等)。
    4、TCP 只能是 1 对 1 的,UDP 支持 1 对 1,1 对多。
    5、TCP 的首部较大为 20 字节,而 UDP 只有 8 字节。
    6、TCP 是面向连接的可靠性传输,而 UDP 是不可靠的。



    收起阅读 »

    一定要优雅,高端前端程序员都应该具备的基本素养

    近来看到很多公司裁员,忽然惊醒,之前是站在项目角度考虑问题,却没站在咱们程序员本身看待问题,险些酿成大错,如果人人都能做到把项目维护得井井有条,无论什么人都能看明白都能快速接手,那咱们的竞争力在哪里呢?这个时候我再看项目中那些被我天天骂的代码,顿时心中就无限景...
    继续阅读 »

    近来看到很多公司裁员,忽然惊醒,之前是站在项目角度考虑问题,却没站在咱们程序员本身看待问题,险些酿成大错,如果人人都能做到把项目维护得井井有条,无论什么人都能看明白都能快速接手,那咱们的竞争力在哪里呢?这个时候我再看项目中那些被我天天骂的代码,顿时心中就无限景仰起来,原来屎山才是真能能够保护我们的东西,哪有什么岁月静好,只是有人替你负屎前行罢了


    为了能让更多人认识到这一点,站在前端的角度上,我在仔细拜读了项目中的那些暗藏玄机的代码后,决定写下此文,由于本人功力尚浅,且之前一直走在错误的道路上,所以本文在真正的高手看来可能有些班门弄斧,在此献丑了🐶



    用 TypeScript,但不完全用


    TypeScript大行其道,在每个团队中,总有那么些个宵小之辈想尽一切办法在项目里引入 ts,这种行为严重阻碍了屎山的成长速度,但同是打工人我们也不好阻止,不过就算如此,也无法阻止我们行使正义


    众所周知,TypeScript 别名 AnyScript,很显然,这就是TypeScript创始人Anders Hejlsberg给我们留下的暗示,我们有理由相信AnyScript 才是他真正的目的

    const list: any = []
    const obj: any = {}
    const a: any = 1

    引入了 ts的项目,由于是在原可运行代码的基础上额外添加了类型注释,所以代码体积毫无疑问会增大,有调查显示,可能会增加 30%的代码量,如果充分发挥 AnyScript 的宗旨,意味着你很轻松地就让代码增加了 30% 毫无用处但也挑不出啥毛病的代码,这些代码甚至还会增加项目的编译时间(毕竟增加了ts校验和移除的成本嘛)


    你不仅能让自己写的代码用上 AnyScript,甚至还可以给那些支持 ts 的第三方框架/库一个大嘴巴子

    export default defineComponent({
    props: {
    // 现在 data 是 any 类型的啦
    data: {
    type: Number as PropType<any>,
    },
    },
    setup(_, { emit }) {
    // 现在 props 是 any 类型的啦
    const props: any = _
    ...
    }
    })

    当然了,全屏 any可能还是有点明显了,所以你可以适当地给部分变量加上具体类型,但是加上类型不意味着必须要正确使用

    const obj: number[] = []
    // ...
    // 虽然 obj 是个 number[],但为了实现业务,就得塞入一些不是 number 的类型,我也不想的啊是不是
    // 至于编辑器会划红线报错?那是小问题,不用管它,别人一打开这个项目就是满屏的红线,想想就激动
    obj.push('2')
    obj.push([3])

    命名应该更自由

    命名一直是个困扰很多程序员的问题,究其原因,我们总想给变量找个能够很好表达意思的名称,这样一来代码的可阅读性就高了,但现在我们知道,这并不是件好事,所以我们应该放纵自我,既摆脱了命名困难症,又加速了屎山的堆积进度

    const a1 = {}
    const a2 = {}
    const a3 = 2
    const p = 1

    我必须强调一点,命名不仅是变量命名,还包含文件名、类名、组件名等,这些都是我们可以发挥的地方,例如类名

    <div class="box">
    <div class="box1"></div>
    <div class="box2"></div>
    <div>
    <div class="box3"></div>

    乍一看似乎没啥毛病,要说有毛病似乎也不值当单独挑出来说,没错,要的就是这个效果,让人单看一段代码不好说什么,但是如果积少成多,整个项目都是 box呢?全局搜索都给你废了!如果你某些组件再一不小心没用 scoped 呢?稍不留意就不知道把什么组件的样式给改了,想想就美得很


    关于 css我还想多说一点,鉴于其灵活性,我们还可以做得更多,总有人说什么 BEMBEM的,他们敢用我们就敢写这样的代码

    &-card {
    &-btn {
    &_link {
    &--right {
    }
    }
    &-nodata {
    &_link {
    &--replay {
    &--create {}
    }
    }
    }
    }
    &-desc {}
    }

    好了,现在请在几百行(关于这一点下一节会说到)这种格式的代码里找出类名 .xxx__item_current.mod-xxx__link 对应的样式吧


    代码一定要长


    屎山一定是够高够深的,这就要求我们的代码应该是够长够多的


    大到一个文件的长度,小到一个类、一个函数,甚至是一个 if 的条件体,都是我们自由发挥的好地方。


    什么单文件最好不超过 400行,什么一个函数不超过 100行,简直就是毒瘤,


    1.jpg


    所以这就要求我们要具备将十行代码就能解决的事情写成一百行的能力,最好能给人一种多即是少的感觉

    data === 1
    ? 'img'
    : data === 2
    ? 'video'
    : data === 3
    ? 'text'
    : data === 4
    ? 'picture'
    : data === 5
    ? 'miniApp'

    三元表达式可以优雅地表达逻辑,像诗一样,虽然这段代码看起来比较多,但逻辑就是这么多,我还专门用了三元表达式优化,不能怪我是不是?什么map映射枚举优化听都没听过

    你也可以选择其他一些比较容易实现的思路,例如,多写一些废话

    if (a > 10) {
    // 虽然下面几个 if 中对于 a 的判断毫无用处,但不仔细看谁能看出来呢?看出来了也不好说什么,毕竟也没啥错
    // 除此之外,多级 if 嵌套也是堆屎山的一个小技巧,什么提前 return 不是太明白
    if (a > 5) {
    if (a > 3 && b) {

    }
    }
    if (a > 4) {

    }
    }

    除此之外,你还可以写一些中规中矩的方法,但重点在于这些方法根本就没用到,这种发挥的地方就更多了,简直就是扩充代码体积的利器,毕竟单看这些方法没啥毛病,但谁能想到根本就用不到呢?就算有人怀疑了,但你猜他敢随便从运行得好好的业务项目里删掉一些没啥错的代码吗?


    组件、方法多多滴耦合


    为了避免其他人复用我的方法或组件,那么在写方法或组件的时候,一定要尽可能耦合,提升复用的门槛


    例如明明可以通过 Props传参解决的事情,我偏要从全局状态里取,例如vuex,独一份的全局数据,想传参就得改 store数据,但你猜你改的时候会不会影响到其他某个页面某个组件的正常使用呢?如果你用了,那你就可能导致意料之外的问题,如果你不用你就得自己重写一个组件


    组件不需要传参?没关系,我直接把组件的内部变量给挂到全局状态上去,虽然这些内部变量确实只有某一个组件在用,但我挂到全局状态也没啥错啊是不是


    嘿,明明一个组件就能解决的事情,现在有了倆,后面还可能有仨,这代码量不就上来了吗?


    方法也是如此,明明可以抽取参数,遵循函数式编程理念,我偏要跟外部变量产生关联

    // 首先这个命名就很契合上面说的自由命名法
    function fn1() {
    // ...
    // fn1 的逻辑比较长,且解决的是通用问题,
    // 但 myObj 偏偏是一个外部变量,这下看你怎么复用
    window.myObj.name = 'otherName'
    window.myObj.children.push({ id: window.myObj.children.length })
    // ...
    }

    魔术字符串是个好东西

    实际上,据我观察,排除掉某些居心不轨的人之外,大部分人还是比较喜欢写魔术字符串的,这让我很欣慰,看着满屏的不知道从哪里冒出来也不知道代表着什么的硬编码字符串,让人很有安全感

    if (a === 'prepare') {
    const data = localStorage.getItem('HOME-show_guide')
    // ...
    } else if (a === 'head' && b === 'repeating-error') {
    switch(c) {
    case 'pic':
    // ...
    break
    case 'inDrawer':
    // ...
    break
    }
    }

    基于此,我们还可以做得更多,比如用变量拼接魔术字符串,debug的时候直接废掉全局搜索

    if (a === query.name + '_head') {

    }

    大家都是中国人,为什么不试试汉字呢?

    if (data === '正常') {

    } else if (data === '错误') {

    } else if (data === '通过') {

    }

    轮子就得自己造才舒心


    众所周知,造轮子可以显著提升我们程序员的技术水平,另外由于轮子我们已经自己造了,所以减少了对社区的依赖,同时又增加了项目体积,有力地推动了屎山的成长进程,可以说是一鱼两吃了


    例如我们可能经常在项目中使用到时间格式化的方法,一般人都是直接引入 dayjs完事,太肤浅了,我们应该自己实现,例如,将字符串格式日期格式化为时间戳

    function format(str1: any, str2: any) {
    const num1 = new Date(str1).getTime()
    const num2 = new Date(str2).getTime()
    return (num2 - num1) / 1000
    }

    多么精简多么优雅,至于你说的什么格式校验什么 safari下日期字符串的特殊处理,等遇到了再说嘛,就算是dayjs不也是经过了多次 fixbug才走到今天的嘛,多一些宽松和耐心好不好啦


    如果你觉得仅仅是 dayjs这种小打小闹难以让你充分发挥,你甚至可以造个 vuexvue官网上写明了eventBus可以充当全局状态管理的,所以我们完全可以自己来嘛,这里就不举例了,这是自由发挥的地方,就不局限大家的思路了


    借助社区的力量-轮子还是别人的好


    考虑到大家都只是混口饭吃而已,凡事都造轮子未免有些强人所难,所以我们可以尝试走向另外一个极端——凡事都用轮子解决


    判断某个变量是字符串还是对象,kind-of拿来吧你;获取某个对象的 keyobject-keys拿来吧你;获取屏幕尺寸,vue-screen-size拿来吧你……等等,就不一一列举了,需要大家自己去发现


    先甭管实际场景是不是真的需要这些库,也甭管是不是杀鸡用牛刀,要是大家听都没听过的轮子那就更好了,这样才能彰显你的见多识广,总之能解决问题的轮子就是好问题,


    在此我得特别提点一下 lodash,这可是解决很多问题的利器,但是别下载错了,得是 commonjs版本的那个,量大管饱还正宗,es module版本是不行滴,太小家子气


    import _ from 'lodash'

    多尝试不同的方式来解决相同的问题


    世界上的路有很多,很多路都能通往同一个目的地,但大多数人庸庸碌碌,只知道沿着前人的脚步,没有自己的思想,别人说啥就是啥,这种行为对于我们程序员这种高端的职业来说,坏处很大,任何一个有远大理想的程序员都应该避免


    落到实际上来,就是尝试使用不同的技术和方案解决相同的问题

    搞个css模块化方案,什么BEMOOCSSCSS ModulesCSS-in-JS 都在项目里引入,紧跟潮流扩展视野

    vue项目只用 template?逊啦你,render渲染搞起来

    之前看过什么前端依赖注入什么反射的文章,虽然对于绝大多数业务项目而言都是水土不服,但问题不大,能跑起来就行,引入引入

    还有那什么 rxjs,人家都说好,虽然我也不知道好在哪里,但胜在门槛高一般人搞不清楚所以得试试

    Pinia 是个好东西,什么,我们项目里已经有 vuex了?out啦,人家官网说了 vue2也可以用,我们一定要试试,紧跟社区潮流嘛,一个项目里有两套状态管理有什么值得大惊小怪的!


    做好自己,莫管他人闲事

    看过一个小故事,有人问一个年纪很大的老爷爷的长寿秘诀是什么,老爷爷说是从来不管闲事

    这个故事对我们程序员来说也很有启发,写好你自己的代码,不要去关心别人能不能看得懂,不要去关心别人是不是会掉进你写的坑里

    mounted() {
    setTimeout(() => {
    const width = this.$refs.box.offsetWidth
    const itemWidth = 50
    // ...
    }, 200)
    }

    例如对于上述代码,为什么要在 mounted里写个 setTimeout呢?为什么这个 setTimeout的时间是 200呢?可能是因为 box 这个元素大概会在 mounted之后的 200ms左右接口返回数据就有内容了,就可以测量其宽度进行其他一系列的逻辑了,至于有没有可能因为网络等原因超过 200ms还是没有内容呢?这些不需要关心,你只要保证在你开发的时候 200ms这个时间是没问题的就行了;
    itemWidth代表另外一个元素的宽度,在你写代码的时候,这个元素宽度就是 50,所以没必要即时测量,你直接写死了,至于后面其他人会不会改变这个元素的宽度导致你这里不准了,这就不是你要考虑的事情了,你开发的时候确实没问题,其他人搞出来问题其他人负责就行,管你啥事呢?


    代码自解释


    高端的程序员,往往采用最朴素的编码方式,高手从来不写注释,因为他们写的代码都是自解释的,什么叫自解释?就是你看代码就跟看注释一样,所以不需要注释


    我觉得很有道理,代码都在那里搁着了,逻辑写得清清楚楚,为啥还要写注释呢,直接看代码不就行了吗?


    乍一看,似乎这一条有点阻碍堆屎山的进程,实则不然


    一堆注定要被迭代无数版、被无数人修改、传承多年的代码,其必定是逻辑错综复杂,难免存在一些不可名状的让人说不清道不明的逻辑,没有注释的加成,这些逻辑大概率要永远成为黑洞了,所有人看到都得绕着走,相当于是围绕着这些黑洞额外搭起了一套逻辑,这代码体积和复杂度不就上来了吗?


    如果你实在手痒,倒也可以写点注释,我这里透露一个既能让你写写注释过过瘾又能为堆屎山加一把力的方法,那就是:在注释里撒谎!


    没错,谁说注释只能写对的?我理解不够,所以注释写得不太对有什么奇怪的吗?我又没保证注释一定是对的,也没逼着你看注释,所以你看注释结果被注释误导写了个bug,这凭啥怪我啊

    // 计算 data 是否可用
    //(实际上,这个方法的作用是计算 data 是否 不可用)
    function isDisabledData(data: any) {
    // ...
    }

    上述这个例子只能说是小试牛刀,毕竟多调试一下很容易被发现的,但就算被发现了,大家也只会觉得你只是个小粗心鬼罢了,怎么好责怪你呢,这也算是给其他人的一个小惊喜了,况且,万一真有人不管不顾就信了,那你就赚大了


    编译问题坚决不改


    为了阻碍屎山的成长速度,有些阴险的家伙总想在各种层面上加以限制,例如加各种lint,在编译的时候,命令行中就会告诉你你哪些地方没有按照规则来,但大部分是 waring 级别的,即你不改项目也能正常运行,这就是我们的突破点了。


    尽管按照你的想法去写代码,lint的事情不要去管,waring报错就当没看到,又不是不能用?在这种情况下,如果有人不小心弄了个 error级别的错误,他面对的就是从好几屏的 warning 中找他的那个 error 的场景了,这就相当于是提前跟屎山来了一次面对面的拥抱


    根据破窗理论,这种行为将会影响到越来越多的人,大家都将心照不宣地视 warning于无物(从好几屏的 warning中找到自己的那个实在是太麻烦了),所谓的 lint就成了笑话


    小结


    一座历久弥香的屎山,必定是需要经过时间的沉淀和无数人的操练才能最终成型,这需要我们所有人的努力,多年之后,当你看到你曾经参与堆砌的屎山中道崩殂轰然倒塌的时候,你就算是真的领悟了我们程序员所掌控的恐怖实力!🐶


    链接:https://juejin.cn/post/7107119166989336583
    收起阅读 »

    vue-cli3 一直运行 /sockjs-node/info?t= 解决方案

    首先 sockjs-node 是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟、全双工的浏览器和web服务器之间通信通道。服务端:sockjs-node(https://github.com/sock...
    继续阅读 »

    首先 sockjs-node 是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟、全双工的浏览器和web服务器之间通信通道。

    服务端:sockjs-node(https://github.com/sockjs/sockjs-node)
    客户端:sockjs-clien(https://github.com/sockjs/sockjs-client)

    如果你的项目没有用到 sockjs,vuecli3 运行 npm run serve 之后 network 里面一直调研一个接口:http://localhost:8080/sockjs-node/info?t=1462183700002

    作为一个有节操的程序猿,实在不能忍受,特意自己研究了下源码,从根源上关闭这个调用

    1. 找到/node_modules/sockjs-client/dist/sockjs.js 

    2.找到代码的 1605行  

    try {
    // self.xhr.send(payload); 把这里注掉
    } catch (e) {
    self.emit('finish', 0, '');
    self._cleanup(false);
    }

    3.刷新,搞定。

    原文:https://www.cnblogs.com/sichaoyun/p/10178080.html


    收起阅读 »

    解决mpvue小程序分享到朋友圈无效问题

    手动修改一下mpvue这个包,在node_modules里面找到mpvue在index里面搜索下onShareAppMessage找到// 用户点击右上角分享onShareAppMessage: rootVueVM.$options.onShareAppMes...
    继续阅读 »

    手动修改一下mpvue这个包,在node_modules里面找到mpvue在index里面
    搜索下onShareAppMessage找到

    // 用户点击右上角分享
    onShareAppMessage: rootVueVM.$options.onShareAppMessage
    ? function (options) { return callHook$1(rootVueVM, 'onShareAppMessage', options); } : null,

    在这一段代码下面添加一个处理就可以了

    // 分享朋友圈
    onShareTimeline: rootVueVM.$options.onShareTimeline
    ? function (options) { return callHook$1(rootVueVM, 'onShareTimeline', options); } : null,

    最好也在LIFECYCLE_HOOKS这个数组中把onShareTimeline这个添加进去

    var LIFECYCLE_HOOKS = [
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeUpdate',
    'updated',
    'beforeDestroy',
    'destroyed',
    'activated',
    'deactivated', 'onLaunch',
    'onLoad',
    'onShow',
    'onReady',
    'onHide',
    'onUnload',
    'onPullDownRefresh',
    'onReachBottom',
    'onShareAppMessage',
    'onShareTimeline',
    'onPageScroll',
    'onTabItemTap',
    'attached',
    'ready',
    'moved',
    'detached'
    ];

    然后打包,完美解决

    如果项目中因为页面问题引入了例如mpvue-factory这种插件的还需要处理一下,用下面这个文件去处理吧,两个问题一起处理。

    再高级一点的话,可以写一个fix命令,复制我下面的,放到build文件夹,检查下你们的相对路径是不是对的,不对的话改一下你们的文件目录指向,然后自己去package里面加命令执行这个文件,直接命令跑一下就可以

    var chalk = require('chalk')
    var path = require('path')
    var fs = require('fs')
    var data = ''
    var dataFactory = ''

    const hookConfig = '\'onShareAppMessage\','
    const hookFn = '// 用户点击右上角分享\n' +
    ' onShareAppMessage: rootVueVM.$options.onShareAppMessage\n' +
    ' ? function (options) { return callHook$1(rootVueVM, \'onShareAppMessage\', options); } : null,'
    const mpVueSrc = '../node_modules/mpvue/index.js'
    const mpVueFactorySrc = '../node_modules/mpvue-page-factory/index.js'

    const factoryHook = 'onShareAppMessage: App.onShareAppMessage ?\n' +
    ' function (options) {\n' +
    ' var rootVueVM = getRootVueVm(this);\n' +
    ' return callHook$1(rootVueVM, \'onShareAppMessage\', options);\n' +
    ' } : null,'
    try {
    data = fs.readFileSync(path.join(__dirname, mpVueSrc), 'utf-8')
    if (data.indexOf('onShareTimeline') === -1) {
    data = replaceHook(data)
    }
    fs.writeFileSync(path.join(__dirname, mpVueSrc), data)
    } catch (e) {
    console.error(e)
    }

    try {
    dataFactory = fs.readFileSync(path.join(__dirname, mpVueFactorySrc), 'utf-8')
    if (dataFactory.indexOf('onShareTimeline') === -1) {
    dataFactory = replaceFactoryHook(dataFactory)
    }
    fs.writeFileSync(path.join(__dirname, mpVueFactorySrc), dataFactory)
    } catch (e) {
    console.error(e)
    }

    // 处理mpvue框架中没有处理onShareTimeline方法的问题
    function replaceHook(str) {
    let res = str.replace(hookConfig, '\'onShareAppMessage\',\n' +
    ' \'onShareTimeline\',')
    res = res.replace(hookFn, '// 用户点击右上角分享\n' +
    ' onShareAppMessage: rootVueVM.$options.onShareAppMessage\n' +
    ' ? function (options) { return callHook$1(rootVueVM, \'onShareAppMessage\', options); } : null,\n' +
    '\n' +
    ' // 分享朋友圈\n' +
    ' onShareTimeline: rootVueVM.$options.onShareTimeline\n' +
    ' ? function (options) { return callHook$1(rootVueVM, \'onShareTimeline\', options); } : null,')

    return res
    }

    // 处理mpvue-factory插件中没有处理onShareTimeline方法的问题
    function replaceFactoryHook(str) {
    let res = str.replace(factoryHook, 'onShareAppMessage: App.onShareAppMessage ?\n' +
    ' function (options) {\n' +
    ' var rootVueVM = getRootVueVm(this);\n' +
    ' return callHook$1(rootVueVM, \'onShareAppMessage\', options);\n' +
    ' } : null,\n' +
    '\n' +
    ' // 用户点击右上角分享\n' +
    ' onShareTimeline: App.onShareTimeline ?\n' +
    ' function (options) {\n' +
    ' var rootVueVM = getRootVueVm(this);\n' +
    ' return callHook$1(rootVueVM, \'onShareTimeline\', options);\n' +
    ' } : null,')
    return res
    }
    console.log(chalk.green(
    ' Tip: fix mpvue_share Success!'
    ))


    原文链接:https://blog.csdn.net/weixin_41961749/article/details/107402802


    收起阅读 »

    还不知道npm私服?一篇教会你搭建私服并发布vue3组件库到nexus

    日常工作时,出于保密性、开发便捷性等需求,或者是还在内部测试阶段,我们可能需要将vue3组件库部署到公司的nexus中。我们可能希望部署vue3组件库的操作是CI/CD中的一环。节点:npm发布依赖包安装建木CI,参考建木快速开始安装nexus搭建npm私服,...
    继续阅读 »

    介绍

    日常工作时,出于保密性、开发便捷性等需求,或者是还在内部测试阶段,我们可能需要将vue3组件库部署到公司的nexus中。我们可能希望部署vue3组件库的操作是CI/CD中的一环。

    现在建木CI有了自动发布构件的官方npm节点,这一切都将变得非常简单。

    节点:npm发布依赖包

    准备工作

    • 安装建木CI,参考建木快速开始

    • 安装nexus搭建npm私服,创建用户、开启token验证、生成token

    1. 安装sonatype nexus

    # docker search nexus 搜索nexus 下载量最高的那个sonatype/nexus3
    docker search nexus

    # 从docker hub中将sonatype nexus拉取下来
    docker pull sonatype/nexus3

    # 启动sonatype nexus并使其监听8081端口
    docker run -d -p 8081:8081 --name nexus sonatype/nexus3
    复制代码

    访问搭建的nexus,可以看到如下界面,那么nexus搭建成功


    接下来,需要登录管理员(第一次登录会提供密码,然后要求改密码),创建Blob Stores的数据池地址,供后面仓库选择


    创建我们的私有npm库,需要注意的是我们要创建三个仓库(仓库名有辨识即可)

    • group见名知意,是一个仓库组,包含多个具体的仓库(proxy、hosted)

    • hosted本地仓库,就是我们内部发布包的地址

    • proxy代理仓库,会去同步代理仓库的npm包(即下载本地仓库没有的包时,会去代理仓库下载,代理仓库可设置为官方仓库)


    创建proxy仓库,需要设置一些值


    创建hosted仓库,需要设置一些值


    创建group仓库,选择我们之前创建的两个仓库


    大功告成!查看这个hosted类型的地址,建木CI流程编排需要这个地址作为参数


    还需要私服的token,需要先创建一个账户,用于本地生成token


    开启nexus的用户token权限验证


    需要本地设置hosted类型仓库地址,npm config set registry http://xxx:8081/xxx/npm_hosted 然后npm login获取token


    添加token到建木的密钥,先创建命名空间npm,在该空间下创建账户的密钥wllgogogo_token


    2. 挑选节点

    建木CI是一个节点编排工具,那么我们需要挑选合适的节点完成一系列的业务操作

    git clone节点

    使用git clone节点,将我们需要部署的前端npm包项目从远程仓库上拉取下来。git clone节点的版本,我们选择1.2.3版本

    如下图:访问建木Hub可以查看节点详细信息,比如,git clone节点的参数,源码,版本说明等信息


    nodejs构建节点

    使用nodejs构建节点,会将我们clone下来的项目进行build构建,本文我们将用到1.4.0-16.13.0版本

    如下图查看此节点的详细信息:


    发布npm依赖包节点

    使用发布npm依赖包节点,会将我们build后的项目发布到公服或私服,从1.1.0-16.15.0版本开始支持私服的发布

    如下图查看此节点的详细信息:


    3. 编排流程

    节点选好了,得把它们编排在一起,目前建木CI提供了两种方式来编排节点:

    1. 使用建木CI的DSL来编排节点

    2. 使用建木CI图形化编排功能来编排节点

    此次我们使用图形化编排功能编辑此测试流程(ps:图形化编排是建木CI 2.4.0推出的重磅级功能,详见「v2.4」千呼万唤的图形化编排,来了!

    首先编辑项目信息


    从左边抽屉中将所需的三个节点拖拽出来


    填充节点参数

    填充参数之前,将三个节点连起来,如图:这个箭头可以完成的功能有:

    • 定义流程运行先后顺序

    • 将上游节点的输出参数输出到下游节点,这里的git clone节点输出参数将被输出到后续所有节点


    点击节点图标开始填充参数

    • git clone节点

      这里我们配置一个需要部署的 npm包 项目的 git 地址,选择1.2.3版本,改名git_clone


    • nodejs构建 节点

      同样配置此节点的必需参数

      1.节点版本:nodejs构建节点的版本选择 1.4.0-16.13.0
      2.工作目录:需要build的项目路径
      3.registry url:给包管理工具设置镜像,一般设置淘宝镜像registry.npmmirror.com/
      4.包管理器类型:根据具体项目情况来选择包管理器,这个项目构建用的是pnpm
      5.项目package.json文件目录相对路径:package.json目录相对地址,读取name和version


      nodejs构建节点的工作目录参数引用了git_clone节点的输出参数(git_clone作为上游节点将它的输出参数作为nodejs构建的输入参数传递给nodejs构建节点),下图演示了下游节点如何选择上游节点的输出参数作为自己的输入参数


    • 发布npm依赖包 节点

      1.节点版本:选择 1.1.0-16.15.0
      2.工作目录:发布包目录
      3.镜像仓库:前面准备工作nexus创建的npm本地仓库地址
      4.token令牌:前面准备工作nexus创建的用户,在本地设置hosted地址后,执行npm login生成的token


    发布 npm包 构件到 nexus

    启动流程

    如下图启动流程


    流程运行中


    流程运行成功


    查看每个节点的运行日志

    git clone节点:


    nodejs构建节点


    发布npm依赖包节点


    在nexus中查看部署的npm依赖包


    至此,我们已经使用建木CI成功将npm依赖包部署到了nexus上!


    作者:Jianmu
    来源:juejin.cn/post/7109026865259479076

    收起阅读 »

    2022 年的 React 生态

    今天的文章,我们将从状态管理、样式和动画、路由、代码风格等多个方面来看看 React 最新的生态,希望你以后在做技术选型的时候能够有所帮助。Next.js 可以支持你生成静态站点,而 Gatsby.js 也支持了服务端渲染。不过就我个人的使用体验而言,我会觉得...
    继续阅读 »

    今天我们来聊 ReactReact 已经风靡前端届很长一段时间了,在这段时间里它发展了一个非常全面而强大的生态系统。大厂喜欢在大型的前端项目中选择 React,它的生态功不可没。

    今天的文章,我们将从状态管理、样式和动画、路由、代码风格等多个方面来看看 React 最新的生态,希望你以后在做技术选型的时候能够有所帮助。

    创建 React 项目


    对于大多数 React 初学者来说,在刚刚开始学习 React 时如何配置一个 React 项目往往都会感到迷惑,可以选择的框架有很多。React 社区中大多数会给推荐 Facebookcreate-react-app (CRA)。它基本上零配置,为你提供开箱即用的简约启动和运行 React 应用程序。


    但现在来看,CRA 使用的工具过时了 — 从而导致我们的开发体验变慢。Vite 是近期最受欢迎的打包库之一,它具有令人难以置信的开发和生产速度,而且也提供了一些模板(例如 React、React + TypeScript)可以选择。


    如果你已很经熟悉 React 了,你可以选择它最流行的框架之一作为替代:Next.jsGatsby.js。这两个框架都基于 React 建立,因此你应该至少熟悉了 React 的基础知识再去使用。这个领域另一个流行的新兴框架是 Remix,它在 2022 年绝对值得一试。


    虽然 Next.js 最初是用来做服务端渲染的,而 Gatsby.js 主要用来做静态站点生成(例如博客和登录页面等静态网站)。然而,在过去几年里,这两个框架之间一直在互相卷...

    Next.js 可以支持你生成静态站点,而 Gatsby.js 也支持了服务端渲染。不过就我个人的使用体验而言,我会觉得 Next.js 更好用一点。

    如果你只想了解一下 create-react-app 这些工具在后台的工作原理,建议尝试一下自己从头开始配置一个 React 项目。从一个简单的 HTML JavaScript 项目开始,并自己添加 React 及其支持工具(例如 Webpack、Babel)。这并不是你在日常工作中必须要做的事情,但这是了解底层工具实现原理的一个很好的方式。

    建议:

    • 优先使用 Vite创建 React客户端应用

      • CRA 备选

    • 优先使用 Next.js 创建 React服务端渲染应用

      • 最新技术:Remix

      • 仅创建静态站点备选 Gatsby.js

    • 可选的学习经验:从0自己搭建一个 React 应用。

    链接:

    阅读:


    状态管理


    React 带有两个内置的 Hooks 来管理本地状态:useStateuseReducer。如果需要全局状态管理,可以选择加入 React 内置的 useContext Hook 来将 props 从顶层组件传递到底层组件,从而避免 props 多层透传的问题。这三个 Hooks 足以让你实现一个强大的状态管理系统了。

    如果你发现自己过于频繁地使用 ReactContext 来处理共享/全局状态,你一定要看看 Redux,它是现在最流行的状态管理库。它允许你管理应用程序的全局状态,任何连接到其全局存储的 React 组件都可以读取和修改这些状态。


    如果你碰巧在用 Redux,你一定也应该查看 Redux Toolkit。它是基于 Redux 的一个很棒的 API,极大地改善了开发者使用 Redux 的体验。

    作为替代方案,如果你喜欢用全局存储的思想管理状态,但不喜欢 Redux 的处理方式,可以看看其他流行的本地状态管理解决方案,例如 Zusand、Jotai、XStateRecoil

    另外,如果你想拥有和 Vue.js 一样的开发体验,建议看看 Mobx

    建议:

    • useState/useReducer 处理共享状态

    • 选择性使用 useContext 管理某些全局状态

    • Redux(或另一种选择) 管理全局状态

    链接:

    阅读:


    远程数据请求


    React 的内置 Hooks 非常适合 UI 状态管理,但当涉及到远程数据的状态管理(也包括数据获取)时,我建议使用一个专门的数据获取库,例如 React Query,它自带内置的状态管理功能。虽然 React Query 本身的定位并不是一个状态管理库,它主要用于从 API 获取远程数据,但它会为你处理这些远程数据的所有状态管理(例如缓存,批量更新)。


    React Query 最初是为使用 REST API 而设计的,但是现在它也支持了 GraphQL。然而如果你正在为你的 React 项目寻找专门的 GraphQL 库,我还是推荐你去看看 Apollo Client(当前最流行的)、urql(轻量级)或 RelayFacebook 维护)。

    如果你已经在使用 Redux,并且想要在 Redux 中添加集成状态管理的数据请求功能,建议你看看 RTK Query,它将数据请求的功能更巧妙的集成到 Redux 中。

    建议:

    • React Query(REST API、GraphQL API 都有)

    • Apollo Client(只有 GraphQL API)

    • 可选的学习经验:了解 React Query 的工作原理

    链接:

    阅读:


    路由


    如果你使用的是像 Next.jsGatsby.js 这样的 React 框架,那么路由已经为你处理好了。但是,如果你在没有框架的情况下使用 React 并且仅用于客户端渲染(例如 CRA),那么现在最强大和流行的路由库是 React Router

    链接:

    阅读:


    样式/CSS

    React 中有很多关于 样式/CSS 的选项和意见,作为一个 React 初学者,可以使用一个带有所有 CSS 属性的样式对象作为 HTML 样式属性的键/值对,从内联样式和基本的 CSS 开始就可以。

    const ConardLi = ({ title }) =>
     <h1 style={{ color: 'blue' }}>
      {title}
     h1>

    内联样式可以在 React 中通过 JavaScript 动态添加样式,而外部 CSS 文件可以包含 React 应用的所有剩余样式:

    import './Headline.css';

    const ConardLi = ({ title }) =>
     <h1 className="ConardLi" style={{ color: 'blue' }}>
      {title}
     h1>

    如果你的应用越来越大了,建议再看看其他选项。首先,我建议你将 CSS Module 作为众多 CSS-in-CSS 解决方案的首选。CRA 支持 CSS Module ,并为提供了一种将 CSS 封装到组件范围内的模块的方法。这样,它就不会意外泄露到其他 React 组件的样式中。你的应用的某些部分仍然可以共享样式,但其他部分不必访问它。在 React 中, CSS Module 通常是将 CSS 文件放在 React 组件文件中:

    import styles from './style.module.css';

    const ConardLi = ({ title }) =>
     <h1 className={styles.headline}>
      {title}
     h1>


    其次,我想向你推荐所谓的 styled components ,作为 React 的众多 CSS-in-JS 解决方案之一。它通过一个名为 styles-components(或者其他例如 emotion 、stitches)的库来实现的,它一般将样式放在 React 组件的旁边:

    import styled from 'styled-components';

    const BlueHeadline = styled.h1`
     color: blue;
    `;

    const ConardLi = ({ title }) =>
     <BlueHeadline>
      {title}
     BlueHeadline>


    第三,我想推荐 Tailwind CSS 作为最流行的 Utility-First-CSS 解决方案。它提供了预定义的 CSS 类,你可以在 React 组件中使用它们,而不用自己定义。这可以提升一些效率,并与你的 React 程序的设计系统保持一致,但同时也需要了解所有的类:

    const ConardLi = ({ title }) =>
     <h1 className="text-blue-700">
      {title}
     h1>

    使用 CSS-in-CSS、CSS-in-js 还是函数式 CSS 由你自己决定。所有的方案在大型 React 应用中都适用。最后一点提示:如果你想在 React 中有条件地应用一个 className,可以使用像 clsx 这样的工具。

    建议:

    • CSS-in-CSS 方案: CSS Modules

    • CSS-in-JS

      方案: Styled Components(目前最受欢迎)

      • 备选: EmotionStitches

    • 函数式 CSS:Tailwind CSS

    • 备选:CSS 类的条件渲染:clsx

    链接:

    阅读:


    组件库

    对于初学者来说,从零开始构建可复用的组件是一个很好的学习经验,值得推荐。无论它是 dropdown、radio button 还是 checkbox ,你最终都应该知道如何创建这些UI组件组件。


    然而,在某些时候,你想要使用一个UI组件库,它可以让你访问许多共享一套设计系统的预构建组件。以下所有的UI组件库都带有基本组件,如 Buttons、Dropdowns、DialogsLists

    尽管所有这些UI组件库都带有许多内部组件,但它们不能让每个组件都像只专注于一个UI组件的库那样强大。例如 react-table-library 提供了非常强大的表格组件,同时提供了主题(例如 Material UI),可以很好的和流行的UI组件库兼容。

    阅读:


    动画库


    Web 应用中的大多数动画都是从 CSS 开始的。最终你会发现 CSS 动画不能满足你所有的需求。通常开发者会选择 React Transition Group,这样他们就可以使用 React组件来执行动画了,React 的其他知名动画库有:


    可视化图表


    如果你真的想要自己从头开始开发一些图表,那么就没有办法绕过 D3 。这是一个很底层的可视化库,可以为你提供开发一些炫酷的图表所需的一切。然而,学习 D3 是很有难度的,因此许多开发者只是选择一个 React 图表库,这些库默认封装了很多能力,但是缺失了一些灵活性。以下是一些流行的解决方案:


    表单


    React 现在最受欢迎的表单库是 React Hook Form 。它提供了从验证(一般会集成 yupzod)到提交到表单状态管理所需的一切。之前流行的另一种方式是 Formik。两者都是不错的解决方案。这个领域的另一个选择是 React Final Form 。毕竟,如果你已经在使用 React UI组件库了,你还可以查看他们的内置表单解决方案。

    建议:

    • React Hook Form

      • 集成 yupzod 进行表单验证

    • 如果已经在使用组件库了,看看内置的表单能不能满足需求

    链接:

    阅读:


    类型检查

    React 带有一个名为 PropTypes 的内部类型检查。通过使用 PropTypes,你可以为你的 React 组件定义 props。每当将类型错误的 prop 传递给组件时,你可以在运行时收到错误消息:

    import PropTypes from 'prop-types';

    const List = ({ list }) =>
     <div>
      {list.map(item => <div key={item.id}>{item.title}div>)}
     div>

    List.propTypes = {
     list: PropTypes.array.isRequired,
    };


    在过去的几年里,PropTypes 已经不那么流行了,PropTypes 也已经不再包含在 React 核心库中了,现在 TypeScript 才是最佳的选择:

    type Item = {
     id: string;
     title: string;
    };

    type ListProps = {
     list: Item[];
    };

    const List: React.FC<ListProps> = ({ list }) =>
     <div>
      {list.map(item => <div key={item.id}>{item.title}div>)}
     div>

    阅读:


    代码风格


    对于代码风格,基本上有两种方案可以选择:

    如果你想要一种统一的、通用的代码风格,在你的 React 项目中使用 ESLint 。像 ESLint 这样的 linter 会在你的 React 项目中强制执行特定的代码风格。例如,你可以在 ESLint 中要求遵循一个流行的风格指南(如 Airbnb 风格指南)。之后,将 ESLint 与你的IDE/编辑器集成,它会指出你的每一个错误。

    如果你想采用统一的代码格式,可以在 React 项目中使用 Prettier。它是一个比较固执的代码格式化器,可选择的配置很少。你也可以将它集成到编辑器或IDE中,以便在每次保存文件的时候自动对代码进行格式化。虽然 Prettier 不能取代 ESLint,但它可以很好地与 ESLint 集成。

    建议:

    阅读:


    身份认证


    React 应用程序中,你可能希望引入带有注册、登录和退出等功能的身份验证。通常还需要一些其他功能,例如密码重置和密码更改功能。这些能力远远超出了 React 的范畴,我们通常会把它们交给服务端去管理。

    最好的学习经验是自己实现一个带有身份验证的服务端应用(例如 GraphQL 后端)。然而,由于身份验证有很多安全风险,而且并不是所有人都了解其中的细节,我建议使用现有的众多身份验证解决方案中的一种:

    阅读:


    测试

    现在最常见的 React 测试方案还是 Jest,它基本上提供了一个全面的测试框架所需要的一切。

    你可以使用 react-test-renderer 在你的 Jest 测试中渲染 React 组件。这已经足以使用 Jest 执行所谓的 Snapshot Tests 了:一旦运行测试,就会创建 React 组件中渲染的 DOM 元素的快照。当你在某个时间点再次运行测试时,将创建另一个快照,这个快照会和前一个快照进行 diff。如果存在差异,Jest 将发出警告,你要么接受这个快照,要么更改一下组件的实现。

    最近 React Testing Library (RTL) 也比较流行(在 Jest 测试环境中使用),它可以为 React 提供更精细的测试。RTL 支持让渲染组件模拟 HTML 元素上的事件成,配合 Jest 进行 DOM 节点的断言。

    如果你正在寻找用于 React 端到端 (E2E) 测试的测试工具,Cypress 是现在最受欢迎的选择。

    阅读:


    数据结构


    Vanilla JavaScript 为你提供了大量内置工具来处理数据结构,就好像它们是不可变的一样。但是,如果你觉得需要强制执行不可变数据结构,那么最受欢迎的选择之一是 Immer 。我个人没用过它,因为 JavaScript 本身就可以用于管理不可变的数据结构,但是如果有人专门问到 JS 的不可变性,有人会推荐它。

    链接:

    阅读:


    国际化


    当涉及到 React 应用程序的国际化 i18n 时,你不仅需要考虑翻译,还需要考虑复数、日期和货币的格式以及其他一些事情。这些是处理国际化的最流行的库:


    富文本编辑


    React 中的富文本编辑器,就简单推荐下面几个,我也没太多用过:


    时间处理


    近年来,JavaScript 本身在处理日期和时间方面做了很多优化和努力,所以一般没必要使用专门的库来处理它们。但是,如果你的 React 应用程序需要大量处理日期、时间和时区,你可以引入一个库来为你管理这些事情:


    客户端


    Electron 是现在跨平台桌面应用程序的首选框架。但是,也存在一些替代方案:

    阅读:


    移动端


    ReactWeb 带到移动设备的首选解决方案仍然是 React Native

    阅读:


    VR/AR


    通过 React,我们也可以深入研究虚拟现实或增强现实。老实说,这些库我还都没用过,但它们是我在 React 中所熟悉的 AR/VR 库:


    原型设计


    如果你是一名 UI/UX 设计师,你可能希望使用一种工具来为新的 React 组件、布局或 UI/UX 概念进行快速原型设计。我之前用的是 Sketch ,现在改用了 Figma 。尽管我两者都喜欢,但我还是更喜欢 FigmaZeplin 是另一种选择。对于一些简单的草图,我喜欢使用 Excalidraw。如果你正在寻找交互式 UI/UX 设计,可以看看 InVision


    文档


    我在很多项目里都在使用 Storybook 作为文档工具,不过也有一些其他好的方案:

    最后

    参考:http://www.robinwieruch.de/react-libra…

    本文完,欢迎大家补充。


    作者:ConardLi
    来源:juejin.cn/post/7085542534943883301

    收起阅读 »

    关于 async/await 你应该认真对待下

    web
    深入理解 async/await一个语法糖 是异步操作更简单返回值 返回值是一个 promise 对象return 的值是 promise resolved 时候的 valueThrow 的值是 Promise rejected 时候的 reasonasync...
    继续阅读 »

    无论是在项目还是在面试过程中,总还是会有那么一小部分同学,没有学会使用 async/await ,今天就特地整理了几个代码段,并以此文进行提醒大家常用的技术点还是要会的,不单单只是应对面试需要,在日常工作中使用,也会提升你的效率及代码质量的,不必每次都使用 .then 进行处理,错误输出可以写个公共方法,统一处理。⛽️ 加油,共勉!!! 无论是在项目还是在面试过程中,总还是会有那么一小部分同学,没有学会使用 async/await ,今天就特地整理下并提醒大家常用的技术点还是要会的,不单是为了应对面试需要,日常工作中也是有利无害的

    深入理解 async/await

    async 函数

    • 一个语法糖 是异步操作更简单

    • 返回值 返回值是一个 promise 对象

      • return 的值是 promise resolved 时候的 value

      • Throw 的值是 Promise rejected 时候的 reason

    async function test() {
     return true
    }
    const p = test()
    console.log(p) // 打印出一个promise,状态是resolved,value是true

    // Promise {: true}
    //   [[Prototype]]: Promise
    //   [[PromiseState]]: "fulfilled"
    //   [[PromiseResult]]: true

    p.then((data) => {
     console.log(data) // true
    })
    async function test() {
     throw new Error('error')
    }
    const p = test()
    console.log(p) // 打印出一个promise,状态是rejected,value是error
    p.then((data) => {
     console.log(data) //打印出的promise的reason 是error
    })

    可以看出 async 函数的返回值是一个 promise

    await 函数

    • 只能出现在 async 函数内部或最外层

    • 等待一个 promise 对象的值

    • await 的 promise 的状态为 rejected,后续执行中断

    await 可以 await promise 和非 promsie,如果非 primse,例如:await 1 就返回 1


    await 为等待 promise 的状态是 resolved 的情况

    async function async1() {
     console.log('async1 start')
     await async2() // await为等待promise的状态,然后把值拿到
     console.log('async1 end')
    }
    async function async2() {
     return Promsie.resolve().then(_ => {
       console.log('async2 promise')
    })
    }
    async1()
    /*
    打印结果
    async1 start
    async2 promise
    async1 end
    */

    await 为等待 promise 的状态是 rejected 的情况

    async function f() {
     await Promise.reject('error')
     //后续代码不会执行
     console.log(1)
     await 100
    }

    // 解决方案1
    async function f() {
     await Promise.reject('error').catch(err => {
       // 异常处理
    })
     console.log(1)
     await 100
    }

    // 解决方案2
    async function f() {
     try {
       await Promise.reject('error')
    } catch (e) {
       // 异常处理
    } finally {
    }
     console.log(1)
     await 100
    }

    async 函数实现原理

    实现原理:Generator+自动执行器

    async 函数是 Generator 和 Promise 的语法糖

    应用

    用 async 函数方案读取文件

    const fs = require('fs')

    async function readFilesByAsync() {
     const files = [
       '/Users/xxx/Desktop/Web/1.json',
       '/Users/xxx/Desktop/Web/2.json',
       '/Users/xxx/Desktop/Web/3.json'
    ]
     const readFile = function(src) {
       return new Promise((resolve, reject) => {
         fs.readFile(src, (err, data) => {
           if (err) reject(err)
           resolve(data)
        })
      })
    }

     const str0 = await readFile(files[0])
     console.log(str0.toString())
     const str1 = await readFile(files[1])
     console.log(str1.toString())
     const str2 = await readFile(files[2])
     console.log(str2.toString())
    }

    作者:Gaby
    来源:juejin.cn/post/7108362437706907685

    收起阅读 »

    Mac修改hosts,域名与ip绑定,vue Invalid Host header

    在移动开发过程中,有时候需要使用域名进行访问(如微信网页开发)本地ip地址服务,或者使用域名访问本地ip地址服务等。这时候可以修改host进行实现。1. 修改host文件在命令终端,使用root用户修改host文件。域名使用root用户打开/etc/hosts...
    继续阅读 »

    在移动开发过程中,有时候需要使用域名进行访问(如微信网页开发)本地ip地址服务,或者使用域名访问本地ip地址服务等。

    这时候可以修改host进行实现。

    1. 修改host文件

    在命令终端,使用root用户修改host文件。域名使用root用户打开/etc/hosts host文件进行修改。添加
    ip及对应的域名

    $ sudo vi /etc/hosts
    127.0.0.1       localhost
    127.0.0.1 zhangguoyedeMacBook-Pro.local
    255.255.255.255 broadcasthost
    ::1 localhost
    ::1 zhangguoyedeMacBook-Pro.local

    # 在这里添加上ip及对应的域名并保存退出
    #(这里假设你设置的是本机ip是 127.0.0.1 访问域名是 guoye.com)
    127.0.0.1 guoye.com

    2. 通过域名访问项目

    现在可以在浏览器上访问你设置的域名guoye.com,跟直接通过ip访问127.0.0.1的内容是一致的。
    通常你的项目会加上端口号,域名也需要加上端口号,如http://guoye.com:4201

    3. vue (Invalid Host header)

    在vue项目开发时,直接通过ip地址访问正常,但通过上面host域名方式访问,浏览器会显示一段文字:Invalid Host header
    这是由于新版webpack-dev-server出于安全考虑,默认检查hostname,如果hostname 没有配置在内的,将中断访问。

    解决方法:
    vue.config.jsdevServer配置文件加上 disableHostCheck: true

    devServer: {
    port: 4201, // 端口配置
    proxy: {
    // 代理配置
    },
    disableHostCheck: true, // 这是由于新版的webpack-dev-server出于安全考虑,默认检查hostname,如果hostname 不是配置内的,将中断访问。
    }

    4. 手机端也通过域名进行访问

    移动开发时,可以使用Charles软件进行代理。
    此时手机端也能通过域名访问本机电脑的应用。

    原文:https://segmentfault.com/a/1190000023077264

    收起阅读 »

    web网页基础知识

    浮动元素重叠1、行内元素与浮动元素发生重叠,边框、背景、内容都会显示在浮动元素之上2、块级元素与浮动元素发生重叠,边框、背景会显示在浮动元素之下,内容会显示在浮动元素之上3、若不浮动的是块级元素,那么浮动的元素将显示在其上方4、若不浮动的是行内元素或者行内块元...
    继续阅读 »

    浮动元素重叠
    1、行内元素与浮动元素发生重叠,边框、背景、内容都会显示在浮动元素之上
    2、块级元素与浮动元素发生重叠,边框、背景会显示在浮动元素之下,内容会显示在浮动元素之上
    3、若不浮动的是块级元素,那么浮动的元素将显示在其上方
    4、若不浮动的是行内元素或者行内块元素,那么浮动的元素不会覆盖它,而是将其挤往左方、、

    表单里面enctype 属性的默认值是“application/x-www-form-urlencoded”,



    Boolean.FALSE与new Boolean(false)的区别

    因为Boolean的 构造函数Boolean(String s) 参数只有为" true "(忽略大小写,比如TRUE,tRue都行)时候才是创建为真的Boolean值。其他情况都为假

     JavaScript的其他数据类型都可以转换成Boolean类型,注意!!!只有这几种类型会转换为false

    undefined
    null
    0
    -0
    NaN
    "" (空字符串)

      其他的都会转换为true。空对象{},空数组[] , 负数 ,false的对象包装等

      重点,new Boolean(false)是布尔值的包装对象 typeof (new Boolean(false)) // 'object' ,所以 转换为boolean是true,而不是false

    内联元素是不可以控制宽和高、margin等;并且在同一行显示,不换行。
    块级元素时可以控制宽和高、margin等,并且会换行。
    行内元素不可以设置宽高,但是可以设置 左右padding、左右margin
    1. inline : 使用此属性后,元素会被显示为内联元素,元素则不会换行
    inline是行内元素,同行可以显示,像span、font、em、b这些默认都是行内元素,不会换行,无法设置宽度、高度、margin、border
    2. block : 使用此属性后,元素会被现实为块级元素,元素会进行换行。
    block,块元素,div、p、ul、li等这些默认都是块元素,会换行,除非设置float
    3. inline-block : 是使元素以块级元素的形式呈现在行内。意思就是说,让这个元素显示在同一行不换行,但是又可以控制高度和宽度,这相当于内敛元素的增强。(IE6不支持)
    inline-block,可以同行显示的block,想input、img这些默认就是inline-block,出了可以同行显示,其他基本block一样
    一.h1~h6标签:有默认margin(top,bottom且相同)值,没有默认padding值。

    在chrome中:16,15,14,16,17,19;

    在firefox中:16,15,14,16,17,20;

    在safari中:16,15,14,16,17,19;

    在opera中:16,15,14,14,17,21;

    在maxthon中:16,14,14,15,16,18;

    在IE6.0中:都是19;

    在IE7.0中:都是19;

    在IE8.0中:16,15,14,16,17,19;
    二.dl标签:有默认margin(top,bottom且相同)值,没有默认padding值。

    在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

    在IE6.0,7.0中:margin:19px 0px;

    dd标签有默认margin-left:40px;(在所有上述浏览器中)。
    三.ol,ul标签:有默认margin-(top,bottom且相同)值,有默认padding-left值

    在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

    在IE6.0,7.0中:margin:19px 0px;

    默认padding-left值:在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中都是padding-left:40px;在IE6.0,7.0中没有默认padding值,因为ol,ul标签的边框不包含序号。
    四.table标签没有默认的margin,padding值;th,td标签没有默认的margin值,有默认的padding值。

    在Chrome,Firefox,Safari,Opera,Maxthon中:padding:1px;

    在IE8.0中:padding:0px 1px 1px;

    在IE7.0中:padding:0px 1px;

    相同内容th的宽度要比td宽,因为th字体有加粗效果。
    五.form标签在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中没有默认的margin,padding值,但在IE6.0,7.0中有默认的margin:19px 0px;
    六.p标签有默认margin(top,bottom)值,没有默认padding值。

    在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

    在IE6.0,7.0中:margin:19px 0px;
    七.textarea标签在上述所有浏览器中:margin:2px;padding:2px;
    八.select标签在Chrome,Safari,Maxthon中有默认的margin:2px;在Opera,Firefox,IE6.0,7.0,8.0没有默认的margin值。

    option标签只有在firefox中有默认的padding-left:3px;

    ###属性继承

    1. 不可继承的:display、margin、border、padding、background、height、min-height、max-height、width、min-width、max-width、overflow、position、left、right、top、bottom、z-index、float、clear、table-layout、vertical-align、page-break-after、page-bread-before和unicode-bidi。
    2. 所有元素可继承:visibility和cursor。
    3. 内联元素可继承:letter-spacing、word-spacing、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction。
    4. 终端块状元素可继承:text-indent和text-align。
    5. 列表元素可继承:list-style、list-style-type、list-style-position、list-style-image。

    收起阅读 »

    uniapp里面可以使用的单利定时器

    主要代码 var HashMap = require('../tools/HashMap') /** * 使用说明: * 1、引入 var timeTool=require("../utils/timeTool.js") ...
    继续阅读 »


    主要代码
    var HashMap = require('../tools/HashMap')
    /**
    * 使用说明:
    * 1、引入 var timeTool=require("../utils/timeTool.js")
    * 2、onload 里面实例化并调用start方法:
    * mtimeTool = new timeTool( this); mtimeTool.start();
    添加监听函数:keykey随意写不要重复就好
    mPKGame.addCallBack("keykey", () => {})
    */
    //

    class PKGame {
    constructor(handler) {
    this.mNetTool = netTool;
    this.mHandler = handler;
    this.mHandler;
    this.commonTimer;
    this.callBackListener = new HashMap();
    this.instance;
    // uni.setStorageSync("token",handler.appInfo.token)
    }

    destroy() {
    this.clearInterval(commonTimer)
    this.commonTimer = null;
    }
    start() {
    if (!this.commonTimer) {
    this.commonTimer = setInterval(() => {
    var values = this.callBackListener.values();
    for (var i in values) {
    typeof values[i] == "function" && values[i]();
    }
    }, 1000);
    }
    // this.createRoom();
    }

    addCallBack(listenerKey, listener) {
    this.callBackListener.put(listenerKey, listener)
    }
    removeCallBack(listenerKey) {
    this.callBackListener.remove(listenerKey)
    }

    static getInstance = function (handler) { //静态方法
    return this.instance || (this.instance = new PKGame(handler))
    }
    }

    module.exports = ( handler) => {
    return PKGame.getInstance( handler)
    };

    hashmap工具类

    /**
    * ********* 操作实例 **************
    * var map = new HashMap();
    * map.put("key1","Value1");
    * map.put("key2","Value2");
    * map.put("key3","Value3");
    * map.put("key4","Value4");
    * map.put("key5","Value5");
    * alert("size:"+map.size()+" key1:"+map.get("key1"));
    * map.remove("key1");
    * map.put("key3","newValue");
    * var values = map.values();
    * for(var i in values){
    * document.write(i+":"+values[i]+" ");
    * }
    * document.write("<br>");
    * var keySet = map.keySet();
    * for(var i in keySet){
    * document.write(i+":"+keySet[i]+" ");
    * }
    * alert(map.isEmpty());
    */

    function HashMap(){
    //定义长度
    var length = 0;
    //创建一个对象
    var obj = new Object();

    /**
    * 判断Map是否为空
    */
    this.isEmpty = function(){
    return length == 0;
    };

    /**
    * 判断对象中是否包含给定Key
    */
    this.containsKey=function(key){
    return (key in obj);
    };

    /**
    * 判断对象中是否包含给定的Value
    */
    this.containsValue=function(value){
    for(var key in obj){
    if(obj[key] == value){
    return true;
    }
    }
    return false;
    };

    /**
    *向map中添加数据
    */
    this.put=function(key,value){
    if(!this.containsKey(key)){
    length++;
    }
    obj[key] = value;
    };

    /**
    * 根据给定的Key获得Value
    */
    this.get=function(key){
    return this.containsKey(key)?obj[key]:null;
    };

    /**
    * 根据给定的Key删除一个值
    */
    this.remove=function(key){
    if(this.containsKey(key)&&(delete obj[key])){
    length--;
    }
    };

    /**
    * 获得Map中的所有Value
    */
    this.values=function(){
    var _values= new Array();
    for(var key in obj){
    _values.push(obj[key]);
    }
    return _values;
    };

    /**
    * 获得Map中的所有Key
    */
    this.keySet=function(){
    var _keys = new Array();
    for(var key in obj){
    _keys.push(key);
    }
    return _keys;
    };

    /**
    * 获得Map的长度
    */
    this.size = function(){
    return length;
    };

    /**
    * 清空Map
    */
    this.clear = function(){
    length = 0;
    obj = new Object();
    };
    }
    module.exports = HashMap;

    收起阅读 »

    uniapp开发px和rpx

    开发中难免出现单位问题,就像获取系统信息,里面的屏幕宽度什么的都是px作为单位的,因此这里说明一下uniapp的转换使用rpx转pxuni.upx2px(rpx的值)px转rpxpx的值/(uni.upx2px(10)/10)使用的时候可以 let px = ...
    继续阅读 »

    开发中难免出现单位问题,就像获取系统信息,里面的屏幕宽度什么的都是px作为单位的,因此这里说明一下uniapp的转换使用
    rpx转px

    uni.upx2px(rpx的值)

    px转rpx

    px的值/(uni.upx2px(10)/10)

    使用的时候可以 let px = uni.upx2px(rpx的值)什么的 返回值就是计算好了的

    收起阅读 »

    瞄准Web3:互联网巨头捍卫流量“王座”之争

    web
    日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。Web3 是什么?有人对它寄予厚望,认为这是真正可实现的下一代互联网;有人表示悲观,觉得这是一个“去中心化”的乌托邦,“就像是一场梦,醒来还是很感动”。对...
    继续阅读 »
    日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。

    Web3 是什么?

    有人对它寄予厚望,认为这是真正可实现的下一代互联网;有人表示悲观,觉得这是一个“去中心化”的乌托邦,“就像是一场梦,醒来还是很感动”。对大多数人来说,Web3

    的定义是什么并不重要。重要的是,在可见的未来,Web3 能给我们带来什么。

    在普遍认知中,Web3 是一个基于区块链技术的去中心化互联网。其中,“去中心化”是Web3 的精神内核。围绕这一内核,Web3 的理想愿景是,将互联网及其生产内容的控制权从少数几家科技巨头手中返还到个人,从而让用户能对自己的身份和数据有更多控制权。

    换句话说,Web3 就像是曾经的“占领华尔街运动”在当今互联网世界的复刻,针对的恰恰是 Web2 时代的既得利益者,即 Meta、亚马逊、谷歌,乃至BAT 这类巨头。面对这一可能的威胁,巨头们也陆续有了动作,纷纷落子,希冀在 Web3 的棋盘上继续巩固各自的生态帝国,继续成为互联网世界中隐形的规则制定者和秩序维护者。

    日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。

    谷歌的布局

    谷歌对于 Web3 的横空出世有其自身的判断。

    在谷歌云的官方博客中,如此描述:“区块链和数字资产正在改变世界存储和传递信息以及价值的方式。如今的 Web3 热潮就如同10-15年前开源和互联网的兴起。正如开源开发是互联网早期不可或缺的一部分一样,区块链正在为用户和企业带来创新的推动力。”

    在今年1月,谷歌云曾对外披露,他们正在研究怎么使用加密货币支付。当时,谷歌云金融业务副总裁 Yolande Piazza 表示,已经成立了一个谷歌云数字资产团队,来协助客户在基于区块链的平台上创建新产品。彼时已经有人猜测,谷歌云未来会接受数字货币作为支付方式。

    而此次谷歌云组建的Web3团队目标指向则更为清晰。它旨在构建 Web3 世界的基础设施,主要为有兴趣编写Web3软件的开发人员提供后端服务。

    在发给团队的电子邮件中,谷歌云副总裁 Amit Zavery 写道,虽然世界仍处于拥抱 Web3的早期阶段,但 Web3 是一个已经显示出巨大潜力的市场,许多客户要求谷歌增加对 Web3 和 Crypto 相关技术的支持。

    在外媒的公开采访中,Zavery明确表示,对于谷歌来说,参与这一趋势的方式不是直接成为加密货币浪潮的一部分,而是为Web3开发者提供基础设施服务。谷歌不参与也不干涉具体业务,而是计划成为基础设施提供商,降低开发者基于区块链设计去中心化系统的门槛,推动企业在业务中使用和利用Web3的分布式特性。

    尽管今年以来,资本对于比特币这一市场的投资热情大为减弱,但Zavery表示,区块链应用不断进入主流,并在金融服务和零售业等行业中有越来越高的参与度。未来,谷歌或许会设计相关系统,使人们更容易探索链上链下数据,同时简化构建和运行区块链节点进行验证和记录交易的过程。

    而在团队构成上,Zavery透露,新组建的Web3团队主要是将内部参与过 Web3 项目的员工合并到一起,然后从外部招募一些区块链开发工程师和其他相关人才。2019年加入谷歌的前花旗集团高管 James Tromans 将领导产品和工程小组,并向Zavery汇报。

    谷歌的动机

    谷歌入场Web3 ,除了未雨绸缪布局 Web3 基础设施之外,是否有其他考量?

    有人注意到,这支 Web3 团队的主导部门是谷歌云,因此猜测,云服务市场的博弈或许也是个中关键。

    谷歌母公司 Alphabet 2022 年一季度财报显示,谷歌云营收同比增长 44% 至 58.2 亿美元。Alphabet 首席财务官 Ruth Porat 表示,谷歌云服务的增长速度已经超过了其核心的广告部门,且员工人数增长最快的就是云部门。

    尽管谷歌云表现不俗,但目前来说,仍旧无法与微软 Azure 抗衡,更不用说在云计算市场一骑绝尘的 AWS 。更值得一提的是,早在2018年,就有大量以太坊、比特币还有其他区块链的节点部署在 AWS 上。而到了2021 年,AWS Marketplace 总监 Marta Whiteaker 曾透露:“目前以太坊全球 25% 的工作负载都运行在 AWS 上。”

    可以说,亚马逊无论是在 Web2 还是 Web3 时代,在云服务领域都占得了先机。而在Web3赛道策略相对保守的谷歌之所以选择在这个时间入场,竞争对手带来的压力可能也是一大诱因。

    在一定程度上可预见的赛道内,Web3 的发展极有可能会冲击到谷歌的云服务市场份额,甚至波及广告业务,进而在更大范围内降低谷歌对全球数字生态的影响力,为了捍卫其庞大的生态版图,适时入场至少不至于在真正交锋时完全陷入被动。

    局中人

    面对 Web3,科技巨头、开发者、用户表现出了泾渭分明的态度。

    除了谷歌之外,其他互联网巨擘也在选择拥抱Web3。

    亚马逊在2018 年便推出了自己的区块链支持服务 Amazon Managed Blockchain ,主要面向在 Hyperledger Fabric 或以太坊中搭建项目的开发人员提供托管和硬件服务,提高客户为 DeFi、供应链、金融服务等业务用例创建和利用可扩展区块链技术的能力。而不久前,亚马逊CEO Andy Jassy也公开表态,亚马逊在数字资产行业和 NFT领域看到了巨大的潜力。

    微软从2015 起就开始为区块链开发商提供支持。今年3月,微软投资区块链初创公司ConsenSys也被视为微软在加密相关领域的一次罕见押注。因为ConsenSys被投资者认为是为 Web3 提供动力的公司之一。有分析人士认为,这一举动展示了微软对Web3日益增长的兴趣。

    在科技巨头们纷纷下注之际,开发者们对 Web3 的反映要冷淡得多。

    对于谷歌入场Web3 ,美国著名软件工程师 Grady Booch在推特上表达了他的失望,并直言这种投入是对资源的浪费。

    调查机构Stack Overflow在今年4月出具的报告也揭示了类似的态度。在接受调查的595名开发人员中,37%的人不知道Web3 是什么;在知道的人群中,25%的人认为 Web3

    是互联网的未来;15%的人认为这是一堆炒作;14%的人认为它对加密货币领域相关应用程序很重要;9%的人认为这是一个骗局。

    对于 Web3 的潜在用户群体或者吃瓜群众来说,Web3更像是一个仍旧遥远的概念。虽然“去中心化”的愿景很美,但他们使用的产品和服务是否完全去中心化在现实角度看或许并不是关注焦点。

    加密通讯应用 Signal 创始人 Moxie Marlinspike 指出,即使是很多极客,也不想运行自己的服务器。即使一家大的软件企业,运行自己的服务器也是很大的负担。基于此,云厂商才会取得成功。Web3 同样如此。“如果谷歌开发出更容易使用的服务,填补市场空白,那么即使该服务没有达到去中心化的程度,人们也会去那里。”

    比如以太坊最大的节点服务提供商 Infura,其运行的节点分散在各地甚至是用户家中,但不断发生的 Infura 宕机事件向人们证明了,在“去中心化”的服务名目下,要真正实现大规模推广,还是要依赖中心化的基础设施。

    用户期望的并不是一个单纯取代 Web2 的Web3 时代,而是一个边界不断拓宽、新场景不断涌现的数字世界,偶有惊喜又值得期待。

    结语

    在Web3 领域,关于“中心化”与“去中心化”之争一直存在。

    矛盾的是,“去中心化”虽然是 Web3 信徒们奉为圭臬的理想,但真正主导 Web3 发展进度的其实是一群 Web2 时代“中心化”规则下的受益者。

    根据网络监控公司Sandvine发布的2021年全球互联网现象报告显示,谷歌、Meta、Netflix、亚马逊、微软和苹果这六家企业产生了超过56%的全球网络流量,他们在2021年产生的流量占比超过了所有其他互联网公司的总和。

    在如此可观的流量背后,这些科技巨头在事实上控制了用户的账号、交互、产出内容甚至是隐私,这也构成了其生态帝国的权力基石。由此来看,谷歌布局 Web3 这件事,依旧是 Web2 时代巨头博弈的续篇。

    但 Web3 的可贵之处在于它仍是一片待开发的荒原,没有人能预判其爆发的时机。它提供了基于区块链的新价值模型,为市场带来了创新和颠覆的可能。不确定性让 Web3 危险,也让它浪漫,因为这块土地无限自由,不拒绝任何人的踏入。巨头的主动不见得是优势,小透明的崛起也不一定荒诞。前景不明,也意味着前景有无数可能。

    参考资料:

      https://www.cnbc.com/2022/05/06/googles-cloud-group-forms-web3-product-and-engineering-team.html

      https://cointelegraph.com/news/amid-crypto-hype-google-s-cloud-unit-creates-web3-team

      https://www.itpro.co.uk/cloud/367612/google-cloud-is-reportedly-building-a-dedicated-team-to-support-web3-developers

      https://stackoverflow.blog/2022/04/20/new-data-developers-web3/

    来源:www.51cto.com/article/710198.html

    收起阅读 »

    2022年前端四大框架谁值得更大的关注

    web
    2022 年 Angular、Vue、React 和 Svelte 四大前端框架从数据分析,谁更值得去学习呐?本文基于 Stack Overflow 和 State of JavaScript 调查以及 JavaScript 性能标准对四大框架进行客观的分析比...
    继续阅读 »

    2022AngularVueReactSvelte 四大前端框架从数据分析,谁更值得去学习呐?

    本文基于 Stack OverflowState of JavaScript 调查以及 JavaScript 性能标准对四大框架进行客观的分析比较。

    文章从使用率、满意度、性能效率以及薪资来分析不同框架,每个标准占比 10 分。

    使用率

    Stack Overflow 方数据显示,

    • 40% 的开发者使用过 React

    • 22% 的开发者使用过 Angular

    • 19% 的开发者使用过 Vue

    • Svelte 使用占比仅为 3%

    State of JavaScript 调查显示: React 的 JS 开发者占比 80%,Angular 的开发者占比 54%,Vue 使用者占比 51%,Svelte 仅为 20%。

    AngularVue 的使用率类似,React 独占鳌头,Svelte 明显落后,最终分数分配: React 5 分,VueAngular 2.5 分,Svelte 0 分。

    开发者满意度

    Stack Overflow 调查显示,Svelte 的满意度最高,达到 71%。ReactVue 满意度紧随其后,分别为 69% 和 64%。Angular 满意度为 55%,满意与不满意人数几乎相同。

    State of JavaScript 调查的结果排名类似,但数值有所不同,Svelte 的满意度为 90%,React 为 84%,Vue 为 80%,Angular 仅为 45%。

    当前标准分数分配: Svelte 4 分,ReactVue 各 3 分,Angular 0 分。

    性能

    使用 JavaScript Framework Benchmark工具来分析各个框架的执行时间、内存占用及启用时间。评测结果将与 ·vanilla JavaScript· 进行比较。输出表格中,每个单元格颜色都是从绿色到红色,越接近红色正证明越偏离基本 JavaScript 。

    三个标准每个分配 10 分,取平均值得出总体相对性能得分。

    执行速度


    执行速度的方面,经过多次测试,Svelte 速度最快,Vue 紧随其后,ReactAngular 速度较慢,分数分配如下: Svelte 5 分,Vue 4 分,React 和 Angualr 各 0.5 分。

    内存占用


    内存占用方面,Svelte 仍然保持大幅度领先,Vue 略微优于并驾齐驱的 ReactAngular。分数分配如下: Svelte 6 分,Vue 3 分,ReactAngular 各 0.5 分。

    启动时间


    Svelte 的启动速度也非常出色,Vue 略逊一筹,ReactAngular 紧随其后。这次结果相对均匀,分数分配如下: Svelte 4 分,Vue 3 分,AngularReact 各 1 分。

    性能整体表现分数

    经过上面三项测试,四大框架在性能方面的得分,最终如下: Svelte 5 分,Vue 3.5 分,ReactAngular 0.5 分。

    薪资

    薪资评测数据来源于 Stack Overflow 的框架薪资中位数,各框架薪资如下:

    • Angular 49k 美元

    • Vue 50k 美元

    • React 58k 美元

    • Svelte 62k 美元

    AngularVue 的薪资接近,ReactSvelte 的薪资遥遥领先,因此分数分配如下: AngularVue 各 1.5 分,React 3 分,Svelte 4 分。

    最终成绩

    经过上面几轮的评估,四大框架最终分数如下:

    • Angular: 4.5

    • Vue: 10.5

    • React: 12

    • Svelte: 13

    得到评估结果后,我们再来客观的分析一下 JavaScript 调查报告。下面的视图结合了用户满意度(从左往右)和使用率(从下到上),同时涵盖了跨时间轨迹。


    Svelte 90% 的满意度主要来源于乐于尝试新技术的开拓者。React 仍然占据使用的主导地位,但未能保持高满意度。

    如果来分析使用率和满意度的四象限图,你会发现,Angular 使用频度一般,收获满意较少;Vue 满意度高但使用率并不高,而且随着时间的推移,满意度正在降低。React 则得到了广泛的使用和赞赏。近期 React 还推出了服务端的 Next.jsRemix,其越来越成为前端的标准。

    如果想了解更多讯息,请参考: javascript.plainenglish.io/angular-vs-…


    作者:战场小包
    来源:https://juejin.cn/news/7102437237203140644

    收起阅读 »

    WebGPU 会取代 WebGL 吗?

    前言 你知道WebGL并使用过吗?如果没有,那你也一定使用Three.js。在本文,我将向你介绍一下WebGL和其后起之秀 WebGPU。 什么是 WebGL ? WebGL 的起源 说起WebGL的起源,就不得不提起OpenGL。 在个人计算机的早期,使用最...
    继续阅读 »

    前言


    你知道WebGL并使用过吗?如果没有,那你也一定使用Three.js。在本文,我将向你介绍一下WebGL和其后起之秀 WebGPU


    什么是 WebGL ?


    WebGL 的起源


    说起WebGL的起源,就不得不提起OpenGL


    在个人计算机的早期,使用最广泛的3D图形渲染技术是Direct3DOpenGLDirect3D是微软DirectX技术的一部分,并主要用于Windows平台。而OpenGL是一种开源的跨平台技术,并赢得了众多开发者的青睐。


    然后,就是一个特殊的版本 - OpenGL ES。它专门为嵌入式计算机,智能手机,家用游戏机和其他设备而设计。他从OpenGL中移除了很多旧的和无用的特性,并为其添加了一些新的特性。例如,去除了矩形等多余的多边形,而只保留了点、线、三角形等基本图形。这使它在保持轻量级的同时仍然保留足够强大的能力来渲染漂亮的3D图形。


    最后,WebGL就是从OpenGL ES衍生而来的。它专注于web3D图形渲染。


    下图显示了它们之间的关系:


    image.png


    WebGL 的历史


    image.png


    从上图可以看出,WebGL已经很老了。不仅仅是因为它的存在时间长,还有它的标准是继承自OpenGL的。
    OpenGL的设计理念可以追溯到1992年,这些古老的概念已经和如今GPU的工作原理不相符合了。


    对于浏览器开发者来说,适配不同GPU的特性,给他们带来了诸多不便。


    从上图中我们可以看到苹果在2014年发布了Metal。而Steve JobsOpenGL ES的忠实支持者,他认为这是行业的未来,所以当时Apple设备上的游戏依然依赖于OpenGL ES(例如愤怒的小鸟,水果忍者)。但在Steve Jobs去世后,苹果放弃了OpenGL ES,开发了新的图形框架Metal


    微软也在2015年发布了自己的D3D12[Direct3D 12]图形框架。紧随其后的是Khronos Group


    image.png


    Khronos Group是图形行业的一个国际组织,类似于前端圈的W3CTC39。它的标准是WebGL。甚至他们也逐渐淡化了WebGL,转而支持现在的Vulkan


    到此为止,MetalD3D12 [Direct3D 12] 和Vulkan并列为三大现代图形框架。这些框架充分释放了GPU 的可编程能力,让开发者可以最大程度地自由控制GPU


    另外,今天的主流操作系统不再将OpenGL作为主要支持。这意味着我们今天编写的每一行WebGL代码90%的不会被OpenGL绘制。在Windows计算机上将使用DirectX绘制,而在Mac计算机上则使用Metal绘制。


    从这些可以看出OpenGL已经很老了。但这并不意味着它会消失。它继续会在嵌入式和科学研究等特殊领域发挥作用。


    WebGL也是如此,大量的适配工作使其难以向前推进。于是推出了WebGPU


    什么是 WebGPU?


    WebGPU的目标是提供现代3D图形和计算能力。它是由W3C组织(前端的老朋友)制定的标准。与WebGL不同,WebGPU不是OpenGL的包装。并且恰恰相反,它指的是当前的图形渲染技术,一种新的跨平台高性能图形界面。


    它的设计更容易被三大图形框架实现,从而减轻了浏览器开发者的负担。它也是一个精确的图形API,完全开放了整个显卡的能力。而不再是像WebGL这样的上层API


    更具体的优点有:



    • 减少了CPU开销

    • 对多线程的良好支持

    • 使用计算着色器将通用计算 (GPGPU) 的强大功能引入Web

    • 全新的着色器语言 - WebGPU Shading Language (WGSL)

    • 未来将支持 实时光线追踪 的技术


    image.png


    WebGPU 的发展现状


    目前,WebGPUAPI仍在开发迭代中,但我们可以在Chrome Canary中试用


    image.png


    在目前的前端框架中,Three.js 已经开始实现WebGPU的后端渲染器,Babylon.js计划在5.x版本中支持 WebGPU


    结论


    我认为WebGPU取代WebGL是大势所趋。而且我相信它在元宇宙场景中有很大的潜力。


    你如何看待WebGL?你看好WebGPU吗?


    作者:Ylimhs
    链接:https://juejin.cn/news/7103527513875447845
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    微前端乾坤使用过程中的坑

    微前端乾坤使用过程中的坑乾坤在启动子应用的时候默认开启沙箱模式{sandbox: true},这样的情况下,乾坤节点下会生成一个 shadow dom,shadow dom 内的样式与外部样式是没有关联的,这样就会给子应用内的样式带来一系列问题。这其...
    继续阅读 »

    微前端乾坤使用过程中的坑

    乾坤在启动子应用的时候默认开启沙箱模式{sandbox: true},这样的情况下,乾坤节点下会生成一个 shadow dom,shadow dom 内的样式与外部样式是没有关联的,这样就会给子应用内的样式带来一系列问题。这其中很多问题并不是乾坤造成的,而是 shadow dom 本身的特性导致的,乾坤还是不错的(不背锅)。随时补充

    1.iconffont 字体在子应用无法加载

    原因:shadow dom 是不支持@font-face 的,所以当引入 iconfont 的时候,尽管可以引入样式,但由于字体文件是不存在的,所以相对应的图标也无法展示。相关链接:@font-face doesn't work with Shadow DOM?Icon Fonts in Shadow DOM

    方案:

    1. 把字体文件放在主应用加载
    2. 使用通用的字体文件,这样就不需要单独加载字体文件了(等于没说~

    2.dom的查询方法找不到指定的元素

    原因:shadow dom 内的元素是被隔离的元素,故 document下查询的方法例如,querySelector、getElementsById 等是获取不到 shadow dom 内元素的。

    方案:代理 document 下各个查询元素的方法,使用子应用外面的 shadow dom 一层查询。如何获取子应用dom对象可以参考乾坤的这个方法 initGlobalState

    3.组件库动态创建的元素无法使用自己的样式

    原因:有些对话框或提示窗是通过document.body.appendChild添加的,所以 shadow dom 内引入的 CSS 是无法作用到外面元素的。方案:代理document.body.appendChild方法,即把新加的元素添加到 shadow dom容器下,而不是最外面的 body节点下。

    补充:类似的问题都可以往这个方向靠,看是不是shadow dom节点或者dom方法的问题。

    4.第三方引入的 JS 不生效

    原因:有些 JS 文件本身是个立即执行函数,或者会动态的创建 scipt 标签,但是所有获取资源的请求是被乾坤劫持处理,所以都不会正常执行,也不会在 window 下面挂载相应的变量,自然在取值调用的时候也不存在这个变量。方案:参考乾坤的 issue,子应用向body添加script标签失败

    5.webpack-dev-server 代理访问的接口 cookie 丢失

    原因:在主应用的端口下请求子应用的端口,存在跨域,axios 默认情况下跨域是不携带 cookie 的,假如把 axios 的 withCredential设置为 true(表示跨域携带 cookie),那么子应用需要设置跨域访问头Access-Control-Allow-Origin(在 devServer 下配置 header)为指定的域名,但不能设置为*,这时候同时存在主应用和子应用端口发出的请求,而跨域访问头只能设置一个地址,就导致无法代理指定服务器接口。

    方案:子应用接口请求的端口使用主应用接口请求的端口,使用主应用的配置代理请求

    // 主应用

    devServer:
    {
    ...
    port: 9600
    proxy: {
    // 代理配置
    }
    }

    // 子应用
    devServer: {
    ...
    port: 9600, // 使用主应用的页面访问端口
    }

    原文:https://segmentfault.com/a/1190000037641251


    收起阅读 »

    Vue + qiankun 快速实现前端微服务

    什么是微前端Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -...
    继续阅读 »

    什么是微前端

    Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

    微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

    qiankun

    qiankun 是蚂蚁金服开源的一套完整的微前端解决方案。具体描述可查看 文档 和 Github

    下面将通过一个微服务Demo 介绍 Vue 项目如何接入 qiankun,代码地址:micro-front-vue)

    二、配置主应用

    1. 使用 vue cli 快速创建主应用;
    2. 安装 qiankun
    $ yarn add qiankun # 或者 npm i qiankun -S
    1. 调整主应用 main.js 文件:具体如下:
    import Vue from "vue"
    import App from "./App.vue"
    import router from "./router"

    import { registerMicroApps, setDefaultMountApp, start } from "qiankun"
    Vue.config.productionTip = false
    let app = null;
    /**
    * 渲染函数
    * appContent 子应用html内容
    * loading 子应用加载效果,可选
    */
    function render({ appContent, loading } = {}) {
    if (!app) {
    app = new Vue({
    el: "#container",
    router,
    data() {
    return {
    content: appContent,
    loading
    };
    },
    render(h) {
    return h(App, {
    props: {
    content: this.content,
    loading: this.loading
    }
    });
    }
    });
    } else {
    app.content = appContent;
    app.loading = loading;
    }
    }

    /**
    * 路由监听
    * @param {*} routerPrefix 前缀
    */
    function genActiveRule(routerPrefix) {
    return location => location.pathname.startsWith(routerPrefix);
    }

    function initApp() {
    render({ appContent: '', loading: true });
    }

    initApp();

    // 传入子应用的数据
    let msg = {
    data: {
    auth: false
    },
    fns: [
    {
    name: "_LOGIN",
    _LOGIN(data) {
    console.log(`父应用返回信息${data}`);
    }
    }
    ]
    };
    // 注册子应用
    registerMicroApps(
    [
    {
    name: "sub-app-1",
    entry: "//localhost:8091",
    render,
    activeRule: genActiveRule("/app1"),
    props: msg
    },
    {
    name: "sub-app-2",
    entry: "//localhost:8092",
    render,
    activeRule: genActiveRule("/app2"),
    }
    ],
    {
    beforeLoad: [
    app => {
    console.log("before load", app);
    }
    ], // 挂载前回调
    beforeMount: [
    app => {
    console.log("before mount", app);
    }
    ], // 挂载后回调
    afterUnmount: [
    app => {
    console.log("after unload", app);
    }
    ] // 卸载后回调
    }
    );

    // 设置默认子应用,与 genActiveRule中的参数保持一致
    setDefaultMountApp("/app1");

    // 启动
    start();
    1. 修改主应用 index.html 中绑定的 id ,需与 el  绑定 dom 为一致;
    2. 调整 App.vue 文件,增加渲染子应用的盒子:
    <template>
    <div id="main-root">
    <!-- loading -->
    <div v-if="loading">loading</div>
    <!-- 子应用盒子 -->
    <div id="root-view" class="app-view-box" v-html="content"></div>
    </div>
    </template>

    <script>
    export default {
    name: "App",
    props: {
    loading: Boolean,
    content: String
    }
    };
    </script>
    1. 创建 vue.config.js 文件,设置 port :
    module.exports = {
    devServer: {
    port: 8090
    }
    }

    三、配置子应用

    1. 在主应用同一级目录下快速创建子应用,子应用无需安装 qiankun
    2. 配置子应用 main.js:
    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import App from './App.vue';
    import routes from './router';
    import './public-path';

    Vue.config.productionTip = false;

    let router = null;
    let instance = null;

    function render() {
    router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
    mode: 'history',
    routes,
    });

    instance = new Vue({
    router,
    render: h => h(App),
    }).$mount('#app');
    }

    if (!window.__POWERED_BY_QIANKUN__) {
    render();
    }

    export async function bootstrap() {
    console.log('vue app bootstraped');
    }

    export async function mount(props) {
    console.log('props from main app', props);
    render();
    }

    export async function unmount() {
    instance.$destroy();
    instance = null;
    router = null;
    }
    1. 配置 vue.config.js
    const path = require('path');
    const { name } = require('./package');

    function resolve(dir) {
    return path.join(__dirname, dir);
    }

    const port = 8091; // dev port

    module.exports = {
    /**
    * You will need to set publicPath if you plan to deploy your site under a sub path,
    * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
    * then publicPath should be set to "/bar/".
    * In most cases please use '/' !!!
    * Detail: https://cli.vuejs.org/config/#publicpath
    */
    outputDir: 'dist',
    assetsDir: 'static',
    filenameHashing: true,
    // tweak internal webpack configuration.
    // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
    devServer: {
    // host: '0.0.0.0',
    hot: true,
    disableHostCheck: true,
    port,
    overlay: {
    warnings: false,
    errors: true,
    },
    headers: {
    'Access-Control-Allow-Origin': '*',
    },
    },
    // 自定义webpack配置
    configureWebpack: {
    resolve: {
    alias: {
    '@': resolve('src'),
    },
    },
    output: {
    // 把子应用打包成 umd 库格式
    library: `${name}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${name}`,
    },
    },
    };

    其中有个需要注意的点:

    1. 子应用必须支持跨域:由于 qiankun 是通过 fetch 去获取子应用的引入的静态资源的,所以必须要求这些静态资源支持跨域;
    2. 使用 webpack 静态 publicPath 配置:可以通过两种方式设置,一种是直接在 mian.js 中引入 public-path.js 文件,一种是在开发环境直接修改 vue.config.js:
    {
    output: {
    publicPath: `//localhost:${port}`;
    }
    }

    public-path.js 内容如下:

    if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
    }
    至此,Vue 项目的前端微服务已经简单完成了。

    但是在实际的开发过程中,并非如此简单,同时还存在应用间跳转、应用间通信等问题。


    原文:https://segmentfault.com/a/1190000021872481


    收起阅读 »

    使用自定义url发图片的坑

    发送URL图片消息 App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true。实际上还得在WEBIM里面再配置一下WebIM.conn = new WebIM.connect...
    继续阅读 »


    发送URL图片消息





    App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true

    实际上还得在WEBIM里面再配置一下

    WebIM.conn = new WebIM.connection({
    appKey: WebIM.config.appkey,
    isMultiLoginSessions: WebIM.config.isMultiLoginSessions,
    https: typeof WebIM.config.https === "boolean" ? WebIM.config.https : location.protocol === "https:",
    url: WebIM.config.xmppURL,
    apiUrl: WebIM.config.apiURL,
    isAutoLogin: false,
    heartBeatWait: WebIM.config.heartBeatWait,
    autoReconnectNumMax: WebIM.config.autoReconnectNumMax,
    autoReconnectInterval: WebIM.config.autoReconnectInterval,
    useOwnUploadFun: WebIM.config.useOwnUploadFun,
    isDebug: false,
    isHttpDNS:false
    });







    单聊通过URL发送图片消息的代码示例如下:


    // 单聊通过URL发送图片消息
    var sendPrivateUrlImg = function () {
    var id = conn.getUniqueId(); // 生成本地消息id
    var msg = new WebIM.message('img', id); // 创建图片消息
    var option = {
    body: {
    type: 'file',
    url: url,
    size: {
    width: msg.width,
    height: msg.height,
    },
    length: msg.length,
    filename: msg.file.filename,
    filetype: msg.filetype
    },
    to: 'username', // 接收消息对象
    };
    msg.set(option);
    conn.send(msg.body);
    }
    收起阅读 »

    微前端框架 qiankun 技术分析

    如何加载子应用single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html e...
    继续阅读 »

    如何加载子应用

    single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html entry 的。

    qiankun 提供了一个 API registerMicroApps 来注册子应用,其内部调用 single-spa 提供的 registerApplication 方法。在调用 registerApplication 之前,会调用内部的 loadApp 方法来加载子应用的资源,初始化子应用的配置。

    通过阅读 loadApp 的代码,我们发现,qiankun 通过 import-html-entry 这个包来加载子应用。import-html-entry 的作用就是通过解析子应用的入口 html 文件,来获取子应用的 html 模板、css 样式和入口 JS 导出的生命周期函数。

    import-html-entry

    import-html-entry 是这样工作的,假设我们有如下 html entry 文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>test</title>
    </head>
    <body>

    <!-- mark the entry script with entry attribute -->
    <script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
    <script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
    </body>
    </html>

    我们使用 import-html-entry 来解析这个 html 文件:

    import importHTML from 'import-html-entry';

    importHTML('./subApp/index.html')
    .then(res => {
    console.log(res.template);

    res.execScripts().then(exports => {
    const mobx = exports;
    const { observable } = mobx;
    observable({
    name: 'kuitos'
    })
    })
    });

    importHTML 的返回值有如下几个属性:

    • template 处理后的 HTML 模板
    • assetPublicPath 静态资源的公共路径
    • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
    • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
    • execScripts 执行脚本的函数

    在 importHTML 的返回值中,除了几个工具类的方法,最重要的就是 template 和 execScripts 了。

    importHTML('./subApp/index.html') 的整个执行过程代码比较长,我们只讲一下大概的执行原理,感兴趣的同学可以自行查看importHTML 的源码

    importHTML 首先会通过 fetch 函数请求具体的 html 内容,然后在 processTpl 函数 中通过一系列复杂的正则匹配,解析出 html 中的样式文件和 js 文件。

    importHTML 函数返回值为 { template, scripts, entry, styles },分别是 html 模板,html 中的 js 文件(包含内嵌的代码和通过链接加载的代码),子应用的入口文件,html 中的样式文件(同样是包含内嵌的代码和通过链接加载的代码)。

    之后通过 getEmbedHTML 函数 将所有使用外部链接加载的样式全部转化成内嵌到 html 中的样式。getEmbedHTML 返回的 html 就是 importHTML 函数最终返回的 template 内容。

    现在,我们看看 execScripts 是怎么实现的。

    execScripts 内部会调用 getExternalScripts 加载所有 js 代码的文本内容,然后通过 eval("code") 的形式执行加载的代码。

    注意,execScripts 的函数签名是这样的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允许我们传入一个沙箱对象,如果子应用按照微前端的规范打包,那么会在全局对象上设置 mountunmount 这几个生命周期函数属性。execScripts 在执行 eval("code") 的时候,会巧妙的把我们指定的沙箱最为全局对象包装到 "code" 中,子应用能够运行在沙盒环境中。

    在执行完 eval("code") 以后,就可以从沙盒对象上获取子应用导出的生命周期函数了。

    loadApp

    现在我们把视线拉回 loadApp 中,loadApp 在获取到 templateexecScripts 这些信息以后,会基于 template 生成 render 函数用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。

    之后,在子应用生命周期函数的基础上,构建新的生命周期函数,再调用 single-spa 的 API 启动子应用。

    在这些新的生命周期函数中,会在不同时机负责启动沙盒、渲染子应用、清理沙盒等事务。

    隔离

    在完成子应用的加载以后,作为一个微前端框架,要解决好子应用的隔离问题,主要要解决 JS 隔离和样式隔离这两方面的问题。

    JS 隔离

    qiankun 为根据浏览器的能力创建两种沙箱,在老旧浏览器中会创建快照模式 的浏览器中创建 VM 模式的沙箱 ProxySandbox

    篇幅限制,我们只看 ProxySandbox 的实现,在其构造函数中,我们可以看到具体的逻辑:首先会根据用户指定的全局对象(默认是 window)创建一个 fakeWindow,之后在这个 fakeWindow 上创建一个 proxy 对象,在子应用中,这个 proxy 对象就是全局变量 window

    constructor(name: string, globalContext = window) {
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
    const proxy = new Proxy(fakeWindow, {
    set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
    get: (target: FakeWindow, p: PropertyKey): any => {},
    has(target: FakeWindow, p: string | number | symbol): boolean {},

    getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {},

    ownKeys(target: FakeWindow): ArrayLike<string | symbol> {},

    defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {},

    deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {},

    getPrototypeOf() {
    return Reflect.getPrototypeOf(globalContext);
    },
    });
    this.proxy = proxy;
    }

    其实 qiankun 中的沙箱分两个类型:

    • app 环境沙箱
      app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。子应用在切换时,实际上切换的是 app 环境沙箱。
    • render 沙箱
      子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。

    上面说的 ProxySandbox 其实是 render 沙箱。至于 app 环境沙箱,qiankun 目前只针对在应用 bootstrap 时动态创建样式链接、脚本链接等副作用打了补丁,保证子应用切换时这些副作用互不干扰。

    之所以设计两层沙箱,是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。

    样式隔离

    qiankun 提供了多种样式隔离方式,隔离效果最好的是 shadow dom,但是由于其存在诸多限制,qiankun 官方在将来的版本中将会弃用,转而推行 experimentalStyleIsolation 方案。

    我们可以通过下面这段代码看到 experimentalStyleIsolation 方案的基本原理。

    const styleNodes = appElement.querySelectorAll('style') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
    css.process(appElement!, stylesheetElement, appInstanceId);
    });

    css.process 的核心逻辑,就是给读取到的子应用的样式添加带有子应用信息的前缀。效果如下:

    /* 假设应用名是 react16 */
    .app-main {
    font-size: 14px;
    }

    div[data-qiankun-react16] .app-main {
    font-size: 14px;
    }

    通过上面的隔离方法,基本可以保证子应用间的样式互不影响。

    小结

    qiankun 在 single-spa 的基础上根据实际的生产实践开发了很多有用的功能,大大降低了微前端的使用成本。

    本文仅仅针对如何加载子应用和如何做好子应用间的隔离这两个问题,介绍了 qiankun 的实现。其实,在隔离这个问题上,qiankun 也仅仅是根据实际中会遇到的情况做了必要的隔离措施,并没有像 iframe 那样实现完全的隔离。我们可以说 qiankun 实现的隔离有缺陷,也可以说是 qiankun 在实际的业务需求和完全隔离的实现成本之间做的取舍。

    原文:https://segmentfault.com/a/1190000041151414

    收起阅读 »

    pc端微信授权登录两种实现方式的总结

    在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。一、跳转微信授权登录页面进行扫码授权这种方法实现非常简单只用跳转链接就可以实现微信授权登录window.location = https://op...
    继续阅读 »

    在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。

    一、跳转微信授权登录页面进行扫码授权

    这种方法实现非常简单只用跳转链接就可以实现微信授权登录

    window.location = https://open.weixin.qq.com/connect/qrconnect?appid=${appid}&redirect_uri=${回调域名}/login&response_type=code&scope=snsapi_login&state=${自定义配置}#wechat_redirect

    跳转之后进行微信扫码,之后微信会带着code,回调回你设置的回调域名,这之后拿到code再和后台进行交互,即可实现微信登陆。
    这种方法相对来说实现起来非常简单,但是因为需要先跳转微信授权登录页面,在体验上来说可能不是太好。

    二、在当前页面生成微信授权登录二维码

    这种方法是需要引入wxLogin.js,动态生成微信登陆二维码,具体实现方法如下:

    const s = document.createElement('script')
    s.type = 'text/javascript'
    s.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
    const wxElement = document.body.appendChild(s)
    wxElement.onload = function () {
    var obj = new WxLogin({
    id: 'wx_login_id', // 需要显示的容器id
    appid: '', // 公众号appid
    scope: 'snsapi_login', // 网页默认即可
    redirect_uri:'', // 授权成功后回调的url
    state: '', // 可设置为简单的随机数加session用来校验
    style: 'black', // 提供"black"、"white"可选。二维码的样式
    href: '' // 外部css(查看二维码的dom结构,根据类名进行样式覆盖)文件url,需要https
    })
    }

    其中href参数项还可以通过node将css文件转换为data-url,实现方式如下:

    var fs = require('fs');
    function base64_encode(file) {
    var bitmap = fs.readFileSync(file);
    return 'data:text/css;base64,'+new Buffer(bitmap).toString('base64');
    }
    console.log(base64_encode('./qrcode.css'))

    在终端对该js文件执行命令:

    node qr.js

    把打印出来的url粘贴到href即可。
    这种实现方法避免了需要跳转新页面进行扫码,二维码的样式也可以进行更多的自定义设置,可能在体验上是更好的选择。

    原文:https://segmentfault.com/a/1190000024492932


    收起阅读 »

    Three.js控制物体显示与隐藏的方法

    本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:visible属性;layers属性。下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本...
    继续阅读 »

    本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:

    1. visible属性;
    2. layers属性。

    下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本为例:

    visible属性

    visible 是Object3D的属性。只有当 visible 是 true 的时候,该物体才会被渲染。任何继承 Object3D 的对象都可以通过该属性去控制它的显示与否,比如:MeshGroupSpriteLight等。

    举个简单的例子:

    // 控制单个物体的显示和隐藏
    const geometry = new THREE.PlaneGeometry(1, 1) // 1*1的一个平面
    const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) // 红色平面
    const plane = new THREE.Mesh(geometry, planeMaterial)
    plane.visible = false // 不显示单个物体
    scene.add(plane)
    // 控制一组物体的显示和隐藏
    const geometry = new THREE.PlaneGeometry(1, 1)
    const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    const plane = new THREE.Mesh(geometry, planeMaterial)
    const group = new THREE.Group()
    group.add(plane)
    group.visible = false // 不显示一组物体
    scene.add(group)

    通过后面的例子可以看出,当我们想要控制一组物体的显示与隐藏,可以把这些物体放入一个 Group 中,只通过控制 Group 的显示与隐藏即可。

    这块的代码逻辑是在WebGLRenderer.js的 projectObject 方法中实现的。

    首先,在 render 方法中调用了 projectObject 方法:

    this.render = function ( scene, camera ) {
    // ...
    projectObject( scene, camera, 0, _this.sortObjects );
    // ...
    }

    projectObject 方法的定义如下:

    function projectObject( object, camera, groupOrder, sortObjects ) {
    if ( object.visible === false ) return; // 注释1:visible属性是false直接返回
    // ...
    var children = object.children; // 注释2:递归应用在children上

    for ( var i = 0, l = children.length; i < l; i ++ ) {

    projectObject( children[ i ], camera, groupOrder, sortObjects ); // 注释2:递归应用在children上

    }
    }

    从注释1可以看出,如果 Group 的 visible 是 false,那么就不会在 children 上递归调用,所以就能达到通过 Group 控制一组对象的显示与隐藏的效果。

    当 visible 是 false 的时候,Raycaster 的 intersectObject 或者 intersectObjects 也不会把该物体考虑在内。这块的代码逻辑是在 Raycaster.js

    intersectObject: function ( object, recursive, optionalTarget ) {
    // ...
    intersectObject( object, this, intersects, recursive ); // 注释1:调用了公共方法intersectObject
    // ...
    },

    intersectObjects: function ( objects, recursive, optionalTarget ) {
    // ...

    for ( var i = 0, l = objects.length; i < l; i ++ ) {

    intersectObject( objects[ i ], this, intersects, recursive ); // 注释1:循环调用了公共方法intersectObject

    }
    // ...
    }

    // 注释1:公共方法intersectObject
    function intersectObject( object, raycaster, intersects, recursive ) {

    if ( object.visible === false ) return; // 注释1:如果visible是false,直接return

    // ...
    }

    从注释1可以看出,如果 Group 或者单个物体的 visible 是 false ,就不做检测了。

    layers属性

    Object3D的layers属性 是一个 Layers 对象。任何继承 Object3D 的对象都有这个属性,比如 Camera 。Raycaster 虽然不是继承自 Object3D ,但它同样有 layers 属性(r113版本以上)。

    和上面的 visible 属性一样,layers 属性同样可以控制物体的显示与隐藏、Raycaster 的行为。当物体和相机至少有一个同样的层的时候,物体就可见,否则不可见。同样,当物体和 Raycaster 至少有一个同样的层的时候,才会进行是否相交的测试。这里,强调了是至少有一个,是因为 Layers 可以设置多个层。

    Layers 一共可以表示 32 个层,0 到 31 层。内部表示为:




    Layers 可以设置同时拥有多个层:

    1. 可以通过 Layers 的 enable 和 disable 方法开启和关闭当前层,参数是上面表格中的 0 到 31 。
    2. 可以通过 Layers 的 set 方法 只开启 当前层,参数是上述表格中的 0 到 31
    3. 可以通过 Layers 的 test 的方法判断两个 Layers 对象是否存在 至少一个公共层 。

    当开启多个层的时候,其实就是上述表格中的二进制进行 按位或 操作。比如 同时 开启 0231 层,那么内部存储的值就是 10000000000000000000000000000101

    layers 属性默认只开启 0 层。

    还是上面那个例子,我们看下怎么控制物体的显示和隐藏:

    // 控制单个物体的显示和隐藏
    const geometry = new THREE.PlaneGeometry(1, 1)
    const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    const plane = new THREE.Mesh(geometry, planeMaterial)
    plane.layers.set(1) // 设置平面只有第1层,相机默认是在第0层,所以该物体不会显示出来
    scene.add(plane)
    // 控制一组物体的显示和隐藏
    const geometry = new THREE.PlaneGeometry(1, 1)
    const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    const plane = new THREE.Mesh(geometry, planeMaterial)
    const group = new THREE.Group()
    group.layers.set(1) // 注释1: 设置group只有第一层,相机默认是在第0层,但是此时平面物体还是显示出来了?
    group.add(plane)
    scene.add(group)

    设置单个物体的 layer 可以看到物体成功的没有显示出来。但是,当我们给 group 设置 layer 之后,发现 group 的 children(平面物体)还是显示了出来。那么,这是什么原因呢?让我们看下源码,同样还是上面的 projectObject 方法:

    function projectObject( object, camera, groupOrder, sortObjects ) {

    if ( object.visible === false ) return;

    var visible = object.layers.test( camera.layers ); // 注释1:判断物体和相机是否存在一个公共层

    if ( visible ) { // 注释1:如果存在,对物体进行下面的处理
    // ...
    }

    var children = object.children; // 注释1:不管该物体是否和相机存在一个公共层,都会对children进行递归

    for ( var i = 0, l = children.length; i < l; i ++ ) {

    projectObject( children[ i ], camera, groupOrder, sortObjects );

    }
    }

    从上述注释1可以看出,即使该物体和相机不存在公共层,也不影响该物体的 children 显示。这也就解释了上述为什么给 group 设置 layers ,但是平面物体还是能显示出来。从这一点上来看,layers 和 visible 属性在控制物体显示和隐藏的方面是不一样的。

    和 visible 属性一样,接下来我们看下 Layers 对 Raycaster 的影响。同样我还是看了 Raycaster.js 文件,但是发现根本就没有 layers 字段。后来,我看了下最新版本 r140 的 Raycaster.js

    function intersectObject( object, raycaster, intersects, recursive ) {

    if ( object.layers.test( raycaster.layers ) ) { // 注释1:判断物体和Raycaster是否有公共层

    object.raycast( raycaster, intersects );

    }

    if ( recursive === true ) { // 注释1:不管该物体和Raycaster是否有公共层,都不影响children

    const children = object.children;

    for ( let i = 0, l = children.length; i < l; i ++ ) {

    intersectObject( children[ i ], raycaster, intersects, true );

    }
    }
    }

    不同于前面,visible 和 layers 都可以用来控制物体的显示与隐藏,visible 和 layers 只有一个可以用来控制 Raycaster 的行为,具体是哪一个生效,可以看下 Three.js的迁移指南

    可以看到,从 r114 版本,废除了 visible ,开始使用 layers 控制 Raycaster 的行为:

    r113 → r114
    Raycaster honors now invisible 3D objects in intersection tests. Use the new property Raycaster.layers for selectively ignoring 3D objects during raycasting.

    总结

    从上面可以看出,visible 和 layers 在控制物体显示与隐藏、Raycaster 是否进行等方面是存在差异的。

    当该物体的 visible 属性为 false 并且 layers 属性测试失败的时候,行为总结如下:


    原文链接:https://segmentfault.com/a/1190000041881241

    收起阅读 »

    qiankun微前端

    本文参考: 官网 你可能并不需要微前端什么是微前端?Techniques, strategies and recipes for building a modern web app with multiple teams that can ship fea...
    继续阅读 »

    本文参考
    官网
    你可能并不需要微前端

    什么是微前端?

    Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

    qiankun是怎么来的?

    所有的技术都是为了解决当前的现实问题,然后通过思考和实践创造出来的。微前端本质上是为了解决组织和团队间协作带来的沟通和管理的问题

    引用微前端作者的思想:

    微前端是康威定律在前端架构上的映射。 康威定律指导思想:既然沟通是大问题,那么就不要沟通就好了

    作者认为大型系统都逃不过熵增定律,宇宙的本质,所有的东西都会从有序走向无序。一个东西如果你不去管理,他就会变成一坨垃圾,所以你想要维持一个东西的有序性,就要付出努力去维护他。所以从中找到平衡,qiankun就诞生了。通过分治的手段,让上帝的归上帝,凯撒的归凯撒

    什么情况下使用qiankun?

    我们在开发中可能会碰到下面的问题

    • 旧的系统不能下,新的需求还在来

    • 公司内部有很多的系统,不同系统间可能需要展示同一个页面

    • 一个系统过于庞大,每个人分别管理一个模块,git分支比较混乱。想要把系统拆分开来

    微前端首先解决的,是如何解构巨石应用

    核心价值:技术栈无关,应用之间不应该有任何直接或间接的技术栈、依赖、以及实现上的耦合。

    作者认为正确的微前端方案的目标应该是

    方案上跟使用 iframe 做微前端一样简单,同时又解决了 iframe 带来的各种体验上的问题

    qiankun的原理

    qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

    qiankun框架内部fetch请求资源,解析出js、css文件和HTML document,插入到主应用指定的容器中(使用HTML Entry接入方式)

    1. 调用import-html-entry模块的importEntry函数,获取到对应子应用的html文件、可执行脚本文件以及publicpath

    2. 调用getDefaultTplWrapper将子应用的html内容用div标签包裹起来

    3. 调用createElement函数生成剔除html、body、head标签后的子应用html内容(通过innerHTML达到过滤效果)

    4. 调用getRender函数得到render函数(所以子应用一定要有render函数)

    5. 调用第4步得到的render,将container内部清空,并将子应用的dom元素渲染到指定的contanter元素上

    6. 调用getAppWrapperGetter函数,生成一个可以获取处理过的子应用dom元素的函数initialAppWrapperGetter,以备后续使用子应用dom元素

    7. 如果sandbox为true,则调用createSandboxContainer函数

    8. 执行execScripts函数,执行子应用脚本

    9. 执行getMicroAppStateActions函数,获取onGlobalStateChange、setGlobalState、offGlobalStateChange,用于主子应用传递信息

    10. 执行parcelConfigGetter函数,包装mount和unmount

    上述步骤的源码

    qiankun如何实现隔离?

    沙箱隔离

    qiankun的沙箱有2种 JS沙箱 和 CSS沙箱

    JS沙箱

    JS沙箱又分为2种,快照沙箱(为了兼容IE)和 代理沙箱

    快照沙箱 snapshotSandbox

    基于diff实现,用来兼容不支持Proxy的浏览器,只适用单个子应用。会污染全局window

    1. 激活沙箱:将主应用window的信息存到windowSnapshot

    2. 根据

      modifyPropMap

      ,恢复为子应用的window信息

      读取和修改的是window中的数据,windowSnapshot是缓存的数据

    3. 退出沙箱:根据windowSnapshot把window恢复为主应用数据,将windowSnapshot和window进行diff,将变更的值存到modifyPropMap中,然后把window恢复为主应用数据

    总结

    • windowSnapshot主应用的window信息

    • modifyPropMap子应用修改的window信息

    相对应的源码

    代理沙箱

    代理沙箱也分为2种,单例和多例,都是由Proxy实现

    单例沙箱 legacySandbox

    为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换。 创建 addedPropsMapInSandbox(沙箱期间新增的全局变量)、modifiedPropsOriginalValueMapInSandbox(沙箱期间更新的全局变量)、currentUpdatedPropsValueMap(持续记录更新的(新增和修改的)全局变量的 map 用于在任意时刻做 snapshot) 三个变量,前两个用来恢复主应用window,最后一个用来恢复子应用window。同样会污染window,但性能比快照沙箱稍好,不用遍历window

    1. 激活沙箱:根据currentUpdatedPropsValueMap还原子应用的window数据

    2. window只要变动,在

      currentUpdatedPropsValueMap

      中进行记录

      1. 判断addedPropsMapInSandbox中是否有对应 key 的记录,没有新增一条,有的话往下执行

      2. 判断modifiedPropsOriginalValueMapInSandbox中是否有对应 key 的记录,没有的话,记录从window中对应key/value,有的话继续往下执行

      3. 修改window对应的key/value

    3. 退出沙箱:根据addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox还原主应用的window信息

    相对性的源码

    多例沙箱 proxySandbox

    主应用和子应用的window独立,不再共同维护一份window,终于JS沙箱也和qiankun微前端的思想统一了…实行了分治。不会污染全局window,支持多个子应用。

    1. 激活沙箱

    2. 取值,先从自己命名空间下的fakeWindow找key,没找到,找window

    3. 赋值,直接给自己命名空间下的fakeWindow赋值

    4. 退出沙箱

    相对应的源码

    CSS沙箱

    严格沙箱 和 实验性沙箱

    严格沙箱

    在加载子应用时,添加strictStyleIsolation: true属性,会将整个子应用放到Shadow DOM内进行嵌入,完全隔离了主子应用


    缺点:子应用中应用的一些弹框组件会因为找不到body而丢失

    实验性沙箱

    在加载子应用时,添加experimentalStyleIsolation: true属性,实现形式类似于vue中style标签中的scoped属性,qiankun会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun=“xxx”],这里的XXX为注册子应用时name的值


    缺点:子应用中应用的一些弹框组件会因为插入到了主应用到body而丢失样式

    相对应的源码


    作者:丙乙
    来源:https://juejin.cn/post/7100825726424711204

    收起阅读 »

    v-for中diff算法

    当没有key时获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都...
    继续阅读 »

    当没有key时


    获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误


    以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都不一样的话,直接创建一个新类型,如果类型一样,值不同,就只更新值,效率会更高,当for循环完毕,新旧数组长度会进行比较,如果旧的长度大有新的长度,就会执行unmountChildren,删除多余的节点,如果新的长度大于旧的长度,就会执行mountChildren,创建新的节点

    当有key时


    第一步,从头部开始遍历


    通过isSameVNodeType进行比较


    如果type 和 key 都一样,继续遍历,如果不同,跳出循环,进入第二步

    第二步,从尾部开始遍历


    和第一步操作一致

    如果不同,跳出循环进入第三步

    第三步,果旧节点遍历完,依然有新的节点,就是添加节点操作,用一个null和新节点进行patch,n1为空值时,是添加



    如果新节点遍历完了,旧节点还有就进入第四步

    第四步,新节点遍历完毕,旧节点还有,就进行删除操作


    第五步,如果是一个无序的节点,vue会从旧的节点里找到新的节点里相同的值并创建一个新的数组,根据key建立一个索引,找到了就放入新数组里,比较完之后,有多余的旧节点就删除,有没有比较过的新节点就添加


    作者:啊哈呀呀呀呀
    来源:juejin.cn/post/7100858461520560135

    收起阅读 »

    IP属地获取,前端获取用户位置信息

    尝试获取用户的位置信息写在前面想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。尝试一:navigator.geolocation尝试了使用 navigat...
    继续阅读 »


    尝试获取用户的位置信息

    写在前面

    想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。

    尝试一:navigator.geolocation

    尝试了使用 navigator.geolocation,但未能成功拿到信息。

    getGeolocation(){
     if ('geolocation' in navigator) {
       /* 地理位置服务可用 */
       console.log('地理位置服务可用')
       navigator.geolocation.getCurrentPosition(function (position) {
         console.dir('回调成功')
         console.dir(position) // 没有输出
         console.dir(position.coords.latitude, position.coords.longitude)
      }, function (error) {
         console.error(error)
      })
    } else {
       /* 地理位置服务不可用 */
       console.error('地理位置服务可用')
    }
    }

    尝试二:sohu 的接口

    尝试使用 pv.sohu.com/cityjson?ie… 获取用户位置信息, 成功获取到信息,信息样本如下:

    {"cip": "14.11.11.11", "cid": "440000", "cname": "广东省"}
    // 需要做跨域处理
    getIpAndAddressSohu(){
     // config 是配置对象,可按需设置,例如 responseType,headers 中设置 token 等
     const config = {
       headers: {
         Accept: 'application/json',
         'Content-Type': 'application/json;charset=UTF-8',
      },
    }
     axios.get('/apiSohu/cityjson?ie=utf-8', config).then(res => {
       console.log(res.data) // var returnCitySN = {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"};
       const info = res.data.substring(19, res.data.length - 1)
       console.log(info) // {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"}
       this.ip = JSON.parse(info).cip
       this.address = JSON.parse(info).cname
    })
    }

    调试的时候,做了跨域处理。

    proxy: {
     '/apiSohu': {
       target: 'http://pv.sohu.com/', // localhost=>target
       changeOrigin: true,
       pathRewrite: {
       '/apiSohu': '/'
      }
    },
    }

    下面是一张获取到位置信息的效果图:


    尝试三:百度地图的接口

    需要先引入百度地图依赖,有一个参数 ak 需要注意,这需要像管理方申请。例如下方这样

    <script src="https://api.map.baidu.com/api?v=2.0&ak=3ufnnh6aD5CST"></script>
    getLocation() { /*获取当前位置(浏览器定位)*/
    const $this = this;
    var geolocation = new BMap.Geolocation();//返回用户当前的位置
    geolocation.getCurrentPosition(function (r) {
      if (this.getStatus() == BMAP_STATUS_SUCCESS) {
        $this.city = r.address.city;
        console.log(r.address) // {city: '广州市', city_code: 0, district: '', province: '广东省', street: '', …}
      }
    });
    }
    function getLocationBaiduIp(){/*获取用户当前位置(ip定位)*/
    function myFun(result){
      const cityName = result.name;
      console.log(result) // {center: O, level: 12, name: '广州市', code: 257}
    }
    var myCity = new BMap.LocalCity();
    myCity.get(myFun);
    }

    成功用户的省市位置,以及经纬度坐标,但会先弹窗征求用户意见。



    写在后面

    尝试结果不太理想,sohu 的接口内部是咋实现的,这似乎没有弹起像下面那样的征询用户意见的提示。


    而在 navigator.geolocation 和 BMap.Geolocation() 中是弹起了的。

    用别人的接口总归是没多大意思,也不知道不用征求用户意见是咋实现的。

    经实测 sohu 的接口和 new BMap.Geolocation() 都可以拿到用户的位置信息(省市、经纬度等)。

    作者:灵扁扁

    来源:https://juejin.cn/post/7100916925504421918

    收起阅读 »

    一种兼容、更小、易用的WEB字体API

    如何使用 Google Fonts CSS API 有效地使用WEB字体?多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Googl...
    继续阅读 »

    如何使用 Google Fonts CSS API 有效地使用WEB字体?

    多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Google Fonts CSS API 的普及,让在WEB中使用特殊字体变得简单、快速、灵活,当然更多的还是面向英文字体,对于做外贸或者英文网站的开发者来说是福音。

    Google Fonts CSS API 在不断发展,以跟上WEB字体技术的变化。它从最初的价值主张——允许浏览器在所有使用API的网站上缓存常用字体,从而使网页加载更快,到现在已经有了很大的进步。现在不再是这样了,但API仍然提供了额外的优化方案,使网站加载迅速,字体工作性能更佳。

    使用Google Fonts CSS API ,网站可以请求它需要的字体数据来保持它的CSS加载时间到最少,确保网站访问者可以尽可能快地加载内容。该API将以最佳的字体响应每个请求的web浏览器。

    所有这一切都是通过在代码中包含一行 HTML 来实现的。

    如何使用 Google Fonts CSS API

    Google Fonts CSS API 文档很好地总结了它:

    你不需要做任何编程;所要做的就是在 HTML 文档中添加一个特殊的样式表链接,然后在 CSS 样式中引用该字体。

    需要做的最低限度是在 HTML 中包含一行,如下所示:

    <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet" />

    复制代码

    当从 API 请求字体时,可以指定想要的一个或多个系列,以及(可选)它们的权重、样式、子集和其他选项。然后 API 将通过以下两种方式之一处理请求:

    1. 如果请求使用 API 已有文件的通用参数,它会立即将 CSS 返回给用户,将定向到这些文件。

    2. 如果请求的字体带有 API 当前未缓存的参数,它将即时对字体进行子集化,使用 HarfBuzz 快速完成,并返回指向它们的 CSS。

    字体文件可以很大,但不一定要很大

    WEB 字体可以很大,在 WOFF2 中,仅一个 Noto Sans Japanese 的大小就几乎是 3.4MB ,将其下载给每一位用户将拖累页面加载时间。当每一毫秒都很重要并且每个字节都很宝贵时,需要确保只加载用户需要的数据。

    Google Fonts CSS API 可以创建非常小的字体文件(称为子集),实时生成,只为用户提供网站所需的文本和样式。可以使用 text 参数请求特定字符,而不是提供整个字体。

    <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap&text=RobtMn" rel="stylesheet" />

    复制代码


    CSS API 还自动为用户提供额外的WEB字体优化,无需设置任何 API 参数。该 API 将为用户提供已启用 unicode-range 的 CSS 文件(如果 Web 浏览器支持),因此只为网站需要的特定字符加载字体。

    unicode-range CSS 描述符是一种现在可用于应对大字体下载的工具,这个 CSS 属性设置 @font-face 声明包含的 Unicode 字符范围。如果在页面上呈现这些字符之一,则下载该字体。这适用于所有类型的语言,因此可以采用包含拉丁文、希腊文或西里尔文字符的字体并制作更小的子集。在前面的图表中,可以看到如果必须加载所有这三个字符集,则将超过 600 个字形。


    这也为 Web 启用了中文、日文和韩文 (CJK) 字体提供支持。在上图中,可以看到 CJK 字体覆盖的字符数是拉丁字符字体的 15-20 倍。 CJK 字体通常非常大,并且这些语言中的许多字符不像其他字体那样频繁使用。

    使用 CSS API 和 unicode-range 可以减少大约 90% 的文件传输。使用 unicode-range 描述符,可以单独定义每个部分,并且只有在内容包含这些字符范围中的一个字符时才会下载每个切片。

    例如只想在 Noto Sans JP 中设置单词 こんにちは ,则可以按照如下方式使用:

    • 自托管自己的 WOFF2 文件

    • 使用 CSS API 检索 WOFF2

    • 使用 CSS API 并将 text= 参数设置为 こんにちは


    在此示例中,可以看到通过使用 CSS API,已经比自托管 WOFF2 字体节省了 97.5%,这要归功于 API 内置支持将大字体分隔到 unicode-range 中功能。通过更进一步并准确指定要显示的文本,可以进一步将字体大小减小到仅 CSS API 字体的 95.3% ,相当于比自托管字体小 99.9%

    Google Fonts CSS API 将自动以用户浏览器支持的最小和最兼容格式提供字体。如果用户使用的是支持 WOFF2 的浏览器,API 将提供 WOFF2 中的字体,但如果他们使用的是旧版浏览器,API 将以该浏览器支持的格式提供字体。为了减少每个用户的文件大小,API 还会在不需要时从字体中删除数据。例如,将为浏览器不需要的用户删除提示数据。

    使用 Google Fonts CSS API 让WEB字体面向未来

    Google 字体团队还为新的 W3C 标准做出了贡献,这些标准继续创新网络字体技术,例如 WOFF2。当前的一个项目是增量字体传输,它允许用户在屏幕上使用字体文件时加载非常小的部分,并按需流式传输其余部分,超过了 unicode-range 的性能。当使用 WEB 字体API时,当用户在浏览器中可用时,就可以获得这些底层字体传输技术的优化改进。

    这就是字体 API 的美妙之处:用户可以从每项新技术改进中受益,而无需对网站进行任何更改。新的WEB字体格式?没问题,新的浏览器或操作系统支持?它已经处理好了。因此,可以自由地专注于用户和内容,而不是陷入WEB字体维护的困境。

    可变字体支持内置

    可变字体是可以在多个轴之间存储一系列设计变化的字体文件,新版本的 Google Fonts CSS API 包括对它们的支持。添加一个额外的变化轴可以使字体具有新的灵活性,但它几乎可以使字体文件的大小增加一倍。

    当 CSS API 请求更具体时,Google Fonts CSS API 可以仅提供网站所需的可变字体部分,以减少用户的下载大小。这使得可以为 WEB 使用可变字体,而不会导致页面加载时间过长。可以通过在轴上指定单个值或指定范围来执行此操作,甚至可以在一个请求中指定多个轴和多个字体系列, API 可以灵活地满足需求。

    总结

    Google Fonts CSS API 可帮助WEB提供以下字体:

    • 更兼容

    • 体积更小

    • 加载快速

    • 易于使用

    有关 Google 字体的更多信息,请访问 fonts.google.com


    作者:天行无忌
    来源:juejin.cn/post/7100927964224700424

    收起阅读 »

    什么是请求参数、表单参数、url参数、header参数、Cookie参数?一文讲懂

    最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他...
    继续阅读 »

    最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。

    回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。

    先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他部门提供的 api 去完成某项业务。

    那个 api 文档只告诉了我请求参数需要传什么,没有提及用什么方式传,比如这样:


    其实如果有经验的话,直接在请求体或 url 里填参数试一下就知道了;另一个是新人有时候不太敢问问题,其实只要向同事确认一下就好的。

    然而由于当时我掌握的编程知识有限,只会用表单提交数据。所以当我下载完同事安利的 api 调用调试工具 postman 后,我就在网上查怎么用 postman 发送表单数据,结果折腾了好久 api 还是没能调通。

    当天晚上我向老同学求助,他问我上课是不是又睡过去了?

    我说你怎么知道?

    他说当然咯,你上课睡觉不学习又不是一天两天的事情......

    后来他告诉我得好好学一下 http 协议,看看可以在协议的哪些位置放请求参数。

    一个简单的 http 服务器还原

    那么,在正式讲解之前,我们先简单搭建一个 http 服务器,阿菌沿用经典的 python 版云你好服务器进行讲解。

    云你好服务器的代码很简单,服务器首先会获取 name 用户名这个参数,如果用户传了这个参数,就返回 Hello xxx,xxx 指的是 name 用户名;如果用户没有传这个参数则返回 Hello World

    # 云你好服务源码
    from flask import Flask
    from flask import request

    app = Flask(__name__)

    # 云你好服务 API 接口
    @app.get("/api/hello")
    def hello():
       # 看用户是否传递了参数 name
       name = request.args.get("name", "")
       # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
       return f"Hello {name}" if name else "Hello World"

    # 启动云你好服务
    if __name__ == '__main__':
       app.run()

    为了快速开发(大伙可以下载一个 python 把这个代码跑一下,用自己的语言实现一个类似的服务器也是可以的),阿菌这里使用了 flask 框架构建后端服务。

    在具体获取参数的时候,我选择了在 request.args 中获取参数。这里提前剧透一下:在 flask 框架中,request.args 指的是从 url 中获取参数(不过这是我们后面讲解的内容,大家有个印象就好)

    抓包查看 http 报文

    有了 http 服务器后,我们开始深入讲解 http 协议,em...个人觉得只在学校上课看教材学计算机网络好像还欠缺了点啥,比较推荐大家下载一个像 Wireshark 这样的网络抓包软件,动手拆解网络包,深入学习各种网络协议。抓取网络包的示例视频

    为了搞清楚什么是请求参数、表单参数、url 参数、Header 参数、Cookie 参数,我们先发一个 http 请求,然后抓取这个请求的网络包,看看一份 http 报文会携带哪些信息。

    呼应开头,用户阿菌是个只会发表单数据的萌新,他使用 postman 向云你好 api 发送了一个 post 请求:


    剧情发展正常,我们没能得到 Hello 阿菌(服务器会到 url 中获取参数,咱们用表单形式提交,所以获取不到)

    由于咱们对请求体这个概念比较模糊,接下来我们重新发一个一模一样的请求,并且通过 Wireshark 抓包看一下:


    可以看到强大的 Wireshark 帮助我们把请求抓取了下来,并把整个网络包的链路层协议,IP层协议,传输层协议,应用层协议全都解析好了。

    由于咱们小码农一般都忙于解决应用层问题,所以我们把目光聚焦于高亮的 Hypertext Transfer Protocol 超文本传输协议,也就是大名鼎鼎的 HTTP 协议。

    首先我们查看一下 HTTP 报文的完整内容:


    可以看到,http 协议大概是这么组成的:

    • 第一行是请求的方式,比如 GET / POST / DELETE / PUT

    • 请求方式后面跟的是请求的路径,一般把这个叫 URI(统一资源标识符)

    补充:URL 是统一资源定位符,见名知义,因为要定位,所以要指定协议甚至是位置,比如这样:http://localhost:5000/api/hello

    • 请求路径后面跟的是 HTTP 的版本,比如这里是 HTTP/1.1

    完整的第一行如下:

    POST /api/hello HTTP/1.1

    第二行的 User-Agent 则用于告诉对方发起请求的客户端是啥,比如咱们用 Postman 发起的请求,Postman 就会自动把这个参数设置为它自己:

    User-Agent: PostmanRuntime/7.28.4

    第三行的 Accept 用于告诉对方我们希望收到什么类型的数据,这里默认是能接受所有类型的数据:

    Accept: */*

    第四行就非常值得留意,Postman-Token 是 Postman 自己传的参数,这个我们放到下面讲!

    Postman-Token: ddd72e1a-0d63-4bad-a18e-22e38a5de3fc

    第五行是请求的主机,网络上的一个服务一般用 ip 加端口作为唯一标识:

    Host: 127.0.0.1:5000

    第六行指定的是咱们请求发起方可以理解的压缩方式:

    Accept-Encoding: gzip, deflate, br

    第七行告诉对方处理完当前请求后不要关闭连接:

    Connection: keep-alive

    第八行告诉对方咱们请求体的内容格式,这个是本文的侧重点啦!比如我们这里指定的是一般浏览器的原生表单格式:

    Content-Type: application/x-www-form-urlencoded

    好了,下面大家要留意了,第九行的 Content-Length 给出的是请求体的大小。

    而请求体,会放在紧跟着的一个空行之后。比如本请求的请求体内容是以 key=value 形式填充的,也就是我们表单参数的内容了:

    Content-Length: 23

    name=%E9%98%BF%E8%8F%8C

    看到这里我们先简单小结一下,想要告诉服务器我们发送的是表单数据,一共需要两步:

    1. Content-Type 设置为 application/x-www-form-urlencoded

    2. 在请求体中按照 key=value 的形式填写请求参数

    什么是协议?进一步了解 http

    好了,接下来我们进一步讲解,大家试想一下,网络应用,其实就是端到端的交互,最常见的就是服务端和客户端交互模型:客户端发一些参数数据给服务端,通过这些参数数据告诉服务端它想得到什么或想干什么,服务端根据客户端传递的参数数据作出处理。

    传输层协议通过 ip 和端口号帮我们定位到了具体的服务应用,具体怎么交互是由我们程序员自己定义的。

    大概在 30 年前,英国计算机科学家蒂姆·伯纳斯-李定义了原始超级文本传输协议(HTTP),后续我们的 web 应用大都延续采用了他定义的这套标准,当然这套标准也在不断地进行迭代。

    许多文献资料会把 http 协议描述得比较晦涩,加上协议这个词听起来有点高大上,初学者入门学习的时候往往感觉不太友好。

    其实协议说白了就是一种格式,就好比我们写书信,约定要先顶格写个敬爱的 xxx,然后写个你好,然后换一个段落再写正文,可能最后还得加上日期署名等等。

    我们只要按照格式写信,老师就能一眼看出来我们在写信;只要我们按协议格式发请求数据,服务器就能一眼看出来我们想要得到什么或想干什么。

    当然,老师是因为老早就学过书信格式,所以他才能看懂书信格式;服务端程序也一样,我们要预先编写好 http 协议的解析逻辑,然后我们的服务器才能根据解析逻辑去获取一个 http 请求中的各种东西。

    当然这个解析 http 协议的逻辑不是谁都能写出来的,就算能写出来,也未必写得好,所以我们会使用厉害的人封装好的脚手架,比如 java 里的 spring 全套、Go 语言里的 Gin 等等。

    回到我们开头给出的示例:

    from flask import Flask
    from flask import request

    app = Flask(__name__)

    # 云你好服务 API 接口
    @app.get("/api/hello")
    def hello():
       # 看用户是否传递了参数 name
       name = request.args.get("name", "")
       # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
       return f"Hello {name}" if name else "Hello World"

    # 启动云你好服务
    if __name__ == '__main__':
       app.run()

    阿菌的示例使用了 python 里的 flask 框架,在处理逻辑中使用了 request.args 获取请求参数,而 args 封装的就是框架从 url 中获取参数的逻辑。比如我们发送请求的 url 为:

    http://127.0.0.1:5000/api/hello?name=ajun

    框架会帮助我们从 url 中的 ? 后面开始截取,然后把 name=ajun 这些参数存放到 args 里。

    切换一下,假设我们是云你好服务提供者,我们希望用户通过表单参数的形式使用云你好服务,我们只要把获取 name 参数的方式改成从表单参数里获取就可以了,flask 在 request.form 里封装了表单参数(关于框架是怎么在数行 http 请求中封装参数的,大家可以看自己使用的框架的具体逻辑,估计区别不大,只是存在一些语言特性上的差异):

    @app.post("/api/hello")
    def hello():
       # 看用户是否传递了参数 name
       name = request.form.get("name", "")
       # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
       return f"Hello {name}" if name else "Hello World"

    思考:我们可以在 http 协议中传递什么参数?

    最后,我们解释本文的标题,其实想要明白各种参数之间的区别,我们可以换一个角度思考:

    咱们可以在一份 http 报文的哪些位置传递参数?

    接下来回顾一下一个 http 请求的内容:

    POST /api/hello HTTP/1.1
    User-Agent: PostmanRuntime/7.28.4
    Accept: */*
    Postman-Token: fbf75035-a647-46dc-adc0-333751a9399e
    Host: 127.0.0.1:5000
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 23

    name=%E9%98%BF%E8%8F%8C

    大家看,咱们的 http 报文,也就是基于传输层之上的应用层报文,大概就长上面这样。

    我们考虑两种情况,第一种情况,我们基于别人已经开发好的脚手架开发 http 服务器。

    由于框架会基于 http 协议进行解析,所以框架会帮助我们解析好请求 url,各种 Header 头(比如:Cookie 等),以及具体的响应内容都帮我们封装解析好了(比如按照 key=value 的方式去读取请求体)。

    那当我们开发服务端的时候,就可以指定从 url、header、响应体中获取参数了,比如:

    • url 参数:指的就是 url 中 ? 后面携带的 key value 形式参数

    • header 参数:指的就是各个 header 头,我们甚至可以自定义 header,比如 Postman-Token 就是 postman 这个软件自己携带的,我们服务端如果需要的话是可以指定获取这个参数的

    • Cookie 参数:其实就是名字为 Cookie 的请求头

    • 表单参数:指的就是 Content-Type 为 application/x-www-form-urlencoded 下请求体的内容,如果我们的表单需要传文件,还会有其他的 Content-Type

    • json 参数:指的就是 Content-Type 为 application/json 下请求体的内容(当然服务端可以不根据 Content-Type 直接解析请求体,但按照协议的规范工程项目或许会更好维护)

    综上所述,请求参数就是对上面各种类型的参数的一个总称了。

    大家会发现,不管什么 url 参数、header 参数、Cookie 参数、表单参数,其实就是换着法儿,按照一定的格式把数据放到应用层报文中。关键在于我们的服务端程序和客户端程序按照一种什么样的约定去传递和获取这些参数。这就是协议吧~

    还有另一种情况,当然这只是开玩笑了,比如以后哪位大佬或者哪家企业定义了一种新的数据传输标准,推广至全球,比如叫 hppt 协议,这样是完全可以自己给各种形式参数下定义取名字的。这可能就是为啥我们说一流的企业、大佬制定标准,接下来的围绕标准研发技术,进而是基于技术卖产品,最后是围绕产品提供服务了。

    一旦标准制定了,整个行业都围绕这个标准转了,而且感觉影响会越来越深远......

    讲解参考链接

    作者:胡涂阿菌
    来源:juejin.cn/post/7100400494081736711

    收起阅读 »

    JavaScript中的事件委托

    事件委托基本概念事件委托,就是一个元素的响应事件的函数委托给另一个元素一般我们都是把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数在了解事件委托之前,我...
    继续阅读 »

    事件委托基本概念

    事件委托,就是一个元素的响应事件的函数委托给另一个元素

    一般我们都是把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数

    在了解事件委托之前,我们可以先了解事件流,事件冒泡以及事件捕获

    事件流:捕获阶段,目标阶段,冒泡阶段

    DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段;

    三个阶段的顺序为:捕获阶段——目标阶段——冒泡阶段

    事件冒泡

    事件的触发响应会从最底层目标一层层地向外到最外层(根节点)

    比如说我现在有一个盒子f,里面有个子元素s

      <div class="f">
           <div class="s"></div>
     </div>

    添加事件

    var f = document.querySelector('.f')
    var s = document.querySelector('.s')
    f.addEventListener('click',()=>{
       console.log('fffff');
    })
    s.addEventListener('click',()=>{
       console.log('sssss');
    })

    当我点击子元素的时候

    冒泡顺序 s -> f

    事件捕获

    事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件

    继续使用上一个例子,只需要将addEventListener第三个参数改为true即可

    添加事件

    var f = document.querySelector('.f')
    var s = document.querySelector('.s')
    f.addEventListener('click',()=>{
       console.log('fffff');
    },true)
    s.addEventListener('click',()=>{
       console.log('sssss');
    },true)

    点击子元素

    捕获顺序 f -> s

    这里我们可以思考一下,如果同时绑定了冒泡和捕获事件的话,会有怎样的执行顺序呢?

    例子不变,稍微改一下js代码

    var f = document.querySelector('.f')
    var s = document.querySelector('.s')
    f.addEventListener('click',()=>{
       console.log('f捕获');
    },true)
    s.addEventListener('click',()=>{
       console.log('s捕获');
    },true)
    f.addEventListener('click',()=>{
       console.log('f冒泡');
    })
    s.addEventListener('click',()=>{
       console.log('s冒泡');
    })

    此时点击子元素

    执行顺序: f捕获->s捕获->s冒泡—>f冒泡

    得出结论:当我们同时绑定捕获和冒泡事件的时候,会先从外层开始捕获到目标元素,然后由目标元素冒泡到外层

    回到事件委托

    了解了事件捕获和事件冒泡,再来看事件委托就很好理解了

    强调一遍,事件委托把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数

    新开一个例子

    <ul class="list">
           <li class="item"></li>
           <li class="item"></li>
           <li class="item"></li>
           <li class="item"></li>
           <li class="item"></li>
    </ul>

    现在我们有一个列表,当我们点击列表中的某一项时可以触发对应事件,如果我们给列表的每一项都添加事件,对于内存消耗是非常大的,效率上需要消耗很多性能

    这个时候我们就可以把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件的时候再去匹配判断目标元素;

    var list = document.querySelector('.list')
    // 利用冒泡机制实现
    list.addEventListener('click',(e)=>{
      e.target.style.backgroundColor='blue'
    })
    // 利用捕获机制实现
    list.addEventListener('click',(e)=>{
      e.target.style.backgroundColor='red'
    },true)

    当我点击其中一个子元素的时候


    总结

    • 事件委托就是根据事件冒泡或事件捕获的机制来实现的

    • 事件冒泡就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点)

    • 事件捕获就是事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件

    补充:

    对于目标元素,捕获和冒泡的执行顺序是由绑定事件的执行顺序决定的

    作者:张宏都
    来源:https://juejin.cn/post/7100468737647575048

    收起阅读 »

    axios 请求拦截器&响应拦截器

    一、 拦截器介绍一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。请求拦截器 在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;响应拦截器 同理,响应拦截...
    继续阅读 »

    一、 拦截器介绍

    一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。

    1. 请求拦截器
      在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;

    2. 响应拦截器
      同理,响应拦截器也是如此功能,只是在请求得到响应之后,对响应体的一些处理,通常是数据统一处理等,也常来判断登录失效等。

    二、 Axios实例

    1. 创建axios实例

    // 引入axios
    import axios from 'axios'

    // 创建实例
    let instance = axios.create({
       baseURL: 'xxxxxxxxxx',
       timeout: 15000  // 毫秒
    })
    1. baseURL设置:

    let baseURL;
    if(process.env.NODE_ENV === 'development') {
       baseURL = 'xxx本地环境xxx';
    } else if(process.env.NODE_ENV === 'production') {
       baseURL = 'xxx生产环境xxx';
    }

    // 实例
    let instance = axios.create({
       baseURL: baseURL,
       ...
    })
    1. 修改实例配置的三种方式

    // 第一种:局限性比较大
    axios.defaults.timeout = 1000;
    axios.defaults.baseURL = 'xxxxx';

    // 第二种:实例配置
    let instance = axios.create({
       baseURL: 'xxxxx',
       timeout: 1000,  // 超时,401
    })
    // 创建完后修改
    instance.defaults.timeout = 3000

    // 第三种:发起请求时修改配置、
    instance.get('/xxx',{
       timeout: 5000
    })

    这三种修改配置方法的优先级如下:请求配置 > 实例配置 > 全局配置

    三、 配置拦截器

    // 请求拦截器
    instance.interceptors.request.use(req=>{}, err=>{});
    // 响应拦截器
    instance.interceptors.reponse.use(req=>{}, err=>{});
    1. 请求拦截器

    // use(两个参数)
    axios.interceptors.request.use(req => {
       // 在发送请求前要做的事儿
       ...
       return req
    }, err => {
       // 在请求错误时要做的事儿
       ...
       // 该返回的数据则是axios.catch(err)中接收的数据
       return Promise.reject(err)
    })
    1. 响应拦截器

    // use(两个参数)
    axios.interceptors.reponse.use(res => {
       // 请求成功对响应数据做处理
       ...
       // 该返回的数据则是axios.then(res)中接收的数据
       return res
    }, err => {
       // 在请求错误时要做的事儿
       ...
       // 该返回的数据则是axios.catch(err)中接收的数据
       return Promise.reject(err)
    })
    1. 常见错误码处理(error)
      axios请求错误时,可在catch里进行错误处理。

    axios.get().then().catch(err => {
       // 错误处理
    })

    四、 axios请求拦截器的案例

    // 设置请求拦截器
    axios.interceptors.request.use(
     config => {
       // console.log(config) // 该处可以将config打印出来看一下,该部分将发送给后端(server端)
       config.headers.Authorization = store.state.token
       return config // 对config处理完后返回,下一步将向后端发送请求
    },
     error => { // 当发生错误时,执行该部分代码
       // console.log(error) // 调试用
       return Promise.reject(error)
    }
    )

    // 定义响应拦截器 -->token值无效时,清空token,并强制跳转登录页
    axios.interceptors.response.use(function (response) {
     // 响应状态码为 2xx 时触发成功的回调,形参中的 response 是“成功的结果”
     return response
    }, function (error) {
     // console.log(error)
     // 响应状态码不是 2xx 时触发失败的回调,形参中的 error 是“失败的结果”
     if (error.response.status === 401) {
       // 无效的 token
       // 把 Vuex 中的 token 重置为空,并跳转到登录页面
       // 1.清空token
       store.commit('updateToken', '')
       // 2.跳转登录页
       router.push('/login')
    }
     return Promise.reject(error)
    })

    作者:我彦祖不会秃
    来源:https://juejin.cn/post/7100470316857557006

    收起阅读 »

    说说你对事件循环的理解

    一、事件循环是什么首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环在JavaScript中,所有的任务都可以分为同步任务:立即执行的任务,同步任务一般会直接进入到主...
    继续阅读 »

    一、事件循环是什么

    首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环

    在JavaScript中,所有的任务都可以分为

    • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行

    • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

    同步任务与异步任务的运行流程图如下:


    从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

    二、宏任务与微任务

    如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:

    console.log(1)

    setTimeout(()=>{
      console.log(2)
    }, 0)

    new Promise((resolve, reject)=>{
      console.log('new Promise')
      resolve()
    }).then(()=>{
      console.log('then')
    })

    console.log(3)

    最终结果: 1=>'new Promise'=> 3 => 'then' => 2

    微任务

    一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

    常见的微任务有:

    • Promise.then

    • MutaionObserver

    • Object.observe(已废弃;Proxy 对象替代)

    • process.nextTick(Node.js)

    宏任务

    宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

    常见的宏任务有:

    • script (可以理解为外层同步代码)

    • setTimeout/setInterval

    • UI rendering/UI事件

    • postMessage、MessageChannel

    • setImmediate、I/O(Node.js)

    这时候,事件循环,宏任务,微任务的关系如图所示


    • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中

    • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

    回到上面的题目

    console.log(1)
    setTimeout(()=>{
      console.log(2)
    }, 0)
    new Promise((resolve, reject)=>{
      console.log('new Promise')
      resolve()
    }).then(()=>{
      console.log('then')
    })
    console.log(3)

    最终结果: 1=>'new Promise'=> 3 => 'then' => 2

    // 遇到 console.log(1) ,直接打印 1
    // 遇到定时器,属于新的宏任务,留着后面执行
    // 遇到 new Promise,这个是直接执行的,打印 'new Promise'
    // .then 属于微任务,放入微任务队列,后面再执行
    // 遇到 console.log(3) 直接打印 3
    // 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
    // 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

    三、async与await

    async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行

    async

    async函数返回一个promise对象,下面两种方法是等效的

    function f() {
      return Promise.resolve('TEST');
    }

    // asyncF is equivalent to f!
    async function asyncF() {
      return 'TEST';
    }

    await

    正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值

    async function f(){
      // 等同于
      // return 123
      return await 123
    }
    f().then(v => console.log(v)) // 123

    不管await后面跟着的是什么,await都会阻塞后面的代码

    async function fn1 (){
      console.log(1)
      await fn2()
      console.log(2) // 阻塞
    }

    async function fn2 (){
      console.log('fn2')
    }

    fn1()
    console.log(3)

    上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

    所以上述输出结果为:1,fn2,3,2

    四、流程分析

    通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解

    这里直接上代码:

    async function async1() {
      console.log('async1 start')
      await async2()
      console.log('async1 end')
    }
    async function async2() {
      console.log('async2')
    }
    console.log('script start')
    setTimeout(function () {
      console.log('settimeout')
    })
    async1()
    new Promise(function (resolve) {
      console.log('promise1')
      resolve()
    }).then(function () {
      console.log('promise2')
    })
    console.log('script end')

    分析过程:

    执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
    遇到定时器了,它是宏任务,先放着不执行
    遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
    跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
    最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
    继续执行下一个微任务,即执行 then 的回调,打印 promise2
    上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout
    所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout

    作者:用户8249803991033
    来源:https://juejin.cn/post/7100468871752056868

    收起阅读 »

    如何美化你的图表,关于SVG渐变你需要了解的一切!

    渐变在网页设计中几乎随处可见,渐变的背景、文字、按钮、图表等等,相比于纯色,渐变的颜色显得更加灵动自然。今天我们要探讨的,就是SVG中的渐变绘制。更多SVG系列文章:SVG基础知识、SVG动画、SVG中的Transform变换。概述或许你有使用css绘制渐变图...
    继续阅读 »

    渐变在网页设计中几乎随处可见,渐变的背景、文字、按钮、图表等等,相比于纯色,渐变的颜色显得更加灵动自然。

    今天我们要探讨的,就是SVG中的渐变绘制。

    更多SVG系列文章:SVG基础知识、SVG动画、SVG中的Transform变换。

    概述

    或许你有使用css绘制渐变图形的经验,如果要绘制一个渐变的矩形,我们可以这样写:

    <div></div>

    .bg{
      height: 100px;
      width: 200px;
      //给元素设置渐变背景
      background: linear-gradient(#fb3,#58a);
    }

    使用SVG绘图,颜色是通过设置元素的fill(填充颜色)和stroke(边框颜色)属性来实现。

    <rect height="100" width="150" stroke="#45B649" stroke-width="2" fill="#DCE35B"></rect>


    对于渐变颜色的设置,我们不能像在css中那样,直接写fill="linear-gradient(color1, color2)",而要使用专门的渐变标签:<linearGradient>(线性渐变) 和 <radialGradient>(径向渐变)。

    线性渐变

    基础使用

    先来看一个最简单的例子,如何绘制一个线性渐变的矩形:

    <svg>
      <defs>
          <linearGradient id="gradient-test">
              <stop offset="0%" stop-color="#DCE35B" />
              <stop offset="100%" stop-color="#45B649" />
          </linearGradient>
      </defs>
      <rect height="100" width="150" fill="url(#gradient-test)"></rect>
    </svg>


    通常,我们将渐变标签<linearGradient>定义在<defs>元素中,<linearGradient>id属性作为其唯一标识,方便后面需要使用的地方对其进行引用。

    <linearGradient>中的<stop>标签定义渐变色的色标,它的offsetstop-color属性分别定义色标的位置和颜色值,它还有一个属性stop-opacity,设定stop-color颜色的透明度。

    如果将色标的位置拉近:

    <linearGradient id="gradient-1">
      <stop offset="30%" stop-color="#DCE35B" />
      <stop offset="70%" stop-color="#45B649" />
    </linearGradient>


    矩形左边的 30% 区域被填充为 #DCE35B 实色,而右边 30% 区域被填充为 #45B649 实色。真正的渐变只出现在矩形中间 40% 的区域。

    如果两个颜色都设为50%,就得到了两块均分矩形的实色。在这基础上,我们可以生成各种颜色的条纹图案。


    渐变的方向和范围

    在没有设置渐变方向的时候,渐变的默认方向是从左向右。

    如果要设定渐变方向,要用到<linearGradient>x1,y1,x2,y2这几个属性。

    <linearGradient id="gradient-1" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#DCE35B" />
      <stop offset="100%" stop-color="#45B649" />
    </linearGradient>

    我们知道,在平面上,方向一般由向量来表示。而渐变的方向由(x1,y1)(起点)和(x2,y2)(点)两个点定义的向量来表示。

    在一般的应用场景中,x1,y1,x2,y2的取值范围是[0,1](或者用百分数[0%, 100%])。

    对于矩形而言,不管矩形的长宽比例是多少,它的左上角对应的都是(0,0),右下角则对应(1,1)


    x1="0" y1="0" x2="0" y2="1"表示从(0,0)(0,1),即渐变方向从矩形上边框垂直向下到下边框。

    x1="0" y1="0.3" x2="0" y2="0.7"的情形如下:


    可以看出,x1,y1,x2,y2不仅决定渐变的方向,还决定了渐变的范围,超出渐变范围的部分由起始或结束色标的颜色进行纯色填充。

    案例1:渐变文字


    <svg width="600" height="270">
      <defs>
          <linearGradient id="background">                         <!--背景渐变色-->
              <stop offset="0%" stop-color="#232526" />
              <stop offset="100%" stop-color="#414345" />
          </linearGradient>
          <linearGradient id="text-color" x1="0" y1="0" x2="0" y2="100%"> <!--文字渐变色-->
              <stop offset="0%" stop-color="#DCE35B" />
              <stop offset="100%" stop-color="#45B649" />
          </linearGradient>
      </defs>
      <rect x="0" y="0" height="100%" width="100%" fill="url(#background)"></rect>
      <text y="28%" x="28%">试问闲情都几许?</text>
      <text y="44%" x="28%">一川烟草</text>
      <text y="60%" x="28%">满城风絮</text>
      <text y="76%" x="28%">梅子黄时雨</text>
    </svg>
    <style>
      text{
          font-size: 32px;
          letter-spacing:5px;
          fill:url(#text-color);     //文字的填充使用渐变色
      }
    </style>

    文字的填充,我们用了垂直方向的渐变色,对于每一行文字,都是从黄色渐变到绿色。

    如果要将这几行文字作为一个整体来设置渐变色,像下面这样,应该怎样设置呢?


    这就要用到gradientUnits属性了。

    gradientUnits属性定义渐变元素(<linearGradient><radialGradient>)要参考的坐标系。 它有两个取值:objectBoundingBoxuserSpaceOnUse

    默认值是objectBoundingBox,它定义渐变元素的参考坐标系为引用该渐变的SVG元素,渐变的起止、范围、方向都是基于引用该渐变的SVG元素(之前的<rect>,这里的<text>)自身,比如这里的每一个<text>元素的左上角都是渐变色的(0,0)位置,右下角都是(100%,100%)

    userSpaceOnUse则以当前的SVG元素视窗区域(viewport) 为渐变元素的参考坐标系。也就是SVG元素的左上角为渐变色的(0,0)位置,右下角为(100%,100%)

    <svg height="200" width="300"> 
      <defs>
          <!-- 定义两个渐变,除了gradientUnits,其他配置完全相同 -->
          <linearGradient id="gradient-1" x1="0" y1="0" x2="100%" y2="100%" gradientUnits="objectBoundingBox">
              <stop offset="0%" stop-color="#C6FFDD" />
              <stop offset="100%" stop-color="#f7797d" />
          </linearGradient>
          <linearGradient id="gradient-2" x1="0" y1="0" x2="100%" y2="100%" gradientUnits="userSpaceOnUse">
              <stop offset="0%" stop-color="#C6FFDD" />
              <stop offset="100%" stop-color="#f7797d" />
          </linearGradient>
      </defs>
      <rect x="0" y="0" ></rect>
      <rect x="150" y="0" ></rect>
      <rect x="0" y="100" ></rect>
      <rect x="150" y="100" ></rect>
    </svg>
    rect{
      height: 100px;
      width: 150px;
      fill: url(#gradient-1); //四个矩形都填充渐变色,下面左图为gradient-1,右图为gradient-2。
    }


    gradientUnits:userSpaceOnUse 适用于画布中有多个图形,但每个图形都是整体渐变中的一部分这样的场景。值得注意的是,当gradientUnits="userSpaceOnUse"时,x1,y1,x2,y2的取值只有用%百分数这样的相对单位才表示比例,如果取值为x2="1",那就真的是1px,这一点与gradientUnits="objectBoundingBox"是不同的。

    案例2:渐变的环形进度条

    上一篇文章中,我们实现了可交互的环形进度条:


    这里我们将其改造成渐变的环形进度条。


    使用渐变色作为描边stroke的颜色,中间使用一个白色透明度渐变的圆,增加立体感。

    <!--改动部分的代码-->
    <svg height="240" width="240" viewBox="0 0 100 100">
      <defs>
          <linearGradient id="circle">
              <stop offset="0%" stop-color="#A5FECB" />
              <stop offset="50%" stop-color="#20BDFF" />
              <stop offset="100%" stop-color="#5433FF" />
          </linearGradient>
          <linearGradient id="center">
              <stop offset="0%" stop-color="rgba(255,255,255,0.25)" />
              <stop offset="100%" stop-color="rgba(255,255,255,0.08)" />
            </linearGradient>
        </defs>
        <!--灰色的背景圆环-->
        <circle cx="50" cy="50" r="40" stroke-width="12" stroke="#eee" fill="none"></circle>
        <!--渐变的动态圆环-->
        <circle      
           
            cx="50" cy="50" r="40"
            transform="rotate(-90 50 50)"
            stroke-width="12"
            stroke="url(#circle)"
            fill="none"
            stroke-linecap="round"
            stroke-dasharray="251"></circle>
        <!--白色透明度渐变的圆,增加立体感-->
        <circle cx="50" cy="50" r="40" fill="url(#center)"></circle>
    </svg>

    径向渐变

    基础使用

    径向渐变是色彩从中心点向四周辐射的渐变。


    <svg height="300" width="200">
    <defs>
    <radialGradient id="test">
    <stop offset="0%" stop-color="#e1eec3" />
    <stop offset="100%" stop-color="#f05053" />
    </radialGradient>
    </defs>
    <rect fill="url(#test)" x="10" y="10" width="150" height="150"></rect>
    </svg>

    和线性渐变的结构类似,我们将径向渐变标签<radialGradient>定义在<defs>元素中,其id属性作为其唯一标识,以便后面需要使用的地方对其进行引用。

    <radialGradient>中的<stop>标签定义渐变色的色标,它的offsetstop-color属性分别定义色标的位置和颜色值。

    渐变的范围

    径向渐变的范围由<radialGradient>cx,cy,r三个属性共同决定,它们的默认值均是50%,是相对值,相对的是引用该渐变的SVG元素

    cxcy定义径向渐变范围的圆心,(50%, 50%)意味着是引用该渐变的SVG元素的中心。r设定渐变范围的半径,当r=50%时,说明渐变范围的半径在xy方向的分别是引用该渐变的SVG元素widthheight的50%。

    //当rect高度减小时,渐变在y方向的半径也减小。
    <rect fill="url(#test)" x="10" y="10" width="150" height="100"></rect>


    cx,cy,r都取默认值的情况下,径向渐变的范围刚好覆盖引用该渐变的SVG元素。实际开发中,我们常常需要调整渐变范围。


    渐变起点的移动

    在默认情况下,渐变起点都是在渐变范围的中心,如果想要不那么对称的渐变,就需要改变渐变起点的位置。

    <radialGradient>fxfy就是用来设置渐变色起始位置的。fxfy的值也是相对值,相对的也是引用该渐变的SVG元素


    我们可以设定渐变的范围(cx,cy,r),也可以设定渐变的起点位置(fx,fy)。但是如果渐变的起点位置在渐变的范围之外,会出现一些我们不想要的效果。


    测试代码如下,可直接运行:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <style>
          body{
              display: flex;
              justify-content: center;
          }
          .control{
              margin-top:20px;
          }
      </style>
    </head>
    <body>
      <svg height="300" width="200">
          <defs>
              <radialGradient id="test">
                  <stop offset="0%" stop-color="#e1eec3" />
                  <stop offset="100%" stop-color="#f05053" />
              </radialGradient>
          </defs>
          <rect fill="url(#test)" x="10" y="10" width="150" height="150"></rect>
      </svg>
      <div>
          <div>cx:<input value="50" type="range" min="0" max="100" id="cx" /></div>
          <div>cy:<input value="50" type="range" min="0" max="100" id="cy" /></div>
          <div>r:<input value="50" type="range" min="0" max="100" id="r" /></div>
          <div>fx:<input value="50" type="range" min="0" max="100" id="fx" /></div>
          <div>fy:<input value="50" type="range" min="0" max="100" id="fy" /></div>
      </div>
      <script>
          const rg = document.getElementById('test')
          document.querySelectorAll('input').forEach((elem) => {
              elem.addEventListener('change', (ev) => {
                  rg.setAttribute(ev.target.id, ev.target.value+'%')
              })
          })
      </script>
    </body>
    </html>

    综合案例:透明的泡泡

    最后我们用线性渐变和径向渐变画一个泡泡。


    分析:

    • 背景是一个用线性渐变填充的矩形。

    • 泡泡分为三个部分:由径向渐变填充的一个圆形和两个椭圆。

    这里的径向渐变主要是颜色透明度的渐变。设定颜色透明度,我们可以直接指定stop-color的值为rgba,也可以通过stop-opacity来设定stop-color颜色的透明度。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <style>
          .bubble{
              animation: move 5s linear infinite;
              animation-direction:alternate;
          }
          //泡泡的运动
          @keyframes move {
              0%{
                  transform: translate(0,0);
              }
              50%{
                  transform: translate(250px,220px);
              }
              100%{
                  transform: translate(520px,50px);
              }
          }
      </style>
    </head>
    <body>
      <svg height="400" width="700">
          <defs>
              <!--背景的线性渐变-->
              <linearGradient id="background">
                  <stop offset="0%" stop-color="#DCE35B" />
                  <stop offset="100%" stop-color="#45B649" />
              </linearGradient>
              <!--光斑的径向渐变,通过fx、fy设置不对称的渐变-->
              <radialGradient id="spot" fx="50%" fy="30%">
                  <stop offset="10%" stop-color="white" stop-opacity=".7"></stop>  
                  <stop offset="70%" stop-color="white" stop-opacity="0"></stop>
              </radialGradient>
              <!--泡泡本体的径向渐变-->
              <radialGradient id="bubble">
                  <stop offset="0%" stop-color="rgba(255,255,255,0)" ></stop>  
                  <stop offset="80%" stop-color="rgba(255,255,255,0.1)" ></stop>
                  <stop offset="100%" stop-color="rgba(255,255,255,0.42)"></stop>
              </radialGradient>
          </defs>
          <rect fill="url(#background)" width="100%" height="100%"></rect>
          <g>
              <circle cx="100" cy="100" r="70" fill="url(#bubble)"></circle>
              <ellipse rx="50" ry="20" cx="80" cy="60" fill="url(#spot)" transform="rotate(-25, 80, 60)" ></ellipse>
              <ellipse rx="20" ry="10" cx="140" cy="130" fill="url(#spot)" transform="rotate(125, 140, 130)" ></ellipse>
          </g>  
      </svg>
    </body>
    </html>

    以上渐变配色均来自网站: uigradients.com/

    作者:Alaso
    来源:juejin.cn/post/7098637240825282591

    收起阅读 »

    H5如何实现唤起APP

    前言写过hybrid的同学,想必都会遇到这样的需求,如果用户安装了自己的APP,就打开APP或跳转到APP内某个页面,如果没安装则引导用户到对应页面或应用商店下载。这里就涉及到了H5与Native之间的交互,为什么H5能够唤起APP并且跳转到对应的页面?就算你...
    继续阅读 »

    前言

    写过hybrid的同学,想必都会遇到这样的需求,如果用户安装了自己的APP,就打开APP或跳转到APP内某个页面,如果没安装则引导用户到对应页面或应用商店下载。这里就涉及到了H5与Native之间的交互,为什么H5能够唤起APP并且跳转到对应的页面?

    就算你没写过想必也体验过,最常见的就是抖音里面的一些广告,如果你点击了广告,他判断你手机装了对应APP,那他就会去打开那个APP,如果没安装,他会帮你跳转到应用商店去下载,这个还算人性化一点的,有些直接后台给你去下载,你完全无感知。

    哈哈,是不是觉得这种技术很神奇,今天我们就一起来看看它是如何实现的~

    如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~

    唤端体验

    实现之前我们先简单体验一下什么是唤端


    从上图中,我们可以看到在浏览器中我们点击打开知乎,系统会提示我们是否在知乎中打开,当我们点击打开时,知乎就被打开了,这就是一个简单的唤端体验。

    有了这项技术我们就可以实现H5唤起APP应用了,现阶段的引流方式大都得益于这种技术,比如广告投放、用户拉新、引流等。

    唤端技术

    体验过后,我们就来聊一聊它的实现技术是怎样的,唤端技术我们也称之为deep link技术。当然,不同平台的实现方式有些不同,一般常见的有这几种,分别是:

    • URL Scheme(通用)

    • Universal Link (iOS)

    • App Link、Chrome Intents(android)

    URL Scheme(通用)

    这种方式是一种比较通用的技术,各平台的兼容性也很好,它一般由协议名、路径、参数组成。这个一般是由Native开发的同学提供,我们前端同学再拿到这个scheme之后,就可以用来打开APP或APP内的某个页面了。

    URL Scheme 组成

    [scheme:][//authority][path][?query][#fragment]

    常用APP的 URL Scheme

    APP微信支付宝淘宝QQ知乎
    URL Schemeweixin://alipay://taobao://mqq://zhihu://

    打开方式

    常用的有以下这几种方式

    • 直接通过window.location.href跳转

    window.location.href = 'zhihu://'
    • 通过iframe跳转

    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = 'zhihu://'
    document.body.appendChild(iframe)
    • 直接使用a标签进行跳转

    • 通过js bridge来打开

    window.miduBridge.call('openAppByRouter', {url: 'zhihu://'})

    判断是否成功唤起

    当用户唤起APP失败时,我们希望可以引导用户去进行下载。那么我们怎么才能知道当前APP是否成功唤起呢?

    我们可以监听当前页面的visibilitychange事件,如果页面隐藏,则表示唤端成功,否则唤端失败,跳转到应用商店。

    OK,我们尝试来实现一下:

    首先我手机上并没有安装腾讯微博,所以也就无法唤起,我们让他跳到应用商店对应的应用下载页,这里就用淘宝的下载页来代替一下~

    <template>
     <div class="open_app">
         <div class="open_app_title">前端南玖唤端测试Demo</div>
         <div class="open_btn" @click="open">打开腾讯微博</div>
     </div>
    </template>

    <script>
    let timer
    export default {
       name: 'openApp',
       methods: {
           watchVisibility() {
               window.addEventListener('visibilitychange', () => {
                   // 监听页面visibility
                   if(document.hidden) {
                       // 如果页面隐藏了,则表示唤起成功,这时候需要清除下载定时器
                       clearTimeout(timer)
                  }
              })
          },
           open() {
               timer = setTimeout(() => {
                 // 没找到腾讯微博的下载页,这里暂时以淘宝下载页代替
                   window.location.href = 'http://apps.apple.com/cn/app/id387682726'
              }, 3000)
               window.location.href = 'TencentWeibo://'
          }
      }
    }
    </script>

    <style lang="less">
    .open_app_title {
       font-size: (20/@rem);
    }
    .open_btn{
       margin-top:(20/@rem);
       padding:(10/@rem) 0;
       border-radius: (8/@rem);
       background: salmon;
       color: #fff;
       font-size: (16/@rem);
    }
    </style>


    适用性

    URL Scheme 这种方式兼容性好,无论安卓或者 iOS 都能支持,是目前最常用的方式。从上图我们能够看出它也有一些比较明显的缺点:

    • 无法准确判断是否唤起成功,因为本质上这种方式就是打开一个链接,并且还不是普通的 http 链接,所以如果用户没有安装对应的 APP,那么尝试跳转后在浏览器中会没有任何反应,通过定时器来引导用户跳到应用商店,但这个定时器的时间又没有准确值,不同手机的唤端时间也不同,我们只能大概的估计一下它的时间来实现,一般设为3000ms左右比较合适;

    • 从上图中我们可以看到会有一个弹窗提示你是否在对应 APP中打开,这就可能会导致用户流失;

    • 有 URL Scheme 劫持风险,比如有一个 app 也向系统注册了 zhihu:// 这个 scheme ,唤起流量可能就会被劫持到这个 app 里;

    • 容易被屏蔽,app 很轻松就可以拦截掉通过 URL Scheme 发起的跳转,比如微信内经常能看到一些被屏蔽的现象。

    Universal Link (iOS)

    Universal Link 是在iOS 9中新增的功能,使用它可以直接通过https协议的链接来打开 APP。 它相比前一种URL Scheme的优点在于它是使用https协议,所以如果没有唤端成功,那么就会直接打开这个网页,不再需要判断是否唤起成功了。并且使用 Universal Link,不会再弹出是否打开的弹出,对用户来说,唤端的效率更高了。

    原理

    • 在 APP 中注册自己要支持的域名;

    • 在自己域名的根目录下配置一个 apple-app-site-association 文件即可。(具体的配置前端同学不用关注,只需与iOS同学确认好支持的域名即可)

    打开方式

    openByUniversal () {
     // 打开知乎问题页
     window.location.href = 'https://oia.zhihu.com/questions/64966868'
     // oia.zhihu.com
    },


    适用性

    • 相对 URL Scheme,universal links 有一个较大优点是它唤端时没有弹窗提示是否打开,提升用户体验,可以减少一部分用户流失;

    • 无需关心用户是否安装对应的APP,对于没有安装的用户,点击链接就会直接打开对应的页面,因为它也是http协议的路径,这样也能一定程度解决 URL Scheme 无法准确判断唤端失败的问题;

    • 只能够在iOS上使用

    • 只能由用户主动触发

    App Link、Chrome Intents(Android)

    App Link

    在2015年的Google I/O大会上,Android M宣布了一个新特性:App Links让用户在点击一个普通web链接的时候可以打开指定APP的指定页面,前提是这个APP已经安装并且经过了验证,否则会显示一个打开确认选项的弹出框,只支持Android M以上系统。

    App Links的最大的作用,就是可以避免从页面唤醒App时出现的选择浏览器选项框;

    前提是必须注册相应的Scheme,就可以实现直接打开关联的App。

    • App links在国内的支持还不够,部分安卓浏览器并不支持跳转至App,而是直接在浏览器上打开对应页面。

    • 系统询问是否打开对应App时,假如用户选择“取消”并且选中了“记住此操作”,那么用户以后就无法再跳转App。

    Chrome Intents

    • Chrome Intent 是 Android 设备上 Chrome 浏览器中 URI 方案的深层链接替代品。

    • 如果 APP 已安装,则通过配置的 URI SCHEME 打开 APP。

    • 如果 APP 未安装,配置了 fallback url 的跳转 fallback url,没有配置的则跳转应用市场。

    这两种方案在国内的应用都比较少。

    方案对比

    URL SchemeUniversal LinkApp Link
    <ios9支持不支持不支持
    >=ios9支持支持不支持
    <android6支持不支持不支持
    >=android6支持不支持支持
    是否需要HTTPS不需要需要需要
    是否需要客户端需要需要需要
    无对应APP时的现象报错/无反应跳到对应的页面跳到对应的页面

    URI Scheme

    • URI Scheme的兼容性是最高,但使用体验相对较差:

    • 当要被唤起的APP没有安装时,这个链接就会出错,页面无反应。

    • 当注册有多个scheme相同的时候,没有办法区分。

    • 不支持从其他app中的UIWebView中跳转到目标APP, 所以ios和android都出现了自己的独有解决方案。

    Universal Link

    • 已经安装APP,直接唤起APP;APP没有安装,就会跳去对应的web link。

    • universal Link 是从服务器上查询是哪个APP需要被打开,所以不会存在冲突问题

    • universal Link 支持从其他app中的UIWebView中跳转到目标app

    • 缺点在于会记住用户的选择:在用户点击了Universal link之后,iOS会去检测用户最近一次是选择了直接打开app还是打开网站。一旦用户点击了这个选项,他就会通过safiri打开你的网站。并且在之后的操作中,默认一直延续这个选择,除非用户从你的webpage上通过点击Smart App Banner上的OPEN按钮来打开。

    App link

    • 优点与 universal Link 类似

    • 缺点在于国内的支持相对较差,在有的浏览器或者手机ROM中并不能链接至APP,而是在浏览器中打开了对应的链接。

    • 在询问是否用APP打开对应的链接时,如果选择了“取消”并且“记住选择”被勾上,那么下次你再次想链接至APP时就不会有任何反应


    作者:南玖
    来源:https://juejin.cn/post/7097784616961966094

    收起阅读 »

    基于react/vue开发一个专属于程序员的朋友圈应用

    前言今天本来想开源自己写的CMS应用的,但是由于五一期间笔者的mac电脑突然崩溃了,所有数据无法恢复,导致部分代码丢失,但庆幸的是cms的打包文件已上传服务器,感兴趣的朋友可以在文末链接中访问查看。今天要写的H5朋友圈也是基于笔者开发的cms搭建的,我将仿照微...
    继续阅读 »

    前言

    今天本来想开源自己写的CMS应用的,但是由于五一期间笔者的mac电脑突然崩溃了,所有数据无法恢复,导致部分代码丢失,但庆幸的是cms的打包文件已上传服务器,感兴趣的朋友可以在文末链接中访问查看。

    今天要写的H5朋友圈也是基于笔者开发的cms搭建的,我将仿照微信朋友圈,带大家一起开发一个能发布动态(包括图片上传)的朋友圈应用。有关服务端部分笔者在本文中不会细讲,后续会在cms2.0中详细介绍。

    你将收获

    • 使用umi快速创建一个H5移动端应用

    • 基于react-lazy-load实现图片/内容懒加载

    • 使用css3基于图片数量动态改变布局

    • 利用FP创建一个朋友圈form

    • 使用rc-viewer查看/旋转/缩放朋友圈图片

    • 基于axios + formdata实现文件上传功能

    • ZXCMS介绍

    应用效果预览

    朋友圈列表


    查看朋友圈图片


    发布动态


    正文

    在开始文章之前,笔者想先粗略总结一下开发H5移动端应用需要考虑的点。对于任何移动端应用来说,我们都要考虑如下问题:

    • 首屏加载时间

    • 适配问题

    • 页面流畅度

    • 动画性能

    • 交互友好

    • 提供用户反馈 这些不仅仅是前端工程师需要考虑的问题,也是产品经理和交互设计师考虑的范畴。当然还有很多实际的考虑点需要根据自身需求去优化,以上几点大致解决方案如下:

    1. 提高首屏加载时间 可以采用资源懒加载+gzip+静态资源CDN来优化,并且提供加载动画来降低用户焦虑。

    2. 适配问题 移动端适配问题可以通过js动态设置视口宽度/比率或者采用css媒介查询来处理,这块市面上已经有非常成熟的方案

    3. 页面流畅度 我们可以在body上设置-webkit-overflow-scrolling:touch;来提高滚动流畅度,并且可以在a/img标签上使用 -webkit-touch-callout: none来禁止长按产生菜单栏。

    4. 动画性能 为了提高动画性能, 我们可以将需要变化的属性采用transform或者使用absolute定位代替,transform不会导致页面重绘。

    5. 提供用户反馈 提供友好的用户反馈我们可以通过合理设置toastmodal等来控制

    以上介绍的只是移动端优化的凤毛麟角,有关前端页面性能优化的方案还有很多,笔者在之前的文章中也详细介绍过,下面我们进入正文。

    1. 使用umi快速创建一个应用

    笔者将采用umi作为项目的前端集成解决方案,其提供了非常多了功能,使用起来也非常方便,并且对于antd和antd-mobile自动做了按需导入,所以熟悉react的朋友可以尝试一下,本文的方案对于vue选手来说也是适用的,因为任何场景下,方法和思维模式都是跨语言跨框架的。

    目前umi已经升级到3.0,本文所使用的是2.0,不过差异不是很大,大家可以放心使用3.0. 具体使用步骤如下

    // umi2.0
    // 新建项目目录
    mkdir friendcircle
    // 创建umi项目
    cd friendcircle
    yarn create umi
    // 安装依赖
    yarn
    yarn add antd-moblie

    这样一个umi项目就创建好了。

    2. 基于react-lazy-load实现图片/内容懒加载

    在项目创建好之后,我们先分析我们需要用到那些技术点:


    笔者在设计时研究了很多懒加载实现方式,目前采用react-lazy-load来实现,好处是支持加载事件通知,比如我们需要做埋点或者广告上报等功能时非常方便。当然大家也可以自己通过observer API去实现,具体实现方案笔者在几个非常有意思的javascript知识点总结文章中有所介绍。 具体使用方式:

    <LazyLoad key={item.uid} overflow height={280} onContentVisible={onContentVisible}>
      // 需要懒加载的组件
      <ComponentA />
    </LazyLoad>

    react-lazy-load使用方式非常简单,大家不懂的可以在官网学习了解。

    3. 使用css3基于图片数量动态改变布局

    目前在朋友圈列表页有个核心的需求就是我们需要在用户传入不同数量的图片时,要有不同的布局,就像微信朋友圈一样,主要作用就是为了让用户尽可能多的看到图片,提高用户体验,如下图所示例子:


    我们用js实现起来很方便,但是对性能及其不友好,而且对于用户发布的每一条动态的图片都需要用js重新计算一遍,作为一个有追求的程序员是不可能让这种情况发生的,所以我们用css3来实现,其实有关这种实现方式笔者在之前的css3高级技巧的文章中有详细介绍,我们这里用到了子节点选择器,具体实现如下:

    .imgItem {
      margin-right: 6px;
      margin-bottom: 10px;
      &:nth-last-child(1):first-child {
        margin-right: 0;
        width: 100%;
      }
      &:nth-last-child(2):first-child,
      &:nth-last-child(3):first-child,
      &:nth-last-child(4):first-child,
      &:first-child:nth-last-child(n+2) ~ div {
        width:calc(50% - 6px);
        height: 200px;
        overflow: hidden;
      }
      &:first-child:nth-last-child(n+5),
      &:first-child:nth-last-child(n+5) ~ div {
        width: calc(33.33333% - 6px);
        height: 150px;
        overflow: hidden;
      }
    }

    以上代码中我们对于一张图片,2-4张图片,5张以上的图片分别设置了不同的尺寸,这样就可以实现我们的需求了,还有一个要注意的是,当用户上传不同尺寸的图片时,有可能出现高低不一致的情况,这个时候为了显示一致,我们可以使用img样式中的object-fit属性,有点类似于background-size,我们可以把img便签看作一个容器,里面的内容如何填充这个容器,完全用object-fit来设置,具体属性如下:

    • fill 被替换的内容正好填充元素的内容框。整个对象将完全填充此框。如果对象的宽高比与内容框不相匹配,那么该对象将被拉伸以适应内容框

    • contain 被替换的内容将被缩放,以在填充元素的内容框时保持其宽高比。 整个对象在填充盒子的同时保留其长宽比,因此如果宽高比与框的宽高比不匹配,该对象将被添加“黑边”

    • cover 被替换的内容在保持其宽高比的同时填充元素的整个内容框。如果对象的宽高比与内容框不相匹配,该对象将被剪裁以适应内容框

    • scale-down 内容的尺寸与 none 或 contain 中的一个相同,取决于它们两个之间谁得到的对象尺寸会更小一些

    • none 被替换的内容将保持其原有的尺寸

    所以为了让图片保持一致,我们这么设置img标签的样式:

    img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    }

    4. 利用FP创建一个朋友圈form

    FP是笔者开源的一个表单配置平台,主要用来定制和分析各种表单模型,界面如下:



    通过该平台可以定制各种表单模版并分析表单数据。这里朋友圈功能我们只需要配置一个简单的朋友圈发布功能即可,如下:


    由于笔者电脑数据丢失导致代码部分损失,感兴趣可以了解一下。

    5. 使用rc-viewer查看/旋转/缩放朋友圈图片

    对于朋友圈另一个重要的功能就是能查看每一条动态的图片,类似于微信朋友圈的图片查看器,这里笔者采用第三方开源库rc-viewer来实现,具体代码如下:

    <RcViewer options={{title: 0, navbar: 0, toolbar: 0}} ref={imgViewRef}>
    <div className={styles.imgBox}>
      {
        item.imgUrls.map((item, i) => {
          return <div className={styles.imgItem} key={i}>
            <img src={item} alt=""/>
          </div>
        })
      }
    </div>  
    </RcViewer>

    由上代码可知我们只需要在RcViewer组件里写我们需要的查看的图片结构就行了,其提供了很多配置选项可是使用,这里笔者在option中配置了title,navbar,toolbar均为0,意思是不显示这些功能,因为移动端只需要有基本的查看,缩放,切换图片功能即可,尽可能轻量化。效果如下:


    当我们点击动态中的某一张图片时,我们可以看到它的大图,并通过手势进行切换。

    6. 基于axios + formdata实现文件上传功能

    实现文件上传,除了采用antd的upload组件,我们也可以结合http请求库和formdata来实现,为了支持多图上传并保证时机,我们采用async await函数,具体代码如下:

    const onSubmit = async () => {
      // ... something code
      const formData = new FormData()
      for(let i=0; i< files.length; i++) {
        formData.delete('file')
        formData.append('file', files[i].file)
        try{
          const res = await req({
            method: 'post',
            url: '/files/upload/tx',
            data: formData,
            headers: {
                'Content-Type': 'multipart/form-data'
            }
          });
          // ... something co
        }catch(err) {
          Toast.fail('上传失败', 2);
        }
      }

    其中req是笔者基于axios封装的http请求库,支持简单的请求/响应拦截,感兴趣的朋友可以参考笔者源码。

    7. ZXCMS介绍

    ZXCMS是笔者开发的一个商业版CMS,可以快速搭建自己的社区,博客等,并且集成了表单定制平台,配置中心,数据分发中心等功能,后期会扩展H5可视化搭建平台和PC端建站平台,成为一个更加只能强大的开源系统。设计架构如下:


    具体界面如下:

    一个笔者配置的社区平台:


    文章详情页:



    社区支持评论,搜索文章等功能。以下介绍后台管理系统:





    简单介绍一下,后期笔者会专门出文章介绍具体实现方式和源码设计。

    8. 源码地址

    由于笔者电脑数据丢失,只能找到部分源码,所以大家可以参考以下地址:

    开源不易,欢迎支持~


    作者:徐小夕
    来源:https://juejin.cn/post/6844904150417801224

    收起阅读 »

    如何利用performance进行性能优化

    可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。各种配置及说明如图所示: 观察下图的报告页,我们可以将它分为三个主要的部分,分别为概览面板、...
    继续阅读 »

    Performance 可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。

    配置 Performance

    各种配置及说明如图所示:20210430104643828.png


    Performance 不仅可以录制加载阶段的性能数据,还可以录制交互阶段,不过交互阶段的录制需要手动停止录制过程。

    观察下图的报告页,我们可以将它分为三个主要的部分,分别为概览面板、性能指标面板和详情面板。image.png


    在概览面板中,Performance 就会将几个关键指标,诸如页面帧速 (FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量 (堆内存) 等,按照时间顺序做成图表的形式展现出来,可以参看上图。

    • 如果 FPS 图表上出现了红色块,那么就表示红色块附近渲染出一帧所需时间过久,帧的渲染时间过久,就有可能导致页面卡顿。

    • 如果 CPU 图形占用面积太大,表示 CPU 使用率就越高,那么就有可能因为某个 JavaScript 占用太多的主线程时间,从而影响其他任务的执行。

    除了以上指标以外,概览面板还展示加载过程中的几个关键时间节点,如 FP、LCP、DOMContentLoaded、Onload 等事件产生的时间点。

    Main 指标

    在性能面板中,记录了非常多的性能指标项,比如 Main 指标记录渲染主线程的任务执行过程,Compositor 指标记录了合成线程的任务执行过程,GPU 指标记录了 GPU 进程主线程的任务执行过程。有了这些详细的性能数据,就可以帮助我们轻松地定位到页面的性能问题。

    简而言之,我们通过概览面板来定位问题的时间节点,然后再使用性能面板分析该时间节点内的性能数据。具体地讲,比如概览面板中的 FPS 图表中出现了红色块,那么我们点击该红色块,性能面板就定位到该红色块的时间节点内了。

    因为浏览器的渲染机制过于复杂,所以渲染模块在执行渲染的过程中会被划分为很多子阶段,输入的 HTML 数据经过这些子阶段,最后输出屏幕上的像素,我们把这样的一个处理流程叫做渲染流水线。一条完整的渲染流水线包括了解析 HTML 文件生成 DOM、解析 CSS 生成 CSSOM、执行 JavaScript、样式计算、构造布局树、准备绘制列表、光栅化、合成、显示等一系列操作。

    渲染流水线主要是在渲染进程中执行的,在执行渲染流水线的过程中,渲染进程又需要网络进程、浏览器进程、GPU 等进程配合,才能完成如此复杂的任务。另外在渲染进程内部,又有很多线程来相互配合。具体的工作方式你可以参考下图:image.pngimage.png


    观察上图,一段段横条代表执行一个个任务,长度越长,花费的时间越多;竖向代表该任务的执行记录。我们知道主线程上跑了特别多的任务,诸如渲染流水线的大部分流程,JavaScript 执行、V8 的垃圾回收、定时器设置的回调任务等等,因此 Main 指标的内容非常多,而且非常重要,所以我们在使用 Perofrmance 的时候,大部分时间都是在分析 Main 指标。

    任务 vs 过程

    渲染进程中维护了消息队列,如果通过 SetTimeout 设置的回调函数,通过鼠标点击的消息事件,都会以任务的形式添加消息队列中,然后任务调度器会按照一定规则从消息队列中取出合适的任务,并让其在渲染主线程上执行。

    Main 指标就记录渲染主线上所执行的全部任务,以及每个任务的详细执行过程image.png


    观察上图,图上方有很多一段一段灰色横条,每个灰色横条就对应了一个任务,灰色长条的长度对应了任务的执行时长。通常,渲染主线程上的任务都是比较复杂的,如果只单纯记录任务执行的时长,那么依然很难定位问题,因此,还需要将任务执行过程中的一些关键的细节记录下来,这些细节就是任务的过程,灰线下面的横条就是一个个过程,同样这些横条的长度就代表这些过程执行的时长。

    直观地理解,你可以把任务看成是一个 Task 函数,在执行 Task 函数的过程中,它会调用一系列的子函数,这些子函数就是我们所提到的过程。为了让你更好地理解,我们来分析下面这个任务的图形:image.png


    观察上面这个任务记录的图形,你可以把该图形看成是下面 Task 函数的执行过程:  

    function A(){
    A1()
    A2()
    }
    function Task(){
    A()
    B()
    }
    Task()  

    分析页面加载过程

    结合 Main 指标来分析页面的加载过程。先来分析一个简单的页面,代码如下所示:

    <html>
    <head>
    <title>Main</title>
    <style>
    area {
    border: 2px ridge;
    }
    box {
    background-color: rgba(106, 24, 238, 0.26);
    height: 5em;
    margin: 1em;
    width: 5em;
    }
    </style>
    </head>

    <body>
    <div class="area">
    <div class="box rAF"></div>
    </div>
    <br>
    <script>
    function setNewArea() {
    let el = document.createElement('div')
    el.setAttribute('class', 'area')
    el.innerHTML = '<div class="box rAF"></div>'
    document.body.append(el)
    }
    setNewArea()
    </script>
    </body>
    </html>

    可以看出,它只是包含了一段 CSS 样式和一段 JavaScript 内嵌代码,其中在 JavaScript 中还执行了 DOM 操作了,我们就结合这段代码来分析页面的加载流程。

    首先生成报告页,再观察报告页中的 Main 指标,由于阅读实际指标比较费劲,所以先手动绘制了一些关键的任务和其执行过程,如下图所示:image.png


    通过上面的图形我们可以看出,加载过程主要分为三个阶段,它们分别是:

    • 导航阶段,该阶段主要是从网络进程接收 HTML 响应头和 HTML 响应体。

    • 解析 HTML 数据阶段,该阶段主要是将接收到的 HTML 数据转换为 DOM 和 CSSOM。

    • 生成可显示的位图阶段,该阶段主要是利用 DOM 和 CSSOM,经过计算布局、生成层树 (LayerTree)、生成绘制列表 (Paint)、完成合成等操作,生成最终的图片。

    那么接下来,我就按照这三个步骤来介绍如何解读 Main 指标上的数据。

    导航阶段

    当你点击了 Performance 上的重新录制按钮之后,浏览器进程会通知网络进程去请求对应的 URL 资源;一旦网络进程从服务器接收到 URL 的响应头,便立即判断该响应头中的 content-type 字段是否属于 text/html 类型;如果是,那么浏览器进程会让当前的页面执行退出前的清理操作,比如执行 JavaScript 中的 beforunload 事件,清理操作执行结束之后就准备显示新页面了,这包括了解析、布局、合成、显示等一系列操作。image.png


    当你点击重新加载按钮后,当前的页面会执行上图中的这个任务:

    • 该任务的第一个子过程就是 Send request,该过程表示网络请求已被发送。然后该任务进入了等待状态。

    • 接着由网络进程负责下载资源,当接收到响应头的时候,该任务便执行 Receive Respone 过程,该过程表示接收到 HTTP 的响应头了。

    • 接着执行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。

    • 这些事件被处理完成之后,那么接下来就接收 HTML 数据了,这体现在了 Recive Data 过程,Recive Data 过程表示请求的数据已被接收,如果 HTML 数据过多,会存在多个 Receive Data 过程。

    • 等到所有的数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行 Finish load 过程,该过程表示网络请求已经完成。


    解析 HTML 数据阶段

    这个阶段的主要任务就是通过解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOM 和 CSSOM。那么继续来分析这个阶段的图形,看看它到底是怎么执行的?可以观看下图:image.png


    观察上图这个图形,可以看出,其中一个主要的过程是 HTMLParser,顾名思义,这个过程是用来解析 HTML 文件,解析的就是上个阶段接收到的 HTML 数据。

    1. 在 ParserHTML 的过程中,如果解析到了 script 标签,那么便进入了脚本执行过程,也就是图中的 Evalute Script。

    2. 要执行一段脚本我们需要首先编译该脚本,于是在 Evalute Script 过程中,先进入了脚本编译过程,也就是图中的 Complie Script。脚本编译好之后,就进入程序执行过程,执行全局代码时,V8 会先构造一个 anonymous 过程,在执行 anonymous 过程中,会调用 setNewArea 过程,setNewArea 过程中又调用了 createElement,由于之后调用了 document.append 方法,该方法会触发 DOM 内容的修改,所以又强制执行了 ParserHTML 过程生成的新的 DOM。

    3. DOM 生成完成之后,会触发相关的 DOM 事件,比如典型的 DOMContentLoaded,还有 readyStateChanged。

    生成可显示位图阶段

    生成了 DOM 和 CSSOM 之后,就进入了第三个阶段:生成页面上的位图。通常这需要经历布局 (Layout)、分层、绘制、合成等一系列操作,同样,将第三个阶段的流程也放大了,如下图所示:


    image.png


    结合上图,我们可以发现,在生成完了 DOM 和 CSSOM 之后,渲染主线程首先执行了一些 DOM 事件,诸如 readyStateChange、load、pageshow。具体地讲,如果你使用 JavaScript 监听了这些事件,那么这些监听的函数会被渲染主线程依次调用。

    接下来就正式进入显示流程了,大致过程如下所示。

    1. 首先执行布局,这个过程对应图中的 Layout。

    2. 然后更新层树 (LayerTree),这个过程对应图中的 Update LayerTree。

    3. 有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程就称为 Paint。

    4. 准备每层的绘制列表之后,就需要利用绘制列表来生成相应图层的位图了,这个过程对应图中的 Composite Layers。

    走到了 Composite Layers 这步,主线程的任务就完成了,接下来主线程会将合成的任务完全教给合成线程来执行,下面是具体的过程,你也可以对照着 Composite、Raster 和 GPU 这三个指标来分析,参考下图:


    image.png

    1. 首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。

    2. 合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。

    3. 当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。

    4. 然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。

    本文解答了个人一个长期困扰的问题:在某些情况下,比如网速比较慢或者页面内容很多的时候,页面是一点一点的显示出来的,原本以为是网络数据是加载一点就渲染一点,其实不是的,数据在导航阶段就已经全部获取回来了。之所以会慢慢渲染出来,是因为浏览器的显示频率是60hz,也就是16.67ms就刷新下浏览器,但是在16.67ms内,渲染流水线可能只进行到一半,但是这个时候也要把渲染一半的画面显示出来,所以就会看到页面是一点一点的绘制出来的。


    作者:小p
    来源:juejin.cn/post/7095647383488299044
    收起阅读 »

    Today,我们不聊技术,聊聊前端发展

    今天是2022年04月26日,一年已经过去三分之一。 掘金里面有很多的技术文章,每一位前端工程师都在这里展现自己的技术水平。有很多时候,我看见很多的技术文章,里面大致上的内容其实都是差不多的,总的来说,其实普通的前端工程师是用不到去学习这么多的技术点的。就比如...
    继续阅读 »

    今天是2022年04月26日,一年已经过去三分之一。


    掘金里面有很多的技术文章,每一位前端工程师都在这里展现自己的技术水平。有很多时候,我看见很多的技术文章,里面大致上的内容其实都是差不多的,总的来说,其实普通的前端工程师是用不到去学习这么多的技术点的。就比如Node.js 。 一般的公司也不会用JavaScript语言来写后端,所以大部分的前端甚至都不需要去了解它,反而更应该了解多一点Ajax与网络请求协议。数据的问题交给后端去处理就好了,前端有自己要做的活。


    我个人认为,技术框架的源码这种东西,如果能不学习,就不要去深入的学习了。很多人其实是没有达到进大厂的门槛的,大部分的前端其实都达不到,而一些中小型的公司,一般也不会去问一个技术架构的源码及核心问题(绝大部分),因为中小型公司需要的是能干活的人,而大部分的项目业务,其实还没有说你不懂源码就做不了的程度。总的来说就是只要你能干活,你懂什么是你自己的事儿,我就给这么多钱,这些项目你能干就来,你做不了我就辞退你。


    其实大部分的前端,只要有请求到后端的接口,然后能把后端接口的数据处理好,并渲染到页面上就可以了。然后一些不懂的问题,一些复杂的功能模块,其实你一百度,基本上都能解决问题,如果你百度都不能解决的问题,那不是百度解决不了,而是你的项目本身就是有问题的。这里面说的是绝大部分的情况,当然也有一些奇怪的例子,这种只是占少部分。


    其实我们前端的活总体来说都不难,就好比开车,其实绝大部分人都会开车,但是要想要把车技提升上去,那就需要去学习了,如果说你只是为了通勤,那么很多时候,你都不需要去提升你的车技。你只需要懂得怎么启动,怎么刹车等一些基本的操作就行了(实在不行就百度)。


    前端往后的生态


    其实前端往后也不会有什么太大的变化,基本上就定型了。像网上说的什么新技术啊,新方向什么的,其实很多都会不了了之,因为在没有发生技术变革的年代,我们想要去改变一些东西是很难的。我们很多人其实都是需要去等待,等待那个奇点的到来。没有很大的改变,其实都只能这样子。就好比我知道的,在网络请求中,其实有很大部分资源都浪费在了一些协议上,而这些协议的束缚,导致了我们的网络传输会消耗掉三分之一的性能,这种问题是历史遗留问题,虽然现在已经有很多方法能够解决掉这个性能消耗问题,但是解决这个问题需要互联网的企业把旧机器换成新机器,而新机器的成本又高于网络传输消耗的成本,所以我们普通人只能这样去无端的消耗掉这些资源,又或者等待那个奇点的到来。


    说到设备又不得不提现如今的大部分互联网用户,在现在的互联网,其实绝大部分用户的设备性能已经是非常高了,而我们缺还有的人说在项目做一些性能优化问题,其实有时候,这种优化是无意义的,还不如不去做这种优化。当然这种场景也是区分项目的体验人的年龄段,如果项目主要服务于年轻人,其实年轻人的设备性能说不定比我们自己的设备都好,你的优化起不到太大的作用。如果项目主要服务于老年人,其实这个时候需要思考的不是设备性能优化的问题,反而更需要注重项目体验上的问题,就是怎么简单怎么来,别让老人觉得用你的东西太麻烦。


    我所期待的前端世界


    随着电子产品的更新换代,设备的性能越来越好,用户的CUP跑得越来越快,我们可以在我们的前端项目中放更多的新颖东西,比如把项目变革为3D场景,让用户在体验产品时,如同进入一个真实的虚拟世界(希望这一天不会超过50年)web3D值得期待。还有就是网页端游戏,现在绝大部分游戏都是部署在用户的设备中,而每个人的设备存放1个G,那一百个人, 就会有100个G的文件是存在重复,如果一款游戏,能把他部署在服务器上面,而用户只需要进入到网页中就可以体验,那真的是非常令人期待。


    END


    其实这些都是我瞎写的,没有什么值得看的地方,各位看官就当做是一个笑话,如果觉得有意思,麻烦点个赞。谢谢


    作者:无我上青云
    链接:https://juejin.cn/post/7090725867441258503
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    前端单点登录实现

    通过token校验登录信息前端单点存储方式共享本地存储数据token值,token存储方式用的是localStorage 或 sessionStorage,由于这两种都会受到同源策略限制。跨域存储想要实现跨域存储,先找到一种可跨域通信的机制,就是 iframe...
    继续阅读 »

    通过token校验登录信息

    前端单点存储方式

    共享本地存储数据token值,token存储方式用的是localStoragesessionStorage,由于这两种都会受到同源策略限制。

    跨域存储

    想要实现跨域存储,先找到一种可跨域通信的机制,就是 iframe postMessage,它可以安全的实现跨域通信,不受同源策略限制(后端要修改配置允许iframe打开其他域的地址)。

    cross-storage.js(开源库)

    原理是用 postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据


    前端后端通讯

    多平台入口页=》某平台中转页=》平台首页

    平台中转页

    主要将其他平台的token 转成当前平台的信任token值

    /** 单点登录获取票据 */
    export async function getTicket(token) {
    return request('/getTicket', {
      method: 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'x-access-token': token },
    });
    }
    /**免登录 */
    export async function singleLogin(data) {
    return request('/singleLogin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'x-access-token': token   },
      data
    });
    }

    export default (props: any) => {
    const _singleLogin = async () => {
      try {
        //根据本地token 获取票据 getTicket 通过localStorage.getItem("token")
        const { code, data } = await getTicket(token);
        //免登录成功后跳转页面
        const link = '/home';
        if (code !== 200 || !data) {
          window.location.href = link;
          return;
        }
        //免登接口 获取登录token值
        const res: any = await singleLogin({
          ticket: data,
          source: '',//平台来源
        });
        if (res?.code === 200) {
          localStorage.setItem('tokneKey', res?.data.tokneKey);
          localStorage.setItem('tokenValue', res?.data.tokenValue);
        } else {
          console.log(res?.msg);
          localStorage.removeItem('tokneKey');
          localStorage.removeItem('tokenValue');
        }
        window.location.href = link;
      } catch (e) {
        window.location.href = link;
      }
    };
    useEffect(() => {
      _singleLogin();
    });

    return (
      <div style={{ width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', display: 'flex' }}>
        <Spin spinning={loading}></Spin>
      </div>
    );
    };


    作者:NeverSettle_
    来源:https://juejin.cn/post/7021407926837313544

    收起阅读 »

    关于防抖函数的思考

    防抖概念本质:是优化高频率执行代码的一种手段。防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。好处:能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次。一个经典的比喻:想象每天上班大厦底下的电梯。把电梯完成一次运送,类...
    继续阅读 »

    防抖概念

    本质:是优化高频率执行代码的一种手段。

    防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。

    好处:能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次。

    一个经典的比喻:

    想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。

    电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这就是防抖策略(debounce)。

    用于测试的HTML结构

    实现效果:鼠标在盒子上移动时,盒子中央打印出数字。



          //未实现防抖时的测试代码
         const container = document.querySelector('#container')
         let count = 0
         function move(e) {
           container.innerHTML = count++
           console.log(this)
           console.log(e)
        }
         container.addEventListener('mousemove', move)

    未实现防抖时对应的页面效果如下:


         //实现防抖后的测试代码
         const container = document.querySelector('#container')
         let count = 0
         function move(e) {
           container.innerHTML = count++
           console.log(this)
           console.log(e)
        }
         const test = debounce(move, 500, true)
         container.addEventListener('mousemove', test)
         const btn = document.querySelector('button')
         btn.onclick = function () {
           test.cancel()
        }

    实现防抖后对应的页面效果如下:


    接下来记录我一步步思考完善的过程。

    v1.0 简单实现一个防抖(非立即执行版本)

    function debounce(func, delay) {
     let timeout
     return function () {
       if (timeout) clearTimeout(timeout)
       timeout = setTimeout(func, delay)
    }
    }

    问题探讨:发现打印出来的this是window,打印出来的e是undefined。实际想要得到的是div#container和mouseEvent。出现这种情况的原因:在container的鼠标移动事件调用debounce函数时,在传递给形参func的实参move里打印了this与e。注意move是在定时器setTimeout里,定时器里的this在非严格模式下指向的是window对象,而window对象里的e自然是undefined。解决办法是在return的function里保存this与arguments,通过apply改变func的this指向同时把保存的参数传递给func。

    v2.0 解决了this指向和event对象的问题。

    function debounce(func, delay) {
     let timeout
     return function () {
       const context = this,
         args = arguments
       if (timeout) clearTimeout(timeout)
       timeout = setTimeout(function () {
         func.apply(context, args)
      }, delay)
    }
    }

    问题探讨:发现第一次不能立即执行,需要等到delay秒以后才会执行第一次。

    v3.0 立即执行版本

    function debounce(func, delay) {
     let timeout
     return function () {
       const context = this,
         args = arguments,
         callNow = !timeout
       if (timeout) clearTimeout(timeout)
       timeout = setTimeout(function () {
         timeout = null
      }, delay)
       if (callNow) func.apply(context, args)
    }
    }

    Q:为什么利用callNow = !timeout来判断?而不是用callNow = true,然后在定时器内将callNow设置为false?

    首先解答为什么不能用布尔值来判断。因为定时器是异步任务,在delay时间段内,callNow始终为true,这就会导致func在delay时间段内会一直触发,直到时间到达delay,callNow变成false才会停止执行func。

    再回到为什么可以利用callNow = !timeout来判断的问题上。在首次触发mousemove事件时,'let timeout'执行,此时timeout为undefined;callNow对timeout取反为true;因为此时timeout为undefined,跳过清除定时器操作;把定时器赋值给timeout,注意此时timeout保存的值是1(第一个定时器的id),但是定时器是异步任务,里面的'timeout = null'尚未执行;接下来判断callNow为true,执行func函数,达到了立即执行的效果。在delay秒内第二次移动鼠标,此时timeout保存的值为1,callNow取反为false;清除上一个id为1的定时器;timeout保存值2(id为2的定时器),判断callNow为false,不执行func;反之如果等到delay秒后第二次移动鼠标,此时异步任务已执行,timeout变为null,callNow取反为true,就会执行func。注意点:这里利用了闭包,timeout是可以被访问的。

    问题探讨:可以通过传入一个参数来判断实际业务需求是要立即执行还是非立即执行。

    v4.0 立即执行与非立即执行结合版本(immediate为true时立即执行,反之非立即执行)

    function debounce(func, delay, immediate) {
     let timeout
     return function () {
       const context = this,
         args = arguments
       if (timeout) clearTimeout(timeout)
       if (immediate) {
         const callNow = !timeout
         timeout = setTimeout(function () {
           timeout = null
        }, delay)
         if (callNow) func.apply(context, args)
      } else {
         timeout = setTimeout(function () {
           func.apply(context, args)
        }, delay)
      }
    }
    }

    问题探讨:继续完善,如果需要获得func函数的返回值该怎么办呢?那就需要把func的执行结果保存为一个result变量return出来。由此又引出了一个问题,setTimeout是一个异步任务,return时获得的是undefined,只有在立即执行的情况下会获得返回值(immediate为true时)。

    v5.0 包含返回值的版本

    function debounce(func, delay, immediate) {
     let result, timeout
     return function () {
       const context = this,
         args = arguments
       if (timeout) clearTimeout(timeout)
       if (immediate) {
         const callNow = !timeout
         timeout = setTimeout(function () {
           timeout = null
        }, delay)
         if (callNow) result = func.apply(context, args)
      } else {
         timeout = setTimeout(function () {
           func.apply(context, args)
        }, delay)
      }
       return result
    }
    }

    问题探讨:当delay设置时间过长时(比如30秒甚至更长),我只有等到delay时间过后才能再次触发,如果可以把取消防抖绑定在一个按钮上,点击之后可以立即执行代码。需要考虑的问题是:可以把这个功能做成是debounce的一个cancel方法,因为函数也是一个对象。具体实现思路应该是把原先return出来的函数用一个变量debounced保存,然后再定义debounced.cancel,赋值为一个函数。

    v6.0 包含取消功能的版本

    function debounce(func, delay, immediate) {
     let timeout, result
     const debounced = function () {
       const context = this,
         args = arguments
       if (timeout) clearTimeout(timeout)
       if (immediate) {
         const callNow = !timeout
         timeout = setTimeout(function () {
           timeout = null
        }, delay)
         if (callNow) result = func.apply(context, args)
      } else {
         timeout = setTimeout(function () {
           func.apply(context, args)
        }, delay)
      }
       return result
    }
     debounced.cancel = function () {
       if (timeout) clearTimeout(timeout)
       //需要注意,这里的目的并不是为了避免内存泄漏!而是为了让取消后鼠标再次移入盒子能立即执行代码。如果不置空,取消过后再移入,是不会立即执行打印数字的操作的。
       timeout = null
    }
     return debounced
    }


    v7.0 ES6箭头函数版本(省略了this指向与参数对象的版本)

    function debounce(func, delay, immediate) {
     let timeout, result
     //注意下面的函数声明不能改成箭头函数,否则this会指向window
     const debounced = function () {
       if (timeout) clearTimeout(timeout)
       if (immediate) {
         const callNow = !timeout
         timeout = setTimeout(() => {
           timeout = null
        }, delay)
         if (callNow) result = func.apply(this, arguments)
      } else {
         timeout = setTimeout(() => {
           func.apply(this, arguments)
        }, delay)
      }
       return result
    }
     debounced.cancel = () => {
       if (timeout) clearTimeout(timeout)
       timeout = null
    }
     return debounced
    }

    作者:GreyJiangy
    来源:https://juejin.cn/post/7093466427805401118

    收起阅读 »

    仅用了81行代码,实现一个简易打包器

    最近打算跳槽到大厂,webpack打包流程必须了解,于是尝试一下手写一个打包器1. 3个js文件index.js -> 依赖 subtraction.js => 依赖 sum.js2. 5个npm依赖包代码const path = require(...
    继续阅读 »

    前言

    最近打算跳槽到大厂,webpack打包流程必须了解,于是尝试一下手写一个打包器

    准备工作

    1. 3个js文件

    index.js -> 依赖 subtraction.js => 依赖 sum.js


    2. 5个npm依赖包


    代码

    const path = require("path")
    const parser = require("@babel/parser")
    const traverse = require("@babel/traverse").default
    const fs = require("fs")
    const { transformFromAst } = require("babel-core")
    const config = {
       entry: "./src/index.js",
       output: {
           path: "./src/",
           filename: "build.js",
      },
    }
    const { output } = config
    let id = 0
    const createAsset = (entryFile) => {
       // 读取文件
       const source = fs.readFileSync(entryFile, "utf-8")
       // 代码转为ast,为了转换成ES5
       const ast = parser.parse(source, {
           sourceType: "module",
      })
       const dependents = {}
       // 借用traverse提取文件import的依赖
       traverse(ast, {
           ImportDeclaration({ node }) {
               dependents[node.source.value] = node.source.value
          },
      })
       // es6语法转es5
       const { code } = transformFromAst(ast, null, {
           presets: ["env"],
      })
       return {
           entryFile,
           dependents,
           code,
           id: id++,
           mapping: {},
      }
    }
    const createGraph = (rootPath) => {
       // 从根路径出发,获取所有与根路径相关依赖存放到modules中
       const mainAsset = createAsset(rootPath)
       const modules = [mainAsset]
       const dirname = path.dirname(rootPath)
       for (let asset of modules) {
           const { dependents } = asset
           for (let dep in dependents) {
               const childPath = path.join(dirname, dependents[dep])
               const childAsset = createAsset(childPath)
               asset.mapping[dependents[dep]] = childAsset.id
               modules.push(childAsset)
          }
      }
       return modules
    }
    // 转换一下数据结构
    const createModules = (graph) => {
       const obj = {}
       graph.forEach((item) => {
           obj[item.id] = [item.code, item.mapping]
      })
       return obj
    }
    // 生成文件
    const writeFiles = (modules) => {
       // 编译模板,modules是不固定的,其他都一样
       const bundle = `
       ;(function (modules) {
           const require = (id) => {
               const [code, mapping] = modules[id]
               const exports = {}
               ;(function (_require, exports, code, mapping) {
                   const require = (path) => {
                       return _require(mapping[path])
                   }
                   eval(code)
               })(require, exports, code, mapping)
               return exports
           }
           require(0)
       })(${JSON.stringify(modules)})
       `
       // 生成文件
       const filePath = path.join(output.path, output.filename)
       fs.writeFileSync(filePath, bundle, "utf-8")
    }
    const graph = createGraph(config.entry)
    const modules = createModules(graph)
    writeFiles(modules)


    作者:SYX
    来源:juejin.cn/post/7091225169120722952

    收起阅读 »

    一种emoji表情判断方法

    Emoji表情输入 常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则...
    继续阅读 »

    image.png


    Emoji表情输入


    常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则插入时会报错。


    在某些场景下,我们并不希望文本中出现emoji表情等非常用字符,那么如何过滤呢?

    对于字符过滤,一般我们第一个想到的大多是正则表达式。然而,实际使用中,由于emoji表情的不断增加或正则表达式本身的缺陷,往往达不到过滤的效果。


    image.png


    发现问题



    欢迎来到王者荣耀😊😊



    字符数量10,字符串长度12


    一次开发中,使用了el-input的字符数统计属性show-word-limit,发现输入emoji表情统计到的字符数量和实际看到的字符数量不一致。

    然后,尝试通过字符串分割成数组,再比较长度,发现str.split('')得到的数组长度和统计到的字符数是一样的,但是和肉眼看到的字符数量还是不一致。


    var str = '欢迎来到王者荣耀😊😊'
    var arr = str.split('')
    console.log(str.length) // 12
    console.log(arr.length) // 12

    解决问题


    那么,是否可以通过字符串的字符数量和字符串长度来判断是否输入了emoji表情呢?

    要验证这个问题,关键的是获取到字符串中字符的数量。


    那么如何获取字符串中字符的数量呢,通过研究(百度)发现,分割utf8字符串的正确方法是使用 Array.from(str) 而不是str.split('')。


    Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。


    var str = '欢迎来到王者荣耀😊😊'
    var arr2 = Array.from(str)
    console.log(str.length) // 12
    console.log(arr2.length) // 10

    一个大胆的猜想


    emoji表情判断,可以通过字符串长度和字符数量的比较判断是否存在emoji表情,当长度和数量不一致的时候,有emoji表情。


    isEmojiStr(str) { 
    if (typeof (str) === 'string') {
    const arr = Array.from(str);
    if (str.length !== arr.length) {
    return true;
    }
    }
    return false;
    }

    image.png


    参考


    # Emoji Unicode Tables

    # 深入理解Emoji(一) —— 字符集,字符集编码

    # 深入理解Emoji(二) —— 字节序和BOM

    # 深入理解Emoji(三) —— Emoji详解


    作者:前端老兵
    来源:https://juejin.cn/post/7090182766158938120
    收起阅读 »

    复盘前端工程师必知的javascript设计模式

    前言 设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是...
    继续阅读 »

    前言

    设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是开发高质量,高可维护性,可扩展性代码的重要手段.

    我们所熟知的金典的几大框架,比如jquery, react, vue内部也大量应用了设计模式, 比如观察者模式, 代理模式, 单例模式等.所以作为一个架构师,设计模式是必须掌握的.

    在中高级前端工程师的面试的过程中,面试官也会适当考察求职者对设计模式的了解,所以笔者结合多年的工作经验和学习探索, 总结并画出了针对javascript设计模式的思维导图和实际案例,接下来就来让我们一起来探索习吧.

    你将收获

    • 单例模式

    • 构造器模式

    • 建造者模式

    • 代理模式

    • 外观模式

    • 观察者模式

    • 策略模式

    • 迭代器模式

    正文

    我们先来看看总览.设计模式到底可以给我们带来什么呢?


    以上笔者主要总结了几点使用设计模式能给工程带来的好处, 如代码可解耦, 可扩展性,可靠性, 条理性, 可复用性. 接下来来看看我们javascript的第一个设计模式.

    1. 单例模式


    1.1 概念解读

    单例模式: 保证一个类只有一个实例, 一般先判断实例是否存在,如果存在直接返回, 不存在则先创建再返回,这样就可以保证一个类只有一个实例对象.

    1.2 作用

    • 模块间通信

    • 保证某个类的对象的唯一性

    • 防止变量污染

    1.3 注意事项

    • 正确使用this

    • 闭包容易造成内存泄漏,所以要及时清除不需要的变量

    • 创建一个新对象的成本较高

    1.4 实际案例

    单例模式广泛应用于不同程序语言中, 在实际软件应用中应用比较多的比如电脑的任务管理器,回收站, 网站的计数器, 多线程的线程池的设计等.

    1.5 代码实现

    (function(){
    // 养鱼游戏
    let fish = null
    function catchFish() {
      // 如果鱼存在,则直接返回
      if(fish) {
        return fish
      }else {
        // 如果鱼不存在,则获取鱼再返回
        fish = document.querySelector('#cat')
        return {
          fish,
          water: function() {
            let water = this.fish.getAttribute('weight')
            this.fish.setAttribute('weight', ++water)
          }
        }
      }
    }

    // 每隔3小时喂一次水
    setInterval(() => {
      catchFish().water()
    }, 3*60*60*1000)
    })()

    2. 构造器模式


    2.1 概念解读

    构造器模式: 用于创建特定类型的对象,以便实现业务逻辑和功能的可复用.

    2.2 作用

    • 创建特定类型的对象

    • 逻辑和业务的封装

    2.3 注意事项

    • 注意划分好业务逻辑的边界

    • 配合单例实现初始化等工作

    • 构造函数命名规范,第一个字母大写

    • new对象的成本,把公用方法放到原型链上

    2.4 实际案例

    构造器模式我觉得是代码的格局,也是用来考验程序员对业务代码的理解程度.它往往用于实现javascript的工具库,比如lodash等以及javascript框架.

    2.5 代码展示

    function Tools(){
    if(!(this instanceof Tools)){
      return new Tools()
    }
    this.name = 'js工具库'
    // 获取dom的方法
    this.getEl = function(elem) {
      return document.querySelector(elem)
    }
    // 判断是否是数组
    this.isArray = function(arr) {
      return Array.isArray(arr)
    }
    // 其他通用方法...
    }

    3. 建造者模式


    3.1 概念解读

    建造者模式: 将一个复杂的逻辑或者功能通过有条理的分工来一步步实现.

    3.2 作用

    • 分布创建一个复杂的对象或者实现一个复杂的功能

    • 解耦封装过程, 无需关注具体创建的细节

    3.3 注意事项

    • 需要有可靠算法和逻辑的支持

    • 按需暴露一定的接口

    3.4 实际案例

    建造者模式其实在很多领域也有应用,笔者之前也写过很多js插件,大部分都采用了建造者模式, 可以在笔者github地址徐小夕的github学习参考. 其他案例如下:

    • jquery的ajax的封装

    • jquery插件封装

    • react/vue某一具体组件的设计

    3.5 代码展示

    笔者就拿之前使用建造者模式实现的一个案例:Canvas入门实战之用javascript面向对象实现一个图形验证码, 那让我们使用建造者模式实现一个非常常见的验证码插件吧!

    // canvas绘制图形验证码
    (function(){
      function Gcode(el, option) {
          this.el = typeof el === 'string' ? document.querySelector(el) : el;
          this.option = option;
          this.init();
      }
      Gcode.prototype = {
          constructor: Gcode,
          init: function() {
              if(this.el.getContext) {
                  isSupportCanvas = true;
                  var ctx = this.el.getContext('2d'),
                  // 设置画布宽高
                  cw = this.el.width = this.option.width || 200,
                  ch = this.el.height = this.option.height || 40,
                  textLen = this.option.textLen || 4,
                  lineNum = this.option.lineNum || 4;
                  var text = this.randomText(textLen);
       
                  this.onClick(ctx, textLen, lineNum, cw, ch);
                  this.drawLine(ctx, lineNum, cw, ch);
                  this.drawText(ctx, text, ch);
              }
          },
          onClick: function(ctx, textLen, lineNum, cw, ch) {
              var _ = this;
              this.el.addEventListener('click', function(){
                  text = _.randomText(textLen);
                  _.drawLine(ctx, lineNum, cw, ch);
                  _.drawText(ctx, text, ch);
              }, false)
          },
          // 画干扰线
          drawLine: function(ctx, lineNum, maxW, maxH) {
              ctx.clearRect(0, 0, maxW, maxH);
              for(var i=0; i < lineNum; i++) {
                  var dx1 = Math.random()* maxW,
                      dy1 = Math.random()* maxH,
                      dx2 = Math.random()* maxW,
                      dy2 = Math.random()* maxH;
                  ctx.strokeStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
                  ctx.beginPath();
                  ctx.moveTo(dx1, dy1);
                  ctx.lineTo(dx2, dy2);
                  ctx.stroke();
              }
          },
          // 画文字
          drawText: function(ctx, text, maxH) {
              var len = text.length;
              for(var i=0; i < len; i++) {
                  var dx = 30 * Math.random() + 30* i,
                      dy = Math.random()* 5 + maxH/2;
                  ctx.fillStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
                  ctx.font = '30px Helvetica';
                  ctx.textBaseline = 'middle';
                  ctx.fillText(text[i], dx, dy);
              }
          },
          // 生成指定个数的随机文字
          randomText: function(len) {
              var source = ['a', 'b', 'c', 'd', 'e',
              'f', 'g', 'h', 'i', 'j',
              'k', 'l', 'm', 'o', 'p',
              'q', 'r', 's', 't', 'u',
              'v', 'w', 'x', 'y', 'z'];
              var result = [];
              var sourceLen = source.length;
              for(var i=0; i< len; i++) {
                  var text = this.generateUniqueText(source, result, sourceLen);
                  result.push(text)
              }
              return result.join('')
          },
          // 生成唯一文字
          generateUniqueText: function(source, hasList, limit) {
              var text = source[Math.floor(Math.random()*limit)];
              if(hasList.indexOf(text) > -1) {
                  return this.generateUniqueText(source, hasList, limit)
              }else {
                  return text
              }  
          }
      }
      new Gcode('#canvas_code', {
          lineNum: 6
      })
    })();
    // 调用
    new Gcode('#canvas_code', {
    lineNum: 6
    })

    4. 代理模式


    4.1 概念解读

    代理模式: 一个对象通过某种代理方式来控制对另一个对象的访问.

    4.2 作用

    • 远程代理(一个对象对另一个对象的局部代理)

    • 虚拟代理(对于需要创建开销很大的对象如渲染网页大图时可以先用缩略图代替真图)

    • 安全代理(保护真实对象的访问权限)

    • 缓存代理(一些开销比较大的运算提供暂时的存储,下次运算时,如果传递进来的参数跟之前相同,则可以直接返回前面存储的运算结果)

    4.3 注意事项

    使用代理会增加代码的复杂度,所以应该有选择的使用代理.

    实际案例

    我们可以使用代理模式实现如下功能:

    • 通过缓存代理来优化计算性能

    • 图片占位符/骨架屏/预加载等

    • 合并请求/资源

    4.4 代码展示

    接下来我们通过实现一个计算缓存器来说说代理模式的应用.

    // 缓存代理
    function sum(a, b){
    return a + b
    }
    let proxySum = (function(){
    let cache = {}
    return function(){
        let args = Array.prototype.join.call(arguments, ',');
        if(args in cache){
            return cache[args];
        }

        cache[args] = sum.apply(this, arguments)
        return cache[args]
    }
    })()

    5. 外观模式


    5.1 概念解读

    外观模式(facade): 为子系统中的一组接口提供一个一致的表现,使得子系统更容易使用而不需要关注内部复杂而繁琐的细节.

    5.2 作用

    • 对接口和调用者进行了一定的解耦

    • 创造经典的三层结构MVC

    • 在开发阶段减少不同子系统之间的依赖和耦合,方便各个子系统的迭代和扩展

    • 为大型复杂系统提供一个清晰的接口

    5.3 注意事项

    当外观模式被开发者连续调用时会造成一定的性能损耗,这是由于每次调用都会进行可用性检测

    5.4 实际案例

    我们可以使用外观模式来设计兼容不同浏览器的事件绑定的方法以及其他需要统一实现接口的方法或者抽象类.

    5.5 代码展示

    接下来我们通过实现一个兼容不同浏览器的事件监听函数来让大家理解外观模式如何使用.

    function on(type, fn){
    // 对于支持dom2级事件处理程序
    if(document.addEventListener){
        dom.addEventListener(type,fn,false);
    }else if(dom.attachEvent){
    // 对于IE9一下的ie浏览器
        dom.attachEvent('on'+type,fn);
    }else {
        dom['on'+ type] = fn;
    }
    }

    6. 观察者模式


    6.1 概念解读

    观察者模式: 定义了一种一对多的关系, 所有观察对象同时监听某一主题对象,当主题对象状态发生变化时就会通知所有观察者对象,使得他们能够自动更新自己.

    6.2 作用

    • 目标对象与观察者存在一种动态关联,增加了灵活性

    • 支持简单的广播通信, 自动通知所有已经订阅过的对象

    • 目标对象和观察者之间的抽象耦合关系能够单独扩展和重用

    6.3 注意事项

    观察者模式一般都要注意要先监听, 再触发(特殊情况也可以先发布,后订阅,比如QQ的离线模式)

    6.4 实际案例

    观察者模式是非常经典的设计模式,主要应用如下:

    • 系统消息通知

    • 网站日志记录

    • 内容订阅功能

    • javascript事件机制

    • react/vue等的观察者

    6.5 代码展示

    接下来我们我们使用原生javascript实现一个观察者模式:

    class Subject {
    constructor() {
      this.subs = {}
    }

    addSub(key, fn) {
      const subArr = this.subs[key]
      if (!subArr) {
        this.subs[key] = []
      }
      this.subs[key].push(fn)
    }

    trigger(key, message) {
      const subArr = this.subs[key]
      if (!subArr || subArr.length === 0) {
        return false
      }
      for(let i = 0, len = subArr.length; i < len; i++) {
        const fn = subArr[i]
        fn(message)
      }
    }

    unSub(key, fn) {
      const subArr = this.subs[key]
      if (!subArr) {
        return false
      }
      if (!fn) {
        this.subs[key] = []
      } else {
        for (let i = 0, len = subArr.length; i < len; i++) {
          const _fn = subArr[i]
          if (_fn === fn) {
            subArr.splice(i, 1)
          }
        }
      }
    }
    }

    // 测试
    // 订阅
    let subA = new Subject()
    let A = (message) => {
    console.log('订阅者收到信息: ' + message)
    }
    subA.addSub('A', A)

    // 发布
    subA.trigger('A', '我是徐小夕')   // A收到信息: --> 我是徐小夕

    7. 策略模式


    7.1 概念解读

    策略模式: 策略模式将不同算法进行合理的分类和单独封装,让不同算法之间可以互相替换而不会影响到算法的使用者.

    7.2 作用

    • 实现不同, 作用一致

    • 调用方式相同,降低了使用成本以及不同算法之间的耦合

    • 单独定义算法模型, 方便单元测试

    • 避免大量冗余的代码判断,比如if else等

    7.3 实际案例

    • 实现更优雅的表单验证

    • 游戏里的角色计分器

    • 棋牌类游戏的输赢算法

    7.4 代码展示

    接下来我们实现一个根据不同类型实现求和算法的模式来带大家理解策略模式.

    const obj = {
    A: (num) => num * 4,
    B: (num) => num * 6,
    C: (num) => num * 8
    }

    const getSum =function(type, num) {
    return obj[type](num)
    }

    8. 迭代器模式


    8.1 概念解读

    迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示.

    8.2 作用

    • 为遍历不同集合提供统一接口

    • 保护原集合但又提供外部访问内部元素的方式

    8.3 实际案例

    迭代器模式模式最常见的案例就是数组的遍历方法如forEach, map, reduce.

    8.4 代码展示

    接下来笔者使用自己封装的一个遍历函数来让大家更加理解迭代器模式的使用,该方法不仅可以遍历数组和字符串,还能遍历对象.lodash里的.forEach(collection, [iteratee=.identity])方法也是采用策略模式的典型应用.

    function _each(el, fn = (v, k, el) => {}) {
    // 判断数据类型
    function checkType(target){
      return Object.prototype.toString.call(target).slice(8,-1)
    }

    // 数组或者字符串
    if(['Array', 'String'].indexOf(checkType(el)) > -1) {
      for(let i=0, len = el.length; i< len; i++) {
        fn(el[i], i, el)
      }
    }else if(checkType(el) === 'Object') {
      for(let key in el) {
        fn(el[key], key, el)
      }
    }
    }

    最后

    如果想了解本文完整的思维导图, 更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

    来源:https://mp.weixin.qq.com/s/xTp3jY0IvXiOWBZhZ5H9fQ

    收起阅读 »