- 👨🏻💻 I'm a full-stack developer
- 💓 Currently developing with java + React + Typescript
- 🛠️ Coding since 2015
yijinc / yijinc.github.io Goto Github PK
View Code? Open in Web Editor NEW我的博客笔记📒
Home Page: https://yijinc.github.io/blog
我的博客笔记📒
Home Page: https://yijinc.github.io/blog
react-native 通过 WebView 组件可以非常简单的实现通信,这里通过从RN中分离出来的react-native-webview 为例,介绍 WebView 的通信机制。
react-native-webview 与 web 通信,提供以下3个方法
injectedJavaScript
属性injectJavaScript
方法(this.refs.webviewr.injectJavaScript(...))onMessage
属性回调, web调用window.ReactNativeWebView.postMessage
import React, { Component } from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';
export default class App extends Component {
render() {
const injectedJavaScriptCode = `document.body.style.backgroundColor = 'red';`
setTimeout(() => {
// rn 注入调用(执行) web的 window.alert 方法
this.webref.injectJavaScript(`window.alert('hello')`);
}, 3000);
return (
<View style={{ flex: 1 }}>
<WebView
ref={r => (this.webref = r)}
source={{
uri: 'https://xxx.com/index.html'
}}
// 向web注入js代码
injectedJavaScript={injectedJavaScriptCode}
// 监听web发送过来的消息
onMessage={event => {
alert(event.nativeEvent.data);
}}
/>
</View>
);
}
}
可以看出,WebView 与 Web 通信非常简单,但却是单向的:WebView 可以触发 Web的方法(一般为全局window下的),Web 也可以发送消息/数据到 WebView。
如果 Web 想要获取 app 内的的数据(甚至携带一些参数),得到一个返回/响应,WebView 如何将这个返回值响应给 Web?
// web code
getAppVersion('ios', (version) => alert(version))
我们可能会在打开webview时 注入一些初始化的js代码和数据,但这肯定无法满足 web 在使用期间需要获取app数据。
// web code
window.initDataFromApp = (data) => { /**do something **/}
// RN code
<WebView
injectedJavaScript={`initDataFromApp && initDataFromApp(${JSON.stringify(data)});`}
/>
要实现 RN -> web 或 web -> RN 有响应的通信,通过webview提供的三个基本方法,我们可以自己封装一套通信回调的机制。
直接上代码
/***
* postJsCode.js
* 预注入webview javascript code
* web端使用:
* window.APP.invokeClientMethod('getList', { page: 1 , size: 10}, callback);
* * */
function clientMethod() {
var APP = {
__GLOBAL_FUNC_INDEX__: 0,
invokeClientMethod: function (type, params, callback) {
var callbackName;
if (typeof callback === 'function') {
callbackName = '__CALLBACK__' + (APP.__GLOBAL_FUNC_INDEX__++);
APP[callbackName] = callback;
}
window.ReactNativeWebView.postMessage(JSON.stringify({type, params, callback: callbackName }));
},
invokeWebMethod: function (callback, args) {
if (typeof callback==='string') {
var func = APP[callback];
if (typeof func === 'function') {
setTimeout(function () {
func.call(this, args);
}, 0);
}
}
},
};
window.APP = APP;
window.webviewCallback = function(data) {
window.APP['invokeWebMethod'](data.callback, data.args);
};
}
WebViewScreen.js
import React, { Component } from 'react';
import { SafeAreaView, Platform } from 'react-native';
import { WebView } from 'react-native-webview';
import clientMethod from './postJsCode';
const patchPostMessageJsCode = `(${String(clientMethod)})(); true;`;
export default class WebViewScreen extends Component {
webref = null;
/***
* 接收web发送过来的消息,调用rn中提供的方法
*/
onMessage = event => {
var data = JSON.parse(event.nativeEvent.data);
if (!data) {
return;
}
const { type, params, callback } = data;
switch (type) {
case 'getUser':
const json = {
callback,
args: {
name: '王者荣耀',
age: 29,
}
};
this.webref.injectJavaScript(`webviewCallback(${JSON.stringify(json)})`);
break;
// 导航到 app 指定 screen/page
case 'navigate':
const { screen } = params;
this.props.navigation.navigate(screen);
break;
}
}
render() {
return (
<SafeAreaView style={{ flex: 1, }}>
<WebView
onMessage={this.onMessage}
ref={r => (this.webref = r)}
injectedJavaScript={patchPostMessageJsCode}
source={{ uri: 'https://xxx.com/index.html' }}
/>
</SafeAreaView>
);
}
}
这里主要预定义了type, params, callback 参数,在web中使用: window.APP.invokeClientMethod(type, params, callback)
, type 在 RN的 onMessage 处理对应 case。
其原理步骤是:
window.APP['__GLOBAL_FUNC_INDEX__0'] = callback
this.webref.injectJavaScript
触发调用 放在全局的 window.APP['__GLOBAL_FUNC_INDEX__0']
回调由于Javascript是单线程执行的,__GLOBAL_FUNC_INDEX__
属性可以确保唯一,所以即使在并发调用时时,也可以正常回调。
注意,injectedJavaScript 属性 注入时机在ios和android表现不一样,在ios上 注入代码页面打开马上可以得到,然而在android 上是延迟被注入的,(像是在页面加载完后在尾部引入Script脚本,没有深入研究了),所以在web开发中 组件 mounted 时 务必要等待 injectedJavaScript 注入完成才做响应处理。
早期RN版本,是web直接调用window.postMessage通信的, 设置onMessage的同时会在webview中注入一个postMessage的全局函数并覆盖可能已经存在的同名实现。可能需要对web原生postMessage做个兼容。然后我们可以在web中 监听 onMessage 触发回调(或者在组件挂载期间监听message)
function patchPostMessageFunction(){
const originalPostMessage = window.postMessage;
const patchedPostMessage = (message, targetOrigin, transfer) => {
originalPostMessage(message, targetOrigin, transfer);
};
patchedPostMessage.toString = () => String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
window.postMessage = patchedPostMessage;
if(window.APP) {
window.document.addEventListener('message', function (e) {
const callbackObj = JSON.parse(e.data);
window.APP['invokeWebMethod'](callbackObj.callback, callbackObj.args);
});
}
}
解析url中的queryString
入参格式参考:
const url = "https://www.youzan.com?name=coder&age=20&callback=https%3A%2F%2Fyouzan.com%3Fname%3Dtest&list[]=a&list[]=b&json=%7B%22str%22%3A%22abc%22,%22num%22%3A123%7D"
出参格式参考:
{
name: "coder",
age: "20",
callback: "https://youzan.com?name=test",
list: ["a", "b"],
json: {
str: 'abc',
num: 123
}
}
[TOC]
跨源资源共享(Cross-Origin Resource Sharing)是一种基于 HTTP 头的机制。出于安全性,浏览器限制脚本内发起的跨域请求, 例如,XMLHttpRequest 和 Fetch API 遵循 同源策略(Same-origin policy)。CORS 机制允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
简单请求(不会触发 CORS 预检请求)需满足所有下述条件:
简单请求涉及的请求头有 Origin、Access-Control-Allow-Origin、Access-Control-Expose-Headers、 Access-Control-Allow-Credentials等;
CORS要求,那些可能对服务器数据产生副作用的 HTTP 请求,浏览器必须首先使用 OPTIONS
方法发起一个 预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。
预检请求头字段
字段名称 | 说明 |
---|---|
Origin | 表明预检请求或实际请求的源站 domain |
Access-Control-Request-Headers | 用于预检请求,将实际请求头告诉服务器 |
Access-Control-Request-Method | 用于预检请求,将实际请求方法告诉服务器 |
预检响应头字段
字段名称 | 说明 |
---|---|
Access-Control-Allow-Origin | 指定允许访问该资源的外域 URI,可以设置为***** 允许所有域的请求 |
Access-Control-Allow-Headers | 响应预检请求,指明了实际请求中允许携带的首部字段 |
Access-Control-Allow-Methods | 响应预检请求,指明了实际请求所允许使用方法 |
Access-Control-Max-Age | 指定预检请求的结果能够被缓存多久,如果在有效期内,再次请求将不会发起预检请求 |
Access-Control-Allow-Credentials | 指明了实际的请求是否可以使用 credentials |
Access-Control-Expose-Headers | 服务器暴露一些自定义的相应头,允许客户端问(否则response是拿不到这些头字段的) |
一般情况,对于跨源 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证息(cookie)。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials字段
Access-Control-Allow-Credentials: true
另一方面,开发者必须在 XMLHttpRequest 或 Fetch 请求中明确指明附带身份凭证
// XMLHttpRequest withCredentials
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.other.com/resources', true);
xhr.withCredentials = true;
xhr.onreadystatechange = handler;
xhr.send();
// Fetch withCredentials
fetch('https://api.other.com/resources', {
mode: "cors",
credentials: "include"
});
服务器在响应附带身份凭证的请求时:CORS 响应头(Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Allow-Methods)的值**不能设为通配符 *** ,而应将其设置为确定的值,否则会请求失败。
Cookie 策略受 SameSite 属性控制,如果 SameSite 值不是 None
,就算设置了withCredentials,cookie 也不会被发送到跨源的服务器。
响应头中也可以携带 Set-Cookie 字段,尝试对 Cookie 进行修改。如果用户浏览器的第三方 cookie 策略设置为拒绝所有第三方 cookies,那么会操作失败,将会抛出异常。
设置允许跨站发送的cookie,但这样可能导致 跨站请求伪造(Cross-site request forgery,CSRF)攻击变得容易。
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly; Secure; SameSite=None
基于上述CORS机制,服务端配置成允许跨源请求就行
如果服务器未使用 *****,而是指定了一个域,那么为了向客户端表明服务器的返回会根据Origin请求头而有所不同,必须在Vary响应头中包含Origin
Access-Control-Allow-Origin: https://www.frontend.com
Vary: Origin
浏览器仅会限制脚本内发起的跨域请求,而 script、img 标签没有跨域限制。所以可以通过script 标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据放到callback函数中,返回给浏览器,浏览器的callback解析执行,从而前端拿到callback函数返回的数据。
其实对于任一服务器都可以做代理转发处理,nginx 大概是用得最多且最简单的服务器代理了。
server {
listen 80;
server_name www.example.com;
location /api {
proxy_pass http://api.server.com; # 后端服务地址
}
location / {
root html/static; # 前端静态资源路径
# proxy_pass http://api.server.com; # 或前端服务地址
}
}
在日常开发中一般会直接使用 构建工具的代理(node server代理)配置,比如 webpack
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
},
},
},
};
我们可以在系统的host文件 增加 ip = host
映射,本地访问域名host时,会去访问真是ip地址,比如 127.0.0.1,一般在调试需要SSO登录的应用的时候经常会用这种方式。
hosts文件位置在
Windows:C:\Windows\System32\drivers\etc\hosts
Mac:/etc/hosts
window.postMessage 方法可以通过第二个参数 targetOrigin 安全地实现跨源通信。
示例:2个跨源页面的通信
aaa.com/a.html 需要跨域请求数据的页面
bbb.com/b.html 是同源的页面
以上是最常见的几种跨域解决方案,还有一些不太常用的方法 比如
# 指令 选项 参数(1) 参数(2)
command [-options] parameter1 parameter2 ...
# [--options] 使用选项的完整全名,例如 --help;
man
查看 command 的使用说明书(manual pages),通常从 /usr/share/man 读取man command
# 搜寻特定指令/文件的man page说明文件
man -f command # 相当于 ==>
whatis command
# 按关键字搜索man page说明文件
man -k command # 相当于 ==>
apropos command
环境变量 $PATH 默认是放置在 /usr/share/info/
set
可以列出目前 bash 环境下的所有变量
按『tab键』:命令与文件补全功能
使用 history
查看执行过的历史指令,指令记录存放在 ~/.bash_history
,按『上下键』可以找到前/后一个输入的指令
命令别名设定: alias
, unalias
。例如 alias rm='rm -i'
路径与指令搜寻顺序:
login shell 会读取两个配置文件:
source
(或小数点) 将配置文件的内容读进来目前的 shell 环境中(更改配置文件后不需要注销立即生效)数据流重导向:
tee
会同时将数据流分送到文件去与屏幕# 将 stdout 与 stderr 分别存到不同的文件去
stdout > log.text 2> error.text
# 将 stdout 与 stderr 都写入同一个文件
stdout > log.text 2>&1
# 要注意! tee 后接的文件会被覆盖,若加上 -a (append) 这个选项则能将讯息累加
ls -l / | tee -a ~/homefile | more
|
仅会处理 standard output,在每个管线后面接的第一个数据必定是『指令』,而且这个指令必须要能够接受 standard input 的数据。常用管线处理命令 grep
、cut
、 sort
、 wc
、 uniq
、 split
、 xargs
Linux下一切皆文件,我们一般会用扩展名来表示不同种类的文件。
下达 ls -al
命令看看文件属性(-a: 包括目录和以 . 开头的隐藏文件;-l: 显示详细列表)
[root@serverxxx some-directory]# ls -la
总用量 8
drwxrwxr-x 7 user1 user1 132 12月 15 12:33 .
drwx------ 3 user1 user1 127 12月 15 14:51 ..
drwxrwxr-x 2 user1 user1 6 12月 15 12:21 .git
-rw-rw-r-- 1 user1 user1 66 12月 15 12:21 .gitignore
-rw-r--r-- 1 user1 user1 73 12月 15 12:21 README.md
-rw-r--r-- 1 user1 user1 1964 12月 15 12:33 package.json
drwxr-xr-x 8 user1 user1 256 12月 15 12:21 src
-rw-r--r-- 1 user1 user1 377 12月 15 12:21 tsconfig.json
每一行都有 7 列,先认识一下上面7个字段个别的意思:
drwxr-xr-x | 8 | user1 | user1 | 256 | 12月 15 12:21 | src |
---|---|---|---|---|---|---|
文件类型与权限 | 链接数 | 所有者 | 群组 | 文件大小 | 最后修改时间 | 文件名 |
第一栏代表这个文件类型与权限,这一栏共 10 个字符:
第 1 个表示文件类型
接下来的字符中,以3个为一组(共三组),且均为 rwx
的三个参数的组合。其中,r 代表可读(read)、w 代表可写(write)、x 代表可执行(execute)。 要注意的是,这三个权限的位置不会改变,如果没有权限,就会用 - 代替占位。
Linux 是个多人多任务的系统,Linux 一般将文件可存取的身份分为三个类别,分别是 owner/group/others,且三种身份各有 read/write/execute 等权限
当创建一个用户的时候,Linux 会为该用户创建一个主目录,路径为 /home/[username],我们可以使用 cd ~
,快捷进入当前用户 home 目录。如果你想放一个私密文件,就可以放在自己的主目录里,然后设置只能自己查看。
每个用户都有一个用户组,方便多人操作的时候,为一群人分配权限。当创建用户的时候,会自动创建一个与它同名的用户组。
如果一个用户同时属于多个组,用户需要在用户组之间切换,才能具有其他用户组的权限。
既不是 Owner 又不属于 Group,就是其他人。
Root 用户是万能的天神,该用户可以访问所有文件
chgrp(change group) 群组名需要存在 /etc/group
# -R:递归更改文件属组
chgrp [-R] 群组名 文件或目录
chown(change owner) 用户账号名需要存在 /etc/passwd
# -R:递归更改文件属组
chown [-R] 账号名称 文件或目录
chown [-R] 账号名称:组名 文件或目录
Linux 文件的基本权限就有九个,分别是 owner/group/others 三种身份各有自己的 read/write/execute 权限,我们也可以用数字表示权限,数字与字母的对应关系为:
每组 rwx 权限用数字累加表示,例如 rwxrwxr-x 对应的数字则是:
owner = rwx = 4+2+1 = 7
group = r-x = 4+0+1 = 5
others= - -x = 0+0+1 = 1
# 数字类型改变文件权限
# xyz 为 rwx 属性数值的相加值,例如上面 chmod 751 filename
chmod [-R] xyz 文件或目录
还有一种就是用字符改变权限的方法,我们用 u(user,也就是owner), g(group), o(others) 来代表三种身份的权限,a(all) 代表全部身份
chmod | u、g、o、a | +(加入)、-(除去)、 =(设定) | r、w、x | 文件或目录 |
---|
# 字符类型改变文件权限
# 设置 u(owner)具有rwx权限,go(group&others)具有rx权限
chmod u=rwx,go=rx text.txt
# u(owner) 加上 x 权限,g(group)和 o(others)除去 x 权限。
chmod u+x,g-x,o-x index.html
ssh [options] [-p PORT] [username@]hostname
# 例
ssh -p 3000 [email protected]
[root@my-azure]$ pwd
/home/root/
cd /home/workspace # 进入/home/workspace
cd ~ # 进入home目录
cd - # 回到上次所在目录,一般来回切换
mkdir folder-name # 创建目录
mkdir -p folder1/folder2/folder3 # 递归创建目录
touch new-file # 创建文件
echo "hello world"
# 将打印内容通过 > 输出到 a.txt 文件,追加使用 >>
echo "some content" > a.txt
cat ~/.ssh/id_rsa.pub
# 如果文件内容太多,可以使用可翻页查看命令 more|less
less /etc/man_db.conf
# 如果只想查看部分内容,可以使用撷取命令 head|tail
tail -n 10 -f /tomcat/log/messages # -n 10 显示10行,-f 监听文件修改实时显示
cp source_file_name target_file_name
cp -r app /home/www/app # -r 复制目录
mv workspace/project/index.html /home/www/app # 移动
mv index.html home.html # 更改文件名
rm package.lock
mv -rf dist # 直接删除整个目录
下面是常用的压缩命令,tar 是打包命令
压缩命令 | 选项与参数 | 打包文件拓展名 | 在 tar 中使用的参数 |
---|---|---|---|
gzip | c d t v # | *.gz | -z |
bzip2 | c d k z v # | *.bz2 | -j |
xz | c d t k l # | *.gz | -J |
我们以使用度最广的压缩指令gzip 为例
# 压缩
tar -zcvf xxx.tar.gz 要被压缩的文件或目录
# 解压
tar -zxvf xxx.tar.gz
which node # /root/.nvm/versions/node/v14.17.6/bin/node
# 如果需要查询系统文件(/bin/sbin、/usr/share/man)可以使用 whereis
whereis nginx
JavaScript 数据类型分为两种:
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。Array.prototype.slice/concat
, Object.assign
扩展运算符...
都是浅拷贝
var obj = { a: 1, b: { foo: 'foo' } };
var newObj = {...obj}; // 或 var newObj = Object.assign({}, obj);
newObj.b.foo = 0;
console.log(obj); // { a: 1, b: { foo: 0 } };
与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。
JSON.parse( JSON.stringify(obj) )
可以简单粗暴的作为深拷贝,但不能拷贝函数
自己实现一个深拷贝
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
一般在开发中会引用第三方工具库,会提供深拷贝方法 如 lodash的_.cloneDeep, jquery的$.extend, immutable的数据转换等
回溯算法
发现在typescript写的页面中, 这种js注入. JSON.stringify(data)做成的json字符串在onMessage里面,调用JSON.parse()会报错.JSON字符串根本不能转换成功.
ssh root@vps_ip -p ssh_port
wget --no-check-certificate -O shadowsocks-all.sh https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-all.sh
chmod +x shadowsocks-all.sh
./shadowsocks-all.sh 2>&1 | tee shadowsocks-all.log
根据命令提示选择 至完成。
启动/配置
ssserver -c /etc/shadowsocks-python/config.json -d start
# 更多命令查看 ssserver --help
客户端下载 macos
TCP BBR 是 Google 开源的 拥塞控制算法,类似锐速的单边加速工具。由于受到各方面限制,国外的vps速度不理想,偶尔有延迟、不稳定的现象出现。而bbr的作用,就是要解决这一问题
使用root用户登录,运行以下命令:
wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh
chmod +x bbr.sh
./bbr.sh
安装完成后,脚本会提示需要重启 VPS,输入 y 并回车后重启。重启后,执行命令:
lsmod | grep bbr
返回值有 tcp_bbr 模块即说明bbr已启动。
[1,'1',1]
, 则输出: [1,'1']
[{a: 1}, {b: 1}, {a: 1}]
, 则输出:[{a: 1}, {b: 1}]
[{a: 1}, {b: 1}, {a: 1}]
, 则输出:[{a: 1}, {b: 1}]
[{a: 1, b: 2}, {b: 1}, {b: 2, a: 1}]
, 则输出:[{a: 1, b: 2}, {b: 1}]
[[1, {a: 1}], [2], [3], [1, {a: 1}]]
, 则输出: [[1, {a: 1}], [2], [3]]
动态规划(Dynamic programming,DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于普通解法。
使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性。动态规划只解决每个子问题一次,并把子问题结果保存起来,以减少重复计算,再利用子问题结果依次计算出需要解决的子问题,直到得出原问题的解。
常用解题思路:
练习题
/**
* [leetcode 53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/)
* dp[i] 表示 第i个元素位置往前的 最大和
***/
function maxSubArray(nums: number[]): number {
const len = nums.length;
const dp = new Array(len).fill(0);
dp[0] = nums[0];
for (let i = 1; i < len; i++) {
if (dp[ i - 1] > 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
}
}
return Math.max(...dp);
};
var str = 'Hello world!'; // var str = new String('Hello world!')
str.substr(2, 4);
str.indexOf('world');
我们经常可以用到的字符串函数 substr
、replace
、indexOf
等,是因为 String 对象上的 prototype 预先定义了这些方法。str
为 String
的一个实例。
JavaScript标准库中常用的内置对象上 prototype 的方法还有:
Array.prototype.push
、 Array.prototype.push
Date.prototype.getDate
、 Date.prototype.getYear
Function.prototype.toString
、 Function.prototype.call
...
我们使用构造函数创建一个对象:
function User() {
}
var user = new User();
user.name = '张三';
console.log(user.name) // 张三
在这个例子中,User
是一个构造函数,我们使用 new
创建了一个实例对象 user
JavaScript 不包含传统的类继承模型,而是使用 prototype 原型模型。
每个函数都有一个 prototype 属性,换句话说, prototype 是函数才会有的属性
function User() {
}
User.prototype.name = '张三';
var user1 = new User();
var user2 = new User();
console.log(user1.name) // 张三
console.log(user2.name) // 张三
函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的 实例 的原型。也就是说这个例子中 User 的属性 prototype 对象是 user1 和 user2 的原型。
既然函数的 prototype 属性指向了一个对象,我们可以重写原型对象
function User() {}
User.prototype = {
name: '张三',
greeting: function() {
console.log('hello!')
}
};
var user = new User();
console.log(user.name); // 张三
user.greeting(); // hello!
这样,我们就可以 new User 对象以后,就可以调用 greeting 方法了。
然而将原子类型赋给 prototype 的操作将会被忽略
function User() {}
User.prototype = 1 // 无效
我们还可以在赋值原型 prototype 的时候使用 function 立即执行的表达式来赋值:
function User() {}
User.prototype = function() {
name = '张三',
greeting = function() {
console.log('hello!', this.name)
}
return {
name: name,
greeting: greeting
}
}();
(new User()).greeting();
它的好处就是可以封装私有的 function,通过 return 的形式暴露出简单的使用名称,以达到public/private的效果。
上述使用原型的时候,都是直接赋值原型对象,这样会覆盖之前已定义好的原型,导致之前原型上的方法或属性丢失,所以通常分开设置/覆盖 一个已知函数的 prototype
User.prototype.update = function() {}
所有 JavaScript 对象(null除外)都有的一个 __proto__
属性,这个属性指向该对象的原型
function User() {
}
var user1 = new User();
var user2 = new User();
console.log(user1.__proto__ === User.prototype); // true
console.log(user1.__proto__ === user2.__proto__); // true
不管你创建多少个 User 对象实例,他们的原型指向的都是同一个 User.prototype
注: _proto_ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,不建议在生产中使用该属性,我们可以使用ES5的方法 Object.getPrototypeOf 方法来获取实例对象的原型。
Object.getPrototypeOf(user) === User.prototype
当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。
到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined。
function User() {
this.name = '张三'
}
User.prototype.name = '李四';
Object.prototype.age = 20
var user = new User();
console.log(user.name); // 张三
console.log(user.age); // 20
delete user.name
console.log(user.name); // 李四
如果一个属性在原型链的上端,则对于查找时间将带来不利影响。特别的,试图获取一个不存在的属性将会遍历整个原型链。
并且,当使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问。
所以在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法, 这将会避免原型对象扩展带来的干扰。
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i);
}
}
通过 new 关键字方式调用的函数都被认为是构造函数
function User() { }
var user = new User();
// user.constructor === user.__proto__.constructor
console.log(user.constructor === User ); // true
console.log(User.prototype.constructor === User); // true
原型 constructor 属性指向构造函数,在构造函数内部,this 指向新创建的对象 Object
如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新创建的对象。
显式的 return 表达式将会影响返回结果,但总是会返回的是一个对象。
function Bar() {
return 2;
}
var bar = new Bar(); // 返回新创建的对象,而不是数字的字面量 2
console.log(bar.constructor === Bar); // true
function Foo() {
this.a = 1;
return {
b: 2
};
}
Foo.prototype.c = 3;
var foo = new Foo(); // 返回的对象 {b: 2}
console.log(foo.constructor === Foo); // false
console.log(foo.a); // undefined
console.log(foo.b); // 2
console.log(foo.c); // undefined
这里得到的 foo 是函数返回的对象,而不是通过new关键字新创建的对象。new Foo() 并不会改变返回的对象 foo 的原型, 也就是返回的对象 foo 的原型不会指向 Foo.prototype 。 因为构造函数的原型会被指向到新创建的对象,而这里的 Foo 没有把这个新创建的对象返回,而是返回了一个包含 b 属性的自定义对象。
如果 new 被遗漏了,则函数不会返回新创建的对象。
function Foo() {
this.abc = 1; // 获取设置全局参数 this===window
}
Foo(); // undefined
为了不使用 new 关键字,经常会使用工厂模式创建一个对象。
function Foo() {
var obj = {};
obj.value = 'blub';
var private = 2;
obj.setValue = function(value) {
this.value = value;
}
obj.getPrivate = function() {
return private;
}
return obj;
}
上面的方式看起来出错,并且可以使用闭包来达到封装私有变量, 但是随之而来的是一些不好的地方。
下面通过 call 实现继承,并将父级 prototype 给子 prototype
function Parent() {
this.value = 1
}
Parent.prototype.method = function() {
console.log('value: ' + this.value)
}
function Child() {
// this -> new Child()
Parent.call(this); // 调用Parent构造函数
}
var c1 = new Child();
console.log(c1.value); // 1
c1.method(); // 报错:Uncaught TypeError: c1.method is not a function
Child.prototype = Parent.prototype; //继承父方法
var c2 = new Child();
c2.method(); // 'value: 1'
但是这样继承存在一个问题,接上面代码继续
Child.prototype.fn = function() {
console.log('abc')
}
var p = new Parent();
p.fn(); // 'abc'
因为 Parent.prototype === Child.prototype,原型是同一个引用,可以直接将子类prototype 与 父类分离
for(var i in Parent.prototype) {
Child.prototype[i] = Parent.prototype[i]
}
# 设置用户名和邮箱(--local 仅在当前仓库设置,全局使用 --global,系统 --system )
git config user.name yijinc --local
git config user.email [email protected] --local
# 设置大小写敏感 (需要在每个项目中各自设置 ignorecase)
git config core.ignorecase false
# 设置别名
git config alias.history "log --color --graph --pretty=format:'%C(bold red)%h%C(reset) - %C(bold green)(%cr)%C(bold blue)<%an>%C(reset) -%C(bold yellow)%d%C(reset) %s' --abbrev-commit" --global
# 添加指定文件到暂存区
git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
git add [dir]
# 添加当前目录的所有文件到暂存区
git add .
# 移出暂存区文件,文件依然修改在工作区中
git reset [file1] [file2] ...
# 撤回到上一次提交,reset 根据下列参数恢复到不同状态
# 1 移动 HEAD 指向到上一次提交 (若指定了 --soft,则到此停止,上一次提交修改存入暂存区)
# 2 使索引看起来像 HEAD (default --mixed,上一次提交修改存入工作区)
# 3 使工作目录看起来像索引 (若指定了 --hard,上一次提交修改完全被撤回/无记录)
git reset HEAD^
# 撤回到指定提交
git reset --hard [commited-hash]
# 提交暂存区到本地仓库
git commit -m "message"
# 提交暂存区的指定文件到本地仓库
git commit -m "message" [file1] [file2] ...
# 允许创建一个没有任何改动的提交
git commit -m "message" --allow-empty
# 提交时显示所有diff信息
git commit -v
# 如果暂存区没有可提交的,则直接改写上一次的提交信息
# 如果暂存区有可提交的,提交变化,替代/覆盖 上一次提交
git commit --amend -m "message"
# (默认在当前分支)新建一个分支,并切换到该分支,可以在最后指定[检出分支]
git checkout -b [new-branch]
git checkout -b [new-branch] [from-branch]
# 切换到指定分支或提交,并切换到当前分支或提交点
git checkout [branch-name] # 等同于 git switch [branch-name]
git checkout [commit-hash]
# 检出某个 commit 的指定文件 放入暂存区
git checkout [commit-hash] [file]
# 切换到上一个分支
git checkout - # 等同于 git switch -
# 放弃/撤销 工作区(未git add)文件修改, . 表示所有文件
git checkout -- [filename]
git checkout .
# 列出所有本地分支
git branch
# 列出所有远程分支
$ git branch -r
# 列出所有本地分支和远程分支
git branch -a
# 查看本地分支关联(跟踪)的远程
git branch -vv
# (默认在当前分支)新建一个分支,停留在当前分支(不会切换过去)
# 也可在最后指定 from-branch 或 commit-hash
git branch [new-branch]
git branch [new-branch] [commit-hash]
# 与指定的远程分支建立追踪关系(
# 默认 local-branch 与 remote/branch名字一致,指定时需要注意
# local-branch 不填默认当前分支,可填当前分支或其他分支名
git branch [local-branch] [remote/branch] --track
git branch [local-branch] --set-upstream-to=[remote/branch]
# 删除本地分支
git branch -d [branch]
git branch -D [branch] # --delete --force
# 删除远程跟踪分支(本地分支/远程分支不会删除)
git branch -dr [remote/branch]
# 删除远程分支
git push origin --delete [branch]
# 获取/下载远程仓库的所有变动
git fetch [remote]
# 合并指定(本地)分支到当前分支
git merge [branch]
# 合并指定(远程)分支到当前分支,指定 origin / branch-name
git merge [remote/branch]
# 合并、忽略解决冲突过程
git merge [branch] --abort
# 拉最新指定远程分支代码,并与当前本地分支合并,(省略后面2个参数,拉取当前分支追踪的远程分支并合并)r
git pull [remote] [branch]
# 拉最新指定远程分支代码,并与当前本地分支合并
git pull [remote] [remote-branch]:[local-branch]
# 推送本地当前分支到指定远程分支仓库,(省略后面2个参数,推到当前分支追踪的远程分支)
git push [remote] [branch]
# 推送本地指定分支到远程指定分支, 以下2行等价
git push [remote] [local-branch]:[remote-branch]
git push [remote] refs/heads/[local-branch]:refs/heads/[remote-branch]
# 推送所有分支到远程仓库
git push [remote] --all
# 显示所有远程仓库
git remote -v
# 显示某个远程仓库的信息,主要远程分支与本地分支追踪情况
git remote show [remote]
# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]
# 显示某次提交的元数据和内容变化
git show [commit-hash]
# 显示某次提交发生变化的文件
$ git show [commit] --name-only
# 显示某次提交filename文件的内容
$ git show [commit]:[filename]
# 列出所有tag
git tag
# 新建一个轻量标签,一般为临时标签
git tag [tag-name]
# 新建一个tag在指定commit
git tag [tag-name] [commit]
# 新建一个附注标签,【建议】
git tag -a [tag-name] -m "annotate message"
# 查看标签信息
git show [tag-name]
# 将tag推送到远程上,push tag跟branch操作类似
# 默认情况下,git push命令并不会传送标签到远程仓库服务器上,必须显式地推送标签
git push [remote] [tag-name]:[tag-name]
# 删除本地tag
git tag -d [tag-name]
# 删除远程分支(需要仓库所有权限)
git push [remote] :[tag-name]
git push [remote] :refs/tags/[tag-name]
# 显示当前状态,当前文件的变化、当前分支、追踪分支
git status
# 列出当前的git配置
git config --list
# 编辑git配置文件
git config -e [--global]
# 设置git用户信息
git config [--global] user.name "name"
git config [--global] user.email "email address"
# 列出当前分支commit日志,翻页,按q退出
git log
# 显示过去n次提交
git log -n
# 提交记录,文件修改统计信息
git log --stat
# 搜索提交历史,根据关键词
git log -S [keyword]
# 显示指定文件的版本历史,以下2行功能一致
git log --follow [file]
git whatchanged [file]
# 显示指定文件的版本历史,带每一次详细diff
git log -p [file]
# 可以设置alias 保存一些常用复杂命令
git config --global alias.history 'log --pretty=format:"%h %ad : %s %d [%an]" --graph --date=iso'
git history
# 将修改未提交的改变暂存到 stash
git stash
# 列出存储的的 stash
git stash list
# 应用上一次存储的stash,pop会从stash list 移除
git stash ( apply | pop )
GIT 规范
为适应多个feature同时并行交错开发,做到每个 feature 独立干净、合并的代码不被意外覆盖,这里有几个良好的代码合并习惯(规范)供大家参考,
merge
操作 而非 rebase
;这样做的好处是:您的feature分支代码总是独立干净的,多人开发时,可以灵活选择哪几个 feature 上线,拥抱产品需求变化
commit message格式
type(scope): subject
用于说明git commit的类别,只允许使用下面的标识。
scope用于说明 commit 影响的范围,比如权限、订单、商品等等,视项目不同而不同。
feat(order)
subject是commit目的的简短描述,不超过50个字符。
fix(product): 修复产品无法删除 Refs #133
保持 master 分支是线上稳定版本, 该分支是受保护的
统一使用 tag 的形式,发布上线:
git tag -a v1.0.0 -m "v1.0.0:一些相关描述,解决了xxx,修复了xxx"
# push tag
git push origin v1.0.0
打 tag 形式对运维操作友好,tag 能准确的指向 commit id,回滚方便;这里我们以版本号格式标记,版本号可以同步产品的版本,也可以开发自己维护。
常见使用3个整数来记录版本号 major.minor.patch
,比如 1.2.3
打完 tag 后,将tag名 告知运维,运维做线上发布;
发布完成后,线上验证完成,运维或项目Owner/Maintainer 做合并到 master 分支操作
⚠️ :视情况 tagName 用 newBranch / commitId 代替
缓存已获取的资源能够有效的提升网站与应用的性能,Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助 HTTP 缓存,Web 站点变得更具有响应性。
缓存必须是由服务端设置响应头才会生效,一般客户端的请求头中只有设置了cache-control为:
no-store
|no-cache
|max-age=0
才会生效(也就是客户端不想走强缓存的时候生效)
我们先来看下与缓存相关的HTTP首部字段
字段名称 | 说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Pragma | HTTP/1.0遗留物,Pragma: no-cache等同于Cache-Control: no-cache |
If-Match | 比较ETag是否一致(如果匹配中) |
If-None-Match | 比较ETag是否不一致(如果未匹配中) |
If-Modified-Since | 比较Last-Modified时间是否一致 |
If-Unmodified-Since | 比较Last-Modified时间是否一致 |
字段名称 | 说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Pragma | HTTP/1.0遗留物,Pragma: no-cache等同于Cache-Control: no-cache |
Expires | 过期时间 |
Last-Modified | 资源最后一次修改时间 |
ETag | 资源的特定版本的标识符 |
Vary | 决定下一个请求头,应该用一个缓存的回复还是向源服务器请求一个新的回复 |
Pragma是一个在 HTTP/1.0 中规定的通用首部,这个首部的效果依赖于不同的实现,所以在“请求-响应”链中可能会有不同的效果。当该字段值为no-cache的时候,与 Cache-Control: no-cache 效果一致,表示禁用缓存。
Expires的值对应一个GMT(格林尼治时间)来告诉浏览器资源缓存过期时间,如果还没到该时间点则不发请求。
Expires所定义的缓存时间是相对服务器上的时间而言的,要求客户端和服务器端的时钟严格同步。客户端的时间是可以修改的,如果服务器和客户端的时间不统一,这就导致有可能出现缓存提前失效的情况,存在不稳定性。 面对这种情况,HTTP1.1引入了 Cache-Control 头来克服 Expires 头的限制。
如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age",那么 Expires 头会被忽略。
Pragma 和 Expires 现在主要用来用来向后兼容只支持 HTTP/1.0 协议的缓存服务器。
Cache-Control 是一个通用首部字段,被用于在http请求和响应中。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中
客户端可以在HTTP请求中使用的标准 Cache-Control 指令
Cache-Control 属性值 | 说明 |
---|---|
no-cache | 告知服务器不直接使用缓存,需要验证 |
no-store | 禁止缓存,资源不会保存到缓存/临时文件 |
no-transform | 表明客户端希望获取实体数据没有被转换 |
max-age=[seconds] | 表明客户端愿意接收一个带缓存时间的资源 |
max-stale[=seconds] | 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间 |
min-fresh=[seconds] | 表明客户端希望接收一个能在seconds秒内被更新过的资源 |
only-if-cached | 表明客户端只接受已缓存的响应,并且不向原服务器检查是否有更新 |
服务器可以在响应中使用的标准 Cache-Control 指令
Cache-Control 属性值 | 说明 |
---|---|
no-cache | 缓存但重新验证(要求协商缓存验证) |
no-store | 不使用任何缓存,资源不应该存储到客户端缓存/临时文件 |
no-transform | 不能对资源进行转换或转变。HTTP头/实体 不能由代理修改 |
public | 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器等)缓存,即使是通常不可缓存的内容(例如,该响应没有max-age指令或Expires消息头) |
private | 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它) |
must-revalidate | 一旦资源过期(比如已经超过max-age),必须向原服务器验证请求 |
proxy-revalidate | 与must-revalidate类似,但仅适用于共享缓存(如代理) |
max-age=[seconds] | 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒) |
s-maxage=[seconds] | 覆盖max-age,但仅限于共享缓存(如代理) |
Cache-Control 允许自由组合可选值,例如:
Cache-Control: public, max-age=3600, must-revalidate
客户端不会向服务器发送任何请求,直接从本地缓存中读取文件并返回 Status Code: 200 OK。 服务器返回 Expires
或 Cache-Control: max-age=31536000
时会生效
优先顺序 from memory cache ,from disk cache,最后是请求网络资源
当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回 Last-Modified
或 ETag
时才会进行验证。
如果缓存的响应头信息里 Cache-control 含有 must-revalidate 或 no-cache 值,也会发起缓存验证
Last-Modified 响应头可以作为一种弱校验器,其值为资源最后更改的时间。
Last-Modified 称为弱校验器 因为它不够精准:
Last-Modified: Tue Aug 20 2019 15:47:05 GMT
如果响应头里含有Last-Modified,客户端可以在后续的请求首部带上 If-Modified-Since
或者 If-Unmodified-Since
验证Last-Modified
If-Modified-Since:若客户端传递的时间值与服务器上该资源最后修改时间是一致(If-Modified-Since==Last-Modified),则说明该资源没有被修改过,直接返回304状态码,内容为空,这样就节省了传输数据量 。如果两个时间不一致,则服务器会发回该完整资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。
当前各浏览器均是使用 If-Modified-Since 请求首部来向服务器传递保存的 Last-Modified 值
If-Unmodified-Since:该值告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端
ETag可以解决上述 Last-Modified 可能存在的不准确的问题,因而称它为强校验器
ETag值为一个唯一标识符,服务器利用文件修改时间、文件大小和inode号等通过某种算法计算得出
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
如果响应头里含有ETag,客户端可以在后续的请求首部带上 If-Match
或者 If-None-Match
验证 ETag
If-None-Match:如果服务器发现 ETag 不匹配(If-None-Match != ETag),那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端。若客户端传递的 etag 跟服务器的etag一致,则直接返回304知会客户端直接使用本地缓存即可;
当前各浏览器均是使用If-Match请求首部来向服务器传递保存的 ETag 值
If-Match:告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段
需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样
ES6标准是在 2015 年 6 月正式发布的,至今已有有 2 年多了。
ES6 拥有一系列 JavaScript 新特性的标准,能让开发变得更简单。可以使用 Babel 将你写的es6转化成es5,不用担心现有环境是否支持
现在大多前端都采用es6去编写代码,或部分使用了es6的新特性,很多框架也是采用并全力推荐使用es6,如 react
1 . 解构赋值:经常可以碰到去请求后端数据多(分类)的时候,提取json,需要依次给变量赋值时
const response = {
homeData: { distance: 100, score: 1 },
guestData: { distance: 200, score: 2 }
}
const { homeData, guestData } = response;
// homeData = { distance: 100, score: 1 }
// guestData = { distance: 200, score: 2 }
模块系统也用到解构赋值
import React, { Component } from 'react'
// Component === React.Component
2 . Object.assign( ):我们需要编辑一个球员player,要求在编辑过程中可以随时取消,所以需要深拷贝这个对象,以便修改这个新对象后,且可以退回到原来的对象
const player = {
id: 1,
name: '张三',
phone: '13800000000'
};
//拷贝player至editPlayer并增加了isEdit标志
const editPlayer = Object.assign({}, player, { isEdit: true });
还有很多实用的操作对象或数组的方法,比如Array.fill()、Array.from()、Object.freeze(obj)
3 . class :类,这里可与Angular 的Component做对比,首先es6是没有private属性的,angular组件中方法在模版中使用不需要关心 this
,而在 es6 中 和一些框架如react 需要谨慎对待 this
const btn = document.getElementById('btn');
class Component {
constructor() {
this.name = "component";
}
}
class MyComponent extends Component {
constructor() {
super();
//如果我们需要单独调用 printName 方法,必须绑定this指向
this.printName = this.printName.bind(this);
}
//在类中定义方法 前面不需要加上 function 关键字
printName() {
console.log(`Hello ${this.name}`);
}
apply() {
//事件绑定 this指向为该对象(btn)
btn.onclick = this.printName
// 2 直接调用类中的方法或者将它绑定给其他事件, 使用箭头函数并将方法 触发写在函数内
// btn.onclick = (e)=> { this.printName() }
}
}
类的所有方法都定义在类的 prototype
属性上面,new MyComponent( ) 出来的的实例对象可直接调用 printName 方法,因为 this
指向实例对象;
而直接调用类中的方法或者将它绑定给其他事件,都会报 this
指向错误,所以需要绑定 this 。例子
深度优先搜索算法(Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。
这个算法可能是前端最需要了解和最简单的算法了,html文档结点本身就是一颗树(dom树),React 虚拟dom也是树结构,webpack 的文件依赖为图结构,他们内部都用到了 dfs
下面是个人整理的学习笔记
function dfs(root: TreeNode | null): number[] {
if (!root) return [];
const array = [];
// 左
array.push(...dfs(root.left))
// 中
array.push(root.val);
// 右
array.push(...dfs(root.right))
return array;
};
不同顺序的遍历只需更换push 顺序就好
// 二叉树的所有路径
function binaryTreePaths(root: TreeNode | null): number[] {
const results = [];
const dfs = (node: TreeNode, path: number[] = []) => {
if (node === null) return;
path.push(node.val);
if (node.left === null && node.right === null) { // isLeaf
results.push([...path]);
return;
}
dfs(node.left, [...path]);
dfs(node.right, [...path]);
}
dfs(root, []);
return results;
};
/**
* 例
* [leetcode 463. 岛屿的周长](https://leetcode.cn/problems/island-perimeter/)
* 岛屿的周长就是岛屿方格和非岛屿方格相邻的边的数量
* 每当在 DFS 遍历中,从一个岛屿方格走向一个非岛屿方格,就将周长加 1
* */
function islandPerimeter(grid: number[][]): number {
const rows = grid.length;
const cols = grid[0].length;
const visited = Array.from({ length: rows }, () => new Array(cols).fill(false));
const dfs = (row: number, col: number) => {
if (row < 0 || row >= rows || col < 0 || col >= cols) {
return 1; // 越界
}
if (grid[row][col] === 0) {
return 1; // 水域
}
if (visited[row][col]) {
return 0;
}
visited[row][col] = true;
return dfs(row - 1, col) // 上
+ dfs(row + 1, col) // 下
+ dfs(row, col - 1) // 左
+ dfs(row, col + 1); // 右
};
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === 1) {
return dfs(i, j);
}
}
}
return 0;
};
优化 requestUserProfile
并发请求
requestUserProfile
是个通用用户信息接口,通过传入uid,拿用户昵称import { isEqual } from 'lodash-es';
// 核心用户请求
let _requestTime = 0;
const requestProfile = (uid: string) => {
// 这个方法的实现不能修改
return Promise.resolve().then(() => {
return new Promise<void>((resolve) => {
setTimeout(() => {
// 模拟 ajax 异步,1s 返回
resolve();
}, 1000);
}).then(() => {
_requestTime++;
return {
uid,
nick: `nick-${uid}`,
age: '18',
};
});
});
};
/**
* @param uid uid
* @param max 最多并发请求数量
*/
const requestUserProfile = (uid = '1', max = 2) => {
// 这里调用requestProfile 进行优化
};
/**
* 以下为测试用例,无需修改
*/
export default async () => {
try {
const star = Date.now();
await Promise.all([
requestUserProfile('1'),
requestUserProfile('2'),
requestUserProfile('3'),
requestUserProfile('1'),
]).then((result) => {
if (Date.now() - star < 2000 || Date.now() - star >= 3000) {
throw new Error('Wrong answer');
}
if (
!isEqual(result, [
{
uid: '1',
nick: 'nick-1',
age: '18',
},
{
uid: '2',
nick: 'nick-2',
age: '18',
},
{
uid: '3',
nick: 'nick-3',
age: '18',
},
{
uid: '1',
nick: 'nick-1',
age: '18',
},
])
) {
throw new Error('Wrong answer');
}
});
return _requestTime === 3;
} catch (err) {
console.warn('测试运行失败');
console.error(err);
return false;
}
};
网上有很多 Promise 实现方式,看了都不是特别理解。
这里以一种更简单的形式一步一步去理解/实现它。这里仅涉及 Promise 构造函数和 then 方法的实现
首先构造一个最基本的 Promise 类
// version_1
class Promise {
callbacks = [];
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
}
_resolve(value) {
this.callbacks.forEach(callback => callback(value));
}
}
// test
new Promise(resolve => {
setTimeout(() => {
console.log('await 2s');
resolve('ok');
}, 2000);
}).then((res) => {
console.log('then', res);
})
.then(onFulfilled)
为何需要用一个数组存放?then 方法可以调用多次,注册的多个onFulfilled,并且这些 onFulfilled callbacks 会在异步操作完成(执行resolve)后根据添加的顺序依次执行
// then 注册多个 onFulfilled 回调
const p = new Promise(resolve => {
setTimeout(() => {
console.log('await 2s');
resolve('ok');
}, 2000);
});
p.then(res => console.log('then1', res));
p.then(res => console.log('then2', res));
p.then(res => console.log('then3', res));
上面 Promise 的实现存在一个问题:如果传入的 executor 不是一个异步函数,resolve直接同步执行,这时 callbacks 还是空数组, 导致后面 then 方法注册的 onFulfilled 回调就不会执行(resolve 比 then 注册先执行)
// 同步执行 resolve
new Promise(resolve => {
console.log('同步执行');
resolve('同步执行');
}).then(res => {
console.log('then', res);
})
我们知道 then 中的回调总是通过异步执行的,我们可以在 resolve 中加入 setTimeout,将 callbacks 的执行时机放置到JS消息队列,这样 then方法的 onFulfilled 会先完成注册,再执行消息队列的 resolve
// version_2
class Promise {
callbacks = [];
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
}
_resolve(value) {
setTimeout(() => {
this.callbacks.forEach(callback => callback(value));
})
}
}
但是这样仍然有问题,如果我们延迟给 then 注册回调,这些回调也都无法执行。因为
还是 resolve 先执行完了,之后注册的回调就无法执行了。
const p = new Promise(resolve => {
console.log('同步执行');
resolve('同步执行');
})
setTimeout(() => {
p.then(res => {
console.log('then', res); // never execute
})
});
可以看出 setTimeout 是无法保证 then 注册的 onFulfilled 正确执行的,所以这里必须加入状态机制(pending、fulfilled、rejected),且状态只能由 pending 转换为解决或拒绝。
// version_3:增加状态机制
class Promise {
callbacks = [];
status = 'pending';
value = undefined;
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
if (this.status === 'pending') {
this.callbacks.push(onFulfilled);
} else {
onFulfilled(this.value);
}
}
_resolve(value) {
this.status = 'fulfilled';
this.value = value;
this.callbacks.forEach(callback => callback(value));
}
}
当增加了状态后,setTimeout 就可以去掉了,状态机制让注册的回调总是能正确工作。
链式调用我们可能很直接想到 then 方法中返回 this,这样 Promise 实例就可以多次调用 then 方法,但因为是同一个实例,调用再多次 then 也只能返回相同的一个结果。而我们希望的链式调用应该是这样的:
new Promise(resolve => {
resolve(1)
}).then(res => res + 2) // 1 + 2 = 3
.then(res => res + 3) // 3 + 3 = 6
.then(res => console.log(res)); // expected 6
每个 then 注册的 onFulfilled 都返回不同结果,并把结果传给下一个 onFulfilled 的参数,所以 then 需要返回一个新的 Promise 实例
// version_4:then 的链式调用
class Promise {
callbacks = [];
status = 'pending';
value = undefined;
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
return new Promise(resolveNext => {
const fulfilled = (value) => {
const results = onFulfilled(value); // 执行 onFulfilled
resolveNext(results); // 再执行 resolveNext
}
if (this.status === 'pending') {
this.callbacks.push(fulfilled);
} else {
fulfilled(this.value);
}
})
}
_resolve(value) {
this.status = 'fulfilled';
this.value = value;
this.callbacks.forEach(callback => callback(value));
}
}
这样一个 Promise 就基本实现了,我们可以看到:
这里还有一种特殊的情况:
这时我们只需用 res instanceof Promise
判断处理下
// version_5:Promise 参数处理
class Promise {
callbacks = [];
status = 'pending';
value = undefined;
constructor(executor) {
executor(this._resolve.bind(this));
}
then(onFulfilled) {
return new Promise(resolveNext => {
const fulfilled = (value) => {
const results = onFulfilled(value);
if (results instanceof Promise) {
// 如果当前回调函数返回Promise对象,必须等待其状态改变后在执行下一个回调
results.then(resolveNext);
} else {
// 否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
resolveNext(results);
}
}
if (this.status === 'pending') {
this.callbacks.push(fulfilled);
} else {
fulfilled(this.value);
}
})
}
_resolve(value) {
this.status = 'fulfilled';
/**
* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
* 当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (value instanceof Promise) {
value.then(nextValue => {
this.value = nextValue;
this.callbacks.forEach(callback => callback(value));
})
} else {
this.value = value;
this.callbacks.forEach(callback => callback(value));
}
}
}
尝试实现下面函数 LazyMan
的功能
LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch
LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
<script>
标签尽可能放置在页面的底部,紧靠 body 关闭标签 </body>
的上方,保证页面在脚本 运行之前完成解析样式<script>
标签越少,页面的加载速度就越快,响应也更加迅速。(单个文件文件也不宜过大,可利用浏览器并行下载能力, 切割成多个文件)<script>
元素;用 XHR 对象下载JavaScript代码,并注入到页面中/***
* 推荐的非阻塞模式(用XHR下载JavaScript代码虽然不立即执行,可能存在跨域)
*/
function loadScript(url, callback) {
var script = document.createElement ("script");
script.type = "text/javascript";
if (script.readyState) { // IE
script.onreadystatechange = function () {
if (script.readyState === "loaded" || script.readyState === "complete") {
script.onreadystatechange = null;
callback();
}
};
} else { // Others
script.onload = function () {
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
innerHTML
和 createElement / appendChild
性能比较:在老的浏览器 innerHTML 要快得多,在新的浏览器差距不大,甚至 createElement 表现更好优化循环工作量的第一步是减少对象成员和数组项查找的次数
倒序循环是编程语言中常用的性能优化方法
forEach 比传统循环(for、while、do while)要慢,每个数组项要关联额外的函数 调用是造成速度慢的原因
尽量减少循环中每次迭代的运算量,并减少循环迭代次数
除非你要迭代遍历一个属性未知的对象,否则不要使用 for-in 循环
switch 表达式总是比 if-else 更快,但只有当条件体数量很大时才明显更快。两者间的主要性能区别在于:当条件体增加时,if-else 性能负担增加的程度比 switch 更多
将最常见的(频率高的)条件体放在 if-else 首位(更快作出选择)
将 if-else 组织成一系列嵌套的 if-else 替换 else if 再判断,可以减少条件判断。
当条件体的数目为大量离散值时,使用查表法(对象/数组/Map 直接选择)
任何可以用递归实现的算法都可以用迭代实现。使用优化的循环替代长时间运行的递归函数可以提高性能, 因为运行一个循环比反复调用一个函数的开销要低。
减少工作量就是最好的性能优化技术。代码所做的事情越少,它的运行速度就越快
使用 memoization 技术,通过缓存先前计算结果为后续计算所重复使用,节省计算时间。
算法本身复杂度优化
字符串合并:大多数情况下 String.prototype.concat 比简单的 + 和 += 慢
JavaScript 和UI界面在同一个主线程内运行,同一时刻只能其中一个可以运行。这意味着当 JavaScript 代码正在运行时,用户界面不能响应,反之亦然。
JavaScript 运行时间不应该超过 100 毫秒。过长的运行时间导致 UI 更新出现可察觉的延迟,从而对整体用户体验产生负面影响。这时可用定时器让出时间片,分解长运行脚本成为较短的片断
/**
* 用定时器让出时间片, 每25ms后再将任务继续加入执行队列
* 并且做了限时运行代码,在50ms内直接继续下一次的执行,(执行超过50ms后再让出时间片)
* @param {array} items 需要处理的数组对象
* @param {function} process 处理函数
* @param {fucntion} callback 完成回调函数
*/
function timedProcessArray(items, process, callback) {
var todo = items.concat(); //create a clone of the original
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);
}
通过使用Web Workers,Worker 可以在独立于主线程(通常是UI线程)的后台线程中,执行费时的处理任务。主线程不会因此被阻塞/放慢。
数据格式 越轻量级越好,最好是 JSON 和字符分隔的自定义格式。xml、json、html、custom各数据格式下载和解析的性能测试
贪心算法
团队问题
技术选型
也许我们都有发现这些问题,然后都妥协了
有时候想想,其实也没想的那么糟
使用 2 个空格缩进。eslint: indent
使用分号。eslint: semi
始终使用大括号包裹代码块。eslint: curly nonblock-statement-body-position
空格风格。eslint: space-before-blocks keyword-spacing space-in-parens array-bracket-spacing object-curly-spacing space-infix-ops key-spacing
对于逗号分隔的多行结构,始终加上最后一个逗号。eslint: comma-dangle
...
对于js/es ,现在基本都是通过 ESlint 做代码风格统一。
我们采用国内使用比较多的 eslint-config-alloy 配置规范,它适用于 React / Vue / Typescript 项目,样式规则交给 Prettier 管理
⚠️ 需在 IDE 中安装 ESLint 扩展才会有提示
文档类型统一使用html5的doctype:
<!DOCTYPE html>
使用 UTF-8 字符编码
<meta charset="UTF-8">
页面需要指定 title 标签,有且仅有 1 个
<title>标题 - 子分类 - 大分类</title>
关键字、描述
<meta name="keywords" content="Keywords为产品名、专题名、专题相关名词,之间逗号隔开" />
<meta name="description" content="不超过150个字符,描述内容要和页面内容相关" />
页面提供给移动设备使用时,需要设置 viewport。
设置 viewport-fit 设置为“cover”来兼容 iPhone X 的刘海屏,了解更多 。
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
引入 CSS 和 JavaScript 时无需指定 type。
根据 HTML5 规范,引入 CSS 和 JavaScript 时通常不需要指明 type,因为 text/css 和 text/javascript 分别是他们的默认值。
在 head 标签内引入 CSS,在 body 结束标签前引入 JS。
在 <body></body>
中指定外部样式表和嵌入式样式块可能会导致页面的重排和重绘,对页面的渲染造成影响。因此,一般情况下,CSS 应在 <head></head>
标签里引入,了解更多。
在 HTTP2(Chrome 浏览器 69 版本之后,Firefox 和 Edge)中可以在 body 中使用 link 标签引入样式文件,但不推荐在 body 中使用
<style>
标签的内联样式。<link rel="stylesheet">
将会阻止后续内容的渲染,而不是整个页面。
除了基础库等必须要在 DOM 加载之前运行的 JavaScript 脚本,其他都在靠近 body
结束标签前引入,以防止出现页面渲染的阻塞,了解更多。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="//g.alicdn.com/lib/style/index-min.css" />
<style>
.mod-example {
padding-left: 15px;
}
</style>
</head>
<body>
...
<script src="path/to/vender.js"></script>
<script src="path/to/my/script.js"></script>
</body>
</html>
标签必须合法且闭合、嵌套正确,标签名小写
不要省略自闭合标签结尾处的斜线,且斜线前需留有一个空格。
虽然 HTML5 规范 中指出结尾的斜线是可选的,但保留它们可以明确表达该标签已闭合的语义,更易于维护和理解。
给 <img>
标签加上 alt 属性。
给 <a>
标签加上 title 属性。
属性值使用双引号,不要使用单引号。
根据以上规约,建议的 HTML 脚手架模板如下:
移动端模版:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>标题 - 子分类 - 大分类</title>
<meta name="keywords" content="Keywords为产品名、专题名、专题相关名词,之间逗号隔开" />
<meta name="description" content="不超过150个字符,描述内容要和页面内容相关" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<!-- 为了防止页面数字被识别为电话号码,可根据实际需要添加: -->
<meta name="format-detection" content="telephone=no">
<!-- 让添加到主屏幕的网页再次打开时全屏展示,可添加: -->
<meta content="yes" name="mobile-web-app-capable">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta name="robots" content="all">
<meta name="author" content="公司-部门或产品" />
<meta name="Copyright" content="公司" />
<link rel="shortcut icon" href="favicon.ico">
</head>
</html>
PC端模版:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>标题 - 子分类 - 大分类</title>
<meta name="keywords" content="Keywords为产品名、专题名、专题相关名词,之间逗号隔开" />
<meta name="description" content="不超过150个字符,描述内容要和页面内容相关" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="robots" content="all">
<meta name="author" content="公司-部门或产品" />
<meta name="Copyright" content="公司" />
<link rel="shortcut icon" href="favicon.ico">
</head>
</html>
{
之间保留一个空格:
和属性值之间保留一个空格>
、+
、~
、||
等组合器前后各保留一个空格,
之后保留一个空格HTTP 有以下安全性问题:
对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。
优点:运算速度快
缺点:密钥无法安全地传输给浏览器
非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。
它需要两个密钥,一个是公开密钥,另一个是私有密钥;公钥可以公开,可任意向外发布;私钥不可以公开,必须由用户自行严格秘密保管;用公钥加密的内容必须用私钥才能解开,同样,用私钥加密的内容(数字签名)只有公钥能解开。
优点:可以安全地将公钥传输给 浏览器 通信发送方;
缺点:运算速度慢。
非对称加密的一对公钥私钥,可以保证单个方向传输的安全性,那用两对公钥私钥,是否就能保证双向传输都安全了?
的确可以!但是由于非对称加密算法非常耗时,HTTPS的加密没使用这种方案。
HTTPS 采用混合的加密机制:
非对称加密仅保证了将公钥传送给通信方,这个公钥是通过明文传输的,若这个公钥被中间人劫持,那他也能用该公钥解密服务器传来的信息。
中间人劫持服务器公钥A后,保存下来,把数据包中的公钥A替换成自己伪造的公钥B(它当然也拥有公钥B对应的私钥B’);浏览器生成一个用于对称加密的密钥X,用公钥B(浏览器无法得知公钥被替换了)加密后传给服务器;中间人劫持后用私钥B’解密得到密钥X,再用公钥A加密后传给服务器。。。
出现这种问题的根本原因是浏览器无法确认收到的公钥是不是网站自己的
数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构,它的作用就是证明公钥是可信的
网站在使用HTTPS前,服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
数字签名的制作过程:
浏览器验证过程:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.