笔者早期开发了一个导航网站,一直想要重构,因为懒拖了好几年,终于,在了解到微前端大法后下了决心,因为工作上一直没有机会实践,没办法,只能用自己的网站试试,思来想去,访问量最高的也就是这个破导航网站了,于是用最快的时间完成了基本功能的重构,然后准备通过微前端来扩展网站的功能,比如天气、待办、笔记、秒表计时等等,这些功能属于附加的功能,可能会越来越多,所以不能和导航本身强耦合在一起,需要做到能独立开发,独立上线,所以使用微前端再合适不过了。
另外,因为有些功能可能非常简单,比如秒表计时,单独创建一个项目显得没有必要,但是又不想直接写在导航的代码里,最好是能直接通过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
,微应用的访问url
为http://lxqnsys.com/applets/hougelou/
,注意最后面的/
不可省略,否则微应用的资源路径可能会出现错误。
另外解释一下激活规则activeRule
,导航网站的url
为:http://lxqnsys.com/d/#/index
,微应用的路由规则为:applet/:appletId
,所以一个微应用的激活规则为页面url
的hash
部分,但是这里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/
的子路由。
效果如下:
如上面的效果所示,微应用内部页面跳转后,如果要回到上一个页面只能通过浏览器的返回按钮,显然不是很方便,可以在标题栏上添加一个返回按钮:
<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);
});
如上面的动图所示,当从列表页进入到详情页再返回列表时,列表回到了顶部,这样的体验是很糟糕的,我们需要记住滚动的位置并恢复。
可以通过把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];
});
正常在关闭小程序时会把页面的路由恢复至页面原本的路由,但是比如我在打开小程序的情况下直接刷新页面,那么因为url
满足小程序的激活规则,所以qiankun
会去加载对应的微应用,然而可能这时页面上连微应用的容器都没有,所以会报错,解决这个问题可以在页面加载后判断初始路由是否是小程序的路由,是的话就恢复一下,然后再去注册微应用:
if (/\/index\/applet\//.test(route.fullPath)) {
router.replace("/index");
}
initMicroApp();
接下来看看使用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-cli
,vue-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
刻意为之的,这在通过构建工具使用打包后的库来说是很方便的,但是我们是需要直接在页面运行的时候动态的引入组件,不经过打包工具的处理,所以exports
、module
、define
、require
等对象或方法都是没有的,没有没关系,我们可以手动注入,我们使用第二个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
。
当然,小程序关闭的时候不要忘记删除添加的样式节点。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/pyWoSsPdP1V5CeDdcENfBA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。