2
头图

前言

简单的知识点搭配合适的业务场景,往往能起到意想不到的效果。这篇文章会用三个最基础人人都知道的前端知识来说明如何助力运营小姐姐、公司48+前端开发同学的日常工作,让他们的工作效率得到极大地提升。

看完您可以会收获

  1. 用vue从零开始写一个chrome插件
  2. 如何用Object.defineProperty拦截fetch请求`
  3. 如何使用油猴脚本开发一个扩展程序
  4. 日常提效的一些思考

油猴脚本入门示例

因为接下来的两个小工具都是基于油猴脚本来实现的,所以我们提前先了解一下它

油猴脚本是什么?

油猴脚本(Tampermonkey)是一个流行的浏览器扩展,可以运行用户编写的扩展脚本,来实现各式各样的功能,比如去广告、修改样式、下载视频等。

如何写一个油猴脚本?

1. 安装油猴

以chrome浏览器扩展为例,点击这里先安装

安装完成之后可以看到右上角多了这个

image.png

2. 新增示例脚本 hello world


// ==UserScript==
// @name         hello world // 脚本名称
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://juejin.cn/* // 表示怎样的url才执行下面的代码
// @icon         https://www.google.com/s2/favicons?domain=juejin.cn
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
  alert('hello world')
    // Your code here...
})();

没错当打开任意一个https://juejin.cn/*掘金的页面时,都会弹出hello world,而其他的网页如https://baidu.com则不会。

到此你就完成了一个最简单的油猴脚本,接下来我们看一下用同样简单的代码,来解决一个实际问题吧!O(∩_∩)O

3行代码让SSO自动登录

问题是什么?

1. 有一天运营小姐姐要在几个系统之间配置点东西

一顿操作,终于把事情搞定了,心情美美的。

但是她心想,为啥每个系统都要我登录一次,不开心 o( ̄ヘ ̄o#)

1.gif

2. 下午一觉醒来,领导让把上午的配置重新改一下(尽职的小姐姐马上开始操作)

但是让她没想到的是:上午的登录页面仿佛许久没有见到她一样,又和小姐姐来了一次亲密接触😭

此时,她的内心已经开始崩溃了

2.gif

3. 但是这不是结束,以后的每一天她都是这种状态😭😭😭

3.gif

痛点在哪里?

看完上面的动图,我猜你已经在替小姐姐一起骂娘了,这做的什么玩意,太垃圾了。SSO是统一登录,你们这搞的是什么东西。

是的,我的内心和你一样愤愤不平, 一样有一万个草泥马在奔腾,这是哪个sb设计的方案,简直不配做人,一天啥事也不干,尽是跳登录页,输入用户名密码点登录按钮了,久而久之,朋友间见面说的第一句话不是“你吃了吗?”,而是“你登录了吗?”。

不过吐槽完,我们还是要想想如何通过技术手段解决这两个痛点,达到只需要登录一次的目的

1. 在A系统登录之后,跑到其他系统需要重新登录。

2. 登录时效只有2小时,2小时后,需要重新登录

该如何解决?

根本原因还是公司的SSO统一登录方案设计的有问题,所以需要推动他们修改,但是这是一个相对长期的过程,短期内有没有什么办法能让我们愉快的登录呢?

痛点1: 1. 在A系统登录之后,跑到其他系统需要重新登录。已无力回天

痛点2: 2. 登录时效只有2小时,2小时后,需要重新登录已无力回天

我们不好直接侵入各个系统去改造登录逻辑,改造其登录时效,但是却可以对登录页面(示例)做点手脚

image.png

最关键的是:

  1. 用户名输入框
  2. 密码输入框
  3. 点击按钮

所以可以借助油猴脚本,在DOMContentLoaded的时候,插入一下代码,来实现自动登录,减少手动操作的过程,大概原理如下。

结构图.jpg

// ==UserScript==
// @name         SSO自动登录
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://*.xxx.com/login* // 这里是SSO登录页面地址,表示只有符合这个规则的才注入这段代码
// @grant        none
// ==/UserScript==

document.querySelector('#username').value = 'xxx' // 用户名
document.querySelector('#password').value = 'yyy' // 密码
document.querySelector('#login-submit').click() // 自动提交登录

是不是太简单了,简单到令人发指,令人痛恨,令人想吐口水!!!,没有一点技术含量

是不是太简单了,简单到令人发指,令人痛恨,令人想吐口水!!!,没有一点技术含量

是不是太简单了,简单到令人发指,令人痛恨,令人想吐口水!!!,没有一点技术含量

image.png

是的,就这 😄,第一次帮小姐姐解决了困扰她许久的问题,晚上就请我吃了麻辣烫,还夸我"技术"好(此处不是开车

试试效果

gif中前半部分没有开启自动登录的脚本需要手动登录,后半部开启了就可以自动登录了。

autoLogin.gif

拦截fetch请求,只留你想要的页面

问题是什么?

前端常见的调试方式

  1. chrome inspect
  2. vconsole
  3. weinre
  4. 等等

这些方式都有各自的优缺点,比如chrome inspect第一次需要翻墙才能使用,只适用于安卓; vconsole不方便直接调试样式; weinre只适用于调试样式等。

基于这些原因,公司很久之前搞了一个远程调试工具,可以很方便的增删DOM结构、调试样式、查看请求、查看application 修改后手机上立即生效。

autoLogin2.gif

远程调试平台使用流程

他的使用流程大概是这样的

  1. 打开远程调试页面列表

    此页面包含测试环境所有人打开的调试页面链接, 多的时候有上百个

image.png

  1. 点击你要调试的页面,就可以进入像chrome控制台一样调试了

image.png

看完流程你应该大概知道问题在哪里了, 远程调试页面列表不仅仅包含我自己的页面,还包括很多其他人的,导致很难快速找到自己想要调试的页面

该如何解决?

问题解析

有什么办法能让我快速找到自己想要调试的页面呢?其实观察解析这个页面会发现列表是

  1. 通过发送一个请求获取的
  2. 响应中包含设备关键字

image.png

拦截请求

所以聪明的你已经猜到了,我们可以通过Object.defineProperty拦截fetch请求,过滤设备让列表中只存在我们指定的设备(毕竟平时开发时调试的设备基本是固定的,而设备完全相同的概率是很低的,所以指定了设备其实就是唯一标识了自己)页面。

具体如何做呢?


// ==UserScript==
// @name         前端远程调试设备过滤
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://chii-fe.xxx.com/ // 指定脚本生效的页面
// @grant        none
// @run-at       document-start // 注意这里,脚本注入的时机是document-start
// ==/UserScript==

;(() => {
  const replaceRe = /\s*/g
  // 在这里设置设备白名单
  const DEVICE_WHITE_LIST = [
      'Xiaomi MI 8',
      'iPhone9,2',
  ].map((it) => it.replace(replaceRe, '').toLowerCase())
  
  const originFetch = window.fetch
  const recordListUrl = 'record-list'
  const filterData = (source) => {
    // 数据过滤,返回DEVICE_WHITE_LIST指定的设备的数据
    // 详细过程省略
    return data
  }
  // 拦截fetch请求  
  Object.defineProperty(window, 'fetch', {
    configurable: true,
    enumerable: true,
    get () {
      return function (url, options) {
        return originFetch(url, options).then((response) => {
          // 只处理指定的url
          if (url.includes(recordListUrl)) {
            if (response.clone) {
              const cloneRes = response.clone()

              return new Promise((resolve, reject) => {
                resolve({
                  text: () => {
                    return cloneRes.json().then(json => {
                      return filterData(JSON.stringify(json))
                    });
                  }
                })
              })
            }
          }

          return response
        })
      }
    }
  })
})()

试试效果

通过下图可以看出,过滤前有37个页面,过滤后只剩3个,瞬间就找到你要调试页面,再也不用从几百个页面中寻找你自己的那个啦!

image.png

助力全公司45+前端开发 - chrome插件的始与终

通过插件一键设置ua,模拟用户登录状态,提高开发效率。

先看结果

插件使用方式

new.gif

插件使用结果

团队48+小伙伴也使用起来了

image.png

image.png

背景和问题

日常c端业务中有很多场景都需要用户登录后才能正常进行,而开发阶段基本都是通过chrome模拟手机设备来开发,所以往往会涉及到在chrome浏览器中模拟用户登录,其涉及以下三步(这个步骤比较繁琐)。

备注:保持用户的登录态一般是通过cookie,但也有通过header来做,比如我们公司是改写ua来做的

  1. 获取ua: 前往公司UA生成平台输入手机号生成ua
  2. 添加ua: 将ua复制到chrome devtool设置/修改device
  3. 使用ua: 选择新添加的ua,刷新页面,重新开发调试

ua.gif

来看一段对话

隔壁98年刚毕业妹子:

又过期了,谁又把我挤下去了嘛

好的,稍等一会哈,我换个账号测测

好麻烦哎!模拟一个用户信息,要这么多步骤,好烦呀!!!

我,好奇的大叔:

“细心”了解下,她正在做一个h5活动项目,场景复杂,涉及的状态很多,需要用不同的账号来做测试。

模拟一两个用户还好,但是此刻小姐姐测这么多场景,已经模拟了好多个(谁都会烦啊)

公司的登录体系是单点登录,一个好不容易模拟的账号,有可能别人也在用,结果又被顶掉了,得重新生成,我TM

看着她快气哭的小眼神,作为隔壁桌友好的邻居,此刻我心里只想着一件事...!帮她解决这个恼人的问题。

分析和解决问题

通过上面的介绍您应该可以感觉到我们开发阶段遇到需要频繁切换账号做测试时的烦恼,相对繁琐的ua生成过程导致了它一定是个费时费力的麻烦事。

有没有什么办法让我们的开发效率得到提升,别浪费在这种事情上呢?一起一步步做起来

需求有哪些

提供一种便捷地模拟ua的方式,助力开发效率提升。
  1. 基本诉求:本地开发阶段,希望有更便捷的方式来模拟用户登录
  2. 多账号: 一个项目需要多个账号,不同项目间的账号可以共享也可以不同
  3. 指定域: 只有指定的下才需要模拟ua,不能影响浏览器正常使用
  4. 过期处理: 账号过期后,可以主动生成,无需手动重新获取

如何解决

  1. 需求1:结合前面生成ua阶段,我们可以通过某种方式让用户能直接在当前页面生成ua,无需跳出,一键设置省略手动过程
  2. 需求2:提供多账号管理功能,能直接选中切换ua
  3. 需求3:限定指定域,该ua才生效
  4. 需求4:当使用到过期账号时,可一键重新生成即可

为什么是chrome插件

  1. 浏览器中发送ajax请求的ua无法直接修改,但是chrome插件可以修改请求的ua(很重要的一点
  2. chrome插件popup模式可直接在当前页面打开,无需跳出开发页面,减少跳出过程

用vue从零开始写一个chrome插件

篇幅原因,这里只做示例级别的简单介绍,如果您希望详细了解chrome插件的编写可以参考这里

从一个小例子开始

接下来我们会以下页面为例,说明用vue如何写出来。

ua3.gif

基本功能

  1. 底部tab切换区域viewAviewBviewC
  2. 中间内容区域:切换viewA、B、C分别展示对应的页面

content部分

借助chrome浏览器可以向网页插入脚本的特性,我们会演示如何插入脚本并且在网页加载的时候弹一个hello world

popup与background通信部分

popup完成用户的主要交互,在viewA页面点击获取自定义的ua信息

修改ajax请求ua部分

会演示如果通过chrome插件修改请求header

1. 了解一个chrome插件的构成

  1. manifest.json
  2. background script
  3. content script
  4. popup

1. manifest.json

几乎所有的东西都要在这里进行声明、权限资源页面等等

{
  "manifest_version": 2, // 清单文件的版本,这个必须写
  "name": "hello vue extend", // 插件的名称,等会我们写的插件名字就叫hello vue extend
  "description": "hello vue extend", // 插件描述
  "version": "0.0.1", // 插件的版本
  // 图标,写一个也行
  "icons": {
    "48": "img/logo.png"
  },
  // 浏览器右上角图标设置,browser_action、page_action、app必须三选一
  "browser_action": {
    "default_icon": "img/logo.png",
    "default_title": "hello vue extend",
    "default_popup": "popup.html"
  },
  // 一些常驻的后台JS或后台页面
  "background": {
    "scripts": [
      "js/hot-reload.js",
      "js/background.js"
    ]
  },
  // 需要直接注入页面的JS
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["js/content.js"],
    "run_at": "document_start"
  }],
  // devtools页面入口,注意只能指向一个HTML文件
  "devtools_page": "devcreate.html",
  // Chrome40以前的插件配置页写法
  "options_page": "options.html",
  // 权限申请
  "permissions": [
    "storage",
    "webRequest",
    "tabs",
    "webRequestBlocking",
    "<all_urls>"
  ]
}

2. background script

后台,可以认为是一个常驻的页面,权限很高,几乎可以调用所有的API,可以与popup、content script等通信

3. content script

chrome插件向页面注入脚本的一种形式(js和css都可以)

4. popup

popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭。

比如我们要用vue做的页面。

image.png

2. 改写vue.config.js

manifest.json对文件引用的结构基本决定了打包后的文件路径

打包后的路径

// dist目录用来chrome扩展导入

├── dist
│   ├── favicon.ico
│   ├── img
│   │   └── logo.png
│   ├── js
│   │   ├── background.js
│   │   ├── chunk-vendors.js
│   │   ├── content.js
│   │   ├── hot-reload.js
│   │   └── popup.js
│   ├── manifest.json
│   └── popup.html

源码目录


├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── js
│       └── hot-reload.js
├── src
│   ├── assets
│   │   ├── 01.png
│   │   ├── disabled.png
│   │   └── logo.png
│   ├── background
│   │   └── background.js
│   ├── content
│   │   └── content.js
│   ├── manifest.json
│   ├── popup
│   │   ├── App.vue
│   │   ├── main.js
│   │   ├── router.js
│   │   └── views
│   │       ├── viewA.vue
│   │       ├── viewB.vue
│   │       └── viewC.vue
│   └── utils
│       ├── base.js
│       ├── fixCaton.js
│       └── storage.js
└── vue.config.js

修改vue.config.js

主需要稍微改造变成可以多页打包,注意输出的目录结构就可以了

const CopyWebpackPlugin = require('copy-webpack-plugin')
const path = require('path')
// 这里考虑可以添加多页
const pagesObj = {}
const chromeName = ['popup']
const plugins = [
  {
    from: path.resolve('src/manifest.json'),
    to: `${path.resolve('dist')}/manifest.json`
  },
  {
    from: path.resolve('src/assets/logo.png'),
    to: `${path.resolve('dist')}/img/logo.png`
  },
  {
    from: path.resolve('src/background/background.js'),
    to: `${path.resolve('dist')}/js/background.js`
  },
  {
    from: path.resolve('src/content/content.js'),
    to: `${path.resolve('dist')}/js/content.js`
  },
]

chromeName.forEach(name => {
  pagesObj[name] = {
    css: {
      loaderOptions: {
        less: {
          modifyVars: {},
          javascriptEnabled: true
        }
      }
    },
    entry: `src/${name}/main.js`,
    filename: `${name}.html`
  }
})

const vueConfig = {
  lintOnSave:false, //关闭eslint检查
  pages: pagesObj,
  configureWebpack: {
    entry: {},
    output: {
      filename: 'js/[name].js'
    },
    plugins: [new CopyWebpackPlugin(plugins)]
  },
  filenameHashing: false,
  productionSourceMap: false
}

module.exports = vueConfig

3. 热刷新

我们希望修改插件源代码进行打包之后,chrome插件对应的页面能主动更新。为什么叫热刷新而不是热更新呢?因为它其实是全局刷新页面,并不会保存状态。

这里推荐一个github上的解决方案crx-hotreload

4. 完成小例子编写

new.gif

文件目录结构


├── popup
│   ├── App.vue
│   ├── main.js
│   ├── router.js
│   └── views
│       ├── viewA.vue
│       ├── viewB.vue
│       └── viewC.vue

main.js


import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

router.js

import Vue from 'vue'
import Router from 'vue-router'

import ViewA from './views/viewA.vue'
import ViewB from './views/viewB.vue'
import ViewC from './views/viewC.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      redirect: '/view/a'
    },
    {
      path: '/view/a',
      name: 'viewA',
      component: ViewA,
    },
    {
      path: '/view/b',
      name: 'viewB',
      component: ViewB,
    },
    {
      path: '/view/c',
      name: 'viewC',
      component: ViewC,
    },
  ]
})

App.vue


<template>
  <div id="app">
    <div class="app-router">
      <router-view />
    </div>
    <div class="app-tab">
      <div class="app-tab-item" v-for="(tabName, i) in tabs" :key="i" @click="onToView(tabName)">
        {{ tabName }}
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: 'App',
  data () {
    return {
      tabs: [
        'viewA',
        'viewB',
        'viewC',
      ]
    }
  },
  methods: {
    onToView (name) {
      this.$router.push({
        name
      })
    }
  }
}
</script>

<style lang="less">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;

  width: 375px;
  height: 200px;
  padding: 15px;
  box-sizing: border-box;

  display: flex;
  justify-content: space-between;
  flex-direction: column;

  .app-router{
    flex: 1;
  }

  .app-tab{
    display: flex;
    align-items: center;
    justify-content: space-between;

    .app-tab-item{
      font-size: 16px;
      color: coral;
      cursor: pointer;
    }
  }
}

</style>

viewA、viewB、viewC

三个页面基本长得是一样的,只有背景色和文案内容不一样,这里我就只贴viewA的代码了。

需要注意的是这里会演示popup与background,通过sendMessage方法获取background后台数据


<template>
  <div class="view-a">我是A页面

    <button @click="onGetCUstomUa">获取自定义ua</button>
  </div>
</template>

<script>
export default {
  name: 'viewA',
  methods: {
    onGetCUstomUa () {
      
      chrome.runtime.sendMessage({type: 'getCustomUserAgent'}, function(response) {
        alert(JSON.stringify(response))
      })
    }
  }
}
</script>

<style lang="less">
.view-a{
  background-color: cadetblue;
  height: 100%;
  font-size: 60px;
}
</style>

background.js

const customUa = 'hello world ua'
// 请求发送前拦截
const onBeforeSendCallback = (details) => {
  for (var i = 0; i < details.requestHeaders.length; ++i) {
    if (details.requestHeaders[i].name === 'User-Agent') {
      details.requestHeaders.splice(i, 1);
      break;
    }
  }
  // 修改请求UA为hello world ua
  details.requestHeaders.push({
    name: 'User-Agent',
    value: customUa
  });
  
  return { requestHeaders: details.requestHeaders };
}

// 前面的sendMessage获取getCustomUserAgent,会被这里监听
const onRuntimeMessageListener = () => {
  chrome.runtime.onMessage.addListener(function (msg, sender, callback) {
    if (msg.type === 'getCustomUserAgent') {
      callback({
        customUa
      });
    }
  });
}

const init = () => {
  onRuntimeMessageListener()
  onBeforeSendHeadersListener()
}

init()

content.js

演示如何往网页中插入代码

function setScript({ code = '', needRemove = true } = params) {
  let textNode = document.createTextNode(code)
  let script = document.createElement('script')

  script.appendChild(textNode)
  script.remove()

  let parentNode = document.head || document.documentElement

  parentNode.appendChild(script)
  needRemove && parentNode.removeChild(script)
}

setScript({
  code: `alert ('hello world')`,
})

ua3.gif

关于一键设置ua插件

大体上和小例子差不都,只是功能相对复杂一些,会涉及到
  1. 数据本地存储chrome.storage.sync.get|setchrome.tabs.query等API
  2. popup与background通信、content与background通信
  3. 拦截请求修改UA
  4. 其他的大体就是常规的vue代码编写啦!

这里就不贴详细的代码实现了。

日常提效的一些思考

工作中咱们时长会遇到一些阻碍我们提高工作效率的问题,这些问题或许是因为老方案设计不合理、或许是因为流程又臭又长,又或许是现有功能不满足新的需求。等等,如果能做到这几点,不仅对自己的成长有所帮助,对团队也会有所贡献。
  1. 主人翁心态: 发现了问题主动尝试去解决问题,不做旁观者
  2. 保持学习力: 发现问题之后,解决方案如果不在你的知识储备范围,一定要尝试去学习新的东西(惭愧,没写一键设置UA插件之前,我自己完全没写过chrome插件),走出舒适圈,会学会更多
  3. 保持热心态:每个人遇到的问题是不一样的,主动和同事或者朋友讨论,需要时伸出你的双手
  4. 执行力:把影响效率(举例,还有其他)的事情看成魔鬼,马上行动起来,达到魔鬼,不要一拖再拖
  5. 学会推广:也许一开始你写的插件只是解决了自己的问题,但同样的工作环境,别人也许也会遇到,要学会往外分享和推广

相约再见

以上就是这篇文章的全部内容啦!愿大家晚安🌛,下次再见。

参考

  1. 【干货】Chrome插件(扩展)开发全攻略
  2. 油猴脚本编写教程

前端胖头鱼
3.7k 声望6.2k 粉丝