From this article you can learn:

  1. Quickly use the Oil monkey plugin for browser plug-in development
  2. Use Wokoo scaffolding to develop two interesting plug-ins: word search MoveSearch and Zhihu directory zhihu-Helper
  3. If you have the energy, you can continue to learn wokoo scaffolding.

Why learn browser plug-in development

Browser plug-in development some students may not contact much, or have questions, learning to develop browser plug-in what is the use? Here are some of the scenarios at work:

Scene:

When the test students find problems, they need to take screenshots, open the test platform, fill in specific questions and submit the test report. If a plugin pops up a test form to fill in on the current page, wouldn’t it reduce the workload of testing students?

Once you’ve learned how to develop browser plugins, you can make some plugins related to work scenarios to help yourself and your colleagues improve efficiency. This is a plus whether it’s a career promotion or a job change.

Start plug-in development

MoveSearch (MoveSearch

The finished product to show

Installation Address:

  1. Install the Oilmonkey plug-in
  2. Install MoveSearch

Wokoo /example/MoveSearch

Development steps

1.1 Project Installation & Initial configuration

npm i wokoo -g
wokoo MoveSearch
Copy the code

Select a template

  • vue
  • react

I’m gonna say react

After the installation is complete, a message is displayed indicating that the installation succeeded

Execute the command as prompted

cd MoveSearch
npm start
Copy the code
  • Open the Oilmonkey script editor and copy the contents of tampermonkey.js into it. (Note: this refers to copying everything in tampermonk. js, including comments. Because the commented //@xxx in this file has meaning, it can be understood in the tamperMonkey development documentation. For example, @match https:// means the plugin will only be launched if the page is HTTPS.)

  • Open the web developer search, the monkey logo appears in the upper right corner, indicating that the environment is running

1.2 Basic Functions

To achieve the delimit word search function, the main steps are as follows:

  1. Listen for the mouseup event, check if any text is selected when triggered, and pop up if so
  2. Send a request to the developer search interface of Baidu to obtain the search content before pop-up display
  3. If the search content is not empty, display the content in the popover
  4. Boundary detection, click the content outside the popup area, popup closed

There is a difficulty here, that is, the developer search interface of Baidu is not allowed to cross domains, for example, the interface requesting developer search in the gold digging webpage will be blocked. To solve this problem, I used the Serverless service provided by Vercel to do a proxy. In the proxy, I added access-Control-Allow-Origin and other fields to the response header to enable cross-domain. The specific method is given in 1.3. At this time, we first develop in the developer search page, and ensure that it is in the same domain.

Step 1. Listen for the mouseup event and check whether any text is selected when triggered

Edit/SRC /app.js file to add mouseup listening:

import React from 'react'
import axios from 'axios'
import './app.less'

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = { show: false}}componentDidMount() {
    document.addEventListener('mouseup'.(e) = > {
      var selectionObj = window.getSelection()
      var selectedText = selectionObj.toString()
      console.log('selectedText', selectedText)
    })
  }

  render() {
    let { show } = this.state
    return <>{show ? <div className="Wokoo"></div> : null}</>}}Copy the code

Step 2. Send the request before the pop-upDeveloper SearchTo get the search content

  • Install axiOS and import AxiOS
  • Modify the code in componentDidMount to ask the developer to search for results
componentDidMount() {
    document.addEventListener('mouseup'.(e) = > {
      var selectionObj = window.getSelection()
      var selectedText = selectionObj.toString()
      console.log('selectionObj::', selectedText)
      if (selectedText.length === 0) {}else {
        axios
          .get(
            `https://kaifa.baidu.com/rest/v1/search?query=${selectedText}&pageNum=1&pageSize=10`
          )
          .then((res) = > {
            let { data } = res.data.data.documents
            console.log(data)
            if (data.length) {
              this.setState({
                data: data,
                show: true,})}})}})}Copy the code
  • Modify render to display search results
render() {
    let { show, data } = this.state
    return (
      <>
        {show ? (
          <div className="Wokoo">
            <ul>
              {data.map((i) => (
                <li>{i.title}</li>
              ))}
            </ul>
          </div>
        ) : null}
      </>)}Copy the code

At this point, you can see the search results in the web page, and the basic functionality is done.

Step 3 Calculate the position of the popover and make it appear under the selected text

We can conclude that the calculation formula is:

left = x + width/2 – MODAL_WIDTH/2

top = y + height

At this point, the app.js code is modified to look like this:

import React from 'react'
import axios from 'axios'
import './app.less'
const MODAL_WIDTH = 350
export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = { show: false.data: []}}componentDidMount() {
    document.addEventListener('mouseup'.(e) = > {
      var selectionObj = window.getSelection()
      var selectedText = selectionObj.toString()
      if (selectedText.length === 0) {}else {
        var selectionObjRect = selectionObj
          .getRangeAt(0)
          .getBoundingClientRect()
        let { x, y, height, width } = selectionObjRect // Get the position of the selected text, x y is the horizontal and vertical coordinates, height width is the height and width of the selected text
        // Select top and left
        var left = x - MODAL_WIDTH / 2 + width / 2
        left = left > 10 ? left : 10
        var top = y + height
        var scrollLeft =
          document.documentElement.scrollLeft || document.body.scrollLeft
        var scrollTop =
          document.documentElement.scrollTop || document.body.scrollTop
        axios
          .get(
            `https://kaifa.baidu.com/rest/v1/search?query=${selectedText}&pageNum=1&pageSize=10`
          )
          .then((res) = > {
            let { data } = res.data.data.documents
            if (data.length) {
              this.setState({
                data,
                show: true.selectedText: selectedText,
                modalPosition: {
                  left: left + scrollLeft,
                  top: top + scrollTop,
                },
              })
            }
          })
      }
    })
  }

  render() {
    let { show, selectedText, modalPosition, data } = this.state
    return (
      <>
        {show && data && data.length ? (
          <div
            className="move-search"
            id="MoveSearchApp"
            style={{
              . modalPosition,}} >
            <div className="move-search-content">
              <ul className="move-search-ul">
                {data.map((l) => (
                  <li className="move-search-li" key={l.id}>
                    <a href={l.url} target="_blank">
                      {l.title}
                    </a>
                    <span>{l.summary}</span>
                  </li>
                ))}
              </ul>
            </div>
            <div className="move-search-bottom-fade"></div>
            <footer className="move-search-footer">
              <a
                href={`https://kaifa.baidu.com/searchPage?wd=${selectedText}`}
                target="_blank"
              >
                Read More
              </a>
            </footer>
          </div>
        ) : null}
      </>)}}Copy the code

Don’t forget to add styles and modify app.less

.move-search {
  position: absolute;
  text-align: center;
  width: 350px;
  max-height: 300px;
  top: 0;
  right: 0;
  border-radius: 5px;
  z-index: 2147483647;
  box-shadow: rgba(0.0.0.0.2) 0px 16px 100px 0px;
  transition: all 0.1 s ease-out 0s;
  border: 1px solide #282a33;
  background: #fff;
  font-size: 12px;
  overflow: scroll;
  text-align: left;
}
Copy the code

Step 4. Boundary detection: Click the popover section to hide the popover

The judgment conditions of boundary detection are as follows:

left+width > x > left && top+height > y > top

function boundaryDetection(x, y, modalPosition = { left: 0, top: 0 }) {
  let { left, top } = modalPosition
  if (
    x > left &&
    x < left + MODAL_WIDTH &&
    y > top &&
    y < top + MoveSearchApp.offsetHeight
  ) {
    return true
  }
  return false
}
Copy the code

Modify app.js to add popover vanishing logic, and determine whether the mouse position is inside the popover when selectedText is empty.

import React from 'react'
import axios from 'axios'
import './app.less'
// Popover width
const MODAL_WIDTH = 350
/** * Border detection, mouse clicks outside of Modal, modal hides *@param {*} X X position of the mouse *@param {*} Y Y position of the mouse *@param {*} ModalPosition pop-ups left and top */
function boundaryDetection(x, y, modalPosition = { left: 0, top: 0 }) {
  let { left, top } = modalPosition
  if (
    x > left &&
    x < left + MODAL_WIDTH &&
    y > top &&
    y < top + MoveSearchApp.offsetHeight
  ) {
    return true
  }
  return false
}
export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = { show: false.data: []}}componentDidMount() {
    document.addEventListener('mouseup'.(e) = > {
      var selectionObj = window.getSelection()
      var selectedText = selectionObj.toString()
      if (selectedText.length === 0) {
        if (this.state.show) {
          // Recalculate whether to close the popover
          // Check whether the mouse position is inside the popover, if not, close the popover
          var inModal = boundaryDetection(
            e.clientX,
            e.clientY,
            this.state.modalPosition
          )
          if(! inModal) {this.setState({
              show: false.data: [],})}}}else {
        var selectionObjRect = selectionObj
          .getRangeAt(0)
          .getBoundingClientRect()
        let { x, y, height, width } = selectionObjRect // Get the position of the selected text, x y is the horizontal and vertical coordinates, height width is the height and width of the selected text
        // Select top and left
        var left = x - MODAL_WIDTH / 2 + width / 2
        left = left > 10 ? left : 10
        var top = y + height
        var scrollLeft =
          document.documentElement.scrollLeft || document.body.scrollLeft
        var scrollTop =
          document.documentElement.scrollTop || document.body.scrollTop
        axios
          .get(
            `https://kaifa.baidu.com/rest/v1/search?query=${selectedText}&pageNum=1&pageSize=10`
          )
          .then((res) = > {
            let { data } = res.data.data.documents
            if (data.length) {
              this.setState({
                data,
                show: true.selectedText: selectedText,
                modalPosition: {
                  left: left + scrollLeft,
                  top: top + scrollTop,
                },
              })
            }
          })
      }
    })
  }

  render() {
    let { show, selectedText, modalPosition, data } = this.state
    return (
      <>
        {show && data && data.length ? (
          <div
            className="move-search"
            id="MoveSearchApp"
            style={{
              . modalPosition,}} >
            <div className="move-search-content">
              <ul className="move-search-ul">
                {data.map((l) => (
                  <li className="move-search-li" key={l.id}>
                    <a href={l.url} target="_blank">
                      {l.title}
                    </a>
                    <span>{l.summary}</span>
                  </li>
                ))}
              </ul>
            </div>
            <div className="move-search-bottom-fade"></div>
            <footer className="move-search-footer">
              <a
                href={`https://kaifa.baidu.com/searchPage?wd=${selectedText}`}
                target="_blank"
              >
                Read More
              </a>
            </footer>
          </div>
        ) : null}
      </>)}}Copy the code

At this point, the basic functionality of the project has been developed πŸŽ‰, and some style issues can be adjusted by themselves.

The code logic can be found at github address: MoveSearch

1.3 Resolving Cross-domain Problems

Baidu’s interface does not allow cross-domain access. That is to say, if we are in a third party page, such as CDN, nuggets and so on, there are cross-domain restrictions to access baidu’s interface, so we cannot access it. I enabled cross-domain by adding the node middle layer and adding access-Control-Allow-Origin headers to the response in Node.

At present, there are ready-made third-party manufacturers that provide serverless service, providing servers, Node environment and other services. As long as we care about the code logic of Node, we can leave the deployment server configuration environment and other problems to the third-party manufacturers to solve. I use services provided by Vercel.

Have much relationship with the following content and plug-in development, if not interested students can directly use I configured domain name: https://movesearch.vercel.app/api/baidu. That is, the URL requested by AXIos in the code is changed from

`https://kaifa.baidu.com/rest/v1/search?query=${selectedText}&pageNum=1&pageSize=10`
Copy the code

replace

`https://movesearch.vercel.app/api/baidu? query=${selectedText}&pageNum=1&pageSize=10`Copy the code

If you are interested in how to implement cross-domain configuration, you can go to πŸ‘‡.

  1. Log in to the Vercel website and follow the instructions to create a Github account.
  2. Click New Next project. Vercel automatically creates new repositories for you on Github and has an initialization project; (Note: you will be asked to enter the repository name, which is associated with the domain name.)
  3. Git clone the project to the local directory and follow the readme instructionsnpm run dev, start the project
  4. increasesrc/api/baidu.jsFile, as follows:

SRC/API /baidu. Js files correspond to/API /baidu interface

const { createProxyMiddleware } = require('http-proxy-middleware')

// restream parsed body before proxying
var restream = function (proxyRes, req, res, options) {
  proxyRes.headers['Access-Control-Allow-Origin'] = The '*'
  proxyRes.headers['Access-Control-Allow-Headers'] = The '*'
  proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
}

const apiProxy = createProxyMiddleware({
  target: 'https://kaifa.baidu.com/'.changeOrigin: true.pathRewrite: { '^/api/baidu': '/rest/v1/search' },
  secure: false.onProxyRes: restream,
})

module.exports = function (req, res) {
  apiProxy(req, res, (result) = > {
    console.log('result:', result)
    if (result instanceof Error) {
      throw result
    }
    throw new Error(
      `Request '${req.url}' is not proxied! We should never reach here! `)})}Copy the code

Git push directly after the modification is complete. After the code is pushed to the repository, Vercel automatically pulls the latest code updates to its server, and we just call the interface to see if it works. The specific code logic can be seen at github address: nextjs

Second, Zhihu column directory zhihu-Helper

The finished product to show

Installation Address:

  1. Install the Oil monkey plugin (if it was installed, do not install it again)
  2. Install zhihu – helper

Code address: wokoo/zhihu- Helper

Development steps

1.1 Project Installation & Initial configuration

npm i wokoo -g
wokoo zhihu-help
Copy the code

Select a template

  • vue
  • react

Select React and wait for the project to install. After the project is installed, run the following command as prompted:

cd zhihu-helper
npm start
Copy the code
  • Open the Oilmonkey script editor and copy the contents of tampermonkey.js into it.

  • When you open the zhihu column, a monkey icon appears in the upper right corner, indicating that the project has been completed.

added

Some browsers here do not display the monkey icon. An error occurs when you open the console:

If you look at the response header for the requested HTML resource, you can see that there is an additional content-security-policy rule. In other words, Zhihu uses the CSP content security policy. According to the script-src field in Content-Security-Policy, Zhihu only allows js of the specified domain name to be loaded. For details, see πŸ‘‰ Content Security Policy (CSP).

There are two ways to bypass this security policy:

  1. Installing a plug-inDisable Content-Security-PolicyIn the debugging zhihu page open plug-in, automatically put the HTML pagecontent-security-policySet to null.

Note, after opening the page, click the plug-in, the button becomes color only then open successfully

  1. For those of you who know how to configure Charles, you can set a forwarding rule

One of these two methods will do.

Web pages that encounter this CSP content security policy cannot host the CDN when they are online to the Oil Monkey store and must copy the code into the edit box.

1.2 Develop basic functions

To compose

  1. Draw the left drawer popover
  2. Request zhihu list interface when popup, get list data
  3. More functionality is loaded when the drop – down is implemented

Let’s implement it step by step

Step 1. Draw the left drawer popover

This. State. Show controls the display and hiding of pop-ups. This step is quite simple, you can directly look at the code :step1, you can replace the app in the index.js entry with step1 to see the effect.

Step 2. RequestZhihu listInterface to get list data

  • Install axios and import
npm install axios
Copy the code
  • Calculate request parameters

Analyzing the URL of the request, the request interface is as follows:

`https://www.zhihu.com/api/v4/columnsThe ${this.queryName}/items? limit=20&offset=${offset}`
Copy the code

Where this.queryName is the column name, or pathname when the page is a column list page; When the page is a column detail page, it needs to be retrieved from the href of the A tag of the columnPageHeader-titlecolumn class.

The getQueryName method should be better understood from these two diagrams

getQueryName = () = > {
    let pathname = location.pathname
    let detailRegExp = /^\/p\/\d+/
    let queryName = ' '
    // Column details page
    if (detailRegExp.test(pathname)) {
      let aTage = document.getElementsByClassName(
        'ColumnPageHeader-TitleColumn'
      )
      let url = aTage[0].href
      queryName = url.slice(url.lastIndexOf('/'))}else {
      // Column list page
      // both http://zhuanlan.zhihu and http://zhihu/column are columns
      if (pathname.indexOf('/column') = = =0) {
        pathname = pathname.slice('/column'.length)
      }
      queryName = pathname
    }
    this.queryName = queryName
  }
Copy the code

And list page there are two types of domain name: www.zhihu.com/column/mand… And zhuanlan.zhihu.com/mandy so https://www.zhihu.com/column/mandy against doing a deal with in the else logic, only keep/mandy

With the getQueryName method, we get the request parameters

  • Send the request and pull the directory list
getList = async() = > {if (!this.state.hasMore) return
    let { offset } = this.state
    let { data } = await axios.get(
      `https://www.zhihu.com/api/v4/columnsThe ${this.queryName}/items? limit=20&offset=${offset}`
    )
    let list = data.data.map((i) = > ({
      title: i.title,
      url: i.url,
      id: i.id,
      commentCount: i.comment_count,
      voteupCount: i.voteup_count,
    }))
    if (data.paging.is_end) {
      this.setState({ hasMore: false })
    }
    offset += limit

    this.setState({
      list: [...this.state.list, ...list],
      offset,
    })
  }
Copy the code

Step2 of the process is completed, the code is here πŸ‘‰step2, you can replace the app in the index.js entry with step2 to see the effect.

At this point, the plugin looks like this, except that it has no pull-down loading.

Step 3. Load more functions when you drop down

The infinite scroll component draws on the react-infinite scroll component library:

npm install react-infinite-scroll-component
Copy the code

Add the InfiniteScroll component to the render function. Note that the height of InfiniteScroll needs to be computed to a fixed value, otherwise it will not trigger scrolling.

<ul className="list-ul" onMouseLeave={this.handleMouseLeave}>
  <InfiniteScroll
		dataLength={list.length}
		next={this.handleInfiniteOnLoad}
		hasMore={hasMore}
		loader={<h4>Loading...</h4>}
    height={document.documentElement.clientHeight - 53}
		endMessage={
  		<p style={{ textAlign: 'center' }}>
    		<b>After all, no content ~</b>
			</p>
		}
  >
    {list.map((i) => (
      <li className="list-li" key={i.id}>.</li>
))}
  </InfiniteScroll>
</ul>
Copy the code

At this point, the function has been developed, and the specific code can be found at πŸ‘‰app.js

Deploy the plugin to the Oilmonkey store

3.1 build

Execute the command

npm run build
Copy the code

3.2 Verify the oilmonkey script file tampermonkey.js

The annotated //@xxx in this file has meaning and can be understood in the TamperMonkey development documentation.

  • @description Plug-in description

  • @match Specifies whether to enable the plug-in under certain domain names. Two plug-ins are configured by default. // @match https://*/* and // @match https://*/* indicate that the plug-in is enabled under all domain names. But here you want to use this plug-in only in the ZHIhu column, so change the @math field.

    // @match https://zhuanlan.zhihu.com/*
    // @match https://www.zhihu.com/column/*
    Copy the code
  • @require Oil monkey script internal help to introduce third party resources, such as jquery, React etc.

    // @require https://unpkg.com/react@17/umd/react.production.min.js
    // @require https://unpkg.com/react-dom@17/umd/react-dom.production.min.js
    Copy the code

3.3 Release plug-ins to the Oil monkey market

The advantage of releasing the oil monkey market is that it is very convenient without review.

  1. Deploy the /dist/app.bundle.js file to the CDN and obtain the corresponding URL.

Note:

  • Js files can be put on Github, if hosted on Github it is best to do CDN acceleration (I use cdn.jsdelivr.net for CDN acceleration).

  • If no CDN server is available, skip this step and copy app.bundle.js directly to the oil Monkey script editor in Step 4

  1. Log in to the Oil Monkey marketplace with a Google account or Github account.

  2. Click on the account name and then “Publish your script”

  3. Go to the edit page and copy the contents of tampermonk. js into the edit box

    Note:

    • If the CDN is hosted in Step 1, you need to replace the LOCALhost :8080 URL in the code with the static resource URL

    • Since there is no managed CDN in Step 1, you cannot directly copy the contents in /dist/app.bundle.js to the edit box. We built app.bundle.js to build libraries like React into the edit box because there was a maximum limit to the code in the edit box.

      The build results need to be unpacked

      4.1 Modify tampermonk. js to introduce react and react-dom via @require

      // ==UserScript==
      // @name zhihu-helper
      // @namespace http://tampermonkey.net/
      / / @ version 0.0.1
      // @description zhihu directory
      // @author xx
      // @match https://zhuanlan.zhihu.com/*
      // @match https://www.zhihu.com/column/*
      // @require https://unpkg.com/react@17/umd/react.production.min.js
      // @require https://unpkg.com/react-dom@17/umd/react-dom.production.min.js
      
      // ==/UserScript==
      
      // app.bundle.js built code
      Copy the code

      4.2 Modifying the entry field of webpack.config.base.js

      entry: {
          app: '/src/index.js'.vendor: [
            // React-dom and react are packaged separately to reduce the size of the package file
            'react'.'react-dom',]},Copy the code

      4.3 Re-execute NPM run build to build new app.bundle.js and copy it to the edit box of oil Monkey market.

  4. Click “Publish Script”

Wokoo scaffolding construction

If you are interested, you can read wokoo Scaffolding.