時(shí)間:2023-05-23 08:21:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2023-05-23 08:21:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
為了實(shí)踐微前端,重構(gòu)了自己的導(dǎo)航網(wǎng)站:筆者早期開(kāi)發(fā)了一個(gè)導(dǎo)航網(wǎng)站,一直想要重構(gòu),因?yàn)閼型狭撕脦啄?,終于,在了解到微前端大法后下了決心,因?yàn)楣ぷ魃弦恢睕](méi)有機(jī)會(huì)實(shí)踐,沒(méi)辦法,只能用自己的網(wǎng)站試試,思來(lái)想去,訪問(wèn)量最高的也就是這個(gè)破導(dǎo)航網(wǎng)站了,于是用最快的時(shí)間完成了基本功能的重構(gòu),然后準(zhǔn)備通過(guò)微前端來(lái)擴(kuò)展網(wǎng)站的功能,比如天氣、待辦、筆記、秒表計(jì)時(shí)等等,這些功能屬于附加的功能,可能會(huì)越來(lái)越多,所以不能和導(dǎo)航本身強(qiáng)耦合在一起,需要做到能獨(dú)立開(kāi)發(fā),獨(dú)立上線,所以使用微前端再合適不過(guò)了。Vue
單文件來(lái)開(kāi)發(fā),然后頁(yè)面上動(dòng)態(tài)的進(jìn)行加載渲染,所以會(huì)在微前端方式之外再嘗試一下動(dòng)態(tài)組件。本文內(nèi)的項(xiàng)目都使用Vue CLI創(chuàng)建,Vue使用的是3.x版本,路由使用的都是hash模式
小程序
,首先要實(shí)現(xiàn)的是一個(gè)小程序的注冊(cè)功能,詳細(xì)來(lái)說(shuō)就是:qiankun
:npm i qiankun -S
主應(yīng)用需要做的很簡(jiǎn)單,注冊(cè)微應(yīng)用并啟動(dòng),然后提供一個(gè)容器給微應(yīng)用掛載,最后打開(kāi)指定的url
即可。qiankun.js
文件:// qiankun.jsimport { registerMicroApps, start } from 'qiankun'import api from '@/api';// 注冊(cè)及啟動(dòng)const registerAndStart = (appList) => { // 注冊(cè)微應(yīng)用 registerMicroApps(appList) // 啟動(dòng) qiankun start()}// 判斷是否激活微應(yīng)用const getActiveRule = (hash) => (location) => location.hash.startsWith(hash);// 初始化小程序export const initMicroApp = async () => { try { // 請(qǐng)求小程序列表數(shù)據(jù) let { data } = await api.getAppletList() // 過(guò)濾出微應(yīng)用 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) }; }) // 注冊(cè)并啟動(dòng)微應(yīng)用 registerAndStart(appList) } catch (e) { console.log(e); }}
一個(gè)微應(yīng)用的數(shù)據(jù)示例如下:{ container: '#appletContainer', name: '后閣樓', entry: 'http://lxqnsys.com/applets/hougelou/', activeRule: getActiveRule('#/index/applet/hougelou')}
可以看到提供給微應(yīng)用掛載的容器為#appletContainer
,微應(yīng)用的訪問(wèn)url
為http://lxqnsys.com/applets/hougelou/
,注意最后面的/
不可省略,否則微應(yīng)用的資源路徑可能會(huì)出現(xiàn)錯(cuò)誤。activeRule
,導(dǎo)航網(wǎng)站的url
為:http://lxqnsys.com/d/#/index
,微應(yīng)用的路由規(guī)則為:applet/:appletId
,所以一個(gè)微應(yīng)用的激活規(guī)則為頁(yè)面url
的hash
部分,但是這里activeRule
沒(méi)有直接使用字符串的方式:#/index/applet/hougelou
,這是因?yàn)楣P者的導(dǎo)航網(wǎng)站并沒(méi)有部署在根路徑,而是在/d
目錄下,所以#/index/applet/hougelou
這個(gè)規(guī)則是匹配不到http://lxqnsys.com/d/#/index/applet/hougelou
這個(gè)url
的,需要這樣才行:/d/#/index/applet/hougelou
,但是部署的路徑有可能會(huì)變,不方便直接寫(xiě)到微應(yīng)用的activeRule
里,所以這里使用函數(shù)的方式,自行判斷是否匹配,也就是根據(jù)頁(yè)面的location.hash
是否是以activeRule
開(kāi)頭的來(lái)判斷,是的話代表匹配到了。src
目錄新增一個(gè)public-path.js
:// public-path.jsif (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}
然后修改main.js
,增加qiankun
的生命周期函數(shù):// main.jsimport './public-path';import { createApp } from 'vue'import App from './App.vue'import router from './router'let app = nullconst render = (props = {}) => { // 微應(yīng)用使用方式時(shí)掛載的元素需要在容器的范圍下查找 const { container } = props; app = createApp(App) app.use(router) app.mount(container ? container.querySelector('#app') : '#app')}// 獨(dú)立運(yùn)行時(shí)直接初始化if (!window.__POWERED_BY_QIANKUN__) { render();}// 三個(gè)生命周期函數(shù)export async function bootstrap() { console.log('[后閣樓] 啟動(dòng)');}export async function mount(props) { console.log('[后閣樓] 掛載'); render(props);}export async function unmount() { console.log('[后閣樓] 卸載'); app.unmount(); app = null;}
接下來(lái)修改打包配置vue.config.js
:module.exports = { // ... configureWebpack: { devServer: { // 主應(yīng)用需要請(qǐng)求微應(yīng)用的資源,所以需要允許跨域訪問(wèn) headers: { 'Access-Control-Allow-Origin': '*' } }, output: { // 打包為umd格式 library: `hougelou`, libraryTarget: 'umd' } }}
最后,還需要修改一下路由配置,有兩種方式: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
這種方式的缺點(diǎn)也是把主應(yīng)用的部署路徑寫(xiě)死在base
里,不是很優(yōu)雅。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
在微前端環(huán)境下把路由都作為/index/applet/hougelou/
的子路由。<div class="backBtn" v-if="isMicroApp" @click="back"> <span class="iconfont icon-fanhui"></span></div>
const back = () => { router.go(-1);};
這樣當(dāng)小程序?yàn)槲?yīng)用時(shí)會(huì)顯示一個(gè)返回按鈕,但是有一個(gè)問(wèn)題,當(dāng)在微應(yīng)用的首頁(yè)時(shí)顯然是不需要這個(gè)返回按鈕的,我們可以通過(guò)判斷當(dāng)前的路由和微應(yīng)用的activeRule
是否一致,一樣的話就代表是在微應(yīng)用首頁(yè),那么就不顯示返回按鈕:<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
和滾動(dòng)位置關(guān)聯(lián)并記錄起來(lái),在router.beforeEach
時(shí)獲取當(dāng)前的滾動(dòng)位置,然后和當(dāng)前的url
關(guān)聯(lián)起來(lái)并存儲(chǔ),當(dāng)router.afterEach
時(shí)根據(jù)當(dāng)前url
獲取存儲(chǔ)的數(shù)據(jù)并恢復(fù)滾動(dòng)位置:const scrollTopCache = {};let scrollTop = 0;// 監(jiān)聽(tīng)容器滾動(dòng)位置appletContainer.value.addEventListener("scroll", () => { scrollTop = appletContainer.value.scrollTop;});router.beforeEach(() => { // 緩存滾動(dòng)位置 scrollTopCache[route.fullPath] = scrollTop;});router.afterEach(() => { if (!isMicroApp.value) { return; } // ... // 恢復(fù)滾動(dòng)位置 appletContainer.value.scrollTop = scrollTopCache[route.fullPath];});
qiankun
會(huì)去加載對(duì)應(yīng)的微應(yīng)用,然而可能這時(shí)頁(yè)面上連微應(yīng)用的容器都沒(méi)有,所以會(huì)報(bào)錯(cuò),解決這個(gè)問(wèn)題可以在頁(yè)面加載后判斷初始路由是否是小程序的路由,是的話就恢復(fù)一下,然后再去注冊(cè)微應(yīng)用:if (///index//applet///.test(route.fullPath)) { router.replace("/index");}initMicroApp();
Vue
組件的方式,筆者的想法是直接使用Vue
單文件來(lái)開(kāi)發(fā),開(kāi)發(fā)完成后打包成一個(gè)js
文件,然后在導(dǎo)航網(wǎng)站上請(qǐng)求該js
文件,并把它作為動(dòng)態(tài)組件渲染出來(lái)。stopwatch
測(cè)試組件,目前目錄結(jié)構(gòu)如下:App.vue
內(nèi)容如下:<template> <div class="countContainer"> <div class="count">{{ count }}</div> <button @click="start">開(kāi)始</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
用來(lái)導(dǎo)出組件:import App from './App.vue';export default App// 配置數(shù)據(jù)const config = { width: 450}export { config}
為了個(gè)性化,還支持導(dǎo)出它的配置數(shù)據(jù)。vue-cli
,vue-cli
支持指定不同的構(gòu)建目標(biāo),默認(rèn)為應(yīng)用模式,我們平常項(xiàng)目打包運(yùn)行的npm run build
,其實(shí)運(yùn)行的就是vue-cli-service build
命令,可以通過(guò)選項(xiàng)來(lái)修改打包行為:vue-cli-service build --target lib --dest dist_applets/stopwatch --name stopwatch --entry src/applets/stopwatch/index.js
上面這個(gè)配置就可以打包我們的stopwatch
組件,選項(xiàng)含義如下:--target app | lib | wc | wc-async (默認(rèn)為app應(yīng)用模式,我們使用lib作為庫(kù)打包模式)--dest 指定輸出目錄 (默認(rèn)輸出到dist目錄,我們改成dist_applets目錄下)--name 庫(kù)或 Web Components 模式下的名字 (默認(rèn)值:package.json 中的 "name" 字段或入口文件名,我們改成組件名稱)--entry 指定打包的入口,可以是.js或.vue文件(也就是組件的index.js路徑)
更詳細(xì)的信息可以移步官方文檔:構(gòu)建目標(biāo)、CLI 服務(wù)。/applets/
目錄下新增build.js
:// build.jsconst { 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()// 創(chuàng)建打包任務(wù)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" }}
運(yùn)行命令npm run buildApplets
,可以看到打包結(jié)果如下:css
文件和umd
類(lèi)型的js
文件,打開(kāi).umd.js
文件看看:factory
函數(shù)執(zhí)行返回的結(jié)果就是組件index.js
里面導(dǎo)出的數(shù)據(jù),另外可以看到引入vue
的代碼,這表明Vue
是沒(méi)有包含在打包后的文件里的,這是vue-cli
刻意為之的,這在通過(guò)構(gòu)建工具使用打包后的庫(kù)來(lái)說(shuō)是很方便的,但是我們是需要直接在頁(yè)面運(yùn)行的時(shí)候動(dòng)態(tài)的引入組件,不經(jīng)過(guò)打包工具的處理,所以exports
、module
、define
、require
等對(duì)象或方法都是沒(méi)有的,沒(méi)有沒(méi)關(guān)系,我們可以手動(dòng)注入,我們使用第二個(gè)else if
,也就是我們需要手動(dòng)來(lái)提供exports
對(duì)象和require
函數(shù)。Vue
組件類(lèi)型的小程序時(shí)我們使用axios
來(lái)請(qǐng)求組件的js
文件,獲取到的是js
字符串,然后使用new Function
來(lái)執(zhí)行js
,注入我們提供的exports
對(duì)象和require
函數(shù),然后就可以通過(guò)exports
對(duì)象獲取到組件導(dǎo)出的數(shù)據(jù),最后再使用動(dòng)態(tài)組件渲染出組件即可,同時(shí)如果存在樣式文件的話也要?jiǎng)討B(tài)加載樣式文件。<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) } // 請(qǐng)求組件js資源 let { data } = await axios.get(payload.value.url); // 執(zhí)行組件js let run = new Function('exports', 'require', `return ${data}`) // 手動(dòng)提供exports對(duì)象和require函數(shù) const exports = {} const require = () => { return Vue; } // 執(zhí)行函數(shù) run(exports, require) // 獲取組件選項(xiàng)對(duì)象,扔給動(dòng)態(tài)組件進(jìn)行渲染 comp.value = exports.stopwatch.default } catch (error) { console.error(error); }};
執(zhí)行完組件的js
后我們注入的exports
對(duì)象如下:exports.stopwatch.default
就能獲取到組件的選項(xiàng)對(duì)象傳遞給動(dòng)態(tài)組件進(jìn)行渲染,效果如下:exports.stopwatch.default
獲取組件導(dǎo)出內(nèi)容我們還需要知道組件的打包名稱stopwatch
,這顯然有點(diǎn)麻煩,我們可以改成一個(gè)固定的名稱,比如就叫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
參數(shù)由之前的${name}
改成寫(xiě)死comp
即可,打包結(jié)果如下:exports
對(duì)象結(jié)構(gòu)變成如下:comp
名稱來(lái)應(yīng)對(duì)任何組件了comp.value = exports.comp.default
。關(guān)鍵詞:導(dǎo)航,實(shí)踐
客戶&案例
營(yíng)銷(xiāo)資訊
關(guān)于我們
客戶&案例
營(yíng)銷(xiāo)資訊
關(guān)于我們
微信公眾號(hào)
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。