打造地基: App拉起基础小程序容器
本节概要
从这一小节开始我们将基于上篇文章中搭建的开发环境,尝试实现 点击小程序
-> 拉起一个基础的小程序容器
的过程;
现在我们先来梳理一下拉起小程序的过程:
这里我们将创建三个类实现相应的功能:
AppManager
用于创建一个小程序App和关闭小程序的入口管理类
MiniApp
小程序的实例类,这个类将完成小程序的初始化和调度管理工作
Application
基础应用类,这个类主要是用来模拟实现客户端挂载小程序,管理小程序的推入推出;
在开始实现之前,我们先进行一些简单的改造: 在应用页面上创建一个根节点用于挂载小程序的页面
// 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
之前,我们先定义下打开一个小程序的所需要的参数类型定义:
export interface AppInfo {appId: string; /* 小程序AppId */path: string; /* 小程序页面path */name?: string; /* 小程序名称 */logo?: string; /* 小程序logo */scene?: number; /* 小程序打开的场景值: 这个是模拟的现在小程序打开的参数,我们这里实际没有用到 */[key: string]: any;
}
打开小程序,实际必须的参数主要是 appId
和 path
,在打开过程中,客户端将会根据小程序 appId
去接口服务获取小程序的详细信息,如 appName
、 logo
等参数;当然还有拉起小程序的参数,一般我们从的 path
参数上去解析出上面带的 query 参数信息;我们简单整理下这个流程如下:
现在我们来实现小程序 AppManager
类,创建 openApp
方法
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=喵游
// 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;
}
关于通过服务接口获取小程序详细信息的逻辑,这里我们先简单实现,直接通过静态数据返回
// 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 态模版
// 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>`;
目前我们先简单实现小程序的实例类,主要包括:
- 初始化小程序参数
- 创建小程序页面初始化函数
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
的实现比较简单,主要实现小程序的推入和推出方法,并添加一些推入推出的动画效果
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
文件中尝试点击小程序列表的时候拉起页面:
+ let application: Application;+ onMounted(() => {
+ application = new Application(miniWindow.value!);
+ });+ function openApp(app: any) {
+ AppManager.openApp(app, application);
+ }
至此我们从列表点击打开一个小程序容器的过程就基本实现了,目前应用效果如下:

项目中关于css样式动画部分文章内省略,大家可前往本小节项目仓库代码查看~~
本小节代码已同步至github: mini-wx-app