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

news2024/11/25 22:45:30

引言

大家如果使用过移动端组件库(比如: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://www.coloradmin.cn/o/1962537.html

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

相关文章

基于python的百度迁徙迁入、迁出数据分析(四)

这篇文章是对上篇文章的可获取数据的时间区间的修正&#xff0c;依然通过开发者模式找寻相关数据源&#xff0c;我直接把数据url贴在这里&#xff0c;可以发现里面包含了相对明面上看不到的数据包括&#xff0c;行政区id、春运迁徙数据等&#xff1a;qianxi.cdn.bcebos.com/app…

LYT-Net——轻量级网络低光照条件图像修复模型推理部署(C++/Python)

1.环境安装 conda create -n LYT_Torch python3.9 -y conda activate LYT_Torchconda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidiapip install matplotlib scikit-learn scikit-image opencv-python yacs joblib natsort h5py tqdm tensor…

Conformal low power-7.运行Conformal Low Power 1801流程

要获取有关原生1801流程的最新信息&#xff08;例如功能、指南、常见问题解答和dofile脚本&#xff09;&#xff0c;请使用SET WEB_INTERFACE ON命令启动Web界面。 左手边列出了所有的Conformal产品。还有一个名为“Sample Dofiles”的部分&#xff0c;提供了在不同场景下运行C…

第一个Python Web程序

1、离线安装Django 由于Python是3.7版本,Django选择2.2.4版本,并且中间需要安装依赖包。全部安装包如下: 打开Anaconda Prompt,先进入Python3.7环境,然后依次安装各个包: 至此,Django离线安装成功。 2、编写第一个Django程序 2.1 创建Django项目 创建项目welcome时…

C#初级——继承

继承 继承是面向对象程序设计中最重要的概念之一。继承允许我们根据一个类来定义另一个类&#xff0c;不需要完全重新编写新的数据成员和成员函数&#xff0c;只需要设计一个新的类&#xff0c;继承了已有的类的成员即可。这个已有的类被称为的基类&#xff08;父类&#xff0…

(自适应手机端)行业协会机构网站模板

(自适应手机端)行业协会机构网站模板PbootCMS内核开发的网站模板&#xff0c;该模板适用于行业协会网站等企业&#xff0c;当然其他行业也可以做&#xff0c;只需要把文字图片换成其他行业的即可&#xff1b;自适应手机端&#xff0c;同一个后台&#xff0c;数据即时同步&#…

git的配置使用

第三周 Tursday 早 git日志的安装使用 [rootweb ~]# yum -y install git.x86_64 //安装软件包 [rootweb ~]# rpm -ql git //查看git的包 ​ [rootweb ~]# mkdir /yy000 //创建新目录 [rootweb ~]# cd /yy000/ [rootweb yy000]# git init //将当前目录做为仓库…

基于SpringBoot+Vue的娱乐代理售票系统(带1w+文档)

基于SpringBootVue的娱乐代理售票系统(带1w文档) 基于SpringBootVue的娱乐代理售票系统(带1w文档) 休闲娱乐代理售票系统的开发运用java技术&#xff0c;MIS的总体思想&#xff0c;以及MYSQL等技术的支持下共同完成了该系统的开发&#xff0c;实现了休闲娱乐代理售票管理的信息…

Why decoder-only? LLM架构的演化之路

如何根据大模型架构进行选型 ©作者|Zhongmei 来源|神州问学 引言 本文想为大型语言模型&#xff08;LLMs&#xff09;及其下游自然语言处理&#xff08;NLP&#xff09;任务的实践者和用户提供一份全面且实用的指南。将从模型架构的角度出发&#xff0c;对比不同架构的…

多线程上下文切换:详解与优化

多线程上下文切换&#xff1a;详解与优化 一、什么是多线程上下文切换&#xff1f;二、对性能的影响2.1 优点2.2 缺点 三、优化策略 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 一、什么是多线程上下文切换&#xff1f; 多线程上下文切…

在 Vim 编辑器中,如果某个单词被意外地高亮显示,使用:noh可以取消高亮显示

文章目录 1、问题出现的背景2、解决办法 1、问题出现的背景 配置镜像加速器&#xff0c;修改 /etc/docker/daemon.json 目录下的文件&#xff0c;不小心高亮显示https&#xff0c;产生问题的步骤是&#xff0c;我先是按esc键退出vim的编辑模式&#xff0c;然后在https的前面按…

Android经典面试题之Kotlin中 if 和 let的区别

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 在Kotlin中&#xff0c;if和let虽然有时候用来处理相似的情景&#xff0c;但它们实际上是用于不同的场景并具有不同的性质。下面我们来详细对比…

K-近邻和神经网络

K-近邻&#xff08;K-NN, K-Nearest Neighbors&#xff09; 原理 K-近邻&#xff08;K-NN&#xff09;是一种非参数分类和回归算法。K-NN 的主要思想是根据距离度量&#xff08;如欧氏距离&#xff09;找到训练数据集中与待预测样本最近的 K 个样本&#xff0c;并根据这 K 个…

安科瑞ASJ系列智能剩余电流继电器介绍

产品概述&#xff1a; 安科瑞ASJ系列智能剩余电流继电器是一种重要的电气安全保护设备&#xff0c;‌主要用于交流50Hz、‌额定电压400V及以下的TT和TN系统配电线路中。‌该系列继电器的主要功能包括对电气线路进行接地故障保护&#xff0c;‌以防止接地故障电流引起的设备损坏…

C语言家教记录(一)

C语言家教记录&#xff08;一&#xff09; 导语C语言简介特点优点缺点 Codeblocks安装和使用简单程序结构变量&#xff08;常量&#xff09;和赋值类型声明常量赋值标识符 基本运算输入输出printf基本格式转义序列 scanf转换说明 示例程序总结和复习 导语 本次授课内容如下&am…

一句JS代码,实现随机颜色的生成

今天我们只用 一句JS代码&#xff0c;实现随机颜色的生成&#xff0c;首先看一下效果&#xff1a; 每次刷新浏览器背景颜色都不一样 实现此效果的JS函数 &#xff1a; let randomColor () > ...: 定义一个箭头函数randomColor&#xff0c;用于生成一个随机颜色。 Math.ra…

苹果发布iPhone AI,Apple Intelligence初版落地!未融入ChatGPT,仅面向付费开发者

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 苹果公司Apple Intelligence初版落地&#xff1a;iPhone AI引领智能化新篇章 在全球科技领域&#xff0c;苹果公司一直以其创新精神和前沿技术…

java算法day27

java算法day27 动态规划初步总结509 斐波那契数杨辉三角打家劫舍完全平方数 动态规划初步总结 如果你感觉某个问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 动态规划的过程就是每一个状态一定是由上一个状态推导出来的&#xff0c;这一点就区分于贪心了。贪心是…

热力图大揭秘!Matplotlib教你如何画出让数据‘火辣辣‘的激情图!

1. 引言 嘿&#xff0c;小伙伴们&#xff01;今天咱们来点不一样的&#xff0c;走进Matplotlib的神奇世界&#xff0c;一起绘制那让人热血沸腾的热力图&#xff01;别误会&#xff0c;这可不是什么天气预报图&#xff0c;而是让数据“火辣辣”展现自我的秘密武器。想象一下&am…