使用 pnpm 替代 yarn, npm

pnpm 的优势不用多说, 懂得自然了解了, 不懂的去官网看下他们的介绍 项目初衷 | pnpm 这里主要的目的是使用 pnpm 替换掉 yarn 和 npm

  • npm 是 node 的包管理工具, 所以我的理解是 node 是基础, 需要 npm 必须要有 node
  • yarn 作为 npm 的一个包, 必须要有 npm , 才能够使用 yarn
  • 然而 node 的版本也可以支持切换, 这样再切换 node 的时候则需要使用到 nvm , 而 nvm 是依赖于 npm 和 yarn 的.

这样造成的依赖循环逃脱不了轮回, 就会导致东西关联较多

在我了解了 pnpm 之后他可以很好的解决以上问题并且还有它自己独到的优势

  • 纯净安装, 依托于 brew
  • 可以管理全局 node , 并支持自动安装
  • 包管理使用软链接方式, 不必在多项目之间重复下载占用磁盘空间

所以就打算用他替换掉 nvm, yarn, npm 这些工具, 仅仅使用一个即可

阅读更多

npm 常见问题

SentryCli 安装太慢

方式 1: ~/.npmrc

找到并编辑 ~/.npmrc

1
$ vim .npmrc
1
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli

方式 2:~/.zshrc

添加环境变量, 这个在安装的时候可以进行识别的

1
2
# @sentry/cli
export SENTRYCLI_CDNURL=https://cdn.npm.taobao.org/dist/sentry-cli

node-sass 安装慢

方式 1: 设置 npm 源
然后在 ~/.npmrc 加入下面内容

1
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

方式 2:安装替代工具

1
2
3
4
5
6
# yarn
$ yarn add node-sass-install

# npm
$ npm i node-sass-install --save-dev
$ npx node-sass-install

这个 node-sass-install 有什么神奇的魔力?其实代码很简单,甚至简单到几乎没有代码。只是在 package.json 的 dependencies 里做了配置(当然因为 npm 比较弱智,所以原项目还是额外增加了两行不太重要的代码):

1
2
3
4
5
{
"dependencies": {
"node-sass": "npm:dart-sass@latest"
}
}

node-pre-gyp ERR! build error 错误的几种处理方式

错误信息:
node-pre-gyp ERR! build error  node-pre-gyp ERR! stack Error: Failed to execute ..

解决方式(可能的解决方法)
删除 node_modules 文件夹, 删除 yarn.lock 或者 package.lock 文件, 然后再重新安装

参考文章

安装 Sentry 进行错误跟踪

  • 性能监控(trace)
  • 错误定位(source-map)

1. 安装并配置命令行工具

安装 sentry-cli

1
2
# 安装 命令行工具 / 全局安装
$ npm install @sentry/cli -g

如果安装比较慢可以查看 Npm FAQ

新建配置文件 .sentryclirc

在工程根目录下新建 .sentryclirc 文件, sentry_cli 会默认读取文件,配置如下:

1
2
3
4
5
6
7
[defaults]
url=http://sentry.demo-domain.com
org='组织名'
project='项目名称'

[auth]
token=AccountApiToken

Token 的获取地址 Account > API> Create New Token

如果这里上传 source-map , 则需要 project:releases权限

获取项目 Dsn

dsn 是项目上报错误的地址, 该 dsn(数据源)告诉 SDK 将事件发送到哪里。如果未提供此值,SDK 将尝试从 SENTRY_DSN 环境变量中读取它。如果该变量也不存在,则 SDK 将不会发送任何事件。请在 sentry.io 中查看“设置”->“项目”->“客户端密钥(DSN)”

2. 使用 Webpack 组件包配置

2.1 Umi/React 示例

1) 项目安装

Sentry 通过在应用程序的运行时中使用 SDK 捕获数据。使用 yarn 或 npm 将 Sentry SDK 添加为依赖项

1
2
$ npm install --save-dev @sentry/webpack-plugin
$ npm install --save @sentry/react

2) 项目对接 Dsn

在项目的根 layout 文件中引入并初始化 Sentry React SDK 和 ErrorBoundary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import * as Sentry from "@sentry/react";

// 这里的版本号使用 env+version 方式, 更方便定位问题
Sentry.init({
dsn: "数据源",
release: "版本号",
environment: "环境,比如生产或者测试",
});

const Base = (props: any) => {
const children = props.children;
return (
<Sentry.ErrorBoundary fallback={() => console.log("An error has occurred")}>{children}</Sentry.ErrorBoundary>
);
};

export default Base;

注: ErrorBoundary 是定义遇到错误时应用程序行为的必要工具。该@sentry/react 软件包公开了一个 ErrorBoundary 组件,该组件自动将 JavaScript 错误从 React 组件树内部发送到 Sentry。像常规 React 组件一样使用 ErrorBoundary . 完成此操作后,Sentry 会自动捕获所有未处理的异常

可以通过在应用程序内的某个地方引发异常来触发开发环境中的第一个事件。例如,呈现一个按钮:

1
return <button onClick={methodDoesNotExist}>Break the world</button>;

3) 部署 source-map

构建项目时,我们会将代码进行压缩混淆,为了在进行日志分析的时候更清楚看到错误发生的原因,我们要对代码进行还原,因此需要 sourcemap 文件,使用 Sentry 的 Webpack 插件在项目构建时会自动上传 sourcemap 文件. 此操作需要身份认证, 这里会使用到之前配置的 sentry-cli 以及其 Token

在 umirc.ts 配置文件中引入并使用

配置文件中增加 SentryWebpackPlugin 和 devtool 配置项, devtool 值设置为”source-map”.

注意 : 这里的版本号使用的是 env + version 的方式, 更方便定位问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
devtool: "source-map",
chainWebpack(config : any, { webpack }: { webpack: any }) {
config.plugin('sentry').use(SentryWebpackPlugin, [
{
include: './dist',
release: `${env}-${version}`,
ignore: ['node_modules'],
org: 'org',
sourceMapReference: true,
},
]);
}
}

2.2) Vite 安装

1) 安装组件

安装组件, 因为官方没有提供 对 vite 的对接, 所以使用三方插件

1
2
$ yarn add vite-plugin-sentry
$ yarn add @sentry/vue

2) 项目对接 dsn

在主项目入口引入并配置

1
2
3
4
5
6
7
8
9
10
11
import * as Sentry from "@sentry/vue";
import { appMode, appVersion, sentryDsnUrl } from "@/utils/conf";

const app = createApp(App);

Sentry.init({
app,
dsn: sentryDsnUrl,
release: `${appMode}-${appVersion}`,
environment: appMode,
});

3) 部署 source-map

这里需要注意

  1. 开启 sourcemap 才可以上传

build.sourcemap 这个选项是开启 source map 的, 根据实际情况来进行开关

  1. sourceMap 的 UrlPrefix 需要填写正确

plugins.viteSentry.sourceMaps.urlPrefix (为了标识路径), 是根据 ~(访问目录代表网站, ~/ 代表项目部署的目录), 如果是子目录则需要在填写子目录的位置, 例如我的部署目录是 webapp/, 这里应该配置 ~/webapp/assets

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
// vite.config.ts
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
...
plugins: [
...
// 使用 NODE_ENV, production 时候才会执行错误搜集
viteSentry({
debug: true,
url: process.env.VITE_SENTRY_URL,
authToken: process.env.VITE_SENTRY_TOKEN,
org: 'dadi',
project: 'proj',
release: `${mode}-${pkgJson.version}`,
deploy: {
env: `${mode}`
},
setCommits: {
auto: false
},
sourceMaps: {
include: [
`build/proj-${mode}/assets`
],
ignore: ['node_modules'],
urlPrefix: '~/assets'
}
})
],
build: {
outDir: `build/proj-${mode}`,
sourcemap: (mode === 'prod' || mode === 'dev'),
...
},
...
}
});

这样在执行打包的时候即可进行 source-map 的上传

1
2
3
4
5
6
7
...
~/assets/vant.31618c38.js (sourcemap at vant.31618c38.js.map)
~/assets/vue3-clipboard-es.f4fdbc1a.js (sourcemap at vue3-clipboard-es.f4fdbc1a.js.map)
Source Maps
~/assets/Auto.f5325f0e.js.map
~/assets/Buy.cca58b67.js.map
...

3 Sentry 性能监控

性能监控是搜集当前项目性能的一个组件工具, 可选安装, 这里简要介绍

3.1 安装跟踪软件包

1
$ yarn add @sentry/tracing

3.2 配置

通过以下两种方式在应用中启用性能监控:

  • tracesSampleRate 统一采样率,设置为 0 和之间的数字 1。(例如,20% 的 transactions 抽样率,设置 tracesSampleRate 为 0.2。)
  • tracesSampler 基于 transactions 本身及其捕获的上下文动态控制采样率(两者同时配置时,优先级高)

1) 自动检测

@sentry/tracing 提供了 BrowserTracing 集成,该 BrowserTracing 集成会为每个页面加载和导航事件生成一个新的事务(transactions),并为每一个 XMLHttpRequest 或 fetch 创建一个 child span(子跨度)。
要启用此自动跟踪,需要在 SDK 配置选项中添加 BrowserTracing integrations 配置.

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
import * as Sentry from "@sentry/browser";
import { Integrations } from "@sentry/tracing"; // Must import second
Sentry.init({
app,
dsn: sentryDsnUrl,
release: `${appMode}-${appVersion}`,
environment: appMode,
integrations: [
new Integrations.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
tracingOrigins: ["domain.com", "dev.domain.com", /^\//],
}),
],
/**
* 线上环境捕捉 1%, 开发环境捕捉完整
* https://docs.sentry.io/platforms/javascript/guides/vue/configuration/sampling/#setting-a-sampling-function
* @param samplingContext
*/
tracesSampler: (samplingContext) => {
if (appMode === "prod") {
return 0.01;
} else {
return 1;
}
},
});

配置选项

  • tracingOrigins
    tracingOrigins 默认值是[‘localhost’, /^//]。
    JavaScript SDK 将 sentry-trace 标头附加到所有输出 XHR / fetch 请求中,这些请求的目的地在列表中包含字符串或与列表中的正则表达式匹配。如果您的前端向其他域发出请求,则需要在其中添加它,
    以将 sentry-trace 标头传播到后端服务,这是将事务链接在一起作为单个跟踪的一部分所必需的。该 tracingOrigins 选项与整个请求 URL 匹配,而不仅仅是域。使用更严格的正则表达式来匹配 URL 的某些部分,可以确保请求不必 sentry-trace 附加标头。
    例如
    前端应用程序 example.com
    后端服务 api.example.com
    前端应用程序对后端进行 API 调用
    因此,该选项需要这样配置: new Integrations.BrowserTracing({tracingOrigins: [‘api.example.com’]})
    现在发出的 XHR / fetch 请求 api.example.com 将 sentry-trace 附加标头
    您将需要配置 Web 服务器 CORS 以允许 sentry-trace 标头。该配置看起来像”Access-Control-Allow-Headers: sentry-trace”,但是该配置取决于您的设置。如果您不允许 sentry-trace 标题,则该请求可能会被阻止。
  • beforeNavigate
    对于 pageload 和 navigation 事务,BrowserTracing 集成使用浏览器的 window.locationAPI 生成事务名称。需要自定义名称可以提供一个 beforeNavigate 选项,使用此选项可以修改事务名称以使其更通用,例如,命名为 GET /users/12312012 的事务和 GET /users/11212012 都可以重命名 GET /users/:userid,以便它们可以组合在一起。
  • shouldCreateSpanForRequest
    此功能可用于过滤掉不需要的跨度,例如 XHR 的运行状况检查或类似的检查。默认情况下,shouldCreateSpanForRequest 已经过滤掉了除 tracingOrigins 之外的所有.

2) 手动检测

要手动检测代码的某些区域,可以创建事务来捕获它们。
这是适用于所有的 JavaScript 的 SDK(包括后端和前端)和独立工作的的 Express,Http 和 BrowserTracing 集成。

1
2
3
4
5
const transaction = Sentry.startTransaction({ name: "test-transaction" });
const span = transaction.startChild({ op: "functionX" }); // This function returns a Span
// functionCallX
span.finish(); // Remember that only finished spans will be sent with the transaction
transaction.finish(); // Finishing the transaction will send it to Sentry

例如,如果要为页面上的用户交互创建事务,请执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Let's say this function is invoked when a user clicks on the checkout button of your shop
shopCheckout() {
// This will create a new Transaction for you
const transaction = Sentry.startTransaction('shopCheckout');
// set the transaction on the scope so it picks up any errors
hub.configureScope(scope => scope.setSpan(transaction));

// Assume this function makes an xhr/fetch call
const result = validateShoppingCartOnServer();

const span = transaction.startChild({
data: {
result
},
op: 'task',
description: `processing shopping cart result`,
});
processAndValidateShoppingCart(result);
span.finish();

transaction.finish();
}

此示例将向 shopCheckoutSentry 发送事务。交易将包含一个 task 跨度,用于衡量 processAndValidateShoppingCart 花费了多长时间。最后,调用 transaction.finish()完成交易并将其发送给 Sentry。
在为异步操作创建跨度时,还可以利用 Promise。但是跨度必须在 transaction.finish()调用之前完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function processItem(item, transaction) {
const span = transaction.startChild({
op: "http",
description: `GET /items/:item-id`,
});

return new Promise((resolve, reject) => {
http.get(`/items/${item.id}`, response => {
response.on("data", () => {});
response.on("end", () => {
span.setTag("http.status_code", response.statusCode);
span.setData("http.foobarsessionid", getFoobarSessionid(response));
span.finish();
resolve(response);
});
});
});
}

4. QA

1. 出现错误 (http status: 413)

error: API request failed
caused by: sentry reported an error: unknown error (http status: 413)

上传的 sourceMap 太多,有可能会导致 413 Request Entity Too Large 错误, 这里需要更改后端对上传的大小限制即可, 如果是 nginx, 需要在指定的配置段中增加

1
client_max_body_size 20m;

参考

基于 Vue 的最佳实践

本项目是基于 Web 开发最佳实践 的扩展

解决 globalThis 的问题

参考 : 解决浏览器端 globalThis is not defined 报错

解决方案
<head> 中加入

1
2
3
<script>
this.globalThis || (this.globalThis = this);
</script>

在低版本手机上访问页面需要对代码打包进行 es2015 转义

vite.config.ts

1
2
3
4
5
// ...
build: {
target: "es2015";
}
// ...

组件的自动加载

自动加载的弱项
如果组件使用自动加载也可以, 但是需要自动引入 app.use(Comp) 这种方式, 并在配置文件中加入 components

全部加载的弱项
全部加载会在打包的时候 minify 会解析所有的 css 样式, 导致时间过长 (10min+)

所以组件使用 unplugin-vue-components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver, ElementPlusResolver, VantResolver } from "unplugin-vue-components/resolvers";
export default defineConfig(({ mode }) => {
return {
// ...
plugins: [
// ...
Components({
resolvers: [AntDesignVueResolver(), ElementPlusResolver(), VantResolver()],
dts: true,
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
}),
// ...
],
// ...
};
});

移除 console 日志

打包启用 terser, 配置 tersor 的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
export default defineConfig(({ mode }) => {
return {
build: {
// ...
minify: "terser",
terserOptions: {
compress: {
drop_console: true,
},
},
},
};
});

参考

favicon 简要说明

个人认为, 如果不是对 icon 要求特别高可以在 header 中放置一个 png 即可

1
<link rel="icon" type="image/png" href="/assets/icon/favicon.png" />

浏览器中使用

favicon.ico

ico 格式是多个不同尺寸图标(最常使用的是 16×16 和 32×32,Mac OS X 有时使用 64×64 和 128×128)整合在一起的, 某些浏览器在访问网站的时候会自动发出一个请求给到后端, 来请求这个图标, 一般这个图标会放置在根目录下的 favicon.ico 文件

资料: Favicon

例如 : Firefox

所以我们建议设置一个图标的位置, 一边我们更好的管理

1
<link rel="icon" href="./assets/icon/favicon.ico" />

png icons

对于现代浏览器, 他们同样也支持 png 的图标, 以便于显示在不同使用场景, 例如我的导航

例如 : Opera(用户使用场景较少)

这样, 这个图标的大小是不满足使用的, 我们可以使用

1
<link rel="icon" type="image/png" href="/assets/icon/favicon.png" />

在填写了 png 图片后, 取消掉 favicon.ico 图标代码, Edge (V8 引擎) 可以正常显示图标

而且在 firefox 浏览器中也未发现 ico 格式的请求

并且在 safri 浏览器中也是支持的

Android : 小米

iOS :

当然对于不同的图标, 在不同的浏览场景下也会区分不同的分辨率, 在 icon 属性中可以定义不同的类型, 以便浏览器选择最合适的图片大小 链接类型, sizes

定义一个在用户界面上代表这个页面的资源,通常是一个图标(包括声音和图像)
media, type and sizes 属性允许浏览器选择其上下文中最合适的图标.如果多种资源符合条件,浏览器会选择最后一个。

在树型序列中,由于这些属性只是提示, 并且这这些资源在进一步检查时可能是不适合的,浏览器可能选择另一个适合的.

Note: 苹果 iOS 不支持此链接类型, 也不支持 sizes 属性, 就像其他移动端浏览器一样,为了 Web Clip 或者启动点位符选择一个页面图标。分别可使用不是标准方法的 apple-touch-icon 和 apple-touch-startup-image 替代.

在之前,经常可以看到 shortcut,但他不是标准的,应该不再使用。

代码

github : https://raw.githubusercontent.com/imvkmark/html-get-started/master/link/favicon.html
demo : https://wulicode.com/demo/html/link/favicon.html

Favicon 的生成

推荐网站 : Favicon Generator. For real.

参考

[转]加快您的网站的最佳实践(Best Practices for Speeding Up Your Web Site)

又名 : 雅虎网站优化 30 条军规
原文地址 : Best Practices for Speeding Up Your Web Site

Yahoo!的 Exceptional Performance 团队为改善 Web 性能带来最佳实践。他们为此进行了 一系列的实验、开发了各种工具、写了大量的文章和博客并在各种会议上参与探讨。最佳实践的核心就是旨在提高网站性能。

Excetional Performance 团队总结出了一系列可以提高网站速度的方法。可以分为 7 大类 35 条。包括内容、服务器、cookie、CSS、JavaScript、图片、移动应用等七部分。

一、内容部分

1、尽量减少 HTTP 请求次数

终端用户响应的时间中,有 80%用于下载各项内容。这部分时间包括下载页面中的图像、 样式表、脚本、Flash 等。通过减少页面中的元素可以减少 HTTP 请求的次数。这是提高 网页速度的关键步骤。

减少页面组件的方法其实就是简化页面设计。那么有没有一种方法既能保持页面内容的 丰富性又能达到加快响应时间的目的呢?这里有几条减少 HTTP 请求次数同时又可能保 持页面内容丰富的技术。

  • 合并文件是通过把所有的脚本放到一个文件中来减少 HTTP 请求的方法,如可以简单地 把所有的 CSS 文件都放入一个样式表中。当脚本或者样式表在不同页面中使用时需要做 不同的修改,这可能会相对麻烦点,但即便如此也要把这个方法作为改善页面性能的重要一步。
  • CSS Sprites 是减少图像请求的有效方法。把所有的背景图像都放到一个图片文件中, 然后通过 CSS 的 background-image 和 background-position 属性来显示图片的不同部分;
  • 图片地图是把多张图片整合到一张图片中。虽然文件的总体大小不会改变,但是可以减少 HTTP 请求次数。图片地图(<map>)只有在图片的所有组成部分在页面中是紧挨在一起的时候 才能使用,如导航栏。确定图片的坐标和可能会比较繁琐且容易出错,同时使用图片地 图导航也不具有可读性,因此不推荐这种方法;
  • 内联图像是使用 data:URL scheme 的方法把图像数据加载页面中。这可能会增加页面的 大小。把内联图像放到样式表(可缓存)中可以减少 HTTP 请求同时又避免增加页面文件 的大小。但是内联图像现在还没有得到主流浏览器的支持。

减少页面的 HTTP 请求次数是你首先要做的一步。这是改进首次访问用户等待时间的最重要的方法。如同 Tenni Theurer 的他的博客 Browser Cahe Usage – Exposed!中所说, HTTP 请求在无缓存情况下占去了 40%到 60%的响应时间。让那些初次访问你网站的人获 得更加快速的体验吧!

2、减少 DNS 查找次数

域名系统(DNS)提供了域名和 IP 的对应关系,就像电话本中人名和他们的电话号码的 关系一样。当你在浏览器地址栏中输入www.800hr.com时,DNS解析服务器就会返回这个 域名对应的 IP 地址。DNS 解析的过程同样也是需要时间的。一般情况下返回给定域名对 应的 IP 地址会花费 20 到 120 毫秒的时间。而且在这个过程中浏览器什么都不会做直到 DNS 查找完毕。

缓存 DNS 查找可以改善页面性能。这种缓存需要一个特定的缓存服务器,这种服务器一 般属于用户的 ISP 提供商或者本地局域网控制,但是它同样会在用户使用的计算机上产 生缓存。DNS 信息会保留在操作系统的 DNS 缓存中(微软 Windows 系统中 DNS Client Service)。大多数浏览器有独立于操作系统以外的自己的缓存。由于浏览器有自己的 缓存记录,因此在一次请求中它不会受到操作系统的影响。

InternetExplorer 默认情况下对 DNS 查找记录的缓存时间为 30 分钟,它在注册表中的 键值为 DnsCacheTimeout。Firefox 对 DNS 的查找记录缓存时间为 1 分钟,它在配置文 件中的选项为 network.dnsCacheExpiration(Fasterfox 把这个选项改为了 1 小时)。

当客户端中的 DNS 缓存都为空时(浏览器和操作系统都为空),DNS 查找的次数和页面 中主机名的数量相同。这其中包括页面中 URL、图片、脚本文件、样式表、Flash 对象 等包含的主机名。减少主机名的数量可以减少 DNS 查找次数。

减少主机名的数量还可以减少页面中并行下载的数量。减少 DNS 查找次数可以节省响应 时间,但是减少并行下载却会增加响应时间。我的指导原则是把这些页面中的内容分割 成至少两部分但不超过四部分。这种结果就是在减少 DNS 查找次数和保持较高程度并行 下载两者之间的权衡了。

3、避免跳转

跳转是使用 301 和 302 代码实现的。下面是一个响应代码为 301 的 HTTP 头:

1
2
3
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html

浏览器会把用户指向到 Location 中指定的 URL。头文件中的所有信息在一次跳转中都是 必需的,内容部分可以为空。不管他们的名称,301 和 302 响应都不会被缓存除非增加 一个额外的头选项,如 Expires 或者 Cache-Control 来指定它缓存。<meta />元素的刷新标签和 JavaScript 也可以实现 URL 的跳转,但是如果你必须要跳转的时候,最好的方法就是使用标准的 3XXHTTP 状态代码,这主要是为了确保“后退”按钮可以正确地使用。

但是要记住跳转会降低用户体验。在用户和 HTML 文档中间增加一个跳转,会拖延页面中所有元素的显示,因为在 HTML 文件被加载前任何文件(图像、Flash 等)都不会被下载。

有一种经常被网页开发者忽略却往往十分浪费响应时间的跳转现象。这种现象发生在当 URL 本该有斜杠(/)却被忽略掉时。例如,当我们要访问 http://astrology.yahoo.com/astrology 时,实际上返回的是一个包含 301 代码的跳 转,它指向的是 http://astrology.yahoo.com/astrology/ (注意末尾的斜杠)。在 Apache 服务器中可以使用 Alias 或者 mod_rewrite 或者 the DirectorySlash 来避免。

连接新网站和旧网站是跳转功能经常被用到的另一种情况。这种情况下往往要连接网站 的不同内容然后根据用户的不同类型(如浏览器类型、用户账号所属类型)来进行跳转。 使用跳转来实现两个网站的切换十分简单,需要的代码量也不多。尽管使用这种方法对 于开发者来说可以降低复杂程度,但是它同样降低用户体验。一个可替代方法就是如果 两者在同一台服务器上时使用 Alias 和 mod_rewrite 和实现。如果是因为域名的不同而 采用跳转,那么可以通过使用 Alias 或者 mod_rewirte 建立 CNAME(保存一个域名和另 外一个域名之间关系的 DNS 记录)来替代。

1
2
3
location ~ .*\.(js|css)?$ {
expires 12h;
}

4、可缓存的 AJAX

Ajax 经常被提及的一个好处就是由于其从后台服务器传输信息的异步性而为用户带来 的反馈的即时性。但是,使用 Ajax 并不能保证用户不会在等待异步的 JavaScript 和 XML 响应上花费时间。在很多应用中,用户是否需要等待响应取决于 Ajax 如何来使用。例 如,在一个基于 Web 的 Email 客户端中,用户必须等待 Ajax 返回符合他们条件的邮件 查询结果。记住一点,“异步”并不异味着“即时”,这很重要。

为了提高性能,优化 Ajax 响应是很重要的。提高 Ajxa 性能的措施中最重要的方法就是使 响应具有可缓存性,具体的讨论可以查看 Add an Expires or a Cache-Control Header。 其它的几条规则也同样适用于 Ajax: Gizp压缩文件减少DNS查找次数精简JavaScript避免跳转配置ETags

让我们来看一个例子:一个 Web2.0 的 Email 客户端会使用 Ajax 来自动完成对用户地址 薄的下载。如果用户在上次使用过 Email web 应用程序后没有对地址薄作任何的修改,而且 Ajax 响应通过 Expire 或者 Cacke-Control 头来实现缓存,那么就可以直接从上一 次的缓存中读取地址薄了。必须告知浏览器是使用缓存中的地址薄还是发送一个新的请 求。这可以通过为读取地址薄的 Ajax URL 增加一个含有上次编辑时间的时间戳来实现,例如,&t=11900241612 等。如果地址薄在上次下载后没有被编辑过,时间戳就不变,则 从浏览器的缓存中加载从而减少了一次 HTTP 请求过程。如果用户修改过地址薄,时间 戳就会用来确定新的 URL 和缓存响应并不匹配,浏览器就会重要请求更新地址薄。 即使你的 Ajxa 响应是动态生成的,哪怕它只适用于一个用户,那么它也应该被缓存起 来。这样做可以使你的 Web2.0 应用程序更加快捷。

5、延迟加载内容

你可以仔细看一下你的网页,问问自己“哪些内容是页面呈现时所必需首先加载的?哪 些内容和结构可以稍后再加载?

把整个过程按照 onload 事件分隔成两部分,JavaScript 是一个理想的选择。例如,如果 你有用于实现拖放和动画的 JavaScript,那么它就以等待稍后加载,因为页面上的拖放 元素是在初始化呈现之后才发生的。其它的例如隐藏部分的内容(用户操作之后才显现 的内容)和处于折叠部分的图像也可以推迟加载

工具可以节省你的工作量:YUI Image Loader 可以帮你推迟加载折叠部分的图片,YUI Get utility 是包含 JS 和 CSS 的便捷方法。比如你可以打开 Firebug 的 Net 选项卡看一下 Yahoo 的首页。

当性能目标和其它网站开发实践一致时就会相得益彰。这种情况下,通过程序提高网站 性能的方法告诉我们,在支持 JavaScript 的情况下,可以先去除用户体验,不过这要保 证你的网站在没有 JavaScript 也可以正常运行。在确定页面运行正常后,再加载脚本来 实现如拖放和动画等更加花哨的效果。

6、预加载

预加载和后加载看起来似乎恰恰相反,但实际上预加载是为了实现另外一种目标。预加 载是在浏览器空闲时请求将来可能会用到的页面内容(如图像、样式表和脚本)。使用 这种方法,当用户要访问下一个页面时,页面中的内容大部分已经加载到缓存中了,因 此可以大大改善访问速度。

下面提供了几种预加载方法:
无条件加载:触发 onload 事件时,直接加载额外的页面内容。以 Google.com 为例,你 可以看一下它的 spirit image 图像是怎样在 onload 中加载的。这个 spirit image 图 像在 google.com 主页中是不需要的,但是却可以在搜索结果页面中用到它。 有条件加载:根据用户的操作来有根据地判断用户下面可能去往的页面并相应的预加载 页面内容。在 search.yahoo.com 中你可以看到如何在你输入内容时加载额外的页面内 容。

有预期的加载:载入重新设计过的页面时使用预加载。这种情况经常出现在页面经过重 新设计后用户抱怨“新的页面看起来很酷,但是却比以前慢”。问题可能出在用户对于 你的旧站点建立了完整的缓存,而对于新站点却没有任何缓存内容。因此你可以在访问 新站之前就加载一部内容来避免这种结果的出现。在你的旧站中利用浏览器的空余时间 加载新站中用到的图像的和脚本来提高访问速度。

7、减少 DOM 元素数量

一个复杂的页面意味着需要下载更多数据,同时也意味着 JavaScript 遍历 DOM 的效率越 慢。比如当你增加一个事件句柄时在 500 和 5000 个 DOM 元素中循环效果肯定是不一样的。 大量的 DOM 元素的存在意味着页面中有可以不用移除内容只需要替换元素标签就可以精 简的部分。你在页面布局中使用表格了吗?你有没有仅仅为了布局而引入更多的<div> 元素呢?也许会存在一个适合或者在语意是更贴切的标签可以供你使用。

YUI CSS utilities 可以给你的布局带来巨大帮助:grids.css 可以帮你实现整体布局, font.css 和 reset.css 可以帮助你移除浏览器默认格式。它提供了一个重新审视你页面 中标签的机会,比如只有在语意上有意义时才使用<div>,而不是因为它具有换行效果 才使用它。

DOM 元素数量很容易计算出来,只需要在 Firebug 的控制台内输入:

1
document.getElementsByTagName(’*').length

那么多少个 DOM 元素算是多呢?这可以对照有很好标记使用的类似页面。比如 Yahoo!主 页是一个内容非常多的页面,但是它只使用了 700 个元素(HTML 标签)。

8、根据域名划分页面内容

把页面内容划分成若干部分可以使你最大限度地实现平行下载。由于 DNS 查找带来的影 响你首先要确保你使用的域名数量在 2 个到 4 个之间。例如,你可以把用到的 HTML 内容 和动态内容放在www.example.org上,而把页面各种组件(图片、脚本、CSS)分别存放 在 statics1.example.org 和 statics.example.org 上。

你可在 Tenni Theurer 和 Patty Chi 合写的文章 Maximizing Parallel Downloads in the Carpool Lane 找到更多相关信息。

9、使 iframe 的数量最小

ifrmae 元素可以在父文档中插入一个新的 HTML 文档。了解 iframe 的工作理然后才能更 加有效地使用它,这一点很重要。
<iframe>优点:
• 解决加载缓慢的第三方内容如图标和广告等的加载问题
• Security sandbox
• 并行加载脚本
<iframe>的缺点:
• 即时内容为空,加载也需要时间 • 会阻止页面加载
• 没有语意

10、不要出现 404 错误

HTTP 请求时间消耗是很大的,因此使用 HTTP 请求来获得一个没有用处的响应(例如 404 没有找到页面)是完全没有必要的,它只会降低用户体验而不会有一点好处。 有些站点把 404 错误响应页面改为“你是不是要找***”,这虽然改进了用户体验但是 同样也会浪费服务器资源(如数据库等)。最糟糕的情况是指向外部 JavaScript 的链 接出现问题并返回 404 代码。首先,这种加载会破坏并行加载;其次浏览器会把试图在 返回的 404 响应内容中找到可能有用的部分当作 JavaScript 代码来执行。

二、服务器端

除了在网站在内容上的改进外,在网站服务器端上也有需要注意和改进的地方

1、使用内容分发网络 (CDN)

用户与你网站服务器的接近程度会影响响应时间的长短。把你的网站内容分散到多个、 处于不同地域位置的服务器上可以加快下载速度。但是首先我们应该做些什么呢? 按地域布置网站内容的第一步并不是要尝试重新架构你的网站让他们在分发服务器上 正常运行。根据应用的需求来改变网站结构,这可能会包括一些比较复杂的任务,如在 服务器间同步 Session 状态和合并数据库更新等。要想缩短用户和内容服务器的距离, 这些架构步骤可能是不可避免的。

要记住,在终端用户的响应时间中有 80%到 90%的响应时间用于下载图像、样式表、脚 本、Flash 等页面内容。这就是网站性能黄金守则。和重新设计你的应用程序架构这样 比较困难的任务相比,首先来分布静态内容会更好一点。这不仅会缩短响应时间,而且 对于内容分发网络来说它更容易实现。

内容分发网络(Content Delivery Network,CDN)是由一系列分散到各个不同地理位 置上的 Web 服务器组成的,它提高了网站内容的传输速度。用于向用户传输内容的服务 器主要是根据和用户在网络上的靠近程度来指定的。例如,拥有最少网络跳数(network hops)和响应速度最快的服务器会被选定。

一些大型的网络公司拥有自己的 CDN,但是使用像 Akamai Technologies,Mirror Image Internet, 或者 Limelight Networks 这样的 CDN 服务成本却非常高。对于刚刚起步的企 业和个人网站来说,可能没有使用 CDN 的成本预算,但是随着目标用户群的不断扩大和 更加全球化,CDN 就是实现快速响应所必需的了。以 Yahoo 来说,他们转移到 CDN 上的网 站程序静态内容节省了终端用户 20%以上的响应时间。使用 CDN 是一个只需要相对简单地 修改代码实现显著改善网站访问速度的方法。

2、为文件头指定 Expires 或 Cache-Control

这条守则包括两方面的内容: 对于静态内容:设置文件头过期时间 Expires 的值为“Never expire”(永不过期) 对于动态内容:使用恰当的 Cache-Control 文件头来帮助浏览器进行有条件的请求 网页内容设计现在越来越丰富,这就意味着页面中要包含更多的脚本、样式表、图片和 Flash。第一次访问你页面的用户就意味着进行多次的 HTTP 请求,但是通过使用 Expires 文件头就可以使这样内容具有缓存性。它避免了接下来的页面访问中不必要的 HTTP 请求。 Expires 文件头经常用于图像文件,但是应该在所有的内容都使用他,包括脚本、样式 表和 Flash 等。 浏览器(和代理)使用缓存来减少 HTTP 请求的大小和次数以加快页面访问速度。Web 服 务器在 HTTP 响应中使用 Expires 文件头来告诉客户端内容需要缓存多长时间。下面这个 例子是一个较长时间的 Expires 文件头,它告诉浏览器这个响应直到 2010 年 4 月 15 日 才过期。

1
Expires: Thu, 15 Apr 2010 20:00:00 GMT

如果你使用的是 Apache 服务器,可以使用 ExpiresDefault 来设定相对当前日期的过期时 间。下面这个例子是使用 ExpiresDefault 来设定请求时间后 10 年过期的文件头:

1
ExpiresDefault “access plus 10 years”

要切记,如果使用了 Expires 文件头,当页面内容改变时就必须改变内容的文件名。依 Yahoo!来说我们经常使用这样的步骤:在内容的文件名中加上版本号,如 yahoo_2.0.6.js

使用 Expires 文件头只有会在用户已经访问过你的网站后才会起作用。当用户首次访问 你的网站时这对减少 HTTP 请求次数来说是无效的,因为浏览器的缓存是空的。因此这种 方法对于你网站性能的改进情况要依据他们“预缓存”存在时对你页面的点击频率 (“预缓存”中已经包含了页面中的所有内容)。Yahoo!建立了一套测量方法,我们发 现所有的页面浏览量中有 75~85%都有“预缓存”。通过使用 Expires 文件头,增加了缓 存在浏览器中内容的数量,并且可以在用户接下来的请求中再次使用这些内容,这甚至 都不需要通过用户发送一个字节的请求。

3、Gzip 压缩文件内容

网络传输中的 HTTP 请求和应答时间可以通过前端机制得到显著改善。的确,终端用户的 带宽、互联网提供者、与对等交换点的靠近程度等都不是网站开发者所能决定的。但是 还有其他因素影响着响应时间。通过减小 HTTP 响应的大小可以节省 HTTP 响应时间。 从 HTTP/1.1 开始,web 客户端都默认支持 HTTP 请求中有 Accept-Encoding 文件头的压缩格 式:

1
Accept-Encoding: gzip, deflate

如果 web 服务器在请求的文件头中检测到上面的代码,就会以客户端列出的方式压缩响 应内容。Web 服务器把压缩方式通过响应文件头中的 Content-Encoding 来返回给浏览器。

1
Content-Encoding: gzip

Gzip 是目前最流行也是最有效的压缩方式。这是由 GNU 项目开发并通过 RFC 1952 来标准 化的。另外仅有的一个压缩格式是 deflate,但是它的使用范围有限效果也稍稍逊色。 Gzip 大概可以减少 70%的响应规模。目前大约有 90%通过浏览器传输的互联网交换支持 gzip 格式。如果你使用的是 Apache,gzip 模块配置和你的版本有关:Apache 1.3 使 用 mod_zip,而 Apache 2.x 使用 moflate。

浏览器和代理都会存在这样的问题:浏览器期望收到的和实际接收到的内容会存在不匹 配的现象。幸好,这种特殊情况随着旧式浏览器使用量的减少在减少。Apache 模块会通 过自动添加适当的 Vary 响应文件头来避免这种状况的出现。

服务器根据文件类型来选择需要进行 gzip 压缩的文件,但是这过于限制了可压缩的文件。 大多数 web 服务器会压缩 HTML 文档。对脚本和样式表进行压缩同样也是值得做的事情, 但是很多 web 服务器都没有这个功能。实际上,压缩任何一个文本类型的响应,包括 XML 和 JSON,都值得的。图像和 PDF 文件由于已经压缩过了所以不能再进行 gzip 压缩。如果 试图 gizp 压缩这些文件的话不但会浪费 CPU 资源还会增加文件的大小。 Gzip 压缩所有可能的文件类型是减少文件体积增加用户体验的简单方法。

4、配置 ETag

Entity tags(ETags)(实体标签)是 web 服务器和浏览器用于判断浏览器缓存中的内 容和服务器中的原始内容是否匹配的一种机制(“实体”就是所说的“内容”,包括图 片、脚本、样式表等)。增加 ETag 为实体的验证提供了一个比使用“last-modified date (上次编辑时间)”更加灵活的机制。Etag 是一个识别内容版本号的唯一字符串。唯一 的格式限制就是它必须包含在双引号内。原始服务器通过含有 ETag 文件头的响应指定页 面内容的 ETag。

1
2
3
4
HTTP/1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
ETag: “10c24bc-4ab-457e1c1f”
Content-Length: 12195

稍后,如果浏览器要验证一个文件,它会使用 If-None-Match 文件头来把 ETag 传回给原 始服务器。在这个例子中,如果 ETag 匹配,就会返回一个 304 状态码,这就节省了 12195 字节的响应。

1
2
3
4
5
GET /i/yahoo.gif HTTP/1.1
Host: us.yimg.com
If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT
If-None-Match: “10c24bc-4ab-457e1c1f”
HTTP/1.1 304 Not Modified

ETag 的问题在于,它是根据可以辨别网站所在的服务器的具有唯一性的属性来生成的。 当浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时 ETag 就会不 匹配,这种情况对于使用服务器组和处理请求的网站来说是非常常见的。

默认情况下, Apache 和 IIS 都会把数据嵌入 ETag 中,这会显著减少多服务器间的文件验证冲突。 Apache 1.3 和 2.x 中的 ETag 格式为 inode-size-timestamp。即使某个文件在不同的服务 器上会处于相同的目录下,文件大小、权限、时间戳等都完全相同,但是在不同服务器 上他们的内码也是不同的。

不同的服务器上的 Apache 和 IIS 即使对于完全 相同的内容产生的 ETag 在也不相同,用户并不会接收到一个小而快的 304 响应;相反他们会接收一个正常的 200 响应并下载全部内容。如果你的网站只放在一台服务器上,就不会存在这个问题。但是如果你的网站是架设在多个服务器上,并且使用 Apache 和 IIS 产生默认的 ETag 配置,你的用户获得页面就会相对慢一点,服务器会传输更多的内容, 占用更多的带宽,代理也不会有效地缓存你的网站内容。即使你的内容拥有 Expires 文件头,无论用户什么时候点击“刷新”或者“重载”按钮都会发送相应的 GET 请求。 如果你没有使用 ETag 提供的灵活的验证模式,那么干脆把所有的 ETag 都去掉会更好。 Last-Modified 文件头验证是基于内容的时间戳的。去掉 ETag 文件头会减少响应和下次 请求中文件的大小。微软的这篇支持文稿讲述了如何去掉 ETag。在 Apache 中,只需要在 配置文件中简单添加下面一行代码就可以了:

1
FileETag none

5、尽早刷新输出缓冲

当用户请求一个页面时,无论如何都会花费 200 到 500 毫秒用于后台组织 HTML 文件。 在这期间,浏览器会一直空闲等待数据返回。在 PHP 中,你可以使用 flush()方法,它 允许你把已经编译的好的部分 HTML 响应文件先发送给浏览器,这时浏览器就会可以下 载文件中的内容(脚本等)而后台同时处理剩余的 HTML 页面。这样做的效果会在后台 烦恼或者前台较空闲时更加明显。

输出缓冲应用最好的一个地方就是紧跟在<head />之后,因为 HTML 的头部分容易生成 而且头部往往包含 CSS 和 JavaScript 文件,这样浏览器就可以在后台编译剩余 HTML 的 同时并行下载它们。 例子:

1
2
3
4
5
<!– css, js –>
</head>
<?php flush(); ?>
<body>
<!– content –>

为了证明使用这项技术的好处,Yahoo!搜索率先研究并完成了用户测试。

6、使用 GET 来完成 AJAX 请求

Yahoo!Mail 团队发现,当使用 XMLHttpRequest 时,浏览器中的 POST 方法是一个“两步走” 的过程:首先发送文件头,然后才发送数据。因此使用 GET 最为恰当,因为它只需发送 一个 TCP 包(除非你有很多 cookie)。IE 中 URL 的最大长度为 2K,因此如果你要发送一个 超过 2K 的数据时就不能使用 GET 了。 一个有趣的不同就是 POST 并不像 GET 那样实际发送数据。根据 HTTP 规范,GET 意味着“获取”数据,因此当你仅仅获取数据时使用 GET 更加有意义(从语意上讲也是如此),相反,发送并在服务端保存数据时使用 POST。

7、避免空 src 的图片(现代浏览器除外)。

空 src 属性的图片的行为可能跟你预期的不一样。它有两种形式:

html 标签:<img src="">
js:var img = new Image(); img.src = "";
两种都会造成同一种后果:浏览器会向你的服务器发请求。

IE,向页面所在的目录发请求。
Safari 和 Chrome,请求实际的页面。
FireFox3 及之前和 Safari/Chrome 一样,但从 3.5 开始修复问题,不再发请求。
Opera 遇到空图片 src 不做任何事。
为什么这种行为很糟糕?

由于发送大量的意料之外的流量,会削弱服务器,尤其那些每天 pv 上百万的页面。
浪费服务器计算周期取生成不会被浏览的页面。可能会破坏用户数据。如果你在跟踪请求状态,通过 cookie 或其它,你可能会破坏数据。即使 image 的请求不会返回图片,但所有的头部数据都被浏览器读取了,包括 cookie。即使剩下的响应体被丢弃,破坏可能已经发生。

这种行为的根源是 uri 解析发生在浏览器。RFC 3986 定义了这种行为,空字符串被当作相对路径,Firefox, Safari, 和 Chrome 都正确解析,而 IE 错误。总之,浏览器解析空字符串为相对路径的行为被认为是符合预期的。

html5 在4.8.2添加了对标签 src 属性的描述,指导浏览器不要发出额外的请求。

The src attribute must be present, and must contain a valid URL referencing a non-interactive, optionally animated, image resource that is neither paged nor scripted. If the base URI of the element is the same as the document’s address, then the src attribute’s value must not be the empty string.

幸运的是将来浏览器不会有这个问题了(在图片上)。不幸的是,<script src=""><link href="">没有这样的规范

三、JavaScript 和 CSS

除此之外,JavaScript 和 CSS 也是我们页面中经常用到的内容,对它们的优化也提高网 站性能的重要方面:

CSS:

  1. 把样式表置于顶部
  2. 避免使用 CSS 表达式(Expression)
  3. 使用外部 JavaScript 和 CSS
  4. 削减 JavaScript 和 CSS
  5. <link>代替@import
  6. 避免使用滤镜

JavaScript

  1. 把脚本置于页面底部
  2. 使用外部 JavaScript 和 CSS 3. 削减 JavaScript 和 CSS
  3. 剔除重复脚本
  4. 减少 DOM 访问
  5. 开发智能事件处理程序

1、把样式表置于顶部

在研究 Yahoo!的性能表现时,我们发现把样式表放到文档的<head />内部似乎会加快页 面的下载速度。这是因为把样式表放到<head />内会使页面有步骤的加载显示。

注重性能的前端服务器往往希望页面有秩序地加载。同时,我们也希望浏览器把已经接 收到内容尽可能显示出来。这对于拥有较多内容的页面和网速较慢的用户来说特别重要。 向用户返回可视化的反馈,比如进程指针,已经有了较好的研究并形成了正式文档。在 我们的研究中 HTML 页面就是进程指针。当浏览器有序地加载文件头、导航栏、顶部的 logo 等对于等待页面加载的用户来说都可以作为可视化的反馈。这从整体上改善了用户体验。 把样式表放在文档底部的问题是在包括 Internet Explorer 在内的很多浏览器中这会中 止内容的有序呈现。浏览器中止呈现是为了避免样式改变引起的页面元素重绘。用户不 得不面对一个空白页面。
HTML 规范清楚指出样式表要放包含在页面的<head />区域内:”和<a />不同,<link /> 只能出现在文档的<head />区域内,尽管它可以多次使用它”。无论是引起白屏还是出 现没有样式化的内容都不值得去尝试。最好的方案就是按照 HTML 规范在文档<head />内 加载你的样式表。

2、避免使用 CSS 表达式(Expression)

CSS 表达式是动态设置 CSS 属性的强大(但危险)方法。Internet Explorer 从第 5 个版 本开始支持 CSS 表达式。下面的例子中,使用 CSS 表达式可以实现隔一个小时切换一次背 景颜色:

1
background-color: expression( (new Date()).getHours()%2 ? “#B8D4FF” : “#F08A00′′ );

如上所示,expression 中使用了 JavaScript 表达式。CSS 属性根据 JavaScript 表达式的 计算结果来设置。expression 方法在其它浏览器中不起作用,因此在跨浏览器的设计中 单独针对 Internet Explorer 设置时会比较有用。

表达式的问题就在于它的计算频率要比我们想象的多。不仅仅是在页面显示和缩放时, 就是在页面滚动、乃至移动鼠标时都会要重新计算一次。给 CSS 表达式增加一个计数器 可以跟踪表达式的计算频率。在页面中随便移动鼠标都可以轻松达到 10000 次以上的计 算量。

一个减少 CSS 表达式计算次数的方法就是使用一次性的表达式,它在第一次运行时将结 果赋给指定的样式属性,并用这个属性来代替 CSS 表达式。如果样式属性必须在页面周 期内动态地改变,使用事件句柄来代替 CSS 表达式是一个可行办法。如果必须使用 CSS 表 达式,一定要记住它们要计算成千上万次并且可能会对你页面的性能产生影响。

3、使用外部 JavaScript 和 CSS

很多性能规则都是关于如何处理外部文件的。但是,在你采取这些措施前你可能会问到 一个更基本的问题:JavaScript 和 CSS 是应该放在外部文件中呢还是把它们放在页面本 身之内呢? 在实际应用中使用外部文件可以提高页面速度,因为 JavaScript 和 CSS 文件都能在浏览 器中产生缓存。内置在 HTML 文档中的 JavaScript 和 CSS 则会在每次请求中随 HTML 文档重 新下载。这虽然减少了 HTTP 请求的次数,却增加了 HTML 文档的大小。从另一方面来说, 如果外部文件中的 JavaScript 和 CSS 被浏览器缓存,在没有增加 HTTP 请求次数的同时可 以减少 HTML 文档的大小。 关键问题是,外部 JavaScript 和 CSS 文件缓存的频率和请求 HTML 文档的次数有关。虽然 有一定的难度,但是仍然有一些指标可以一测量它。如果一个会话中用户会浏览你网站 中的多个页面,并且这些页面中会重复使用相同的脚本和样式表,缓存外部文件就会带 来更大的益处。 许多网站没有功能建立这些指标。对于这些网站来说,最好的坚决方法就是把 JavaScript 和 CSS 作为外部文件引用。比较适合使用内置代码的例外就是网站的主页, 如 Yahoo!主页和 My Yahoo!。主页在一次会话中拥有较少(可能只有一次)的浏览量, 你可以发现内置 JavaScript 和 CSS 对于终端用户来说会加快响应时间。

很多性能规则都是关于如何处理外部文件的。但是,在你采取这些措施前你可能会问到 一个更基本的问题:JavaScript 和 CSS 是应该放在外部文件中呢还是把它们放在页面本 身之内呢?

在实际应用中使用外部文件可以提高页面速度,因为 JavaScript 和 CSS 文件都能在浏览 器中产生缓存。内置在 HTML 文档中的 JavaScript 和 CSS 则会在每次请求中随 HTML 文档重 新下载。这虽然减少了 HTTP 请求的次数,却增加了 HTML 文档的大小。从另一方面来说, 如果外部文件中的 JavaScript 和 CSS 被浏览器缓存,在没有增加 HTTP 请求次数的同时可 以减少 HTML 文档的大小。

关键问题是,外部 JavaScript 和 CSS 文件缓存的频率和请求 HTML 文档的次数有关。虽然 有一定的难度,但是仍然有一些指标可以一测量它。如果一个会话中用户会浏览你网站 中的多个页面,并且这些页面中会重复使用相同的脚本和样式表,缓存外部文件就会带 来更大的益处。

许多网站没有功能建立这些指标。对于这些网站来说,最好的坚决方法就是把 JavaScript 和 CSS 作为外部文件引用。比较适合使用内置代码的例外就是网站的主页, 如 Yahoo!主页和 My Yahoo!。主页在一次会话中拥有较少(可能只有一次)的浏览量, 你可以发现内置 JavaScript 和 CSS 对于终端用户来说会加快响应时间。

对于拥有较大浏览量的首页来说,有一种技术可以平衡内置代码带来的 HTTP 请求减少与 通过使用外部文件进行缓存带来的好处。其中一个就是在首页中内置 JavaScript 和 CSS, 但是在页面下载完成后动态下载外部文件,在子页面中使用到这些文件时,它们已经缓 存到浏览器了。

4、削减 JavaScript 和 CSS

精简是指从去除代码不必要的字符减少文件大小从而节省下载时间。消减代码时,所有 的注释、不需要的空白字符(空格、换行、tab 缩进)等都要去掉。在 JavaScript 中, 由于需要下载的文件体积变小了从而节省了响应时间。精简 JavaScript 中目前用到的最 广泛的两个工具是JSMinYUI Compressor。YUI Compressor 还可用于精简 CSS。

混淆是另外一种可用于源代码优化的方法。这种方法要比精简复杂一些并且在混淆的过 程更易产生问题。在对美国前 10 大网站的调查中发现,精简也可以缩小原来代码体积 的 21%,而混淆可以达到 25%。尽管混淆法可以更好地缩减代码,但是对于 JavaScript 来说精简的风险更小。

除消减外部的脚本和样式表文件外,<script><style>代码块也可以并且应该进行消 减。即使你用 Gzip 压缩过脚本和样式表,精简这些文件仍然可以节省 5%以上的空间。由于 JavaScript 和 CSS 的功能和体积的增加,消减代码将会获得益处。

前面的最佳实现中提到 CSS 应该放置在顶端以利于有序加载呈现。
在 IE 中,页面底部@import 和使用<link>作用是一样的,因此最好不要使用它。

6、避免使用滤镜

IE 独有属性 AlphaImageLoader 用于修正 7.0 以下版本中显示 PNG 图片的半透明效果。 这个滤镜的问题在于浏览器加载图片时它会终止内容的呈现并且冻结浏览器。在每一个 元素(不仅仅是图片)它都会运算一次,增加了内存开支,因此它的问题是多方面的。 完全避免使用 AlphaImageLoader 的最好方法就是使用 PNG8 格式来代替,这种格式能在 IE 中很好地工作。如果你确实需要使用 AlphaImageLoader,请使用下划线_filter 又使 之对 IE7 以上版本的用户无效。

7、把脚本置于页面底部

脚本带来的问题就是它阻止了页面的平行下载。HTTP/1.1 规范建议,浏览器每个主机 名的并行下载内容不超过两个。如果你的图片放在多个主机名上,你可以在每个并行下 载中同时下载 2 个以上的文件。但是当下载脚本时,浏览器就不会同时下载其它文件了, 即便是主机名不相同。 在某些情况下把脚本移到页面底部可能不太容易。比如说,如果脚本中使用了 document.write 来插入页面内容,它就不能被往下移动了。这里可能还会有作用域的问 题。很多情况下,都会遇到这方面的问题。 一个经常用到的替代方法就是使用延迟脚本。DEFER 属性表明脚本中没有包含 document.write,它告诉浏览器继续显示。不幸的是,Firefox 并不支持 DEFER 属性。在 Internet Explorer 中,脚本可能会被延迟但效果也不会像我们所期望的那样。如果脚 本可以被延迟,那么它就可以移到页面的底部。这会让你的页面加载的快一点。

8、剔除重复脚本

在同一个页面中重复引用 JavaScript 文件会影响页面的性能。你可能会认为这种情况并不多见。对于美国前 10 大网站的调查显示其中有两家存在重复引用脚本的情况。有 两种主要因素导致一个脚本被重复引用的奇怪现象发生:团队规模和脚本数量。如果真 的存在这种情况,重复脚本会引起不必要的 HTTP 请求和无用的 JavaScript 运算,这降 低了网站性能。

在 Internet Explorer 中会产生不必要的 HTTP 请求,而在 Firefox 却不会。在 Internet Explorer 中,如果一个脚本被引用两次而且它又不可缓存,它就会在页面加载过程中产 生两次 HTTP 请求。即时脚本可以缓存,当用户重载页面时也会产生额外的 HTTP 请求。 除增加额外的 HTTP 请求外,多次运算脚本也会浪费时间。在 Internet Explorer 和 Firefox 中不管脚本是否可缓存,它们都存在重复运算 JavaScript 的问题。 一个避免偶尔发生的两次引用同一脚本的方法是在模板中使用脚本管理模块引用脚本。 在 HTML 页面中使用<script />标签引用脚本的最常见方法就是:

1
<script type=”text/javascript” src=”menu_1.0.17.js”></script>

在 PHP 中可以通过创建名为 insertScript 的方法来替代:

1
<?php insertScript(”menu.js”) ?>

为了防止多次重复引用脚本,这个方法中还应该使用其它机制来处理脚本,如检查所属 目录和为脚本文件名中增加版本号以用于 Expire 文件头等。

9、减少 DOM 访问

使用 JavaScript 访问 DOM 元素比较慢,因此为了获得更多的应该页面,应该做到:
• 缓存已经访问过的有关元素
• 线下更新完节点之后再将它们添加到文档树中
• 避免使用 JavaScript 来修改页面布局

有关此方面的更多信息请查看 Julien Lecomte 在 YUI 专题中的文章 “高性能 Ajax 应用程序“。

10、开发智能事件处理程序

有时候我们会感觉到页面反应迟钝,这是因为 DOM 树元素中附加了过多的事件句柄并且 些事件句病被频繁地触发。这就是为什么说使用 event delegation(事件代理)是一种 好方法了。如果你在一个 div 中有 10 个按钮,你只需要在 div 上附加一次事件句柄就可 以了,而不用去为每一个按钮增加一个句柄。事件冒泡时你可以捕捉到事件并判断出是 哪个事件发出的。

你同样也不用为了操作 DOM 树而等待 onload 事件的发生。你需要做的就是等待树结构中 你要访问的元素出现。你也不用等待所有图像都加载完毕。

你可能会希望用 DOMContentLoaded 事件来代替 onload,但是在所有浏览器都支持它之前 你可使用YUI 事件应用程序中的onAvailable方法。

有关此方面的更多信息请查看 Julien Lecomte 在 YUI 专题中的文章 “高性能 Ajax 应用程序“。

四、Cookie,图片及移动应用

图片和 Coockie 也是我们网站中几乎不可缺少组成部分,此外随着移动设备的流行,对 于移动应用的优化也十分重要。这主要包括:
Cookie:

  1. 减小 Cookie 体积
  2. 对于页面内容使用无 coockie 域名
    图片:
  3. 优化图像
  4. 优化 CSS Spirite
  5. 不要在 HTML 中缩放图像
  6. favicon.ico 要小而且可缓存
    移动应用:
  7. 保持单个内容小于 25K
  8. 打包组件成复合文本

HTTP coockie 可以用于权限验证和个性化身份等多种用途。coockie 内的有关信息是通 过 HTTP 文件头来在 web 服务器和浏览器之间进行交流的。因此保持 coockie 尽可能的小以 减少用户的响应时间十分重要。

有关更多信息可以查看 Tenni Theurer 和 Patty Chi 的文章 “When the Cookie Crumbles“。 这们研究中主要包括:

• 去除不必要的 coockie
• 使 coockie 体积尽量小以减少对用户响应的影响
• 注意在适应级别的域名上设置 coockie 以便使子域名不受影响
• 设置合理的过期时间。较早地 Expire 时间和不要过早去清除 coockie,都会改
善用户的响应时间。

2、对于页面内容使用无 coockie 域名

当浏览器在请求中同时请求一张静态的图片和发送 coockie 时,服务器对于这些 coockie 不会做任何地使用。因此他们只是因为某些负面因素而创建的网络传输。所有你应该确 定对于静态内容的请求是无 coockie 的请求。创建一个子域名并用他来存放所有静态内 容。

如果你的域名是www.example.org,你可以在static.example.org上存在静态内容。但 是,如果你不是在www.example.org上而是在顶级域名example.org设置了coockie,那 么所有对于 static.example.org 的请求都包含 coockie。在这种情况下,你可以再重新 购买一个新的域名来存在静态内容,并且要保持这个域名是无 cookie 的。Yahoo!使用 的是 ymig.com,YouTube 使用的是 ytimg.com,Amazon 使用的是 images-anazon.com 等等。 使用无 coockie 域名存在静态内容的另外一个好处就是一些代理(服务器)可能会拒绝对 cookie 的内容请求进行缓存。一个相关的建议就是,如果你想确定应该使用 example.org 还是www.example.org作为你的一主页,你要考虑到coockie带来的影响。 忽略掉 www 会使你除了把 coockie 设置到*.example.org(*是泛域名解析,代表了所有子域名)外没有其它选择,因此出于性能方面的考虑最好是使用带有 www 的子 域名并且在它上面设置 cookie。

3、优化图像

设计人员完成对页面的设计之后,不要急于将它们上传到 web 服务器,这里还需要做几 件事:

• 你可以检查一下你的 GIF 图片中图像颜色的数量是否和调色板规格一致。 使 用imagemagick中下面的命令行很容易检查:

1
identify -verbose image.gif

如果你发现图片中只用到了 4 种颜色,而在调色板的中显示的 256 色的颜色槽, 那么这张图片就还有压缩的空间。

• 尝试把 GIF 格式转换成 PNG 格式,看看是否节省空间。大多数情况下是可以压 缩的。由于浏览器支持有限,设计者们往往不太乐意使用 PNG 格式的图片,不 过这都是过去的事情了。现在只有一个问题就是在真彩 PNG 格式中的 alpha 通 道半透明问题,不过同样的,GIF 也不是真彩格式也不支持半透明。因此 GIF 能 做到的,PNG(PNG8)同样也能做到(除了动画)。下面这条简单的命令可以安 全地把 GIF 格式转换为 PNG 格式:

1
convert image.gif image.png

“我们要说的是:给 PNG 一个施展身手的机会吧!”
• 在所有的 PNG 图片上运行 pngcrush(或者其它 PNG 优化工具)。例如:

1
pngcrush image.png -rem alla -reduce -brute result.png

• 在所有的 JPEG 图片上运行 jpegtran。
这个工具可以对图片中的出现的锯齿等做
无损操作,同时它还可以用于优化和清除图片中的注释以及其它无用信息(如 EXIF 信息):

1
jpegtran -copy none -optimize -perfect src.jpg dest.jpg

4、优化 CSS Spirite

• 在 Spirite 中水平排列你的图片,垂直排列会稍稍增加文件大小;

• Spirite 中把颜色较近的组合在一起可以降低颜色数,理想状况是低于 256 色以
便适用 PNG8 格式;

• 便于移动,不要在 Spirite 的图像中间留有较大空隙。这虽然不大会增加文件大小但对于用户代理来说它需要更少的内存来把图片解压为像素地图。 100×100 的图片为 1 万像素,而 1000×1000 就是 100 万像素。

5、不要在 HTML 中缩放图像

不要为了在 HTML 中设置长宽而使用比实际需要大的图片。如果你需要:

1
<img width=”100′′ height=”100′′ src=”mycat.jpg” alt=”My Cat” />

那么你的图片(mycat.jpg)就应该是 100×100 像素而不是把一个 500×500 像素的图 片缩小使用。

6、favicon.ico 要小而且可缓存

favicon.ico 是位于服务器根目录下的一个图片文件。它是必定存在的,因为即使你不 关心它是否有用,浏览器也会对它发出请求,因此最好不要返回一个 404 Not Found 的 响应。由于是在同一台服务器上,它每被请求一次 coockie 就会被发送一次。这个图片 文件还会影响下载顺序,例如在 IE 中当你在 onload 中请求额外的文件时,favicon 会 在这些额外内容被加载前下载。
因此,为了减少 favicon.ico 带来的弊端,要做到:

• 文件尽量地小,最好小于 1K
• 在适当的时候(也就是你不要打算再换 favicon.ico 的时候,因为更换新文件
时不能对它进行重命名)为它设置 Expires 文件头。你可以很安全地把 Expires 文件头设置为未来的几个月。你可以通过核对当前 favicon.ico 的上次编辑时间来作出判断。
Imagemagick可以帮你创建小巧的 favicon。

7、保持单个内容小于 25K

这条限制主要是因为 iPhone 不能缓存大于 25K 的文件。注意这里指的是解压缩后的大小。 由于单纯 gizp 压缩可能达不要求,因此精简文件就显得十分重要。 查看更多信息,请参阅 Wayne Shea 和 Tenni Theurer 的文件“Performance Research, Part 5: iPhone Cacheability – Making it Stick”。

8、打包组件成复合文本

把页面内容打包成复合文本就如同带有多附件的 Email,它能够使你在一个 HTTP 请求中 取得多个组件(切记:HTTP 请求是很奢侈的)。当你使用这条规则时,首先要确定用户 代理是否支持(iPhone 就不支持)。

Web 常见问题

Error: ResizeObserver loop limit exceeded 问题

发现一个报错 ResizeObserver loop limit exceeded,这个报错是在公司平台项目监听系统中提示的,而浏览器的 console 中却没有提示

如果要在本地开发中调试定位这个问题,可以在项目代码里加入一个方法,在控制台中输出这个错误:

1
2
3
window.onerror = function (errorMessage, scriptURI, lineNumber, columnNumber, error) {
console.log('错误', errorMessage);
};

对于一些说法是这个错误可以给予忽略

参考地址 : https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded

Vue 常见问题

1. Vue 使用代理(proxy) 注意

首先记录两个地址, 在 vue/vue-cli/axios 中经常会遇到代理以及请求的问题, 为了这个, 我们需要设置代理服务器等信息便于开发调试

注意, 在设置 axios 的 baseUrl 之后 proxy 就不可用了. 需要注意这一点

1
2
// 这里设置代理, 根据环境来设定
const baseUrl = "development" === process.env.NODE_ENV ? "/api" : process.env.VUE_APP_URL;

2. 路由死循环

1
2
3
4
5
6
7
8
9
10
11
vue-router.esm.js?8c4f:2257 RangeError: Maximum call stack size exceeded
at JSON.stringify (<anonymous>)
at eval (vuex-persistedstate.es.js?0e44:1)
at eval (vuex-persistedstate.es.js?0e44:1)
at eval (vuex.esm.js?2f62:472)
at Array.forEach (<anonymous>)
at Store.commit (vuex.esm.js?2f62:472)
at Store.boundCommit [as commit] (vuex.esm.js?2f62:409)
at Object.vue__WEBPACK_IMPORTED_MODULE_0__.default.$hideLoading (use.js?2e7e:13)
at eval (router.js?41cb:141)
at iterator (vue-router.esm.js?8c4f:2300)

3. store 中使用 commit 无法触发数据的变更

流程: 初始化 -> 网络请求 -> 调用 muation 修改数据不会触发 store 数据的变更, 在页面中使用 computed 的时候数据无法获取到数据, 在 action 则可

Axios

返回数据的错误处理

官方链接 : 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
axios.get("/user/12345").catch(function (error) {
if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// `error.request` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
console.log(error.request);
} else {
// 发送请求时出了点问题
console.log("Error", error.message);
}
console.log(error.config);
});

无网络

无 response 返回

1
2
3
4
5
6
7
8
{
"message": "Network Error",
"name": "Error",
"stack": "Error: Network Error...",
"config": {
// ...
}
}

请求超时

无 response 返回

1
2
3
4
5
6
7
8
9
{
"message": "timeout of 2000ms exceeded",
"name": "Error",
"stack": "Error: timeout of 2000ms exceeded...",
"config": {
// ...
},
"code": "ECONNABORTED"
}

返回错误码

返回 response

1
2
3
4
5
6
7
8
{
"message": "Request failed with status code 401",
"name": "Error",
"stack": "Error: Request failed with status code 401...",
"config": {
...
}
}

response 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"data": "Unauthorized Jwt.",
"status": 401,
"statusText": "Unauthorized",
"headers": {
"cache-control": "no-cache, private",
"content-type": "text/html; charset=UTF-8"
},
"config": {
...
},
"request": {
...
}
}

对于 config 中的结构格式查看 请求配置

ApiCloud 常见问题(FAQ)

1. 证书错误

证书生成地址见 : iOS 证书及描述文件制作流程

如果证书类型存在错误, 则会出现以下的问题, 这里应该使用开发证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
note: Using new build system
note: Building targets in parallel
note: Planning build
note: Constructing build description
error: Provisioning profile "Ad-Hoc" doesn't include signing certificate "iPhone Developer: Yufei Li (M5L32Z773W)". (in target 'UZApp' from project 'UZApp')
warning: NewsstandKit is deprecated. (in target 'UZApp' from project 'UZApp')
warning: OpenGLES is deprecated. Consider migrating to Metal instead. (in target 'UZApp' from project 'UZApp')
warning: MobileCoreServices has been renamed. Use CoreServices instead. (in target 'UZApp' from project 'UZApp')
warning: GLKit is deprecated. Consider migrating to MetalKit instead. (in target 'UZApp' from project 'UZApp')
warning: AddressBook is deprecated. Consider migrating to Contacts instead. (in target 'UZApp' from project 'UZApp')
warning: AddressBookUI is deprecated. Consider migrating to ContactsUI instead. (in target 'UZApp' from project 'UZApp')
warning: AssetsLibrary is deprecated. Consider migrating to Photos instead. (in target 'UZApp' from project 'UZApp')

** ARCHIVE FAILED **

** EXPORT FAILED **

error: archive not found at path '/uzmap/temp/UiC1Cw5M31nvsNL/ios/UZApp.xcarchive'

发布 npm 包

关于如何发布可以查看 [译] 创建并发布一个小而美的 npm 包

关于如何搭建一个 npm 包并进行单元测试, 这里有个解决方案 jslib-base

在发布包的时候遇到的常见问题如下

常见问题

1. PUT https://registry.npm.taobao.org/@… - [no_perms] Private mode enable, only admin can publish this module

使用 npm publish 或者使用 yarn publish 出现 couldn’t publish package:”https://registry.npm.taobao.org/包...:unauthorized

出现原因:使用的是淘宝源 cnpm,登陆到的是 cnpm

解决方法:切换到 npmjs 的网址,代码如下

1
2
3
4
npm config set registry http://registry.npmjs.org/

# 切换回淘宝源
npm config set registry https://registry.npm.taobao.org/

2. You must sign up for private packages

这里代表发布的时候必须要加入 --access public, 因为默认的 @ 符号开始的必须是私有包, 如果默认需要显式的传成公共方法, 则需要自己主动约定

3. Couldn’t publish package: Scope not found”

如果是用户名, 这里不需要创建 scope, 如果是除了自己用户名之外的, 需要确认 scope 是否存在, 如果不存在, 则可以创建一个 organization

参考地址 How to publish NPM Scoped Packages / NPM scope not found?

4. 取消发布

1
$ yarn unpublish @pkg/name --force

5. yarn upgrade 更新依赖包时 yarn.lock 更新但 package.json 不同步更新版本信息

1
2
# 需要手动选择升级的依赖包,按空格键选择,a 键切换所有,i 键反选选择
$ yarn upgrade-interactive --latest

[转+] 用 JSDOC 编写 JavaScript 文档

JSDOC 是一个 API 文档生成器,你只需要在代码中添加特定格式的注释,它就可以从注释中为你生成 HTML 文档。

安装

全局安装:

1
$ npm install -g jsdoc

如果你更倾向项目内使用,你也可以选择:

1
$ npm install jsdoc --save-dev

基本使用

使用 JSDOC 非常简单,先为 JavaScript 文件写好注释,然后用 JSDOC 去解析即可:

1
/path/to/jsdoc file1.js file2.js ...

但这种方式只适合少量文件时使用,当文件数量多了,再加上其他参数,维护起来就会非常麻烦。 所以更好的做法是编写一份配置文件 jsdoc.json,然后通过 -c 参数来指定:

1
/path/to/jsdoc -c jsdoc.json

无论 JSDOC 是通过全局安装,还是局部安装,都建议使用 npm scripts 来调用它,即在 package.jsonscripts 中添加命令(这里命名成 build:doc,可以根据自己爱好定义其他名字):

1
2
3
4
5
{
"scripts": {
"doc": "jsdoc -c jsdoc.json"
}
}

这样我们就可以通过 npm run doc 来生成文档了。

如果实现修改文件后自动生成文档,只需要用类似 nodemon 之类的工具,监听指定文件的变化,然后再自动执行 npm run doc 就好了!

配置文件

JSDOC 的配置项非常多,最常用的是下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"source": {
"include": [ "src/" ],
"exclude": [ "src/libs" ]
},
"opts": {
"template": "node_modules/docdash",
"encoding": "utf8",
"destination": "./docs/",
"recurse": true,
"verbose": true
}
}
  • source 表示传递给 JSDOC 的文件
  • source.include 表示 JSDOC 需要扫描哪些文件
  • source.exclude 表示 JSDOC 需要排除哪些文件
  • opts 表示传递给 JSDOC 的选项
  • opts.template 生成文档的模板,默认是 templates/default
  • opts.encoding 读取文件的编码,默认是 utf8
  • opts.destination 生成文档的路径,默认是 ./out/
  • opts.recurse 运行时是否递归子目录
  • opts.verbose 运行时是否输出详细信息,默认是 false

注释

我们知道,JSDOC 的工作原理是通过分析 JavaScript 文件中的注释来生成 HTML 文档的。 但是,如果想 JSDOC 生成正确的结果,我们需要编写正确格式的注释才行。它接受如下格式的注释:

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
/**
* @author Scarlex
* @class
* @name Application
* @description Base Class of Application.
* @param {Element} canvas The canvas dom element.
* @param {Object} options The options of Application. See {@link Option} for detail.
* @return {Application}
*
* @example
* // create your application
* new Application(canvas, options);
*/
export default class Aplication {

/**
* @private
* @function
* @name Application#intialize
* @description Initialize the application.
*/
initialize() {

}
}

这是我们在 JavaScript 中常见的级块注释。 需要注意的是,JSDOC 的解析器要求注释必须以 /** 开头,如果是以 /*/*** 或多于三个星号的注释都会被忽略。

标签 (Tags)

有了这种级块注释,我们就可以在里面根据需要编写文档了。JSDOC 为我们提供了非常丰富的标签,它的解析器会对这些标签进行额外处理。这些标签大概可以分成两类:级块标签和行内标签。

  • 级块标签:位于注释的最顶层。JSDOC 中绝大部分标签都是级块标签。
  • 行内标签:位于级块标签内的标签,如 @link@tutorial

下面介绍一些常见的级块标签:

  • @author 该类/方法的作者。
  • @class 表示这是一个类。
  • @function/@method 表示这是一个函数/方法(这是同义词)。
  • @private 表示该类/方法是私有的,JSDOC 不会为其生成文档。
  • @name 该类/方法的名字。
  • @description 该类/方法的描述。
  • @param 该类/方法的参数,可重复定义。
  • @return 该类/方法的返回类型。
  • @link 创建超链接,生成文档时可以为其链接到其他部分。
  • @example 创建例子。

命名路径 (Namepaths)

不知道你有没有发现,上面例子中是用 Application#initialize 来表示一个实例方法的。如果是静态方法,那应该怎么表示呢?JSDOC 有自己的解析规则:

  • Constructor.Method 表示静态方法
  • Constructor#Method 表示实例方法
  • Constructor~Method 表示内部方法

了解了这些之后,我们就可以用 JSDOC 为 JavaScript 代码编写文档了,快去试试吧!

使用 Docstrap

由于 JSDoc 默认的文档模板比较单调,而 docstrap 提供了 14+种 bootstrap 风格的模板,因此建议下载 docstrap

安装

1
2
$ npm i ink-docstrap
$ yarn add ink-docstrap -D

移除 Docstrap 对 google 字体的引用

由于引用了 google 字体,国内环境下会导致页面卡顿。

打开 node_modules\ink-docstrap\template\static\styles 目录,将所有引用 google 字体的内容删除:

1
@import url("https://fonts.googleapis.com/css?family=Roboto:400,500");

指定模板:在 jsdoc 的配置文件 conf.json 下的 template 选项 配置为 docstrap/template 即可

如果要手动修改模板样式:修改文件

1
docstrap\template\tmpl\details.tmpl

Docstrap 模板配置项说明参考

Docstrap 使用的还是 jsdoc 的配置项,同时新增了几个配置项。打开 node_modules/ink-docstrap/template/jsdoc.conf.json 文件,这里面 templates 那部分就是 Docstrap 新增的配置项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
...
"templates": {
"systemName" : "{string}",
"footer" : "{string}",
"copyright" : "{string}",
"includeDate" : "{boolean}",
"navType" : "{vertical|inline}",
"theme" : "{theme}",
"linenums" : "{boolean}",
"collapseSymbols" : "{boolean}",
"inverseNav" : "{boolean}",
"outputSourceFiles" : "{boolean}" ,
"outputSourcePath" : "{boolean}",
"dateFormat" : "{string}",
"syntaxTheme" : "{string}",
"sort" : "{boolean|string}"
}
...
}

解释下其中几个配置项的作用:

  • outputSourceFiles:是否输出 js 源文件,也就是生成的 jsdoc 里是否显示源 js 文件的链接。不想让用户看到 js 源文件的话,这项改成 false,或者整个 default 项删除也行。
  • systemName:js 产品的名称。也就是生成的 jsdoc 页面上方的名称。这个改成你自己的。
  • copyright:版权信息。
  • navType:导航方式。就是页面上方的 Classes 导航下拉菜单。支持 vertical 和 inline 两种方式。建议用 vertical。inline 我觉得不方便。
  • theme:皮肤模板。默认这个就挺好。Docstrap 现在提供了 13 种效果。感兴趣的,可以自己去看看其它效果:https://github.com/terryweiss/docstrap
  • linenums:是否显示所在行数。比如当前方法位于 js 源文件 12 行。false 的话,就不显示这个信息。
  • collapseSymbols:是否将类,方法,属性等 doc 信息以加号的方式收起。

Docstrap 提供了一个默认的配置文件可供参考:

1
node_modules\ink-docstrap\template\jsdoc.conf.json

创建和编辑 JSDoc 配置文件

JSDoc 提供的默认配置文件在这里:

1
node_modules\jsdoc\gen.json

结合 JSDoc 和 Docstrap 的默认配置,我们创建一个项目使用的配置文件。

在项目目录新建一个 json 文件,如:jsdoc-conf.json,内容参考如下:

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
47
{
"tags": {
"allowUnknownTags": true
},
"source": {
"include": ["src"], //JavaScript 文件(目录)列表
"exclude": ["src/core", "src/ui"], //在 include 中需要过滤的文件(目录)
"includePattern": ".+\\.(js|es)$" //正则过滤符合规则的文件
},
"plugins": ["plugins/markdown"], //使用markdown 插件
"markdown": {
tags: ["file"], //增加额外需要解析的标签
"excludeTags": ["author"], //排除不用解析的标签
"parser": "gfm", //gfm
"hardwrap": true //允许多行
},
"templates": { //模板配置,包含了 DocStrap 的配置参数
//"logoFile": "images/logo.png", //logo 文件路径
"cleverLinks": false,
"monospaceLinks": false,
"dateFormat": "ddd MMM Do YYYY", //当需要打印日期时使用的格式
"outputSourceFiles": true, //是否输出文件源码
"outputSourcePath": true, //是否输出源码路径
"systemName": "Common Modules", //系统名称
"footer": "", //页脚内容
"copyright": "https://lzw.me.", //页脚版权信息
"navType": "vertical", //vertical 或 inline
//docstrap 模板主题。可取值: cosmo, cyborg, flatly, journal, lumen, paper,
//readable, sandstone, simplex, slate, spacelab, superhero, united, yeti
"theme": "cosmo",
"linenums": true, //是否显示行号
"collapseSymbols": false, //是否折叠太长的内容
"inverseNav": true, //导航是否使用 bootstrap 的 inverse header
"protocol": "html://", //生成文档使用的阅读协议
"methodHeadingReturns": true //method 方法标题上是否包含返回类型
},
//命令行执行参数配置。在这里配置了后
//命令行只需要执行: jsdoc -c jsdoc-conf.json 即可
"opts": {
//"template": "templates/default", //使用 JSDoc 默认模板
"template": "./node_modules/ink-docstrap/template", //使用 docstrap 模板
"destination": "./docs/", //输出目录。等同于 -d ./out/
"recurse": true, //是否递归查找。 -r
"debug": true, //启用调试模式。--debug
"readme": "README.md" //要写到文档首页的 readme 文档。-R README.md
}
}

参考如上的示例说明编写你自己的配置。确认无误,在项目目录下执行如下命令,即可生成项目 API 文档:

1
jsdoc -c ./jsdoc-conf.json

注意点:

  1. 只使用配置文件中的 opts 配置命令行参数。即只使用 -c 参数指定配置文件。因为命令行的参数与配置文件中可能出现重叠,那么就会存在优先级、合并等问题。在不清楚这些问题的情况下,可能会出现各种细节的问题。
  2. source 部分的配置,应简洁清晰明了。这里的 include/exclude/includePattern/excludePattern 以及命令行中附带的文件路径,存在着优先级以及合并的问题。
  3. 推荐配置 markdown 插件,这对详细注释很有帮助。
  4. 遇到错误或奇怪的问题时,多查阅官方文档。JSDoc 中文文档
  5. 理解名称路径,有利于书写和生成更合适的文档。
  6. ES6 模块化方式,某些情况下对导出模块的声明,可借助 @alisas 标签。示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @file test
* @module test
*/

let abc = 'abc...';

/**
* @alias module:test
*/
const test = {
abc: abc
};

export default test;

使用 IDE 编辑器插件快速生成

通过 IDE 插件,在编辑器中可以快速的插入 JSDoc 规范的注释。大型的 IDE 则甚至在内核中已集成相关功能。

例如使用 /** + enter 便可以在 jetbrains 中生成文档

sublime text 插件

DocBlockr (https://github.com/spadgos/sublime-jsdocs)

vscode 插件

add jsdoc comments

使用命令:

1
$ jsdoc -c path/to/conf.json -t ./node_modules/ink-docstrap/template -R README.md -r .

参考

yarn 更换源, 使用国内镜像

为什么慢

执行 yarn 各种命令的时候,默认是去 npm/yarn 官方镜像源获取需要安装的具体软件信息

以下命令查看当前使用的镜像源

1
yarn config get registry

默认源地址在国外,从国内访问的速度肯定比较慢

如何修改镜像源

阿里旗下维护着一个完整的 npm 镜像源 https://registry.npm.taobao.org/ 同样适用于 yarn

1. 临时修改

1
yarn save 软件名 --registry https://registry.npm.taobao.org/

2. 全局修改

1
yarn config set registry https://registry.npm.taobao.org/

3. 使用第三方软件快速修改、切换 yarn 镜像源

yrm YARN registry manager
yrm 不仅可以快速切换镜像源,还可以测试自己网络访问不同源的速度

安装 yrm

1
npm install -g yrm

列出当前可用的所有镜像源

1
2
3
4
5
6
7
8
9
$ yrm ls

npm ----- https://registry.npmjs.org/
cnpm ---- http://r.cnpmjs.org/
taobao -- https://registry.npm.taobao.org/
nj ------ https://registry.nodejitsu.com/
rednpm -- http://registry.mirror.cqupt.edu.cn
skimdb -- https://skimdb.npmjs.com/registry
yarn ---- https://registry.yarnpkg.com

使用淘宝镜像源

1
yrm use taobao

测试访问速度

1
yrm test taobao

更多用法查看 yrm GitHub

基于 Umi 的最佳实践

  • 本项目使用 yarn 进行包管理
  • 全局安装的命令为 jsdoc

本项目是基于 Web 开发最佳实践 的扩展

目录结构以及解释

目录结构

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
├── build           # 打包目录
├── config # 配置目录
│ ├── config.dev.js
│ ├── config.js
│ └── ...
├── docs # 文档目录
│ ├── html
│ └── jsdoc.json
├── jsconfig.json # IDE 的代码提示 @/
├── package.json # 版本以及依赖文件
├── public # 附加到 public 目录 的文件
│ └── robots.txt
└── src
├── assets # 资源文件
│ ├── images # 图片文件
│ └── less # 样式文件
│ ├── style.less # 样式主文件
│ └── ...
├── build.md # 版本定义说明
├── layout # 布局文件夹
│ └── index.js
├── models # 模型文件, 用户全局的数据管理
│ └── poppy.js
├── pages # 所有页面
├── services # api 服务定义页面
│ └── poppy.js
└── utils # util 包, 多个项目内容应该一致
├── conf.js # 配置
├── request.js # 基于 axios 的请求
├── routes.js # 路由定义
├── ui # UI 部件
│ └── IconFont.js # IconFont 字体
└── util.js # 辅助功能

配置文件

config.test.js

1
2
3
4
5
6
7
8
9
10
import { defineConfig } from "umi";

export default defineConfig({
// 打包路径
outputPath: "build/proj-test",
define: {
// 定义的URL被覆盖
"process.env.API_URL": "https://t.proj.domain.com",
},
});

配置中使用的变量说明, 其他变量间 umi 官方文档
config.js

1
2
3
4
5
6
7
8
9
10
11
export default defineConfig({
// ...
define: {
// 版本号, 取 package.json 中的版本
"process.env.VERSION": version,
// 接口请求主地址, 尾部不包含斜线
"process.env.API_URL": process.env.API_URL || "",
// 打包环境, 用户 sentry 提交错误对应正确的环境
"process.env.UMI_ENV": process.env.UMI_ENV || "",
},
});

打包以及运行说明

命令放置在 package.json 文件中

1
2
3
4
# 运行本地环境(参考package.json启动其他环境)
$ yarn start:test
# 构建测试环境(参考package.json构建其他环境)
$ yarn build:dev
  • UMI_ENV : 代表的是运行环境, 可以在配置中取到相应的文件并进行相应的处理
1
2
3
4
5
6
7
8
9
10
 {
"..."
"scripts": {
"...",
"start:test": "cross-env UMI_ENV=test yarn umi-dev",
"build:test": "cross-env UMI_ENV=test umi build",
"..."
}
}

打包出来的路径为 build/proj-test

文档

文档使用的 jsdoc. 这里的配置文件使用的是 ink-docstrap  模板, 配置如下
使用如下命令生成

1
$ jsdoc -c docs/jsdoc.json

jsdoc.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc", "closure"]
},
"source": {
"include": ["./src/", "README.md"],
"exclude": [],
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": [],
"opts": {
"template": "./node_modules/ink-docstrap/template",
"encoding": "utf8",
"destination": "./docs/html",
"recurse": true
}
}

版本定义

项目版本放在 version 中, 每次上线或者切换分支的时候都需要更新, 便于后端进行统计
package.json

1
2
3
4
5
6
{
"name": "proj-mobile",
"version": "1.1.0",
"..."
}

服务的定义以及使用

使用 api 作为前缀, 便于区分
src/services/poppy.js

1
2
3
4
5
6
7
8
9
10
/**
* 国别码
*/
export function apiPamLogin(params) {
return request({
url: "/api_v1/system/pam/login",
method: "post",
data: params,
});
}

使用

1
2
3
4
5
6
apiPamLogin({ passport: country + "-" + mobile, captcha: captcha }).then((resp, status, message, data) => {
// resp : 包含完整的请求结果
// status : 错误码信息
// data : data 中的数据
// message : 返回的信息
});

模型的定义以及说明

模型定义

src/models/poppy.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
39
40
41
42
43
44
45
46
47
48
49
50
import { Toast } from "antd-mobile";
import { apiAreaCountry } from "@/services/poppy";
import { storageKey } from "@/utils/conf";
import { sessionStore } from "@/utils/util";

const poppyModel = {
namespace: "poppy",
state: {
// 1. 初始数据值
countryCode: [],
},

effects: {
// 2. 定义的请求
*reqAreaCountry({ payload: params }, { call, put }) {
// 3. 获取数据
let storeData = sessionStore(storageKey.PY_AREA_COUNTRY);
if (!storeData) {
// apiAreaCountry 在 模型中的使用, 参数可以在第二个参数传入
const { data, success, message: msg } = yield call(apiAreaCountry, {});
if (success && data) {
sessionStore(storageKey.PY_AREA_COUNTRY, data);
storeData = data;
} else {
Toast.fail(msg);
}
}
// 4. 更新数据
yield put({
type: "UPDATE_COUNTRY_CODE",
payload: {
countryCode: storeData,
},
});
},
},

reducers: {
// 5. 将数据写入 redux store 中
UPDATE_COUNTRY_CODE: (state, { payload }) => {
return {
...state,
...{
countryCode: payload.countryCode,
},
};
},
},
};
export default poppyModel;

数据的调用

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import React, { Component } from 'react';
import $ from 'jquery';
import { CloseCircleOutlined } from '@ant-design/icons'
import BScroll from 'better-scroll';
import { get } from 'lodash-es';
import { connect } from 'umi';

// 1. 连接 store
@connect(({ poppy }) => {
return {
countryCode: poppy.countryCode
}
})
class CountryCode extends Component
{
scroll = '';
touch = {};

constructor(props) {
super(props);
this.state = {
list: {},
alphaList: []
}
}

componentDidMount() {
// 2. 触发数据更新
// 这里的 dispatch 必须使用 store 之后才可以触发, 毕竟读取的是store 里边的数据
this.props.dispatch({
type: 'poppy/reqAreaCountry'
})
}

// 3. 接收数据更新
static getDerivedStateFromProps(nextProps, prevState) {

const countryCode = get(nextProps, 'countryCode');
const alphaList = get(prevState, 'alphaList');

if (countryCode && alphaList.length === 0) {
return CountryCode.initList(countryCode)
}

return null;
}

static initList(countryCode) {
let alpha_li = [];
let list_li = {};
countryCode.length > 0 && countryCode.forEach((item) => {
let key = item['py'];
alpha_li.push(key);
list_li[key] = list_li[key] ? list_li[key].concat(item) : [item];
})
return {
alphaList: [...new Set(alpha_li)],
list: list_li
}
}


render() {
const { list, alphaList } = this.state;
return (
// 数据渲染
);
}
}

export default CountryCode;

React 常见问题

1. Uncaught Error: Invariant failed

Uncaught Error: Invariant failed , You should not use <Route> outside a <Router>
参考地址 : https://www.freeformatter.com/url-parser-query-string-splitter.html

出现原因: 系统中存在两个实例, 例如

1
2
3
4
5
import { Route } from "react-router";
import { Switch } from "react-router-dom";

// 应该修改为
import { Switch, Route } from "react-router-dom";

2. 加载自定义 Url

将加载的 URL 放到 div 中, 从而实现远程地址的加载来加载变量

1
2
3
4
5
6
7
8
9
10
11
import postscribe from "postscribe";

let script = serverUrl + "/" + apiUrl.pamWechatConfig + "?url=" + window.location.href + "&type=base";
postscribe("#J_wxAuth", '<script src="' + script + '"></script>', {
error: (e) => {
console.log(e);
},
done: (e) => {
wx.config(window.wxAuthConfig);
},
});

3. 关掉线上 map 生成

参考文章 : 什么时候 create-react-app 会混淆或缩小代码?

如果有 map 生成, 则许多源码对外来讲便是可视化的. 例如之前的一个代练项目

项目代码一览无余, 所以对于项目上线打包的时候必须要关闭源码 map

对于 cra 项目, 使用了 react-app-rewired, 则可以在 config-overrides.js 中增加如下相关配置, 来关闭 sources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const {override, addLessLoader, fixBabelImports, addWebpackAlias} = require('customize-cra');
const alias = require('./alias');

+ process.env.GENERATE_SOURCEMAP = "false";

function myOverrides(config) {
// do stuff to config
return config
}

module.exports = override(
myOverrides,
addLessLoader({
javascriptEnabled : true
}),
fixBabelImports('import', {
libraryName : 'antd-mobile',
style : true
}
),
addWebpackAlias(alias.resolve.alias)
)

关掉 map 之后则看起来放心多了

4. uncaught at check call: argument [object Promise] is not a function

主要原因在 yield call(addGroup(payload)); 的使用姿势问题,对于需要传递参数的去哪个,不能直接这么干,应该改为

1
yield call(addGroup, payload);

[转+] Npm 更换源使用国内镜像

TaoNpm 的更新流程示意图:

为什么要换源? npm 官方站点 http://www.npmjs.org/ 并没有被拦截,但是下载第三方依赖包的速度由于和外网联通的限制, 速度不能满足实际的使用需求.为了加速访问, 我们可以使用镜像来进行访问

国内有几个镜像站点可以供我们使用

速度非常快,镜像站会实时更新,为我们节省了好多时间.

临时更换访问源

通过 config 配置指向国内镜像源

1
2
$ npm config set registry https://registry.npmmirror.com
$ npm info express

通过 npm 命令指定下载源

1
2
# 在安装时候临时指定
$ npm --registry https://registry.npmmirror.com info express

永久更换访问源

使用 nrm 来更换访问源

nrm 是 NPM Registry Manager 的缩写, 通过他可以快速切换源, 文档地址 : https://www.npmjs.com/package/nrm

1
2
$ npm install -g nrm
$ yarn global add nrm
1
2
3
4
5
6
7
8
9
10
11
12
# list all
$ nrm ls

* npm ---------- https://registry.npmjs.org/
yarn --------- https://registry.yarnpkg.com/
tencent ------ https://mirrors.cloud.tencent.com/npm/
cnpm --------- https://r.cnpmjs.org/
taobao ------- https://registry.npmmirror.com/
npmMirror ---- https://skimdb.npmjs.com/registry/

# 替换使用
$ nrm use taobao

[linux]在配置文件 ~/.npmrc 文件写入源地址

1
2
3
4
# 打开配置文件
$ vim ~/.npmrc
# 写入配置文件
registry=https://registry.npmmirror.com/

如果你不想使用国内镜像站点,只需要将 写入 ~/.npmrc 的配置内容删除即可.
下面是我本地下载 ejs 包的截图,可以看到默认源地址指向了 cnpm

使用 cnpm 来替代 npm

使用说明查看 : https://npmmirror.com

cnpm 支持所有 npm 的命令并且可以快速同步任意模块

1
$ cnpm sync koa connect mocha

如果不想安装 cnpm cli 怎么办? 我们还有一个 web 页面:
例如我想马上同步 koa, 直接打开浏览器: http://npmmirror.com/sync/koa
或者你是命令行控, 通过 open 命令打开:

1
open http://npmmirror.com/sync/koa

如果你安装的模块依赖了 C++ 模块, 需要编译, 肯定会通过 node-gyp 来编译, node-gyp 在第一次编译的时候, 需要依赖 node 源代码, 于是又会去 node dist 下载, 于是大家又会吐槽, 怎么 npm 安装这么慢…
好吧, 于是又要提到 --disturl参数, 通过中国镜像来下载:

1
2
3
$ npm install microtime \
--registry=http://registry.npmmirror.com \
--disturl=https://npmmirror.com/mirrors/node

再次要提到 cnpm cli, 它已经默认将 --registry--disturl 都配置好了, 谁用谁知道 . 写到这里, 就更快疑惑那些不想安装 cnpm cli 又吐槽 npm 慢的同学是基于什么考虑不在本地安装一个 cnpm 呢?

nodejs 源码路径
对于在淘宝上下载 nodejs 源码指定的地址是: [https://npmmirror.com/dist](https://npmmirror.com/dist)

直接更改源文件中的配置文件地址来更改加载路径
~/node_modules/npm/lib/config/defaults.js
Line : 181
registry : "https://registry.npmjs.org/"
将这个注册地址 更改为: [https://registry.npmmirror.com/](https://registry.npmmirror.com/)

Nodejs Release 镜像使用帮助

Nodejs Release 为各平台提供预编译的 nodejs 和 npm 等二进制文件,是 https://nodejs.org/dist/ 的镜像。

使用方法:

1
2
# 设定环境变量
export NODE_MIRROR=http://npmmirror.com/mirrors/node

参考网站:

更新说明

2021 年 10 月 27 日

[WIP] 怎样在 JavaScript 中检测 `null`

原文地址 : https://javascript.plainenglish.io/how-to-check-for-null-in-javascript-dffab64d8ed5

因为一些历史 bug, typeof null 在 JavaScript 中返回 object – 那么我们怎么检测 null 呢?

什么是 null ?

“The value null represents the intentional absence of any object value. It is one of JavaScript’s primitive values.” — MDN Docs

The JavaScript primitive type null represents an intentional absence of a value — it is usually set on purpose to indicate that a variable has been declared but not yet assigned any value.

This contrasts null from the similar primitive value undefined , which is an unintentional absence of any object value.

That is because a variable that has been declared but not assigned any value is undefined, not null.

Unfortunately, typeof returns "object" when called on a null value, because of a historical bug in JavaScript that will never be fixed.

That means checking for null cannt be performed using typeof.

null is falsy

null is a falsy value (i.e. it evaluates to false if coerced to a boolean)” — Josh Clanton at A Drip of JavaScript

The simplest way to check for null is to know that null evaluates to false in conditionals or if coerced to a [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean) value:

Of course, that does not differentiate null from the other falsy values.

Next, I explore using the == or === equality operators to check for null.

Falsy equality using ==

“Despite the fact that null is [falsy], it isn’t considered loosely equal to any of the other falsy values in JavaScript. In fact, the only values that null is loosely equal to are undefined and itself.” —Josh Clanton at A Drip of JavaScript

One way to check for null in JavaScript is to check if a value is loosely equal to null using the double equality [==](https://medium.com/better-programming/making-sense-of-vs-in-javascript-f9dbbc6352e3) operator:

As shown above, null is only loosely equal to itself and undefined, not to the other falsy values shown.

This can be useful for checking for the absence of value — null and undefined both indicate an absence of value, thus they are loosely equal (they have the same value even though they are different types).

So, when programming to check if a variable has any value at all before trying to process it, you can use == null to check for either null or undefined.

Strict equality using ===

Tomake sure we have exactly a null value, excluding any undefined values, using the triple equality [===](https://medium.com/better-programming/making-sense-of-vs-in-javascript-f9dbbc6352e3) operator will do the trick:

Generally, it is a good idea to catch both null and undefined values, as both represent an absence of a value.

That means checking for null is one of the few times in JavaScript that using == is recommended, while otherwise [===](https://medium.com/better-programming/making-sense-of-vs-in-javascript-f9dbbc6352e3) is generally recommended.

Comparing == vs === when checking for null

Some JavaScript programmers prefer everything to be explicit for clarity, and there is nothing wrong with that.

Indeed, the code linter JSLint explicitly disallows [==](https://jslint.com/help.html) to prevent accidental bugs resulting from type coercion.

Another popular code linter, ESLint, has similar but more-configurable behavior around the use of == vs. ===.

That means that if you (or your linter) are in the habit of always using the strict equality operator ===, then you can check whether a value strictly equals null OR (||) strictly equals undefined instead of using ==:

It is more verbose than the == operator, but everyone who reads your code will clearly know that both null and undefined are being checked for.

A real world example of when to check for null

“One way this error [‘null is not an object’] might occur in a real world example is if you try using a DOM element in your JavaScript before the element is loaded. That’s because the DOM API returns null for object references that are blank.” — Rollbar on the Top 10 JavaScript errors

This TypeError (“null is not an object”) can occur if the DOM elements have not been created before loading the script, such as if the script higher than the HTML on the page, which is interpreted from top-to-bottom.

The solution would be using an event listener that will notify us when the page is ready, and then running the script.

But still, it might be prudent to check if the DOM element is null before trying to access it.

Use typeof anyway with falsy power

“Thankfully since null isn’t really an object, it’s the only ‘object’ that is a falsy value, empty objects are truthy.” — Casey Morris in Daily JS

Another method of checking for null is based on the fact that null is falsy, but empty objects are truthy, so null is the only falsy object.

This can be conveniently checked using the logical NOT [!](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Description) operator:

Using typeof can be a helpful trick, because if a variable is undeclared, then trying to reference it will throw a [ReferenceError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError).

But, the typeof an undeclared value is undefined, so using typeof can be a good way to check for nullundefined, and undeclared variables.

Using Object.is()

The ES6 function [Object.is()](https://medium.com/coding-at-dawn/es6-object-is-vs-in-javascript-7ce873064719) differs from the strict [===](https://medium.com/better-programming/making-sense-of-vs-in-javascript-f9dbbc6352e3) and loose [==](https://medium.com/better-programming/making-sense-of-vs-in-javascript-f9dbbc6352e3) equality operators in how it checks for [NaN](https://medium.com/coding-in-simple-english/how-to-check-for-nan-in-javascript-4294e555b447) and negative zero [-0](https://medium.com/coding-at-dawn/is-negative-zero-0-a-number-in-javascript-c62739f80114).

For null, the behavior of Object.is() is the same as ===:

That means that you will need to explicitly check for both null and undefined if you are using Object.is(), which is the helper method checking for changes in state under the hood in React.

Conclusion

Checking for null is a common task that every JavaScript developer has to perform at some point or another.

The typeof keyword returns "object" for null, so that means a little bit more effort is required.

Comparisons can be made: null === null to check strictly for null or null == undefined to check loosely for either null or undefined.

The value null is falsy, but empty objects are truthy, so typeof maybeNull === "object" && !maybeNull is an easy way to check that a value is not null.

Finally, to check if a value has been declared and assigned a value that is neither null nor undefined, use typeof:

Now go out there and check for null with confidence!