Green Sock | GSAP 动画库
**GreenSock Animation Platform(GSAP)**是一个业界知名的动画工具套件,在超过1100万个网站上使用,其中包括大量获奖网站! 您可以使用GSAP在任何框架中制作几乎任何JavaScript可以触及的动画。无论你是想制作UI、SVG、Three.js还是React组件的动画,GSAP都能满足你的需求。核心库包含创建快速、跨浏览器友好的动画所需的一切。除了核心,还有各
1.什么是“GSAP”?
GreenSock Animation Platform(GSAP) 是一个业界知名的动画工具套件,在超过1100万个网站上使用,其中包括大量获奖网站! 您可以使用GSAP在任何框架中制作几乎任何JavaScript可以触及的动画。无论你是想制作UI、SVG、Three.js还是React组件的动画,GSAP都能满足你的需求。
核心库包含创建快速、跨浏览器友好的动画所需的一切。除了核心,还有各种插件。你不需要学习它们来开始,但它们可以帮助解决特定的动画挑战,如基于滚动的动画,可拖动的交互,变形等。
2.简介
官方文档:https://gsap.com/resources/get-started/
2.1 入门
让我们先动画化一个类名为 box 的 HTML 元素。
import gsap from "gsap";
gsap.to(".box", { x: 200 })
这样一个将元素水平右移 200 像素的一个动画在 GASP 中被称为 ‘tween’ ,为了方便理解,本文中统一称为【动画】。
如果我们对.box
元素进行元素检查,我们会发现GSAP实际上是不停的修改transform
属性,直至最终停留在transform: translate(200px, 0px)
;我们继续回到上面的代码。
2.2 动画语法
接下来让我们自习看看语法:
一个动画的使用包含一个方法 method、一个目标 target(s) 和一个 vars 对象,它们都包含了关于动画的信息
Method
方法名称 | 描述 |
---|---|
gsap.to() | 这是最常见的动画类型。.to()将从元素的当前状态开始,并将动画设置为动画中定义的值。 即设置动画结束位置,开始位置为当前位置。 |
gsap.from() | 类似于向后的.to(),它从动画中定义的值开始设置动画,并在元素的当前状态结束。 即设置动画开始位置,结束位置为当前位置。 |
gsap.fromTo() | 定义起始值和结束值 |
gsap.set() | 立即设置属性(无动画)。它本质上是一个零持续时间的.to() |
动画示例:
Target
接下来,我们必须告诉GSAP我们想要动画。GSAP使用document.querySelectorAll(),因此对于HTML或SVG目标,我们可以使用选择器文本,如".class"和"#id"。或者你可以传入一个变量甚至一个数组。
// use a class or ID
gsap.to(".box", { x: 200 });
// a complex CSS selector
gsap.to("section > .box", { x: 200 });
// a variable
let box = document.querySelector(".box");
gsap.to(box, { x: 200 })
// or even an Array of elements
let square = document.querySelector(".square");
let circle = document.querySelector(".circle");
gsap.to([square, circle], { x: 200 })
Variables
vars对象包含有关动画的所有信息。这些属性可以是要设置动画的任意属性,也可以是影响动画行为的特殊属性、函数,如duration、onComplete或repeat等。
gsap.to(target, {
// this is the vars object
// it contains properties to animate
x: 200,
rotation: 360,
// and special properties
duration: 2
})
2.3 动画属性
GSAP可以动画几乎任何东西,没有预先确定的列表。这包括CSS属性,自定义对象属性,甚至CSS变量和复杂字符串!最常见的动画属性是变换(transforms)和不透明度(opacity)。
tips: transforms是网络动画师最好的朋友。它们可以用来移动你的元素,放大它们,旋转它们。transforms和opacity也是非常有性能的,因为它们不影响布局,所以浏览器的工作量更少。
如果可能的话,使用变换和不透明度来制作动画,而不是像“顶部”、“左侧”或“边缘”这样的布局属性。你会得到更平滑的动画!
transforms
你可能对transforms from CSS很熟悉:
transform: rotate(360deg) translateX(10px) translateY(50%);
GSAP提供了transforms的简写:
{ rotation: 360, x: 10, yPercent: 50 }
下面是一个速记transforms和其他一些常用属性的列表。
GSAP | Description or equivalent CSS |
---|---|
x: 100 | transform: translateX(100px) |
y: 100 | transform: translateY(100px) |
xPercent: 50 | transform: translateX(50%) |
yPercent: 50 | transform: translateY(50%) |
scale: 2 | transform: scale(2) |
scaleX: 2 | transform: scaleX(2) |
scaleY: 2 | transform: scaleY(2) |
rotation: 90 | transform: rotate(90deg) |
rotation: “1.25rad” | Using Radians - no CSS alternative |
skew: 30 | transform: skew(30deg) |
skewX: 30 | transform: skewX(30deg) |
skewY: “1.23rad” | Using Radians - no CSS alternative |
transformOrigin: “center 40%” | transform-origin: center 40% |
opacity: 0 | adjust the elements opacity |
autoAlpha: 0 | shorthand for opacity & visibility |
duration: 1 | animation-duration: 1s |
repeat: -1 | animation-iteration-count: infinite |
repeat: 2 | animation-iteration-count: 2 |
delay: 2 | animation-delay: 2 |
yoyo: true | animation-direction: alternate |
gsap.to(".box", {
duration: 2,
x: 200,
rotation: 360,
});
单位
默认情况下,GSAP将使用px和度进行变换,但您可以使用其他单位,如vw,弧度,甚至可以自己进行JS计算或相对值!
x: 200, // use default of px
x: "+=200" // relative values
x: '40vw', // or pass in a string with a different unit for GSAP to parse
x: () => window.innerWidth / 2, // you can even use functional values to do a calculation!
rotation: 360 // use default of degrees
rotation: "1.25rad" // use radians
CSS属性
transforms, color, padding, border radius, GSAP可以动画这一切!只需记住将属性转换为大小写-例如,background-color变为backgroundColor。
gsap.to(".box", {
duration: 2,
backgroundColor: '#8d3dae',
});
尽管GSAP可以对几乎所有CSS属性进行动画处理,但我们建议尽可能使用transforms和opacity。像filter和boxShadow这样的属性对于浏览器渲染来说是CPU密集型的。小心制作动画,并确保在低端设备上进行测试。
SVG属性
就像HTML元素一样,SVG元素可以用transform简写来动画化。此外,您可以使用width对象动画SVG属性,如height,fill,stroke,cx,opacity,viewBox甚至SVG attr本身。
gsap.to(".svgBox", {
duration: 2,
x: 100, // use transform shorthand (this is now using SVG units not px, the SVG viewBox is 100 units wide)
xPercent: -100,
// or target SVG attributes
attr: {
fill: '#8d3dae',
rx: 50,
},
});
任何数值、颜色或包含数字的复杂字符串
GSAP甚至不需要DOM元素就可以使属性具有动画效果。你可以直接定位任何JS对象的任何属性,甚至是你像这样创建的任意属性,onUpdate函数用于监听动画的更新过程:
//create an object
let obj = { myNum: 10, myColor: "red" };
gsap.to(obj, {
myNum: 200,
myColor: "blue",
onUpdate: () => console.log(obj.myNum, obj.myColor)
});
特殊属性
特殊属性用来调整动画的表现形式,我们在上面用到了repeat和duration,下面列出了一些常用的属性:
Property | Description |
---|---|
duration | 动画持续时间(秒)默认值:0.5 |
delay | 动画应开始之前的延迟量(秒)。您可以按一定的秒数来delay动画的开始。您还可以使用repeatDelay来为任何重复迭代的开始添加延迟。 |
repeat | 动画应该重复多少次。无限重复使用repeat: -1 |
yoyo | 如果为真,则补间将每隔一次重复以相反的方向运行。(like a yoyo)默认:false |
stagger | 每个目标动画开始之间的时间(秒)(如果提供了多个目标) |
ease | 控制动画过程中的变化速率,如运动的“个性”或感觉。默认值:“power1.out” |
onComplete | 动画完成时运行的函数 |
更多参考文档:https://gsap.com/docs/v3/GSAP/Tween/
2.4 Easing
Easing可能是动作设计中最重要的部分。一个精心选择的Easing将让你的动画更有个性和有活力。
在下面的演示中看看没有Easing和有Easing之间的区别!没有Easing的绿色盒子以恒定的速度旋转,而有“弹跳”Easing的紫色盒子加速,沿着跑,然后弹跳到一个停止。
gsap.to(".green", { rotation: 360, duration: 2, ease: "none" });
gsap.to(".purple", { rotation: 360, duration: 2, ease: "bounce.out" });
“Easing”是一种数学计算,它控制补间期间的变化率。但别担心,GSAP为您做所有的数学!您只需选择最适合您的动画的Easing。
你可以通过 GSAP 的官网可视化工具来参考不同的 Easing 可选值和效果:https://gsap.com/resources/getting-started/Easing
对于大多数情况下,你可以指定一个Easing类型。有三种类型:in,out和inOut。它们控制着Easing过程中的动量。
像"power1.out"这样的缓动动画是UI过渡的最佳选择;它们开始的速度很快,这有助于UI的响应,然后它们在结束时缓动,给人一种自然的摩擦感。
ease: "power1.in"
// 开始慢,结束快,就像一个重物在下落
ease: "power1.out"
// 开始快,结束慢,就像一个滚动的球慢慢地停下来
ease: "power1.inOut"
// 开始缓慢,结束缓慢,就像汽车加速和减速一样
2.5 交错动画
如果你还没有尝试过在GSAP中创建交错动画,你会得到一种享受-交错是完全可配置的,而且超级强大。如果动画具有多个目标,则可以轻松地在每个动画的开始之间添加一些延迟:
<h3>Click a box to transition out</h3>
<div class="box green"></div>
<div class="box purple"></div>
<div class="box orange"></div>
<div class="box purple"></div>
<div class="box green"></div>
gsap.to(".box", {
duration: 1,
rotation: 360,
opacity: 1,
delay: 0.5,
stagger: 0.2,
ease: "sine.out",
force3D: true
});
document.querySelectorAll(".box").forEach(function(box) {
box.addEventListener("click", function() {
gsap.to(".box", {
duration: 0.5,
opacity: 0,
y: -100,
stagger: 0.1,
ease: "back.in"
});
});
});
通过配置 stagger 值来让多个元素间隔触发动画。
更多玩法参考官方文档:https://gsap.com/resources/getting-started/Staggers/
2.6 时间线
从上面的例子已经可以看出通常情况下我们的动画不止一个,如何控制这些动画的顺序和时间呢?
虽然我们可以使用上面的delay进行简单的控制,延迟物体的动画开始时间;但是如果中间某个物体的动画执行时间突然延长了,那么其后面所有的动画时间需要进行手动进行延迟,这显得非常不方便。
时间线是创建易于调整、弹性动画序列的关键。将动画添加到时间轴时,默认情况下,它们将按照添加顺序一个接一个地播放。 并且同样可以使用 delay 使其偏移时间轴
// create a timeline
let tl = gsap.timeline()
// add the tweens to the timeline - Note we're using tl.to not gsap.to
tl.to(".green", { x: 600, duration: 2 });
tl.to(".purple", { x: 600, duration: 1, delay: 1 });
tl.to(".orange", { x: 600, duration: 1 });
但是如果我们想要在一个动画开始的同时,执行另一个动画,除了再额外创建一条时间线,我们可以在to函数后面加一些小参数来进行精确的控制。
const t1 = gsap.timeline();
t1.to(".red", { x: 400,duration: 1 });
// 在1秒开始插入动画(绝对值)
t1.to(".green", { x: 400, duration: 1 }, 1);
// 在上个动画的开始插入动画
t1.to(".purple", { x: 400, duration: 1 }, "<");
// 在最后一个动画结束后一秒插入动画
t1.to(".orange", { x: 400, duration: 1 }, "+=1");
时间线将如下图所示:
这里可以发现,从 timeline的方法中,有一个 position的可选参数:
.method( target, vars, position )
针对position,参数类型如下:
-
绝对值:在某个绝对秒数来执行动画。
-
<符
和\>符
:"<“在上个动画开始,”>"在上个动画结束。 -
相对符:
+=
在最后一个动画结束后,\-=
在最后一个动画结束前。 -
label值:直接用某个时间点的label名。
const t1 = gsap.timeline();
t1.to(".green", { x: 400, duration: 1 })
.add("myLabel", 2)
.to(".purple", { x: 400, duration: 1 }, "myLabel+=1")
.to(".orange",{ x: 400, duration: 1 }, "myLabel-=1");
通过gsap.add
函数,我们在2秒处放置了一个myLabel的标识,在后面使用myLabel+=1和myLabel-=1相对这个标识的时间进行控制。时间线如下:
不同时间线中的动画可能会有相同的特殊属性,比如repeat和delay等,我们可以在时间线的创建函数中统一设置,避免重复:
const tl = gsap.timeline({ repeat: 1, repeatDelay: 1, yoyo: true });
tl.to(".green", { rotation: 360 })
.to(".purple", { rotation: 360 })
.to(".orange", { rotation: 360 });
如果你发现某个属性你重复使用了很多次,比如x、scale、duration等,我们就可以使用defaults属性
,任何加到defaults属性中的参数都会被下面的函数继承。
const tl = gsap.timeline({
defaults: {
scale: 1.2,
duration: 2,
},
});
tl.to(".green", {
x: 200,
})
.to(".purple", {
x: 400,
})
.to(".orange", {
x: 600,
});
2.7 分配实例
为了以后控制动画实例,将其分配给一个变量(GSAP是方便的面向对象):
let tween = gsap.to(".class", { rotation: 360, duration: 5, ease: "elastic" });
//now we can control it!
tween.pause();
tween.seek(2);
tween.progress(0.5);
tween.play();
2.8 控制方法
到目前为止,我们看到的所有动画都是在页面加载或delay之后播放的。但是如果你想对你的动画有更多的控制呢?一个常见的用例是在某个交互(如按钮单击或悬停)上播放动画。
控制方法可以用于补间和时间线,并允许您播放,暂停,逆转,甚至加快您的动画!
// store the tween or timeline in a variable
let tween = gsap.to("#logo", {duration: 1, x: 100});
//pause
tween.pause();
//resume (honors direction - reversed or not)
tween.resume();
//reverse (always goes back towards the beginning)
tween.reverse();
//jump to exactly 0.5 seconds into the tween
tween.seek(0.5);
//jump to exacty 1/4th into the tween 's progress:
tween.progress(0.25);
//make the tween go half-speed
tween.timeScale(0.5);
//make the tween go double-speed
tween.timeScale(2);
//immediately kill the tween and make it eligible for garbage collection
tween.kill();
// You can even chain control methods
// Play the timeline at double speed - in reverse.
tween.timeScale(2).reverse();
在用户交互事件的事件侦听器中,我们可以使用控制方法来精细控制动画的播放状态。
在下面的示例中,我们为每个元素创建一个时间轴(这样它就不会在所有实例上触发相同的动画),将该时间轴的引用附加到元素本身,然后在元素悬停时播放相关的时间轴,在鼠标离开时反转它。我们也在调整速度,所以倒车时更快,进入时更慢。这是一个很好的UX模式。
gsap.set(".information", { yPercent: 100 });
gsap.utils.toArray(".container").forEach((container) => {
let info = container.querySelector(".information"),
let silhouette = container.querySelector(".silhouette .cover"),
let tl = gsap.timeline({ paused: true });
tl.to(info, { yPercent: 0 }).to(silhouette, { opacity: 0 }, 0);
container.addEventListener("mouseenter", () => tl.timeScale(1).play());
container.addEventListener("mouseleave", () => tl.timeScale(3).reverse());
});
2.9 回调函数
如果你想在动画开始,或者当动画结束时运行一些JS,你可以使用回调。所有动画和时间线都有这些回调:
-
onComplete:动画完成时调用。
-
onStart:动画开始时调用
-
onUpdate:每次动画更新时调用(在动画处于活动状态时的每一帧上)。
-
onRepeat:每次动画重复时调用。
-
onReverseComplete:当动画反转后再次到达其开始时调用。
gsap.to(".class", {
duration: 1,
x: 100,
// arrow functions are handy for concise callbacks
onComplete: () => console.log("the tween is complete")
});
// If your function doesn't fit neatly on one line, no worries.
// you can write a regular function and reference it
gsap.timeline({onComplete: tlComplete}); // <- no () after the reference!
function tlComplete() {
console.log("the tl is complete");
// more code
}
3.GSAP In React
GSAP本身是完全与框架无关的,可以在任何JS框架中使用,无需任何特殊的包装器或依赖项。然而,为了贴合 React 的最佳实践和一些设计上的摩擦点,也为了让我们专注于更有趣的内容,GSAP 提供了一个钩子函数。
3.1 安装
# Install the GSAP library
yarn add gsap
# Install the GSAP React package
yarn add @gsap/react
3.2 useGSAP()
该 hook 是useEffect()或useLayoutEffect()的直接替代品,使用gsap.context()自动处理清理,符合 React 中 Effect 的规范(https://react.dev/learn/synchronizing-with-effects),当卸载组件并拆除钩子时,执行useGSAP()钩子时创建的所有GSAP动画、ScrollTriggers、Draggables和SplitText实例将自动恢复。
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
const container = useRef();
useGSAP(() => {
// gsap code here...
gsap.to(".box", {x: endX}); // <-- automatically reverted
}, { scope: container }); // <-- scope is for selector text (optional)
这个钩子在Next或其他服务器端渲染环境中使用是安全的。它实现了useIsomorphicLayoutEffect技术,首选React的useLayoutEffect(),但如果没有定义window,则返回useEffect()。
3.3 配置对象
类似useEffect()该钩子第二个属性是可选的。你可以传递一个依赖数组或者一个配置对象以获得更大的灵活性。
字段 | 类型 | 描述 |
---|---|---|
dependencies | Array / null : default [] | 传递给内部useEffect的依赖数组。 |
scope | React ref | 指定一个容器作为作用域,确保 useGSAP() 钩子的作用域是该容器的后代。 |
revertOnUpdate | Boolean : default false | 默认为 false,只有当组件被卸载并且钩子被拆除时,它们才会恢复。 如果您希望在每次钩子重新挂起时(当任何依赖项发生变化时)都恢复上下文,则可以设置为 true。 |
// config object offers maximum flexibility
useGSAP(() => {
// gsap code here...
}, { dependencies: [endX], scope: container, revertOnUpdate: true});
useGSAP(() => {
// gsap code here...
}, [endX]); // simple dependency array setup like useEffect, good for state-reactive animation
useGSAP(() => {
// gsap code here...
}); // defaults to an empty dependency array '[]' and no scoping.
3.4 为什么 Cleanup 如此重要?
正确的动画清理对于框架来说非常重要,尤其是对于React。React 18默认在本地以严格模式运行,这会导致你的Effects被调用两次。如果你没有正确地还原,这可能会导致重复的、冲突的动画或补间动画的逻辑问题。useGSAP() 勾子跟随 React的最佳实践 用于动画清理。
3.5 注意动画的上下文安全!
当useGSAP()钩子执行时创建的所有GSAP动画、ScrollTriggers、Draggables和SplitText实例将自动添加到内部 gsap.context(),并在组件卸载和钩子拆除时恢复。这些动画被认为是“上下文安全”的。
⚠️ 但是,如果您创建的任何动画在useGSAP()
钩子执行后被调用(如单击事件处理程序,setTimeout()
中的某些内容或任何延迟),则这些动画将不是上下文安全的。
钩子函数****useGSAP()提供了更多的能力~
该钩子函数下还有一个属性和一个函数:
-
context:gsap.context()实例,用于跟踪所有动画。
-
contextSafe:将任何函数转换为上下文安全的函数,以便在该函数执行时创建的任何GSAP相关对象将在该Context恢复(清理)时恢复。context-safe函数中的contextText也将使用Context的作用域。contextSafe()接受一个函数并返回该函数的一个新的上下文安全版本。
我们可以在contextSafe()函数中包装我们的点击动画,以便将其添加到上下文。有两种方法可以访问此功能:
1)使用返回的对象属性(用于useGSAP()钩子外部)
点击事件时添加的动画被添加到上下文安全函数内部
const container = useRef();
const { contextSafe } = useGSAP({scope: container}); // we can pass in a config object as the 1st parameter to make scoping simple
// ✅ wrapped in contextSafe() - animation will be cleaned up correctly
// selector text is scoped properly to the container.
const onClickGood = contextSafe(() => {
gsap.to(".good", {rotation: 180});
});
return (
<div ref={container}>
<button onClick={onClickGood} className="good"></button>
</div>
);
2)使用第二个参数(用于useGSAP()钩子内部)
上下文安全点击事件时添加的动画被添加到内部上下文
如果您手动添加事件侦听器,这在React中并不常见,请不要忘记返回一个清除函数,以便删除事件侦听器!
const container = useRef();
const badRef = useRef();
const goodRef = useRef();
useGSAP((context, contextSafe) => {
// ✅ safe, created during execution
gsap.to(goodRef.current, {x: 100});
// ❌ DANGER! This animation is created in an event handler that executes AFTER useGSAP() executes. It's not added to the context so it won't get cleaned up (reverted). The event listener isn't removed in cleanup function below either, so it persists between component renders (bad).
badRef.current.addEventListener("click", () => {
gsap.to(badRef.current, {y: 100});
});
// ✅ safe, wrapped in contextSafe() function
const onClickGood = contextSafe(() => {
gsap.to(goodRef.current, {rotation: 180});
});
goodRef.current.addEventListener("click", onClickGood);
// 👍 we remove the event listener in the cleanup function below.
return () => { // <-- cleanup
goodRef.current.removeEventListener("click", onClickGood);
};
}, {scope: container});
return (
<div ref={container}>
<button ref={badRef}></button>
<button ref={goodRef}></button>
</div>
);
4.插件
插件为GSAP的核心增加了额外的功能。这允许GSAP核心保持相对较小的规模,并允许您仅在需要时添加功能。
使用插件只需要 2 步:
- 安装插件
yarn add [plugin-name]
- 注册插件
gsap.registerPlugin([plugin-name1], [plugin-name2])
4.1 ScrollTrigger
这是一个将时间线和滚动条挂钩的插件
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
gsap.to(".green", {
rotation: 360,
scale: 1.5,
backgroundColor: "red",
scrollTrigger: {
trigger: ".green",
scrub: true,
},
});
gsap.to(".purple", {
rotation: 360,
scale: 1.5,
backgroundColor: "red",
scrollTrigger: {
trigger: ".purple",
scrub: 1,
},
});
trigger表示当前动画触发的元素,scrub表示是否将动画效果链接到滚动条,随着滚动条平滑处理;如果是false(默认),随着元素出现在视窗内,直接触发动画,如果是true,则平滑动画。
scrub还可以是某个具体的数值,表示延迟滚动条多少秒动画;比如这里的1,延迟1秒执行动画。
我们在滚动浏览器时,可以使用pin属性将某个元素固定在某个位置;pin可以是css选择器字符串、布尔值或者直接dom元素;如果是true,则直接固定当前的动画元素;我们这里使用pin将purple元素固定起始位置:
gsap.to(".green", {
x: 400,
duration: 2,
scrollTrigger: {
trigger: ".green",
pin: ".purple",
},
});
start(浏览器底部)和end(浏览器顶部)属性用来决定滚动触发元素开始的位置,可以是字符串、数值或者函数,两者的用法类似,我们以start为例;start的值默认是"top bottom"
,它的含义是当触发物体(trigger)的顶部(top)碰到浏览器的底部(bottom)时;达到触发位置。
第一个top值表示物体的上边界,同样的我们可以设为bottom(物体下边界)、center(物体中间)或者具体数值(100px、80%),即控制的是物体旁边的start线。
第二个值表示浏览器视窗滚动触发的scroller-start线,bottom表示视窗的底部,我们也设为top或者center或者数值,以及百分比(例如80%,表示整个视窗的80%高度),甚至是相对位置,比如bottom-=100px。
gsap.to(".green", {
rotate: 360,
scrollTrigger: {
trigger: ".green",
start: "top bottom-=100px",
end: "center 10%",
markers: true,
scrub: true,
},
});
该段配置表示元素的中间从距离浏览器顶部的 10% 的位置开始旋转,旋转到元素的顶部碰到距浏览器底部 100 像素的地方,刚好旋转 360 度,完成动画。示例:https://gallery.xieyufei.com/case/gsap/demo#demo23
4.2 Draggable
gsap.registerPlugin(Draggable)
Draggable.create("#yourID");
4.3 更多插件
https://gsap.com/docs/v3/Plugins
5.进阶
本文更多在于对官方文档的翻译和整理,让大家对 GSAP 的基础功能有一个简单了解和印象,更多的精细用法和更绚烂的动画制作还需要大家自行继续探索~
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)