序言

image.png
晚上好,欢迎大家观看又拍云 Open Talk 公开课。我是来自又拍云的 CDN 运维开发工程师张召,本次直播主要给大家分享”油猴脚本 —- 只为更好的交互体验”.

下面咱们开始进入正题…

从个人简介中可以看到, 我是运维开发工程师, 并不是专业的前端, 但油猴脚本是一个 Chrome 插件属于前端范畴的东西, 那我为什么要谈这个话题呢? 因为在我的个人实践中发现, 只要运用得当, “油猴脚本” 能够和我们平常写的 bash 脚本一样, 发挥非常大的威力, 提升工作效率, 起到事半功倍的效果.

What 什么是“更好的交互体验”?

image.png
首先是 What? 什么是 “更好的交互体验” ? 我们要在这一节对 “好的交互体验” 做一个定义. 交互体验, 直接定义可能比较抽象, 所以我打算先举几个反例.

  • 百度, 我们搜索一个关键字 “Python 迭代器”, 看这个展现的搜索结果, 底部有广告, 右边展示的是热搜榜, 整个搜索结果的页面比例也不是非常舒服, 左侧我们关注的搜索结果只占了整个页面的 1/3, 而右侧我们不是非常关心的内容占据了大部分. 这些都是交互体验中比较差的部分.
  • 脚本之家, 打开这个页面, 发现里面的广告琳琅满目, 绚丽多彩, 轮播/浮框等各种类型的广告都有, 这种的交互体验可以说是”不堪入目”了.
  • 知乎, 这是我常逛的网站, 整体来说它的交互体验要比上面的两个好很多, 但是如果没有右边的侧边框和伪装成问题的广告, 然后字体再大一点, 再有”一键转载” 转载功能的话, 我觉得交互体验会更棒.

“更好的交互体验”是

image.png
对于上面的三个例子, 多多少少都有一些不满意的地方, 那么我们在这里定义一下, 更好的交互体验是指:

  • 清爽: 没有五花八门的广告. 无论是弹窗广告还是伪装成正常内容的广告, 都希望能去掉.
  • 高效: 平时使用网络, 搜索需求居多, 那搜索结果的主体如果更加醒目, 那我们的效率应该会更高
  • 个性化: 比如知乎, 整体的浏览体验是 ok 的, 但是我个人可能还存在一些 “喜欢老年字体”/“希望不显示侧边栏” 等个人性化需求, 但是知乎目前是没有提供此类功能的.

Why 为什么需要 “更好的交互体验” ?

image.png

这部分内容我是想向大家再次向大家强调一下这个观点, “好的交互体验可以成倍的提高工作效率“. 这个观点并不是我总结出来的, 我第一次听到这个观点是在 2018 年的蚂蚁金服的 SeeConf 上. 我们一起看下这篇文章.

当时的演讲是举了这样一个例子, 2013 年和 2017 年这两个网站用的技术基本相同. 但是我们明显可以感受到 2013 年的网站的用户体验差很多, 而且他们做过统计, 2017 年相比 2013 年的这个网站做同一个事, 平均鼠标要少点五六次, 一个繁琐的运维操作的话要节省十多分钟, 这对工作效率而已是一个非常大的提升.

所以, 好的交互体验真的是非常重要.

HOW 交互体验探索之路

image.png

如果大家认同我上面的观点, 那么自然就会想迫不及待的提示自己的交互体验, 别急, 听我一一道来.

HOW 1. Chrome 插件推荐

image.png

安装 Chrome 插件是大家最常用的选择, 我也一样, 安装了不少 Chrome 插件. 下面我挑几个常用的和大家分享一下:

  • 夜晚模式,DARK READER
  • Vimium,可以在页面上使用 vim
  • 谷歌上网助手,不翻墙的情况下访问谷歌应用
  • oneTab,一键关闭 tabs. 当你发现有太多的标签页时,单击OneTab图标,将所有标签页转换成一个列表保存起来。如果再次访问这些标签页时,可以再单击 OneTab 图标恢复。
    • 我觉得这个非常实用, 因为经常会打开上百个网页, 然后其中有些有价值的内容还没有做笔记舍不得管, 这种情况下 Onetab 非常合适.

Finish? 安装一堆插件就万事大吉了吗?

image.png

换句话说, 我们当初定下的 “良好的交互体验” 目标已经完全实现了吗? 我们再看下定下的这些目标:

  • 清爽:没有五花八门的广告
  • 高效:主体内容要醒目
  • 个性化:可定制性强

远远没有, 如果你和我一样对 Chrome 插件狂热过的话, 就难免同一个类型的插件安装了多个, 毕竟每个插件有每个插件的优点, 以我上面提到的暗夜模式举例, 这些插件之间会相互冲突, 如果所有暗夜模式的插件都打开的情况下, 页面会变得面目全非.

并且 Chrome 插件安装的过多, 也会使得浏览器变卡, 并且还可能存在安全方面的问题.

所以, 安装 Chrome 插件并不是非常好的解决办法.

HOW 2. 自己动手写插件

image.png

放弃靠安装大量堆砌功能后, 我开始尝试自己写插件. 在网上找到了一个比较好的攻略. 我们来一起看一下.
简单浏览一下, 大家什么感受? 作者确实写得非常好非常详尽, 可是我十动然拒, 十分感动然后拒绝看下去了, 我周末也曾花时间体验过, 综合感受下来, 这种写插件的方法是在做一个前端工程, 如果想达到良好的浏览体验的话, 可能要花费非常多的时间. 我总结了以下缺陷:

  • 入门曲线陡峭, 需要理解的概念多
  • 开发效率低,不能快速开发
  • 注册为开发者需付费,上架后更新插件需审核

现在我只是简单总结, 后面我会详细的展开讲这几条缺陷.
那到底是否存在简单粗暴的解决办法呢?有的, 这就是我们今天重点介绍的 “油猴” 脚本管理器.

HOW 3. “油猴”脚本管理器

image.png
什么是油猴脚本? 油猴脚本质依旧也是 Chrome 插件,不过它可以把我们编写的 JS 脚本注入到页面中。油猴脚本出现的背景是什么呢? 我们可以一起看下这个链接.
最早是给Firefox用的. 因为Firefox早期的扩展开发极其繁琐, 所以说油猴诞生之初, 就有着代替一般浏览器扩展的基因.

Why 对比写浏览器扩展和写油猴脚本

image.png
我们来对比一下自己写 Chrome 插件和写油猴脚本的区别.

之前讲到了自己写 Chrome 插件的话, 入门曲线陡峭, 需要理解许多前置概念. 而写油猴脚本除了需要入门级的前端知识外, 几乎没有其他新的概念了, 而且入门级的前端知识我们后面也会讲到, 可以说这次课程是包教包会.

开发效率低, 写扩展因为每次写了之后需要打包然后浏览器加载后才能调试, 而且因为前置概念多, 很容易出现错误. 所以开发效率低. 而油猴为我们准备了一个编辑器, 我们直接写了之后保存就能运行, 刷新页面就能看到效果, 开发简便自然效率就高了. 而且如果大家配合 Chrome 开发者工具的话, 写的代码量非常少, 基本上粘贴复制一下就可以, 比写 bash 脚本还要简单.

浏览器扩展比如 Chrome 上的扩展, 需要上架到 Chrome 的扩展商店, 才能让大家方便的下载, 而油猴脚本也有类似于 github 一样的开放的脚本仓库, 写好后是后台程序审核, 只要符合一定的规则就可以发布.

Where 从哪里获取想要的脚本?

image.png
油猴也有一个类似于谷歌扩展商店一样的脚本仓库, 所以我们有写油猴脚本需求的时候, 可以先搜索一下是否有类似功能的脚本, 另外它们的代码也能给我做一些参考.

我们今天要演示的脚本已经上传到这个脚本仓库, 我们可以现场演示一下是否能搜到. 输入 open talk 或 又拍云
另外知乎上也有一些相关的回答, 对不错的脚本进行了汇总.

我们下面就体验一个人气脚本 AC-百度, 看看安装后我们的浏览体验是否真的得到了提升. 看现在搜索内容已经居中, 并且页面已经变成了 feed 流自动翻页, 注意这个 block 按钮, 如果某些网站的内容质量非常差以后不想展示, 还可以通过这个按钮 block 掉.

HOW 如何写“油猴”脚本?

image.png

写油猴脚本非常简单, 唯一需要的是需要有基础的前端知识, 对前端三剑客有一定的了解. 不过可能有些专供后端技术的同学听到前端知识内心可能有些抵触, 或者感觉我是来骗大家学前端的. 我想说, 不懂没关系, 本课程包教包会. 好, 我们下面一起快速学习一下前端知识.

Why JavaScript

image.png

这张前端发展史的图是我引用自阿里前端委员会主席圆心的公开分享. 我主要是想通过这张图想向大家强调一下 JavaScript 的重要作用以及我们为什么要从 JavaScript 的角度去看待 HTML 和 CSS, 因为现在流行的前端框架都是基于 JS 进行开发的, 为什么能够基于 JS 开发呢? 因为 JS 能够操作 HTML 和 CSS, 所以通过 JS 生成 HTML 和 CSS, 最终渲染我们想要的效果.

看 2003 年这个时间点, 前端岗位的出现, 从图上看这是前端的猿猴时期, 前端岗位一般都被人戏称为 “切图仔”. 那是什么导致了猿猴学会了直立行走? 是 Ajax 技术的出现伴随着 JavaScript 技术的成熟, 2009 年出现了 Nodejs.

到 2013 年这个时间点学会了使用工具呢? 当然是现在非常火的前端三大框架了, Vue/React和 Angular…

从前端猿猴时代到类人猿时代, 我们可以看到 JavaScript 扮演了非常重要的角色. 那我们今天前端入门的课程定位呢? 是这只小猴子, 连猿猴也算不上, 所以说是非常基础/非常简单的.

What HTML Element

image.png
HTML Element 也就是 HTML 元素, 也被成为节点. 它的构成也非常简单的. 尤其是写 Java 或者配置过 xml 的同学, 看这个毫无难度. 开始标签, 结束标签, 组成一对标签, 中间的内容是在页面上展示的数据. 这些东西加起来就是一个元素了.

What JavaScript: DOM Element

image.png
在 JS 的角度看来, 1 个 HTML 元素就是 1 个 DOM 节点.
什么是 DOM, DOM 是缩写, 全写是 Document Object Model, 直译是 “文档对象模型”, 也就是把 HTML 节点看成是一个类, HTML 的属性就好比类中的属性, HTML 的方法就好比类中的方法一样.

另外我们看这个语法模板, 通过这个接口, 我们可以创建一个 DOM 节点.

  1. // 创建 1 个 DOM 节点
  2. // eleName 代表标签名称, 如 'p','div','h1',...
  3. document.createElement(eleName);

What HTML Attribute

image.png
这个 class="editor-note" 就是 HTML 的属性, HTML 的属性就是这样一个个键值对中间用等号连接.

键值为 class 的属性比较特殊, 它是专门用来表示样式的属性, 浏览器会把 class 对应的值进行渲染, 这是属于 CSS 部分的内容了, 我们马上会讲到.

再来看一下, 对应的 JS 的 API 接口.

  1. // 语法
  2. // attribute 是一个包含 attributeName 属性值的字符串。
  3. // attributeName 是你想要获取的属性值的属性名称。
  4. let attribute = element.getAttribute(attributeName);
  5. // 例子
  6. element.getAttribute('class');

HTML Attributes

image.png
介绍一下常用的 HTML 属性, class 和 style 都是表示样式的, id 一般用来指代独一无二的事物, 具有唯一性, 在HTML 也不例外, 在一个页面中, id 只能又来表示一个节点, 一般是不允许重复的.

类可以自定义属性, html 的属性也不例外, 我们也可以随意自定义一些属性. 这些属性在 React 或者 Vue 中作用巨大, 可以用来存储信息, 一会的例子中我们可以详细看到.

What HTML Event

image.png
在 HTML 的属性中除了我们刚才列举的那些外, 还有一类属性比较特殊, 这些属性一般都以 on... 开头, 比如这个例子, onclick, on 表示监听, click 表示点击, 这个作用是表示当前的 button 元素监听一个鼠标点击事件, 如果监听到了, 就会执行后面的操作.

HTML 事件是非常重要的内容, 因为没有事件, 那么我们与页面就没有互动, 没有互动就是没有交互, 没有交互也就没有本次的分享内容了, 所以 HTML 事件在现在的前端技术中地位非常重要.

我们看下相关的 JS 接口.

  1. // type, 事件类型
  2. // listener, 事件触发后执行的操作
  3. // 添加监听事件
  4. target.addEventListener(type, listener, options);
  5. // 移除监听事件
  6. target.removeEventListener(type, listener[, options]);

HTML Event

image.png
看一下常见的 HTML 事件.

  • 键盘事件,
  • 鼠标事件,
  • window 事件.

    DOM Tree

    image.png
    有句话叫, “独木不成林”, 同样单个 HTML 元素撑不起整个页面, 现在看到的页面这样多姿多彩, 是因为HTML 元素允许按照一定的规则进行嵌套.

image.png
从另外一个角度看 DOM tree. 说起 DOM Tree, 可能会让我们联想起红黑树/二叉树等数状的数据结构, 在二叉树中有一个很重要的知识点就是如何在树中查找元素. 在 DOM Tree 中也同样存在着元素定位的问题, 我们如何定位所有 HTML 标签为 div 的节点, 或者找属性为 href 的 a 标签节点?

如果不能准确的定位某个或某组节点, 那么想精确的渲染样式也是无从谈起的. 我们来看下在 CSS 中是如何解决这个问题的?

What CSS

image.png
这就是 CSS 基本的语法模型. 左上角的 Selector 是选择器, 表示选择哪些 HTML 节点. 这个实例的意思是选择所有的 html 标签为 p 的节点.

花括号中的内容是声明的样式, 也是一个键值对的形式. color red 的意思是把文本字体渲染为红色.

这个例子的完整意思是, 把所有的 p 节点内的文本的字体颜色设置为红色.

CSS 选择器

image.png
CSS 选择器的类型有很多, 我在这只是列出常用的几个, 有兴趣还可以通过下面的链接进一步了解.

.class 的形式, 我们看这个例子 .info 表示选择所有 html 节点中 class 属性值为 intro 的所有节点.

第一个语法 .class 的形式是定位含特定属性的所有节点, 如果我不指定某一类节点怎么办呢? 可以通过前面加上 ele, 后面加上方括号的形式, 就可以定位指定的 HTML 节点.

比如例子2 中的 div[class="Question-mainColumn"] , 就是选取所有 class 值为 Question-mainColumn 的 div 节点.

第2个和第三个最大的区别在于, 第 3 个中间有个波浪线, 这表示一个包含关系. 而第二个是表示一个等于的关系的. 第 3 个的含义是, 只要 class 中包含 Question-mainColum 就是可以的.

What CSS Box Model

image.png

在 CSS 中海油一个非常重要的概念是 Box Model 也就是盒子模型, 这个的概念的本质其实大家都懂, 只不过不知道这个概念的名称. 我们看左边的这张报纸图片, 看我标线的部分, 是不是和右侧的这个 CSS Box Model 图有些类似, 都是核心的部分是内容, 这部分内容在盒子模型中叫做 content box, 内容盒子.

然后内容和黄线之间有点距离, 这个距离在盒子模型中就叫做 padding. 而且你可以看到这个 padding box 看起来有点像在 content box 的基础上又套了一层, 这个叫做 padding box.

padding box 外面再套一层盒子, 这个盒子就叫做 border box. 这个黑色的粗线就是边框, 叫做 border.

举一反三, 最后的 margin box 和 margin 也分别表示最外层的盒子和最外层的边框.

那为什么这么设计呢? 因为在互联网早期是以静态页面为主的, 它的网页排版自然就会受到平面媒体比如杂志, 报纸等的影响, 你把这个盒子模型理解成报纸的排版就觉得非常恰当了. 比如说如果 margin 外边距没有控制好, 左图中的红框的这个新闻消息就会和其他的新闻挤到一起.

所以理解盒子模型把握住两点, 一是把它的布局联想成一张报纸, 二是这种盒子套盒子的解构就像一个俄罗斯套娃.

What JS 如何通过 CSS Selector 定位 HTML

Element? image.png
在 js 中也是预留了相关的接口, 能够让我们通过调用 CSS 选择器来定位元素.

  1. // 语法
  2. element = document.querySelector(selectors);
  3. // 例子: 查找第一个匹配 class属性的html元素
  4. var el = document.querySelector(".myclass");
  5. // 语法
  6. elementList = parentNode.querySelectorAll(selectors);
  7. // 要获取文档中所有<p>元素的 NodeList
  8. var matches = document.querySelectorAll("p");

小结

image.png

以上的内容大致介绍了这三块的内容,

  • html 元素在 JS 的角度讲是就是 DOM 节点, 然后DOM 节点有对应的属性和事件
  • CSS 的话,需要重点把握两个概念:
    • CSS 选择器
    • 盒子模型
  • 以上的内容, 我们都讲到了对应的 JS 接口. 可以实现通过 JS 操作 HTML 和 CSS 的目的.


    掌握了以上内容, 就可以写油猴脚本了.

安装油猴脚本管理器

非常简单, 因为油猴脚本管理器是一个插件, 所以它的安装步骤和普通的插件安装是一样的.
image.png

动手写油猴脚本(1)

image.png
我们今天带大家一起写一个油猴脚本, 在知乎问题页添加一个 “一键转载” 的按钮.

下载完成后, 我们就可以点击这个图标, 会出现这样一个图标, 我们先点击 “获取新脚本” 来获取我们今天的演示课件.
选择 greasy fork, 搜索又拍云就可以找到现在的课件.

我已经提前把这个脚本发布了, 所以大家可以安装一下, 作为入门参考.

管理面板

点击安装, 安装后, 我们可以通过管理面板查看安装的脚本. 选择这个 “demo open talk” 的脚本, 可以先看下代码.

  1. // ==UserScript==
  2. // 脚本名称
  3. // @name demo open talk
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.1
  6. // @description 课件. 又拍云 open talk 公开课 油猴脚本 —— 只为更好的交互体验
  7. // @author You
  8. // 在哪些页面生效, 支持通配符
  9. // @match https://www.zhihu.com/question/*
  10. // GM_addStyle 油猴内置的 api, `@grant GM_addStyle` 的作用相当于 import, 这样就可以在当前页面调用 GM_addStyle 这个接口了. GM_addStyle 这个可以用来添加样式.
  11. // @grant GM_addStyle
  12. // ==/UserScript==
  13. // 隐藏右边栏. display: none 是要隐藏选中的节点
  14. // 因为 CSS 中可能会存在多个样式渲染到同一个节点上, 为了达到我们想要的效果, 这些样式有一个优先级,
  15. // 使用 `!important` 就表明我们当前的样式优先级比较高
  16. GM_addStyle('.Question-sideColumn {display: none !important}');
  17. // 加宽内容栏
  18. // with 表示盒子的宽度,
  19. GM_addStyle('.Question-mainColumn {width: 1000px !important}');
  20. (function() {
  21. 'use strict';
  22. // Your code here...
  23. // 创建元素
  24. function createEle(eleName, text, attrs){
  25. let ele = document.createElement(eleName);
  26. // innerText 也就是 <p>text会被添加到这里</p>
  27. ele.innerText = text;
  28. // attrs 的类型是一个 map
  29. for (let k in attrs) {
  30. // 遍历 attrs, 给节点 ele 添加我们想要的属性
  31. ele.setAttribute(k, attrs[k]);
  32. }
  33. // 返回节点
  34. return ele;
  35. }
  36. // 复制到剪贴板
  37. function updateClipboard(newClip) {
  38. // 把内容复制到剪贴板. then 是代表回调的意思
  39. navigator.clipboard.writeText(newClip).then(function() {
  40. // 一切都没问题的话会执行 alert 操作
  41. alert('succeed copy');
  42. }, function(err) {
  43. // 失败时执行的函数
  44. /* clipboard write failed */
  45. console.info('failed copy', err);
  46. alert('faild copy')
  47. });
  48. }
  49. const added = [];
  50. // btnStyle 是一个我事先写好的样式.
  51. // 因为写样式调试的时间比较久,我就不一一向大家演示了.
  52. const btnStyle = 'background-color: #0084ff; margin-top: 15px; margin-bottom: 15px; margin-left:-5px; cursor:pointer; color: #fff; border-radius: 3px; border: 1px solid; padding: 3px 6px';
  53. // 加转载按钮
  54. function addBtn() {
  55. // 获取回答列表
  56. // querySelectorAll 这个 api 可以获取一组 api 节点
  57. const all = document.querySelectorAll('div[class="List-item"]');
  58. for (let item of all) {
  59. // 定位到每个节点的 meta 节点
  60. const meta = item.querySelector('div[class="ContentItem-meta"]');
  61. // 因为知乎规则, 每个人只允许回答一次, 所以我们可以使用 who 作为每个答案的 id;
  62. // todo: 这个地方是有 bug 的, 因为知乎规则中是允许匿名回答的, 匿名的时候会导致当前节点的节点为空, 导致重复添加
  63. // 解决办法是, 多从 meta 中获取几个属性, 拼接成一个独一无二的 id, 这里为了演示, 简单起见, 暂不考虑这种情况.
  64. const who = meta.querySelector('meta[itemprop="url"]').getAttribute('content').split('/').pop();
  65. // added 是一个全局变量, 用来保存已经添加过按钮的节点.
  66. // 这步的作用是, 已经添加按钮节点不重复添加
  67. if (added.indexOf(who) > -1) {
  68. continue;
  69. }
  70. // createEle 是我封住的一个工具函数, 可以生成一个节点.
  71. const btn = createEle('button', '转载按钮', {style: btnStyle});
  72. // text 是我们选中的文本.
  73. const text = item.querySelector('div[class="RichContent-inner"]').innerText;
  74. // 给 btn 节点添加一个鼠标点击事件
  75. // 当 btn 节点接听到鼠标事件后, 会触发后面的方法
  76. // ()=>{}, 这种是箭头函数, 是 ES6 的语法.
  77. btn.addEventListener('click', ()=>{updateClipboard(text)});
  78. // 把创建好的 btn 节点添加到 meta 后面.
  79. meta.append(btn);
  80. // 添加了 btn后的推到 added 列表, 不再重复添加.
  81. added.push(who);
  82. }
  83. }
  84. // 给 window 对象添加一个滚动事件, 当滚动时触发就执行对应的操作.
  85. window.addEventListener('scroll', addBtn);
  86. })();