图片找出处

本文献给自己,愿你能走出人生低谷,摆脱迷茫沼泽。

平时搜这搜那,没想到复盘起来颇有侦探内味,于是把一次图片找出处的搜索历程记录下来。

最初是在大众点评用户 “阳台上的咖啡叔hah” 笔记中见到的,笔记发布于 5月5日。

来自大众点评用户 “阳台上的咖啡叔hah” 发布笔记中的一张图

阳台上的咖啡叔hah 在#大众点评#发布了内容:我的私藏店铺|海边小店

最终的目标是找到这家小店在哪里

线索一 评论

大众点评用户评论

从网友评论中猜测小店在韩国济州岛

线索二 景物

左图是小店大橱窗,右图为小店门口

套图共有五张图,通过前两张能发现:

  • 小店是靠近岸边的,岸边的路沿石由碎石砌成
  • 远处有防浪堤,附近有小船停靠,或者有个小港口
  • 周围都是较矮的建筑物
  • 房子外的牌子写着 HOUSE ??? STORE ,好像是店铺名?

既然猜测是济州岛,那么可以借助街景确认有没有相似的地方

北纬 33.44426485682723, 东经 126.28560481567496 附近的景色

打开谷歌地图街景,进入济州岛北部,靠近海边的道路发现这种碎石路是存在的。

线索三 以图搜图

谷歌以图搜图结果

使用百度、谷歌的搜图,搜索第一张室内的图,能找到被两处国内网站收录了:

新浪微博用户“@一起来旅游” 在5月28日发布的一条微博名
企鹅号“晴子姐姐”在腾讯网发布的“治愈系背景图 ”文中

谷歌以图搜图结果

微博中配图是转发自用户 pillter_ 在 Instagram 发布的照片。

线索四 Instagram 用户 pillter_

韩国摄影师 pilsung

instagram 主页 https://www.instagram.com/pillter_/

naver 主页 https://blog.naver.com/p__s0718

在 Instagram APP 上找到 pillter_ 的主页

아기자기한 소품들과 함께 구경하는 재미도 쓸쓸했던 곳
다음엔 콘타르트 꼭 도전 한다
집의기록상점
(제주 제주시 한림읍 귀덕리 952-1)

作者已经将店名标记在图片中,小店叫 house_rec.store ,地址在 952-1, Gwideok-ri, Hallim-eup, Jeju-si, Jeju-si

house_rec.store (집의기록 상점)

house_rec.store 是一家面包店,位于济州市翰林邑,营业时间下午一点到五点,每逢周三四五六休息,只提供外卖。

house_rec.store (집의기록 상점)

在 Google 上搜索 집의기록 상점 能找到更多关于这家店的信息。

instagram: https://www.instagram.com/house_rec.store/

网站: https://houserecords-store.com/

游客 vlog

感谢我们活在互联网时代,光看图片不够劲爆的话,youtube 上有非常多视频以 vlog 的形式记录了这家店。

제주도 일상 | 아꼼헤어 | 수풀 | 집의기록상점

该文章能帮助你在 Vue 项目中存储用户信息;理解 Vuex 的使用场景;降低组件间的耦合度。

随着软件产品的规模越来越庞大,维护难题犹如洪水般便向工程师涌来。前端工程师不仅要将设计稿中的元素组件化,便于组合使用;同时要关注各种逻辑代码间的耦合,避免产生过多的重复片段。

前端经常遇到两个页面共用一个状态,如显示用户的昵称;判断是否登录,显示“登录”按钮等。在简单的应用中,我们可以编写一个维护状态的模块,在适当的时候读取或者更新状态。

使用 User State 模块管理两个页面中的状态

但状态模块难以应付在大型应用中复杂的场景。还需引入发布订阅的设计,让页面无需时刻关注状态更新;一旦多处使用,很难追踪到底是哪里更新了状态。

为什么是 Vuex?

避免出现状态模块那样的缺陷,Vuex 有两个特点:

  • 像 Vue 响应式数据那样,状态以响应式结果反馈至组件、页面;
  • 为保证状态的变化是可追踪的,只允许以一种方式改变状态;

通过阅读状态管理模式, Vuex 中存在几个核心概念:

  • State 意为状态源,以对象结构储存的值
  • Getter 可以从 State 中派生出一些状态,方便具体情况使用
  • Mutation 是唯一可以改变 State 的方法,且必须是同步函数调用
  • Action 可以异步地提交 Mutation

通过理解以上概念,梳理出与用户相关的功能。例如:

  • 用户昵称、认证信息等储存在 State 中;
  • 将异步获取用户信息的操作编写在 Action 函数;
  • 把具体更新昵称、认证信息等操作编写在 Mutation 函数;
  • Getter 则返回用来快速判断用户是否登录的判断;

应用场景

回到最初要显示用户昵称,判断是否登录的场景。意味着 Vuex 必须暴露两个 Getter ,isLoginaccount 提供外部使用。

// store/account.js

export const IS_LOGIN = 'account/isLogin'
export const ACCOUNT = 'account/account'

export const getters = {
    isLogin() {},
    account() {}
}

isLogin 为布尔值,表示是否登录。
account 对象含有用户的信息,包括昵称、会员等级等。

导出到计算属性

为了能在组件中快速读取用户的信息,使用 Vuex 提供的 mapGetters 将两个 Getter isLoginaccount 生成为计算属性,并抽象成混入 (mixins)。

// packages/mixins/Auth.mixin.js

import { mapGetters } from "vuex"
import { IS_LOGIN, ACCOUNT } from "@/store/account"

export default function () {
    return {
        computed: {
            ...mapGetters({
                'isLogin': IS_LOGIN,
                'account': ACCOUNT
            })
        }
    }
}
// Vue Component
import AuthMixin from "./Auth.mixin"
export default function() {
    return {
        mixins: [AuthMixin()],
        beforeMount () {
            console.log(this.isLogin)
            console.log(this.account)
        }
    }
}

Auth.mixin 混入到组件,随时随地能读取到 isLogin account 。接下来开始完善 Vuex 的内部状态源 State 。

状态源

export const state = () => ({
    user: null,
    token: "",
})

更新状态

export const mutations = {
    set_user(state, user) {
        state.user = user
    },
    set_token(state, token) {
        state.token = token
    },
}

export const actions = {
    async getUser({ state, commit }, { user_id, token }) {
        const result = await $user.find({ token, user_id })
        result.id = user_id
        commit('set_user', result)
        commit('set_token', token)
        return Promise.resolve(result)
    },
}

在 Vue 框架中,Vuex 状态管理会使得简单的应用变得复杂,开发者需要权衡其带来的好处和效益。

参考

背景

现今中小型的互联网公司的运营手段多依赖网页或小程序的展现形式。小程序可以说是微信圈内对用户粘性较高的网页应用,有较好的推广效果。

网页则被互联网行业称为 H5 ,拥有访问门槛低、功能可控度高等特性,非常适合用于产品的迭代。同是也是公司探索市场新模式,寻找产品卖点,布局营销宣传的工具。

现有架构

在 APP 架构中,网页的存在,能很好以弥补开发周期长,迭代慢的缺点。使其成为强而有力的工具,帮助功能快速实现落地。在这种架构中,网页应用依然离不开其宿主浏览器(WebView)。

围绕着用户进行设计的产品,用户系统作为支撑业务的底层十分重要。针对其特殊的环境,团队的工程师对APP 架构中的网页应用支持进行了如下设计:

  1. 建立 APP 与网页应用的通道,提供网页主动调用的函数;
  2. 搭建业务功能,如获取用户信息和登录的功能;

产生异常的情况

提供的登录功能主要包括,支持调起原生登录页,且包含登录成功的回调。但受限于时间和技术水平,登录回调被设计成只有登录成功,才刷新当前网页。

现有一个推向微信平台和 APP 网页项目,我在开发调试过程中,发现安卓端登录成功后面刷新功能失效的情况,但苹果端正常。看起来情况特殊,调查刻不容缓。

登录成功后页面没有刷新(左图)。而功能测试案例中的页面则正常,获取到用户 ID(右图)

正常和异常的结果均在同一版本(7.3.2)APP 中得出,那么可以简单地判定为开发项目存在的差异,导致了刷新功能失效。

定位导致异常的原因

开发中的项目项目使用了基于 Vue.js 的 Nuxt.js 框架,并启用了路由和状态管理功能。而页面路由的存在,访问的地址比普通的地址多出部分内容,长成这样:

http://domain.my/index.html/#/mypage

多出来的内容通常以 # 开头。# 号字符读作 hash ,称为 hash mark。

图为 fragment 位于 URI 结构中的位置

# 在 URI(统一资源定位符)中被称作 fragment ,用来标识次级资源。在网页制作中,最广泛的用途是被制作页面锚点。它有两个值得关注特点:

  • fragment 的值并不像 query 查询参数那样,它的值并不会随着 URL 发送到 HTTP 服务器;
  • fragment 值的改变不会触发浏览器刷新页面,但是会生成浏览历史;
Clients are not supposed to send URI fragments to servers when they retrieve a document, and without help from a local application (see below) fragments do not participate in HTTP redirections.

前端利用 fragment 实现的应用路由

得益于上述的两种特性,配合浏览器提供的 window.location 对象(BOM),现代前端框架如 Vue.js 实现了页面上的路由。

导致页面不刷新的原因很可能是页面存在 fragment ,但不禁怀疑“刷新当前网页”的功能当初是如何定义的。

功能设计缺陷

带着疑问,翻阅了安卓端项目对应登录功能的源代码。

public void onEventMainThread(LoginSuccess success) {
  
  webView.loadUrl(originUrl); // not working in hash on the url
  
  historyUrls.clear();
}

刷新功能被设计成重新加载原先的 URL ,而不是采用组件提供的重载方式(类似浏览器的重新加载按钮)。

对于不含有 # 的普通地址来,重新加载 URL 即可触发浏览器刷新。这时如果加载了含有 # 的地址,重新加载是不会触发浏览器刷新的

安卓开发者文档中提供了重新加载当前 URL 的方法 webview.reload,二话不说,修改源代码,进行测试。

public void onEventMainThread(LoginSuccess success) {
  
  webView.reload(); // using new method replace with loadUrl
  
  historyUrls.clear();
}

将代码修改成新的加载方式,网页顺利刷新。

更优雅的解决方案

原本登录业务的实现方案受团队和时间的限制,具体实现方式又少有人知。面对日新月异的应用场景,现有的方案显得捉襟见肘。久而久之积累下技术债务,是导致本次功能失效的主要原因。

往后 APP 将会面临更多与网页有交互的挑战,必须提前准备好解决方案。比如登录业务原本设计成刷新网页,刷新网页会降低用户体验,同时也增大了业务状态本就复杂的网页开发难度。

设计成函数回调的方式,从编程的角度达到结构的作用,大幅提升了系统的可维护性。

public void onEventMainThread(LoginSuccess success) {
  
  webView.loadUrl("javascript:webViewLoginSuccess()"); // login success callback
 
}

以安卓端为例。网页开发者提前在项目中定 义JavaScript 函数 webViewLoginSuccess 函数, 一旦登录成功后,在安卓端 WebView 执行该函数,达到告知网页登录的效果。

根据实际情况,你可以从下方的参考链接中找到合适的解决方案。

在传统网页应用中,登录功能由用户中心承载,用户认证信息主要以 Cookies 的形式存储在浏览器。亦或是现代前端应用,将用户认证信息转化成数据结构存储在 localStorage 中,作为持久化数据使用。

参考链接

BFC

BFC 全称是Block Formatting Context,即块格式化上下文。它是 CSS2.1 规范定义的,属于 CSS渲染定位的一个概念。

块格式化上下文是 Web页面的可视 CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

听起来非常难理解,但编写代码时往往会无意中创建了 BFC 元素。要创建 BFC 元素也很简单,为元素设置以下之一属性:

  • float 属性为 leftright
  • overflow 属性为 autoscrollhidden
  • display 属性为 table-cellinline-block
  • position 属性为 absolutefixed

当然在普遍的情况下也会有 BFC 情况,它会形成相对独立的布局

  • html 标签内的元素
  • Flexbox 布局中的子元素
  • Grid 布局中的子元素
  • 表格元素

还有若干种情况会创建 BFC ,但相对少见。见结尾处的参考链接。

相对独立的布局

BFC 的特别之处在于,它会创建相对独立的布局,这里的独立指的是不影响外部元素,不被外部元素影响,也不被其中的元素所影响。

不影响外部元素

image

如上图所示,因为外边距合并的特性存在,标题三的外边距会溢出到两个蓝色块之间,影响了两个块之间的布局。

垂直外边距会合并的情况是 W3C CSS2 规范定义之初就存在的,并不能说是 bug ,而是 future。

通过给第二个蓝色块设置 overflow: hidden,标题三的外边距只会在 BFC 内部生效。

不受外部元素影响

image

在实际项目中,往往会有左右两列的结构,比如评论列表,右侧文字会自动填满剩余空间。

<div>
    <img style="float:left; margin-right: 10px;" 
        src="http://placekitten.com/64/64" alt="">
    <p>
      块格式化上下文(Block Formatting Context,BFC) 
      是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,
      也是浮动元素与其他元素交互的区域。
    </p>
</div>
p {
    overflow: hidden;
}

浮动设计之初只是为了实现文字环绕效果,但却被过度使用,阴差阳错地形成了浮动布局。结合浮动和 BFC 来实现两列结构,是可行的。

给段落设置 overflow: hidden,使段落形成 BFC ,消除了文字对图片环绕的效果。

不被包含的元素所影响

image

<div>
    <img src="http://placekitten.com/50/50" alt="">
    <p>Cat</p>
</div>
div {
     overflow: hidden;
}

浮动元素由于脱离了正常文档流,容器不会包含浮动元素的高度,只有正常文档流的高度,这也是大家常说的高度塌陷。这时候容器设置 overflow: hidden,形成 BFC,父元素的高度就会正常。也是一种清除浮动方式。

参考

W3C Visual formatting model

Understanding CSS Layout And The Block Formatting Context

CSS 世界 - 6.3 CSS 世界的结界 —— BFC

块格式化上下文 - MDN

float | CSS-Tricks

提到 MVVM ,首先要单向绑定和双向绑定。

MVVM 模式

MVVM 模式通过关联 View 和 Model ,开发者无需关心 DOM 的结构,只关心数据结构和存储的值。

传统模式下的 DOM 操作

count: <span id="count">0</span>

<button id="btn">count +1</button>
var countNum = 0
var countDom = document.querySelector('#count');
var btn = document.querySelector('#btn');

btn.addEventListener('click', function (e) {
  countNum += 1
  countDom.innerText = countNum
}, false)

image

传统情况下更新页面内容,要操作变量和对应的 DOM,才算完成。又或者直接读取 DOM 内容,赋值后写入到 DOM 中。

使用 Vue 展示单向绑定

采用单向绑定的模式,将计数值 count 和 DOM 捆绑在一块,只需更新 count 的值 ,即可完成页面上内容的更新。

为演示单向绑定模式,使用 Vue 框架演示这一实现。

<div id="app">
    <p>count: <span>{{ count }}</span> </p>
</div>
<button id="btn">count +1</button> 

<script src="https://cdn.staticfile.org/vue/2.6.12/vue.min.js"></script>
var vm = new Vue({
  el: '#app',
  data: {
      count: 0,
  }
});
window.vm = vm;

var btn = document.querySelector('#btn');

btn.addEventListener('click', function (e) {
  vm.count += 1
}, false)

按钮事件处理函数不变,只控制 count 的值实现 DOM 的更新。由于 Vue 暴露了 data 变量,对 count 进行赋值,框架即可知道变量有变化且与原来的值不同,促使框架更新 DOM 页面上的内容。

image

单向绑定意味着数据的值与视图有绑定关系,仅在值有变化时才更新视图。

双向绑定

数据的值变化能驱动视图更新,反过来,如果视图触发的事件,能直接的反应到数据上的实现,称为双向绑定。

简单的例子就是表单

<div id="app">
    type your name: <input type="text" v-model="name">

    <p>my name is {{ name }}</p>
</div>

<script src="https://cdn.staticfile.org/vue/2.6.12/vue.min.js"></script>
var vm = new Vue({
  el: '#app',
  data: {
    name: '',
  }
});
window.vm = vm;

image

v-model 语法它责监听用户的输入事件以更新数据。键盘输入会触发 DOM input 事件,框架捕获时间获取 DOM 值。由于依赖变量 name 已存在,赋值即可,视图内容也会随之更新。