Chromium 中chrome.bookmarks扩展接口c++实现

一、扩展接口定义 chrome.bookmarks

使用 chrome.bookmarks API 创建、整理以及以其他方式操纵书签。另请参阅覆盖网页(可用于创建自定义"书签管理器"页面)。

更多参考chrome.bookmarks | API | Chrome for Developers (google.cn)

扩展可以请从 chrome-extension-samples 安装 bookmarks API 示例 存储库

二、扩展接口c++定义

chrome\common\extensions\api\bookmarks.json

cpp 复制代码
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

[
  {
    "namespace": "bookmarks",
    "description": "Use the <code>chrome.bookmarks</code> API to create, organize, and otherwise manipulate bookmarks. Also see <a href='override'>Override Pages</a>, which you can use to create a custom Bookmark Manager page.",
    "properties": {
      "MAX_WRITE_OPERATIONS_PER_HOUR": {
        "value": 1000000,
        "deprecated": "Bookmark write operations are no longer limited by Chrome.",
        "description": ""
      },
      "MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE": {
        "value": 1000000,
        "deprecated": "Bookmark write operations are no longer limited by Chrome.",
        "description": ""
      }
    },
    "types": [
      {
        "id": "BookmarkTreeNodeUnmodifiable",
        "type": "string",
        "enum": ["managed"],
        "description": "Indicates the reason why this node is unmodifiable. The <var>managed</var> value indicates that this node was configured by the system administrator. Omitted if the node can be modified by the user and the extension (default)."
      },
      {
        "id": "BookmarkTreeNode",
        "type": "object",
        "description": "A node (either a bookmark or a folder) in the bookmark tree.  Child nodes are ordered within their parent folder.",
        "properties": {
          "id": {
            "type": "string",
            "minimum": 0,
            "description": "The unique identifier for the node. IDs are unique within the current profile, and they remain valid even after the browser is restarted."
          },
          "parentId": {
            "type": "string",
            "minimum": 0,
            "optional": true,
            "description": "The <code>id</code> of the parent folder.  Omitted for the root node."
          },
          "index": {
            "type": "integer",
            "optional": true,
            "description": "The 0-based position of this node within its parent folder."
          },
          "url": {
            "type": "string",
            "optional": true,
            "description": "The URL navigated to when a user clicks the bookmark. Omitted for folders."
          },
          "title": {
            "type": "string",
            "description": "The text displayed for the node."
          },
          "dateAdded": {
            "type": "number",
            "optional": true,
            "description": "When this node was created, in milliseconds since the epoch (<code>new Date(dateAdded)</code>)."
          },
          "dateLastUsed": {
            "type": "number",
            "optional": true,
            "description": "When this node was last opened, in milliseconds since the epoch. Not set for folders."
          },
          "dateGroupModified": {
            "type": "number",
            "optional": true,
            "description": "When the contents of this folder last changed, in milliseconds since the epoch."
          },
          "unmodifiable": {
            "$ref": "BookmarkTreeNodeUnmodifiable",
            "optional": true,
            "description": "Indicates the reason why this node is unmodifiable. The <var>managed</var> value indicates that this node was configured by the system administrator or by the custodian of a supervised user. Omitted if the node can be modified by the user and the extension (default)."
          },
          "children": {
            "type": "array",
            "optional": true,
            "items": { "$ref": "BookmarkTreeNode" },
            "description": "An ordered list of children of this node."
          }
        }
      },
      {
        "id": "CreateDetails",
        "description": "Object passed to the create() function.",
        "inline_doc": true,
        "type": "object",
        "properties": {
          "parentId": {
            "type": "string",
            "serialized_type": "int64",
            "optional": true,
            "description": "Defaults to the Other Bookmarks folder."
          },
          "index": {
            "type": "integer",
            "minimum": 0,
            "optional": true
          },
          "title": {
            "type": "string",
            "optional": true
          },
          "url": {
            "type": "string",
            "optional": true
          }
        }
      }
    ],
    "functions": [
      {
        "name": "get",
        "type": "function",
        "description": "Retrieves the specified BookmarkTreeNode(s).",
        "parameters": [
          {
            "name": "idOrIdList",
            "description": "A single string-valued id, or an array of string-valued ids",
            "choices": [
              {
                "type": "string",
                "serialized_type": "int64"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "serialized_type": "int64"
                },
                "minItems": 1
              }
            ]
          }
        ],
        "returns_async": {
          "name": "callback",
          "parameters": [
            {
              "name": "results",
              "type": "array",
              "items": { "$ref": "BookmarkTreeNode" }
            }
          ]
        }
      },
      {
        "name": "getChildren",
        "type": "function",
        "description": "Retrieves the children of the specified BookmarkTreeNode id.",
        "parameters": [
          {
            "type": "string",
            "serialized_type": "int64",
            "name": "id"
          }
        ],
        "returns_async": {
          "name": "callback",
          "parameters": [
            {
              "name": "results",
              "type": "array",
              "items": { "$ref": "BookmarkTreeNode"}
            }
          ]
        }
      },
      {
        "name": "getRecent",
        "type": "function",
        "description": "Retrieves the recently added bookmarks.",
        "parameters": [
          {
            "type": "integer",
            "minimum": 1,
            "name": "numberOfItems",
            "description": "The maximum number of items to return."
          }
        ],
        "returns_async": {
          "name": "callback",
          "parameters": [
            {
              "name": "results",
              "type": "array",
              "items": { "$ref": "BookmarkTreeNode" }
            }
          ]
        }
      },
      {
        "name": "getTree",
        "type": "function",
        "description": "Retrieves the entire Bookmarks hierarchy.",
        "parameters": [],
        "returns_async": {
          "name": "callback",
          "parameters": [
            {
              "name": "results",
              "type": "array",
              "items": { "$ref": "BookmarkTreeNode" }
            }
          ]
        }
      },
      {
        "name": "getSubTree",
        "type": "function",
        "description": "Retrieves part of the Bookmarks hierarchy, starting at the specified node.",
        "parameters": [
          {
            "type": "string",
            "serialized_type": "int64",
            "name": "id",
            "description": "The ID of the root of the subtree to retrieve."
          }
        ],
        "returns_async": {
          "name": "callback",
          "parameters": [
            {
              "name": "results",
              "type": "array",
              "items": { "$ref": "BookmarkTreeNode" }
            }
          ]
        }
      },
      {
        "name": "search",
        "type": "function",
        "description": "Searches for BookmarkTreeNodes matching the given query. Queries specified with an object produce BookmarkTreeNodes matching all specified properties.",
        "parameters": [
          {
            "name": "query",
            "description": "Either a string of words and quoted phrases that are matched against bookmark URLs and titles, or an object. If an object, the properties <code>query</code>, <code>url</code>, and <code>title</code> may be specified and bookmarks matching all specified properties will be produced.",
            "choices": [
              {
                "type": "string",
                "description": "A string of words and quoted phrases that are matched against bookmark URLs and titles."
              },
              {
                "type": "object",
                "description": "An object specifying properties and values to match when searching. Produces bookmarks matching all properties.",
                "properties": {
                  "query": {
                    "type": "string",
                    "optional": true,
                    "description": "A string of words and quoted phrases that are matched against bookmark URLs and titles."
                  },
                  "url": {
                    "type": "string",
                    "optional": true,
                    "description": "The URL of the bookmark; matches verbatim. Note that folders have no URL."
                  },
                  "title": {
                    "type": "string",
                    "optional": true,
                    "description": "The title of the bookmark; matches verbatim."
                  }
                }
              }
            ]
          }
        ],
        "returns_async": {
          "name": "callback",
          "parameters": [
            {
              "name": "results",
              "type": "array",
              "items": { "$ref": "BookmarkTreeNode" }
            }
          ]
        }
      },
      {
        "name": "create",
        "type": "function",
        "description": "Creates a bookmark or folder under the specified parentId.  If url is NULL or missing, it will be a folder.",
        "parameters": [
          {
            "$ref": "CreateDetails",
            "name": "bookmark"
          }
        ],
        "returns_async": {
          "name": "callback",
          "optional": true,
          "parameters": [
            {
              "name": "result",
              "$ref": "BookmarkTreeNode"
            }
          ]
        }
      },
      {
        "name": "move",
        "type": "function",
        "description": "Moves the specified BookmarkTreeNode to the provided location.",
        "parameters": [
          {
            "type": "string",
            "serialized_type": "int64",
            "name": "id"
          },
          {
            "type": "object",
            "name": "destination",
            "properties": {
              "parentId": {
                "type": "string",
                "optional": true
              },
              "index": {
                "type": "integer",
                "minimum": 0,
                "optional": true
              }
            }
          }
        ],
        "returns_async": {
          "name": "callback",
          "optional": true,
          "parameters": [
            {
              "name": "result",
              "$ref": "BookmarkTreeNode"
            }
          ]
        }
      },
      {
        "name": "update",
        "type": "function",
        "description": "Updates the properties of a bookmark or folder. Specify only the properties that you want to change; unspecified properties will be left unchanged.  <b>Note:</b> Currently, only 'title' and 'url' are supported.",
        "parameters": [
          {
            "type": "string",
            "serialized_type": "int64",
            "name": "id"
          },
          {
            "type": "object",
            "name": "changes",
            "properties": {
              "title": {
                "type": "string",
                "optional": true
              },
              "url": {
                "type": "string",
                "optional": true
              }
            }
          }
        ],
        "returns_async": {
          "name": "callback",
          "optional": true,
          "parameters": [
            {
              "name": "result",
              "$ref": "BookmarkTreeNode"
            }
          ]
        }
      },
      {
        "name": "remove",
        "type": "function",
        "description": "Removes a bookmark or an empty bookmark folder.",
        "parameters": [
          {
            "type": "string",
            "serialized_type": "int64",
            "name": "id"
          }
        ],
        "returns_async": {
          "name": "callback",
          "optional": true,
          "parameters": []
        }
      },
      {
        "name": "removeTree",
        "type": "function",
        "description": "Recursively removes a bookmark folder.",
        "parameters": [
          {
            "type": "string",
            "serialized_type": "int64",
            "name": "id"
          }
        ],
        "returns_async": {
          "name": "callback",
          "optional": true,
          "parameters": []
        }
      }
    ],
    "events": [
      {
        "name": "onCreated",
        "type": "function",
        "description": "Fired when a bookmark or folder is created.",
        "parameters": [
          {
            "type": "string",
            "name": "id"
          },
          {
            "$ref": "BookmarkTreeNode",
            "name": "bookmark"
          }
        ]
      },
      {
        "name": "onRemoved",
        "type": "function",
        "description": "Fired when a bookmark or folder is removed.  When a folder is removed recursively, a single notification is fired for the folder, and none for its contents.",
        "parameters": [
          {
            "type": "string",
            "name": "id"
          },
          {
            "type": "object",
            "name": "removeInfo",
            "properties": {
              "parentId": { "type": "string" },
              "index": { "type": "integer" },
              "node": { "$ref": "BookmarkTreeNode" }
            }
          }
        ]
      },
      {
        "name": "onChanged",
        "type": "function",
        "description": "Fired when a bookmark or folder changes.  <b>Note:</b> Currently, only title and url changes trigger this.",
        "parameters": [
          {
            "type": "string",
            "name": "id"
          },
          {
            "type": "object",
            "name": "changeInfo",
            "properties": {
              "title": { "type": "string" },
              "url": {
                "type": "string",
                "optional": true
              }
            }
          }
        ]
      },
      {
        "name": "onMoved",
        "type": "function",
        "description": "Fired when a bookmark or folder is moved to a different parent folder.",
        "parameters": [
          {
            "type": "string",
            "name": "id"
          },
          {
            "type": "object",
            "name": "moveInfo",
            "properties": {
              "parentId": { "type": "string" },
              "index": { "type": "integer" },
              "oldParentId": { "type": "string" },
              "oldIndex": { "type": "integer" }
            }
          }
        ]
      },
      {
        "name": "onChildrenReordered",
        "type": "function",
        "description": "Fired when the children of a folder have changed their order due to the order being sorted in the UI.  This is not called as a result of a move().",
        "parameters": [
          {
            "type": "string",
            "name": "id"
          },
          {
            "type": "object",
            "name": "reorderInfo",
            "properties": {
              "childIds": {
                "type": "array",
                "items": { "type": "string" }
              }
            }
          }
        ]
      },
      {
        "name": "onImportBegan",
        "type": "function",
        "description": "Fired when a bookmark import session is begun.  Expensive observers should ignore onCreated updates until onImportEnded is fired.  Observers should still handle other notifications immediately.",
        "parameters": []
      },
      {
        "name": "onImportEnded",
        "type": "function",
        "description": "Fired when a bookmark import session is ended.",
        "parameters": []
      }
    ]
  }
]

bookmarks.json生成对应文件:

out\Debug\gen\chrome\common\extensions\api\bookmarks.cc

out\Debug\gen\chrome\common\extensions\api\bookmarks.h

三、bookmarks函数实现:

chrome\browser\extensions\api\bookmarks\bookmarks_api.h

chrome\browser\extensions\api\bookmarks\bookmarks_api.cc

cpp 复制代码
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_EXTENSIONS_API_BOOKMARKS_BOOKMARKS_API_H_
#define CHROME_BROWSER_EXTENSIONS_API_BOOKMARKS_BOOKMARKS_API_H_

#include <stdint.h>

#include <memory>
#include <set>
#include <string>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/values.h"
#include "components/bookmarks/browser/base_bookmark_model_observer.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "extensions/browser/browser_context_keyed_api_factory.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_function.h"
#include "ui/shell_dialogs/select_file_dialog.h"

class Profile;

namespace base {
class FilePath;
}

namespace bookmarks {
class BookmarkModel;
class ManagedBookmarkService;
}

namespace content {
class BrowserContext;
}

namespace extensions {

namespace api {
namespace bookmarks {
struct CreateDetails;
}
}

// Observes BookmarkModel and then routes the notifications as events to
// the extension system.
class BookmarkEventRouter : public bookmarks::BookmarkModelObserver {
 public:
  explicit BookmarkEventRouter(Profile* profile);
  BookmarkEventRouter(const BookmarkEventRouter&) = delete;
  BookmarkEventRouter& operator=(const BookmarkEventRouter&) = delete;
  ~BookmarkEventRouter() override;

  // bookmarks::BookmarkModelObserver:
  void BookmarkModelLoaded(bookmarks::BookmarkModel* model,
                           bool ids_reassigned) override;
  void BookmarkModelBeingDeleted(bookmarks::BookmarkModel* model) override;
  void BookmarkNodeMoved(bookmarks::BookmarkModel* model,
                         const bookmarks::BookmarkNode* old_parent,
                         size_t old_index,
                         const bookmarks::BookmarkNode* new_parent,
                         size_t new_index) override;
  void BookmarkNodeAdded(bookmarks::BookmarkModel* model,
                         const bookmarks::BookmarkNode* parent,
                         size_t index,
                         bool added_by_user) override;
  void BookmarkNodeRemoved(bookmarks::BookmarkModel* model,
                           const bookmarks::BookmarkNode* parent,
                           size_t old_index,
                           const bookmarks::BookmarkNode* node,
                           const std::set<GURL>& removed_urls) override;
  void BookmarkAllUserNodesRemoved(bookmarks::BookmarkModel* model,
                                   const std::set<GURL>& removed_urls) override;
  void BookmarkNodeChanged(bookmarks::BookmarkModel* model,
                           const bookmarks::BookmarkNode* node) override;
  void BookmarkNodeFaviconChanged(bookmarks::BookmarkModel* model,
                                  const bookmarks::BookmarkNode* node) override;
  void BookmarkNodeChildrenReordered(
      bookmarks::BookmarkModel* model,
      const bookmarks::BookmarkNode* node) override;
  void ExtensiveBookmarkChangesBeginning(
      bookmarks::BookmarkModel* model) override;
  void ExtensiveBookmarkChangesEnded(bookmarks::BookmarkModel* model) override;

 private:
  // Helper to actually dispatch an event to extension listeners.
  void DispatchEvent(events::HistogramValue histogram_value,
                     const std::string& event_name,
                     base::Value::List event_args);

  raw_ptr<content::BrowserContext> browser_context_;
  raw_ptr<bookmarks::BookmarkModel> model_;
  raw_ptr<bookmarks::ManagedBookmarkService> managed_;
};

class BookmarksAPI : public BrowserContextKeyedAPI,
                     public EventRouter::Observer {
 public:
  explicit BookmarksAPI(content::BrowserContext* context);
  ~BookmarksAPI() override;

  // KeyedService implementation.
  void Shutdown() override;

  // BrowserContextKeyedAPI implementation.
  static BrowserContextKeyedAPIFactory<BookmarksAPI>* GetFactoryInstance();

  // EventRouter::Observer implementation.
  void OnListenerAdded(const EventListenerInfo& details) override;

 private:
  friend class BrowserContextKeyedAPIFactory<BookmarksAPI>;

  raw_ptr<content::BrowserContext> browser_context_;

  // BrowserContextKeyedAPI implementation.
  static const char* service_name() {
    return "BookmarksAPI";
  }
  static const bool kServiceIsNULLWhileTesting = true;

  // Created lazily upon OnListenerAdded.
  std::unique_ptr<BookmarkEventRouter> bookmark_event_router_;
};

class BookmarksFunction : public ExtensionFunction,
                          public bookmarks::BaseBookmarkModelObserver {
 public:
  // ExtensionFunction:
  ResponseAction Run() override;

 protected:
  ~BookmarksFunction() override {}

  // Run semantic equivalent called when the bookmarks are ready.
  // Overrides can return nullptr to further delay responding (a.k.a.
  // RespondLater()).
  virtual ResponseValue RunOnReady() = 0;

  // Helper to get the BookmarkModel.
  bookmarks::BookmarkModel* GetBookmarkModel();

  // Helper to get the ManagedBookmarkService.
  bookmarks::ManagedBookmarkService* GetManagedBookmarkService();

  // Helper to get the bookmark node from a given string id.
  // If the given id can't be parsed or doesn't refer to a valid node, sets
  // |error| and returns nullptr.
  const bookmarks::BookmarkNode* GetBookmarkNodeFromId(
      const std::string& id_string,
      std::string* error);

  // Helper to create a bookmark node from a CreateDetails object. If a node
  // can't be created based on the given details, sets |error| and returns
  // nullptr.
  const bookmarks::BookmarkNode* CreateBookmarkNode(
      bookmarks::BookmarkModel* model,
      const api::bookmarks::CreateDetails& details,
      std::string* error);

  // Helper that checks if bookmark editing is enabled.
  bool EditBookmarksEnabled();

  // Helper that checks if |node| can be modified. Returns false if |node|
  // is nullptr, or a managed node, or the root node. In these cases the node
  // can't be edited, can't have new child nodes appended, and its direct
  // children can't be moved or reordered.
  bool CanBeModified(const bookmarks::BookmarkNode* node, std::string* error);

  Profile* GetProfile();

 private:
  // bookmarks::BaseBookmarkModelObserver:
  void BookmarkModelChanged() override;
  void BookmarkModelLoaded(bookmarks::BookmarkModel* model,
                           bool ids_reassigned) override;

  // ExtensionFunction:
  void OnResponded() override;
};

class BookmarksGetFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.get", BOOKMARKS_GET)

 protected:
  ~BookmarksGetFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksGetChildrenFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.getChildren", BOOKMARKS_GETCHILDREN)

 protected:
  ~BookmarksGetChildrenFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksGetRecentFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.getRecent", BOOKMARKS_GETRECENT)

 protected:
  ~BookmarksGetRecentFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksGetTreeFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.getTree", BOOKMARKS_GETTREE)

 protected:
  ~BookmarksGetTreeFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksGetSubTreeFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.getSubTree", BOOKMARKS_GETSUBTREE)

 protected:
  ~BookmarksGetSubTreeFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksSearchFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.search", BOOKMARKS_SEARCH)

 protected:
  ~BookmarksSearchFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksRemoveFunctionBase : public BookmarksFunction {
 protected:
  ~BookmarksRemoveFunctionBase() override {}

  virtual bool is_recursive() const = 0;

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksRemoveFunction : public BookmarksRemoveFunctionBase {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.remove", BOOKMARKS_REMOVE)

 protected:
  ~BookmarksRemoveFunction() override {}

  // BookmarksRemoveFunctionBase:
  bool is_recursive() const override;
};

class BookmarksRemoveTreeFunction : public BookmarksRemoveFunctionBase {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.removeTree", BOOKMARKS_REMOVETREE)

 protected:
  ~BookmarksRemoveTreeFunction() override {}

  // BookmarksRemoveFunctionBase:
  bool is_recursive() const override;
};

class BookmarksCreateFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.create", BOOKMARKS_CREATE)

 protected:
  ~BookmarksCreateFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksMoveFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.move", BOOKMARKS_MOVE)

 protected:
  ~BookmarksMoveFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

class BookmarksUpdateFunction : public BookmarksFunction {
 public:
  DECLARE_EXTENSION_FUNCTION("bookmarks.update", BOOKMARKS_UPDATE)

 protected:
  ~BookmarksUpdateFunction() override {}

  // BookmarksFunction:
  ResponseValue RunOnReady() override;
};

}  // namespace extensions

#endif  // CHROME_BROWSER_EXTENSIONS_API_BOOKMARKS_BOOKMARKS_API_H_

总结:扩展通过chrome.bookmarks.get 等方法会进入此实现,需要拦截更改可以在此处修改。

相关推荐
每天都要喝奶茶20 分钟前
vue3uniapp实现自定义拱形底部导航栏,解决首次闪烁问题
前端·vue.js·uni-app
May_Xu_22 分钟前
vue3+less使用主题定制(多主题定制)可切换主题
前端·javascript·vue.js·vue·less·css3
qq_4275060822 分钟前
less解决function中return写法在浏览器被识别成Object导致样式失败的问题
前端·css·less
Elastic 中国社区官方博客28 分钟前
将你的 Kibana Dev Console 请求导出到 Python 和 JavaScript 代码
大数据·开发语言·前端·javascript·python·elasticsearch·ecmascript
北京_宏哥1 小时前
《最新出炉》系列入门篇-Python+Playwright自动化测试-41-录制视频
前端·python·测试
小霖家的混江龙1 小时前
Vite 打包 H5 如何注入版本号
前端·vite
叶不休2 小时前
DOM---鼠标事件类型(移入移出)
开发语言·前端·javascript·css·chrome·前端框架·html
爱编程的鱼2 小时前
web前后端交互方式有哪些?
前端·okhttp
言6662 小时前
vue点击菜单,出现2个相同tab,啥原因
前端·javascript·vue.js
Z_ One Dream2 小时前
css 在 hover 子元素时,不要让父元素触发 hover 效果
前端·javascript·css