前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue仿掘金chrome插件

vue仿掘金chrome插件

作者头像
Maic
发布2022-12-21 19:19:13
7980
发布2022-12-21 19:19:13
举报

掘金chrome插件装在浏览器上,既可记笔记,又可看github热点项目,还可交友吃瓜,这沸点就差个树洞功能了,掘金为广大程序员提供了交友的机会,今年脱单看沸点,明年生娃取名,也看沸点了。

言归正传,掘金导航这个插件功能还是挺可以的,那么我们自己写个chrome掘金导航插件吧。

在开始正文之前,内容主要会从以下几点去认识一个掘金插件有哪些功能

  • 我们一安装插件,导航首页都换了,这是怎么实现的
  • 掘金插件的换肤是怎么实现的
  • 学会EazyMock模拟一份在线接口数据
  • 不考虑工程化构建,如何使用vue实现一个掘金插件基础功能

正文开始...

manifest.json

这是一个插件基础配置,插件配置必要的一个配置


{
    "manifest_version": 3,
    "version": "0.1",
    "name": "仿掘金导航",
    "author": "maicFir",
    "description": "仿掘金导航",
    "action": {
        "default_title": "仿掘金导航",
        "default_popup": "index.html#/popup",
        "default_icon": "images/logo.png"
    },
    "icons": {
        "16": "images/icon-16.png",
        "32": "images/icon-32.png",
        "48": "images/icon-48.png",
        "128": "images/icon-128.png"
    },
    "content_scripts": [
        {
            "js": [
                "scripts/content.js"
            ],
            "matches": [
                "<all_urls>"
            ]
        }
    ],
    "chrome_url_overrides": {
        "newtab": "index.html"
    },
    "host_permissions": [
        "<all_urls>"
    ]
}

我们有看到配置中,default_popup:index.html#popup,我们的打开的popup与我们访问的当前页面都是index.html,我们通过控制hash值来显示对应的内容

这在官方掘金插件也是是这么做的,他是有用到vue-router,根据不同路由显示不同的内容,不过我们这里并不打算用这个路由插件

我们在根目录下新建一个index.html,然后引入vue3,但是此时会报错

vue.global.min.js:15 Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".

查询资料所得,原来manifest_version: 3已经禁用了evelnew Function,所以只能弃用vue3,看掘金插件源码里有用到vue的组合式API源码里也有new Function,但这是打包后的混淆代码,我怀疑是通过babel插件做了特殊处理的。

这个版本不准备用工程化方式去构建,我准备用最原始的方式去模仿写个插件,而且也会vue版本,不过此时vue的版本是csp版本

默认打开首页

当我们安装插件时,此时导航栏默认变成掘金的了,那这个是怎么实现的呢,主要归功于manifest.json的这个配置,会覆盖当前的新标签默认首页


 "chrome_url_overrides": {
        "newtab": "index.html"
    },

插件主页面index.html


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>仿掘金导航</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div id="app"></div>
    <script src="./scripts/fetch.js"></script>
    <script src="./scripts/vue.csp.js"></script>
    <script src="./index.js"></script>
  </body>
</html>

插件主页面很简单

我们发现掘金插件的主要模块功能主要以下几块

  • 头部
  • 搜索框,快捷导航
  • 热点模块内容,主要分了三大区域
  • 设置操作抽屉

我们看下头部是怎么实现的,在index.js


// index.js
const initData = {
    listData: [],
    gitHubData: [],
    fdlistData: [],
    formCondation: {
        type: '2',
        source: '0',
        listStatus: 0,
        gitStatus: 0,
        fdStatus: 0
    },
    theme: 'light', // light dart
    typeData: [
        {
            value: '0',
            name: '前端'
        },
        {
            value: '1',
            name: '后端'
        },
        {
            value: '2',
            name: '综合'
        }


    ],
    githubData: [
        {
            value: '0',
            name: 'Github'
        },
        {
            value: '1',
            name: 'Gitee'
        },
        {
            value: '2',
            name: '博客园'
        }
    ],
    navConfig: {
        left: [
            {
                name: '课程',
                link: '',
                popup: true,
            },
            {
                name: 'APP',
                link: '',
                popup: true,
            },
            {
                name: '马上掘金',
                link: '',
                popup: false,
            },
            {
                name: '会员',
                link: '',
                popup: true,
            }
        ],
        right: [
            {
                type: 'theme',
                Icon: '<svg class="icon-night" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-v-1c036078=""><path d="M13 2C13.6567 2 14.3059 2.06344 14.9409 2.18842L16.488 2.49289L15.553 3.76254C14.5494 5.12552 14 6.7699 14 8.5C14 12.3391 16.725 15.617 20.4453 16.3492L21.9921 16.6536L21.0575 17.9232C19.1853 20.4667 16.2196 22 13 22C7.47715 22 3 17.5228 3 12C3 6.47715 7.47715 2 13 2Z" data-v-1c036078=""></path></svg>',
                activeIcon: '<svg class="icon-day" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-v-1c036078=""><path fill-rule="evenodd" clip-rule="evenodd" d="M13 1.1C13 0.768629 12.7761 0.5 12.5 0.5H11.5C11.2239 0.5 11 0.768629 11 1.1V2.9C11 3.23137 11.2239 3.5 11.5 3.5H12.5C12.7761 3.5 13 3.23137 13 2.9V1.1ZM20.4998 4.20711L19.7927 3.5C19.5975 3.30474 19.2809 3.30474 19.0856 3.5L17.6567 4.92894C17.4614 5.1242 17.4614 5.44078 17.6567 5.63605L18.3638 6.34315C18.5591 6.53841 18.8757 6.53841 19.0709 6.34315L20.4998 4.91421C20.6951 4.71895 20.6951 4.40237 20.4998 4.20711ZM12 4.5C16.1421 4.5 19.5 7.85786 19.5 12C19.5 16.1421 16.1421 19.5 12 19.5C7.85786 19.5 4.5 16.1421 4.5 12C4.5 7.85786 7.85786 4.5 12 4.5ZM5.63589 17.6569L6.343 18.364C6.53826 18.5592 6.53826 18.8758 6.343 19.0711L4.99995 20.4141C4.80469 20.6093 4.48811 20.6093 4.29284 20.4141L3.58574 19.707C3.39047 19.5117 3.39047 19.1951 3.58574 18.9999L4.92879 17.6569C5.12405 17.4616 5.44063 17.4616 5.63589 17.6569ZM12.5 20.5C12.7761 20.5 13 20.7239 13 21V23C13 23.2761 12.7761 23.5 12.5 23.5H11.5C11.2239 23.5 11 23.2761 11 23V21C11 20.7239 11.2239 20.5 11.5 20.5H12.5ZM20.4142 19.0002L19.0709 17.6569C18.8757 17.4616 18.5591 17.4616 18.3638 17.6569L17.6567 18.364C17.4614 18.5592 17.4614 18.8758 17.6567 19.0711L19 20.4144C19.1953 20.6096 19.5118 20.6096 19.7071 20.4144L20.4142 19.7073C20.6095 19.512 20.6095 19.1954 20.4142 19.0002ZM3.5 11.5C3.5 11.2239 3.27614 11 3 11H1C0.723858 11 0.5 11.2239 0.5 11.5V12.5C0.5 12.7761 0.723858 13 1 13H3C3.27614 13 3.5 12.7761 3.5 12.5V11.5ZM23 11C23.2761 11 23.5 11.2239 23.5 11.5V12.5C23.5 12.7761 23.2761 13 23 13H21C20.7239 13 20.5 12.7761 20.5 12.5V11.5C20.5 11.2239 20.7239 11 21 11H23ZM4.91414 3.50014L6.343 4.92894C6.53826 5.1242 6.53826 5.44079 6.343 5.63605L5.63589 6.34315C5.44063 6.53841 5.12405 6.53841 4.92879 6.34315L3.49992 4.91435C3.30466 4.71909 3.30466 4.40251 3.49992 4.20724L4.20703 3.50014C4.40229 3.30487 4.71887 3.30487 4.91414 3.50014Z"></path></svg>'
            },
            {
                type: 'set',
                Icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-v-8e6f4044=""><path fill-rule="evenodd" clip-rule="evenodd" d="M12.0001 1.09094C12.809 1.09094 13.607 1.17997 14.3838 1.35486L14.832 1.45578L16.2299 4.64258L19.6717 4.26633L19.9828 4.60521C21.072 5.79163 21.8893 7.20671 22.3684 8.75213L22.5034 9.18779L22.2349 9.55649L20.4554 12L22.5034 14.8123L22.3684 15.2479C21.8893 16.7934 21.072 18.2084 19.9828 19.3949L19.6717 19.7337L16.2299 19.3575L14.832 22.5443L14.3838 22.6452C13.607 22.8201 12.809 22.9091 12.0001 22.9091C11.191 22.9091 10.3928 22.8201 9.61586 22.6451L9.16756 22.5441L8.98302 22.1233L7.77022 19.3575L4.32851 19.7337L4.01738 19.3949C2.928 18.2083 2.11072 16.793 1.63163 15.2474L1.49658 14.8117L1.76517 14.443L3.54473 12L1.49658 9.18838L1.63163 8.75267C2.11072 7.20704 2.928 5.79177 4.01738 4.60521L4.32851 4.26633L7.77022 4.64258L9.16756 1.45593L9.61586 1.35498C10.3928 1.18001 11.191 1.09094 12.0001 1.09094ZM12.0001 7.45457C14.5019 7.45457 16.5282 9.49061 16.5282 12C16.5282 14.5094 14.5019 16.5455 12.0001 16.5455C9.49827 16.5455 7.47192 14.5094 7.47192 12C7.47192 9.49061 9.49827 7.45457 12.0001 7.45457ZM9.29004 12C9.29004 10.4928 10.5043 9.27277 12 9.27277C13.4957 9.27277 14.71 10.4928 14.71 12C14.71 13.5072 13.4957 14.7273 12 14.7273C10.5043 14.7273 9.29004 13.5072 9.29004 12Z"></path></svg>',
                activeIcon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" data-v-8e6f4044=""><path fill-rule="evenodd" clip-rule="evenodd" d="M12.0001 1.09094C12.809 1.09094 13.607 1.17997 14.3838 1.35486L14.832 1.45578L16.2299 4.64258L19.6717 4.26633L19.9828 4.60521C21.072 5.79163 21.8893 7.20671 22.3684 8.75213L22.5034 9.18779L22.2349 9.55649L20.4554 12L22.5034 14.8123L22.3684 15.2479C21.8893 16.7934 21.072 18.2084 19.9828 19.3949L19.6717 19.7337L16.2299 19.3575L14.832 22.5443L14.3838 22.6452C13.607 22.8201 12.809 22.9091 12.0001 22.9091C11.191 22.9091 10.3928 22.8201 9.61586 22.6451L9.16756 22.5441L8.98302 22.1233L7.77022 19.3575L4.32851 19.7337L4.01738 19.3949C2.928 18.2083 2.11072 16.793 1.63163 15.2474L1.49658 14.8117L1.76517 14.443L3.54473 12L1.49658 9.18838L1.63163 8.75267C2.11072 7.20704 2.928 5.79177 4.01738 4.60521L4.32851 4.26633L7.77022 4.64258L9.16756 1.45593L9.61586 1.35498C10.3928 1.18001 11.191 1.09094 12.0001 1.09094ZM12.0001 7.45457C14.5019 7.45457 16.5282 9.49061 16.5282 12C16.5282 14.5094 14.5019 16.5455 12.0001 16.5455C9.49827 16.5455 7.47192 14.5094 7.47192 12C7.47192 9.49061 9.49827 7.45457 12.0001 7.45457ZM9.29004 12C9.29004 10.4928 10.5043 9.27277 12 9.27277C13.4957 9.27277 14.71 10.4928 14.71 12C14.71 13.5072 13.4957 14.7273 12 14.7273C10.5043 14.7273 9.29004 13.5072 9.29004 12Z"></path></svg>'
            }
        ]
    },
    searchConig: {
        nav: [
            {
                name: '快捷导航',
                link: ''
            },
            {
                name: 'Web技术学苑',
                link: 'https://learn.wmcweb.cn/'
            },
            {
                name: '稀土掘金',
                link: 'https://juejin.cn/'
            },
            {
                name: '知乎',
                link: 'https://www.zhihu.com'
            },
            {
                name: '哔哩哔哩',
                link: 'https://www.bilibili.com/'
            }
        ]
    },
    popupList: [
        {
            title: '打开新标签页',
            type: 'newTab'
        },
        {
            title: '设置',
            type: 'set'
        }
    ],
    showSet: false
};


var vm = new Vue({
    el: '#app',
    components: {
       NavHeader,
    },
    data: initData,
})

我们注意到导航的左右数据都是来自data中的navConfig


<div id="app">
  <nav-header
          :nav-config="navConfig"
          :theme="theme"
          :change-theme="changeTheme"
          :open-set="openSet"
   ></nav-header>
</div>

并且还有theme,change-theme,open-set传入子组件的属性与方法

nav-header组件


// index.js
const NavHeader = {
    name: 'navHeader',
    props: ['navConfig', 'changeTheme', 'openSet', 'theme'],
    template: `<div class="nav-header">
    <div class="nav-header-left">
      <ul>
        <div class="logo"></div>
        <li v-for="(index, item) in navConfig.left" :key="index">
          {{item.name}}
        </li>
      </ul>
    </div>
    <div class="nav-header-right">
      <ul>
        <li v-for="(index, item) in navConfig.right" :key="index" @click="handleSet(item)">
          <span v-html="theme === 'light' ? item.Icon : item.activeIcon"></span>
        </li>
      </ul>
      <div class="author">
        <a href="https://juejin.cn/user/4142615542920680" target="_blank">Maic</a>
      </div>
    </div>
  </div>`,
    methods: {
        handleSet({ type }) {
            // 修改主题
            if (type === 'theme') {
                this.changeTheme();
            } else if (type === 'set') {
                this.openSet();
            }
        }
    }
}

new Vue实例中注册


var vm = new Vue({
    el: '#app',
    components: {
       NavHeader
    },
    data: initData,
    methods: {
        changeTheme() {
            // 修改主题
            if (this.theme === 'light') {
                this.theme = 'dart'
            } else {
                this.theme = 'light';
            }
        },
        openSet() {
            // 打开右侧设置抽屉
            this.showSet = true;
        },
    },
})

这样我们就把NavHeader注册到app里了,所以我们在index.html中可以使用了。

其他组件也是类似一样的做法,依次局部注册到组件中即可


const Search = {
    name: 'search',
    props: ['searchConig', "searchStyle"],
    template: `<div class="search" v-bind:style="searchStyle">
    <div class="search-input">
      <input type="text" placeholder="请搜索..." />
    </div>
    <div class="quick-nav">
      <p v-for="(index, item) in searchConig.nav" :key="index">
        <a v-if="item.link" :href="item.link">{{item.name}}</a>
        <span v-else>{{item.name}}</span>
      </p>
    </div>
  </div>`
}
const ItemCard = {
    name: 'item-card',
    props: ["formCondation", "typeData", "listData"],
    template: `...`
}
const BaseLayoutContent = {
    name: 'base-content',
    template: `<div class="content">
    <div class="card-left">
      <slot name="left" />
    </div>
    <div class="card-center">
      <slot name="center" />
    </div>
    <div class="card-right">
      <slot name="right" />
    </div>
  </div>`
}
const PopupList = {
    name: 'popup-list',
    props: ['popupList', 'openSet'],
    template: `...`,
}
const SetPopup = {
    name: 'SetPopup',
    props: ['handleClose', 'showSet'],
    template: `...`
}

换肤主题切换

我们发现掘金插件是可以切换主题的,在掘金插件换主题颜色中,我们会发现做法也是非常的简单,主要利用了css的变量,就是同一个样式,我们背景以及字体颜色我们用css变量来控制

我们会在:root:root .dart中定义两份值不同的样式变量,比如在dart模式下--jjext-color-secondary-bg:#272727,而默认模式下就会获取到:root{}中的变量


:root {
  --header-bg-color: #fff;
  --jjext-color-divider: #e5e6eb;
  --jjext-color-secondary-bg: #fff;
  --jjext-color-nav-title: #86909c;
  --logo: url("https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg");
  --jjext-color-main-bg: #f4f5f5;
  --card-height: calc(100vh - 186px - 30px);
  --jjext-color-brand: #1e80ff;
  --jjext-color-navbar-icon: #1e80ff;
}
:root .dart {
  --jjext-color-primary: #e3e3e3;
  --jjext-color-divider: #4a4a4a;
  --header-bg-color: #272727;
  --jjext-color-nav-title: #e3e3e3;
  --logo: url("https://lf-cdn-tos.bytescm.com/obj/static/xitu_extension/static/brand-dark.3111cff6.svg");
  --jjext-color-main-bg: #121212;
  --jjext-color-secondary-bg: #272727;
  --jjext-color-brand: #fff;
  --jjext-color-navbar-icon: #e3e3e3;
}

我们在使用的时候,同一个地方只需要写入一个变量即可


.card-left,
.card-center,
.card-right {
  border-radius: 5px;
  background-color: var(--jjext-color-secondary-bg);
}

当我们切换成dart的时候,就会--jjext-color-secondary-bg就会取:root .dart中定义的变量

所以网站换肤核心在于css变量

Mock数据

页面显示的数据就是Mock的,我简单的用fetch封装了一个request方法


// request.js
function request({ url, headers = {}, method = "get" }) {
    return new Promise((resolve, reject) => {
        fetch(url, {
            method,
            headers
        })
            .then(function (response) {
                response
                    .json()
                    .then(function (res) {
                        resolve(res.data);
                    })
                    .catch(function (error) {
                        console.error("Fetch error:" + error);
                        alert("请求失败");
                        reject(error);
                    });
            })
            .catch(function (error) {
                console.error("Fetch error:" + error);
                alert("请求失败");
                reject(error);
            });
    })
}

在页面中我们请求数据


  ...
  methods: {
      async getData(typeStatus, status = 0) {
            const { listData, gitHubData, fdlistData } = await request({ url: 'https://mock.mengxuegu.com/mock/637e3376cedbd00ec9c7854a/chrome/query' });
            if (typeStatus === 'gitStatus') {
                this.gitHubData = gitHubData;
            } else if (typeStatus === 'fdStatus') {
                this.fdlistData = fdlistData;
            } else if (typeStatus === 'listStatus') {
                this.listData = listData;
            } else {
                this.gitHubData = gitHubData;
                this.fdlistData = fdlistData;
                this.listData = listData;
            }
            this.formCondation[typeStatus] = status;
        },
  }

我们看下request这个请求的接口数据 ,我在eazyMock平台新建了一个项目

编辑接口,我们在控制台中写入Mock的接口字段


{
  success: true,
  data: {
    default: "hah",
    _req: function({
      _req
    }) {
      return _req
    },
    name: function({
      _req
    }) {
      return _req.query.name || this.default
    },
    "listData|10-20": [{
      'title': '@ctitle',
      'time': '@date',
      'approve|1-1000': 1000,
      'remark|1-100': 100,
    }],
    "gitHubData|10-20": [{
      'title': '@ctitle',
      'time': '@date',
      'approve|1-1000': 1000,
      'remark|1-100': 100,
    }],
    "fdlistData|10-20": [{
      'title': '@cword(60, 100)',
      'author': '@cname',
      'time': '@date',
      'approve|1-1000': 1000,
      'remark|1-100': 100,
    }]
  },
}

保存即可,接口就可以返回数据

请求的地址就是xxx/chrome/query就可以了

预览

最后,vue构建的chrome插件就已经ok了,我们并没有用脚手架方式去构建,就是最原始方式实现了chrome插件,我们也看到不用脚手架的方式构建页面,虽然满足了业务,但是相比较脚手架方式,并没有模块化,所以可维护性没有工程化方面好,但是在实际开发中一些简单的页面采用这样的方式开发效率也是很高的,因为不会去折腾一些打包配置工具,写完丢到服务器上就行,在v2.0版本中,我会考虑用webpack5搭建chrome插件,以及会升级,完善更多的功能。

总结

  • 我们自己尝试写一个chrome掘金插件,明白插件如何修改默认导航页的
  • 知道如何换肤主题,主要利用css变量
  • 如何EazyMock在线模拟接口数据
  • 原始方式利用vue构建应用
  • 本文示例源码 code example[1]

参考资料

[1] code example: https://github.com/maicFir/chrome-plugun-juejin

最后,看完觉得有收获的,点个赞,在看,转发,收藏等于学会,欢迎关注Web技术学苑,好好学习,天天向上!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Web技术学苑 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • manifest.json
  • 默认打开首页
  • 插件主页面index.html
  • 换肤主题切换
  • Mock数据
  • 预览
  • 总结
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档