掌握甩锅技术 Typescript 运行时数据校验



背景


大家出来写 代码的,难免会出 Bug。


文章背景就发生在一个 Bug 身上,


有一天,测试慌张中带着点兴奋冲过来:
测试:”xxx 系统前端线上出 Bug 了,点进 xx 页面一片空白啊”。
我:”纳尼?我写的 Bug 怎么会出现代码呢?”。




虽然大脑一片空白,但是锅还是要背的。
进入页面一看,哦豁,完蛋,cannot read the property 'xx' of undefined。确实是前端常见的报错呀。


背锅王,我当定了?未必。


我眉头一皱,发现事情并不是那么简单,经过一番猛如虎的操作之后,最终定位到问题是:后端接口响应的 JSON 数据中,一个嵌套比较深的字段没有返回,即前端只读到了 undefined


咱按章程办事,后端提供的接口文档指定了数据结构,那你没有返回正确数据结构,这就是你后端的锅,虽然严谨点前端也能捕获到错误进行处理,但归根到底,是你后端数据接口处理有问题,这锅,我不背。


甩锅又是一门扯皮的事情,杀敌一千自伤八百,锅已经扣下来了,想甩出去就难咯,。


唉,要是在接口出错的时候,能立刻知道接口数据出问题,先发制人,马上把锅甩出去那就好咯。


这就是本文即将要讲述的 “Typescript 运行时数据校验”。

为什么要运行时校验数据?


众所周知,TypescriptJavaScript 超集,可以给我们的项目代码提供静态类型检查,避免因为各种原因而未及时发现的代码错误,在编译时就能发现隐藏的代码隐患,从而提高代码质量。


但是,TypeScript 项目的一个常见问题是: 如何验证来自外部源的数据并将验证的数据与 TypeScript 类型联系起来。 即,如何避免后端 API 返回的数据与 Typescript 类型定义不一致导致的运行时错误。


Typescript 能用于运行时校验数据类型,那么有没有一种方法,能让我们在 运行时 也进行 Typescript 数据类型校验呢?

io-ts 解决方案?


业界开源了一个运行时校验的工具库:io-ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  io-ts 例子
import * as t from 'io-ts'

// ts 定义
interface Category {
name: string
categories: Array<Category>
}

// 对应上述ts定义的 io-ts 实现
const Category: t.Type<Category> = t.recursion('Category', () =>
t.type({
name: t.string,
categories: t.array(Category)
})
)


但是,如上面的代码所示,这工具看起来就有点啰嗦有点难用,对代码的侵入性非常强,要全盘依据它的语法来重写代码。这对于一个团队来说,存在一定的迁移成本。


而我们更希望做到的理想方案是:


写好接口的数据结构 typescript 定义,不需要做太多的额外变动,直接就能校验后端接口响应的数据结构是否符合 typescript 接口定义

理想方案探索


首先,我们了解到,后端响应的数据接口一般为 JSON,那么,抛开 Typescript,如果要校验一个 JSON 的数据结构,我们可以怎么做到呢?


答案是JSON schema

JSON schema


JSON schema 是一种描述 JSON 数据格式的模式。


例如 typescript 数据结构:

1
2
3
4
5
6
type TypeSex = 1 | 2 | 3
interface UserInfo {
name: string
age?: number
sex: TypeSex
}


等价于以下的 json schema :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"$id": "api",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"UserInfo": {
"properties": {
"age": {
"type": "number"
},
"name": {
"type": "string"
},
"sex": {
"enum": [
1,
2,
3
],
"type": "number"
}
},
"required": [
"name",
"sex"
],
"type": "object"
}
}
}


根据已有 json-schema 校验库,即可校验数据对象

1
someValidateFunc(jsonSchema, apiResData)


这里大家可能就又会困惑:这json-schema写起来也太费劲了?还不一样要学习成本,那和 io-ts 有什么区别。


但是,既然我们同时知道 typescriptjson-schema 的语法定义规则,那么就两者必然能够互相转换。


也就是说,即便我们不懂 json-schema 的规范与语法,我们也能通过typescript 转化生成 json-schema


那么,在以上的前提下,我们的思路就是:既然 typescript 本身不支持运行时数据校验,那么我们可以将 typescript 先转化成 json schema, 然后用 json-schema 校验数据结构

typescript -> json-schema


要将 typescript 声明转换成 json-schema ,这里推荐使用 typescript-json-schema


我们可以直接使用它的命令行工具,这里就不仔细展开说明了,感兴趣的可以看下官方文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Usage: typescript-json-schema <path-to-typescript-files-or-tsconfig> <type>

Options:
--refs Create shared ref definitions. [boolean] [default: true]
--aliasRefs Create shared ref definitions for the type aliases. [boolean] [default: false]
--topRef Create a top-level ref definition. [boolean] [default: false]
--titles Creates titles in the output schema. [boolean] [default: false]
--defaultProps Create default properties definitions. [boolean] [default: false]
--noExtraProps Disable additional properties in objects by default. [boolean] [default: false]
--propOrder Create property order definitions. [boolean] [default: false]
--required Create required array for non-optional properties. [boolean] [default: false]
--strictNullChecks Make values non-nullable by default. [boolean] [default: false]
--useTypeOfKeyword Use `typeOf` keyword (https://goo.gl/DC6sni) for functions. [boolean] [default: false]
--out, -o The output file, defaults to using stdout
--validationKeywords Provide additional validation keywords to include [array] [default: []]
--include Further limit tsconfig to include only matching files [array] [default: []]
--ignoreErrors Generate even if the program has errors. [boolean] [default: false]
--excludePrivate Exclude private members from the schema [boolean] [default: false]
--uniqueNames Use unique names for type symbols. [boolean] [default: false]
--rejectDateType Rejects Date fields in type definitions. [boolean] [default: false]
--id Set schema id. [string] [default: ""]


github 上也有所有类型转换的 测试用例,可以对比看看 typescript 和 转换出的 json-schema 结果

json-schema 校验库


利用 typescript-json-schema 工具生成了 json-schema 文件后,我们需要根据该文件进行数据校验。


json-schema 数据校验的库很多,ajvjsonschema 之类的,这里用 jsonschema 作为示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Validator } from 'jsonschema'

import schema from './json-schema.json'

const v = new Validator()
// 绑定schema,这里的 `api` 对应 json-schema.json 的 `$id`
v.addSchema(schema, '/api')


const validateResponseData = (data: any) => {
// 校验响应数据
const result = v.validate(data, {
// SomeInterface 为 ts 定义的接口
$ref: `api#/definitions/SomeInterface`
})

// 校验失败,数据不符合预期
if (!result.valid) {
console.log('data is ', data)
console.log('errors', result.errors.map((item) => item.toString()))
}

return data
}


当我们校验以下数据时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明文件
interface UserInfo {
name: string
sex: string
age: number
phone?: number
}

// 校验结果
validateResponseData({
name: 'xxxx',
age: 'age应该是数字'
})
// 得出结果
// data is { name: 'xxxx', age: 'age应该是数字' }
// errors [ 'instance.age is not of a type(s) number',
// 'instance requires property "sex"' ]


完全例子请看 github


配合上前端上报系统,当线上系统接口返回了非预料的数据,导致出 bug,就可以实时知道到底错在哪了,并且及时甩锅给后端啦。

commit 时自动更新 json-schema


前面提到,我们需要执行 typescript-json-schema <path-to-typescript-files-or-tsconfig> <type> 命令来声明 typescript 对应的 json-schema 文件。


那么,这里就有个问题,接口数量有可能增加,接口数据也有可能变动,那也就代表着,我们每次变更接口数据结构,都要重新跑一下 typescript-json-schema ,时刻保持 json-schema 和 typescript 一一对应。


这我们就可以用 huskyprecommit , 加上 lint-staged 来实现每次更新提交代码时,自动执行 typescript-json-schema,无需时刻关注 typescript 接口定义的变更。


完全例子请看 github

总结


综上,我们实现了

  1. typescript 声明文件 转换生成 json-schema 文件
  2. 代码接口层拦截校验数据,如校验失败,通过前端上报系统 (如:sentry) 进行相关上报
  3. 通过 husky + lint-staged 每次提交代码自动执行 步骤 1,保持 git 仓库的代码 typescript 声明 和 json-schema 时刻保持一致。


那么,当 Bug 出现的时候,你甚至可以在测试都还没发现这个 Bug 之前,就已经把锅甩了出去。


只要你跑得足够快,Bug 就会追不上你。




https://github.com/SunshowerC/blog/issues/13

rollup初体验

进入一个目录,npm init 进行初始化,可以一路回车


安装ts
npm install -D typescript      


生存配置文件
./node_modules/.bin/tsc –init




根目录下新建一个rollup配置文件
rollup.config.js

添加以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import clear from 'rollup-plugin-clear'; // 转换cjs
import commonjs from 'rollup-plugin-commonjs'; // 转换cjs
import { terser } from 'rollup-plugin-terser'; // 压缩,可以判断模式,开发模式不加入到plugins
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import typescript from 'rollup-plugin-typescript';

export default {
input: 'src/index.ts', // 源文件入口
output: [
{
file: 'dist/browser-version.esm.js', // package.json 中 "module": "dist/browser-version.esm.js"
format: 'esm', // es module 形式的包, 用来import 导入, 可以tree shaking
sourcemap: false
}, {
file: 'dist/browser-version.cjs.js', // package.json 中 "main": "dist/browser-version.cjs.js",
format: 'cjs', // commonjs 形式的包, require 导入
sourcemap: false
}, {
file: 'dist/browser-version.umd.js',
name: 'GLWidget',
format: 'umd', // umd 兼容形式的包, 可以直接应用于网页 script
sourcemap: false,
}
],
plugins: [
clear({
targets: ['dist']
}),
resolve(),
babel({
exclude: 'node_modules/**'
}),
typescript(),
commonjs(),
terser(),
]
}


修改packagejson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"name": "browser-version-tool-html",
"version": "1.0.0",
"description": "Print tips on outdate browser. ",
"main": "dist/browser-version.cjs.js",
"module": "dist/browser-version.esm.js",
"browser": "dist/browser-version.umd.js",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "ts-node test/test.ts",
"pretest": "npm run build"
},
"author": "Yenkos",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.12.10",
"@rollup/plugin-html": "^0.2.0",
"@types/ms": "^0.7.31",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-latest": "^6.24.1",
"rollup": "^2.35.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-hash": "^1.3.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript": "^1.0.1",
"rollup-plugin-uglify": "^6.0.4",
"ts-node": "^9.1.1",
"tslib": "^2.0.3",
"typescript": "^4.1.3"
},
"types": "dist/index.d.ts"
}


npm i 安装依赖


进入src/index.ts 编写代码


rollup 打包
npm run build


发布包到npm

首先需要注册npm账号

npm adduser
npm login
npm publish



package-lock.json的作用

什么是package-lock.json

在前端项目根目录里有一个package-lock.json文件,有同学疑惑这个文件是干嘛用,不是已经有一个package文件了吗?这个文件前身是npm-shrinkwrap.json,因为npm的依赖管理非常宽松,一个项目在同一天内都可能会安装到不同版本的依赖包,依赖包的升级可能会给项目带来bug。

这时shrinkwrap就应运而生,但是大家都懒得写,所以npm在5+版本之后,收到yarn.lock的启发,默认会在安装时生成package-loack文件。


举个例子

总结

package-lock是用来严格控制版本依赖的。

package-lock.json 详细解析

package-lock.json会为npm修改node_modules树或的任何操作自动生成package.json。它描述了生成的确切树,因此无论中间依赖项更新如何,后续安装都可以生成相同的树。该文件旨在提交到源存储库中,并具有多种用途:

  • 描述依赖关系树的单个表示,这样可以确保队友,部署和持续集成安装完全相同的依赖关系。
  • 为用户提供一种工具,使其可以“时间旅行”到以前的状态,node_modules而不必提交目录本身。
  • 为了通过可读的源代码控制差异更好地了解树的变化。
  • 并允许npm跳过以前安装的软件包的重复元数据解析,从而优化安装过程。

关于package-lock.json它的一个关键细节是它无法发布,并且如果在顶级软件包之外的任何地方找到它,它将被忽略。它与共享格式npm-shrinkwrap.json,该文件本质上是相同的文件,但可以发布。除非部署CLI工具或使用发布过程来生产生产软件包,否则不建议这样做。如果软件包的根目录中同时存在package-lock.jsonnpm-shrinkwrap.jsonpackage-lock.json将被完全忽略。

文件格式

name

这是程序包锁定的程序包名称。这必须与中的内容匹配 package.json

version

这是程序包锁定的程序包版本。这必须与中的内容匹配 package.json

lockfileVersion

整数版本,1从此文档的版本号开始,在生成this时使用了其语义package-lock.json

packageIntegrity

这是从中创建的子资源完整性package.jsonpackage.json不应进行任何预处理。子资源完整性字符串可由类似的模块生成 ssri

表示安装是在NODE_PRESERVE_SYMLINKS启用环境变量的情况下完成的 。安装程序应坚持使该属性的值与该环境变量匹配。

dependencies

程序包名称到依赖对象的映射。依赖项对象具有以下属性:version这是一个说明符,可唯一标识此程序包,并应可用于获取其新副本。

  • 捆绑的依赖关系:不管来源如何,这都是一个纯粹用于参考目的的版本号。
  • 注册表源:这是一个版本号。(例如,1.2.3
  • git源:这是一个具有已解决承诺的git说明符。(例如,git+https://example.com/foo/bar#115311855adb0789a0466714ed48a1499ffea97e
  • http tarball来源:这是tarball的URL。(例如,[https://example.com/example-1.3.0.tgz](https://example.com/example-1.3.0.tgz)
  • 本地tarball来源:这是tarball的文件URL。(例如file:///opt/storage/example-1.3.0.tgz
  • 本地链接源:这是链接的文件URL。(例如file:libs/our-module
integrity

这是此资源的标准子资源完整性

  • 对于捆绑的依赖项,无论来源如何,均不包括在内。
  • 对于注册表源,这是integrity注册表提供的,或者如果未提供SHA1 shasum
  • 对于git源,这是我们从中克隆的特定提交哈希。
  • 对于远程tarball源,这是基于文件SHA512的完整性。
  • 对于本地tarball源:这是基于文件SHA512的完整性字段。
resolved
  • 对于捆绑的依赖项,无论来源如何,均不包括在内。
  • 对于注册表源,这是相对于注册表URL的压缩包的路径。如果tarball URL与注册表URL不在同一服务器上,则这是完整的URL。
bundled

如果为true,则为捆绑的依赖关系,并将由父模块安装。安装时,此模块将在提取阶段从父模块中提取,而不是作为单独的依赖项安装。

dev

如果为true,则此依赖项仅是顶层模块的开发依赖项,或者是一个传递性依赖项。对于既是顶层的开发依赖关系又是顶层的非开发依赖关系的传递依赖关系的依赖关系,这是错误的。

optional

如果为true,则此依赖项仅是顶层模块的可选依赖项,或者是一个传递性依赖项。对于既是顶层的可选依赖关系又是顶层的非可选依赖关系的传递性依赖关系的依赖关系,则为false。
即使所有可选依赖项都可以在当前平台上卸载,也应包括在内。

requires

这是模块名称到版本的映射。这是此模块所需的所有内容的列表,无论它将安装在何处。版本应通过正常匹配规则匹配我们dependencies或更高级别的依赖关系 。

dependencies

此依赖关系的依赖关系,与顶层完全相同。

再次梳理AMD、CMD、CommonJS、ES6 Module的区别

前言


回想起之前的一次面试,第一轮面试官问我 AMD 和 CMD 的区别,我只回答说 AMD 是提前加载,CMD 是按需加载。第二轮面试官又问了我 CommonJS 和 ES6 Module 的区别,emmm…,我大致回答说新的比旧的好~~,虽然面试官并没有说什么,不过显然这样的答案并不是有助于面试、有助于自己的技术积累的。


所以有必要进行一次梳理,以便更清晰地了解它们的特点及差异。

AMD


AMD 一开始是 CommonJS 规范中的一个草案,全称是 Asynchronous Module Definition,即异步模块加载机制。后来由该草案的作者以 RequireJS 实现了 AMD 规范,所以一般说 AMD 也是指 RequireJS。

RequireJS 的基本用法


通过define来定义一个模块,使用require可以导入定义的模块。

1
2
3
4
5
6
7
8
9
10
11
//a.js
//define可以传入三个参数,分别是字符串-模块名、数组-依赖模块、函数-回调函数
define(function(){
return 1;
})

// b.js
//数组中声明需要加载的模块,可以是模块名、js文件路径
require(['a'], function(a){
console.log(a);// 1
});

RequireJS 的特点


对于依赖的模块,AMD 推崇依赖前置,提前执行。也就是说,在define方法里传入的依赖模块 (数组),会在一开始就下载并执行。

CMD


CMD 是 SeaJS 在推广过程中生产的对模块定义的规范,在 Web 浏览器端的模块加载器中,SeaJS 与 RequireJS 并称,SeaJS 作者为阿里的玉伯。

SeaJS 的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//a.js
/*
* define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串,
* factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
* define 也可以接受两个以上参数。字符串 id 表示模块标识,数组 deps 是模块依赖.
*/
define(function(require, exports, module) {
var $ = require('jquery');

exports.setColor = function() {
$('body').css('color','#333');
};
});

//b.js
//数组中声明需要加载的模块,可以是模块名、js文件路径
seajs.use(['a'], function(a) {
$('#el').click(a.setColor);
});

SeaJS 的特点


对于依赖的模块,CMD 推崇依赖就近,延迟执行。也就是说,只有到require时依赖模块才执行。

CommonJS


CommonJS 规范为 CommonJS 小组所提出,目的是弥补 JavaScript 在服务器端缺少模块化机制,NodeJS、webpack 都是基于该规范来实现的。

CommonJS 的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//a.js
module.exports = function () {
console.log("hello world")
}

//b.js
var a = require('./a');

a();//"hello world"

//或者

//a2.js
exports.num = 1;
exports.obj = {xx: 2};

//b2.js
var a2 = require('./a2');

console.log(a2);//{ num: 1, obj: { xx: 2 } }

CommonJS 的特点

  • 所有代码都运行在模块作用域,不会污染全局作用域;
  • 模块是同步加载的,即只有加载完成,才能执行后面的操作;
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
  • CommonJS 输出是值的拷贝 (即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

ES6 Module


ES6 Module 是 ES6 中规定的模块体系,相比上面提到的规范, ES6 Module 有更多的优势,有望成为浏览器和服务器通用的模块解决方案。

ES6 Module 的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//a.js
var name = 'lin';
var age = 13;
var job = 'ninja';

export { name, age, job};

//b.js
import { name, age, job} from './a.js';

console.log(name, age, job);// lin 13 ninja

//或者

//a2.js
export default function () {
console.log('default ');
}

//b2.js
import customName from './a2.js';
customName(); // 'default'

ES6 Module 的特点 (对比 CommonJS)

  • CommonJS 模块是运行时加载,ES6 Module 是编译时输出接口;
  • CommonJS 加载的是整个模块,将所有的接口全部加载进来,ES6 Module 可以单独加载其中的某个接口;
  • CommonJS 输出是值的拷贝,ES6 Module 输出的是值的引用,被输出模块的内部的改变会影响引用的改变;
  • CommonJS this指向当前模块,ES6 Module this指向undefined;


目前浏览器对 ES6 Module 兼容还不太好,我们平时在 webpack 中使用的export/import,会被打包为exports/require

写在后面


这里比较宽泛地把 JavaScript 中的几大模块化规范列举出来,希望借此对 JavaScript 模块化有大致的认识,而未对细节展开具体分析,感兴趣的可以自行探索。


https://juejin.cn/post/6844903983987834888

私有npm仓库搭建与使用

NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种:

允许用户从NPM服务器下载别人编写的第三方包到本地使用。允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

一、为什么需要私有npm仓库

  • 安全:内部源码不能发布到npm上开源,避免业务代码公开
  • 免费:在npm上发布私有内容需要收费
  • 效率:统一管理积累内部开发资源,提高开发效率

二、技术选型

  • sinopia 不考虑,好几年没更新了
  • cnpmjs 太笨重了,使用的方式基于作用域,略显繁琐
  • verdaccio 轻量,使用简单,基于registry的概念,侵入少 👏👏👏👌

三、verdaccio搭建步骤-使用docker-安装在一组开发服务器上

1.下载镜像

docker pull verdaccio/verdaccio

image.png

2.容器启动配置

在 /alidata/www 目录下新建 npm 文件夹,并且创建 docker-compose.yml 配置文件

docker-compose是用来编排容器的,主要作用是将容器配置写在配置文件中,避免每次启动都需要手动运行 docker run 一堆东西。

cd /alidata/www
mkdir npm && touch npm/docker-compose.yml

写入以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: '2'

services:
verdaccio:
image: verdaccio/verdaccio
container_name: verdaccio
networks:
- node-network
environment:
- VERDACCIO_PORT=4873
ports:
- 4873:4873
volumes:
- ./storage:/verdaccio/storage
- ./conf:/verdaccio/conf
- ./plugins:/verdaccio/plugins
networks:
node-network:
driver: bridge

主要做了这些事情

  • 将 npm/storage 目录挂载到容器的 /verdaccio/storage
  • 将 npm/conf 目录挂载到容器的 /verdaccio/conf
  • 将 npm/plugins 目录挂载到容器的 /verdaccio/plugins
  • 容器和主机端口都设置为4873

/alidata/www/npm 文件夹下的内容可以定期git提交到云效,进行备份。主要包括用户资料,包代码。

3.verdaccio配置

配置说明文档 链接

在 /alidata/www/npm/conf 下新建一个文件

touch npm/conf/config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
storage: /verdaccio/storage
auth:
htpasswd:
file: /verdaccio/conf/htpasswd
uplinks:
npmjs:
url: https://registry.npm.taobao.org/
packages:
# scope packages
'@scope/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
proxy: npmjs
logs:
- {type: stdout, format: pretty, level: http}
  • htpasswd 存放的是 npm 用户及密码信息的文件。
  • uplinks 配置了npm替代地址,这里配置了淘宝源。如果在当前仓库找不相应软件包,就到uplinks配置的源获取。

4.启动容器

docker-compose up -d –build

image.png

5.nginx配置

先找到一组服务器的配置文件

ps aux|grep nginx

image.png

在 /rrzuji/nginx/conf/vhosts 下新建配置

1
2
3
4
5
6
7
8
server {
listen 80;
server_name npm.rruzji.net;
location / {
proxy_pass http://127.0.0.1:4873/;
proxy_set_header Host $host;
}
}

6.访问服务 npm.rrzuji.net

出现以下界面说明容器已经正常运行
image.png

7.设置访问权限

尝试新建用户,发现出现500错误
image.png
查看docker日志,提示 permission denied 显然,这是一个文件权限问题,容器无法访问宿主路径。

docker logs –tail 20 verdaccio

image.png
查看文档
image.png

执行下面代码,10001是容器内verdaccio使用的UID,65533是GID

sudo chown -R 10001:65533 /alidata/www/npm/conf/htpasswd
sudo chown -R 10001:65533 /alidata/www/npm/storage

测试功能是否正常
image.png

到这里安装已经结束。接下来介绍一下如何使用以及注意事项。

四、使用方法

1.安装nrm

没有安装nrm的可以先安装nrm,nrm是用来给npm快速换源的工具。安装好之后,新增一个名为 rnpm 的源。

npm i nrm -g
nrm add rnpm http://npm.rrzuji.net

image.png

http://npm.rrzuji.net 相当于私有源+淘宝源的组合,以下统称rnpm。

2.用户注册

image.png

3.用户登录

这个登录表示用户登录到rnpm,会在 userconfig 的文件里新增一条登录信息,不会影响到旧的npm用户授权认证记录。nrm use npm 切换后,就变成了npm用户,再 nrm use rnpm,就变成了 rnpm用户。
image.png
image.png

4.上传软件包

登录后,就可以上传软件包了,进入到软件包目录,执行

npm publish

注意,提交的软件包名需要为私有库的格式(@scope/*),同时也是为了和普通的包区别开来,否则会被拦截。

image.png
image.png

5.项目安装使用

安装私有包

npm install @scope/browser-version-tool-html

安装私有包和公共npm仓库的包,这是一个混用的例子

npm install @scope/browser-version-tool-html axios

image.png

6.删除软件包

npm unpublish

7.更新软件包

更新版本号后,重新发布进行更新

npm publish

web性能优化的15条实用技巧

javascript 在浏览器中运行的性能,可以认为是开发者所面临的最严重的可用性问题。这个问题因为 javascript 的阻塞性而变得复杂,事实上,多数浏览器使用单一进程来处理用户界面和 js 脚本执行,所以同一时刻只能做一件事。js 执行过程耗时越久,浏览器等待响应的时间越长。

加载和执行

一. 提高加载性能

1.IE8,FF,3.5,Safari 4 和 Chrome 都允许并行下载 js 文件,当 script 下载资源时不会阻塞其他 script 的下载。但是 js 下载仍然会阻塞其他资源的下载,如图片。尽管脚本下载不会互相影响,但页面仍然必须等待所有 js 代码下载并执行完才能继续。因此仍然存在脚本阻塞问题. 推荐将所有 js 文件放在 body 标签底部以减少对整个页面的影响。

2. 减少页面外链脚本文件的数量将会提高页面性能:

http 请求会带来额外的开销,因此下载单个 300k 的文件将比下载 10 个 30k 的文件效率更高。

3. 动态脚本加载技术:

无论何时启动下载,文件的下载和执行都不会阻塞页面其他进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function laodScript(url, callback) {
var script = document.createElement("script");
script.type = "text/javascript";

if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
};
} else {
script.onload = function () {
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}

// 使用
loadScript("./a.js", function () {
loadScript("./b.js", function () {
loadScript("./c.js", function () {
console.log("加载完成");
});
});
});
  1. 无阻塞加载类库——LABjs, 使用方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script src="lab.js"></script>
// 链式调用时文件逐个下载,.wait()用来指定文件下载并执行完毕后所调用的函数
$LAB.script('./a.js')
.script('./b.js')
.wait(function(){
App.init();
})

// 为了保证执行顺序,可以这么做,此时a必定在b前执行
$LAB.script('./a.js').wait()
.script('./b.js')
.wait(function(){
App.init();
})

二. 数据存取与 JS 性能

1. 在 js 中,数据存储的位置会对代码整体性能产生重大影响。数据存储共有 4 种方式:字面量,变量,数组项,对象成员。他们有着各自的性能特点。

2. 访问字面量和局部变量的速度最快,相反,访问数组和对象相对较慢

3. 由于局部变量存在于作用域链的起始位置,因此访问局部变量的比访问跨域作用变量更快

4. 嵌套的对象成员会明显影响性能,应尽量避免

5. 属性和方法在原型链位置越深,访问他的速度越慢

6. 通常我们可以把需要多次使用的对象成员,数组元素,跨域变量保存在局部变量中来改善 js 性能

三. DOM 编程

1. 访问 DOM 会影响浏览器性能,修改 DOM 则更耗费性能,因为他会导致浏览器重新计算页面的几何变化。**< 通常的做法是减少访问 DOM 的次数,把运算尽量留在 JS 这一端。**

注:如过在一个对性能要求比较高的操作中更新一段 HTML,推荐使用 innerHTML,因为它在绝大多数浏览器中运行的都很快。但对于大多数日常操作而言,并没有太大区别,所以你更应该根据可读性,稳定性,团队习惯,代码风格来综合决定使用 innerHTML 还是 createElement()

  1. HTML 集合优化

HTML 集合包含了 DOM 节点引用的类数组对象,一直与文档保持连接,每次你需要最新的信息时,都会重复执行查询操作,哪怕只是获取集合里元素的个数。

① 优化一——集合转数组 collToArr

1
2
3
4
5
6
function collToArr(coll) {
for (var i = 0, a = [], len = coll.length; i < len; i++) {
a.push(coll[i]);
}
return a;
}

② 缓存集合 length

③ 访问集合元素时使用局部变量(即将重复的集合访问缓存到局部变量中,用局部变量来操作)

  1. 遍历 DOM

① 使用只返回元素节点的 API 遍历 DOM, 因为这些 API 的执行效率比自己实现的效率更高:

| 属性名 | 被替代属性 |
| children | childNodes |
| childElementCount | childNodes.length |
| firstElementChild | firstChild |
| lastElementChild | lastChild |
| nextElementSibling | nextSibling |
| previousElementSibling | previousSibling |

② 选择器 API——querySelectorAll()

querySelectorAll() 方法使用 css 选择器作为参数并返回一个 NodeList——包含着匹配节点的类数组对象,该方法不会返回 HTML 集合,因此返回的节点不会对应实时文档结构,着也避免了 HTML 集合引起的性能问题。

1
let arr = document.querySelectorAll("div.warning, div.notice > p");

4. 重绘和重排

浏览器在下载完页面的所有组件——html,js,css, 图片等之后,会解析并生成两个内部数据结构—— DOM 树,渲染树. 一旦 DOM 树和渲染树构建完成,浏览器就开始绘制页面元素(paint).

① 重排发生的条件:

添加或删除可见的 DOM 元素位置变化 元素尺寸改变 内容改变 页面渲染器初始化 浏览器窗口尺寸变化 出现滚动条时会触发整个页面的重排

重排必定重绘

5. 渲染树变化的排列和刷新

大多数浏览器通过队列化修改并批量执行来优化重排过程,然而获取布局信息的操作会导致队列强制刷新。

`offsetTop,offsetWidth...``scrollTop,scrollHeight...``clientTop,clientHeight...``getComputedStyle()`

一些优化建议:将设置样式的操作和获取样式的操作分开:

1
2
3
4
5
6
// 设置样式
body.style.color = "red";
body.style.fontSize = "24px";
// 读取样式
let color = body.style.color;
let fontSize = body.style.fontSize;

另外,获取计算属性的兼容写法:

1
2
3
4
function getComputedStyle(el){
var computed = (document.body.currentStyle ? el.currentStyle : document.defaultView.getComputedStyle(el,'');
return computed
}

6. 最小化重绘和重排

①. 批量改变样式

1
2
/* 使用cssText */
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 20px';

②. 批量修改 dom 的优化方案——使元素脱离文档流 - 对其应用多重改变 - 把元素带回文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function appendDataToEl(option){
var targetEl = option.target || document.body,
createEl,
data = option.data || [];

var targetEl_display = targetEl.style.display;
targetEl.style.display = 'none';


var fragment = document.createDocumentFragment();

for(var i=0, max = data.length; i< max; i++){
createEl = document.createElement(option.createEl);
for(var item in data[i]){
if(item.toString() === 'text'){
createEl.appendChild(document.createTextNode(data[i][item]));
continue;
}
if(item.toString() === 'html'){
createEl.innerHTML = item,data[i][item];
continue;
}
createEl.setAttribute(item,data[i][item]);
}

fragment.appendChild(createEl);
}

targetEl.appendChild(fragment);

targetEl.style.display = targetEl_display;
}

// 使用
var wrap = document.querySelectorAll('.wrap')[0];
var data = [
{name: 'xujaing',text: '选景', title: 'xuanfij'},
{name: 'xujaing',text: '选景', title: 'xuanfij'},
{name: 'xujaing',text: '选景', title: 'xuanfij'}
];

appendDataToEl({
target: wrap,
createEl: 'div',
data: data
});

上面的优化方法使用了文档片段:  当我们把文档片段插入到节点中时,实际上被添加的只是该片段的子节点,而不是片段本身。可以使得 dom 操作更有效率。

③. 缓存布局信息

1
2
3
4
5
6
7
//缓存布局信息
let current = el.offsetLeft;
current++;
el.style.left = current + "px";
if (current > 300) {
stop();
}

④. 慎用: hover

如果有大量元素使用: hover, 那么会降低相应速度,CPU 升高

⑤. 使用事件委托(通过事件冒泡实现)来减少事件 处理器的数量,减少内存和处理时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function delegation(e, selector, callback) {
e = e || window.event;
var target = e.target || e.srcElement;

if (
target.nodeName !== selector ||
target.className !== selector ||
target.id !== selector
) {
return;
}

if (typeof e.preventDefault === "function") {
e.preventDefault();
e.stopPropagation();
} else {
e.returnValue = false;
e.cancelBubble = true;
}

callback();
}

四. 算法和流程控制

1. 循环中减少属性查找并反转 (可以提升 50%-60% 的性能)

1
2
3
4
5
6
7
8
9
// for 循环
for(var i=item.length; i--){
process(item[i]);
}
// while循环
var j = item.length;
while(j--){
process(item[i]);
}

2. 使用 Duff 装置来优化循环(该方法在后面的文章中会详细介绍)

3. 基于函数的迭代(比基于循环的迭代慢)

1
2
3
items.forEach(function(value,index,array){
process(value);
})

4. 通常情况下 switch 总比 if-else 快,但是不是最佳方案

五. 字符串和正则表达式

1. 除了 IE 外,其他浏览器会尝试为表达式左侧的字符串分配更多的内存,然后简单的将第二个字符串拷贝到他的末尾,如果在一个循环中,基础字符串位于最左侧,就可以避免重复拷贝一个逐渐变大的基础字符串。2. 使用[\s\S]来匹配任意字符串 3. 去除尾部空白的常用做法:

1
2
3
4
5
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+/, "").replace(/\s\s*$/, "");
};
}

六. 快速响应的用户界面

1. 浏览器的 UI 线程:用于执行 javascript 和更新用户界面的进程。

2. 在 windows 系统中定时器分辨率为 15 毫秒,因此设置小于 15 毫秒将会使 IE 锁定,延时的最小值建议为 25ms.

3. 用延时数组分割耗时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function multistep(steps, args, callback) {
var tasks = steps.concat();

setTimeout(function () {
var task = tasks.shift();
task.apply(null, args || []);

if (tasks.length > 0) {
setTimeout(arguments.callee, 25);
} else {
callback();
}
}, 25);
}

4. 记录代码运行时间批处理任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function timeProcessArray(items, process, callback) {
var todo = item.concat();

setTimeout(function () {
var start = +new Date();

do {
process(todo.shift());
} while (todo.length > 0 && +new Date() - start < 50);

if (todo.length > 0) {
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
}, 25);
}

5. 使用 Web Worker:它引入了一个接口,能使代码运行且不占用浏览器 UI 线程的时间。一个 Worker 由如下部分组成:

① 一个 navigator 对象,包括 appName,appVersion,user Agent 和 platform.

② 一个 location 对象,只读。

③ 一个 self 对象,指向全局 worker 对象

④ 一个 importScripts() 方法,用来加载 worker 所用到的外部 js 文件

⑤ 所有的 ECMAScript 对象。如 object,Array,Date 等

⑥ XMLHttpRequest 构造器

⑦ setTimeout(),setInterval()

⑧ 一个 close() 方法,它能立刻停止 worker 运行

应用场景

  1. 编码 / 解码大字符串

2.  复杂数学运算(包括图像,视屏处理)

3.  大数组排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

var worker = new Worker('code.js');

worker.onmessage = function(event){
console.log(event.data);
}

worker.postMessage('hello');




importScripts('a.js','b.js');

self.onmessage = function(event){
self.postMessage('hello' + event.data);
}

七. ajax 优化

1. 向服务器请求数据的五种方式:

① XMLHttpRequest

② Dynamic script tag insertion 动态脚本注入

③ iframes

④ Comet(基于 http 长连接的服务端推送技术)

⑤ Multipart XHR(允许客户端只用一个 http 请求就可以从服务器向客户端传送多个资源)

2. 单纯向服务端发送数据(beacons 方法)——信标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 唯一缺点是接收到的响应类型是有限的
var url = '/req.php';
var params = ['step=2','time=123'];

(new Image()).src = url + '?' + params.join('&');

// 如果向监听服务端发送回的数据,可以在onload中实现
var beacon = new Image();
beacon.src = ...;
beacon.onload = function(){
...
}
beacon.onerror = function(){
...
}

3.ajax 性能的一些建议

缓存数据

1. 在服务端设置 Expires 头信息确保浏览器缓存多久响应(必须 GET 请求)

2. 客户端把获取到的信息缓存到本地,避免再次请求

八. 编程实践

1. 避免重复工作

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.延迟加载
var a = (x, y) => {
if (x > 4) {
a = 0;
} else {
a = 1;
}
};
// 需要使用时调用
a();

// 2.条件预加载(适用于函数马上执行并频繁操作的场景)
var b = a > 0 ? "4" : "0";

2. 使用 Object/Array 字面量

3. 多用原生方法

九. 构建与部署高性能的 js 应用

1.js 的 http 压缩 当 web 浏览器请求一个资源时,它通常会发送一个 Accept-Encoding HTTP 头来告诉 Web 服务器它支持那种编码转换类型。这个信息主要用来压缩文档以获取更快的下载,从而改善用户体验。Accept-Encoding 可用的值包括:gzip,compress,deflate,identity. 如果 Web 服务器在请求中看到这些信息头,他会选择最合适的编码方式,并通过 Content-Encoding HTTP 头通知 WEB 浏览器它的决定。

2. 使用 H5 离线缓存

3. 使用内容分发网络 CDN

4. 对页面进行性能分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检测代码运行时间
var Timer = {
_data: {},
start: function (key) {
Timer._data[key] = new Date();
},
stop: function (key) {
var time = Timer._data[key];
if (time) {
Timer._data[key] = new Date() - time;
}
console.log(Timer._data[key]);
return Timer._data[key];
},
};

十. 浏览器缓存

1. 添加 Expires 头

2. 使用 cache-control cache-ontrol 详解   浏览器缓存机制

十一. 压缩组件

1.web 客户端可以通过 http 请求中的 Accept-Encoding 头来表示对压缩的支持

1
2
3
4

Accept-Encoding: gzip

Content-Encoding: gzip

2. 压缩能将响应的数据量减少将近 70%,因此可考虑对 html, 脚本,样式,图片进行压缩

十二. 白屏现象的原因

浏览器(如 IE)在样式表没有完全下载完成之前不会呈现页面,导致页面白屏。如果样式表放在页面底部,那么浏览器会花费更长的时间下载样式表,因此会出现白屏,所以最好把样式表放在 head 内。白屏是浏览器对 “无样式闪烁” 的修缮。如果浏览器不采用 “白屏” 机制,将页面内容逐步显示(如 Firefox),则后加载的样式表将导致页面重绘重排,将会承担页面闪烁的风险。

十三. css 表达式使用一次性表达式 (但最好避免 css 表达式)

使用 css 表达式时执行函数重写自身

1
2
3
4
5
6
7
8
// css
p{
background-color: expression(altBgcolor(this))
}
// js
function altBgcolor(el){
el.style.backgroundColor = (new Date()).getHours() % 2 ? "#fff" : "#06c";
}

十四. 减少 DNS 查找

DNS 缓存和 TTL

1.DNS查找可以被缓存起来以提高性能:DNS信息会留在操作系统的DNS缓存中(Microsoft Windows上的“DNS Client服务”,之后对该主机名的请求无需进行过多的查找2.TTL(time to live): 该值告诉客户端可以对记录缓存多久。建议将TTL值设置为一天// 客户端收到DNS记录的平均TTL只有最大TTL值的一半因为DNS解析器返回的时间是其记录的TTL的剩余时间,对于给定的主机名,每次执行DNS查找时接收的TTL值都会变化3.通过使用Keep-Alive和较少的域名来减少DNS查找4.一般建议将页面组件分别放到至少2个,但不要超过4个主机名下复制代码

十五. 避免重定向

这块需要前后端共同配合,对页面路由进行统一规范。

最后

欢迎一起探索打造高性能的 web 应用,在公众号《趣谈前端》加入前端大家庭,和我们一起讨论吧!

汇总系列推荐

欢迎关注下方公众号,获取更多前端知识精粹和学习社群:

回复  学习路径,将获取笔者多年从业经验的前端学习路径的思维导图

回复 lodash,将获得本人亲自翻译的 lodash API 中文思维导图

趣谈前端

Vue、React、小程序、Node

前端算法 | 性能 | 架构 | 安全
https://mp.weixin.qq.com/s?__biz=MzU2Mzk1NzkwOA%3D%3D&chksm=fc531be6cb2492f04475bac0fecbd1a9f9781ca67f35bd30c320964b24a8cbc2ca3d7bbd5345&idx=1&mid=2247483933&scene=21&sn=c2729ef1fd4a28f4707bb923a5ffae79#wechat_redirect