抽象行為(behavior)模式#
圖片預覽#
我們的任務是這樣的,給一個固定列表中的圖片元素增加 “預覽” 功能。在線演示
對應的 HTML 頁面如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>圖片預覽</title>
<style>
#list {
list-style-type: none;
justify-content: flex-start;
display: flex;
flex-wrap: wrap;
}
#list li {
padding: 10px;
margin: 0;
}
#list img {
height: 200px;
cursor: pointer;
}
</style>
</head>
<body>
<ul id="list">
<li>
<img src="https://p4.ssl.qhimg.com/t01713d89cfdb45cdf5.jpg">
</li>
<li>
<img src="https://p4.ssl.qhimg.com/t01e456146c8f8a639a.jpg">
</li>
<li>
<img src="https://p1.ssl.qhimg.com/t015f613e2205b573d8.jpg">
</li>
<li>
<img src="https://p0.ssl.qhimg.com/t01290338a28018d404.jpg">
</li>
<li>
<img src="https://p3.ssl.qhimg.com/t01d9aa5ae469c8862e.jpg">
</li>
<li>
<img src="https://p3.ssl.qhimg.com/t01cb20d35fc4aa3c0d.jpg">
</li>
<li>
<img src="https://p5.ssl.qhimg.com/t0110b30256941b9611.jpg">
</li>
</ul>
</body>
</html>
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
const target = evt.target;
if(target.tagName === 'IMG') {
preview(list, target);
}
});
list.addEventListener('preview', ({detail}) => {
detail.showMask();
});
function useBehavior(context) {
const {type, getDetail} = context;
return function (subject, target) {
const event = new CustomEvent(type, {bubbles: true, detail: getDetail.call(context, subject, target)});
target.dispatchEvent(event);
};
}
const preview = useBehavior({
type: 'preview',
/*
@subject: <ul>元素
@target: 選中的圖片元素
*/
getDetail(subject, target) {
const imgs = Array.from(subject.querySelectorAll('img'));
const selected = imgs.indexOf(target); // 獲取選中圖片在圖片集合中的索引號
let mask = document.getElementById('mask');
// 如果mask不存在,創建一個mask元素
if(!mask) {
mask = document.createElement('div');
mask.id = 'mask';
mask.innerHTML = `
<a class="previous" href="###"><</a>
<img src="${imgs[selected].src}">
<a class="next" href="###">></a>
`;
// 給 #mask 元素設置樣式:
Object.assign(mask.style, {
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.8)',
display: 'none',
alignItems: 'center',
justifyContent: 'space-between',
});
// 給 #mask 元素左右兩邊的<a>元素設置樣式:
mask.querySelectorAll('a').forEach((a) => {
Object.assign(a.style, {
width: '30px',
textAlign: 'center',
fontSize: '2rem',
color: '#fff',
textDecoration: 'none',
});
});
document.body.appendChild(mask);
// 給#mask元素添加點擊事件處理函數:
let idx = selected;
mask.addEventListener('click', (evt) => {
const target = evt.target;
if(target === mask) { // 如果點擊的對象是mask元素,則隱藏mask元素
mask.style.display = 'none';
} else if(target.className === 'previous') { // 顯示上一張圖片
update(--idx);
} else if(target.className === 'next') { // 顯示下一張圖片
update(++idx);
}
});
}
// 設置img元素的src屬性指向指定圖片
function update(idx) {
const [previous, next] = [...mask.querySelectorAll('a')];
previous.style.visibility = idx ? 'visible' : 'hidden';
next.style.visibility = idx < imgs.length - 1 ? 'visible' : 'hidden';
const img = mask.querySelector('img');
img.src = imgs[idx].src;
}
return {
showMask() { // 顯示選中圖片的預覽
mask.style.display = 'flex';
update(selected);
},
};
},
});
圖片選擇器#
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>抽象行為</title>
<style>
#list {
list-style-type: none;
justify-content: flex-start;
display: flex;
flex-wrap: wrap;
}
#list li {
padding: 10px;
margin: 0;
}
#list img {
height: 200px;
cursor: pointer;
box-sizing: border-box;
padding: 5px;
}
#list img.selected {
border: solid 5px #37c;
padding: 0;
}
</style>
</head>
<body>
<ul id="list">
<li>
<img src="https://p4.ssl.qhimg.com/t01713d89cfdb45cdf5.jpg">
</li>
<li>
<img src="https://p4.ssl.qhimg.com/t01e456146c8f8a639a.jpg">
</li>
<li>
<img src="https://p1.ssl.qhimg.com/t015f613e2205b573d8.jpg">
</li>
<li>
<img src="https://p0.ssl.qhimg.com/t01290338a28018d404.jpg">
</li>
<li>
<img src="https://p3.ssl.qhimg.com/t01d9aa5ae469c8862e.jpg">
</li>
<li>
<img src="https://p3.ssl.qhimg.com/t01cb20d35fc4aa3c0d.jpg">
</li>
<li>
<img src="https://p5.ssl.qhimg.com/t0110b30256941b9611.jpg">
</li>
</ul>
</body>
</html>
function useBehavior(context) {
const {type, getDetail} = context;
return function(subject, target) {
const event = new CustomEvent(type, {bubbles: true, detail: getDetail.call(context, subject, target)});
target.dispatchEvent(event);
}
}
const select = useBehavior({
type: 'select',
data: {
picked: new Set(), // 選中的圖片集合
},
getDetail(subject, target) {
const picked = this.data.picked;
if(picked.has(target)) {
target.className = '';
picked.delete(target);
} else {
target.className = 'selected';
picked.add(target);
}
return {
changed: target,
picked,
};
},
});
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
const target = evt.target;
if(target.tagName === 'IMG') {
select(list, target);
}
});
list.addEventListener('select', ({detail}) => {
// do nothing
console.log(detail.changed, detail.picked);
});
通過第一個故事和第二個故事,我們可以把 “預覽” 和 “選擇” 這兩個行為組合起來。比如,當鼠標點擊的同時,我們按下 alt 鍵,就表示選擇圖片;否則就是預覽圖片:
/* ...省略其他代碼... */
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
const target = evt.target;
if(target.tagName === 'IMG') {
if(evt.altKey) {
select(list, target);
} else {
preview(list, target);
}
}
});
list.addEventListener('preview', ({detail}) => {
const {showMask} = detail;
showMask();
});
如上代碼所示,抽象行為的模式允許一個組件可以靈活的組合或卸載多個行為,且互不衝突。
中間人(Mediator)模式#
我們接下來的新任務是實現一個同步滾動的編輯與預覽區,這是一種在線編輯類 Web 應用常見的交互形式。在線演示
<body>
<textarea id="editor" oninput="this.editor.update()"
rows="6" cols="60">
在 2001 到 2003 年間,Judith Miller 在紐約時報上發表了一批文章,宣稱伊拉克有能力和野心生產大規模殺傷性武器。這是假新聞。
回顧當年,我們無法確定 Miller 寫的這些故事在美國 2013 年做出發動伊拉克戰爭的決定中扮演了怎樣的角色;與 Miller 相同來源的消息與小布什政府的對外政策團隊有很大關聯。但是,紐約時報仍然起到了為這一政策背書的作用,尤其是對民主黨人,本來他們應該會更堅定地反對小布什的政策。畢竟,紐約時報可不是一些無人問津的地方小報,它是整個美國影響力最大的報刊,它一般被認為具有左傾傾向。Miller 的故事某種程度上吻合報紙的政治傾向。
我們可以把 Miller 的錯誤和最近關於 Facebook 的假新聞問題聯繫起來看;Facebook 用自己的故事告訴我們“假新聞是壞的”。然而,我持有不同的觀點:**新聞假不假沒那麼重要,由誰來決定什麼是新聞才是第一重要的**。
<!--more-->
#### Facebook 的媒體商業化
在[聚集理論](https://stratechery.com/2015/aggregation-theory/)中,我描述了基於分配的經濟權利的消亡導致強勢中介的崛起,它們掌控客戶體驗並將它們的供應商商品化。[在 Facebook 的例子裡](https://stratechery.com/2016/the-fang-playbook/),社交網絡之所以興起,是因為之前存在的線下社會網絡在往線上網絡轉變。考慮到人類本質是社會化的,用戶開始將時間花在 Facebook 上閱讀、發表觀點和獲取新聞。
...(此處省略)
</textarea>
<div id="preview"> </div>
<div id="hintbar"> 0% </div>
</body>
body{
display: flex;
}
#editor {
width: 45%;
height: 350px;
margin-right: 10px;
}
#preview {
width: 45%;
height: 350px;
overflow: scroll;
}
#hintbar {
position: absolute;
right: 10px;
}
function Editor(input, preview) {
this.update = function () {
preview.innerHTML = markdown.toHTML(input.value);
};
input.editor = this;
this.update();
}
new Editor(editor, preview);
class PubSub {
constructor() {
this.subscribers = {};
}
pub(type, sender, data){
var subscribers = this.subscribers[type];
subscribers.forEach(function(subscriber){
subscriber({type, sender, data});
});
}
sub(type, receiver, fn){
this.subscribers[type] = this.subscribers[type] || [];
this.subscribers[type].push(fn.bind(receiver));
}
}
function scrollTo({data:p}){
this.scrollTop = p * (this.scrollHeight - this.clientHeight);
}
var mediator = new PubSub();
mediator.sub('scroll', preview, scrollTo);
mediator.sub('scroll', editor, scrollTo);
mediator.sub('scroll', hintbar, function({data:p}){
this.innerHTML = Math.round(p * 100) + '%';
});
function debounce(fn, ms = 100) {
let debounceTimer = null;
return function(...args) {
if(debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fn.apply(this, args);
}, ms);
}
}
let scrollingTarget = null;
editor.addEventListener('scroll', debounce(function(evt){
scrollingTarget = null;
}));
function updateScroll(evt) {
var target = evt.target;
if(!scrollingTarget) scrollingTarget = target;
if(scrollingTarget === target) {
var scrollRange = target.scrollHeight - target.clientHeight,
p = target.scrollTop / scrollRange;
mediator.pub('scroll', target, p);
}
}
editor.addEventListener('scroll', updateScroll);
preview.addEventListener('scroll', updateScroll);