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 模式 从应用变为库 导出函数而非直接执行
相关推荐
打瞌睡的朱尤1 小时前
Vue day11商品详细页,加入购物车,购物车
前端·javascript·vue.js
用户600071819102 小时前
【翻译】Rolldown工作原理:模块加载、依赖图与优化机制全揭秘
前端
SuperEugene2 小时前
《对象与解构赋值:接口数据解包的 10 个常见写法》
前端·javascript
Mr_Xuhhh2 小时前
博客文章:HTML核心概念与常见标签速览
前端
打瞌睡的朱尤2 小时前
Vue day12 Vue3认识,写法区分
前端·javascript·vue.js
阿珊和她的猫2 小时前
Vue Router 的使用指南
前端·javascript·vue.js
打瞌睡的朱尤2 小时前
day8 Vue-x
前端·javascript·vue.js
Web打印2 小时前
Phpask(php集成环境)之04配置网站
开发语言·前端·php
Zhencode2 小时前
vue3运行时核心模块之runtime-dom
前端·javascript·vue.js