Elpis 框架 npm 包抽离思路

前言

最近完成了 elpis 的 npm 包抽离工作,说实话,把框架从业务代码里抽出来做成 npm 包,听起来好像挺简单的,但真正动手的时候才发现,路径问题、加载顺序、构建配置...每一个都是坑。

架构设计

抽离后的结构:

scss 复制代码
elpis (框架包)              elpis-demo (业务项目)
├── elpis-core/            ├── app/
├── app/                   │   ├── controller/
└── index.js               │   └── router/
                           └── server.js

框架提供基础能力,业务项目通过 npm 依赖使用。

核心难点

1. 路径解析问题

这是最大的坑。框架需要同时加载两个位置的文件:

  • 框架自身的文件(在 node_modules 中)
  • 业务项目的文件(在项目根目录)
javascript 复制代码
module.exports = {
  start(options = {}) {
    const app = new Koa();
    
    // 业务项目根目录
    app.baseDir = process.cwd();
    
    // 业务代码目录
    app.businessPath = path.resolve(app.baseDir, `./app`);
    
    return app;
  }
}

关键点:

  • 用 process.cwd() 定位业务项目
  • 用 __dirname 定位框架内部文件
  • 统一使用 path.sep 处理跨平台路径

2. 动态加载器的优先级

框架和业务都有 Controller、Service,如何处理?

采用"框架先行,业务覆盖"策略:

javascript 复制代码
module.exports = (app) => {
  const controller = {};

  // 1. 先加载框架的
  const elpisFileList = glob.sync(path.resolve(__dirname, `../../app/controller/**/*.js`));
  elpisFileList.forEach(file => handleFile(file));

  // 2. 再加载业务的(会覆盖同名)
  const businessFileList = glob.sync(path.resolve(app.businessPath, `./controller/**/*.js`));
  businessFileList.forEach(file => handleFile(file));

  app.controller = controller;
};

3. Webpack 构建配置

需要同时扫描框架和业务的入口文件:

javascript 复制代码
// 扫描框架入口
const elpisEntryList = glob.sync(
  path.resolve(__dirname, '../../pages/**/entry.*.js')
);

// 扫描业务入口
const businessEntryList = glob.sync(
  path.resolve(process.cwd(), './app/pages/**/entry.*.js')
);

module.exports = {
  entry: Object.assign({}, elpisPageEntries, businessPageEntries),
  
  module: {
    rules: [{
      test: /\.js$/,
      include: [
        path.resolve(__dirname, '../../pages'),
        path.resolve(process.cwd(), './app/pages'),
      ],
      use: { loader: require.resolve('babel-loader') }
    }]
  },
  
  resolve: {
    alias: {
      '$elpisWidgets': path.resolve(__dirname, '../../pages/widgets'),
      // 业务扩展配置(不存在则指向空模块)
      '$businessConfig': fs.existsSync(businessConfigPath)
        ? businessConfigPath
        : path.resolve(__dirname, '../libs/blank.js')
    }
  }
};

关键点:

  • 使用 require.resolve 确保 loader 路径正确
  • 通过 alias 提供统一引用路径
  • 业务扩展不存在时指向空模块

4. 中间件注册顺序

javascript 复制代码
// 先注册框架中间件
const elpisMiddleware = require(path.resolve(__dirname, `../app/middleware.js`));
elpisMiddleware(app);

// 再注册业务中间件(可选)
try {
  require(`${app.businessPath}/middleware.js`)(app);
} catch (error) {
  console.log('no global businessMiddleware file');
}

5. 对外 API 设计

保持简洁:

javascript 复制代码
// elpis/index.js
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);
  }
};

业务项目使用:

javascript 复制代码
// server.js
const { serverStart } = require('@gordonlzg/elpis');
const app = serverStart({ name: 'ElpisDemo' });

// build.js
const { frontEndBuild } = require('@gordonlzg/elpis');
frontEndBuild(process.env._ENV);

// controller/business.js
const { Controller } = require('@gordonlzg/elpis');
module.exports = (app) => {
  return class BusinessController extends Controller.Base {
    async list(ctx) { /* ... */ }
  }
}

踩坑点

1. require.resolve 的妙用

javascript 复制代码
// ❌ 错误:业务项目找不到
use: { loader: 'vue-loader' }

// ✅ 正确:从框架包中解析
use: { loader: require.resolve('vue-loader') }

2. glob 路径处理

javascript 复制代码
// ❌ 错误:Windows 上会失败
glob.sync('./app/**/*.js')

// ✅ 正确:使用 path.resolve
glob.sync(path.resolve(app.businessPath, `./**/*.js`))

3. 文件不存在的处理

javascript 复制代码
// ❌ 直接 require 会报错
const config = require(`${app.businessPath}/config.js`);

// ✅ 先判断是否存在
if (fs.existsSync(configPath)) {
  const config = require(configPath);
}

4. Webpack alias 空模块技巧

javascript 复制代码
alias: {
  '$businessConfig': fs.existsSync(businessConfigPath)
    ? businessConfigPath
    : path.resolve(__dirname, '../libs/blank.js')  // 空模块
}

// blank.js
module.exports = {};

总结

抽离 npm 包的核心难点:

  1. 路径解析 - 正确处理框架和业务的路径关系
  2. 加载顺序 - 框架先行,业务覆盖
  3. 构建配置 - Webpack 的路径和 loader 处理
  4. API 设计 - 简洁易用
相关推荐
前端双越老师20 小时前
程序员 1v1 访谈系列第七期:失业半年所想到的
agent·ai编程·全栈
梅雅达编程笔记2 天前
Day 17 | uni-app 小程序:前端从零搭建
微信小程序·typescript·uni-app·vue3·pinia·前端开发·全栈
百慕大三角4 天前
从 CLAUDECODE 到 AI_AGENT:前端与 CLI 工具如何感知 AI 编码代理?
人工智能·后端·全栈
SL_staff4 天前
金融风控规则引擎的深度实践:JVS-Rules 四大原生能力解析
spring cloud·产品·全栈
SL_staff4 天前
计划系统落地难?从开发者视角重构执行入口体验
java·开源·全栈
前端双越老师4 天前
程序员 1v1 访谈系列第六期:国企也降薪裁员
agent·ai编程·全栈
狂师4 天前
夯爆了!又一款开源企业级一站式AI智能测试平台!(附平台架构、技术选型)
人工智能·测试·全栈
SL_staff5 天前
从‘改一行代码等三天’到‘改完即生效’:实施工程师的逻辑引擎避坑手记
java·产品·全栈
SL_staff5 天前
企业知识平台细粒度权限设计实践:从成员组、文档库到操作级的三层防线
java·开源·全栈
SL_staff6 天前
私有化低代码运维三重雷区:JVS开源栈中的Kafka/ES/RabbitMQ治理实践
java·低代码·全栈