Vite如何自动导入指定目录内的静态资源文件,并生成导出模块?

上一次详细学习Vite的文档的时候还是Vite2.x,现在Vite已经到了Vite5,在此期间已经有了很多新的变化和特性,刚好今天抽空重新学习一下。

关于Vite5

Vite 不再支持 Node.js 14 / 16 / 17 / 19,因为它们已经到了 EOL。现在需要 Node.js 18 / 20+。

查看Vite预设的各种开发模板:https://github.com/vitejs/vite/tree/main/packages/create-vite

社区维护的各种开发模板:https://github.com/vitejs/awesome-vite#templates

1.库模式

可用于快速便捷的开发JS库,库模式打包的时候将打包单独的js文件。

相关文档:https://cn.vitejs.dev/guide/build.html#library-mode

2.server.hmr

进行公众号网页开发的时候,需要解析实际的域名并配置https才能进行授权操作。

一般会通过内网穿透来进行开发,但是内网穿透需要自己搭建代理,有时候请求速度还不尽如人意。

所以我会选择把域名解析到本地的局域网,然后反向代理到开发服务器。

印象中在以前版本的Vite中,热重载依赖的Websocker服务器端口是跟随当前访问的地址,当配置如下:

{
  server: {
    hmr: {
       path: "/ws"
    }
  }
}

解析好域名(dev.nicen.cn)并配置好反向代理:

location /
{
    proxy_set_header Connection "keep-alive";
    proxy_set_header X-Real-IP $remote_addr;
    fastcgi_connect_timeout 600;
    fastcgi_send_timeout 600;
    fastcgi_read_timeout 600;

    if (!-f $request_filename) {
     proxy_pass http://192.168.2.63:4001;
    }

}

location /ws {
	proxy_pass http://192.168.2.63:4001;
	proxy_http_version 1.1;
	proxy_read_timeout 3600s;
	proxy_send_timeout 300s;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
}

以前Vite连接的websocket地址会是:wss://dev.nicen.cn/ws

现在Vite连接的websocket地址会是:wss://dev.nicen.cn:4001/ws

所以现在需要将vite配置改为:

{
  server: {
    hmr: {
       clientPort: 443,
       path: "/ws"
    }
  }
}

Vite插件开发

项目中经常会用到很多图片,用的时候每次都得写很多import,假如能够自动引入指定目录下所有图片的资源链接到一个公共对象上面,这样的话使用某个图片就只需要引入这个对象。

尝试了网上很多现成的自动引入图片的插件,但是都封装成了单个组件,于是就有了下面这些代码。

1.最终效果

使用方法如下:

// 从生成的模块中引入
import images from '@/config/images';

// 模板中使用
// 代表调用目录内名称是smile的资源文件
<img :src='images.smile'/>

2.插件代码

import fs from 'fs/promises';
import path from 'path';

/**
 * 递归读取目录下的所有文件
 * @param dir
 * @returns {Promise<unknown[]>}
 */
async function readDirectoryFiles(dir) {
    const dirs = await fs.readdir(dir, {withFileTypes: true});
    const files = await Promise.all(dirs.map(dirent => {
        const res = path.resolve(dir, dirent.name);
        return dirent.isDirectory() ? readDirectoryFiles(res) : res;
    }));
    return Array.prototype.concat(...files);
}


/**
 * 创建目录,如果目录不存在
 * @param dir
 * @returns {Promise<void>}
 */
async function ensureDirExists(dir) {
    try {
        await fs.mkdir(dir, {recursive: true});
    } catch (err) {
        if (err.code !== 'EEXIST') {
            throw err;
        }
    }
}

/**
 * JS关键字
 * @type {string[]}
 */
const keywords = [
    'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
    'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
    'for', 'function', 'if', 'import', 'in', 'instanceof', 'new',
    'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof',
    'var', 'void', 'while', 'with', 'let', 'yield'
];


/**
 * 定义插件
 * @param options
 * @returns {{name: string, config(): Promise<void>}}
 */
export default function createAuto(options = {}) {

    const {
        image, // 默认的图片文件目录
        output, // 输出 JS 文件的路径
        prefix, //import的前缀
        type //需要识别的文件类型
    } = options;


    return {
        name: 'vite-plugin-autoload-image',
        async config() {

            /* 参数不完整,终止代码 */
            if (!image || !output || !prefix || !type) {
                return;
            }

            /* 读取指定目录下的所有文件 */
            const files = await readDirectoryFiles(image);

            /* 过滤出所有的指定类型的文件 */
            const imageFiles = files.filter(file => type.indexOf(path.extname(file)) > -1);

            /* 所有文件的名称 */
            const filesName = [];

            /* 为每个 image 文件生成 import 语句 */
            const imports = imageFiles.map(file => {

                /* 路径判断 */
                const relativePath = prefix + path
                    .relative(path.dirname(image), file)
                    .replace(/\\/g, '/');

                const fileName = path.basename(file, path.extname(file))
                    .replace(/[^0-9a-zA-Z]/g, "_");

                /* 排除js关键字 */
                if (keywords.indexOf(fileName) > -1 || filesName.indexOf(fileName) > -1) {
                    filesName.push(`_${fileName}`); //记录文件名
                    return `import _${fileName} from '${relativePath}';`;
                } else {
                    filesName.push(fileName); //记录文件名
                    return `import ${fileName} from '${relativePath}';`;
                }
            });


            /* 确保输出文件的目录存在 */
            const outputDir = path.dirname(output);
            await ensureDirExists(outputDir);

            /* 将所有 import 和 export 语句写入到指定的 JS 文件中 */
            const content = imports.join('\n') + "\n\n\n\n\n" + `export default {\n\t${filesName.join(",\n\t")}\n}`;
            await fs.writeFile(output, content, 'utf-8');

            /* 拼接ts类型 */
            const types = filesName.map(item => {
                return `\t\t\t${item} : string`
            }).join(",\n");

            /* 添加ts类型,让IDE能够识别 */
            await fs.writeFile(`${outputDir}/global.d.ts`, `
declare module "@vue/runtime-core" {
    export interface ComponentCustomProperties {
        images: { \n ${types} \n\t\t\t} 
    }
}

export {}
                                            `, 'utf-8');

        },
    };
}

3.Vite配置

export default defineConfig({
    plugins: [
        vue(),
        createAuto({
            /* 需要引入的图片目录 */
            image: 'src/assets',
            /* 输出的模块路径 */
            output: 'src/config/images.js',
            /* 引入时添加的前缀 */
            prefix: '@/',
            /* 引入哪些类型的文件 */
            type: ['.jpg', '.svg', '.png']
        }),
    ],
}