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 设计 - 简洁易用
相关推荐
HashTang1 天前
从 Next.js 完全迁移到 vinext 的实战踩坑指南
ai编程·全栈·next.js
牛奶2 天前
AI辅助开发实战:会问问题比会写代码更重要
人工智能·ai编程·全栈
牛奶2 天前
为什么2026年还要学全栈?
人工智能·ai编程·全栈
wing982 天前
通往“全干”之路一:前端部署
前端·vue.js·全栈
飞雪飘摇12 天前
Elpis 动态组件扩展设计:配置驱动的边界与突破
前端框架·全栈
全栈前端老曹13 天前
【Redis】Pipeline 与性能优化——批量命令处理、提升吞吐量、减少网络延迟
前端·网络·数据库·redis·缓存·性能优化·全栈
全栈前端老曹13 天前
【Redis】 监控与慢查询日志 —— slowlog、INFO 命令、RedisInsight 可视化监控
前端·数据库·redis·缓存·全栈·数据库监控·slowlog
全栈前端老曹14 天前
【Redis】发布订阅模型 —— Pub/Sub 原理、消息队列、聊天系统实战
前端·数据库·redis·设计模式·node.js·全栈·发布订阅模型
全栈前端老曹15 天前
【Redis】Redis 持久化机制 RDB 与 AOF
前端·javascript·数据库·redis·缓存·node.js·全栈