打造地基: App拉起基础小程序容器

本节概要

从这一小节开始我们将基于上篇文章中搭建的开发环境,尝试实现 点击小程序 -> 拉起一个基础的小程序容器 的过程;

现在我们先来梳理一下拉起小程序的过程:

这里我们将创建三个类实现相应的功能:

AppManager

用于创建一个小程序App和关闭小程序的入口管理类

MiniApp

小程序的实例类,这个类将完成小程序的初始化和调度管理工作

Application

基础应用类,这个类主要是用来模拟实现客户端挂载小程序,管理小程序的推入推出;

在开始实现之前,我们先进行一些简单的改造: 在应用页面上创建一个根节点用于挂载小程序的页面

vue 复制代码
// src/App.vue

<script lang="ts" setup>
+ import { ref, onMounted } from "vue";

+ const miniWindow = ref<HTMLElement>();
</script>

<template>
+ <Teleport to="body">
+   <div class="mini-body absolute top-0 left-0 z-10" ref="miniWindow"></div>
+ </Teleport>
</template>

AppManager 小程序应用管理类

在实现小程序管理类 AppManager 之前,我们先定义下打开一个小程序的所需要的参数类型定义:

ts 复制代码
export interface AppInfo {
  appId: string; /* 小程序AppId */
  path: string;  /* 小程序页面path */
  name?: string; /* 小程序名称 */
  logo?: string; /* 小程序logo */
  scene?: number; /* 小程序打开的场景值: 这个是模拟的现在小程序打开的参数,我们这里实际没有用到 */
  [key: string]: any;
}

打开小程序,实际必须的参数主要是 appIdpath,在打开过程中,客户端将会根据小程序 appId 去接口服务获取小程序的详细信息,如 appNamelogo 等参数;当然还有拉起小程序的参数,一般我们从的 path 参数上去解析出上面带的 query 参数信息;我们简单整理下这个流程如下:

现在我们来实现小程序 AppManager 类,创建 openApp 方法

ts 复制代码
class AppManager {
  // 管理所有创建的小程序
  static appStack: MiniApp[] = [];
  
  /**
   * 打开小程序App
   * @param opts 小程序参数
   * @param wx 客户端应用管理类,主要用来实现小程序页面的最终挂载
   */
  static async openApp(opts: AppInfo, wx: Application) {
    const { appId, path, scene } = opts;
    // 1. 解析 path 上的query 参数
    const { pagePath, query } = queryPath(path);
    // 2. 通过接口获取小程序的详细信息
    const { appName, logo } = await getMiniAppInfo(appId);
    // 3. 创建小程序App实例
    const miniApp = new MiniApp({
      appId,
      scene,
      logo,
      query,
      path: pagePath,
      name: appName,
    });
    this.appStack.push(miniApp);
    wx.presentView(miniApp);
  }
  
  /**
   * 关闭小程序App
   */
  static closeApp(miniApp: MiniApp) {
    miniApp.parent?.dismissView({
      destroy: false,
    });
  }
} 

这里我们需要创建一个工具函数 queryPath 来解析 path 参数,拆分出真实的路径信息和参数数据

例如一个 path 参数的形式如下: pages/index/index?name=喵游

ts 复制代码
// src/utils/util.ts
export function queryPath(path: string) {
  const [pagePath, paramsStr] = path.split('?')[1];
  const result = {
    query: {},
    pagePath,
  };

  if (!paramsStr) {
    return result;
  }

  let paramList = paramsStr.split('&');

  paramList.forEach((param) => {
    let key = param.split('=')[0];
    let value = param.split('=')[1];

    result.query[key] = value;
  });

  return result;
}

关于通过服务接口获取小程序详细信息的逻辑,这里我们先简单实现,直接通过静态数据返回

ts 复制代码
// src/service/index.ts
const appInfo = {
  douyin: {
    appName: '抖音',
    logo: 'https://img.zcool.cn/community/0173a75b29b349a80121bbec24c9fd.jpg@1280w_1l_2o_100sh.jpg'
  },
  meituan: {
    appName: '美团',
    logo: 'https://s3plus.meituan.net/v1/mss_e2821d7f0cfe4ac1bf9202ecf9590e67/cdn-prod/file:9528bfdf/20201023%E7%94%A8%E6%88%B7%E6%9C%8D%E5%8A%A1logo/%E7%BE%8E%E5%9B%A2app.png'
  },
  jingdong: {
    appName: '京东',
    logo: 'https://ts1.cn.mm.bing.net/th/id/R-C.8e130498abf4685d15ecb977869a5a39?rik=%2f%2bLRdQM48y8y0A&riu=http%3a%2f%2fwww.xiue.cc%2fwp-content%2fuploads%2f2017%2f09%2fjd.jpg&ehk=hUzDTV9xjw%2flaGD5eZcKGl%2fN7UkzBSHRjo73I%2bMeVvo%3d&risl=&pid=ImgRaw&r=0'
  }
};

export function getMiniAppInfo(appId: string) {
  return new Promise<MiniAppInfo>((resolve) => {
    resolve(appInfo[appId]);
  });
}

MiniApp 小程序实例类

在实现小程序类之前,我们先来创建下小程序页面的HTML模板,主要包括:

  • 小程序页面右上方药丸按钮
  • webview 挂载节点
  • 小程序页面启动的 loading 态模版
ts 复制代码
// src/miniApp/tpl.ts
export const miniAppTpl = `<div class="wx-mini-app">
    <!-- 右上方药丸按钮 -->
    <ul class="wx-mini-app-navigation__actions">
        <li class="wx-mini-app-navigation__actions-variable"></li>
        <li class="wx-mini-app-navigation__actions-close"></li>
    </ul>

    <!-- webview挂载节点 -->
    <div class="wx-mini-app__webviews"></div>

    <!-- 启动loading页面 -->
    <div class="wx-mini-app__launch-screen">
        <div class="wx-mini-app__launch-screen-content">
            <div class="wx-mini-app__logo">
                <div class="wx-mini-app__logo-img">
                    <img class="wx-mini-app__logo-img-url">
                </div>
                <div class="wx-mini-app__logo-circle"></div>
                <span class="wx-mini-app__green-point"></span>
            </div>
            <h1 class="wx-mini-app__name"></h1>
        </div>
    </div>
</div>`;

目前我们先简单实现小程序的实例类,主要包括:

  • 初始化小程序参数
  • 创建小程序页面初始化函数
ts 复制代码
class MiniApp {
  /* 小程序appId */
  appId: string;
  /* 小程序App信息 */
  app: OpenMiniAppOpts;
  /* application实例 */
  parent: Application | null = null;
  /* 小程序页面根节点 */
  el: HTMLElement;
  /* 小程序webview的挂载节点 */
  webviewContainer: HTMLElement | null = null;
  
  constructor(opts: OpenMiniAppOpts) {
    this.app = opts;
    this.appId = opts.appId;
    // 创建小程序页面的根节点
    this.el = document.createElement('div');
    this.el.classList.add('wx-native-view');
  }
  
  /* 初始化小程序页面 */
  viewDidLoad() {
    // 初始化小程序页面模版
    this.initMiniAppFrame();
    this.webviewContainer = this.el.querySelector('.wx-mini-app__webviews');
    // 显示小程序加载状态信息
    this.showLaunchScreen();
    // 绑定小程序关闭事件
    this.bindCloseEvent();
  }
  
  initMiniAppFrame() {
    this.el.innerHTML = miniAppTpl;
  }
  
  /**
   * 显示小程序加载状态
   */
  showLaunchScreen() {
    const launchScreen = this.el.querySelector('.wx-mini-app__launch-screen') as HTMLElement;
    const name = this.el.querySelector('.wx-mini-app__name') as HTMLElement;
    const logo = this.el.querySelector('.wx-mini-app__logo-img-url') as HTMLImageElement;

    name.innerHTML = this.app.name;
    logo.src = this.app.logo;
    launchScreen.style.display = 'block';
  }
  
  bindCloseEvent() {
    const closeBtn = this.el.querySelector('.wx-mini-app-navigation__actions-close') as HTMLElement;

    closeBtn.onclick = () => {
        AppManager.closeApp(this);
    };
  }
}

Application 客户端管理类

Application 的实现比较简单,主要实现小程序的推入和推出方法,并添加一些推入推出的动画效果

ts 复制代码
class Application {
  /* 应用的根容器 */
  el: HTMLElement;
  /* 应用页面挂载节点 */
  window: HTMLElement | null = null;
  /* 存储应用的视图列表 */
  views: MiniApp[] = [];
  /* 页面加载状态: 用于避免在一个小程序加载阶段再加载别的 */
  done: boolean = true;
  
  constructor(el: HTMLElement) {
    this.el = el;
    this.init();
  }
  
  init() {
    // 创建应用页面的挂载节点,并添加到根容器中
    this.window = document.createElement('div');
    this.window.classList.add('wx-native-window');
    this.el.appendChild(this.window);
  }
  
  /**
   * 拉起小程序页面
   */
  async presentView(view: MiniApp) {
    if (!this.done) return;
    this.done = false;

    view.parent = this;
    view.el.style.zIndex = `${this.views.length + 1}`;
    // 初始化小程序为止: 将小程序为止调整到屏幕-1屏,再添加划入动画
    view.el.classList.add('wx-native-view--before-present');
    view.el.classList.add('wx-native-view--enter-anima');
    this.window?.appendChild(view.el);
    this.views.push(view);
    view.viewDidLoad();
    await sleep(20);
    // 小程序入场: 调整小程序为止
    view.el.classList.add('wx-native-view--instage');
    await sleep(540);
    this.done = true;
    // 移除初始化样式类
    view.el.classList.remove('wx-native-view--before-present');
    view.el.classList.remove('wx-native-view--enter-anima');
  }
  
  /**
   * 退出小程序
   */
  async dismissView(opts: any = {}) {
    if (!this.done) return;
    this.done = false;
    
    // 推出小程序主要是将当前小程序页面推出
    // 将前一个小程序页面显示出来
    
    // 这里推出小程序可能是直接从页面上把节点直接卸载掉;
    // 或者是直接添加特定的样式,将小程序移入负一屏,这样在下次拉起的时候可以直接复用;
    
    const preView = this.views[this.views.length - 2];
    const currentView = this.views[this.views.length - 1];
    const { destroy = true } = opts;
    
    // 将当前的小程序推出, 添加推出动画
    currentView.el.classList.add('wx-native-view--enter-anima');
    preView?.el.classList.add('wx-native-view--enter-anima');
    preView?.el.classList.add('wx-native-view--before-presenting');
    await sleep(0);
    // 添加推出样式类,及最终推出实际是将小程序页面先移到-1屏
    currentView.el.classList.add('wx-native-view--before-present');
    currentView.el.classList.remove('wx-native-view--instage');
    preView?.el.classList.remove('wx-native-view--presenting');
    await sleep(540);
    this.done = true;
    // 卸载: 从页面上移除掉页面节点
    destroy && this.el!.removeChild(currentView.el);
    this.views.pop();
    preView?.el.classList.remove('wx-native-view--enter-anima');
    preView?.el.classList.remove('wx-native-view--before-presenting');
  }
}

小程序列表点击拉起小程序页面

完成上面三个类的创建后,我们在App.vue 文件中尝试点击小程序列表的时候拉起页面:

App.vue 复制代码
+ let application: Application;

+ onMounted(() => {
+   application = new Application(miniWindow.value!);
+ });

+ function openApp(app: any) {
+   AppManager.openApp(app, application);
+ }

至此我们从列表点击打开一个小程序容器的过程就基本实现了,目前应用效果如下:

项目中关于css样式动画部分文章内省略,大家可前往本小节项目仓库代码查看~~

本小节代码已同步至github: mini-wx-app

相关推荐
teeeeeeemo1 小时前
Number.toFixed() 与 Math.round() 深度对比解析
开发语言·前端·javascript·笔记
阿珊和她的猫1 小时前
`toRaw` 与 `markRaw`:Vue3 响应式系统的细粒度控制
前端·javascript·vue.js·typescript
网络点点滴1 小时前
探索 Vue 替代方案
前端·javascript·vue.js
想用offer打牌2 小时前
一站式了解CDN😈
后端·架构·cdn
江城开朗的豌豆2 小时前
Vue的keep-alive缓存揭秘:多出来的生命周期怎么玩?
前端·javascript·vue.js
BoredWait2 小时前
vite+vue-ts 如何在项目中实现多语言
前端·javascript
用户30742971671582 小时前
Spring AI Chain工作流模式完整指南
java·架构
年纪轻轻就扛不住2 小时前
keep-alive实现原理及Vue2/Vue3对比分析
前端·javascript·vue.js
这是个栗子2 小时前
黑马头条-数据管理平台
前端·javascript·ajax
2501_915373882 小时前
如何在 Chrome 浏览器中保存从商店下载的扩展程序到本地
前端·chrome