0%


一、背景介绍

Spring 容器是 Spring 框架的核心。容器负责配置对象,注入对象,管理对象,并完成整个生命周期。Spring 容器依赖控制反转(Inversion of Control),也即依赖注入(Dependency Injection)来配置并注入对象。这样的对象被成为 Bean。具体过程可抽象为:Spring 读取程序中的 Bean 配置信息,并据此在容器中生成一份 Bean 配置注册表,然后再根据注册表实例化 Bean ,装配好待用。

Spring 提供了两种容器:BeanFactoryApplicationContextBeanFactorySpring Ioc 功能提供了底层的实现基础,但为了实现框架化,Spring 新增了ApplicationContext 接口。 ApplicationContext 继承于 BeanFactory,并涵盖其所有功能。官方对两个容器做出比较:

特征 BeanFactory ApplicationContext
Bean 实例化 / 连接
集成生命周期管理
BeanPostProcessor 自动注册
BeanFactoryPostProcessor 自动注册
利用 MessageSource 进行国际化
嵌入式 ApplicationEvent 发布机制

本文分析 ApplicationContext 容器。Spring 源码参考版本 5.2.5.RELEASE

二、ApplicationContext 容器涉及的类关系图

ApplicationContext 有很多实现类,这里我们以 Java EE web (采用 Spring 框架)应用的启动过程为例。为了更直观地描述初始化过程,我们有必要认识 XmlWebApplicationContext 的关联类图:

三、ApplicationContext 容器初始化

Java EE Web 项目启动,会触发 ContextLoaderListener 监听器,完成 WebApplicationContext -根容器的初始化。具体实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* web.xml 中 <context>contextLoaderListener</context> 触发根容器初始化
*
* @param servletContext 整个应用的上下文; 一个应用只有一个 ServletContext
* @return 根容器
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 根容器挂载在 ServletContext 下,有且仅有一个根容器
if (servletContext
.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)
!= null) {
throw new IllegalStateException("...");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
if (this.context == null) {
// 默认生成的根容器为 XmlWebApplicationContext
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac
= (ConfigurableWebApplicationContext) this.context;
// 根容器还没有被刷新
if (!cwac.isActive()) {
// 添加父容器
if (cwac.getParent() == null) {
// 对于 web 应用, 父容器默认为空
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置并刷新容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 将根容器挂载在 ServletContext 下,指定属性 name 值,方便在应用内部随时使用
servletContext
.setAttribute(WebApplicationContext
.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// 获取当前线程的上下文加载器
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
} else if (ccl != null) {
// 若上下文加载器不是 ContextLoader 加载器,则存至 currentContextPerThread 静态域
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in "
+ elapsedTime + " ms");
}
return this.context;
} catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
// 若发生异常,则将异常挂载到 ServletContext 上下文中,此后不再初始化
servletContext
.setAttribute(WebApplicationContext
.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}

/**
* 创建根容器 WebApplicationContext
*
* @param sc ServletContext 应用上下文; 一个应用只有一个应用上下文
* @return WebApplicationContext 根容器
*/
WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 判断根容器的实现类
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("...");
}
// 实例化根容器
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

/**
* 判断根容器的实现类
*
* @param servletContext 应用上下文; 一个应用只有一个应用上下文
* @return 根容器的实现类; 默认为 XmlWebApplicationContext
*/
Class<?> determineContextClass(ServletContext servletContext) {
// 读取web.xml中的配置 <context-param>contextClass</context-param>
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName,
ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
} else {
// 若未配置, 则创建默认的根容器 XmlWebApplicationContext
contextClassName =
defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName,
ContextLoader.class.getClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

从代码实现可以得出:

  • 一个 Web 应用,根容器只能创建一次;

  • 根容器的实现类可配置,默认为 XmlWebApplicationContext

  • 需要为根容器指定父容器,旨在实现父容器-子容器的继承架构。目前持保留实现,默认为 null

  • 根容器 WebApplicationContext 是挂载在应用上下文 ServletContext 下的,并指定属性名 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

下面介绍根容器的配置与刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* WebApplicationContext 根容器的配置与刷新
*
* @param wac WebApplicationContext 根容器的可配置继承
* @param sc ServletContext 应用上下文; 一个应用只有一个应用上下文
*/
void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac,
ServletContext sc) {
// 1. 为根容器配置 ID,用于后面加载Spring-MVC的配置文件
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
} else {
// 生成默认的 ID
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 2. 为根容器配置应用上下文
wac.setServletContext(sc);
// 3. 为根容器添加配置参数地址
// 读取web.xml中的配置 <context-param>configLocation</context-param>
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// 4. 初始化环境属性
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// 5. 自定义上下文
customizeContext(sc, wac);
// 6. 刷新根容器
wac.refresh();
}

/**
* 自定义上下文
*
* @param sc ServletContext 应用上下文; 一个应用只有一个应用上下文
* @param wac WebApplicationContext 根容器的可配置继承
*/
void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
initializerClasses =
determineContextInitializerClasses(sc);
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>>
initializerClass : initializerClasses) {
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass,
ApplicationContextInitializer.class);
if (initializerContextClass != null
&& !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format("..."));
}
// 创建 Initializer 实例,并添加到 contextInitializers
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer<ConfigurableApplicationContext>
initializer : this.contextInitializers) {
// 依次执行初始化程序
initializer.initialize(wac);
}
}
}

从代码实现可以得出:

  • 通过 <context-parm>configLocation</context-param> 为根容器添加配置信息;
  • 因为只要刷新上下文,就要调用环境的 initPropertySources 方法,所以需要提前初始化环境属性,以保证根容器刷新之前的一些操作,如后置处理或初始化过程,都可以直接获取到 Servlet 属性;
  • 在根容器添加配置后,刷新前,执行自定义操作。根据 ServletContextcontextInitializerClassesglobalInitializerClasses 配置加载所有的上下文初始化程序,并依次执行;
  • 自定义根容器后,执行根容器的刷新。放在下一个专栏分解。

1. 问题描述

上篇介绍了通过Blob 类文件对象实现文件下载,但具体操作下去,会发现我留下了一个坑—-浏览器跨域请求问题CORS。如下图提示:

toVMsf.png

为实现应用解耦,前后端分离已然成为主流设计。生产环境中,前端请求需要经过代理和负载(如: NigixLVS)处理,才能传递到后端。此种模式下,前后端的交互实现都需要跨域。

同源安全策略是浏览器的一种安全限制,默认阻止跨域获取资源,防止跨站攻击。但 CORS 为跨域请求提供了可能。CORS 将跨域权限交给 Web 服务端,即通过配置就可以实现跨域地资源访问。

2. 跨域请求

跨域场景 示例
域名不同 spring.io zhihu.com
域名相同,端口不通 http://127.0.0.1:8080; http://127.0.0.1:8081
二级目录不同 document.spring.io; reference.spring.io

综上: 当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

3. 解决方案

Access-Control-Allow-Origin 是响应头中一个重要的属性,它规定浏览器可以获取哪些域的资源。既然 CORS 将跨域访问权限交给了服务器,那么只需要服务器设置 Access-Control-Allow-Origin属性即可。以 Java 为后端语言的服务中,可以做如下设置:

1
2
// 为实现服务的可扩展,采用通配符域名,即允许访问所有域的资源
response.setHeader("Access-Control-Allow-Origin", "*");

继续实现下去,我们可能又会遇到如下问题:

tol4mR.png

查阅资料发现,对于跨域请求,请求头的可获取性配置在属性 Access-Control-Expose-Headers 中,W3C 规定客户端获取的响应头字段仅限于simple response header ,包括:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

而文件名在后端的设置方式如下:

1
response.setHeader("fileName", encodeFileName);

因此还后端将请求头 fileName 暴露给前端,配置如下:

1
2
response.setHeader("fileName", encodeFileName);
response.setHeader("Access-Control-Expose-Headers", "fileName");

以上。


一、背景介绍

Axios 是一个基于 PromiseHTTP 库,可以用在浏览器和 node.js 中。与传统的 Ajax 相比,Axios 的优势主要体现在: 支持 Promise API 、支持并发、支持请求与响应拦截、自动转换 JSON 数据、支持防御 XSRF、符合 MVVM 设计理念。在目前大火的前端框架 Vue 中也内置了 Axios 框架

正是因为 Axios 的数据处理为 json 格式,所以只能获取文件流,但不能给与正确处理。具体表现为:

  • response.statusrepsponse.headers 与期望相同,但 response.data 为一团乱码
  • 浏览器没有自动下载文件

二、引入 Blob 容器实现文件下载

既然 Axios 无法正确处理文件流,便需要采用其他技术来达到预期效果。Blob 对象表示一个不可变、原始数据的类文件对象;从字面意思来看,Blob 也是一个可以存储二进制文件的容器。通过这项技术,可以完美地弥补 Axios 的不足。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 下载文件请求
export function download (param) {
return axios({
url: '/web/bill/download',
method: 'post',
data: param,
responseType: 'blob'
})
}

// 下载文件入口方法
download(this.param).then(response => {
// 执行文件下载
this.exeDownloadFile(response)
})

// 执行文件下载
exeDownloadFile (response) {
// 创建 Blob 对象
let blob = new Blob([response.data], {type: response.headers['content-type']})
// 获取文件名
let fileName = response.headers['content-disposition'].match(/filename=(.*)/)[1]
// 创建指向 Blob 对象地址的URL
let href = window.URL.createObjectURL(blob)
// 创建用于跳转至下载链接的 a 标签
let downloadElement = document.createElement('a')
// 属性配置
downloadElement.style.display = 'none'
downloadElement.href = href
downloadElement.download = fileName
// 将 a 标签挂载至当前页面
document.body.appendChild(downloadElement)
// 触发下载事件
downloadElement.click()
// 移除已挂载的 a 标签
document.body.removeChild(downloadElement)
// 释放 Blob URL
window.URL.revokeObjectURL(href)
}

虽然以上可以实现文件下载,但美中不足的是,当后台返回异常时,通过 Blob 对象依然可以下载到一个 undefined 文件。因此,在执行下载之前,需要对下载请求是否存在异常做出判断。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 下载文件入口方法
download(this.param).then(response => {
let fr = new FileReader()
fr.readAsText(response.data)
fr.onload = function () {
// 新增-异常校验; 若 Blob 对象可 JSON 格式化,则说明文件下载异常
try {
let jsonRet = JSON.parse(this.result)
} catch (e) {
// 执行文件下载
this.exeDownloadFile(response)
}
}
})

一、背景介绍

HTTP 为请求-响应式协议,指客户端先向服务器发送请求,然后服务器接收请求后,再向客户端发送响应信息。服务器向客户端发送的响应信息,包含三个部分: 状态行、消息报头和消息正文。状态行包括 HTTP 版本和状态码。消息报头包括浏览器、服务器或消息正文的相关信息。消息正文为返回的实体数据。

在以 Java 语言为服务的响应中,消息报头是存储在 Header 属性中的。主要实现方法有两种:HttpServletResponse.setHeader(name, MIME)HttpServletResponse.addHeader(name, MIME)。下面详细介绍两种方法的底层实现原理及其区别。参考 Tomcat 源码版本8.5.31。


二、底层实现

三、setHeaderaddHeader 的区别

根据上面的分析可以看出,响应头的属性可以分为两种: 特殊属性与普通属性。其中,Content-typeContent-length 为特殊属性,MimeHeaderFields 中为普通属性。响应头的属性都在 coyote/Response 类中。

从底层实现原理看,setHeader 是通过新建或覆盖来实现属性配置的,而 addHeader 只会新增属性到队列中。此处需要说明,属性若已存在,setHeader 的进行覆盖后,还会将其他同名属性移除队列。源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 属性组
private MimeHeaderField[] headers = new MimeHeaderField[8];

/**
* Allow "set" operations, which removes all current values
* for this header.
* @param name The header name
* @return the message bytes container for the value
*/
public MessageBytes setValue(String name) {
for(int i = 0; i < this.count; ++i) {
if (this.headers[i].getName().equalsIgnoreCase(name)) {
for(int j = i + 1; j < this.count; ++j) {
if (this.headers[j].getName().equalsIgnoreCase(name)) {
this.removeHeader(j--);
}
}
return this.headers[i].getValue();
}
}
MimeHeaderField mh = this.createHeader();
mh.getName().setString(name);
return mh.getValue();
}

继续分析,addHeader 为属性设置多个值,那么我们可以获取哪个值呢?通过属性名查询属性的方法有 getHeader(name)getHeaders(name)。实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* getHeader(name)
* @param name 属性名
* @return 属性
*/
public MessageBytes getValue(String name) {
for (int i = 0; i < count; i++) {
if (headers[i].getName().equalsIgnoreCase(name)) {
return headers[i].getValue();
}
}
return null;
}

/**
* getHeaders(name)
* @param name 属性名
* @return 该属性名对应的所有属性值
*/
public Collection<String> getHeaders(String name) {
Enumeration<String> enumeration =
getCoyoteResponse().getMimeHeaders().values(name);
List<String> result = new ArrayList<>();
while (enumeration.hasMoreElements()) {
result.add(enumeration.nextElement());
}
return result;
}

从方法实现可以看出,getHeader 会返回匹配到的第一个属性值,而 getHeaders 则返回相同属性名的所有属性值。我们可以通过程序示例证实:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
response.setHeader("set", "one");
response.setHeader("set", "two");
response.addHeader("add", "a");
response.addHeader("add", "b");
response.addHeader("add", "c");
response.addHeader("add", "d");

public static void main(String[] args) {

String setName = "set";
String addName = "add";

log.info("setHeader -> getHeader 方式查询到: {}", response.getHeader(setName));
log.info("setHeader -> getHeaders 方式查询到: {}", response.getHeaders(setName));

log.info("addHeader -> getHeader 方式查询到: {}", response.getHeader(addName));
log.info("addHeader -> getHeaders 方式查询到: {}", response.getHeaders(addName));
}

----- 输出
setHeader -> getHeader 方式查询到: two
setHeader -> getHeaders 方式查询到: two

addHeader -> getHeader 方式查询到: a
addHeader -> getHeaders 方式查询到: [a, b, c, d]


1、为什么要搭建个人博客

在互联网时代,拥有一个个人博客有很多好处:

  • 知识沉淀:将学习笔记、技术总结整理成文章,方便日后查阅
  • 个人品牌:展示技术能力,吸引潜在雇主或合作伙伴
  • 完全自主:相比第三方平台,个人博客完全由自己掌控
  • 成本低廉:GitHub Pages 免费提供静态网站托管服务

主流的博客搭建方案有 HexoHugoJekyllWordPress 等。其中 Hexo 基于 Node.js,具有以下优点:

  • 生成静态页面,访问速度快
  • 主题丰富,可定制性强
  • 支持 Markdown 写作
  • 一键部署到 GitHub Pages

本文将手把手教你搭建一个基于 Hexo + GitHub Pages 的个人博客。

2、环境准备
2.1 安装 Node.js

Hexo 基于 Node.js 运行,需要先安装 Node.js

Windows 系统:

  1. 访问 Node.js 官网
  2. 下载 LTS(长期支持版)安装包,推荐 18.x20.x 版本
  3. 双击安装包,一路点击 “Next” 完成安装

验证安装:

打开命令行(按 Win + R,输入 cmd,回车),执行以下命令:

1
2
node -v
npm -v

如果显示版本号,说明安装成功:

2.2 安装 Git

Git 是版本控制工具,用于将博客部署到 GitHub

Windows 系统:

  1. 访问 Git 官网
  2. 下载 Windows 安装包
  3. 双击安装包,默认选项安装即可

验证安装:

打开命令行,执行:

1
git --version

显示版本号则安装成功。

2.3 注册 GitHub 账号

如果还没有 GitHub 账号,请前往 GitHub 官网 注册。注册完成后,建议配置 SSH 密钥,方便后续部署。

生成 SSH 密钥:

1
2
3
4
5
6
# 配置 Git 用户信息
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"

# 生成 SSH 密钥
ssh-keygen -t rsa -C "你的邮箱"

连续按三次回车,使用默认配置。密钥会生成在 C:\Users\你的用户名\.ssh 目录下。

添加 SSH 密钥到 GitHub:

  1. 用记事本打开 .ssh 目录下的 id_rsa.pub 文件,复制全部内容
  2. 登录 GitHub,点击右上角头像 → Settings → SSH and GPG keys → New SSH key
  3. 粘贴密钥内容,点击 “Add SSH key”

验证 SSH 连接:

1
ssh -T git@github.com

如果显示 Hi xxx! You've successfully authenticated...,说明配置成功。

3、安装 Hexo
3.1 创建博客目录

选择一个合适的位置存放博客文件,比如 D:\Blog。在命令行中执行:

1
2
3
4
5
6
7
8
# 切换到 D 盘
D:

# 创建博客目录
mkdir myblog

# 进入目录
cd myblog
3.2 安装 Hexo 脚手架
1
npm install -g hexo-cli

安装完成后,验证:

1
hexo -v
3.3 初始化博客
1
2
3
4
5
6
7
8
# 初始化博客,blog 是博客文件夹名称
hexo init blog

# 进入博客目录
cd blog

# 安装依赖
npm install

初始化完成后,目录结构如下:

1
2
3
4
5
6
7
8
blog
├── _config.yml # 站点配置文件
├── package.json # 依赖配置
├── scaffolds # 文章模板
├── source # 资源文件
│ ├── _drafts # 草稿
│ └── _posts # 已发布的文章
└── themes # 主题文件夹
3.4 本地预览博客
1
2
3
4
5
# 启动本地服务器
hexo server

# 或者简写
hexo s

打开浏览器访问 http://localhost:4000,即可看到博客初始页面。

Ctrl + C 停止服务器。

4、GitHub Pages 配置
4.1 创建仓库
  1. 登录 GitHub,点击右上角 “+” → “New repository”
  2. 仓库名称必须是 你的用户名.github.io(例如:donehub.github.io
  3. 选择 “Public”(公开)
  4. 点击 “Create repository”

注意:仓库名称必须是 用户名.github.io 格式,这是 GitHub Pages 的要求。

4.2 配置部署信息

编辑博客根目录下的 _config.yml 文件,找到 deploy 部分,修改为:

1
2
3
4
5
# Deployment
deploy:
type: git
repo: git@github.com:你的用户名/你的用户名.github.io.git
branch: master

例如:

1
2
3
4
deploy:
type: git
repo: git@github.com:donehub/donehub.github.io.git
branch: master
4.3 安装部署插件
1
npm install hexo-deployer-git --save
5、配置博客
5.1 修改站点信息

打开 _config.yml,修改以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Site
title: 我的博客 # 博客标题
subtitle: '记录学习和生活' # 博客副标题
description: '' # 博客描述
keywords: # 关键词
author: 你的名字 # 作者名称
language: zh-CN # 语言,中文
timezone: 'Asia/Shanghai' # 时区

# URL
url: https://你的用户名.github.io # 博客地址
root: /
permalink: :year/:month/:day/:title/ # 文章链接格式
5.2 常用命令
命令 简写 说明
hexo new "文章标题" hexo n "文章标题" 新建文章
hexo generate hexo g 生成静态文件
hexo server hexo s 启动本地服务器
hexo deploy hexo d 部署到远程仓库
hexo clean - 清除缓存文件

常用组合:

1
2
# 清除缓存 + 生成 + 部署
hexo clean && hexo g && hexo d
6、更换主题

Hexo 默认主题比较简单,推荐安装 NexT 主题,简洁美观,功能强大。

6.1 安装 NexT 主题
1
2
# 进入博客根目录,安装 NexT 主题
npm install hexo-theme-next --save

或者使用 Git 克隆:

1
2
cd blog
git clone https://github.com/next-theme/hexo-theme-next.git themes/next
6.2 启用主题

修改 _config.yml 中的主题配置:

1
theme: next
6.3 主题配置

NexT 主题有独立的配置文件 _config.next.yml(推荐)或 themes/next/_config.yml

创建 _config.next.yml 进行个性化配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 选择主题风格
scheme: Muse # 默认风格,黑白分明
# scheme: Mist # 类似 Muse,更简洁
# scheme: Pisces # 双栏布局
# scheme: Gemini # 类似 Pisces,更精致

# 菜单配置
menu:
home: / || fa fa-home
archives: /archives/ || fa fa-archive
tags: /tags/ || fa fa-tags
categories: /categories/ || fa fa-th
about: /about/ || fa fa-user

# 侧边栏社交链接
social:
GitHub: https://github.com/你的用户名 || fab fa-github

# 头像
avatar:
url: /images/avatar.png # 头像图片路径
rounded: true # 圆形头像
opacity: 1

# 代码高亮主题
codeblock:
theme:
light: default
dark: stackoverflow-dark
6.4 创建分类和标签页面
1
2
3
4
5
6
7
8
# 创建分类页面
hexo new page categories

# 创建标签页面
hexo new page tags

# 创建关于页面
hexo new page about

编辑 source/categories/index.md

1
2
3
4
5
---
title: 分类
date: 2026-04-06 15:00:00
type: "categories"
---

编辑 source/tags/index.md

1
2
3
4
5
---
title: 标签
date: 2026-04-06 15:00:00
type: "tags"
---
7、写作指南
7.1 创建文章
1
hexo new "我的第一篇文章"

执行后会在 source/_posts/ 目录下生成 我的第一篇文章.md 文件。

7.2 文章结构

每篇文章开头是 Front Matter,用于设置文章属性:

1
2
3
4
5
6
7
8
---
title: 文章标题
date: 2026-04-06 15:00:00
tags: [标签1, 标签2]
categories: 分类名
---

这里写正文内容...
7.3 Markdown 基础语法
语法 效果
# 标题 一级标题
## 标题 二级标题
**粗体** 粗体
*斜体* 斜体
[链接文字](url) 超链接
![图片描述](图片url) 图片
`代码` 行内代码
```语言名 代码块 ``` 代码块
> 引用内容 引用块
- 列表项 无序列表
1. 列表项 有序列表
7.4 插入图片

方式一:使用图床

推荐搭建个人图床,上传图片后获取外链,方便图片管理和文章加载:

📖 延伸阅读用 PicGo + Gitee 搭建个人图床 — 介绍了如何使用 PicGo 工具配合 Gitee 仓库搭建免费稳定的个人图床,实现图片一键上传和链接获取。

1
![图片描述](图片外链地址)

方式二:本地图片

_config.yml 中设置:

1
post_asset_folder: true

然后使用 hexo new 创建文章时,会自动创建同名文件夹存放图片。引用方式:

1
{% asset_img example.jpg 图片描述 %}
8、部署博客
8.1 首次部署
1
2
3
4
5
6
7
8
# 清除缓存
hexo clean

# 生成静态文件
hexo generate

# 部署到 GitHub
hexo deploy

或者合并命令:

1
hexo clean && hexo g && hexo d
8.2 访问博客

部署完成后,访问 https://你的用户名.github.io 即可看到博客。

注意:首次部署可能需要等待几分钟才能生效。

8.3 持续更新

每次修改文章或配置后,执行以下命令重新部署:

1
hexo clean && hexo g && hexo d
9、绑定自定义域名(可选)

如果你有自己的域名,可以绑定到 GitHub Pages,让博客地址更加简洁易记。

📖 延伸阅读GitHub Pages 绑定个人域名 — 详细介绍了域名申请、CNAME 配置、DNS 解析(A 记录和 CNAME 记录)的完整流程,帮助你将 username.github.io 替换为个性化域名。

9.1 添加 CNAME 文件

source 目录下创建 CNAME 文件(无后缀),内容为你的域名:

1
www.yourdomain.com
9.2 配置 DNS 解析

在域名服务商处添加 DNS 解析记录:

记录类型 主机记录 记录值
CNAME www 你的用户名.github.io
A @ 185.199.108.153
A @ 185.199.109.153
A @ 185.199.110.153
A @ 185.199.111.153
9.3 启用 HTTPS
  1. 进入 GitHub 仓库 → Settings → Pages
  2. 在 “Custom domain” 处填写你的域名
  3. 勾选 “Enforce HTTPS”
10、博客优化
10.1 安装实用插件
1
2
3
4
5
6
7
8
# 站点地图(SEO 优化)
npm install hexo-generator-sitemap --save

# RSS 订阅
npm install hexo-generator-feed --save

# 本地搜索
npm install hexo-generator-searchdb --save

_config.yml 中添加配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 站点地图
sitemap:
path: sitemap.xml

# RSS
feed:
type: atom
path: atom.xml
limit: 20

# 本地搜索
search:
path: search.xml
field: post
content: true
10.2 NexT 主题开启搜索

_config.next.yml 中启用本地搜索:

1
2
3
# 本地搜索
local_search:
enable: true

📖 延伸阅读NexT 主题集成搜索功能 — 介绍了 NexT 主题集成 Local Search 搜索服务的详细配置步骤,支持标题检索和内容检索,方便读者快速查找文章。

10.3 添加统计功能

可以使用 不蒜子 添加访问统计:

1
2
3
4
5
6
7
8
9
# 在 _config.next.yml 中
busuanzi_count:
enable: true
total_visitors: true
total_visitors_icon: fa fa-user
total_views: true
total_views_icon: fa fa-eye
post_views: true
post_views_icon: fa fa-eye
11、常见问题
11.1 部署失败

问题:执行 hexo d 时报错 ERROR Deployer not found: git

解决:安装部署插件

1
npm install hexo-deployer-git --save
11.2 样式不显示

问题:本地预览正常,部署后样式丢失

解决:检查 _config.yml 中的 urlroot 配置:

1
2
url: https://你的用户名.github.io
root: /
11.3 中文乱码

问题:文件编码不是 UTF-8 导致中文乱码

解决:使用编辑器(如 VS Code)将文件保存为 UTF-8 编码

11.4 文章不显示

问题:新建的文章在博客中看不到

解决

  1. 检查文件是否在 source/_posts 目录
  2. 确认文章内有正确的 Front Matter
  3. 执行 hexo clean && hexo g 重新生成
12、总结

通过本文,你已经学会了:

  • 安装和配置 Hexo 博客框架
  • 创建和配置 GitHub Pages 仓库
  • 安装和配置 NexT 主题
  • 使用 Markdown 写作
  • 部署博客到 GitHub Pages
  • 绑定自定义域名
  • 常用插件和优化技巧