2023年3月

Gateway API 作为新一代的流量管理标准,对原有 Ingress 的扩展不规范、移植性差等问题做出了改进。从兼容K8s生态和优化网关体验出发,Rainbond 支持以插件的形式扩展平台网关能力,目前已经有多家社区提供了 Gateway API 的实现,将其制作成平台插件后,一键部署后即可在平台中使用拓展网关能力。我们可以制作不同的网关实现插件来应对不同的场景和需求,同时可以将自己制作的插件发布到应用商店供大家使用。

本篇文章将以 Envoy Gateway 为例详细介绍如何制作并发布你的 Kubernetes Gateway API 插件。最后发布到开源应用商店的 Gateway API 插件将可以被其他用户使用,同时积极参与贡献也有机会获得由我们提供的小礼品。

前提条件

  • Rainbond 版本大于 v5.13

  • Rainbond 已经对接过开源应用商店并拥有推送权限

Rainbond 与 Gateway API 集成机制

在 Rainbond 中,之前仅支持内置网关,应用定义好路由规则后,外部流量即可直接访问到对应应用。而 Gateway API 是以插件和能力扩展的形式与平台进行结合的。在平台中,只有安装了 Gateway API 自定义资源以及至少有一个网关实现后,才可以扩展平台网关能力。

如下图所示,如果
App 4

App 5
等应用想要使用支持 Gateway API 的网关实现,那么首先需要定义 Gateway API 的相关资源,而这类资源是由
Gateway API 基础资源插件
提供的,它主要包含了 Gateway API 资源类型的定义以及相关的 WebHook 资源。同时它在平台上暴露了 GatewayClass 和 Gateway 类型的资源,在平台能力扩展中可以看到。这样用户可以自定义网关行为和配置。

因此我们只需要制作一个网关插件,即可读取 Gateway 类型的资源并生成对应的配置,向外提供网关能力。目前 Gateway API 已有多种实现,如 Envoy、Nginx、Istio 等。这里我们选择 Envoy 作为网关,这样外部流量进入 Envoy后,即可根据对应的路由策略到达
App 4
等应用上。

制作自定义网关插件的步骤

实现 Gateway API 插件的完整流程如上图所示,主要分为以下五步:

  1. 部署 Gateway API 基础资源:目前 Gateway API 主要由一系列自定义资源(CRD)组成,在集群中使用其能力时,需要先部署这些基础资源,才能使集群识别该类型的资源。
  2. 选择 Gateway API 网关实现:目前 Gateway API 已有多家
    下游实现
    ,这些网关实现都可以自由选择,提供对外服务的能力。
  3. 平台部署网关并测试:需要将网关实现转化为平台资源进行部署测试。只有这样最后才可以一键发布到开源应用商店供他人使用。
  4. 制作和发布插件:定义插件相关元数据,并发布到开源应用商店。
  5. 完善插件信息并上架:完善插件的介绍后,可以让用户更好的使用该插件。

下面将会针对这几个步骤详细说明。

部署 Gateway API 基础资源

在制作下游网关实现插件之前,我们需要安装 Gateway API 基础的 CRD 和控制器等资源,平台已经将这些资源打包成插件应用上架到开源应用商店。我们只需要在
平台管理->应⽤市场->开源应⽤商店->搜索 GatewayAPI-Base
并进行安装即可,由于 Gateway API 中 RBAC 相关资源对命名空间有依赖,所以我们需要在安装时,新建一个团队,团队英文名设置为
gateway-system
,这样将会将其安装至
gateway-system
命名空间下,最好单独创建⼀个应⽤,应⽤的名称⻅名知意,便于后期管理。

选择 Gateway API 网关实现

k8s
Gateway API 实现列表
中有多个实现,制作的话可以去这里挑选,由于目前 k8s Gateway API 目前 HttpRoute 已支持到 Beta 版本,所以我们需要挑选 HTTPRoute 资源支持到 beta 版本的下游实现,如
Istio

Cilium

Kong
等。由于
Envoy Gateway
已支持到 Beta 版本,所以我们本次使用其作为网关插件的扩展。

在Rainbond上部署并测试

挑选好实现后,你可以在实现的官网中看到如何安装实现,拿 envoy 为例,envoy 官网给出了两组 Yaml 如下:

kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v0.3.0/install.yaml
kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/v0.3.0/examples/kubernetes/http-routing.yaml
  • install.yaml
    此 YAML 文件中存放的便是我们插件所需的基础资源。
  • http-routing.yaml
    这个 YAML 文件我们需要进行处理,只保留我们插件所需的 GatewayClass 资源和 Gateway 资源,HttpRoute 资源不需要保留,在平台定义网关策略后将会自动生成。

将整理好的资源 YAML 后,在应用视图的 k8s 资源管理处创建,功能位置:
应用视图 ---> k8s 资源 ---> 添加

⚠️注意:如果有RoleBinding 等需要标识命名空间的资源,则需要确保标识的命名空间和当前上传的团队所对应的命名空间是否一致,以免造成权限不足等问题,示例如下:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
...
subjects:
- kind: ServiceAccount
  name: certgen
  namespace: envoy-gateway-system

上传创建完成后,我们还需要在
平台管理视图->扩展->能力
中处理一下 Gateway 资源,将网关的 Service 名称或前缀标记出来,后续在创建 HTTP 策略的时候便可获取并展示你的域名解析地址。

labels:
    service-name: envoy-envoy-gateway-system-envoy

NodePort 是从节点上获取的 IP ,默认为 NodeInternalIP ,如果存在 NodeExternalIP 则优先使用 NodeExternalIP 。

LoadBalancerIP 是从 Service 资源上的 ExternalIPs 获取IP,如果不存在则不展示。

完成以上操作后,我们需要进行测试,主要检查以下几项。

  1. 检查组件是否都运行正常,状态是否都为运行中。
  2. 检查应用下的 k8s 资源是否都创建成功。
  3. 当所有资源的状态都正常后,参考 Gateway API 网关使用文档进行使用测试,查看是否可以正常使用。

制作和发布插件

如果想将该网关实现作为平台网关插件进行发布,那么还需要准备标志应用为插件的 RBDPlugin 资源,定义好该资源后,才可以在
平台管理->插件
中查看到该插件并进行管理。示例如下:

apiVersion: rainbond.io/v1alpha1
kind: RBDPlugin
metadata:
  name: RBDPlugin 资源名称
spec:
  alias: 插件别名
  author: 插件制作人
  description: 插件简介
  icon: 插件图标
  version: 插件版本

定义好该资源后,我们可以进行发布了,在应用拓扑图页面,点击左侧
发布
按钮,选择
发布到云应用商店
,即可将其发布到开源应用商店。

完善插件信息并上架

发布到开源应用商店的插件或应用,我们需要
登录开源应用商店
编辑其信息并上架后,该应用才可被其他用户查看和使用。可以参考
如何分享插件或应用到 Rainbond 应用商店

登录完成后点击右上角控制台,选择
管理应用
。这时候应该可以看到刚刚发布的 Envoy 插件。点击应用名称进入详情页面,此时需要编辑应用的名称、Logo、详细信息。

当应用基础信息补充完成后,我们需要为其添加一个套餐,才可以上架。套餐在这里的作用主要是将应用的版本管理起来。用户使用不同的套餐安装的版本也不同。

在补充完应用的基本信息和套餐后,就可以准备上架了。只有上架的应用才可以被其他用户浏览和使用。回到管理应用的页面,选择上架即可。

最终效果

我们可以在开源应用商店查看到我们制作的网关插件,如下图所示,其余用户也可以在 Rainbond 中一键部署使用,具体使用可以参考 Gateway API 使用文档。

作者:京东物流 宁冲

1 前言

什么是微前端?
微前端是指存在于浏览器中的微服务。

本文主要通过对微前端框架single-spa的基座应用加载子应用的single-spa-vue函数库进行分析,通过代码维度分析让大家了解在single-spa加载子应用的时候都做了哪些事情。如何通过优化single-spa-vue函数库保持子应用的状态。

由于是在代码维度进行分析,要求读者对single-spa有一定的了解,阅读效果会更好。

2 single-spa加载子应用的过程

基座应用中配置的加载子应用配置,在配置子应用对象的app方法中会把子应用chunk-vendors.js、app.js插入到页面,并且执行chunk-vendors.js、app.js两个子应用依赖的js。

app方法执行结束以后,子应用依赖的js插入页面以后的dom结构。

由上面app方法的执行过程,我们可以看出基座应用加载子应用,和vue框架打包后加载js资源的方式类似。那么为什么只需要加载子应用的app.js和chunk-vendors.js就可以把子应用渲染到页面上面。下面我们再来看看子应用的入口文件main.js方法中是如何进行配置的。

3 子应用的main.js中的配置

在子应用main.js中我们可以看到,把常规的配置1替换为配置2的格式。

在配置2中我们会把当前vue的配置传给single-spa-vue函数库中。包括el、render方法、Vue对象。通过singleSpaVue的包装,返回single-spa生命周期方法bootstrap、mount、unmount。

  • bootstrap:引导函数,应用内容首次挂载到页面前调用,只会执行一次。
  • mount:挂载函数,子应用每次被挂载的时候都会执行。
  • unmount:卸载函数,子应用每次被卸载的时候都会执行。

也就是说基座应用在加载子应用的时候就是通过single-spa-vue函数的处理,生成了single-spa的各个生命周期,给基座应用在加载子应用的时候调用。
下面我们来看看single-spa-vue究竟是如何生成各种生命周期函数的。

4 single-spa-vue源码结构

single-spa-vue函数库可以帮助我们来生成加载子应用的生命周期。你可以通过下面链接:single-spa-vue GIT地址,下载single-spa-vue函数库的源码。
single-spa-vue的源码非常简单,只有几个生成single-spa生命周期的函数。

  • single-spa-vue方法:single-spa-vue函数库对外提供服务的唯一一个方法,用来接收配置项,并且返回一个包含各个single-spa生命周期的对象。
  • single-spa-vue中的其他方法bootstrap、mount、update、unmount是用来生成single-spa对应生命周期函数的方法。

下面我们详细介绍一下这几个方法的作用和实现。

5 single-spa-vue源码解析

single-spa-vue中提供的singleSpaVue方法会接收userOpts配置信息,配置信息会和默认的配置项defaultOpts合并以后再进行进行后续处理。

5.1 配置项defaultOpts

对于默认的配置项会有下面这几项,这里只介绍一下必填项,appOptions、Vue/createApp。其中的template在并没有在single-spa-vue中被用到,但是在single-spa-html中有使用到。

  1. // 默认配置项列表

  2. const defaultOpts = {

  3. // required opts

  4. appOptions: null,

  5. template: null,

  6. // sometimes require opts

  7. Vue: null,

  8. createApp: null,

  9. handleInstance: null

  10. }

1)appOptions配置项介绍

  • appOptions:应用的配置项,会在初始化Vue实例的时候作为参数传给Vue方法,下面具体介绍一下appOptions中的配置项。
  • el:子应用需要挂载的基座dom,即vue需要挂载的dom。
  • render/template:vue的render/template配置项。
  • data:初始化的参数对象,会在执行挂载函数mount的时候直接挂载到vue实例上。

2)Vue/createApp配置项介绍

Vue/createApp配置项是用来生成vue实例的,single-spa-vue函数库可以通过传入的Vue对象在mount方法中来生成vue实例。也可以传入createApp方法,由子应用在createApp方法中返回vue实例。

下面我们会在生命周期生成方法中看到这些配置项的作用

5.2 入口方法:singleSpaVue

singleSpaVue方法会对入参先进行下列有效性校验,具体校验的内容有一下四项。

  1. 配置项userOpts是否为对象。
  2. 用来创建vue实例的配置Vue/createApp是否存在。
  3. 用来生成vue实例的配置项appOptions是否存在。
  4. vue实例挂载的dom是否正确appOptions.el有效性校验。

有效性校验通过以后,会调用bootstrap、mount、unmount、update方法用来分别生成single-spa加载子应用的生命周期函数。
下面详细介绍一下各个生命周期函数是如何生成的。

5.3 引导函数:bootstrap

引导函数bootstrap,当应用内容首次挂载到页面前调用。

bootstrap函数,会先判断一下是否在配置项中存在loadRootComponent。配置项loadRootComponent笔者理解是可以用来加载当前子应用依赖的父级应用或者其他的依赖资源。
比如A页面依赖B组件,那在加载A页面的时候可以loadRootComponent方法中把B组件的静态资源加载下来,并且渲染到页面上,A页面就可以直接使用B组件中提供的一些资源或者dom。
如果未配置loadRootComponent,则直接返回,不做任何处理。

5.4 子应用挂载生命周期:mount

在子应用每次被挂载到页面上的时候,single-spa会执行mount生命周期函数,single-spa-vue在mount函数中用来初始化子应用的vue实例,并且把实例挂载到页面上。
mount函数会接收三个参数,分别是singleSpaVue传入的参数opts,single-spa-vue当前全局挂载的子应用实例列表mountedInstances,在基座应用中注册的当前子应用的single-spa实例props
props入参接收的数据,这里只用到了props的name属性,用来标识当前挂载的子应用。

在mount方法中主要做了以下几件事

1)格式化应用配置项appOptions

mount方法中会通过resolveAppOptions方法来格式化应用的配置项appOptions。
在resolveAppOptions方法来中如果appOptions是一个方法的话,则执行方法,并且传递基座应用通过customProps参数传入的参数props。
子应用可以在appOptions方法中获取父应用传递的参数、根据父应用的状态做不同的处理等。

2)初始化子应用需要挂载的DOM对象。

初始化dom的配置和方法比较多,会从很多个配置项中来获取el,代码比较简单,这里就不赘述了。只列出来取值的优先级供大家参考。
appOptions.el > props.domElement > props.name

dom节点被格式化完成以后,下面就开始把页面挂载到dom上面。

3)创建vue示例,并且挂载到页面上

在挂载页面之前会有一个配置项:replaceMode,replaceMode配置项是用来标识是否需要替换el节点下的内容。默认replaceMode为false是会在el节点下面新建一个class为single-spa-container的空白div来保存子应用。

以demo中的代码为例:

默认replaceMode为false的情况下,基座中的的内容和vue1中的内容会并存。如下图:

如果把replaceMode改为true的情况下,会发现基座中的内容会被vue1中的内容覆盖掉。
如下图:

设置挂载方式以后,就开始把子应用真正挂载到dom上面。
这里支持两种挂载方式,一种是使用vue3的createApp方式挂载,一种是使用vue2的new Vue方式进行挂载。这里的挂载方式其实和我们在使用vue挂载到dom方式的配置是一样的。

通过上面的步骤,此时子应用就被挂载到了html的页面中的dom上了。

5.5 更新生命周期函数:update

当调用parcel.update()会触发update方法,笔者由于并没有用的高级特性parcel,在此处不再做过多介绍。

5.6 子应用卸载生命周期:unmount

当页面url发生变化的时候,离开子应用页面路由的时候会触发unmount方法。
在unmount方法中主要做的事情有:

  1. 卸载vue应用
  2. 删除vue实例
  3. 同时清空页面dom。

执行unmount操作以后子应用的数据和dom就会被清除干净。

5.7 小结

以上就是single-spa官方提供的single-spa-vue函数库来挂载子应用的全部流程。通过上面的分析,我们发现single-spa-vue函数库会通过调用singleSpaVue方法返回一个包含bootstrap、mount、update、unmount四个方法的对象。在基座应用加载子应用的生命周期会执行对应的方法,通过执行方法把子应用挂载到基座应用或者从基座应用销毁子应用。

通过single-spa-vue中加载子应用的过程,single-spa把各个子应用组装成一个复杂的应用,在用户无感的情况下对各个子应用进行分别治理。

single-spa在加载子应用的时候,除了vue的版本之外,还有其他的版本,但是加载的思路类似,都是在函数库中来把子应用加载到html中,此处不再介绍其他类型的加载。
single-spa其他加载子应用方式:single-spa-react、single-spa-html

6 子应用状态保持

在实际开发的过程中笔者遇到有些页面需要使用vue中的keep-alive来保留状态,在页面切换的过程中虽然页面被销毁但是页面状态需要在保留,但是我们在分析single-spa-vue的unmount方法实现的时候发现,如果页面切换,子应用和vue实例被销毁,此时子应用中的keep-alive是没有生效的。

如下图:

当在input中输入内容以后,传统的spa项目可以通过keep-alive来记录状态,当页面路由切换以后,再切回来,仍然保持状态。但是如果直接使用single-spa-vue,当子应用触发unmount的时候input中的输入内容6666661会被清空。针对这种情况,笔者对single-spa-vue类库进行了一些改造。

改造内容如下:

6.1 子应用由销毁改为隐藏

在single-spa-vue配置项中增加了一个配置项,根据配置项中是否存在isKeepAlive配置项,来判断把当前的dom隐藏掉,还是删除。这样当子应用unmount方法触发的时候,子应用并未被删除,而是仍然保留。

同时笔者对vue的route配置也进行了一些优化,当页面不存在的时候,此时会把子应用中的一个空组件挂载到dom上面,避免虽然页面被隐藏掉,但是dom仍然在html中,导致页面dom过多。

6.2 子应用由新建改为显示

在single-spa-vue配置项中调用mount方法挂载子应用的时候,会判断当前子应用是否存在,如果子应用存在则直接把子页面显示出来,由于子应用并未被销毁,此时子应用中的keep-alive就会一直生效,并且保存页面的状态。

通过对single-spa-vue类库的mount、unmount方法的优化,用户在使用页面的时候真正和使用vue那样流畅,并且可以保持页面状态,提升用户的体验。

简介

现在的app功能越来越强大,除了基本的图文之外,还需要各种各样的其他的功能,比如视频,和直播。

直播可能会比较复杂,因为涉及到了拉流和推流,需要服务器端的支持,但是视频播放就比较简单了,那么如何在flutter中使用媒体播放器呢?

一起来看看吧。

使用前的准备工作

flutter本身是不支持媒体播放功能的,为了实现这个功能,我们需要使用额外的第三方插件叫做video_player。

首先我们需要向flutter应用中添加video_player。添加起来也非常简单,只需要执行下面的命令即可:

flutter pub add video_player 

该命令会向pubspec.xml中添加如下的内容:

dependencies:
  flutter:
    sdk: flutter

  video_player: ^2.4.7

添加好依赖包之后,我们还需要为应用添加相应的权限,你确保能够使用影音播放的权限。

如果是在android中,需要向AndroidManifest.xml文件中添加类似下面的内容:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application ...>

    </application>

    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>

在IOS中则需要在Info.plist中添加下面的内容:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

在flutter中使用video_player

video_player中和video播放相关的类叫做VideoPlayerController,在IOS中底层使用的是AVPlayer,在Android中底层使用的是ExoPlayer。

VideoPlayerController有好几种构造方法,我们一起来看看。

  VideoPlayerController.asset

asset方法表示video是从应用程序的asset中获取的。

 VideoPlayerController.network

network方法表示video是从网络中获取的。

  VideoPlayerController.file

file方法表示video是通过'file://${file.path}' 这样的格式来获取的。

还有一个只用在andorid中的方法,表示从contentUri中加载video:

  VideoPlayerController.contentUri

为了简单起见,这里我们选择网易上面的一个科教视频,作为要播放的video。

那么我们可以通过 VideoPlayerController.network方法来构建这个controller:

    videoPlayerController = VideoPlayerController.network(
      'https://flv.bn.netease.com/1c04bfd72901f0661b486465e09cfdc01754c20db0686786f4e20a5f7d271ba0de6c1177a0da1c4c2d7c367e20ee16d4a90ac7ff4ea664820ba1b401f3e53f135f72cdff855e78ca5fb7849fb6ff7ccb9de1613ad3bfc59db83493b5f18a0a27f15048df6585361cd67c3b37551e10981c40dcdfdb77b7e6.mp4',
    );

在使用video之前,还需要进行初始操作,初始化是调用它的initialize方法,这个方法的作用是打开给定的数据源,并加载它的元数据。

因为initialize方法是一个耗时的操作,所以这个方法返回类型是Future:

  Future<void> initialize() async {

我们可以这样使用:

late Future<void> playerFuture;
playerFuture = videoPlayerController.initialize();

有了播放器的Future,我们可以配合flutter中的FutureBuilder一起使用:

body: FutureBuilder(
        future: playerFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            return AspectRatio(
              aspectRatio: videoPlayerController.value.aspectRatio,
              child: VideoPlayer(videoPlayerController),
            );
          } else {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),

在FutureBuilder中,我们通过判断connectionState来判断视频是否加载完毕,如果没有加载完毕,则使用CircularProgressIndicator表示正在加载中。

如果加载完毕之后,就直接展示VideoPlayer组件即可。

因为不同的video有不同的纵横比,为了在flutter界面上完美的展示加载的video,我们将VideoPlayer封装在一个AspectRatio组件中。

最后我们还要添加一个控制装置,用来控制video的暂停和播放:

floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            if (videoPlayerController.value.isPlaying) {
              videoPlayerController.pause();
            } else {
              videoPlayerController.play();
            }
          });
        },
        child: Icon(
          videoPlayerController.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      )

这里通过videoPlayerController.value.isPlaying来判断视频是否在播放状态,同时在onPressed方法中调用了setState来调用videoPlayerController.pause或者videoPlayerController.play方法。

总结

这样一个可以播放外部视频的app就做好了,运行之后它的界面是这样的:

大家可以在这个播放器的基础上进行扩张,一个属于你自己的视频APP就完成了。

本文的例子:
https://github.com/ddean2009/learn-flutter.git

其他章节请看:

webgl 系列

着色器语言

本篇开始学习着色器语言 ——
GLSL
全称是 Graphics Library Shader Language (
图形库着色器语言

GLSL 是一门独立的语言,和其他语言一样有自己的变量、运算符、函数、循环(for)、控制语句(if)、函数、数组等等。

GLSL 比较
简单
。其专门用于编写着色器,舍弃了许多编程语言中复杂的东西,比如没有字符串,
只有数字

Tip
: webgl 1.0 绝大多数浏览器都支持,webgl 2.0 支持度差些。webgpu 旨在取代WebGL,浏览器兼容惨不忍睹。

准备

开始前稍微准备一下环境,根据前面
三角形
的学习,我们很容易绘制如下矩形:

// 顶点着色器
const VSHADER_SOURCE = `
attribute vec4 a_Position;
void main() {
  gl_Position = a_Position;
  gl_PointSize = 10.0;               
}
`

// 片元着色器
const FSHADER_SOURCE = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`

function main() {
    const canvas = document.getElementById('webgl');
    const gl = canvas.getContext("webgl");
    if (!gl) {
        console.log('Failed to get the rendering context for WebGL');
        return;
    }

    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('Failed to intialize shaders.');
        return;
    }

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const vertices = {
        data: new Float32Array([
            -0.5, 0.5,
            -0.5, -0.5,
            0.5, -0.5,
            0.5, 0.5
        ]),
        vertexNumber: 4,
        count: 2,
    }

    initVertexBuffers(gl, vertices)

    gl.drawArrays(gl.TRIANGLE_FAN, 0, vertices.vertexNumber);
}

function initVertexBuffers(gl, {data, count}) {
    const vertexBuffer = gl.createBuffer();
    if (!vertexBuffer) {
        console.log('创建缓冲区对象失败');
        return -1;
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('Failed to get the storage location of a_Position');
        return -1;
    }

    gl.vertexAttribPointer(a_Position, count, gl.FLOAT, false, 0, 0);

    gl.enableVertexAttribArray(a_Position);
}


顶点着色器

片元着色器
提取出单独的 js 文件。就像这样:

// demo\shader\fShader.js

const FSHADER_SOURCE = /* glsl */ `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`

// demo\shader\vShader.js

const VSHADER_SOURCE = /* glsl */ `
attribute vec4 a_Position;
void main() {
  gl_Position = a_Position;
  gl_PointSize = 10.0;               
}
`;

这里的
/* glsl */
用于 vscode 高亮 glsl。就像这样(左侧高亮):

高亮 glsl

笔者选择 vscode 安装
Comment tagged templates
(重启后仍未生效,后切换安装的版本,再次重启即可):

如果你是基于 webpack 这种模块化的开发,可以安装
WebGL GLSL Editor
,直接编写
.glsl
就能高亮:

基础

  • 区分大小写
  • 每个语句以一个分号结束
  • 单行注释
    //
    、多行注释
    /* */
// 单行注释
/*
多行注释
*/

变量名

变量命名规则如下:

  • 只能包含字母、数字、下划线
  • 首字母不能是数字
  • 不能是关键字和保留字
  • 不能以
    gl_

    webgl_

    _webgl_
    开头,这部分已经被 OpengGL ES 保留了

Tip
: 变量的命名就按照js的习惯来就好,如果有问题,浏览器会给出较好的错误提示。就像这样:

// 只能包含字母、数字、下划线 - ERROR: 0:15: '$' : invalid character
int a$ = 1;

// 首字母不能是数字 - ERROR: 0:11: '0aA0_' : invalid number
int 0aA0_ = 1;

// 不能是关键字 - ERROR: 0:19: 'for' : syntax error
int for = 1;

// 不能是保留字 - Illegal use of reserved word 非法使用保留字
int class = 1;

// 不能以 gl_、webgl_ 或 _webgl_ 开头 - ERROR: 0:24: 'gl_' : reserved built-in name 保留的内置名称
int gl_a = 1;

基本类型

不像 js 有 number、string、boolean、null、undefined、bigint、symbol 等 7 种
基本类型
,这里只有数字(
int

float
)和布尔(
bool
)。

// 精度限定字。否则浏览器控制台报错:No precision specified for (float)
precision mediump float;
int a = 1;     // 整型
float b = 1.0; // 浮点
bool c = true; // 布尔类型

Tip
: js 不区分整数和浮点数,其数字都是64位的双精度,类似 java 中的 double。而如果把整数赋值给 flat(
float b = 1;
) 就会报错。

类型转换

由于基本类型就三种(int、float、bool),所以类型转化也较js简单多了,共 6 种。

  • int(float)
    - 转整型。删除浮点小数部分,转为整数(比如 3.14 转为 3)
  • int(bool)
    - 转整型。true 转 1,false 转 0
  • float(int)
    - 转浮点。(比如 1 转 1.0)
  • float(bool)
    - 转浮点。true 转 1.0,false 转 0.0
  • bool(int)
    - 0 转 false,非 0 转 true
  • bool(float)
    - 0 转 false,非 0 转 true

例如将浮点转为整型:
int d = int(1.0);

运算符

glsl 支持的运算符和 js 中类似。比如都有:

  • 加减乘除(+、-、
    、/、+=、-=、
    =、/=)
  • 自增、自减(++、--)
  • 比较(>、<、>=、<=、==、!=)
  • 取反(!)
  • 与或(&&、||)
  • 三元运算符

更详细的请看下表:

Tip

[1]
与运算,第一个表达式为 true 才进行第二个表达式计算;或运算,第一个表达式为 false 才进行第二个表达式计算。
[2]
逻辑异或,只有左右两个表达式有且只有一个为 true 时,运算结果才为 true,否则是 false。

虽然glsl运算符与 js 中类似,但有些运算符在用法上和js还是有些许差异。比如:

  • 参与运算的类型必须都是整数
// 参与运算的类型必须都是整数
int e = 1 + 1.0; // 错
int e = 1 + 1; // 对
  • ! 只能操作布尔类型
// ! 只能操作布尔类型
bool e = !1; // 错
bool e = !true; // 对
  • 三元运算符的 condition 类型得是 bool
// condition 类型得是 bool
int e = 1 ? 1 : 2; // 错
int e = true ? 1 : 2; // 对

复杂类型

矢量和矩阵类型

矢量(或
向量
)和矩阵类型非常适合处理计算机图形。

矢量和矩阵类型的变量包含多个元素,每个元素是一个数值(整型、浮点、布尔)。

Tip
:可以将矢量和矩阵理解成基本类型的集合。矢量将多个元素排成一行,矩阵则将这些元素划分成行和列。

glsl 支持的矢量(
vector
)和矩阵(
matrix
)类型如下:

Tip
: vec 是浮点矢量、ivec 是整型矢量、bvec 是布尔矢量。

矢量

几维向量就得传几个分量,否则报错。例如:

// vec4 变量需要4个浮点数分量
vec4 e = 1.0;  // 错
vec4 e = vec4(1.0, 0.0, 0.0, 4.0); // 对

由于向量很重要,所以glsl提供了灵活的创建方式。例如:

vec3 v3 = vec3(1.0, 2.0, 3.0);

// 使用 v3 的前两个元素
vec2 v2 = vec2(v3); 

// 将 v4 设置为 (1, 1, 1, 1)
vec4 v4 = vec4(1);

// 取v2两个元素,v4也取前两个
vec4 v4b = vec4(v2, v4);

如果向量接收不止一个参数,但参数个数又比矢量元素少,就会报错。例如:

// 错。接收不止一个参数,但参数个数又比矢量元素少
vec4 v4_2 = vec4(1, 1);

通过
分量名
可以取得向量相应的元素。例如:

vec3 v3 = vec3(1.0, 2.0, 3.0);
float x = v3.x; // 1.0
float y = v3.y; // 2.0
float z = v3.z; // 3.0

第一个分量除了可以通过 x 取得,还可以是 r 或 s。请看下表:

Tip
:即使你声明一个颜色向量,依然可以用 x 来取得向量中第一个分量值。就像这样:

vec3 color = vec3(1.0, 0.0, 0.0);
float x = v3.x; // 1.0

可以从矢量中同时抽取多个分量,这个叫
混合
。例如:

vec3 v3 = vec3(1.0, 2.0, 3.0);
vec2 v2 = v3.xz; // v2 设为 (1.0, 3.0)
vec2 v2 = v3.zz; // v2 设为 (3.0, 3.0)

疑惑
:还可以用来赋值(测试失败):

// ERROR: 0:31: 'v3' : syntax error
vec3 v3 = vec3(1.0, 2.0, 3.0);
v3.xz = vec2(5.0, 6.0); // 预期:v3 设为 (5.0, 2.0, 6.0)

向量直接可以进行运算。下面显示一个白色图形:

vec3 color1 = vec3(1.0, 1.0, 1.0);
vec3 color2 = vec3(0.0, 0.0, 1.0);
void main() {
  // color1 + color2 等于 (1.0 + 0.0, 1.0 + 0.0, 1.0 + 1.0),由于 大于1也是等于1,所以最后是 (1.0, 1.0, 1.0)
  gl_FragColor = vec4(color1 + color2, 1.0);
}

Tip
: 对于颜色,小于0则是0,大于1则是1。

矩阵

通过 mat
[1234]
() 构造矩阵,例如创建一个平移矩阵,让图形向左平移 0.5。就像这样:

const VSHADER_SOURCE = `
attribute vec4 a_Position;
// 向左平移 0.5
float tX = -0.5;
// 行列得颠倒
mat4 matrix = mat4(
  1,  0, 0, 0,
  0,  1, 0, 0,
  0,  0, 1, 0,
  tX, 0, 0, 1
);
void main() {
  gl_Position = matrix * a_Position;
}
`;

类似 js 操作数组的方式可以取得矩阵对应元素。就像这样:

matrix[0]    => [1, 0, 0, 0]
matrix[1][1] => 1

数组

glsl 只支持一维数组,没有 pop()、push() 等操作。声明数组无需使用 new ,只需要在变量名后添加
[]
和数组长度。就像这样:

float floatArray[4]; // 含4个浮点数的数组
vec4 vec4Array[2]; // 含2个4维向量的数组

数组不能在声明时初始化,必须显示的对每个元素进行初始化。就像这样:

// 'vec4Array' : syntax error
vec4 vec4Array[2];
vec4Array[0] = vec4(1.0, 0.0, 0.0, 1.0);
vec4Array[1] = vec4(1.0, 1.0, 0.0, 1.0);

疑惑
:报错
'vec4Array' : syntax error


:下面这段代码会报错,需要增加
const

// 错误。constant expression required
int size = 4;
vec4 vec4Array[size];
// 正确。
const int size = 4;
vec4 vec4Array[size];

取样器

glsl 提供一种内置类型:取样器(sample),必须通过该类型变量访问纹理。有两种基本取样器:sampler2D、samplerCube。

例如在
绘制猫
时,我们用到 sampler2D:
uniform sampler2D u_Sampler;

结构体

glsl 通过 struct 可以自定义类型。类似 js 中的对象,访问也是通过 .。就像这样:

// 定义一个结构体。类似 js 的对象。
struct light {
  vec4 color;
  vec3 position;
}

// 申明一个 light 类型的变量 l1
light l1;

// 赋值和构造
l1 = light(vec4(1.0, 0.0, 0.0, 1.0), vec3(1.0, 2.0, 3.0));
// 通过.访问成员
vec4 color = l1.color;
vec3 postion = l1.position;

精度限定字

glsl 引入精度限定字,用于帮助着色器程序提高运行效率,减小开销。

精度限定字用于表示每种数据具有的精度(比特数)。高精度的需要更大的开销(内存、计算时间),低精度开销则小。

在低精度下,webgl 程序运行结果比较粗糙,甚至不准确,这个需要考虑程序效能和性能的平衡。

webgl 支持高中低三种精度:

如果不确定,可以选择中精度。例如为所有浮点都选择中精度:
precision mediump float;

也可以为每个变量设置精度。就像这样:
mediump float size;

数据类型的默认精度请看下表:

由于片元着色器中的 float 没有精度,所以需要手动指定,否则会报错:
No precision specified for (float)

循环和分支

for

语法:

for(初始化表达式; 条件表达式; 循环步进表达式){

}

例如:

for(int i = 0; i < 10; i++){
  // 循环体
}

以下几点需要注意:

  • 循环变量(例如 i)只能在初始化表达式中定义
  • 条件表达式如果为空,则返回 true
  • 只允许一个循环变量,循环变量只能是 int 或 float
  • 循环表达式必须是(假如是 i): i++、i--、i+=、i-=
  • 条件表达式必须是循环变量和整型常量的比较(
    i < 10.0
    错误)

疑惑
:如下for语句导致浏览器报错

// Failed to compile shader: ERROR: 0:29: 'for' : syntax error
int sum = 0;
for (int i = 0; i < 10; i++){
  sum += i;
}

if...else

语法:

if(条件表达式){

}else if(条件表达式){

}else{

}

条件表达式:布尔或产生布尔的表达式。请看:

// if(1) - 报错
if(bool(1)){
    gl_FragColor = vec4(color1, 1.0);
}else{
    gl_FragColor = vec4(color2, 1.0);
}

函数

在 js 中定义一个函数:

function sum(a, b){
    return a + b;
}

如果不 return,js 默认返回 undefined。

glsl 中如果没有返回值则需要用到
void
,否则得申明具体返回类型。就像这样:

// 不返回
void sum(int a, int b){
   
}
// 返回 int
int sum(int a, int b){
    return a + b;
}

glsl 也提供了一些内置函数,例如角度转弧度的
radians()
,可直接使用。请看下表:

其他章节请看:

webgl 系列

配置开发环境

脚手架工具create-react-app

储备知识:终端或命令行、代码编辑器

React官方中文文档

create-react-app

其是基于Node的快速搭建React项目的脚手架工具。

npx create-react-app testdemo
cd testdemo
npm i

npx命令是npm v5.2.0引入的一条命令,无需安装脚手架包,就可以直接使用这个包提供的命令

yarn是Facebook发布的包管理器,功能与npm相同,具有快速、可靠和安全的特点

React with TypeScript

npx create-react-app testdemo-ts --template typescript
cd testdemo
npm i

About TypeScript

  • TypeScript是JavaScript的超集
  • 给原生JavaScript添加类型检查
  • 与ES6一样目前无法被主流浏览器直接读取

Compile TypeScript

编译器:ts-loader、awesome-typescript-loader以及
babel-loader

编译器配置文件:tsconfig.json

{
  "compilerOptions": {
    "noImplicitAny": false, //不需要显示地声明变量的类型any
    "target": "es5", //编译后的目标js版本
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ], //库文件,过这些库文件,告诉typescript编译器可以使用哪些功能
    "allowJs": true, //允许混合编译js文件
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true, //允许使用commonJs方式import默认文件
    "strict": true,  
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext", //配置代码模块系统,Node.js的commonJs、ES6的esnext、requireJs的AMD
    "moduleResolution": "node",// 决定编译器的工作方式
    "resolveJsonModule": true,
    "isolatedModules": true, //编译器会将每个文件作为单独的模块使用
    "noEmit": true, //发生错误时候,编译器不会生成Js代码
    "jsx": "react-jsx" //允许编译器支持编译react代码
  },
  "include": [
    "src"
  ] //使用此选项列出我们需要编译的文件, “文件路径”选项需要文件的相对或绝对路径
}

Update to TypeScript

1、npm install --save typescript @types/node @types/react @types/react-dom @types/jest
2、所有js文件改为jsx文件
3、import react

React基础

About React

History of FE

HTML、CSS、JavaScript——Ajax——jQuery——Angular MVC——Vue、React MVVM

Why is React?

  • 单向数据流
  1. 你只需要描述UI(HTML)看起来是什么样子,就和写HTML一样
  2. React负责渲染UI,并在数据变化时更新UI
  • 虚拟DOM

类似于Docker或VMware的Snapshot的快照技术

image

  • 组件化
  1. 保持交互的一致性
  2. 保持视觉风格的统一
  3. 便于程序员之间的协作
  • 学习一次,随意使用(必杀技)
  1. 使用React可以开发Web应用
  2. 使用React可以开发移动端原生应用(react-native)
  3. 使用React可以开发VR(虚拟现实)(react 360)

What is React?

React
是一个用于
构建用户界面
(HTML页面)的
JavaScript库

主要用来写HTML页面,或构建Web应用

如果从MVC角度来看,React仅仅是视图层(V),只负责视图的渲染,不提供完整的M和C的功能

(经典MVC模式中,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现
代码
分离,从而使同一个程序可以使用不同的表现形式。)

起源:Facebook的内部项目

JSX

What is JSX?

What's this?HTML?JS?

const element = <h1>Hello,world!</h1>

这是ReactJS自创的语言:JSX

  • JSX不是标准的ECMAScript语法,只是语法扩展
  • 对于React项目使用js和jsx都可以
  • ts对应tsx语法

拓展阅读:
react-jsx、react-jsxdev

JSX 其实是
React.createElement
的语法糖,下图的两种写法完全等价:
image

Why is JSX?

  • React并不强制使用JSX,也可以使用原生的JavaScript
  • React认为视图的本质是:渲染逻辑与UI视图表现的内在统一
  • React把HTML与渲染逻辑进行了耦合,形成了JSX

Features of JSX

  • HTML代码可以与JSX兼容
  • 可以在JSX中嵌入表达式
  • 使用JSX指定子元素

Attention

  • React元素的属性名使用
    小驼峰
    命名法
  • 特殊的属性名:class->className、for->htmlFor、tabindex->tabIndex
  • 如果元素没有子节点可以用
    />
    结束
  • 推荐使用
    小括号包裹JSX
    ,从而避免JS中的自动插入分号陷阱

基本使用

JXS中使用(嵌入)Js表达式

数据存储在JS中,语法:{JavaScript表达式} 注意:语法是
单大括号

const name = '张三'
// 1、使用JSX创建react元素
const title = <h1>Hello {name}</h1>

//2、渲染react元素
ReactDOM.render(title, document.getElementById('root'))

React DOM 在渲染所有输入内容之前,默认会进行
转义
。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止
XSS(cross-site-scripting, 跨站脚本)
攻击。

拓展阅读:
如何防止XSS攻击?

JSX的条件渲染
  • 场景:loading效果
  • 条件渲染:根据条件渲染特点的JSX结构
  • 可以使用
    if/else
    或三元运算符或
    逻辑与运算符
    来实现
const loadData = () => {
  if (loading) {
    return <div>loading...</div>
  }

  return <div>数据加载完成</div>
}

JSX的列表渲染
  • 应该使用数组的
    map()
    方法(映射)
  • 渲染列表时应该添加key属性,
    key属性的值要保证唯一
  • 原则:map()遍历谁,就给谁加key属性
  • 注意:
    尽量避免使用索引号作为key
const songs = [
  { id: 1, name: '爱你' },
  { id: 2, name: '年少有为' },
  { id: 3, name: '南山南' },
]  
// 1、使用JSX创建react元素
const list = (
  <ul>
    {songs.map(item => <li key={item.id}>{item.name}</li>)}
  </ul>
)
JSX的样式处理

详见:
React的行内样式与CSS

CSS of React

The way of import css file

  • 直接引入整个css文件
import './index.css'
<div className="app">

使用简单,但可能会造成样式的全局污染和样式冲突。

  • JSS模块化引入组件
import style from './index.css'
<div className={style.app}>

需要额外配置,ts环境需要配置
*.d.ts
的类型声明文件

declare module "*.css" {
    const css: {
        [key: string]: string //约定:导出key所在的对象,原始的类名和内容都会和转化为这个对象
    };
    export default css;
}

CSS module/JSS

  • 每个jsx或tsx文件被视为一个独立存在的原件
  • 原件所包含的所有内容也同样都应该是独立存在的

拓展阅读:
CSS in Js

CSS & TypeScript

Ts的优势就是给Js进行类型检查,那么通过JSS将CSS转换为Js对象,是不是也可以给CSS添加类型?

插件:
typescript-plugin-css-modules

npm i typescript-plugin-css-modules --save-dev

在tsconfig.json文件compilerOptions新增:

"plugins": [{
   "name":"typescript-plugin-css-modules"
}]

新建.vscode文件夹——新建文件settings

{
    "typescript.tsdk": "node_modules/typescript/lib",
    "typescript.enablePromptUseWorkspaceTsdk": true
}

配置后会发现编写style也会有只能提示

Media & fonts

src/assets/images
src/assets/fonts
src/assets/icons

State & Props

Difference

  • props是组件对外的接口,而state是组件对内的接口
  • props用于组件间的数据传递,而state用于组件内部的数据传递

State

State是组件的“私有属性”

初始化

//构造函数constructor是唯一可以初始化state的地方
constructor(props){
    super(props);
    this.state = {
        count: 0
    }
}

修改

//使用setState()修改数据,更新组件状态,调用render函数重新渲染
onClick = {() => {
    this.setState({isOpen: !this.state.isOpen});
}}

异步更新 同步执行

调用setState后,state不会立即改变,是异步操作(React会将多个修改合并为一个)。所以,不要依赖当前的State,计算下个State。

setState本身并非异步,但对state的处理机制给人一种异步的假象。

onClick = {() => {
    this.setState((preState, preProps) => {
        return {count: preState.count + 1}
    },() => {
        console.log("count" ,this.state.conut)
    });
    
    this.setState((preState, preProps) => {
        return {count: preState.count + 1}
    },() => {
        console.log("count" ,this.state.conut)
    });
}}

Props

本质上,props就是传入函数的参数,是父组件传向子组件的数据。

父组件

<ul>
	{robots.map(r => <Robot id={r.id} name={r.name} email={r.email}></Robot>)}
</ul>

子组件

//为Robot指定类型React.FC,FC(functional component)函数式组件的接口,接受泛型参数P(Props)
const Robot: React.FC<RobotProps> = (props) => {
    const id = props.id;
    const name = props.name;
    const email = props.email;
    return (<div className={styles.cardContainer}>
        <img src={`https://robohash.org/${id}`} alt="robot" />
        <h2>{name}</h2>
        <p>{email}</p>
    </div>
    );
}

Immutable

props是
只读的
,一旦创建不可被改变,只能通过销毁、重建来改变数据。

优点:通过判断内存是否一致,来确认对象是否有经过修改,极大提高性能效率

使用
Immutable
来编写程序的方式就是函数式编程(组件)。