• 作者:老汪软件技巧
  • 发表时间:2024-09-06 17:02
  • 浏览量:

本插件已发布npm包,并开源到 github unplugin-mediafit,如果觉得本插件对你有帮助或文章对你有启发,麻烦动动手指给我个吧,非常感谢!

背景

最近有个需求需要做响应式页面,这不多媒体查询、flex布局啥的一套撸,问题不大,但是,这些页面涉及很多视频的展示,而设计同学只提供PC尺寸下的视频文件,为了在不同尺寸的设备上加载不同尺寸的视频(移动端减少加载资源大小),我都是复制一份资源,然后手动用 ffmpeg 修改视频的分辨率,这弄一两次还好,但当数量多时,而设计资源经常更新时,就很烦了。。于是凭着重复工作自动化的精神,我在想能不能写个工具自动化这些步骤呢?

思路及实现

刚好之前研究了一下vite(研究vite心得),通过它的插件机制,我们能轻松自定义资源的解析-加载-转换逻辑!

vite 插件运行在node端,nodejs能干嘛你就能干嘛,放飞你的想象力吧!

设计

经过一段时间的实践与思考,我敲定了插件的工作流程,如下:

在文件后缀名前面增加 query,格式如下:

import xx from "xxx@fit:fitFuncKey(a=xx&b=xx).xx"
                   ^^^^^----------^^^^^^^^^^^
                   固定标识    |     自定义参数
                          fitFunc标识

fit 有转换的意思

然后在插件里根据固定标识识别出入口,然后解析出目标文件路径;fitFunc(转换函数)标识及调用参数;运行转换函数转换目标文件生成新的文件;转换函数及参数支持自定义,通过插件options配置;实现区分入口,处理路径

在 resolveId 钩子里,根据固定标识识别出入口,解析出绝对路径,简要如下:

//...
    const mediaFitTag = '@fit:';
    // ...
    resolveId(source: string, importer: string) {
      if (source.includes(mediaFitTag)) {
        let absolutePath;
        
        if (source.includes(root)) {
          // 如果路径已经是绝对路径了,直接复制
          absolutePath = source;
        } else {
          absolutePath = path.join(path.dirname(importer || ""), source);
        }
        return absolutePath;
      }
      return null;
    }
    //...

解析转换信息,转换文件

在 load 钩子进行解析相关信息,执行转换处理,把转换结果写入新文件,最后返回 js 虚拟模块,简要如下:

//...
    async load(id: string) {
      if (id.includes(mediaFitTag)) {
        // 1. 解码参数、目标文件地址、结果文件地址
        // 1.1 提取参数
        const fitFuncInfoArr = decodeParamStr(id);
        // 1.2  目标文件地址、结果文件地址(这里取简单做法)
        let inputFilePath = id.replace(/@fit:.*\./g, ".");
        // 那些需要转换格式的 fitfunc 会改变 outputFilePath
        let outputFilePath = id;
        // 2. 匹配处理函数、运行转换函数,生成结果文件
        // 暂不支持串联调用 fitfunc,暂时只考虑支持一个fitfunc函数调用
        for (let index = 0; index < fitFuncInfoArr.length; index++) {
          const { fitFuncName, params } = fitFuncInfoArr[index];
          // 碰到转换格式的,需要更新输出文件格式
          if (params.f) {
            const originFormat = inputFilePath.split(".").pop()!;
            outputFilePath = outputFilePath.replace(originFormat, params.f);
          }
          // 验证是否已存在 outputFilePath ,有则跳过,没有继续
          if (!existsSync(outputFilePath)) {
            const fitFunc = fitKit[fitFuncName];
            await Promise.resolve(
              fitFunc({
                inputFilePath,
                outputFilePath,
                ctx: fitFuncContext,
                params: params,
              })
            );
          }
        }
        // 3. 返回文件路径导出语句
        if (mode == "development") {
          let code = `export default "${outputFilePath.replace(root, "")}";`;
          return code;
        } else {
          // 如果文件是构建时生成的,需要将生成的文件添加到构建产物中
          const referenceId = this.emitFile({
            type: "asset",
            name: path.basename(outputFilePath),
            needsCodeReference: true,
            source: readFileSync(outputFilePath),
          });
          let code = `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;
          return code;
        }
      }
      return null;
    },
    //...

本插件针对视频、图片资源内置了 3 个常用的转换函数,详情请看这里:

builtInFitKit = {
  scale: videoScaleFit, // 基于ffmpeg, 调整视频分辨率,用法: @fit:scale(w=xx&h=xx)
  rs: imageResizeFit, // 基于sharp, 调整图片尺寸,用法: @fit:rs(w=xx&h=xx&f=cover...)
  imgtf: imgTransformFit, // 基于sharp, 转换图片格式、质量等等,用法:@fit:imgtf(f=png&q=80)
};

支持自定义转换

插件支持 options.fitKit 配置,可轻松自定义转换逻辑,如下:

// vite.config.ts
import mediaFit from "unplugin-mediafit/vite";
export default defineConfig({
  plugins: [
    mediaFit({
      /* options */
    }),
  ],
});
// options简要
interface IOptions {
  /**
   * 自定义fitFunc集合,key为使用时的缩写
   */
  fitKit?: { [key: string /**使用标识 */]: FitFunc };
  /**
   * 如需要使用ffmpeg, 请配置ffmpeg命令行工具路径,如果ffmpeg命令全局可用,传'ffmpeg'即可
   */
  ffmpegPath?: string;
}
type FitFunc = (param: IFitFuncParam) => void;
// ...

例如,内置的转换视频分辨率的 FitFunc 实现如下:

// fitFunc 一般包含以下逻辑
// 1. 读取 inputFilePath 文件
// 2. 处理
// 3. 将处理结果写入 outputFilepath 中
/**
 * 调整视频分辨率,用法 scale(w=xx&h=xx)
 * @param data IFitFuncParam
 */
const videoScaleFit: FitFunc = async (data: IFitFuncParam) => {
  const { inputFilePath, outputFilePath, ctx, params } = data;
  try {
    const { w = -1, h = -1 } = params;
    const argsStr = `-i ${inputFilePath} -vf scale=${w}:${h} ${outputFilePath}`;
    await ctx.ffmpeg.run(argsStr);
  } catch (error) {
    // 删除文件
    rm(outputFilePath);
  }
};

最终 V1.0.0 版本安装

# using npm
npm install -D unplugin-mediafit
# using pnpm
pnpm install -D unplugin-mediafit
# using yarn
yarn add --dev unplugin-mediafit

在公司早下班_宝宝语言早开发_

配置

暂时只支持 vite

// vite.config.ts
import mediaFit from "unplugin-mediafit/vite";
export default defineConfig({
  plugins: [
    mediaFit({
      /* options */
    }),
  ],
});

使用

踩坑使用设计

一开始想要把query写在文件后缀名后面的,像这样:

import xx from "xxx.png@fit:fitFuncKey(a=xx&b=xx)"
                       ^^^^^----------^^^^^^^^^^^
                       固定标识    |     自定义参数
                              fitFunc标识

但这样有个问题,typescript找不到模块声明,会报错

如果要解决的话,需要对每一个这种模块自动增加模块声明(像我上一个插件 那样),会增加一点工作量。最终决定把query放到文件后缀前,这样可以不用考虑typescript的问题!

内置支持 ffmpeg 调用能力

一开始调研有3种方案:

引入 ffmpeg.wasm,0.12 之前的版本(用户免安装)插件里塞一个 ffmpeg 可执行文件,我插件里用 child_process 进行调用(用户免安装)让用户自己安装ffmpeg,配置ffmpeg cli工具路径,我插件里用 child_process 进行调用(用户需要先安装ffmpeg)

我是比较倾向于‘用户免安装’方案的,毕竟让用户开箱即用在我看来是非常重要的交互。但实践过程中我发现以下问题:

ffmpeg.wasm 0.12 版本在node.js端使用时要求 node 版本 16.x,其他版本报错(不推荐)一个官方构建的 ffmpeg 可执行文件大小有80M左右,太大了。。。

后面我了解到可以自定义编译ffmpeg源码,只保留需要的最小功能!于是经过漫长的AI辅助与实验,在Mac M1电脑上用以下编译配置编译出了只保留修改 mp4 格式视频的分辨率等功能的最小版本产物,大小只有2.2M!

./configure \
  --disable-everything \
  --enable-small \
  --enable-gpl \
  --enable-nonfree \
  --enable-libx264 \
  --enable-encoder=libx264 \
  --enable-decoder=h264 \
  --enable-parser=h264 \
  --enable-protocol=file \
  --enable-filter=scale \
  --enable-demuxer=mov,mp4 \
  --enable-muxer=mp4 \
  --enable-static

make clean && make -j$(sysctl -n hw.ncpu) && make install DESTDIR=$(pwd)/install

但自定义编译ffmpeg还有以下问题:

还需要针对Linux、window平台各编译一个可执行文件,插件里根据平台分别调用;自定义编译只满足特定的功能,如果用户需要其他功能,自定义的可执行文件就废了‍;

经过一番思考与取舍,考虑到安装ffmpeg极其简单,最终采取第三种方案,当用户需要ffmpeg能力时,由用户自己安装ffmpeg,然后配置 ffmpeg 可执行文件的路径。

最后

本插件已发布npm包,并开源到 github unplugin-mediafit,如果觉得本插件对你有帮助或文章对你有启发,麻烦动动手指给我个吧,非常感谢!