XXHolic

XXHolic 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/XXHolic 编辑
编辑

个人动态

XXHolic 发布了文章 · 1月11日

CentOS 7 下 Git 超时

引子

想要把 GitHub 的一个库弄到服务器上,使用 Git 克隆的时候,发现很慢或提示超时,想起来之前碰到过,这次记录一下。

系统:CentOS 7

解决方式

国内对 GitHub 的访问进行了限制,可以通过指定 IP 方式访问。

第一步:找 IP

首先找到 github.com 的 IP 地址,可以在 IPAddress 网站上查找。还可以添加 github.global.ssl.fastly.net 的映射,这个有助于加速。

注意:隔段时间地址会变,网上找的有些都过期了,建议还是自己去查询一下。

第二步:修改 host

vim /etc/hosts

将找到的最新地址映射添加到 hosts 中。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 1月4日

Windows 下 Nginx 开机自启动

引子

最近经常用到 Nginx ,每次开机要手动启动一下,想设置为开机自动执行 start nginx ,找了下资料尝试后总结一下。

  • 系统:Windows 10 家庭中文版
  • 版本号:20H2
  • 操作系统版本:19042.685
  • nginx:版本 1.18.0 ,只配置了端口和 root 字段。
  • Origin
  • My GitHub

解决方式

Windows 可以通过手动修改注册表设置启动项,感觉有些麻烦,还是找个工具。找到工具 WinSW ,它可以将任何应用包裹并作为一个 Windows 服务管理。在这里可以下载编译好的可执行文件。本次使用的版本是 WinSW v3.0.0-alpha.7 。

WinSW 作为一个全局工具使用:

  1. 下载 WinSW.exeWinSW.zip
  2. 新建 myapp.xml (更详细说明见文档示例)。
  3. 运行 winsw install myapp.xml [options] 安装服务。
  4. 运行 winsw start myapp.xml 开启服务。
  5. 运行 winsw status myapp.xml 检查服务是否启动和运行。

在实际操作过程中发现了其它注意点:

  • 输入命令时参数 myapp.xml 并不是必需,想要省略,让配置文件名称跟 WinSW.exe 文件的名称一致即可,否则不带配置文件名称参数会报错。
  • WinSW.exe 文件需要放置在 nginx 安装目录下,否则执行指令的时候会提示找不到 nginx 的配置文件。
  • 出错误的时候,会输出日志,看日志有助于排查问题。

下面是个人配置步骤示例。

第 1 步

将下载的 exe 文件移动到 nginx 目录下,重命名为 winsw.exe ,新建配置文件 winsw.xml ,写入下面的配置:

<service>
  <id>nginx service</id>
  <name>Nginx</name>
  <description>This service runs Nginx.</description>
  <env name="NGINX_COMIC" value="%BASE%" />
  <prestart>start D:\nginx-1.18.0\nginx.exe</prestart>
  <executable>D:\nginx-1.18.0\nginx.exe</executable>
  <prestop>D:\nginx-1.18.0\nginx.exe -s stop</prestop>
  <log mode="roll" />
  <onfailure action="none" />
</service>

83-step1

第 2 步

在当前路径下打开命令窗口(当前文件路径上输入 cmd 回车即可),执行命令:

winsw install

83-cmd

成功会出现下面的提示:

83-install

查看是否成功添加了服务,“此电脑”右键 -> “管理” -> “服务和应用程序” -> “服务”:

83-check

第 3 步

检测启动服务是否正常:

winsw start

启动成功也会出现提示。访问 nginx 配置的地址,确认是否有效。确认后,重启电脑,看下是否会自启动。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-12-21

前端异常上报

引子

前端异常解析:Source Map 中讨论了 Source Map 的使用,接着看看异常上报的方式。

影响因素

异常上报,可能产生影响的因素有:

  1. 上报的频率。当出现死循环,不断触发异常上报时,这个就跟 DDOS 攻击差不多了。
  2. 上报的数据量。不同的请求方式,能携带的数据量有限制。如果想要录制用户的操作,那么产生的数据量,不同情况下会不一样。
  3. 跨域。日志服务器有些是单独的。
  4. 不同 web 服务器对于请求的 body 大小限制不同,见 Can HTTP POST be limitless?
  5. 上报请求的响应方式。响应同样会消耗资源。

以上是一些比较容易想到的因素,在实际的情况中,根据不同监控的需要,可能会出现其它的影响因素。

上报方式

图片 src

优点:

  • 不受跨域限制。
  • 可以不用响应。

不足:

XHR/Fetch

优点:

  • POST 的方式可以传送更多的数据,理论上没有限制请求 body 的大小,但不同的服务器,接受的请求的 body 大小可能有限制。

不足:

  • 受跨域限制。
  • 可能与其它优先级更高的网络请求竞争资源。

Beacon

Beacon(信标)接口用于安排异步和非阻塞数据传输,从而能最大限度地减少与其它关键型操作的资源争用,同时还能将请求发送到目的地。特点有:

  • 信标请求使用的 POST 方式而且不需要响应。
  • 信标请求会避免与关键操作和优先级高的网络请求竞争资源。
  • 用户代理可以有效的合并信标请求,以优化移动设备上的能量使用。
  • 信标请求保证在页面卸载之前初始化,并且允许运行完成,而不需要阻塞请求或阻塞其它用户交互事件处理。
window.navigator.sendBeacon(url,data)
  • url : 传送数据的 URL 。
  • data : 传送的数据,这个参数可选,支持 ArrayBufferView、Blob、DOMString、FormData ,更详细类型见 BodyInit unions

兼容性见 Can I use Beacon

不足:

  • 这个方法不会提供任何关于数据传送是否成功的信息。
  • 受跨域限制。

频率

针对不同的异常和目的,上报的频率可以人为的进行控制。比较概括的可以分为三类:即时上报、批量上报、用户主动上报。

即时上报

即时上报就是触发了异常马上上报,这类异常一般会严重影响到用户的使用。

当比较多的异常连续触发或出现了无限循环触发异常时,上报的请求也需要一定的管理。下面是 @sentry/utils version 5.8.0 中管理的一种方式:

import { SentryError } from './error';
import { SyncPromise } from './syncpromise';
/** A simple queue that holds promises. */
export class PromiseBuffer {
    constructor(_limit) {
        this._limit = _limit;
        /** Internal set of queued Promises */
        this._buffer = [];
    }
    /**
     * Says if the buffer is ready to take more requests
     */
    isReady() {
        return this._limit === undefined || this.length() < this._limit;
    }
    /**
     * Add a promise to the queue.
     *
     * @param task Can be any PromiseLike<T>
     * @returns The original promise.
     */
    add(task) {
        if (!this.isReady()) {
            return SyncPromise.reject(new SentryError('Not adding Promise due to buffer limit reached.'));
        }
        if (this._buffer.indexOf(task) === -1) {
            this._buffer.push(task);
        }
        task
            .then(() => this.remove(task))
            .then(null, () => this.remove(task).then(null, () => {
            // We have to add this catch here otherwise we have an unhandledPromiseRejection
            // because it's a new Promise chain.
        }));
        return task;
    }
    /**
     * Remove a promise to the queue.
     *
     * @param task Can be any PromiseLike<T>
     * @returns Removed promise.
     */
    remove(task) {
        const removedTask = this._buffer.splice(this._buffer.indexOf(task), 1)[0];
        return removedTask;
    }
    /**
     * This function returns the number of unresolved promises in the queue.
     */
    length() {
        return this._buffer.length;
    }
    /**
     * This will drain the whole queue, returns true if queue is empty or drained.
     * If timeout is provided and the queue takes longer to drain, the promise still resolves but with false.
     *
     * @param timeout Number in ms to wait until it resolves with false.
     */
    drain(timeout) {
        return new SyncPromise(resolve => {
            const capturedSetTimeout = setTimeout(() => {
                if (timeout && timeout > 0) {
                    resolve(false);
                }
            }, timeout);
            SyncPromise.all(this._buffer)
                .then(() => {
                clearTimeout(capturedSetTimeout);
                resolve(true);
            })
                .then(null, () => {
                resolve(true);
            });
        });
    }
}

主要思路:

  • 初始化的时候,会提供一个数组 _buffer 和阈值 _limit
  • 所有的请求会进行类似 Promise 机制包裹,通过方法 add 放到数组中。请求是异步的,根据事件循环机制,可以连续增加新的请求。
  • 当添加的请求数量超过 _limit 时,就不会继续添加了,这样就可以达到一定控制的效果。

批量上报

将收集到的信息先存储到本地,当数据量或间隔时间达到一定的阈值,将本地存储的信息一次性打包上传。追踪用户的操作路径这类信息比较适合这种方式。

用户主动上报

提供一个异常上报入口,由用户自己反馈所遇到的问题。

如果考虑隐私方面的政策,在进入系统的时候,需要告知会进行一些信息采集。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-12-14

前端异常解析:Source Map

引子

前端异常解析中介绍了异常的解析,这些异常信息上报后,一般也难以直接的看出什么错误源,因为正式的线上环境中,代码往往都经过了压缩混淆,异常的一些信息都是指向压缩文件。这时候可以根据 Source Map 文件追溯源文件的位置。

简介

最初的源映射格式是由 Joseph Schorr 创建,在 Closure Inspector(谷歌的公共工具)中用来开启 JavaScript 源码级别的调试优化。随着使用了源映射的项目规模扩大,格式冗余开始成为一个问题。v2 版本为了减小源映射整体大小,牺牲了一些简单性和灵活性。目前最新的版本是 v3 。更多信息见 Source Map Revision 3

格式

按照约定,源映射文件跟源文件拥有相同的名称,只是后缀为 .map 。比如 page.js 的产生的源映射名称是 page.js.map 。这个约定并不是强制性的。

源映射整个文件是一个 JSON 对象:

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "bar.js"],
  "sourcesContent": [null, null],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}
  • version :source map 规范版本,必须是一个正整数。
  • file :可选项,转换后产生的源映射文件名。
  • sourceRoot :可选项,资源更路径,在服务器上重新定位源文件和移除 sources 中重复的值有用处。这个值会预先添加到 sources 字段中每一个值。如果是跟源文件相同的路径,则为空。
  • sources :存放 mappings 中使用的源文件。
  • sourcesContent :可选性,存放源内容。列表的顺序跟 sources 字段中顺序一致。如果一些源要按照名称检索,可能会使用 null
  • names :mappings 中使用的一些标识名。
  • mappings :记录了映射信息的字符串。

结合示例看含义就更清晰了:

70-source-map

更多信息见 Source Map Revision 3

使用

从 Chrome 39 开始,开发者工具中 Source Maps 设置项默认是开启的,更加详细的说明见这里

本地开发的时候,类似 Webpack 构建工具都支持生成 Source Map 文件,调试的时候在 Chrome 中就可以在开发者工具 Sources 栏中看到对应源代码位置。

在正式线上环境中,需不需要生成并部署 Source Map 文件,就看各自的考虑,可以参考一下这篇文章 Should I Use Source Maps in Production? 。要说明一下,浏览器默认不会请求这类文件,不用担心带来额外的请求。如果想要方便排查线上的问题,又不想别人查看源码,服务器可以对 Source Map 文件的访问进行限制。

在正式线上环境中,没办法随时能够操作用户的电脑排查问题,出现异常的时候,假设有 Source Map 文件,该如何进行处理?由此有了下面的疑问:

  1. 如何检测是否有对应 Source Map 文件?
  2. 如何获取到 Source Map 文件?
  3. 如何解析 Source Map 文件?

下面针对这三个问题,依次进行解答。

如何检测是否有 Source Map 文件?

如果压缩后同时生成了 Source Map 文件,那么在压缩后代码的最后一行会是这样:

// js 文件最后一行
//# sourceMappingURL=<url>

// css 文件最后一行
/*# sourceMappingURL=<url> */

所以在出现异常时,得到异常所处的文件之后,判断该文件内容中是否有上面的标记,就可以判断是否有 Source Map 文件。source-map-support 中就是这样进行判断。

如何获取到 Source Map 文件?

在第一个问题里面,知道了有 Source Map 文件,同时也就获取了文件所在位置,直接去请求就可以了。在规范中,推荐在响应头部返回 SourceMap 指向关联的 Source Map 文件,在最新 Chrome 中试了下,并不会默认带上的,可能需要自己手动设置。这个是示例页面

如何解析 Source Map 文件?

理解规范里面编码的方式,就可以反向的进行解析。现有 source-map 库提供了解析的功能。这个是示例页面

什么时候

在上面已经知道怎么去使用 source map 文件,那么该什么时候去进行这个过程?上面给出的示例,都是在前端进行处理,实际上服务器端也可以进行处理,这个时候需要考虑的因素有:

  • 是否会造成影响主流程的运行,因为如果放在前端处理,JavaScript 是单线程,可能会产生影响。
  • 是否能接受额外的请求,上面示例中就有请求 source map 文件,其大小一般都比压缩后源文件要大。
  • 是否能接受源码泄漏的风险。

前端异常处理上报,总的来说是为了及时甚至提前发现问题并处理,为用户提供更好的体验,也为改进产品提供数据参考。从这个方便来看,异常处理上报是一个辅助的功能,应该尽量减少占有的资源,所以 source map 的解析建议放到服务器端处理。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-12-07

前端异常解析

引子

前端异常示例里面观察了各类异常的示例,按照区分判断的思路 3 ,看下该如何解析异常信息。主要还是 SentryTraceKit 里面部分处理逻辑。

@sentry/browser 版本 : 5.9.1 。 TraceKit 版本: 0.4.6 。

TraceKit 解析

TraceKit 对于 JavaScript 定义的 Error 中几种类型有比较好的处理,对异常相关信息进行再解析格式化。具体可见 Read TraceKit

Sentry 解析

Sentry 中在之前提过区分的逻辑上,提供了对应的几种处理方法:

// 非完整源码,主要为了体现思路
let event
if (isErrorEvent(exception) && exception.error) {
  event = eventFromStacktrace(computeStackTrace(exception));
  return event;
}

if (isDOMError(exception) || isDOMException(exception)) {
  event = eventFromString(exception);
  return event;
}

// isError 包含的类型有 [object Error] [object Exception] [object DOMException] ,以及继承自 Error 对象的自定义对象。
if (isError(exception)) {
  event = eventFromStacktrace(computeStackTrace(exception));
  return event;
}

// isPlainObject 检查类型是 [object Object],isEvent检查的是 wat instanceof Event
if (isPlainObject(exception) || isEvent(exception)) {
  event = eventFromPlainObject(exception);
  return event;
}

event = eventFromString(exception);

return event;

其中 computeStackTrace(ex) 方法是基于 TraceKit 中的处理方法进行一些改造。

下面对处理的方法进一步的了解。

eventFromStacktrace

export function eventFromStacktrace(stacktrace: TraceKitStackTrace): Event {
  const exception = exceptionFromStacktrace(stacktrace);

  return {
    exception: {
      values: [exception],
    },
  };
}

export function exceptionFromStacktrace(stacktrace: TraceKitStackTrace): Exception {
  const frames = prepareFramesForEvent(stacktrace.stack); // 该方法进行一些数据过滤

  const exception: Exception = {
    type: stacktrace.name,
    value: stacktrace.message,
  };

  if (frames && frames.length) {
    exception.stacktrace = { frames };
  }

  // tslint:disable-next-line:strict-type-predicates
  if (exception.type === undefined && exception.value === '') {
    exception.value = 'Unrecoverable error caught';
  }

  return exception;
}

该方法主要是对处理过后的异常信息,进行数据再次格式化,以及对一些情况的补充完善,例如 Unrecoverable error caught 。

最后产生的数据格式比较完整的是这样:

{
  type: '',
  value: '',
  stacktrace: {},
}

eventFromString

export function eventFromString(
  input: string,
  syntheticException?: Error,
  options: {
    attachStacktrace?: boolean;
  } = {},
): Event {
  const event: Event = {
    message: input,
  };

  //  这块逻辑跟自定义的异常有关,暂不讨论
  if (options.attachStacktrace && syntheticException) {
    const stacktrace = computeStackTrace(syntheticException);
    const frames = prepareFramesForEvent(stacktrace.stack);
    event.stacktrace = {
      frames,
    };
  }

  return event;
}

该方法主要对数据进行再组装格式化。

最后产生的数据格式比较完整的是这样:

{
  message: '',
  stacktrace: {},
}

eventFromPlainObject

export function eventFromPlainObject(exception: {}, syntheticException?: Error, rejection?: boolean): Event {
  const event: Event = {
    exception: {
      values: [
        {
          type: isEvent(exception) ? exception.constructor.name : rejection ? 'UnhandledRejection' : 'Error',
          value: `Non-Error ${
            rejection ? 'promise rejection' : 'exception'
          } captured with keys: ${extractExceptionKeysForMessage(exception)}`,
        },
      ],
    },
    extra: {
      __serialized__: normalizeToSize(exception),
    },
  };

  //  这块逻辑跟自定义的异常有关,暂不讨论
  if (syntheticException) {
    const stacktrace = computeStackTrace(syntheticException);
    const frames = prepareFramesForEvent(stacktrace.stack);
    event.stacktrace = {
      frames,
    };
  }

  return event;
}

该方法主要对数据进行再组装格式化。

最后产生的数据格式比较完整的是这样:

{
  values: [{}],
  extra: {},
  stacktrace: {},
}

小结

Sentry 相对于 TraceKit 提供了更多异常类型的处理,并且不同类型的异常可能会有单独的描述属性。如果需要根据自身的需求定制数据格式,建议基于 TraceKit 的处理结果再次处理。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-12-02

前端异常示例

引子

前端异常类型及捕获方式 之后,尝试了自己去封装一下,然后去看了 sentry 的源码,发现之前的那篇只是一个概括,当采集的时候,需要更加细致的解析,接下来看看各种异常具体是什么样的。

基础知识点

前端异常一定会接触到 Error 对象,先来简单的了解一下。

错误的类型:

  • Error : 基类型,其它错误类型都是继承自该类型。
  • EvalError : 使用 eval() 函数而发生异常时抛出。
  • RangeError : 数值超出相应有效范围时的异常。
  • ReferenceError : 访问无效的引用的异常。
  • SyntaxError : 语法错误的异常。
  • TypeError : 变量或参数不是一个有效类型的异常。
  • URIError : encodeURI()decodeURI() 传入无效参数的异常。
  • AggregateError : 一个操作导致多个异常需要报告,例如 Promise.any()
  • InternalError : JavaScript 引擎内部抛出的错误,这个还没有标准化。

实例共有的标准属性:

  • message : 异常信息。
  • name : 异常名称。

实例共有的非标准属性:

  • description : 微软的非标准属性,类似 message 。
  • number : 微软的非标准属性,异常数字。
  • fileName : Mozilla 的非标准属性,异常产生所在文件的路径。
  • lineNumber : Mozilla 的非标准属性,异常产生所在文件的行数。
  • columnNumber : Mozilla 的非标准属性,异常产生所在文件的列数。
  • stack : Mozilla 的非标准属性,堆栈跟踪。

更多异常相关的信息见 ecma-262 ErrorWebIDL Exceptions

下面看看每种类型的示例。(暂没有考虑框架)

以下示例环境:

  • Chrome :86.0.4240.198(正式版本) (x86_64)。
  • 使用 nginx 启动了一个本地服务,原生 js 。
  • 为了方便查看更多信息,使用了 try-catch、onerror、onunhandledrejection 。

EvalError

EvalError 已不再被 JavaScript 抛出,现在该对象为了保持兼容性存在。用下面的方式可以看到这种异常:

try {
  throw new EvalError('Hello, EvalError');
} catch (e) {
  console.log(e instanceof EvalError); // true
  console.log(e.message);              // "Hello, EvalError"
  console.log(e.name);                 // "EvalError"
}

查一些资料说下面方式就会抛出该类型异常,但试了一下抛出的异常是 TypeError

new eval();

RangeError

const arr = new Array(-10)

67-range-error

ReferenceError

let a = undefinedVariable

67-reference-error

SyntaxError

eval('hello syntax')

67-syntax-error

有些语法错误是无法捕获的,因为可能导致整个程序无法正常运行,不过这类型大多在编写阶段就会很容易发现。

const a++;

TypeError

const a = 'hell';
a.fun();

67-type-error

URIError

decodeURIComponent('%')

67-uri-error

AggregateError

Promise.any([
  Promise.reject(new Error("some error")),
]).catch(e => {
  throw e; // 这里抛出,让 onunhandledrejection 接收会有详细的异常信息
});

67-aggregate-error

DOMException

const node = document.querySelector('#demo'); // 这个要存在
const refnode = node.nextSibling;
const newnode = document.createTextNode('异常');
node.insertBefore(newnode, refnode);

67-dom-exception

DOMError

已经不推荐使用了,但一些浏览器还是兼容了这个。

const err = new DOMError('DOMError');
throw err;

67-dom-error

ErrorEvent

const err = new ErrorEvent('ErrorEvent');
throw err; // onerror 会捕获到

67-error-event

资源加载异常

常见的 linkscriptimg 标签加载异常都是这样类似的信息:

67-static-error

接口请求异常

基础 XMLHttpRequest 示例:

const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:6677/index");
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    if (xhr.status == 200) {
      console.info('success')
    } else {
      console.info('xhr error:',xhr)
    }
  }
};
xhr.send();

67-xhr-error

基础 fetch 示例:

fetch("http://localhost:6677/index").then((res) => {
  if (!res.ok) {
    throw res;
  }
}).catch((e) => {
  console.info('fetch error:',e);
})

67-fetch-error

Script error.

这个本地(浏览器直接文件路径访问)很容易出现,例如点击按钮的时候,抛出了一个异常,onerror 也捕获到了,但什么信息也没有:

67-script-error

换成 try-catch 捕获,有了一些信息:

67-try-script-error

这个应该是一个对象,用对象的形式打印一下:

67-error-object

这样就可以拿到相关的信息进行解析了,可参考 TraceKit

区分

看到上面的示例,有好几种形式的异常,进行处理的时候,区分判断的依据是什么?

思路 1

比较简单的做法,就是不用区分,既然都是异常,整个全都上报,在后台人工查看一样可以达到异常分析排查的目的。

思路 2

通过检查特定字段,或者字段的组合来判断区分。针对特定的异常可以做到,所有的就不太清楚了。

思路 3

在看 sentry 源码的时候,发现了另外一种思路:根据异常的类型来区分

使用的主要方法是:

function getType(value) {
  return Object.prototype.toString.call(value);
}

按照这个思路,异常类型有:

  • [object Error] : 属于这种类型的异常有 RangeErrorReferenceErrorSyntaxErrorTypeErrorURIError
  • [object Exception] : 这个没找到,但 sentry 里面有写。
  • [object DOMException] : 属于这种类型的异常有 DOMException
  • [object DOMError] : 属于这种类型的异常有 DOMError
  • [object ErrorEvent] : 属于这种类型的异常有 ErrorEvent
  • [object PromiseRejectionEvent] : 这个有些特殊,当上面的 AggregateError 在 catch 里面检查类型时属于 [object Error] ,如果 throw 时,在 onunhandledrejection 里面类型变成了 [object PromiseRejectionEvent]

这里大概的写一下 sentry 的区分逻辑:

const exception = 'some value';
let format;
if (isErrorEvent(exception) && exception.error) {
  format = doSomething1(exception);
  return format;
}

if (isDOMError(exception) || isDOMException(exception)) {
  format = doSomething2(exception);
  return format;
}

// isError 包含的类型有 [object Error] [object Exception] [object DOMException] ,以及继承自 Error 对象的自定义对象。
if (isError(exception)) {
  format = doSomething3(exception);
  return format;
}

// isPlainObject 检查类型是 [object Object],isEvent检查的是 wat instanceof Event
if (isPlainObject(exception) || isEvent(exception)) {
  format = doSomething4(exception);
  return format;
}

format = doSomething5(exception);

return format;

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-11-23

本地包调试 :npm link

引子

再次碰到调试 npm 本地包的情况,一时想不起来,看了下文档,实际操作了一下,发现了一些文档上没明写的东西。

介绍

根据文档介绍,包的链接分为两步。

第一步

在一个包文件夹内执行 npm link 将在全局文件 {prefix}/lib/node_modules/<package> 内,创建一个符号链接(symlink),这个链接指向 npm link 命令执行的地方。

第二步

到其它目录下,执行 npm link packageName 命令,将会创建一个从全局安装的 packageName 到当前文件内的 node_modules 下的符号链接。

需要注意的的是, packageName 是取自包的 package.jsonname 字段,不是文件夹名称。

包的名称可能有作用域前缀,如果有, packageName 也要对应加上。

实践

基于文档,结合实际的操作,对照看下产生的效果。

运行环境

  • 项目是基于 webpack 简单配置,本地运行的 server 。
  • node 使用 nvm 管理的。
  • 项目已引用了包,需在基础上进行修改调试。
  • 系统是 macOS 。

操作

首先在包根目录下面执行 npm link 命令,出现下面的提示:

79-link

到对应的目录下,发现生成了提示中所说的文件,就是文档中所说符号链接(symlink):

79-result

试着改了一下本地的源文件,发现全局包里面对应的文件内容也跟着变化。

然后到项目中执行 npm link packageName 指令,出现了下面的提示:

79-link-package

到 node_modules 下发现对应的依赖包已经发生了变化:

79-link-module

这里的包跟全局那个生成的包是一样的,包更新了,本地看没什么效果,原因是本地的 sever 有缓存,需要重新启动一下。注意这个文件夹图标多了一个箭头的标记,未 link 之前没有这个。

重启服务后,到源库修改源码,发现项目 node_modules 下同步了修改的内容,webpack 也检测到变化,自动刷新。

修改好后,想要恢复到原本的包,解除 link 该怎么做?很奇怪,在 npm 官方文档上没找到说明。

解除 link

到项目下执行下面的命令:

npm unlink --no-save package && npm install

npm uninstall 文档中可以发现,unlink 其实是 uninstall 的别名,实质上也是删除了包。

包不需要的 link 的时候,建议也解除,到包目录下执行下面的命令:

npm unlink

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-11-16

npm 发布包遇到的问题

引子

虽然有发布过包的经历,但没有发布过自己的包,于是就参照 npm developer guide 尝试了一下,有了下面的收获。

403 Forbidden

按照文档中的步骤,注册登录后,准备好了库,然后执行 npm publish,报了下面的错误:

Error: 403 Forbidden - PUT https://registry.npmjs.org/xx - You do not have permission to publish "xx". Are you logged in as the correct user?

查询资料后,发现了这个 issue,原来是因为公开的库里面,已经有了同名的库。由于是免费的账号,所以想在 npm 上发包,要么换个名称,要么花钱创建私有包。还有一种方式就是在内网搭建自己的服务。

Error: 402 Payment Required

换了个名字,想起了自己见过的库,就模仿加了个 @ 前缀的包,再次发布的时候,报了下面的错误:

Error: 402 Payment Required - PUT https://registry.npmjs.org/@x... - You must sign up for private packages

查看文档,发现 @npm/package-name 这种形式的包名,是有作用域的包名形式,执行 npm publish 的时候默认是发布私有的包。因此,第一种方式是花钱买私有包的服务,另外一种方式就是指定参数,表示公开:

npm publish --access public

需要注意的是这种形式的包名跟 npm 账户有对应关系,不能随便填写。

npm init --scope=@my-org

这种形式表示是一个组织,my-org 对应是 npm 中的组织名。

npm init --scope=@my-username

这种形式表示是个人,my-username 对应是 npm 中的用户名。

对旧包的处理

在尝试的过程中,发布了一些包,想着只是测试的包,想要移除掉,但 npm 官方很不推荐这种做法,推荐用 npm deprecate 指令,告知安装者相关信息,例如“我不再维护这个库了,请不要使用”。

npm deprecate package-name "This is test package, do not use it!"

其中 package-name 是指发布的包的名称,可能跟库的名称不一样。

发布成功了,但在 npm 上找不到包

在一次发布中,看到发布成功的提示,但到 npm 账户发现没有相关的包。

反复尝试了几次,发现原来是因为自己本地 registry 指向了内网的一个地址,并没有指向 npm 官方的地址 https://registry.npmjs.org/

所以发包的时候,先确定一下发布的指向:

npm config get registry
或
npm config list

如果发现指向不对,有两种修改的方式:
方式 1:使用 npm config set registry 改成对应的指向,但这个是全局修改,之后如果需要发布其它指向的包,又要修改回来。

方式 2:执行指令时添加 --registry 参数
package.json 中添加参数:

"publishConfig": { "registry": "https://npm.pkg.github.com/" }

登录的时候,添加对应的参数:

npm login --registry=https://registry.npmjs.org/

在使用 npm deprecate 指令的时候,如果本地 registry 指向不对,也要添加 --registry 参数才会有效。

如何判断 npm 账户是否已登录

由于发布的包有不同的 registry 指向,在发布包的时候,登录要指向不同的源,所以发布之前想要看看是否已登录。

查看了文档,没有找到查看登录状态的特定命令,但可以使用能触发登录检查的命令,从侧面看是否已登录。

很容易想到的就是 npm login 命令,但登录过一次后,再执行时还是提示输入账号密码。

后来发现了一个简单的指令 npm whoami ,这个指令只能在登录状态下执行,没有登录就会提示:

npm ERR! code ENEEDAUTH

npm ERR! need auth This command requires you to be logged in.

npm ERR! need auth You need to authorize this machine using npm adduser

需要注意的是,这指令不带 registry 参数时,会指向 npm 默认的 registry ,如果这个不匹配,也是没有效果的。

当有多个 registry 时,一定要注意,不少的指令都需要一致的 registry 才有效。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-11-09

【译】npm developer guide

引子

npm Getting started 之后,发现了 npm-developers 这篇文档,介绍从开始、本地测试、打包到发布的一个相对完整的流程。感觉是 Getting started 的一个补充,于是就把篇文档翻译成中文,加深下印象。

原文:npm-developers

简述

所以,你已经打算使用 npm 来开发(并且可能发布或部署)你的项目了。

太棒了!

在你的用户安装程序的简单步骤之上,您需要做一些事情。

关于这些文档

这些是手册页。如果你安装了 npm,你应该可以在 npm 手册中找到特定主题的文档,或者通过 npm 帮助来获取相同的信息。

一个 package 是什么

一个 package 是:

1. 一个包含了描述程序的 package.json 文件的文件夹。
2. 包含有一个 gzip 压缩的压缩包(在 1 的基础上)。
3. 一个解析为(2)中包的 url。
4. 一个已经在登记处发布的 <name>@<version>(在 3 的基础上)。
5. 一个指向的(4)的 <name>@<tag>。
6. 一个满意的有 “latest” 标签的 <name>(在 5 的基础上)。
7. 一个克隆的 git 仓库链接,指向(1)中的文件夹。

即使你从来没有发布过你的包,如果你只是想写一个 node 项目,或者在打包成压缩包后,你想在其它地方能很容易的安装,你仍然可以从使用 npm 中受益匪浅。

Git 的链接可是下面的一些形式:

git://github.com/user/project.git#commit-ish
git+ssh://user@hostname:project.git#commit-ish
git+http://user@hostname/project/blah.git#commit-ish
git+https://user@hostname/project/blah.git#commit-ish

commit-ish 可以是任何的 tag、sha 或者能够提供给 git checkout 作为参数的分支名。默认值是 master

package.json 文件

你需要在项目根目录下有个 package.json 文件,这样才能用 npm 做很多事情。这基本上就是整个交接口。

详细的文件信息,可见 package.json。你至少需要知道的有:

  • name:这个应该是标识你项目的字符串。请不要使用 name 来指明这个项目是基于 node 或者使用了 JavaScript。你可以用 “engines” 字段来表明你项目,需要什么版本的 node(或者其它什么),它几乎假设是 JavaScript。
    也不需要匹配你的 github 库的名称。
    因此,node-foobar-js 是不好的命名。foobar 的命名会更好。
  • version:semver 兼容版本。
  • engines:指明你的项目基于那个版本 node(或其它什么) 运行。node API 改变很频繁,你依赖的版本,可能有一些 bug 或新的功能。明确的指出来。
  • author:作者。
  • scripts:如果你有特定的编译或安装脚本,那么你应该把它放到 scripts 对象中。你应该至少有一个基本的 smoke test 命令作为 “scripts.test” 字段。见 npm-scripts
  • main:如果你有一个单独的模块作为你程序的入口(就像 “foo” 包通过 requre("foo") 获取),那么你需要在 “main” 字段指明。
  • directories:这是一个映射名称到文件夹的对象。最好包括 “lib” 和 “doc”,但是如果你使用 “man” 指定一个满是手册页的文件夹,它们将会像安装这些手册页一样进行安装。

你可以在你项目的根目录下,使用 npm init 初始化基本的 package.json 文件。更多信息见 npm-init

过滤文件

使用 .npmignore 文件排除包中的一些东西。如果没有 .npmignore 文件,但有 .gitignore 文件,那么 npm 将忽略该文件中匹配到的东西。如果你想包含你 .gitignore 文件中排除的文件,你可以创建一个空的 .npmignore 文件覆盖它。与 git 一样,npm 在包的所有子目录中查找 .npmignore.gitignore 文件,而不仅仅是根目录。

.npmignore.gitignore 遵循相同的规则模式:

  • # 开始的行会被忽略。
  • 标准的全局模式有效。
  • 你可以使用正斜杠结束模式 / 指定目录。
  • 你可以通过使用感叹号 !来否定一个模式。

默认的,下面的路径和文件会被忽略,因此不需要额外将这些添加到 .npmignore 中:

  • .*.swp
  • ._*
  • .DS_Store
  • .git
  • .hg
  • .npmrc
  • .lock-wscript
  • .svn
  • .wafpickle-*
  • config.gypi
  • CVS
  • npm-debug.log

此外,node_module 中所有的东西会被忽略,除了绑定的依赖。npm 会自动为你处理这个。所以不必将 node_module 加入到 .npmignore 中。

下面的路径和文件绝不会被忽略,所以把这些加入到 .npmignore 中没有意义:

  • package.json
  • README (and its variants)
  • CHANGELOG (and its variants)
  • LICENSE / LICENCE

如果考虑到项目的结构,你发现 .npmignore 是一个维护难题,那么可以尝试填充 package.json 文件的 files 属性,该属性是一个数组,里面是应该包含在包中的文件或目录名。有时候白名单比黑名单更容易管理。

测试你的 .npmignore 或文件配置是否有效

如果你想要再次确认,当发布时,你想要的都包含在包中,你可以本地运行 npm pack 命令,这个命令将在工作目录生成一个压缩包,发布包也是使用这种方式。

Link 包

npm link 旨在安装一个开发包并实时查看更改,而不用再次重新安装。(当然,你需要重新 link 或 npm rebuidl -g 来更新编译的包。)

实际操作见本地包调试 :npm link ,文档信息见 npm-link

发布之前:确保你的包可正常安装运行

这很重要。

如果你的包不能本地安装,你试图发布它,将会有问题。或者更糟糕的是,你能发布,但发布了一个破损或者无意义的包。所以不要这样做。

在你包的根目录下,执行下面命令:

npm install . -g

这将会告诉你是否可行。如果你更喜欢创建一个 link 的包,指向你的工作目录,那么这么做:

npm link

使用 npm ls -g 查看是否存在。

测试本地安装,到另外一个文件夹下,执行:

cd ../some-other-folder
npm install ../my-package

在其它地方,本地安装到 node_module 文件夹下。

然后进入 node-repl,尝试使用 require(“my-thing”)引入你模块的主模块。

创建一个用户账户

通过 adducer 命令创建一个用户。像这样运行:

npm adduser

之后就根据提示进行。详细见 npm-adduser

发布你的包

这部分很简单。到你文件夹根目录下,执行这个:

npm publish

你可以给一个压缩包,一个压缩包的文件名,或者一个文件夹的路径发布一个 url。

请注意,默认情况下,文件夹里面几乎所有的内容将会被公开。所以,如果你在里面有秘密的东西,使用一个 .npmignore 文件列出要全局忽略的东西,或者从一个新的 checkout 发布。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-11-02

npm Getting started

引子

最近工作上要接触相关的东西,开始熟悉文档,这里主要是 Getting started 部分中,感觉有帮助的内容记录。

组成

npm 包含三个不同的部分:

  • 网站
  • 命令行界面(CLI)
  • 登记处

通过使用网站,可以查找发现包、设置简介、管理你的其它方面 npm 经历。比如,你可以建立 Orgs ,用来管理共有或私有的包。

命令行界面通过终端运行,这个是大多数开发者与 npm 交互的方式。

登记处是一个包含大量 JavaScript 软件和元信息的公开信息库。

注册

按照提示注册后,用 npm 测试登录。

npm login

根据提示输入帐号、密码、邮箱。输入后检测是否登录成功

npm whoami

如果显示出你的用户名,表示登录成功。

配置本地环境

关于 npm CLI 版本

npm 命令行界面按照一定的节奏正常发布。推荐安装的版本:

  • latest release:最新的稳定版本。
  • next release:最新的未发布的版本,即将成为最新版本。

安装 Node.js 的时候,npm 会自动安装。然而 npm 的版本更新比 Node.js 频繁的多,所以安装最新稳定版本的 npm 指令如下:

npm install npm@latest -g

安装 next release 版本指令为:

npm install npm@next -g

需要注意的是,next release 版本包含的功能,在最新的稳定版本中不一定会有。

安装

强烈建议用 Node 版本管理工具来安装 Node.js 和 npm。不要使用 Node 安装工具,因为 Node 安装进程安装 npm 需要本地文件的允许,当运行全局 npm 包的时候可能会导致权限错误。
检查npm Node的版本

node -v
npm -v

使用 Node 版本管理工具来安装 Node.js 和 npm。

Node 版本管理允许你在你自己环境上,安装和切换不同版本的 Node.js 和 npm。这样就能够测试不同的版本,确保它们在不同用户版本上正常运作。

OSX、Linux Node 版本管理工具

Window Node 版本管理工具

如果无法使用版本管理工具,则使用 Node 安装工具,这是下载页面

参考资料

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 15 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-03-30
个人主页被 906 人浏览