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 设计 - 简洁易用
相关推荐
明月_清风5 天前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈
狂师5 天前
AI 测试提效 | 告别手工写脚本,分享我的 Playwright + Skill 批量生成 UI 自动化脚本方案
人工智能·测试·全栈
用户938515635076 天前
从零构建《天龙八部》知识库:EPUB 加载→文本分割→向量嵌入→Milvus 存储→RAG 问答,一条链路打通
javascript·人工智能·全栈
何时梦醒6 天前
React + TypeScript 企业级开发实战:从零搭建到组件架构演进
前端·react.js·全栈
前端双越老师6 天前
Nest.js 和 Spring Boot 为何如此像?写给前端转 Java 的同学
java·spring boot·全栈
Staticy7 天前
Claude Code 接国产模型不能识图?一个 MCP 让纯文本模型也能看图
人工智能·ai编程·全栈
极简前端打杂工7 天前
从0到1搭建通用低代码平台(-)— 表单设计器
前端·全栈
swipe7 天前
11|(前端转全栈)购物车不能只存在前端:用户维度数据如何在后端落库
前端·后端·全栈
swipe7 天前
10|(前端转全栈)库存扣减为什么最容易出事故?SKU、并发与原子更新
前端·后端·全栈
_lucas7 天前
给知识库网站接入AI问答
前端·ai编程·全栈