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

869 阅读5分钟

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

npm私有仓库

一.原理

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

二.常用的仓库地址

npm : registry.npmjs.org/ cnpm : r.cnpmjs.org/ taobao: registry.npm.taobao.org/ 三.优势

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

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

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 http://localhost:4873 使用nrm切换到本地仓库 nrm use 使用nrm查看是否新增成功 nrm ls 注册verdaccio账号,一定要先保证切换到本地的源仓库的前提下,因为你注册的账号是保存在对应仓库源上的。 npm adduser // 输入账号和密码 上传仓库 // 登录 npm login // 发布 npm publish 访问http://ip:port 进行登录 unpkg私有化部署

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

一.拉取unpkg源码

git clone github.com/mjackson/un…

安装依赖

$ 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 = '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' || '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

文档地址: 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 HEL_APP_HOME_PAGE是Hel-micro的参数 http://10.250.4.121:9999是unpkg私服的地址 note-comps@0.0.2是包名称与版本号,都要跟package.json保持一直 打包完成后,执行发布命令 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) })

在组件里面使用

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

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