注意:AI结果,仅供参考
使用方法:
1. 环境要求
-
Node.js 16+
-
项目目录中包含
package.json和源代码
2. 安装依赖
bash
npm install glob fs-extra @babel/parser @babel/traverse
3. 运行脚本
将脚本保存为 analyze-migration.js,然后在项目根目录执行:
bash
node analyze-migration.js [项目路径]
如果不指定路径,默认为当前目录。
4. 输出结果
会在项目根目录生成 migration-report.md 报告文件。
三、核心代码
bash
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// 配置
const config = {
targetDir: process.argv[2] || '.',
outputFile: path.join(process.argv[2] || '.', 'migration-report.md'),
};
// 浏览器 API 黑名单
const browserApis = [
'window', 'document', 'localStorage', 'sessionStorage', 'location', 'history',
'alert', 'confirm', 'prompt', 'fetch', 'XMLHttpRequest', 'WebSocket',
'IntersectionObserver', 'ResizeObserver', 'Canvas', 'Image', 'FileReader'
];
// 第三方库兼容性映射
const libCompat = {
// UI 库(不兼容)
'element-ui': '❌ 不兼容,需替换为 uni-ui 或 uView',
'ant-design-vue': '❌ 不兼容,需替换为 uni-ui 或 uView',
'vant': '❌ 不兼容,需替换为 uni-ui 或 uView',
'vuetify': '❌ 不兼容',
// 图表库(需适配)
'echarts': '⚠️ 有微信小程序版本,但绘图逻辑需重写',
'highcharts': '⚠️ 无官方小程序版,需替换',
// 工具库(兼容)
'lodash': '✅ 纯 JS 库,可保留',
'dayjs': '✅ 纯 JS 库,可保留',
'moment': '✅ 纯 JS 库,可保留',
// 其他
'axios': '⚠️ 需替换为 uni.request',
'vue-router': '⚠️ 需替换为 pages.json 配置',
'vuex': '✅ 可保留,持久化需改用 uni.storage',
'pinia': '✅ 可保留,持久化需改用 uni.storage',
};
// 1. 读取 package.json
function readPackageJson() {
const pkgPath = path.join(config.targetDir, 'package.json');
if (!fs.existsSync(pkgPath)) {
console.error('未找到 package.json');
process.exit(1);
}
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
}
// 2. 分析依赖
function analyzeDependencies(pkg) {
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const analysis = {};
for (const [lib, version] of Object.entries(deps)) {
if (libCompat[lib]) {
analysis[lib] = { version, status: libCompat[lib] };
} else {
// 默认未知,标记为需人工确认
analysis[lib] = { version, status: '❓ 未知,需人工确认' };
}
}
return analysis;
}
// 3. 扫描源代码,检测浏览器 API 和 Vue 特性
function scanSourceCode() {
const patterns = ['**/*.{vue,js,ts}', '!node_modules/**', '!dist/**'];
const files = glob.sync(patterns, { cwd: config.targetDir, absolute: true });
let results = {
browserApiUsage: new Set(),
vueFeatures: {
customDirective: false,
dynamicComponent: false,
compositionApi: false,
ts: false,
renderFunction: false,
},
uiLibraries: new Set(),
hardwareApiUsage: new Set(),
};
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8');
const ext = path.extname(file);
// 检测浏览器 API(简单正则)
for (const api of browserApis) {
const regex = new RegExp(`\\b${api}\\b`, 'g');
if (regex.test(content)) {
results.browserApiUsage.add(api);
}
}
// 检测 Vue 特性(通过 AST 分析更准确,这里简单正则)
if (content.includes('Vue.directive') || content.includes('app.directive')) {
results.vueFeatures.customDirective = true;
}
if (content.includes('<component :is') || content.includes(':is=')) {
results.vueFeatures.dynamicComponent = true;
}
if (content.includes('composition-api') || content.includes('setup(') || content.includes('<script setup')) {
results.vueFeatures.compositionApi = true;
}
if (file.endsWith('.ts') || content.includes('lang="ts"')) {
results.vueFeatures.ts = true;
}
if (content.includes('render(') && ext === '.vue') {
results.vueFeatures.renderFunction = true;
}
// 检测 UI 库引用
for (const lib of Object.keys(libCompat)) {
if (content.includes(lib)) {
results.uiLibraries.add(lib);
}
}
// 检测硬件 API
const hardwareKeywords = ['wx.scanCode', 'uni.scanCode', 'getLocation', 'chooseImage', 'requestPayment'];
for (const kw of hardwareKeywords) {
if (content.includes(kw)) {
results.hardwareApiUsage.add(kw);
}
}
}
return results;
}
// 4. 检测路由与状态管理
function detectRouterAndStore(pkg, sourceResults) {
let router = null;
if (pkg.dependencies['vue-router']) {
router = 'vue-router 已安装';
}
let store = null;
if (pkg.dependencies['vuex']) {
store = 'vuex 已安装';
} else if (pkg.dependencies['pinia']) {
store = 'pinia 已安装';
}
return { router, store };
}
// 5. 检测构建工具(通过配置文件)
function detectBuildTool() {
const root = config.targetDir;
if (fs.existsSync(path.join(root, 'vite.config.js')) || fs.existsSync(path.join(root, 'vite.config.ts'))) {
return 'Vite';
} else if (fs.existsSync(path.join(root, 'webpack.config.js'))) {
return 'Webpack';
} else if (fs.existsSync(path.join(root, 'vue.config.js'))) {
return 'Vue CLI (Webpack)';
}
return '未知';
}
// 6. 静态资源分析(简单统计)
function analyzeAssets() {
const patterns = ['**/*.{png,jpg,jpeg,gif,svg,ttf,woff,woff2}', '!node_modules/**', '!dist/**'];
const files = glob.sync(patterns, { cwd: config.targetDir, absolute: true });
let totalSize = 0;
let largeFiles = [];
for (const file of files) {
const size = fs.statSync(file).size;
totalSize += size;
if (size > 100 * 1024) { // 超过 100KB
largeFiles.push({ file: path.relative(config.targetDir, file), size });
}
}
return { totalSize, largeFiles };
}
// 7. 生成报告
function generateReport(pkg, depsAnalysis, sourceScan, routerStore, buildTool, assets) {
const lines = [];
lines.push('# H5+Vue 项目转微信小程序可行性评估报告\n');
lines.push(`生成时间:${new Date().toLocaleString()}\n`);
lines.push(`项目路径:${config.targetDir}\n`);
// 1. 技术栈概览
lines.push('## 1. 技术栈概览\n');
lines.push(`- **Vue 版本**:${pkg.dependencies['vue'] || '未检测到'}`);
lines.push(`- **构建工具**:${buildTool}`);
lines.push(`- **TypeScript**:${sourceScan.vueFeatures.ts ? '是' : '否'}`);
lines.push(`- **Composition API**:${sourceScan.vueFeatures.compositionApi ? '是' : '否'}`);
lines.push(`- **自定义指令**:${sourceScan.vueFeatures.customDirective ? '是' : '否'}`);
lines.push(`- **动态组件**:${sourceScan.vueFeatures.dynamicComponent ? '是' : '否'}`);
lines.push(`- **渲染函数**:${sourceScan.vueFeatures.renderFunction ? '是' : '否'}\n`);
// 2. 依赖兼容性
lines.push('## 2. 第三方依赖兼容性分析\n');
lines.push('| 依赖包 | 版本 | 兼容性评估 |');
lines.push('|--------|------|------------|');
for (const [lib, info] of Object.entries(depsAnalysis)) {
lines.push(`| ${lib} | ${info.version} | ${info.status} |`);
}
lines.push('');
// 3. 浏览器 API 使用情况
lines.push('## 3. 浏览器 API 使用情况\n');
if (sourceScan.browserApiUsage.size > 0) {
lines.push('以下 API 在小程序中不可用,需要替换:');
for (const api of sourceScan.browserApiUsage) {
lines.push(`- \`${api}\``);
}
} else {
lines.push('未检测到明显的浏览器专有 API(仅通过正则扫描,可能存在漏报)。');
}
lines.push('');
// 4. UI 库使用
if (sourceScan.uiLibraries.size > 0) {
lines.push('## 4. UI 库使用情况\n');
lines.push('检测到以下 UI 库,均需替换为 uni-app 兼容的组件库:');
for (const lib of sourceScan.uiLibraries) {
lines.push(`- ${lib}`);
}
lines.push('');
}
// 5. 路由与状态管理
lines.push('## 5. 路由与状态管理\n');
lines.push(`- **路由**:${routerStore.router || '未使用 vue-router'}`);
lines.push(`- **状态管理**:${routerStore.store || '未使用'}`);
if (routerStore.router) {
lines.push(' - ⚠️ vue-router 需替换为 pages.json 配置,路由守卫需重写。');
}
if (routerStore.store) {
lines.push(' - ⚠️ 状态管理逻辑可保留,但持久化需改用 uni.storage。');
}
lines.push('');
// 6. 硬件能力调用
if (sourceScan.hardwareApiUsage.size > 0) {
lines.push('## 6. 硬件能力调用\n');
lines.push('检测到以下硬件相关 API,小程序有对应能力但调用方式不同:');
for (const api of sourceScan.hardwareApiUsage) {
lines.push(`- \`${api}\``);
}
lines.push('');
}
// 7. 静态资源
lines.push('## 7. 静态资源分析\n');
lines.push(`- **总大小**:${(assets.totalSize / 1024 / 1024).toFixed(2)} MB`);
lines.push(`- **大文件(>100KB)**:${assets.largeFiles.length} 个`);
if (assets.largeFiles.length > 0) {
lines.push(' 建议将大文件上传至 CDN,避免影响小程序包体积。');
}
lines.push('');
// 8. 综合评估与建议
lines.push('## 8. 综合评估与建议\n');
let score = 100;
if (sourceScan.browserApiUsage.size > 0) score -= 20;
if (sourceScan.uiLibraries.size > 0) score -= 20;
if (sourceScan.vueFeatures.dynamicComponent) score -= 10;
if (routerStore.router) score -= 10;
if (assets.largeFiles.length > 5) score -= 10;
score = Math.max(0, score);
lines.push(`**兼容性评分**:${score}/100(分数越高,迁移成本越低)\n`);
if (score >= 80) {
lines.push('✅ **建议**:项目兼容性较好,可考虑使用 uni-app 进行快速迁移,预计工作量较小。');
} else if (score >= 50) {
lines.push('⚠️ **建议**:项目存在中等程度的不兼容项,需预留充足时间进行适配。推荐使用跨端框架重构,同时保留 H5 版本。');
} else {
lines.push('❌ **建议**:项目兼容性较差,直接迁移成本较高。建议评估是否单独开发小程序,或仅将部分页面嵌入 WebView。');
}
lines.push('\n---\n');
lines.push('*注:本报告基于静态扫描生成,可能存在误判或漏报,请结合人工审查最终确认。*');
return lines.join('\n');
}
// 主函数
async function main() {
const pkg = readPackageJson();
const depsAnalysis = analyzeDependencies(pkg);
const sourceScan = scanSourceCode();
const routerStore = detectRouterAndStore(pkg, sourceScan);
const buildTool = detectBuildTool();
const assets = analyzeAssets();
const report = generateReport(pkg, depsAnalysis, sourceScan, routerStore, buildTool, assets);
fs.writeFileSync(config.outputFile, report, 'utf-8');
console.log(`报告已生成:${config.outputFile}`);
}
main().catch(console.error);