编程技术文章分享与教程

网站首页 > 技术文章 正文

「实践」Deno bytes 模块全解析 decode bytes

hmc789 2024-11-10 10:38:12 技术文章 2 ℃


作者:semlinker

转发连接:https://mp.weixin.qq.com/s/VsuPz2DC0DjtEzWqC0tJXA

目录

Deno 正式发布,彻底弄明白和 node 的区别

「干货」通俗易懂的Deno 入门教程

「干货」了不起的 Deno 实战教程

「干货」Deno TCP Echo Server 是怎么运行的?

前言

本文在介绍 ArrayBuffer 和 TypedArray 的基础上,详细剖析了 Deno bytes 模块的功能与具体实现,并站在 v8 的角度简单分析了 JSArrayBuffer 和 JSTypedArray 类。

一、基础知识

1.1 ArrayBuffer

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

ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。

这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。

来源:https://www.zhihu.com/question/30401979

语法

new ArrayBuffer(length)

  • 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节。
  • 返回值:一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。
  • 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。

示例

下面的例子创建了一个 8 字节的缓冲区,并使用一个 Int32Array 来引用它:

let buffer = new ArrayBuffer(8);
let view   = new Int32Array(buffer);

从 ECMAScript 2015 开始,ArrayBuffer 对象需要用 new 运算符创建。如果调用构造函数时没有使用 new,将会抛出 TypeError 异常。比如执行该语句 let ab = ArrayBuffer(10) 将会抛出以下异常:

VM109:1 Uncaught TypeError: Constructor ArrayBuffer requires 'new'
    at ArrayBuffer (<anonymous>)
    at <anonymous>:1:10

对于一些常用的 Web API,如 FileReader API 和 Fetch API 底层也是支持 ArrayBuffer,这里我们以 FileReader API 为例,看一下如何把 File 对象读取为 ArrayBuffer 对象:

const reader = new FileReader();

reader.onload = function(e) {
  let arrayBuffer = reader.result;
}

reader.readAsArrayBuffer(file);

1.2 Unit8Array

Uint8Array 数组类型表示一个 8 位无符号整型数组,创建时内容被初始化为 0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。

语法

new Uint8Array(); // ES2017 最新语法
new Uint8Array(length); // 创建初始化为0的,包含length个元素的无符号整型数组
new Uint8Array(typedArray);
new Uint8Array(object);
new Uint8Array(buffer [, byteOffset [, length]]);

示例

// new Uint8Array(length); 
var uint8 = new Uint8Array(2);
uint8[0] = 42;
console.log(uint8[0]); // 42
console.log(uint8.length); // 2
console.log(uint8.BYTES_PER_ELEMENT); // 1

// new TypedArray(object); 
var arr = new Uint8Array([21,31]);
console.log(arr[1]); // 31

// new Uint8Array(typedArray);
var x = new Uint8Array([21, 31]);
var y = new Uint8Array(x);
console.log(y[0]); // 21

// new Uint8Array(buffer [, byteOffset [, length]]);
var buffer = new ArrayBuffer(8);
var z = new Uint8Array(buffer, 1, 4);

// new TypedArray(object); 
// 当传入一个 object 作为参数时,就像通过 TypedArray.from() 
// 方法创建一个新的类型化数组一样。
var iterable = function*(){ yield* [1,2,3]; }(); 
var uint8 = new Uint8Array(iterable); 
// Uint8Array[1, 2, 3]

1.3 ArrayBuffer 和 TypedArray

ArrayBuffer 本身只是一行 0 和 1 串。ArrayBuffer 不知道该数组中第一个元素和第二个元素之间的分隔位置。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)

为了提供上下文,实际上要将其分解为多个盒子,我们需要将其包装在所谓的视图中。可以使用类型数组添加这些数据视图,并且你可以使用许多不同类型的类型数组。

例如,你可以有一个 Int8 类型的数组,它将把这个数组分成 8-bit 的字节数组。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)

或者你也可以有一个无符号 Int16 数组,它会把数组分成 16-bit 的字节数组,并且把它当作无符号整数来处理。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)

你甚至可以在同一基本缓冲区上拥有多个视图。对于相同的操作,不同的视图会给出不同的结果。

例如,如果我们从这个 ArrayBuffer 的 Int8 视图中获取 0 & 1 元素的值(-19 & 100),它将给我们与 Uint16 视图中元素 0 (25837)不同的值,即使它们包含完全相同的位。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers)

这样,ArrayBuffer 基本上就像原始内存一样。它模拟了使用 C 之类的语言进行的直接内存访问。你可能想知道为什么我们不让程序直接访问内存,而是添加了这种抽象层,因为直接访问内存将导致一些安全漏洞

1.4 v8 句柄

句柄提供对 JavaScript 对象在堆中位置的引用。V8 垃圾收集器回收了无法再访问的对象所使用的内存。在垃圾收集过程中,垃圾收集器通常将对象移动到堆中的不同位置。当垃圾收集器移动对象时,垃圾收集器还会使用对象的新位置来更新所有引用该对象的句柄。

如果无法从 JavaScript 访问对象并且没有引用该对象的句柄,则该对象被视为垃圾。垃圾收集器会不时删除所有被视为垃圾的对象。V8 的垃圾回收机制是 V8 性能的关键。

句柄在 V8 中只是一个统称,它其实还分为多种类型:

  • 本地句柄(v8::Local):本地句柄保存在堆栈中,并在调用适当的析构函数时被删除。这些句柄的生存期由一个句柄作用域决定,该作用域通常是在函数调用开始时创建的。删除句柄作用域后,垃圾回收器可以自由地释放先前由句柄作用域中的句柄引用的那些对象,前提是它们不再可从 JavaScript 或其他句柄访问。
  • 持久句柄(v8::Persistent):持久句柄提供对堆分配的 JavaScript 对象的引用,就像本地句柄一样。有两种类型,它们处理的引用的生存期管理不同。当需要为多个函数调用保留对一个对象的引用时,或者当句柄生存期不对应于 C++ 作用域时,请使用持久句柄。
  • 永生句柄(v8::Eternal):Eternal 适用于永远不会被删除的 JavaScript 对象的持久句柄。它的使用成本更低,因为它使垃圾回收器不必确定对象的活动性。
  • 其他句柄

用一个更形象的比喻,那么 v8::Local 更像是 JavaScript 中的 let 。在 V8 中,内存的分配都交付给了 V8,那么我们就最好不要使用自己的 new 方法来创建对象,而是使用 v8::Local 里的各种方法来创建一个对象。由 v8::Local 创建的对象,能够被 v8 自动进行管理,也就是传说中的GC (垃圾清理机制)。

Persistent 代表的是持久的意思,更类似全局变量,申请和释放一定要记得使用:Persistent::New,Persistent::Dispose 这两个方法,否则会内存侧漏。

来源于:https://zhuanlan.zhihu.com/p/35371048 —— V8概念以及编程入门

二、Bytes 模块详解

bytes 模块旨在为字节切片的操作提供支持,接下来我们将逐一分析该模块提供的所有方法。

2.1 repeat

作用:重复给定二进制数组的字节并返回新的二进制数组。

使用示例:

import { repeat } from "https://deno.land/std/bytes/mod.ts";

repeat(new Uint8Array([1]), 3); // returns Uint8Array(3) [ 1, 1, 1 ]

源码实现:

// std/bytes/mod.ts
import { copyBytes } from "../io/util.ts";

export function repeat(b: Uint8Array, count: number): Uint8Array {
  if (count === 0) {
    return new Uint8Array();
  }

  if (count < 0) {
    throw new Error("bytes: negative repeat count");
  } else if ((b.length * count) / count !== b.length) {
    throw new Error("bytes: repeat count causes overflow");
  }

  const int = Math.floor(count);

  if (int !== count) {
    throw new Error("bytes: repeat count must be an integer");
  }

  // 根据源Uint8Array的长度与重复次数来创建新的空间 
  const nb = new Uint8Array(b.length * count);

  // 执行字节拷贝任务
  let bp = copyBytes(b, nb);

  for (; bp < nb.length; bp *= 2) {
    copyBytes(nb.slice(0, bp), nb, bp);
  }

  return nb;
}

在以上代码中,会对 count 参数的值进行各种校验,从而保证代码的安全性。之后,会根据源Uint8Array的长度与重复次数来创建新的空间,然后使用封装 copyBytes 方法执行字节拷贝操作。这里我们从 V8 的角度来简单认识一下 ArrayBuffer 和 Uint8Array 。

在 src/api/api.h 文件中,我们可以看到 DECLARE_OPEN_HANDLE 和 OPEN_HANDLE_LIST 这两个宏:

// src/api/api.h 
#define DECLARE_OPEN_HANDLE(From, To)                              \
  static inline v8::internal::Handle<v8::internal::To> OpenHandle( \
      const From* that, bool allow_empty_handle = false);

  OPEN_HANDLE_LIST(DECLARE_OPEN_HANDLE)
    
#define OPEN_HANDLE_LIST(V)                    \
  V(Template, TemplateInfo)                    \
  V(ArrayBuffer, JSArrayBuffer)                \
  V(ArrayBufferView, JSArrayBufferView)        \
  V(TypedArray, JSTypedArray)                  \
  V(Uint8Array, JSTypedArray)                  \
  V(Uint8ClampedArray, JSTypedArray)           \
  V(Int8Array, JSTypedArray)                   \
  V(Uint16Array, JSTypedArray)                 \
  V(Int16Array, JSTypedArray)                  \
  V(Uint32Array, JSTypedArray)                 \
  V(Int32Array, JSTypedArray)                  \
  V(Float32Array, JSTypedArray)                \
  V(Float64Array, JSTypedArray)                \
  V(DataView, JSDataView)                      \
  V(SharedArrayBuffer, JSArrayBuffer)          \
  ...

接着我们来看一下 ArrayBuffer 和 Uint8Array 经过宏替换后的结果:

static inline v8::internal::Handle<v8::internal::JSArrayBuffer> OpenHandle( \
      const ArrayBuffer* that, bool allow_empty_handle = false);

static inline v8::internal::Handle<v8::internal::JSTypedArray> OpenHandle( \
      const Uint8Array* that, bool allow_empty_handle = false);

下面我们顺藤摸瓜,先找到 JSArrayBuffer 类:

// src/objects/js-array-buffer.h
namespace v8 {
namespace internal {

class ArrayBufferExtension;

class JSArrayBuffer
    : public TorqueGeneratedJSArrayBuffer<JSArrayBuffer, JSObject> {
 public:
// V8支持的的JSArrayBuffer的最大长度
// 在32位架构上,我们将此限制为2GB。因此,我们可以继续使用
// Unsigned31 校验边界来限制其最大长度。      
#if V8_HOST_ARCH_32_BIT
  static constexpr size_t kMaxByteLength = kMaxInt;
#else
// 对于非32位架构,如64位架构,最大值为2^53-1    
  static constexpr size_t kMaxByteLength = kMaxSafeInteger;
#endif
  }
}

上述代码中 kMaxSafeInteger 的定义如下:

// ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER
constexpr uint64_t kMaxSafeIntegerUint64 = 9007199254740991;  // 2^53-1
constexpr double kMaxSafeInteger = static_cast<double>(kMaxSafeIntegerUint64);

这里知道对于非 32 位架构,JSArrayBuffer 的大小最大为 2^53-1。那为什么是这个值呢?这里我们得先来了解一下 Number.MAX_SAFE_INTEGER 常量,它表示在 JavaScript 中最大的安全整数(2^53-1)。

MAX_SAFE_INTEGER 是一个值为 9007199254740991 的常量。因为 JavaScript 的数字存储使用了 IEEE 754 中规定的双精度浮点数数据类型,而这一数据类型能够安全存储 -(2^53 - 1) 到 2^53 - 1 之间的数值(包含边界值)。

这里安全存储的意思是指能够准确区分两个不相同的值,例如 Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 将得到 true 的结果,而这在数学上是错误的,参考 Number.isSafeInteger() 获取更多信息。

前面我们已经提到了创建 ArrayBuffer 的语法是:new ArrayBuffer(length),其中参数 length 的类型是 Number 类型,所以其对应的最大的安全整数为(2^53-1)。

介绍完上述内容我们再来看一下 repeat 函数中的 (b.length * count) / count !== b.length 这行代码:

// std/bytes/mod.ts
export function repeat(b: Uint8Array, count: number): Uint8Array {
  if (count < 0) {
      throw new Error("bytes: negative repeat count");
  } else if ((b.length * count) / count !== b.length) {
      throw new Error("bytes: repeat count causes overflow");
  }
  //...
}

为什么通过 (b.length * count) / count !== b.length 这行代码可以判断是否越界呢?这里废话不多说,我们直接看以下计算结果:

(9007199254740991 * 1.1) / 1.1
9007199254740990

好了,下面我们继续介绍如何创建 Handle<JSTypedArray> 句柄:

// src/heap/factory.cc
Handle<JSTypedArray> Factory::NewJSTypedArray(ExternalArrayType type,
                                              Handle<JSArrayBuffer> buffer,
                                              size_t byte_offset,
                                              size_t length) {
  size_t element_size;
  ElementsKind elements_kind;
  ForFixedTypedArray(type, &element_size, &elements_kind);
  size_t byte_length = length * element_size;

  CHECK_LE(length, JSTypedArray::kMaxLength);
  CHECK_EQ(length, byte_length / element_size);
  CHECK_EQ(0, byte_offset % ElementsKindToByteSize(elements_kind));

  Handle<Map> map;
  switch (elements_kind) {
#define TYPED_ARRAY_FUN(Type, type, TYPE, ctype)                              \
  case TYPE##_ELEMENTS:                                                       \
    map =                                                                     \
        handle(isolate()->native_context()->type##_array_fun().initial_map(), \
               isolate());                                                    \
    break;

    TYPED_ARRAYS(TYPED_ARRAY_FUN)
#undef TYPED_ARRAY_FUN

    default:
      UNREACHABLE();
  }
  Handle<JSTypedArray> typed_array =
      Handle<JSTypedArray>::cast(NewJSArrayBufferView(
          map, empty_byte_array(), buffer, byte_offset, byte_length));
  typed_array->set_length(length);
  typed_array->SetOffHeapDataPtr(isolate(), buffer->backing_store(),
                                 byte_offset);
  return typed_array;
}

通过观察上述代码,我们可以知道再创建 Handle<JSTypedArray> 句柄时,会先使用 NewJSArrayBufferView 对 JSArrayBuffer 对象进行封装,然后再调用 Handle<JSTypedArray>::cast 方法把 NewJSArrayBufferView 对象转换为最终的 Handle<JSTypedArray> 。

这里也进一步印证前面提到的内容:即 ArrayBuffer 不能直接操作,而是要通过 TypedArray 对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

2.2 concat

作用:合并两个二进制数组并返回新的二进制数组。

使用示例:

import { concat } from "https://deno.land/std/bytes/mod.ts";

concat(new Uint8Array([1, 2]), new Uint8Array([3, 4]));
// returns Uint8Array(4) [ 1, 2, 3, 4 ]

源码实现:

export function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
  const output = new Uint8Array(a.length + b.length);
  output.set(a, 0);
  output.set(b, a.length);
  return output;
}

在 concat 方法体中,Uint8Array 对象的 set 方法用于从指定数组中读取值,并将其存储在类型化数组中。该方法的签名是:

typedarray.set(array[, offset])
typedarray.set(typedarray[, offset])

其中 offset 参数是可选的,该参数指定从什么地方开始使用源数组的值进行写入操作。如果忽略该参数,则默认为 0(也就是说,从目标数组的下标为 0 处开始,使用源数组的值覆盖重写)。

2.3 findIndex

作用:从给定的二进制数组中查找二进制模式的第一个索引。

使用示例:

import { findIndex } from "https://deno.land/std/bytes/mod.ts";

findIndex(
  new Uint8Array([1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 3]),
  new Uint8Array([0, 1, 2])
);
// => returns 2

源码实现:

// std/bytes/mod.ts
export function findIndex(a: Uint8Array, pat: Uint8Array): number {
  const s = pat[0];
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== s) continue;
    // 记录第一个匹配元素的位置
    const pin = i;
    // 已匹配的元素个数
    let matched = 1;
    let j = i;
    // 循环匹配其余的元素
    while (matched < pat.length) {
      j++;
      if (a[j] !== pat[j - pin]) {
        break;
      }
      matched++;
    }
    if (matched === pat.length) {
      return pin;
    }
  }
  return -1;
}

2.4 findLastIndex

作用:从给定的二进制数组中查找二进制模式的最后一个索引。

使用示例:

import { findLastIndex } from "https://deno.land/std/bytes/mod.ts";

findLastIndex(
  new Uint8Array([0, 1, 2, 0, 1, 2, 0, 1, 3]),
  new Uint8Array([0, 1, 2])
);
// => returns 3

源码实现:

// std/bytes/mod.ts
export function findLastIndex(a: Uint8Array, pat: Uint8Array): number {
  const e = pat[pat.length - 1];
  for (let i = a.length - 1; i >= 0; i--) {
    if (a[i] !== e) continue;
    const pin = i;
    let matched = 1;
    let j = i;
    while (matched < pat.length) {
      j--;
      if (a[j] !== pat[pat.length - 1 - (pin - j)]) {
        break;
      }
      matched++;
    }
    if (matched === pat.length) {
      return pin - pat.length + 1;
    }
  }
  return -1;
}

2.5 equal

作用:检查给定的二进制数组是否相等。

使用示例:

import { equal } from "https://deno.land/std/bytes/mod.ts";

equal(new Uint8Array([0, 1, 2, 3]), new Uint8Array([0, 1, 2, 3])); // returns true
equal(new Uint8Array([0, 1, 2, 3]), new Uint8Array([0, 1, 2, 4])); // returns false

源码实现:

// std/bytes/mod.ts
export function equal(a: Uint8Array, match: Uint8Array): boolean {
  // 优先判断TypedArray数组的长度是否相等
  if (a.length !== match.length) return false;
  // 对TypedArray数组的每一项进行比对
  for (let i = 0; i < match.length; i++) {
    if (a[i] !== match[i]) return false;
  }
  return true;
}

2.6 hasPrefix

作用:检查二进制数组是否具有二进制前缀。

使用示例:

import { hasPrefix } from "https://deno.land/std/bytes/mod.ts";

hasPrefix(new Uint8Array([0, 1, 2]), new Uint8Array([0, 1])); // returns true
hasPrefix(new Uint8Array([0, 1, 2]), new Uint8Array([1, 2])); // returns false

源码实现:

// std/bytes/mod.ts
export function hasPrefix(a: Uint8Array, prefix: Uint8Array): boolean {
  for (let i = 0, max = prefix.length; i < max; i++) {
    if (a[i] !== prefix[i]) return false;
  }
  return true;
}

推荐Vue学习资料文章:

细品pdf.js实践解决含水印、电子签章问题「Vue篇」

基于vue + element的后台管理系统解决方案

Vue仿蘑菇街商城项目(vue+koa+mongodb)

基于 electron-vue 开发的音乐播放器「实践」

「实践」Vue项目中标配编辑器插件Vue-Quill-Editor

消息队列助你成为高薪 Node.js 工程师

Node.js 中的 stream 模块详解

「干货」Deno TCP Echo Server 是怎么运行的?

「干货」了不起的 Deno 实战教程

「干货」通俗易懂的Deno 入门教程

Deno 正式发布,彻底弄明白和 node 的区别

「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台

「实践」深入对比 Vue 3.0 Composition API 和 React Hooks

前端网红框架的插件机制全梳理(axios、koa、redux、vuex)

深入Vue 必学高阶组件 HOC「进阶篇」

深入学习Vue的data、computed、watch来实现最精简响应式系统

10个实例小练习,快速入门熟练 Vue3 核心新特性(一)

10个实例小练习,快速入门熟练 Vue3 核心新特性(二)

教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」

2020前端就业Vue框架篇「实践」

详解Vue3中 router 带来了哪些变化?

Vue项目部署及性能优化指导篇「实践」

Vue高性能渲染大数据Tree组件「实践」

尤大大细品VuePress搭建技术网站与个人博客「实践」

10个Vue开发技巧「实践」

是什么导致尤大大选择放弃Webpack?【vite 原理解析】

带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】

带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】

实践Vue 3.0做JSX(TSX)风格的组件开发

一篇文章教你并列比较React.js和Vue.js的语法【实践】

手拉手带你开启Vue3世界的鬼斧神工【实践】

深入浅出通过vue-cli3构建一个SSR应用程序【实践】

怎样为你的 Vue.js 单页应用提速

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

【新消息】Vue 3.0 Beta 版本发布,你还学的动么?

Vue真是太好了 壹万多字的Vue知识点 超详细!

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

手把手教你深入浅出vue-cli3升级vue-cli4的方法

Vue 3.0 Beta 和React 开发者分别杠上了

手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件

Vue3 尝鲜

总结Vue组件的通信

手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】

Vue 开源项目 TOP45

2020 年,Vue 受欢迎程度是否会超过 React?

尤雨溪:Vue 3.0的设计原则

使用vue实现HTML页面生成图片

实现全栈收银系统(Node+Vue)(上)

实现全栈收银系统(Node+Vue)(下)

vue引入原生高德地图

Vue合理配置WebSocket并实现群聊

多年vue项目实战经验汇总

vue之将echart封装为组件

基于 Vue 的两层吸顶踩坑总结

Vue插件总结【前端开发必备】

Vue 开发必须知道的 36 个技巧【近1W字】

构建大型 Vue.js 项目的10条建议

深入理解vue中的slot与slot-scope

手把手教你Vue解析pdf(base64)转图片【实践】

使用vue+node搭建前端异常监控系统

推荐 8 个漂亮的 vue.js 进度条组件

基于Vue实现拖拽升级(九宫格拖拽)

手摸手,带你用vue撸后台 系列二(登录权限篇)

手摸手,带你用vue撸后台 系列三(实战篇)

前端框架用vue还是react?清晰对比两者差异

Vue组件间通信几种方式,你用哪种?【实践】

浅析 React / Vue 跨端渲染原理与实现

10个Vue开发技巧助力成为更好的工程师

手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】

1W字长文+多图,带你了解vue的双向数据绑定源码实现

深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现

手把手教你D3.js 实现数据可视化极速上手到Vue应用

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】

Vue3.0权限管理实现流程【实践】

后台管理系统,前端Vue根据角色动态设置菜单栏和路由

作者:semlinker

转发连接:https://mp.weixin.qq.com/s/VsuPz2DC0DjtEzWqC0tJXA

Tags:

标签列表
最新留言