跨平台兼容性
概述
全面指南,用于编写在Windows、macOS和Linux上无缝工作的代码。涵盖文件路径处理、环境检测、平台特定功能和测试策略。
何时使用
- 为多个操作系统构建应用程序
- 处理文件系统操作
- 管理平台特定依赖项
- 检测操作系统和架构
- 使用环境变量
- 构建跨平台CLI工具
- 处理行尾和字符编码
- 管理平台特定的构建过程
指南
1. 文件路径处理
Node.js路径模块
// ❌ 不好:带有平台特定分隔符的硬编码路径
const configPath = 'C:\\Users\\user\\config.json'; // 仅限Windows
const dataPath = '/home/user/data.txt'; // Unix专用
// ✅ 好:使用路径模块
import path from 'path';
import os from 'os';
// 跨平台路径构建
const configPath = path.join(os.homedir(), 'config', 'app.json');
const dataPath = path.join(process.cwd(), 'data', 'users.txt');
// 解析相对路径
const absolutePath = path.resolve('./config/settings.json');
// 获取路径组件
const dirname = path.dirname('/path/to/file.txt'); // '/path/to'
const basename = path.basename('/path/to/file.txt'); // 'file.txt'
const extname = path.extname('/path/to/file.txt'); // '.txt'
// 规范化路径(处理..和.)
const normalized = path.normalize('/path/to/../file.txt'); // '/path/file.txt'
Python路径处理
# ❌ 不好:硬编码分隔符
config_path = 'C:\\Users\\user\\config.json' # 仅限Windows
data_path = '/home/user/data.txt' # Unix专用
# ✅ 好:使用pathlib
from pathlib import Path
import os
# 跨平台路径构建
config_path = Path.home() / 'config' / 'app.json'
data_path = Path.cwd() / 'data' / 'users.txt'
# 处理路径
if config_path.exists():
content = config_path.read_text()
# 获取路径组件
dirname = config_path.parent
filename = config_path.name
extension = config_path.suffix
# 解析相对路径
absolute_path = Path('./config/settings.json').resolve()
# 创建目录
output_dir = Path('output')
output_dir.mkdir(parents=True, exist_ok=True)
Go路径处理
package main
import (
"os"
"path/filepath"
)
func main() {
// ❌ 不好:硬编码路径
// configPath := "C:\\Users\\user\\config.json"
// ✅ 好:使用filepath包
homeDir, _ := os.UserHomeDir()
configPath := filepath.Join(homeDir, "config", "app.json")
// 获取路径组件
dir := filepath.Dir(configPath)
base := filepath.Base(configPath)
ext := filepath.Ext(configPath)
// 清理和规范化路径
cleaned := filepath.Clean("path/to/../file.txt")
// 转换为绝对路径
absPath, _ := filepath.Abs("./config/settings.json")
}
2. 平台检测
Node.js平台检测
// platform-utils.ts
import os from 'os';
export const Platform = {
isWindows: process.platform === 'win32',
isMacOS: process.platform === 'darwin',
isLinux: process.platform === 'linux',
isUnix: process.platform !== 'win32',
get current(): 'windows' | 'macos' | 'linux' | 'unknown' {
switch (process.platform) {
case 'win32': return 'windows';
case 'darwin': return 'macos';
case 'linux': return 'linux';
default: return 'unknown';
}
},
get arch(): string {
return process.arch; // 'x64', 'arm64', 等
},
get homeDir(): string {
return os.homedir();
},
get tempDir(): string {
return os.tmpdir();
}
};
// 使用
if (Platform.isWindows) {
// Windows特定代码
console.log('Running on Windows');
} else if (Platform.isMacOS) {
// macOS特定代码
console.log('Running on macOS');
} else if (Platform.isLinux) {
// Linux特定代码
console.log('Running on Linux');
}
// 架构检测
if (Platform.arch === 'arm64') {
console.log('Running on ARM architecture');
}
Python平台检测
# platform_utils.py
import platform
import sys
class Platform:
@staticmethod
def is_windows():
return sys.platform.startswith('win')
@staticmethod
def is_macos():
return sys.platform == 'darwin'
@staticmethod
def is_linux():
return sys.platform.startswith('linux')
@staticmethod
def is_unix():
return not Platform.is_windows()
@staticmethod
def current():
if Platform.is_windows():
return 'windows'
elif Platform.is_macos():
return 'macos'
elif Platform.is_linux():
return 'linux'
return 'unknown'
@staticmethod
def arch():
return platform.machine() # 'x86_64', 'arm64', 等
@staticmethod
def version():
return platform.version()
# 使用
if Platform.is_windows():
# Windows特定代码
print('Running on Windows')
elif Platform.is_macos():
# macOS特定代码
print('Running on macOS')
elif Platform.is_linux():
# Linux特定代码
print('Running on Linux')
3. 行尾
// line-endings.ts
import os from 'os';
export const LineEnding = {
LF: '
', // Unix/Linux/macOS
CRLF: '\r
', // Windows
CR: '\r', // 旧Mac(OS X前)
get platform(): string {
return os.EOL; // 返回平台特定的行尾
},
normalize(text: string, target: string = os.EOL): string {
// 将所有行尾规范化为目标
return text.replace(/\r
|\r|
/g, target);
},
toUnix(text: string): string {
return this.normalize(text, this.LF);
},
toWindows(text: string): string {
return this.normalize(text, this.CRLF);
}
};
// 使用
const fileContent = fs.readFileSync('file.txt', 'utf8');
// 规范化为平台特定的行尾
const normalized = LineEnding.normalize(fileContent);
// 强制Unix行尾(适用于git等)
const unixContent = LineEnding.toUnix(fileContent);
// 写入平台特定的行尾
fs.writeFileSync('output.txt', normalized);
4. 环境变量
// env-utils.ts
export class EnvUtils {
// 获取环境变量,有回退
static get(key: string, defaultValue?: string): string | undefined {
return process.env[key] || defaultValue;
}
// 获取PATH分隔符(Unix上的:,Windows上的;)
static get pathSeparator(): string {
return process.platform === 'win32' ? ';' : ':';
}
// 分割PATH为数组
static getPaths(): string[] {
const pathVar = process.env.PATH || '';
return pathVar.split(this.pathSeparator);
}
// 获取常见路径
static get home(): string {
return process.env.HOME || process.env.USERPROFILE || '';
}
static get user(): string {
return process.env.USER || process.env.USERNAME || '';
}
// 检查是否在CI中运行
static get isCI(): boolean {
return !!(
process.env.CI ||
process.env.CONTINUOUS_INTEGRATION ||
process.env.GITHUB_ACTIONS ||
process.env.GITLAB_CI
);
}
}
5. Shell命令
// shell-utils.ts
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export class ShellUtils {
// 使用平台特定的处理执行命令
static async execute(command: string): Promise<string> {
try {
const { stdout, stderr } = await execAsync(command, {
shell: this.getShell()
});
if (stderr) console.error(stderr);
return stdout.trim();
} catch (error) {
throw new Error(`命令失败:${error.message}`);
}
}
// 获取平台特定的shell
static getShell(): string {
if (process.platform === 'win32') {
return 'cmd.exe';
}
return process.env.SHELL || '/bin/sh';
}
// 平台特定命令
static async listFiles(directory: string): Promise<string> {
if (process.platform === 'win32') {
return this.execute(`dir "${directory}"`);
}
return this.execute(`ls -la "${directory}"`);
}
static async clearScreen(): Promise<void> {
if (process.platform === 'win32') {
await this.execute('cls');
} else {
await this.execute('clear');
}
}
static async openFile(filepath: string): Promise<void> {
if (process.platform === 'win32') {
await this.execute(`start "" "${filepath}"`);
} else if (process.platform === 'darwin') {
await this.execute(`open "${filepath}"`);
} else {
await this.execute(`xdg-open "${filepath}"`);
}
}
}
6. 文件权限
// permissions.ts
import fs from 'fs';
import path from 'path';
export class FilePermissions {
// 使文件可执行(仅限Unix)
static makeExecutable(filepath: string): void {
if (process.platform !== 'win32') {
fs.chmodSync(filepath, 0o755);
}
}
// 检查文件是否可执行
static isExecutable(filepath: string): boolean {
if (process.platform === 'win32') {
// 在Windows上,检查文件扩展名
const ext = path.extname(filepath).toLowerCase();
return ['.exe', '.bat', '.cmd', '.com'].includes(ext);
}
try {
fs.accessSync(filepath, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
// 使用特定权限创建文件(Unix)
static createWithPermissions(
filepath: string,
content: string,
mode: number = 0o644
): void {
fs.writeFileSync(filepath, content, { mode });
}
}
7. 进程管理
// process-utils.ts
import { spawn, ChildProcess } from 'child_process';
export class ProcessUtils {
// 使用平台特定的信号通过PID杀进程
static kill(pid: number, signal?: string): void {
if (process.platform === 'win32') {
// Windows不支持信号,使用taskkill
spawn('taskkill', ['/pid', pid.toString(), '/f', '/t']);
} else {
process.kill(pid, signal || 'SIGTERM');
}
}
// 使用平台特定的处理来生成进程
static spawnCommand(
command: string,
args: string[] = []
): ChildProcess {
if (process.platform === 'win32') {
// Windows需要cmd.exe来运行命令
return spawn('cmd', ['/c', command, ...args], {
stdio: 'inherit',
shell: true
});
}
return spawn(command, args, {
stdio: 'inherit',
shell: true
});
}
// 通过名称查找进程
static async findProcess(name: string): Promise<number[]> {
if (process.platform === 'win32') {
const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${name}"`);
// 解析Windows tasklist输出
const pids: number[] = [];
const lines = stdout.split('
');
for (const line of lines) {
const match = line.match(/\s+(\d+)\s+/);
if (match) pids.push(parseInt(match[1]));
}
return pids;
} else {
const { stdout } = await execAsync(`pgrep ${name}`);
return stdout.split('
').filter(Boolean).map(Number);
}
}
}
8. 平台特定依赖项
// package.json
{
"name": "my-app",
"dependencies": {
"common-dep": "^1.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.2"
},
"devDependencies": {
"@types/node": "^18.0.0"
}
}
// platform-specific-module.ts
export async function loadPlatformModule() {
if (process.platform === 'win32') {
return await import('./windows/module');
} else if (process.platform === 'darwin') {
return await import('./macos/module');
} else {
return await import('./linux/module');
}
}
// 平台特定模块的优雅回退
export function useFSEvents() {
try {
// fsevents仅限macOS
if (process.platform === 'darwin') {
const fsevents = require('fsevents');
return fsevents;
}
} catch (error) {
console.warn('fsevents不可用,使用回退');
}
// 回退到chokidar或fs.watch
return require('chokidar');
}
9. 跨平台测试
GitHub Actions Matrix
# .github/workflows/test.yml
name: 跨平台测试
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: 设置Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: 安装依赖项
run: npm ci
- name: 运行测试
run: npm test
- name: 平台特定测试
if: runner.os == 'Windows'
run: npm run test:windows
- name: 平台特定测试
if: runner.os == 'macOS'
run: npm run test:macos
- name: 平台特定测试
if: runner.os == 'Linux'
run: npm run test:linux
平台特定测试
// tests/platform.test.ts
import { Platform } from '../src/platform-utils';
describe('平台特定测试', () => {
describe('文件路径', () => {
it('应该正确处理路径', () => {
const configPath = path.join(os.homedir(), 'config.json');
if (Platform.isWindows) {
expect(configPath).toMatch(/^[A-Z]:\\/);
} else {
expect(configPath).toMatch(/^\//);
}
});
});
describe.skipIf(Platform.isWindows)('Unix-only测试', () => {
it('应该使用符号链接', () => {
// 符号链接测试
});
it('应该处理文件权限', () => {
// 权限测试
});
});
describe.skipIf(!Platform.isWindows)('Windows-only测试', () => {
it('应该使用UNC路径', () => {
// UNC路径测试
});
it('应该处理驱动器字母', () => {
// 驱动器字母测试
});
});
});
10. 字符编码
// encoding-utils.ts
import iconv from 'iconv-lite';
export class EncodingUtils {
// 用特定编码读取文件
static readFile(filepath: string, encoding: string = 'utf8'): string {
const buffer = fs.readFileSync(filepath);
if (encoding === 'utf8') {
// 如果存在BOM,则移除
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
return buffer.slice(3).toString('utf8');
}
return buffer.toString('utf8');
}
return iconv.decode(buffer, encoding);
}
// 用特定编码写文件
static writeFile(
filepath: string,
content: string,
encoding: string = 'utf8'
): void {
if (encoding === 'utf8') {
fs.writeFileSync(filepath, content, 'utf8');
} else {
const buffer = iconv.encode(content, encoding);
fs.writeFileSync(filepath, buffer);
}
}
// 检测编码
static detectEncoding(filepath: string): string {
const buffer = fs.readFileSync(filepath);
// 检查BOM
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
return 'utf8';
}
if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
return 'utf16be';
}
if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
return 'utf16le';
}
// 默认为UTF-8
return 'utf8';
}
}
11. 构建配置
// rollup.config.js
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs'
},
{
file: 'dist/index.esm.js',
format: 'esm'
}
],
external: [
// 将平台特定模块标记为外部
'fsevents'
],
plugins: [
// 在构建时替换平台检查,以实现更好的tree-shaking
replace({
'process.platform': JSON.stringify(process.platform),
preventAssignment: true
})
]
};
最佳实践
✅ 要做
- 使用path.join()或path.resolve()处理路径
- 使用os.EOL处理行尾
- 需要时在运行时检测平台
- 在所有目标平台上进行测试
- 使用optionalDependencies用于平台特定模块
- 优雅地处理文件权限
- 使用shell转义用户输入
- 在文本文件中规范化行尾
- 默认使用UTF-8编码
- 文档化平台特定行为
- 为平台特定功能提供回退
- 使用CI/CD在多个平台上进行测试
❌ 不要
- 硬编码带有反斜杠或正斜杠的文件路径
- 假设Unix-only特性(信号、权限、符号链接)
- 忽略Windows特定怪癖(驱动器字母、UNC路径)
- 无回退使用平台特定命令
- 假设区分大小写的文件系统
- 忘记不同的行尾
- 使用平台特定API而不检查
- 硬编码环境变量访问模式
- 忽略字符编码问题
常见模式
模式1:平台工厂
export interface PlatformHandler {
openFile(path: string): Promise<void>;
getConfigPath(): string;
}
class WindowsHandler implements PlatformHandler {
async openFile(path: string) {
await exec(`start "" "${path}"`);
}
getConfigPath() {
return path.join(process.env.APPDATA!, 'myapp', 'config.json');
}
}
class UnixHandler implements PlatformHandler {
async openFile(path: string) {
await exec(`xdg-open "${path}"`);
}
getConfigPath() {
return path.join(os.homedir(), '.config', 'myapp', 'config.json');
}
}
export function createPlatformHandler(): PlatformHandler {
return process.platform === 'win32'
? new WindowsHandler()
: new UnixHandler();
}
模式2:条件导入
const platformModule = await (async () => {
switch (process.platform) {
case 'win32':
return import('./platforms/windows');
case 'darwin':
return import('./platforms/macos');
default:
return import('./platforms/linux');
}
})();
工具与资源
- cross-env: 跨平台设置环境变量
- cross-spawn: 跨平台spawn
- rimraf: 跨平台rm -rf
- mkdirp: 跨平台mkdir -p
- cpy: 跨平台文件复制
- del: 跨平台文件删除
- execa: 更好的child_process
- pkg: 为所有平台打包Node.js应用程序