elpis的npm抽离与发布

3 阅读4分钟

前言

话接上文,在上一个学习阶段中,elpis已经基本开发完成了,具备了动态生成页面和组件的能力,那么,在这一章节中,我们要做的就是把项目进行改造,并发布到npm上去,供大家进行使用

附上我的npm包:npm i @yxcheng/augustus

抽离过程

一:本地连接

在发布之前,我们需要先在本地进行调试,这个命令是npm link,所以我们需要在本地新建一个demo文件,然后通过npm link xxx(此处为npm用户的名称+包名) 进行连接

二:相关文件处理

以controller为例,当我们把elpis当做一个公共包来使用时,我们需要注意的是不仅要挂载业务下的controller到app,还需兼容自身的controller,所以对如下代码进行改造

module.exports = (app) => {
  const controller = {};
  // 读取augusts/controller/**/**.js目录下的文件
  const augustsBusinessControllPath = path.resolve(
    __dirname,
    `..${sep}..${sep}app${sep}controller`,
  );
  const augustsFileList = glob.sync(
    path.resolve(augustsBusinessControllPath, `.${sep}**${sep}**.js`),
  );
  augustsFileList.forEach((file) => {
    handleFile(file);
  });
  // 读取业务/controller/**/**.js目录下的文件
  const businessControllPath = path.resolve(
    app.businessPath,
    `.${sep}controller`,
  );
  const businessFileList = glob.sync(
    path.resolve(businessControllPath, `.${sep}**${sep}**.js`),
  );
  businessFileList.forEach((file) => {
    handleFile(file);
  });
  // 把内容加载到app.controller下
  function handleFile(file) {
    // 提取文件名
    let name = path.resolve(file);
    // 截取路径 app/controller/custom-module/custom-controller.js => custom-module/custom-controller
    name = name.substring(
      name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length,
      name.lastIndexOf("."),
    );
    // 把’-‘统一改为驼峰式
    name = name.replace(/[_-][a-z]/gi, (s) => s.substring(1).toUpperCase());
    //   挂在controller到内存app对象中
    let tempController = controller;
    const names = name.split(sep);
    for (let i = 0, len = names.length; i < len; i++) {
      if (i === len - 1) {
        const ControllerMoule = require(path.resolve(file))(app);
        tempController[names[i]] = new ControllerMoule();
      } else {
        if (!tempController[names[i]]) {
          tempController[names[i]] = {};
        }
        tempController = tempController[names[i]];
      }
    }
  }
  app.controller = controller;
};

对于自身和即将使用的业务文件进行同样的处理,包括extend.js,config.js,middleware.js,router-schema.js,router.js,service.js文件同样依次处理

webpack部分的处理

1.将 dev 和 prod 两个打包方式暴露出去,然后项目就可以通过该函数去执行相应的打包方式

const FEBuildDev = require("./app/webpack/dev.js");
const FEBuildProd = require("./app/webpack/prod.js");
module.exports = {
  /**
   * 服务端基础
   */
  Controller: {
    Base: require("./app/controller/base.js"),
  },
  Service: {
    Base: require("./app/service/base.js"),
  },
  /**
   * 编译构建前端工程
   *  @params env 环境变量 local/prod
   */
  frontendBuild(env) {
    if (env === "local") {
      FEBuildDev();
    } else if (env === "production") {
      FEBuildProd();
    }
  },
 
};

2.webpack.base的修改

resolve: {
      extensions: [".js", ".vue", ".less", ".css"],
      alias: (() => {
        const alaisMap = {};
        const blankModulePath = path.resolve(__dirname, "../libs/blank.js");
        // dashboard路由拓展配置
        const businessDashboardRouterConfig = path.resolve(
          process.cwd(),
          "./app/pages/dashboard/router.js",
        );
        alaisMap["$businessDashboardRouterConfig"] = fs.existsSync(
          businessDashboardRouterConfig,
        )
          ? businessDashboardRouterConfig
          : blankModulePath;
        // schema-view component 扩展配置
        const businessComponentConfig = path.resolve(
          process.cwd(),
          "./app/pages/dashboard/complex-view/schema-view/components/component-config.js",
        );
        alaisMap["$businessComponentConfig"] = fs.existsSync(
          businessComponentConfig,
        )
          ? businessComponentConfig
          : blankModulePath;
        // schema-form 扩展配置
        const businessFormItemConfig = path.resolve(
          process.cwd(),
          "./app/pages/widgets/schema-form/form-item-config.js",
        );
        alaisMap["$businessFormItemConfig"] = fs.existsSync(
          businessFormItemConfig,
        )
          ? businessFormItemConfig
          : blankModulePath;

        // schema-search-bar  扩展配置
        const businessSearchItemConfig = path.resolve(
          process.cwd(),
          "./app/pages/widgets/schema-search-bar/search-item-config.js",
        );
        alaisMap["$businessSearchItemConfig"] = fs.existsSync(
          businessSearchItemConfig,
        )
          ? businessSearchItemConfig
          : blankModulePath;

        return {
          vue: require.resolve("vue"),
          "@babel/runtime/regenerator":
            require.resolve("@babel/runtime/regenerator"),
          "@babel/runtime/helpers/asyncToGenerator":
            require.resolve("@babel/runtime/helpers/asyncToGenerator"),
          $elpisPages: path.resolve(__dirname, "../../pages"),
          $elpisCommon: path.resolve(__dirname, "../../pages/common"),
          $elpisCurl: path.resolve(__dirname, "../../pages/common/curl.js"),
          $elpisUtils: path.resolve(__dirname, "../../pages/common/utils.js"),

          $elpisWidgets: path.resolve(__dirname, "../../pages/widgets"),
          $elpisHeaderContainer: path.resolve(
            __dirname,
            "../../pages/widgets/header-container/header-container.vue",
          ),
          $elpisSiderContainer: path.resolve(
            __dirname,
            "../../pages/widgets/sider-container/sider-container.vue",
          ),
          $elpisSchemaTable: path.resolve(
            __dirname,
            "../../pages/widgets/schema-table/schema-table.vue",
          ),
          $elpisSchemaForm: path.resolve(
            __dirname,
            "../../pages/widgets/schema-form/schema-form.vue",
          ),
          $elpisSchemaSearchBar: path.resolve(
            __dirname,
            "../../pages/widgets/schema-search-bar/schema-search-bar.vue",
          ),
          $elpisStore: path.resolve(__dirname, "../../pages/store"),
          $elpisBoot: path.resolve(__dirname, "../../pages/boot.js"),
          ...alaisMap,
        };
      })(),
    },

配置公共组件部分

import createForm from "./create-form/create-form.vue";
import editForm from "./edit-form/edit-form.vue";
import detailPanel from "./detail-panel/detail-panel.vue";
// 业务拓展component配置
import BusinessComponentConfig from "$businessComponentConfig";
const componentConfig = {
  createForm: {
    component: createForm,
  },
  editForm: {
    component: editForm,
  },
  detailPanel: {
    component: detailPanel,
  },
};
export default {
  ...componentConfig,
  ...BusinessComponentConfig,
};

这里面改动的部分在于

  1. 路径别名修改,使用之前的路径别名会定位到我们的业务目录下,所以需要进行修改
  2. 由于我们的包中提供了自己的 loader ,所以需要使用 require.resolve('相关loader') 包裹起来
  3. 配置外部公共组件,由于我们需要提供给使用者一个扩展公共组件的方法,因此除了原先 elpis 自带的公共组件路径外,还需要配置一个能够读取项目(业务)公共组件路径的方法

npm包发布

  1. 注册npm账号
  2. 查看是否具有镜像源,如果存在则需要清空
  3. 登录npm并确认npm名
  4. 发布npm包
// 查看镜像源 
npm config get 
// 清空镜像源 
npm config set registry 
// 登录 npm 
npm login //会给邮箱发送验证邮件
// 确定当前登录的 npm 账号和package.json里面的name是否对应
npm whoami 
// 发布 npm 
npm publish --access public // 公有化提交,第一次提交需要进行公有化提交
npm publish

一开始npm publish --access public一直403提交不上去,查看是因为two-factor authentication

查了一下

自2025年底起,npm 为了安全,采取了两项关键措施:

  1. 废弃经典令牌:传统的 npm login 方式或 Classic Token 已无法满足发布要求。
  2. 强制2FA或等效方案:所有粒度令牌(Granular Token)默认需要2FA验证。然而,CLI工具只能接受来自验证器应用(如 Google Authenticator)的 TOTP 码,无法使用 Security Key 或恢复码。这导致了一个矛盾:即使您在网页端启用了2FA,如果只配置了 Security Key 而未启用 TOTP,CLI 发布依然会失败 解决方法如下,亲测有效 生成正确的令牌
  • 登录 npmjs.com,点击右上角头像进入 “Access Tokens”
  • 点击 “Generate New Token” ,对permission需要允许读写,然后日期尽量长一点。
  • 配置权限:为您要发布的包 @yxcheng/augustus 选择 “Read and Write”“Publish” 权限。
  • 核心步骤:务必勾选 “Bypass two-factor authentication” 选项。这是解决403错误的关键。
  • 生成并复制以 npm_ 开头的长字符串令牌(只显示一次,请妥善保存) 写入令牌 npm config set //registry.npmjs.org/:_authToken=你的token

执行完之后可以通过npm whoami验证是否生效,能输出用户名说明 OK,之后再npm publish --access public应该问题就没有了