详解:使用 vuepress 实现项目文档(扩展篇)

1,112

前言

本篇文章是对上篇文章 详解:使用 vuepress 实现项目文档 的扩展,主要介绍 VuePress 主题的编辑、移动端组件库文档类型的左侧菜单与右侧 iframe 组件示例页面交互的实现。

一、 效果预览

注:以下效果图是从我的微信小程序组件库 Mind UI WeApp 文档中截取。

1. 首页样式

首页内容与样式重写,并隐藏顶部导航栏。
文档首页

2. 菜单联动

左侧菜单样式优化,右侧增加预览功能。
nav.gif

二、VuePress 默认主题

1. 默认主题介绍

在实现自定义首页与增加 iframe 页面前,我们先了解下 VuePress主题功能
所谓主题,我们可以简单理解为一套皮肤,比如掘金文章编辑器中的 markdown 可以选择多种主题,其主要是通过模板语法与样式的搭配,将同样的内容以不同的形式展现出来。
网上有许多小伙伴开发了不同的三方主题,可以直接安装使用。本篇文章侧重点不在如何开发一套主题,而是如何修改默认主题,以适配自己的需求。

2. 默认主题结构

我们在项目的 /.vuepress 文件夹下,新建 /theme 文件夹,接下来找到 npm 包中默认主题,路径为 node_modules\vuepress\node_modules\@vuepress\theme-default,将文件夹中 /node_modulespackage.json 以外的文件复制到 /theme 文件夹下,其目录结构为:

├─ theme
│  ├─ components          // 主题相关组件
│  │  ├─ FooterBar.vue    // 页底,比如写入版权信息
│  │  ├─ Home.vue         // 首页 (接下来会重写的页面)
│  │  ├─ Navbar.vue       // 顶部菜单栏
│  │  ├─ Page.vue         // 文档页面 (在此组件中增加 `iframe` 页面,并实现消息发送与接收)
│  │  ├─ Sidebar.vue      // 侧边栏导航菜单 (会调整其样式)
│  │  └─ ...              // 其它组件
│  ├─ global-components   // 会被自动注册为全局组件,在 `.md` 中可使用
│  │  └─ Badge.vue        // 角标组件
│  ├─ layouts
│  │  ├─ 404.vue          // 404页面
│  │  └─ Layout.vue       // 布局组件
│  ├─ styles              // 样式文件
│  │  ├─ index.styl       // 全局样式
│  │  ├─ mobile.styl      // 移动端样式
│  │  ├─ markdown.styl    // markdown 主题
│  │  └─ ...              // 其它样式文件
│  ├─ utils
│  │  └─ index.js         // 默认工具函数
│  ├─ index.js            // 主题文件的入口文件
│  └─ noopModule.js
└─ ...

三、页面优化

1. 文档首页

我们打开 .vuepress/theme/components/Home.vue 文件,该文件对应的就是文档首页的结构与样式,我们删除 <template> 中的内容,根据自己的需求,重写结构与样式。
注意点:

  • 模板中使用 /public 中的图片,需要放在 $withBase 方法中,该方法对应的路径为 config.jsbase 属性的值,该方法支持 Vue 变量。

    <img :src="$withBase('/logo.png')" />
    
  • 为了支持移动端的展示,需要对首页的样式通过媒体查询做好对应的适配:

    @media (max-width: $MQMobile) {
      // 移动端样式
    }
    

    $MQMobile 为框架提供的移动端宽度变量。

  • 隐藏首页顶部导航栏
    我们打开 /layouts/Layout.vue 文件,其中的 <Navbar> 组件即为顶部导航栏,我们找到计算属性中的 shouldShowNavbar 属性,其中的 frontmatter 存放着当前页面的一些属性信息,我们用其中的 home 属性来判断是否为首页,代码如下:

    // 43行,是否显示左侧导航栏判断,在判断条件中增加 frontmatter.home 条件,如果是首页,则返回 false
    shouldShowNavbar() {
      const { themeConfig } = this.$site;
      const { frontmatter } = this.$page;
      if (
        frontmatter.navbar === false ||
        themeConfig.navbar === false ||
        frontmatter.home
      ) {
        return false;
      }
      // ...
    },
    

2. 左侧菜单

左侧菜单对应文件为 /theme/components/ 下的 Sidebar.vueSidebarXxx.vue 文件,我们可以根据需求改写相关文件结构,或者重写其样式。

3. 右侧内容

文档内容区域对应组件为 /theme/components/Page.vue 文件,我们增加一个 iframe 内容区域:

<template>
  <div class="wrapper">
    <main class="page">
      <slot name="top" />

      <Content class="theme-default-content" />
      <PageEdit />

      <PageNav v-bind="{ sidebarItems }" />

      <slot name="bottom" />
    </main>
    
    <!-- iframe 内容区域 -->
    <div class="preview">
      <iframe
        class="iframe"
        width="100%"
        height="100%"
        :src="iframeUrl"
        ref="iframe"
      ></iframe>
    </div>
  </div>
</template>

对应的 Page.vue 组件的 class="page" 样式需要调整,以给右侧留出展示 iframe 的空间。主要涉及 /theme/styles 文件夹下的 index.stylmobile.styl 文件。

// index.styl 中增加的部分样式
.page
  padding-left 18rem

// mobile.styl 中增加的部分样式
@media (max-width: $MQMobile)
  .page
    padding-right 0  // 移动端文档为全屏,右侧边距置为0
  .preview
    display none     // 移动端时隐藏 iframe 预览

新增的 preview 标签样式如下:

.preview
  z-index 1
  position fixed
  right 15px
  top: 70px
  min-width 340px
  height calc(100vh - 90px)  // 高度设置为去除顶部导航栏与底部留白高度
  border-radius 6px
  box-shadow 0 0 6px #ccc
  background-color #fff

以上只是部分样式的代码,主要是项目的样式做了多种媒体查询适配,我们需要对其一一处理。

四、文档联动

1. iframe 通信

通过 window.postMessage('message', e.origin);window.addEveantListener('message',(event)=>{}) 函数来发送与监听消息,他们是支持跨域传递消息的,其更多的介绍与使用方式可在 mdn 上查看,在此不做赘述。

2. 左右联动实现

左侧菜单与右侧内容联动,主要分为以下两种情况:

  1. 文档菜单控制 iframe 页面路由
    思路: 点击文档菜单后,watch 监听文档的路由变化,将页面跳转后的路由信息发送给 iframe 页面, iframe 页面接收后,根据发送来的信息,跳转到对应的页面。

    • 文档页面发送消息代码如下:
    // ./vuepress/theme/components/Page.vue 组件
    export default {
      // ... 其它代码
      watch: {
        // 监听路由变化
        $route: "routerChange",
      },
      methods: {
        routerChange(route) {
          // 由于未找到设置当前页面路由名称的方法,所以以截取路由中 .html 名称作为当前路由名称 
          let paths = route.path.split("/");
          let lastPath = paths[paths.length - 1];
          // pathUrl 为接收页面需要跳转的地址
          let pathUrl = lastPath.replace(/.html/, "");
    
          // 设置传空值给接受页的白名单
          const ignorePathList = ['', 'install', 'started'];
          if(ignorePathList.includes(pathUrl)) {
            pathUrl = ''
          }
    
          // 获取 iframe 实例,通过该实例去发送消息
          this.iframeWindow = this.$refs.iframe.contentWindow;
          // 将路径名称发送给 iframe 页面
          this.iframeWindow.postMessage(
            {
              url: pathUrl
            },
            "*"
          );
        }
      }
    }
    
    • iframe 接收消息代码:
    // App.vue
    export default {
      mounted() {
        // 当前网页被放入 iframe 时才执行此监听
        if (window.parent !== window.self) {
          window.addEventListener("message", this.handleMessage);
        }
      },
      methods: {
        handleMessage(ev) {
          if (ev.data) {
            let url = ev.data.url;
            // 当前页面与跳转页面路由一致时不跳转
            const currentPath = this.$route.path;
            if (currentPath === "/" + url) return;
    
            if (pathList.includes(url)) {
              this.$router.push(url);
            } else {
              if (currentPath === "/") return;
              this.$router.push("/");
            }
          }
        }
      }
    }
    

    由于我的文档网站菜单与示例的网站路由是能对应上的,所以只做了简单的地址对应。

  2. iframe 页面控制文档页面
    与上面方式一样,只是改由 iframe 页面向文档页面发送消息,文档页面接收消息后做对应路由跳转,差异处会在代码中注明。
    以下为 iframe 页面部分代码:

    // App.vue
    export default {
      watch: {
        // 监听路由变化
        $route: "routerChange",
      },
    
      methods: {
        routerChange(route) {
          // 如果当前页面不是在 iframe 内,则不执行以下操作
          if (window.parent === window.self) return;
    
          if (!route.path) return;
          // 注意此处为 window.parent 指向父级页面
          window.parent.postMessage(
            {
              url: route.path,
            },
            "*"
          );
        }
      }
    }
    

    以下为文档页面的 Page.vue 消息接收页面部分代码:

    // ./vuepress/theme/components/Page.vue
    export default {
      mounted() {
        // 全局监听消息
        window.addEventListener("message", this.handleMessage);
      },
      methods: {
        handleMessage(ev) {
          // 根据传入的消息内容,拼接处理出需要跳转的文档地址。
          let url = ev.data.url;
          // ... 处理代码略
          let path = `/component/${prefix}/${url}.html`;
    
          // 如果当前页面地址与传入地址一致,则不跳转
          if (path === this.$route.path) return;
          this.$router.push(path);
        }
      }
    }
    

结束语

以上即为我在使用 VuePress 的过程中感觉有用些的知识,希望能给需要人带来帮助。