背景与目标
在早期的 elpis 项目中,前后端代码、业务逻辑、工程化构建都混杂在一起,项目启动、构建、路由、中间件等都紧耦合于具体业务。随着项目演进,我们希望将核心能力抽象为一个可复用的 SDK(npm 包),以便:
-
在多个业务项目中复用 elpis 的启动、路由加载、构建流程等能力;
-
保持业务项目的灵活性,让业务方能做扩展或覆盖;
-
使得核心框架与业务代码分离,便于维护、升级和版本管理。
所以,本次工作的目标是把 elpis 拆分成一个 npm 包(比如 @your-org/elpis-core 或类似命名),然后发布到 npm 上供业务项目安装使用。
下面我按步骤讲解这个拆分、封装与发布的过程。
整体思路与拆分策略
在拆分过程中,我主要考虑以下几点:
-
清晰边界:哪些代码属于框架核心(启动流程、加载器、插件机制、路由/中间件加载等),哪些代码属于业务项目(具体 controller、service、视图页面等)要划清边界;
-
加载机制:核心包内部要能够“读取”业务项目目录下的 controller、service、middleware、路由定义等,并合并/注册它们;
-
配置合并:核心包应带默认配置,业务项目可以提供配置覆盖或扩展;
-
前端工程集成:核心包中集成前端构建能力(webpack 配置、页面入口扫描、模板注入等),业务项目可以按需注入自定义配置;
-
别名与路径映射:在 webpack、模块解析、alias 等处,需要让核心包和业务项目文件能互相寻址;
-
对外 API:在核心包的
index.js或主入口处暴露出启动、构建、控制器、服务、扩展点等接口; -
打包与发布:决定包中的文件结构、哪些文件要纳入 npm 包、发布流程、版本管理等。
总的来说,就是“核心包做框架骨架 + 约定式加载业务代码 + 业务项目可配置/覆盖”的模式。
步骤详解
下面是一个较为完整的拆分流程和技术细节:
1. 构建核心包的 package.json
-
包名通常采用命名空间形式(
@your-org/elpis或@your-org/elpis-core等),便于区分管理与版本升级。 -
指定入口文件(
main字段),如index.js,作为对外暴露能力的入口。 -
在
package.json中配置files字段,指定哪些文件被打包到 npm(例如核心目录、loader、模板、公共资源等)。 -
如果是公开包,设定发布权限:
"publishConfig": { "registry": "https://registry.npmjs.org", "access": "public" } -
添加标准的
scripts(如 lint、test 等)和版本管理(利用npm version)等。
2. 在核心包里设计加载器(Loader)机制
核心包需要能在启动时,扫描并加载业务项目中的 module(middleware、controller、service、router、schema 等)。具体流程大致如下(灵感源自上文文章):
-
通过
process.cwd()确定业务项目根目录(app.businessPath)。 -
在核心包内定义一系列 loader(middlewareLoader、configLoader、controllerLoader、serviceLoader、routerLoader 等)。
-
每个 loader:
-
先加载自身 core 包内部对应目录的资源;
-
再扫描业务项目目录下对应路径(如
app/controller/**、app/service/**、app/middleware/**等); -
合并、覆盖或挂载业务的模块到核心应用上下文。
-
-
在加载某些文件时,用
glob等工具遍历文件列表,并用统一的handleFile(file)逻辑处理(提取文件名、驼峰命名、require 模块、挂载到app.controller、app.services等)。 -
对于配置文件(如
config.default.js、config.local.js等),进行合并:默认配置 + 业务覆盖配置。 -
对于路由、schema、扩展点,也做类似的加载与合并。
3. 暴露对外 API:启动、构建、拓展等
在核心包的入口文件(index.js 或类似)中暴露外部使用者调用的接口:
module.exports = {
serverStart(options = {}) {
const app = ElpisCore.start(options);
return app;
},
frontendBuild(env) {
if (env === 'local') FEBuildDev();
else if (env === 'production') FEBuildProd();
},
Controller: {
Base: require('./app/controller/base.js'),
},
Service: {
Base: require('./app/service/base.js'),
},
// 如有必要还可暴露扩展点、插件 API 等
};
这样业务项目就可以这样使用:
const { serverStart, frontendBuild, Controller } = require('@your-org/elpis-core');
serverStart({ /* 配置选项 */ });
frontendBuild('production');
4. 前端工程化集成
一个完整的 elpis 框架既包含后端服务,也包含前端构建流程。因此在核心包中还要处理前端部分:
-
在核心包内部维护默认的前端页面目录、入口文件(如
core/pages/entry.*.js等); -
业务项目可以在其
app/pages/**/entry.*.js下定义自己的页面入口; -
在打包阶段,扫描核心包目录和业务目录的入口文件,将它们合并成 webpack 的
entry配置; -
构建时引入 HtmlWebpackPlugin 等插件,将打包产物注入到模板中;
-
允许业务通过在根目录下提供
app/webpack.config.js来扩展 webpack 配置。核心包要用merge.smart(或其他合并策略)将默认配置和业务配置合并。 -
在 loader 里引入核心包内(或业务内)各种资源时,应优先使用
require.resolve(...)等方法,确保加载的是核心包内的模块而不是业务项目的同名模块。 -
在 webpack 的 alias 配置里,需要把核心包的页面路径(如
$elpisPages、$elpisCommon等)映射过去,同时给一些可扩展模块(如$businessDashboardRouterConfig、$businessComponentConfig等)留一个空白模块或 fallback,以便业务项目覆盖。 -
在构建流程里,协同业务项目自定义的 webpack 配置中的插件、loader、别名、路径等与核心包的默认配置融合。
-
对于资源(图片、CSS、Less、字体等)的规则也要统一处理,确保核心包和业务项目的资源能一起被处理。
5. 路由与扩展能力
业务项目通常需要在默认路由基础上做扩展或定制。为此:
-
核心包内部维护自己的路由(headerRoutes、siderRoutes、schemaRoutes 等);
-
业务项目可以提供如
app/pages/dashboard/business-router.js(或命名类似)来定义业务自己的 headerRoutes/siderRoutes; -
在启动阶段,核心包会读取业务的路由配置(若存在),然后把业务路由插入或合并到默认路由中;
-
对于路由以外的扩展点(如业务自定义组件、表单项、搜索栏项、schema 组件等),可以通过 alias 映射 + 业务提供的模块(如
$businessComponentConfig)来覆盖或扩展。
6. 打包 & 发布流程
当核心包搭建好了、API 定义好了、loader 机制实现完毕之后,就可以做发布准备:
-
版本管理
使用npm version patch/npm version minor/npm version major来更新版本号,自动修改package.json并打 git tag。
-
切换 npm registry / 登录
如果之前曾使用镜像(如淘宝镜像),需要切回官方 registry,如:npm config set registry https://registry.npmjs.org/然后执行
npm login -
发布命令
对于普通公开包:npm publish --access public如果有自定义 registry 或作用域包,可加
--registry等参数。
-
验证 & 安装测试
发布之后,在一个干净的业务项目里尝试安装:npm install @your-org/elpis-core然后编写最小示例代码验证
serverStart、frontendBuild、业务 controller/route 是否能正常加载运行。 -
弃用 / 删除策略
如果到了某个版本想废弃旧包,可以使用npm deprecate给包打弃用提示;而对于某个版本删除,需要注意 npm 对 unpublish 的限制(通常只能在发布后 72 小时内删除)等。
7. 遇到的挑战与思考
在这个拆分 + 发布过程中,会碰到一些比较棘手的问题,下面列出几条常见挑战与思路:
| 挑战 | 解决思路 / 注意点 |
|---|---|
| 如何保证业务代码的异步加载 / 热更新机制 | 在开发环境下,可以让核心包启动服务时启用 webpack dev middleware + hot middleware,业务项目同样参与热更新;业务部分也要让其入口符合核心包识别机制 |
| 模块版本冲突 / 重复依赖 | 在核心包 dependencies 中声明各类依赖,且在业务项目中尽量避免再重复引入同版本,以免版本冲突;通过 peerDependencies 或指导业务项目统一依赖版本 |
| 模板路径 / 静态资源路径解析问题 | 在 alias 和路径映射上要精心设计,使得核心包和业务项目之间模块导入路径不会冲突;模板填充、静态文件拷贝等也要兼顾两者的目录结构 |
| 可扩展性与可配置性平衡 | 核心包应内建合理默认值、插件能力和扩展点,但不要让扩展体系过于臃肿;业务项目要能按需渐进式覆盖默认能力 |
| 发布包体积 & 依赖控制 | files 字段、 .npmignore 等方式控制不要把不必要的测试、配置、文档、源码(如果不必暴露)都打包进去。 |
完整示例结构(目录示意)
下面是一个拆分完成后的示例目录结构示意:
elpis-core/
├── package.json
├── index.js // 对外暴露启动 / 构建 / 扩展接口
├── app/ // 核心包内置的 controller / service / middleware 等
│ ├── controller/
│ ├── service/
│ ├── middleware/
│ └── ...
├── loader/ // 各种 loader(configLoader、controllerLoader、routerLoader…)
├── pages/ // 核心默认的前端页面入口等
├── view/ // html / tpl 模板文件
├── webpack/ // 默认 webpack 配置文件(base / dev / prod)
└── utils/ // 工具 / 公共方法等
业务项目则目录可能如下:
my-app/
├── package.json
├── app/
│ ├── controller/
│ ├── service/
│ ├── middleware/
│ ├── pages/
│ │ ├── dashboard/
│ │ └── ... // 自定义页面入口等
│ ├── config/
│ │ └── config.default.js
│ └── webpack.config.js // 业务可选的 webpack 扩展配置
└── scripts / src / public / … // 其他业务项目目录
在业务项目入口里只需很少的启动逻辑:
const { serverStart, frontendBuild } = require('@your-org/elpis-core');
serverStart({
// 业务自定义配置覆盖核心默认值
});
frontendBuild('production');
核心包启动时会自动扫描 my-app/app 下面的 controller/service/router 等,并挂载到主应用中,从而整体运行。
总结与感悟
通过这次 elpis 拆分和 npm 发布的实践,我有以下几点体会:
-
架构先行思考很重要:在拆分之前,先把框架核心能力、扩展点、业务需求都画一个蓝图,有助于之后分层拆解。
-
加载器 + 扫描机制是关键:让核心包能够“透明”地识别业务项目结构,并把它们合并到运行时环境中,是整个拆分方案的中枢。
-
配置合并与约定优于配置:给出合理的默认配置,同时提供覆盖扩展方式,是兼顾灵活性与简洁性的良方。
-
对外接口设计要稳妥:暴露给业务项目的 API 要简洁、清晰、向下兼容,避免后续演进带来的大破坏。
-
发布流程 & 包管理也要谨慎:版本控制、包体积、依赖管理、弃用策略都需要提前设计好。
elpis-npm包拆分与发布实践
1315

被折叠的 条评论
为什么被折叠?



