从零搭建自己的web宠物工具库

2,023 阅读9分钟

(一)前言

每次看到有些网站底部有类似二次元看板娘的东西都忍不住想去点点,作为一个前端天天和网页打交道,于是本着别人有,我也想有的精神,自己也得鼓捣一个。

但是看板娘只能挂在自己的网站上,便显得有些无趣,本着一物多用的态度,便想着能否搞个不只是挂在自己网站上的宠物看板娘,甚至是在任何网页上浏览时都会跳出这样的宠物,若是更进一步,还能复用成桌面应用搞成自己的QQ宠物级别应用,那这想法就太妙了。


依据这样的梳理逻辑:

① 个人网站上的宠物精灵(相当于网页这个平台) --> ② 升级为全网站通用的宠物精灵(相当于浏览器这个平台) --> ③ 最后有条件再升级为桌面级别宠物精灵(相当于桌面这个平台)

由于一个网页由html,js,css组成,

1、个人网站平台最简单,因为所有的东西都能由自己控制。

2、第二若想要全网站通用,就得从浏览器层面去加以控制,主要的实现方式便是通过浏览器插件的形式,将自己的宠物精灵当成一段js脚本,插进所有打开的网页中。

3、第三则是用electron等将一个内嵌的浏览器打包成桌面应用。(以后再说)

(二)Chrome 插件知识

由于chrome浏览器使用频率较高,所以选择使用chrome插件实现,将来的electron也是用的chrome内核,也许可以复用。
只简单测试了chrome,360浏览器。

简单的三个文件就能生成一个chrome插件

1.jpg

通过加载已解压的扩展程序导入包含这三个文件的目录即可

3.jpg

1.index.html

这是点击插件图标时打开的一个小窗口html网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互

2.jpg

2.manifest.json

manifest.json是Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。 其中manifest_version、name、version是3个不可缺少的参数

manifest.json简单配置

{
    "name": "plug",
    "description": "plug",
    "version": "1.0",
    "manifest_version": 2,
    "browser_action": {
        "default_popup": "index.html",
        "default_icon": "logo.png",
        "default_title": "plug"
    },
    "icons": {
        "16": "logo.png",
        "48": "logo.png",
        "128": "logo.png"
    },
    "background": {
        "scripts": ["background.js"]
    },
    "content_scripts": [{
        "matches": ["*://*/*"],
        "css": [
        ],
        "js": [
            "./dist/main.js"
        ],
        "run_at": "document_end"
    }]
}

content_scripts包含注入的文件信息

    "content_scripts": [{
        //表示匹配所有地址
        "matches": ["*://*/*"],
        "css": [
        ],
        //多个JS按顺序注入
        "js": [
            "./main.js"
        ],
        //代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
        "run_at": "document_end"
    }]

完成这一步,打开任意页面都能看到main.js被注入进去

注:关于chrome插件知识推荐这篇文章
blog.haoji.me/chrome-plug…

(三)使用webpack进行基础架构

配置文件manifest.json中可以单纯引入css和js,但多个css,js文件单独引入并不适合模块化开发,文件不压缩也会影响原有页面的体验。

使用webpack目的是将所有的js,css 打包成一个js

也可以直接选择一些vue,react等框架,为了熟悉webpack所以我选择从零开始搭建。

1.初始化项目和搭建目录结构

初始化项目:npm init

开发目录结构:

1.png

1.api为接口目录
2.config为配置页目录
3.routes为路由目录 4.src为主要功能目录
5.utils为工具目录

chrome插件目录结构:

开发打包后的内容放在dist中

2.png

2.安装webpack

cnpm install webpack webpack-cli --save-dev

3.依赖安装

在js文件里引用css,需要安装css-loader和style-loader

cnpm install css-loader style-loader --save-dev

添加babel-loader babel-core, babel-preset-env转换es6语法

cnpm i babel-loader babel-core babel-preset-env @babel/core @babel/preset-env --save-dev

4.配置

添加webpack.config.js配置文件

//webpack解析不了相对路径,只能解析绝对路径,需要引入path来解析路径
const path = require('path')
const ExtractTextPlugin = require("extract-text-webpack-plugin")
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

function resolve(dir) {
  return path.join(__dirname, dir)
}

// 打包优化选项
const threadLoader = {
  loader: 'thread-loader',
  options: {
    workers: require('os').cpus().length - 1,
  }
}
const cacheLoader = {
  loader: 'cache-loader'
}


let mainRoutes = require('./routes/main.js')
let configRoutes = require('./routes/config.js')

module.exports = {
  entry: {
    main: mainRoutes.map(res => {
      return res.path
    }),
    config: configRoutes.map(res => {
      return res.path
    })
  },
  output: {
    path: path.resolve(__dirname, '../ChromePlug/dist'), // 必须使用绝对地址,输出文件夹
    filename: '[name].js',
    // publicPath: 'build/'
  },
  resolve: {
    alias: {
      '@': resolve('/'),
      '@src': resolve('/src/'),
      '@utils': resolve('/utils/'),
      '@api': resolve('/api/'),
      '@config': resolve('/config/'),
    }
  },
  module: {
    rules: [
      // babel
      {
        test: /(\.js|\.jsx)$/,
        use: [
          cacheLoader,
          threadLoader,
          {
            loader: 'babel-loader',
            query: {
              presets: ["@babel/preset-env"]
            },
          }
        ],
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader'
      },
      // 图片格式正则
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: [{
          loader: 'url-loader',
          options: {
            // 图片大小小于10000B的会转换成base64
            limit: 10000,
            esModule: false,
            name: 'images/[name].[hash].[ext]'
          }
        }]
      }
    ]
  },
  // 插件列表
  plugins: [
    new ExtractTextPlugin("css/[name].[hash].css"),
    // 热更新模块
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      filename: './index.html',
      template: './index.html',
      inject: true
    }),
  ],
  devServer: {
    hot: true,
    host: '192.168.22.195',
    port: 8888
  }
}

虽然只需要打包js,但开发阶段需要本地启动项目,故添加一个index.html进行实时预览

devServer首先会在内存中创建类似的dist目录,再由浏览器打开进行预览。

  const HtmlWebpackPlugin = require('html-webpack-plugin')
  const webpack = require('webpack') 

  // 插件列表
  plugins: [
    // 热更新模块
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      filename: './index.html',
      template: './index.html',
      inject: true
    }),
  ]

添加.babelrc文件,对babel进行配置,babel-preset-env,用最新的规范做编译

.babelrc文件:

{
  "presets": [
      ["@babel/preset-env", {
        "loose": true,
        "modules": false
      }]
    ]
}

router文件夹加了要打包的模块的路径,进行统一管理

3.png

安装webpack-dev-server,修改package.json,用webapck启动

  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "serve": "webpack-dev-server --config webpack.config.js"
  },

js引入css:

require(index.css)

本地调试命令:

npm run serve

打包命令:

npm run build


由于后期需要在js中写大量dom元素

html和js混合方式大致有: 1.innerHTML 2.virtual dom 3.react jsx

免得麻烦会用到jsx,所以补充一些react配置:

cnpm i react react-dom

babel:

// @babel/core-babel核心模块 @babel/preset-env-编译ES6等 @babel/preset-react-转换JSX

cnpm i babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react -D

.babelrc文件: { "presets": ["@babel/preset-env","@babel/preset-react"], "plugins": ["@babel/plugin-transform-runtime"] }

(四)宠物制作

1.资源准备

由于希望宠物能进行互动,所以选择了骨骼动画这个方案。它的优点在于所有的骨骼都是相对独立的,可以根据自己的想法进行编排。

由于不会设计,所以在PDD花了几毛钱掏了几个带骨骼动画的人物、动物。

主要包含三个文件:

1.jpg

  1. .png 文件

最基本的就是一张png雪碧图,它包含了身体各个组成部分

2.jpg

  1. atlas 文件

atlas文件 则是各个骨骼部分在雪碧图中对应的位置。
例如:size:512,128 则是整张雪碧图的宽高大小,则head头的位置则是x:2,y:15,宽高是93和107
3.jpg
通过canvas 的drawImage就能画出单独的头
ctx.drawImage(img, 2, 15, 93, 107, 0, 0, 93, 107)
4.jpg

  1. .json文件

这个文件包含了上述骨骼部分如何组合的信息和一些骨骼动画的信息 比如:这里定义了一个伤心的动画
5.jpg
使用atlas和png就能简单搭建出来一个骨骼生物

6.jpg

2.动作设计

由于动作涉及过于复杂,所以只简单的让精灵的眼睛跟随鼠标进行移动。

  • 首先实时获取鼠标的位置
      let pos = {
        x: 0,
        y: 0
      }
      document.addEventListener('mousemove', (e) => {
        pos.x = e.pageX
        pos.y = e.pageY
      })
  • 然后根据鼠标位置改变眼睛的方位即可

7.jpg

由于眼睛只能在眼眶范围内移动,看向这个动作,只需要把眼睛向着鼠标的位置移动一小段距离即可

    let offsetX = pos.x / 250  

    let offsetY = pos.y / 250  

    ctx.drawImage(img, 416, 92, 30, 30, 12 + offsetX, 28 + offsetY, 30, 30) //left eye  

    ctx.drawImage(img, 416, 92, 30, 30, 48 + offsetX, 28 + offsetY, 30, 30) //right eye

3. 简单版本的完整代码

html:

<canvas id='canvas'></canvas>

script:


 let canvas = document.getElementById('canvas')
  canvas.width = 512
  canvas.height = 128
  let ctx = canvas.getContext('2d')
  if (ctx) {
    let img = new Image()
    img.src = './BOBO/BOBO.png'
    img.onload = () => {
      ctx.translate(100, 0)
      let pos = {
        x: 0,
        y: 0
      }
      document.addEventListener('mousemove', (e) => {
        pos.x = e.pageX
        pos.y = e.pageY
      })
      //根据浏览器的重绘时间调用绘画,render中也需要调用
      requestAnimationFrame(render.bind(window, img, ctx, pos))
    }
  }
  
  //画图
  function render(img, ctx, pos) {
    ctx.clearRect(0, 0, 512, 128)
    let offsetX = pos.x / 250
    let offsetY = pos.y / 250
    ctx.save()
    ctx.drawImage(img, 149, 14, 25, 29, 50, 100, 25, 29) //left foot
    ctx.drawImage(img, 432, 61, 25, 29, 15, 100, 25, 29) //right foot

    ctx.save()
    ctx.rotate(Math.PI * 2 / 4)
    ctx.drawImage(img, 97, 45, 69, 36, 35, -105, 69, 36) //right leg 
    ctx.restore()

    ctx.save()
    ctx.rotate(Math.PI * 2 / 4)
    ctx.drawImage(img, 189, 86, 69, 36, 35, -23, 69, 36) //left leg 
    ctx.restore()

    ctx.drawImage(img, 2, 15, 93, 107, 0, 0, 93, 107) //body
    ctx.drawImage(img, 97, 83, 90, 39, 0, 0, 90, 39) //hair

    ctx.drawImage(img, 416, 92, 30, 30, 12 + offsetX, 28 + offsetY, 30, 30) //left eye
    ctx.drawImage(img, 416, 92, 30, 30, 48 + offsetX, 28 + offsetY, 30, 30) //right eye
    ctx.save()
    ctx.rotate(Math.PI * 2 / 4)
    ctx.drawImage(img, 312, 98, 50, 24, 40, -57, 50, 24) //mouth
    ctx.restore()

    //根据浏览器的重绘时间调用绘画
    requestAnimationFrame(render.bind(window, img, ctx, pos))
  }

4.spine官方提供的调用方式

上述为简单的绘制过程,其实只需要一张png雪碧图即可,而atlas文件和json动画文件则是用的spine官方使用的文件格式,是在spine中制作好成品骨骼动画后导出的文件。

atlas文件的解析方式和json动画的调用都需要使用spine 官方提供的运行库来调用

8.jpg 附上github地址:github.com/EsotericSof…


由于需要将atlas,png等文件上传到服务器中(请看下文白嫖的服务器),所以会通过ajax请求获取文件,需要改动到源代码,这里我将canvas运行库的代码copy出来形成一个新的js

9.jpg

里面添加了两种获取文件的方式,

1.一是普通的ajax请求,但这种方式会有跨域的问题,而且由于无法更改服务器配置,所以不采用。
2.二则是通过上文介绍的chrome插件的形式进行请求,跨域产生的原因本身便是浏览器从中作祟,而通过插件从浏览器层面进行请求,就不会有跨域的问题。

10.jpg

这里将运行库的调用封装到了一个sprite类中,要生成什么样的宠物精灵就调用这个类传入相应的参数即可

11.jpg

    let props={
      id:'BOBO',
      width:120,
      height:120,
      parentDom:document.body, //父节点
      spinePath:'', //atlas,png等资源文件夹路径
      skelName:'BOBO', //atlas,png等资源的文件名,这里名字都是同一个
      animName:'sad' //动作名字
    }
    let sprite = new Sprite(props)
    sprite.init()

将所有宠物资源的信息整理到了一个list中,保存在spriteList.js,方便调用修改

12.png

(五)资源存放的服务器

由于没有属于自己的服务器,本着能白嫖就白嫖的策略,就把资源都存放到了gitee上,当然也可以选择github,但github这速度实在不敢苟同。

上文宠物制作提到的png,atlas等文件便通过这种方式获取。

1.首先建立一个Gitee Pages,则能够将它作为一个仓库,并且直接通过外链链接访问(并且作为登录回调页)

1.jpg

观察上文的spriteList文件就能发现,就是src->Sprite->pet目录下的资源

2.png

zaneblbl.gitee.io/z-plug/Z-Pl…

1.api的调用

由于很多资源或是api涉及到跨域的问题,无法简单通过ajax请求访问,但插件拥有一个可以运行在后台的background.js文件则不受影响,可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS。

首先通过chrome.extension.sendMessage发送消息给background.js,告诉它需要做什么事情

  //通知后台background.js发送跨域请求
  chrome.extension.sendMessage({
    'url': '',
    'type':'get'
  }, function (ret) {});

其次background.js通过chrome.extension.onMessage.addListener获取发送过来的消息,然后进行一些操作,再返回数据

chrome.extension.onMessage.addListener(function (objRequest, _, sendResponse) {
  var extension_type = objRequest.extension_type || ''
  var fun = objRequest.fun || ''
  var url = objRequest.url || '';
  var type = objRequest.type || 'get';
  var params = objRequest.params || {};
  // 其他需要background操作的事件,例如书签操作等
  if (extension_type == 'bookmark') {
    bookmark[fun].apply(this, params).then(result => {
      sendResponse(result);
    })
  }
  // 存储相关
  else if (extension_type == 'storage') {
    storage[fun].apply(this, params).then(result => {
      sendResponse(result)
    })
  }
  // ajax请求相关
  else {
    background_ajax(type, url, params, params.access_token).then((result) => {
      // 将正确信息返回content_script
      sendResponse(result);
    }).catch((error) => {
      // 将错误信息返回content_script
      sendResponse(error);
    });
  }
  //由于需要异步调用sendResponse,所以需要加上return true,通知sendResponse函数等待调用
  return true;
});

比较频繁的操作就是ajax的请求,这里简单封装了一下,原先的打算是使用webpack将background.js一并进行打包,但尝试后发现background.js运行报错,不然就可以直接使用axios而不必如此麻烦。

function blobToDataURI(blob, callback) {
  var reader = new FileReader();
  reader.readAsDataURL(blob);
  reader.onload = function (e) {
    callback(e.target.result);
  }
}
//  ajax请求封装
function background_ajax(type, url, params, access_token) {
  return new Promise((resolve, reject) => {
    var xhr = new XMLHttpRequest();
    if (type.toLowerCase() === 'get') {
      if (access_token) {
        url += ('?access_token=' + access_token)
      }
      xhr.onreadystatechange = function () {
        if (xhr.status === 200 && xhr.readyState === 4) {
          resolve(xhr.responseText);
        }
      };
      xhr.open('get', url, true);
      xhr.send();
    } else if (type.toLowerCase() === 'post') {
      let paramstr = ''
      paramstr = Object.keys(params).map(function (key) {
        return key + "=" + encodeURIComponent(params[key])
      }).join("&")
      xhr.onreadystatechange = function () {
        if (xhr.status === 200 && xhr.readyState === 4) {
          resolve(xhr.responseText);
        }
      };
      xhr.open('post', url, true);
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
      if (access_token) {
        xhr.setRequestHeader('Authorization', "token " + access_token);
      }

      xhr.send(paramstr);
    } else if (type.toLowerCase() === 'put') {
      xhr.onreadystatechange = function () {
        if (xhr.status === 200 && xhr.readyState === 4) {
          resolve(xhr.responseText);
        }
      };
      xhr.open('put', url, true);
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.send(JSON.stringify(params));
    } else if (type.toLowerCase() === 'getblob') {
      // blob图片获取
      xhr.onreadystatechange = function () {
        if (xhr.status === 200 && xhr.readyState === 4) {
          let result = xhr.response
          let blob = new Blob([result], {
            type: "image/png"
          })
          blobToDataURI(blob, function (result) {
            resolve(result);
          })
        }
      };
      xhr.responseType = "blob";
      xhr.open('get', url, true);

      if (access_token) {
        xhr.setRequestHeader('Authorization', "token " + access_token);
      }
      xhr.send();
    } else {
      console.log('不支持的请求格式');
    }
  });
}

2.gitee api调用

例如之后要做的保存书签功能,书签的信息需要保存到云端

并且书签的信息量不大,且只是些文本文件,唯一需要的一些操作无非就是增删改查

而增删改查gitee也提供了相应的api方便对文件进行这些操作

所以将gitee的常用api进行了封装

3.png

附上官方api地址:
gitee.com/api/v5/swag…

注:所有api访问都需要token私人令牌


获取文件内容需要先获取文件的sha参数,再通过sha去获取文件内容

import httpAjax from '@api/httpAjax.js'
import {Base64} from '@utils/utils'

// 获取文件信息
export function getFileData(opts){
  return new Promise((resolve)=>{
    getFileDataSha(opts).then(res=>{
      getFileDataContent({...opts,sha:res.sha}).then(result=>{
        resolve(result)
      })
    })
  })
}

// 获取文件sha
export function getFileDataSha(opts){
  return new Promise((resolve)=>{
      let url=`https://gitee.com/api/v5/repos/${opts.owner}/${opts.repo}/contents/${opts.path}`
      let params={
        access_token:opts.access_token,
      }
      httpAjax.get(url,params).then(res=>{
        resolve(res)
      })
  })
}

// 通过sha获取文件内容
export function getFileDataContent(opts){
  return new Promise((resolve)=>{
      let url=`https://gitee.com/api/v5/repos/${opts.owner}/${opts.repo}/git/blobs/${opts.sha}`
      let params={
        access_token:opts.access_token,
      }
      httpAjax.get(url,params).then(res=>{
        res.content=Base64.decode(res.content)
        resolve(res)
      })
  })
}

3.gitee oAuth2认证方式登录

由于这些api的操作都需要提供token,而私人令牌token可以直接生成。

4.png

但有个麻烦的点就是生成之后,若是不立马保存下来,gitee上是没法再次看到的,但若是直接保存在本机上,换台机子就没法获得这个token

所以思考能否使用自己创建的账号密码登录来获取token,而且账号密码的信息可以创建一个私有的库保存,只要通过gitee api调用比对就可以了,那美滋滋。

但实际并不适用,因为gitee api调用的前提得有token。

所以只能退而求其次通过oAuth2认证方式来获取token。

three.png

5.png

总结来说:
1.通过创建第三方APP获取到clientID和clientSecret进行请求,返回带有code的回调页面的url
2.解析这个url获取code
3.使用code获取token
4.保存token到chrome store中,这样就能确保所有页面都能拿到token

4.进行登录

但这里的问题在于如何解决回调页面url上带上的code的获取,因为oAuth2认证这种方式一般是用于个人网站使用gitee账号授权登录。

由于上文gitee服务器已配置过gitee pages,所以可以将它直接作为code的回调页面(也即是登录页面)。并且url上code使用插件获取即可。

require('./login.css')
import {
  getSearchObj
} from '@utils/utils'
import * as giteeApi from '@api/gitee/giteeApi'
import StorageConfig from '@config/storageConfig'
let login = {
  gitAccount: '',
  clientID: '',
  clientSecret: '',
  redirect_uri: '',
  access_token: '',
  loginSwitch: false,
  switch () {
    if (chrome.storage) {
      chrome.storage.sync.get('loginSwitch', (item) => {
        if (item.loginSwitch) {
          this.clear()
        } else {
          this.init()
        }
      })
    } else {
      if (this.loginSwitch) {
        this.clear()
        this.loginSwitch = false
      } else {
        this.init()
        this.loginSwitch = true
      }
    }
  },
  init(info) {
    let self = this
    let storage=null
    new StorageConfig().getStorage().then(res => {
      storage=res
      let {username,config__clientID,config__clientSecret,redirect_uri}=info?info:storage
      this.gitAccount = username ?? ''
      this.clientID = config__clientID ?? ''
      this.clientSecret = config__clientSecret ?? ''
      this.redirect_uri = redirect_uri ?? this.redirect_uri
      self.loginListener()
    })
  },
  clear() {
    let login = document.getElementById('zPlug__login__btn')
    if (login) {
      document.body.removeChild(login)
    }
    if (chrome.storage) {
      chrome.storage.sync.set({
        'loginSwitch': false
      })
    }
  },
  toLogin(opts) {
    giteeApi.getOauthToken(opts).then(res => {
      if (chrome.storage) {
        chrome.storage.sync.set({access_info:JSON.parse(res)})
        document.write('login success')
      }
    })
  },
  loginListener() {
    let code = getSearchObj()['code']
    if (code) {
      // 存在code,获取token
      let param = {
        'client_id': this.clientID,
        'client_secret': this.clientSecret,
        'code': code,
        'redirect_uri': this.redirect_uri
      }
      this.toLogin(param)
    } else {
      // 获取code
      let opts = {
        client_id: this.clientID,
        redirect_uri: this.redirect_uri
      }
      let url=giteeApi.getOauthCode(opts)
      window.location.href =url;
    }
  }
}

export default login

5.全局本地存储

由于插件运行在所有网页上,所以无法使用LocalStorage,sessionStorage,IndexedDB这些受同源策略影响,只能访问对应域名下的数据的方式。
chrome.storage则能获取浏览器下全局的本地存储

// 存储相关方法
let storage = {
  get: function () {
    if (chrome.storage) {
      return new Promise(resolve => {
        let obj = arguments[0] || null
        // 获取所有存储数据
        chrome.storage.sync.get(obj, function (items) {
          resolve(items)
        })
      })

    } else {
      console.log('no chrome storage');
    }
  },
  set: function () {
    if (chrome.storage) {
      return new Promise(resolve => {
        // 存储数据  
        let obj = arguments[0]
        let storage = {}
        Object.assign(storage, obj)
        chrome.storage.sync.set(storage, function () {
          resolve('success')
        });
      })
    } else {
      console.log('no chrome storage');
    }
  }
}

(六)工具制作

1.宠物功能

宠物工具主体展示,双击就可调出工具列表,按住拖动可以实现移动

1.png

所有的信息都在配置页中进行配置,gitee登录所需要的信息在这里填写。

4.png

2.天气功能

成品效果图:

2.png

实现步骤很简单:1.请求获取天气信息的api,2.按自己喜欢的方式展示出来

天气信息api

这里找了个免费版的api,直接调用即可:

wthrcdn.etouch.cn/weather_min…

信息UI展示

核心就几行代码

<!-- 创建div -->
let temp = document.createElement('div');
<!-- 添加到body页面上 -->
document.body.appendChild(temp);

当然要做的花里胡哨就是其他方面的工作了

其中的几个难点就是跨域的api请求问题和被限制的图片请求问题,毕竟这些api和图片都是网上随便找的,会有很多限制。
api的请求和跨域图片的获取在前文已经详细赘述,这里直接调用封装的方法,核心方法就是chrome.extension.sendMessage,绕过浏览器的同源策略,与background通信,让background代替发送请求

function get_window_data(city) {
  return new Promise((resolve, reject) => {
    try {
      chrome.extension.sendMessage({
        'url': window_url + city,
        'type': 'get'
      }, function (ret) {
        if (ret) {
          resolve(ret);
        }
      });
    } catch (e) {
      reject(e)
    }
  });
}

可移动

由于页面可能存在有多个可移动元素情况,使用单例模式统一进行管理移动元素列表

class StageMove{
  constructor(){
    this.targetList=[]
    this.target={}
    // 单例模式,new 这个类时,都会返回同一个实例
    if(!StageMove.instance) {
        StageMove.instance = this
    }
    return StageMove.instance
  }
  move(target){
    let self=this
    let tar=self.targetList.find(item=>{
      return item.target==target
    })
    if(!tar){
      // 多个可移动的列表
      self.targetList.push({
        target:target,
        isDrop:false,
        x:0,
        y:0,
      })
    }
    target.onmousedown = (e)=> {
      let tar=self.targetList.find(item=>{
        return item.target==target
      })
      var e = e || window.event; //要用event这个对象来获取鼠标的位置
      tar.x = e.clientX - tar.target.offsetLeft;
      tar.y = e.clientY - tar.target.offsetTop;
      tar.isDrop=true
      this.target=tar // 存储当前选择的移动对象
    }
    document.onmousemove = (e)=> {
      //当前选择对象是否为可移动状态
      let tar=self.target
      if (tar.isDrop) {
        var e = e || window.event;
        var moveX = e.clientX - tar.x; //得到距离左边距离                      
        var moveY = e.clientY - tar.y; //得到距离上边距离
  
        var maxX = document.documentElement.clientWidth - tar.target.offsetWidth;
        var maxY = document.documentElement.clientHeight - tar.target.offsetHeight;
        //范围限定  当移动的距离最小时取最大  移动的距离最大时取最小
        moveX = Math.min(maxX, Math.max(0, moveX));
        moveY = Math.min(maxY, Math.max(0, moveY));
        tar.target.style.left = moveX + "px";
        tar.target.style.top = moveY + "px";
      } else {
        return;
      }
    }
    document.onmouseup = ()=> {
      let tar=self.target
      tar.isDrop = false; //设置为false不可移动
    }
  }
}

调用,指定调用的元素即可

  let movetarget = document.getElementById('zPlug__main');
  new StageMove().move(movetarget);

ps:单例模式

1.保证一个类只有一个实例
2.为该实例提供一个全局访问节点。

3.翻译功能

3.png

4.书签功能

书签包含两个功能,一上传本地书签到远程服务器,二下载远程服务器书签替换本地书签

需要在gitee上新建仓库用以保存上传的书签文件

5.png

并且在配置页填写要保存到的路径

sign.png

  • 上传书签
  upload() {
    //获取本地书签列表,bookmark接口无法直接调用,走background返回
    chrome.runtime.sendMessage({
      'extension_type': 'bookmark',
      'fun': 'getBookMark'
    }, function (marklist) {
      //上传到github上
      new StorageConfig().getStorage().then(res => {
        let {
          username,
          bookmark_path,
          access_info
        } = res
        let params = {
          access_token: access_info.access_token,
          owner: username,
          repo: bookmark_path.substring(0, bookmark_path.indexOf('/')),
          path: bookmark_path.substring(bookmark_path.indexOf('/') + 1),
          content: JSON.stringify(marklist)
        }
        giteeApi.updateFile(params).then(res=>{
          if(res){
            alert('上传成功!')
          }
        })
      })
    });
  }
  • 下载书签
  download() {
    new StorageConfig().getStorage().then(res => {
      let {
        username,
        bookmark_path,
        access_info
      } = res
      let params = {
        owner: username,
        repo: bookmark_path.substring(0, bookmark_path.indexOf('/')),
        path: bookmark_path.substring(bookmark_path.indexOf('/') + 1),
        access_token: access_info.access_token
      }
      giteeApi.getFileData(params).then(data => {
        let marksobj = JSON.parse(data.content);
        if (marksobj) {
          // 获取根书签(id=0)的书签栏(id=1)列表(其他书签id=2)
          let marklist = marksobj.find(function (x) {
            return x.id === '0'
          }).children;
          // 删除再重新添加
          chrome.runtime.sendMessage({
            'extension_type': 'bookmark',
            'fun': 'remove',
            'params': ['0']
          }, function () {
            chrome.runtime.sendMessage({
              'extension_type': 'bookmark',
              'fun': 'createlist',
              'params': [marklist, '0']
            }, function () {
              if(res){
                alert('下载成功!')
              }
            });
          });

        }
      })
    })
  }
  • background.js 中对书签的操作方法封装
// 书签相关方法
let bookmark = {
  getBookMark: function () {
    return new Promise(resolve => {
      chrome.bookmarks.getTree(function (marklist) {
        resolve(marklist)
      })
    })
  },
  // 移除根目录书签,并重新下载新的书签列表
  remove: function () {
    let id = arguments[0] || '0'
    chrome.bookmarks.getChildren(id, function (marklist) {
      // 循环删除除id为1,2的书签列表
      for (let l in marklist) {
        if (marklist[l].id === '1' || marklist[l].id === '2') {
          bookmark.remove(marklist[l].id);
        } else {
          chrome.bookmarks.removeTree(marklist[l].id, function () {});
        }
      }
    });
  },
  //循环添加
  createlist: function () {
    let marklist = arguments[0] || []
    let parentId = arguments[1] || 0
    for (let m in marklist) {
      //父节点
      marklist[m].parentId = parentId;
      //id=1和id=2为固定不可更改书签
      if (marklist[m].id === '1' || marklist[m].id === '2') {
        bookmark.createlist(marklist[m].children, marklist[m].id);
      } else {
        //删除不必要的,书签id等会自动添加
        if (marklist[m].dateAdded) delete marklist[m].dateAdded;
        if (marklist[m].id) delete marklist[m].id;
        if (marklist[m].dateGroupModified) delete marklist[m].dateGroupModified;
        //存在文件夹时,递归调用
        if (marklist[m].children) {
          let children = marklist[m].children;
          delete marklist[m].children;
          chrome.bookmarks.create(marklist[m], function (msg) {
            bookmark.createlist(children, msg.id)
          });
        } else {
          chrome.bookmarks.create(marklist[m]);
        }
      }
    }
  }
}

最后附上gitee地址:
gitee.com/zaneblbl/z-…