zone.js由入门到放弃之一——通过一场游戏认识zone.js

单词乱序游戏设计原理:从认知科学到自适应难度调控 单词乱序游戏(Jumbled Word Game)并非简单的字母重排,而是基于词形识别、音节结构与语义网络的词汇学习工具。其核心原理在于控制认知负荷——通过首尾字母保护维持词形线索,采用音节级扰动替代随机打乱,并结合词根词缀、常见错误模式生成高信度干扰项。技术价值体现在动态难度调控(α系数)与实时反馈闭环,使练习始终落在最近发展区(ZPD)。广泛应用于K12词汇教学、中高考易混词训练及雅思学术词汇巩固等场景,真正实现‘玩中诊断、练中强化’。 阅读详情

之前有写过一些介绍Angular中一些理念的文章,接下来我们来聊聊Angular中的一些依赖,比如zone.js。它是一个跨多个异步任务的执行上下文,在拦截或追踪异步任务方面有着特别强大的能力。来跟着啸达同学的文章,一起了解一下吧~

前言

最近一段时间因为工作上的安排,需要研究Angular中的一些内部机制和模块。Angular作为一个专门为大型前端项目而设计的优秀框架,实际上有很多值得大家学习和借鉴的优点的。之前了解到Angular的变更检测跟Vue和React有本质的区别,而Angular的检测体系是离不开zone.js的,所以本系列就针对zone.js进行一些分享,也希望能够随着个人对zone.js逐步学习制作一个由浅入深地学习指导,欢迎大家积极上车,一起学习、探讨。


1_zone_1676015102729.jpeg

为什么要学习zone.js

这个系列的文章会包含大量的猜想、验证、demo和源码分析。在我自己学习的过程中,也出现过多次想要放弃,或者觉得差不多就行了的想法。所以如果想要坚持一件事,需要有一些明确的动机,毕竟动机不纯,想装纯也难。那么就我个人而言,除了工作上的需要之外,我觉得有以下两点驱动力:

动机一

17年的时候,有幸参加了一次大厂的面试,技面的时候我被问到Angular是如何处理变更检测的。我对这块的知识十分模糊,所以乱答了一气。我把脑子里跟变更检测相关的词都掏了出来,结果越描越黑,最后不能自圆其说。面试官跟我说,希望我以后把这块内容理理顺。我答应了他,但没想到竟是6年以后。

动机二

相信每个Angular的开发者都会见过类似下面这样的报错信息,甚至有些初学Angular的同事也因此觉得Angular的学习曲线比较陡峭、错误信息极不友好。其实,大家认为这些不友好的错误信息正是zone.js强大的地方。在不了解zone.js的前提下,这确实有点反人类。所以希望在学完这个系列后,不仅知道这样的错误意味着什么,还能清楚这样的问题是怎么产生的。

at HTMLButtonElement.throwError (https://zonejs-basic.stackblitz.io/~/index.js:19:11)
at _ZoneDelegate.invokeTask (https://unpkg.com/zone.js:446:35)
at Zone.runTask (https://unpkg.com/zone.js:214:51)
at ZoneTask.invokeTask [as invoke] (https://unpkg.com/zone.js:528:38)
at invokeTask (https://unpkg.com/zone.js:1730:22)
at globalCallback (https://unpkg.com/zone.js:1761:31)
at HTMLButtonElement.globalZoneAwareCallback (https://unpkg.com/zone.js:1797:20)
at ____________________Elapsed_496_ms__At__Fri_Jan_20_2023_16_20_20_GMT_0800_________ (http://localhost)
at Object.onScheduleTask (https://unpkg.com/zone.js@0.8.20/dist/long-stack-trace-zone.js:108:22)
at _ZoneDelegate.scheduleTask (https://unpkg.com/zone.js:426:55)
at Zone.scheduleTask (https://unpkg.com/zone.js:257:47)
at Zone.scheduleEventTask (https://unpkg.com/zone.js:283:29)
at HTMLButtonElement.addEventListener (https://unpkg.com/zone.js:2038:37)
at HTMLButtonElement.bindSecondButton (https://zonejs-basic.stackblitz.io/~/index.js:16:8)
at _ZoneDelegate.invokeTask (https://unpkg.com/zone.js:446:35)
at Zone.runTask (https://unpkg.com/zone.js:214:51)
at ____________________Elapsed_1801_ms__At__Fri_Jan_20_2023_16_20_18_GMT_0800_________ (http://localhost)
at Object.onScheduleTask (https://unpkg.com/zone.js@0.8.20/dist/long-stack-trace-zone.js:108:22)
at _ZoneDelegate.scheduleTask (https://unpkg.com/zone.js:426:55)
at Zone.scheduleTask (https://unpkg.com/zone.js:257:47)
at Zone.scheduleEventTask (https://unpkg.com/zone.js:283:29)
at HTMLButtonElement.addEventListener (https://unpkg.com/zone.js:2038:37)
at main (https://zonejs-basic.stackblitz.io/~/index.js:5:8)
at _ZoneDelegate.invoke (https://unpkg.com/zone.js:412:30)
at Zone.run (https://unpkg.com/zone.js:169:47)

简单认识一下

我不太会抽象概括,幸好Angular团队对zone.js的定义只有一句,但是它简单抽象到让你看了和没看都没什么区别:

A Zone is an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs.

到目前为止,我觉得大家也不必太在意这里描述了个啥,后面我会用其它的方式让你渐渐了解它。这里姑且对几个词有点印象即可:

  • 执行上下文:execute context
  • 保持:persist
  • 异步任务:async tasks
  • 有Java背景的可以回忆一下ThreadLocal的作用和用法;用过JS沙箱的也可以类比一下。都没用过的也不碍事,不影响后面的阅读。

PS:Angular团队对zone.js还有个视频介绍,建议大家可以等看完这篇文章后去了解一下。

ngZone和zone.js,傻傻分不清

最后在真正开始之前我要再补充一个知识点,有些人在学习zone.js的还见过ngZone这个东西,就认为这两个是一个东西。这里做个简单的声明,Angular团队基于zone.js构建了ngZone服务。NgZone定义Angular的执行上下文,可以先简单理解为是一个专门给Angular使用的定制化以后的zone.js。那么关于ngZone的知识,可以关注一下系列四和西系列五(如果有的话),在那里会有对ngZone和Angular具体的变更检测方法做详细介绍。

所以一句话概括两者的关系,ngZone生于zone.js;长于Angular(生于斯,长于斯)。

从一个游戏开始了解zone.js

本文由真实事迹改编,如有雷同,绝非偶然

2022年底,本人工作的部门组织了一场switch对决赛。游戏有A、B两支参赛队伍,每支队伍15人。要求每天两队之间进行3场对决,比赛一共会进行5一天,取得积分优势的团队获胜(输的那队要请赢的队伍吃饭)。主办方提供了3款游戏:第一场回旋镖;第二场马8;第三场明星大乱斗。

如果只是关心哪里有这么好的部门的话,可以直接留言

2_switchgames_1676015798462.png

既然游戏的种类和比赛顺序是固定的,那么每天各队派出的3位参赛选手的顺序就很重要:比如可以让熟悉某款游戏的选手去比对应的游戏;或者通过田忌赛马的方式耗费对方的潜力。那么,我们今天的示例就从队长选人开始:

第一版:无脑排兵被偷窥

这里我们有两支参赛队伍teamA、teamB;这里假设teamA只能顺序排人,teamB只能倒序排人。同时还有一个裁判,负责收集teamA、teamB两支队每天的排序情况。下图中代码大概意思就是,随着裁判一声令下,teamA、teamB(代码AB是顺序执行的,但是读者在这里先别纠结)分别开始排名布阵,排好之后,裁判函数打印两队排序:

// demo0/demo0.js

const teamA = {
  name: 'teamA',
  team: [],
  sort: function() {
    this.team.push(1);
    this.team.push(2);
    this.team.push(3);
  }
};

const teamB = {
  name: 'teamB',
  team: [],
  sort: function() {
    // console.log(`${this.name}偷看${teamA.name}排名布阵, ${teamA.name}当前阵容是: `, teamA.team);
    this.team.push(3);
    this.team.push(2);
    this.team.push(1);
  }
};

function judgement() {
  teamA.sort();
  teamB.sort();

  console.log('teamA: ', teamA.team);
  console.log('teamB: ', teamB.team);
}

judgement();

// teamB偷看teamA排名布阵, teamA当前阵容是:  [ 1, 2, 3 ]
// [console.log] teamA:  [ 1, 2, 3 ]
// [console.log] teamB:  [ 3, 2, 1 ]

但是偏偏有年轻人不讲武德,耗子尾汁,在两队排兵期间偷窥对方阵容:如上文中注释代码所示,teamB的队长在teamA排阵过程中悄悄打印出teamA的阵型,导致teamB队长可以针对性地进行兵力调整,已达到最好的效果。

这里真的想点名批评一下伍队长,不讲武德的人就是你

3_mouse_1676016163588.jpeg

那么造成上述问题的原因主要是因为teamA和teamB对彼此都是可见的,即两队在排兵布阵的过程对对方是完全裸露的,导致让对手有了可乘之机,所以这也是接下来要调整的重点。

第二版:小黑屋中探讨军机

为了不让两队在排兵中知晓对方的阵容,需要将两队进行隔离。JS中进行数据隔离有很多办法,从早期的闭包、后续有了通过模块(文件)进行隔离,到现在在JS中也可以使用面向对象的编程思想。这里,我们就先通过模块将两队隔离起来,文件结构如下:

    ├─demo1
    │  ├─teamA.js
    │  └─teamB.js
    │  └─judgement.js

teamA与tramB中代码类似:

// demo1/teamA.js

const teamA = {
  name: 'teamA',
  team: [],
  sort: function() {
    this.team.push(1);
    this.team.push(2);
    this.team.push(3);
  }
};

module.exports = teamA
// demo1/teamB.js

const teamB = {
  name: 'teamB',
  team: [],
  sort: function() {
    this.team.push(3);
    this.team.push(2);
    this.team.push(1);
  }
};

module.exports = teamB
// demo1/judgement.js

const teamA = require('./teamA');
const teamB = require('./teamB');

function judgement() {
  teamB.sort();
  teamA.sort();

  console.log('teamA: ', teamA.team);
  console.log('teamB: ', teamB.team);
}

judgement();

// teamA:  [ 1, 2, 3 ]
// teamB:  [ 3, 2, 1 ]

这一次,通过裁判程序将teamA和teamB导入,各队排序过程相对独立不受干扰。

第三版:容我想想

隔离的问题虽然解决了,这时teamB的队长觉得每次排序都太仓促了,需要把人员排序的任务领回去跟团队协商一下才行。这里我们使用异步任务来模拟各位队长将任务带回去排序的效果。

文件结构如下:

    ├─demo2
    │  ├─teamA.js
    │  └─teamB.js
    │  └─judgement.js
    │  └─thinking.js

这一次,我们新增一个冥想程序:thinking.js,这里提供一个函数,通过异步的setTimeout随机等待03秒。两位队长针对每次比赛的出场人员顺序进行认真地思考,这里使用延时03秒的thinking.js模块模拟队长做出决定的过程。

// demo2/thinking.js

// 获取0~3随机数
function getRandomSec() {
  return Math.random() * 3;
}

module.exports = function(cb) {
  const random = getRandomSec() * 1000;
  setTimeout(cb, random);
}

队长代码示例:

// demo2/teamA.js

const thinking = require('./thinking');

const teamA = {
  name: 'teamA',
  team: [],
  sort: function() {
    // 此处容我想想
    thinking(() => {
      this.team.push(this.team.length + 1);
    });
    thinking(() => {
      this.team.push(this.team.length + 1);
    });
    thinking(() => {
      this.team.push(this.team.length + 1);
    });
  },
};

module.exports = team
// demo2/teamB.js

const thinking = require('./thinking');

const teamB = {
  name: 'teamB',
  team: [],
  sort: function() {
    thinking(() => {
      this.team.unshift(this.team.length + 1);
    });
    thinking(() => {
      this.team.unshift(this.team.length + 1);
    });
    thinking(() => {
      this.team.unshift(this.team.length + 1);
    });
  },
};

module.exports = team

到这里,两个队长把各组的任务领回去了,可对战室里还有个裁判呢。因为之前AB两组瞬间就把阵容排好了,裁判马上就能知道各队的排序结果。现在大家都回去各排各的了,把裁判一个人晾这了。而且,这个裁判不知道要等多久两位队长才能把人排完(每个队长都需要0~3秒),所以裁判只能无奈地按照最长时间进行等待,即裁判要等待3秒再回来收集大家的结果:

// demo2/judgement.js

const teamA = require('./teamA');
const teamB = require('./teamB');

function judgement() {
  teamB.sort();
  teamA.sort();

  setTimeout(() => {
    console.log('teamA: ', teamA.team);
    console.log('teamB: ', teamB.team);
  }, 3000);
}

judgement();

// 苦等3秒出结果
// teamA:  [ 1, 2, 3 ]
// teamB:  [ 3, 2, 1 ]

第四版:zone.js版本

当你觉得所有人应该都满足的时候,裁判站出来了说他不满意。裁判不愿意一直在那里傻等结果,希望大家能在排序完毕后第一时间通知他,避免浪费时间。接下来就来看一下zone.js是如何解决这些问题的。

PS:这里大家先不要纠结API的用法和一些具体概念,后面的文章会一点一点给大家扫盲,先通过示例感受一下zone.js的功能。

4_waiting_1676016587223.jpg

示范前还是再澄清一下几个比较重要的需求:

  • 两队的排序数据需要隔离
  • 两队排序时需要有思考时间(0~3s)
  • 裁判要第一时间知道两队排序已结束,并公布结果

前文介绍中说过,zone.js的一个关键概念是执行上下文,当时我们说可以把这个异步上下文类比成Java的LocalThread,即可以在单个线程内共享数据。那么在JS中,这个执行上下文也是有类比的,可以把它想象成一个沙箱——一个JS的VM。在这个沙箱中,你可以把你的JS代码放在沙箱中运行,同时沙箱也有一个上下文的概念,这是一段共享的内存空间,可以供运行在沙箱中的代码所使用;同时沙箱和沙箱之间相互隔离、无法相互干扰。

Mark1:创建一个zone,zone.js通过fork方法可以创建一个zone,我们可以先理解它就是一个沙箱。
Mark2:zone.js中有一个静态方法,可以获取到zone,Zone.current

有了这两个方法,就可以分别为teamA和teamB创建两个zone。通过下面示例可见,代码中分别创建两个zone,他们分别持有teamA和teamB的对象,而teamA、B对象保存在zone的properties中。

// demo3/judgement.js

require('zone.js');
const thinking = require('./thinking');

// 创建zone
// Zone.current 获取当前 zone;当前zone为rootZone
// Zone.current.fork  创建一个基于当前zone的子zone
const zoneA = Zone.current.fork({
  // zone的名字
  name: 'teamA',

  // zone中可以通过properties设置一段共享内存
  properties: {
    // teamA对象
    team: {
      name: 'teamA',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
      },
    },
  },
});

const zoneB = Zone.current.fork({
  name: 'teamB',
  properties: {
    team: {
      name: 'teamB',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
      },
    },
  },
});

上面代码中,teamA和teamB不再被分别定义到两个文件中了,为了验证两个队伍的数据是否能够被不同的zone隔离开,我们分别在zoneA和zoneB中执行相同的代码(打印properties中的组名)。为此,我们还需要了解一下zone.js提供的另外两个API。

Mark3:zone.js提供一个run方法,可以在zone中执行一段代码
Mark4:zone.js提供一个get方法,可以获取当前zone的properties属性

// 在zoneA的上下文中执行函数
zoneA.run(() => {
  // 获取当前zone
  const currentZone = Zone.current;
  // 从properties中获取team属性
  const team = currentZone.get('team');
  console.log(team.name); // tramA
});

zoneB.run(() => {
  const currentZone = Zone.current;
  const team = currentZone.get('team');
  console.log(team.name); // tramB
});

可以看到,两个zone中数据相互隔离,在run的作用域中,只能获取到自己zone中的数据。

第一步改造

这里,我们首先做到了让teamA和teamB的数据隔离。两位队长把每队的人员信息都保存在各自的zone中,并在各自zone的上文中执行排序任务。整个任务期间,两个zone互不干涉。

function judgement() {

  // teamA领任务回去
  zoneA.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });

  // teamB领任务回去
  zoneB.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });

  // 裁判3s后收集结果
  setTimeout(() => {
    // 打印teamA的结果
    zoneA.run(() => {
      const currentZone = Zone.current;
      const team = currentZone.get('team');
      console.log('teamA: ', team.team);
    });
    // 打印teamB的结果
    zoneB.run(() => {
      const currentZone = Zone.current;
      const team = currentZone.get('team');
      console.log('teamB: ', team.team);
    });
  }, 3000);
}

judgement();

但是上述代码还有俩个问题:

  • 裁判还是要等待3s后才能知道两位队长的人员排序
  • 由于数据隔离,裁判也不知道两位队长的人员排序结果。裁判只能委托两个队长自己打印排序结果

有没有办法让裁判自己能感知到两位队长何时排序结束,然后在两位队长排序完第一时间宣布排序结果呢?

其实如果大家仔细看下zone.js对fork方法的定义后就能知道,fork实际上只是创建出一个child zone。zone.js在初始化的时候回创建一个根zone,然后所有的通过fork后会在根zone下创建一个子zone。也就是说,zone是具有继承关系的,官方习惯把这种关系叫做zone的可组合性。而每个子zone都保存了其父zone的对象;每个父zone也能监听到子zone的事件。

Mark5:可组合性:每个子zone都保存了其父zone的引用;每个父zone也能监听到子zone的事件。

每个子zone都保存了其父zone的引用这个好理解,那么每个父zone也能监听到子zone的事件怎么理解?其实这个就是zone.js最神奇的地方,zone.js在初始化的时候对很多API都做了“手脚”——Monkey Patch,将这些异步方法封装成了zone.js中的异步任务。同时,由于在这些任务中定义很多勾子函数,导致zone.js可以完全监控这些异步任务的整个生命周期。

Mark6:追踪异步任务

正是由于zone的这种特性,使得zone被经常地用于异步任务的跟踪和调试中。比如上文在动机2中展示的那个难以理解的错误堆栈,就是zone跟踪异步异常的结果。

5_tracking_1676017737697.png

终极改造

最后这一版,我们给裁判也fork出一个zone,而teamA和tramB的zone都是fork自裁判zone的。这么处理后,裁判zone中就可以检测到teamA和tramB中异步任务执行的全部生命周期。其中,示例中只是用了zone.js众多勾子中的一个——onHasTask。这个函数会在执行队列中加入函数或没有函数时被调用。

本例中,teamA执行完毕后会把执行结果更新到裁判zone中;teamB也做同样的事。当两队都结束排序后,裁判zone通过配置的回调函数第一时间打印两位队长的排序结果。至此,该示例满足了我们上述的所有需求。

源码奉上:
require('zone.js');
const thinking = require('./thinking');

// 创建一个裁判zone,当做teamA和teamB的父zone
const zoneJudgement = Zone.current.fork({
  name: 'judgement',
  properties: {
    // 存放teamA、teamB的排序结果
    result: [],
  },

  // 异步任务状态改变时的回调
  onHasTask: function (parentZoneDelegate, currentZone, targetZone, hasTaskState) {
    
    // setTimeout属于宏任务,!hasTaskState.macroTask标识有宏任务执行完毕
    if (!hasTaskState.macroTask) {
      // 裁判任务执行结束
      switch (targetZone.name) {
        case 'judgement':
          console.log(currentZone.get('result'));
          break;
        // A组排序任务执行结束
        case 'teamA':
          currentZone.get('result').push({
            teamA: targetZone.get('team').team,
          });
          break;
        // B组排序任务执行结束
        case 'teamB':
          currentZone.get('result').push({
            teamB: targetZone.get('team').team,
          });
          break;
        default:
          break;
      }
    }
    // 事件上抛
    parentZoneDelegate.onHasTask(parentZoneDelegate, currentZone, targetZone, hasTaskState);
  }
});

const zoneA = zoneJudgement.fork({
  name: 'teamA',
  properties: {
    team: {
      name: 'teamA',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
      },
    },
  },
});
const zoneB = zoneJudgement.fork({
  name: 'teamB',
  properties: {
    team: {
      name: 'teamB',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
      },
    },
  },
});

function judgement() {
  zoneA.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });

  zoneB.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });
}

zoneJudgement.run(judgement); // [ { teamA: [ 1, 2, 3 ] }, { teamB: [ 3, 2, 1 ] } ]

总结

自此,通过一个小例子展示了一下zone.js的功能,同时根据例子浅述了一下zone.js的几个特点。前文为了方便理解,一直把zone.js类比成LocalThread或者沙箱。其实,zone.js的能力远不止这些类比的对象,它还被大量用在处理异步任务和异步错误的跟踪中。至此,大家别忘了看下Angular团队给出的zone.js的视频介绍,可以更好地加深一下对本文的印象。

接下来,对于zone这个名字,个人感觉起的很到位(老外起名字总是很考究的)。zone被翻译成地区、区域。就拿我们国家的区域划分来说,国家、省、市、区、街道…每个同级的地区划分都是相互隔离的,一级一级区域划分又是可以嵌套的。不得不说,这种嵌套又隔离的特点在上面示例中展示的淋漓尽致。

6_map_1676018597784.png

这是本系列的第一篇文章,只是浅浅地入门了一下zone.js,后续会针对zone.js的API、源码、以及如何跟Angular配合做进一步分析说明,感兴趣的可以蹲个续~

关于 OpenTiny

OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 移动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,拥有主题配置系统 / 中后台模板 / CLI 命令行等效率提升工具,可帮助开发者高效开发 Web 应用。

核心亮点:

  1. 跨端跨框架:使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。
  2. 组件丰富:PC 端有100+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等
  3. 配置式组件:组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化
  4. 周边生态齐全:提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme

联系我们:

更多视频内容也可以关注OpenTiny社区,B站/抖音/小红书/视频号。

OmniBox Docker部署指南:影视服务中枢的容器化实践 影视聚合服务正从传统脚本部署迈向标准化容器化运维。OmniBox作为TVBox等客户端背后的影视调度中枢,其核心价值在于通过Docker实现环境一致性、配置可移植与服务可编排。它基于Node.js构建三层架构(数据层、服务层、协议层),深度适配TVBox JSON协议,并融合TMDB元数据增强能力。技术上依托Alpine+Node.js 18+静态FFmpeg镜像,规避系统依赖;工程上通过docker-compose实现声明式运维与一键交付。适用于私有影视库搭建、TVBox源管理及二次开发场景,是家庭NAS 阅读详情

相关推荐

Open UI5 源代码解析之1207:LRepConnector.js

文章摘要: LRepConnector.js是SAP OpenUI5框架中一个关键的后端连接器模块,位于contentbrowser支持工具中,负责前端与分层仓库(LRep)的通信。该文件采用UI5模块化设计,封装了读取(getContent)、保存(saveFile)和删除(deleteFile)核心功能,并处理CSRF令牌管理、URL拼接和错误统一处理。其设计注重与SAP后端生态的兼容性,通过参数化分层访问、元数据查询等特性支持灵活的内容管理,同时遵循企业级安全规范。作为支持工具的基础设施层,它在保证可

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 853

Angular系列教程之zone.js和NgZone

在Angular中,zone.js是一个非常重要的库,它为我们提供了一种跟踪和管理异步操作的机制。它的核心概念是Zone,它可以帮助我们捕获和处理异步操作的上下文。当我们执行异步操作(例如定时器、网络请求或者订阅Observables)时,往往需要确保这些操作可以正确地传播并影响到整个应用程序。Zone就像是一个可观察范围,它将所有相关的异步操作包裹在内,并且能够追踪和管理它们。

zzcoder的博客 1740

通用虚拟盲盒系统:概率引擎与游戏化激励中间件

虚拟盲盒系统本质是一种基于不确定性的行为激励机制,其核心在于可配置的概率分配、事务安全的状态流转与低延迟的前端反馈。它依托加权随机算法实现数学可信的掉落控制,通过MySQL行锁+Redis缓存构建高并发抗压架构,并以WebAssembly签名、Nginx Lua网关和存储过程状态机构筑三层防刷体系。该技术已广泛应用于教育SaaS的关卡解锁、硬件零售的实体奖品发放及音乐社区的插件试用分发等场景,成为连接用户行为与数字资产价值的关键中间件。文中重点解析了‘Fl盲盒’交互性能优化与‘硬件盲盒’地理围栏集成方案。

weixin_30239339的博客 377

Zone.js:JavaScript的Zone增强库

Zone.js是一个由Angular团队维护的开源项目,专门用于JavaScript环境中的异步执行上下文管理。它基于“Zones”的概念,提供了一种跨库、跨框架跟踪异步操作的强大方式。项目主要采用TypeScript进行开发,同时也兼容JavaScript。通过在异步边界上插入额外的逻辑,Zone.js能够改变并扩展JavaScript的事件循环机制,帮助开发者更好地理解和控制异步流程。 ##...

gitblog_00365的博客 496

AI智能体赋能Cypress:自动化前端测试的新范式

自动化测试是现代软件工程保障质量的核心实践,其原理是通过脚本模拟用户操作,对软件功能进行验证。随着大语言模型(LLM)和智能体(Agent)技术的发展,测试自动化正迈向智能化新阶段。智能体能够理解自然语言描述的测试需求,并自主规划、执行测试步骤,这为应对前端UI频繁变更、测试用例维护成本高等挑战提供了新思路。在工程实践中,将Cypress这一流行的前端测试框架封装为智能体可调用的标准化技能(Skill),是实现这一愿景的关键。通过设计清晰的交互协议和状态管理机制,智能体可以像人类测试工程师一样“感知”页面状

weixin_33726318的博客 686

保姆级教程:在Ubuntu 22.04上从零安装SUMO并运行第一个交通仿真

本教程详细介绍了在Ubuntu 22.04系统上从零安装SUMO交通仿真软件并运行第一个仿真的完整流程。涵盖环境准备、PPA源安装、核心组件验证、首次仿真案例运行以及进阶配置技巧,特别针对netedit路网编辑器和地图格式转换等实用工具进行重点讲解,帮助用户快速掌握SUMO在交通仿真领域的应用。

weixin_30546189的博客 422

Zone.js 使用教程

Zone.js 是一个由 Angular 团队开发的 JavaScript 库,它为 JavaScript 提供了执行上下文,可以在异步任务之间进行持久性传递。Zone.js 的设计灵感来源于 Dart 语言,它类似于 Java 中的 TLS(线程本地存储)技术,但适用于 JavaScript 环境。Zone.js 通过猴子补丁(Monkey-patched)的方式,将 JavaScript 中的

gitblog_00380的博客 468

STM32H7 Flash擦除后数据读取异常的缓存问题解析

本文深入分析了STM32H7系列芯片在Flash擦除后数据读取异常的缓存问题,揭示了由于数据缓存(D-Cache)和指令缓存(I-Cache)导致的数据一致性问题。通过详细的解决方案,包括手动维护缓存一致性和操作期间禁用缓存,帮助开发者有效解决这一常见但棘手的嵌入式开发难题。

weixin_29158413的博客 392

zone.js由入门到放弃之三——zone.js 源码分析【setTimeout篇】

分析zone.js源码的过程是痛苦的,光从思维图上就可以看出,zone.js的绝大多数逻辑都是围绕Zone、ZoneDelegate、ZoneTask展开的。这兄弟三个之间相互引用、相互依赖,即使在我省略掉很多代码之后还是存在很多错综复杂的调用关系。如果你是一个颈椎病患者,那么建议你可以深度体验一下,你的脖子大概率会问候一下zone.js的全体研发团队。今天这篇文章其实只分析setTimeout的Patch逻辑,zone.js其实对很多其它API也都下手了。

OpenTiny的博客 346

python 写一个简单的 telegram bot (电报机器人)

replit.com 环境配置太恶心了,只能使用 poetry.poetry 这个名字是挺好听,用起来也是很垃圾,太恶心。heroku.com 注册登录不方便,需要各种验证。所以,部署的问题,目前搁置。

waterHBO的博客 3197

腾讯会议视频过期了怎么办?亲测可用:用这个免费插件抢救你的会议录制与回放

本文提供了三种亲测有效的腾讯会议视频过期恢复方案,包括浏览器缓存提取、专业工具组合及预防性措施。特别推荐使用免费插件抢救过期会议录制与回放,帮助职场人解决重要视频资料丢失的难题。

weixin_33686714的博客 264

zone.js由入门到放弃之四——Angular对zone.js的应用

Angular其实并没有对zone.js的框架或是核心做任何改动,只是利用zone.js的执行上下文来监听异步事件,从而指导Angular在合适的时机进行变更检测。

OpenTiny的博客 483

Debezium报错处理系列之二十一:Unable to connect: Communications link failure.The driver has not received any

Debezium报错系列之二十一:Unable to connect: Communications link failure.The driver has not received any。

zhengzaifeidelushang的博客 547

zone.js由入门到放弃之二——zone.js API大练兵

本文重点介绍了zone.js中各个API的使用方式及相互间关系,通过大量的实验demo简单演示了一下这些API的用法。最后,还通过一个较底层的API打包了自己的Task。

OpenTiny的博客 355

3、基于视觉的智能分拣机:原理、算法与应用

本文介绍了一种基于视觉的智能分拣系统,利用树莓派、相机模块和多种检测算法实现低成本、高精度的物体分拣。系统支持颜色、形状和尺寸三种检测方式,准确率达99%,广泛适用于制造业、农业和物流等领域。文章详细阐述了各检测算法原理、测试结果及系统优化方向,并探讨了其实际应用案例与未来发展前景。

mqtt6iot的博客 237

Autosar NvM配置-手动配置Nvblock及使用-基于ETAS软件

NVM作为存储协议栈中最顶层的模块,是必须要掌握的。目前项目基本使用MCU带的Dflash模块,使用Fee模拟eeprom。在项目前期阶段,应该充分讨论需要存储的内容,包括应用数据,诊断数据等,用以评估Dflash空间是否够用。本文基于ETAS软件介绍NVBlock配置生成NVM参数及使用。使用NVBlock配置生成,基本不用手写代码,还是比较方便的。对于Nv block的划分,建议将同时写的一类数据放到一个block中,因为block除了data以外,Fee还会增加一些header数据,占用不少内存。

赞的博客 1884

zone.js由入门到放弃之五——NgZone & ApplicationRef源码分析

啸达同学刚写zone.js系列就说过,NgZone影响着Angular中的变更检测,历时一个多月的笔耕不辍,终于到了他初次下笔时的目的地~

OpenTiny的博客 455

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Three.js加载超大BIM模型卡顿?试试用‘颜色盒子’做遮挡剔除(附代码避坑)

本文深入探讨了Three.js中加载超大BIM模型时的性能优化策略,重点介绍了‘颜色盒子’遮挡剔除技术的实战应用。通过精确几何代理、多级颜色编码和动态更新策略,有效解决了传统方案在BIM场景中的性能瓶颈,并结合八叉树实现混合优化,显著提升帧率。附有详细代码示例和性能对比数据,为开发者提供实用参考。

weixin_30300225的博客 445

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

Rocky Linux 8 防火墙必选 firewalld:nftables 后端与服务抽象原理

Linux 防火墙本质是 netfilter 内核框架的用户态策略接口,其演进路径从 iptables 的过程式规则管理,发展为 nftables 的高效原子化引擎,再升维至 firewalld 的声明式服务抽象。firewalld 并非简单封装,而是通过 zone 区域模型、service 服务定义和 rich rule 富策略,将端口开放、模块加载、IPv4/IPv6 双栈、Docker 协同等运维需求统一建模。在 Rocky Linux 8 中,它默认绑定 nftables 后端,规避 iptable

diandingyin9417的博客 465

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

乐高EV3无线遥控方案:2.4G手柄集成与Python控制实现

在嵌入式系统与机器人控制领域,设备间的无线通信是实现灵活交互的关键。通过USB HID协议,外部输入设备能够被Linux系统识别为标准游戏控制器,这为硬件集成提供了通用接口。在机器人项目中,这种技术价值在于摆脱了有线束缚,实现了低延迟、高自由度的实时操控。具体到乐高EV3平台,其基于Linux的系统特性允许通过标准输入设备接口接入无线手柄。本文以DFR0182 2.4G无线手柄为例,详细阐述了从设备识别、驱动调试到使用Python的pygame库读取数据,并结合ev3dev2库实现电机差速控制的全过程。方案

weixin_30729609的博客 383

VPS上Nginx配置本质:Linux网络栈与应用层的契约构建

Web服务器是运行在操作系统之上的网络服务程序,其核心职责是在TCP/IP协议栈与上层应用之间建立可靠、安全、高效的HTTP通信桥梁。Nginx作为高性能Web服务器和反向代理,其配置过程远不止修改conf文件——它实质是定义端口所有权、文件系统权限、TLS握手策略、请求转发语义等多层系统契约。尤其在Virtual Private Server(VPS)环境中,用户拥有完整Linux控制权,必须直面systemd服务管理、SELinux/AppArmor策略、内核网络参数等底层约束。本文聚焦Nginx在VP

weixin_33748818的博客 451

Cursor实操指南:AI编程中需求翻译与上下文驱动开发

AI编程不是让模型代写代码,而是构建人与大模型之间的精准协作机制。其核心在于将模糊业务需求转化为AI可解析的技术约束,即‘需求翻译’;而真正决定生成质量的,是开发者提供的上下文完整性——包括文件结构、框架规范、错误日志与数据库配置。Cursor作为当前最成熟的AI原生编辑器,其价值不在于自动补全,而在于支持上下文感知的Agent式开发流程。本文聚焦于Next.js+Prisma+PostgreSQL全栈场景,详解如何通过结构化需求文档、分层指令设计、上下文锚定和模型协同策略,实现从0到可部署MVP的47分钟

weixin_34221332的博客 398

轻量级开票系统数据库与API设计实战

开票系统是典型的业务驱动型财务应用,其核心在于准确表达法律效力、税务合规与资金流意图。理解发票(Invoicing)的本质——即作为债权凭证的结构化单据——是设计合理数据库与API的前提;在此基础上,轻量(Lightweight)并非功能删减,而是通过精准建模(如分离issue_date/due_date)、冗余固化(如unit_price锁定)、动词优先路由(如POST /invoice/issue)等手段,规避过度工程。这种设计直接支撑中小企业多币种、多税率、PDF交付、审计追溯等真实场景,同时保障高并

djph26741的博客 391

小出版商如何科学应对大模型抓取:不封、不慌、不伤流量

robots.txt和User-Agent屏蔽是基础网络爬虫控制机制,但对LLM训练数据采集效果有限——因其多依赖匿名爬虫、镜像存档与非标准请求头。理解爬虫行为指纹(如Accept头偏好、路径遍历规律、无Cookie交互)比识别UA更可靠;结合X-Robots-Tag响应头、结构化JSON-LD元数据与分层robots.txt策略,可在保障SEO权重和学术引用的前提下,实现内容归属声明与训练用途限制。小出版商的核心挑战不是技术封锁,而是建立‘可度量、可回滚、可分级’的可见性治理框架,平衡版权保护与AI时代的

weixin_30252709的博客 398

Ubuntu 18.04源码编译Nginx实战:HTTP/2与TLS 1.3部署指南

Nginx作为最主流的Web服务器与反向代理,其核心能力取决于底层协议栈支持。理解Nginx工作原理需从模块化架构、事件驱动模型及与OpenSSL/PCRE等依赖的协同机制入手;技术价值在于通过源码编译实现协议升级(如HTTP/2 ALPN协商、TLS 1.3握手)、安全加固(规避CVE-2026-27654等高危漏洞)和性能优化(jemalloc内存管理、PCRE JIT正则)。典型应用场景包括政企旧系统迁移、教育平台HTTPS网关、SaaS服务的WebSocket/gRPC反向代理。本文聚焦Ubuntu

weixin_34379433的博客 434

Unity编辑器扩展实战:基于UI Toolkit构建高效自定义工具栏

在Unity开发中,编辑器扩展是提升工作效率的关键技术,它允许开发者将高频、重复的操作封装为可视化工具。其核心原理在于通过扩展Unity编辑器自身的功能,创建自定义窗口、面板和工具栏,从而优化工作流程。从技术价值看,优秀的编辑器扩展能显著减少手动操作,降低出错率,并实现复杂任务的自动化。在应用场景上,无论是资源管理、数据批处理还是场景编辑,自定义工具都扮演着重要角色。本文聚焦于利用Unity新一代的UI系统——UI Toolkit,来构建一个模块化、数据驱动的生产级工具栏。UI Toolkit采用类似Web

weixin_30251829的博客 347
上一篇: OpenTiny Vue 3.10.0 版本发布:组件 Demo 支持 Composition 写法,新增4个新组件
下一篇: zone.js由入门到放弃之二——zone.js API大练兵
OpenTiny社区
博客等级 码龄4年 1540粉丝 266原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值