开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

解决Android Studio不能创建aidl文件问题

发表于 2024-04-27

如图,在准备创建aidl文件时选项置灰

image.png

仔细看其实就是配置文件的问题,在build.gradle(:app)下的配置添加

1
2
3
gradle复制代码    buildFeatures {
aidl true
}

完整配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
gradle复制代码plugins {
id 'com.android.application'
}

android {
namespace 'com.example.aidl'
compileSdk 34

defaultConfig {
applicationId "com.example.aidl"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
aidl true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {

implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何构建Android应用动态图标?

发表于 2024-04-27

揭开 Android 中动态应用图标的秘密, 通过本分步指南彻底改变应用的用户体验!

简介

你是否曾瞥一眼手机屏幕, 发现某个应用的图标看起来焕然一新, 与众不同? 这不仅仅是视觉上的炫耀, 而是动态应用图标的魔力在发挥作用. 这项迷人的功能允许 Android 应用动态更改图标, 而无需从 Play Store 进行更新! 这是一种微妙而强大的策略, 可以吸引用户并为你的应用增添活力. 虽然这看似高深莫测, 但掌握动态应用图标绝对不是难事. 让我们一起踏上学习之旅吧!

如果你是 Android 开发的新手, 或者即使你已经在游戏中摸爬滚打多年, 你可能还没有探索过动态应用图标这个有趣的世界. 不过不用担心! 本指南旨在揭开这一过程的神秘面纱, 提供在 Android 中创建动态应用图标的逐步过程. 无论你是刚刚开始编码冒险, 还是希望为自己的工具包添加另一项技能, 本篇文章都将指导你以独特的方式增强应用的用户体验.

为什么要使用动态应用图标?

试想一下, 你的应用的图标会随着节日的到来而改变, 会根据用户偏好的主题进行调整, 或者会显示出新的功能. 这不仅仅是为了美观, 而是为了在应用和用户之间创建动态互动. 让我们深入了解如何实现这一点.

实现图标更改逻辑

在跳转到代码之前, 让我们先澄清一些事情, 以确保每个人都站在同一起跑线上. 在 Android 的世界里, 图标代表着应用的大门. 它是用户与之交互的第一件事, 因此让它充满活力可以显著提升用户体验.

在本节中, 我们将通过实现动态更改应用图标的逻辑, 深入了解 Android 中动态应用图标的核心. 我们将逐步介绍代码, 解释每个部分的作用.

在 AndroidManifest.xml 中设置

我们需要在 AndroidManifest.xml 文件中处理一个重要步骤. 如果你想让你的应用通过动态图标变化大显身手, 你必须为你的主Activity设置一个activity-alias.

了解activity-alias

在 Android 中, 你可以为清单文件中的主Activity创建activity-alias, 以启用动态应用图标更改. 这些别名允许你将不同的图标与同一个主Activity关联起来, 你可以通过编程启用或禁用这些别名来动态更改应用的图标.

可以将其视为主应用图标的诱饵或替身. 它允许你的应用在同一名称下的不同图标之间切换.

以下是如何在 AndroidManifest.xml 文件中定义activity-alias的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码<application
android:icon="icon"
android:roundIcon="icon">
<activity
// ...
</activity>

<activity-alias
android:name=".MainActivityAlias"
android:icon="icon_2"
android:roundIcon="icon_2"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>

在此示例中, .MainActivityAlias 是别名的名称, .MainActivity 是别名指向的目标Activity. <intent-filter> 就像是在告诉你的应用: “嘿, 这个别名也是启动应用的一种方式!”

请记住, 将代码中的 MainActivityAlias 替换为你在 AndroidManifest.xml 文件中为activity-alias命名的实际名称. 这样可以确保在动态图标更改时, 代码和配置保持一致.

现在, 要在 Android 中创建动态应用图标, 请按照以下步骤操作:

1. 创建广播接收器

我们先创建一个 BroadcastReceiver, 它将监听特定的广播事件, 并负责图标切换. 让我们创建一个新的 IconChangeReceiver 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码class IconChangeReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "com.example.app.ACTION_CHANGE_ICON") {
changeAppIcon(context)
}
}

private fun changeAppIcon(context: Context?) {
context?.let {
// This is where the magic happens. We'll get to this in a bit!
}
}
}

在这段代码中, IconChangeReceiver 将充当勤勉的监听器, 同时热切地等待信号com.example.app.ACTION_CHANGE_ICON. 当它听到这个信号时, 就会调用 changeAppIcon - 这是你切换图标的提示.

2. 注册广播接收器

在 AndroidManifest.xml 文件中, 确保使用意图过滤器注册 IconChangeReceiver, 以监听自定义操作:

1
2
3
4
5
6
7
8
ini复制代码<receiver
android:name=".IconChangeReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.app.ACTION_CHANGE_ICON" />
</intent-filter>
</receiver>

3. 实现图标更改逻辑

在IconChangeReceiver的onReceive方法中, 实现动态更改应用图标的逻辑. 你可以使用条件语句, 根据特定条件选择不同的图标. 下面是一个简化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
kotlin复制代码class IconChangeReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "com.example.app.ACTION_CHANGE_ICON") {
changeAppIcon(context)
}
}

private fun changeAppIcon(context: Context?) {
context?.let { ctx ->
// Example: Conditionally select a different alias based on some criteria
val aliasToEnable = when (someCondition) {
true -> ctx.getString(R.string.alias_1)
false -> ctx.getString(R.string.alias_2)
}

val aliasToDisable = when (aliasToEnable) {
ctx.getString(R.string.alias_1) -> ctx.getString(R.string.alias_2)
else -> ctx.getString(R.string.alias_1)
}

// Change the app icon by enabling one alias and disabling the other
val packageManager = ctx.packageManager
enableComponent(ctx, packageManager, aliasToEnable)
disableComponent(ctx, packageManager, aliasToDisable)
}
}

private fun enableComponent(
context: Context,
packageManager: PackageManager,
componentNameString: String
) {
val componentName = ComponentName(context, componentNameString)

packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}

private fun disableComponent(
context: Context,
packageManager: PackageManager,
componentNameString: String
) {
val componentName = ComponentName(context, componentNameString)

packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
}

在这里, 我们首先要检查上下文(应用的当前状态)是否为空. 我们需要上下文, 因为它能让我们访问特定于应用的资源和类.

然后, 我们根据特定条件决定显示哪个图标. 这就是你可以发挥创意的地方. 例如, 你可以根据一天中的时间, 节假日或应用中的特殊事件来更改图标.

最后, 有了选定的图标资源, 我们就可以开始关键的一步: 更改应用图标. 我们使用 PackageManager 启用代表新图标的activity-alias, 并禁用当前的activity-alias. 这就好比告诉 Android: “嘿, 别用这个图标了, 改用这个吧”.

我们通过在主Activity及其别名上调用 setComponentEnabledSetting 来实现这一点. 使用新图标启用别名并禁用当前图标, 可以有效地切换图标.

4. 触发图标更改

要真正触发图标更改, 我们要发送一个广播, 其中包含我们的 IconChangeReceiver 正在监听的特定操作. 具体方法是创建一个包含动作 com.example.app.ACTION_CHANGE_ICON 的 Intent 并在上下文中调用 sendBroadcast. 通常是这样做的:

如果你是在 Activity 或其他持有 Context 的组件中调用它, 你可以使用:

1
2
scss复制代码val iconChangeIntent = Intent("com.example.app.ACTION_CHANGE_ICON")
sendBroadcast(iconChangeIntent)

而如果你在应用的另一部分, 并拥有对 Context 的引用, 你可以使用:

1
2
ini复制代码val iconChangeIntent = Intent("com.example.app.ACTION_CHANGE_ICON")
context.sendBroadcast(iconChangeIntent)

请记住, 我们的 IconChangeReceiver 将接收广播, 然后根据我们设置的逻辑更改应用的图标.

总结一下

在不断发展的 Android 应用开发世界中, 如何让你的应用脱颖而出, 关键在于如何吸引用户并加入一些个人特色. 动态应用图标可以让你获得身临其境的全新用户体验. 它们能让你的应用图标随时演变, 无需更新或重新安装. 你可以尽情发挥: 庆祝特殊时刻, 切换主题或展示新功能, 一切尽在动态变化之中!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter - 使用Pigeon实现视频缓存插件 🐌

发表于 2024-04-27

欢迎关注微信公众号:FSA全栈行动 👋

BiliBili: www.bilibili.com/video/BV1mt…

一、概述

Pigeon 是一个可以帮助我们生成 Flutter 与 原生 的通信代码的工具,我们只需要关注其两侧主要的数据处理逻辑即可,从而提升效率。

Flutter 端对于视频缓存功能主要还是依赖原生端比较成熟的实现方案,如下两个开源库

  • iOS: github.com/ChangbaDevs…
  • 安卓: github.com/danikula/An…

其功能是:丢给它一个视频链接,它将生成一个具备缓存功能的播放代理链接。

接下来我们一起看看,如何使用 Pigeon 并结合上述两个库来实现视频缓存插件。

二、创建 Plugin

使用如下命令生成插件项目,这里我指定iOS使用的是 Swift,安卓使用的是 Kotlin

1
2
3
4
shell复制代码flutter create --template=plugin --platforms=android,ios -i swift -a kotlin 项目名

# 如:
# flutter create --template=plugin --platforms=android,ios -i swift -a kotlin video_cache

三、原生依赖

iOS

打开在 ios 目录下的 podspec 文件(这里是 video_cache.podspec),添加相关的第三方库依赖,比如我这里依赖的是 KTVHTTPCache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
diff复制代码#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint video_cache.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'video_cache'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '11.0'

# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'

+ # KTVHTTPCache
+ s.dependency 'KTVHTTPCache', '~> 3.0.0'
end

安卓

打开在 android 目录下的 build.gradle 文件,添加

1
2
3
4
5
6
7
8
9
10
11
12
diff复制代码...
android {
...

dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.0.0'
+ implementation 'com.danikula:videocache:2.7.1'
}

...
}

然后在 example/android 目录下的 build.gradle 和 settings.gradle 文件添加如下 maven,否则会找不到依赖库

1
2
3
4
5
6
7
8
9
10
11
diff复制代码// build.gradle

allprojects {
repositories {
+ maven { url "https://jitpack.io" }
+ maven { url 'https://maven.aliyun.com/repository/public' }
google()
mavenCentral()
}
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
diff复制代码// settings.gradle

pluginManagement {
...
repositories {
+ maven { url "https://jitpack.io" }
+ maven { url 'https://maven.aliyun.com/repository/public' }
google()
mavenCentral()
gradlePluginPortal()
}
}
...

四、Pigeon

添加依赖

在 pubspec.yaml 的 dev_dependencies 下添加 pigeon 依赖

1
2
yaml复制代码dev_dependencies:
pigeon: ^17.3.0

定义通信接口

在 lib 目录外创建一个用来定义通信接口的 dart 文件。

这里我们新建了一个与 lib 目录同级的 pigeons 文件夹,来存放与 Pigeon 相关的文件

1
2
3
4
5
6
7
8
shell复制代码.
├── lib
│   ├── ...
│   └── ...
├── pigeons
│   ├── cache.dart
│   └── ...
├── ...

cache.dart 就是我用来定义视频缓存功能相关的通信接口的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dart复制代码import 'package:pigeon/pigeon.dart';

// https://github.com/flutter/packages/blob/main/packages/pigeon/example/README.md
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/plugin/pigeon.g.dart',
kotlinOut:
'android/src/main/kotlin/com/lxf/video_cache/VideoCacheGeneratedApis.g.kt',
kotlinOptions: KotlinOptions(
// https://github.com/fluttercommunity/wakelock_plus/issues/18
errorClassName: "LXFVideoCacheFlutterError",
),
swiftOut: 'ios/Classes/LXFVideoCacheGeneratedApis.g.swift',
))
@HostApi()
abstract class LXFVideoCacheHostApi {
/// 转换为缓存代理URL
String convertToCacheProxyUrl(String url);
}

生成交互代码

再执行如下命令,指定根据 cache.dart 来生成相应的繁杂且重要的交互代码。

1
shell复制代码flutter pub run pigeon --input pigeons/cache.dart

坑点

一定一定,一定要自定义 kotlinOptions 里的 errorClassName,不然它会给你生成默认的 FlutterError,单单自己的插件编译可能不会怎样,但是一旦集成的项目里也有用到其它用 Pigeon 生成了 FlutterError 的插件时,就会报如下错误了

1
shell复制代码Type FlutterError is defined multiple times

自定义 kotlinOptions 里的 errorClassName:

1
2
3
4
5
6
7
8
dart复制代码@ConfigurePigeon(PigeonOptions(
...
kotlinOptions: KotlinOptions(
// https://github.com/fluttercommunity/wakelock_plus/issues/18
errorClassName: "LXFVideoCacheFlutterError"
),
...
))

五、编写原生代码

iOS

进入到 example/ios 目录下,安装依赖

1
2
shell复制代码cd example/ios 
pod install --repo-update

使用 Xcode 打开 Runner.xcworkspace 开始编写原生代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
swift复制代码// VideoCachePlugin.swift

import Flutter
import UIKit
import KTVHTTPCache

// 创建插件时自动生成的类
public class VideoCachePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
// 注册实现
LXFVideoCacheHostApiSetup.setUp(
binaryMessenger: registrar.messenger(),
api: LXFVideoCacheHostApiImplementation()
)
}
}

class LXFVideoCacheHostApiImplementation: LXFVideoCacheHostApi {
/// 是否可以代理
private var canProxy: Bool?

func convertToCacheProxyUrl(url: String) throws -> String {
// 还未试过开启代理服务
if (self.canProxy == nil) {
self.canProxy = ((try? KTVHTTPCache.proxyStart()) != nil)
}
// 无法代理
if !self.canProxy! { return url }
// 无法转 URL 对象
guard let urlObj = URL(string: url) else { return url }

guard let proxyUrlObj = KTVHTTPCache.proxyURL(withOriginalURL: urlObj) else {
// 代理失败
return url
}
// 代理成功
return proxyUrlObj.absoluteString
}
}

安卓

使用 AndroidStudio 打开 example/android,找到外层的 android 项目开始编写原生代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
kotlin复制代码package com.lxf.video_cache

import LXFVideoCacheHostApi
import com.danikula.videocache.HttpProxyCacheServer

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

/** VideoCachePlugin */
class VideoCachePlugin : FlutterPlugin, MethodCallHandler {

private lateinit var videoCacheHostApiImplementation: LXFVideoCacheHostApiImplementation

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
videoCacheHostApiImplementation = LXFVideoCacheHostApiImplementation(flutterPluginBinding)
// 初始化插件
LXFVideoCacheHostApi.setUp(
flutterPluginBinding.binaryMessenger,
videoCacheHostApiImplementation,
)
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
// 关闭服务
videoCacheHostApiImplementation.shutdown()
}

override fun onMethodCall(call: MethodCall, result: Result) {}
}

class LXFVideoCacheHostApiImplementation(
private val flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
) : LXFVideoCacheHostApi {
/// 懒加载缓存服务
private val cacheServer by lazy { HttpProxyCacheServer.Builder(flutterPluginBinding.applicationContext).build() }

/// 重写并通过 cacheServer 将原 url 转换为具备缓存功能的 url
override fun convertToCacheProxyUrl(url: String): String {
return cacheServer.getProxyUrl(url)
}

/// 关闭服务
fun shutdown() {
cacheServer.shutdown()
}
}

六、开源库

上述视频缓存插件已开源,并发布至 GitHub:github.com/LinXunFeng/…

你可以通过如下步骤集成使用:

在 pubspec.yaml 中添加 video_cache 依赖

1
2
yaml复制代码dependencies:
video_cache: latest_version

使用

1
2
3
4
5
6
7
8
9
dart复制代码// 导入
import 'package:video_cache/video_cache.dart';

// 将原视频链接转为缓存代理链接
String url = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
url = await VideoCache().convertToCacheProxyUrl(url);

// url转换结果
// http://localhost:50050/https%3A%2F%2Fflutter%2Egithub%2Eio%2Fassets%2Dfor%2Dapi%2Ddocs%2Fassets%2Fvideos%2Fbee%2Emp4/KTVHTTPCachePlaceHolder/KTVHTTPCacheLastPathComponent.mp4

然后把转换后的 url 丢给播放器就可以了~

七、结尾

以上就是 Flutter 与原生交互拿到代理 url 的例子,使用的是 @HostApi,而如果你如果在原生端去调用 Flutter 的 api,则使用 @FlutterApi 去标注相关抽象类即可,使用方法是差不多的。

需要注意的是,当你使用 Swift 去写插件,且使用了 @FlutterApi 去生成相应的原生代码后编译,可能会遇到这个错误

1
bash复制代码type 'FlutterError' does not conform to protocol 'Error'

添加如下拓展即可

1
2
swift复制代码// https://github.com/flutter/flutter/issues/136081
extension FlutterError: Error {}

八、资料

  • github.com/flutter/pac…

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 Android,Flutter,Python 等文章, 可能有你想要了解的技能知识点哦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java热更新神器——十秒钟热更新线上代码 分享一个很好用的

发表于 2024-04-27

分享一个很好用的热更新插件ArthasHotSwap

  • github地址:github.com/xxxtai/Arth…

我们在测试环境进行开发调试的时候,会有想要热更新几个文件的需求

例如想要把下面的张三改成李四

image-20240427165838595

如果只是做了一小部分的修改,就去重新发布的,有点得不偿失(因为发布常常得几分钟甚至十几分钟),这时候我们就可以使用ArthasHotSwap这个插件帮我们进行热更新,并且操作特别简单

只需要下载该插件,然后修改代码,进行编译(因为需要class文件)

image-20240427171359813

然后使用插件

image-20240427171524170

再到服务器上粘贴就好了!😁

这里需要注意的是,该插件是将修改后的字节码文件上传到了oss中,然后目标服务器再下载下来进行热更新的,如果需要自定义上传的对象存储的话可以去作者github issue中找到方法

image-20240427171651771

我们可以看到修改已经成功了!

image-20240427183948357

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

远程组件 -- 引领未来组件开发新浪潮

发表于 2024-04-27

一、背景

由于公司有一个业务需求:是要小程序中动态加载卡片内容。由于卡片内容经常变化,并且样式布局也可能改动较大。还有一种情况是卡片内容也可能由服务商提供,代码内容无法把控。出于此背景,我们调研了可能的解决方案,感觉都不能满足现状要求:

  • 方案一: iframe 方案:由于代码运行在小程序端,iframe 方案无法满足性能要求,并且如果卡片比较多,附加众多 iframe 也不是那么一回事,
  • 方案二: 微前端方案:由于卡片内容比较小,一般第三方也不会为此特意部署一台服务器,而且要考虑加载资源跨域问题。
  • 方案三:提供源码方案:由于卡片内容经常变化,每次随着他们变动而变更也不是那回事。
  • 终极方案,加载远程组件。服务商只要将自己的代码打包成 umd 格式,然后存放在远程可访问的地址上就行,比如 S3、OSS 等。当他们需求变动时,只需求重新打包上传就 OK 了,基座也不用随着他们一起发版变更。

二、优势

  • 资源高效
  • 灵活便捷
  • 一次部署,N 次受益

三、使用场景

  • 快速演示组件特性
  • 卡片组件,自由定义
  • 协同开发,自由组合
  • 嵌入开发,业务附属
  • 广告投放,引流

四、如何使用

0. 前提

  • 远程组件必须使用 umd 格式
  • 远程地址必须可以跨域访问

1. 查看 DEMO

1.1 react-demo,加载 Antd5 按钮组件示例,在线预览。

  • 效果图:
  • image
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
tsx复制代码import React from "react";
import ReactDOM from "react-dom";
import dayjs from "dayjs";
import LoadRemoteComponent from "./components/LoadRemoteComponent";

<LoadRemoteComponent
urls={["https://cdnjs.cloudflare.com/ajax/libs/antd/5.16.2/antd.min.js"]}
name="antd.Button"
options={{
props: {
type: "primary",
loading: true,
},
externals: {
react: {
import: React,
export: "React",
},
"react-dom": {
import: ReactDOM,
export: "ReactDOM",
},
dayjs: {
import: dayjs,
export: "dayjs",
},
},
}}
>
按钮文字
</LoadRemoteComponent>;

1.2 vue-demo,加载 element-plus 按钮组件示例,在线预览。

  • 效果图:
  • image
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
vue复制代码<template>
<LoadRemoteComponent
:urls="urls"
name="ElementPlus.ElButton"
:options="options"
>
按钮文字
</LoadRemoteComponent>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import LoadRemoteComponent from "./components/LoadRemoteComponent/Index.vue";

const urls = ref([
"https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.7.0/index.full.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.7.0/index.min.css",
]);

const options = ref({
props: {
type: "primary",
loading: true,
},
externals: {
vue: {
import: "",
export: "Vue",
},
},
});

onMounted(() => {
import("vue").then((vue) => {
options.value.externals["vue"].import = vue;
});
});
</script>

2. 属性解释

  • urls: 加载的远程资源地址数组;
  • name: 导出的组件名称;
  • options
+ props: 导出的远程组件的属性;
+ externals: 远程组件的依赖库;

五、源码

点击访问 GitHub:Import-Remote-Component,👏🏻👏🏻👏🏻 欢迎 Star,欢迎批评指正。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Vue中的钩子函数 前言 生命周期钩子 onUnMounte

发表于 2024-04-27

前言

今天我们来聊聊Vue中的钩子函数, 例如生命周期的钩子函数,常见的 Vue.js 生命周期钩子包括 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy 和 destroyed。

再是路由的钩子函数以及keep-alive

生命周期钩子

所谓生命周期就是让函数在页面加载的过程中去自动执行,而这些生命周期钩子函数都是去接受一个回调函数。

那么我们就可以将函数放在这些生命周期钩子函数中,去控制它们什么时候执行。

首先,我们在template中写入一段代码时,那么Vue就会先去编译这段template,编译完之后经过一系列操作然后进行一个挂载mount,然后生命周期就会去执行。

onBeforeMount

该钩子函数在组件挂载之前被调用,注意: 这个钩子在服务器端渲染期间不会被调用。

onMounted

该钩子函数在组件挂载完成后被调用,同样,这个钩子在服务器端渲染期间不会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码<script setup>
import { onMounted, onBeforeMount } from 'vue';

console.log('hello')

onMounted(() => {
console.log('onMounted')
})

onBeforeMount(() => {
console.log('onBeforeMount')
})
</script>

这段代码的打印顺序是怎么样的呢?

image.png

首先,先执行的是全局的console.log,这个执行是在编译之后执行的,全局函数最优先执行,所以打印hello。

当模板template完之后,进行挂载,那么再挂载之前,执行onBeforeMount,所以打印onBeforeMount。

再挂载时,执行onMounted,所以,执行onMounted。

如果我们想要获得拿到一个DOM结构,那么在哪个生命周期可以拿到呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
js复制代码<template>
<div ref="el">
<p>hello</p>
</div>
</template>

<script setup>
import { onMounted, onBeforeMount, ref } from 'vue';

const el = ref(null);

console.log('hello')
console.log(el.value);

onMounted(() => {
console.log('onMounted')
console.log(el.value);
})

onBeforeMount(() => {
console.log('onBeforeMount')
console.log(el.value);
})
</script>


<style lang="scss" scoped></style>

这里我们通过ref去获得一个标签的DOM结构,并且在全局和两个生命周期中都去获取DOM结构el。

image.png

我们发现,只能在挂载完成onMounted中去获取一个元素的DOM结构。

只有挂载了之后,我们才能去获取元素的DOM结构,而在全局中和在组件挂载之前我们是并不能去获取DOM结构的。

再拓展一下:如果我们向后端发送一个网络请求,那么这段请求代码放在哪里执行比较合适呢?

其实以上三个地方放哪里都可以,但是我们一般会放在onMounted当中,因为请求到的数据可能会对DOM结构进行操作。

onUnMounted

该钩子函数在页面卸载时的时候执行,也就是离开页面的时候。

可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

这个钩子在服务器端渲染期间不会被调用。

onBeforeUnmount

该钩子函数在页面卸载之前执行。

当这个钩子被调用时,组件实例依然还保有全部的功能。

这个钩子在服务器端渲染期间不会被调用。

onUpdated()

该钩子函数在组件因为响应式状态变更而更新其 DOM 树之后调用。

这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的,因为多个状态变更可以在同一个渲染周期中批量执行 (考虑到性能因素)。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码<template>
<div>
<p @click="change">{{ count }}</p>
</div>
</template>

<script setup>
import { ref, onUpdated } from 'vue';
const count = ref(0);

onUpdated(() => {
console.log('onUpdated');
});


const change = () => {
count.value++;
}

</script>


<style lang="scss" scoped></style>

通过ref将count变成响应式数据,点击p标签时,count值+1。

onUpdated在响应式状态变更时执行,所以,我们每次将count值增加,都会打印一遍onUpdated。

image.png

onBeforeUpdated

该钩子函数在组件即将因为响应式状态变更而更新其 DOM 树之前调用。

这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

这个钩子在服务器端渲染期间不会被调用。

用法跟onUpdated一样。

路由的钩子函数

路由的钩子函数一般写入路由配置文件router/index.js中,我们也可以写入main.js中进行配置。

全局守卫

全局前置守卫 router.beforeEach

该钩子函数接受三个参数to, from, next。

to表示我们想要跳转到的页面

from表示我们从哪个页面跳转过来

next表示跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
js复制代码import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue'),
meta: {
title: '商城首页'
}
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue'),
meta: {
title: '关于我们'
}
}
]

const router = createRouter({
history: createWebHistory(),
routes
})

// 全局的前置钩子
router.beforeEach((to, from, next) => {
console.log(to, from);
next()
})

这里我们先去创建一份路由, 有两个页面, 一个是/, 一个是/about。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码<template>
<div>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view />
</div>
</template>

<script setup>

</script>

<style lang="scss" scoped>

</style>

image.png

这就是我们的页面,当我们点击关于跳到/about页面时:

image.png

第一行打印to,也就是我们想要去到的地方/about

第二行打印from,也就是我们来的地方/。

当用户想要去其他页面时,我们可以使用这个钩子函数去判断用户有没有登录,如果没有登录就送他去登录页面。

并且我们还可以使用此钩子去给页面设置一个标题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码router.beforeEach((to, from, next) => {
// console.log(to, from);
document.title = to.meta.title
if (to.path !== '/') {
const isLogin = localStorage.getItem('isLogin')
if (isLogin) {
next()
} else {
// router.push('/')
alert('请先登录')
return
}
}
next()
})

这里我们通过一个简单的办法来模拟一下:

如果用户登录了,我们就在本地存储localStorage当中存入用户信息。

先使用to.path去判断用户是不是想去非登录页面,如果是的话,就去检查localStorage当中是否有用户信息。如果没有的话,就不使用next()进行一个跳转,并弹出一个警告。

image.png
如图,注意看url地址栏,我们这里点击关于页面,那么它本该会跳去/about

但是通过路由守卫判断出我们并没有登录,所以弹出警告,跳转失败。

我们看左上角,发现多了一个标题关于我们,这是因为我们在路由配置中加入了meta,去设置一个路由的详细信息。

然后通过document.title = to.meta.title,这样就实现了一个标题。

但是这样去判断一个用户是否登录是不严谨的,用户可以手动的去localStorage中设置一个isLogin为true的字段。

全局解析守卫 router.beforeResolve

解析钩子和全局前置区别不大,解析钩子是在路由被解析,代码被编译之前触发。

我们同样可以使用该钩子去处理上面的事情。

全局后置钩子 router.afterEach

它只有两个参数, to和from

1
2
3
js复制代码router.afterEach((to, from) => {
console.log(to, from)
})

image.png

该方法表示路由跳转后去干什么事情,它没有next参数

同样to和from表示去哪和从哪来。

上面我们所介绍的三个全局钩子函数, 它们是全局的,我们只要用了这三个钩子中的任意一个,那么它将会影响项目中所有路由的跳转。

独享守卫

接下来我们来看看独享守卫。

独享守卫是什么意思?

在有些场景,我们希望跳去某些页面时,钩子触发,而跳去别的页面时,钩子不触发。

beforeEnter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
js复制代码import { createRouter, createWebHistory } from 'vue-router'

const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue'),
meta: {
title: '商城首页'
}
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue'),
meta: {
title: '关于我们'
},
beforeEnter: (to, from, next) => { // 单独的路由守卫
console.log(to, from);
next()
}
}
]

const router = createRouter({
history: createWebHistory(),
routes
})

比如,我们只想跳到关于页面去触发钩子函数,那么我们只需要在/about的路由配置中加入beforeEnter。

同样,to, from, next的作用都是与全局守卫钩子相同。

从/跳去/about:

image.png

从about跳去/:

image.png

可以看出,是没有触发钩子函数的。

我们还可以想象一个场景:

假设一个项目有一百个页面,只有一个页面才需要登录后才能查看,那么我们只需要给那个页面单独配置一个独享守卫钩子, 去进行一个判断是否登录的逻辑。

组件内的守卫

onBeforeRouteLeave

它可以用来在我们离开时做的一个操作。

比如我们常见的: 当我们跳转一个页面时,浏览器弹出一个: 你确定要离开该页面吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码<template>
<div>
About
</div>
</template>

<script setup>
import { onBeforeRouteLeave } from 'vue-router';

onBeforeRouteLeave((to, from, next) => {
console.log(to, from);
const flag = window.confirm('你确定要离开这个页面吗?')
if (flag) {
next()
}
});
</script>

<style lang="scss" scoped>

</style>

我们将该钩子写在about组件中,当我们跳转到/时,该钩子触发:

image.png

onBeforeUpadated

当我们两个页面共同用到了一个组件时,当它们进行相互跳转时,那么该钩子函数就会触发。

keep-alive

在我们去开发一个Vue项目中的时候,若是两个页面共用到了一个组件,当跳转页面时,这个共用的组件也是会进行再次渲染的。因为跳转路由的时候页面内所有组件都是会重新销毁并加载的。

但是很多组件是没有必要再次渲染的,这样可以减少性能开销。

比如每个页面都有广告,且广告的内容是一样的,如果每次去重新加载该广告组件也是没有必要的。我们为了减少一个性能开销,就可以使用`keep-alive’

keep-alive 的作用就是包裹一个需要去缓存的组件。

首先我们有两个页面,一个首页页面/,一个关于页面/about,首页中我们加入onMounted去检测是否重新加载了这个组件。

我们在App.vue中使用keep-alive去缓存首页页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码<template>
<div>
<nav>
<router-link to="/">首页 </router-link>
<router-link to="/about">关于</router-link>
</nav>

<router-view v-slot="{ Component }">
<KeepAlive :include="['Home']">
<component :is="Component" />
</KeepAlive>
</router-view>
</div>
</template>

<script setup>

</script>

<style lang="scss" scoped></style>

首先,我们进入到首页页面:

image.png

可以看到是执行了onMounted,因为组件是初次加载,加载完之后才会缓存。

然后我们再跳转到关于页面。

然后我们从/about页面跳到首页/时,再看打印:

image.png

发现我们成功缓存住了首页组件,onMounted没有触发。

Vue中与keep-alive搭配使用的生命周期钩子

onActivated

当被缓存的组件生效时触发

onDeactivated

当离开缓存的组件时触发

这两个钩子一定根缓存有关, 所以一定要跟keep-alive一起使用

同样,我们利用上面的代码来举例:

我们在首页Home组件中加入这两个钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码<template>
<div>
<p>home page</p>
</div>
</template>

<script setup>
import { onActivated, onDeactivated, onMounted } from 'vue';

onMounted(() => {
console.log('首页页面的onMounted');
})

onActivated(() => { // 当被缓存的组件生效时触发
console.log('首页页面的onActivated');
})

onDeactivated(() => { // 当被缓存的组件离开时触发
console.log('首页页面的onDeactivated');
})

</script>#

<style lang="scss" scoped>

</style>

当我们进入首页页面, onActivated触发,因为被缓存的Home组件生效了

image.png

当我们进入关于页面, onDeactivated触发,因为我们离开了缓存的组件Home

image.png

最后

若是在面试的时候,面试官有提到生命周期,那么提到两个跟缓存有关的钩子是十分加分的,因为往往我们在学习前端的过程中容易去忽视掉这两个钩子函数。

写文章不易,如果帮助到了小伙伴们,可以给本文点赞收藏评论三连呀。有不懂的地方欢迎到评论区留言,我会及时回复。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android原生WebView与JSP交互

发表于 2024-04-27

前言

我们也经常做平板和大屏项目的时候常常会遇到要开发网站壳子,一般网站前端用的Nodejs之类的框架来交互的,但是与JSP交互的少之又少,他们一般不会前端开发更加重视后端内容,导致一些交互他们无法实现,这时候我就来分享了我对于这块的解决方案了。

概念

WebView 对象允许您将 Web 内容显示为活动布局的一部分,但缺乏成熟浏览器的一些功能。当您需要增强对 UI 和高级配置选项的控制(允许您将网页嵌入到为应用程序专门设计的环境中)时,WebView 非常有用。

关于JS和Android交互的两种形式

这里我着重描述与JS的交互,我们需要写以下代码来实现android方面的交互

android部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
kotlin复制代码class MainActivity : AppCompatActivity(){
private lateinit var webView: WebView
private lateinit var mainBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainBinding = ActivityMainBinding.inflate(layoutInflater)
webView = WebView(this)
val params = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
mainBinding.contentView.addView(webView, params)
val webSettings: WebSettings = webView.settings
webSettings.setJavaScriptEnabled(true)
webView.loadUrl("file:///android_asset/android_test/index.html")
webView.addJavascriptInterface(JsInterface(), "control")
}
//android调用前端部分
fun showToast(value:String){
webView.evaluateJavascript("javascript:showToast('$value');", ValueCallback {
Log.e("toast", it)
})
}
inner class JsInterface {
//前端调用android部分
@JavascriptInterface
fun showToast(str: String) {
Toast.makeText(this@MainActivity, str, Toast.LENGTH_LONG).show()
}
}
}

前端部分

android调用前端:

JS

1
2
3
4
5
6
7
8
xml复制代码<script>    
/**
* android原生调用前端部分
* @param value
*/
function showToast(value){ alert("显示内容:"+value);
}
</script>

前端调用android

H5

1
2
3
4
xml复制代码<body>
<div >
<van-button type="primary" @click="showToastAndroid('今天天气不错')" size="large">{{text||"消息提示"}}</van-button></div>
</body>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码var app=new Vue({   
el: '#app',
methods: {
data() {
return {
text:null,
}
},
/**
* 消息提示
*/
showToastAndroid(value){
javascript:control.showToast(value);
},
}});

问题

android原生与JS交互以上方法均可以正常运行,但是与JSP交互的话,我实际发现的是android方面引用前端方法的话无法引用到,这时候应该怎么处理的呢?

解决方案

我们可以采用android的webview直接loadURL刷新页面,通过拼接字符串的方式来解决以上问题:

1
2
3
4
5
6
7
8
9
dart复制代码var url="file:///android_asset/android_test/index.html"
var thisUrl="file:///android_asset/android_test/index.html?data1='萨摩耶'&data2='哈士奇'"
if(this.thisUrl.contains("?")){
this.thisData ="&data1=${this.data1}&data2=$data2"
}else{
this.thisData ="?data1=${this.tata1}&data2=$data2"
}
url="${this.thisUrl}${this.thisData}"
webView.loadUrl(url)

读取url完成以后,页面刷新加载数据,最终JSP前端得到了android主动调用的数据。

补充一下

android原生和前端交互的有几处需要对应

1
less复制代码webView.addJavascriptInterface(JsInterface(), "control")

这里的”control“需要跟前端约定好,所以前端调用的时候 也必须在这里用control

1
css复制代码javascript:control.showToast(value);

关于方法方面android部分@JavascriptInterface 下面的方法也必须与前端部分的要调用的方法相一致

1
2
3
4
less复制代码@JavascriptInterface
fun showToast(str: String) {
Toast.makeText(this@MainActivity, str, Toast.LENGTH_LONG).show()
}

和

1
css复制代码javascript:control.showToast(value);

以上代码里面的showToast必须一致

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何用Android Studio 开发原生插件

发表于 2024-04-27

说明

公司使用uniapp开发的app,后期可能要对接银行的一些服务,可能会用到U盾之类的,这种没做过也不太懂,其次就是对接大华摄像头视频播放也要用到SDK之类的,设备初始化之类的。就先研究下怎么生成可供uniapp调用的原生插件了。

环境准备

  1. 下载uniapp官方Android 离线SDK下载完成解压,后面会用到。

image.png
2. 安装java环境,这个很简单,下载JDK,我装的是jdk11,安装完成配置下环境变量即可。
3. Android Studio 下载安装

开发demo插件

  1. 打开AndroidStudio,在菜单栏选择File>New>New Project,新建自定义项目,

image.png
2. 创建名称为MyDemoPlugin的测试项目,按照下面的配置就行。

image.png
新建完成后,项目如下图:

image.png
3. 接下来我们就要创建名为testPlugin插件模块,依次点击File>New>New Module。

image.png
4.创建完成,我们的项目里就多了个testPlugin的文件夹:

image.png
5.下面就要把上面第一步下载的SDK中的UniPlugin-Hello-AS下的app>libs下的文件复制到自己项目的app>libs

image.png
6. 然后就是修改我们的testPlugin中的build.gradle文件里的配置信息,选中文件,双击打开,将dependencies下默认生成的依赖注释掉,添加uni-app所需库依赖。最后一项是引入我们上面复制过来的libs文件目录。

1
2
3
4
5
php复制代码compileOnly 'androidx.recyclerview:recyclerview:1.0.0'
compileOnly 'androidx.legacy:legacy-support-v4:1.0.0'
compileOnly 'androidx.appcompat:appcompat:1.0.0'
compileOnly 'com.alibaba:fastjson:1.2.83'
compileOnly fileTree(include: ['uniapp-v8-release.aar'], dir: '../app/libs')

image.png
7.配置完保存,点击文件上方的立刻同步:

image.png
8. 上面基本配置完了,剩下的就是写个测试代码,在我们所创建的testPlugin下面,下图所示位置右键创建个TestModule类:

image.png

  • 注意:Module 扩展必须继承 UniModule 类
  • 扩展方法必须加上@UniJSMethod (uiThread = false or true) 注解。UniApp会根据注解来判断当前方法是否要运行在UI 线程,和当前方法是否是扩展方法。
    示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码package com.test.testplugin;
import com.alibaba.fastjson.JSONObject;
import io.dcloud.feature.uniapp.annotation.UniJSMethod;
import io.dcloud.feature.uniapp.bridge.UniJSCallback;
import io.dcloud.feature.uniapp.common.UniModule;
public class TestModule extends UniModule {
@UniJSMethod(uiThread = false)
public void open(UniJSCallback callback) {
JSONObject data = new JSONObject();
data.put("hello world:","插件调用成功");
callback.invoke(data);
}
}
  1. 注册插件,在app>src>main目录下创建assets文件夹,然后将下载的SDK中assets中的dcloud_uniplugins.json文件复制过来。修改下里面的内容如下:
    修改前:

image.png

  • dcloud_uniplugins.json说明
  • nativePlugins: 插件跟节点 可存放多个插件
  • hooksClass: 生命周期代理(实现AppHookProxy接口类)格式(完整包名加类名)
  • plugins: 插件数组
  • name : 注册名称
  • class : module 或 component 实体类完整名称
  • type : module 或 component类型。

根据自身项目修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码{
"nativePlugins":[
{
"hooksClass":"",
"plugins":[
{
"type":"module",
"name":"Test-Plugin",
"class":"com.test.testplugin.TestModule"
}
]
}
]
}

image.png
10. 至此,就可以打包插件了。菜单build->make moudule,下图所示:

image.png
打包需要等一会,完成后会生成如下文件,arr目录中的就是打包后的插件:

image.png

这样java开发的demo插件就算完成了,剩下的就是uniapp调用的问题了。算是完成了第一步吧,过程有点繁琐。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SwiftUI-日期显示总结

发表于 2024-04-27

当 SwiftUI 需要显示日期时,可以有多种选择,下面总结一些常见的使用方式。

选择显示

比较常见的方式是通过日期选择器选择某个日期后显示。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
swift复制代码import SwiftUI

struct ContentView: View {
@State private var birthDay = Date()

var body: some View {
DatePicker(selection: $birthDay, displayedComponents: [.hourAndMinute,.date]) {
Text("出生日期")
}
.environment(\.locale, Locale(identifier: "zh_Hans_CN"))
.padding()
}
}

效果如下:

日期选择器.png

选择显示到Text

借助于DateFormatter,首先格式化成需要的日期格式,然后显示。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
swift复制代码import SwiftUI

struct ContentView: View {
@State private var selectedDate = Date()
var formatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss"
return dateFormatter
}

var body: some View {
VStack {
DatePicker(
selection: $selectedDate,
displayedComponents: [.hourAndMinute, .date],
label: { Text("选择日期") }
)
.padding()

Text(formatter.string(from: selectedDate))
}
}
}

效果如下:

DateFormatter方式.png

不选择显示到Text

SwiftUI 1.0

SwiftUI 1.0 时 Text 就可以显示日期字符串,而且可以同时使用DateFormatter。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vbnet复制代码import SwiftUI

struct ContentView: View {
let date = Date()

var formatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss"
return dateFormatter
}

var body: some View {
VStack {
Text("\(date)")

Text(formatter.string(from: date))
}
}
}

效果如下:

SwiftUI 1.0

SwiftUI 2.0

SwiftUI 2.0 之后,Text 可以直接显示日期,而且支持多种不同的形式。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
swift复制代码import SwiftUI

struct ContentView: View {
var date = Date()

var body: some View {
VStack {
Text(date, style: .date)

Text(date, style: .time)

Text(date, style: .relative)

Text(date, style: .offset)

Text(date.addingTimeInterval(600), style: .timer)

Text(date.getCurrentTime(), style: .timer)
}
}
}

extension Date {
func getCurrentTime() -> Date {
let calendar: Calendar = Calendar.current
let year = calendar.component(.year, from: self)
let month = calendar.component(.month, from: self)
let day = calendar.component(.day, from: self)
let components = DateComponents(year: year, month: month, day: day, hour: 0, minute: 0, second: 0)
return Calendar.current.date(from: components)!
}
}

效果如下:

SwiftUI 2.0.gif

SwiftUI 3.0

WWDC21 推出了获取当前日期与格式化日期的新方法,因此 SwiftUI 3.0 之后显示日期更加方便。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
swift复制代码import SwiftUI

struct ContentView: View {
var date = Date.now

var body: some View {
VStack {
Text(date.formatted(.dateTime.locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.day().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.week().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.weekday().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.weekday(.wide).locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.month().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.month(.wide).locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.year().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.day().weekday().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.day().weekday().month().locale(Locale(identifier: "zh_Hans_CN"))))

Text(date.formatted(.dateTime.day().month().year().locale(Locale(identifier: "zh_Hans_CN"))))
}
}
}

效果如下:

SwiftUI 3.0.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

几分钟带你了解预编译,拿下大厂面试题 前言 正文 小结

发表于 2024-04-27

前言

我们执行代码的过程,在 JavaScript 引擎的眼里可以分为两个重要的步骤,分别是预编译和执行。

预编译阶段会处理一些语法解析、变量声明提升等工作,为后续的代码执行做好准备;在预编辑完成后代码才开始执行。

我会用底层逻辑详细讲解代码执行的过程需要经历的过程。并且解答:

  • 为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域。
  • 变量声明的声明提升和函数声明的整体提升是如何实现的。

正文

我们在开始了解什么是预编译和预编译要经历的过程之前,我们需要先了解函数的自带属性、作用域和作用域连。

函数的自带属性

在JavaScript中,函数有一些自带的属性,一下是一些常见的属性:

  1. length:表示函数的参数个数。
  2. prototype:指向函数的原型对象,原型对象用于定义构造函数的公共属性和方法。
  3. name:函数的名称。
  4. arguments:函数调用时传递的参数数组。

除了这些常见的属性外,函数还存在隐式属性,其中就包括[[scope]]属性。[[scope]]是 JavaScript 中函数的一个隐式属性,其中scope翻译为域或范围。[[scope]]属性仅供 JavaScript 引擎使用,我们无法直接访问。

在函数定义时,系统会通过scope的内部原理定期去调用它,但不会让用户去用。当函数执行时,系统会创建一个执行期上下文的内部对象,此时[[scope]]的值会发生变化。在函数内部访问变量时,实际访问的就是变量的scope(作用域),scope里有作用域链,系统会从作用域链底端依次向下去找变量。

预编译流程

我们用一个例子深入了解一下。

1
2
3
4
5
6
7
8
9
10
javascript复制代码function a() {
function b() {
var b = 55
console.log(a);
}
var a = 200
b()
}
var glob = 50
a()

这段代码会输出什么呢?

让我们通过这段代码一起跟着JavaScript 引擎进入底层世界。

该代码大致流程:首先在全局预编译代码,然后全局执行,然后调用函数a;停止全局执行,开始预编译函数体a,预编译结束后执行函数a,最后调用函数b;停止执行函数a,预编译函数b,预编译完成后执行b;执行完b函数后返回执行a函数,a函数执行完返回全局,然后结束。

  1. 首先JavaScript 引擎对代码进行预编译(发生在全局中):
1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。在这几个预编译的步骤中,只会在寻找变量声明和函数声明,其他语句一律跳过。按顺序依次执行完这些步骤后,我们可以得到一个Global Object。

屏幕截图 2024-04-27 133421.png

  1. 对代码的预编译结束后进行全局执行。
1
2
3
javascript复制代码function a() {}
var glob = 50
a()

在执行到a()时开始调用函数,这时JavaScript 引擎会停止执行代码而去调用a函数并且对a函数进行预编译再执行。
3. 在函数体a中进行预编译(发生在函数体中):

1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
3. 形参和实参相互统一。
4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。在这几个预编译的步骤中,需要按顺序依次执行。

进行a步骤:创建一个函数上下文对象(Activation Object)

屏幕截图 2024-04-27 135402.png

进行b步骤:在函数体里找形参和变量声明

1
2
3
4
5
6
7
8
9
10
javascript复制代码Activation Object={
a:undefined, (形参)
a:undefined (实参)
}//是错误的

因为对象里不能存在相同的键,所以如果会进行重叠覆盖

Activation Object={
a:undefined
}

进行c步骤:形参和实参相互统一。

1
2
3
javascript复制代码Activation Object={
a:undefined
}

进行d步骤:在函数体内找函数声明

1
2
3
4
javascript复制代码Activation Object={
a:undefined,
b:function
}
  1. 执行函数a。
1
2
3
4
5
6
7
8
javascript复制代码function a() {
function b() {
var b = 55
console.log(a);
}
var a = 200
b()
}

当执行到var a = 200时

1
2
3
4
css复制代码Activation Object={
a:200,
b:function
}

屏幕截图 2024-04-27 163958.png

当执行到b()时调用b函数,停止执行函数a,对函数体b进行预编译。

  1. 在函数体b中进行预编译(发生在函数体中):
1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
3. 形参和实参相互统一。
4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。你会发现函数预编译的方法是一样的。

我们会得到函数b的函数上下文对象为

1
2
3
javascript复制代码Activation Object={
b:undefined
}
  1. 执行函数b
1
2
3
javascript复制代码Activation Object={
b:55
}

屏幕截图 2024-04-27 170005.png

代码执行完成。

小结

预编译的具体步骤。

  1. 在全局进行预编译(发生在全局中):
    1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
    2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
    3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。
  2. 在函数体中进行预编译(发生在函数体中):
    1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
    2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
    3. 形参和实参相互统一。
    4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。

解答

变量声明的声明提升和函数声明的整体提升是如何实现的?

根据预编译的流程,JavaScript 引擎找到变量声明和函数声明后会在运行前赋值,分别赋值为undefined和function(函数体),然后再运行。这样就实现了变量声明的声明提升和函数声明的整体提升。

作用域和作用域链

作用域是执行期上下文对象的集合,这种集合呈链式连接,我们把这种链状关系称之为作用域链。

我们通过这个代码进行解释。

1
2
3
4
5
6
7
8
9
10
javascript复制代码function a() {
function b() {
var b = 55
console.log(a);
}
var a = 200
b()
}
var glob = 50
a()

在这个代码中有3个作用域,分别是全局作用域和a.[[scope]]和b.[[scope]]。它们之间的关系是这样的。

屏幕截图 2024-04-27 170806.png

这个关系是怎么形成的呢?

  1. 在全局预编译完成后,函数a被整体提升生成作用域,并且作用域的0号位指向Global Object。

屏幕截图 2024-04-27 172549.png

  1. 在函数a预编译时:函数a的作用域的0号位指向自己的上下文对象,1号位指向Global Object;函数b在函数a的预编译过程中被整体提升,生成作用域,并且作用域的0号位指向a的作用域。

屏幕截图 2024-04-27 173104.png

  1. 在函数b预编译时:函数b的作用域的0号位指向自己的上下文对象;1号位指向函数a的作用域。

屏幕截图 2024-04-27 173444.png

通过这些步骤就可以理解作用域链是什么,怎么形成的。

解答

为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域?

因为在作用域中只能从低位向高位查找,不能从高位找回低位。

屏幕截图 2024-04-27 173104.png

我们通过在这张图进行理解。a函数执行阶段通过作用域的0号位查找需要的有效标识符,如果没有找到便通过作用域的1号位继续查找需要的有效标识符。

小结

我们通过预编译的底层逻辑解答了

  • 为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域。
  • 变量声明的声明提升和函数声明的整体提升是如何实现的。

并且了解了预编译的具体步骤:

  • 在全局进行预编译(发生在全局中):
    1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
    2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
    3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。
  • 在函数体中进行预编译(发生在函数体中):
    1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
    2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
    3. 形参和实参相互统一。
    4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。

最后我们用运行代码结尾吧

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码function test(a, b) {
console.log(a);
c = 0
var c;
a = 3
b = 2
console.log(b);
function b() { }
console.log(b);
}
test(1)

自己动手试试会输出什么。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…567…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%