整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

WordPress Body Class 101:主

WordPress Body Class 101:主题设计师的提示和技巧

过使用WordPress的经验,我们发现主题设计师经常过度使用某种功能。当他们需要的只是一些简单的CSS时,他们正在寻找疯狂的WordPress过滤器和钩子来完成任务。WordPress默认生成很多CSS类。(WordPress CSS备忘单)。其中一个CSS类区域是body类样式。在本文中,我们将解释WordPress体类101以及为初学主题设计者分享一些有用的提示和技巧。

什么是WordPress Body Class?

Body Class(body_class)是一个WordPress函数,它为body元素提供不同的类,因此主题作者可以使用CSS有效地设置其网站的样式。HTML body标签主要存在于header.php文件中,该文件在每个页面上加载。编码主题时,可以将body_class函数附加到body元素,如下所示:

<body <?php body_class($class); ?>>

通过添加这个小元素,您可以灵活地使用CSS轻松修改每个页面的外观。根据所加载页面的类型,WordPress会自动添加相应的类。例如,如果您在存档页面上,如果您选择使用它,WordPress将自动将存档类添加到body元素。几乎每一页都能做到这一点。以下是WordPress可能添加的常见类的一些示例:

.rtl {}

.home {}

.blog {}

.archive {}

.date {}

.search {}

.paged {}

.attachment {}

.error404 {}

.single postid-(id) {}

.attachmentid-(id) {}

.attachment-(mime-type) {}

.author {}

.author-(user_nicename) {}

.category {}

.category-(slug) {}

.tag {}

.tag-(slug) {}

.page-parent {}

.page-child parent-pageid-(id) {}

.page-template page-template-(template file name) {}

.search-results {}

.search-no-results {}

.logged-in {}

.paged-(page number) {}

.single-paged-(page number) {}

.page-paged-(page number) {}

.category-paged-(page number) {}

.tag-paged-(page number) {}

.date-paged-(page number) {}

.author-paged-(page number) {}

.search-paged-(page number) {}

如何使用WordPress Body Class

在大多数情况下,WordPress body类是主题设计者经常忽略的简单解决方案。大约一年前,我需要设置仅在特定页面上设置WordPress菜单项的样式。我决定深入挖掘以找到一个过滤器,为菜单项添加一个特殊的导航类,这些菜单项将使用条件语句添加。例如,如果它是单个页面,则将“Blog”类添加到菜单项。我花了很多时间搞清楚所有这些并写一篇关于它的帖子,有用户指出可以很容易地用现有的体类来完成它,如下所示

.single #navigation .leftmenublog div{display: inline-block !important;}

注意:它如何使用在所有单个帖子页面中添加的.single body类。

给你另一个例子。一旦用户想要一种基于类别页面更改其网站上的徽标图像的方法。包括他在内的大多数主题设计都是使用CSS(#header .logo)加载徽标。我建议如何使用.category-slug body类来改变他的徽标:

.category-advice #header .logo{background: url(images/logo-advice.png) no-repeat !important;}

.category-health #header .logo{background: url(images/logo-health.png) no-repeat !important;}

希望现在您已经掌握了如何在主题设计中利用类。但是等等,这会变得更加强大,因为有一种方法可以使用过滤器添加自定义主体类。

如何添加自定义正文类

WordPress有一个过滤器,您可以使用它在需要时添加自定义正文类。向您展示如何使用过滤器添加一个body类,然后再向您展示特定的用例场景,以便每个人都可以在同一页面上。

因为body类是特定于主题的,所以你需要在functions.php文件中编写一个自定义函数,如下所示:

function

my_class_names(

$classes

) {

// add 'class-name' to the $classes array

$classes

[]=

'test-class-name'

;

// return the $classes array

return

$classes

;

}

//Now add test class to the filter

add_filter(

'body_class'

,

'my_class_names'

);

上面的代码将在您网站的每个页面上的body标签中添加一个“test-class-name”类。该函数的真正强大之处在于条件标记。您可以说只在单页等上添加XYZ类。

将类别类添加到单个帖子页面

假设您要自定义每个类别的单个帖子页面。根据自定义的类型,您可以为其使用正文类。为了简单起见,我们假设您要根据单个帖子的类别更改页面的背景颜色。您可以通过向单个帖子页面视图添加类别类来实现。将以下函数粘贴到主题的functions.php文件中:

// add category nicenames in body class

function category_id_class($classes) {

global $post;

foreach((get_the_category($post->ID)) as $category)

$classes[]=$category->category_nicename;

return $classes;

}

add_filter('body_class', 'category_id_class');

上面的代码将在您的正文类中为单个帖子页面添加类别类。然后,您可以根据需要使用css类来设置样式。

在WordPress主题的Body Class中添加Page Slug

将以下代码粘贴到主题的functions.php文件中:

//Page Slug Body Class

function add_slug_body_class( $classes ) {

global $post;

if ( isset( $post ) ) {

$classes[]=$post->post_type . '-' . $post->post_name;

}

return $classes;

}

add_filter( 'body_class', 'add_slug_body_class' );

浏览器检测和浏览器特定的正文类

有时您可能需要特殊的CSS样式才能在某些浏览器中正常工作(HINT:Internet Explorer)。好吧弥敦道赖斯提出了一个非常酷的解决方案,基于用户正在访问从网站浏览器上增加了浏览器的具体类体类。

您所要做的就是将以下代码粘贴到functions.php文件中:

<?php

add_filter('body_class','browser_body_class');

function browser_body_class($classes) {

global $is_lynx, $is_gecko, $is_IE, $is_opera, $is_NS4, $is_safari, $is_chrome, $is_iphone;

if($is_lynx) $classes[]='lynx';

elseif($is_gecko) $classes[]='gecko';

elseif($is_opera) $classes[]='opera';

elseif($is_NS4) $classes[]='ns4';

elseif($is_safari) $classes[]='safari';

elseif($is_chrome) $classes[]='chrome';

elseif($is_IE) $classes[]='ie';

else $classes[]='unknown';

if($is_iphone) $classes[]='iphone';

return $classes;

}

?>

然后,您可以使用以下类:

.ie .navigation {some item goes here}

更多用例:

还有更多用例,这篇文章它的重点是帮助新主题开发人员理解WordPress体类的基础知识及其优点。用例仅限于您的想象力。已经看到商业主题开发人员(Genesis)使用body类来提供各种布局选项。例如,全页面布局,补充工具栏内容,内容边栏等。这样,用户可以轻松切换页面布局而不必担心外观问题,因为已经使用CSS.0进行了处理

还可以看到一些开发人员添加HTTP请求来加载他们提供的各种配色方案的CSS文件。在很多情况下,变化也很小。当一个人可以完成工作时,没有必要加载3个样式表。只需使用body类。

传统的网页开发中,JavaScript通常被放置在HTML文档的<script>标签内,而在大多数情况下,建议将这些<script>标签放置在</body>标签之前,即在<body>标签的尾部引入JavaScript代码。这种做法有以下几个原因:

加载顺序

浏览器在解析HTML文档时是按顺序执行的,当遇到<script>标签时,浏览器会停止解析HTML,去加载并执行JavaScript代码。如果将<script>标签放在<head>标签中,那么在JavaScript加载和执行期间,HTML解析会暂停,页面呈现会延迟,给用户带来不好的体验。而将<script>标签放在</body>标签之前,可以确保页面的HTML结构已经完全加载和解析,然后再去加载和执行JavaScript代码,不会影响页面的呈现速度和用户体验。

优化加载时间

将JavaScript代码放在</body>标签之前,可以确保页面的其他内容(如HTML结构、CSS样式、图片等)都已加载完毕,这样可以最大程度地减少JavaScript加载和执行的时间。因为JavaScript通常会操作和修改页面的DOM结构,如果在DOM还未完全加载的情况下执行JavaScript代码,可能会导致JavaScript无法找到或操作相关的DOM元素,造成错误或异常。通过将JavaScript代码放在</body>标签之前,可以确保DOM已经完全加载,并且用户可以尽早地看到页面内容,提高了网页的整体加载速度。

兼容性考虑

在旧版的Internet Explorer(IE)浏览器中,将<script>标签放在<head>标签中时,可能会导致JavaScript无法正常工作。这是因为旧版IE浏览器在解析HTML文档时,会在遇到<script>标签时立即执行其中的代码,并且在继续解析HTML文档之前必须等待JavaScript代码的加载和执行完成。如果JavaScript代码比较大或执行时间较长,用户在这段时间内将看不到页面内容,给用户造成不好的体验。通过将JavaScript代码放在</body>标签之前,可以规避这个问题,确保页面内容能够尽快呈现给用户。

综上所述,将JavaScript代码放在</body>标签之前是一种较好的习惯,可以优化页面加载时间,提高用户体验,并确保脚本在正确的上下文中执行,避免兼容性问题。然而,随着Web开发技术的不断发展,我们可以根据具体的需求和使用的工具选择合适的方式来引入JavaScript代码,以达到更好的性能和开发体验。

vent Loop(事件循环)是前端工程师经常讨论到的话题。

作者:lzaneli,腾讯 CDC 前端开发工程师,博客:Lzane.com

Microtasks(微任务)是事件循环中一类优先级比较高的任务,本文通过一个有趣的例子探索其运行时机。从两年前被动接受知识 "当浏览器JS引擎调用栈弹空的时候,才会执行 Microtasks 队列",到两年后主动深入探索源码后了解到的 "当 V8 执行完调用要返回 Blink 时,由于 MicrotasksScope 作用域失效,在其析构函数中检查 JS 调用栈是否为空,如果为空就会运行 Microtasks。"。同时文章中介绍了用于探索浏览器运行原理的一些工具。

一个有趣的例子

刚学前端那会学习事件循环,说事件循环存在的意义是由于 JavaScript 是单线程的,所以需要事件循环来防止JS阻塞,让网络请求等I/O操作不阻塞主线程。

而 Microtasks 是一类优先级比较高的任务,我们不能像 Macrotasks(宏任务) 一样插入 Macrotasks 队列末端,等待多个事件循环后才执行,而需要插入到 Microtasks 的队列里面,在本轮事件循环中执行。

比如下面这个有趣的例子:

document.body.innerHTML=` 
    <button id="btn" type="button">btn</button> 
`; 

const button=document.getElementById('btn')
 
button.addEventListener('click',()=>{
  Promise.resolve().then(()=>console.log('promise resolved 1'))
  console.log('listener 1')
})
 
button.addEventListener('click',()=>{
  Promise.resolve().then(()=>console.log('promise resolved 2'))
  console.log('listener 2')
})
 
// 1. 手动点击按钮 
// button.click() // 2. 解开这句注释,用JS触发点击行为

当我手动点击按钮的时候,大家觉得浏览器的输出是下面的A还是B?

  • A. listener1 -> promise resolved 1 -> listener2 -> promise resolved 2
  • B. listener1 -> listener2 -> promise resolved 1 -> promise resolved 2

大家可以在这里试一下:

https://codesandbox.io/static/img/play-codesandbox.svg

当我将上面代码中的最后一行注释打开,使用JS触发点击行为的时候,浏览器的输出是A还是B?

大家觉得上面1、2两种情况的输出顺序是否一样?

答案非常有意思

  • 当我们使用1. 手动点击按钮时,浏览器的输出是A
  • 当我们使用2. 用JS触发点击行为时,浏览器的输出是B

被动接受知识

为什么会出现这种情况呢? 这个 Microtasks 的运行时机有关。两年前当我带着这个问题搜索资料并询问大佬的时,大佬告诉我:

当浏览器JS引擎调用栈弹空的时候,才会执行Microtasks队列

按照这个结论,我使用 Chrome Devtool 中的 Performance 做了一次探索

人工点击按钮

人工点击的时候输出为 listener1 -> promise resolved 1 -> listener2 -> promise resolved 2 。

  • 从上图中我们可以看到,一次点击事件之后,浏览器会调用 Function Call 进入JS引擎,执行 listener1,输出listener1
  • 弹栈时发现JS调用栈为空,这时候就会执行 Microtasks 队列中的所有 Microtask,输出promise resolved 1
  • 接着浏览器调用 Function Call 进入JS引擎,执行 listener2,输出listener 2
  • 弹栈时发现JS调用栈为空,这时候就会执行 Microtasks 队列中的所有Microtask,输出promise resolved 2

JS触发点击事件

在JS代码中触发点击时输出为 listener1 -> listener2 -> promise resolved 1 -> promise resolved 2

  • 从上图中我们可以看到,浏览器运行JS代码时,调用了 button.click 这个函数
  • 进入事件处理,执行 listener1,输出listener1
  • 弹栈时发现JS调用栈非空(button.click函数还在运行)
  • 执行 listener2,输出listener 2
  • 弹栈时发现JS调用栈为空,这时候就会执行 Microtasks 队列中的所有 Microtask,输出promise resolved 1promise resolved 2

探索工具

Chrome Devtool 中的 Performance 是一个 sample profiler (采样分析仪),即它的运行机制是每1ms暂停一下vm,将当前的调用栈记录下来,最后利用这部分信息做出可视化。

由于它是一种 sample 的机制,所以在两个 sample 之间的运行状态可能会被丢失,所以我们在使用这个工具的时候可以

  1. 使CPU变慢:在 Devtool 中打开 "CPU 6x slowdown"
  2. 在要探索的函数中执行一段比较长的for循环占用CPU时间(如上面的 heavy)

强烈建议大家学会使用这个工具,本文例子的 profile 结果文件也会文章最后给到大家,大家有兴趣可以导入试一试。

主动探索V8源码

两年的时间过去了,在上周整理笔记的时候,我开始质疑这一个知识,"当浏览器 JS 引擎调用栈弹空的时候,才会执行 Microtasks 队列"。

因为它其实是个表现,我想知道浏览器和 JS 引擎到底是怎么实现这样的机制的。

因此我使用chrome://tracing进行探索,

下面探索基于 Chrome Version 88.0.4324.192 (Official Build) (x86_64),不同浏览器的实现有不同

人工点击按钮

  • 从上图中我们可以看到,一次点击事件之后,Blink(Blink是一个渲染引擎,Chrome 的 Renderer 进程中的主线程大部分时间会在 Blink 和 V8 两者切换)会调用 v8.callFunction 进入 V8 引擎,执行 listener1,输出listener1
  • 返回 Blink 时发现 V8 调用栈为空,这时候就会执行 V8.RunMicrotasks 执行 Microtasks 队列中的所有 Microtask,输出promise resolved 1
  • Blink 调用 v8.callFunction 进入 V8 引擎,执行 listener2,输出listener 2
  • 返回 Blink 时发现 V8 调用栈为空,这时候就会执行 Microtasks 队列中的所有 Microtask,输出promise resolved 2

注意,chrome://tracing 中的v8.xxx小写v开头的为 Blink 的调用,V8.xxx大写的V才是真正的V8引擎。

详细源码

tracing 工具还有一个非常好用的功能,点击下图中的放大镜,就可以直接打开 Chromium Code Search 查看 Chromium 的源码。这个工具也自带搜索功能,可以查看函数的声明、定义以及调用。

下面源码的探索基于commit e8b6574c 的Chromium,并且为了简化隐藏了无关的代码,用...替代

比如我们在上面的 tracing 里面看到有v8.callFunction的调用,我们点击可以找到这个这个函数调用,是在 Blink 中调用 V8 的入口。

third_party/blink/renderer/bindings/core/v8/v8_script_runner.cc

v8::MaybeLocal<v8::Value> V8ScriptRunner::CallFunction(
   v8::Local<v8::Function> function,
   ExecutionContext* context,
   v8::Local<v8::Value> receiver,
   int argc,
   v8::Local<v8::Value> args[],
   v8::Isolate* isolate 
){
  ... 
  TRACE_EVENT0("v8", "v8.callFunction"); // 这就是我们在 tracing 中看到的 v8.callFunction
  ...
  v8::MicrotaskQueue* microtask_queue=ToMicrotaskQueue(context); // 拿到 microtask 队列
  ...
  v8::MicrotasksScope microtasks_scope(isolate, microtask_queue,
                                       v8::MicrotasksScope::kRunMicrotasks); // 这个 scope 很可疑,这里构造之后在这个函数后面并没有使用
  ... 
  probe::CallFunction probe(context, function, depth);
  v8::MaybeLocal<v8::Value> result=function->Call(isolate->GetCurrentContext(), receiver, argc, args); // 函数调用
  CHECK(!isolate->IsDead());
  ...
}

这里类型为v8::MicrotasksScope的变量很可疑,在创建之后并没有在后续的函数里面使用,所以我们来看一下他的声明和定义

v8/include/v8.h

/**
 * This scope is used to control microtasks when MicrotasksPolicy::kScoped
 * is used on Isolate. In this mode every non-primitive call to V8 should be
 * done inside some MicrotasksScope.
 * Microtasks are executed when topmost MicrotasksScope marked as kRunMicrotasks
 * exits.
 * kDoNotRunMicrotasks should be used to annotate calls not intended to trigger
 * microtasks.
 */
class V8_EXPORT V8_NODISCARD MicrotasksScope {
 public:
  enum Type { kRunMicrotasks, kDoNotRunMicrotasks };

  MicrotasksScope(Isolate* isolate, Type type);
  MicrotasksScope(Isolate* isolate, MicrotaskQueue* microtask_queue, Type type);
  ~MicrotasksScope(); // 注意这个析构函数
  ...

上面这段注释告诉我们,这个类是用来控制 Microtasks 的(当 MicrotasksPolicy::kScoped这个策略被使用的时候,我们在后面会拎出来讲,这里大家先默认 Blink 是设置了这个策略)。

这里的析构函数非常的可疑,因为我们在前面一步发现变量microtasks_scope创建之后并没有在后续的函数里面使用,而析构函数会在变量被销毁时执行。我们继续来看 v8::MicrotasksScope 的定义

v8/src/api/api.cc

MicrotasksScope::~MicrotasksScope() {
  if (run_) {
    microtask_queue_->DecrementMicrotasksScopeDepth(); // 这里将函数调用栈减少一层
    if (MicrotasksPolicy::kScoped==microtask_queue_->microtasks_policy() && // 这里检查策略是否是 MicrotasksPolicy::kScoped
        !isolate_->has_scheduled_exception()) {
      DCHECK_IMPLIES(isolate_->has_scheduled_exception(),
                     isolate_->scheduled_exception()==                 i::ReadOnlyRoots(isolate_).termination_exception());
      microtask_queue_->PerformCheckpoint(reinterpret_cast<Isolate*>(isolate_)); // 这一步尝试执行 Microtasks 队列
    }
  }
  ...
}

v8/src/execution/microtask-queue.cc

void MicrotaskQueue::PerformCheckpoint(v8::Isolate* v8_isolate) {
  if (!IsRunningMicrotasks() && !GetMicrotasksScopeDepth() && // 注意,这一步检查了调用栈是否为空
      !HasMicrotasksSuppressions()) {
    Isolate* isolate=reinterpret_cast<Isolate*>(v8_isolate);
    RunMicrotasks(isolate); // 执行队列中的Microtasks
    isolate->ClearKeptObjects();
  }
}

...

int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {
  ...
    {
      TRACE_EVENT_CALL_STATS_SCOPED(isolate, "v8", "V8.RunMicrotasks"); // 我们在 tracing 里面也可以看到这个输出
      maybe_result=Execution::TryRunMicrotasks(isolate, this,
                                                 &maybe_exception);
      processed_microtask_count=  static_cast<int>(finished_microtask_count_ - base_count);
    } 
  ...
}

到这里我们就知道了 Microtasks 的运行时机了,当 V8 执行完调用要返回 Blink 时,由于 MicrotasksScope 作用域失效,在其析构函数中检查 JS 调用栈是否为空,如果为空的话就会运行 Microtasks

下图为完整的调用路径

观察到的现象即是 "当浏览器 JS 引擎调用栈弹空的时候,才会执行 Microtasks 队列"

所以现在我如果问你,是不是 Macrotasks(宏任务)执行完才会执行 Microtasks 呢? 答案显然是否定的,如同这个例子,我们的 Macrotask 是处理点击输入,而 Microtasks 在其中被执行了两次。

JS触发点击事件

用JS触发点击事件其实也是同理的,同样是使用V8::MicrotasksScope的析构函数来进行调用,只是前面几次都因为调用栈非空(GetMicrotasksScopeDepth),所以等到最后面才执行。

V8::MicrotasksPolicy

那是不是所有使用V8引擎的应用 Microtasks 的运行时机都是一样的呢?答案是否定的,Microtasks 的运行时机是由V8::MicrotasksPolicy来决定的。

v8/include/v8.h

/**
 * Policy for running microtasks:
 *   - explicit: microtasks are invoked with the
 *               Isolate::PerformMicrotaskCheckpoint() method;
 *   - scoped: microtasks invocation is controlled by MicrotasksScope objects;
 *   - auto: microtasks are invoked when the script call depth decrements
 *           to zero.
 */
enum class MicrotasksPolicy { kExplicit, kScoped, kAuto };

由上面的源码注释我们可以知道

  • explicit模式下,由应用自己主动调用才会运行 Microtasks。目前 Node 是使用了这种策略。
  • scoped模式下,由MicrotasksScope控制,但作用域失效时,在其析构函数中运行 Microtasks。目前 Blink 是使用这种策略,如下面的代码段为 Blink 设置 MicrotasksPolicy。
  • auto模式为 V8 的默认值,当调用栈为空的时候就会执行 Microtasks

third_party/blink/renderer/bindings/core/v8/v8_initializer.cc

static void InitializeV8Common(v8::Isolate* isolate) {
  ...
  isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kScoped);
  ...
}

探索工具

chrome://tracing/ 是一个 structural profiler 或叫 CPU profiler,与 Chrome Devtool performance 的 sample profiler 不同,他是由代码中主动的去埋点打印出来的,所以每一次函数调用都会被记录下来,不会像sample profiler一样漏掉采样时刻之间的状态。

使用方法如下,首先进入 chrome://tracing 点击右上角的 Record,勾选住你想 profile 的组件。

然后去到你的 demo 页执行你想要探索的操作,回到 tracing 页面点 Stop,接着在 Processes 里面筛选掉其他 Tab(标签页)的信息。

最后使用键盘 w(放大)s(缩小)a(左移)d(右移)探索

强烈建议大家学会使用这个工具,本文例子的 profile 结果文件也会文章最后给到大家,大家有兴趣可以导入试一试。

总结

Event Loop(事件循环)是前端工程师经常讨论到的话题,往深挖可以挖出 JS 如何实现异步、requestAnimationFrame、浏览器渲染机制、Macrotasks、Microtasks等等问题。

本文主要探索了Microtasks的运行时机,我从两年前被动接受知识 "当浏览器JS引擎调用栈弹空的时候,才会执行 Microtasks 队列"

到两年后主动使用工具深入探索源码后了解到的 "当 V8 执行完调用要返回 Blink 时,由于 MicrotasksScope 作用域失效,在其析构函数中检查 JS 调用栈是否为空,如果为空就会运行 Microtasks。"

这也是计算机最吸引我的地方,当你每隔一段时间回来看一个东西的时候,都能够更往深一步,发现到更神奇的原理,也可以够感受到自己的进步

在探索的过程中还使用了一些工具,如 Chrome Devtool Performance、Chrome tracing、Chromium Code Search 等,希望感兴趣的同学,也可以使用这些工具,更深入的探索浏览器内部原理。