我基本上遇到了这个问题,因为我还使用第三方脚本(firebase、stripe...),并且在运行任何这些脚本之前我需要用户的同意。
我围绕 Yett 构建我的解决方案(https://github.com/elbywan/yett),它会阻止属于先前定义的黑名单的脚本。你甚至可以自己实现这个功能,作者写了一个有趣的中等文章.
就我而言,我只有“基本”脚本,因此我构建了一个解决方案,其中仅当用户同意所有必要的脚本时,flutter 应用程序才会加载。但如果需要对用户的 cookie 设置进行更细粒度的控制,调整此解决方案应该不会太困难,并且我添加了第二个“分析”条目作为可能的起点。
我将用户的设置存储在 localStorage 中,并直接在应用程序启动时检索它们以创建黑名单并决定是否应显示 cookie 横幅。
这是我的index.html
.
它引用了以下脚本:get_consent.js
, set_consent.js
, init_firebase.js
and load_app.js
(有关它们的更多信息如下)。
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="flutter_utils">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Assigns blacklist of urls based on localStorage (must be placed before yett script) -->
<script src="get_consent.js"></script>
<!-- Yett is used to block all third-party scripts that are part of the blacklist (must be placed before all other (third-party) scripts) -->
<script src="https://unpkg.com/yett"></script>
<script src="https://js.stripe.com/v3/"></script>
<title>flutter_utils</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<!-- The standard consent popup (hidden by default) -->
<div id="consent-popup" class="hidden consent-div">
<h2>We use cookies and other technologies</h2>
<p>This website uses cookies and similar functions to process end device information and personal data. The processing serves the integration of content, external services and elements of third parties, statistical analysis/measurement, personalized advertising and the integration of social media. Depending on the function, data may be passed on to third parties within the EU in the process. Your consent is always voluntary, not required for the use of our website and can be rejected or revoked at any time via the icon at the bottom right.
</p>
<div>
<button id="accept-btn" class="btn inline">Accept</button>
<button id="reject-btn" class="btn inline">Reject</button>
<button id="info-btn" class="btn inline">More info</button>
</div>
</div>
<!-- Detailed consent popup allows the user to control scripts by their category -->
<div id="consent-popup-details" class="hidden consent-div">
<h2>Choose what to accept</h2>
<div>
<div class="row-div">
<h3>Essential</h3>
<label class="switch">
<!-- Essentials must always be checked -->
<input id="essential-cb" type="checkbox" checked disabled=true>
<span class="slider round"></span>
</label>
</div>
<p>
Here you can find all technically necessary scripts, cookies and other elements that are necessary for the operation of the website.
</p>
</div>
<div>
<div class="row-div">
<h3>Analytics</h3>
<label class="switch">
<input id ="analytics-cb" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<p>
For the site, visitors, web page views and diveerse other data are stored anonymously.
</p>
</div>
<div>
<button id="save-btn" class="btn inline">Save</button>
<button id="cancel-btn" class="btn inline">Cancel</button>
</div>
</div>
<!-- Updates localStorage with user's cookie settings -->
<script src="set_consent.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-firestore.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-storage.js"></script>
<!-- Initializes firebase (if user gave consent) -->
<script src="init_firebase.js"></script>
<!-- Loads flutter app (if user gave consent) -->
<script src="load_app.js"></script>
</body>
</html>
The get_consent.js
是第一个脚本,从 localStorage 检索用户的设置并定义 Yett 黑名单:
const essentialCookies = ["js.stripe.com", "www.gstatic.com"];
const analyticsCookies = ["www.google-analytics.com"];
const allCookies = [...essentialCookies, ...analyticsCookies];
const consentPropertyName = "cookie_consent";
const retrieveConsentSettings = () => {
const consentJsonString = localStorage.getItem(consentPropertyName);
return JSON.parse(consentJsonString);
};
const checkConsentIsMissing = () => {
const consentObj = retrieveConsentSettings();
if (!consentObj || consentObj.length == 0) {
return true;
}
return false;
};
const consentIsMissing = checkConsentIsMissing();
var blacklist;
if (consentIsMissing) {
blacklist = allCookies;
} else {
const acceptedCookies = retrieveConsentSettings();
// Remove all script urls from blacklist that the user accepts (if all are accepted the blacklist will be empty)
blacklist = allCookies.filter( ( el ) => !acceptedCookies.includes( el ) );
}
// Yett blacklist expects list of RegExp objects
var blacklistRegEx = [];
for (let index = 0; index < blacklist.length; index++) {
const regExp = new RegExp(blacklist[index]);
blacklistRegEx.push(regExp);
}
YETT_BLACKLIST = blacklistRegEx;
set_consent.js
负责使用用户的设置更新 localStorage 并隐藏/显示相应的 div 以获取 cookie 同意。通常,人们可以简单地调用window.yett.unblock()
解除阻止脚本,但由于它们的顺序很重要,我决定在更新 localStorage 后简单地重新加载窗口:
const saveToStorage = (acceptedCookies) => {
const jsonString = JSON.stringify(acceptedCookies);
localStorage.setItem(consentPropertyName, jsonString);
};
window.onload = () => {
const consentPopup = document.getElementById("consent-popup");
const consentPopupDetails = document.getElementById("consent-popup-details");
const acceptBtn = document.getElementById("accept-btn");
const moreInfoBtn = document.getElementById("info-btn");
const saveBtn = document.getElementById("save-btn");
const cancelBtn = document.getElementById("cancel-btn");
const rejectBtn = document.getElementById("reject-btn");
const acceptFn = (event) => {
const cookiesTmp = [...essentialCookies, ...analyticsCookies];
saveToStorage(cookiesTmp);
// Reload window after localStorage was updated.
// The blacklist will then only contain items the user has not yet consented to.
window.location.reload();
};
const cancelFn = (event) => {
consentPopup.classList.remove("hidden");
consentPopupDetails.classList.add("hidden");
};
const rejectFn = (event) => {
console.log("Rejected!");
// Possible To-Do: Show placeholder content if even essential scripts are rejected.
};
const showDetailsFn = () => {
consentPopup.classList.add("hidden");
consentPopupDetails.classList.remove("hidden");
};
const saveFn = (event) => {
const analyticsChecked = document.getElementById("analytics-cb").checked;
var cookiesTmp = [...essentialCookies];
if (analyticsChecked) {
cookiesTmp.push(...analyticsCookies);
}
saveToStorage(cookiesTmp);
// Reload window after localStorage was updated.
// The blacklist will then only contain items the user has not yet consented to.
window.location.reload();
};
acceptBtn.addEventListener("click", acceptFn);
moreInfoBtn.addEventListener("click", showDetailsFn);
saveBtn.addEventListener("click", saveFn);
cancelBtn.addEventListener("click", cancelFn);
rejectBtn.addEventListener("click", rejectFn);
if (consentIsMissing) {
consentPopup.classList.remove("hidden");
}
};
init_firebase.js
是用于初始化服务的常用脚本,但我仅在获得同意的情况下进行初始化:
var firebaseConfig = {
// your standard config
};
// Initialize Firebase only if user consented
if (!consentIsMissing) {
firebase.initializeApp(firebaseConfig);
}
相同的逻辑应用于脚本load_app.js
。仅当用户同意时才会加载 Flutter 应用程序。
因此,人们可能会在其中添加一些后备内容index.html
如果用户拒绝必要的脚本,则会显示该信息。根据您的使用案例,也可能是无论如何加载应用程序的选项,然后通过从 localStorage 访问用户的设置来在应用程序内进行区分。
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement("script");
scriptTag.src = "main.dart.js";
scriptTag.type = "application/javascript";
document.body.append(scriptTag);
}
// Load app only if user consented
if (!consentIsMissing) {
if ("serviceWorker" in navigator) {
// Service workers are supported. Use them.
window.addEventListener("load", function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl =
"flutter_service_worker.js?v=" + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener("statechange", () => {
if (serviceWorker.state == "activated") {
console.log("Installed new service worker.");
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing ?? reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log("New service worker available.");
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log("Loading app from service worker.");
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
"Failed to load app from service worker. Falling back to plain <script> tag."
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
}
这是我的style.css
:
html,
body {
height: 100%;
width: 100%;
background-color: #2d2d2d;
font-family: Arial, Helvetica, sans-serif;
}
.hidden {
display: none;
visibility: hidden;
}
.consent-div {
position: fixed;
bottom: 40px;
left: 10%;
right: 10%;
width: 80%;
padding: 14px 14px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background-color: #eee;
border-radius: 5px;
box-shadow: 0 0 5px 5px rgba(0, 0, 0, 0.2);
}
.row-div {
display: flex;
justify-content: space-between;
align-items: center;
}
#accept-btn,
#save-btn {
background-color: #103900;
}
#reject-btn,
#cancel-btn {
background-color: #ff0000;
}
.btn {
height: 25px;
width: 140px;
background-color: #777;
border: none;
color: white;
border-radius: 5px;
cursor: pointer;
}
.inline {
display: inline-block;
margin-right: 5px;
}
h2 {
margin-block-start: 0.5em;
margin-block-end: 0em;
}
h3 {
margin-block-start: 0.5em;
margin-block-end: 0em;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 25px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 4px;
bottom: 4px;
background-color: white;
}
input:checked + .slider {
background-color: #2196f3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
-webkit-transform: translateX(24px);
-ms-transform: translateX(24px);
transform: translateX(24px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}