里程碑5 - 完成框架 npm 包抽象封装并发布

0 阅读3分钟

一、目标

将之前elpis框架开发的代码抽象为sdk,将代码进行整合,区分elpis框架和具体业务;最后发布到 npm 上,实现部署

二、代码整合

1. 整合loader加载

在之前完成的代码里面,loader解析的仅是elpis框架内的相关文件;现在对这些loader进行整合,保留之前对框架的解析的同时,添加对业务逻辑的处理,样例代码如下:


    // 获取 elpis框架的 app目录
    app.appBaseDir = path.resolve(__dirname, `..${sep}app`);

    // 读取elpis框架下 app/XXXXXX/**/**.js 下所有的文件
    const XXXXXXPath = path.resolve(app.appBaseDir, `.${sep}XXXXXX`);
    const fileList = glob.sync(
        path.resolve(XXXXXXPath, `.${sep}**${sep}**.js`)
    );
    fileList.forEach(file => handleFile(file));

    let businessControllerPath;
    let businessFileList;
    if (app.businessPath !== app.appBaseDir) {
        // 读取 业务根目录下 app/XXXXXX/**/**.js 下所有的文件
        businessXXXXXXPath = path.resolve(app.businessPath, `.${sep}XXXXXX`);
        businessFileList = glob.sync(path.resolve(businessXXXXXXPath, `.${sep}**${sep}**.js`));
        businessFileList.forEach(file => handleFile(file));
    }

2. DSL整合

将由DSL模板衍生出来的 model 和 projec 挪到业务项目的 /model/文件下;让业务项目自己完成具体配置,elpis框架中只保留模板解析相关代码。

3. 自定义页面扩展

之前在elpis框架中,我们将自定义页面的router定义为 todo,留给业务进行自定义开发,elpis框架中保留共同组件。

  • 自定义页面、侧边栏 router修改

    1. 将elpis框架中 todo 相关的目录和文件删除,
    2. 在业务项目中app/pages/dashboard/XXX 目录下完成XXX页面开发。
    3. 将自定义页面路由和自定义侧边栏路由配置到app/pages/dashboard/router.js 文件中
    4. 在elpis框架的webpack配置中,确保这个配置文件是存在的,并定义路径别名
    5. 在dashboard路由配置中引入业务项目导出的模块,完成路由整合。
  • 自定义动态组件扩展

    1. 在业务项目 app/pages/dashboard/complex-view/schema-view/components 目录下写组件
    2. 配置到 app/pages/dashboard/complex-view/schema-view/components/component-config.js
    3. 在elpis框架的webpack配置中,确保这个配置文件是存在的,并定义路径别名
    4. 在elpis框架的 component-config.js 文件中,引入配置,整合后导出;配置项重复时,允许业务项目配置覆盖elpis框架配置
  • 引用业务中的自定义FormItem

    1. 在业务项目 app/pages/widgets/schema-form/complex-view 目录下写控件
    2. 配置到 app/pages/widgets/schema-form/form-item-config.js
    3. 在elpis框架的webpack配置中,确保这个配置文件是存在的,并定义路径别名
    4. 在elpis框架的 form-item-config.js 文件中,引入配置,整合后导出;配置项重复时,允许业务项目配置覆盖elpis框架配置
  • 引用业务中的自定义SearchItem

    1. 在业务项目 app/pages/widgets/schema-search-bar/complex-view 目录下写控件
    2. 配置到 app/pages/widgets/schema-search-bar/search-item-config.js
    3. 在elpis框架的webpack配置中,确保这个配置文件是存在的,并定义路径别名
    4. 在elpis框架的 search-item-config.js 文件中,引入配置,整合后导出;配置项重复时,允许业务项目配置覆盖elpis框架配置

三、实现 npm 部署

  1. 提交前确认package.json中的 name 和 version,多次提交记得修改版本号
  2. 部署前使用命令 npm config get registry,确认指向 https://registry.npmjs.org/,如果指向别的地方,输入命令 npm config set registry 清空源
  3. 执行npm login进行登录,登陆后可以使用 npm whoami 查询
  4. 第一次提交可能会需要使用命令 npm publish --access public,告知npm我们的包为共有包,后续使用 npm publish 命令就行

四、整合过程中遇到的小问题

1. 修改config-loader的时候,不管我怎么切换环境验证,返回的始终是demo项目中default.cinfog配置的值

刚开始我demo项目的package.json中设置的启动命令是"dev": "set _ENV = 'local' && nodemon ./server.js"

后来发现按照windows cmd里set的语法:= 两面是不能有空格的、且变量值不需要引号;我配置的启动命令set _ENV = 'local'实际上相当于变量名: "_ENV ",变量值: " 'local'"

我将启动命令按照规则修改为"dev": "set _ENV=local && nodemon ./server.js",发现process.env._ENV可以成功获取的值,但是返回的还是default.cinfog中的值

打印出来之后发现,package.json中启动命令set _ENV=local,在windows解析set的时候,被解析成 _ENV="local ",后面有一个空格。。。

在判断环境的地方增加trim()之后可以正常取得环境值。

2. 在prod构建和启动的时候,浏览器中提示Uncaught Error: [HMR] Hot Module Replacement is disabled.GET http://127.0.0.1:9002/__webpack_hmr net::ERR_CONNECTION_REFUSED

这个很让人感到意外,因为我们webpack的配置是只有dev环境启动的时候才会热更新。

刚开始以为是在执行完build:dev后,没有把public删除导致的,后续删了之后重新执行build:prod后,启动prod环境发现还是提示这个错误。

根据浏览器控制台中错误提示信息,发现是在vandor包里面打入了hmr相关的内容;而浏览器发现我们的bundle文件中有webpack-hot-middleware,就会自动连接,但是我们的server没有启HMR,就会发生这个错误

经过查找发现是在webpack.dev.js的代码中向 entry 配置加入 hmr 的时候,修改了 baseConfig.entry,导致 HMR client 被打进 vendor,并污染了生产环境的构建

修改方案:配置中加入hmr的时候,不修改baseConfig.entry,而是将值给到新的devEntry,完成之后在webpackConfig中配置 entry: devEntry,代码参考:

  const devEntry = {};
  // 开发环境的 entry 配置需要加入 hmr
  Object.keys(baseConfig.entry).forEach(v => {
      // 第三方包不作为hmr入口
      if (v !== "vendor") {
          devEntry[v] = [
              baseConfig.entry[v],
              `${require.resolve('webpack-hot-middleware/client')}?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}`
          ];
      } else {
          devEntry[v] = baseConfig.entry[v];
      }
  });

3. 在elpis框架的alias中配置'vue': require.resolve("vue"),而不是在新的业务代码里面npm i vue

这让人很疑惑,为什么不是在业务代码里面npm i vue下载Vue资源,而是引用elpis框架中的Vue?难道不应该是elpis框架包中不包含vue,然后在新的业务代码里面npm i vue吗?

实际上这两种方法应该都是可以的,只是我们之前使用npm link进行整合和拆分,为了避免出现多个vue实例,才在elpis框架的alias中配置'vue': require.resolve("vue"),是为了强制所有地方只使用同一个vue。

不然的话,elpis自己依赖一个vue,业务项目又进行了npm i vue,最终会导致node_modules中的层级可能会是这样的:

node_modules
 ├ vue (业务项目安装)
 └ elpis
     └ node_modules
         └ vue (elpis 自己安装)

此时项目中会存在两个vue实例,这样会导致provide/inject失效vue.component注册组件找不到Vue.use(plugin)失效;造成这些问题的原理是:

项目里面存在两个vue构造函数实例时,业务代码的Vue1 !== 内部组件的Vue2

vue.component全局注册组件时挂在vue构造函数上,如果组件是在vue1全局注册Vue1.component(...),而组件是在vue2进行创建Vue2.extend(…),vue在渲染时检查 component instanceof Vue1结果是false,vue会认为组件不存在

provide/inject的实现依赖Vue prototype chain. Vue1.prototype !== Vue2.prototype,所以inject找不到provide

插件安装Vue.use(plugin)实际上是plugin.install(Vue),如果plugin install在Vue1,但是组件运行在Vue2,插件逻辑根本不会生效

后续修改完成后可以将elpis 改为只引用、不打包,由业务项目安装Vue;实现最终运行时全局只有一个Vue,下面是具体实现方式(验证中):

  1. package.json中下载的vue要放在 peerDependencies 里,而不是dependencies里;要注意的是要写清楚版本范围,如果 elpis用的是 vue 2,项目安装的是 vue 3,可能会直接炸掉。
    {
        "peerDependencies": {
            "vue": ^3.3.4
        }
    }
  1. 在elpis的webpack配置中添加 externals,含义是当代码里面写import Vue from "vue"时,不打包,而是从运行环境获取,具体配置为:

    // 简化版
    {
        externals: {
            vue: "vue"
        }
    }
    
    // 兼容不同环境版
    {
        externals: {
            vue: {
                commonjs: "vue",
                commonjs2: "vue",
                amd: "vue",
                root: "vue",
            }
        }
    }
  1. 在业务项目中进行 npm install vue,这样配置的话目录结构会变成:

    elpis-demo
        ├ node_modules
        │   ├ vue
        │   └ @togurodi/elpis