Elpis系列之npm发布

42 阅读6分钟

前言

npm 是一个沉淀了 80% 中后台系统的重复劳动且提供 20% 拓展能力的一个框架。前面的章节已经介绍了核心框架elpis-core,前端工程化,页面搭建等相关内容。本文将介绍如何将 Elpis 基础框架功能抽离封装成 npm 包,方便在客制化项目中直接引用,实现业务代码与基础架构的分离。

elpis-core 抽离

入口改造

将相关方法在入口处暴露,提供给接入方,方便使用。此处暴露了以下几个方法:

  • serverStart: 启动 elpis;
  • frontedBuild: 入参env,根据环境变量走不同的 webpack 配置;不仅仅使用 elpis 中的 webpack 配置,同时也会结合接入方对应目录下的配置。后文会有说明;
  • ServiceController: 服务端基础方法;
const ElpisCore = require('./elpis-core')
// 引入前端工程化构建方法
const FEBuildDev = require('./app/webpack/dev')
const FEBuildProd = require('./app/webpack/prod')

module.exports = {
    /**
     * 启动 elpis
     * @param {Object} options - 项目配置,透传到ElpisCore
     */
    serverStart(options = {}) {
        const app = ElpisCore.start(options)
        return app
    },
    /**
     * 编译构建前端工程
     * @param {String} env 环境变量 dev/prod
     */
    frontedBuild(env) {
        if (env === 'dev') {
            FEBuildDev()
        } else if (env === 'prod') {
            FEBuildProd()
        }
    },
    /**
     * 服务端基础
     */
    Controller: {
        Base: require('./app/controller/base')
    },
    Service: {
        Base: require('./app/service/base')
    }
}

loader 改造

elpis-core 是一个约定大于配置的服务框架,在使用时,只需要在对应的目录下写下相对应的文件,elpis-core 会帮你配置好其他的细节,并将内容挂载到 app 实例下。为此,不仅要处理 elpis 本身的loader(常用__dirname标识文件位置),也要加载接入方业务的 loader(常用process.cwd()标识文件位置)。遂做了以下改造,以 controller 为例。

  // 读取 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);
  });

webpack 工程化抽离

别名配置

process.cwd() 改为 __dirname两者区别

使用require.resolve() 提升可移植性

在开发 elpis 时使用了第三方依赖。当讲 elpis 作为一个 npm 包发布时,不能要求接入方再去安装 elpis 需要的第三方依赖。因此改用require.resolve()解析 elpis 中使用到的第三方依赖的路径并使用他们。在碰到 can not resolve module 或 第三方那个依赖加载报错时,尤其要注意这点。

使用 webpack

如前文所述,会暴露出 elpis 本地开发和生产打包的配置,供接入方使用。同时支持结合使用接入方在约定目录下自定义的 webpack 配置

// 动态构造 elpis entry入口elpisPageEntries 和 elpisHtmlWebpackPluginList
const elpisPageEntries = {};
const elpisHtmlWebpackPluginList = [];
//  path.resolve 构建绝对路径,entryStr:/pages/**/entry.*.js。 *用于给glob做匹配
const elpisEntryStr = path.resolve(__dirname, "../../pages/**/entry.*.js");
// 获取elpis/app/pages 目录下所有入口文件(entry.**.js)
glob.sync(elpisEntryStr).forEach((file) => {
  handleFile(file, elpisPageEntries, elpisHtmlWebpackPluginList);
});

// 动态构造业务 business entry 入口 businessPageEntries 和 businessHtmlWebpackPluginList
const businessPageEntries = {};
const businessHtmlWebpackPluginList = [];
//  path.resolve 构建绝对路径,entryStr:/pages/**/entry.*.js。 *用于给glob做匹配
const businessEntryStr = path.resolve(
  process.cwd(),
  "./app/pages/**/entry.*.js"
);
// 获取elpis/app/pages 目录下所有入口文件(entry.**.js)
glob.sync(businessEntryStr).forEach((file) => {
  handleFile(file, businessPageEntries, businessHtmlWebpackPluginList);
});

// 加载业务webpack 配置
let businessWebpackConfig = {}
try {
    businessWebpackConfig = require(path.resolve(process.cwd(), './app/webpack.config.js'))
} catch (error) {
    console.error('加载业务webpack配置失败:', error)
}

// const elpisWebpackConfig =  ...

module.exports = merge.smart(elpisWebpackConfig, businessWebpackConfig)

依赖更改

除部分 eslint、git、单测相关的开发依赖外,其他的开发依赖 devDependencies 都转为生产依赖dependencies

terser错误

记录一下自己踩的坑:在使用 npm link 时能正常打包。但发布成 npm 包以后再执行 npm run build 就会报错 Error in xxx.js from terserxxx undefined in terser。这是因为低版本的 terser-webpack-plugin 和在压缩代码时对于 es6 语法支持不完全导致的。升级 terser-webpack-plugin 版本并开启 ecma 即可

optimization: {
    // 使用 TerserWebpackPlugin 的并发和缓存,提升压缩阶段的性能
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        parallel: true, // 利用多核CPU的优势濑加快压缩速度
        terserOptions: {
          ecma: 2020,
          compress: {
            drop_console: true, // 移除所有的console语句
          },
        },
      }),
      // 优化并压缩 css 资源
      new CssMinimizerPlugin(),
    ],
}

页面与组件的抽离与拓展

如图,elpis 支持全新页面,dashboard页面的拓展,也支持自定义组件(scheme-view 组件、form 组件、search-bar 组件)的拓展,只需在约定目录下创建文件即可。具体配置可点我查看。这儿再一次体现 elpis 的强大拓展性和约定大于配置的思想。

image.png

支持页面拓展

  1. webpack.base.js 中,定义了接入方业务路由的别名 $businessDashboardRouteConfig,指向path.resolve(process.cwd(), "./app/pages/dashboard/router.js")该目录, 同时为了防止未按规范建立目录结构而报错,做了相关错误处理。
// 业务 dashboard 路由扩展配置
const businessDashboardRouteConfig = path.resolve(
  process.cwd(), "./app/pages/dashboard/router.js"
);
aliasMap["$businessDashboardRouteConfig"] = fs.existsSync(businessDashboardRouteConfig)
  ? businessDashboardRouteConfig
  : blankModulePath;
  1. 在 elpis 中,将 routessiderRoutes 传给接入方
import businessDashboardRouteConfig from "$businessDashboardRouteConfig";
import boot from "$elpisPages/boot";
import dashboard from "./dashboard.vue";

const routes = [ ... ]
const siderRoutes = [ ... ]

if (
  businessDashboardRouteConfig &&
  typeof businessDashboardRouteConfig === "function"
) {
  businessDashboardRouteConfig({ routes, siderRoutes });
}

boot(dashboard, { routes });

  1. 在接入方业务路由中,接收routessiderRoutes 参数,并添加业务路由。
module.exports = ({ routes, siderRoutes }) => {
  // 头部路由
  routes.push({
    path: "/view/dashboard/todo",
    component: () => import("./todo/todo.vue"),
  });

  // 侧边栏路由
  siderRoutes.push({
    path: "todo",
    component: () => import("./todo/todo.vue"),
  });
};

这样在 elpis 中就能拿到接入方业务路由,也就支持了接入方拓展页面。

支持组件拓展

  1. 以 scheme-view 组件为例,与页面拓展类似, 在 webpack.base.js 中,定义了接入方业务路由的别名 $businessComponentConfig,指向path.resolve(process.cwd(), "./app/pages/dashboard/complex-view/schema-view/components/component-config.js")该目录, 同时为了防止未按规范建立目录结构而报错,做了相关错误处理。
  2. 在 elpis app/pages/dashboard/complex-view/schema-view/components/component-config.js 中引入 $businessComponentConfig,就可以拿到接入方自定义的业务组件
import createForm from "./create-form/create-form.vue";
import detailPanel from "./detail-panel/detail-panel.vue";
import editForm from "./edit-form/edit-form.vue";

// 业务拓展 components 配置
import BusinessComponentConfig from "$businessComponentConfig";

const ComponentConfig = {
  createForm: {
    component: createForm,
  },
  editForm: {
    component: editForm,
  },
  detailPanel: {
    component: detailPanel,
  },
};

export default {
  ...ComponentConfig,
  ...BusinessComponentConfig,
};

页面抽离

由于 elpis 是一个普适的框架,原在开发中留有相关业务,测试的页面如 project-list、model配置都应该抽离。elpis 中只保留基础代码。

npm 发布

npm 开发环境调试

以上这些步骤,是如何开发调试通过的呢?毕竟需要确保正确才能发布到npm,这就涉及到开发环境使用本地 npm 包:

  1. 创建一个工程当做需要使用 elpis 的接入方,如 elpis-demo;

  2. 在 elpis 目录下使用 npm link 创建 elpis 的全局软链;

  3. 在 elpis-demo 目录下使用 npm link elpis 进行连接,此时会在 elpis-demo 的 node_modules 目录下找到 elpis;

    • npm ls <package-name> : 验证链接是否成功
    • npm ls --link: 查看当前项目中通过 npm link 链接的包
    • 开发完成后,记得解除链接并清理缓存:npm unlink <package-name>npm cache clean --force

npm 发布步骤

  1. 首次发布需到 npm 官网注册账号;
  2. 将当前镜像源设置为官方镜像源 npm config set registry;不传参则默认官方镜像
  3. 登录 npm: npm login
  4. 查看 npm 账号是否正确 npm whoami
  5. 确认 package.json 中版本号正确,每次发布都需要使用新版本号;
  6. 发布 npm publish。首次发布,需要加上 --access public 参数表示包发布为公共包,允许所有人安装和使用;

npm 地址

www.npmjs.com/package/@hb…