前端常用设计模式

抽象行为（behavior）模式

图片预览

我们的任务是这样的，给一个固定列表中的图片元素增加 “预览” 功能。在线演示
对应的 HTML 页面如下所示：

Copy






图片预览

#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;
}





























Copy
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();
});

Copy
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);
};
}

Copy
const preview = useBehavior({
type: 'preview',

/*
@subject: 元素
@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 = `
<

>
`;
// 给 #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 元素左右两边的元素设置样式：
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);
},
};
},
});

图片选择器

在线演示

Copy






抽象行为

#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;
}





























Copy
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,
};
},
});

Copy
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 建，就表示选择图片；否则就是预览图片：

Copy
/* ...省略其他代码... */

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 应用常见的一种交互形式。在线演示

Copy

<textarea id="editor" oninput="this.editor.update()"
rows="6" cols="60">
在 2001 到 2003 年间，Judith Miller 在纽约时报上发表了一批文章，宣称伊拉克有能力和野心生产大规模杀伤性武器。这是假新闻。

回顾当年，我们无法确定 Miller 写的这些故事在美国 2013 年做出发动伊拉克战争的决定中扮演了怎样的角色；与 Miller 相同来源的消息与小布什政府的对外政策团队有很大关联。但是，纽约时报仍然起到了为这一政策背书的作用，尤其是对民主党人，本来他们应该会更坚定地反对小布什的政策。毕竟，纽约时报可不是一些无人问津的地方小报，它是整个美国影响力最大的报刊，它一般被认为具有左倾倾向。Miller 的故事某种程度上吻合报纸的政治倾向。

我们可以把 Miller 的错误和最近关于 Facebook 的假新闻问题联系起来看；Facebook 用自己的故事告诫我们“假新闻是坏的”。然而，我持有不同的观点：**新闻假不假没那么重要，由谁来决定什么是新闻才是第一重要的**。



#### Facebook 的媒体商业化

在[聚集理论](https://stratechery.com/2015/aggregation-theory/)中，我描述了基于分配的经济权利的消亡导致强势中介的崛起，它们掌控客户体验并将它们的供应商商品化。[在 Facebook 的例子里](https://stratechery.com/2016/the-fang-playbook/)，社交网络之所以兴起，是因为之前存在的线下社会网络在往线上网络转变。考虑到人类本质是社会化的，用户开始将时间花在 Facebook 上阅读、发表观点和获取新闻。

...（此处省略）



0%


Copy
body{
display: flex;
}

#editor {
width: 45%;
height: 350px;
margin-right: 10px;
}

#preview {
width: 45%;
height: 350px;
overflow: scroll;
}

#hintbar {
position: absolute;
right: 10px;
}

Copy
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);

References:
https://juejin.cn/book/6891929939616989188/section/6891931173539282957