Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
fantasticsoul opened this issue Apr 11, 2023 · 0 comments
Labels
documentation Improvements or additions to documentation

Comments

@fantasticsoul
Copy link
Collaborator

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

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

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

npm私有仓库

一.原理

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

二.常用的仓库地址
三.优势
  • 私密性高,仅团队共享。
  • 安全性高,能够有效的防治恶意代码攻击。
  • 使用局域网,传输速度快。
四.准备环境

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

  • node 14.19
  • git
  • verdaccio
  • nrm(快速切换仓库源)
  • pm2(守护进程)
五.使用verdaccio搭建私有npm服务
  • 安装
npm install -g verdaccio
  • 运行。启动时间会很久,断开cmd会关掉服务,可以使用pm2守护进程即可。
// 访问http://localhost:4837
verdaccio
  • 配置config.yaml,使局域网下能共享访问,否则只能本机访问。
// 最后面添加以下配置
listen: 0.0.0.0:4873
  • 重启,必须重启电脑配置才能生效。
  • 重新运行
// 访问http://ip:port/
// 不要访问本地localhost下的
verdaccio
  • 使用nrm新建本地仓库
nrm add <registry> http://localhost:4873
  • 使用nrm切换到本地仓库
nrm use <registry>
  • 使用nrm查看是否新增成功
nrm ls
  • 注册verdaccio账号,一定要先保证切换到本地的源仓库的前提下,因为你注册的账号是保存在对应仓库源上的。
npm adduser
// 输入账号和密码
  • 上传仓库
// 登录
npm login
// 发布
npm publish

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文件了,思路大概如下:

  • 私库地址为http,需要修改https为http;
  • 设置我们私库的端口;
  • 根据npmConfig.js中的scopes去匹配unpkg请求的包,如果是私库的包,就走内网的npm源,如果没有匹配到,就走互联网npm地址;
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/[email protected]/hel_dist npm run build
npm publish

注意用nrm检查下是否已经切换到自己私有的npm源了

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

消费方式

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

  • 安装hel-micro
hel-micro
  • 安装A系统刚才提供出的模块
npm i note-comps
  • 改造main.js
import { preFetchLib } from 'hel-micro'

;(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)
})

  • 在组件里面使用
<template>
  <div class="user-info">
    <div>
      <RemoteComp name="我是子模块" msg="子模块随时更新、部署驱动开发" />
    </div>
  </div>
</template>

<script>
import comps from 'note-comps'
export default {
  name: 'UserInfo',
  components: {
    RemoteComp: comps.Card // hi, remote component
  }
}
</script>

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

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

@fantasticsoul fantasticsoul added the documentation Improvements or additions to documentation label Apr 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant