记录一次打包静态站点资源的优化
errol发表于2024-12-15 | 分类为 编程 | 标签为静态站点打包java

静态站点的含义不再过多复述,简单来说,是一种不包含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字符串存储的文件,其基本格式为“文件名->文件最后修改日期”。

image

图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文件,这里主要指代的是,除“文章详情页”以外的页面,至于“文章详情页”仍保持根据“状态”来决定创建与否。

image

图2 文章状态

不同于静态资源只需要判断最后修改日期,html文件需要从更多的维度来决定是否重新创建,最基础的如文件内容与数据库数据是否相同,编排是否保持一致等。

但也跟静态资源一样,此处也存在一个文件的数据映射表,不过记录的数据不同,它里面记录的是每种数据的id,及其最后更新时间(数据库字段,lastUpdatedAt)。

image

图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),如此之后,可在很大程度上减少文件的拷贝&创建,避免了非必要的性能开销。

以上。

返回