Angular 17 新特性超详细解析,值得收藏系列

DevUI是面向企业中后台产品的开源前端解决方案,其设计价值观基于高效、开放、可信、乐趣四种自然与人文相结合的理念,旨在为设计师、前端开发者提供标准的设计体系,并满足各类落地场景,是一款企业级开箱即用的产品。

感谢我们的DevUI社区贡献者Allen5288提供的优质好文,真的超详细~!

Angular 17 新特性

经过过去几个版本的快速发展,Angular 已经迎来了全新的面貌。凭借基于信号的反应性、水化、独立组件、指令组合等创新功能,它已经得到了数百万开发人员的实战检验和广泛喜爱。

一个全新的logo,也象征着一个新的阶梯开始。

1、面向未来的文档

Url:angular.dev/

新家: - 新的结构 - 新的指南 - 改进的内容 - 交互式学习之旅平台

文档的内容:

  1. 新文档的基石是的嵌入式教程,这是一种直接在浏览器中学习 Angular 的新方法。教程是用WebContainers编写的,现在提供了简短的步骤以及并行运行的代码示例,以学习(或回到 😉)Angular 的核心概念。上面的reveal answers可以显示答案

  2. 推出Angular.dev/playground --- 一个直接在浏览器中探索最新 Angular 概念的游乐场。从"Hello World"开始,或选择模板之一并探索我们的最新功能,包括控制流、信号等。这里有比较完整的从components,hrml,css的示例,可以在这里调试你想要的功能或者进行功能的学习了解

  3. Reference,这里可能是最具有实用性之一的,angular内部不同内容的参考说明;

当前为angular.dev 的 Beta 预览版,计划在 v18 中将其设为 Angular 的默认网站。 在"Announcing angular.dev"中了解更多信息。

2、内置控制流程

内置控制流可以: 1. 更符合人体工程学的语法,让代码更加直观,减少文档查找的需求。 2. 通过更优化的类型检查,提供更好的类型安全性。 3. 该功能主要在构建时起作用,最大限度地减少了运行时占用空间,从而有可能将包大小减少30kb,同时提高 Core Web Vital 得分。 4. 无需额外导入,该功能自动适用于模板。

2.1 先前的ngIf/ngFor/Switch是如何工作的

指令与视图 Angular编译器 在框架中,将组件的模板分解为"视图"。视图是具有静态 HTML 内容的模板片段。它可以具有动态属性和文本,但 HTML 元素是稳定的。

css 复制代码
<h1>test1</h1>
<ul *ngIf-"condition">
    <li ngFor-"let user of users">{{ user.name }}</1i>
</ul>

该*语法实际上是在ng-template元素上应用属性指令的语法糖;

css 复制代码
<h1>test1</h1>
<ng-template [nglf]-"condition">
<ul>
    <ng-template ngFor [ngFor]="users" let-user>
        <li>{{ user.name }}</11>
    </ng-template>
</ul>
</ng-template>

这里ngIf和ngFor是简单的指令。然后每个ng-template都会生成一个"视图"。每个视图都有一个永不改变的静态结构。但这些视图需要在某个时刻动态插入。这就是发挥作用的地方。

xml 复制代码
<h1>test1</h1>
<!-- special comment -->
<ul>
 <!-- special comment -->
</ul>
<li>{{ user.name }}</li>

Angular 有个概念叫做ViewContainer。ViewContainer是Angular的一个内部API。一个ViewContainer就像一个盒子,您可以在其中插入/删除子视图。为了标记这些容器的位置,Angular 在创建的 DOM 中使用特殊的 HTML 注释。

scss 复制代码
createelement( hi );
if (condition) (
    createElement('ul');
    for (user of users) {
        createElement('li');
    }
}

视图的创建与销毁

所以说,ngIf和else是通过创建和销毁视图来实现的。这是Angular的一种称为"虚拟DOM"的技术,它允许Angular在不直接操作DOM的情况下更新DOM。 当Angular编译模板时,它会创建一个虚拟DOM,这是一个与实际DOM相似的数据结构,但是它不会影响实际的DOM。当Angular更新虚拟DOM时,它会比较虚拟DOM和实际DOM的差异,并只更新必要的部分。 当ngIf的表达式值从false变为true时,Angular会创建一个新的子视图,并将其插入到虚拟DOM中。然后,Angular会将这些更改反映到实际的DOM中。 当*ngIf的表达式值从true变为false时,Angular会销毁子视图,并从虚拟DOM中移除它。然后,Angular会将这些更改反映到实际的DOM中。

总而言之 结构指令很强大,但也有一些缺点。修复这些缺点需要在编译器和框架中进行大量工作。 这就是为什么 Angular 团队决定引入一种新语法来在模板中编写控制流语句!

2.2 新的控制流语法使用

该语法由 Angular 编译器解释,并创建与之前的模板相同的视图,但没有创建结构指令的开销,因此它的性能也更高一些(因为它在内部使用了全新的编译指令)生成的代码)。由于这不是指令,因此类型检查也更好。

像"*ngIf"这样的结构型指令,由于能够修改DOM结构,在使用过程中可能会出现模板类型错误【模板表达式或数据绑定中,所使用的变量类型与预期不符或者不支持的操作】。"@if"因为它不会更改DOM结构,从而避免了一些可能出现的错误。

不但解决了else的笨拙性,也增加了else if的可能性

@if

scss 复制代码
@if (condition) {
    xxxx
} @else {
    yyyy
}

@for

scss 复制代码
@for (user of users; track user.id) {
    {{ user.name }}
} @empty {
    Empty list of users
}

控制流语法的非常有用的补充之一是空集合的处理。以前,如果集合为空或为空,您必须使用 ngIf来显示消息null,然后使用ngFor来迭代集合。

我们仍然可以访问以前*ngFor中使用的变量,如下:

bash 复制代码
- $index获取当前项目的索引
- $first知道当前项目是否是第一个
- $last知道当前项目是否是最后一项
- $even了解当前项目是否处于偶数索引
- $odd了解当前项目是否处于奇数索引
- $count获取集合的长度

与*ngFor 不同是,我们不必为这些变量添加别名即可使用它们,但如果需要,仍然可以这样做,例如在使用嵌套循环时。

*ngFor指令是一种声明性语法,用于在模板中循环遍历数组或可迭代对象。这个指令会创建一个视图列表,列表中的每一项都是原始数据的一个副本。这意味着,如果原始数据发生变化,这些变化不会影响到已经渲染的列表项。为了实现这种"隔离"效果,Angular需要在背后进行一些额外的处理,这可能会增加一些性能开销。 还值得注意的是,当@for中得集合发生变化时,控制流在底层使用了一种新的算法来更新 DOM。它应该比 *ngFor所使用的算法快很多,angular内部统计增速90%,因为它在大多数情况下不分配中间映射。

@siwtch

vbnet 复制代码
@switch (accessLevel) {
    @case ('admin') { <admin-dashboard/> }e
    @case ('moderator') { <moderator-dashboard/> }
    @default { <user-dashboard/> }
}

这可能是新类型检查最耀眼的地方,因为在case中使用不存在的值现在会引发编译错误! 还值得注意的是,该@switch语句使用严格相等(===)来比较值,而*ngSwitchused则使用松散相等(==)。Angular v17 引入了一项重大更改,*ngSwitch现在也使用严格相等,如果您使用宽松相等,则在开发过程中控制台会发出警告:

2.3 迁移

控制流语法是 Angular v17 中引入的新"开发者预览"功能,并且可能会成为将来编写模板的推荐方式(计划是在经过实战测试后使其在 v18 中稳定)。 Angular团队 还提供了自动迁移,将结构指令转换为现有应用程序中的控制流语句。该迁移在 Angular v17 中作为开发者预览版提供。

scss 复制代码
ng g @angular/core:control-flow
ng g @angular/core:control-flow --path src/app/app.component.html

延迟视图

@defer: 定义一个模板块,在渲染或者在加载时候,为渲染提供一个占位符,可以跳过应用程序渲染部分,仅在发生某些事件(立即、滑动到、点击到等)的时候显示他们(该块中求全部得组件、管道、指令、库) Defer内容会打包到文件中,稍后加载该文件,也使得初始化块更小,

在Angular中,确实存在一种延迟加载的模式,它被称为懒加载。这种技术的主要目的是为了减小应用程序的初始加载时间并提高用户体验。默认情况下,Angular会一次性加载所有的组件,随着项目功能的增加,这可能会导致一些不需要的库或模块也被加载,从而增加了加载的时间。 要在Angular中实现模块的延迟加载,可以在AppRoutingModule routes然而,懒加载并不意味着所有的模块都会被延迟加载。实际上,只有那些在需要时才会被真正加载的模块才会被延迟加载。例如,在一些大型的应用程序中,可能有很多路由,这时就可以考虑使用延迟加载------一种根据需要加载NgModules的设计模式。 中使用loadChildren代替component进行配置。此外,还需要注意从AppModule上卸下已经被延迟加载的模块。

3.1 基础用法

随着控制流语法的引入,Angular 团队还引入了一种延迟加载组件的新方法(目前作为开发人员预览版)。我们在 Angular 中已经有了延迟加载,但它主要是基于路由的。 Angular v17 得 @defer 增加了一种使用模板中的语法延迟加载组件的新方法。

@defer允许您定义一个模板块,当满足条件时将延迟加载该模板块(该块中使用的所有组件、管道、指令和库也将延迟加载)。可以使用几个条件。例如,它可以是"尽快(无条件)"、"当用户滚动到该部分时"、"当用户单击该按钮时"或"2 秒后"。

假设您的主页显示一个很大型化得 ChartComponent它使用图表库和一些其他依赖项,例如管道FromNow: 假设该组件最初在主页上不可见,可能是因为它位于页面底部,或者因为它位于不活动的选项卡中。避免立马加载他是有意义的,像我们刚才说的。因为它会减慢页面的初始加载速度。

Angular 编译器会将静态导入改为ChartComponent 动态导入(() => import('./chart.component')),只有满足条件时才会加载组件。由于该组件现在是动态导入的,因此它不会包含在主包中。bundle将为它创建一个新块:chunk-xxxx.js只有满足条件时才会加载该文件,并ChartComponent显示 。

默认情况下,延迟块在被触发之前不会渲染任何内容。@占位符是一个可选的块,它声明了在延迟块被触发之前要显示的内容。一旦加载完成,这个占位符内容将被替换为主要内容。我们可以再占位符部分中添加任何内容,包括纯HTML、组件、指令和管道;但请记住,占位符块的依赖项是需要加载的。 您可以用 @placeholder 定义一个占位符模板 ,在满足加载条件之前将显示该模板。 占位符块接受一个可选参数,以指定此占位符应该显示的最小时间量。此最小参数以毫秒(ms)或秒(s)的时间增量指定。@占位符块的最小计时器在此@占位符块的初始渲染完成后开始。

typescript 复制代码
@defer (when show) {
    <ns chart />
}
@placeholder (minium XXX m/s) {
    <div>Something until the loading starts</div>
}

然后,在加载块时,您可以使用@loading 来显示加载模板。@loading块是一个可选的块,它允许您声明将在加载任何延迟依赖项期间显示的内容。例如,您可以显示一个加载微调器。如果未定义@loading块,则占位符将保留在那里,直到加载块为止。 两个参数,用于指定此占位符应该显示的最小时间量以及加载开始后在显示加载模板之前等待的时间量。。@加载块的min和after定时器都在加载被触发后立即开始 由于@defer块加载可能非常快,因此存在加载块显示和隐藏太快的风险,从而导致"闪烁"效果。为了避免这种情况,您可以使用该after选项来指定应在多少毫秒后显示加载。 如果目标块的加载时间小于此延迟,则@loading对应得块内容永远不会显示。您还可以使用该minimum选项指定加载的最短持续时间。如果加载速度快于最短持续时间,则加载将显示最短持续时间(这仅适用于加载已经被显示出来的情况)。

bash 复制代码
@loading  (after YYY ms/s, minium XXX m/s) {
    <div>Loading...</div>
}

您还可以用@error定义一个错误模板,他也是可选的。如果块加载失败,将显示该错误模板。 请注意,如果用到对应得占位符,加载,错误块,对应得依赖均需要对应得被加载

css 复制代码
@error {
    <div>Something went wrong</div>
}

大页面加载 - --->加载占位符内容 - --->条件满足

  • --->准备开始加载组件 - --->占位内容消失 - --->加载中内容 - --->加载中内容消失 - --->主要内容登场

3.2 控制语法

1、无条件: default

scss 复制代码
@defer {
  <ns-chart />
}

2、idle 同1

python 复制代码
@defer (on idle) {
  <ns-chart />
}

3、简单得布尔条件

sql 复制代码
@defer (when show) {
  <ns-chart />
}

4、on immediate 立即

typescript 复制代码
@defer (on immediate) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

5、on timer(x) 计时

python 复制代码
@defer (on timer(500ms)) {
  <calendar-cmp />
}

6、on viewport (x)

typescript 复制代码
@defer (on viewport) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

7、on interaction(x) 事件

typescript 复制代码
@defer (on interaction) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

8、on hover (x)

css 复制代码
@defer (on hover(greeting)) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}

3.3 Testing:

Angular提供了TestBed API来简化测试@delay块的过程,并在测试过程中触发不同的状态。

  1. DeferBlockState.Placeholder:显示块的占位符状态
  2. DeferBlockState.Loading:显示方块的加载状态
  3. DeferBlockState.Error:显示该块的错误状态
  4. DeferBlockState.Complete:显示延迟块,就像加载完成一样
scss 复制代码
class ComponentA {}

const componentFixture = TestBed.createComponent(ComponentA);
  //获取了所有延迟块的列表,并选择第一个延迟块
  const deferBlockFixture = componentFixture.getDeferBlocks()[0];
  //状态设置为加载状态(Loading),并验证其渲染完成的样子。
  await deferBlockFixture.render(DeferBlockState.Loading);
  expect(componentFixture.nativeElement.innerHTML).toContain('Loading');
  //状态设置为已完成状态,并验证渲染完成样子.
  await deferBlockFixture.render(DeferBlockState.Completed);
  expect(componentFixture.nativeElement.innerHTML).toContain('<large-component>');
});

4、Signal

4.1 Background

信号当前在很多框架中应用,如SolidJS,Vue等,它本身是通过提供一些原型来定义应用程序中的反应状态,并允许框架知道哪些组件受到更改的影响,而不必检测整个组件树上的更改。 对于Angular,依赖于 zone.js ---> 信号可以进队受更改影响组件检查,更高效。

4.2 信号API

Angular 提供了一个 singal() 函数;count得类型是 WritableSignal;

基本使用方式如下所示:

派生值 -- 随着依赖信号自动计算,Lazy Loading;不可写

信号变化 相应函数 -- effect() -- 类似于BehaviorSubject,但是effect使用了弱引用,无需取消订阅

排除信号 -- untracked()

注意1!:mutate 方法再Angular16中短暂测试登场,在17中已然落幕了 注意2!:computed里面不能做set操作,会进入无限循环

javascript 复制代码
import { signal } from '@angular/core';
// 定义一个信号
const count = signal(0);
// 获取
const value = count();
// 设置
count.set(1);
// 更新
count.update((value) => value + 1);
// 配置只读
const readonlyCount = count.asReadonly();
// 派生值
const double = computed(() => count() * 2);
//  相应信号变化
const effectRef: EffectRef = effect(() => console.log(count()));
//  停止响应
effectRef.destroy();
//  排除信号
const multiplier = signal(2);
const total = computed(() => count() * untracked(() => multiplier()));

Angular 独特的一个功能是可以将 ValueEqualityFn 传递给 signal函数。如👉

scss 复制代码
const user = signal({ id: 1, name: 'Cédric' }, { equal: (previousUser, newUser) => previousUser.id === newUser.id });
//  upperCaseName不会重新计算当用户变化时,ID不变
const uppercaseName = computed(() => user().name.toUpperCase());

4.3 组件间信号共享

我们可以使用Service来做共享: 定义 CountService 如下:

kotlin 复制代码
@Injectable({ providedIn: 'root' })
export class CountService {
  count = signal(0);
}

在我们的组件中,我们可以注入服务并使用信号:

ini 复制代码
export class UserComponent {
  count = inject(CountService).count;
}

4.4 内存泄漏

无需像 Rxjs 那样担心需要取消订阅观察量来避免泄漏问题, Angular 会帮助我们收集已无 consumer 的信号;无需 destroy,Angular 在内部利用 DestroyRef 已经在做了。

当然,我们也可以同过 effect 来观察和控制;并且我们可以开启特性选项来手动控制,当如此做时,我们需要手动销毁:

javascript 复制代码
this.logEffect = effect(() => console.log(count()), { manualCleanup: true });
ngOnDestroy() {
  this.logEffect.destroy();
}

effect 可能很少使用,但在某些情况下它们可以很方便: 记录/跟踪; 将状态同步到 DOM 或存储等。

4.5 Signal 与 Rxjs 交互

我们可以一起使用信号和可观察量,并且可以将其中一种转换为另一种。全新的 @angular/core/rxjs-interop 软件包中提供了两个可以实现的功能。

1)将 signal 转换为 observable

scss 复制代码
const count$ = toObservable(count);

2)将 observable 转换为 signal

ini 复制代码
const count = toSignal(count$);

由于 observable的异步性,我们可谓其提供初始值

ini 复制代码
const count = toSignal(count$, { initialValue: 0 });

如果没有给初值,在 emit 出一个值前,该值为 undefined, 可以使用以下选项再次场景时抛错

ini 复制代码
const count = toSignal(count$, { requireSync: true });

5、Angular CLI 新的构建系统

在 v17 及更高版本中,新的构建系统提供了一种改进的方式来构建 Angular 应用程序。这个新的构建系统包括:

  1. 使用 ESM 的现代输出格式,具有动态导入表达式以支持延迟模块加载。
  2. 针对初始构建和增量构建时的构建时间,有着更快更高的性能。
  3. 较新的 JavaScript 生态系统工具,例如 esbuild 和 Vite 。
  4. 集成 SSR 和预渲染功能。

几个构建对比

小结:

WebPack: 支持多种模块化规范、插件丰富、生态庞大; 构建速度较慢,体积大,配置复杂

Vite: 简单易用、打包小、快速冷启动、及时热更新; 旧浏览器兼容差、社区生态还在早期起步阶段

Esbuild: 编译快、支持多模块格式、配置简单 功能较基础、灵活性低

这个新的构建系统已经稳定了,并且完全支持与 Angular 应用程序一起使用。可以将使用browser构建器的应用程序迁移到新的构建系统

1、对于新的项目 在生成新项目时默认使用application 的新构建器

2、对于现有的程序,可以选择在每个应用程序的基础上使用新的构建器 browser-esbuild 仅构建应用程序的客户端包,该应用程序旨在现存的构建系统上兼容 browser构建器。它可以作为现有browser应用程序的直接替代品。 Application 构建器可涵盖整个应用程序,无论是客户端还是服务器端的渲染,并执行静态页面的构建时预渲染。

3、 '--standalone' 当前为CLI的默认行为:ng new 将生成一个 standalone 的 项目、组件、管道、指令等;

4、 ng new 现在 默认启用路由。

6、SSR 渲染

Angular 17的服务器端渲染(SSR)是一个将Angular应用的渲染工作从浏览器转移到服务器的过程,这使得初始HTML内容可以在浏览器加载前就发送出来,从而优化页面的首次加载速度。在Angular 17中,添加SSR变得非常简单,用户只需要运行下面命令即可完成相关的设置。

sql 复制代码
ng new --ssr
ng add @angular/ssr 

server.get('*', ...):这是一个Express.js的路由处理器,它会捕获所有的HTTP GET请求。 const { protocol, originalUrl, baseUrl, headers } = req;:这行代码从请求对象中提取了一些有用的信息,包括协议类型(http或https)、原始URL、基础URL和请求头。 commonEngine.render(...):这是调用Angular Universal的渲染方法,它会生成一个HTML字符串,这个字符串包含了完全渲染的Angular应用。 res.send(html):这行代码会将生成的HTML字符串作为HTTP响应发送给客户端。 next(err):如果在渲染过程中发生错误,行代码会将错误传递给下一个错误处理中间件。

Angular Universal的实现并不依赖于特定的服务器端技术。虽然有一些例子使用Node.js和Express.js来实现服务器端渲染,但实际上只要能够提供HTTP服务并解析Angular Universal所需的特定路由,任何类型的后端服务都可以与之配合。

总的来说,Angular 17的SSR功能不仅使得应用程序的加载速度得到提升,同时也改善了用户体验,并进一步提升了Angular作为前端开发框架的地位。

7、Hydration

Hydration 已不再是开发者预览版,并且在所有使用 SSR 的新应用程序中默认启用!

首先需要看下:CSR和SSR的基本区别:

  • CSR(Client-side Rendering):在CSR中,整个应用程序的构建和渲染都发生在客户端浏览器中。
  • SSR(Server-side Rendering):在SSR中,服务器在接收到客户端请求时,会在服务器上预先渲染HTML内容,并将其发送到客户端浏览器。

加载过程:

Hydration的优势和应用场景:

  • 改善首屏加载性能
  • 提高SEO
  • 渐进增强

Hydration的挑战和注意事项:

  • 额外的复杂性
  • 代码拆分
  • 初始状态同步

前后对比:

8、新的生命周期 hooks

新增了两个生命周期 hooks: afterRender --- 注册一个回调函数,每次应用完成渲染时调用 afterNextRender --- 注册一个回调函数,应用下次完成渲染时调用

关键词:只适用于浏览器,独立函数,接受回调,不依赖特定组件实例,应用程序范围Hooks

渲染后阶段:指定Phase来控制DOM操作顺序

Phase有四个阶段,按以下顺序进行

9、DevTools 中增加的依赖注入调试

这里,我们想找到我们的todoServices,我们从源端,一致找到我们的root去找到inject得地方

当你的项目非常大的时候,这个会很有帮助;如果一些service 在很多地方提供,你不知道在那个地方提供得内容正在服务于你现在得组件;

Injector Tree tab页,我们看下有什么功能: 他将图形化你当前应用得injector层次结构;你可以点击这些injector,看这些providers都有什么。能解析哪些内容。 你可以看到,这个层次结构可能会非常大,尤其对于一些大型项目; 当你inspect element 在你的页面时候,也可以浏览到对应的injector 结构 当然,environment 层级结构也是在这里得。可以看到,我们点到不同地方,上面得环境起始点是会变的,他不会从最后的这个开始跳转了。

10、下一步计划

1、Angular 18 -- 预计今年5月:

2、反应系统的增强: 增加与基于Zone.js 变更检测前后兼容和互操作性 Signal 开发者版本同时,effect继续在预览状态迭代语义 下个版本将推出基于 Signal的输入、视图查询功能

3、测试系统: Jest继续试验中 尝试Web Test Runner

4、Material3: 重构 Angular Material 的内部结构,合并设计令牌 在V17小版本里开启M3和设计令牌支持 之后,Angular Material v16 将不再与较新版本的Angular兼容

11、学习平台

1、新系列官方Angular介绍课程: www.youtube.com/watch?v=xAT...

2、Sololearn 合作学习平台,创建了一个互动学习之旅,在过去几个月内参与了超过 10+ 万人! www.sololearn.com/en/onboardi...

🔥 加入我们

DevUI是面向企业中后台产品的开源前端解决方案,其设计价值观基于高效、开放、可信、乐趣四种自然与人文相结合的理念,旨在为设计师、前端开发者提供标准的设计体系,并满足各类落地场景,是一款企业级开箱即用的产品。

如果你今天刚刚加入我们,可以先看看官网上的示例组件,你可以在左侧导航栏中切换想要查看的组件,然后通过右侧的快速前往在不同Demo之间切换。

如果你准备添加 Vue DevUI,请前往快速开始文档,只需要几行代码。

如果你对我们的开源项目感兴趣,并希望参与共建,欢迎加入我们的开源社区,关注DevUI微信公众号:DevUI 。

文 / DevUI社区贡献者 Allen5288

相关推荐
拾光拾趣录4 分钟前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css
莫空00005 分钟前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试
guojl6 分钟前
深度剖析Kafka读写机制
前端
FogLetter6 分钟前
图片懒加载:让网页飞起来的魔法技巧 ✨
前端·javascript·css
Mxuan7 分钟前
vscode webview 插件开发(精装篇)
前端
Mxuan8 分钟前
vscode webview 插件开发(交付篇)
前端
Mxuan9 分钟前
vscode 插件与 electron 应用跳转网页进行登录的实践
前端
拾光拾趣录10 分钟前
JavaScript 加载对浏览器渲染的影响
前端·javascript·浏览器
Codebee10 分钟前
OneCode图表配置速查手册
大数据·前端·数据可视化
然我10 分钟前
React 开发通关指南:用 HTML 的思维写 JS🚀🚀
前端·react.js·html