【插件系列】别摸鱼了,来开发一个掘金专属版的chrome天气预报插件

如果生活有奇迹,那一定是努力的轨迹!

Hello,大家好,我是小羽同学,一个平凡而又不甘于平凡的前端开发工程师~

不知道小伙伴们有没有用过Octotree这个插件呢?

没用过也没关系。

因为github是在国外的,所以延迟还算是比较高的,当我们需要找一个隐藏的比较深的文件时,可能就需要花费不少时间。而Octotree这个插件主要是为我们的github提供了一个树形的目录结构,进来就加载了整个项目的目录,这样就可以快速找到我们所需要的的文件,节省了由于延迟需要的时间

image-20210613150747723

那小伙伴们有没有兴趣快速的撸一个自己的chrome插件,给掘金网站添加一些自己喜欢的功能呢?

来,下面小羽就带着大家撸一个可作用于掘金网站上的chrome插件。

img

1.创建chrome插件项目

这里的话小羽直接使用自己搭建的一个chrome插件脚手架进行项目的创建,该脚手架是基于vue3.x进行搭建的,详情可以点击这里去查看。

全局安装sulg-plugin-cli

npm install sulg-plugin-cli -g
复制代码

查看sulg-plugin-cli版本,出现如下图所示,说明脚手架安装成功

sulgPlugin -v
复制代码

image-20210205011012117

创建项目

sulgPlugin create douban-movie-plugin
复制代码

选择模板类型,包含了三个模板

default是不带任何ui框架

antd2.0则是基于ant-design-vue2.0框架的模板

elementPlus则为官方elementui的vue3版本

这里选择了elementPlus

image-20210613152854865

然后选择模板的下载地址,可以选择github或者gitee,国内的小伙伴建议选择gitee,速度会更快一些 。

image-20210613152915481

接着就是填入一些项目的相关信息,然后就会自动下载好我们的模板。

image-20210613152949861

2.启动项目

2.1安装modules

先安装node_modules

yarn install
复制代码

image-20210613155946934

启动命令有两个,一个是用来编辑插件的,一个用来编辑插件介绍页面的

让小羽来逐个说明。

2.2编辑插件命令

yarn watch
复制代码

这个就是编辑插件命令,它主要就是把我们的插件编译并拷贝到dist文件夹中(如下图),然后热更新。

打开我们的chrome浏览器,加载我们的刚刚的插件。

然后打开百度的网址,就可以发现在右下角多出了一个弹窗

image-20210613160218932

image-20210613160701110

image-20210613160921109

image-20210613161120869

2.3编辑插件介绍页命令

yarn serve
复制代码

该命令主要是用来撰写插件介绍页面的。注意这里的下载是暂时无法下载的,因为小羽写的初始化模板中的下载,需要结合dockerFile构建镜像,然后在通过docker部署服务,才可以下载的。

image-20210613170456867

3.编写插件

3.1 插件结构介绍

sulgPlugin创建的模板大体结构大概如下图所示。

  • mainfest.json是一个Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_versionnameversion3个是必不可少的,descriptionicons是推荐的。
  • content部分是chrome插件中可以向页面中注入的一种形式。
  • background部分是chrome拆件的常驻模块,随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在这里。
  • popup部分是点击插件icon后出现的一个小弹窗,生命周期就是弹窗显示的时间。

image-20210615221536985

3.2 修改插件配置

首先修改我们的插件使用网站,这里小羽修改为掘金的网址,然后打开掘金的页面,发现我们的插件已经可以在掘金的右下方显示一个小弹窗了。并且这时候我们重新打开一个百度的页面,在右下方也不会有弹窗的显示了。

路径:src/plugins/manifest.json

ps:如果要匹配所有的地址,matches的值为["<all_urls>"]

image-20210613172322047

image-20210613172621174

接着修改咱们插件的相关描述信息,然后移动鼠标到插件图标上/打开chrome的扩展程序面板,可以发现发现我们的插件描述信息已经发生了改变。

image-20210613212002154

image-20210613211859805

image-20210613212027970

如果小伙伴们想修改icon的话,修改这两处即可。

image-20210613212218945

3.3 调试api接口

删除旧的test.js文件,然后新增weather.js。

这里的话,用到了百度中国天气网的api接口。

image-20210615230043455

import axios from "axios";

class Weather {
  // 获取地理坐标
  async getAreaLocation(){
    return await axios.get(`https://map.baidu.com/?qt=ipLocation&t=${new Date().getTime()}`).then(res=>{
      if(!res.data.rgc || !res.data.rgc.result.location){
        // 如果定位失败则返回广州的地理位置
        return {
          lat: 23.188855624765,
          lng: 113.45977672508
        }
      }
      return res.data.rgc.result.location
    })
  }

  // 获取id
  async getAreaId(lat,lng){
    let params = {"method":"stationinfo",lat,lng}
    let res = await axios.get(`https://d4.weather.com.cn/geong/v1/api?params=${encodeURI(JSON.stringify(params))}`).then(res=>res.data.data.station.areaid)
    return res
  }
  // 获取天气信息
  async getWeather(areaId) {
    console.log("areaId",areaId)
    let res1 = await axios.get(`https://d1.weather.com.cn/wap40/${areaId}.html?_=${new Date().getTime()}`).then(res => {
      return JSON.parse(res.data.split('=')[1])
    })
    let res2 = await axios.get(`https://d1.weather.com.cn/sk_2d/${areaId}.html?_=${new Date().getTime()}`).then(res => {
      return JSON.parse(res.data.split('=')[1])
    })
    return {
      list: res1,
      current: res2,
    }
  }
}

const weather = new Weather()
export default weather;
复制代码

修改content/components/app.vue,所有演示文件直接删除,修改为如下的测试代码,保存后测试一下api接口是否正常。

<!--
 * @Description: 
 * @Author: 小羽
 * @LastEditors: 小羽
 * @Date: 2021-06-15 23:09:55
 * @LastEditTime: 2021-06-15 23:15:18
-->
<template>
	<div class="content-page">
		<el-button @click="apiTest">api测试</el-button>
	</div>
</template>

<script>
	export default {
    methods:{
      apiTest(){
        this.$api.weatherApi.getAreaLocation()
      }
    }
	}
</script>

<style lang="less" scoped>
.content-page {
  text-align: left;
  box-shadow: 0 0 5px #999;
  background: #fff;
  padding: 10px;
  //color: red;
  position: fixed;
  z-index: 100001;
  right: 0;
  bottom: 0;
}
</style>



复制代码

apitest

嗯,咱们的api测试按钮的确可以正常显示了,但是api请求跨域了!!!

跨域而已嘛,简单。

常见方法——cors:

我:后端大佬,你看方便帮忙加个cors头不?

后端大佬:哦哦,这个简单,要加那个接口?

我:就是那个百度的api接口,很简单吧,嘿嘿,两分钟能搞定了吧?

后端大佬:你说啥?百度的api接口,你信不信老子给你一锤?

常见方法——代理:

得,后端大佬帮不了忙,那就自己来写代理呗,多大点事。

nginx可以做代理,node貌似也ok。

那就整起来。

咳咳,兄弟,你是不是忘了一件事情。

掘金的nginx服务器在你手中吗?

你问过优弧大佬的意见没?

整天就在瞎搞!!!

哦豁,没得玩了,散场,小伙伴们下次见。。。

img

嘿嘿,开个玩笑,你们不会以为小羽是这么容易就放弃的人吧?【惊恐】

经过查阅资料后发现,background中的权限非常大。

大到什么程度呢?

可以直接无视浏览器的跨域问题。

那咱们继续走起。

修改manitest.json中的permissions

image-20210615234510407

修改src/background/main.js,然后在chrome扩展程序中打开咱们插件的background控制台。

可以发现没有报跨域的错误了,完美~

/*
 * @description: 
 * @author: 小羽
 * @Date: 2021-01-12 18:39:58
 * @LastEditTime: 2021-06-16 00:39:30
 * @Copyright: 1.0.0
 */
import hotReload from '@/utils/hotReload'
import api from "@/api/index.js"
import { getCurrentTabId } from "../utils/chrome"

hotReload()
console.log('this is background main.js')

api.weatherApi.getAreaLocation().then(res=>{
  console.log("api-test",res)
})

// chrome长连接消息发送
async function longConnectMsgSend(type,data){
  let connectObj = {
    content:"sulg-long-connect-content",
    popup:"sulg-long-connect-popup"
  }
  let tabId = await getCurrentTabId()
  let port = chrome.tabs.connect(tabId, {name: connectObj[type]});
  port.postMessage(data)
}
复制代码

image-20210616004213706

image-20210616004254988

3.4 content与background的通信

在这之前咱们先来学习两个知识点。

在chrome中有两种通信方式,短连接和长连接。

短链接(chrome.tabs.sendMessagechrome.runtime.sendMessage)的时间非常短,而且不会对async/await进行等待,只要断开了就需要重新发送。

而长连接(chrome.tabs.connectchrome.runtime.connect)的话,则可以进行async/await等操作。

小羽在脚手架生成的项目中,已经对短链接长连接做了一些简单的封装。直接使用即可。

修改src/content/components/app.vue

<!--
 * @Description: 
 * @Author: 小羽
 * @LastEditors: 小羽
 * @Date: 2021-06-15 23:09:55
 * @LastEditTime: 2021-06-16 01:01:04
-->
<template>
	<div class="content-page">
		<el-button @click="apiTest">api测试</el-button>
	</div>
</template>

<script>
  import { sortConnectMsgSend } from "../../utils/chrome";
	export default {
    methods:{
      async apiTest(){
        let res = await sortConnectMsgSend({ name: "getWeatherData",data:{}})
        console.log("收到短连接消息",res)
        chrome.runtime.onConnect.addListener(port=>{
        if(port.name == 'sulg-long-connect-content') {
          port.onMessage.addListener(msg => {
            console.log('收到长连接消息:', msg);
          });
        }
      });
      }
    }
	}
</script>

<style lang="less" scoped>
.content-page {
  text-align: left;
  box-shadow: 0 0 5px #999;
  background: #fff;
  padding: 10px;
  //color: red;
  position: fixed;
  z-index: 100001;
  right: 0;
  bottom: 0;
}
</style>



复制代码

修改src/backgournd/main.js

/*
 * @description: 
 * @author: 小羽
 * @Date: 2021-01-12 18:39:58
 * @LastEditTime: 2021-06-16 01:03:16
 * @Copyright: 1.0.0
 */
import hotReload from '@/utils/hotReload'
import api from "@/api/index.js"
import { getCurrentTabId } from "../utils/chrome"

hotReload()
console.log('this is background main.js')

// 监听来自content-script的消息
chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) {
  console.log("收到content发来的短连接消息")
  sendResponse(sender)
  let requectFunc = {
    getWeatherData:async data =>{
      let areaLocation = await api.weatherApi.getAreaLocation()
      let areaId = await api.weatherApi.getAreaId(areaLocation.lat,areaLocation.lng)
      let weatherData = await api.weatherApi.getWeather(areaId)
      longConnectMsgSend('content',{name:'getWeatherData',weatherData})
    }
  }
  requectFunc[request.name] && requectFunc[request.name](request.data)
});

// chrome长连接消息发送
async function longConnectMsgSend(type,data){
  let connectObj = {
    content:"sulg-long-connect-content",
    popup:"sulg-long-connect-popup"
  }
  let tabId = await getCurrentTabId()
  let port = chrome.tabs.connect(tabId, {name: connectObj[type]});
  port.postMessage(data)
}
复制代码

image-20210616010524070

emmm,可以看到我们的数据已经可以正常返回到前端了,剩下来的就是渲染页面啦。

20210616014142.gif

3.5 完成插件

其实到上一小结其实整个流程就已经基本完成插件的编写,剩下的就是简单的将数据渲染出来。

小羽还没怎么学vue3,所以还是使用vue2options api的方式(vue3也是兼容options api的)

修改src/content/components/app.vue

<!--
 * @description: 
 * @author: 小羽
 * @Date: 2021-01-12 18:40:31
 * @LastEditTime: 2021-06-16 00:55:38
 * @Copyright: 1.0.0
-->
<template>
  <div class="content-page">
    <div class="content-page-main" v-if="weatherData.current">
      <div class="content-page-main-left">
        <div class="weather-temp">
          <span class="weather-temp-num">{{weatherData.current.temp}}</span>
          <span class="weather-temp-symbol">°C</span>
          <span class="weather-temp-type">{{weatherData.current.weather}}</span>
        </div>
        <div class="weather-other">
          <div class="weather-other-item">湿度:{{weatherData.current.SD}} |</div>
          <div class="weather-other-item">PM2.5:{{weatherData.current.aqi_pm25}}</div>
        </div>
      </div>
      <div class="content-page-main-right">
        <div>{{weatherData.current.cityname}} {{weatherData.current.date}}</div>
        <div>更新时间: {{weatherData.current.time}}</div>
        <div>{{weatherData.current.WD}}</div>
      </div>      
    </div>
    <div ref="myChart" class="chart-box"></div>
  </div>
</template>

<script>
import { sortConnectMsgSend } from "../../utils/chrome";
export default {
  data() {
    return {
      //weatherData:{}
      weatherData:{
        
      }
    };
  },
  async mounted() {
    await this._initData();
  },
  methods: {
    /**
     * @description: 数据初始化
     * @Date: 2021-01-14 13:31:30
     * @author: 小羽
     * @param {*}
     * @return {*}
     */
    async _initData() {
      sortConnectMsgSend({ name: "getWeatherData",data:{}})
      chrome.runtime.onConnect.addListener(port=>{
        if(port.name == 'sulg-long-connect-content') {
          port.onMessage.addListener(msg => {
            console.log('收到长连接消息:', msg);
            if(msg.name === "getWeatherData"){
              this.weatherData = msg.weatherData
              console.log(this.weatherData.current)
              this.drawEchart()
            }
          });
        }
      });
    },

    drawEchart(){
      let timeList = [],hightTemp = [],lowTemp = []
      for(let i = 0;i<7;i++){
        let current = this.weatherData.list.datas[i]
        timeList.push(current.tm.match(/(\d{4})(\d{2})(\d{2})/).filter((item,index) => index > 0).join('-'))
        hightTemp.push(current.max)
        lowTemp.push(current.min)
      }
      let  options = {
              tooltip: {
                  trigger: 'axis'
              },
              color:["#ffdc51","#9dc0f7"],
              grid:{
                bottom:10,
                right:35,
                containLabel:true
              },
              legend: {
                  data: ['最高气温', '最低气温']
              },
              xAxis: {
                  type: 'category',
                  boundaryGap: false,
                  data: timeList
              },
              yAxis: {
                  type: 'value',
                  axisLabel: {
                      formatter: '{value} °C'
                  }
              },
              series: [
                  {
                      name: '最高气温',
                      type: 'line',
                      data: hightTemp,
                      markPoint: {
                          data: [
                              {type: 'max', name: '最大值'},
                          ]
                      }
                  },
                  {
                      name: '最低气温',
                      type: 'line',
                      data: lowTemp,
                      markPoint: {
                          data: [
                              {type: 'min', name: '最小值'}
                          ]
                      },
                      
                  }
              ]
          };
        this.$nextTick(()=>{
					let myChart = this.$echarts.init(this.$refs.myChart);
					myChart.setOption(options)
				})
    }
  },
};
</script>

<style lang="less" scoped>
.content-page {
  text-align: left;
  background: #1e77e9;
  color: #fff;
  box-shadow: 0 0 5px #999;
  padding: 10px;
  //color: red;
  position: fixed;
  z-index: 100001;
  right: 0;
  bottom: 0;
  &-main {
    display: flex;
    margin-bottom: 10px;
    &-left{
      width: 50%;
      .weather-temp{
        display: flex;
        align-items: flex-start;
        &-num{
          font-size: 32px;
          font-weight: bold;
        }
        &-type{
          margin-left: 5px;
        }
      }
      .weather-other{
        display: flex;
        align-items: center;
        &-item{
          margin-right: 5px;
        }
      }
    }
    &-right{
      width: 50%;
      text-align: right;
    }
  }
  .chart-box{
    width: 300px;
    height: 200px;
    background: #fff;
    border-radius: 5px;
    padding: 10px;
  }
}
</style>

复制代码

修改src/popup/components/app.vue

<template>
  <div class="popup_page">这是一个chrome天气插件</div>
</template>

<script>
export default {
  name: "popup",
  mounted() {
  },
  methods: {
  },
};
</script>

<style lang="less" scoped>
.popup_page {
  width: 200px;
  color: #fff;
  background: #1e77e9;
  padding: 10px;
  text-align: center;
}
</style>
复制代码

最终结果演示

success

4.编写插件介绍页面

这里的话小羽就简单的写下介绍页面。这个页面当初主要是设计用来介绍chrome插件的用法的,剩下的小伙伴们自己发挥吧~

修改src/index/components/app.vue

<!--
 * @Description: 
 * @Author: 小羽
 * @LastEditors: 小羽
 * @Date: 2021-06-13 15:29:37
 * @LastEditTime: 2021-06-24 00:16:12
-->
<template>
	<div class="index-page">
		<div class="index-page-main">
			<h1 class="chrome-title">掘金天气插件</h1>
			<div class="chrome-description">这是一个chrome天气插件,可作用于掘金网站。</div>
			<el-link href="/weather-plugin.zip" type="primary" target="_blank">下载</el-link>
		</div>
	</div>
</template>

<script>
	export default {

	}
</script>

<style lang="less" scoped>
	.index-page{
    display: flex;
		justify-content: center;
		&-main{
			text-align: center;
			width: 300px;
			border: solid 1px #eee;
			padding: 0 30px 30px 30px;
			.chrome{
				&-title{}
				&-description{
					text-indent: 2em;
					text-align: left;
				}
			}
		}
	}
</style>
复制代码

启动

yarn serve
复制代码

打开http://localhost:8080 ,就可以看到咱们的介绍页面就写好了

image-20210624005559390

5.关于部署

5.1 docker容器部署

sulgPlugin脚手架elementPlusantdv模板中默认配置了dockerFile

所以咱们可以通过dockerFile结合jenkins完成流水线(CI/CD)的一键部署,但是由于篇幅过长,在这里暂时就不说了。

咱们就简单的说下在win中配合docker desktop简单的完成部署。

docker不感兴趣的同学可以跳到下一小节了~

下载安装docker desktop

关于docker 的用法可以看这里

通过docker build命令,构建docker镜像,等待构建完成,然后打开docker desktop,可以看到咱们刚刚构建好的镜像。

docker build -t xiaoyu/weather-plugin:v1 .
复制代码

image-20210624015118228

然后可以点击run按钮,可视化启动docker。也可以通过docker run命令启动docker

docker run -itd --name weather -p 35256:80 xiaoyu/weather-plugin:v1
复制代码

image-20210624015200918

image-20210625002604595

然后点击打开页面的按钮,或者在浏览器中直接输入http://localhost:35256 ,即可看到咱们编写的插件介绍页面,点击下载后也是可以下载咱们的插件。

image-20210625003325742

关于怎么加载插件

大兄弟,请看2.2小节

img

5.2 传统服务器部署

当然除了dockerFile配合jenkins完成流水线的部署外咱们也是可以使用传统的方式部署。

通过yarn build打包,然后把dist中的文件压缩成zip文件,并重命名weather-plugin.zip

把index.html和需要的文件拷贝到nginx指向的路径中(小羽写的dockerFile中是没有分的,直接把整个dist文件拷走,反正也不大,关于需要的文件可以看下下图)

把weather-plugin.zip拷贝到对应的路径中

重启nginx,完成部署

image-20210625000918908

参考文献

www.cnblogs.com/liuxianan/p…

后语

本文小羽和小伙伴们基于sulgPlugin脚手架,结合vue3+axios完成了一个可作用于掘金网站上的chrome天气插件

不过,有点小小的遗憾,就是小羽最近刚刚转react,没怎么学vue3composition API,就没有献丑了。还是使用原来的options API的方式。

如果看这篇文章后,感觉有收获的小伙伴们可以点赞+关注哦~

img

如果想和小羽交流技术可以加下wx,也欢迎小伙伴们来和小羽唠唠嗑,嘿嘿~

ps:本文原创,转载请标明出处

最后奉上chrome天气插件的相关连接

官网链接:weather.sulg.top/

GitHub链接:github.com/sulgweb/wea…

如果条件允许的话,希望小伙伴们给小羽的github添加一颗小星星哦~

img

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享