返回

Vue 应用中 index.html 与 manifest.json 图标路径匹配方案

vue.js

Vue 应用中 index.html 和 manifest.json 图标路径匹配问题

构建 Vue 应用时,经常会遇到 index.htmlmanifest.json 中图标路径不一致的问题。 这篇文章会聊聊产生这个问题的原因,以及对应的处理方法。

问题

index.html 里,通常这样定义应用图标:

<link rel="icon" type="image/svg+xml" href="src/assets/telepathic.svg"/>
<link rel="apple-touch-icon" href="src/assets/telepathic.svg">

构建应用时,src/assets 下的文件会被复制到 dist/assets,文件名会加上哈希值。挺好!

同时,为了让应用在移动设备上表现更好,需要一个 manifest.json 文件。 在这个文件里, 需要指定图标的路径:

{
"icons": [
    {
      "src": "/telepathic.svg",
      "sizes": "48x48 72x72 96x96 128x128 256x256 512x512",
      "type": "image/svg+xml",
      "purpose": "any"
    }
  ]
}

问题来了,如果像上面这样写路径,就得把图标文件 telepathic.svg 放到 public 目录。

但我想用和 index.html 里一样的图标文件 (src/assets/telepathic.svg)。如果直接写这个路径,构建时路径不会被转换成最终的发布路径。

有没有办法解决呢?

我也试过在 manifest.json 里用相对路径:

{
"icons": [
    {
      "src": "src/assets/telepathic.svg",
      "sizes": "48x48 72x72 96x96 128x128 256x256 512x512",
      "type": "image/svg+xml",
      "purpose": "any"
    }
  ]
}

但还是不行,因为 manifest.json 里的路径不会被处理。

问题原因

根本原因在于 index.htmlmanifest.json 处理资源路径的方式不同:

  • index.html 中的资源路径(比如 link 标签的 href 属性)通常由构建工具(如 Webpack)处理,会进行路径转换、文件哈希等操作。
  • manifest.json 是一个静态文件,构建工具通常不会处理其中的路径。

解决方案

下面提供几种解决这个问题的方法。

1. 使用 Webpack 插件 (推荐)

使用 Webpack 插件是最推荐的方法,可以自动处理 manifest.json 中的路径,并保证与 index.html 中的路径一致。

原理: Webpack 插件会在构建过程中拦截 manifest.json 文件的生成,并修改其中的图标路径,将其替换为正确的、经过哈希处理后的路径。

操作步骤:

  1. 安装插件(以 webpack-manifest-plugin 为例,但它似乎不会直接替换路径, 这里只是一个占位,可以尝试 copy-webpack-plugin 配合 transform):

    npm install --save-dev copy-webpack-plugin
    
  2. 修改 Webpack 配置文件(通常是 vue.config.js):

    const CopyPlugin = require("copy-webpack-plugin");
    
    module.exports = {
      configureWebpack: {
        plugins: [
          new CopyPlugin({
            patterns: [
              {
                from: "public/manifest.json",
                to: "manifest.json",
                transform(content, path) {
                   //获取文件的Buffer
                  const manifest = JSON.parse(content.toString());
    
                   // 假设 telepathic.svg 的最终路径会变成 /assets/telepathic.[hash].svg
                  manifest.icons[0].src = '/assets/telepathic.[hash].svg'.replace('[hash]', Date.now()); // 这里只是一个简易的例子,实际应该根据 webpack 的 output.filename 规则来替换
    
                  return Buffer.from(JSON.stringify(manifest));
                },
              },
            ],
          }),
        ],
      },
    };
    

注意: 上述代码中的 '[hash]' 部分只是一个占位符。你需要根据 Webpack 的 output.filename 配置规则,来确定正确的哈希值替换方式。一种方式可以使用正则进行处理.
更好的方案是直接引入图片,从而获取编译后的路径:
```javascript
const CopyPlugin = require("copy-webpack-plugin");
const telepathicIcon = require('./src/assets/telepathic.svg'); // 直接引入你的 SVG 文件

module.exports = {
  configureWebpack: {
    plugins: [
      new CopyPlugin({
        patterns: [
          {
            from: "public/manifest.json",
            to: "manifest.json",
            transform(content) {
               //获取文件的Buffer
              const manifest = JSON.parse(content.toString());
            manifest.icons[0].src = telepathicIcon;
              return Buffer.from(JSON.stringify(manifest));
            },
          },
        ],
      }),
    ],
  },
   // ...其他配置, 一定要保证 webpack 能处理 svg 文件.
   chainWebpack: config => {
      config.module
        .rule('svg')
        .test(/\.svg$/)
        .use('file-loader')
           .loader('file-loader')
           .options({
            name: 'assets/[name].[hash:8].[ext]'
           })
      }
};

```
  1. manifest.json 文件放到 public 文件夹,其中的 src 使用占位符,例如:

    {
    "icons": [
        {
          "src": "placeholder.svg", // 占位符
          "sizes": "48x48 72x72 96x96 128x128 256x256 512x512",
          "type": "image/svg+xml",
          "purpose": "any"
        }
      ]
    }
    

2. 使用环境变量

通过环境变量,可以在构建时动态生成 manifest.json 中的图标路径。

原理: 在构建脚本中,读取图标文件的真实路径(带哈希),并将其设置为环境变量。然后,在 manifest.json 中使用这个环境变量。

操作步骤:

  1. 安装 dotenv (如果还没有):

    npm install --save-dev dotenv
    
  2. 在项目根目录下创建一个 .env 文件(如果还没有):

    # .env 文件
    VUE_APP_ICON_PATH=
    

    VUE_APP_ICON_PATH 将在下一步被填充.

  3. 修改构建脚本(例如 package.json 中的 build 脚本):

    {
      "scripts": {
        "build": "node ./scripts/set-icon-path.js && vue-cli-service build"
      }
    }
    

    这里使用了 &&,确保node脚本执行完后再构建。

  4. 创建一个 scripts/set-icon-path.js 文件:

    const fs = require('fs');
    const path = require('path');
    const crypto = require('crypto');
    
    const iconPath = path.resolve(__dirname, '../src/assets/telepathic.svg'); //图标地址
    const iconContent = fs.readFileSync(iconPath);
    const hash = crypto.createHash('md5').update(iconContent).digest('hex').substring(0, 8); //计算文件hash,取前8位
    const finalIconPath = `/assets/telepathic.${hash}.svg`; // 假设 webpack 的 output.filename 设置成这样的格式.
    
     //读取 .env 文件.
      let envData = fs.readFileSync(path.resolve(__dirname, '../.env'), 'utf-8');
     //使用正则匹配并且替换.
      envData = envData.replace(/VUE_APP_ICON_PATH=(.*)/, `VUE_APP_ICON_PATH=${finalIconPath}`);
    
      fs.writeFileSync(path.resolve(__dirname, '../.env'), envData);
      console.log('Icon path set to:', finalIconPath);
    

    这个脚本做的事:

    • 读取图标文件内容。
    • 计算文件内容的 MD5 哈希值,并取前8位 (或者其他适合你项目的哈希策略)。
    • 根据哈希值生成最终的图标路径 (例如 /assets/telepathic.a1b2c3d4.svg)。
    • 更新.env,设置VUE_APP_ICON_PATH。
  5. manifest.json 中使用环境变量:

    {
      "icons": [
        {
          "src": "%VUE_APP_ICON_PATH%",
          "sizes": "48x48 72x72 96x96 128x128 256x256 512x512",
          "type": "image/svg+xml",
          "purpose": "any"
        }
      ]
    }
    

    需要确保 public 目录下的 manifest.json 在被复制时可以进行环境变量的替换, 这可能也需要配置 copy-webpack-plugin.

        new CopyPlugin({
          patterns: [
              {
                from: "public/manifest.json",
                to: "manifest.json",
                transform(content) {
                    let manifestString = content.toString();
                   manifestString = manifestString.replace(/%VUE_APP_ICON_PATH%/g, process.env.VUE_APP_ICON_PATH);
                  return Buffer.from(manifestString);
                },
              },
            ],
    }),
    

3. 后处理脚本 (不太推荐,但更直接)

在构建完成后,使用一个脚本直接修改 dist/manifest.json 中的图标路径。

原理: 构建过程结束后, 手动运行脚本,根据 dist/assets 目录下的实际文件名,更新dist/manifest.json

操作步骤:

  1. 创建一个脚本文件(例如 scripts/fix-manifest.js):

    const fs = require('fs');
    const path = require('path');
    
    const manifestPath = path.resolve(__dirname, '../dist/manifest.json');
    const assetsDir = path.resolve(__dirname, '../dist/assets');
    
    fs.readFile(manifestPath, 'utf8', (err, data) => {
      if (err) {
        console.error('Error reading manifest.json:', err);
        return;
      }
    
      let manifest = JSON.parse(data);
    
      // 找到 assets 目录下的实际图标文件名
       //遍历资源文件寻找对应的图片文件。
      fs.readdir(assetsDir, (err, files) => {
           if (err) {
               console.error("读取资源文件夹失败", err);
                return;
            }
         const iconFile = files.find(file => file.startsWith('telepathic.') && file.endsWith('.svg'));
    
          if (iconFile) {
               manifest.icons[0].src = `/assets/${iconFile}`; //更新路径.
    
             fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8', (err) => {
            if (err) {
              console.error('Error writing manifest.json:', err);
            } else {
              console.log('manifest.json updated successfully!');
             }
              });
         }else {
            console.log("找不到图标文件!");
         }
    
      });
    });
    

这个脚本:
* 读取 dist/manifest.json.
* 寻找dist/assets中的图标.
* 更新图标的src.
* 重新写入 dist/manifest.json

  1. 修改构建脚本(例如 package.json 中的 build 脚本):

    {
      "scripts": {
        "build": "vue-cli-service build && node scripts/fix-manifest.js"
      }
    }
    

    通过&&,确保构建完毕再运行修复脚本。

进阶技巧: 多尺寸图标处理

如果需要处理多个尺寸的图标, 上述方法也都是适用的, 只是需要在处理路径替换的地方循环操作多个图标配置. 例如,在使用 webpack 插件时, 可以在transform 函数里遍历 manifest.icons 数组,对每个图标的 src 进行替换。

安全建议

不管选择哪种方案, 都请确保:

  • 图标文件本身没有安全问题。
  • 如果使用了自定义脚本, 要注意脚本的权限和执行环境。
  • 如果使用 Webpack, 也要留意相关插件的安全更新。

希望这些方法对你有帮助!