抽离 elpis npm 包

npm 包抽离

经过前面步骤的开发后,到了 npm 包抽离阶段,首先的工作是把原本在 elpis 中便于功能验证和调试所留下的业务代码。

启动文件

js 复制代码
// 引入elpis-core
const ElpisCore = require('./elpis-core');

// 启动项目
ElpisCore.start({
    name:'elpis',
    homePage: '/view/project-list'
});

这是原本在 elpis 中我们的启动文件,只需要考虑运行我们的 elpis 时引入 elpis-core,但是既然作为 npm 包提供能力出去,显然是不够的,需要把启动项目作为方法暴露出去给用户在自己的工程中调用,所以需要改造一下启动文件。

js 复制代码
// index.js
module.exports = {
    /**
     * 启动 elpis
     * @param {*} options 项目配置,透传到 elpis-core
     * @returns  app 实例
     */
    serverStart(options = {}) {
        const app = ElpisCore.start(options);
        return app;
    }
}

经过改造之后,用户在自己的项目启动文件中,引入 serverStart 之后并调用,就可以启动 elpis。

服务端

服务端需要处理的首先就是 elpis-core 中的逻辑,前面讲到 elpis-core 中通过各种 loader 扫描目录和加载对应目录中的文件,把 controllerserviceextendrouter 等等挂载到 app 实例中。那么现在的问题是,之前 elpis 作为一个项目运行,我们通过以下方式读取文件,而定义好的业务路径 app.businessPath 也就是 app.businessPath = path.resolve(process.cwd(), .${sep}app); 只向的是运行目录下的 app 目录,那么一来当用户在自己的工程目录下并且使用我们的 npm 包时,指向的就是工程目录下的 app 目录,原本 elpis 中我们内置的一些功能就没有被加载和挂在到 app 上。

js 复制代码
// 读取app/controller/**/**.js 下所有的文件
const controllerPath = path.resolve(app.businessPath, `.${sep}controller`);
const fileList = glob.sync(path.resolve(controllerPath, `.${sep}**${sep}**.js`));

所以首先需要对我们的 loader 都进行改造,思路就是分两部分进行加载,一是 elpis 自带的文件,二是业务下的文件。以 controllerloader 为例,有关 elpis 内置能力的文件目录指向要用 __dirname 替换掉原本的 process.cwd() 以及路径做一些相应的更改使得内置的文件能够正确加载进来,以及把处理文件挂载部份的代码逻辑抽成函数,方便这两部份文件处理。

js 复制代码
// 读取 elpis/app/controller/**/**.js 下所有的文件
const elpisControllerPath = path.resolve(__dirname, `..${sep}..${sep}app${sep}controller`);
const elpisFileList = glob.sync(path.resolve(elpisControllerPath, `.${sep}**${sep}**.js`));
    elpisFileList.forEach(file => {
        handleFile(file);
    })

// 读取 业务/app/controller/**/**.js 下所有的文件
const businessControllerPath = path.resolve(app.businessPath, `.${sep}controller`);
const businessFileList = glob.sync(path.resolve(businessControllerPath, `.${sep}**${sep}**.js`));
businessFileList.forEach(file => {
    handleFile(file);
})

其余几个 loader 的处理思路基本和 controller loader 的处理思路大差不差,都需要对两部分文件的路径进行处理和分别加载,需要注意的是 elpis-core 的启动文件 index.js 中也需要对全局中间件进行处理。

处理完这些还没结束,像 elpis 中提供的 Controller、Service 的基类,我们也得暴露出去提供给用户使用,所以入口文件中还需要把这部分也给加进去。

js 复制代码
//index.js
module.exports = {
    /**
     * 服务端基础
     */
    Controller: {
        Base: require('./app/controller/base.js')
    },
    Service: {
        Base: require('./app/service/base.js')
    },
    ....
}    

自此服务端方面的抽离、改造也基本结束

前端工程化

webpack 配置方面需要改造的地方就不少了,首先要做的是把 elpis 中的 webpack 启动文件dev.js 和 prod.js 改造成方法暴露出去,然后在 elpis 的启动文件中把这两个方法暴露出去。

js 复制代码
// index.js
// 引入前端工程化构建方法
const FEBuildDev = require('./app/webpack/dev.js');
const FEBuildProd = require('./app/webpack/prod.js');
module.exports = {
    ...
    /**
     * 编译构建前端工程化
     * @param {*} env 环境变量 dev/prod 
     */
    frontendBuild(env) {
        if(env === 'local') {
            FEBuildDev();
        } else if(env === 'production') {
            FEBuildProd();
        }
    },
}

这么一来在用户侧则可以自定义自己的工程化启动文件中,通过不同环境启动对应的 elpis 工程化。

js 复制代码
// build.js
const {
    frontendBuild
} = require('@woon_san/elpis');

// 编译构建前端产物
frontendBuild(process.env.NODE_ENV)

接下来需要做的是把用户工程里的潜在 webpack 配置拿过来跟我们的配置合并。

js 复制代码
// 加载 业务 webpack 配置文件
let businessWebpackConfig = {};
try{
    businessWebpackConfig = require(`${process.cwd()}/app/webpack.config.js`);
} catch(e) {
    console.error('Failed to load business webpack config:', e);
}
module.exports = merge.smart({ elpis webpack 配置...}, businessWebpackConfig)

接下来需要处理的是入口配置,因为是多页面应用,所以在原有配置中我们已经有动态构造 pageEntries 的逻辑了,只不过现在不仅需要构造 elpis 中的一些页面,还需要把用户工程目录中的页面入口也动态构造出来,思路跟前面也是差不多的,分 elpis 和 business 两部分构造,分别获取对应目录下的所有入口文件 entry.x.js 结合 HtmlWebpackPlugin 处理,最终得到以下配置。

js 复制代码
// 入口配置
entry: Object.assign({}, elpisPageEntries, businessPageEntries),
...
// 构造最终渲染的页面模板
...elpisHtmlWebpackPluginList,
...businessHtmlWebpackPluginList

处理完入口配置,在用户侧启动工程化时会遇到卡点,会报一堆类似以下的错误

js 复制代码
Cannot find package '@babel/plugin-transform-runtime' imported from /Users/woonsan/development/elpis-demo/babel-virtual-resolve-base.js

原因则是我们的 webpack 中用到很多 loader 比如 babel-loaderurl-loader 等等,默认使用构建时的 cwd 也就是我们的用户工程目录,但是用户工程中是大概率没有安装这些包的,所以我们需要把用到 loader 的地方改造成 require.resolve('xxx-loader') 固定从 Elpis 加载。

接下来则是把一些公用的组件、工具函数、状态管理等等加上 elpis 前缀以 alias 别名的方式暴露出去。在处理这些别名的过程中,遇到相对卡住的地方,像用户自定义页面的话也许会定义一个 router.js 或者 route.js 之类的路由拓展配置,我们需要把这部份路由配置给整合到页面路由中,如果我们只是生硬的定义一个别名并且从用户侧去取,webpack 在静态构建时取不到正确文件时候则会报错。

js 复制代码
alias: {
    ...
    // 有可能会报错
    $businessDashboardRouterConfig: path.resolve(process.cwd(), './app/pages/dashboard/router.js')
}

因此 alias 这部分也需要改造一下, 在 webpack 配置目录下的 libs 目录中建一个空白导出的文件, 通过先判断路由文件是否存在,不存在则以这个到处空白对象的文件替换,保证构建不报错。

js 复制代码
alias: (() => {
  const aliasMap = {};
  const blankModulePath = path.resolve(__dirname, '../libs/blank.js');
  // dashaboard 路由拓展配置
  const businessDashboardRouterConfigPath = path.resolve(process.cwd(), './app/pages/dashboard/router.js');
  aliasMap['$businessDashboardRouterConfig'] = fs.existsSync(businessDashboardRouterConfigPath) ? businessDashboardRouterConfigPath : blankModulePath;
  return {
      ...,
      ...aliasMap
  }  
})()

同理我们还需要处理 schema-view component 组件拓展配置、widgets schema-form form-item 组件拓展配置、wigets schema-search-bar search-item 组件拓展配置。这么一来工程化这部分的改造也基本完成。

npm 发布

在发布之前,还需要把开发依赖 devDependencies 中的部分依赖迁移到 dependencies 中去,只留下部分必要的依赖。 npm 发布的流程就不再赘述了,需要注意的是写好详细的文档,指引使用这个包的用户如何使用。

相关推荐
Setsuna_F_Seiei3 小时前
前端转型 Agent 开发 04 之 MCP 与 Skill(赋予 Agent 更广工作能力)
前端·agent
刘发财4 小时前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
郑州光合科技余经理5 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
梦想平凡5 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
yume_sibai6 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
计算机魔术师8 小时前
特朗普上台打给黄仁勋:AI末日论是骗局,我们绝不让它发生
前端
troy1288 小时前
Python 基础语法(八):Web 后端开发、数据分析与可视化、网络爬虫、人工智能 / 大模型应用
前端·python·数据分析
计算机魔术师8 小时前
CEO说要慢下来,黑客说别做梦了——同一篇论文,两种命运
前端
kyriewen9 小时前
我花3天抓了一个幽灵bug,凶手藏在第4层
前端·javascript·程序员
IT_陈寒9 小时前
Java里用Stream.parallel()翻车实录,这性能还不如单线程
前端·人工智能·后端