乐趣区

关于javascript:为了实践微前端重构了自己的导航网站

笔者晚期开发了一个导航网站,始终想要重构,因为懒拖了好几年,终于,在理解到微前端大法后下了信心,因为工作上始终没有机会实际,没方法,只能用本人的网站试试,思来想去,访问量最高的也就是这个破导航网站了,于是用最快的工夫实现了基本功能的重构,而后筹备通过微前端来扩大网站的性能,比方天气、待办、笔记、秒表计时等等,这些性能属于附加的性能,可能会越来越多,所以不能和导航自身强耦合在一起,须要做到能独立开发,独立上线,所以应用微前端再适合不过了。

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

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

小程序注册

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

1. 提供一个表单,输出小程序名称、形容、图标、url、类型(微前端形式还须要配置激活规定,组件形式须要配置款式文件的 url),如下:

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

微前端形式

先来看看微前端的实现形式,笔者抉择的是 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/ 的子路由。

成果如下:

优化

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);
});

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];
});

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

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

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

Vue 组件形式

接下来看看应用 Vue 组件的形式,笔者的想法是间接应用 Vue 单文件来开发,开发实现后打包成一个 js 文件,而后在导航网站上申请该 js 文件,并把它作为动静组件渲染进去。

简略起见咱们间接在导航我的项目下新建一个文件夹作为小程序的目录,这样能够间接应用我的项目的打包工具,新增一个 stopwatch 测试组件,目前目录构造如下:

组件 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,能够看到打包后果如下:

咱们应用其中 css 文件和 umd 类型的 js 文件,关上 .umd.js 文件看看:

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 对象如下:

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

功败垂成,最初咱们再略微批改一下,因为通过 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 即可,打包后果如下:

exports对象构造变成如下:

而后咱们就能够通过 comp 名称来应答任何组件了comp.value = exports.comp.default

当然,小程序敞开的时候不要遗记删除增加的款式节点。

总结

本文简略了尝试两种网站性能的扩大形式,各位如果有更好的形式的话能够评论留言分享,线上成果演示地址 http://lxqnsys.com/d/。

退出移动版