Vue组件库移动端预览实现原理

引言

大家如果使用过移动端组件库(比如:Vant),会发现在网站右侧有一个手机端的预览效果。

image.png

而且这个手机端预览的内容和外面的组件代码演示是同步的,切换组件的时候,移动端预览的内容也会发生相应的变化。

这是怎么实现的呢?我们一起来看看Vue DevUI组件库的实践吧!

先看下最终实现的效果动画:

移动端预览效果动画1.gif

1 方案选型

通过对竞品进行分析,发现移动端预览都是由嵌入的iframe完成的,也就是说

移动端预览是一个完整的网站。

既然要做一个完整网站,在现有工程结构下想到了以下方案:

1.1 最初的设想

当前文档是通过vitepress搭建的,并且vitepress提供了定制主题的功能,开发完成的组件可以直接在md文档中展示效果,所以就想到通过在vitepress下新建一个mobile路由来包含移动端预览所需的页面。

这会有一个问题,移动端预览页面不需要顶部导航栏和左侧菜单栏,调研发现,vitepress仅支持定制一种主题,虽然貌似可以通过判断路由的方式在模板里面隐藏顶部导航栏和左侧菜单栏的方案(未验证是否可行),但是考虑到md毕竟是来写文档的,虽然可以简单的展示组件效果,但是要做一些复杂的功能(比如目前需要的预览页面首页组件列表),在md文档里面完成可能就不太合适了。

甚至如果以后要做的功能太复杂而无法在md文档里面完成,那么当前的设计就面临着推倒重做的风险。

故排除此方案。

1.2 另一种更好的方案

最好是新起一个移动端预览的工程,并且技术栈与组件库保持一致vue3+vite

这样的话,组件效果可以在新工程里面直接预览,并且这种建站方案成熟,可以做很多事情,即使以后需要做比较复杂的演示效果也不会有大问题。

故采用此方案。

2 移动端工程搭建

2.1 新工程单独出来还是整合到组件库里面?

考虑到组件库应该是一个整体的工程,无论是组件开发、文档输出还是效果预览都应该在一个工程下,如果将移动端预览这部分单独到一个工程里面,会造成维护上的困难。那如何将移动端预览整合到组件库的工程里面呢?

通过在根目录新建mobile文件夹,用来存放移动端预览工程相关的文件,根据vite的配置,还需要新建一个mobile.html入口文件,并在文件里面添加

<script type="module" src="/mobile/main.ts"></script>

开发环境下,可以通过

yarn run app:dev

启动,但是vite默认将index.html文件作为入口,所以在访问路径中增加/mobile.html可以访问移动端预览的页面,完整访问路径为:http://localhost:3001/mobile.html#/button

2.2 路由方式优化

当前组件库工程里面已包含业务场景的工程,采用的history路由,如果移动端预览也采用history路由,在开发阶段会和业务场景冲突,故移动端预览采用的hash路由。

2.3 打包优化

根据vite多页面应用模式的配置,最初将业务场景的代码和移动端预览的代码打包到一起了,后面考虑到两个工程的代码会有分开部署的需求,故对打包配置做了调整,调整后如下:

// vite.config.ts 
// 业务场景代码的配置文件,修改输出目录即可
export default defineConfig({// 其他配置...build: {outDir: 'dist-src'}
})
// 根目录新建vite.config.mobile.ts
// 移动端预览配置文件,指定打包入口和输出目录
export default defineConfig({// 其他配置...build: {outDir: 'dist-mobile',rollupOptions: {input: {mobile: path.resolve(__dirname, 'mobile.html')}}}
})

3 父子页面路由同步

3.1 场景

工程搭建方面的配置基本完成,接下来就需要考虑具体功能实现了。

目前官网(后面用父页面来表示)通过vitepress能够正常访问,移动端预览(后面用子页面来表示)通过vue3+vite的建站方案也能够正常访问。

那两个网站的路由状态如何保持一致呢?

即需要满足以下几个场景:

  • 点击父页面的菜单,子页面路由需要相应修改
  • 点击子页面的菜单,父页面路由需要相应修改
  • 点击浏览器前进和后退按钮,父子页面路由需要同步修改
  • 点击子页面的返回按钮,父子页面路由需要同步修改

3.2 父子页面通信

由于我们采用的是iframe的方案,所以此功能就涉及到iframe父子传参通信,通信方式分为两种:同域跨域

简而言之,同域通信就是通过访问到对应域的window对象,然后进行相应的操作。

跨域通信就是通过postMessage的能力进行消息同步。

同域存在以下两个问题:

  • 由于本地开发,两个工程分别启动在两个端口下面,无法实现同域通信。
  • 同域通过window.location去修改路由,会存在刷新页面的情况,而iframe标签上的src地址是在父页面渲染时去初始化的,如果刷新页面会再次初始化iframe标签上的src地址。而我们想要实现的效果是,只在父页面渲染的时候初始化一次iframe标签上的src地址,后续的操作,iframe标签上的src地址不再变化,只是内部路由变化。

故同域通信的方案被排除。

接下来就是跨域通信的方案了~

因为我们的需求是父子页面路由状态双向同步,所以在父子页面均需要发送消息和接受消息。

父页面通过watch监听在路由变化时,发送消息,代码如下:

// 父页面发送消息
watch(() => route.path,(newPath) => {// 根路由,path为'/';其他路由,为'/component/xxx/'const pathArr = newPath?.split('/');const oFrame = document.querySelector('iframe');oFrame?.contentWindow?.postMessage({type: 'devui',value: newPath === '/' ? '/' : pathArr[pathArr.length - 2],},'*',);},
);

父页面接受消息代码如下:

// 父页面接受消息
window.addEventListener('message', function (event) {if (event.data.type && event.data.type === 'devui') {router.go(event.data.value ? `/components/${event.data.value}/` : '/');}
});

子页面同样在路由变化时发送消息:

watch(router.currentRoute, (newPath) => {// 路由变更来源分为两种:// parent---由父页面影响,包括父页面路由变更,预览页面同步切换;页面首次渲染。触发watch// ''-------点击子页面首页的菜单时,触发watch// 值为parent的情况为父页面影响预览页面,无需再次通过postMessage通信const routeChangeFromParent =sessionStorage.getItem('routeChangeFromParent') === 'parent';if (routeChangeFromParent) {sessionStorage.setItem('routeChangeFromParent', '');return;}window.parent.postMessage({type: 'devui',value: getLinkUrl(newPath.path),},'*',);
});

子页面接受消息代码如下:

window.addEventListener('message', (event) => {if (event.data.type && event.data.type === 'devui') {sessionStorage.setItem('routeChangeFromParent', 'parent');router.replace(event.data.value);}
});

子页面返回上一页面的实现:

点击返回按钮,需要执行浏览器的返回操作,即history.back()。此时父页面的watch监听到路由变化,然后通知子页面变更路由。

3.3 遇到的问题

一、路由变更死循环

由于子页面路由变化来源有两种:父页面通知和点击子页面菜单,为了避免死循环:子通知父变更,父变更之后,再次通知子变更,子又通知父变更…,通过sessionStorage做了标记进行判断拦截。

二、页面显示不出来

mobile-preview-blank-page.png

这个问题出现在页面渲染的时候。

三、选中页面文本,页面错乱

mobile-preview-error-page.png

这个问题出现在选中页面文本的时候。

通过对第二三个问题的分析,发现是message事件被“意外”触发,定位之后发现是因为某些Chrome插件(Augury和沙拉查词)触发了message事件,所以增加了devui的标志,对message事件来源进行过滤。

4 子页面生成

组件库支持PC端和Mobile端组件,在编写文档的时候,演示demo写入了文档中。

增加移动端预览的功能之后,预览框中也需要演示demo,并且演示demo需要和文档中的保持一致。

这时就需要维护两份演示demo。

为了降低后期维护的工作量,就想到了通过自动化脚本的方式,从Markdown文档中提取演示demo代码,然后再输出到移动端预览工程中。

自动化脚本需要支持单个组件和全部组件转换,输入all表示全部转换。

整个自动化脚本大概分为以下几步:

4.1 如何获知要转换哪个组件?

通过命令行的方式,输入命令之后,提示想要转换的组件名称,组件名称需要和组件文件夹名字保持一致,用来从指定文件夹下提取文档内容。

命令交互实现如下:

// mobile-cli-index.js
// 通过commander创建命令
#!/usr/bin/env node
const { Command } = require('commander');
const { create } = require('./generate-mobile-demo');
const { version } = require('../package.json');const program = new Command();program.command('create').description('从md提取演示demo代码,自动输出到mobile文件夹').action(create);program.parse().version(version);
// mobile-inquirers.js
// 通过inquirer提供交互操作
exports.inputComponentName = () => ({name: 'name',type: 'input',message:'(必填)请输入组件名字,将该组件md中的演示代码提取到mobile中,仅支持mobile组件,all表示转换所有Mobile组件:',validate: (value) => {if (value.trim() === '') {return '组件 name 是必填项';}return true;},
});
// generate-mobile-demo.js
// 提供输入命令之后要执行的操作
const inquirer = require('inquirer');
const { inputComponentName } = require('./mobile-inquirers');exports.create = async () => {const { name } = await inquirer.prompt([inputComponentName()]);if (!validateComponentName(name.toLocaleLowerCase())) {console.log('该组件不支持Mobile');process.exit(0);}if (name === 'all') {// 通过遍历的方式,对所有Mobile组件进行转换mobileComponents.forEach((value, key) => {reset();generateSingleComponent(value, key);});} else {// 单个组件转换generateSingleComponent(mobileComponents.get(name), name);}
};

4.2 提取Markdown内容

通过node的能力读取文档内容,实现如下:

const mdFileContent = fs.readFileSync(path.resolve(__dirname, `../docs/components/${componentName}/index.md`),'utf8',
);

4.3 如何从Markdown内容中提取想要的信息?

目前想到两种方案可以实现:

  • 一种是用正则对字符串进行切割;
  • 另一种方案是将Markdown文档转成AST结构,对AST进行遍历,获取到想要的数据。

AST转换更加准确严谨,具备清晰的结构化信息,更加灵活;

而正则表达式需要考虑太多边界情况,无法有效分析上下文。

故采取AST的方案。

借用@textlint/markdown-to-ast插件将Markdown文档转成AST的结构:

const mdParser = require('@textlint/markdown-to-ast').parse;
const mdAST = mdParser(mdFileContent);

AST的结构如下(只截图了部分):

markdown-ast.PNG

通过对AST遍历提取代码,约定好文档中的stylescript、三级标题及其后面紧跟的html或者CodeBlock为我们需要的数据。具体提取过程如下:

mdAST.children.forEach(({ type, depth, lang, raw }) => {if (STYLE_REG.test(raw)) {styleStr = raw;} else if (SCRIPT_REG.test(raw)) {scriptStr = raw;} else if (type === 'Header' && depth === 3) {needPickHtml = true;let title = raw.replace('### ', '');isFlow? (htmlStr += `<div class="demo-block__title">${title}</div>`): tabTitleArr.push(title);} else if (isPureMobile) {if (needPickHtml && type === 'CodeBlock' && lang && lang === 'html') {let content = raw.replace('```html', '').replace('```', '');isFlow ? (htmlStr += content) : tabContentArr.push(content);needPickHtml = false;}} else {if (needPickHtml && type === 'Html') {isFlow ? (htmlStr += raw) : tabContentArr.push(raw);needPickHtml = false;}}
});
  • 通过STYLE_REGSCRIPT_REG识别到Markdown文档中的stylescript代码。
  • 识别三级标题,作为演示demo中的分类标题。
  • 因纯Mobile组件,演示demo不方便直接在文档中展示,故提取type === CodeBlock的代码。
  • 因支持PCMobile的组件,CodeBlock中的代码在预览框中不一定能正常展示,故提取type === html的代码。

4.4 拼接输出字符串

完成htmlstylescript代码提取之后,接下来就是拼接字符串。

字符串的拼接有三种情况:

  • 一种是Markdown文档中没有script代码,这种输出的代码里面需要写好script代码;
  • 第二种是有script代码的情况,这种需要把htmlstylescript的代码都拼接进去;
  • 第三种是输出的Mobile不是流式布局(参考预览框的Button组件),而是Tab结构的布局(参考预览框的PullRefresh组件),这种需要对拼接的字符串做处理。

具体拼接不做过多介绍,下面只展示第二种情况的代码:

function createTemplate() {return `${scriptStr}<template><div class="component-content">${htmlStr}</div></template>${styleStr}`
}

字符串拼接完成之后,就是通过node的能力输出到文件中。这个比较简单,不做过多描述了,直接贴代码:

let fileStr = createTemplate()
fs.outputFile(path.resolve(componentDir, 'index.vue'), fileStr, 'utf8');

4.5 移动端预览首页和路由表生成

移动端预览首页同样是自动生成的,因文档左侧的菜单栏是根据docs/.vitepress/config/sidebar.ts文件中的配置生成的,所以移动端预览首页也是读取的此文件中的配置生成的,只不过需要做个过滤,只渲染Mobile组件。

移动端预览工程的路由表同样也是通过读取docs/.vitepress/config/sidebar.ts文件中的配置生成的,通过拼接字符串的形式输出到mobile/route/index.ts文件中。

5 小结

移动端预览是一个完整的网站,通过iframe的方式嵌入到组件库官网中,然后通过postMessage的能力实现父子页面路由同步。

预览页面通过生成AST的方式,从Markdown文档中提取演示代码自动生成,移动端预览的首页和路由表也同样根据组件库的菜单栏配置文件自动生成。

这样就只需要在Markdown文档中维护菜单栏和演示代码即可,降低维护的工作量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3281013.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

守护线程(Daemon Threads)详解:与非守护线程的区别

守护线程&#xff08;Daemon Threads&#xff09;详解&#xff1a;与非守护线程的区别 1、守护线程是什么&#xff1f;2、守护线程与非守护线程的区别2.1 JVM关闭行为2.2 任务性质2.3 线程设置2.4 示例代码 3、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收…

pytorch 绘制Depth Anything网络结构

pytorch 绘制模型的网络结构有很多中方法&#xff0c;个人比较喜欢 torchview 生成的 Graphviz 风格的图片。 Graphviz介绍 Graphviz是一款开源的图形可视化软件&#xff0c;其名称来源于“Graph Visualization Software”的缩写。它通过使用一种名为DOT的描述语言来定义图形…

不踩雷的护眼大路灯有哪些?五款盲选不踩雷的护眼大路灯推荐

不踩雷的护眼大路灯有哪些&#xff1f;作为一名专业的实测博主温馨提示大家&#xff0c;虽然护眼落地灯是个好东西&#xff0c;它能够提供柔和舒适的环境光&#xff0c;减少对眼睛的伤害&#xff0c;但是千万别乱买跟风&#xff0c;盲目入手踩雷率80%以上。那么如何辨别一盏护眼…

创客项目秀 | 基于 XIAO 开发板的语音向导

背景 柴火创客空间作为大湾区科技创新的窗口&#xff0c;每年到访空间的社区伙伴众多&#xff0c;为了更好的进行空间信息交互&#xff0c;我们希望有一个装置是可以解决&#xff1a;当空间管理员不在现场的时候&#xff0c;到访者可以通过装置获得清晰的介绍与引导。 为了解…

vue2 封装插槽组件

安装 element-ui npm install element-ui --save ---force main.js 导入 import Vue from vue; import ElementUI from element-ui; import element-ui/lib/theme-chalk/index.css; import App from ./App.vue; Vue.use(ElementUI); new Vue({ el: #app, render: h > h(Ap…

全渠道AI数字化商品管理 零售品牌增长“超级引擎”

随着“流量红利”时代的终结 品牌面临增速放缓、利润下滑的双重挑战。 消费者的诉求日益理性和个性化&#xff0c; 国内外品牌角逐市场份额 A1、大数据等先进技术迅猛发展 品牌商品计划管理变得更加复杂而多维。 零售品牌正加速数字化与全渠道融合以应对挑战。 可持续盈利…

对于一家企业来说,电气数字化是否有那么重要?

时代大背景下&#xff0c;尤其是在复杂的国际与社会环境交织之中&#xff0c;全社会的“数字化”转型已成必然之势。对于电子产业而言&#xff0c;“数字化”无疑是重大机遇。 众所周知&#xff0c;在蒸汽机时代&#xff0c;身为机械工程师堪称幸运&#xff0c;彼时涌现出众多…

MySQL 将查询结果导出到文件(select … into Statement)

我们经常会遇到需要将SQL查询结果导出到文件&#xff0c;以便后续的传输或数据分析的场景。为了满足这个需求&#xff0c;MySQL的select语句提供了into子句可以将的查询结果直接导出到文本文件。本文就MySQL中select…into的用法进行演示。 文章目录 一、select…into语句简介…

AWS账号注册:AWS 用借记卡注册是否有风险?

亚马逊云服务&#xff08;Amazon Web Services&#xff0c;简称 AWS&#xff09;作为全球领先的云服务提供商&#xff0c;吸引了众多企业和个人用户。注册 AWS 账户时&#xff0c;提供支付方式是必要的步骤&#xff0c;许多用户会选择使用借记卡来完成注册。那么&#xff0c;使…

idea、webstorm、navicat等2024大佬总结亲测可用

宝藏网址&#xff0c;亲测可用。 关于JetBrains全家桶激活。 扫码关注&#xff1a;JAVA和人工智能。回复 idea 或 webStorm 或 navicat 获取 仅学习使用&#xff0c;不要用于商业用途&#xff01;

【剑指offer】

剑指offer 面试题67&#xff1a;字符串转成整数面试题1&#xff1a;赋值运算符函数面试题3&#xff1a;数组中重复的数字 面试题67&#xff1a;字符串转成整数 LeedCode&#xff1a;LCR 192. 把字符串转换成整数 (atoi) 测试atoi的功能和异常效果 #include <iostream> #…

二叉树的介绍及其顺序结构的实现

Hello, 亲爱的小伙伴们&#xff0c;你们的作者菌又回来了&#xff0c;之前我们学习了链表、顺序表、栈等常见的数据结构&#xff0c;今天我们将紧跟之前的脚步&#xff0c;继续学习二叉树。 好&#xff0c;咱们废话不多说&#xff0c;开始我们今天的正题。 1.树 1.1树的概念和…

vue3框架Arco Design输入邮箱选择后缀

使用&#xff1a; <a-form-item field"apply_user_email" label"邮箱&#xff1a;" ><email v-model"apply_user_email" class"inputborder topinputw"></email> </a-form-item>import email from /componen…

Java语言程序设计基础篇_编程练习题***15.35/15.34 (动画:自回避随机漫步)

***15.34 (模拟&#xff1a;自回避随机漫步) 在一个网格中的自回避漫步是指从一个点到另一点的过程中&#xff0c;不重复两次访问一个点。自回避漫步已经广泛应用在物理、化学和数学学科中。它们可以用来模拟像溶剂和聚合物这样的链状物。编写一个程序&#xff0c;显示一个从中…

Educational Codeforces Round 168 (Rated for Div. 2)

据说这场比赛非常简单&#xff0c;但本蒟蒻却认为比以往还要难(;༎ຶД༎ຶ) A.Strong Password 输入样例&#xff1a; 4 a aaa abb password输出样例&#xff1a; wa aada abcb pastsword思路&#xff1a; 我们只需在原来字符串中连续的两个字符之间插入一个不同的字符&…

React 学习——自定义Hook实现,使用规则

使用规则&#xff1a; 只能在组件中或者其他自定义Hook函数中调用只能在组件的顶层调用&#xff0c;不能嵌套在 if、for、其他函数中 import { useState } from "react"// 封装函数 function useToggle(){const [show,setShow] useState(true);const toggle ()&…

机器学习算法——常规算法,在同的业务场景也需要使用不同的算法(二)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

火山引擎VeDI数据技术分享:两个步骤,为Parquet降本提效

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 作者&#xff1a;王恩策、徐庆 火山引擎 LAS 团队 火山引擎数智平台 VeDI 是火山引擎推出的新一代企业数据智能平台&#xff0c;基于字节跳动数据平台多年的“数据…

迪文屏使用记录

项目中要使用到迪文屏&#xff0c;奈何该屏资料太琐碎&#xff0c;找的人头皮发麻&#xff0c;遂进行相关整理。 屏幕&#xff1a;2.4寸电容屏 型号&#xff1a;DWG32240C024_03WTC 软件&#xff1a;DGUS_V7.647 1.竖屏横显 打开软件左下方的配置文件生成工具&#…

前端如何实现更换项目主题色的功能?

1、场景 有一个换主题色的功能&#xff0c;如下图&#xff1a; 切换颜色后&#xff0c;将对页面所有部分的色值进行重新设置&#xff0c;符合最新的主题色。 2、实现思路 因为色值比较灵活&#xff0c;可以任意选取&#xff0c;所以最好的实现方式是&#xff0c;根据设置的…