蒙 ArkTS(HarmonyOS ArkTS)是适用于鸿蒙操作系统(HarmonyOS)的 TypeScript 开发框架。在鸿蒙 ArkTS 中实现动态添加 `tabcontent` 的功能,与在 Unity 中使用 ArkTS 的过程类似,都需要通过编程来控制用户界面的显示。
以下是一个基于鸿蒙 ArkTS 的简单示例,用于动态添加 `tabcontent`:
1. **定义Tab组件**:首先,你需要定义一个Tab组件,用于表示每个Tab页。这个组件应该包含所有你希望在Tab页上展示的内容。
2. **创建Tab容器**:在ArkTS中,你可以创建一个容器来管理所有的Tab页面。这个容器可以是一个简单的StackPanel或者Grid,用于排列所有的Tab内容。
3. **动态添加Tab内容**:通过编程的方式,你可以根据需要动态地创建Tab组件,并将其添加到Tab容器中。
下面是一个简化的ArkTS TypeScript示例代码,说明如何实现动态添加Tab内容的逻辑:
import { Component, Prop, h } from '@akeros/arkts';
@Component
export class TabContent extends HTMLElement {
@Prop() tabId: string;
@Prop() content: string;
constructor(private host: HTMLElement) {
super();
this.render();
}
render() {
this.innerHTML = `
<div class="tab-content">
<h2>${this.content}</h2>
</div>
`;
}
}
@Component
export class TabController extends HTMLElement {
private tabs: Map<string, TabContent> = new Map();
addTab(tabId: string, content: string) {
const tabContent = new TabContent(this.host);
tabContent.tabId = tabId;
tabContent.content = content;
this.tabs.set(tabId, tabContent);
this.host.appendChild(tabContent);
}
updateTab(tabId: string, newContent: string) {
const tabContent = this.tabs.get(tabId);
if (tabContent) {
tabContent.content = newContent;
tabContent.render();
}
}
removeTab(tabId: string) {
const tabContent = this.tabs.get(tabId);
if (tabContent) {
tabContent.remove();
this.tabs.delete(tabId);
}
}
}
在这个示例中,`TabContent` 组件用于表示单个Tab的内容,而 `TabController` 组件则负责管理所有的Tab页面。`addTab` 方法用于添加新的Tab内容,`updateTab` 方法用于更新现有Tab的内容,而 `removeTab` 方法则用于删除不再需要的Tab。
请注意,这个代码示例是一个简化的版本,用于说明如何在鸿蒙 ArkTS 中动态添加和更新Tab内容。在实际应用中,你可能需要根据具体的鸿蒙 ArkTS 版本和项目需求来调整代码。
在鸿蒙 ArkTS 项目中,你可以通过调用 `TabController` 组件的方法来动态地添加、更新或删除Tab内容,从而实现灵活的用户界面管理。
您2019猪事顺利,心想事成。
Tab 切换是种很常见的网页呈现形式,不管是PC或者H5都会经常看到,今天就为小伙伴们提供多种纯CSS Tab 切换的实现方式,同时对比一下那种代码更方便,更通俗易懂。
3种纯CSS方式实现Tab 切换
纯CSS实现都面临2个问题:
1、 如何接收点击事件?
2、 如何操作相关DOM?
拥有 checked 属性的表单元素, <input type="radio"> 或者 <input type="checkbox"> 能够接收到点击事件。
知识点:
1、 使用 radio 标签的 :checked 伪类,加上 <label for> 实现纯 CSS 捕获点击事情
2、 使用了 ~ 选择符对样式进行控制
<div class="container"> <input class="nav1" id="li1" type="radio" name="nav"> <input class="nav2" id="li2" type="radio" name="nav"> <ul class='nav'> <li class='active'><label for="li1">tab1</label></li> <li><label for="li2">tab2</label></li> </ul> <div class="content"> <div class="content1 default">tab1 内容:123456</div> <div class="content2">tab2 内容:abcdefgkijkl</div> </div> </div>
添加样式
.container *{ padding: 0; margin: 0; } .container { position: relative; width: 400px; margin: 50px auto; } .container input { display: none; } .nav { position: relative; overflow: hidden; } .nav li { width: 200px; float: left; text-align: center; background: #ddd; list-style: none; } .nav li label { display: block; width: 200px; line-height: 36px; font-size: 18px; cursor: pointer; } .content { position: relative; overflow: hidden; width: 400px; height: 100px; border: 1px solid #999; box-sizing: border-box; padding: 10px; } .content1, .content2 { display: none; width: 100%; height: 100%; } .nav1:checked ~ .nav li { background: #ddd; color: #000; } .nav1:checked ~ .nav li:first-child { background: #ff7300; color: #fff; } .nav2:checked ~ .nav li { background: #ddd; color: #000; } .nav2:checked ~ .nav li:last-child { background: #ff7300; color: #fff; } .nav1:checked ~ .content > div { display: none; } .nav1:checked ~ .content > div:first-child { display: block; } .nav2:checked ~ .content > div { display: none; } .nav2:checked ~ .content > div:last-child { display: block; } .nav li.active { background: #ff7300; color: #fff; } .content .default { display: block; }
知识点:
1、 要使用 :target 伪元素,需要 HTML 锚点,以及锚点对应的 HTML 片段
2、 核心是使用 :target 伪类接收点击事件
3、 通过兄弟选择符 ~ 控制样式
<div class="container"> <div id="content1" class="active">tab 1内容:123456</div> <div id="content2">tab 2内容:abcdefgkijkl</div> <ul class='nav'> <li class="active"><a href="#content1">tab1</a></li> <li><a href="#content2">tab2</a></li> </ul> <div class="wrap"></div> </div>
添加样式
.container *{ padding: 0; margin: 0; } .container { position: relative; width: 400px; margin: 50px auto; } .nav { position: relative; overflow: hidden; } li { width: 200px; float: left; text-align: center; background: #ddd; list-style: none; } li a { display: block; width: 200px; line-height: 36px; font-size: 18px; cursor: pointer; text-decoration: none; color: #000; } #content1, #content2 { position: absolute; overflow: hidden; top: 36px; width: 400px; height: 100px; border: 1px solid #999; box-sizing: border-box; padding: 10px; } #content1, #content2 { display: none; width: 100%; background: #fff; } #content1:target, #content2:target { display: block; } #content1.active { display: block; } .active ~ .nav li:first-child { background: #ff7300; color: #fff; } #content1:target ~ .nav li { background: #ddd; color: #000; } #content1:target ~ .nav li:first-child { background: #ff7300; color: #fff; } #content2:target ~ .nav li { background: #ddd; color: #000; } #content2:target ~ .nav li:last-child { background: #ff7300; color: #fff; } .wrap { position: absolute; overflow: hidden; top: 36px; width: 400px; height: 100px; border: 1px solid #999; box-sizing: border-box; }
:focus-within 它表示一个元素获得焦点,或该元素的后代元素获得焦点。
重点:它或它的后代获得焦点。
这也就意味着,它或它的后代获得焦点,都可以触发 :focus-within。
知识点
1、 这个属性有点类似 Javascript 的事件冒泡,从可获焦元素开始一直冒泡到根元素 html,都可以接收触发 :focus-within 事件
2、 本例子的思路就是通过获焦态来控制其他选择器,以及最重要的是利用了父级的 :not(:focus-within) 来设置默认样式
<div class="container"> <div class="nav-box"> <button class="nav1">tab1</button> <button class="nav2">tab2</button> <div class="content-box"> <div class="content1"> content-1 </div> <div class="content2"> content-2 </div> </div> </div> </div>
添加样式
.container { width: 300px; margin: 50px auto; padding: 10px; boder: 1px solid #ddd; } .nav-box { font-size: 0; } button { width: 150px; height: 40px; box-sizing: border-box; outline: none; background: #fff; border: 1px solid #ddd; font-size: 18px; cursor: pointer; } button:focus-within { color: #fff; background: #ff7300; } .content-box { font-size: 24px; border: 1px solid #ddd; height: 100px; } .content-box div { display: none; } .nav-box:not(:focus-within) .nav1 { color: #fff; background: #ff7300; } .nav-box:not(:focus-within) .content1 { display: block; } .nav1:focus-within ~ .content-box .content1 { display: block; } .nav2:focus-within ~ .content-box .content2 { display: block; }
3种纯CSS方式实现Tab 切换
这个效果就很差一些,因为,在tab失去焦点时,就会复原,回到tab1上面,并不推荐这种方式来实现。小编推荐第一种:checked实现方式,更容易理解。
喜欢小编的点击关注,了解更多知识!
源码地址和源文件下载请点击下方“了解更多”
着入职时间变长,工作不断的深入,在需要同时处理多个任务的同时,打开几十上百个浏览器 Tab 页就必不可少了,而我的工作几乎都是在各种浏览器 Tab 页之间来回切换,如写文档、学习新知识、处理 Bug 单流转、上线等流程,所以我需要对浏览器的 Tab 页进行精细化管理,以达到精细化管理工作流程的目的,于是乎,我对于浏览器的使用变成了下面几个阶段:
Chrome - 杂乱无章阶段
Chrome - 进行适当整理
Edge - 竖向侧边栏
但是无论浏览器层面提供多少这样或那样的辅助,但毕竟浏览器的职责主要是负责帮助你更好、更快、更高效的浏览网页,并非是帮你管理知识和工作流程,所以如果需要个性化定制的需求,就得自己上手开发啦!毕竟作为程序员,自己动手,丰衣足食嘛 。
我希望能够开发一个 Chrome 浏览器插件,当前其他浏览器如 Edge、Firefox、Brave,以及其他所有使用 Chromimum 开发的浏览器都是支持 Chrome 插件格式的,而这几大浏览器几乎占据了近 83% 左右的桌面端浏览器市场,所以这个 Chrome 插件可以在我喜欢的浏览器上运行。
以下是 2020.3 到 2021.3 的桌面端浏览器占比数据
这个浏览器支持传统的插件点击弹出栏,以及每次打开一个新 Tab 都能展示我的应用,这样能够帮助我随时了解我当前正在进行的工作,大致形式如下:
弹出栏:
新 Tab:
针对上面需求的形式不知道大家是否比较熟悉了?没错,这个插件的框架形式和 掘金 的插件类似,我们看下掘金的 Chrome 插件:
弹出框:
新 Tab:
也就是说,在看完本次文章,你基本上拥有了开发一个掘金插件的能力,心动了?
随便一提,我们本次开发插件的技术栈如下:
通过先进的技术栈来编写 Chrome 插件。
Chrome 插件实际上包含几个部分:
上述 5 大文件组成了一个 Chrome 插件所需要的必须元素,逻辑关系如下:
image.png
可以看到,其实开发一个 Chrome 的插件也是使用 HTML/JavaScript/CSS 这些知识,只不过使用场景,每种 JavaScript 使用的权限与功能、操作的 API 不太一样,那么既然是使用基本的 Web 基础技术,我们就可以借助更为上层的 Web 开发框架如 React 等来将 Chrome 插件的开发上升到一个现代化的程度。
确保你安装了最新版的 Node.js,然后在命令行中运行如下命令:
npx create-react-app chrome-react-extension --template typescript
初始化好项目、安装完依赖之后,我们可以看到 CRA 产生的模板代码,其中就有我们需要的 public/manifest.json 文件:
当然内容并没有我们上图那样丰富我们需要做一些修改,将内容改为如下内容:
{
"name": "Chrome React Extension",
"description": "使用 React TypeScript 构建 Chrome 扩展",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Open the popup"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
}
}
上述的字段说明如下:
实际上 Chrome 插件只能理解原生的 JavaScript,CSS,HTML 等, 所以我们使用 React 学完之后,需要进行构建,将构建的产物打包给到浏览器插件去加载使用,在构建时,还有一个需要注意的就是,为了保证最优化性能,CRA 的脚本在构建时会将一些小的 JS 文件等,内联到 HTML 文件中,而不是打包成独立的 JS 文件,在 Chrome 插件的运行环境下,这种形式的 HTML 是不支持的,会触发插件的 CSP(内容安全策略)错误。
所以为了测试我们的插件当前效果,我们修改构建脚本,在 package.json 里面:
"scripts": {
"start": "react-scripts start",
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
通过设置 INLINE_RUNTIME_CHUNK=false 确保所有的 JS 会构建成独立的文件,然后引入到 HTML 中加载使用。
一切准备完毕,是时候构建我们的 React 应用了~ 在命令行中运行如下命令:
npm run build
会发现内容构建输出在 build/xxx 下面,包含 manifest.json、index.html、对应的 JS/CSS 文件还有图片等,其中 manifest 中索引了 index.html 来作为点击插件时的 Popup 的展示页,这个时候我们就可以使用 Chrome 加载我们构建好的文件,来查看插件运行效果了:
我们打开扩展程序面板,设置开发者模式,然后点击加载文件,选择我们的 build 文件地址加载:
Magic !我们可以在浏览器里面看到我们的插件,并使用它了,一个最简化插件完成!
当然这里我们虽然能够使用 React/TypeScript 以及一切现代的 Web 开发技术来写插件,但是目前没有很好的方式能够实时的进行开发-查看效果,就是我们常见的 HMR、Live Reload 这种技术暂时还没有很好的支持到 Chrome 插件的开发,所以每次我们需要查看编写的效果都需要构建之后点击插件查看。
当然如果纯针对 UI 或者和 Chrome API 无关的逻辑,那么你可以放心的直接在 Web 里面开发,等到开发完毕再构建到 Chrome 插件预览即可。
我们之前的逻辑是,只要新开一个 Tab,那么就会访问我们提供的页面,类似掘金的插件,而且我们也主要到,其实针对 Popup 页面只是几个按钮,而重头戏都在新 Tab 页界面展示,也就是我们这里其实需要一个多页应用?因为最终要生成页面,一个用在 Popup 页面展示,一个用在新 Tab 页展示。
但是我们知道 CRA 脚手架生成的模板是主要用于单页应用,如果需要切换到多页应用有一定的成本,但是我们的 Popup 页面实际上就只有几个按钮,所以这里可以做一层简化,即 Popup 页面直接手动写最原始的 HTML/JS/CSS,然后将重头戏、复杂的新 Tab 页的逻辑来用 React TypeScript 等现代 Web 技术来开发。
通过这样设计之后,我们的目录结构变成了如下形式:
其中 manifest.json 的逻辑变成了如下:
{
"name": "Chrome React SEO Extension",
"description": "The power of React and TypeScript for building interactive Chrome extensions",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "./popup/index.html",
"default_title": "Open the popup"
},
"chrome_url_overrides": {
"newtab": "index.html"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
}
}
我们可以看到,点击 Chrome 插件弹出的页面 Popup,换成了 ./popup/index.html ,而我们新加了一个 chrome_url_overrides 字段,在 newtab 时,我们打开构建后的 index.html 文件。
通过上面的操作,我们每次打开一个新 Tab,都会展示下面的页面:
完美!我们已经实现了掘金的插件的核心思想:便捷的获取技术知识,就在你每次打开 Tab 时。
接下来我们尝试改造一下我们的 Popup 页面,同样是对标掘金,我们知道掘金的 Popup 页面是一个比较简单的菜单栏,里面主要是一些用于跳转到新 Tab 或者设置页的操作:
我们现在也需要实现类似的点击某个按钮,跳转到我们新 Tab 页,打开我们上一部分定制的 Tab 逻辑。
这一部分我们就需要修改 popup/index.html ,添加相关的 JS 逻辑如下:
<!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>Fake Juejin Extensions</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<ul>
<li class="open_new_tab">打开新标签页</li>
<li class="go_to_github">访问 Github</li>
<li class="go_to_settings">设置</li>
</ul>
<script src="popup.js"></script>
</body>
</html>
这一次需求我们只会操作打开新标签页、访问 Github,设置我们不做操作,留给读者自己去扩展。
可以看到我们导入了 popup.js 文件,在这个 JS 文件里,我们需要完成对应打开新标签页、和访问 Github 的逻辑配置:
document.querySelector(".open_new_tab").addEventListener("click", (e) => {
chrome.tabs.create({}, () => {});
});
document.querySelector(".go_to_github").addEventListener("click", (e) => {
window.open("https://github.com");
});
可以看到,因为 popup.js 是运行在 Chrome 插件的沙箱环境下的,所以它能够使用到 chrome 这个 API,进行页面、浏览器等相关的操作。
当我们写入了上述逻辑之后,我们就可以点击对应的打开新标签页,访问新标签页并展示我们上一节说到的内容,访问 Github,则会跳转到 Github 页面。
我们已经开发了新 Tab 页,开发了 Popup 逻辑,接下来我们可以尝试一下通过 content 脚本,来实现用户页面与插件脚本进行通信,以间接的操作 DOM。
首先我们需要在 manifest.json 里面注册 content 相关的脚本:
{
"name": "Chrome React Extension",
// ...
"permissions": ["activeTab", "tabs"],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["./static/js/content.js"]
}
]
}
上述脚本通过 content_scripts 指定 content 脚本,matches 指定匹配到那些域名时,才执行这个注入脚本的逻辑,js 代表需要注入的脚本的位置,这里我们填写的为 ./static/js/content.js ,即为通过构建之后产生的 JS 内容地址。
接着我们在 Tab 页的 React 项目里面去建立与 content 脚本的通信:
import React from "react";
import "./App.css";
import { DOMMessage, DOMMessageResponse } from "./types";
function App() {
// 前置逻辑
React.useEffect(() => {
/**
* We can't use "chrome.runtime.sendMessage" for sending messages from React.
* For sending messages from React we need to specify which tab to send it to. */ chrome.tabs &&
chrome.tabs.query(
{
active: true,
currentWindow: true,
},
(tabs) => {
/**
* Sends a single message to the content script(s) in the specified tab,
* with an optional callback to run when a response is sent back.
*
* The runtime.onMessage event is fired in each content script running
* in the specified tab for the current extension. */ chrome.tabs.sendMessage(
tabs[0].id || 0,
{ type: "GET_DOM" } as DOMMessage,
(response: DOMMessageResponse) => {
setTitle(response.title);
setHeadlines(response.headlines);
}
);
}
);
});
return (
// ... 模板
);
}
export default App;
可以看到我们通过 chome API,去查询当前正在激活的 Tab 页,然后给这个 Tab 页的 content 脚本,通过 chrome.tabs.sendMessage 发了一个 { type: "GET_DOM" } 的消息。
然后我们创建对应的 content 的脚本,在 src/chromeServices 下创建 DOMEvaluator.ts:
import { DOMMessage, DOMMessageResponse } from "../types";
// Function called when a new message is received const messagesFromReactAppListener = (
msg: DOMMessage,
sender: chrome.runtime.MessageSender,
sendResponse: (response: DOMMessageResponse) => void
) => {
console.log("[content.js]. Message received", msg);
const headlines = Array.from(document.getElementsByTagName<"h1">("h1")).map(
(h1) => h1.innerText
);
// Prepare the response object with information about the site const response: DOMMessageResponse = {
title: document.title,
headlines,
};
sendResponse(response);
};
/**
* Fired when a message is sent from either an extension process or a content script. */ chrome.runtime.onMessage.addListener(messagesFromReactAppListener);
这个脚本在加载的时候,通过 onMessage.addListener 监听,然后回调 messagesFromReactAppListener ,在函数里面,可以直接获取 DOM,查询这个页面中的 标题 和所有的 H1 标签,然后返回。
import React from "react";
import "./App.css";
import { DOMMessage, DOMMessageResponse } from "./types";
function App() {
const [title, setTitle] = React.useState("");
const [headlines, setHeadlines] = React.useState<string[]>([]);
// ...消息通信逻辑
return (
// ... 模板
<div className="App">
<h1>SEO Extension built with React!</h1>
<ul className="SEOForm">
<li className="SEOValidation">
<div className="SEOValidationField">
<span className="SEOValidationFieldTitle">Title</span>
<span
className={`SEOValidationFieldStatus ${
title.length < 30 || title.length > 65 ? "Error" : "Ok"
}`}
>
{title.length} Characters
</span>
</div>
<div className="SEOVAlidationFieldValue">{title}</div>
</li>
<li className="SEOValidation">
<div className="SEOValidationField">
<span className="SEOValidationFieldTitle">Main Heading</span>
<span
className={`SEOValidationFieldStatus ${
headlines.length !== 1 ? "Error" : "Ok"
}`}
>
{headlines.length}
</span>
</div>
<div className="SEOVAlidationFieldValue">
<ul>
{headlines.map((headline, index) => (
<li key={index}>{headline}</li>
))}
</ul>
</div>
</li>
</ul>
</div>
);
}
export default App;
然后扩展一下 CSS 代码:
.App {
background: #edf0f6;
padding: 0.5rem;
}
.SEOForm {
list-style: none;
margin: 0;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 10%), 0 1px 2px 0 rgb(0 0 0 / 6%);
background: #fff;
padding: 1rem;
}
.SEOValidation {
margin-bottom: 1.5rem;
}
.SEOValidationField {
width: 100%;
display: flex;
justify-content: space-between;
}
.SEOValidationFieldTitle {
font-size: 1rem;
color: #1a202c;
font-weight: bold;
}
.SEOValidationFieldStatus {
color: #fff;
padding: 0 1rem;
height: 1.5rem;
font-weight: bold;
align-items: center;
display: flex;
border-radius: 9999px;
}
.SEOValidationFieldStatus.Error {
background-color: #f23b3b;
}
.SEOValidationFieldStatus.Ok {
background-color: #48d660;
}
.SEOVAlidationFieldValue {
overflow-wrap: break-word;
width: 100%;
font-size: 1rem;
margin-top: 0.5rem;
color: #4a5568;
}
Nice!我们成功编写了新 Tab 页模板、逻辑与样式,以及创建了 Content 脚本逻辑,最后我们的展示效果如下:
然后我们需要进行代码构建,因为 content 我们使用 TypeScript 语法写,将 content 的逻辑构建为单独的 JS 输出,我们安装 craco 依赖,然后修改对应的脚本:
yarn add -D craco
// package.json
"scripts": {
"start": "react-scripts start",
"build": "INLINE_RUNTIME_CHUNK=false craco build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
将 react-scripts 改为 craco 。
然后新建 craco.config.js ,添加如下内容:
module.exports = {
webpack: {
configure: (webpackConfig, { env, paths }) => {
return {
...webpackConfig,
entry: {
main: [
env === "development" &&
require.resolve("react-dev-utils/webpackHotDevClient"),
paths.appIndexJs,
].filter(Boolean),
content: "./src/chromeServices/DOMEvaluator.ts",
},
output: {
...webpackConfig.output,
filename: "static/js/[name].js",
},
optimization: {
...webpackConfig.optimization,
runtimeChunk: false,
},
};
},
},
};
准备完毕,开始构建:yarn`` build ,我们会发现构建目录输出如下:
在本篇文章中,我们完整体验了使用 React+TypeScript,开发新 Tab 内容展示页以及 content 通信脚本,然后通过配置 react-scripts 为 craco 进行了分文件构建,以及直接开发原生的 popup 页,通过这种融汇的技术,成功开发出了一个类似掘金框架的 Chrome 插件。
这篇文章没有介绍的有 background 脚本,以及整体插件内容还不够完善,希望有兴趣的读者可以继续探索,将其完善。
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~
欢迎关注公众号 程序员巴士,来自字节、虾皮、招银的三端兄弟,分享编程经验、技术干货与职业规划,助你少走弯路进大厂。
*请认真填写需求信息,我们会在24小时内与您取得联系。