shoyuf

shoyuf 查看完整档案

广州编辑  |  填写毕业院校Web  |  前端开发 编辑 shoyuf.top 编辑
编辑

Web Developer

个人动态

shoyuf 赞了文章 · 3月9日

从一次报错聊聊 Point 事件

同步自我的博客,欢迎交流

这篇文章在草稿箱里躺了很久,因为最近又遇到了相关问题,于是又整理了一下。请注意这里讲的不是 csspointer-events

起因

从某个月黑风高的晚上开始,有人发现我们的 web-app 在 Chrome 模拟器里开始出现报错,报错信息大概就是下面这样。

VM1023:1 Uncaught TypeError: Cannot read property '0' of undefined

但是只有他的浏览器有问题,而且对功能毫无影响,本着在我的机器上不复现的精神(好吧,当时比较忙),这个问题的优先级排的不高,但是后面一段时间慢慢有人也出现相同的问题,于是我开始在意这个问题了。

定位问题

根据调用栈很快定位到了代码,源码定位到之前一位同事写的组件代码,大概是这样的:

dom.on('touchstart  pointerdown', function (event) {
        /*部分业务代码*/
        
        var touch = event.touches[0]; //报错的地方
        
        /*部分业务代码*/
})

debug 发现是触发了 pointdown 事件,因为 event 没有 touches 这个字段,导致抛出异常。但是之前用的好好的呀,难道是浏览器的 API 变化了?而且我也没有了解过 pointerdown 事件,这个事件是用来处理什么的呢?于是我带着两个问题开启了搜索之旅:

  1. 什么是 pointerdown 事件

  2. 为什么突然开始爆发错误

聊聊 pointer events

查问题,最简单的问题就是搂一遍 W3C 的官方文档了。这里简单说下我的理解。

设备输入形式的多样化

在 PC 时代,我们通过鼠标与屏幕交互,这时候,我们设计系统时只需要考虑鼠标事件就好了。但是如今,有很多新的设备,比如智能手机,平板电脑,他们包含了其他的输入方式,比如触摸,手写笔,官方也为这些输入形式都提供了新的事件。

但是对于开发者来说,这是件很麻烦的事,因为这意味着你需要为你的网页适配各种事件,比如你要根据用户的移动来画图,你需要兼容 PC 和手机,你的代码可能就会是下面这样

dom.addEventListener('mousemove',
  draw);
dom.addEventListener('touchmove',
  draw);

如果需要兼容更多的输入设备呢?比如手写笔,这样的话代码就会很复杂。而且,为了兼容现有的基于鼠标事件的代码,很多浏览器都会为所有的输入类型触发鼠标事件(例如在 touchmove 时触发 mousemove,我在 Chrome 试验了一下不会触发,但是因为没有设备,手写笔的情况没有试),这也会导致无法确认是否真的是鼠标触发的事件。

如何兼容多种输入形式

为了解决这一系列的问题,W3C 定义了一种新的输入形式,即 pointer。任何由鼠标、触摸、手写笔或者其他输入设备在屏幕上触发的接触,都算是 pointer 事件。

它的 API 和鼠标事件很像,非常容易迁移。除了提供鼠标事件常用的属性,比如 clientXtarget 等等,还提供了一些用于其他输入设备的属性,比如压力,接触面,倾斜角度等等,这样开发者就可以利用 pointer 事件为所有的输入设备开发自己的功能了!

提供的属性

pointer 事件提供了一些特有的事件属性

  • pointerId:当前指针事件的唯一标识,主要是在多点触控时标识唯一的一个输入源

  • width:接触面的宽度

  • height:接触面的高度

  • pressure:接触的压力值,范围是0-1,对于不支持压力的硬件,比如鼠标,按压时该值必须为 0.5,否则为 0

  • tiltX,titltY:手写笔的角度

  • pointerType:事件类型,目前有 mousepentouch,如果是无法探测的指针类型,则该值为空字符串

  • isPrimary:用于标识是否是主指针,主要是在多点触控中生效,开发者也可以通过忽略非主指针的指针事件来实现单点触控。
    如何确定主指针:

    • 鼠标输入:一定是主指针

    • 触摸输入:如果 pointerdown 触发时没有其他激活的触摸事件,isPrimarytrue

    • 手写笔输入:与触摸事件类似,pointerdown 触发时没有其他激活的 pointer 事件

相关事件

事件名称作用
pointerovermouseover 行为一致
pointerentermouseenter 行为一致
pointerdown指针进入活动状态,比如触摸了屏幕,类似于 touchstart
pointermove指针进行了移动
pointerup指针取消活动状态,比如手指离开了屏幕,类似于 touchend
pointercancel类似于 touchcancel
pointerout指针离开元素边缘或者离开屏幕,类似于 mouseout
pointerleave类似于 mouseleave
gotpointercapture元素捕获到指针事件时触发
lostpointercapture指针被释放时触发

可以看到,pointer 事件与已知的事件类型基本一致,但是有一点区别:在触摸屏上,我们可能会滑动屏幕来触发页面滚动,缩放或者刷新,对于 touch 事件,这时会触发 touchmove,但是对于 pointer 事件,当触发这些浏览器行为时,你却会接收到 pointercancel 事件以便于通知你浏览器已经接管了你的指针事件。

如何检测

首先,pointer 事件的支持程度已经很不错了,你可以使用 Pointer Events polyfill来进行兼容,也可以自行检测

if (window.PointerEvent) {
    // 支持
} else {
  // 不支持
}

导致问题的原因

这时候,对于本文一开始提到的问题就显而易见了,因为 point events 是没有 touches 这个属性的。那么我们还有两个问题。

为什么之前会用到 point events

后来我看了下 zepto 的源码,在事件处理时是考虑到了 point event 的,同事之前写的代码大概是参考了 zepto 的事件系统。

为什么会突然爆发这个问题?

很简答,Chrome 55 开始支持这个 API,Chrome 具体的支持信息可以参考官方日志,至于怎么检测浏览器支持,可以参考上面的内容

总结

  1. 对于开发来说,一定要钻进去,任何 bug 都是有原因的

  2. 代码报错应该有相应的监控机制,让机器来帮我们发现问题,而不是靠人工去干预

参考
https://www.w3.org/Submission...
https://developers.google.com...

查看原文

赞 5 收藏 7 评论 2

shoyuf 赞了文章 · 2020-12-09

图片上传姿势以及你不知道的Typed Arrays

在思否答题遇到几个关于图片上传的问题,中间都涉及到ArrayBuffer的概念,心心念念想整理下这方面的知识,也希望让更多人能有所收获。

各位看官,一起开始吧。

1. 如何上传文件

前端中上传一般使用FormData创建请求数据,示例如下:

var formData = new FormData();

formData.append("username", "Groucho");

// HTML 文件类型input,由用户选择
formData.append("userfile", fileInputElement.files[0]);

// JavaScript file-like 对象
var content = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文...
var blob = new Blob([content], { type: "text/xml"});

formData.append("webmasterfile", blob);

var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);
FormData 对象的字段类型可以是 Blob, File, 或者 string,如果它的字段类型不是Blob也不是File,则会被转换成字符串。

我们通过<input type="input"/>选择图片,把获取到的file放到FormData,再提交到服务器。

如果上传多个文件,就追加到同一个字段中。

fileInputElement.files.forEach(file => {
  formData.append('userfile', file);
})

其中的file-likenew Blob的示例说明我们可以构造一个新的文件直接上传。

场景1:剪辑图片上传

我们通过裁剪库可以得到data url或者canvas

cropperjs举例,使用getCroppedCanvas获取到canvas,然后利用自身的toBlob获取到file数据,再通过FormData上传。

转换的核心代码可以参考下面:

canvas = cropper.getCroppedCanvas({
  width: 160,
  height: 160,
});

initialAvatarURL = avatar.src;
avatar.src = canvas.toDataURL();

// 从canvs获取blob数据
canvas.toBlob(function (blob) {
  var formData = new FormData();
  formData.append('avatar', blob, 'avatar.jpg');
  
  // 接下来可以发起请求了
  makeRequest(formData)
})

场景2:base64图片上传

获取到base64形式的图片后,我们通过下面函数转为blob形式:

function btof(base64Data, fileName) {
  const dataArr = base64Data.split(",");
  const byteString = atob(dataArr[1]);

  const options = {
    type: "image/jpeg",
    endings: "native"
  };
  const u8Arr = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; i++) {
    u8Arr[i] = byteString.charCodeAt(i);
  }
  return new File([u8Arr], fileName + ".jpg", options);
}

这样我们拿到了文件file,然后就可以继续上传了。

场景3:URL图片上传

想要直接用图片URL上传,我们可以分成两部来做:

  1. 获取base64
  2. 然后转为file

其中关键代码是如何从URL中创建canvas,这里通过创建Image对象,在图片挂载之后,填充到到canvas中。

var img =
  "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=508387608,2848974022&fm=26&gp=0.jpg"; //imgurl 就是你的图片路径
  
var image = new Image();
image.src = img;
image.setAttribute("crossOrigin", "Anonymous");
image.onload = function() {
  // 第1步:获取base64形式的图片
  var base64 = getBase64Image(image);

  var formData = new FormData(); 

  // 第2步:转换base64到file
  var file = btof(base64, "test");
  formData.append("imageName", file);
};

function getBase64Image(img) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0, img.width, img.height);
  var ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
  var dataURL = canvas.toDataURL("image/" + ext);

  return dataURL;
}

<p class="codepen" data-height="355" data-theme-id="0" data-default-tab="js,result" data-user="ineo6" data-slug-hash="MWgpGQZ" data-preview="true" style="height: 355px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="url image转为base64">
<span>See the Pen
url image转为base64
by neo (@ineo6)
on CodePen.</span>
</p>
<script async data-original="https://static.codepen.io/ass...;></script>

2. 思考

虽然前文提到的场景我们解决了,但是里面包含了这些关键词,不得不让人思考:

  • Blob
  • File
  • Uint8Array
  • ArrayBuffer
  • TypedArray
  • Base64
  • atob,btoa

这些关键词都指向"文件"、"二进制"、"编码",也是我们平时不太会注意的点。

之前使用到FileBlob时心里也一直有疑惑。

到底这些有什么作用呢?接下来可以看看我整理的这些知识。

3. 概念

3.1 Blob

Blob 对象表示一个不可变、原始数据的类文件对象。

File接口也是基于Blob对象,并且进行扩展支持用户系统的文件格式。

3.1.1 创建Blob对象

要从其他非blob对象和数据构造Blob,就要使用Blob()构造函数:

var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});

3.1.1 读取Blob对象

使用FileReader可以读取Blob对象中的内容。

var reader = new FileReader();
reader.addEventListener("loadend", function() {
   //reader.result 就是内容
   console.log(reader.result)
});
reader.readAsArrayBuffer(blob);

3.1.1 Object URLs

Object URLs指的是以blob:开头的地址,可以用来展示图片、文本信息。

这里就有点类似base64图片的展示,所以我们同样可以用来预览图片。

下面代码片段就是把选中的图片转为Object URLs形式。

function handleFiles(files) {
  if (!files.length) {
    fileList.innerHTML = "<p>No file!</p>";
  } else {
    fileList.innerHTML = "";
    var list = document.createElement("ul");
    fileList.appendChild(list);
    for (var i = 0; i < files.length; i++) {
      var li = document.createElement("li");
      list.appendChild(li);
      
      var img = document.createElement("img");
      // 从文件中创建object url
      img.src = window.URL.createObjectURL(files[i]);
      img.height = 60;
      img.onload = function() {
        // 加载完成后记得释放object url
        window.URL.revokeObjectURL(this.src);
      }
      li.appendChild(img);
      var info = document.createElement("span");
      info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
      li.appendChild(info);
    }
  }
}

demo

3.2 Typed Arrays - 类型化数组

类型化数组是一种类似数组的对象,提供了访问原始二进制数据的功能。但是类型化数组和正常数组并不是一类的,Array.isArray()调用会返回false

Typed Arrays有两块内容:

  • 缓冲(ArrayBuffer)
  • 视图(TypedArray 和 DataView)

3.2.1 ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
ArrayBuffer 不能直接操作,而是要通过TypedArrayDataView对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

ArrayBuffer主要用来高效快速的访问二进制数据,比如 WebGL, Canvas 2D 或者 Web Audio 所使用的数据。

接下来我们结合TypedArray一起理解下。

3.2.2 TypedArray

TypedArray可以在ArrayBuffer对象之上,根据不同的数据类型建立视图。

// 创建一个8字节的ArrayBuffer
const b = new ArrayBuffer(8);

// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
const v1 = new Int32Array(b);

// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
const v2 = new Uint8Array(b, 2);

// 创建一个指向b的Int16视图,开始于字节2,长度为2
const v3 = new Int16Array(b, 2, 2);

Int32Array,Uint8Array之类指的就是TypedArrayTypedArray对象描述的是底层二进制数据缓存区的一个类似数组(array-like)的视图。

它有着众多的成员:

Int8Array(); 
Uint8Array(); 
Uint8ClampedArray();
Int16Array(); 
Uint16Array();
Int32Array(); 
Uint32Array(); 
Float32Array(); 
Float64Array();

15722463800941.jpg

再来看一个小栗子:

var buffer = new ArrayBuffer(2) 
var bytes = new Uint8Array(buffer)

bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'

// 查看buffer内容
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB

字节序

上面的例子中,我们先写入'A',再写入'B',当然我们也可以通过Uint16Array一下写入两个字节。

var buffer = new ArrayBuffer(2) // 两个字节的缓冲
var word = new Uint16Array(buffer) // 以16位整型访问缓冲

// 添加'A'到高位,添加'B'到低位
var value = (65 << 8) + 66
word[0] = value

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // BA

执行这段代码你会发现,为什么看到的是"BA"而不是"AB"?

这是因为还有"字节序"的存在,分别是小端字节序和大端字节序。

比如,一个占据四个字节的 16 进制数0x12345678,决定其大小的最重要的字节是“12”,最不重要的是“78”。小端字节序将最不重要的字节排在前面,储存顺序就是78563412;大端字节序则完全相反,将最重要的字节排在前面,储存顺序就是12345678。

因为浏览器使用的是小端字节序,就导致我们看到的是"BA"。为了解决字节序不统一的问题,我们可以使用DataView设定字节序。

TypedArray.prototype.buffer

TypedArray实例的buffer属性,返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);

上面代码的a视图对象和b视图对象,对应同一个ArrayBuffer对象,即同一段内存。

TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

byteLength属性返回 TypedArray 数组占据的内存长度,单位为字节。byteOffset属性返回 TypedArray 数组从底层ArrayBuffer对象的哪个字节开始。这两个属性都是只读属性。

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4

v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2
TypedArray.prototype.length

length属性表示 TypedArray 数组含有多少个成员。注意将 length 属性和 byteLength 属性区分,前者是成员长度,后者是字节长度。

const a = new Int16Array(8);

a.length // 8
a.byteLength // 16
TypedArray.prototype.set()

TypedArray数组的set方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

set方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象。

TypedArray.prototype.subarray()

subarray方法是对于 TypedArray 数组的一部分,再建立一个新的视图。

const a = new Uint16Array(8);
const b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2
TypedArray.prototype.slice()

TypeArray 实例的slice方法,可以返回一个指定位置的新的TypedArray实例。

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]
TypedArray.of()

TypedArray 数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。

Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]

下面三种方法都会生成同样一个 TypedArray 数组。

// 方法一
let tarr = new Uint8Array([1,2,3]);

// 方法二
let tarr = Uint8Array.of(1,2,3);

// 方法三
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;
TypedArray.from()

静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。

Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]

这个方法还可以将一种TypedArray实例,转为另一种。

const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true

from方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map方法。

Int8Array.of(127, 126, 125).map(x => 2 * x)
// Int8Array [ -2, -4, -6 ]

Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
// Int16Array [ 254, 252, 250 ]

上面的例子中,from方法没有发生溢出,这说明遍历不是针对原来的 8 位整数数组。也就是说,from会将第一个参数指定的 TypedArray 数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式。

复合视图

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

上面代码将一个 24 字节长度的ArrayBuffer对象,分成三个部分:

  • 字节 0 到字节 3:1 个 32 位无符号整数
  • 字节 4 到字节 19:16 个 8 位整数
  • 字节 20 到字节 23:1 个 32 位浮点数

3.2.3 DataView - 视图

如果一段数据包含多种类型,我们还可以使用DataView视图进行操作。

DataView 视图提供 8 个方法写入内存。

dataview.setXXX(byteOffset, value [, littleEndian])

  • byteOffset 偏移量,单位为字节
  • value 设置的数值
  • littleEndian 传入false或undefined表示使用大端字节序

setInt8:写入 1 个字节的 8 位整数。
setUint8:写入 1 个字节的 8 位无符号整数。
setInt16:写入 2 个字节的 16 位整数。
setUint16:写入 2 个字节的 16 位无符号整数。
setInt32:写入 4 个字节的 32 位整数。
setUint32:写入 4 个字节的 32 位无符号整数。
setFloat32:写入 4 个字节的 32 位浮点数。
setFloat64:写入 8 个字节的 64 位浮点数。

相应也有8个方法读取内存:

getInt8:读取 1 个字节,返回一个 8 位整数。
getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
getInt16:读取 2 个字节,返回一个 16 位整数。
getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
getInt32:读取 4 个字节,返回一个 32 位整数。
getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
getFloat32:读取 4 个字节,返回一个 32 位浮点数。
getFloat64:读取 8 个字节,返回一个 64 位浮点数。

下面是表格里是BMP文件的头信息:

Byte描述
2"BM"标记
4文件大小
2保留
2保留
4文件头和位图数据之间的偏移量

我们使用DataView可以这样简单实现:

var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)

view.setUint8(0, 66)     // 写入1字节: 'B'
view.setUint8(1, 67)     // 写入1字节: 'M'
view.setUint32(2, 1234)  // 写入4字节的大小: 1234
view.setUint16(6, 0)     // 写入2字节保留位
view.setUint16(8, 0)     // 写入2字节保留位
view.setUint32(10, 0)    // 写入4字节偏移量

里面对应的结构应该是这样的:

Byte  |    0   |    1   |    2   |    3   |    4   |    5   | ... |
Type  |   I8   |   I8   |                I32                | ... |    
Data  |    B   |    M   |00000000|00000000|00000100|11010010| ... |

回到前面遇到的"BA"问题,我们用DataView重新执行下:

var buffer = new ArrayBuffer(2) 
var view = new DataView(buffer)

var value = (65 << 8) + 66
view.setUint16(0, value)

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB

这下我们得到了正确结果"AB",这个也说明DataView默认使用大端字节序。

参考文章

本文同步发表于作者博客: 图片上传姿势以及你不知道的Typed Arrays

wechat-find-me.png

查看原文

赞 1 收藏 0 评论 1

shoyuf 赞了回答 · 2020-12-03

解决jsonp跨域被CORB拦截,有何解决方法?

将链接写到script标签src 用safari 打开,会发现请求是返回了json格式数据,数据格式不是jsonp形式(callback(data))图片描述

图片描述

json格式数据,在chrome 里面触发了Cross-Origin Read Blocking (CORB)链接描述 所以显示为
图片描述

所以原因在于:请求返回数据的格式不对(或者请求的参数不对)
上文的链接:修改参数format=jsonp 即如下
https://c.y.qq.com/v8/fcg-bin...
使用这个链接会看到callback也用错了,再修改jsonpCallback = MusicJsonCallback 数据就正常返回了(通常是Callback等于__jp0,服务器端就会返回__jp0(data)形式,此处却不是,毕竟是别人的接口)。
(Cross-Origin Read Blocking,走过路过了解一下~~ 哈哈)

关注 5 回答 4

shoyuf 回答了问题 · 2020-06-24

解决webpack 如何设置全局变量

使用 ProvidePlugin

详情参见: https://www.yuque.com/fanxian...

关注 8 回答 7

shoyuf 发布了文章 · 2020-04-30

Vite 是什么

本文基于 vite@0.7.0 编写,可能与目前代码不符,如有疑问欢迎邮件或评论沟通

什么是 vite

vite 是一个基于 Vue3 单文件组件的非打包开发服务器

这和传统基于打包(例如 Webpack)的开发服务器有什么区别

vite 在开发的时候没有打包的过程,ES 模块源码直接传输给浏览器,浏览器使用自带的 <script module> 进行解析支持,通过 HTTP 请求进行每次 import,开发服务器拦截请求和对需要转换的代码进行转换。

例如:*.vue 文件会在发回浏览器之前进行编译

这样操作有许多优势:

  • 开发服务器启动后不需要进行打包操作,启动会变得非常迅速
  • 代码在需要的时候进行编译,所以只有代码真正在屏幕上展现的时候才进行编译。开始开发的时候再也不需要等待整个应用编译完成,这对大型应用是一个巨大的改变
  • 热模块替换的性能和模块的数量之间的关系解耦,热模块替换变得非常快

导入本地 ES 模块可能会引发深层的导入链路,整个页面重新加载会比依赖打包的开发服务器略慢。然而这是一个本地开发服务器,这部分增加的时间和实际编译的时间相比应该非常小(编译的文件会被缓存在内存中)

vite 的编译本质上还是的 Node.js 中进行,从技术上讲它可以支持打包工具能支持的各种代码转换,没有什么可以阻止你将代码包用于生产,实际上,vite 提供了vite build的脚本用于这个操作,因此不会在生产环境中遭遇到网络流爆炸的问题

当前 vite 尚处于实验性阶段,不适合用于生产环境,但希望有一天能做到这个目标

特性

模块解析

本地 ES 模块导入不支持如下的导入方式

import { createApp } from 'vue'

默认情况下将会导致一个错误,vite 在 js 文件中检测到这种情况将会将其改写为@modules/{package-name},在这些特殊的路径下,vite 执行以下的方式找到正确的文件

  • vue 有特殊的处理,你不需要安装这个模块,如果需要使用特殊的版本,vite 将会使用node_modules内部的模块包
  • 如果web_modules目录存在,将会使用它
  • 如果其它方式都没有定位到模块,将会在node_modules中查找

热模块替换

  • 对于 *.vue 文件将会得到开箱即用的替换功能
  • 对于*.js 需要提供类似于 webpack HMR 的 API
import { foo } from "./foo.js";
import { hot } from "@hmr";

foo();

hot.accept("./foo.js", ({ foo }) => {
  // the callback receives the updated './foo.js' module
  foo();
});

CSS 预处理器

安装模块即可在 *.vue 中使用

<style lang="scss">
/* use scss */
</style>

生产构建

执行 vite build,当前支持 --root--cdn 两个参数

API

可以使用 API 定制开发服务器,vite 支持插件形式扩展,可以定制化访问 vite 内部的 koa 实例和增加相关的中间件

下一步开发计划

  • Source Map 支持
  • 自动加载 postcss 配置

解析

启动一个 vite 开发服务器

  • http://localhost:3000/ 首屏页面
<div id="app"></div>
<script type="module">
  import { createApp } from "/@modules/vue"; // 此模块中包含相关热加载逻辑
  import App from "./App.vue"; // 此文件为SFC主模板

  createApp(App).mount("#app"); // 渲染模版
</script>
  • http://localhost:3000/App.vue 主模板
import { updateStyle } from "/@hmr"; // 加载更新style方法

const __script = {
  data: () => ({ count: 0 })
};

updateStyle("c44b8200-0", "/App.vue?type=style&index=0");
__script.__scopeId = "data-v-c44b8200";
import { render as __render } from "/App.vue?type=template"; // 加载template模板
__script.render = __render;
__script.__hmrId = "/App.vue";
__script.__file = "/Users/shoyuf/work/vite-app/App.vue";
export default __script;
  • /@hmr 更新逻辑
console.log("[vite] connecting...");
const socket = new WebSocket(`ws://${location.host}`);
// Listen for messages
socket.addEventListener("message", ({ data }) => {
  const { type, path, id, index, timestamp } = JSON.parse(data);
  switch (type) {
    case "connected": // 连接成功
      console.log(`[vite] connected.`);
      break;
    case "vue-reload": // 当script改变的情况下,需要重新加载
      import(`${path}?t=${timestamp}`).then(m => {
        __VUE_HMR_RUNTIME__.reload(path, m.default);
        console.log(`[vite] ${path} reloaded.`);
      });
      break;
    case "vue-rerender": // 当template改变的情况下,需要重新渲染
      import(`${path}?type=template&t=${timestamp}`).then(m => {
        __VUE_HMR_RUNTIME__.rerender(path, m.render);
        console.log(`[vite] ${path} template updated.`);
      });
      break;
    case "vue-style-update": // 当css改变情况下更新style
      updateStyle(id, `${path}?type=style&index=${index}&t=${timestamp}`);
      console.log(
        `[vite] ${path} style${index > 0 ? `#${index}` : ``} updated.`
      );
      break;
    case "vue-style-remove": // css改变后移除旧的css引用
      const link = document.getElementById(`vite-css-${id}`);
      if (link) {
        document.head.removeChild(link);
      }
      break;
    case "js-update": // js 模块更新重新加载
      const update = jsUpdateMap.get(path);
      if (update) {
        update(timestamp);
        console.log(`[vite]: js module reloaded: `, path);
      } else {
        console.error(
          `[vite] got js update notification but no client callback was registered. Something is wrong.`
        );
      }
      break;
    case "full-reload": // 导入链进入死胡同,需要进行页面重新加载
      location.reload();
  }
});
// ping server
socket.addEventListener("close", () => {
  console.log(`[vite] server connection lost. polling for restart...`);
  setInterval(() => {
    new WebSocket(`ws://${location.host}`).addEventListener("open", () => {
      location.reload();
    });
  }, 1000);
});
export function updateStyle(id, url) {
  const linkId = `vite-css-${id}`;
  let link = document.getElementById(linkId);
  if (!link) {
    link = document.createElement("link");
    link.id = linkId;
    link.setAttribute("rel", "stylesheet");
    link.setAttribute("type", "text/css");
    document.head.appendChild(link);
  }
  link.setAttribute("href", url);
}
const jsUpdateMap = new Map();
export const hot = {
  accept(importer, deps, callback) {
    jsUpdateMap.set(importer, timestamp => {
      if (Array.isArray(deps)) {
        Promise.all(deps.map(dep => import(dep + `?t=${timestamp}`))).then(
          callback
        );
      } else {
        import(deps + `?t=${timestamp}`).then(callback);
      }
    });
  }
};
  • /App.vue?type=template 主模板 HTML 部分
import {
  createVNode as _createVNode,
  toDisplayString as _toDisplayString,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createBlock as _createBlock,
  withScopeId as _withScopeId,
  pushScopeId as _pushScopeId,
  popScopeId as _popScopeId
} from "/@modules/vue";
const _withId = _withScopeId("data-v-c44b8200");

_pushScopeId("data-v-c44b8200");
const _hoisted_1 = _createVNode(
  // 创建Virtual DOM
  "h1",
  null,
  "Hello Vite + Vue 3!",
  -1 /* HOISTED */
);
const _hoisted_2 = _createVNode(
  "p",
  null,
  "Edit ./App.vue to test hot module replacement (HMR).",
  -1 /* HOISTED */
);
_popScopeId();

export const render = _withId(function render(_ctx, _cache) {
  // 渲染函数
  return (
    _openBlock(),
    _createBlock(
      _Fragment,
      null,
      [
        _hoisted_1,
        _hoisted_2,
        _createVNode("p", null, [
          _createVNode(
            "span",
            null,
            "Count is: " + _toDisplayString(_ctx.count),
            1 /* TEXT */
          ),
          _createVNode(
            "button",
            {
              onClick: _cache[1] || (_cache[1] = $event => _ctx.count++)
            },
            "increment"
          )
        ])
      ],
      64 /* STABLE_FRAGMENT */
    )
  );
});
  • /App.vue?type=style&index=0 主模板 css 部分,包括 scopedId
h1[data-v-c44b8200] {
  color: #4fc08d;
}
h1[data-v-c44b8200],
p[data-v-c44b8200] {
  font-family: Arial, Helvetica, sans-serif;
}
  • ws://localost:3000/ 执行热替换的数据交互,与/@hmr相联

Example:

{
  path: "/App.vue",
  timestamp: 1588242356511,
  type: "vue-reload"
}

type 与@hmr的相关方法一致

  • vue-reload
  • vue-rerender
  • vue-style-update
  • vue-style-remove
  • js-update
  • full-reload

webpack 很慢

vite

参考

  1. vuejs/vite: Experimental no-bundle dev server for Vue SFCs
查看原文

赞 9 收藏 5 评论 0

shoyuf 回答了问题 · 2020-03-20

Vue run dev报错:CSP: 页面设置阻止读取位于 inline 的一项资源("default-src")。

CSP 阻止了 Vue.js 的内部函数执行,这应该是部分版本开发脚手架的问题,可以尝试更新脚手架工具,例如vue-cli或webpack-dev-server相关

关注 5 回答 3

shoyuf 关注了用户 · 2020-02-27

快乐动起来 @cucygh

喜欢不断挑战的前端实践者

关注 471

shoyuf 赞了文章 · 2019-07-24

创建一个离线优先,数据驱动的渐进式 Web 应用程序

原文地址:Build an offline-first, data-driven PWA
译文出自:我的个人博客

概述

在本文中,您将学习如何使用 Workbox 和 IndexedDB 创建离线优先、数据驱动的渐进式Web应用程序(PWA)。在离线的情况下也可以使用后台同步功能将应用程序与服务器同步。

将会学习到

  • 如何使用 Workbox 缓存应用程序
  • 如何使用 IndexedDB 存储数据
  • 如何在用户脱机时从 IndexedDB 中检索和显示数据
  • 脱机时如何保存数据
  • 如何在脱机时使用后台同步更新应用程序

应该了解的

  • HTML, CSS, 和 JavaScript
  • ES2015 Promises
  • 如何使用命令行
  • 熟悉一下 Workbox
  • 熟悉一下 Gulp
  • 熟悉一下 IndexedDB

需具备的条件

  • 拥有 terminal/shell 访问权限的电脑
  • Chrome 52 或更高版本
  • 编辑器
  • Nodejs 和 npm

设置

如果你没有安装 Nodejs 需要安装一下

之后通过下面的方式 clone 快速启动仓库

git clone https://github.com/googlecodelabs/workbox-indexeddb.git

或者直接下载 压缩包

安装依赖并启动服务

到下载好的 git 仓库目录中,转到 project 文件夹

cd workbox-indexeddb/project/

然后安装依赖并启动服务

npm install
npm start

说明

这个步骤中会根据 package.json 定义的依赖并安装,打开 package.json 文件查看,有很多依赖,大部分是开发环境需要的(你可以忽略),主要的依赖是:

npm start 会构建并输出到 build 文件夹,启动 dev server,并且会开启一个 gulp watch 任务。gulp watch 会监听文件的修改自动构建。concurrently 可以同时跑 gulp 和 dev server

打开应用

打开 Chrome 并且跳转到 localhost:8081 你会看到一个事件列表的控制台,在弹出的权限确认菜单中点击允许

img

我们使用通知系统来告知用户 app 的后台同步已经更新,试着测试一下页面底部的添加功能

说明

这个小项目的目标是离线保存用户的事件日历。你可以查看一下 app/js/main.js 文件的 loadContentNetworkFirst 方法当前是怎么工作的,首先会请求 server,成功则更新页面,失败会在控制台打印一个信息,目前脱机是无法使用的,接下来我们添加一些方法使它脱机可用。

缓存 app shell

编写 service worker

要想脱机工作,就需要 server worker,现在写一个。

把下面的代码添加到 app/src/sw.js

importScripts('workbox-sw.dev.v2.0.0.js');
importScripts('workbox-background-sync.dev.v2.0.0.js');

const workboxSW = new WorkboxSW();
workboxSW.precache([]);

说明

在开头我们引入了 workbox-swworkbox-background-sync

  • workbox-sw 包含了 precache 和向 service worker 添加路由的方法
  • workbox-background-sync 是在 service worker 中后台同步的库,稍后会提到

precache 方法接收一个文件列表的数组,先用一个空的,下一步我们会用 workbox-build 去计算出这个数组的结果。

构建 service worker

推荐使用 Workbox 的构建模块,比如 workbox-build

把下面的代码添加进 project/gulpfile.js

gulp.task('build-sw', () => {
  return wbBuild.injectManifest({
    swSrc: 'app/src/sw.js',
    swDest: 'build/service-worker.js',
    globDirectory: 'build',
    staticFileGlobs: [
      'style/main.css',
      'index.html',
      'js/idb-promised.js',
      'js/main.js',
      'images/**/*.*',
      'manifest.json'
    ],
    templatedUrls: {
      '/': ['index.html']
    }
  }).catch((err) => {
    console.log('[ERROR] This happened: ' + err);
  });
});

现在取消一些注释:

gulpfile.js:

// uncomment the line below:
const wbBuild = require('workbox-build');

// ...

gulp.task('default', ['clean'], cb => {
  runSequence(
    'copy',
    // uncomment the line below:
    'build-sw',
    cb
  );
});

保存修改,因为修改了 gulp,我们得重新跑一下,Ctrl + C 退出当前的进程,重新运行 npm start,会看到 service worker 的文件被生成在了 build/service-worker.js

取消 app/index.html 中 service worker 注册代码的注释

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('service-worker.js')
    .then(function(registration) {
      console.log('Service Worker registration successful with scope: ',
      registration.scope);
    })
    .catch(function(err) {
      console.log('Service Worker registration failed: ', err);
    });
}

保存修改,刷新浏览器 service worker 就会被安装。Ctrl + C 关闭 dev server,再返回到浏览器中刷新页面,已经可以脱机运行了!

说明

在这一步中,workbox-buildbuild-sw 任务被合并到我们的 gulp 文件中,我们的构建过程是使用 workbox-build 库来从 swSrc(app/src/sw.js) 中生成 service work 到 swDest(build/service-worker.js),来自 globDirectory(build)staticFileGlobs 文件被注入到 build/service-worker.js 以供 precache 调用,还有每个文件的修订哈希。templatedUrls 选项告诉 Workbox 我们的站点以 index.html 的内容响应请求。

顺便贴一个 injectManifest 的链接

安装生成好的 service worker 缓存 app shell 的资源文件,Workbox 会自动去:

  • 为缓存资源设置缓存优先策略,允许应用程序离线加载
  • service work 更新时,使用修订哈希来更新缓存的文件

创建 IndexedDB 数据库

目前为止还不能离线加载数据,我们接下来创建一个 IndexDB 来保存程序的数据,数据库命名为 dashboardr

添加下面代码到 app/js/main.js

function createIndexedDB() {
  if (!('indexedDB' in window)) {return null;}
  return idb.open('dashboardr', 1, function(upgradeDb) {
    if (!upgradeDb.objectStoreNames.contains('events')) {
      const eventsOS = upgradeDb.createObjectStore('events', {keyPath: 'id'});
    }
  })
}

取消调用 createIndexedDB 的注释:

const dbPromise = createIndexedDB();

保存文件,重启 server:

npm start

回到浏览器刷新页面,激活 skipWaiting 并再次刷新页面,在 Chrome 中,你可以在开发者工具中的 Application 面板中选择 Service Workers 点击 skipWaiting,之后使用 开发者工具 检查数据库是否存在。在 Chrome 中你可以在 Application 面板中点击 IndexedDB 选择 dashboardr 查看 events 对象是否存在。

注意:开发者工具的 IndexedDB UI 可能不会准确的反应你数据库的情况,在 Chrome 中你可以刷新数据库查看,或者重新打开开发者工具

说明

在上面的代码中,我们创建了一个 dashboardr 数据库,并把他的版本号设置为 1 ,然后检查 events 对象是否存在,这个检查是为了避免潜在的错误,我们还给 event 提供了一个唯一的 key path id

由于我们修改了 app/main.js 文件,gulp 的 watch 任务会自动构建,Workbox 会自动更新修订哈希,然后智能更新缓存中的 main.js

保存数据到 IndexedDB 中

现在我们保存数据到刚创建的数据库 dashboardr 中的 event 对象中。

function saveEventDataLocally(events) {
  if (!('indexedDB' in window)) {return null;}
  return dbPromise.then(db => {
    const tx = db.transaction('events', 'readwrite');
    const store = tx.objectStore('events');
    return Promise.all(events.map(event => store.put(event)))
    .catch(() => {
      tx.abort();
      throw Error('Events were not added to the store');
    });
  });
}

然后更新 loadContentNetworkFirst 方法,现在这是完整的方法:

function loadContentNetworkFirst() {
  getServerData()
  .then(dataFromNetwork => {
    updateUI(dataFromNetwork);
    saveEventDataLocally(dataFromNetwork)
    .then(() => {
      setLastUpdated(new Date());
      messageDataSaved();
    }).catch(err => {
      messageSaveError(); 
      console.warn(err);
    });
  }).catch(err => { // if we can't connect to the server...
    console.log('Network requests have failed, this is expected if offline');
  });
}

取消注释 addAndPostEvent 中的 saveEventDataLocally 调用

function addAndPostEvent() {
  // ...
  saveEventDataLocally([data]);
  // ...
}

保存文件,刷新页面重新激活 service worker。再次刷新页面,检查一下来自网络的数据是否被保存到 events 中去(你可能需要刷新一下开发者工具中的 IndexedDB

说明

saveEventDataLocally 接收一个数组并一条条的保存到 IndexedDB 数据库中,我们把 store.put 写在了 Promise.all 中,这样如果某一条更新出错我们就可以终止事务。

loadContentNetworkFirst 方法中,一旦收到来自服务器的数据,就会更新 IndexedDB 和页面。然后,数据成功保存时,将存储时间戳,并通知用户数据可供离线使用。

addAndPostEvent 中调用 saveEventDataLocally 方法保证了添加新的 event 时本地会存有最新的数据。

从 IndexedDB 中获取数据

离线的时候,我们就要查询本地缓存的数据。

添加下面的代码到 app/js/main.js 中:

function getLocalEventData() {
  if (!('indexedDB' in window)) {return null;}
  return dbPromise.then(db => {
    const tx = db.transaction('events', 'readonly');
    const store = tx.objectStore('events');
    return store.getAll();
  });
}

然后更新 loadContentNetworkFirst 方法,完整的方法如下:

function loadContentNetworkFirst() {
  getServerData()
  .then(dataFromNetwork => {
    updateUI(dataFromNetwork);
    saveEventDataLocally(dataFromNetwork)
    .then(() => {
      setLastUpdated(new Date());
      messageDataSaved();
    }).catch(err => {
      messageSaveError();
      console.warn(err);
    });
  }).catch(err => {
    console.log('Network requests have failed, this is expected if offline');
    getLocalEventData()
    .then(offlineData => {
      if (!offlineData.length) {
        messageNoData();
      } else {
        messageOffline();
        updateUI(offlineData); 
      }
    });
  });
}

保存文件,刷新浏览器激活更新的 service worker,现在 Ctrl + C 关闭 dev server,返回到浏览器中刷新页面,现在 app 和数据都可以离线加载了!

说明

loadContentNetworkFirst 被调用的时候如果没有网络连接,getServerData 会被 reject,之后便会进入到 catch 中去,然后 getLocalEventData 会调用本地缓存的数据。有网络连接的话会正常的请求 server 并且 updateUI

使用 workbox-background-sync

我们的 app 已经可以离线保存和浏览数据,现在我们来用 workbox-background-sync 把离线状态下保存的数据同步到服务端去。

把下面的的代码添加到 app/src/sw.js

let bgQueue = new workbox.backgroundSync.QueuePlugin({
  callbacks: {
    replayDidSucceed: async(hash, res) => {
      self.registration.showNotification('Background sync demo', {
        body: 'Events have been updated!'
      });
    }
  }
});

workboxSW.router.registerRoute('/api/add',
  workboxSW.strategies.networkOnly({plugins: [bgQueue]}), 'POST'
);

保存,现在转到命令行:

npm run start

刷新浏览器,激活更新的 service worker

Ctrl + C 把 app 变为离线状态,添加一个 event 确认请求 /api/add 已经被添加进 bgQueueSyncDBQueueStore 对象。

说明

当用户试图在离线情况下添加 event 的时候,workbox-background-sync 会把失败的请求保存为一个离线队列,当用户重新联网 backgroundSync 会重新发送这些请求,甚至都不需要用户打开 app!但是,从联网到重新发请求的这个过程大概需要 5 分钟,下一节我们将会介绍如何在 app 中立即发送这些请求。

重发请求

因为重发请求会有延迟,所以用户可能回到 app 之后还没有同步数据,所以我们在用户联网的时候立即发送这些请求。

把下面的代码添加到 app/src/sw.js

workboxSW.router.registerRoute('/api/getAll', () => {
  return bgQueue.replayRequests().then(() => {
    return fetch('/api/getAll');
  }).catch(err => {
    return err;
  });
});

只要用户请求服务端数据(加载或刷新页面时),该路由就会 replay 排队的请求,然后返回最新的服务端数据。这很好,但是用户还是得刷新页面去重新获取数据,我们还有更好的做法。

把下面的代码添加进 app/js/main.js

window.addEventListener('online', () => {
  container.innerHTML = '';
  loadContentNetworkFirst();
});

重启 server

npm start

刷新浏览器激活新的 service worker,并再次刷新页面。

Ctrl + C 把 app 变为离线状态

添加一条 event

重启 server

npm start

这时你应该能立即收到一条数据更新的通知,检查 server-data/events.json 中的数据是否已经更新。

说明

页面加载的时候会请求 /api/getAll,我们拦截了这个请求,之后主要做了两件事:

  • 同步本地的离线数据
  • 重新请求 /api/getAll

也就是在重新获取服务端的数据之前先同步

注意:本例中的网络请求设计的非常简单,实际情况下你可能需要考虑更多因素去减少请求的数量。

添加删除功能

下面的时间就交给你了,添加一个删除的功能,记得删除 IndexedDB 中的数据。

查看原文

赞 3 收藏 2 评论 0

shoyuf 发布了文章 · 2019-05-24

用中文说说 git stash

当你使用 git 正在开发一个功能的时候,如果你突然需要到另一个分支去开发却不想放弃当前的改动的时候,你可以使用 git stash

命令

git stash list

列出所有储藏

git stash show [<stash>:Number]

显示某一个(默认最近一个)储藏详情

git stash drop [-q|--quiet][<stash>]

删除某一个(默认最近一个)储藏

git stash ( pop | apply ) [--index][-q|--quiet] [<stash>]

恢复储藏并删除 (pop) / 不删除 (apply) 恢复的储藏

git stash branch <branchname> [<stash>]

从储藏创建分支

git stash [push [-p|--patch]-k|--[no-]keep-index] [-q|--quiet] [-u|--include-untracked] [-a|--all] [-m|--message <message>] [--] [<pathspec>…​]]

储藏,但默认不会储藏未跟踪的文件和被忽略的文件

git stash clear

删除所有储藏

git stash create [<message>]

创建一个悬空提交 (dangling commit),不会将 ref 存储在任何地方,使用 git stash store 保存它

git stash store [-m|--message <message>][-q|--quiet] <commit>

存储上一个命令中创建的悬空提交

参数

  • -q|--quiet 静默模式
  • -p|--patch 以 patch 方式 push stash
  • -k|--[no-]keep-index 保留 index 序号
  • -u|--include-untracked untracked 状态的文件也会被 push
  • -a|--all untracked 和 ignored 的文件也会被 push
  • -m|--message <message> 输出 stash 信息
  • -- [<pathspec>] 针对特定的路径 push

参考资料:

  1. Git - git-stash Documentation
  2. Git - 储藏(Stashing)
  3. “git stash create x” - Where is it? - StackOverflow

本文首发地址

blog.shoyuf.top

查看原文

赞 7 收藏 4 评论 0

shoyuf 回答了问题 · 2019-03-11

解决Vue中,如何实现在main.js实列化vue前,其他js配置文件中的vue-router也能生效

可以把 router 单独到一个文件中 export 出来,如果拦截器先加载完毕可以调整加载顺序或者直接在拦截器中使用 location.href

关注 6 回答 5

认证与成就

  • 获得 78 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-03
个人主页被 1.1k 人浏览