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 模式 从应用变为库 导出函数而非直接执行
相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95275 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大5 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师5 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学5 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端