GithubHelp home page GithubHelp logo

yijinc / yijinc.github.io Goto Github PK

View Code? Open in Web Editor NEW
6.0 3.0 0.0 61.7 MB

我的博客笔记📒

Home Page: https://yijinc.github.io/blog

JavaScript 53.38% TypeScript 29.56% CSS 1.52% SCSS 15.53%
front-end-development nothing

yijinc.github.io's Introduction

Hi, there 👋

yijinc Blue7Yijinc yijinc @yijinc yijinc

  • 👨🏻‍💻 I'm a full-stack developer
  • 💓 Currently developing with java + React + Typescript
  • 🛠️ Coding since 2015

Languages and Tools

html5 css3 sass bootstrap javascript express nodejs react vue angular reactnative mysql csharp figma

yijinc.github.io's People

Contributors

yijinc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

yijinc.github.io's Issues

react-native-webview 通信

react-native 通过 WebView 组件可以非常简单的实现通信,这里通过从RN中分离出来的react-native-webview 为例,介绍 WebView 的通信机制。

react-native-webview 与 web 通信,提供以下3个方法

  • React Native -> Web: injectedJavaScript 属性
  • React Native -> Web: injectJavaScript 方法(this.refs.webviewr.injectJavaScript(...))
  • Web -> React Native: 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。

其原理步骤是:

  • web 调用 invokeClientMethod方法,将回调 callback 放入 APP对象的属性上:window.APP['__GLOBAL_FUNC_INDEX__0'] = callback
  • 然后web再真正发起 postMessage(JSON.string)到RN
  • RN 的 onMessage 接收到 event(JSON.parse)
  • RN 处理完后,通过 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

解析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
  }
}

排序算法

排序算法是《数据结构与算法》中最基本的算法

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

十大经典排序算法有:

HTTP之跨域

HTTP之跨域

[TOC]

跨源资源共享 CORS

跨源资源共享(Cross-Origin Resource Sharing)是一种基于 HTTP 头的机制。出于安全性,浏览器限制脚本内发起的跨域请求, 例如,XMLHttpRequestFetch API 遵循 同源策略(Same-origin policy)。CORS 机制允许服务器声明哪些源站通过浏览器有权限访问哪些资源。

简单请求

简单请求(不会触发 CORS 预检请求)需满足所有下述条件:

  • Request-Method 只能是 GET | HEAD | POST
  • Request-Headers 允许人为设置的字段只能包含 Accept、Accept-Language、Content-Language、Content-Type
  • Content-Type 只能是 text/plain | multipart/form-data | application/x-www-form-urlencoded
  • XMLHttpRequest 对象没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问
  • 请求中没有使用 ReadableStream 对象

简单请求涉及的请求头有 Origin、Access-Control-Allow-Origin、Access-Control-Expose-Headers、 Access-Control-Allow-Credentials等;

预检请求(preflight request)

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是拿不到这些头字段的)

附带身份凭证的请求(withCredentials)

一般情况,对于跨源 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

跨域常见解决方案

1、服务端直接配置 Access-Control-Allow-Origin

基于上述CORS机制,服务端配置成允许跨源请求就行

如果服务器未使用 *****,而是指定了一个域,那么为了向客户端表明服务器的返回会根据Origin请求头而有所不同,必须在Vary响应头中包含Origin

Access-Control-Allow-Origin: https://www.frontend.com
Vary: Origin

2、JSONP跨域

浏览器仅会限制脚本内发起的跨域请求,而 script、img 标签没有跨域限制。所以可以通过script 标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据放到callback函数中,返回给浏览器,浏览器的callback解析执行,从而前端拿到callback函数返回的数据。

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
    function handleCallback(data) {
	    console.log('get data', data);
	}
    script.src = 'https://api.other.com/xx?callback=handleCallback';
    document.head.appendChild(script);
</script>

3、nginx 服务器代理

其实对于任一服务器都可以做代理转发处理,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; # 或前端服务地址
        }
}

4、构建工具代理(开发环境)

在日常开发中一般会直接使用 构建工具的代理(node server代理)配置,比如 webpack

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        pathRewrite: { '^/api': '' },
      },
    },
  },
};

5、系统host添加域名

我们可以在系统的host文件 增加 ip = host 映射,本地访问域名host时,会去访问真是ip地址,比如 127.0.0.1,一般在调试需要SSO登录的应用的时候经常会用这种方式。

hosts文件位置在
Windows:C:\Windows\System32\drivers\etc\hosts
Mac:/etc/hosts

6、window.postMessage + iframe

window.postMessage 方法可以通过第二个参数 targetOrigin 安全地实现跨源通信。

示例:2个跨源页面的通信

aaa.com/a.html 需要跨域请求数据的页面

<iframe
  src="http://bbb.com/b.html"
  id="iframe"
></iframe>
<script>
	const targetOrigin = 'http://bbb.com';
	function receiveMessage(event) {
		// 我们始终使用 origin 和 source 属性验证发件人的身份
	    if (event.origin !== targetOrigin) return;
		console.log(event.data);
	}
	window.addEventListener("message", receiveMessage, false);
	window.postMessage('fetchUserInfo', targetOrigin);
</script>

bbb.com/b.html 是同源的页面

<script>
	const targetOrigin = 'http://aaa.com';
	function receiveMessage(event) {
	    if (event.origin !== targetOrigin) return;
		if (event.data === 'fetchUserInfo') {
			const user = { /** mock data */ };
			window.postMessage(JSON.stringify(user), targetOrigin);	
		}
	}
	window.addEventListener("message", receiveMessage, false);
</script>

以上是最常见的几种跨域解决方案,还有一些不太常用的方法 比如

  • document.domain + Iframe(只能用于二级域名相同的情况下)
  • window.location.hash + Iframe
  • window.name+ Iframe
  • Websocket

前端需要掌握的Linux命令

认识 BASH 这个shell

# 指令   选项       参数(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'

  • 路径与指令搜寻顺序:

    • 1 以相对/绝对路径执行指令;
    • 2 由 alias 找到该指令来执行;
    • 3 由 bash 内建的 (builtin) 指令来执行;
    • 4 透过 $PATH 这个变量的顺序搜寻到的第一个指令来执行。
  • login shell 会读取两个配置文件:

    • 1 /etc/profile(系统设定),
    • 2 ~/.bash_profile 或 ~/.bash_login 或 ~/.profile(个人设定,其中一个)
      然后会通过这2个文件脚本载入其他文件配置。source (或小数点) 将配置文件的内容读进来目前的 shell 环境中(更改配置文件后不需要注销立即生效)
  • 数据流重导向:

    • 1 标准输入 (stdin):代码为0,使用<或<< ;
    • 2 标准输出 (stdout):代码为 1 ,使用 > 或 >> ;
    • 3 标准错误输出(stderr):代码为 2 ,使用 2> 或 2>> ;
      双向重导向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 的数据。常用管线处理命令 grepcutsortwcuniqsplitxargs

Linux 文件权限属性

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 个表示文件类型

    • d (directory) 表示目录
    • - 表示文件
    • l (link file) 表示连接文件
    • b (block) 表示区块设备文件(可供储存的接口设备)
    • c (character) 示为字符设备文件(串行端口的接口设备)例如键盘、鼠标
  • 接下来的字符中,以3个为一组(共三组),且均为 rwx 的三个参数的组合。其中,r 代表可读(read)、w 代表可写(write)、x 代表可执行(execute)。 要注意的是,这三个权限的位置不会改变,如果没有权限,就会用 - 代替占位。

    • 第一组 rwx 为文件拥有者的权限
    • 第二组 r-x 为加入此群组的账号的权限
    • 第三组 r-x 为其他人的权限

Linux 是个多人多任务的系统,Linux 一般将文件可存取的身份分为三个类别,分别是 owner/group/others,且三种身份各有 read/write/execute 等权限

文件所有者(Owner)

当创建一个用户的时候,Linux 会为该用户创建一个主目录,路径为 /home/[username],我们可以使用 cd ~,快捷进入当前用户 home 目录。如果你想放一个私密文件,就可以放在自己的主目录里,然后设置只能自己查看。

群组(Group)

每个用户都有一个用户组,方便多人操作的时候,为一群人分配权限。当创建用户的时候,会自动创建一个与它同名的用户组。

如果一个用户同时属于多个组,用户需要在用户组之间切换,才能具有其他用户组的权限。

其他人(Others)

既不是 Owner 又不属于 Group,就是其他人。

超级用户(Root)

Root 用户是万能的天神,该用户可以访问所有文件

chgrp:改变文件所属群组

chgrp(change group) 群组名需要存在 /etc/group

# -R:递归更改文件属组
chgrp [-R] 群组名 文件或目录

chown:改变文件拥有者

chown(change owner) 用户账号名需要存在 /etc/passwd

# -R:递归更改文件属组
chown [-R] 账号名称 文件或目录
chown [-R] 账号名称:组名 文件或目录

chmod:改变文件的权限

Linux 文件的基本权限就有九个,分别是 owner/group/others 三种身份各有自己的 read/write/execute 权限,我们也可以用数字表示权限,数字与字母的对应关系为:

  • r : 4
  • w : 2
  • x : 1

每组 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

Linux 常用命令

1. ssh 远程连接

ssh [options] [-p PORT] [username@]hostname
#
ssh -p 3000 [email protected]

2. pwd 显示当前目录

[root@my-azure]$ pwd
/home/root/

3. cd 切换工作目录

cd /home/workspace  # 进入/home/workspace
cd ~  # 进入home目录
cd -  # 回到上次所在目录,一般来回切换

4. mkdir 创建目录

mkdir folder-name # 创建目录
mkdir -p folder1/folder2/folder3  # 递归创建目录

5. touch 创建文件

touch new-file # 创建文件

6. echo 打印输出

echo "hello world"
# 将打印内容通过 > 输出到 a.txt 文件,追加使用 >>
echo "some content" > a.txt

7. cat 查阅一个文件的内容

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 监听文件修改实时显示

8. cp 复制文件或目录

cp source_file_name target_file_name

cp -r app /home/www/app # -r 复制目录

9. mv 移动并重命名

mv workspace/project/index.html /home/www/app # 移动
mv index.html home.html # 更改文件名

10. rm 删除一个文件或者目录​

rm package.lock
mv -rf dist # 直接删除整个目录

11. tar 文件的压缩打包

下面是常用的压缩命令,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

12. which 查看指令对应的文件位置

which node # /root/.nvm/versions/node/v14.17.6/bin/node

# 如果需要查询系统文件(/bin/sbin、/usr/share/man)可以使用 whereis
whereis nginx

JavaScript的深拷贝浅拷贝,深拷贝/比较 与 immutable 数据差异

JavaScript深拷贝浅拷贝

JavaScript 数据类型分为两种:

  • 基础类型:像Number、String、Boolean等
  • 引用类型:像Object、Array、Function等

浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。Array.prototype.slice/concatObject.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的数据转换等

JSON.parse的问题

发现在typescript写的页面中, 这种js注入. JSON.stringify(data)做成的json字符串在onMessage里面,调用JSON.parse()会报错.JSON字符串根本不能转换成功.

vps 搭建 Shadowsocks

登录 vps

ssh root@vps_ip -p ssh_port

安装 shadowsocks

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 拥塞控制算法(加速优化)

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已启动。

参考:

数组去重

example

  • 输入:[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

动态规划(Dynamic programming,DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于普通解法。

使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性。动态规划只解决每个子问题一次,并把子问题结果保存起来,以减少重复计算,再利用子问题结果依次计算出需要解决的子问题,直到得出原问题的解。

常用解题思路:

  • 1、理解题意,找规律,定义 dp[i]
  • 2、找出 dp[i]与前面计算得出的 dp[i-1]、dp[i-2]...的关系,转化成(条件)方程

练习题

/**
* [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);
};

深入理解 JavaScript prototype

var str = 'Hello world!';    // var str = new String('Hello world!')
str.substr(2, 4);
str.indexOf('world');

我们经常可以用到的字符串函数 substrreplaceindexOf等,是因为 String 对象上的 prototype 预先定义了这些方法。strString 的一个实例。

JavaScript标准库中常用的内置对象上 prototype 的方法还有:
Array​.prototype​.pushArray​.prototype​.push
Date​.prototype​.get​DateDate​.prototype​.get​Year
Function​.prototype​.toStringFunction​.prototype​.call
...

我们使用构造函数创建一个对象:

function User() {
}
var user = new User();
user.name = '张三';
console.log(user.name) // 张三

在这个例子中,User 是一个构造函数,我们使用 new 创建了一个实例对象 user

prototype


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() {} 

_proto_


所有 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]
}

git常用命令

git config

# 设置用户名和邮箱(--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

# 添加指定文件到暂存区
git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
git add [dir]

# 添加当前目录的所有文件到暂存区
git add .

git reset

# 移出暂存区文件,文件依然修改在工作区中
git reset [file1] [file2] ...

# 撤回到上一次提交,reset 根据下列参数恢复到不同状态
# 1 移动 HEAD 指向到上一次提交 (若指定了 --soft,则到此停止,上一次提交修改存入暂存区)
# 2 使索引看起来像 HEAD (default --mixed,上一次提交修改存入工作区)
# 3 使工作目录看起来像索引 (若指定了 --hard,上一次提交修改完全被撤回/无记录)
git reset HEAD^ 

# 撤回到指定提交
git reset --hard [commited-hash]  

git commit

# 提交暂存区到本地仓库
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

# (默认在当前分支)新建一个分支,并切换到该分支,可以在最后指定[检出分支]
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

# 列出所有远程分支 
$ 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

# 获取/下载远程仓库的所有变动
git fetch [remote]

git merge

# 合并指定(本地)分支到当前分支
git merge [branch]
# 合并指定(远程)分支到当前分支,指定 origin / branch-name
git merge [remote/branch]

# 合并、忽略解决冲突过程
git merge [branch] --abort

git pull

# 拉最新指定远程分支代码,并与当前本地分支合并,(省略后面2个参数,拉取当前分支追踪的远程分支并合并)r
git pull [remote] [branch]

# 拉最新指定远程分支代码,并与当前本地分支合并
git pull [remote] [remote-branch]:[local-branch]

git push

# 推送本地当前分支到指定远程分支仓库,(省略后面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

# 显示所有远程仓库
git remote -v

# 显示某个远程仓库的信息,主要远程分支与本地分支追踪情况
git remote show [remote]

# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]

git show

# 显示某次提交的元数据和内容变化
git show [commit-hash]

# 显示某次提交发生变化的文件
$ git show [commit] --name-only

# 显示某次提交filename文件的内容
$ git show [commit]:[filename]

git tag

# 列出所有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 status

git config

# 列出当前的git配置
git config --list

# 编辑git配置文件
git config -e [--global]

# 设置git用户信息
git config [--global] user.name "name"
git config [--global] user.email "email address"

git log

# 列出当前分支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

git stash

# 将修改未提交的改变暂存到 stash
git stash

# 列出存储的的 stash
git stash list 

# 应用上一次存储的stash,pop会从stash list 移除
git stash ( apply | pop )

GIT规范

GIT 规范

合并规范

为适应多个feature同时并行交错开发,做到每个 feature 独立干净、合并的代码不被意外覆盖,这里有几个良好的代码合并习惯(规范)供大家参考,

  • 为了让代码变化更好追踪,总是使用 merge 操作 而非 rebase
  • 总是在自己的 开发(feature)分支 修改代码,无论开发阶段还是解决bug阶段;
  • 每当需要部署时,请切换到 部署分支,比如 test,merge 您的 开发(feature)分支 代码到 test;请不要将您的环境变量配置一起合并过去;
  • 您不需要将 部署分支 同步(merge)到您的 开发(feature)分支,它会污染您的代码。

这样做的好处是:您的feature分支代码总是独立干净的,多人开发时,可以灵活选择哪几个 feature 上线,拥抱产品需求变化

提交规范

commit message格式

type(scope): subject

type(必须) :

用于说明git commit的类别,只允许使用下面的标识。

  • feat:新功能(feature)。
  • fix:修复bug,可以是QA发现的BUG,也可以是研发自己发现的BUG。
  • chore:构建过程或辅助工具的维护。
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)。
  • docs:文档(documentation)。
  • style:格式(不影响代码运行的变动)。
  • perf:优化相关,比如提升性能、体验。
  • test:增加测试。
  • revert:回滚到上一个版本。
  • ci:持续集成相关。

scope(可选)

scope用于说明 commit 影响的范围,比如权限、订单、商品等等,视项目不同而不同。

feat(order)

subject(必须)

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

  • major 主版本号:大改版,不兼容老版本,major+1
  • minor 次版本号:普通迭代,不影响之前版本功能,minor+1
  • patch 补丁版本号:小修改,bug修复,patch+1

打完 tag 后,将tag名 告知运维,运维做线上发布;
发布完成后,线上验证完成,运维或项目Owner/Maintainer 做合并到 master 分支操作

⚠️:视情况 tagName 用 newBranch / commitId 代替

HTTP之缓存

缓存已获取的资源能够有效的提升网站与应用的性能,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

Pragma是一个在 HTTP/1.0 中规定的通用首部,这个首部的效果依赖于不同的实现,所以在“请求-响应”链中可能会有不同的效果。当该字段值为no-cache的时候,与 Cache-Control: no-cache 效果一致,表示禁用缓存。

Expires

Expires的值对应一个GMT(格林尼治时间)来告诉浏览器资源缓存过期时间,如果还没到该时间点则不发请求。

Expires所定义的缓存时间是相对服务器上的时间而言的,要求客户端和服务器端的时钟严格同步。客户端的时间是可以修改的,如果服务器和客户端的时间不统一,这就导致有可能出现缓存提前失效的情况,存在不稳定性。 面对这种情况,HTTP1.1引入了 Cache-Control 头来克服 Expires 头的限制。

如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age",那么 Expires 头会被忽略。


Pragma 和 Expires 现在主要用来用来向后兼容只支持 HTTP/1.0 协议的缓存服务器。

Cache-Control

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。 服务器返回 ExpiresCache-Control: max-age=31536000 时会生效

优先顺序 from memory cachefrom disk cache,最后是请求网络资源

缓存验证(协商缓存)

当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回 Last-ModifiedETag 时才会进行验证。
如果缓存的响应头信息里 Cache-control 含有 must-revalidateno-cache 值,也会发起缓存验证

1. Last-Modified

Last-Modified 响应头可以作为一种弱校验器,其值为资源最后更改的时间。
Last-Modified 称为弱校验器 因为它不够精准:

  • 只能精确到一秒,在1秒内资源改变,因为时间比较相同,而不会去更新最新的资源
  • 一个资源被修改了,但其实际内容根本没发生改变,会因为 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) 状态码给客户端

2. ETag

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却不一样

es6Share

ES6

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

深度优先搜索 Depth First Search

深度优先搜索算法(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;
};

Promise.all 调用接口并发请求控制

目标

优化 requestUserProfile 并发请求

要求

  • requestUserProfile 是个通用用户信息接口,通过传入uid,拿用户昵称
  • 在一个支付宝群聊里有10多个用户,点击群聊信息,展示各个人的昵称
    • 10个并发请求,会阻塞接口
    • 10个依次请求,耗时久,显示昵称太慢
  • 需要优化请求,在并发和耗时之间掌握一个平衡
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 实现方式,看了都不是特别理解。
这里以一种更简单的形式一步一步去理解/实现它。这里仅涉及 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);
})
  1. Promise 构造函数会立即执行用户传入的函数 executor,并且把 _resolve 方法作为 executor 的参数,传给用户处理
  2. 调用 then 方法(同步),将 onFulfilled 放入callbacks队列,其实也就是注册回调函数,类似于观察者模式。
  3. executor 模拟了异步,这里是过2s后执行 resolve,对应触发 _resolve 内的 callbacks

.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));

异步执行处理 setTimeout vs status

上面 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 就可以去掉了,状态机制让注册的回调总是能正确工作。

  • 当 resolve 同步执行时,立即执行 resolve,将 status 设置为 fulfilled ,并把 value 的值存起来, 在此之后调用 then 添加的新回调,都会立即执行
  • 当 resolve 异步执行时,pending 状态执行 then 会添加回调函数, 等到 resolve 执行时,回调函数会全部被执行。

then的链式调用

链式调用我们可能很直接想到 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 就基本实现了,我们可以看到:

  • then 方法中,创建并返回了新的 Promise 实例,这是串行 Promise 的基础
  • 我们把 then 方法传入的 形参 onFulfilled 以及创建新 Promise 实例时传入的 resolveNext 合成一个 新函数 fulfilled,这是衔接当前 Promise 和后邻 Promise 的关键所在

处理返回 Promise 类型的回调

这里还有一种特殊的情况:

  • resolve 方法传入的参数为一个 Promise 对象时
  • onFulfilled 方法返回一个 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

参考答案from Daily-Interview-Question

高性能JavaScript笔记

JavaScript加载

  • 将所有 <script> 标签尽可能放置在页面的底部,紧靠 body 关闭标签 </body> 的上方,保证页面在脚本 运行之前完成解析样式
  • 将脚本成组打包 / 压缩。页面的 <script> 标签越少,页面的加载速度就越快,响应也更加迅速。(单个文件文件也不宜过大,可利用浏览器并行下载能力, 切割成多个文件)
  • 非阻塞方式下载 JavaScript:动态创建 <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);
}

数据访问

  • 局部变量比域外变量快,因为它位于作用域链的第一个对象中。变量在作用域链中的位置越深,访问所需的时间就越长。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。
  • 一个对象的属性或方法在原形链中的位置越深,访问它的速度就越慢,查找属性需要往原型链上遍历搜索,所以尽量使用直接量(简单类型)

DOM 编程

  • 最小化 DOM 访问,修改 DOM 元素会造成重绘和重新排版
  • 在ECMAScript处理数据,最后一次性调用dom操作
  • innerHTMLcreateElement / appendChild 性能比较:在老的浏览器 innerHTML 要快得多,在新的浏览器差距不大,甚至 createElement 表现更好
  • 遍历数组比遍历集合(HTML Collection 类数组)快,集合的 length 属性缓存到一个变量中,遍历集合时可先转化成数组副本
  • 遍历 children 比 childNodes 更快
  • 使用速度更快的 API,诸如 querySelectorAll()和 firstElementChild
    • offsetTop, offsetLeft, offsetWidth, offsetHeight
    • scrollTop, scrollLeft, scrollWidth, scrollHeight
    • clientTop, clientLeft, clientWidth, clientHeight
    • getComputedStyle() (currentStyle in IE)(在 IE 中此函数称为 currentStyle)
    获取布局信息这些属性和方法,需返回最新的数据,浏览器不得不刷新 渲染队列并重排版。所以 不要在布局信息改变时查询它
  • 页面中存在大量元素绑定了事件,使用事件托管

算法和流程控制

  • 优化循环工作量的第一步是减少对象成员和数组项查找的次数

  • 倒序循环是编程语言中常用的性能优化方法

  • 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);
}

编程实践

  • eval,Function构造器,setTimeout 和 setInterval 允许在程序中运行包含代码的字符串,执行过程中会发生二次评估(二次评估是一项昂贵的操作),且可能是不安全的。所以,避免使用 eval 和 Function 构造器避免二次评估;给 setTimeout() 和 setInterval() 传递函数参数而不是字符串参数
  • 创建对象或数组使用直接量最快,占用较少空间
  • 不要做不必要的工作,不要重复做已经完成的工作
  • 延迟加载、条件预加载
  • 使用速度快的部分
  • 尽量使用原生方法

构建与部署

  • 预处理JavaScript文件:
  • 合并 JavaScript 文件,减少 HTTP 请求的数量
  • JavaScript Minification(紧凑): 剔除 js 文件中一切运行无关的内容,包括注释和不必要的空格
  • JavaScript Compression(压缩):携带 Accept-Encoding 的 HTTP 头(gzip 编码)
  • Caching JavaScript Files:使用 http缓存,通过向文件名附加时间戳或版本号解决缓存问题;使用 HTML 5 离线应用程序缓存
  • 使用内容传递网络(CDN)提供 JavaScript 文件,CDN 不仅可以提高性能,它还可以为你管理压缩和缓 存

在开发中发现的问题

团队问题

  • 每个人代码风格不一样,有的人的代码灵活简洁,有的人规整冗长
  • 直接在IDE中禁用jslint (tslint),按自己风格来
  • 没有团队 leader ,没有项目架构设计和讨论,缺乏沟通,各写各的
  • 每一次的开发,就只是完成任务,不明白产品要做的是什么,完全没有预想过这个项目最终是怎么样的,我们只是在实现UI图和需求中的文案,和基本功能
  • bug多,不好维护,不好( 害怕 )接手他人写过代码
  • 后面增改需求很尴尬,怎么当初会这样写?真想把之前的都重新改了
  • 基础薄弱,直接会用框架,快速入门,然而对框架外的东西,比如需要自定义指令或组件,无从下手,或者最后硬憋出来一个糟糕的实现,(这是前端简单、要求低的原因吗)
  • 有时候产品的设计确实有些坑,它们好像总想创新,强加一些交互在不适用于的场景上,还得有所改变(原理上能实现的一般没有拒绝,然后发现坑好多)

技术选型

  • 公司一直用AngularJs,直到出了angular4 后便一直采用angular4了。在选择框架上没有讨论,哪个框架比较(长期)适合目前产品的开发,react、vue、angular? 我觉得应该不是angular,不过它用起来确实很不错,很强大。公司都是一些小项目,用react或vue也许会更简洁快速,这里可能是个人倾向
  • 在需要有seo,在服务器渲染时候,没有考虑 node?

也许我们都有发现这些问题,然后都妥协了
有时候想想,其实也没想的那么糟

前端代码规范

JavaScript

对于js/es ,现在基本都是通过 ESlint 做代码风格统一。
我们采用国内使用比较多的 eslint-config-alloy 配置规范,它适用于 React / Vue / Typescript 项目,样式规则交给 Prettier 管理

⚠️ 需在 IDE 中安装 ESLint 扩展才会有提示

HTML

DOCTYPE 设置

文档类型统一使用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/csstext/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>

CSS

示例代码标注图

  • 所有声明都应该以分号结尾
  • 使用 2 个空格缩进,不要使用 4 个空格或 tab 缩进
  • 使用一个空格:
    • 选择器和 { 之间保留一个空格
    • : 和属性值之间保留一个空格
    • >+~|| 等组合器前后各保留一个空格
    • 在使用 , 分隔的属性值中,, 之后保留一个空格
    • 注释内容和注释符之间留有一个空格
  • 使用多个选择器时,每个选择器应该单独成行
  • 尽量不要使用 !important 重写样式
  • 长度值为 0 时,省略掉长度单位
  • 嵌套选择器的深度不要超过 3 层,否则可能带来一些副作用

参考

HTTPS加密原理 🔐

为什么需要加密?

HTTP 有以下安全性问题:

  • 使用明文进行通信,内容可能会被窃听;
  • 不验证通信方的身份,通信方的身份有可能遭遇伪装;
  • 无法证明报文的完整性,报文有可能遭篡改。

对称加密

image.png

对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。

优点:运算速度快
缺点:密钥无法安全地传输给浏览器

非对称加密

image.png

非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。

它需要两个密钥,一个是公开密钥,另一个是私有密钥;公钥可以公开,可任意向外发布;私钥不可以公开,必须由用户自行严格秘密保管;用公钥加密的内容必须用私钥才能解开,同样,用私钥加密的内容(数字签名)只有公钥能解开。

优点:可以安全地将公钥传输给 浏览器 通信发送方;
缺点:运算速度慢。

HTTPS 采用的加密方式

非对称加密的一对公钥私钥,可以保证单个方向传输的安全性,那用两对公钥私钥,是否就能保证双向传输都安全了?
的确可以!但是由于非对称加密算法非常耗时,HTTPS的加密没使用这种方案。

HTTPS 采用混合的加密机制:

  • 使用非对称密钥加密,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性;
  • 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率。(下图中的 Session Key 就是 Secret Key)

image.png

认证

非对称加密仅保证了将公钥传送给通信方,这个公钥是通过明文传输的,若这个公钥被中间人劫持,那他也能用该公钥解密服务器传来的信息。

中间人劫持服务器公钥A后,保存下来,把数据包中的公钥A替换成自己伪造的公钥B(它当然也拥有公钥B对应的私钥B’);浏览器生成一个用于对称加密的密钥X,用公钥B(浏览器无法得知公钥被替换了)加密后传给服务器;中间人劫持后用私钥B’解密得到密钥X,再用公钥A加密后传给服务器。。。

出现这种问题的根本原因是浏览器无法确认收到的公钥是不是网站自己的

数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构,它的作用就是证明公钥是可信的

网站在使用HTTPS前,服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。

进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。

image.png

数字签名的制作过程:

  • CA机构拥有非对称加密的私钥和公钥
  • CA机构对证书明文数据(Certificate Data)进行 Hash
  • 对hash后的值用私钥加密,得到数字签名(Signature)
  • 明文(Certificate Data)和数字签名(Signature)共同组成了数字证书,就可以颁发给网站了

浏览器验证过程:

  • 拿到数字签名的数据,得到明文(Certificate Data)和数字签名(Signature)
  • 用CA机构的公钥对 数字签名(Signature)解密,得到 X
  • 用证书里指定的hash算法对明文(Certificate Data)进行 Hash 得到 Y
  • 如果 X == Y,说明明文(Certificate Data)和数字签名(Signature)没有被篡改,表明证书可信任;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.