Git Product home page Git Product logo

fe-notes's People

Contributors

zh-rocco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

bbkmaisi tq-allen

fe-notes's Issues

【Nginx】HTTPS 配置

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

# 运行用户
user nginx;

# 启动进程,通常设置成和cpu的数量相等
worker_processes auto;

# 全局错误日志
error_log /var/log/nginx/error.log;

# PID文件,记录当前启动的nginx的进程ID
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

# 工作模式及连接数上限
events {
    # 单个后台worker process进程的最大并发链接数
    worker_connections 1024;
}

http {
    # 设定日志
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;

    # 连接超时时间
    keepalive_timeout   65;
    tcp_nodelay         on;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    # gzip压缩开关
    gzip  on;
    # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
    gzip_min_length  1k;
    # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用CPU时间
    # gzip_comp_level 大于2时效果并不是很明显,所以可以将值设置为1或者2
    gzip_comp_level  2;
    # 进行压缩的文件类型,只需要为 ttf、otf 和 svg 字体启用 gzip,对其他字体格式进行 gzip 压缩时效果不明显
    gzip_types  text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
    # 是否在http header中添加Vary: Accept-Encoding,建议开启
    gzip_vary  on;
    # 禁用IE 6 gzip
    gzip_disable  "MSIE [1-6]\.";

    # 设定实际的服务器列表
    upstream blog {
        server 127.0.0.1:3000;
    }

    server {
        listen       80;
        server_name  singple.com www.singple.com;
        return 301   https://singple.com$request_uri;
    }

    server {
        listen       443;
        server_name  www.singple.com;
        return 301   https://singple.com$request_uri;
    }

    server {
        listen 443 ssl http2 default_server;
        server_name  singple.com;

        ssl                        on;
        ssl_certificate            ssl/singple.crt;
        ssl_certificate_key        ssl/singple.key;
        ssl_session_timeout        5m;
        ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers  on;

        location / {
            proxy_pass   http://blog;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }

    server {
        listen       80 default;
        return       501;
    }

# Settings for a TLS enabled server.
#
#    server {
#        listen       443 ssl http2 default_server;
#        listen       [::]:443 ssl http2 default_server;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers HIGH:!aNULL:!MD5;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        location / {
#        }
#
#        error_page 404 /404.html;
#            location = /40x.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#            location = /50x.html {
#        }
#    }

}

【JS】前端跨页面通信

localStorage

具体方案

// A.html
localStorage.setItem('message', 'hello');

// B.html
window.onstorage = e => {
  // e.key, e.oldValue, e.newValue
};

tips

  1. 触发写入操作的页面下的 storage listener 不会被触发
  2. storage 事件只有在发生改变的时候才会触发,即重复设置相同值不会触发 listener
  3. safari 隐身模式下无法设置 localStorage 值

优劣

API 简单直观,兼容性好,除了跨域场景下需要配合其他方案,无其他缺点

BroadcastChannel

具体方案

localStorage 方案基本一致,需要额外初始化

// A.html
const channel = new BroadcastChannel('tabs');
channel.onmessage = e => {
  // e.data
};

// B.html
const channel = new BroadcastChannel('tabs');
channel.postMessage('hello');

优劣

localStorage 方案没特别区别,都是同域、API 简单,BroadcastChannel 方案兼容性差些(chrome > 58),但比 localStorage 方案生命周期短(不会持久化),相对干净些。

参考

【MySQL】使用 Navicate 建库建表

安装 Navicate

  1. 官网下载
  2. 安装。

开启 MySQL 服务

  1. 以管理员权限启动 CMD 或 PowerShell,键入 net start mysql

Navicate 连接数据库

  1. 打开 Navicate,点击左上角的 连接,打开 新建连接 弹窗,输入连接名(MySQL)、密码,点击 确定
  2. 界面左侧出现刚才添加的 连接:MySQL,鼠标右键点击 MySQL,然后选择 打开连接,MySQL 变成绿色表示连接成功。

新建 blog 数据库

  1. 鼠标右键点击 MySQL,打开 新建数据库 弹窗,输入数据库名(blog_db)、字符集(utf8)、排序规则(默认即可,空),点击 确定
  2. 鼠标右键点击刚才新建的数据库:blog_db,然后选择 打开数据库,blog_db 变成绿色表示数据库连接成功。

新建 article 数据表

  1. 鼠标右键点击 blog_db 下的 ,然后选择 新建表,界面中部打开建表区。
  2. 添加第一条记录,名:id、类型:int、长度:255、不是 null:选中、键:点击设为主键,底部默认,勾选自动递增、无符号。
  3. 点击 添加字段,添加第二条记录,名:title、类型:varchar、长度:255、不是 null:选中。
  4. 点击 添加字段,添加第三条记录,名:author、类型:varchar、长度:255、不是 null:选中。
  5. 点击 添加字段,添加第四条记录,名:content、类型:varchar、长度:255、不是 null:选中。
  6. 点击 保存,打开 输入表名 弹窗,输入 article,点击 确定

【JS】Code Snippets

Object

访问成员属性

function getDescendantProp(obj, desc) {
  var arr = desc.split('.');
  while (arr.length) {
    obj = obj[arr.shift()];
  }
  return obj;
}

var obj = { a: { b: { c: 0 } } };
var propPath = getPropPath(); // returns e.g. "a.b.c"
var result = getDescendantProp(obj, propPath);

参考:

设置成员属性

function setDescendantProp(obj, desc, value) {
  var arr = desc.split('.');
  while (arr.length > 1) {
    obj = obj[arr.shift()];
  }
  return (obj[arr[0]] = value);
}

var obj = { a: { b: { c: 0 } } };
var propPath = getPropPath(); // returns e.g. "a.b.c"
var result = setDescendantProp(obj, propPath, 1); // test.a.b.c will now be 1

Promise / await

await 异常处理

function isPromise(obj) {
  return obj instanceof Object && obj.then instanceof Function;
}

// converting promise or function to flat array response with [error, result]
function awaitWrapper(fn) {
  if (fn instanceof Function && !isPromise(fn)) {
    throw new Error('Argument must be a function or Promise');
  }

  const successFn = (value) => [null, value];
  const errorFn = (error) => [error];

  return isPromise(fn) ? fn.then(successFn, errorFn) : funcWrapper(fn);
}

function funcWrapper(fn) {
  try {
    return [null, fn()];
  } catch (error) {
    return [error];
  }
}

参考:

图片操作

/**
 * 根绝 File 实例获取图片的像素尺寸
 *
 * @export
 * @param {File} file
 * @returns {Promise} resolve({ width: 100, height: 100 }) / reject()
 */
export function getPixelSize(file) {
  return new Promise(function(resolve, reject) {
    const _URL = window.URL || window.webkitURL;
    const img = new Image();
    img.onload = function() {
      resolve({
        width: img.width,
        height: img.height,
      });
    };
    img.onerror = function() {
      reject();
    };
    img.src = _URL.createObjectURL(file);
  });
}

数字格式化

/**
 * 格式化文件大小
 *
 * @export
 * @param {Number} fileSize 文件大小
 * @param {Number} [idx=0] 起始索引
 * @returns {String} 500KB
 */
function formatFileSize(fileSize, idx = 0) {
  const units = ['B', 'KB', 'MB', 'GB'];
  if (fileSize < 1024 || idx === units.length - 1) {
    return fileSize.toFixed(0) + units[idx];
  }
  return formatFileSize(fileSize / 1024, ++idx);
}

【JS】函数式编程

特点

  • 声明式(Declarative)
  • 纯函数(Pure Function)
  • 数据不可变性(Immutability)

纯函数

  • 函数的执行过程完全由输入参数决定,不受除参数之外的任何数据的影响
  • 函数不会修改任何外部状态,比如修改全局变量或传入的参数对象,即无副作用

副作用:(包含但不限于)

  • 改变全局变量的值
  • 改变输入参数引用的对象
  • 读取用户输入,比如调用了 alert 或者 confirm 函数
  • 抛出一个异常
  • 网络输入/输出操作,比如 Ajax
  • 操作 DOM

高阶函数

定义:

高阶函数至少满足以下一个条件:

  • 接受一个或多个函数作为参数
  • 返回一个函数

高阶函数实现 AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些 跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。

在 JavaScript 中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中;AOP 是 JavaScript 语言中一种非常特别和巧妙的装饰者模式实现。

Function.prototype.before = function(beforeFn) {
  var __self = this; // 保存原函数的引用
  return function() {
    // 返回包含了原函数和新函数的"代理"函数
    beforeFn.apply(this, arguments); // 执行新函数,修正 this
    return __self.apply(this, arguments); // 执行原函数
  };
};

Function.prototype.after = function(afterFn) {
  var __self = this;
  return function() {
    var ret = __self.apply(this, arguments);
    afterFn.apply(this, arguments);
    return ret;
  };
};

var func = function() {
  console.log(2);
};

func = func
  .before(function() {
    console.log(1);
  })
  .after(function() {
    console.log(3);
  });

func();
// 1
// 2
// 3

函数组合

未完待续...

资料

【HTTP】HTML 防缓存

HTML

添加 meta 标签(仅在 HTML4 下生效

<!-- index.html -->
<head>
  <meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
  <meta http-equiv="pragma" content="no-cache">
  <meta http-equiv="expires" content="0">
</head>

Nginx

添加配置

location / {
    index index.html;
    add_header Cache-Control "no-cache, no-store, must-revalidate"; # HTTP/1.1
    add_header Pragma "no-cache"; # HTTP/1.0
    expires 0; # Proxies
}

参考

【Script Tag】async / defer attributes

API Explain
async 在页面继续解析时执行脚本
defer 在页面完成解析时执行脚本

注意:

  • asyncdefer 只对外部脚本有效
  • 存在 asyncdefer 时,<script> 的下载不阻塞 HTML 解析
  • 如果同时存在 asyncdefer,应用 async 的执行规则

图解:

async & defer

参考

【JS】You may not know

连续赋值

(function() {
  var a = (b = 5);
})();
console.log(b); // 5
console.log(a); // ERROR

因为赋值是从右向左结合:var a = b = c = d = 5; 等价于 var a = (b = (c = (d = 5)));,其中只有 a 被声明了,b, c 和 d 都是自动解析为全局变量。

函数作用域内出现和该函数名同名的变量

内部变量会被屏蔽掉

var b = 10;
console.log(b); // 10
(function b() {
  b = 20;
  console.log(b); // f b() { ... }
})();
console.log(b); // 10

对比:

var b = 10;
console.log(b); // 10
(function a() {
  b = 20;
  console.log(b); // 20
})();
console.log(b); // 20

深入理解原型

Object.prototype.a = 10;
console.log(a); // 10

解释:

  1. JS 引擎查找有无声明 a 变量,未找到
  2. JS 引擎去 window 上查找有无 a 属性,未找到
  3. JS 引擎继续去 window 的隐式原型上查找有无 a 属性( window 隐式原型的终点是 Object.prototype ),找到

【Nginx】常用配置(路由转发、路径重写、HTTPS等)

Nginx 基础配置说明

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

# 运行用户
user nginx;

# 启动进程,通常设置成和cpu的数量相等
worker_processes auto;

# 全局错误日志
error_log /var/log/nginx/error.log;

# PID文件,记录当前启动的nginx的进程ID
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

# 工作模式及连接数上限
events {
    # 单个后台worker process进程的最大并发链接数
    worker_connections 1024;
}

http {
    # 设定日志
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;

    # 连接超时时间
    keepalive_timeout   65;
    tcp_nodelay         on;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    # gzip压缩开关
    gzip  on;
    # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
    gzip_min_length  1k;
    # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用CPU时间
    # gzip_comp_level 大于2时效果并不是很明显,所以可以将值设置为1或者2
    gzip_comp_level  2;
    # 进行压缩的文件类型,只需要为 ttf、otf 和 svg 字体启用 gzip,对其他字体格式进行 gzip 压缩时效果不明显
    gzip_types  text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
    # 是否在http header中添加Vary: Accept-Encoding,建议开启
    gzip_vary  on;
    # 禁用IE 6 gzip
    gzip_disable  "MSIE [1-6]\.";
}

路由转发(proxy_pass

    server {
        listen       5080;
        server_name  localhost;
        client_max_body_size    600m;
        location /api {
            proxy_pass  http://xxx.xxx.xxx.xxx:3000/api; # remote api server
            proxy_set_header  X-Real-IP $remote_addr;
            proxy_set_header  Host $host;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

路径重写(rewrite

    server {
        listen       5080;
        server_name  localhost;
        client_max_body_size    600m;
        location /api {
            rewrite  ^/api/(.*)$ /$1 break;
            proxy_pass  http://xxx.xxx.xxx.xxx:3000;
            proxy_set_header  X-Real-IP $remote_addr;
            proxy_set_header  Host $host;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

静态资源服务(alias

    server {
        listen       5080;
        server_name  localhost;
        client_max_body_size    600m;
        location /static/ {
            alias  /path/to/local/direction/;
            autoindex  on;
        }
    }

注意:

alias 后面跟的目录最后一定要加 / 符号,如 /Users/me/code/static/

开启 HTTPS

# 设定实际的服务器列表
upstream blog {
    server 127.0.0.1:3000;
}

server {
    listen       80;
    server_name  singple.com www.singple.com;
    return 301   https://singple.com$request_uri;
}

server {
    listen       443;
    server_name  www.singple.com;
    return 301   https://singple.com$request_uri;
}

server {
    listen 443 ssl http2 default_server;
    server_name  singple.com;

    ssl                        on;
    ssl_certificate            ssl/singple.crt;
    ssl_certificate_key        ssl/singple.key;
    ssl_session_timeout        5m;
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers  on;

    location / {
        proxy_pass   http://blog;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

server {
    listen       80 default;
    return       501;
}

【Vue】provide / inject(依赖注入)

说明

可以把依赖注入看作一部分“大范围有效的 prop”,并且:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里

注意

  • provideinject 绑定并不是可响应的
  • provideinject 主要为高阶插件/组件库提供用例,并不推荐直接用于应用程序代码中

使用

// Ancestor.vue

<script>
export default {
  name: 'Ancestor',

  provide() {
    return {
      ancestor: this,
      someFuncOfAncestor: this.someFuncOfAncestor
    }
  },

  methods: {
    someFuncOfAncestor() {
      // ...
    }
  }
}
</script>
// Descendant.vue

<script>
export default {
  name: 'Descendant',

  inject: ['ancestor', 'someFuncOfAncestor'],

  created() {
    console.log(this.ancestor) // => Ancestor 组件实例
  }
}
</script>

参考

【性能优化】常见性能优化方法

性能优化

**:性能优化先优化性能瓶颈(短板),具体问题具体分析。

性能优化的最关键点是首屏。

用户交互时也会出现性能瓶颈:大量 dom 更新(react 性能优势在这,vdom 可以减少不必要的 dom 更新),频繁的页面的重排,动画(动画写的不好性能会很差,比如频繁的页面的重排)。

常见的优化方法

客户端优化

  1. 加载优化(网络方面)

    • 减少 HTTP 请求
      • 合并 CSS、JS
      • 使用雪碧图
      • 小图转 base64,可以受益于 gzip
    • 减少资源体积
      • 压缩 CSS、JS
      • 压缩图片
  2. 渲染和 DOM 操作方面

    • 在网页初步加载时,获取到 HTML 文件之后,最初的工作是构建 DOM 和构建 CSSOM 两个树,之后将他们合并形成渲染树,最后对其进行打印
    • 优化网页渲染
      • CSS 放在头部
        • 保证解析 DOM 的同时解析 CSS
        • CSS(外链或内联)会阻塞整个 DOM 渲染,但是不会阻塞 DOM 解析
      • JS 放在 body 结束标签前或者异步
        • JS(外链或内联)会阻塞后续的 DOM 解析,DOM 渲染也会被阻塞
    • 优化 DOM 操作
      • 避免在 document 上直接进行频繁的 DOM 操作(减少回流和重绘)
      • 使用 className 代替大量的内联样式修改(减少回流和重绘)
      • 对于复杂的 UI 元素,设置 position 为 absolute 或 fixed(减少回流和重绘)
      • 避免强制性同步布局(减少回流和重绘)
        • 比如给某个元素添加了一个类,然后再读取布局信息,这个时候为了获得真实的布局信息,浏览器需要强制性对页面进行布局
      • 批量操作 DOM(减少回流和重绘)
      • 尽量使用 CSS 动画
      • 尽量减少 CSS 表达式(expression)
      • 使用 requestAnimationFrame 代替 setInterval 操作
      • 使用事件代理
    • 操作细节注意
      • 避免图片或者 iframe 使用空 src
        • 避免使用空的 src 属性可以缩减浏览器首屏渲染的时间,因为浏览器在渲染过程中会把 src 属性中的空内容进行加载,直至加载失败,影响 DOMContentLoaded 与 Loaded 事件之间的资源准备过程,拉长了首屏渲染所用的时间;但空的 href 属性对首屏渲染的影响比较小
      • 在 CSS 属性为 0 时,去掉单位
      • 禁止图像缩放
      • 正确的 CSS 前缀,autoprefix
      • 移除空的 CSS 规则
      • 对于可继承的属性,尽量使用继承
      • 缩短 CSS 选择器,BEM
    • 移动端优化
      • 长列表滚动优化,-webkit-overflow-scrolling: touch,视口之外 visibility: hidden
      • 函数防抖和函数节流
      • 使用 touchstart、touchend 代替 click,或者 fastclick
      • 开启 GPU 渲染加速,transform3d、transformZ、will-change
  3. 数据方面

    • 图片加载处理
      • 图片预加载
      • 图片懒加载
      • 首屏加载时显示进度条
    • 异步请求的优化
      • 使用正常的 json 数据格式进行交互
      • 缓存常用的不变数据
      • 数据埋点和统计
    • 组件懒加载、预加载
    • 合理控制 cookie 的大小(每次请求都会包含 cookie)

服务端优化

  1. 服务器开启 gzip

  2. 缓存

    • DNS 缓存,将资源分配到恰当数量的主机名;以减少 DNS 查询
    • 避免重定向,减少多余的中间访问
    • CDN 加速
    • HTTP 缓存:设定缓存时间
      • cache-control
        • public:响应被缓存,并且在多用户间共享
        • private:默认值,响应只能够作为私有的缓存(e.g., 在一个浏览器中),不能再用户间共享
        • no-cache:响应不会被缓存,而是实时向服务器端请求资源
        • max-age:数值,单位是秒,从请求时间开始到过期时间之间的秒数。基于请求时间
      • date:生成消息的具体时间和日期
      • expires:指定了在浏览器上缓冲存储的页距过期还有多少时间,等同 Cache-control 中的 max-age 的效果,如果同时存在,则被 Cache-Control 的 max-age 覆盖。若把其值设置为 0,则表示页面立即过期。并且若此属性在页面当中被设置了多次,则取其最小值
      • last-modified / if-modified-since:本地文件在服务器上的最后一次修改时间。缓存过期时把浏览器端缓存页面的最后修改时间发送到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行对比,如果时间一致,那么返回 304,客户端就直接使用本地缓存文件
      • etag / if-none-match
        • (EntityTags)是 URL 的 tag,用来标示 URL 对象是否改变,一般为资源实体的哈希值。和 Last-Modified 类似,如果服务器验证资源的 ETag 没有改变(该资源没有更新),将返回一个 304 状态告诉客户端使用本地缓存文件。Etag 的优先级高于 Last-Modified,Etag 主要为了解决 Last-Modified 无法解决的一些问题
        • 文件也许会周期性的更改,但是他的内容并不改变,不希望客户端重新加载
        • If-Modified-Since 能检查到的粒度是 s 级
        • 某些服务器不能精确的得到文件的最后修改时间
  3. 负载均衡

参考

【MySQL】MySQL 5.7 安装教程(Windows 平台)

安装 MySQL

  1. 选择免安装版 mysql,官网下载
  2. 将下载的 zip 包解压至 D:\Program Files\ 目录。
  3. 将 mysql 的 bin 目录(D:\Program Files\mysql-5.7.20-winx64\bin)添加至系统环境变量。
  1. 新建 mysql.ini,并编辑内容为:

    [mysqld]
    basedir=D:\Program Files\mysql-5.7.20-winx64\
    datadir=D:\Program Files\mysql-5.7.20-winx64\data\
    port=3306
    skip-grant-tables
    
    # basedir 表示 mysql 安装路径
    # datadir 表示 mysql 数据文件存储路径
    # port 表示 mysql 端口
    # skip-grant-tables 表示忽略密码
    
  2. 以管理员权限启动 CMD 或 PowerShell,并将路径切换至 MySQL 的 bin 目录(D:\Program Files\mysql-5.7.20-winx64\bin),然后输入 mysqld -install

  3. 命令行输入 mysqld --initialize 自动生成带随机密码的 root 用户。

  4. 命令行输入 net start mysql 启动 mysql 服务。

  5. 修改 root 用户的随机密码:

    • 打开 mysql 安装目录下的 data 文件夹(D:\Program Files\mysql-5.7.20-winx64\data);
    • 找到 *.err 格式的文件,打开,搜索 root@localhost:,复制后面的密码;
    • 命令行输入 mysql -u root -p,键入密码,以 root 身份进入 mysql 管理界面;
    • 进入 mysql 管理界面后(出现 mysql> 标识)键入 SET PASSWORD FOR "root"@"localhost" = PASSWORD("123456");(注意不要漏掉最后的 ;);
    • 命令行输入 flush privileges; 刷新权限;
    • 命令行输入 clear 清屏,然后输入 quit 退出 mysql 管理界面;
  6. 修改 mysql.ini 文件删除最后一句 skip-grant-tables

  7. 命令行输入 net stop mysql 停止 mysql 服务;

  8. 命令行输入 net start mysql 重启 mysql 服务;

参考

【Webpack】

webpack.config.js Hot Reload

package.json

{
  "scripts": {
    "dev": "nodemon --watch webpack.config.js --exec webpack-dev-server --hot"
  }
}

参考:

Proxy Hot Reload

思路

  1. fs.watch(): 监听 file
  2. require.resolve('filePath'): 获取 module 的 key
  3. delete require.cache['cacheKey']: 删除 cache

webpack-proxy.config.js

module.exports = {
  target: 'http://localhost:3000/',
};

webpack.config.js

const fs = require('fs');
const proxyConfig = require('./webpack-proxy.config');

let proxyOptions = {
  context: '/api',
  target: proxyConfig.target,
  pathRewrite: proxyConfig.pathRewrite,
  changeOrigin: true,
};

fs.watch('./proxy-config.js', () => {
  delete require.cache[require.resolve('./webpack-proxy.config')];
  try {
    const newProxyConfig = require('./webpack-proxy.config');
    if (proxyOptions.target !== newProxyConfig.target) {
      console.log('Proxy target changed:', newProxyConfig.target);
      proxyOptions = {
        context: '/api',
        target: newProxyConfig.target,
        pathRewrite: newProxyConfig.pathRewrite,
        changeOrigin: true,
      };
    }
  } catch (e) {
    // eslint-disable-line
  }
});

module.exports = {
  // ...
  devServer: {
    proxy: [
      function proxy() {
        return proxyOptions;
      },
    ],
  }
}

参考:

在同一个页面中加载多个 webpack 实例

方式一

webpack.config.js

module.exports = {
  output: {
    jsonpFunction: 'webpackJsonp' + Date.now(),
  },
};

方式二

安装依赖

npm i modify-chunk-id-webpack-plugin -D

webpack.config.js

const ModifyChunkIdPlugin = require('modify-chunk-id-webpack-plugin');

module.exports = {
  plugins: [
    // ...
    new ModifyChunkIdPlugin({ random: process.env.NODE_ENV === 'development' }),
  ],
};

参考:

CSS 打包顺序

注意 CSS 的引用顺序, 一定要将外部依赖的 CSS 库先于项目 CSS 引用

以 Vue 为例

main.js

import Vue from 'vue';
// 第三方 CSS 库
import 'path/to/thirdparty.css';

import App from './App';

App.vue

<style lang="less">
  // 项目CSS
  @import 'path/to/index.less';

  .ellipsis {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
</style>

模块上下文 require.context

3 个参数:要搜索的文件夹目录,是否还应该搜索它的子目录,以及一个匹配文件的正则表达式

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

示例:

require.context('./test', false, /\.test\.js$/);
//(创建了)一个包含了 test 文件夹(不包含子目录)下面的、所有文件名以 `.test.js` 结尾的、能被 require 请求到的文件的上下文。

加载所有的图片

const imageContext = require.context('@/assets/kittens/', false, /\.jpg$/);

const img = imageContext('./name.jpg');

提取公共依赖

webpack 配置:

module.exports = {
  // ...
  externals: {
    vue: 'Vue',
    'element-ui': 'ELEMENT',
  },
};

HTML 单独引入:

<head>
  <script src="path/to/vue.js"></script>
  <script src="path/to/element-ui.js"></script>
</head>

模块分包 /* webpackChunkName: name */

export default [
  {
    path: '/',
    component: () => import(/* webpackChunkName: "index" */ '@/views/Index.vue'),
    children: [
      {
        path: 'page-a',
        component: () => import(/* webpackChunkName: "page" */ '@/views/PageA.vue'),
      },
      {
        path: 'page-b',
        component: () => import(/* webpackChunkName: "page" */ '@/views/PageB.vue'),
      },
    ],
  },
];

打包后输出 indexpage 两个 chunk

lodash 按需加载

1. 安装依赖

yarn add babel-plugin-lodash -D

2. 配置 babel

babel.config.js

module.exports = {
  plugins: ['lodash'],
};

3. 使用

Transforms

import _ from 'lodash';
import { add } from 'lodash/fp';

const addOne = add(1);
_.map([1, 2, 3], addOne);

roughly to

import _add from 'lodash/fp/add';
import _map from 'lodash/map';

const addOne = _add(1);
_map([1, 2, 3], addOne);

参考: babel-plugin-lodash

使用 optional chaining 语法( obj?.prop

1. 安装依赖

yarn add @babel/plugin-proposal-optional-chaining -D

2. 配置 babel

babel.config.js

module.exports = {
  plugins: ["@babel/plugin-proposal-optional-chaining"]
};

3. 使用

Transforms

a?.b

roughly to

a == null ? undefined : a.b

参考: Optional Chaining for JavaScript

图解 webpack

webpack 编译流程

webpack 编译流程

webpack 进阶

【JS】作用域链 & 闭包

相关文章

  • 执行上下文(#51

作用域链

作用域链是一个 对象列表(list of objects),用以检索上下文代码中出现的 标识符(identifiers)

标示符[Identifiers] 可以理解为变量名称、函数声明和普通参数。

var x = 10;

function foo() {
  var y = 20;
  console.log(x + y);
}

foo(); // 30

函数 foo 如何访问到变量 x (函数能访问一个更高一层上下文的变量对象)?

这种机制是通过函数内部的 [[scope]] 属性来实现的。[[scope]] 是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

  • [[scope]] 是函数的一个属性,而不是一个上下文环境。
  • [[scope]] 在函数创建时被存储(静态作用域),直至函数销毁。即:函数可以永不调用,但 [[scope]] 属性已经写入,并存储在函数对象中。

通过构造函数创建的函数的 [[scope]]

通过函构造函数创建的函数的 [[scope]] 属性总是唯一的全局对象。考虑到这一点,如通过这种函数创建除全局之外的最上层的上下文闭包是不可能的。

var x = 10;

function foo() {
  var y = 20;

  function barFD() {
    // 函数声明
    console.log(x);
    console.log(y);
  }

  var barFE = function() {
    // 函数表达式
    console.log(x);
    console.log(y);
  };

  var barFn = Function('console.log(x); console.log(y);');

  barFD(); // 10, 20
  barFE(); // 10, 20
  barFn(); // 10, "y" is not defined
}

foo();

代码执行时对作用域链的影响

在 ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是 with 声明和 catch 语句。它们添加到作用域链的 最前端,对象须在这些声明中出现的标识符中查找。

with

var foo = { x: 10, y: 20 };

with (foo) {
  console.log(x); // 10
  console.log(y); // 20
}
var x = 10;
var y = 10;

with ({ x: 20 }) {
  var x = 30;
  var y = 30;

  console.log(x); // 30
  console.log(y); // 30
}

console.log(x); // 10
console.log(y); // 30
  1. 变量声明赋值:x = 10, y = 10;
  2. 对象 { x: 20 } 添加到作用域的最前端;
  3. with 内部,遇到了 var 声明,当然什么也没创建,因为在进入上下文时,所有变量已被解析添加;
  4. 在第二步中,仅修改变量 x,实际上对象中的 x 现在被解析,并添加到作用域链的最前端,x 为 20,变为 30;
  5. 同样也有变量对象 y 的修改,被解析后其值也相应的由 10 变为 30;
  6. 此外,在 with 声明完成后,它的特定对象从作用域链中移除(已改变的变量 x 30 也从那个对象中移除),即作用域链的结构恢复到 with 得到加强以前的状态。
  7. 在最后两个 console 中,当前变量对象的 x 保持同一,y 的值现在等于 30,在 with 声明运行中已发生改变。

catch

同样,catch 语句的异常参数变得可以访问,它创建了只有一个属性的新对象:异常参数名

try {
  // ...
} catch (ex) {
  console.log(ex);
}

作用域链修改为:

var catchObject = {
  ex: <exception object>
};

Scope = catchObject + AO|VO + [[Scope]]

catch 语句完成运行之后,作用域链恢复到以前的状态

闭包

ECMAScript 中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

闭包的作用

1. 私有化变量

2. 延续局部变量的寿命

var report = function(src) {
  var img = new Image();
  img.src = src;
};
report('http://xxx.com/getUserInfo');

因为一些低版本浏览器的实现存在 bug,在这些浏览器下使用 report 函数进行数据上报会丢失 30% 左右的数据,也就是说,report 函数并不是每一次 都成功发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。

把 img 变量用闭包封闭起来,便能解决请求丢失的问题:

var report = (function() {
  var imgs = [];
  return function(src) {
    var img = new Image();
    imgs.push(img);
    img.src = src;
  };
})();

闭包与内存管理

  • 对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
  • 而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁。

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript 的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象是使用 C++ 以 COM 对象的方式实现的,而 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。

如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null 即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。

参考

【JavaScript 设计模式与开发实践】面向对象的 JavaScript

多态

含义: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。

在电影的拍摄现场,当导演喊出 "action" 时,主角开始背台词,照明师负责打灯 光,后面的群众演员假装中枪倒地,道具师往镜头里撒上雪花。在得到同一个消息时,每个对象都知道自己应该做什么。如果不利用对象的多态性,而是用面向过程的方式来编写这一段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人的面前,确认它们的职业分工(类型),然后告诉他们要做什么。如果映射到程序中,那么程序中将充斥着条件分支语句。

将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。

对象的多态性提 示我们,"做什么" 和 "怎么去做" 是可以分开的。

封装

目的: 隐藏信息;具体表现为隐藏数据,隐藏实现细节、设计细节以及隐藏对象的类型等。

封装数据

在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 privatepublicprotected 等关键字来提供不同的访问权限。
但 JavaScript 并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 privatepublic 这两种封装性。

var myObject = (function() {
  var __name = "sven"; // 私有(private)变量
  return {
    getName: function() {
      return __name;
    }
  };
})();
console.log(myObject.getName()); // 输出: sven
console.log(myObject.__name); // 输出: undefined

封装实现

从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。

继承

JavaScript 基于原型实现继承。

原型模式

原型模式不单是一种设计模式,也被称为一种编程泛型。

从设计模式的角度讲,原型模式是用于创建对象的一种模式,如果我们想要创建一个对象,一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

原型模式的实现关键,是语言本身是否提供了 clone 方法。ECMAScript 5 提供了 Object.create 方法,可以用来克隆对象。

Object.create polyfill:

Object.create =
  Object.create ||
  function(obj) {
    var F = function() {};
    F.prototype = obj;
    return new F();
  };

克隆是创建对象的手段

原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。

JavaScript 就是使用原型模式来搭建整个面向对象系统的。在 JavaScript 语言中不存在类的概念,对象也并非从类中创建出来的,所有的 JavaScript 对象都是从某个对象上克隆而来的。

原型编程范型的一些规则

ObjectAnimal 的原型,而 AnimalDog 的原型,它们之间形成了一条原型链。这个原型链是很有用处的,当我们尝试调用 Dog 对象的某个方法时,而它本身却没有 这个方法,那么 Dog 对象会把这个请求委托给它的原型 Animal 对象,如果 Animal 对象也没有这个属性,那么请求会顺着原型链继续被委托给 Animal 对象的原型 Object 对象,这样一来便能得到继承的效果,看起来就像 AnimalDog 的 "父类",ObjectAnimal 的 "父类"。

基于原型链的委托机制就是原型继承的本质。

规则:

  • 所有的数据都是对象。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型。
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

JavaScript 中的原型继承

所有的数据都是对象

按照 JavaScript 设计者的本意,除了 undefined 之外,一切都应是对象。为了实现这一目标,numberbooleanstring 这几种基本类型数据也可以通过 "包装类" 的方式变成对象类型数据来处理。

JavaScript 中的根对象是 Object.prototype 对象。Object.prototype 对象是一个空的对象。我们在 JavaScript 遇到的每个对象,实际上都是从 Object.prototype 对象克隆而来的,Object.prototype 对象就是它们的原型。

【JS】Generator 生成器

理解

朋友聚会去饭馆,点好菜可以有两种吃法,第一种,等菜上完大家动筷子,第二种,上一道吃一道。如果按第一种吃法,吃饭的程序要等待做饭的程序(饭馆)全部执行完;按第二种吃法,吃饭的程序和做饭的程序就完成了解耦,做饭的做他的,快或者慢,只要上,吃饭的就吃,不依赖一个最终状态。这两种方法不改变菜的品种质量。但流程效益却不同,如果上一道吃一道,吃完的盘子就可以清掉,如果按第一种吃法,上到中间发现桌子不够大,盘子要叠罗汉。而且第一种吃法,喜欢吃某个菜的朋友可以先吃到,不必死等流口水。如果点三道菜,两种吃法或许没太大差异。但是你想像一下,假设点了3000道菜,两种吃法的效率当下立现,如果按第一种吃法,大家都饿死了,即使做好了,桌子(内存)也放不下。

我上面说解耦,并不完全准确,准确得说,吃饭的程序不再依赖于整桌菜,而只依赖于下一道菜,而依赖于前者往往空间和时间效益都低,而依赖于后者较高。

这是 Generator 的实际意义,在值的使用者和值的生产者之间,建立效益更高的依赖,规避效益不高的依赖。

感谢大神 IT浪人 的解释

作用

  • 悬停/挂起 流程

迭代器(iterator + for...of)

ES6 中除了新特性外,还有一个新的规范,那就是关于迭代的规范,他包括两部分分别是 “可迭代规范(iterable protocol)” 和 “迭代器规范(iterator protocol)”。任何实现了前者的对象,都可以进行 for…of 循环。

String, Array, Map, Set 等是原生可迭代对象,因为他们都在原型(prototype)对象中实现了 Symbol.iterator 键对应的方法

for…of 是对象迭代器的遍历,而 for…in 是对象中 可枚举 值的遍历

// 1. 迭代器规范
const iter = {
  counter: 0,
  next(){    // 迭代器是实现了 "next()" 函数的对象
    if(++this.counter < 10){
      return {    // 返回一个含有两个键值对的对象,Object {done => boolean, value => any}
        done: false,
        value: this.counter
      }
    }else{
      this.counter = 0;
      return {    // done = true 时,value非必须
        done: true
      }
    }
  }
};

// 2. 可迭代规范,实现 "Symbol.iterator => func()" 键值对;而 "func()" 返回一个 迭代器对象
const iterObj = {};
for(var i of iterObj){};    // TypeError: iterObj is not iterable
iterObj[Symbol.iterator] = function() {
  return iter;
};
for(var i of iterObj){
  console.log(i);    // 1,2,3,4,5,6,7,8,9
};

参考

【Git】常用命令

生成 SSH keys

# 查看 SSH keys 是否存在
ls -al ~/.ssh

# 生成新的 SSH key
ssh-keygen -t rsa -C "[email protected]"

# 测试 SSH
ssh -T [email protected]

gerrit 多 SSH 配置

~/username/.ssh/config:

# gerrit
Host gerrit_host_url
HostName gerrit_host_url
# gerrit 对应的 email 或者用户名
User email
PreferredAuthentications publickey
# github对应的私钥
IdentityFile ~/.ssh/id_rsa_gerrit

查看/修改用户信息

# 查看
git config user.name
git config user.email

#修改
git config --global user.name "rocco"
git config --global user.email "[email protected]"

查看/创建/切换 分支

# 查看本地分支
git branch

# 查看本地和远程分支
git branch -a

# 新建分支
git branch <branchName>

# 切换分支
git checkout <branchName>

# 新建并切换至新建的分支
git checkout -b <branchName>

删除分支

# 删除本地的某个分支
git branch -D <branchName>

# 删除远程的分支
git branch -r -d origin/<branchName>
git push origin :<branchName>

# 解释:
# git branch -r -d origin/<branchName> 只是删除本地的索引,而不是真正删除远程分支的内容
# 要想真正删除远程分支上的内容,把一个空分支 push 到 server 上,等于删除该分支,git push origin :<branchName>
# 注意:冒号前面的空格不能少

撤销提交(复位)

# 该命令撤消上一个commit,但保留add的文件,Git 会暂存所有的因复位带来的差异,但不提交它们
git reset --soft HEAD^

# 强制复位前一个提交
git reset --hard HEAD^

参考

  1. Git 撤销提交和修改相关操作

清空当前分支

# 清空当前分支
# 注意不要遗漏 -r 后面的 .
git rm --cached -r .
git clean -f -d

# 创建空的 commit
git commit --allow-empty -m "[empty] initial commit"

# 推送空分支
git push

新建空分支

# 新建空分支
# 注意不要遗漏 -r 后面的 .
git branch -b <new_branch>
git rm --cached -r .
git clean -f -d

# 创建空的 commit
git commit --allow-empty -m "[empty] initial commit"

# 推送空分支
git push origin <new_branch>

修改文件名大小写

git config core.ignorecase true

git mv <source> <destination> # 重命名 "source" 为 "destination"

修改提交后的用户名和邮箱

#!/usr/bin/env bash

# set -ex

git filter-branch --env-filter '
    an="$GIT_AUTHOR_NAME"
    am="$GIT_AUTHOR_EMAIL"
    cn="$GIT_COMMITTER_NAME"
    cm="$GIT_COMMITTER_EMAIL"
    if [ "$GIT_COMMITTER_EMAIL" = "要修改的邮箱地址" ]
    then
        cn="想要改成的用户名"
        cm="想要改成的邮箱地址"
    fi
    if [ "$GIT_AUTHOR_EMAIL" = "要修改的邮箱地址" ]
    then
        an="想要改成的用户名"
        am="想要改成的邮箱地址"
    fi
    export GIT_AUTHOR_NAME="$an"
    export GIT_AUTHOR_EMAIL="$am"
    export GIT_COMMITTER_NAME="$cn"
    export GIT_COMMITTER_EMAIL="$cm"
'

删除 Git 仓库的所有提交记录

Checkout

git checkout --orphan latest_branch

Add all the files

git add -A

Commit the changes

git commit -am "commit message"

Delete the branch

git branch -D master

Rename the current branch to master

git branch -m master

Finally, force update your repository

git push -f origin master --set-upstream origin master

恢复误删的 git stash 记录

背景

使用 Sourcetree 贮藏工作现场后,误删了这个 stash

第一步:git fsck --unreachable

git-fsck 文档

查找所有的 unreachable 记录

第二步:git show <sha>

其中 sha 是记录的 key(如下面的:bb3cc162df1270fcabac0dec53effc8166b9563b)

命令行中 drop:

$ git stash drop
Dropped refs/stash@{0} (bb3cc162df1270fcabac0dec53effc8166b9563b)

Sourcetree 中 drop:

使用 Sourcetree 删除时,我们是不知道 sha 的,此时回到第一步 git fsck --unreachable

查找所有的 unreachable 记录,例如:

$ git fsck --unreachable

Checking object directories: 100% (256/256), done.
Checking objects: 100% (3730/3730), done.
unreachable tree 3f80ada1c908812f125a5869ca7f77429e42a8c8
unreachable tree 7a409b8150ff5533ab4d22ebfbec520d4ce0bf7d
unreachable blob 8600c5b5609fbb541a2aa492d4eb4ed72c1e43a0
unreachable blob ee006fdb6554149a9f6aa6c3ef3d21853292b44f
unreachable blob b0c14ad2a35149815261ade2f89f3cf8e9fa096c
unreachable blob 57c221b28812aec98f2c44ee226ae75325ef28f3
unreachable blob 2783aa80d2dcdb51ac8cfe2f0cdbfd813ac6bc15
unreachable blob 65439852e70c0649d605a8452d79b4e16d40dada
unreachable blob 4b0472cf3317cbe8a476abd35ead1db1f791ebb7
unreachable tree c8c4ae2a9580bf788b39bf9ad770cc50cdeec734
unreachable tree 178594b9b22143860b6894377bc72b5c0ecbf6ea
unreachable tree 2a45ce0eb08dac0efe095b65eb9e98e3b1e26625
unreachable commit 49863934d99402979b2f43abd77b05e5a19780f2

对所有 unreachable commit 开头的记录,依次执行 git show <sha>,查看是否为误删的提交,例如:

$ git show bb3cc162df1270fcabac0dec53effc8166b9563b

commit bb3cc162df1270fcabac0dec53effc8166b9563b
Merge: 2800dc1 d7625d4
Author: *** <***@***.com>
Date:   Thu Jul 5 11:43:10 2018 +0800

    WIP on dev: 2800dc1 style(pc): 样式修改

diff --cc src/common/filters/index.js
index ac9ea48,ac9ea48..1f72657
--- a/src/common/filters/index.js
+++ b/src/common/filters/index.js
@@@ -1,5 -1,5 +1,3 @@@
  import currency from './currency'

--export default {
--  currency
--}
++export default { currency }

第三步:git stash apply <sha>

找到误删的 stash 后,使用 git stash apply <sha> 即可恢复

参考

删除最后一次提交记录

git reset --hard HEAD~1
git push --force

修改最后一次提交记录

git reset commitId # (注: 不要带 --hard)到上个版本
git stash # 暂存修改
git push --force # 强制 push, 远程的最新的一次 commit 被删除
git stash pop # 释放暂存的修改, 开始修改代码
git add .
git commit -m "massage"
git push

Gerrit Push

#!/usr/bin/env bash

# set -ex

branch=$(git symbolic-ref --short -q HEAD)

if [[ ! -n "$1" ]]; then
    echo "You have not input a branch, use the current branch:" $branch
else
    branch=$1
fi

echo "push branch: $branch"

git push origin HEAD:refs/for/$branch

exit 0

【JS】try...catch

注意

catch 内一定要加 console.error(error),不然 try 块里出现 BUG 时你会想哭的

示例代码:

try {
...
} catch (error) {
  console.error(error)
}

谁加谁知道,加了都说好

类似的还有 Promise,Promise 里的函数是被 try...catch 包裹着的

return in finally override try

function example() {
  try {
    return true;
  } finally {
    return false;
  }
}

example(); // false

According to ECMA-262 (5ed, December 2009), in pp. 96:

The production TryStatement : try Block Finally is evaluated as follows:

Let B be the result of evaluating Block.
Let F be the result of evaluating Finally.
If F.type is normal, return B.
Return F.

And from pp. 36:

The Completion type is used to explain the behaviour of statements (break, continue, return and throw) that perform nonlocal transfers of control. Values of the Completion type are triples of the form (type, value, target), where type is one of normal, break, continue, return, or throw, value is any ECMAScript language value or empty, and target is any ECMAScript identifier or empty.

It's clear that return false would set completion type of finally as return, which cause try ... finally to do 4. Return F.

参考

【VueRouter】router.addRoutes

说明:动态添加更多的路由规则

// router.js

import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import(/* webpackChunkName: "home" */ './views/Home.vue'),
    },
    {
      path: '/about',
      name: 'about',
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
    },
    {
      path: '*',
      redirect: '/',
    },
  ],
});
App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link v-for="r in routes"
                   :key="r.path"
                   :to="r.path">{{ r.name }}</router-link>
    </div>
    <router-view/>

    <button @click="addRoutes">add routes</button>
  </div>
</template>

<script>
const AsyncComponent = () => import(/* webpackChunkName: "async" */ '@/views/Async');

export default {
  name: 'app',

  data() {
    return {
      routes: [{ name: 'Home', path: '/' }, { name: 'About', path: '/about' }],
    };
  },

  computed: {
    routeMap() {
      return this.routes.reduce((accu, curr) => {
        accu[curr.name] = curr.path;
        return accu;
      }, {});
    },
  },

  methods: {
    addRoutes() {
      if (this.routeMap.Async) return;

      this.$router.addRoutes([
        {
          path: '/async',
          component: AsyncComponent,
        },
      ]);

      this.routes.push({ name: 'Async', path: '/async' });
    },
  },
};
</script>

参考

【JS】参数传递策略

参数传递策略是求值策略的特殊情况。

ECMAScript 中所有的参数都是按值传递的。 -- 《JavaScript 高级程序设计》 4.1.3

基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样(复制指针)。

传值调用(Call By Value)

对于 基本类型值 的传递是传值调用

特征: 对于传递过来的变量进行修改,不会影响到原变量。

传引用调用(Call By Reference)

对于 引用类型值 的传递是传引用调用

特征: 对于变量的成员进行修改时,会直接影响原变量;而如果对传递过来的变量进行重新赋值,则不会影响原变量,并且此后再修改变量的成员,也不会影响原变量。

其它的求值策略

传共享对象调用(Call By Sharing)

特征: 无论是对于变量成员的修改,还是对变量重新赋值,都会影响到原对象。

传名调用(Call By Name)

特征: 如果实际参数在函数的求值中未被用到,则它永不被求值;如果这个实际参数使用多次,则它每次都被重新求值。

传需求调用(Call By Need)

特征: “传需求调用”是传名调用的记忆化版本,如果“函数的实际参数被求值了”,这个值被存储起来已备后续使用。当函数实际参数被使用两次或更多次的时候,传需求调用总是更快。

传需求调用的条件:纯函数。

More ...

参考

【CSS】常用选择器 & CSS 权重计算规则

常用选择器

  • * 通用选择器:选择所有元素,不参与计算优先级,兼容性:IE6+
  • #X id 选择器:选择 id 值为 X 的元素,兼容性:IE6+
  • .X 类选择器:选择 class 包含 X 的元素,兼容性:IE6+
  • X Y 后代选择器:选择满足 X 选择器的后代节点中满足 Y 选择器的元素,兼容性:IE6+
  • X 元素选择器:选择标所有签为 X 的元素,兼容性:IE6+
  • :link:visited:focus:hover:active 链接状态:选择特定状态的链接元素,顺序 LoVe HAte,兼容性:IE4+
  • X + Y 直接兄弟选择器:在 X 之后第一个兄弟节点中选择满足 Y 选择器的元素,兼容性:IE7+
  • X > Y 子选择器:选择 X 的子元素中满足 Y 选择器的元素,兼容性:IE7+
  • X ~ Y 兄弟:选择 X 之后所有兄弟节点中满足 Y 选择器的元素,兼容性:IE7+
  • [attr]:选择所有设置了 attr 属性的元素,兼容性:IE7+
  • [attr=value]:选择属性值刚好为 value 的元素
  • [attr~=value]:选择属性值为空白符分隔,其中一个的值刚好是 value 的元素
  • [attr|=value]:选择属性值刚好为 value 或者 value- 开头的元素
  • [attr^=value]:选择属性值以 value 开头的元素
  • [attr$=value]:选择属性值以 value 结尾的元素
  • [attr*=value]:选择属性值中包含 value 的元素
  • [:checked]:选择单选框,复选框,下拉框中选中状态下的元素,兼容性:IE9+
  • X:afterX::after::after 伪元素,选择元素虚拟子元素(元素的最后一个子元素),CSS3 中 :: 表示伪元素。兼容性 :after 为 IE8+,::after 为 IE9+
  • :hover:鼠标移入状态的元素,兼容性:a 标签 IE4+, 所有元素 IE7+
  • :not(selector):选择不符合 selector 的元素。不参与计算优先级,但是在计算选择器数量时会把其中的选择器当做普通选择器进行计数,兼容性:IE9+
  • ::first-letter:伪元素,选择块元素第一行的第一个字母,兼容性:IE5.5+
  • ::first-line:伪元素,选择块元素的第一行,兼容性:IE5.5+
  • :nth-child(an + b):伪类,选择前面有 an + b - 1 个兄弟节点的元素,其中 n >= 0, 兼容性:IE9+
  • :nth-last-child(an + b):伪类,选择后面有 an + b - 1 个兄弟节点的元素 其中 n >= 0,兼容性:IE9+
  • X:nth-of-type(an+b):伪类,X 为选择器,解析得到元素标签,选择前面有 an + b - 1 个相同标签兄弟节点的元素。兼`容性:IE9+
  • X:nth-last-of-type(an+b):伪类,X 为选择器,解析得到元素标签,选择后面有 an+b-1 个相同标签兄弟节点的元素`。兼容性:IE9+
  • X:first-child:伪类,选择满足 X 选择器的元素,且这个元素是其父节点的第一个子元素。兼容性:IE7+
  • X:last-child:伪类,选择满足 X 选择器的元素,且这个元素是其父节点的最后一个子元素。兼容性:IE9+
  • X:only-child:伪类,选择满足 X 选择器的元素,且这个元素是其父元素的唯一子元素。兼容性:IE9+
  • X:only-of-type:伪类,选择 X 选择的元素,解析得到元素标签,如果该元素没有相同类型的兄弟节点时选中它。兼容性:IE9+
  • X:first-of-type:伪类,选择 X 选择的元素,解析得到元素标签,如果该元素是此类型元素的第一个兄弟。选中它。兼容性:IE9+

CSS 权重计算规则

权重的基本规则

  1. 不同的权重,权重值高则生效
  2. 相同的权重:以后面出现的选择器为最后规则
  3. 无论多少个元素组成的选择器,都没有一个 class 选择器权重高(不要被元素 +1,class +10 迷惑)
  4. 要特别注意 :not() 伪类,在优先级计算中不会被看作是伪类,但是在计算选择器数量时会把 其中的选择器 当做普通选择器进行计数
  5. 继承来的属性( colorline-height )权重为 0

笔试时遇到一个很有意思的 CSS 权重题目

HTML:

<div class="outer" id="app">
  <div class="inner">
    <p class="highlight">测试文本</p>
  </div>
</div>

CSS:

#app .inner:not(#div) .highlight {
  color: red;
}

#app .highlight:nth-of-type(1):nth-last-of-type(1) {
  color: blue;
}

请问 测试文本 的颜色为?

根据很久之前看过的一篇博文,隐约记得里面有一个权重口诀:

从 0 开始,一个行内样式 +1000,一个 id +100,一个 class / 属性选择器或者伪类 +10,一个元素名或者伪元素 +1,通配符 * 不计权重。

我天真的选择了:测试文本,蓝色。😭

正确答案为:测试文本,红色!

权重计算:

  1. 要特别注意 :not() 伪类,在优先级计算中不会被看作是伪类,但是在计算选择器数量时会把 其中的选择器 当做普通选择器进行计数,MDN

  2. 权重为:100 (#app) + 10 (.inner) + 100 (#div) + 10 (.highlight) = 220

    #app .inner:not(#div) .highlight {
      color: red;
    }
  3. 权重为:100 (#app) + 10 (.highlight) + 10 (:nth-of-type(1)) + 10 (:nth-last-of-type(1)) = 130

    #app .highlight:nth-of-type(1):nth-last-of-type(1) {
      color: blue;
    }
  4. 所以最终颜色为红色

权重等级

根据选择器种类的不同可以分为四类,也决定了四种不同等级的权重值。

行内样式

行内样式包含在你的 html 中 对你的元素产生直接作用,比如:

<h1 style="color: #fff;">header</h1>

ID 选择器

Id 也是元素的一种标识,比如 #div

类,属性选择器和伪类选择器

这一类包括各种 class;属性选择器,比如: [title];伪类选择器,比如 :hover 等等

元素和伪元素

元素跟伪元素选择器,比如 div::before::after::first-letter::first-line::selecton

如何快速确定权重

记忆口诀:从 0 开始,一个行内样式 +1000,一个 id +100,一个 class 属性选择器或者伪类 +10,一个元素名或者伪元素 +1,通配符 * 不计权重,继承来的属性( colorline-height )权重为 0。

例如:

body #content .data img:hover {
}

最终权重为:1 (body) + 100 (#content) + 10 (.data) + 1 (img) + 10 (:hover) = 122

关于 !important

!important 会覆盖任何其他的样式声明。

这个规则有点毒,除非特殊情况,不然不要使用。

参考:

【JS】iframe 踩坑之路

通信(postMessage)

HTML 结构

<!-- parent.html -->
<body>
  <h1>this is parent page</h1>

  <iframe src="./child-a.html" id="child-a" name="child-a"></iframe>

  <iframe src="./child-b.html" id="child-b" name="child-b"></iframe>
</body>

<!-- child-a.html -->
<body>
  <h1>this is child-a page</h1>
</body>

<!-- child-b.html -->
<body>
  <h1>this is child-b page</h1>
</body>

1. 父 -> 子

// parent.html

const frameA = document.getElementById('child-a');
frameA.onload = () => {
  iFrame.contentWindow.postMessage('some message form parent', '*');
};

// child-a.html

window.addEventListener(
  'message',
  (event) => {
    console.log('received msg:', event);
  },
  false,
);

2. 子 -> 父

// child-a.html

window.parent.postMessage({ msg: 'some message form child-a' }, '*');

// parent.html

window.addEventListener(
  'message',
  (event) => {
    console.log('received msg:', event);
  },
  false,
);

3. 兄弟之间

// child-a.html

window.parent.frames[1].contentWindow.postMessage(
  'some message form child-a',
  '*',
);

// child-b.html

window.addEventListener(
  'message',
  (event) => {
    console.log('received msg:', event);
  },
  false,
);

FireFox back button bug

iframe.contentWindow.location.replace(newUrl) 的方式设置 iframe URL,不要用 iframe.src = newUrl,不然 FireFox 下需要点两次返回按钮才可以返回到上一页。

参考

【HTML】meta 标签设置

一、PC 端

1. viewport 模板

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1">
    <meta name="format-detection" content="telephone=no, email=no">
    <link rel="shortcut icon" type="image/x-icon" href="">
    <title>标题</title>
    <meta name="description" content="不超过150个字符">
    <meta name="keywords" content="">
</head>

<body> 这里开始内容 </body>

</html>

二、移动端

1. viewport 模板

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1">
    <meta name="format-detection" content="telephone=no, email=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <!-- IOS添加到主屏后的标题和图标 -->
    <meta name="apple-mobile-web-app-title" content="标题">
    <link rel="apple-touch-icon-precomposed" sizes="76x76" href="">

    <link rel="shortcut icon" type="image/x-icon" href="">
    <title>标题</title>
    <meta name="description" content="不超过150个字符">
    <meta name="keywords" content="">

    <link rel="stylesheet" href="index.css">
</head>

<body> 这里开始内容 </body>

</html>

三、meta 标签详细说明

1. H5 页面窗口自动调整到设备宽度,并禁止用户缩放页面

<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

属性基本含义:

  1. width=device-width - 控制 viewport 的大小,device-width 为设备的宽度
  2. initial-scale - 初始的缩放比例
  3. minimum-scale - 允许用户缩放到的最小比例
  4. maximum-scale - 允许用户缩放到的最大比例
  5. user-scalable - 用户是否可以手动缩放

2. 启用 360 浏览器的极速模式(webkit)

<meta name="renderer" content="webkit">

3. ie=edge 告诉 IE 使用最新的引擎渲染网页,chrome=1 则可以激活 Chrome Frame

<meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1">

4. 取消微信/百度对网站的自动转码

<meta http-equiv="Cache-Control" content="no-transform">
<meta http-equiv="Cache-Control" content="no-siteapp">

5. 国产浏览器强制显示

<!-- UC强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">

6. 忽略将页面中的数字识别为电话号码

<meta name="format-detection" content="telephone=no">

7. 忽略 Android 平台中对邮箱地址的识别

<meta name="format-detection" content="email=no">

8. 当网站添加到主屏幕快速启动方式,可隐藏地址栏,仅针对 ios 的 safari

<meta name="apple-mobile-web-app-capable" content="yes">

9. 将网站添加到主屏幕快速启动方式,仅针对 ios 的 safari 顶端状态条的样式

<meta name="apple-mobile-web-app-status-bar-style" content="black"> <!– 可选default、black、black-translucent –>

10. 需要在网站的根目录下存放 favicon 图标,防止 404 请求(使用 fiddler 可以监听到),在页面上需加 link 如下:

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

11. QQ 聊天框中发送网站 URL,预览内容修改方法:

<meta name="description" itemprop="description" content="摘要">
<meta itemprop="name" content="标题">
<meta itemprop="image" content="图片">

【Config】浏览器下载文件,图片、PDF直接预览

最近做项目时,发现点击 URL,浏览器始终默认下载文件,而不是预览。

解决方案

修改文件的响应头

1. 移除 Content-Disposition 字段

说明:一个可以让客户端下载文件并建议文件名的头部。文件名需要用双引号包裹。

例子:Content-Disposition: attachment; filename="filename.ext"

2. 设置 Content-Type 字段

说明:当前内容的 MIME 类型

例子:Content-Type: text/html; charset=utf-8

参考

【方案】基于 Vue 的前端项目集成方案

DEMO

vue-simple-micro-frontends

目标

  1. 子项目支持单独开发,单独部署(避免前端巨无霸,多团队同时开发)
  2. 单一的入口 HTML(不同项目之间切换时无白屏现象)

思路

  1. 将子项目打包成库文件,umd 模块
  2. 在入口项目中加载子项目
  3. 使用 vue-router 的 router.addRoutes 将子项目的路由动态注册到入口项目中
  4. 使用 Vuex 的 store.registerModule 将子项目的 store module 动态注册到入口项目中

具体实现

一、初始化项目

# 安装 Vue CLI 3
yarn global add @vue/cli

mkdir projects
cd ./projects

vue create entry-app
vue create sub-app-one

二、入口项目 (entry-app)

1. 提取公共依赖

// vue.config.js
module.exports = {
  configureWebpack: {
    externals: {
      vue: 'Vue',
      vuex: 'Vuex',
      'vue-router': 'VueRouter',
      'element-ui': 'ELEMENT',
      moment: 'moment',
    },
  },
};

2. 手动引入外部依赖

<!-- index.html -->
<head>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.3.9/theme-chalk/index.css" rel="stylesheet">
</head>

<body>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.min.js"></script>
</body>

3. 挂载 router / store 实例

在子项目中用到了这两个实例

// main.js
window.microApp = {};
// store.js
const store = new Vuex.Store({
  state: { name: 'entry-app' },
  mutations: {},
  actions: {},
});

window.microApp.store = store;
// router.js
const router = new Router({ routes });

window.microApp.router = router;

4. 加载子项目

// load-module.js
export default function loadModule(url, options = {}) {
  if (!url || typeof url !== 'string') {
    return Promise.reject('Illegal parameter: url, expect a not empty string.');
  }

  if (typeof options !== 'object') {
    return Promise.reject('Illegal parameter: options, expect an object.');
  }

  const noCache = !!options.noCache;

  return new Promise((resolve) => {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.async = true;
    script.onload = ({ type }) => resolve({ type, url });
    script.onerror = ({ type }) => resolve({ type, url });
    script.src = noCache ? addTimestamp(url) : url;
    document.body.appendChild(script);
  });
}

function addTimestamp(url) {
  return url.indexOf('?') > -1 ? `${url}&t=${Date.now()}` : `${url}?t=${Date.now()}`;
}
// main.js
import loadModule, { remove } from 'path/to/load-module.js';

const modules = [
  'http://localhost:7200/sub-app-one/dist/sub-app-one.umd.min.js',
  'http://localhost:7200/sub-app-two/dist/sub-app-two.umd.min.js',
];

Promise.all(modules.map((url) => loadModule(url))).then((res) => {
  console.log(res);
  remove();
});

三、子项目 (sub-app-one)

1. 修改 build script

package.json

{
  "scripts": {
    "build": "vue-cli-service build --target lib --name sub-app-one ./src/module.js"
  }
}

这样可以将 sub-app-one 构建成一个库,然后在 entry-app 中引用,参考

2. 提取公共依赖

// vue.config.js
const webpack = require('webpack');

const APP_NAME = require('./package.json').name;
const IS_DEV = process.env.NODE_ENV === 'development';

module.exports = {
  configureWebpack: {
    externals: IS_DEV
      ? {}
      : {
          vue: 'Vue',
          vuex: 'Vuex',
          'vue-router': 'VueRouter',
          'element-ui': 'ELEMENT',
          moment: 'moment',
        },

    plugins: [
      new webpack.DefinePlugin({
        'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME),
      }),
    ],
  },
};

开发时使用 node_modules 文件,构建后使用外部依赖

3. 动态注册路由

为子项目路由添加命名空间

// router-list.js
const IS_DEV = process.env.NODE_ENV === 'development';
const APP_NAME = process.env.VUE_APP_NAME;

export default [
  {
    path: IS_DEV ? '/' : `/${APP_NAME}`,
    name: APP_NAME,
    title: APP_NAME,
    redirect: { name: `${APP_NAME}.page-a` },
    component: () => import(/* webpackChunkName: "index" */ '@/views/Index.vue'),
    children: [
      {
        path: 'page-a',
        name: `${APP_NAME}.page-a`,
        component: () => import(/* webpackChunkName: "page-a" */ '@/views/PageA.vue'),
      },
      {
        path: 'page-b',
        name: `${APP_NAME}.page-b`,
        component: () => import(/* webpackChunkName: "page-b" */ '@/views/PageB.vue'),
      },
    ],
  },
];

注册路由

// module.js
import routes from 'path/to/router-list.js';

const routerInstance = window.microApp.router; // 后面有解释
routerInstance.addRoutes(routes);

4. 动态注册 Vuex module

提取 Vuex module,添加命名空间

// store-module.js
export default {
  namespaced: true, // namespaced must be true in module app.
  state: { name: process.env.VUE_APP_NAME },
  mutations: {},
  actions: {},
};

注册 Vuex module

// module.js
import store from 'path/to/store-module.js';

const storeInstance = window.microApp.store; // 后面有解释
const moduleName = process.env.VUE_APP_NAME;
storeInstance.registerModule(moduleName, store);

5. 自动添加 CSS 命名空间

添加插件

yarn add postcss-selector-namespace -D

使用插件

// postcss.config.js
const APP_NAME = require('./package.json').name;

module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-selector-namespace': { namespace: `.${APP_NAME}` },
  },
};

不足 & 待解决的问题

  1. 开发环境复杂,子项目开发时依赖 �Nginx 转发
  2. 子项目开发模式下无法加载公共导航模块

杂项

参考

Web Components

技术组成

  • Custom elements,允许开发者创建自定义的元素
  • Shadow DOM,即影子 DOM,通常是将 Shadow DOM 附加到主文档 DOM 中,并可以控制其关联的功能。而这个 Shadow DOM 则是不能直接用其它主文档 DOM 来控制的
  • HTML templates,即 <template><slot> 元素,用于编写不在页面中显示的标记模板
  • HTML Imports,用于引入自定义组件

缺点

  • 上下游生态不完善
  • 系统架构复杂,组件通信问题
  • 兼容性差

工具

【CSS】使用构建工具自动为 CSS 添加命名空间

使用

安装依赖

yarn add postcss-selector-namespace -D

修改 postcss 配置

postcss.config.js

module.exports = {
  plugins: {
    'postcss-selector-namespace': { namespace: '.custom-namespace' },
  },
};

修改 index.html(挂载点)

添加 .custom-namespace 类名

<div class="custom-namespace" id="app"></div>

示例

input.css

h1,
.h1 {
  color: red;
}

output.css

.custom-namespace h1,
.custom-namespace .h1 {
  color: red;
}

说明

禁止添加 namespace

在不想添加 namespace 的样式前增加 :root

input.css

:root #app {
  font-weight: bold;
}

output.css

#app {
  font-weight: bold;
}

参考

【Vuex】store.registerModule

说明:动态注册 vuex module

// store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    name: 'my app',
  },
  mutations: {},
  actions: {},
  modules: {
    a: {
      namespaced: true,
      state: {
        name: 'a',
      },
    },
  },
});
component.vue

<template>
  <div id="app">
    <!-- 注册 template 模块 -->
    <button @click="registerModule('template')">register module</button>
  </div>
</template>

<script>
export default {
  methods: {
    registerModule(namespace) {
      if (this.$store.state[namespace]) return;

      this.$store.registerModule(namespace, {
        namespaced: true,
        state: { name: namespace },
        mutations: {
          setName(state, payload) {
            state.name = payload;
          },
        },
      });
    },
  },

  mounted() {
    // 注册 mounted 模块
    this.registerModule('mounted');
  },
};
</script>

说明

调用 registerModule 后 Vue Devtools 的 Vuex 面板未更新
解决方法:重启 chrome 开发者工具,参考

参考

【PWA】应用清单

manifest.json 文件

{
  "name": "Rocco's Blog",
  "short_name": "Rocco",
  "description": "An front-end engineer who loves and pursues simple.",
  "start_url": "/index.html",
  "theme_color": "#333",
  "background_color": "#fff",
  "display": "standalone",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/images/icons/icon-64.png",
      "sizes": "64x64",
      "type": "image/png"
    },
    {
      "src": "/images/icons/icon-128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/images/icons/icon-256.png",
      "sizes": "256x256",
      "type": "image/png"
    }
  ]
}

说明:

  • name 指定提示用户安装应用时横幅上的文本
  • short_name 指定应用安装后出现在用户主屏幕上的文本
  • start_url 指定当用户从设备的主屏幕开启 Web 应用时所出现的第一个页面,如果你想追踪有多少人是通过主屏幕图标访问网站的,可以这样设置 start_url:/index.html?from=screen
  • theme_color 指定浏览器地址栏的颜色
  • background_color 指定当用户从设备的主屏幕开启 Web 应用时
  • icons 指定 Web 应用被添加到设备主屏幕时所显示的图标
  • display 指定 Web 应用的显示模式,是可选项,默认为 browser 模式
    • fullscreen 打开 Web 应用并占用整个可用的显示区域。
    • standalone 打开 Web 应用以看起来像一个独立的原生应用。此模式下,用户代理将排除诸如 URL 栏等标准浏览器 UI 元素,但可以包括诸如状态栏和系统返回按钮的其他系统 UI 元素。
    • minimal-ui 此模式类似于 fullscreen,但为终端用户提供了可访问的最小 UI 元素集合,例如,后退按钮、前进按钮、重载按钮以及查看网页地址的一些方式。
    • browser 使用操作系统内置的标准浏览器来打开 Web 应用。

在 HTML 中引入:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Progressive Web Application</title>
  <link rel="manifest" href="/manifest.json">
</head>

<body>

</body>

</html>

添加到主屏幕

添加到桌面横幅出现条件

  • 需要 manifest.json 文件
  • 清单文件需要启动 URL
  • 需要 144x144 的 PNG 图标 *?*
  • 网站正在使用通过 HTTPS 运行的 Service Worker
  • 用户需要至少浏览网站两次,并且两次访问间隔在五分钟之上 *?*

高级用法

取消提示

window.addEventListener('beforeinstallprompt', function(e) {
  e.preventDefault();
  return false;
});

代码会监听 beforeinstallprompt 事件并防止操作栏的默认行为触发。代码使用了标准 JavaScript 的 preventDefault() 功能来取消事件并返回 false,两处代码都是需要的,以确保操作栏不会出现。

用户点击统计

window.addEventListener('beforeinstallprompt', function(event) {
  event.userChoice.then(result => {
    console.log(result.outcome);
    if (result.outcome == 'dismissed') {
      // 发送数据以进行分析
    } else {
      // 发送数据以进行分析
    }
  });
});

推迟提示

这可以让用户控制是否要添加你的网站,而不是浏览器决定何时显示横幅。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Progressive Web Application</title>
  <link rel="manifest" href="/manifest.json">
</head>

<body>
  <!-- 点击此按钮会显示安装横幅 -->
  <button id="btnSave" disabled>Click this to show the prompt</button>
</body>
<script>
  window.addEventListener('DOMContentLoaded', function () {
    let btnSave = document.getElementById('btnSave');
    let savedPromptEvent;
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker
        .register('/sw.js')
        .then(registration => {
          // 注册成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(error => {
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', error);
        });
    }

    window.addEventListener('beforeinstallprompt', function (event) {
      event.preventDefault();
      btnSave.removeAttribute('disabled');
      savedPromptEvent = event;
      return false;
    });

    btnSave.addEventListener('click', function () {
      if (savedPromptEvent !== undefined) {
        savedPrompt.prompt();
        savedPrompt.userChoice.then(result => {
          if (result.outcome == 'dismissed') {
            console.log('User dismissed homescreen install');
          } else {
            console.log('User added to homescreen');
          }
          savedPrompt = null;
        });
      }
    });
  });
</script>

</html>

参考

【Tools】Prettier 配置

推荐

如果项目中配置 eslint 的话,在 VSCode settings.json 里添加配置 "prettier.eslintIntegration": true

参考配置

.prettierrc

{
  "printWidth": 80,  // 换行字符串阈值
  "singleQuote": true,  // 使用单引号
  "semi": true,  // 句末加分号
  "trailingComma": "all",  // 对象 / 数组 多行时最后一行加逗号
  "bracketSpacing": true,  // 对象 / 数组 边界加空格
  "arrowParens": "always" // 箭头函数参数加括号
}

在命令行使用 prettier 格式化项目代码

yarn global add prettier

prettier --config ./.prettierrc --write "src/**/*.{js,vue,less}"

参考文档

【Tools】Debugger for Chrome 配置

.vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch vuejs",
      "url": "http://localhost:8080",
      "webRoot": "${workspaceFolder}/src",
      "breakOnLoad": true,
      "sourceMapPathOverrides": {
        "webpack:///src/*": "${webRoot}/*"
      }
    }
  ]
}
// vue.config.js

module.exports = {
  configureWebpack: {
    devtool: 'source-map',
  },
};

参考

【JS】scrollTop & scrollLeft

兼容写法

const scrollTop =
  window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
const scrollLeft =
  window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
API Chrome 68 IE 11 EDGE 17 Firefox
window.pageYOffset true true true true
document.documentElement.scrollTop true true false true
document.body.scrollTop false false true false

参考

【TypeScript】TS 项目内引用 JS 文件

JS 文件:calendar.js

/// <reference path='path/to/calendar.d.ts' />

const calendar = {
  // balabala...
};

export default calendar;

添加类型声明文件:calendar.d.ts

export const calendar: any;

使用

<script lang="ts">
import calendar from 'path/to/calendar.js';
</script>

参考

【SEO】入门

搜索引擎优化(Search engine optimization,简称 SEO),指为了提升网页在搜索引擎自然搜索结果中(非商业性推广结果)的收录数量以及排序位置而做的优化行为。

一、机器可读

  1. Baiduspider只能读懂文本内容;
  2. 使用文字而不是 flash、图片、Javascript 等来显示重要的内容或链接;
  3. Ajax 等搜索引擎不能识别的技术,只用在需要用户交互的地方,不把希望搜索引擎“看”到的导航及正文内容放到 Ajax 中;
  4. 不使用 frame 和 iframe 框架结构,通过 iframe 显示的内容可能会被百度丢弃;

二、网站结构

  1. 确保每个页面都可以通过至少一个文本链接到达;
  2. 重要的内容,应该能从首页或者网站结构中比较浅的层次访问到;
  3. 合理分类网站上的内容,不要过度细分;
  4. 导航中使用文字链接,不使用复杂的 js 或者 flash;
  5. 使用图片做导航时,可以使用 Alt 注释,用 Alt 告诉搜索引擎所指向的网页内容是什么;

三、子域名与目录的选择

选择使用子域名还是目录来合理的分配网站内容,对网站在搜索引擎中的表现会有较大的影响。

  1. 在某个频道的内容没有丰富到可以当做一个独立站点存在之前,使用目录形式;等频道下积累了足够的内容,再转换成子域名的形式

四、规范、简单的 URL

如果你的网站上已经存在多种 url 形式,建议按以下方式处理:
  1. 在系统中只使用正常形式 url,不让用户接触到非正常形式的 url;
  2. 不把 Session id、统计代码等不必要的内容放在 url 中;
  3. 不同形式的 url,301 永久跳转到正常形式;
  4. 防止用户输错而启用的备用域名,301 永久跳转到主域名;
  5. 使用 robots.txt 禁止 Baiduspider 抓取您不想向用户展现的形式;
建议:
  1. 让用户能从 url 判断出网页内容以及网站结构信息,并可以预测将要看到的内容;
  2. URL 尽量短,长 URL 不仅不美观,用户还很难从中获取额外有用的信息;
  3. 正常的动态 url 对搜索引擎没有影响。url 是动态还是静态对搜索引擎没有影响,但建议尽量减少动态 url 中包含的变量参数,这样即有助于减少 url 长度,也可以减少让搜索引擎掉入黑洞的风险;
  4. 不添加不能被系统自动识别为 url 组成部分的字符,如“;”、“,”等字符,用户在推荐这些 url 时,不能被自动识别为链接,会因为这些字符断开;

五、改版/换域名

  1. 在改版或者换域名时,将旧网页 301 永久重定向到内容对应的新网页,这样百度更容易发现这个转变,并迅速的将旧网页积累的权值传递给对应的新网页;
  2. 如非必要,不要做整站内容的完全更换,网站改版或者网站内重要页面链接发生变动时,应该将改版前的页面 301 永久重定向到改版后的对应的页面;
  3. 网站更换域名,应该将旧域名的所有页面 301 永久重定向到新域名上对应的页面,网站更换域名后,维持旧域名能稳定访问尽可能长的时间,给用户多一些时间记忆新域名;
  4. 网站改版/更换域名后,请把新的 URL/新域名下的 URL,通过 sitemap 提交给百度,帮助百度更快发现和作出调整。

六、合理的返回码

百度 spider 对常用的 http 返回码的处理逻辑是这样的:
  1. 404:含义是“NOT FOUND”,百度会认为网页已经失效,那么通常会从搜索结果中删除,并且短期内 spider 再次发现这条 url 也不会抓取;
  2. 503:含义是“Service Unavailable”,百度会认为该网页临时不可访问,通常网站临时关闭,带宽有限等会产生这种情况。对于网页返回 503,百度 spider 不会把这条 url 直接删除,短期内会再访问。届时如果网页已恢复,则正常抓取;如果继续返回 503,短期内还会反复访问几次。但是如果网页长期返回 503,那么这个 url 仍会被百度认为是失效链接,从搜索结果中删除;
  3. 403:含义是“Forbidden”,百度会认为网页当前禁止访问。对于这种情况,如果是新发现的 url,百度 spider 暂不会抓取,短期内会再次检查;如果是百度已收录 url,当前也不会直接删除,短期内同样会再访问。届时如果网页允许访问,则正常抓取;如果仍不允许访问,短期内还会反复访问几次。但是如果网页长期返回 403,百度也会认为是失效链接,从搜索结果中删除;
  4. 301:含义是“Moved Permanently”,百度会认为网页当前跳转至新 url。当遇到站点迁移,域名更换、站点改版的情况时,推荐使用 301 返回码,尽量减少改版带来的流量损失。虽然百度 spider 现在对 301 跳转的响应周期较长,但我们还是推荐大家这么做;
建议:
  1. 如果站点临时关闭,当网页不能打开时,不要立即返回 404,建议使用 503 状态。503 可以告知百度 spider 该页面临时不可访问,请过段时间再重试;
  2. 如果百度 spider 对您的站点抓取压力过大,请尽量不要使用 404,同样建议返回 503。这样百度 spider 会过段时间再来尝试抓取这个链接,如果那个时间站点空闲,那它就会被成功抓取了;
  3. 有一些网站希望百度只收录部分内容,例如审核后的内容,累积一段时间的新用户页等等。在这种情况,建议新发内容暂时返回 403,等审核或做好处理之后,再返回正常状态的返回码;
  4. 站点迁移,或域名更换时,请使用 301 返回;

七、良好标题

每个网页的内容都是不同的,每个网页都应该有独一无二的 title。

建议网页标题这样描述:
  1. 首页:"网站名称" OR "网站名称+提供服务介绍/产品介绍";
  2. 频道页:"频道名称+网站名称";
  3. 文章页:"文章 title+频道名称+网站名称";
推荐做法
  1. 每个网页应该有一个独一无二的标题,切忌所有的页面都使用默认标题;
  2. 标题要主题明确,包含这个网页中最重要的内容;
  3. 简明精练,不罗列与网页内容不相关的信息;
  4. 用户浏览通常是从左到右的,重要的内容应该放到 title 的靠前的位置;
  5. 使用用户所熟知的语言描述。如果你有中、英文两种网站名称,尽量使用用户熟知的那一种做为标题描述;

八、良好的内容建设

  1. 内容建设要符合网站的主题;
  2. 搜索引擎只是网站的一个普通访客,提供符合用户需求的原创内容至关重要;
  3. 写好锚文本,用户接触到你的网页是从其他网页的链接开始的,这个链接的描述能否让用户理解对吸引用户访问至关重要,在搜索引擎刚发现一个新网页时,锚文本也对这个网页的描述是唯一的参考因素;
  4. 为图片加 alt 说明;
  5. 资源较丰富的内容,可以以专题等更丰富的内容组织形式提供给用户,让用户以最低的成本获取所有需要的信息;
  6. 管理好 web2.0 等用户产生内容的产品,如果被作弊者利用,可能会影响整个站点的权重;
  7. Web2.0 类型的网站,应该充分利用自己的优势,让用户通过投票、评论等手段自己去判断资源的质量,这些对质量的判断,也可能会被搜索引擎用来判断资源的价值;

九、赢得用户对网站的推荐

当你网站上的内容对用户有用时,用户会推荐给别人,推荐的形式可能多种多样:即时通讯工具上发给自己的朋友、在自己常泡的论坛里转帖推荐、在自己网站上做友情链接推荐等等。这些推荐信息,都会被搜索引擎用来判断网页/网站价值的高低。适当的鼓励、引导用户推荐你的网站,对网站在搜索引擎中的表现有很大帮助。

十、良好展现

吸引眼球的 Title:
  1. 标题要主题明确,包含这个网页中最重要的内容;
  2. 文章页 title 中不要加入过多的额外描述,会分散用户注意力;
  3. 使用用户所熟知的语言描述;
  4. 如果您的网站用户比较熟,建议将网站名称列 title 中合适的位置,品牌效应会增加用户点击的机率;
  5. 标题要对用户有吸引力;
  6. 能让用户产生信任感;
善用 meta description:
  1. 准确的描述网页,不要堆砌关键词;
  2. 为每个网页创建不同的 description,避免所有网页都使用同样的描述;
  3. 长度合理,不过长不过短;
搜索引擎流量分析:
  1. 跳出率:只浏览一页便离开的用户的比例,跳出率高,通常代表网站对用户没有吸引力,也可能是网站内容之间的联系不够紧密;
  2. 退出率:用户从某个页面离开次数占总浏览量的比例。流程性强的网站,可以进行转换流程上的退出率分析,用于优化流程。比如购物网站,从商品页浏览-点击购买-登录-确认商品-付费这一系列的流程中每一步的退出率都记录下来,分析退出率异常的步骤,改进设计;
  3. 用户停留时间:用户停留时间反映了网站粘性及用户对网站内容质量的判断;
以上是统计分析的最基本的三个指标。行为分析可以看出用户的检索需求没有在你网站上得到满足,更进一步,思考如何更好的满足他的需求。

十一、网站信任度

网站信用度指用户给予你网站的信任程度。用户对网站的信任度是用户在网站上进行活动的基础。

  1. 页面美观、整洁,有自己的风格;
  2. 让用户可以很容易了解到网站的背景;
  3. 详细的网站介绍、联系方式,让用户可以方便的联系;
  4. 有用户评论、顾客反馈等信息,让原有的用户影响新用户;
  5. 在网站设计中注重强化网站的品牌,让用户更了解、进而信任你的网站;
不断强化品牌概念
  1. 最低层次,让用户知道他所获取的内容来自你的网站;
  2. 进阶,让用户下次再想找这个信息时,能想到你的网站;
  3. 最终,能让用户在找同类内容时,能第一时间想到你的网站;

十二、百度如何定义作弊

任何利用和放大搜索引擎的策略缺陷,利用恶意手段获取与网页质量不符的排名,引起用搜索结果质量和用户搜索体验下降的行为都会被搜索引擎当做作弊行为。

十三、其他

更换空间怎么办?

参照以下步骤:

  1. 开通新的空间,并将网站完整的迁移到新空间,并保持流畅访问;
  2. 将域名的服务器指向更新为新空间的 ip;
  3. 保证旧空间能持续访问一段时间;
  4. 关注新空间的访问日志,等 Baiduspider 的抓取完全迁移到新空间后,停止旧空间的服务;
百度是否支持 nofollow
<meta name="robots" content="nofollow" /> OR <a rel="nofollow" href="url">123</a>

百度支持以上两种写法的 nofollow,带有 nofollow 属性的 url,不会传递权值。

百度支持不支持 https 协议?

百度目前只能收录少部分 https 网页,大部分 https 网页无法收录。网站首页和对所有用户都公开的内容页面,建议不要使用 https 协议,如果非用不可,尽量将首页和重要页面做个 http 可访问版,方面百度收录。

修改网站标题是否会对网站排名带来消极影响?

title是极重要的内容。大幅修改,可能会带来大幅波动。所以请慎重对待网页标题。建议按照我们上面所推荐的写法,实事求是的将页面主旨反映在标题中即可,如无必要,尽量不做大幅修改。

修改首页的meta description是否会受到惩罚?

meta description 只是摘要的一个选择目标,修 meta description 只会影响摘要。我们鼓励大家通过 meta description 来撰写网站的简介。只是过于频繁的修改,未必会及时的反馈在摘要中。

百度建议 URL 静态化吗?

搜索引擎处理不好动态 url,主要是因为动态 url 中参数过多,很容易制造出大量内容相同、url 不同的无限循环的“黑洞”,spider 陷入其中,浪费大量的资源。尽量减少动态 url 中包含的变量参数,一方面可以减短 url 长度,另一方面,也减少把 Baiduspider 带入“黑洞”的风险。

百度支持哪些 Robots Meta 标签?
  1. <meta name="robots" content="noarchive"> :防止所有搜索引擎显示您网站的快照,请将此元标记置入网页;
  2. <meta name="robots" content="nofollow"> :禁止搜索引擎追踪此网页上的链接,且不传递链接的权重,请将此元标记置入;
站点启用 CDN、反向代理、开启 gzip 压缩等服务会不会影响搜索引擎收录?

Baiduspider对站点的抓取方式和普通用户访问一样,只要普通用户能访问到的内容,我们就能抓取到。不管是用什么技术,只要能保证用户能流畅的访问网站,对搜索引擎就没有影响。我们建议尽量选择有实力的服务商和成熟的技术,不成熟的技术容易导致访问不稳定,这就有可能影响搜索引擎的抓取了。

百度如何对待一个主题完全更换、改版的网站?

如果是内容发生根本性变化,则理论上会被视为一个全新网站,旧有超链失效。

【PWA】Service Worker

Service Worker 有以下功能和特性:

  1. 出于安全的考虑,必须在 HTTPS 环境下才能工作(host 为 localhost 或者 127.0.0.1 也可以)
  2. 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context
  3. 不能直接操作 DOM
  4. 一旦被 install,就永远存在,除非被 uninstall
  5. 需要的时候可以直接唤醒,不需要的时候自动睡眠(有效利用资源,此处有坑)
  6. 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
  7. 离线内容开发者可控
  8. 能向客户端推送消息
  9. 异步实现,内部大都是通过 Promise 实现

基础知识

注册 Service Worker

注册

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('sw.js', { scope: '/' })
    .then(function(reg) {
      console.log('Service Worker 注册成功,域名: ', reg.scope);
    })
    .catch(function(err) {
      console.log('Service Worker 注册失败: ', err);
    });
}

说明:

  1. 首先是要判断 Service Worker API 是否可用。
  2. 如果浏览器支持,在页面 onload 的时候注册位于 /sw.js 的 Service Worker。
  3. 每次页面加载成功后,就会调用 register() 方法,浏览器将会判断 Service Worker 线程是否已注册并做出相应的处理。
  4. register() 方法的 scope 参数是可选的,用于指定你想让 Service Worker 控制的内容的子目录,关于 register() 方法的 scope 参数,需要说明一下:
    Service Worker 线程将接收 scope 指定网域目录上所有事项的 fetch 事件,如果我们的 Service Worker 的 javaScript 文件在 /a/b/sw.js,不传 scope 值的情况下,scope 的值就是 /a/b,scope 的值的意义在于,如果 scope 的值为 /a/b,那么 Service Worker 线程只能捕获到 path 为 /a/b 开头的(/a/b/page1,/a/b/page2,...)页面的 fetch 事件,通过 scope 的意义我们也能看出 Service Worker 不是服务单个页面的,所以在 Service Worker 的 js 逻辑中全局变量需要慎用。
  5. then() 函数链式调用我们的 Promise,当 Promise resolve 的时候,里面的代码就会执行。
  6. 最后面我们链了一个 catch() 函数,当 Promise rejected 的时候执行。

查看 Service Worker 是否注册成功

Chrome 地址栏中输入 chrome://serviceworker-internals 可以查看 Service Worker 详情。

注册失败的原因

  • 不是 HTTPS 环境,不是 localhost 或 127.0.0.1;
  • Service Worker 文件的地址没有写对,需要相对于 origin;
  • Service Worker 文件在不同的 origin 下。

Service Worker 的生命周期

生命周期分为这几个状态:installing、installed、activating、activated、redundant。

  1. installing(安装中):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存,
    install 事件回调中有两个方法:

    • event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
    • self.skipWaiting():self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。
  2. installed(安装后):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭;

  3. activating(激活中):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联的旧缓存资源,等待新的 Service Worker 线程被激活,activate 回调中有两个方法:

    • event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
    • self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存,旧的 Service Worker 脚本不再控制着页面,之后会被停止。
  4. activated(激活后):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会),并可以处理功能性的事件 fetch (请求)、sync (后台同步)、push (推送)。

  5. redundant(废弃):这个状态表示一个 Service Worker 的生命周期结束,这里特别说明一下,进入 redundant(废弃) 状态的原因可能为这几种:

    • 安装 (install) 失败。
    • 激活 (activating) 失败。
    • 新版本的 Service Worker 替换了它并成为激活状态。

Service Worker 支持的事件

  1. install:Service Worker 安装成功后被触发的事件,在事件处理函数中可以添加需要缓存的文件。
  2. activate:当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件,通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等。
  3. message:Service Worker 运行于独立 context 中,无法直接访问当前页面主线程的 DOM 等信息,但是通过 postMessage API,可以实现他们之间的消息传递,这样主线程就可以接受 Service Worker 的指令操作 DOM。
  4. Service Worker 有几个重要的功能性的的事件,这些功能性的事件支撑和实现了 Service Worker 的特性。
  5. fetch (请求):当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并得到传有 response 参数的回调函数,回调中就可以做各种代理缓存的事情了。
  6. push (推送):push 事件是为推送准备的,首先需要了解一下 Notification APIPUSH API,通过 PUSH API,当订阅了推送服务后,可以使用推送方式唤醒 Service Worker 以响应来自系统消息传递服务的消息,即使用户已经关闭了页面。
  7. sync (后台同步):sync 事件由 background sync (后台同步)发出,background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个可以实现注册和监听同步处理的方法,但它还不在 W3C Web API 标准中。

安装 Service Worker

const VERSION = '180315-01';
const OFFLINE_CACHE = 's-offline-' + VERSION;
// 需要缓存的离线页面
const FILES_TO_CACHE = [
  '/',
  '/index.html',
  '/offline.html',
  '/manifest.json',
  '/css/style.css',
  '/fancybox/jquery.fancybox.css',
  'https://cdn.bootcss.com/jquery/2.0.3/jquery.min.js',
  '/fancybox/jquery.fancybox.pack.js',
  '/js/script.js',
  '/images/favicon.ico',
  '/images/avatar.jpg',
  '/images/offline-image.png',
  '/css/images/banner.jpg',
  '/css/fonts/fontawesome-webfont.woff'
];

self.addEventListener('install', function(event) {
  console.log('[SW]:', '开始安装 Service Worker');

  event.waitUntil(
    caches
      .open(OFFLINE_CACHE)
      .then(cache => cache.addAll(FILES_TO_CACHE))
      .then(() =>
        console.log('[SW]:', '离线资源缓存完毕,当前版本:', OFFLINE_CACHE)
      )
      .then(() => self.skipWaiting())
  );
});

说明:

  1. self 是 Service Worker 线程内的顶级对象,类似于浏览器内的 window 对象。
  2. 首先新增一个 install 事件监听器。
  3. 使用 caches.open() 方法可以打开一个缓存,调用后该方法会返回了一个 Promise,当它 resolved 的时候(执行 then())就可以调用缓存实例上的 addAll() 方法,addAll() 方法接收一个相对于 origin 的 URL 组成的数组,这些 URL 就是想缓存的资源列表。
  4. 如果 Promise 被 rejected,安装就会失败,在下次注册时会再次尝试缓存。
  5. 在 install 事件中执行 self.skipWaiting() 方法可以跳过 waiting 状态,然后直接进入 activate 阶段,和 self.clients.claim() 一起使用可以确保更新 Service Worker 时立即生效。
  6. 当 install 完成之后,就会激活 Service Worker。

自定义请求响应(fetch)

任何被 Service Worker 控制的资源被请求到时,都会触发 fetch 事件,这些资源包括指定 scope 内的 html 文档,和这些 html 文档内引用的任何资源(比如 index.html 发起了一个跨域的请求来嵌入一个图片,这个也会通过 Service Worker)。

// 缓存版本
const CACHE_VERSION = 's-data-v1';

self.addEventListener('fetch', function(event) {
  const request = event.request;

  event.respondWith(
    caches.match(request).then(response => {
      // 如果匹配到缓存,就直接返回,减少一次 HTTP 请求
      if (response) {
        console.log('[SW]:', '读取缓存', request.method, request.url);
        return response;
      }

      // 如果未匹配到缓存,就直接发起网络请求
      const requestClone = request.clone(); // 拷贝原始请求
      return fetch(requestClone).then(httpResponse => {
        const responseClone = httpResponse.clone();
        caches.open(CACHE_VERSION).then(cache => {
          cache.put(request, responseClone);
          console.log('[SW]:', '写入缓存', request.method, request.url);
        });
        return httpResponse;
      });
    })
  );
});

说明:

  1. 在 install 事件中缓存静态资源,在 fetch 事件中处理回调来代理页面请求从而实现动态地资源缓存;install 和 fetch 缓存的区别:

    • install 时缓存的优点是第二次访问即可离线,缺点是需要将需要缓存的 URL 在编译时插入到脚本中,增加代码量和降低可维护性。
    • fetch 时缓存的优点是无需更改编译过程,也不会产生额外的流量,缺点是需要多一次访问才能离线可用。
  2. 接着调用 event 上的 respondWith() 方法来劫持 HTTP 响应。

  3. caches.match() 方法将网络请求的资源和 cache 里可获取的资源进行匹配,查看缓存中是否有相应的资源。

  4. 由于请求/响应流只能被读取一次,为了给浏览器返回响应以及把它缓存起来,需要将请求/响应克隆一份。

  5. fetch() 方法必须接受一个参数:资源的路径;无论请求成功与否,它都返回一个 Promise 对象,resolve 时(then())返回对应请求的 Response。

  6. 使用 caches.open() 方法打开一个缓存,然后调用 cache.put() 方法将克隆的响应存储到缓存中。

  7. 最后将原始的响应返回给浏览器(return httpResponse;)。

更新 Service Worker

// 缓存版本
const CACHE_VERSION = 's-data-v2';

self.addEventListener('activate', function(event) {
  console.log('[SW]:', '激活 Service Worker');

  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys
          .filter(key => key !== CACHE_VERSION) // 过滤出来需要删除的资源
          .map(key => {
            console.log('[SW]:', '移除过时缓存:', key);
            caches.delete(key);
          }) // 删除旧版本资源,caches.delete() 返回 Promise 对象
      )
    )
  );

  return self.clients.claim();
});

说明:

  1. 更改 CACHE_VERSION 变量的值,当安装发生的时候,前一个版本依然在响应请求,新的版本正在后台安装,由于调用了一个新的缓存 s-data-v2,所以前一个 s-data-v1 版本的缓存依然存在。
  2. 传给 waitUntil() 的 Promise 会阻塞其他的事件,直到它完成,所以可以确保清理操作(caches.delete())会在此次 fetch 事件之前完成。
  3. 使用 Promise.all() 保证所有过期的缓存被删除。
  4. 使用 Array.filter() 过滤出来需要删除的资源
  5. 最后调用 caches.delete() 方法删除过期的缓存。

参考

相关工具

【JS】惰性计算

求值策略

在计算机科学中,求值策略(Evaluation strategy)是确定编程语言中表达式的求值的一组(通常确定性的)规则。重点典型的位于函数或算子上——求值策略定义何时和以何种次序求值给函数的实际参数,什么时候把它们代换入函数,和代换以何种形式发生。 -- 维基百科

严格求值(Strict evaluation)

严格求值下,传给函数的实际参数总是在调用这个函数之前被求值。

多数现存编程语言对函数使用严格求值。

非严格求值 / 惰性求值(Non-strict evaluation)

非严格求值下,传给函数的实际参数并不会立即求值,是否需要求值依赖于这个实际参数在函数执行中有没有被使用。

JS 中的惰性计算

缓存

注意:对 纯函数 缓存才有意义

memoize.js

const memoize = function(func) {
  if (typeof func != 'function') {
    throw new TypeError('Expected a function');
  }

  const cache = new Map();
  const memorized = function(...args) {
    const key = args.join(',') || 'default';

    if (!cache.has(key)) {
      cache.set(key, func.apply(this, args));
    }

    return cache.get(key);
  };

  memorized.isMemorized = true;

  return memorized;
};

示例 1:

const func = function() {
  // some expensive operation
  console.log('func is executed!');
  return 'func';
};

const memorizedFunc = memoize(func);

console.log(memorizedFunc());
console.log(memorizedFunc());
console.log(memorizedFunc());

// console:

// func is executed!
// func
// func
// func

func 只会执行一次

示例 2(斐波那契函数):

let counter = 0;

let fib = function(n) {
  counter++;
  switch (n) {
    case 0:
      return 0;
    case 1:
      return 1;
    default:
      return fib(n - 1) + fib(n - 2);
  }
};

console.log('fib 20:', fib(20));
console.log('counter:', counter, '\n');

counter = 0; // 重置 counter
fib = memoize(fib); // 重写 fib

console.log('memorizedFib 20:', fib(20));
console.log('counter:', counter);

// console:

// fib 20: 6765
// counter: 21891

// memorizedFib 20: 6765
// counter: 21

斐波那契函数时间复杂度为 O(n) = 2^n,性能极差。

缓存后的 fib 函数只会执行 n 次(0 ~ n)函数体。

惰性数组

问题:

// (1) 商品名转为大写
// (2) 取出三个价格低于 10 的商品

const gems = [
  { name: 'Sunstone', price: 4 },
  { name: 'Amethyst', price: 15 },
  { name: 'Prehnite', price: 20 },
  { name: 'Sugilite', price: 7 },
  { name: 'Diopside', price: 3 },
  { name: 'Feldspar', price: 13 },
  { name: 'Dioptase', price: 2 },
  { name: 'Sapphire', price: 20 }
];

解法 1(函数式迭代)

let filterCounter = 0;
let transformCounter = 0;

const filter = item => {
  filterCounter++;
  console.log('filter is run.');
  return item.price < 10;
};
const transform = item => {
  transformCounter++;
  console.log('transform is run.');
  return {
    name: item.name.toUpperCase(),
    price: item.price
  };
};

const result = gems
  .filter(filter)
  .map(transform)
  .slice(0, 3);

console.log('filterCounter:', filterCounter);
console.log('transformCounter:', transformCounter);
console.log(result);

// console:

// filter is run.
// filter is run.
// filter is run.
// filter is run.
// filter is run.
// filter is run.
// filter is run.
// filter is run.
// transform is run.
// transform is run.
// transform is run.
// transform is run.
// filterCounter: 8
// transformCounter: 4
// [
//   { name: 'SUNSTONE', price: 4 },
//   { name: 'SUGILITE', price: 7 },
//   { name: 'DIOPSIDE', price: 3 }
// ]

图示:

normal

解法 2(常规方式)

let filterCounter = 0;
let transformCounter = 0;

const filter = item => {
  filterCounter++;
  console.log('filter is run.');
  return item.price < 10;
};
const transform = item => {
  transformCounter++;
  console.log('transform is run.');
  return {
    name: item.name.toUpperCase(),
    price: item.price
  };
};

const result = [];

for (let i = 0, len = gems.length; i < len; i++) {
  const item = gems[i];
  if (filter(item)) {
    result.push(transform(item));
  }
  if (result.length > 2) {
    break;
  }
}

console.log('filterCounter:', filterCounter);
console.log('transformCounter:', transformCounter);
console.log(result);

// console:

// filter is run.
// transform is run.
// filter is run.
// filter is run.
// filter is run.
// transform is run.
// filter is run.
// transform is run.
// filterCounter: 5
// transformCounter: 3
// [
//   { name: 'SUNSTONE', price: 4 },
//   { name: 'SUGILITE', price: 7 },
//   { name: 'DIOPSIDE', price: 3 }
// ]

图示:

normal

解法 3(Lodash、Lazy.js)

let filterCounter = 0;
let transformCounter = 0;

const filter = item => {
  filterCounter++;
  console.log('filter is run.');
  return item.price < 10;
};
const transform = (item, index, array) => {
  transformCounter++;
  console.log('transform is run.');
  // console.log(item, index);
  return {
    name: item.name.toUpperCase(),
    price: item.price
  };
};

let result;

result = _
  .chain(gems)
  .filter(filter)
  .map(transform)
  .take(3)
  .value();

console.log('filterCounter:', filterCounter);
console.log('transformCounter:', transformCounter);
console.log(result);

// console:

// filter is run.
// transform is run.
// filter is run.
// filter is run.
// filter is run.
// transform is run.
// filter is run.
// transform is run.
// filterCounter: 5
// transformCounter: 3
// [
//   { name: 'SUNSTONE', price: 4 },
//   { name: 'SUGILITE', price: 7 },
//   { name: 'DIOPSIDE', price: 3 }
// ]

TODO: 实现一个简易的 Lazy.js

短路求值(Short-circuit evaluation)*

《JavaScript 面向对象编程指南》2.3.4.3

const func = function() {
  console.log('func is executed!');
  return 'some value form func.';
};

console.log('a1: start');
const a1 = true || func(); // func 未执行
console.log('a1: end\n');

console.log('a2: start');
const a2 = false || func(); // func 执行了
console.log('a2: end\n');

console.log('b1: start');
const b1 = true && func(); // func 执行了
console.log('b1: end\n');

console.log('b2: start');
const b2 = false && func(); // func 未执行
console.log('b2: end\n');

// console:

// a1: start
// a1: end

// a2: start
// func is executed!
// a2: end

// b1: start
// func is executed!
// b1: end

// b2: start
// b2: end

ES6 中的默认参数 *

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。 -- ECMAScript 6 入门

// 默认参数
// 参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let counter = 0;

const func = function() {
  console.log('func is executed!');
  counter++;
  return 10;
};

const a = function(val = func()) {
  console.log('before use "val"');
  console.log('val:', val);
};

console.log('第 1 次执行');
a();
console.log('counter:', counter);
console.log('第 1 次结束\n');

console.log('第 2 次执行');
a();
console.log('counter:', counter);
console.log('第 2 次结束\n');

console.log('第 3 次执行');
a(20);
console.log('counter:', counter);
console.log('第 3 次结束\n');

// console:

// 第 1 次执行
// func is executed!
// before use "val"
// val: 10
// counter: 1
// 第 1 次结束

// 第 2 次执行
// func is executed!
// before use "val"
// val: 10
// counter: 2
// 第 2 次结束

// 第 3 次执行
// before use "val"
// val: 20
// counter: 2
// 第 3 次结束

从第 1 次调用 a 方法可以看出:

  • 默认参数 func 并不会在 a 方法定义时执行
  • 默认参数 funca 方法体执行前调用,而不是实际参数被使用时调用

从第 1、2 次调用 a 方法可以看出(观察 counter 值):

  • 每次 a 方法被调用时,默认参数 func 都会重新执行,即:每次都重新计算默认值表达式的值,属于 Call By Name 求值策略

从第 2、3 次调用 a 方法可以看出(观察 counter 值):

  • a 方法被传入参数后,默认参数 func 不会执行

延时加载 *

《高性能 JavaScript》 第 8 章 - 避免重复工作

初版

const addHandler = function(target, eventType, handler) {
  document.body.addEventListener
    ? // DOM2 Events
      target.addEventListener(eventType, handler, false)
    : // IE
      target.addEventListener('on' + eventType, handler);
};

const removeHandler = function(target, eventType, handler) {
  document.body.removeEventListener
    ? // DOM2 Events
      target.removeEventListener(eventType, handler, false)
    : // IE
      target.detachEvent('on' + eventType, handler);
};

每次调用时都会判断事件类型,然后再执行 add / remove 事件方法

优化版(条件预加载)

const addHandler = document.body.addEventListener
  ? function(target, eventType, handler) {
      // DOM2 Events
      target.addEventListener(eventType, handler, false);
    }
  : function(target, eventType, handler) {
      // IE
      target.attachEvent('on' + eventType, handler);
    };

const removeHandler = document.body.removeEventListener
  ? function(target, eventType, handler) {
      // DOM2 Events
      target.removeEventListener(eventType, handler, false);
    }
  : function(target, eventType, handler) {
      // IE
      target.detachEvent('on' + eventType, handler);
    };

脚本加载时会进行条件检测,而不是加载后

优化版(延时加载)

function addHandler(target, eventType, handler) {
  if (target.addEventListener) {
    // DOM2 Events
    addHandler = function(target, eventType, handler) {
      target.addEventListener(eventType, handler, false);
    };
  } else {
    // IE
    addHandler = function(target, eventType, handler) {
      target.attachEvent('on' + eventType, handler);
    };
  }

  addHandler(target, eventType, handler);
}

function removeHandler(target, eventType, handler) {
  if (target.addEventListener) {
    // DOM2 Events
    removeHandler = function(target, eventType, handler) {
      target.removeEventListener(eventType, handler, false);
    };
  } else {
    // IE
    removeHandler = function(target, eventType, handler) {
      target.detachEvent('on' + eventType, handler);
    };
  }

  addHandler(target, eventType, handler);
}

调用延时加载函数时,第一次总会消耗较长的时间

参考

相关笔记:

【CSS】BFC & IFC

目录

BFC

Block Formatting Context(块格式化上下文)W3C CSS 2.1 规范中的一个概念,在 CSS3 中被修改为 flow root
格式化表明了在这个环境中,元素处于此环境中应当被初始化。元素如果创建了 BFC,则元素被 BFC 的特性约束。也就是 BFC 提供了一个环境,HTML 元素在这个环境中按照一定的渲染规则进行布局。一个环境中的元素不会影响到其他环境中的布局。

创建 BFC

  1. 根元素或包含根元素的元素
  2. overflow 值不为 visible 的块元素
  3. 浮动元素(元素的 float 不是 none
  4. 绝对定位元素(元素的 positionabsolutefixed
  5. 行内块元素(元素的 displayinline-block
  6. 弹性元素(displayflexinline-flex 元素的直接子元素)
  7. 网格元素(displaygridinline-grid 元素的直接子元素)
  8. 多列容器(元素的 column-countcolumn-width 不为 auto,包括 column-count 为 1)column-spanall 的元素始终会创建一个新的 BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)
  9. 表格元素
    • 表格单元格(元素的 displaytable-cell,HTML 表格单元格默认为该值)
    • 表格标题(元素的 displaytable-caption,HTML 表格标题默认为该值)
    • 匿名表格单元格元素(元素的 displaytabletable-rowtable-row-grouptable-header-grouptable-footer-group(分别是 HTML table、row、tbody、thead、tfoot 的默认属性)或 inline-table
  10. display 值为 flow-root 的元素
  11. contain 值为 layoutcontentstrict 的元素

BFC 的规则

  1. BFC 环境内的盒子会在垂直方向,一个接一个地放置
  2. 盒子垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻盒子的 margin 会发生重叠
  3. 每个盒子的左边界都要紧靠包含容器的左边界
  4. BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素
  5. 计算 BFC 的高度时,浮动元素也参与计算

兼容 IE

IE 浏览器中不支持 BFC 标准,但 IE 中有 LayoutLayoutBFC 基本等价,为了处理 IE 的兼容性,在需要触发 BFC 时,我们除了需要用上面的 CSS 属性来触发 BFC,还需要针对 IE 浏览器使用 zoom: 1; 来触发 IE 浏览器的 Layout

IFC

当块容器盒(block container box)不包括任何块级盒(block-level boxes)时,就会创建一个行内格式化上下文(IFC)。

IFC 的规则

  1. 盒子一个接一个的在水平方向摆放,当容器宽度不够时就会换行
  2. 每一行将生成一个匿名行盒(line box),包括该行的所有行内级盒
  3. 水平方向上,当所有盒的总宽度小于匿名行盒的宽度时,那么水平方向排版由 text-align 属性来决定
  4. 垂直方向上,行内级盒的对齐方式由 vertical-align 控制,默认对齐为 baseline
  5. 行盒的高度由内部子元素中实际高度最高的盒子计算出来,值得注意的是,行内盒(inline boxes)的垂直的 border,padding 与 margin 都不会撑开行盒的高度

注:在 IFC 的环境中,是不能存在块级元素的,如果将块级元素插入到 IFC 中,那么此 IFC 将会被破坏掉变成 BFC,而块级元素前的元素或文本和块级元素后的元素或文本将会各自自动产生一个匿名块盒其包围。

参考

【JavaScript 设计模式与开发实践】设计原则

单一职责原则(SRP)

就一个类而言,应该仅有一个引起它变化的原因。在 JavaScript 中,单一职责原则更多地是被运用在对象或者方法级别上。

单一职责原则体现为: 一个对象(方法)只做一件事情。

要明确的是,并不是所有的职责都应该一一分离。一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。

单一职责原则的优缺点

优点: 降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。

缺点: 最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。

最少知识原则

最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。

解释:

某军队中的将军需要挖掘一些散兵坑。下面是完成任务的一种方式:将军可以通知上校让他叫来少校,然后让少校找来上尉,并让上尉通知一个军士,最后军士唤来一个士兵,然后命令士兵挖掘一些散兵坑。

等价代码:

general
  .getColonel(c)
  .getMajor(m)
  .getCaptain(c)
  .getSergeant(s)
  .getPrivate(p)
  .digFoxhole();

让代码通过这么长的消息链才能完成一个任务,这就像让将军通过那么多繁琐的步骤才能命令别人挖掘散兵坑一样荒谬!而且,这条链中任何一个对象的改动都会影响整条链的结果。

最少知识原则在设计模式中体现得最多的地方是 中介者模式外观模式

外观模式

外观模式容易跟普通的封装实现混淆。这两者都封装了一些事物,但外观模式的关键是定义一个高层接口去封装一组“子系统”。

外观模式主要是为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使子系统更加容易使用。大多数客户都可以通过请求外观接口来达到访问子系统的目的。但在一段使用了外观模式的程序中,请求外观并不是强制的。如果外观不能满足客户的个性化需求,那么客户也可以选择越过外观来直接访问子系统。

封装在很大程度上表达的是数据的隐藏。把变量的可见性限制在一个尽可能小的范围内,这个变量对其他不相关模块的影响就越小,变量被改写和发生冲突的机会也越小。这也是广义的最少知识原则的一种体现。

最少知识原则也叫迪米特法则(Law of Demeter,LoD)

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。

现在可以引出开放-封闭原则的**:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

过多的条件分支语句是造成程序违反开放-封闭原则的一个常见原因。每当需要增加一个新 的 if 语句时,都要被迫改动原函数。把 if 换成 switch-case 是没有用的,这是一种换汤不换药的做法。实际上,每当我们看到一大片的 if 或者 switch-case 语句时,第一时间就应该考虑,能否利用对象的多态性来重构它们。

【Linux】服务器配置(Nginx,Node,PM2,HTTPS,HTTP2)

环境:腾讯云服务器,64 位 CentOS-7,root 用户

连接服务器

通过 Xshell 连接服务器

1. 安装

2. 新建 SSH 连接

新建 SSH 连接

绑定服务器主机 IP

输入完成后点击 "确定";

输入用户名

输入密码

点击确定连接。

参考:

  1. 腾讯云服务器部署 Node.js 应用

打开多个 Shell 界面

鼠标移到已经连接的服务器 标签(标红处)上,点击右键,选择 复制 SSH 渠道

Xshell 复制 SSH 渠道

更新系统和软件包

yum clean all
yum update
yum upgrade

安装必备软件

配置编译环境

# 安装 make
yum -y install gcc automake autoconf libtool make

# 安装 g++
yum -y install gcc gcc-c++

安装 lrzsz(Xshell 环境下用于文件上传和下载)

yum -y install lrzsz

选择源码安装目录

可以是任何目录,我选择的是 /usr/local/src

cd /usr/local/src

更新/安装 OpenSSL

# 查看已安装的 SSL 版本
openssl version

# 如果显示 openssl 版本低于 1.02,则需要更新(为了后续开启 http2)
# 切换到源码安装目录
cd /usr/local/src

# 下载源码包
wget https://www.openssl.org/source/openssl-1.0.2l.tar.gz

# 解压
tar -zxvf openssl-1.0.2l.tar.gz

# 切换到源码目录内
cd openssl-1.0.2l

# 添加配置
./config --prefix=/usr/local/openssl

# 安装
make && make install

# 保存老版本的 openssl
mv /usr/bin/openssl /usr/bin/openssl.OFF
mv /usr/include/openssl /usr/include/openssl.OFF

# 创建软链
ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl
ln -s /usr/local/openssl/include/openssl /usr/include/openssl
echo "/usr/local/openssl/lib">>/etc/ld.so.conf

# 检查是否安装成功
openssl version

参考:

  1. CentOS 之——升级 openssl 为最新版

安装 Nginx

# 切换到源码安装目录(如果更换其他目录,下面引用的路径同样需要修改)
cd /usr/local/src

# 下载源码包
wget http://nginx.org/download/nginx-1.12.0.tar.gz
wget https://www.openssl.org/source/openssl-1.0.2l.tar.gz
wget http://zlib.net/zlib-1.2.11.tar.gz
wget https://ftp.pcre.org/pub/pcre/pcre-8.35.tar.gz

# 解压
tar -zxvf nginx-1.12.0.tar.gz
tar -zxvf openssl-1.0.2l.tar.gz
tar -zxvf pcre-8.35.tar.gz
tar -zxvf zlib-1.2.11.tar.gz

# 切换目录
cd nginx-1.12.0

# 创建一个 nginx 目录用来存放运行的临时文件夹
mkdir -p /var/cache/nginx

# 添加配置
# --prefix=/usr/local/nginx --> 指定安装路径
# --conf-path=/etc/nginx/nginx.conf --> 指定配置文件的位置
# --with-http_ssl_module --> 启用 SSL 模块
# --with-http_v2_module --> 启用 http2 模块

# 转义符:"\" 可以实现在多行输入一句命令
./configure --prefix=/usr/local/nginx \
            --sbin-path=/usr/sbin/nginx \
            --conf-path=/etc/nginx/nginx.conf \
            --error-log-path=/var/log/nginx/error.log \
            --http-log-path=/var/log/nginx/access.log \
            --pid-path=/var/run/nginx.pid \
            --lock-path=/var/run/nginx.lock \
            --http-client-body-temp-path=/var/cache/nginx/client_temp \
            --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
            --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
            --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
            --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
            --user=nobody \
            --group=nobody \
            --with-openssl=/usr/local/src/openssl-1.0.2l \
            --with-pcre=/usr/local/src/pcre-8.35 \
            --with-zlib=/usr/local/src/zlib-1.2.11 \
            --with-http_ssl_module \
            --with-http_v2_module \
            --with-http_realip_module \
            --with-http_addition_module \
            --with-http_sub_module \
            --with-http_dav_module \
            --with-http_flv_module \
            --with-http_mp4_module \
            --with-http_gunzip_module \
            --with-http_gzip_static_module \
            --with-http_random_index_module \
            --with-http_secure_link_module \
            --with-http_stub_status_module \
            --with-http_auth_request_module \
            --with-mail \
            --with-mail_ssl_module \
            --with-file-aio \
            --with-ipv6 \
            --with-http_v2_module \
            --with-threads \
            --with-stream \
            --with-stream_ssl_module \
            --with-threads \
            --with-debug

# 安装
make && make install

# 重新启动 Xshell,或者重启 Shell 窗口 !!!
# 检查是否安装成功
nginx -V

# 配置 systemctl 服务
vim /usr/lib/systemd/system/nginx.service

# 复制代码
# 按 i 输入以下内容

[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -c /etc/nginx/nginx.conf
ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

# 按 Esc 推出 vim 编辑模式,输入 :wq 然后点击 Enter,保存并退出

# 重新启动 Xshell,或者重启 Shell 窗口 !!!
# 重启 nginx
systemctl start nginx.service

# 开机自启 nginx
systemctl enable nginx.service

参考:

  1. nginx 支持 HTTP2 的配置过程
  2. nginx 的安装及配置
  3. Nginx 网站服务器学习与入门
  4. nginx 如何启用对 HTTP2 的支持
  5. CentOS 7 中 Nginx1.9.5 编译安装教程 systemctl 启动

安装 Git

# 安装 Git
yum install git

# 查看 Git 版本
git --version

安装 Node 和 PM2

# 安装 Node.js
yum install nodejs

# 查看 Node 版本
node -v
# 查看 NPM 版本
npm -v

# 全局安装 PM2
npm i -g pm2

搭建 Blog

部署 Node 服务器环境

# 我将博客放在了 /home 挂载点内
# 将 Node 静态服务模板克隆到 /home/blog 目录下
git clone https://github.com/no-nothing/server.git /home/blog

# 切换到 blog 目录内
cd /home/blog

# 安装依赖
npm i

部署 Blog 页面

# 克隆自己的 Blog 页面到 /home/blog/public 目录下
git clone https://github.com/no-nothing/no-nothing.github.io.git /home/blog/public

或者上传本地的 Blog 页面:

先将本地的网页压缩成 zip 包(Centos 默认支持解压 zip 文件)

# 切换到 blog 目录内
cd /home/blog

# 上传
rz -y

# 解压缩包到 /home/blog/public 目录下
unzip 压缩包名.zip -d /home/blog/public

启用 Node 服务

# 切换到 blog 目录内
cd /home/blog

# 以 blog 为 PM2 进程名称
pm2 start app.js --name blog

# 设置 pm2 启动的服务开机自启
pm2 save
pm2 startup

现在可以通过 IP 地址:3000 访问刚才部署的 Blog 了。

配置 Nginx(同时开启 https)

需要先完成域名解析

获取证书

  1. 打开腾讯云-SSL 证书管理,点击 申请证书,按提示一步步操作;
  2. 下载证书;
  3. 解压下载好的压缩包,选择符合自己服务环境的证书;

将证书上传至服务器

# 我是通过 yum install nginx 安装的 nginx,所以 nginx 根目录在 /etc/nginx
cd /etc/nginx

# 创建 ssl 文件夹存放证书
mkdir ssl

cd ssl

# 通过 lrzsz (Xshell环境下用于文件上传和下载)上传证书
rz
# 选择 *.crt 和 *.key 两个文件

修改 nginx 配置文件

1. 从服务器取出 nginx.conf 文件

cd /etc/nginx

sz nginx.conf

2. 修改 nginx.conf 文件

...

# 设定实际的服务器列表
upstream blog {
    server 127.0.0.1:3000;
}

# http 请求重定向到 https
server {
    listen       80;
    server_name  singple.com www.singple.com; #换成你的域名
    return 301   https://www.singple.com$request_uri; #这里的 www.singple.com 换成你的域名或者 $server_name
}

# https 配置
server {
    listen 443 ssl http2 default_server;
    server_name  www.singple.com; #换成你的域名

    ssl                        on;
    ssl_certificate            ssl/1_www.singple.com_bundle.crt; #证书文件
    ssl_certificate_key        ssl/2_www.singple.com.key; #秘钥文件
    ssl_session_timeout        5m;
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers  on;

    location / {
        proxy_pass   http://blog;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

# 其他的 URL 请求返回 501 (可选配置)
server {
    listen       80 default;
    return       501;
}

...

补充:

完整的 nginx.conf 文件

3. 上传修改后的 nginx.conf,并重新加载 nginx 配置

# 修改 nginx.conf 后,上传
rz -y

# 重启 nginx
nginx -s reload

# 开机自启 nginx
systemctl enable nginx.service

4. 如果无法访问

# 启用https服务
firewall-cmd --permanent --zone=public --add-service=https

# 开启 443 端口(https)
firewall-cmd --zone=public --add-port=443/tcp --permanent

# 重新加载防火墙,使新添的规则生效
firewall-cmd --reload

参考:

  1. 个人网站如何开启 HTTPS?
  2. Redirect all HTTP requests to HTTPS with Nginx

开启防火墙

安装

# 查看版本
firewall-cmd --version

# 若提示 command not found,安装 firewalld
yum install firewalld firewall-config

开启/关闭指定端口

# 启动 firewalld
systemctl start firewalld.service

# 获取 firewalld 状态
firewall-cmd --state

# 开机自启 firewalld
systemctl enable firewalld.service

# firewalld 默认会关闭所有端口访问

# 开启 80 端口(此时只有 80 端口可以访问)
# --permanent 永久
firewall-cmd --zone=public --add-port=80/tcp --permanent

# 启用https服务
firewall-cmd --zone=public --add-service=https --permanent

# 开启 443 端口(https)
firewall-cmd --zone=public --add-port=443/tcp --permanent

# 重新加载防火墙,生效新添的规则
firewall-cmd --reload

# 查看 public 下开启的端口列表
firewall-cmd --zone=public --list-ports

附录:

Systemd 常用命令:

# 立即激活单元
systemctl start <单元>

# 立即停止单元:
systemctl stop <单元>

# 重启单元:
systemctl restart <单元>

# 重新加载配置:
systemctl reload <单元>

# 输出单元运行状态:
systemctl status <单元>

# 检查单元是否配置为自动启动:
systemctl is-enabled <单元>

# 开机自动激活单元:
systemctl enable <单元>

# 取消开机自动激活单元:
systemctl disable <单元>

参考:

  1. systemd-ArchWiki
  2. Systemd 入门教程:命令篇

Nginx 常用命令:

# nginx 启动
# 其中参数 -c 指定 nginx 启动时加载的配置文件,当然也可以不指定配置文件,省略 -c,也可以启动,表示使用默认的配置文件
nginx -c /etc/nginx/nginx.conf
# OR
nginx

# nginx 停止
# 例如在我们的编辑环境中已经安装好了 nginx,并且已启动,在命令提示符下直接输入 nginx -s stop 就可以停止了
nginx -s stop
# OR
nginx -s quit
# OR
pkill -9 nginx

# nginx 重载配置
nginx -s reload

# 检查配置文件是否正确
nginx -t

参考:

  1. systemd-ArchWiki
  2. Systemd 入门教程:命令篇

firewalld 常用命令:

# 查看版本
firewall-cmd --version

# 获取 firewalld 状态
firewall-cmd --state

# 查看开启的端口列表
firewall-cmd --list-ports

# 查看 public 下开启的端口列表
firewall-cmd --zone=public --list-ports

# 启动 firewalld
systemctl start firewalld.service

# 重启 firewalld
systemctl restart firewalld.service

# 开机自启 firewalld
systemctl enable firewalld.service

# 关闭开机自启 firewalld
systemctl disable firewalld.service

# firewalld 默认会关闭所有端口访问

# 开启 80 端口(此时只有 80 端口可以访问)
# --permanent 永久
firewall-cmd --zone=public --add-port=80/tcp --permanent

# 启用https服务
firewall-cmd --permanent --zone=public --add-service=https

# 开启 443 端口(https)
firewall-cmd --zone=public --add-port=443/tcp --permanent

# 重新加载防火墙,生效新添的规则
firewall-cmd --reload

参考:

  1. centos7 firewall 防火墙 命令

PM2 常用命令:

# 1. 启动
pm2 start app.js
# 以 process-name 为 PM2 进程名称
pm2 start app.js --name process-name
# 根据 CPU 核数启动进程个数
pm2 start app.js -i 0
# 实时监控 app.js 的方式启动,当 app.js 文件有变动时,PM2 会自动reload
pm2 start app.js --watch

# 2. 查看进程
pm2 list
# 查看进程详细信息,0 为 PM2 进程 id
pm2 show 0
# 或者
pm2 info 0

# 3. 监控
pm2 monit

# 4. 停止
# 停止 PM2 列表中所有的进程
pm2 stop all
# 停止 PM2 列表中进程为 0 的进程
pm2 stop 0

# 5. 重载
# 重载 PM2 列表中所有的进程
pm2 reload all
# 重载 PM2 列表中进程为 0 的进程
pm2 reload 0

# 6. 重启
# 重启 PM2 列表中所有的进程
pm2 restart all
# 重启 PM2 列表中进程为 0 的进程
pm2 restart 0

# 7. 删除 PM2 进程
# 删除 PM2 列表中所有的进程
pm2 delete all
# 删除 PM2 列表中进程为 0 的进程
pm2 delete 0

# 8. 日志操作
# 显示所有进程日志
pm2 logs
# 清除所有日志
pm2 flush
# 重载所有日志
pm2 reloadLogs

# 9. 升级 PM2
# 安装最新的 PM2 版本
npm install pm2@lastest -g
# 升级 PM2
pm2 updatePM2

# 10. 更多命令参数请查看帮助
pm2 --help

参考:

  1. PM2 官网
  2. 腾讯云服务器部署 Node.js 应用

【JS】this

this

this 是和 执行上下文环境 息息相关的一个特殊对象。因此,它也可以称为 上下文对象[context object] (激活执行上下文的上下文)。

this 是执行上下文环境的一个属性,而不是某个变量对象的属性。

this 指向

this 的指向。除去不常用的 witheval的情况,具体到实际应用中,this 的指向大致可以分为以下 4 种:

  • 作为对象的方法调用
  • 作为普通函数调用
  • 构造器调用
  • callapplybind 调用

1. 作为对象的方法调用

当函数作为对象的方法被调用时,this 指向该对象

2. 作为普通函数调用

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指向全局对象。在浏览器的 JavaScript 里,这个全局对象是 window 对象。

在 ECMAScript 5 的 strict 模式下,这种情况下的 this 已经被规定为不会指向全局对象,而是 undefined

3. 构造器调用

当用 new 运算符调用函数时,该函数 总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象。

但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this

var MyClass = function() {
  this.name = 'sven';
  // 显式地返回一个对象
  return {
    name: 'anne',
  };
};
var obj = new MyClass();
alert(obj.name); // 输出: anne

如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题:

var MyClass = function() {
  this.name = 'sven';
  return 'anne'; // 返回 string 类型
};
var obj = new MyClass();
alert(obj.name); // 输出: sven

4. callapplybind 调用

当使用 call 或者 apply 的时候,如果我们传入的第一个参数为 null,函数体内的 this 会指向默认的宿主对象,在浏览器中则是 window;但如果是在严格模式下,函数体内的 this 还是为 null

有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null 来代替某个具体的对象:

Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5

实现一个 bind

简易版

Function.prototype.bind = function(context) {
  var self = this; // 保存原函数
  // 返回一个新的函数
  return function() {
    return self.apply(context, arguments); // 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this
  };
};

完整版

Function.prototype.bind = function() {
  var self = this, // 保存原函数
    context = [].shift.call(arguments),
    args = [].slice.call(arguments);
  // 返回一个新的函数
  // 需要绑定的 this 上下文
  // 剩余的参数转成数组
  return function() {
    return self.apply(context, [].concat.call(args, [].slice.call(arguments))); // 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this,并且组合两次分别传入的参数,作为新函数的参数
  };
};

var obj = {
  name: 'sven',
};
var func = function(a, b, c, d) {
  alert(this.name); // 输出: sven
  alert([a, b, c, d]); // 输出: [ 1, 2, 3, 4 ]
}.bind(obj, 1, 2);
func(3, 4);

【Tools】Yarn & NPM

安装 yarn

常用命令

npm yarn 功能
npm init yarn init 初始化项目
npm i yarn installyarn 安装依赖包
rm -rf node_modules && npm i yarn upgrade 重新安装依赖包
npm i [package] -S yarn add [package] 安装生产依赖包
npm i [package] -D yarn add [package] -D 安装开发依赖包
npm i [package] -g yarn global add [package] 全局安装包
npm update -g yarn global upgrade 全局更新包
npm uninstall [package] -S yarn remove [package] 移除生产依赖包
npm uninstall [package] -D yarn remove [package] 移除开发依赖包
npm cache clean yarn cache clean 清缓存
npm list -g --depth=0 yarn global list 查看全局安装的包
npm login/logout/publish yarn login/logout/publish 登录/登出/发布
npm config list yarn config list 查看全局配置
npm config get registry yarn config get registry 查看源(全局)
npm config set registry http://r.cnpmjs.org/ yarn config set registry https://registry.yarnpkg.com 设置源(全局)
npm help yarn help 帮助

常见问题

依赖包安装慢

# 更换淘宝源
npm config set registry https://registry.npm.taobao.org
yarn config set registry https://registry.npm.taobao.org

# 或者 npm 下使用 nrm
npm i nrm -g
# 查看源
nrm ls
# 测试源的延迟
nrm test
# 使用速度最快的源(这里演示使用 taobao)
nrm use taobao

# 或者 yarn 下使用 yrm
yarn global add yrm
# 查看源
yrm ls
# 测试源的延迟
yrm test
# 使用速度最快的源(这里演示使用 taobao)
yrm use taobao

安装 puppeteer node-sass phantomjs 失败

1. npm

项目根目录下增加 .npmrc 配置文件

puppeteer_download_host=https://npm.taobao.org/mirrors/
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/
chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver/

或者,修改 npm 全局配置

# 设置 puppeteer taobao源
npm config set puppeteer_download_host https://npm.taobao.org/mirrors/
# 设置 node-sass taobao源
npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
# 设置 phantomjs taobao源
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
# 查看 config 信息
npm config list

2. yarn

yarn 没有类似于 .npmrc 的配置文件,只能修改 yarn 全局配置

# 设置 puppeteer taobao源
yarn config set puppeteer_download_host https://npm.taobao.org/mirrors/
# 设置 node-sass taobao源
yarn config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
# 设置 phantomjs taobao源
yarn config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
# 查看 config 信息
yarn config list

使用 nvm 更改 node 版本后,全局依赖不可用

vim ~/.npmrc # 修改 node 版本

npm 技巧

使用相对路径安装 npm 包

cd path/to/my-project
npm install path/to/my-utils

调试本地模块 npm link

1. 模块目录,把模块 link 到全局

cd path/to/my-utils
npm link

2. 项目目录,把模块 link 到当前项目

cd path/to/my-project
npm link my-utils

3. 移除 link

npm unlink my-utils

参考

【JS】模块化

CommonJS 和 ES6 Module 区别

  • CommonJS 导出的是变量的一份拷贝; ES6 Module 导出的是变量的绑定(引用)
  • CommonJS 是单个值导出; ES6 Module 可以导出多个
  • CommonJS 是动态语法可以写在判断里; ES6 Module 静态语法只能写在顶层
  • CommonJSthis 是当前模块; ES6 Modulethisundefined

参考

【Linux】基础与常用命令

起步

命令行前的 [root@localhost ~]# 是什么

root:        当前登录用户
localhost:   主机名
~:           当前所在目录(家目录)
#:           超级用户的提示符(普通用户的提示符是$)

Linux 文件权限

-rw-r--r-- (其中, r: 读, w: 写, x: 执行)
共10位

第1位:     文件类型(共7种, 常用3种 -: 文件, d: 目录, l: 快捷方式)
第2~4位:   所有者(u)权限
第5~7位:   所属组(g)权限
第8~10位:  其他人(o)权限

Shell 和 Bash

1. 常用快捷键

  • ctrl+c 强制终止当前命令;
  • ctrl+l 清屏;
  • ctrl+a 光标移动到命令行首;
  • ctrl+e 光标移动到命令行尾;
  • ctrl+u 从光标所在位置删除到行首;
  • ctrl+z 把命令放入后台;
  • ctrl+r 在历史命令中 搜索;
  • Tab 自动补全目录或者命令

2. 历史命令

history <选项> <历史命令保存文件>

<选项>:

  • -c:清空历史命令;
  • -w:把缓存中的历史命令写入历史命令保存文件 /root/.bash_history

cat .bash_history 可以查看这个文件,默认关机时会执行 history -w 命令;

历史命令默认保存 1000 条,可以在环境变量配置文件 /etc/profile 中进行修改,修改 HISTSIZE 的值;

!n 可以重复执行 history 里第 n 条命令;

!! 重复执行上一条命令;

!字符串 重复执行最后一条已该字符串开头的命令;

3. 输出重定向(很有用)

date > date.log 将时间信息输入到 date.log 文件中,而不是显示输出;

date >> date.log 将时间信息追加到 date.log 文件中,而 > 是覆盖;

date &>> date.log 同时保存正确和错误信息,如果有错误信息;

date >> date.log 2>> error.log 保存正确信息到 date.log,保存错误信息到 error.log;

4. 输入重定向

wc <选项> <文件名>

<选项>:

  • -c:统计字节数;
  • -w:统计单词数;
  • -l: 统计行数;

例如:wc < date.log 或者直接 wc date.log 统计 date.log 文件里的字节数、单词数、行数;

5. 管道符

多命令顺序执行:

命令1 ;  命令2    多个命令顺序执行,命令之间没有任何逻辑联系
命令1 && 命令2    逻辑与,只有当命令1正确执行,则执行命令2
命令1 || 命令2    逻辑或,只有当命令1执行不正确,才执行命令2

管道符:(命令 1 和 2 有数据传递)

# 命令1的正确输出作为命令2的操作对象
命令1 | 命令2

6. 通配符

?      :匹配一个任意字符;
*      :匹配0个或任意多个任意字符,也就是可以匹配任何内容;
[ ]    :匹配中括号内的任意一个字符,例如:[abc] 代表一定匹配一个字符,或者是a、或者是b、或者是c;
[ - ]  :匹配中括号内的任意一个字符,- 代表一个范围,例如:[a-z] 代表匹配一个小写字母;
[ ^ ]  :逻辑非,表示匹配不是中括号内的一个字符,例如:[^0-9] 代表匹配一个不是数字的字符;

7. 第一个脚本

a. 首先新建并打开一个文件

vim hello.sh
# 或者
vi hello.sh

b. 然后编辑内容

#!/bin/bash            脚本开头必须内容
#The first program     注释
echo -e "hello"

c. 脚本执行

# 赋予脚本执行权限,直接运行
chmod 755 hello.sh

# 然后相对路径调用
./hello.sh

# 或者绝对路径调用
/root/hello.sh

# 或者通过 Bash 调用执行脚本
bash hello.sh

目录切换与查询

# 进入当前用户的家目录,命令英文原意:change directory
cd ~
# 或者
cd

# 进入上次目录
cd -

# 进入上一级目录
cd ..

# 进入当前目录(比 cd .. 少一个 .)
cd .

# 进入根目录
cd /

# 查询所在目录位置,命令英文原意:print working directory
pwd

补充:Linux 目录说明

  • 根目录下的 binsbinusr 目录下的 binsbin,这四个目录都是用来保存系统命令的;
    bin 下的命令所用用户都可以执行,而 sbin 下的命令只有超级用户才可以执行。
  • dev 目录内是一些硬件文件,没事别动。
  • etc 目录下是系统的默认配置文件。
  • lib 目录下是函数库,用的时候可以调用,不用就没必要调用,这样可以避免 Linux 变得臃肿。
  • procsys 目录不能直接操作,这两个目录保存的是内存的挂载点,也就是内存的盘符。
  • tmp 是临时目录。
  • var 保存系统的可变文档目录。

显示目录下的文件

ls <选项> <文件或目录>

<选项>:

  • -a 显示所有文件,包括隐藏文件
  • -l 显示详细信息(ll = ls -l
  • -d 查看目录属性
  • -h 人性化显示文件大小
  • -i 显示 inode,查看 id 号

获取帮助

# 获取指定命令的帮助,manual(手册)缩写
man <命令>

# 查询shell命令,如 cd
help <命令>

查看用户登录信息

# 详细信息
w

# 简洁信息
who

# 查询当前登录和过去登陆的用户信息
last

# 查询所有用户的最后一次登录时间
lastlog

文件操作

创建目录

mkdir <目录名>

# 递归创建,在没有 xiaomi 目录的前提下创建 xiaomi/redmi 必须加 -p
mkdir -p xiaomi/redmi

目录复制

# 格式
cp <选项> <原文件或目录> <目标目录>

# 连带文件属性复制
cp -p <原文件或目录> <目标目录>

# 若原文件是链接文件,则复制链接属性
cp -d <原文件或目录> <目标目录>

# 复制目录
cp -r <原文件或目录> <目标目录>

# 相当于 -pdr
cp -a <原文件或目录> <目标目录>

# 把 folder1 里面的文件和文件夹等复制到 folder2 目录下
cp -rf folder1/* folder2

目录移动

# 把 folder1 里面的文件和文件夹等移动到 folder2 目录下
mv folder1/* folder2

目录重命名

# 把 folder1 重命名为 folder2
mv folder1 folder2

文件/目录删除

# 删除文件
rm <文件名>

# 删除目录
rm -rf <目录名>

参考:

  1. linux 下文件夹的创建、复制、剪切、重命名、清空和删除命令

创建软链

# 格式
ln -s <原文件> <目标文件>

# 创建软链
ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx

# 删除软链
rm 软链

搜索文件/命令/字符串

搜索文件

locate

# 在后台数据库中按文件名搜索,搜索速度快
# 注意:locate命令搜索的是后台数据库,新建的文件需要 updatedb 后才能搜索到(因为,mlocate 数据库并不是实时更新)
locate <文件名>

find

# 耗费资源,比较慢,但是功能非常强大
# 注意:避免大范围的搜索
find <搜索范围> <搜索条件>

搜索条件:

  • -name: 按文件名索搜,只显示文件名一模一样的文件
  • -iname: 不区分大小写

通配符:(放在搜索格式结尾)

  • *: 匹配任意内容
  • ?: 匹配任意一个字符
  • []: 匹配任意一个中括号内的字符
  • -user <用户名>: 按照所有者搜索
  • -nouser: 查找没有所有者的文件
  • -mtime <num>: 修改文件内容时间
  • -atime <num>: 文件访问时间
  • -ctime <num>: 改变文件属性时间
    • -10: 10 天内修改的文件
    • 10: 10 天当天修改的文件
    • +10: 10 天前修改的文件;
  • -size <num>: 按文件大小查找
    • -10k: 小于 10KB 的文件 / -10M:小于 10MB 的文件(注意大小写)
    • 10k: 等于 10KB 的文件
    • +10k: 大于 10KB 的文件;
  • -inum <节点数>: 按 i 节点查找;(ls -i 查看文件的 i 节点)

例子:
find /etc -size +20k -a -size -50k -exec ls -lh {} \

解释:

  • 查找 /etc 目录下大于 20KB 小于 50KB 的文件,
  • -a: and 逻辑与,两个条件都满足,
  • -o: or 逻辑或,两个条件满足一个即可,
  • -exec <命令> {} \: 对搜索结果执行操作;

搜索命令

whereis

# 搜索命令所在路径以及帮助文档所在位置
whereis <选项> <命令名>

选项:

  • -b: 只查找可执行文件;
  • -m: 只查找帮助文件;

which

# 搜索命令的所在位置以及命令的别名(如果有的话,pwd 便没有别名)
# 注意:whereis 和 which 无法搜索到 cd 命令,因为 cd 命令是 shell 自带的命令
which <命令名>

搜索字符串

# 在文件当中匹配符合条件的字符串
grep <选项> <字符串> <文件名>

选项:

  • -i: 忽略大小写;
  • -v: 排除指定字符串,也就是取反;

压缩与解压

.zip 格式压缩(可以压缩目录和文件)

# 压缩文件,原文件保留
zip <压缩文件名> <原文件>

# 压缩目录,原目录保留
zip -r <压缩文件名> <原文件夹>

# 解压缩 .zip 文件,原文件保留,解压后默认和压缩文件保存在同一目录
unzip <原文件>

.gz 格式压缩(可以压缩目录和文件)

# 压缩为 .gz 格式的文件,原文件会消失
gzip <原文件>

# 压缩为 .gz 格式的文件,原文件保留
gzip -c <原文件> > <压缩文件>
# 例如
gzip -c meizu > meizu.gz

# 压缩目录下所有的子文件,但是不能压缩目录,原文件会消失
gzip -r <目录>

# 解压缩文件,原文件会消失
gzip -d <压缩文件名>

# 解压缩文件,原文件会消失
gunzip <压缩文件名>

.bz2 格式压缩(只可以压缩文件)

# 压缩为 .bz2 格式的文件,原文件会消失
bzip2 <原文件>

# 压缩为 .bz2 格式的文件,原文件保留
bzip2 -k <原文件>

# 解压缩文件,原文件会消失;(加 -k 可以保存)
bzip2 -d <压缩文件名>

# 解压缩文件,原文件会消失;(加 -k 可以保存)
bunzip2 <压缩文件名>

打包命令 tar(会保留原文件)

# 打包
tar -cvf <打包文件名> <原文件>
# 例如
tar -cfg meizu.tar meizu

# 解打包
tar -xvf <打包文件名>
# 例如
tar -xvf meizu.tar

选项:

  • -c: 打包
  • -v: 显示过程
  • -f: 指定打包后的文件名
  • -x: 解打包

.tar.gz 格式压缩(可以压缩目录和文件)(会保留原文件)

其实 .tar.gz 格式是先打包为 .tar 格式,再压缩为 .gz 格式。

# 同时将两个文件打包压缩到一个压缩文件下
tar -zcvf <压缩包名.tar.gz> <原文件> (<另一个原文件>)

# 解压缩 .tar.gz 格式的文件
tar -zxvf <压缩包名.tar.gz>

选项:

  • -c: 压缩为 .tar.gz 格式
  • -x: 解压缩 .tar.gz 格式的文件

.tar.bz2 格式压缩(可以压缩目录和文件)(会保留原文件)

其实 .tar.bz2 格式是先打包为 .tar 格式,再压缩为 .bz2 格式。

# 压缩
tar -jcvf <压缩包名.tar.bz2> <原文件>

# 解压缩
tar -jxvf <压缩包名.tar.bz2>

# 解压缩到指定目录
tar -jxvf <压缩包名.tar.bz2> -C <其他目录>
# 例如
tar -jxvf meizu.tar.bz2 -C /tmp/;

# 只查看里面的内容,而不解压
tar -jtvf <压缩包名.tar.bz2>

选项:

  • -j: 压缩为 .tar.bz2 格式
  • -x: 解压缩 .tar.bz2 格式的文件
  • -t: 查看里面的内容

查询和挂载

查询命令

# 查询系统中已经挂载的设备
mount

# 依据配置我文件 /etc/fstab 的内容自动挂载
mount -a

挂载命令

mount <-t 文件系统> <-o 特殊选项> <设备文件名> <挂载点>

<选项>:

  • -t 文件系统:加入文件系统类型来指定挂在的类型,可以 ext3、ext4、iso9660 等文件系统;
  • -o 特殊选项:可以制定挂载的额外选项;

挂载光盘

# 建立挂载点
mkdir /mnt/cdrom

# 挂载光盘(推荐使用 sr0)
mount -t iso9660 /dev/sr0 /mnt/cdrom

# 或者
# 挂载光盘(cdrom 和 sr0 是软链接关系)
mount -t iso9660 /dev/cdrom /mnt/cdrom

# 或者
# 挂载光盘(简写)
mount /dev/sr0 /mnt/cdrom

卸载光盘命令(挂载后必须卸载)

umount <设备文件名或挂载点>

# 通过文件名卸载光盘
umount /dev/sr0

# 或者
# 通过挂载点卸载光盘
umount /mnt/cdrom

挂载 U 盘

# 查看U盘设备文件名
fdisk -l

# 建立挂载点
mkdir /mnt/usb

# 挂载U盘
mount -t vfat /dev/ <U盘设备文件名> /mnt/usb

关机 / 重启 / 退出登录

关机命令

# 安全,关机时会正确保存正在运行的任务
shutdown -h now

# 或者
halt

# 或者
poweroff

# 或者
init 0

重启命令

shutdown -r now

# 或者
reboot

# 或者
init 6

退出登录命令

logout

附录:

Systemd 常用命令:

# 立即激活单元
systemctl start <单元>

# 立即停止单元:
systemctl stop <单元>

# 重启单元:
systemctl restart <单元>

# 重新加载配置:
systemctl reload <单元>

# 输出单元运行状态:
systemctl status <单元>

# 检查单元是否配置为自动启动:
systemctl is-enabled <单元>

# 开机自动激活单元:
systemctl enable <单元>

# 取消开机自动激活单元:
systemctl disable <单元>

参考:

  1. systemd-ArchWiki
  2. Systemd 入门教程:命令篇

Nginx 常用命令:

# nginx 启动
# 其中参数 -c 指定 nginx 启动时加载的配置文件,当然也可以不指定配置文件,省略 -c,也可以启动,表示使用默认的配置文件
nginx -c /etc/nginx/nginx.conf
# OR
nginx

# nginx 停止
# 例如在我们的编辑环境中已经安装好了 nginx,并且已启动,在命令提示符下直接输入 nginx -s stop 就可以停止了
nginx -s stop
# OR
nginx -s quit
# OR
pkill -9 nginx

# nginx 重载配置
nginx -s reload

# 检查配置文件是否正确
nginx -t

参考:

  1. systemd-ArchWiki
  2. Systemd 入门教程:命令篇

firewalld 常用命令:

# 查看版本
firewall-cmd --version

# 获取 firewalld 状态
firewall-cmd --state

# 查看开启的端口列表
firewall-cmd --list-ports

# 查看 public 下开启的端口列表
firewall-cmd --zone=public --list-ports

# 启动 firewalld
systemctl start firewalld.service

# 重启 firewalld
systemctl restart firewalld.service

# 开机自启 firewalld
systemctl enable firewalld.service

# 关闭开机自启 firewalld
systemctl disable firewalld.service

# firewalld 默认会关闭所有端口访问

# 开启 80 端口(此时只有 80 端口可以访问)
# --permanent 永久
firewall-cmd --zone=public --add-port=80/tcp --permanent

# 启用https服务
firewall-cmd --permanent --zone=public --add-service=https

# 开启 443 端口(https)
firewall-cmd --zone=public --add-port=443/tcp --permanent

# 重新加载防火墙,生效新添的规则
firewall-cmd --reload

【Git】恢复误删的 `git stash` 记录

背景

使用 Sourcetree 贮藏工作现场后,误删了这个 stash

恢复方法

Google 一通,找到:

恢复步骤

第一步:git fsck --unreachable

git-fsck 文档

查找所有的 unreachable 记录

第二步:git show <sha>

其中 sha 是记录的 key(如下面的:bb3cc162df1270fcabac0dec53effc8166b9563b)

1. 命令行中 drop:

$ git stash drop
Dropped refs/stash@{0} (bb3cc162df1270fcabac0dec53effc8166b9563b)

2. Sourcetree 中 drop:

使用 Sourcetree 删除时,我们是不知道 sha 的,此时回到第一步 git fsck --unreachable

查找所有的 unreachable 记录,例如:

$ git fsck --unreachable

Checking object directories: 100% (256/256), done.
Checking objects: 100% (3730/3730), done.
unreachable tree 3f80ada1c908812f125a5869ca7f77429e42a8c8
unreachable tree 7a409b8150ff5533ab4d22ebfbec520d4ce0bf7d
unreachable blob 8600c5b5609fbb541a2aa492d4eb4ed72c1e43a0
unreachable blob ee006fdb6554149a9f6aa6c3ef3d21853292b44f
unreachable blob b0c14ad2a35149815261ade2f89f3cf8e9fa096c
unreachable blob 57c221b28812aec98f2c44ee226ae75325ef28f3
unreachable blob 2783aa80d2dcdb51ac8cfe2f0cdbfd813ac6bc15
unreachable blob 65439852e70c0649d605a8452d79b4e16d40dada
unreachable blob 4b0472cf3317cbe8a476abd35ead1db1f791ebb7
unreachable tree c8c4ae2a9580bf788b39bf9ad770cc50cdeec734
unreachable tree 178594b9b22143860b6894377bc72b5c0ecbf6ea
unreachable tree 2a45ce0eb08dac0efe095b65eb9e98e3b1e26625
unreachable commit 49863934d99402979b2f43abd77b05e5a19780f2

对所有 unreachable commit 开头的记录,依次执行 git show <sha>,查看是否为误删的提交,例如:

$ git show bb3cc162df1270fcabac0dec53effc8166b9563b

commit bb3cc162df1270fcabac0dec53effc8166b9563b
Merge: 2800dc1 d7625d4
Author: *** <***@***.com>
Date:   Thu Jul 5 11:43:10 2018 +0800

    WIP on dev: 2800dc1 style(pc): 样式修改

diff --cc src/common/filters/index.js
index ac9ea48,ac9ea48..1f72657
--- a/src/common/filters/index.js
+++ b/src/common/filters/index.js
@@@ -1,5 -1,5 +1,3 @@@
  import currency from './currency'
  
--export default {
--  currency
--}
++export default { currency }

第三步:git stash apply <sha>

找到误删的 stash 后,使用 git stash apply <sha> 即可恢复

【Go】Windows 10 配置 Go 开发环境

安装

  1. 下载地址
  2. 一路 next
  3. 查看 Go 版本: go version

解决安装 tools / lint 失败问题(VSCode)

Go 默认安装在 C:\Go\ 路径下

  1. C:\Go\src\ 目录下创建 golang.org 文件夹
  2. C:\Go\src\golang.org 目录下创建 x 文件夹
  3. 打开 PowerShell, cdC:\Go\src\golang.org\x 目录
  4. 下载 Go tools 源码 git clone https://github.com/golang/tools.git tools
  5. 下载 Go lint 源码 git clone https://github.com/golang/lint.git lint
  6. 打开 VSCode 安装 ms-vscode.Go 插件, 完成后重启 VSCode
  7. 根据 VSCode 提示安装 Go tools

参考

【JS】你不知道的 DOM

document.currentScript

返回其所包含的脚本中正在被执行的 <script> 元素

值得注意的是,如果当前正在执行的代码是处在某个回调函数或者事件处理函数中的,那么 currentScript 属性不会指向包含那个函数的 <script> 元素,而是会返回 null

相关: current-script-polyfill

【Vue】深度作用选择器

如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:

<style scoped>
.a >>> .b { /* ... */ }
</style>

上述代码将会编译成:

.a[data-v-f3f3eg9] .b {
  /* ... */
}

像 Sass、Less 之类的预处理器无法正确解析 >>>。这种情况下可以使用 /deep/ 操作符取而代之 ———— 它是 >>> 的别名,同样可以正常工作。

参考

【HTTP】图解 HTTP

目录

三次握手 & 四次挥手

三次握手 & 四次挥手

三次握手 & 四次挥手

ARP 协议

ARP(Address Resolution Protocol)是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址。

HTTP keep-alive

HTTP 持久连接(HTTP persistent connection,也称作 HTTP keep-alive 或 HTTP connection reuse)是使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。

优势

  • 较少的 CPU 和内存的使用(由于同时打开的连接的减少了)
  • 允许请求和应答的 HTTP 管线化
  • 降低拥塞控制(TCP 连接减少了)
  • 减少了后续请求的延迟(无需再进行握手)
  • 报告错误无需关闭 TCP 连接

劣势

  • 对于现在的广泛普及的宽带连接来说,Keep-Alive 也许并不像以前一样有用。web 服务器会保持连接若干秒(Apache 中默认 15 秒),这与提高的性能相比也许会影响性能。
  • 对于单个文件被不断请求的服务(例如图片存放网站),Keep-Alive 可能会极大的影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。

HTTP/2.0

多路复用(Multiplexing)

多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。

在 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」。

多个静态资源 CDN 域名可以变相解决浏览器针对同一域名的请求限制阻塞问题。

HTTP/1.1 & HTTP/2.0

首部压缩(Header Compression)

HTTP/2.0 对消息头采用 HPACK(专为 http2 头部设计的压缩格式)进行压缩传输,能够节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。

首部压缩

服务端推送(Server Push)

服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。服务端可以主动推送,客户端也有权利选择接收与否。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。

HTTPS

HTTP & HTTPS

HTTPS

简而言之,HTTPS 就是在 HTTP 下加入了 SSL 层,从而保护了交换数据隐私和完整性,提供对网站服务器身份认证的功能,简单来说它就是安全版的 HTTP。

  • HTTPS 需要 CA 证书
  • HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,HTTPS 运行在 SSL/TLS 之上,SSL/TLS 运行在 TCP 之上,所有传输的内容都经过加密的
  • HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443
  • HTTPS 可以有效的防止运营商劫持,解决了防劫持的一个大问题

HTTP 状态码及其含义

  • 2XX:成功状态码
    • 200:请求已成功,请求所希望的响应头或数据体将随此响应返回
    • 202:服务器已接受请求,但尚未处理。最终该请求可能会也可能不会被执行,并且可能在处理发生时被禁止
    • 204:服务器成功处理了请求,没有返回任何内容
  • 3XX:重定向
    • 301:永久重定向
    • 302:临时重定向
    • 304:资源未被修改,因为请求头指定的版本 If-Modified-Since 或 If-None-Match。在这种情况下,由于客户端仍然具有以前下载的副本,因此不需要重新传输资源
  • 4XX:客户端错误
    • 400:由于明显的客户端错误(例如,格式错误的请求语法,太大的大小,无效的请求消息或欺骗性路由请求),服务器不能或不会处理该请求
    • 401:类似于 403 Forbidden,401 语义即“未认证”,即用户没有必要的凭据
    • 403:服务器已经理解请求,但是拒绝执行它
    • 404:请求失败,请求所希望得到的资源未被在服务器上发现,但允许用户的后续请求
  • 5XX:服务器错误

从浏览器地址栏输入 URL 到显示页面的步骤(以 HTTP 为例)

  1. 在浏览器地址栏输入 URL
  2. 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤
    • 如果资源未缓存,发起新请求
    • 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
    • 检验新鲜通常有两个 HTTP 头进行控制 Expires 和 Cache-Control:
      • HTTP1.0 提供 Expires,值为一个绝对时间表示缓存新鲜日期
      • HTTP1.1 增加了 Cache-Control: max-age=300000,值为以秒为单位的最大新鲜时间
  3. 浏览器解析 URL,获取协议、主机、端口、path
  4. 浏览器组装一个 HTTP(GET)请求报文
  5. 浏览器获取主机 IP 地址,过程如下:
    • 浏览器缓存
    • 本机缓存
    • hosts 文件
    • 路由器缓存
    • ISP DNS 缓存
    • DNS 递归查询(可能存在负载均衡导致每次 IP 不一样)
  6. 打开一个 socket 与目标 IP 地址,端口建立 TCP 链接,三次握手如下:
    • 客户端发送一个 TCP 的 SYN=1,Seq=X 的包到服务器端口
    • 服务器发回 SYN=1,ACK=X+1,Seq=Y 的响应包
    • 客户端发送 ACK=Y+1,Seq=Z
  7. TCP 链接建立后发送 HTTP 请求
  8. 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用 HTTP Host 头部判断请求的服务程序
  9. 服务器检查 HTTP 请求头是否包含缓存验证信息如果验证缓存新鲜,返回 304 等对应状态码
  10. 处理程序读取完整请求并准备 HTTP 响应,可能需要查询数据库等操作
  11. 服务器将响应报文通过 TCP 连接发送回浏览器
  12. 浏览器接收 HTTP 响应,然后根据情况选择关闭 TCP 连接或者保留重用,关闭 TCP 连接的四次挥手如下:
    • 主动方发送 Fin=1,Ack=Z,Seq=X 报文
    • 被动方发送 ACK=X+1,Seq=Z 报文
    • 被动方发送 Fin=1,ACK=X,Seq=Y 报文
    • 主动方发送 ACK=Y,Seq=X 报文
  13. 浏览器检查响应状态吗:是否为 1XX,3XX,4XX,5XX,这些情况处理与 2XX 不同
  14. 如果资源可缓存,进行缓存
  15. 对响应进行解码(例如 gzip 压缩)
  16. 根据资源类型决定如何处理(假设资源为 HTML 文档)
  17. 解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,执行 JS 脚本,这些操作没有严格的先后顺序,以下分别解释
  18. 构建 DOM 树:
    • Tokenizing:根据 HTML 规范将字符流解析为标记
    • Lexing:词法分析将标记转换为对象并定义属性和规则
    • DOM construction:根据 HTML 标记关系将对象组成 DOM 树
  19. 解析过程中遇到图片、样式表、JS 文件,启动下载
  20. 构建 CSSOM 树:
    • Tokenizing:字符流转换为标记流
    • Node:根据标记创建节点
    • CSSOM:节点创建 CSSOM 树
  21. 根据 DOM 树和 CSSOM 树构建渲染树:
    • 从 DOM 树的根节点遍历所有可见节点,不可见节点包括:
      • script,meta 这样本身不可见的标签。
      • 被 css 隐藏的节点,如 display: none
    • 对每一个可见节点,找到恰当的 CSSOM 规则并应用
    • 发布可视节点的内容和计算样式
  22. JS 解析如下:
    • 浏览器创建 Document 对象并解析 HTML,将解析到的元素和文本节点添加到文档中,此时 document.readystate 为 loading
    • HTML 解析器遇到没有 async 和 defer 的 script 时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用 document.write() 把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作 script 和他们之前的文档内容
    • 当解析器遇到设置了 async 属性的 script 时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用 document.write(),它们可以访问自己 script 和之前的文档元素
    • 当文档完成解析,document.readState 变成 interactive
    • 所有 defer 脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用 document.write()
    • 浏览器在 Document 对象上触发 DOMContentLoaded 事件
    • 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState 变为 complete,window 触发 load 事件
  23. 显示页面(HTML 解析过程中会逐步显示页面)

参考

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.