Elpis npm 包抽离总结

6 阅读3分钟

本文档总结将 elpis 框架抽离为 npm 包过程中的核心难点与解决方案


核心难点

1. 路径解析的双重性挑战 ⭐⭐⭐

问题:npm 包内代码需要同时访问包内资源和业务项目资源,两类路径的解析方式完全不同。

核心区别

路径类型使用变量指向位置特点
包内路径__dirnamenpm 包安装目录(如 node_modules/@david-yjy/elpis/...固定不变
业务路径process.cwd()执行命令时的当前工作目录(业务项目根目录)动态变化

代码示例

// webpack.base.js

// ✅ 包内路径:使用 __dirname
const elpisEntryList = path.resolve(__dirname, '../../pages/**/entry.*.js');

// ✅ 业务路径:使用 process.cwd()
const businessEntryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');

常见卡点

// ❌ 错误:混淆两种路径类型
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。

代码对比

// ❌ 错误:可能找不到或版本不对
module: {
  rules: [{
    test: /\.vue$/,
    use: 'vue-loader'
  }]
}

// ✅ 正确:使用 require.resolve() 从包内解析
module: {
  rules: [{
    test: /\.vue$/,
    use: {
      loader: require.resolve('vue-loader'),  // 确保版本一致
    }
  }]
}

为什么这样做?

  • 版本一致性:确保使用包内定义的 loader 版本
  • 路径可靠性:无论在什么环境下都能找到正确的 loader
  • 隔离性:包内依赖不会影响业务项目

3. 业务扩展点的可选性设计 ⭐⭐

问题:框架需要提供扩展点让业务项目自定义配置,但这些配置必须是可选的,缺失时不能报错。

解决方案:使用 fs.existsSync() 检查 + 空模块降级。

代码示例

// 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):

module.exports = {}  // 确保业务代码可以安全 require

设计模式

  1. 检测fs.existsSync() 检查文件是否存在
  2. 降级:不存在时提供默认实现(空模块)
  3. 透明:通过 webpack alias,业务代码无需关心配置来源

4. 配置合并与容错加载 ⭐⭐

问题:业务项目可能想要自定义 webpack 配置,但这个配置应该是可选的。

解决方案:使用 try-catch 容错 + webpack-merge 合并。

代码示例

// 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()确保从包内解析,版本一致

容错设计模式

// 模式1:可选配置检查 + 空模块降级
const config = fs.existsSync(configPath) ? configPath : blankModulePath;

// 模式2:try-catch 容错
let config = {};
try {
  config = require(configPath);
} catch (error) {
  // 静默处理
}

配置合并策略

// 使用 webpack-merge 的 smart 策略
const finalConfig = merge.smart(baseConfig, businessConfig);
// - 对象属性:后面的覆盖前面的
// - 数组:会合并去重(如 plugins、rules)

最佳实践

1. 路径处理规范

// ✅ 推荐:使用辅助函数明确区分
function getPackagePath(...paths) {
  return path.resolve(__dirname, '../../', ...paths);
}

function getProjectPath(...paths) {
  return path.resolve(process.cwd(), ...paths);
}

2. 依赖管理

  • 框架包:构建依赖放在 dependencies,确保版本一致
  • 业务项目:只需安装框架包,依赖由包提供

总结

将框架抽离为 npm 包的核心挑战在于路径上下文的管理。通过合理使用:

  1. __dirname - 访问包内资源(固定)
  2. process.cwd() - 访问业务项目资源(动态)
  3. require.resolve() - 解析包内依赖(版本一致)
  4. 容错设计 - 可选配置优雅降级