0%

跨域问题一则

问题提出

今天在编写一个简单的html时,遇到了如下问题,html中引入了一个js文件,且html和js位于同一个目录下。

index.html

1
2
3
4
5
6
7
8
<html>
<body>
<script type="module">
import add from "./index.js";
console.log(add(1, 2));
</script>
</body>
</html>

index.js

1
2
3
export default function add(a, b) {
return a + b;
}

当我使用浏览器打开html文件时,Chrome浏览器控制台报错:

1
Access to script at 'file:///Users/philip/Projects/test.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

查看network tab,发现如下信息,在status一栏,发现跨域错误:CORS error

Alt text

首先复习一下什么是同源策略
同源策略是浏览器的一个安全功能,它要求一个网页只能与其本身的服务器进行交互,而不能与其他域的服务器进行交互。这样可以防止恶意的网站窃取数据。

从具体的实现来分析,协议,域名,端口号都相同的两个url就是同源的。以https://www.example.com:8080为例,它的组成部分如下:

如果省略端口,那么http默认是80,https默认是443。

那么问题来了,为什么我引入的js文件会报错呢?因为浏览器认为这是一个跨域请求,而不是同源请求。这是因为我们的html文件是通过http://协议打开的(双击html文件时,会使用系统默认的浏览器打开,这时观察浏览器中的地址,可知是http协议。),而html中引用的js文件是通过file://协议引入的,协议不同,所以浏览器认为这是一个跨域请求。

解决办法

  1. WebStorm - WebStorm打开网页时没有这个问题,因为WebStorm内置了一个服务器,所以它的协议是http://,所以不会报错。
  2. VSCode - 安装插件Live Server,使用Live Server打开网页,也不会报错。
    1. npm install -g live-server
    2. Open terminal, cd 到html文件所在目录
    3. run live-server .

当然也可以使用http-server

问题提出

今天刚到公司,前端组的小美找到我,说有一个数组类型搞不定,让我帮忙看一下,简单来说就是小美调用api后返回的数据无法赋值给下面这个类型。

先看一下这个返回值类型定义,注意这里的students是一个数组字面量,而不是普通的数组,数组中的元素是一个对象,对象中有nameage两个属性。

1
2
3
4
5
6
7
8
interface response {
students: [
{
name: string;
age: number;
}
];
}

下面是小美的API返回的数据,注意这个返回值是一个数组,而非数组字面量。

1
2
3
4
5
6
7
8
9
10
11
12
const data: response = {
students: [
{
name: 'Philip',
age: 18,
},
{
name: 'Tom',
age: 20,
},
],
};

返回值中students数组中的元素个数是不对的,因为students的类型定义是一个元素的数组字面量,但是返回值中的students却包含两个元素(这相当于把students当作数组类型来使用,而不是当作数组字面量来使用)。所以TS会给出如下提示:

1
2
xxx is not assignable to type [{ name: string; age: number; }]
Source has 2 element(s) but target allows only 1

解决办法

为了避免上面对的错误提示,我们可以使用Array类型来定义students

1
2
3
4
5
6
interface response {
students: Array<{
name: string;
age: number;
}>;
}

如此定义,则students是一个数组,不再是数组字面量,这样就可以包含任意个元素了。当然也可以使用[]来定义students

1
2
3
4
5
6
interface response {
students: {
name: string;
age: number;
}[];
}

这种方法和上面的方法是等价的。这时,我们再使用上面的data就不会报错了。

1
2
3
4
5
6
7
8
9
10
11
12
const data: response = {
students: [
{
name: 'Philip',
age: 18,
},
{
name: 'Tom',
age: 20,
},
],
};

但是有一种特殊情况,就是类型定义有时是无法修改的,比如定义在第三方库中,这时候我们不直接赋值,而是把返回值push到数组中,这样就不会报错了。

1
2
3
4
5
6
7
8
9
10
const data: response = {
students: [
{
name: 'Philip',
age: 18,
},
],
};

data.students.push({ name: 'John', age: 20 })

github port 22 not working

今天在使用github的时候,发现git push的时候报错:

1
2
3
4
5
ssh: connect to host github.com port 22: Connection timed out
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

解决办法,使用443端口,首先测试是否可用。

1
ssh -T -p 443 git@ssh.github.com

如果可以正常连接,那么就可以修改~/.ssh/config文件,添加如下内容:

1
2
3
Host github.com
Hostname ssh.github.com
Port 443

然后再次测试:

1
ssh -T git@github.com

输出如下信息:

1
Hi Philip! You've successfully authenticated, but GitHub does not provide shell access.

接下来就可以愉快的使用git了。

Reference

Groovy JSON operation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// Convert a groovy map to json object
Map user = [name: 'zdd', age: 18, info: [address: 'beijing', phone: '123456789']];
println JsonOutput.toJson(user)

// Convert a json object to groovy map
def jsonString = '{"name": "John", "age": 30, "city": "New York"}'
def jsonSlurper = new JsonSlurper()
def jsonObject = jsonSlurper.parseText(JsonOutput.toJson(user))
println jsonObject

// Given a string "feat(configuration): id-xxxx add configuration for user page", please extract the jira id: id-xxx
// can you do it with regex? the target string start with ": " and end with " ", return null if not found
def str = "feat(configuration): id-xxxx add configuration for user page"
def jiraId2 = str =~ /: (.*?) /
println jiraId2[0][1]

async & defer, what’s the differences?

Html引入外部脚本

Html中引入外部脚本的方式有很多种,最常见的就是使用<script>标签,我们可以在<head>或者<body>中引入外部脚本。

1
2
3
4
5
<head>
<script src="./my_script.js"></script>
<script async src="./my_script1.js"></script>
<script defer src="./my_script2.js"></script>
</head>
1
2
3
4
5
<body>
<script src="./my_script.js"></script>
<script async src="./my_script1.js"></script>
<script defer src="./my_script2.js"></script>
</body>

那么从<head><body>中引入外部脚本有什么区别呢?

  • <head>中引入外部脚本,会阻塞Dom的解析,直到脚本下载完毕并执行完毕。
  • <body>末尾中引入外部脚本,不会阻塞Dom的解析,脚本下载和Dom解析并行进行。

你一定听说过上面的说法,但是上面的说法有个前提,那就是同步引入脚本,也就是不加async或者defer.

如果使用async或者defer关键字,那么在<head>中引入外部脚本和在<body>末尾引入外部没有太大区别。(只是<head>中的脚本会先于<body>中的脚本下载)

async & defer

在Html引入外部脚本时,可以使用async或者defer,那么两者有和区别呢?我们通过一个表格来分析一下:

关键字 下载时机 执行时机 是否阻断Dom解析 多个脚本的执行顺序
遇到对应的<script>标签,立即下载 下载后立即执行 是,下载阶段和执行阶段都阻断DOM解析。 按顺序执行
async 遇到对应的<script>标签,立即下载 下载后立即执行,只能保证在window.load事件之前执行,但是可能在window.DomContentLoade之前或之后。 没有固定顺序,取决于哪个脚本先下载完成。
defer 遇到对应的<script>标签,立即下载 在页面解析完之后,且在DomContentLoaded事件触发之前执行。 <script>标签出现的顺序执行

注意事项:

  1. async脚本的执行无固定顺序,谁先下载完,谁先执行。
  2. defer脚本按出现的先后顺序执行。

以下代码中,我们使用defer加载两个脚本,其中short.js非常小,很快就下载完了,而long.js非常大,下载时间很长。因为defer脚本是按照书写顺序进行执行的,所以即使short.js先下载完了,也要等到long.js下载完毕才能开始执行。

1
2
<script defer src='https://xxx/yyy/long.js'>
<script defer src='https://xxx/yyy/short.js'>

用一张图来总结一下,图里中颜色含义如下:
绿色线条 - 表示dom解析
蓝色线条 - 表示脚本的下载
红色线条 - 表示脚本的执行。
script分为普通脚本和模块化脚本。

https://html.spec.whatwg.org/images/asyncdefer.svg

解释一下上面这张图:

  1. 第一行,普通脚本,没有指定async或者defer,下载和执行阶段会阻断dom解析。
  2. 第二行,普通脚本,有defer关键字,下载和执行阶段都不阻断dom解析。(下载和dom解析并行进行,执行在dom解析完成后开始)
  3. 第三行,普通脚本,有async关键字,下载阶段不阻断dom解析,但执行阶段有可能阻断dom解析(如果脚本已经下载完毕,但是dom解析尚未完成的情况下)
  4. 第四行,模块化脚本,默认包含defer属性,该脚本及其依赖的其他脚本的下载与dom解析平行进行,待dom解析完毕开始执行脚本。
  5. 第五行,模块化脚本,有async关键字,该脚本及其依赖的其他脚本的下载与dom解析平行进行,下载完毕后立即执行脚本。

async & defer

  • 标记为async或defer的script,下载阶段都不会阻断Dom的解析,但是async是下载后立即执行,而defer是下载后且等待dom解析完毕才执行,所以两者唯一的区别就是:async脚本执行阶段可能会阻断dom解析(前提是脚本已经下载完毕,但dom解析尚未完成)。
  • module script默认包含defer属性
  • 多个标记为async的脚本,无法保证执行顺序。
  • 多个defer脚本按照script标签出现的顺序执行。
  • 没有标记async或defer的脚本会阻断Dom的解析。

window.load和window.DomContentLoaded

这是两个重要的事件,与async及defer的执行时机息息相关。

  • window.load - 标志整个页面全部加载完成,包括images,styles和JavaScript等所有外部资源。
  • window.DomContentLoaded - Html文件解析和加载完成(parsed and loaded),且所有标记为defer的js脚本全部下载并执行完成后触发,注意,该事件不会等待其他资源,比如images,subframes,或者标记为async的script下载完成。另外,该事件不会等待stylesheet完成,但是:因为defer脚本会等待stylesheet加载完才执行,而该事件又在defer脚本执行完才触发,所以如果有defer脚本存在的话,那么该事件一定会等待stylesheet加载完才触发。

问题来了

看起来async和defer没有太大的区别,那么两者分别在什么场合使用呢?

  • async一般用在与当前页面无关联的外部脚本,比如Google统计,计数脚本等。
  • defer一般用于需要操作当前页面的脚本,所以它需要等Dom解析完之后才执行。

一道小题

下面代码的输出结果是什么?

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="container"></div>
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded event triggered");
});
window.addEventListener("load", () => {
console.log("load event triggered");
});
</script>
<script async src="./async_script.js"></script>
<script defer src="./defer_script.js"></script>
</body>

async_script.js

1
console.log('async script loaded');

defer_script.js

1
console.log('defer script loaded');

答案:

1
2
3
4
async script loaded
defer script loaded
DOMContentLoaded event triggered
load event triggered

多执行几次,你会发现,有时候输出还会是下面这样的。这充分印证了上面的结论,defer script一定在DOMContentLoaded之前执行,但是async script可能在DOMContentLoaded之前,也可能在其之后。

1
2
3
4
defer script loaded
DOMContentLoaded event triggered
async script loaded
load event triggered

References:

https://javascript.info/script-async-defer

https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer V8引擎

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script

https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event

https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event

https://javascript.info/onload-ondomcontentloaded

Famous NPM Packages

cross-env

cross-env is a cross platform solution to setting and using environment variables. It’s available as a command line utility as well as a Node.js module.

cross-env 是一个用于处理跨平台环境变量的 Node.js 包。在 Node.js 应用程序中,设置环境变量通常使用 process.env。然而,不同的操作系统(如 Windows、macOS 和 Linux)有不同的方式来设置和获取环境变量。cross-env 的目标是提供一种简单的方法来在跨平台上设置和获取环境变量。

How Jest Works(Jest是如何工作的)

1. 什么是Jest

Jest是由Facebook(Meta)开发的一个Javascript测试框架。支持多种前端框架,比如Babel, TypeScript, Node, React, Angular, Vue等等。它的特点是快速,简单,可扩展。

那么多文件都是干啥的?

如果你配置过Jest的测试环境,你会发现Jest用到很多文件,比如jest.config.js, tsconfig.json, setupTest.js, 等等。它们的作用是什么呢?

简单来说,当你在命令行执行npm run test时,node会调用Jest,Jest会读取jest.config.js文件,解析配置,然后执行测试。而jest.config.js文件中会指定一些其他的配置文件,setupTest.js等等。

2. 基于Nx框架的Jest配置

我们目前的项目基于Nx框架,其实Nx本身对于Jest已经支持的很好了,几乎不用做太多的配置,但是本文仍以Angular为例,看看如何配置Jest。

当我们使用Nx创建一个Angular项目时,会自动创建一个jest.config.ts文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* eslint-disable */
export default {
displayName: 'todos',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
},
coverageDirectory: '../../coverage/apps/todos',
transform: {
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

transform一节,我们看到了jest-preset-angular,这个是Angular官方提供的一个Jest预设,它会自动帮我们处理Angular的一些特殊文件,比如.ts, .html, .svg等等。如果我们不使用Angular,而是使用React,那么我们需要使用jest-preset-react,如果是Vue,那么我们需要使用vue-jest。如果我们使用的是Nx,那么我们可以使用@nrwl/jest,它会自动帮我们处理Nx的一些特殊文件,比如.ts, .html, .svg等等。

为什么需要transform呢?因为Jest只能处理JavaScript,对于TypeScript/JSX等高级的文件格式,我们必须先将其转换为JavaScript,然后再交给Jest处理。

transform块的含义是,对于所有以.ts, .mjs, .js, .html结尾的文件,都使用jest-preset-angular来处理。

如果transform配置不正确,就会出现如下错误:

1
2
3
4
5
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.

How jest parse config and execute tests?

Jest是如何解析配置文件,并执行测试的呢?我们可以通过--debug参数来查看Jest的执行过程。

To be continue…

References

如何写榜书

榜书需用熟纸,最好是铜版纸,十分光滑。墨需要用浓墨,最好买超浓墨汁,也可以自制宿墨(将墨水准备好之后,放置一晚),最见精神,如果写完纸张发皱,可用熨斗熨一下。笔可用兼毫,写大字时一定要将笔毫铺开,方能如意!不可只用笔尖,那是小字的笔法。
初练榜书时,可用报纸,将一张报纸从中间裁开,每张写一字,

如何让墨汁好写

可以在墨汁中添加洗洁精或者液态胶水,今天晚上我就加了一点液态胶水,效果很好,墨汁不会太稀,也不会太浓,很好写。非常丝滑,胶水不用太好的,就文具店那种白色液体胶水即可。

宿墨

宿墨可以自行制作,也可以买现成的宿墨,今日见抖音作者用宣和宿墨写豆腐宣,很是得手。

笔法

写字需笔笔交待清楚!要把动作做到位,不可草率。

毛笔

  1. 山马就是马尾巴,这个称呼来自岛国,这事今天在抖音武京生老师直播间学到的,特此记录。

How many ways to convert a value to string in JavaScript

1. value.toString()

1
2
const num = 123;
const str = num.toString();

2. String(value)

1
2
3
const num = 123;
const str = String(num);
console.log(str);

3. value + ‘’

1
2
const num = 123;
const str = num + '';

4. `${value}`

const num = 123;
const str = `${num}`;

5. JSON.stringify(value)

1
2
const num = 123;
const str = JSON.stringify(num);

What’s the difference between them?

undefined in javascript

undefined是可以说是javascript中最特殊的一个类型,许多其他语言中都没有这个类型。它表示一个变量已经声明,但还没有被赋值。

1
let a; // a的值是undefined

如果没有掌握好undefined,则可能会写出一些非常业余的代码,我们来看一个例子,这是在做code review时遇到的真实例子,你能看出下面的代码有什么问题吗?

1
2
3
4
5
6
7
function getUserName(user) {
if (user) {
return user.name;
} else {
return undefined;
}
}

在揭晓答案之前,我们先系统学习一下什么情况下JavaScript会产生undefined类型。

1. 显式undefined类型

1
2
const a = undefined;
console.log(a); // undefined.

2. 未初始化的变量

1
2
let a;
console.log(a); // undefined.

3. 访问对象中不存在的属性

1
2
3
4
5
6
7
8
9
10
const person = {
name: 'zdd',
age: 41,
};

console.log(person.gender); // undefined.


const a = [1, 2, 3];
console.log(a[3]); // 数组只有三个元素,而a[3]表示第四个元素,所以它们的值是undefined

4. 函数没有返回值

函数没有返回值的时候,实际上返回的就是undefined

1
2
3
4
5
function test() {
console.log('hello, world!');
}

console.log(test()); // undefined.

5. 调用函数没有传递对应的参数

下面的代码中,函数add没有传入任何参数,所以ab的值都是undefined

1
2
3
4
5
6
7
function add(a, b) {
console.log(a); // 输出undefined.
console.log(b); // 输出undefined.
return a + b;
}

add(); // 没有传递参数

undefined != not defined

需要注意的是,undefinednot defined是两个不同的概念。undefined表示一个变量已经声明但还没有被赋值,而not defined表示一个变量没有被声明。

1
2
3
let a;
console.log(a); // undefined.
console.log(b); // error, b is not defined

undefined vs void 0

既然已经有了undefined,为什么有很多JavaScript库中还使用void 0呢? 原因就是undefined是一个值,而不是关键字,能被用户串改,看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
const undefined = 1; // undefined被用户篡改!

const add = (a, b) => {
// 这里判断参数是否传入,结果失效了,因为undefined值在前面被改成了1
if (a === undefined || b === undefined) {
console.error('请输入两个数');
} else {
return a + b;
}
};

add(1, 2); // 这里会执行add函数中的if分支,是不是很崩溃?

使用void 0就不会有这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
const undefined = 1;

const add = (a, b) => {
// 写成void 0就没有问题了,无论undefined被改成什么,都不影响。
if (a === void 0 || b === void 0) {
console.error('请输入两个数');
} else {
return a + b;
}
};

console.log(add(1, 2));

那么为什么void 0返回undefined呢?这是因为void是一个操作符,它的作用是对其后面的表达式求值,然后返回undefined。在JavaScript中,void 0等价于undefined,其实你也可以写void 1, void 'hello'等,结果都是undefined

void expression的求值规则 - 先对expression求值,然后返回undefined

正则表达式中的undefined

在正则表达式中,可以使用test来测试某个字符串是否满足特定的规则。

1
console.log(/^hello/.test('hello, world!')); //true

如果你没有传递参数给test,那么它会尝试匹配字符串undefined.

1
console.log(/undefined/.test()); // true

This is equivalent to the following code, since undefined convert to string is 'undefined', so the result is true.

1
console.log(/undefined/.test(undefined));

详情请看这里

undefined vs null

undefinednull经常被放到一起比较,那么他们之间有什么区别呢?

  • undefined表示一个变量已经声明但还没有被赋值,
  • null表示一个变量已经被赋值为一个空值。
  • null是JS中的关键字,但是undefined是一个全局属性。

undefined与其他类型之间的转换

这里面比较特殊的是和数字类型之间的转换,undefined转换为数字类型时会返回NaN,而null转换为数字时会返回0

1
2
3
console.log(String(undefined)); // "undefined"
console.log(Number(undefined)); // NaN
console.log(Boolean(undefined)); // false

注意null转换为其他类型时与undefined的区别

1
2
3
console.log(String(null)); // "null"
console.log(Number(null)); // 0
console.log(Boolean(null)); // false

回到文章开始的问题,根据上面第四点,函数没有返回值时,返回的就是undefined,所以上面的代码可以简化为如下形式,else分支完全没有必要。

1
2
3
4
5
function getUserName(user) {
if (user) {
return user.name;
}
}

References:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#undefined_type
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void