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 等通用页面,但业务项目需要能够:
- 扩展路由
- 扩展自定义组件
- 扩展表单项类型
如何在编译时动态判断业务项目是否提供了扩展配置?
解决方案
引入 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 模式 | 从应用变为库 | 导出函数而非直接执行 |