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

这里我们将创建三个类实现相应的功能:
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;
}
打开小程序,实际必须的参数主要是 appId
和 path
,在打开过程中,客户端将会根据小程序 appId
去接口服务获取小程序的详细信息,如 appName
、 logo
等参数;当然还有拉起小程序的参数,一般我们从的 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