为了实践微前端,重构了自己的导航网站

简介: 为了实践微前端,重构了自己的导航网站

笔者早期开发了一个导航网站,一直想要重构,因为懒拖了好几年,终于,在了解到微前端大法后下了决心,因为工作上一直没有机会实践,没办法,只能用自己的网站试试,思来想去,访问量最高的也就是这个破导航网站了,于是用最快的时间完成了基本功能的重构,然后准备通过微前端来扩展网站的功能,比如天气、待办、笔记、秒表计时等等,这些功能属于附加的功能,可能会越来越多,所以不能和导航本身强耦合在一起,需要做到能独立开发,独立上线,所以使用微前端再合适不过了。


另外,因为有些功能可能非常简单,比如秒表计时,单独创建一个项目显得没有必要,但是又不想直接写在导航的代码里,最好是能直接通过Vue单文件来开发,然后页面上动态的进行加载渲染,所以会在微前端方式之外再尝试一下动态组件。


本文内的项目都使用Vue CLI创建,Vue使用的是3.x版本,路由使用的都是hash模式


小程序注册


为了显得高大上一点,扩展功能我把它称为小程序,首先要实现的是一个小程序的注册功能,详细来说就是:


1.提供一个表单,输入小程序名称、描述、图标、url、类型(微前端方式还需要配置激活规则,组件方式需要配置样式文件的url),如下:


image.png


2.导航页面上显示注册的小程序列表,点击后渲染对应的小程序:


image.png


微前端方式


先来看看微前端的实现方式,笔者选择的是qiankun框架。


主应用


主应用也就是导航网站,首先安装qiankun


npm i qiankun -S


主应用需要做的很简单,注册微应用并启动,然后提供一个容器给微应用挂载,最后打开指定的url即可。


因为微应用列表都存储在数据库里,所以需要先获取然后进行注册,创建qiankun.js文件:


// qiankun.js
import { registerMicroApps, start } from 'qiankun'
import api from '@/api';
// 注册及启动
const registerAndStart = (appList) => {
  // 注册微应用
  registerMicroApps(appList)
  // 启动 qiankun
  start()
}
// 判断是否激活微应用
const getActiveRule = (hash) => (location) => location.hash.startsWith(hash);
// 初始化小程序
export const initMicroApp = async () => {
  try {
    // 请求小程序列表数据
    let { data } = await api.getAppletList()
    // 过滤出微应用
    let appList = data.data.filter((item) => {
      return item.type === 'microApp';
    }).map((item) => {
      return {
        container: '#appletContainer',
        name: item.name,
        entry: item.url,
        activeRule: getActiveRule(item.activeRule)
      };
    })
    // 注册并启动微应用
    registerAndStart(appList)
  } catch (e) {
    console.log(e);
  }
}


一个微应用的数据示例如下:


{
  container: '#appletContainer',
  name: '后阁楼',
  entry: 'http://lxqnsys.com/applets/hougelou/',
  activeRule: getActiveRule('#/index/applet/hougelou')
}


可以看到提供给微应用挂载的容器为#appletContainer,微应用的访问urlhttp://lxqnsys.com/applets/hougelou/,注意最后面的/不可省略,否则微应用的资源路径可能会出现错误。


另外解释一下激活规则activeRule,导航网站的url为:http://lxqnsys.com/d/#/index,微应用的路由规则为:applet/:appletId,所以一个微应用的激活规则为页面urlhash部分,但是这里activeRule没有直接使用字符串的方式:#/index/applet/hougelou,这是因为笔者的导航网站并没有部署在根路径,而是在/d目录下,所以#/index/applet/hougelou这个规则是匹配不到http://lxqnsys.com/d/#/index/applet/hougelou这个url的,需要这样才行:/d/#/index/applet/hougelou,但是部署的路径有可能会变,不方便直接写到微应用的activeRule里,所以这里使用函数的方式,自行判断是否匹配,也就是根据页面的location.hash是否是以activeRule开头的来判断,是的话代表匹配到了。

微应用

微应用也就是我们的小程序项目,根据官方文档的介绍Vue 微应用,首先需要在src目录新增一个public-path.js


// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}


然后修改main.js,增加qiankun的生命周期函数:


// main.js
import './public-path';
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
let app = null
const render = (props = {}) => {
    // 微应用使用方式时挂载的元素需要在容器的范围下查找
    const { container } = props;
    app = createApp(App)
    app.use(router)
    app.mount(container ? container.querySelector('#app') : '#app')
}
// 独立运行时直接初始化
if (!window.__POWERED_BY_QIANKUN__) {
    render();
}
// 三个生命周期函数
export async function bootstrap() {
    console.log('[后阁楼] 启动');
}
export async function mount(props) {
    console.log('[后阁楼] 挂载');
    render(props);
}
export async function unmount() {
    console.log('[后阁楼] 卸载');
    app.unmount();
    app = null;
}


接下来修改打包配置vue.config.js


module.exports = {
    // ...
    configureWebpack: {
        devServer: {
            // 主应用需要请求微应用的资源,所以需要允许跨域访问
            headers: {
                'Access-Control-Allow-Origin': '*'
            }
        },
        output: {
            // 打包为umd格式
            library: `hougelou`,
            libraryTarget: 'umd'
        }
    }
}


最后,还需要修改一下路由配置,有两种方式:


1.设置base


import { createRouter, createWebHashHistory } from 'vue-router';
let routes = routes = [
    { path: '/', name: 'List', component: List },
    { path: '/detail/:id', name: 'Detail', component: Detail },
]
const router = createRouter({
    history: createWebHashHistory(window.__POWERED_BY_QIANKUN__ ? '/d/#/index/applet/hougelou/' : '/'),
    routes
})
export default router


这种方式的缺点也是把主应用的部署路径写死在base里,不是很优雅。


2.使用子路由


import { createRouter, createWebHashHistory } from 'vue-router';
import List from '@/pages/List';
import Detail from '@/pages/Detail';
import Home from '@/pages/Home';
let routes = []
if (window.__POWERED_BY_QIANKUN__) {
    routes = [{
        path: '/index/applet/hougelou/',
        name: 'Home',
        component: Home,
        children: [
            { path: '', name: 'List', component: List },
            { path: 'detail/:id', name: 'Detail', component: Detail },
        ],
    }]
} else {
    routes = [
        { path: '/', name: 'List', component: List },
        { path: '/detail/:id', name: 'Detail', component: Detail },
    ]
}
const router = createRouter({
    history: createWebHashHistory(),
    routes
})
export default router


在微前端环境下把路由都作为/index/applet/hougelou/的子路由。


效果如下:


image.png


优化


1.返回按钮


如上面的效果所示,微应用内部页面跳转后,如果要回到上一个页面只能通过浏览器的返回按钮,显然不是很方便,可以在标题栏上添加一个返回按钮:


<div class="backBtn" v-if="isMicroApp" @click="back">
  <span class="iconfont icon-fanhui"></span>
</div>


const back = () => {
  router.go(-1);
};


这样当小程序为微应用时会显示一个返回按钮,但是有一个问题,当在微应用的首页时显然是不需要这个返回按钮的,我们可以通过判断当前的路由和微应用的activeRule是否一致,一样的话就代表是在微应用首页,那么就不显示返回按钮:


<div class="backBtn" v-if="isMicroApp && isInHome" @click="back">
  <span class="iconfont icon-fanhui"></span>
</div>


router.afterEach(() => {
  if (!isMicroApp.value) {
    return;
  }
  let reg = new RegExp("^#" + route.fullPath + "?$");
  isInHome.value = reg.test(payload.value.activeRule);
});


image.png


2.微应用页面切换时滚动位置恢复


如上面的动图所示,当从列表页进入到详情页再返回列表时,列表回到了顶部,这样的体验是很糟糕的,我们需要记住滚动的位置并恢复。


可以通过把url和滚动位置关联并记录起来,在router.beforeEach时获取当前的滚动位置,然后和当前的url关联起来并存储,当router.afterEach时根据当前url获取存储的数据并恢复滚动位置:


const scrollTopCache = {};
let scrollTop = 0;
// 监听容器滚动位置
appletContainer.value.addEventListener("scroll", () => {
  scrollTop = appletContainer.value.scrollTop;
});
router.beforeEach(() => {
  // 缓存滚动位置
  scrollTopCache[route.fullPath] = scrollTop;
});
router.afterEach(() => {
  if (!isMicroApp.value) {
    return;
  }
  // ...
  // 恢复滚动位置
  appletContainer.value.scrollTop = scrollTopCache[route.fullPath];
});


image.png


3.初始url为小程序url的问题


正常在关闭小程序时会把页面的路由恢复至页面原本的路由,但是比如我在打开小程序的情况下直接刷新页面,那么因为url满足小程序的激活规则,所以qiankun会去加载对应的微应用,然而可能这时页面上连微应用的容器都没有,所以会报错,解决这个问题可以在页面加载后判断初始路由是否是小程序的路由,是的话就恢复一下,然后再去注册微应用:


if (/\/index\/applet\//.test(route.fullPath)) {
  router.replace("/index");
}
initMicroApp();


Vue组件方式


接下来看看使用Vue组件的方式,笔者的想法是直接使用Vue单文件来开发,开发完成后打包成一个js文件,然后在导航网站上请求该js文件,并把它作为动态组件渲染出来。


简单起见我们直接在导航项目下新建一个文件夹作为小程序的目录,这样可以直接使用项目的打包工具,新增一个stopwatch测试组件,目前目录结构如下:


image.png


组件App.vue内容如下:


<template>
  <div class="countContainer">
    <div class="count">{{ count }}</div>
    <button @click="start">开始</button>
  </div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
const start = () => {
  setInterval(() => {
    count.value++;
  }, 1000);
};
</script>
<style lang="less" scoped>
.countContainer {
  text-align: center;
  .count {
    color: red;
  }
}
</style>


index.js用来导出组件:


import App from './App.vue';
export default App
// 配置数据
const config = {
    width: 450
}
export {
    config
}


为了个性化,还支持导出它的配置数据。


接下来需要对组件进行打包,我们直接使用vue-clivue-cli支持指定不同的构建目标,默认为应用模式,我们平常项目打包运行的npm run build,其实运行的就是vue-cli-service build命令,可以通过选项来修改打包行为:


vue-cli-service build --target lib --dest dist_applets/stopwatch --name stopwatch --entry src/applets/stopwatch/index.js


上面这个配置就可以打包我们的stopwatch组件,选项含义如下:


--target      app | lib | wc | wc-async (默认为app应用模式,我们使用lib作为库打包模式)
--dest        指定输出目录 (默认输出到dist目录,我们改成dist_applets目录下)
--name        库或 Web Components 模式下的名字 (默认值:package.json 中的 "name" 字段或入口文件名,我们改成组件名称)
--entry       指定打包的入口,可以是.js或.vue文件(也就是组件的index.js路径)


更详细的信息可以移步官方文档:构建目标CLI 服务


但是我们的组件是不定的,数量可能会越来越多,所以直接在命令行输入命令打包会非常的麻烦,我们可以通过脚本来完成,在/applets/目录下新增build.js


// build.js
const { exec } = require('child_process');
const path = require('path')
const fs = require('fs')
// 获取组件列表
const getComps = () => {
    let res = []
    let files = fs.readdirSync(__dirname)
    files.forEach((filename) => {
        // 是否是目录
        let dir = path.join(__dirname, filename)
        let isDir = fs.statSync(dir).isDirectory
        // 入口文件是否存在
        let entryFile = path.join(dir, 'index.js')
        let entryExist = fs.existsSync(entryFile)
        if (isDir && entryExist) {
            res.push(filename)
        }
    })
    return res
}
let compList = getComps()
// 创建打包任务
let taskList = compList.map((comp) => {
    return new Promise((resolve, reject) => {
        exec(`vue-cli-service build --target lib --dest dist_applets/${comp} --name ${comp} --entry src/applets/${comp}/index.js`, (error, stdout, stderr) => {
            if (error) {
                reject(error)
            } else {
                resolve()
            }
        })
    });
})
Promise.all(taskList)
    .then(() => {
        console.log('打包成功');
    })
    .catch((e) => {
        console.error('打包失败');
        console.error(e);
    })


然后去package.json新增如下命令:


{
  "scripts": {
    "buildApplets": "node ./src/applets/build.js"
  }
}


运行命令npm run buildApplets,可以看到打包结果如下:


image.png

我们使用其中css文件和umd类型的js文件,打开.umd.js文件看看:


image.png



factory函数执行返回的结果就是组件index.js里面导出的数据,另外可以看到引入vue的代码,这表明Vue是没有包含在打包后的文件里的,这是vue-cli刻意为之的,这在通过构建工具使用打包后的库来说是很方便的,但是我们是需要直接在页面运行的时候动态的引入组件,不经过打包工具的处理,所以exportsmoduledefinerequire等对象或方法都是没有的,没有没关系,我们可以手动注入,我们使用第二个else if,也就是我们需要手动来提供exports对象和require函数。


当我们点击Vue组件类型的小程序时我们使用axios来请求组件的js文件,获取到的是js字符串,然后使用new Function来执行js,注入我们提供的exports对象和require函数,然后就可以通过exports对象获取到组件导出的数据,最后再使用动态组件渲染出组件即可,同时如果存在样式文件的话也要动态加载样式文件。


<template>
  <component v-if="comp" :is="comp"></component>
</template>


import * as Vue from 'vue';
const comp = ref(null);
const load = async () => {
    try {
      // 加载样式文件
      if (payload.value.styleUrl) {
        loadStyle(payload.value.styleUrl)
      }
      // 请求组件js资源
      let { data } = await axios.get(payload.value.url);
      // 执行组件js
      let run = new Function('exports', 'require', `return ${data}`)
      // 手动提供exports对象和require函数
      const exports = {}
      const require = () => {
        return Vue;
      }
      // 执行函数
      run(exports, require)
      // 获取组件选项对象,扔给动态组件进行渲染
      comp.value = exports.stopwatch.default
    } catch (error) {
      console.error(error);
    }
};


执行完组件的js后我们注入的exports对象如下:


image.png

所以通过exports.stopwatch.default就能获取到组件的选项对象传递给动态组件进行渲染,效果如下:


image.png


大功告成,最后我们再稍微修改一下,因为通过exports.stopwatch.default获取组件导出内容我们还需要知道组件的打包名称stopwatch,这显然有点麻烦,我们可以改成一个固定的名称,比如就叫comp,修改打包命令:


// build.js
// ...
exec(`vue-cli-service build --target lib --dest dist_applets/${comp} --name comp --entry src/applets/${comp}/index.js`, (error, stdout, stderr) => {
  if (error) {
    reject(error)
  } else {
    resolve()
  }
})
// ...


--name参数由之前的${name}改成写死comp即可,打包结果如下:


image.png


exports对象结构变成如下:


image.png


然后我们就可以通过comp名称来应对任何组件了comp.value = exports.comp.default


当然,小程序关闭的时候不要忘记删除添加的样式节点。


总结


本文简单了尝试两种网站功能的扩展方式,各位如果有更好的方式的话可以评论留言分享,线上效果演示地址lxqnsys.com/d/



相关文章
|
2月前
|
缓存 前端开发 JavaScript
优化前端性能:从理论到实践的全面指南
前端性能优化是提升用户体验的关键环节,但这一过程常被技术细节和优化策略所困扰。本文将系统地探讨前端性能优化的理论基础及实践技巧,包括关键性能指标、有效的优化策略、以及常见工具的应用。我们将从最基本的优化方法入手,逐步深入到高级技巧,为开发者提供一套全面的性能提升方案,以实现更快的加载时间、更流畅的用户交互体验。
|
15天前
|
缓存 前端开发 JavaScript
优化前端性能:关键策略与实践
随着互联网技术的发展,用户对网页加载速度和交互体验的要求日益提高,前端性能优化成为提升用户体验和网站竞争力的关键。本文探讨了前端性能优化的重要性和七大关键策略,包括压缩资源文件、利用浏览器缓存、减少HTTP请求、异步加载、使用CDN、优化CSS和JavaScript执行及第三方脚本优化,并提供了实践案例,帮助开发者构建更快、更高效的网站。
|
15天前
|
移动开发 缓存 前端开发
构建高效的前端路由系统:从原理到实践
在现代Web开发中,前端路由系统已成为构建单页面应用(SPA)不可或缺的核心技术之一。不同于传统服务器渲染的多页面应用,SPA通过前端路由技术实现了页面的局部刷新与无缝导航,极大地提升了用户体验。本文将深入剖析前端路由的工作原理,包括Hash模式与History模式的实现差异,并通过实战演示如何在Vue.js框架中构建一个高效、可维护的前端路由系统。我们还将探讨如何优化路由加载性能,确保应用在不同网络环境下的流畅运行。本文不仅适合前端开发者深入了解前端路由的奥秘,也为后端转前端或初学者提供了从零到一的实战指南。
|
5天前
|
缓存 前端开发 JavaScript
优化前端性能:关键策略与实践
在现代web开发中,前端性能优化至关重要。本文探讨了提升用户体验、转化率及降低服务器负载的关键策略,包括压缩资源文件、利用浏览器缓存、减少HTTP请求、异步加载、使用CDN、优化CSS/JavaScript执行、优化第三方脚本等,并介绍了Webpack/Rollup模块打包、HTTP/2特性、性能预算及Lighthouse/WebPageTest测试工具的应用。通过这些方法,可显著提高网站性能。
|
1月前
|
缓存 监控 前端开发
前端性能优化实战:让你的网站快如闪电的十大秘籍
【9月更文挑战第3天】通过以上十大秘籍的实践,您可以显著提升网站的前端性能,让您的网站在竞争激烈的互联网环境中脱颖而出,为用户带来更加流畅和愉悦的体验。记住,前端性能优化是一个永无止境的过程,只有不断迭代和优化,才能保持网站的竞争力。
|
2月前
|
API Java 数据库连接
从平凡到卓越:Hibernate Criteria API 让你的数据库查询瞬间高大上,彻底告别复杂SQL!
【8月更文挑战第31天】构建复杂查询是数据库应用开发中的常见需求。Hibernate 的 Criteria API 以其强大和灵活的特点,允许开发者以面向对象的方式构建查询逻辑,同时具备 SQL 的表达力。本文将介绍 Criteria API 的基本用法并通过示例展示其实际应用。此 API 通过 API 构建查询条件而非直接编写查询语句,提高了代码的可读性和安全性。无论是简单的条件过滤还是复杂的分页和连接查询,Criteria API 均能胜任,有助于提升开发效率和应用的健壮性。
67 0
|
2月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
23 0
|
2月前
|
开发者 Java
JSF EL 表达式:乘技术潮流之风,筑简洁开发之梦,触动开发者心弦的强大语言
【8月更文挑战第31天】JavaServer Faces (JSF) 的表达式语言 (EL) 是一种强大的工具,允许开发者在 JSF 页面和后台 bean 间进行简洁高效的数据绑定。本文介绍了 JSF EL 的基本概念及使用技巧,包括访问 bean 属性和方法、数据绑定、内置对象使用、条件判断和循环等,并分享了最佳实践建议,帮助提升开发效率和代码质量。
28 0
|
2月前
|
前端开发 大数据 数据库
🔥大数据洪流下的决战:JSF 表格组件如何做到毫秒级响应?揭秘背后的性能魔法!💪
【8月更文挑战第31天】在 Web 应用中,表格组件常用于展示和操作数据,但在大数据量下性能会成瓶颈。本文介绍在 JavaServer Faces(JSF)中优化表格组件的方法,包括数据处理、分页及懒加载等技术。通过后端分页或懒加载按需加载数据,减少不必要的数据加载和优化数据库查询,并利用缓存机制减少数据库访问次数,从而提高表格组件的响应速度和整体性能。掌握这些最佳实践对开发高性能 JSF 应用至关重要。
45 0
|
2月前
|
前端开发 API 开发者
【前端数据革命】React与GraphQL协同工作:从理论到实践全面解析现代前端数据获取的新范式,开启高效开发之旅!
【8月更文挑战第31天】本文通过具体代码示例,介绍了如何利用 GraphQL 和 React 搭建高效的前端数据获取系统。GraphQL 作为一种新型数据查询语言,能精准获取所需数据、提供强大的类型系统、统一的 API 入口及实时数据订阅功能,有效解决了 RESTful API 在复杂前端应用中遇到的问题。通过集成 Apollo Client,React 应用能轻松实现数据查询与实时更新,大幅提升性能与用户体验。文章详细讲解了从安装配置到查询订阅的全过程,并分享了实践心得,适合各层次前端开发者学习参考。
30 0
下一篇
无影云桌面