本文档总结将 elpis 框架抽离为 npm 包过程中的核心难点与解决方案
核心难点
1. 路径解析的双重性挑战 ⭐⭐⭐
问题:npm 包内代码需要同时访问包内资源和业务项目资源,两类路径的解析方式完全不同。
核心区别:
| 路径类型 | 使用变量 | 指向位置 | 特点 |
|---|---|---|---|
| 包内路径 | __dirname |
npm 包安装目录(如 node_modules/@david-yjy/elpis/...) |
固定不变 |
| 业务路径 | process.cwd() |
执行命令时的当前工作目录(业务项目根目录) | 动态变化 |
代码示例:
javascript
// webpack.base.js
// ✅ 包内路径:使用 __dirname
const elpisEntryList = path.resolve(__dirname, '../../pages/**/entry.*.js');
// ✅ 业务路径:使用 process.cwd()
const businessEntryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
常见卡点:
javascript
// ❌ 错误:混淆两种路径类型
const businessConfig = require(path.resolve(__dirname, './app/config.js'));
// ✅ 正确:明确区分
const businessConfig = require(path.resolve(process.cwd(), './app/config.js'));
学习要点:
__dirname= 模块文件所在目录(包内,固定)process.cwd()= 进程当前工作目录(业务项目,动态)- 永远不要假设业务项目路径相对于包的位置
2. Loader 解析的路径问题 ⭐⭐⭐
问题:直接使用字符串路径配置 loader 会导致路径找不到或版本冲突。
解决方案 :使用 require.resolve() 确保从包内解析 loader。
代码对比:
javascript
// ❌ 错误:可能找不到或版本不对
module: {
rules: [{
test: /\.vue$/,
use: 'vue-loader'
}]
}
// ✅ 正确:使用 require.resolve() 从包内解析
module: {
rules: [{
test: /\.vue$/,
use: {
loader: require.resolve('vue-loader'), // 确保版本一致
}
}]
}
为什么这样做?
- ✅ 版本一致性:确保使用包内定义的 loader 版本
- ✅ 路径可靠性:无论在什么环境下都能找到正确的 loader
- ✅ 隔离性:包内依赖不会影响业务项目
3. 业务扩展点的可选性设计 ⭐⭐
问题 :框架需要提供扩展点让业务项目自定义配置,但这些配置必须是可选的,缺失时不能报错。
解决方案 :使用 fs.existsSync() 检查 + 空模块降级。
代码示例:
javascript
// webpack.base.js
resolve: {
alias: (() => {
const aliasMap = {};
const blankModulePath = path.resolve(__dirname, '../libs/blank.js');
// 检查业务配置文件是否存在
const businessConfig = path.resolve(process.cwd(), './app/pages/dashboard/root.js');
// ✅ 关键:存在用业务配置,不存在用空模块
aliasMap['$businessDashboardRouterConfig'] =
fs.existsSync(businessConfig)
? businessConfig // 存在:使用业务配置
: blankModulePath; // 不存在:使用空模块
return aliasMap;
})()
}
空模块 (blank.js):
javascript
module.exports = {} // 确保业务代码可以安全 require
设计模式:
- 检测 :
fs.existsSync()检查文件是否存在 - 降级:不存在时提供默认实现(空模块)
- 透明:通过 webpack alias,业务代码无需关心配置来源
4. 配置合并与容错加载 ⭐⭐
问题:业务项目可能想要自定义 webpack 配置,但这个配置应该是可选的。
解决方案:使用 try-catch 容错 + webpack-merge 合并。
代码示例:
javascript
// webpack.base.js
// ✅ 容错加载业务配置
let businessWebpackConfig = {};
try {
businessWebpackConfig = require(`${process.cwd()}/app/webpack.config.js`);
} catch (error) {
// 静默处理:业务配置不存在是正常的
console.log('未找到业务 webpack 配置');
}
// ✅ 使用 webpack-merge 的 smart 策略合并
module.exports = merge.smart(
baseConfig, // 框架基础配置
businessWebpackConfig // 业务配置(可选,会覆盖基础配置)
);
关键点:
- ✅ 业务配置缺失不影响框架功能
- ✅ 业务配置会智能合并(数组合并,对象覆盖)
- ✅ 降低使用门槛,渐进增强
关键技术点总结
路径解析的核心原则
| 场景 | 使用变量 | 原因 |
|---|---|---|
| 包内资源路径 | __dirname |
指向包安装位置,固定不变 |
| 业务资源路径 | process.cwd() |
指向执行目录,动态变化 |
| Loader/插件路径 | require.resolve() |
确保从包内解析,版本一致 |
容错设计模式
javascript
// 模式1:可选配置检查 + 空模块降级
const config = fs.existsSync(configPath) ? configPath : blankModulePath;
// 模式2:try-catch 容错
let config = {};
try {
config = require(configPath);
} catch (error) {
// 静默处理
}
配置合并策略
javascript
// 使用 webpack-merge 的 smart 策略
const finalConfig = merge.smart(baseConfig, businessConfig);
// - 对象属性:后面的覆盖前面的
// - 数组:会合并去重(如 plugins、rules)
最佳实践
1. 路径处理规范
javascript
// ✅ 推荐:使用辅助函数明确区分
function getPackagePath(...paths) {
return path.resolve(__dirname, '../../', ...paths);
}
function getProjectPath(...paths) {
return path.resolve(process.cwd(), ...paths);
}
2. 依赖管理
- 框架包 :构建依赖放在
dependencies,确保版本一致 - 业务项目:只需安装框架包,依赖由包提供
总结
将框架抽离为 npm 包的核心挑战在于路径上下文的管理。通过合理使用:
- ✅
__dirname- 访问包内资源(固定) - ✅
process.cwd()- 访问业务项目资源(动态) - ✅
require.resolve()- 解析包内依赖(版本一致) - ✅ 容错设计 - 可选配置优雅降级