載說明:原創(chuàng)不易,未經(jīng)授權(quán),謝絕任何形式的轉(zhuǎn)載
模態(tài)框(彈出層對(duì)話框)在大多數(shù)現(xiàn)代應(yīng)用程序中非常常見。它們主要用于呈現(xiàn)簡潔的信息,非常適合顯示廣告和促銷內(nèi)容。模態(tài)框提供了一種快速傳達(dá)信息的方式,并提供了用戶友好的關(guān)閉選項(xiàng)。
在本文中,我們將使用Vuejs構(gòu)建一個(gè)彈出模態(tài)框。該模態(tài)框?qū)ㄒ粋€(gè)取消或關(guān)閉按鈕,以方便用戶在完成任務(wù)后關(guān)閉它。此外,我們還將實(shí)現(xiàn)一個(gè)功能,允許用戶在模態(tài)框區(qū)域外點(diǎn)擊以關(guān)閉它。
<script setup lang="ts">
import { ref } from 'vue';
const message = ref('This is a modal popup');
const emit = defineEmits(['close']);
const closeModal = () => {
emit('close');
};
</script>
<template>
<div class="popup" @click.self="closeModal">
<div class="popup-content">
<div class="popup-header">
<h2 class="popup-title">{{ message }}</h2>
<button class="popup-close-button" @click.prevent="closeModal">X</button>
</div>
<article>
<div class="popup-content-text">
This is a simple modal popup in Vue.js
</div>
</article>
</div>
</div>
</template>
Script Section
<script setup lang="ts">
import { ref } from 'vue';
const message = ref('This is a modal popup');
const emit = defineEmits(['close']);
const closeModal = () => {
emit('close');
};
</script>
在這個(gè)部分,我們從Vue中導(dǎo)入所需的功能。
Template Section
<template>
<div class="popup" @click.self="closeModal">
<div class="popup-content">
<div class="popup-header">
<h2 class="popup-title">{{ message }}</h2>
<button class="popup-close-button" @click.prevent="closeModal">X</button>
</div>
<article>
<div class="popup-content-text">
This is a simple modal popup in Vue.js
</div>
</article>
</div>
</div>
</template>
本段代碼義了模板中模態(tài)框的結(jié)構(gòu)。
<script setup lang="ts">
import { ref } from 'vue'
import Popup from "@/components/Popup.vue"; // @ is an alias to /src
const msg = ref('Hello World!')
const isOpened = ref(false)
</script>
<template>
<div>
<h1>{{ msg }}</h1>
<button @click="isOpened = !isOpened">Open Popup</button>
<Teleport to="body">
<Popup v-if="isOpened" @close="isOpened = !isOpened" />
</Teleport>
</div>
</template>
數(shù)據(jù)和狀態(tài)管理:
代碼使用Vue的ref函數(shù)創(chuàng)建了兩個(gè)響應(yīng)式變量:
按鈕點(diǎn)擊事件
模板中有一個(gè)帶有點(diǎn)擊事件監(jiān)聽器(@click)的<button>元素。當(dāng)按鈕被點(diǎn)擊時(shí),它會(huì)切換isOpened變量的值,從而有效地打開或關(guān)閉彈出窗口。
導(dǎo)入彈出框組件
組件之間的通信:
您可以在CodeSandbox上使用本文中設(shè)計(jì)的代碼進(jìn)行操作。
https://codesandbox.io/s/suspicious-kepler-993dmh?file=%2Fsrc%2Fviews%2FHome.vue%3A0-420
由于文章內(nèi)容篇幅有限,今天的內(nèi)容就分享到這里,文章結(jié)尾,我想提醒您,文章的創(chuàng)作不易,如果您喜歡我的分享,請(qǐng)別忘了點(diǎn)贊和轉(zhuǎn)發(fā),讓更多有需要的人看到。同時(shí),如果您想獲取更多前端技術(shù)的知識(shí),歡迎關(guān)注我,您的支持將是我分享最大的動(dòng)力。我會(huì)持續(xù)輸出更多內(nèi)容,敬請(qǐng)期待。
作為 TOB 的業(yè)務(wù)方,我們偶爾會(huì)收到一些如下圖所示的反饋。
作為 PC 頁面為主的業(yè)務(wù)方,大多數(shù)用戶在一天的工作中,可能都不太會(huì)刷新或者重新打開我們的頁面,導(dǎo)致我們在下午或者白天發(fā)布的前端版本,往往需要到幾個(gè)小時(shí)甚至第二天,才能覆蓋到 98% 以上的用戶。
我們統(tǒng)計(jì)了 bscm 平臺(tái) 5 次下午 2-3 點(diǎn)左右發(fā)布的版本,在發(fā)布后每個(gè)時(shí)間段內(nèi)老版本用戶的占比情況。選擇這個(gè)時(shí)間點(diǎn)發(fā)布的原因是這個(gè)時(shí)間點(diǎn)基本是平臺(tái)用戶的上班時(shí)間,是最有可能出現(xiàn)用戶已經(jīng)打開了頁面同時(shí)我們在發(fā)布新代碼的場景的,比較具有代表性。按平臺(tái)用戶六七點(diǎn)下班來看,我們可以看到還有將近 6% 的用戶在當(dāng)天是會(huì)一直訪問老版本的前端代碼的,按照 bscm 平臺(tái) 1w+的 uv 來看,約有 600 多人會(huì)可能遇到前端版本過低導(dǎo)致的使用問題。
首先介紹兩個(gè)概念,本地版本號(hào)和云端版本號(hào)。本地版本號(hào)是用戶請(qǐng)求到的前端頁面的代碼版本號(hào),是用戶訪問頁面時(shí)決定;云端版本號(hào)可以理解為最新前端版本號(hào),它是每次開發(fā)者發(fā)布前端代碼時(shí)決定的。
有了彈窗的觸發(fā)條件,我們還需要去決定什么時(shí)候判斷彈窗是否滿足觸發(fā)的條件,上面也提到了,出現(xiàn)這類問題的場景多見于用戶在使用過程中,開發(fā)者進(jìn)行了前端代碼發(fā)布,那我們主要可以有兩個(gè)類型的時(shí)機(jī)去進(jìn)行觸發(fā)條件的判斷。
我們對(duì)這些時(shí)機(jī)在更新是否及時(shí),判斷次數(shù)多少、實(shí)現(xiàn)成本高低等維度進(jìn)行一個(gè)對(duì)比。
?? 越多表示這個(gè)維度得分越高
根據(jù)表格可以看到 websocket 消息推送和前端事件監(jiān)聽這兩種方案綜合來看是更合適一些的,但是前端事件監(jiān)聽其實(shí)它的劣勢在實(shí)際運(yùn)用場景中會(huì)被弱化(一天的上線數(shù)量有限,請(qǐng)求次數(shù)一天不會(huì)多太多次),但是實(shí)現(xiàn)成本遠(yuǎn)低于 websocket,所以無疑是實(shí)際落地場景中比較理想的選擇。
根據(jù) can i use 的結(jié)果我們也可以發(fā)現(xiàn) visibilitychange 事件也基本符合我們目前 B 端頁面對(duì)于 PC 瀏覽器的要求。
本地版本號(hào)
本地版本號(hào)是用戶訪問時(shí)決定的,那無疑頁面的 html 文件就是這個(gè)版本號(hào)存在的最佳載體,我們可以在打包時(shí)通過 plugin 給 html 文件注入一個(gè)版本號(hào)。
云端版本號(hào)
云端版本號(hào)的選擇則有很多方式了,數(shù)據(jù)庫、cdn 等等都可以滿足需求。不過考慮到實(shí)現(xiàn)成本和泳道的情況,想了一下兩個(gè)思路一個(gè)是打包的同時(shí)生成一個(gè) version.json 文件,配一個(gè)路由去訪問;另一個(gè)是直接訪問對(duì)應(yīng)的 html 代碼,解析出注入的版本號(hào),二者各自有適合的場景。
我們現(xiàn)在的大多數(shù)項(xiàng)目都包含了主應(yīng)用和子應(yīng)用,那其實(shí)不管是子應(yīng)用的更新還是主應(yīng)用的更新都應(yīng)該有相關(guān)的提示,而且相互獨(dú)立,但同時(shí)又需要保證彈窗不同時(shí)出現(xiàn)。
想要沿用之前的方案其實(shí)只需要解決三個(gè)問題。
正如前文提到的,本身版本發(fā)布不是一個(gè)高頻事件,但是監(jiān)聽事件的頻次有時(shí)候可能過高了,不希望頻繁的去進(jìn)行觸發(fā)條件判斷。同時(shí)如果出現(xiàn)一天內(nèi)多次發(fā)布的場景,也不希望這個(gè)彈窗對(duì)于用戶有過多的打擾,所以需要去添加一個(gè)頻控邏輯。
plugin
/* eslint-disable */
import { CoraWebpackPlugin, WebpackCompiler } from '@ies/eden-web-build';
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
interface IVersion {
name?: string; // 編譯完的文件夾名稱
subName?: string; // 子應(yīng)用的名稱,主應(yīng)用可以不傳
}
export class VersionPlugin implements CoraWebpackPlugin {
readonly name = 'versionPlugin'; // 插件必須要有一個(gè)名字,這個(gè)名字不能和已有插件沖突
private _version: number;
private _name: string;
private _subName: string;
constructor(params: IVersion) {
this._version = new Date().getTime();
this._name = params?.name || 'build';
this._subName = params?.subName || ''
}
apply(compiler: WebpackCompiler): void {
compiler.hooks.afterCompile.tap('versionPlugin', () => {
try {
const filePath = path.resolve(`./${this._name}/template/version.json`);
fs.writeFile(filePath, JSON.stringify({ version: this._version }), (err: any) => {
if (err) {
console.log('@@@err', err);
}
});
const htmlPath = path.resolve(`./${this._name}/template/index.html`);
const data = fs.readFileSync(htmlPath);
const $ = cheerio.load(data);
$('body').append(`<div id="${this._subName}versionTag" style="display: none">${this._version}</div>`);
fs.writeFile(htmlPath, $.html(), (err: any) => {
if (err) {
console.log('@@@htmlerr', err);
}
});
} catch (err) {
console.log(err);
}
});
}
}
彈窗組件
import React, { useEffect } from 'react';
import { Modal } from '@ecom/auxo';
import axios from 'axios';
import moment from 'moment';
export interface IProps {
isSub?: boolean; // 是否為子應(yīng)用
subName?: string; // 子應(yīng)用名稱
resourceUrl?: string; // 子應(yīng)用的資源url
}
export type IType = 'visibilitychange' | 'popstate' | 'init';
export default React.memo<IProps>(props => {
const { isSub = false, subName = '', resourceUrl = '' } = props || {};
const cb = (latestVersion: number | undefined, currentVersion: number | undefined, type: IType) => {
try {
// 版本落后,提示可以刷新頁面
if (latestVersion && currentVersion && latestVersion > currentVersion) {
// 提醒過了就設(shè)置一個(gè)更新提示過期時(shí)間,一天內(nèi)不需要再提示了,彈窗過期時(shí)間暫時(shí)全局只需要一個(gè)!!
localStorage.setItem(`versionUpdateExpireTime`, moment().endOf('day').format('x'));
if (!document.getElementById('versionModalTitle')) {
Modal.confirm({
title: <div id="versionModalTitle">版本更新提示</div>,
content:
'您已經(jīng)長時(shí)間未使用此頁面,在此期間平臺(tái)有過更新,如您此時(shí)在頁面中沒有填寫相關(guān)信息等操作,請(qǐng)點(diǎn)擊刷新頁面使用最新版本!',
okText: <div data-text={`前端版本升級(jí)引導(dǎo)-立即更新 ${type}`}>刷新頁面</div>,
cancelText: <div data-text={`前端版本升級(jí)引導(dǎo)-我知道了 ${type}`}>我知道了</div>,
onCancel: () => {
console.log('fe-version-watcher INFO: 未更新~');
},
onOk: () => {
location.reload();
},
});
}
}
// 不管版本是否落后,半小時(shí)內(nèi)都不需要去重新請(qǐng)求判斷
localStorage.setItem(`versionInfoExpireTime`, String(new Date().getTime() + 1000 * 60 * 30));
} catch {}
};
const formatVersion = (text?: string) => (text ? Number(text) : undefined);
useEffect(() => {
try {
const fn = function (type: IType) {
if (document.visibilityState === 'visible') {
/**
* @desc 為了防止打擾,版本更新每個(gè)應(yīng)用一天只提示一次 所以過期時(shí)間設(shè)為當(dāng)天23:59:59,沒過期則直接return
*/
if (Number(localStorage.getItem(`versionUpdateExpireTime`) || 0) >= new Date().getTime()) {
return;
}
/**
* @desc 不需要每次切換頁面都去判斷資源,每次從服務(wù)器獲取到的版本信息,給半個(gè)小時(shí)的緩存時(shí)間,需要區(qū)分子應(yīng)用
*/
if (Number(localStorage.getItem(`versionInfoExpireTime`) || 0) > new Date().getTime()) {
return;
}
if (!isSub) {
/**
* @desc 主應(yīng)用使用version.json文件來獲取最新的版本號(hào)
*/
const dom = document.getElementById('versionTag');
const currentVersion = formatVersion(dom?.innerText);
axios.get(`/version?timestamp=${new Date().getTime()}`).then(res => {
const latestVersion = res?.data?.version;
cb(latestVersion, currentVersion, type);
});
} else {
/**
* @desc 子應(yīng)用使用最新html中的innerText來獲取最新版本號(hào)
*/
if (resourceUrl) {
const dom = document.getElementById(`${subName}versionTag`);
const currentVersion = dom?.innerText ? Number(dom?.innerText) : undefined;
axios.get(resourceUrl).then(res => {
/** ignore_security_alert */
try {
const html = res.data;
const doc = new DOMParser().parseFromString(html, 'text/html');
const latestVersion = formatVersion(doc.getElementById(`${subName}versionTag`)?.innerText);
cb(latestVersion, currentVersion, type);
} catch {}
});
}
}
}
};
const visibleFn = () => {
fn('visibilitychange');
};
const routerFn = () => {
fn('popstate');
};
if (isSub) {
// 子應(yīng)用可能會(huì)有緩存,初始化的時(shí)候先判斷一次
fn('init');
}
document.addEventListener('visibilitychange', visibleFn);
window.addEventListener('popstate', routerFn);
return () => {
document.removeEventListener('visibilitychange', visibleFn);
window.removeEventListener('popstate', routerFn);
};
} catch {}
}, []);
return <div />;
});
npm i @ecom/fe-version-watcher-plugin # 安裝plugin
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安裝引導(dǎo)彈窗
import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';
// 有些項(xiàng)目打包后template文件夾下的名字不是build而是build_cn
// 可以根據(jù)自己項(xiàng)目的實(shí)際情況傳入{name: build_cn}
{
...,
plugins: [
...,
[VersionPlugin, {}],
]
}
import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';
<FeVersionWatcher />
采用 version.json 的方案,引入 FersionWatcher 組件就不再需要任何參數(shù),目前主應(yīng)用只支持這種模式。未來也將參考子應(yīng)用,主應(yīng)用支持讀取 html 中版本標(biāo)識(shí)的能力,將配置路由的工作改成組件 props 傳入資源 url,開發(fā)者可以根據(jù)實(shí)際情況自行選擇。
npm i @ecom/fe-version-watcher-plugin # 安裝plugin
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安裝引導(dǎo)彈窗
import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';
// 有些項(xiàng)目打包后template文件夾下的名字不是build而是build_cn
// 可以根據(jù)自己項(xiàng)目的實(shí)際情況傳入{name: build_cn}
{
...,
plugins: [
...,
[VersionPlugin, {subName: 'general-supplier', name: 'build_cn'}],
]
}
import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';
// subName需要和plugin的參數(shù)保持一致,resourceUrl為子應(yīng)用資源的路徑(子引用goofy上配置的路由)
<FeVersionWatcher isSub subName="general-supplier" resourceUrl="/webApp/general-supplier" />
resourceUrl一般就是goofy上配置的路由設(shè)置,,如果不同平臺(tái)有區(qū)分,可以動(dòng)態(tài)傳入。
發(fā)布成功后,可以根據(jù)如下步驟測試:
同樣我們截取了 4 次該平臺(tái) 2-3 點(diǎn)發(fā)布的版本情況,可以看到老版本用戶的 uv 占比有著明顯的下降。
上線至今共計(jì)提示 10 萬+用戶,幫助約 5 萬人次及時(shí)更新了前端代碼。
作者:費(fèi)昀鋒
來源:微信公眾號(hào):字節(jié)前端 ByteFE
出處:https://mp.weixin.qq.com/s/PT0PZ3S1Cvh2nltcIKwa3g
生javascript實(shí)現(xiàn)帶動(dòng)畫的提示型彈窗,常用于網(wǎng)站彈層的彈窗也有很多,一般用插件比較多,所以今天就來寫一寫該功能,如有錯(cuò)誤之處請(qǐng)指出!
彈出跟消失都有放大縮小動(dòng)畫在里面!
實(shí)現(xiàn)方法:
html:
可以自己輸入內(nèi)容,再點(diǎn)擊彈出即可看到彈窗效果
css:
javascript:
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。