前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >YARN任务运行中的Token

YARN任务运行中的Token

作者头像
陈猿解码
发布于 2023-02-28 06:59:15
发布于 2023-02-28 06:59:15
87400
代码可运行
举报
文章被收录于专栏:陈猿解码陈猿解码
运行总次数:0
代码可运行

上一篇文章中,主要讲解了token的一些通用知识,以及hadoop中,token的实现和通用数据结构及流程。

本文主要讲述yarn任务提交运行过程中涉及的几个重要token:AMRMToken,NMToken,ContainerToken。

【AMRMToken】


用于保证ApplicationMaster(下面均简称AM)与RM之间的安全通信,即AM向RM注册,以及后续向RM申请资源的rpc请求,都会带上该token。

AMRMToken在客户端向RM提交任务后,由RM创建生成,然后通过rpc请求传递给NM;NM通过将token持久化到本地文件,让AM启动后从对应文件中加载到token,这样AM就可以使用正确的token向RM注册并完成rpc请求交互了。接下来就展开说明下。

1)token的生成

客户端提交任务请求后,RM在内部的处理中,为AM构造对应的container启动上下文时,创建了AMRMToken,相关代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// AMLauncher.java
private void launch() throws IOException, YarnException {
    ...
    // 构造 container 启动上下文
    ContainerLaunchContext launchContext = 
        createAMContainerLaunchContext(applicationContext, masterContainerID);
    ...
}

private ContainerLaunchContext createAMContainerLaunchContext(
    ApplicationSubmissionContext applicationMasterContext,
    ContainerId containerID) throws IOException {
    ...
    setupTokens(container, containerID);
    ...
}

protected void setupTokens(ContainerLaunchContext container, ContainerId, containerID) 
    throws IOException {
    ...
    // 构造 AMRMToken
    Token<AMRMTokenIdentifier> amrmToken = createAndSetAMRMToken();
    if (amrmToken != null) {
        credentials.addToken(amrmToken.getService(), amrmToken);
    }
    ...
}

protected Token<AMRMTokenIdentifier> createAndSetAMRMToken() {
    Token<AMRMTokenIdentifier> amrmToken =
        this.rmContext.getAMRMTokenSecretManager()
            .createAndGetAMRMToken(application.getAppAttemptId());
    ((RMAppAttemptImpl)application).setAMRMToken(amrmToken);
    return amrmToken;
}

2)AMRMToken的传递

a. RM --> NM

在构造完container启动上下文后,将启动上下文随container启动请求(StartContainerRequest)发送给NM。

b. NM --> AM

NM收到请求后,内部构造Container实例对象,并从请求中取出credential保存在实例对象中,在真正需要启动AM时,将token信息写到本地文件中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ContainerLauncher.java
public Integer call() {
    // token存储在 nmPrivate 中的路径
    Path nmPrivateTokensPath = 
        dirsHandler.getLocalPathForWrite(
            getContainerPrivateDir(appIdStr, containerIdStr) + Path.SEPARATOR +
            String.format(TOKEN_FILE_NAME_FMT, containerIdStr));
    // Set th token location too.
    // 为AM设置环境变量
    addToEnvMap(
        environment, nmEnvVars,
        ApplicationConstants.CONTAINER_TOKEN_FILE_ENV_NAME,
        new Path(
            containerWorkDir,
            FINAL_CONTAINER_TOKENS_FILE).toUri().getPath());
    // 将token写入文件中
    try (DataOutputStream tokensOutStream = 
        lfs.create(nmPrivateTokensPath, EnumSet.of(CREATE, OVERWRITE))) {
        Credentials creds = container.getCredentials();
        creds.writeTokenStorageToStream(tokensOutStream);
    }
    ...
}

//DefaultContainerExecutor.java
public int launchContainer(ContainerStartContext ctx) {
    // copy container tokens to work dir
    Path tokenDst = 
        new Path(containerWorkDir, Containerlaunch.FINAL_CONTAINER_TOKENS_FILE);
    copyFile(nmPrivateTokensPath, tokenDst, user);
}

从上面的代码可以看到,实际上先将token写入nmPrivate目录中,以container的ID作为文件名,".tokens"作为文件后缀,然后将token文件拷贝到container的工作目录中,并重命名为container.tokens。

例如,存储在nmPrivate目录下的token:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@dn-nm-0 container_e301_1652243949356_2011_01_000003]# pwd
/home/hncscwc/hadoop/yarn/nodemanager/local/nmPrivate/application_1652243949356_2011/container_e301_1652243949356_2011_01_000003
[root@dn-nm-0 container_e301_1652243949356_2011_01_000003]# ll
total 52
-rw-r--r-- 1 hadoop hadoop 8 May 13 16:27 container_e301_1652243949356_2011_01_000003.pid
-rw-r--r-- 1 hadoop hadoop 387 May 13 16:27 container_e301_1652243949356_2011_01_000003.tokens
-rw-r--r-- 1 hadoop hadoop 43441 May 13 16:27 launch_container.sh

存储在container工作目录下的token:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@dn-nm-0 container_e301_1652243949356_2011_01_000003]# pwd
/home/hncscwc/hadoop/yarn/nodemanager/local/usercache/dcp/appcache/application_1652243949356_2011/container_e301_1652243949356_2011_01_000003
[root@dn-nm-0 container_e301_1652243949356_2011_01_000003]# ll
total 72
lrwxrwxrwx 1 hadoop hadoop 100 May 13 16:27 __app__.jar -> /home/hncscwc/hadoop/yarn/nodemanager/local/usercache/hncscwc/filecache/3906/spark-examples_2.11-2.4.4.jar
-rw-r--r-- 1 hadoop hadoop 387 May 13 16:27 container_tokens
-rwx------ 1 hadoop hadoop 750 May 13 16:27 default_container_executor_session.sh
-rwx------ 1 hadoop hadoop 805 May 13 16:27 default_container_executor.sh
-rwx------ 1 hadoop hadoop 43441 May 13 16:27 launch_container.sh
lrwxrwxrwx 1 hadoop hadoop 91 May 13 16:27 metrics-influxdb.jar -> /home/hncscwc/hadoop/yarn/nodemanager/local/usercache/hncscwc/filecache/3910/metrics-influxdb.jar
lrwxrwxrwx 1 hadoop hadoop 89 May 13 16:27 metrics.properties -> /home/hncscwc/hadoop/yarn/nodemanager/local/usercache/hncscwc/filecache/3909/metrics.properties
lrwxrwxrwx 1 hadoop hadoop 89 May 13 16:27 __spark_conf__ -> /home/hncscwc/hadoop/yarn/nodemanager/local/usercache/hncscwc/filecache/3908/__spark_conf__.zip
lrwxrwxrwx 1 hadoop hadoop 94 May 13 16:27 spark-influxdb-sink.jar -> /home/hncscwc/hadoop/yarn/nodemanager/local/usercache/hncscwc/filecache/3907/spark-influxdb-sink.jar
drwxr-xr-x 2 hadoop hadoop 12288 May 13 16:27 __spark_libs__
drwx--x--- 2 hadoop hadoop 55 May 13 16:27 tmp

3)AM启动后的注册校验

AM进程启动后,构造UGI(UserGroupInformation)对象时,会根据环境变量HADOOP_TOKEN_FILE_LOCATION的值,从指定文件中加载token信息,然后附在rpc请求中向RM进行注册。RM收到请求后由对应的SecretManager(这里对应于AMRMTokenSecretManager)完成认证逻辑。认证的逻辑在上一篇文章有详细介绍。

需要注意的是:CONTAINER_TOKEN_FLIE_ENV_NAME的值与HADOOP_TOKEN_FILE_LOCATION的值是相同的,这样就可以保证正确读取到对应的token。

【NMToken】


NMToken则是用于与NM的安全通信。

从任务提交运行的流程中可以知道,RM和AM都会和NM通信请求启动container,其中RM向NM请求启动AM;而AM则是向NM请求启动任务container。因此,在RM与NM的通信、AM与NM的通信中都会用到NMToken。

1) NM向RM注册获取NMToken的MasterKey

由于NMToken是由RM生成的,但最终在NM中进行校验,因此NM需要和RM使用一样的密钥,这个密钥是在NM向RM注册时获取的,并在心跳请求中更新密钥信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ResourceTrackerService.java
public RegisterNodeManagerResponse registerNodeManager(
    egisterNodeManagerRequest request) throws YarnException, IOException {
    ...
    RegisterNodeManagerResponse response = 
    recordFactory.newRecordInstance(RegisterNodeManagerResponse.class);
    ...
    // 返回 containerToken 和 NMToken 的密钥信息
    response.setContainerTokenMasterKey(
        containerTokenSecretManager.getCurrentKey());
    response.setNMTokenMasterKey(
        nmTokenSecretManager.getCurrentKey());
}

// NodeStatusUpdaterImpl.java
protected void registerWithRM() 
    throws YarnException, IOException {
    ...
    regNMResponse =
        resourceTracker.registerNodeManager(request);
    MasterKey masterKey = 
        regNMResponse.getContainerTokenMasterKey();
    // do this now so that its set before we start heartbeating to RM
    // It is expected that status updater is started by this point and
    // RM gives the shared secret in registration during
    // StatusUpdater#start().
    if (masterKey != null) {
       this.context.getContainerTokenSecretManager()
           .setMasterKey(masterKey);
    }

    masterKey = regNMResponse.getNMTokenMasterKey();
    if (masterKey != null) {
        this.context.getNMTokenSecretManager()
            .setMasterKey(masterKey);
    }
}

2)RM向NM请求启动AM

在请求中会携带NMToken:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// AMLauncher.java 
private void launch() throws IOException, YarnException {
    connect();
    ...
}

private void connect() throws IOException {
    ContainerId masterContainerID = masterContainer.getId();
    
    containerMgrProxy = getContainerMgrProxy(masterContainerID);
}

protected ContainerManagementProtocol getContainerMgrProxy(
    final ContainerId containerId) {
    final NodeId node = masterContainer.getNodeId();
    final InetSocketAddress containerManagerConnectAddress =
        NetUtils.createSocketAddrForHost(node.getHost(), node.getPort());

    final YarnRPC rpc = getYarnRPC();

    UserGroupInformation currentUser =
        UserGroupInformation.createRemoteUser(containerId
            .getApplicationAttemptId().toString());

    String user =
        rmContext.getRMApps()
            .get(containerId.getApplicationAttemptId().getApplicationId())
            .getUser();
    org.apache.hadoop.yarn.api.records.Token token =
        rmContext.getNMTokenSecretManager().createNMToken(
            containerId.getApplicationAttemptId(), node, user);
    currentUser.addToken(ConverterUtils.convertFromYarn(token,
        containerManagerConnectAddress));

    return NMProxy.createNMProxy(conf, ContainerManagementProtocol.class,
        currentUser, rpc, containerManagerConnectAddress);
}

NM在请求处理中校验:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ContainerManagerImpl.java
public StartContainersResponse startContainers(
    StartContainersRequest requests) throws YarnException, IOException {
    UserGroupInformation remoteUgi = getRemoteUgi();
    NMTokenIdentifier nmTokenIdentifier = selectNMTokenIdentifier(remoteUgi);
    authorizeUser(remoteUgi, nmTokenIdentifier);
    ...
}

protected NMTokenIdentifier selectNMTokenIdentifier(
    UserGroupInformation remoteUgi) {
    Set<TokenIdentifier> tokenIdentifiers = remoteUgi.getTokenIdentifiers();
    NMTokenIdentifier resultId = null;
    for (TokenIdentifier id : tokenIdentifiers) {
        if (id instanceof NMTokenIdentifier) {
            resultId = (NMTokenIdentifier) id;
            break;
        }
    }
    return resultId;
}

protected void authorizeUser(UserGroupInformation remoteUgi,
    NMTokenIdentifier nmTokenIdentifier) throws YarnException {
    if (nmTokenIdentifier == null) {
      throw RPCUtil.getRemoteException(INVALID_NMTOKEN_MSG);
    }
    if (!remoteUgi.getUserName().equals(
      nmTokenIdentifier.getApplicationAttemptId().toString())) {
      throw RPCUtil.getRemoteException("Expected applicationAttemptId: "
          + remoteUgi.getUserName() + "Found: "
          + nmTokenIdentifier.getApplicationAttemptId());
    }
}

3)AM启动向RM注册后,从注册的响应中获取NMToken

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// AMRMClientImpl.java
private RegisterApplicationMasterResponse registerApplicationMaster()
      throws YarnException, IOException {
    RegisterApplicationMasterRequest request =
        RegisterApplicationMasterRequest.newInstance(this.appHostName,
            this.appHostPort, this.appTrackingUrl);
    RegisterApplicationMasterResponse response =
        rmClient.registerApplicationMaster(request);
    synchronized (this) {
      lastResponseId = 0;
      if (!response.getNMTokensFromPreviousAttempts().isEmpty()) {
        populateNMTokens(response.getNMTokensFromPreviousAttempts());
      }
    }
    return response;
}

// 将Token放到缓存中
protected void populateNMTokens(List<NMToken> nmTokens) {
    for (NMToken token : nmTokens) {
      String nodeId = token.getNodeId().toString();
      if (LOG.isDebugEnabled()) {
        if (getNMTokenCache().containsToken(nodeId)) {
          LOG.debug("Replacing token for : " + nodeId);
        } else {
          LOG.debug("Received new token for : " + nodeId);
        }
      }
      getNMTokenCache().setToken(nodeId, token.getToken());
    }
}

4)AM向NM请求启动任务container时,将token放到ugi中

从缓存中取出对应NM节点的的token,然后放到ugi中,随请求一并发送给NM。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// NMClientImpl.java
public Map<String, ByteBuffer> startContainer(
    Container container, ContainerLaunchContext containerLaunchContext)
        throws YarnException, IOException {
    ...
    proxy =
      cmProxy.getProxy(container.getNodeId().toString(),
        container.getId());
    // 注意containerToken
    StartContainerRequest scRequest =
      StartContainerRequest.newInstance(containerLaunchContext,
        container.getContainerToken());
    List<StartContainerRequest> list = new ArrayList<StartContainerRequest>();
    list.add(scRequest);
    StartContainersRequest allRequests =
      StartContainersRequest.newInstance(list);
    StartContainersResponse response =
      proxy.getContainerManagementProtocol().startContainers(allRequests);
    ...
}

// ContainerManagementProtocolProxy.java
public synchronized ContainerManagementProtocolProxyData getProxy(
    String containerManagerBindAddr, ContainerId containerId)
    throws InvalidToken {
    ...
    if (proxy == null) {
      proxy =
          new ContainerManagementProtocolProxyData(rpc, containerManagerBindAddr,
              containerId, nmTokenCache.getToken(containerManagerBindAddr));
      if (maxConnectedNMs > 0) {
        addProxyToCache(containerManagerBindAddr, proxy);
      }
    }
    ...
}

public ContainerManagementProtocolProxyData(YarnRPC rpc,
    String containerManagerBindAddr,
    ContainerId containerId, Token token) throws InvalidToken {
    this.containerManagerBindAddr = containerManagerBindAddr;
    this.activeCallers = 0;
    this.scheduledForClose = false;
    this.token = token;
    this.proxy = newProxy(rpc, containerManagerBindAddr, containerId, token);
}

protected ContainerManagementProtocol newProxy(final YarnRPC rpc,
    String containerManagerBindAddr, ContainerId containerId, Token token)
    throws InvalidToken {
    UserGroupInformation user =
      UserGroupInformation.createRemoteUser(containerId
        .getApplicationAttemptId().toString());

    org.apache.hadoop.security.token.Token<NMTokenIdentifier> nmToken =
      ConverterUtils.convertFromYarn(token, cmAddr);
    user.addToken(nmToken);

    return NMProxy.createNMProxy(conf, ContainerManagementProtocol.class,
        user, rpc, cmAddr);
}

【ContainerToken】


在向NM请求启动container时,除了需要NMToken之外,还需要ContainerToken,以验证container的合法性。

ContainerToken和NMToken采用相同的方式,因此密钥的获取方式与流程以及更新,和前面NMToken中讲到的几乎是同一个流程。

首先,同样是在NM的注册与定时心跳请求中,RM向NM同步并更新密钥。RM向NM请求container时,直接创建并带上ContainerToken;而AM则是通过向RM申请资源时,RM创建了ContainerToken,并随请求的应答传递给了AM。此后AM再向NM请求启动container时,则带上了对应的Token信息,有兴趣的朋友可以对照流程走读相关源码。

另外,该token大的类型虽然都是containerToken,但实际上又细分为ApplicaitonMaster和Task两类,分别用于RM与NM通信、AM与NM通信中。

【LocalizerToken】


LocalizerToken主要用于NM的资源本地化服务与NM之间的通信。由于NM资源本地化服务是以一个独立进程的方式运行的,并且会通过rpc协议不断向NM汇报资源下载情况,因此使用Token来保证通信安全。

【总结】


小结一下,本文主要讲解了Yarn运行中涉及的几个token,具体包括token的作用,如何创建,具体使用的流程。

另外,除了上面介绍的几个token之外,各个任务(mr/spark/flink)在运行时,也还存在一些其他的token,例如mr中会用到的ClientToAMToken等,有兴趣的可以自行摸索下~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陈猿解码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
终于过审!首批小程序插件诞生了
3 月 13 日小程序上线小程序插件功能,在此之后许多小程序开发者提交了插件等待审核,然而微信团队对于此事十分严谨,一次次告知「代码审核未通过」。 近几日,在历经重重「打击」后,首批过审的小程序插件终于诞生。 「知晓云 SDK」与「腾讯地图」插件无疑是其中的佼佼者,而这两款插件也将作为本文的范例,从使用插件前后的对比、使用插件方式等几个方面为大家做一个简单的介绍。同时,我们也将公开 AppID,方便大家申请使用。 知晓云是个好用、顺手的小程序 BaaS (Backend As A Service)后端云服务
知晓君
2018/07/04
1.1K0
知晓云助力小程序开发
小程序开发遇到瓶颈 虽然腾讯提供了小程序解决方案,https://cloud.tencent.com/solution/la。但是对于普通开发者或者小企业的开发人员来说,购买域名,网站备案、部署SSL
八哥
2018/01/18
2K0
知晓云助力小程序开发
知晓云 | 5 分钟实现小程序模板消息推送,你可以这样做
但是,有了知晓云,你不用再头疼如何开发模板消息模块。只需要几步简单的操作,就可以轻松实现模板消息推送。
知晓君
2018/07/30
1.5K0
只要 5 分钟,让你立刻拥有自己的小程序 | 知晓云
Hello,各位知晓程序的读者们,我是犯迷糊的小羊。目前是 ifanr 的一只前端攻城狮,同时也是知晓云团队的一员。
知晓君
2018/08/01
1.1K0
只要 5 分钟,让你立刻拥有自己的小程序 | 知晓云
微信小程序开发
要求开发者有一些前端知识(HTML,CSS ,JavaScript), “工欲善其事必先利其器”,我们得先:
狂奔滴小马
2021/11/15
7.3K0
微信小程序开发
开发 | 无需后端编码,10 分钟教你实现一个朋友圈小程序
虽然目标功能的业务逻辑并不复杂,但其背后需要一套靠谱的权限控制系统,也意味着需要一个完整的后端服务系统来支持运行。
知晓君
2018/07/27
8150
开发 | 教你刷爆朋友圈:2 招搞定小程序生成分享图片功能
最近频频刷屏的许多 HTML 5 作品,都用到了生成含有用户信息的图片并保存分享的功能。
知晓君
2018/07/27
7420
2019-面向小白的微信小程序-视频教学-基础
微信小程序,简称小程序,英文名Mini Program,是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或搜一下即可打开应用
万少
2025/02/08
1840
2019-面向小白的微信小程序-视频教学-基础
接口人小程序快速开发--知晓云体验
今天要分享一个好东西,花叔保证不是广告。 话说,有一天,爱范儿的运营经理Angela找到我,说他们做了一个叫“知晓云”的东东,想邀请我来体验一下。 我说好啊,他们就给了我一枚体验码,然后我周末花了两天
花叔
2018/04/18
1.2K0
接口人小程序快速开发--知晓云体验
【腾讯游戏人生】微信小程序开发总结
目前【腾讯游戏人生】小程序已经发布上线,大家可以扫小程序码进行体验。接下来主要介绍在开发该款小程序过程中的一些思考和积累。
一时两无
2018/06/08
3.2K3
微信小程序自定义组件
其中,components为组件目录,nodemodules为模块目录,pages为小程序的页面目录,utils为一些基础功能的封装。好比安装的第三方百度统计功能在此。
mySoul
2018/09/15
2.7K0
【愚公系列】《微信小程序与云开发从入门到实践》029-自定义组件基础
随着移动互联网的快速发展,微信小程序作为一种新兴的应用形态,已经成为了众多开发者和企业的重要选择。它不仅拥有广泛的用户基础,还具备轻便、便捷的特点,能够实现丰富的功能和良好的用户体验。而在小程序的开发过程中,自定义组件的使用则是提升代码复用性和维护性的重要手段。
愚公搬代码
2025/01/21
1260
开发 | 一篇文章,带你从 0 到 1 开发小程序插件
作者:郑智文 知晓程序注: 前不久,微信释放了一个重磅新能力:微信小程序插件功能。有了它,小程序开发者就可以通过这个功能,强化自身小程序能力;小程序服务提供商也可以用它,为开发者、用户提供强大的小程序功能支持,进一步拓展小程序能力。 插件固然好,但如何从零开发一个插件呢?今天,知晓程序就来手把手,教你如何从零开发一款微信小程序插件。 关注「知晓程序」微信公众号,回复「开发」,获取小程序开发技巧大全。 新建插件工程 新建插件的操作非常简单。只需要在微信开发者工具中新建小程序项目,并选择「建立插件快速启动模板」
知晓君
2018/07/04
4670
微信开发--微信小程序(一)
微信小程序开发相对于微信公众号的开发显得更为重要,下面就来简单介绍一下微信小程序的开发.
生南星
2019/07/22
16.4K0
微信开发--微信小程序(一)
开发 | 谁说 LBS 小程序开发难?前端女王大人手把手教会你
利用它,你可以在小程序中调用一个功能完整的地图,让小程序里所展示的地点更直观、更精确。
知晓君
2018/07/30
9030
微信小程序自定义tab,多层tab嵌套实现
小程序最近是越来越火了…… 做小程序有一段时间了,总结一下项目中遇到的问题及解决办法吧。
solocoder
2022/04/06
8060
微信小程序自定义tab,多层tab嵌套实现
小程序开发知识必备-自定义组件
在 component 文件目录下,创建一个 select 文件夹,随后 select 文件夹下手动创建:select.js、select.json、select.wxml、select.wxss 四个文件。
leader755
2022/03/09
1.4K0
小程序开发知识必备-自定义组件
【推荐】开源项目minapp-重新定义微信小程序的开发
minapp 重新定义微信小程序的开发 官网:https://qiu8310.github.io/minapp/ 作者:Mora minapp 重新定义微信小程序的开发 使用 用 npm 安装命令行工具: npm install -g @minapp/cli --registry "https://registry.npmjs.org/" (避免从淘宝镜像上安装,它上面的还是老版本,已经给他们提了一个 issue) 初始化项目:minapp init <你要创建项目的文件夹> (同时支持创建 js 和
iKcamp
2018/03/30
1.4K0
【推荐】开源项目minapp-重新定义微信小程序的开发
[猫头虎分享21天微信小程序基础入门教程] 第12天:小程序的自定义组件开发
大家好,我是猫头虎,一名全栈软件工程师。今天我们继续微信小程序的学习,重点了解如何开发自定义组件。自定义组件可以提高代码的复用性和模块化程度,使开发更加高效和灵活。🚀
猫头虎
2024/05/26
1220
探索微信小程序的奇妙世界:从入门到进阶
https://cloud.tencent.com/developer/article/2465647?shareByChannel=link
忆愿
2024/12/01
1980
探索微信小程序的奇妙世界:从入门到进阶
推荐阅读
相关推荐
终于过审!首批小程序插件诞生了
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验