Elpis NPM 包抽离过程

Elpis NPM 包抽离过程 - 难点与卡点分析

背景

将 elpis 从一个独立运行的应用抽离成可被其他项目引用的 npm 包(@shhhwm/elpis)。


难点一:路径解析的根本性变化

问题描述

独立应用时,process.cwd()__dirname 指向同一个项目根目录。但作为 npm 包被引用后:

  • process.cwd() → 业务项目根目录
  • __dirname → node_modules/@shhhwm/elpis/xxx

解决方案

所有 loader 都需要区分两套路径:

javascript 复制代码
// elpis 框架自身的路径(使用 __dirname)
const elpisControllerPath = path.resolve(__dirname, `..${sep}..${sep}app${sep}controller`);

// 业务项目的路径(使用 process.cwd())
const businessControllerPath = path.resolve(app.businessPath, `.${sep}controller`);

难点二:Webpack Loader 找不到

问题描述

作为 npm 包时,webpack 配置中直接写 loader: "vue-loader" 会报错找不到 loader,因为 webpack 默认从业务项目的 node_modules 查找。

解决方案

所有 loader 配置改用 require.resolve() 确保从 elpis 包内解析:

javascript 复制代码
// 错误写法
use: "vue-loader"

// 正确写法
use: require.resolve("vue-loader")

同理,样式相关 loader 也需要处理:

javascript 复制代码
use: [
  require.resolve("style-loader"),
  require.resolve("css-loader"),
  require.resolve("less-loader"),
]

难点三:双层加载机制设计

问题描述

框架需要同时加载自身的 controller/service/router 和业务项目的,且业务项目的可以覆盖框架的。

解决方案

所有 loader 改造为"先加载 elpis,再加载业务"的模式:

javascript 复制代码
// 1. 先加载 elpis 框架的 controller
const elpisFileList = glob.sync(path.resolve(elpisControllerPath, `.${sep}**${sep}**.js`));
elpisFileList.forEach((file) => handleFile(file));

// 2. 再加载业务项目的 controller(可覆盖同名)
const businessFileList = glob.sync(path.resolve(businessControllerPath, `.${sep}**${sep}**.js`));
businessFileList.forEach((file) => handleFile(file));

config 加载也是类似逻辑,业务配置覆盖框架默认配置:

javascript 复制代码
defaultConfig = {
  ...elpisDefaultConfig,
  ...businessConfig,
};

难点四:前端钩子扩展机制

问题描述

框架提供了 dashboard、schema-form、schema-table 等通用页面,但业务项目需要能够:

  1. 扩展路由
  2. 扩展自定义组件
  3. 扩展表单项类型

如何在编译时动态判断业务项目是否提供了扩展配置?

解决方案

引入 blank.js 空模块 + webpack alias 动态判断:

javascript 复制代码
// blank.js - 空模块作为默认值
module.exports = {};

webpack alias 配置中动态判断:

javascript 复制代码
const blankModulePath = path.resolve(__dirname, "../libs/blank.js");

// 判断业务项目是否提供了路由扩展配置
const businessDashboardRouterConfig = path.resolve(
  process.cwd(),
  "./app/pages/dashboard/router.js"
);
aliasMap["$businessDashboardRouterConfig"] = fs.existsSync(businessDashboardRouterConfig)
  ? businessDashboardRouterConfig
  : blankModulePath;

业务代码中使用:

javascript 复制代码
import businessDashboardRouterConfig from "$businessDashboardRouterConfig";

// 如果业务项目提供了扩展,则执行
if (typeof businessDashboardRouterConfig === "function") {
  businessDashboardRouterConfig({ routes, siderRoutes });
}

支持的扩展点:

  • $businessDashboardRouterConfig - 路由扩展
  • $businessComponentConfig - schema-view 组件扩展
  • $businessFormItemConfig - schema-form 表单项扩展
  • $businessSearchItemConfig - schema-search-bar 搜索项扩展

难点五:依赖分类调整

问题描述

npm 包被安装时,devDependencies 不会被安装。但 webpack、babel、loader 等构建依赖在运行时是必需的。

解决方案

将构建相关依赖从 devDependencies 移到 dependencies

json 复制代码
{
  "dependencies": {
    "webpack": "^5.88.1",
    "webpack-cli": "^5.1.4",
    "vue-loader": "^17.2.2",
    "babel-loader": "^8.0.4",
    "css-loader": "^0.23.1",
    "less-loader": "^11.1.3",
    // ... 其他构建依赖
  },
  "devDependencies": {
    // 只保留开发/测试工具
    "eslint": "^7.32.0",
    "mocha": "^6.1.4"
  }
}

难点六:中间件分层加载

问题描述

框架有自己的全局中间件,业务项目也可能有自己的中间件,需要确保加载顺序正确。

解决方案

在 elpis-core 中分两步加载:

javascript 复制代码
// 1. 先注册 elpis 框架的全局中间件
const elpisMiddleware = require(`${elpisPath}/app/middleware.js`);
elpisMiddleware(app);

// 2. 再注册业务项目的全局中间件
try {
  require(`${app.businessPath}/middleware.js`)(app);
} catch (e) {
  console.log("[exception] there is no global business middleware file.");
}

难点七:入口改造为 SDK 模式

问题描述

原来是直接启动应用,现在需要导出 API 供业务项目调用。

解决方案

javascript 复制代码
// 之前:直接执行
ElpisCore.start({ name: 'Elpis' });

// 之后:导出 SDK 接口
module.exports = {
  Controller: { Base: require("./app/controller/base.js") },
  Service: { Base: require("./app/service/base.js") },

  frontendBuild(env) {
    if (env === "local") FEBuildDev();
    else if (env === "production") FEBuildProd();
  },

  serverStart(options) {
    return ElpisCore.start(options);
  }
};

webpack 构建脚本也从"直接执行"改为"导出函数":

javascript 复制代码
// 之前
const compiler = webpack(webpackConfig);
app.listen(port);

// 之后
module.exports = () => {
  const compiler = webpack(webpackConfig);
  app.listen(port);
};

总结

难点 核心问题 解决思路
路径解析 cwd vs dirname 语义变化 区分 elpis 路径和业务路径
Loader 找不到 webpack 默认从业务项目查找 使用 require.resolve()
双层加载 框架和业务代码需要合并 先加载框架,再加载业务
钩子扩展 编译时动态判断扩展配置 blank.js + webpack alias
依赖分类 devDeps 不会被安装 构建依赖移到 dependencies
中间件分层 加载顺序问题 框架中间件先于业务中间件
SDK 模式 从应用变为库 导出函数而非直接执行
相关推荐
天平2 小时前
油猴脚本创建webworker踩坑记录
前端·javascript·typescript
原则猫4 小时前
前端基础大厦
前端
陈随易5 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·后端·程序员
SoaringHeart6 小时前
Flutter进阶:基于 EasyRefresh 的下拉刷新封装 n_easy_refresh_mixin.dart
前端·flutter
IT_陈寒8 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰8 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding
竹林8189 小时前
用 The Graph 查询链上数据实战:从手搓 RPC 到 Subgraph,我的 NFT 项目数据加载快了 10 倍
前端·javascript
妙码生花9 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码代码逐行目检
前端·后端·go
Awu122710 小时前
⚡从零开发 Agent CLI(五)实现一个可治理、可扩展的工具系统
前端·人工智能·claude
咪库咪库咪10 小时前
Vue3-生命周期
前端