开发指南/开发网页应用/如何高效配置移动端主页
# 如何高效配置移动端主页

当你把网页应用的移动端配置为`open.larksuite.com/open-apis/authen/v1/authorize`（[获取授权登录授权码](https://open.larksuite.com/document/common-capabilities/sso/api/obtain-oauth-code)的 HTTP 请求 URL）时，可以快速实现应用免登访问，但由于每次访问主页都会进行免登流程，从而影响主页的加载速度。本文主要介绍如何选择更贴合业务的免登方案，配置更加高效的首页地址来提高首页加载速度。

## 背景信息

在开发网页应用时，将移动端主页地址设置为`https://open.larksuite.com/open-apis/authen/v1/authorize?app_id={APPID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}`，可以快速实现免登访问应用的能力。
实现的免登访问应用的场景如下：

- 场景一：在Lark客户端的工作台中，打开网页应用实现免登。详情参见[Lark网页应用单点登录](https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/login-overview)。

- 场景二：通过浏览器打开网页应用实现免登。详情参见[浏览器内网页登录](https://open.larksuite.com/document/uYjL24iN/ukTO4UjL5kDO14SO5gTN#9f534005)。

![](//sf16-sg.larksuitecdn.com/obj/open-platform-opendoc-sg/95cbd8e7322e4ce52580310c500fd0fa_RKXgtBtGpI.png?height=264&lazyload=true&maxWidth=600&width=1696)

以上的免登配置方式，可以快速实现应用的免登流程，但存在主页加载速度低、部分用户使用应用时出现 HTTPS 安全链接建立失败等问题。出现这类问题的主要原因是因为：用户每次打开网页应用时，都会发生一次重定向，且会向 open.larksuite.com 域名发起接口请求，从而增加了应用主页的加载时间。同时，重定向次数的增加，也会导致整个页面加载链路出现异常的概率增大。

## 配置思路

本章节将为你介绍推荐的免登方式，以及免登流程配置思路。
基于开放平台收到的其他开发者实际反馈数据，参考推荐的免登方式以及配置思路，可降低主页加载耗时 500ms 左右（数据仅供参考）。

1. 选择应用免登方式。

- 推荐方式：使用 [requestAccess](https://open.larksuite.com/document/uYjL24iN/uUzMuUzMuUzM/requestaccess)( 该接口适用于移动端、PC 端，暂不适用于浏览器，LarkV6.9.0版本前使用[requestAuthCode](https://open.larksuite.com/document/uYjL24iN/uUzMuUzMuUzM/20220308) )实现应用免登。免登流程说明以及示例代码，可参见[步骤三（可选）：应用免登流程](https://open.larksuite.com/document/uYjL24iN/uMTMuMTMuMTM/development-guide/step-3)。

- 不推荐方式：使用[获取授权登录授权码](https://open.larksuite.com/document/common-capabilities/sso/api/obtain-oauth-code)接口实现应用免登，即在移动端配置`open.larksuite.com/open-apis/authen/v1/authorize`的方式，该方式因存在性能上的损耗所以不推荐，仅适用于业务主页也需要用户身份认证的场景。

2. 合理管理登录态，避免多次请求与重复请求。

- 移动端主页地址配置为应用自身的域名

由于访问`open.larksuite.com/open-apis/authen/v1/authorize`时每次都需要请求获取临时授权码 code，并执行一次重定向，因此会增加页面加载时间。当你将移动端主页地址配置为应用自身的域名时，可有效降低用户访问应用时的重定向次数与请求次数。如下图展示了不同主页地址在特定场景下，客户端重定向次数以及 OpenAPI 请求次数的数据统计情况。

![image.png](//sf16-sg.larksuitecdn.com/obj/open-platform-opendoc-sg/7921faae92f3440ba28cae994f41ab1b_swydx5QR8m.png?height=492&lazyload=true&maxWidth=600&width=1936)

- 避免用户每次打开网页应用时都需请求临时授权码

如果你的应用需要获取Lark用户信息，则可以通过 cookie 进行登录态管理。在 cookie 的有效期内，只需要用户首次打开应用时执行免登逻辑，从而减少用户非首次访问首页的耗时。

此外，Lark用户在切换租户或者重新登录时，均会清空本地所有的 cookie（应用在上一次配置的 cookie 也会被清除）。用户在打开网页应用的时候会重新走免登的逻辑，配置新的业务 cookie，所以无需担心用户态信息有误或者被干扰。

- 如下图所示，建议你检查登录态的接口（图中接口 A）和用于免登的 CallBack 接口（图中接口 B），做好接口分离，各自负责各自的业务逻辑。

![](//sf16-sg.larksuitecdn.com/obj/open-platform-opendoc-sg/c5a693e001803b0f59644d1e00b297e9_ohQdkB0vfk.png?height=834&lazyload=true&maxWidth=600&width=1295)

配置思路总结：

- 移动端首页地址直接配置为业务自身的域名。
- 无论选择哪种免登方式，均需要做好登录态管理，区分用户首次和非首次打开网页应用。

## 免登流程示例代码

为了方便你理解免登流程，开放平台以前后端不同视角展示相关代码逻辑，仅供参考。

### 后端代码逻辑

```JavaScript
const LJ_TOKEN_KEY = 'lk_token'
//处理免登请求，返回用户的user_access_token
async function getUserAccessToken(ctx) {
    console.log("\n-------------------[接入服务端免登处理 BEGIN]-----------------------------")
    console.log(`接入服务方第① 步: 接受到前端免登请求`)
    const accessToken = ctx.session.userinfo
    const lkToken = ctx.cookies.get(LJ_TOKEN_KEY) || ''
    if (accessToken && accessToken.access_token && lkToken.length > 0 && accessToken.access_token == lkToken) {
        console.log("接入服务方第② 步: 从Session中获取AccessToken，用户已登录，返回")
        ctx.body = serverUtil.okResponse(accessToken)
        console.log("-------------------[接入服务端免登处理[已登录] END]-----------------------------\n")
        return
    }
    let code = ctx.query["code"] || ""
    console.log("接入服务方第② 步: 获取临时授权码code, ", code.length > 0)
    //请求app_access_token：https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/app_access_token_internal
    console.log("接入服务方第③ 步: 根据AppID和App Secret请求应用授权凭证app_access_token")
    const internal = await axios.post("https://open.larksuite.com/open-apis/auth/v3/app_access_token/internal", {
        "app_id": serverConfig.config.appId,
        "app_secret": serverConfig.config.appSecret
    }, { headers: { "Content-Type": "application/json" } })
    if (!internal.data) {
        ctx.body = serverUtil.failResponse("app_access_token request error")
        return
    }

if (internal.data.code != 0) { //非0表示失败
        ctx.body = serverUtil.failResponse(`app_access_token request error${internal.data.msg}`)
        return
    }
    console.log("接入服务方第④ 步: 获得颁发的应用授权凭证app_access_token")
    const app_access_token = internal.data.app_access_token ||""
    console.log("接入服务方第⑤ 步: 根据授权码Code和app_access_token请求用户授权凭证user_access_token")
    //根据登录预授权码code获取 user_access_token: https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/access_token/create
    const authenv1 = await axios.post("https://open.larksuite.com/open-apis/authen/v1/access_token", { "grant_type": "authorization_code", "code": code }, {
        headers: {
            "Content-Type": "application/json; charset=utf-8",
            "Authorization": "Bearer " + app_access_token
        }
    })
    if (!authenv1.data) {
        ctx.body = serverUtil.failResponse("user_access_token request error")
        return
    }
    if (authenv1.data.code != 0) { //非0表示失败
        ctx.body = serverUtil.failResponse(`user_access_token request error${authenv1.data.msg}`)
        return
    }
    console.log("接入服务方第⑥ 步: 获取颁发的用户授权码凭证的user_access_token, 返回给前端")
    const newAccessToken = authenv1.data.data
    if (newAccessToken) {
        ctx.session.userinfo = newAccessToken
        serverUtil.setCookie(ctx, LJ_TOKEN_KEY, newAccessToken.access_token || '')
    } else {
        serverUtil.setCookie(ctx, LJ_TOKEN_KEY, '')
    }
    ctx.body = serverUtil.okResponse(newAccessToken)
    console.log("-------------------[接入服务端免登处理 END]-----------------------------\n")
}
```

### 前端代码逻辑

```JavaScript
/// ---------------- 应用免登 部分 核心JS -------------------------
const LJ_TOKEN_KEY = 'lk_token'
//处理用户免登逻辑
export async function handleUserAuth(complete) {
    console.log("\n----------[接入方网页免登处理 BEGIN]----------")
    let lj_tokenString = Cookies.get(LJ_TOKEN_KEY) || ""
    if (lj_tokenString.length > 0) {
        console.log("接入方前端[免登处理]第① 步: 用户已登录，请求后端验证...")
        requestUserAccessToken("", complete)
    } else {
        if (!window.h5sdk) {
            console.log('invalid h5sdk')
            complete()
            return
        }
        console.log("接入方前端[免登处理]第① 步: 依据App ID调用JSAPI tt.requestAccess 请求免登授权码")
        //依据App ID调用JSAPI tt.requestAccess 请求临时授权码code
        window.h5sdk.ready(() => {
            console.log("window.h5sdk.ready");
            if (window.tt.requestAccess) {
              window.tt.requestAccess({
                appID: clientConfig.appId,
                scopeList: [],
                success: (res) => {
                  // 用户授权后返回预授权码
                  const { code } = res;
                  if (code.length <= 0) {
                      console.error('auth code为空')
                      complete()
                  } else {
                      requestUserAccessToken(code, complete)
                  }
                },
                fail: (error) => {
                  // 需要额外根据errno判断是否为 客户端不支持requestAccess导致的失败
                  const { errno, errString } = error;
                  if (errno === 103) {
                    // 客户端版本过低，不支持requestAccess，需要改为调用requestAuthCode
                    callRequestAuthCode(complete);
                  } else {
                    // 用户拒绝授权或者授权失败
                    console.error('auth failed')
                    complete()
                  }
                },
              });
            } else { // JSSDK版本过低，不支持requestAccess，需要改为调用requestAuthCode
              callRequestAuthCode(complete);
            }
        });
    }
}
function callRequestAuthCode(complete) {
  window.tt.requestAuthCode({
    appId: clientConfig.appId,
    success: (info) => {
        const code = info.code
        if (code.length <= 0) {
            console.error('auth code为空')
            complete()
        } else {
            requestUserAccessToken(code, complete)
        }
    },
    fail: (error) => {
        complete()
        console.error("window.tt.requestAuthCode", error)
    }
  });
}
function requestUserAccessToken(code, complete) {

// 获取user_access_token信息
    console.log("接入方前端[免登处理]第② 步: 去接入方服务端获取user_access_token信息")
    axios.get(`${getOrigin(clientConfig.apiPort)}${clientConfig.getUserAccessTokenPath}?code=${code}`,
        { withCredentials: true }   
    ).then(function (response) {  
        if (!response.data) {
            console.error(`${clientConfig.getUsee} response is null`)
            complete()
            return
        }
        const data = response.data.data
        if (data) {
            console.log("接入方前端[免登处理]第③ 步: 获取user_access_token信息")
            complete(data)
            localStorage.setItem(LJ_TOKEN_KEY, data.access_token)
            console.log("----------[接入网页方免登处理 END]----------\n")
        } else {
            console.error("接入方前端[免登处理]第③ 步: 未获取user_access_token信息")
            complete()
            console.log("----------[接入网页方免登处理 END]----------\n")
        }
    }).catch(function (error) {
        console.log(`${clientConfig.getSignParametersPath} error:`, error)
        complete()
        console.log("----------[接入网页方免登处理 END]----------\n")
    })
}
```