继微信原生小程序的一次gulp构建后,需要来个uniapp迁移,于是搞它。

一、项目目录介绍

源码目录

├── build
│ ├── pagesJsonConfigs
│ ├── pageUrlConfigs
│ └── env.config.json
├── config
│ └── constants.js
├── .gitignore
├── envPlugin.js
├── package.json
└── tsconfig.json

  • build:用于存放构建相关的配置文件,是前端工程化的重要部分,里面的子目录和文件会在项目构建过程中发挥作用,以生成符合小程序要求的代码或资源。
    • pagesJsonConfigs:存放微信小程序pages.json文件,这里可以根据环境量选择不同的pages.json。
    • pageUrlConfigs:这个目录用于存放小程序路由map文件{key:路径},这个文件不是必须的,但有了它,便于管理页面跳转地址。
    • env.config.json:环境配置文件,可根据不同的运行环境(如开发环境、生产环境)配置不同的参数,像接口地址、调试开关等,实现项目在不同环境下的灵活切换。
  • envPlugin.js:vite环境量插件。
  • package.json:项目的配置文件,包含了项目的基本信息(如名称、版本)、依赖包列表、脚本命令等,是 npm 管理项目的核心文件。
  • vite.config.js: vite构建配置

二、目录文件介绍

2.1 build/pagesJsonConfigs

  1. index.json 配置索引文件
{
    "customer1": "customer1_pages.json" // 环境:配置文件名
}
  1. default_pages.json 默认的小程序pages.json文件配置
  2. customer1_pages.json 环境量customer1对应的pages.json配置文件,当环境量是customer1时,选取此文件作为pages.json文件

2.2 build/pageUrlConfigs

  1. index.json配置索引文件
{
    "customer1": "customer1_pageUrl.js" // 环境:配置文件名
}
  1. default_pageUrl.js 默认的小程序pageUrl.js文件配置
export default {
    HOME: '/pages/index/index',
    LOG: '/pages/logs/logs'
}
  1. customer1_pageUrl.js环境量customer1对应的pageUrl.js配置文件,当环境量是customer1时,选取此文件作为pageUrl.js文件

注意

以上index.json索引文件的key还可以加上运行平台标志,比如alipay、weixin,然后在envPlugin中修改读取key的逻辑

{
    "alipay:customer1": "alipay_customer1_***.js", // 环境:配置文件名
    "weixin:customer1": "weixin_customer1_***.js"
}

2.3 build/env.config.json

环境量配置文件

{
    "common": {
   		"PAGE_TITLE": "测试项目"
    },
    "env": {
        "customer1": {
            "dev": {
                "API_URL": "http://test.com.cn/customer1"
            },
            "prod": {
                "API_URL": "http://prod.com.cn/customer1"
            }
        },
        "customer2": {
            "dev": {
                "API_URL": "http://test.com.cn/customer2"
            },
            "prod": {
                "API_URL": "http://prod.com.cn/customer2"
            }
        }
    }
}

环境量customer1对应的dev环境量就是

{
	"API_URL": "http://test.com.cn/customer1"
}

2.6 package.json

{
  "name": "uniapp-vue3",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
	"uni-app": {
		"scripts": {
			"weixin:dev:customer1": { "title": "customer1(wx:dev)", "env": { "UNI_PLATFORM": "mp-weixin" }},
			"alipay:dev:customer1": { "title": "customer1(alipay:dev)", "env": { "UNI_PLATFORM": "mp-alipay" }}
		}
	}
}


启动的时候在运行在菜单中,找到title对应的命令就行

三、envPlugin.js vite插件介绍

const fs = require('fs')
const fsPromises = fs.promises;
const path = require('path')

function envPlugin(options = {}) {
	// 配置文件路径
	const pathConfig = {
		pagesJsonConfigPath: options.pagesJsonConfigPath || './build/pagesJsonConfigs',
		pageUrlConfigPath: options.pageUrlConfigPath || './build/pageUrlConfigs',
		envConfigPath: options.envConfigPath || './build/env.config.json',
		outputPagesJsonPath: options.outputPagesJsonPath || './pages.json',
		outputPageUrlPath: options.outputPageUrlPath || './config/pageUrl.js',
		outputEnvPath: options.outputEnvPath || './config/envConfig.js',
	}

	// 定位项目根目录(通过 package.json 确保准确性)
	function getProjectRoot() {
		let currentDir = __dirname;
		while (currentDir !== path.dirname(currentDir)) {
			if (fs.existsSync(path.join(currentDir, 'package.json'))) {
				return currentDir;
			}
			currentDir = path.dirname(currentDir);
		}
		return __dirname; // 兜底
	}
	const projectRoot = getProjectRoot();

	// 解析环境标识
	function parseEnvName(envName) {
		const [platform, envType, client] = envName.split(':')
		return { platform, envType, client }
	}

	// 检查输出目录是否存在,不存在就创建
	function ensureOutputDirs(outputPaths) {
		outputPaths.forEach(pathStr => {
			// 基于项目根目录解析路径
			const outputDir = path.dirname(path.resolve(projectRoot, pathStr));
			if (!fs.existsSync(outputDir)) {
				fs.mkdirSync(outputDir, { recursive: true });
				console.log(`创建目录: ${outputDir}`);
			}
		});
	}

	// 基于项目根目录解析路径
	function resolvePath(filePath) {
		const resolvedPath = path.resolve(projectRoot, filePath);
		return resolvedPath;
	}

	// 读取文件(改进:使用fsPromises并确保读取最新内容)
	async function readFile(filePath, needParse = false) {
		try {
			const fullPath = resolvePath(filePath);
			// 检查文件是否存在
			await fsPromises.access(fullPath);
			// 读取文件内容,确保获取最新内容
			const content = await fsPromises.readFile(fullPath, 'utf-8');
			return needParse ? JSON.parse(content) : content;
		} catch (error) {
			throw new Error(`解析文件失败 ${filePath}: ${error.message}`);
		}
	}
	// 生成envConfig配置文件
	async function generateEnvConfig(platform, envType, client) {
		try {
			ensureOutputDirs([pathConfig.outputEnvPath]);
			// ------------读取env配置源文件------------
			const envConfig = await readFile(resolvePath(pathConfig.envConfigPath), true);
			const envData = envConfig.env?.[client]?.[envType];
			const commonEnvData = envConfig['common'] || {};
			if (!envData) {
				throw new Error(`${pathConfig.envConfigPath} 中未找到 env:${client} -- ${envType} 配置`);
			}

			// 生成envConfig.js
			const envContent = `
// 构建时自动生成,请勿修改!
export default ${JSON.stringify({
	...envData,
	CUSTOMER_CODE: client,
	ENV: envType,
	...commonEnvData
}, null, 2)}
      `.trim();
			await fsPromises.writeFile(resolvePath(pathConfig.outputEnvPath), envContent, 'utf-8');
			console.log(`${pathConfig.outputEnvPath}配置已更新: ${platform}:${envType}:${client}`);
			return true;
		} catch (error) {
			console.error('配置文件生成失败:', error.message);
			return false;
		}
	}
	// 生成pages.json配置文件
	async function generatePagesJson(platform, envType, client) {
		try {

			ensureOutputDirs([pathConfig.outputPagesJsonPath]);
			// -------------读取pages.json配置源文件-------------------------
			const pageConfigPath = resolvePath(`${pathConfig.pagesJsonConfigPath}/index.json`);
			const pageConfig = await readFile(pageConfigPath, true);
			const pagesDataFileName = pageConfig[`${platform}:${client}`] || 'default_pages.json';
			const pagesDataPath = resolvePath(`${pathConfig.pagesJsonConfigPath}/${pagesDataFileName}`);
			// 增加日志确认使用的pages配置文件
			console.log(`使用页面配置文件: ${pagesDataPath}`);
			const pagesContent = await readFile(pagesDataPath);
			// 生成pages.json
			const outputPagesPath = resolvePath(pathConfig.outputPagesJsonPath);
			await fsPromises.writeFile(outputPagesPath, pagesContent, 'utf-8');
			// 验证写入内容
			const writtenContent = await fsPromises.readFile(outputPagesPath, 'utf-8');
			if (writtenContent !== pagesContent) {
				console.warn(`页面配置写入不一致: ${outputPagesPath}`);
			}
			console.log(`${pathConfig.outputPagesJsonPath}配置已更新: ${platform}:${envType}:${client}`);

			return true;
		} catch (error) {
			console.error('配置文件生成失败:', error.message);
			return false;
		}
	}

	// 生成pageUrl配置文件
	async function generatePageUrl(platform, envType, client) {
		try {
			ensureOutputDirs([pathConfig.outputPageUrlPath]);
			// -------------读取pageUrl.js配置源文件-------------
			const pageUrlConfig = await readFile(resolvePath(`${pathConfig.pageUrlConfigPath}/index.json`), true);
			const pageUrlFileName = pageUrlConfig[`${platform}:${client}`] || 'default_pageUrl.js';
			const pageUrlData = await readFile(`${pathConfig.pageUrlConfigPath}/${pageUrlFileName}`);

			// 生成pageUrl.js
			const pageUrlContent = `
// 构建时自动生成,请勿修改!
${pageUrlData}
      `.trim();
			await fsPromises.writeFile(resolvePath(pathConfig.outputPageUrlPath), pageUrlContent, 'utf-8');
			console.log(`${pathConfig.outputPageUrlPath}配置已更新: ${platform}:${envType}:${client}`);

			return true;
		} catch (error) {
			console.error('配置文件生成失败:', error.message);
			return false;
		}
	}
	// 记录已监听的目录,避免重复监听
	const watchedDirs = new Set();
	// 平台判断
	const isWindows = process.platform === 'win32';
	const isMac = process.platform === 'darwin';

	function watchDirectory(targetPath) {
		// 标准化路径,解决不同平台路径格式问题
		const normalizedPath = path.normalize(targetPath);
		// 避免重复监听同一目录
		if (watchedDirs.has(normalizedPath)) {
			return;
		}
		watchedDirs.add(normalizedPath);

		// 监听当前目录
		const watcher = fs.watch(normalizedPath, {
			recursive: false,
			encoding: 'utf8'
		}, async (eventType, filename) => {
			if (!filename) {
				return;
			}
			// 处理不同平台的事件类型差异
			let actualEventType = eventType;
			// Windows下的rename事件可能包含change
			if (isWindows && eventType === 'rename') {
				// 检查文件是否存在来判断是新增/删除还是重命名
				try {
					await fs.promises.access(path.join(normalizedPath, filename));
					actualEventType = 'change';
				} catch {
					actualEventType = 'unlink';
				}
			}
			// 处理macOS的特殊情况
			if (isMac && eventType === 'rename') {
				// macOS下rename事件可能需要额外处理
				actualEventType = 'change';
			}
			const fullPath = path.join(normalizedPath, filename);
			try {
				// 获取文件状态判断是文件还是目录
				const stats = await fs.promises.stat(fullPath);

				if (stats.isDirectory()) {
					// 如果是目录且有变化,重新监听子目录
					watchDirectory(fullPath);
				} else if (actualEventType === 'change') {
					await updateFile(fullPath);
				}
			} catch (err) {
				// 文件可能已被删除,忽略错误
				if (err.code !== 'ENOENT') {
					console.error('Watch error:', err);
				}
			}
		});

		// 监听错误事件
		watcher.on('error', (err) => {
			console.error(`Watcher error for ${normalizedPath}:`, err);
			// 出错时移除监听记录,允许重新监听
			watchedDirs.delete(normalizedPath);
		});

		// 初始递归监听子目录
		try {
			const dirents = fs.readdirSync(normalizedPath, { withFileTypes: true });
			dirents.forEach(dirent => {
				if (dirent.isDirectory()) {
					const subDir = path.join(normalizedPath, dirent.name);
					watchDirectory(subDir);
				}
			});
		} catch (err) {
			console.error(`Failed to read directory ${normalizedPath}:`, err);
			watchedDirs.delete(normalizedPath);
		}

		return watcher;
	}
	let debounceTimer = null; // 定时器存储到状态对象中
	async function updateFile(target) {
		// 防抖计时器ID(使用闭包存储)
		if (debounceTimer) {
			clearTimeout(debounceTimer);
		}
		debounceTimer = setTimeout(async () => {
			const envName = process.env.UNI_SCRIPT;
			const { platform, envType, client } = parseEnvName(envName);
			// 检查文件属于哪种配置类型,并执行对应生成方法
			if (target.includes('env.config.json')) {
				await generateEnvConfig(platform, envType, client);
			} else if (target.includes('pagesJsonConfigs')) {
				await generatePagesJson(platform, envType, client);
			} else if (target.includes('pageUrlConfigs')) {
				await generatePageUrl(platform, envType, client);
			}
			// 执行完毕后清除计时器
			debounceTimer = null;
		}, 300); // 防抖延迟时间,单位ms
	}
	let hasWatcher = false;
	return {
		name: 'vite-env-plugin',
		// 在配置解析前执行
		async config() {
			const envName = process.env.UNI_SCRIPT;
			if (envName) {
				console.log('开始生成配置(UniApp编译前)...');
				const { platform, envType, client } = parseEnvName(envName);
				await generateEnvConfig(platform, envType, client);
				await generatePagesJson(platform, envType, client);
				await generatePageUrl(platform, envType, client);
				console.log('配置生成完成,即将启动UniApp编译流程');
			} else {
				console.warn('未设置UNI_SCRIPT环境变量,跳过配置生成');
			}
		},

		// 在构建开始时启动监听(小程序运行时会触发)
		async buildStart() {
			const envName = process.env.UNI_SCRIPT;
			if (!envName) return;
			if (!hasWatcher) {
				hasWatcher = true
				const target = resolvePath('./build');
				if (fs.existsSync(target)) {
					console.log('开始监听./build');
					watchDirectory(target);
				}
			}
		},
	}
}

module.exports = envPlugin;

四、vite.config.js介绍

import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import envPlugin from './envPlugin.js'

export default defineConfig(() => {
  return {
    plugins: [ // 注意envPlugin一定放在uni前面,先执行环境量相关配置,再执行原本的uniapp运行步骤
			envPlugin(), uni(), 
		],
    define: {
      'process.env': process.env
    },
  };
});
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐