静态站点的含义不再过多复述,简单来说,是一种不包含ajax请求(或不包含数据库)、以html、css、js、img为基础的网站。
但虽同为静态网站,又与纯手写的静态页面有所区别,它是一种动态生成的静态,即是说,它依赖于数据库&服务器数据,存在着一个将动态数据转换为静态文件的过程,俗称“打包”。
不过,当前执行该流程时,采取了极其暴力的手段:全文件覆盖,导致有很多文件在无任何修改的情况下,也会进行拷贝&创建。
比如,不少js、css、img资源,在博客建立后就没改动过;更新文章时,本应该只需创建特定页面的文件,但实际上却是几乎把所有的文件重新创建了一遍(株连九族)...
在造成性能浪费的同时,也显然不符合编程人的理念。因此,今天就针对这个问题,对相关的代码做一些优化。
本文主要以“时间”要素作为依据,为动(文章数据)和静两部分资源添加条件判断,来控制资源的拷贝&创建与否。
一、拷贝静态资源
本博客拷贝静态资源使用的是阿帕奇的io工具库FileUtils。
1、LastModifiedFileFilter
LastModifiedFileFilter类的构造方法接收一个文件名与其最后修改日期的映射表(map),并会根据accept()的判断结果更新数据。
// 基于最后修改日期的静态文件过滤器;
// 根据磁盘上的日期数据(记录上一次执行复制操作的文件的修改日期),判断文件是否应该被复制;
class LastModifiedFileFilter implements IOFileFilter {
// 存储文件名与其最好修改日期的映射表
private final Map<String, Long> map;
private boolean updated;
LastModifiedFileFilter(Map<String, Long> map) {
this.map = map;
}
public Map<String, Long> getMap() {
return map;
}
public boolean getUpdated() {
return updated;
}
// 每次判断的同时,记录最后的修改日期
public void setFileModified(String name, Long modified) {
map.put(name, modified);
if (!updated) {
updated = true;
}
}
@Override
public boolean accept(File file) {
// 文件夹默认返回true
if (file.isDirectory()) {
return true;
}
final String name = file.getName();
final Long modified = map.get(name);
long lastModified = file.lastModified();
if (modified == null || modified < lastModified) {
setFileModified(name, lastModified);
return true;
}
return false;
}
@Override
public boolean accept(File dir, String name) {
return false;
}
}
2、读取&保存静态文件映射表
虽然感觉不是很必要,挺简单的方法
2.1、readFileCaches()
从磁盘上读取以json字符串存储的文件,其基本格式为“文件名->文件最后修改日期”。
图1 static_caches.json
public <T> Map<String, T> readFileCaches(String name) {
File file = new File(getResourcePath() + name);
if (file.exists()) {
try {
String jsonStr = FileUtils.readFileToString(file, StandardCharsets.UTF_8.name());
return JSON.parseObject(jsonStr, new TypeReference<Map<String, T>>() {});
} catch (Exception e) {
e.printStackTrace();
throw new CustomException("读取配置失败:" + e.getMessage());
}
}
return new HashMap<>();
}
2.2、saveFileCaches()
public <T> void saveFileCaches(Map<String, T> map, String name) {
try {
File file = new File(getResourcePath() + name);
FileUtils.writeStringToFile(
file,
JSON.toJSONString(map, SerializerFeature.DisableCircularReferenceDetect),
StandardCharsets.UTF_8.name());
} catch (Exception e) {
e.printStackTrace();
throw new CustomException("保存配置失败:" + e.getMessage());
}
}
3、拷贝ing
调用FileUtils.copyDirectory(),并传入LastModifiedFileFilter实例,拷贝整个静态资源目录。
public void copyStatic() {
// 输出目录
String outputPath = appConfig.getOutputPath();
try {
// 读取静态文件映射数据
final Map<String, Long> staticFilesConfig = appConfig.readFileCaches("static_caches.json");
LastModifiedFileFilter lastModifiedFileFilter = new LastModifiedFileFilter(staticFilesConfig);
String[] excluded = {"admin", "temp"};
val excludedFilter = new NameFileFilter(excluded);
IOFileFilter filter = FileFilterUtils.and(FileFilterUtils.notFileFilter(excludedFilter), lastModifiedFileFilter);
// 复制服务器静态资源【包括图片
FileUtils.copyDirectory(
new File(appConfig.getStaticPath()),
new File(outputPath), filter
);
// 根据条件保存映射数据
if (lastModifiedFileFilter.getUpdated()) {
appConfig.saveFileCaches(lastModifiedFileFilter.getMap(), "static_caches.json");
}
} catch (IOException e) {
e.printStackTrace();
throw new CustomException("复制静态资源失败:" + e.getMessage());
}
}
二、创建html文件
创建html文件,这里主要指代的是,除“文章详情页”以外的页面,至于“文章详情页”仍保持根据“状态”来决定创建与否。
图2 文章状态
不同于静态资源只需要判断最后修改日期,html文件需要从更多的维度来决定是否重新创建,最基础的如文件内容与数据库数据是否相同,编排是否保持一致等。
但也跟静态资源一样,此处也存在一个文件的数据映射表,不过记录的数据不同,它里面记录的是每种数据的id,及其最后更新时间(数据库字段,lastUpdatedAt)。
图3 html_caches.json
下面以创建文章分页页面为例,以思路为主。
根据创建的html文件名称,读取配置文件中对应的数据,并与当前分页数据一一比较。
这其实与拷贝静态文件是一致的,都属于“判断是否发生了变化”。
1、getFileCache()
private List<String> getFileCache(String key) {
// 当key为空时,返回一个长度为0的列表;
return htmlFilesConfig.computeIfAbsent(key, k -> new ArrayList<>());
}
2、创建文章分页html
private void createIndexHtml() {
// 略...
int iSize = (int) PaginationUtil.SIZE;
for (long i=1; i<=articlePages; i++) {
// 生成每页的html文件名,并对应页的配置
String htmlName = i > 1 ? String.format("index_%d.html", i) : "index.html";
List<String> indexConfig = getFileCache(htmlName);
int size = indexConfig.size();
// 用于保存每页的文章数据
List<Article> articleList = new ArrayList<>();
long offset = PaginationUtil.getPaginationOffset(i, PaginationUtil.SIZE); // 1=0-10,10-20
long destination = i * PaginationUtil.SIZE;
if (total < destination) {
destination = total;
}
int _count = count;
for (long j=offset; j<destination; j++) {
int index = (int) j;
// pending为待处理数据(包括已发布、待发布)
Article item = pending.get(index);
articleList.add(item);
String id = item.getId() + ":" + item.getLastUpdatedAt().getTime();
int _index = index % iSize;
// 当_index大于size时,说明有新数据;
// 注意,如果ArrayList调用set()超过当前的元素个数时,比如当前size为5,调用方法时,将下标设置为10,那么,中间的元素会被默认置为空;
if (0 == size || _index >= size) {
_count++;
indexConfig.add(id);
}
else if (!indexConfig.get(_index).equals(id)) {
_count++;
indexConfig.set(_index, id);
}
}
dataMap.put(FreemarkerUtil.TEMPLATE_PATH, appConfig.getTemplatePath());
dataMap.put("webConfig", appConfig.getWebConfig());
// 根据判断结果,决定是否需要重新创建html文件
if (_count > count) {
count++;
dataMap.put("current", i);
dataMap.put(FreemarkerUtil.HTML_NAME, outputPath + File.separator + htmlName);
dataMap.put(FreemarkerUtil.TEMPLATE_NAME, INDEX);
dataMap.put("articles", articleList);
dataMap.put("pages", articlePages);
dataMap.put("sdList", Collections.emptyList());
// 创建html文件
FreemarkerUtil.createHtml(dataMap);
}
}
// 略...
}
同样地,在调用了创建html文件的方法后,再作一次判断,是否需要写入数据。
public void publish(PublishingForm form) {
resetProgress();
// html_caches.json,html文件缓存;
// 以index.html为例,保存有第一页的文章id及其修改时间(在json文件中,以"id:lastUpdatedAt"的形式存在),在发布时判断,若发布时的配置与磁盘上的配置不同,则重新创建该index.html,否则保持不变;
htmlFilesConfig = appConfig.readFileCaches("html_caches.json");
publishingForm = form;
try {
progress.put(PublicationStatusKey.PERCENTAGE, 10);
createHtml();
progress.put(PublicationStatusKey.PERCENTAGE, 40);
delHtml();
progress.put(PublicationStatusKey.PERCENTAGE, 60);
copyStatic();
progress.put(PublicationStatusKey.PERCENTAGE, 80);
progress.put(PublicationStatusKey.PERCENTAGE, 100);
progress.put(PublicationStatusKey.CODE, 0);
// 略...
// 若html配置发生变动,重新将数据写入磁盘
if (count > 0) {
appConfig.saveFileCaches(htmlFilesConfig, "html_caches.json");
}
} catch (Exception e) {
// 略...
}
}
本文主要以思路为主,代码为辅,结合服务器上存储的文件映射表,对打包过程进行了优化。
其主要焦点是“时间”,以目标对象是否发生了变化作为下个步骤的前提条件,如拷贝静态文件是根据“最后修改时间”,创建html文件则是根据数据库表的更新字段(lastUpdatedAt),如此之后,可在很大程度上减少文件的拷贝&创建,避免了非必要的性能开销。
以上。