Tencent / hel

A module federation SDK which is unrelated to tool chain for module consumer. 工具链无关的运行时模块联邦sdk.
https://tencent.github.io/hel/
Other
931 stars 79 forks source link

【doc】Hel-micro + npm私有仓库 + unpkg私有部署实现模块联邦的最佳实践 #64

Open fantasticsoul opened 1 year ago

fantasticsoul commented 1 year ago

声明:此文章来源于hel交流群的网友实践,仅供参考

Hel-micro + npm私有仓库 + unpkg私有部署实现模块联邦的最佳实践

所谓工欲善其事,必先利其器(搭建环境)

npm私有仓库

一.原理

我们平时使用npm publish进行发布时,上传的仓库默认地址是npm,通过Verdaccio工具在本地新建一个仓库地址,再把本地的默认上传仓库地址切换到本地仓库地址即可。当npm install时没有找到本地的仓库,则Verdaccio默认配置中会从npm中央仓库下载。

二.常用的仓库地址
四.准备环境

兵马未动,粮草先行,既然是搭建私有仓库应用,基础环境得备好。

五.使用verdaccio搭建私有npm服务

unpkg私有化部署

上一章节将npm搭建到服务器了,接下来就是要搭建unpkg cdn服务,并且将上一章节搭建的npm私有仓库连接到unpkg私服上 首先搭建unpkg私服

一.拉取unpkg源码

git clone https://github.com/mjackson/unpkg.git
# 安装依赖
$ npm i

在package.json的script添加start命令:

"scripts": {
    "build": "rollup -c",
    ...
    "watch": "rollup -c -w",
    "start":"set NODE_ENV=production&&node server.js"
  },

执行编译命令:

npm run build

命令运行完后会在根目录生成server.js文件; 启动服务:

npm run start

我们自己搭建的unpkg已经可以正常的使用了,但是目前我们私库的npm包还是不能访问,记下来就是添加私库支持了;

二.unpkg添加私库支持

根目录新建npmConfig.js来存放私库包的命名空间:

//存放私库包的命名空间
export const scopes = [
    '@cz','@syl'
];
/****
 * 私库地址,代理端口会解析url的端口号
 * const privateNpmRegistryURLArr = privateNpmRegistryURL.split(":");
 * const privateNpmPort = privateNpmRegistryURLArr[privateNpmRegistryURLArr.length - 1]
 * 拉取一些npm的包会返回302的情况,unpkg暂时没有处理,会不会和本地的npm源有关?
 ***/
export const privateNpmRegistryURL = 'http://10.250.4.121:8088';

//互联网npm地址
export const publicNpmRegistryURL = 'http://registry.npmjs.org';

export default scopes;

接下来就是修改修改modules/utils/npm.js文件了,思路大概如下:

import url from 'url';
import http from 'http';
import gunzip from 'gunzip-maybe';
import LRUCache from 'lru-cache';

import bufferStream from './bufferStream.js';

const npmRegistryURL =
  'http://10.250.4.121:8088' || 'https://registry.npmjs.org';

const oneMegabyte = 1024 * 1024;
const oneSecond = 1000;
const oneMinute = oneSecond * 60;

const cache = new LRUCache({
  max: oneMegabyte * 40,
  length: Buffer.byteLength,
  maxAge: oneSecond
});

const notFound = '';

function get(options) {
  return new Promise((accept, reject) => {
    http.get(options, accept).on('error', reject);
  });
}

function isScopedPackageName(packageName) {
  return packageName.startsWith('@');
}

function encodePackageName(packageName) {
  return isScopedPackageName(packageName)
    ? `@${encodeURIComponent(packageName.substring(1))}`
    : encodeURIComponent(packageName);
}

async function fetchPackageInfo(packageName, log) {
  const name = encodePackageName(packageName);
  const infoURL = `${npmRegistryURL}/${name}`;

  log.debug('Fetching package info for %s from %s', packageName, infoURL);

  const { hostname, pathname,port } = url.parse(infoURL);
  const options = {
    hostname: hostname,
    path: pathname,
    port:port,
    headers: {
      Accept: 'application/json'
    }
  };

  const res = await get(options);

  if (res.statusCode === 200) {
    return bufferStream(res).then(JSON.parse);
  }

  if (res.statusCode === 404) {
    return null;
  }

  const content = (await bufferStream(res)).toString('utf-8');

  log.error(
    'Error fetching info for %s (status: %s)',
    packageName,
    res.statusCode
  );
  log.error(content);

  return null;
}

async function fetchVersionsAndTags(packageName, log) {
  const info = await fetchPackageInfo(packageName, log);
  return info && info.versions
    ? { versions: Object.keys(info.versions), tags: info['dist-tags'] }
    : null;
}

/**
 * Returns an object of available { versions, tags }.
 * Uses a cache to avoid over-fetching from the registry.
 */
export async function getVersionsAndTags(packageName, log) {
  const cacheKey = `versions-${packageName}`;
  const cacheValue = cache.get(cacheKey);

  if (cacheValue != null) {
    return cacheValue === notFound ? null : JSON.parse(cacheValue);
  }

  const value = await fetchVersionsAndTags(packageName, log);

  if (value == null) {
    cache.set(cacheKey, notFound, 5 * oneMinute);
    return null;
  }

  cache.set(cacheKey, JSON.stringify(value), oneMinute);
  return value;
}

// All the keys that sometimes appear in package info
// docs that we don't need. There are probably more.
const packageConfigExcludeKeys = [
  'browserify',
  'bugs',
  'directories',
  'engines',
  'files',
  'homepage',
  'keywords',
  'maintainers',
  'scripts'
];

function cleanPackageConfig(config) {
  return Object.keys(config).reduce((memo, key) => {
    if (!key.startsWith('_') && !packageConfigExcludeKeys.includes(key)) {
      memo[key] = config[key];
    }

    return memo;
  }, {});
}

async function fetchPackageConfig(packageName, version, log) {
  const info = await fetchPackageInfo(packageName, log);
  return info && info.versions && version in info.versions
    ? cleanPackageConfig(info.versions[version])
    : null;
}

/**
 * Returns metadata about a package, mostly the same as package.json.
 * Uses a cache to avoid over-fetching from the registry.
 */
export async function getPackageConfig(packageName, version, log) {
  const cacheKey = `config-${packageName}-${version}`;
  const cacheValue = cache.get(cacheKey);

  if (cacheValue != null) {
    return cacheValue === notFound ? null : JSON.parse(cacheValue);
  }

  const value = await fetchPackageConfig(packageName, version, log);

  if (value == null) {
    cache.set(cacheKey, notFound, 5 * oneMinute);
    return null;
  }

  cache.set(cacheKey, JSON.stringify(value), oneMinute);
  return value;
}

/**
 * Returns a stream of the tarball'd contents of the given package.
 */
export async function getPackage(packageName, version, log) {
  const tarballName = isScopedPackageName(packageName)
    ? packageName.split('/')[1]
    : packageName;
  const tarballURL = `${npmRegistryURL}/${packageName}/-/${tarballName}-${version}.tgz`;

  log.debug('Fetching package for %s from %s', packageName, tarballURL);

  const { hostname, pathname,port } = url.parse(tarballURL);
  const options = {
    hostname: hostname,
    path: pathname,
    port:port
  };

  const res = await get(options);

  if (res.statusCode === 200) {
    const stream = res.pipe(gunzip());
    // stream.pause();
    return stream;
  }

  if (res.statusCode === 404) {
    return null;
  }

  const content = (await bufferStream(res)).toString('utf-8');

  log.error(
    'Error fetching tarball for %s@%s (status: %s)',
    packageName,
    version,
    res.statusCode
  );
  log.error(content);

  return null;
}

修改npm.js完毕之后,执行npm run build重新生成server.js文件,然后启动服务:npm run start; 现在私库和公网npm都可以正常预览了

Hel-micro

文档地址: https://tnfe.github.io/hel/ 具体不详细说明啦,请参照作者文档使用

接下来说一下Hel-micro + npm私服 + unpkg服务的一个落地实践

假设我有A、B两个业务系统,那么A与B既是模块的使用者又是模块的提供者,既是0又是1??

oh~有点复杂,我们先说0 1的情况吧,明白了0 1,1 0的相互转化也就为所欲为啦~

A系统 => 模块提供者 B系统 => 模块消费者

我们现在把作者提供的远程组件书写方法集成到了A系统,目前是直接放到了src下

远程组件的书写方式可参照上边的文档连接

A系统暴露的远程模块书写成功后,我们执行下如下命令

HEL_APP_HOME_PAGE=http://10.250.4.121:9999/note-comps@0.0.2/hel_dist npm run build

发布成功后,我们就可以在任意项目里面消费远程组件啦,包括在A项目

消费方式

假设我们要在B系统消费刚才A系统产生的模块,我们只需要修改一点点地方即可

;(async function() { // await preFetchLib('hel-tpl-remote-vue-comps');

// 自定义前缀 await preFetchLib('note-comps', { apiPrefix: 'http://10.250.4.121:9999' })

// 调试本地开发中的远程组件 // const enableCustom = !!window.location.port; // await preFetchLib('hel-tpl-remote-vue-comps', { // custom: { // host: 'http://localhost:7001', // 基于 web-dev-server 开发中生成产物联调 // // host: 'http://localhost:9001', // 基于 http-server 已构建好的产物联调 // enable: enableCustom, // }, // });

import('./loadApp') })().catch((err) => { console.error('loadApp err: ', err) })

> http://10.250.4.121:9999是我们搭建的unpkg私服的地址

- loadApp.js就是之前main.js里面的内容

import Vue from 'vue'

import App from './App' import store from './store' import router from './router' // import * as Sentry from '@sentry/vue' // import { BrowserTracing } from '@sentry/tracing' import i18n from './lang'

import WujieVue from 'wujie-vue2'

Vue.mixin(mixins) Vue.use(CzUI, { size: 'small', i18n: (key, value) => i18n.t(key, value) })

Vue.use(WujieVue)

// 预加载流程引擎和权限引擎 const { setupApp, preloadApp } = WujieVue

new Vue({ el: '#app', router, store, i18n, render: (h) => h(App) })

- 在组件里面使用

至此,A系统的模块更新后发布后,其他系统的这个模块都会自动更新,那么B系统也可以随意往外暴露各种远程模块给各个系统调用啦。

此时模块联邦就可以在不同系统中随意调度,但是还缺乏一个管控平台
目前我们是微模块 + 微前端配合食用的,具体食用方式,我们还会再出一篇文章详细介绍。