一路繁花似锦绣前程
失败的越多,成功才越有价值

导航

 

一、学习前准备知识

* 扩展插件加载到chrome浏览器的步骤
    - 地址栏输入:chrome://extensions/
    - 开启开发者模式
    - 点击加载已解压的扩展程序
    - 选择文件夹(manifest.json目录)
* webstorm配置chrome扩展插件开发提示
    - 下载:https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/chrome/index.d.ts
    - webstorm settings:languages & frameworks > javascript >libraries
    - 点击add > 点击+号 > 点击attach files > 选择声明文件
* chrome通知失效解决步骤
    - 地址栏输入:chrome://flags
    - 搜索栏输入:notifications
    - Enable system notifications选项设置为:Disabled
    - relaunch:重启浏览器

二、浏览器扩展插件开发(MV3)

1、hello world
  • manifest.json
{
  "manifest_version": 3,
  "name": "有钱",
  "version": "1.0.0",
  "description": "描述信息",
  "homepage_url": "http://liujinkai.com/",
  "icons": {
    "32": "icon32.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "action": {
    "default_icon": {
      "16": "icon16.png",
      "32": "icon32.png"
    },
    "default_popup": "popup.html",
    "default_title": "有钱"
  },
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_end"
    }
  ]
}
  • popup.html
<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <title>有钱</title>
  <script src="popup.js"></script>
  <style>
  #app {
    width: 240px;
    height: 320px;
    text-align: center;
  }
  </style>
</head>
<body>
<div id="app">
  <button id="btn">点击探索</button>
  <p id="result"></p>
</div>
</body>
</html>
  • popup.js
window.onload = function () {
  const btn = document.getElementById("btn");
  btn.onclick = () => {
    chrome.tabs.query({active: true, currentWindow: true}, tabs => {
      chrome.tabs.sendMessage(tabs[0].id, {action: "checkForContent"}, response => {
        const result = document.getElementById("result")
        result.innerText = response?.results || "没有收到content script的内容"
      })
    })
  }
}
  • contentscript.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "checkForContent") {
    sendResponse({
      results: `content script发送的document.title:${document.title}`
    })
  }
})
2、核心机制
a、manifest.json
{
  // 必填项,最新版本3
  "manifest_version": 3,
  // 必填项,扩展名称
  "name": "扩展名称",
  // 必填项,扩展的版本号
  "version": "1.0.0",

  // 以下为推荐项
  // 右上角浏览器图标点击事件设定
  "action": {...},
  "default_locale": "en",
  "description": "a plain text description",
  // 图标,用png格式,按要求大小提供
  "icons": {...},

  // 以下为可选项
  "author": ...,
  "automation": ...,
  // 可以运行在后台的脚本配置
  "background": {
    // 必填
    "service_worker": "background.js",
    // 可选
    "type": ...
  },
  "chrome_settings_overrides": {...},
  "chrome_url_overrides": {...},
  "commands": {...},
  "content_capabilities": ...,
  // 直接注入页面的脚本文件
  "content_scripts": [{...}],
  "content_security_policy": {...},
  "converted_from_user_script": ...,
  "cross_origin_embedder_policy": {"value": "require-corp"},
  "cross_origin_opener_policy": {"value": "same-origin"},
  "current_locale": ...,
  "declarative_net_request": ...,
  "devtools_page": "devtools.html",
  "differential_fingerprint": ...,
  "event_rules": [{...}],
  "externally_connectable": {
    "matches": ["*://*.example.com/*"]
  },
  "file_browser_handlers": [...],
  "file_system_provider_capabilities": {
    "configurable": true,
    "multiple_mounts": true,
    "source": "network"
  },
  "homepage_url": "https://path/to/homepage",
  // 扩展可访问的网站规则配置
  "host_permissions": [...],
  "import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
  "incognito": "spanning, split, or not_allowed",
  "input_components": ...,
  "key": "publicKey",
  "minimum_chrome_version": "versionString",
  "nacl_modules": [...],
  "natively_connectable": ...,
  "oauth2": ...,
  "offline_enabled": true,
  "omnibox": {
    "keyword": "aString"
  },
  "optional_host_permissions": ["..."],
  "optional_permissions": ["tabs"],
  "options_page": "options.html",
  "options_ui": {
    "page": "options.html"
  },
  // 权限设定
  "permissions": ["tabs"],
  "platforms": ...,
  "replacement_web_app": ...,
  "requirements": {...},
  "sandbox": [...],
  "short_name": "Short Name",
  "storage": {
    "managed_schema": "schema.json"
  },
  "system_indicator": ...,
  "tts_engine": {...},
  "update_url": "https://path/to/updateInfo.xml",
  "version_name": "aString",
  // 扩展内可访问的资源
  "web_accessible_resources": [...]
}
b、消息通信
  • 一次性的简单消息发送&接收
// contentscript.js,发起一个从content script到扩展的消息
chrome.runtime.sendMessage({greeting: "hello"}, function (response) {
  console.log(response.farewell)
})
// 扩展到content script的消息,需要给出当前tab的id
chrome.tabs.query(
  {active: true, currentWindow: true},
  function (tabs) {
    chrome.tabs.sendMessage(
      tabs[0].id,
      {greeting: "hello"},
      function (response) {
        console.log(response.farewell)
      }
    )
  }
)
// 接收消息的一方,chrome.runtime.onMessage事件监听来处理消息
chrome.runtime.onMessage.addListener(
  function (request, sender, sendResponse) {
    console.log(sender.tab ?
      "from a content script:" + sender.tab.url :
      "from the extension")
    if (request.greeting === "hello") {
      sendResponse({farewell: "goodbye"})
    }
    // return true
  }
)
  • 长连接模式-chrome.runtime.Port对象
// content scripts主动建立通道如下
var port = chrome.runtime.connect({name: "yisheng"});// 通道名称
port.postMessage({joke: "knock knock"});//发送消息
port.onMessage.addListener(function (msg) {// 监听消息
  if (msg.question === "who's there?") {
    port.postMessage({answer: "yisheng"})
  } else if (msg.question === "madame who?") {
    port.postMessage({answer: "madame... bovary"})
  }
})
// chrome扩展程序页面主动建立通道如下
chrome.tabs.query(
  {active: true, currentWindow: true},
  function (tabs) {
    var port = chrome.tabs.connect(// 建立通道
      tabs[0].id,
      {name: "yisheng"}// 通道名称
    )
  }
)
// content scripts或google chrome扩展程序页面
// 监听建立连接的请求如下
chrome.runtime.onConnect.addListener(function (port) {
  console.assert(port.name === "yisheng")
  port.onMessage.addListener(function (msg) {
    if (msg.joke === "knock knock") {
      port.postMessage({question: "who's there?"})
    } else if (msg.answer === "madame") {
      port.postMessage({question: "madame who?"})
    } else if (msg.answer === "madame... bovary") {
      port.postMessage({question: "i don't get it."})
    }
  })
})
  • google chrome扩展程序之间消息模式
  • google chrome扩展程序接收指定的web页面发送的消息
chrome.runtime.onMessageExternal.addListener(
  function (request, sender, sendResponse) {
    if (sender.url === blacklistedWebsite) {
      return // don't allow this web page access
    }
    if (request.openUrlInEditor) {
      openUrl(request.openUrlInEditor)
    }
  }
)

三、打包发布

* 开发者注册
    - 需要支付5.00美元的一次性费用。待注册完成就可以上传自己的扩展到应用商店了
    - 注册地址:https://chrome.google.com/webstore/devconsole
* 打包上传
    - 将项目压缩为.zip的文件,准备上传。登录chrome应用的开发者中心
    - 访问:https://chrome.google.com/webstore

四、自动更新

  • 概念
* 每隔几个小时,浏览器就会检查已安装的扩展是否有更新URL。对于每一个,它都向该URL发出请求,查找更新清单xml文件
* 可以使用扩展管理页面上的立即更新扩展按钮强制进行更新
* 为了确保一个给定的更新将只适用于谷歌chrome版本或高于特定版本,添加“prodversionmin”属性即可
  • manifest.json
{
  "update_url": "https://myhost.com/mytestextension/updates.xml"
}
  • updates.xml
<?xml version="1.0" encoding="UTF-8"?>
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
    <app appid="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa">
        <updatecheck codebase="https://myhost.com/mytestextension/mte_v2.crx" version="2.0"
                     prodversionmin="106.0.5249.119"/>
    </app>
</gupdate>

五、高级知识点-数据存储

1、知识点
* google的chrome.storage.*API实现的浏览器存储
* google的chrome.cookies.*API实现的cookie存储
* IndexedDB适用复杂大数据的存储
2、chrome.storage.*
  • 概念
* chrome.storage.local
    - 方式只能够将数据存储在当前登录的设备本地
    - 大小限制约为5M
    - 如果声明权限unlimitedStorage则不受大小限制
* chrome.storage.sync
    - 只要用户启用了同步,存储的数据将自动同步到任何用户登录的chrome浏览器
    - 大小限制约为100kb
  • manifest.json
{
  "permissions": [
    "storage"
  ]
}
  • 使用
// 存储数据
chrome.storage.local.set({key: value}, function () {
  console.log("value is set to " + value)
})
// 读取数据
chrome.storage.local.get(["key"], function (result) {
  console.log("value currently is " + result.key)
})
3、chrome.cookie.*
  • 概念
* 可以获取或修改cookie,还可以监控cookie的变化
  • manifest.json
{
  "host_permissions": [
    "*://*.google.com/"
  ],
  "permissions": [
    "cookies"
  ]
}
  • 使用
// 获得一个cookie对象
chrome.cookies.get(object details, function (Cookie cookie) {...})

// 设置cookie对象
chrome.cookies.set(object details, function (Cookie cookie) {...})

// 根据名字删除cookie对象
chrome.cookies.remove(object details, function (object details) {...})

// 监听cookie对象的变化
chrome.cookies.onChanged.addListener(function (object changeInfo) {...})
4、IndexedDB
  • 概念
* 对于任何数量/复杂性的数据都非常快
* 没有大小的限制可存储的数据类型,如ArrayBuffer,文件,Blob,类型化数组,集合,Map
* 在Content Script中不可用,所以您必须使用消息传递
* 不好的地方是API过时而笨拙,可以使用第三方库来解决这个问题
* 使用第三方封装的库来操作IDB更方便些,这里推荐:https://jsstore.net/
  • 使用
// indexedDB在service worker中使用时已经定义
var dbName = "DatabaseName";
var open = indexedDB.open(dbName, 1);

六、高级知识点-跨域访问

  • 概念
* 通常出于安全的考虑,通常web页面的XHR(XMLHttpRequest)对象不能访问其他域的服务器。但是
  chrome浏览器扩展没有这个限制,只要设置了跨域访问的权限,chrome浏览器扩展的XMLHttpRequest
  对象可以访问声明的任何域的服务器
* 在Manifest V3中,background页面(由Service worker提供)不支持XMLHttpRequest。考虑使用fetch()方法
  • manifest.json
{
  "host_permissions": [
    "http://www.google.com/",
    "https://*.google.com/",
    "http://*/"
  ]
}
  • 使用
// axios基于promise的封装XMR(XMLHttpRequest)的异步ajax请求库
axios.get(
  "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1"
).then(response => {
  console.log(response)
}).catch(error => {
  console.log(error)
})
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere
xhr.open("GET", chrome.runtime.getURL("config_resources/config.json"), true);
// 访问内部位于config_resources目录下的config.json文件
xhr.send();

七、高级知识点-扩展内页面

  • 概念
* chrome.runtime.getURL(string path)将扩展程序安装目录中的相对路径转换为完整的URL
* 在扩展管理页面上(chrome://extensions),找到应用(扩展)ID,
  访问chrome-extension://extensionId/filename
  • 使用
// 打开新的窗口
window.open(chrome.runtime.getURL("capture.html"));

// 打开新的tab页
chrome.tabs.create({"url": chrome.runtime.getURL("capture.html")}, function (tab) {
  // tab opened
  console.log("after created")
})

八、高级知识点-多语言

1、配置
  • 概念
* 需要把所有用户可见字符串保存在文件名为messages.json的文件里,文件位于项目的根目录下的
  _locales\locale_Code,其中的locale_Code有专门的国际标准规定,比如中国大陆的简体中文
  对应zh_CN,而一旦有_locales目录就必须在manifest.json文件中指定默认Locale:
  • manifest.json
{
  "default_locale": "zh_CN"
}
2、引用
  • 概念
* 在manifest.json和css文件中的引用方法如下:__MSG_key__
* 在javascript文件中的引用方法如下:chrome.i18n.getMessage("key")
  • manifest.json
{
  "name": "__MSG_extName__",
  "default_locale": "zh_CN"
}
  • javascript文件
// chrome.i18n.getMessage(...)方法还可以带第二个参数,已替换资源值中的占位符。
// 第二个参数要么是一个字符串,要么是一个字符串数组(数组中最多9个元素)
const title = chrome.i18n.getMessage("extName")
  • _locales\zh_CN\messages.json
{
  "extName": {
    "message": "你好世界",
    "description": "扩展名称"
  }
}
3、预定义资源
  • 概念
* @@extension_id
    - chrome浏览器扩展的id,可用于动态构建与某chrome浏览器扩展相关的URL,只能用于css
      和javascript文件,如:__MSG_@@extension_id__。manifest.json中不可用
* @@ui_locale
    - 当前页面的Locale,可用于动态构建与Locale相关的URL
  • 使用
/* 使用预定义资源 */
body {
  background-image: url("chrome-extension://__MSG_@@extension_id__/background.png");
}

/* 如果扩展的ID是abcdefghijklmnopgrstuvwxyzabcdef,那么上述代码中的粗线部分变成: */
background-image: url("chrome-extension://abcdefghijklmnopgrstuvwxyzabcdef/background.png");

九、高级知识点-background

1、背景页
* 浏览器扩展是基于事件的程序。事件是浏览器的触发器,可以说扩展要想“动”起来,就离不开事件Event。
  而service worker中的脚本监听这些事件,并对事件进行响应。manifest.json文件通过background
  指定后台运行的脚本
2、配置
  • 概念
* 一般,背景页不需要任何html,仅仅需要js文件,浏览器的扩展系统会自动根据scripts字段指定的所有js文件自动生
  成背景页。如果的确需要自己的背景页,可以使用page字段
* 我们也可以在service worker中使用javascript modules。只需在清单中将类型属性设置为module
* 从popup.js中访问background.js
* background一直处于休眠状态,只在事件发生时加载并运行,触发加载的场景如下
    - chrome扩展第一次被安装或升级到新版本
    - 触发了后台脚本的监听事件
    - content script或其他扩展发出了一个Event
    - 扩展中的其他页面调用了runtime.getBackgroundPage方法
  • manifest.json
{
  "manifest_version": 3,
  "background": {
    "page": "background.html",
    "service_worker": [
      "background.js"
    ],
    "type": "module"
  }
}
  • 使用
// background.js这将将service worker作为es模块加载
// 从而允许您在service worker中使用import关键字导入其他模块
import * as module from "./scripts/jsencrypt.min.js"
//在popup.js中调用background.js中的变量和方法
var bg = chrome.extension.getBackgroundPage()
console.log(bg.value)
3、demo:合并浏览器窗口
  • manifest.json
{
  "manifest_version": 3,
  "name": "浏览器合并窗口",
  "version": "1.0.0",
  "description": "合并其它的浏览器窗口到当前的窗口",
  "icons": {
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_icon": "icon32.png",
    "default_title": "窗口合并"
  }
}
  • background.js
let targetWindow = null // 当前激活的浏览器窗口
let tabCount = 0 // 当前激活浏览器窗口里的tab的数量

function moveTabs(windows) {
  let numWindows = windows.length
  let tabPosition = tabCount
  for (let i = 0; i < numWindows; i++) {
    let win = windows[i]
    if (targetWindow.id !== win.id) {
      let numTabs = win.tabs.length
      for (let j = 0; j < numTabs; j++) {
        let tab = win.tabs[j]
        chrome.tabs.move(tab.id, {windowId: targetWindow.id, index: tabPosition})
        tabPosition++
      }
    }
  }
}

function getTabs(tabs) {
  tabCount = tabs.length
  chrome.windows.getAll({populate: true}, moveTabs)
}

function getWindows(win) {
  targetWindow = win
  chrome.tabs.query({windowId: targetWindow.id}, getTabs)
}

/**
 * 点击浏览器图标绑定的事件
 * @param tab
 */
function start(tab) {
  chrome.windows.getCurrent(getWindows)
}

// 地址栏图标绑定点击事件
chrome.action.onClicked.addListener(start)

十、高级知识点-选项页

1、配置显示
  • 概念
* 为了让用户设定扩展的功能,可能需要提供一个选项页。如果提供了选项页,在扩展管理页面
  chrome://extensions上会提供一个链接
  • manifest.json
{
  "options_page": "options.html"
}
2、demo:消息通知
  • manifest.json
{
  "manifest_version": 3,
  "name": "消息通知",
  "version": "1.0.0",
  "description": "定时显示消息通知",
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "permissions": [
    "notifications",
    "storage"
  ],
  "options_page": "options.html",
  "background": {
    "service_worker": "background.js"
  }
}
  • background.js
const LS = chrome.storage.local

function show() {
  let time = /(..)(:..)/.exec(new Date()) // 当前时间
  let hour = time[1] % 12 || 12 // 小时
  let period = time[1] < 12 ? "a.m." : "p.m." // 上午、下午
  chrome.notifications.create({
    type: "basic",
    iconUrl: "icon48.png",
    title: "是时候起来溜达一下了~~",
    message: hour + time[2] + " " + period,
    priority: 0
  })
}

// 判断是否已经初始化
async function init() {
  let isInitializedInit = await LS.get(["isActivated"])
  if (!isInitializedInit) {
    LS.set({"isActivated": true}) // 是否激活
    LS.set({"frequency": 1}) // 显示间隔,分钟
    LS.set({"isInitialized": true}) // 初始化状态
  }
  // 浏览器是否支持通知
  let window = window ?? self
  if (window.Notification) {
    // 加载时就先显示一下
    let tmp = await LS.get(["isActivated"])
    if (!tmp.isActivated) {
      show()
    }

    let interval = 0 // 间隔分钟数

    setInterval(async () => {
      interval++
      let isActivatedObj = await LS.get(["isActivated"])
      let frequencyObj = await LS.get(["frequency"])
      if (isActivatedObj.isActivated && (frequencyObj.frequency <= interval)) {
        show()
        interval = 0
      }
    }, 60000)
  }
}

init()
  • options.html
<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <title>消息通知</title>
  <script src="options.js"></script>
</head>
<body>
<form id="options">
  <input type="checkbox" name="isActivated" checked>
  每
  <select name="frequency">
    <option>1</option>
    <option>2</option>
    <option>3</option>
    <option>4</option>
  </select>
  分钟显示消息
</form>
</body>
</html>
  • options.js
const LS = chrome.storage.local

function trigger(isDeactivated) {
  // 设定颜色
  document.getElementById("options").style.color = isDeactivated ? "graytext" : "black"
  // 设定可用状态
  document.getElementById("options").frequency.disabled = isDeactivated
}

// 页面加载方法
window.addEventListener("load", async function () {
  // 获取页面对象
  const obj = document.getElementById("options")
  // 显示存储的值
  let isActivatedObj = await LS.get(["isActivated"])
  obj.isActivated.checked = isActivatedObj.isActivated || true
  // 显示存储的值
  let frequencyObj = await LS.get(["frequency"])
  obj.frequency.value = frequencyObj.frequency

  // 根据存储值来设定页面上的勾选状态
  if (!obj.isActivated.checked) {
    trigger(true)
  }

  // 绑定勾选的变化事件
  obj.isActivated.onchange = function () {
    LS.set({"isActivated": obj.isActivated.checked}) // 设定本地存储
    trigger(!obj.isActivated.checked)
  }
  // 绑定间隔的变化事件
  obj.frequency.onchange = function () {
    LS.set({"frequency": obj.frequency.value}) // 设定本地存储
  }
})

十一、高级知识点-vue框架开发

1、步骤
* vite脚手架初始化项目命令:npm init vite
    - vue+ts需添加*.vue的类型声明
* package.json文件script属性添加键值对
    - "serve": "vite build --watch"
2、核心代码(非常重要)
* https://gitee.com/YiSiYiNian/chrome-extension.git

十二、注意事项

1、安全之csp
  • 概念
* csp(content security policy)的主要目的是防止跨站脚本攻击(xss)。
  通俗来说:csp的实质就是白名单策略,开发者明确声明,哪些外部资源可以加载和执行
    - 解决办法就是需要将页面内js写到一个js文件里引入进来
    - extension_pages:配置会影响到扩展中的页面,包括html文件和service worker
        ~ script-src: self|none|Anyurl
        ~ object-src: self|none|Anyurl
        ~ worker-src: self|none|Anyurl
    - sandbox:这个配置会影响sandbox相关的页面。在sandbox中有两个含义
        ~ 不能直接访问非sandbox页面(可以通过postMessage()与通信)
        ~ 不受扩展的内容安全策略(csp)的约束
  • manifest.json
{
  "content_security_policy": {
    "extension_pages": "script-src 'self';object-src 'none'",
    "sandbox": "sandbox allow-scripts: script-src 'self' https://example.com"
  },
  "sandbox": {
    "page": [
      "page1.html",
      "directory/page2.html"
    ]
  }
}
2、提交审核被拒
* 在提交扩展到浏览器商店的审核过程中,请遵循最少使用权限原则,不然很可能会因为
  权限滥用的问题被拒绝
* 被拒的话,按照被拒绝的原因改正后重新提交即可。但是是官方的回复一般都不说明具体的原因

十三、其它浏览器扩展

1、edge
* 开发好的chrome扩展也可以提交到microsoft扩展网站,使其可供其他microsoft edge用户使用。
  需要开发者账号提交,如果没有则需要注册
    - edge提交地址:https://microsoftedge.microsoft.com/addons/Microsoft-Edge-Extensions-Home
    - 开发者账号注册:https://partner.microsoft.com/dashboard/microsoftedge/public/login
    - 详细步骤说明:https://learn.microsoft.com/zh-cn/microsoft-edge/extensions-chromium/publish/publish-extension
* 将扩展提交到合作伙伴中心
    - 启动新提交
    - 上传扩展包
    - 提供可用性详细信息
    - 选择扩展的属性
    - 为扩展添加应用商店列表详细信息
    - 完成提交
2、firefox
* firefox的提交步骤跟edge的差不多,也是需要有开发者账号,然后按要求提交文件即可
    - firefox的扩展提交地址:https://addons.mozilla.org/zh-CN/firefox/
    - firefox的开发者:https://addons.mozilla.org/zh-CN/developers/
* firefox目前还没有对manifest V3的支持,官方说在2022年底会进行支持。之后再给大家
  更新firefox上的MV3的支持说明

十四、案例-一键生成网页截图工具(非常重要)

* https://gitee.com/YiSiYiNian/chrome-extension.git

十五、案例-微信群合影

* 想法
* 开发
    - 页面page.js获取图片头像url后,发消息到popup.js,popup.html里绘制图形
    - 构建账号体系,实现收费用户和普通用户的区别,后台用的数据库是LeanCloud(免费版)
    - 花55块钱阿里云上买了个国际域名,利用github page搭建免费的网站介绍页
    - 每天早上早期2个小时时间,来开发插件
* 发布
    - 尽快的验证用户需求,需要尽快的开发出产品原型上线,得到用户的反馈
    - 原型非常的粗糙
    - 最早的版本甚至没有收费版本,因为我也不确定用户是否有这样的市场需求,免费版本
      只是在生成图片的右下角留下了自己的微信二维码
* 变现
    - 验证用户需求的最好办法,就是用户是否愿意付费使用
    - 用户的抱怨,心理却很美
    - 用户传播的力量
    - 我列出了10个点,最后选定抱怨最多的头像编辑功能作为收费的点
    - low办法,让整个流程跑起来,建立起变现模式
    - 第1个月的付费情况和1年后的付费情况

十六、总结

* 浏览器扩展的技术,13个知识点(核心机制、开发调试、打包发布、自动更新、数据存储、跨域访问、扩展内页面、
  多语言、Background、选项页面、Vue开发框架、CSP、其它浏览器扩展)
* 4个演示Demo(Hello world、消息通知、合并窗口、一图一诗)
* 2个案例(屏幕截图、微信群合影)
* 打开一扇窗,知道了小产品变现的方法
* 课程中的所有demo可以在github上下载到
    - https://github.com/ljinkai/chrome-extension-demo
posted on 2022-12-06 00:27  一路繁花似锦绣前程  阅读(281)  评论(0编辑  收藏  举报