Cnpm,官方解释为Company npm。html
因为团队需求,如今须要搭建一个npm私服,用来更方便地管理团队的组件库,而且更快速更稳定地提供服务,我踏上了搭建npm私服的道路。node
git clone https://github.com/cnpm/cnpmjs.org.git
mysql
下载完代码后,我们先来大概瞄一眼项目目录react
+-- bin/ ---一些命令脚本
| --- nodejsctl ---npm start启动的脚本
| --- ...
+-- common/ ---公共目录,存放日志配置、邮件配置等
+-- config/
| --- index.js ---主要配置文件
+-- controllers/
| --- registry/ ---7001端口的controller层
| --- web/ ---7002端口的controller层
| --- sync_module_worker.js ---sync的主进程文件
| --- ...
+-- docs/
| --- db.sql ---数据库建表sql
| --- ...
+-- lib/
+-- middleware/
+-- models/ ---数据库操做目录
+-- public/
+-- routes/
| --- registry.js ---7001端口的路由文件
| --- web.js ---7002端口的路由文件
| --- ...
+-- servers/
| --- registry.js ---7001端口的服务器入口文件
| --- web.js ---7002端口的服务器入口文件
| --- ...
+-- services/
+-- sync/
| --- sync_all.js ---sync模式选择all时执行的文件
| --- sync_exist.js ---sync模式选择exist时执行的文件
| --- ...
+-- test/
+-- tools/
+-- view/
--- dispatch.js ---启动npm服务的主要文件,bin/nodejsctl中执行的就是这个文件
--- package.json
复制代码
咱们能够发现,cnpm使用的是koa框架,结构是经典的route->controller->services->modelgit
同步模块的具体流程是在controllers/sync_module_worker.js文件中的github
1. 根据设置的sync模式,从上游源中下载模块到一个临时路径/root/.cnpmjs.org/downloads/xxxxx.tgz
2. 调用nfs.upload方法将临时路径存储的tgz上传到指定存储位置
3. 不管是否上传成功,都删除刚刚下载的临时文件
复制代码
看到这里,喜欢思考的同窗或许会说了,我到底应该怎么搭建本身的npm私服?你说了半天,我仍是啥都不知道,好比吧:web
var config = {
version: version,
dataDir: dataDir,
/**
* Cluster mode
*/
enableCluster: true,
numCPUs: os.cpus().length,
/*
* server configure
*/
registryPort: 7001,
webPort: 7002,
bindingHost: '0.0.0.0', // only binding on 127.0.0.1 for local access
// debug mode
// if in debug mode, some middleware like limit wont load
// logger module will print to stdout
debug: process.env.NODE_ENV === 'development',
// page mode, enable on development env
pagemock: process.env.NODE_ENV === 'development',
// session secret
sessionSecret: 'cnpmjs.org test session secret',
// max request json body size
jsonLimit: '10mb',
// log dir name
logdir: path.join(dataDir, 'logs'),
// update file template dir
uploadDir: path.join(dataDir, 'downloads'),
// web page viewCache
viewCache: false,
// config for koa-limit middleware
// for limit download rates
limit: {
enable: false,
token: 'koa-limit:download',
limit: 1000,
interval: 1000 * 60 * 60 * 24,
whiteList: [],
blackList: [],
message: 'request frequency limited, any question, please contact fengmk2@gmail.com',
},
enableCompress: true, // enable gzip response or not
// default system admins
admins: {
// name: email
sunxiuguo: 'sunxiuguo@my.com',
},
// email notification for errors
// check https://github.com/andris9/Nodemailer for more informations
mail: {
enable: false,
appname: 'cnpmjs.org',
from: 'cnpmjs.org mail sender <adderss@gmail.com>',
service: 'gmail',
auth: {
user: 'address@gmail.com',
pass: 'your password'
}
},
logoURL: 'https://os.alipayobjects.com/rmsportal/oygxuIUkkrRccUz.jpg', // cnpm logo image url
adBanner: '',
customReadmeFile: '', // you can use your custom readme file instead the cnpm one
customFooter: '', // you can add copyright and site total script html here
npmClientName: 'cnpm', // use `${name} install package`
packagePageContributorSearch: true, // package page contributor link to search, default is true
// max handle number of package.json `dependencies` property
maxDependencies: 200,
// backup filepath prefix
backupFilePrefix: '/cnpm/backup/',
/**
* database config
*/
database: {
db: '******', // 库名
username: '*********', // 数据库用户名
password: '************', // 数据库密码
// the sql dialect of the database
// - currently supported: 'mysql', 'sqlite', 'postgres', 'mariadb'
dialect: 'mysql',
// the Docker container network hostname defined at docker-compose.yml
host: '**************', // 数据库域名
// custom port; default: 3306
port: 3318, // 数据库端口号
// use pooling in order to reduce db connection overload and to increase speed
// currently only for mysql and postgresql (since v1.5.0)
pool: {
maxConnections: 10,
minConnections: 0,
maxIdleTime: 30000
},
dialectOptions: {
// if your server run on full cpu load, please set trace to false
trace: true,
},
// the storage engine for 'sqlite'
// default store into ~/.cnpmjs.org/data.sqlite
storage: path.join(dataDir, 'data.sqlite'),
logging: !!process.env.SQL_DEBUG,
},
// package tarball store in local filesystem by default
nfs: aws.create({
accessKeyId: '*************', // s3 accessKeyId
secretAccessKey: '****************', // s3 secretAccessKey
// change to your endpoint
endpoint: '*****************', // https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html
bucket: 'npm-online', // s3 bucket名称
signatureVersion: 'v4', // s3 api版本
mode: 'private', // public: 经过url下载tar包; private: 经过key下载tar包
}),
// if set true, will 302 redirect to `nfs.url(dist.key)`
downloadRedirectToNFS: false,
// registry url name
registryHost: 'registry.npm.my.com',
/**
* registry mode config
*/
// enable private mode or not
// private mode: only admins can publish, other users just can sync package from source npm
// public mode: all users can publish
enablePrivate: false,
// registry scopes, if dont set, means do not support scopes
scopes: [ '@cnpm', '@sunxiuguo', '@companyName' ],
// some registry already have some private packages in global scope
// but we want to treat them as scoped private packages,
// so you can use this white list.
privatePackages: [],
/**
* sync configs
*/
// the official npm registry
// cnpm wont directly sync from this one
// but sometimes will request it for some package infomations
// please dont change it if not necessary
officialNpmRegistry: 'https://registry.npmjs.com',
officialNpmReplicate: 'https://replicate.npmjs.com',
// sync source, upstream registry
// If you want to directly sync from official npm registry
// please drop them an email first
sourceNpmRegistry: 'https://registry.npm.taobao.org',
sourceNpmWeb: 'https://npm.taobao.org',
// upstream registry is base on cnpm/cnpmjs.org or not
// if your upstream is official npm registry, please turn it off
sourceNpmRegistryIsCNpm: true,
// if install return 404, try to sync from source registry
syncByInstall: true,
// sync mode select
// none: do not sync any module, proxy all public modules from sourceNpmRegistry
// exist: only sync exist modules
// all: sync all modules
syncModel: 'exist', // 'none', 'all', 'exist'
syncConcurrency: 1,
// sync interval, default is 10 minutes
syncInterval: '10m',
// sync polular modules, default to false
// because cnpm can not auto sync tag change for now
// so we want to sync popular modules to ensure their tags
syncPopular: false,
syncPopularInterval: '1h',
// top 100
topPopular: 100,
// sync devDependencies or not, default is false
syncDevDependencies: false,
// try to remove all deleted versions from original registry
syncDeletedVersions: true,
// changes streaming sync
syncChangesStream: false,
handleSyncRegistry: 'http://127.0.0.1:7001',
// default badge subject
badgeSubject: 'cnpm',
// defautl use https://badgen.net/
badgeService: {
url: function(subject, status, options) {
options = options || {};
let url = `https://badgen.net/badge/${utility.encodeURIComponent(subject)}/${utility.encodeURIComponent(status)}`;
if (options.color) {
url += `/${utility.encodeURIComponent(options.color)}`;
}
if (options.icon) {
url += `?icon=${utility.encodeURIComponent(options.icon)}`;
}
return url;
},
},
packagephobiaURL: 'https://packagephobia.now.sh',
packagephobiaSupportPrivatePackage: false,
// custom user service, @see https://github.com/cnpm/cnpmjs.org/wiki/Use-Your-Own-User-Authorization
// when you not intend to ingegrate with your company user system, then use null, it would
// use the default cnpm user system
userService: null,
// always-auth https://docs.npmjs.com/misc/config#always-auth
// Force npm to always require authentication when accessing the registry, even for GET requests.
alwaysAuth: false,
// if you are behind firewall, need to request through http proxy, please set this
// e.g.: `httpProxy: 'http://proxy.mycompany.com:8080'`
// httpProxy: 'http://gfw.guazi-corp.com',
httpProxy: null,
// snyk.io root url
snykUrl: 'https://snyk.io',
// https://github.com/cnpm/cnpmjs.org/issues/1149
// if enable this option, must create module_abbreviated and package_readme table in database
enableAbbreviatedMetadata: true,
// global hook function: function* (envelope) {}
// envelope format please see https://github.com/npm/registry/blob/master/docs/hooks/hooks-payload.md#payload
globalHook: null,
opensearch: {
host: '',
},
};
复制代码
是否是成功打开文件了~恭喜你!你成功的迈出了第二步!
什么?你问第一步是什么?第一步是clone代码啊sql
syncModel属性控制sync模式,分为none,exist,all三种状况。docker
上游源就是你同步包的地址,好比你的上游源是淘宝源,那么你的npm私服就会从淘宝源进行包的同步。shell
uploadDir属性设置同步的模块存放的临时路径,默认为path.join(dataDir, 'downloads'),即root/.cnpmjs.org/downloads
nfs属性控制包存储,包括上传,下载等等。nfs的意思是network file system
registryPort属性默认为7001,webPort属性默认为7002.
registry服务主要是用来提供给用户源相关操做,好比设置npm源 web服务主要是提供给用户的一个图形化管理界面,好比在界面上查询某个模块
由于咱们的npm私服是放在docker里,包文件不可能使用fs-cnpm存储在docker里,因此咱们接入了amazon s3的对象存储服务。
官方提供了接入npm的协议NFS-Guide
Can download the uploaded file through http request. like qn-cnpm.
uploadBuffer: use options.key to customize the filename, then callback {url: 'http://test.com/xxx.tgz'}.
url: accept a key and respond download url.
remove: remove file by key
Can not download by http request. like sfs-client or oss-cnpm.
uploadBuffer: upload the file, and must callback {key: 'xxx'}, so cnpmjs.org can record the key, and use this key to download or remove.
download: need provide download api to download the file by key.
createDownloadStream: streaming download file by key
remove: remove file by key
复制代码
若是存储系统支持经过http请求下载包文件,就提供uploadBuffer,url,remove方法 若是存储系统不支持经过http请求下载包文件,就须要提供uploadBuffer,download,createDownloadStream,remove方法。
而且全部方法都须要是async的,或者是generatord的。
由于咱们使用的bucket,首先要提供一个create的方法来实例化一个s3对象。
exports.create = function (options) {
return new AwsWrapper(options);
};
function AwsWrapper(options) {
this.client = new S3(options);
this.mode = options.mode;
this.bucket = options.bucket;
var params = {
Bucket: options.bucket,
CreateBucketConfiguration: {
LocationConstraint: ":npm"//桶所在服务区
}
};
this.client.createBucket(params, function (err, data) {
if (err) {
// an error occurred
logger.syncInfo(err);
} else {
// successful response
console.log(data.Location);
}
});
}
复制代码
而后按照协议提供对应的方法
调用路径在controllers/registry/package/save.js,当publish包时会进入这个方法,入参为fileBuffer和options;
这个方法很简单,只需调用对应存储系统提供的api,把buffer上传便可。
const key = trimKey(options.key);
logger.syncInfo(`enter aws->uploadBuffer key=${key}`);
let result = {
key,
};
let uploadParams = {
Bucket: this.bucket,
Key: key,
Body: fileBuffer
};
this.client.upload (uploadParams, function (err, data) {
if (err) {
logger.syncInfo(err);
}
});
复制代码
调用路径在controllers/sync_module_worker.js,当从上游同步包的时候会进入这个方法,入参为filePath和options
upload和uploadBuffer不一样的是,upload是读取传入的filePath的文件做为body上传,uploadBuffer是直接把传入的buffer对象做为body上传。
const key = trimKey(options.key);
logger.syncInfo(`进入aws->upload key=${key} filePath=${filePath}`);
let result = {
key,
};
let fileStream = fs.createReadStream(filePath);
fileStream.on('error', function(err) {
logger.syncInfo(err);
});
let uploadParams = {
Bucket: this.bucket,
Key: key,
Body: fileStream
};
await this.client.upload (uploadParams, function (err, data) {
if (err) {
logger.syncInfo(err);
}
});
return result;
复制代码
调用路径在controllers/registry/package/download.js,当下载包的时候会进入这个方法,入参为key和options,用于获取包的存放的url地址
const params = { Bucket: this.bucket, Key: trimKey(key) };
logger.syncInfo(`进入aws->url key=${key} trimKey=${trimKey(key)}`);
return this.client.getSignedUrl('getObject', params);
复制代码
调用路径在controllers/registry/package/remove.js controllers/registry/package/remove_version.js 和 controllers/sync_module_worker.js,当删除包或者删除版本的时候会进入这个方法,入参为key和options
const params = { Bucket: this.bucket, Key: trimKey(key) };
logger.syncInfo(`进入aws->remove key=${key} trimKey=${trimKey(key)}`);
await this.client.deleteObject(params);
复制代码
调用路径在controllers/utils.js,当下载包的时候会进入这个方法,入参为key和options,把可读流做为用户下载请求的response的body
utils.js中是惟一调用download和createDownloadStream的地方,然而咱们仔细看源码,能够发现若是定义了createDownloadStream方法,就会直接返回createDownloadStream的结果,而不会继续进行下面的download操做。
也就是说,咱们只须要定义createDownloadStream方法便可。
const params = { Bucket: this.bucket, Key: trimKey(key) };
logger.syncInfo(`进入aws->createDownloadStream key=${key} trimKey=${trimKey(key)}`);
return this.client.getObject(params).createReadStream();
复制代码
设置npm源为刚搭建的私有源
npm config set registry http://registry.npm.my.com
查看当前的registry地址
npm get registry
清理npm缓存
npm cache clean --force
随便选一个项目 删除node_modules包
rm -rf node_modules
安装
npm install
手动同步一个包,好比react(能够在web界面上的/sync/路径下输入包名进行同步)
npm sync react
复制代码
只是手动安装一个项目的依赖包可能没法说明什么,咱们来写一个简单的自动测试脚本
require('shelljs/global')
const logger = require('./log').logger;
const fs = require('fs');
const MODULE_DIR = '/node_modules';
const PARENT_PATH = '/Users/sunxiuguo/project/';
const projectName = [
'test1',
'test2',
'test3',
'test4',
]
const absolutePath = projectName.map(item => {
return {
modulesPath: PARENT_PATH + item + MODULE_DIR,
parentPath: PARENT_PATH + item,
}
});
const startTime = new Date('2018/11/06 21:00:000').getTime();
const endTime = new Date('2018/11/08 10:00:000').getTime();
/**
* 读取路径
* @param path
*/
function getStat(path){
if (exec(`cd ${path}`).code == 0) {
return true;
}
return false;
}
async function npmCachecleanAndInstall(projectPath) {
cd(projectPath);
logger.info(`cd ${projectPath}`);
exec('pwd');
await execAndLogAsync(`npm cache clean --force`);
await execAndLogAsync(`npm install --registry=http://registry.npm.my.com`)
}
async function execAndLogAsync(command) {
logger.info(command);
let result = await exec(command);
if (result.stderr) {
logger.error(result.stderr);
}
}
async function install(path) {
logger.info(`install: path = ${JSON.stringify(path)}`);
let isExists = getStat(path.modulesPath);
if (!isExists) {
// 若是不存在 npm install
logger.info(`install: 不存在${path.modulesPath}目录,开始npm install`)
await npmCachecleanAndInstall(path.parentPath);
} else {
// 若是存在,删除 && npm install
logger.info(`install: 存在${path.modulesPath}目录,开始删除`)
await execAndLogAsync(`rm -rf ${path.modulesPath}`);
logger.info(`install: 删除${path.modulesPath}成功,开始npm install`)
await npmCachecleanAndInstall(path.parentPath);
}
}
logger.info('beginning!')
if (new Date().getTime() < startTime) {
logger.info(`未到开始时间, 开始时间为2018/11/06 22:00:000`)
exit(1);
}
for(let path of absolutePath) {
(async function(){
while (new Date().getTime() < endTime) {
await install(path);
}
})()
}
复制代码
首先添加一个用户,添加后会默认以这个用户登陆
npm adduser
username:sunxiuguo
password:sunxiuguo
email:sunxiuguo1@qq.com
进入要发布的目录
npm publish
查看刚才发布的包信息(也能够在web界面上查询)
npm view moduleName
这时若是其余小伙伴也要发布这个包,就会报错了,由于其余小伙伴不是这个包的maintainer
我们来查看一下这个包的owner都有谁
npm owner ls moduleName
而后添加wangwang为这个包的owner
npm owner add wangwang moduleName
什么?!!又报错了?!
不要慌,那是由于根本没有wangwang这个用户,须要执行npm adduser添加一下
npm adduser
username:wangwang
password:1231131313
email:wangwang@guazi.com
再次添加owner
npm owner add wangwang moduleName
成功了!今后wangwang也能够发布这个包了
之后若是想登陆,直接Login便可
npm login
username:wangwang
password:1231131313
email:wangwang@guazi.com
复制代码
强调一下,撤销发布包是很危险的一件事情,若是有其余同窗用了你的包,而后你心血澎湃地把这个包撤销了??其余同窗确定一脸问号
npm unpublish moduleName
复制代码
- 根据规范,只有在发包的24小时内才容许撤销发布的包( unpublish is only allowed with versions published in the last 24 hours)
- 即便你撤销了发布的包,发包的时候也不能再和被撤销的包的名称和版本重复了(即不能名称相同,版本相同,由于这二者构成的惟一标识已经被“占用”了)
若是你再也不维护你发布的moduleA了,可使用下面这个命令
这个命令并不会撤销已发布的包,只是会在其余人用的你的包时收到警告
npm deprecate moduleA
复制代码
版本格式:主版号.次版号.修订号,版号递增规则以下:
主版号:当你作了不相容的API 修改,
次版号:当你作了向下相容的功能性新增,
修订号:当你作了向下相容的问题修正,好比修复了一个bug。
改变当前package的版本号,update_type为patch, minor, or major其中之一,分别表示修订号,次版号,主版号
npm version <update_type>
好比当前版本号为0.1.0
npm version patch
0.1.1
npm version minor
0.2.0
npm version major
1.0.0
复制代码
Error: could not get uid/gid
[ 'nobody', 0 ]
at /usr/lib/node_modules/npm/node_modules/uid-number/uid-number.js:37:16
at ChildProcess.exithandler (child_process.js:205:5)
at emitTwo (events.js:106:13)
at ChildProcess.emit (events.js:191:7)
at maybeClose (internal/child_process.js:891:16)
at Socket.<anonymous> (internal/child_process.js:342:11)
at emitOne (events.js:96:13)
at Socket.emit (events.js:188:7)
at Pipe._handle.close [as _onclose] (net.js:497:12)
在全局安装前执行下面这条命令便可
npm config set unsafe-perm true
复制代码
清一下缓存
npm cache clean --force
复制代码
这个问题我本身的状况是,在controllers/utils.js里,调用nfs.download方法,writeStream尚未写完,就开始了readStream而且清理了临时路径,致使文件被截断了,因此必定要注意异步的问题,而且调试的时候尽可能写好try catch和日志,方便之后定位问题。
固然也能够直接定义一个createDownloadStream方法,直接返回可读流给body。
复制代码
期间还踩过好多好多坑,遗憾的是忘记记录下来了....
以上是在下关于npm私服搭建的一点拙见,若有不足,望诸位客官多多指正。