Little H title

this is subtitle


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 公益404

vim配置

发表于 2019-03-30 | 分类于 后端教程

vim配置

主要讲下~/.vimrc的配置, 然后还有一些插件的使用

大致划分的话,我们通过 vimrc 告诉 vim 如下几类信息:

  • 插件
  • 界面设置
  • 操作定义

开始

三本基础的书, 按阶段看

  • vimtutor.
  • Pratical Vim
  • Learn Vimscript the Hard Way

基本操作

缓冲区的
Vim 多文件编辑:缓冲区

1
2
3
4
5
6
<c+i>
<c+o>

:ls
:bn :bp
:b123

操作定义

vim 入坑指南(二)— vim 的模式

公式一:[数字] + operator + motion
公式二:operator + [数字] + operator (前后均为同一个 operator)

bug

删除键没用, 删不掉上一行, 用set backspace=2
vim中delete(backspace)键不能向左删除

对于在插入模式下按方向键结果出来OAOBOCOD这种, 设置了set nocp即set nocompatible情况下还是这种效果的, 因为我用了inoremap <esc> <nop>
Arrow keys type OA, OB, OC, and OD
当然还有更厉害的ESC O D

界面设置

vim自带有一些基本的色彩主题,一般在/usr/share/vim/vim74/colors/中

然后你可以下载使用, 一般放在在~/.vim/colors,然后在~/.vimrc中设置colorscheme xxx

参考:Vim Colors - Online Preview
参考:vim官方收集的各种主题包:Vim.org色彩主题集

插件

流行的有4种吧, Vundle, NeoBundle, VimPlug, Pathogen

我暂时用vim-plug

基本上看建议是vim有哪些插件管理程序?都有些什么特点? - LiTuX的回答 - 知乎

基本使用方式Vim-plug:极简 Vim 插件管理器或者VIM 插件管理工具 vim-plug 简明教程

在这个网站上找到一个插件后使用fugitive.vim

基本上就是这么方便

再讲下使用吧

安装后在~/.vimrc中配置

记住,当你在配置文件中声明插件时,列表应该以 call plug#begin(PLUGIN_DIRECTORY) 开始,并以 plug#end() 结束。 所有的在VimAwesome上找到的插件使用的时候都要放在这个里面

下面是一个普遍的例子

1
2
3
call plug#begin('~/.vim/plugged')
Plug 'scrooloose/nerdtree', { 'on': 'NERDTreeToggle' }
call plug#end()

增加后用:source ~/.vimrc来, 或者不退出, 用:source %

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
" 看插件状态
:PlugStatus

" 按配置安装插件, 安装时也会有插件状态提示信息
:PlugInstall

" 更新插件
:PlugUpdate

" 查看不同, 这货没啥用
:PlugDiff

" 删除插件是记得不仅要在vimrc中删了, 也要运行命令, 防止在~/.vim/plugged/中还有
:PlugClean

" 升级VimPlug
:PlugUpgrade

常用插件

vim-cheat40

emacs

两者可以共同使用
mac 终端光标的快捷键操作
一年成为 Emacs 高手 (像神一样使用编辑器)

终端中的快捷键操作

常用的快捷键:
Ctrl + d 删除一个字符,相当于通常的Delete键(命令行若无所有字符,则相当于exit;处理多行标准输入时也表示eof)
Ctrl + h 退格删除一个字符,相当于通常的Backspace键
Ctrl + u 删除光标之前到行首的字符
Ctrl + k 删除光标之前到行尾的字符
Ctrl + c 取消当前行输入的命令,相当于Ctrl + Break
Ctrl + a 光标移动到行首(Ahead of line),相当于通常的Home键
Ctrl + e 光标移动到行尾(End of line)
Ctrl + f 光标向前(Forward)移动一个字符位置
Ctrl + b 光标往回(Backward)移动一个字符位置
Ctrl + l 清屏,相当于执行clear命令
Ctrl + p 调出命令历史中的前一条(Previous)命令,相当于通常的上箭头
Ctrl + n 调出命令历史中的下一条(Next)命令,相当于通常的上箭头
Ctrl + r 显示:号提示,根据用户输入查找相关历史命令(reverse-i-search)

次常用快捷键:
Alt + f 光标向前(Forward)移动到下一个单词
Alt + b 光标往回(Backward)移动到前一个单词
Ctrl + w 删除从光标位置前到当前所处单词(Word)的开头
Alt + d 删除从光标位置到当前所处单词的末尾
Ctrl + y 粘贴最后一次被删除的单词

移动就是f,b, a,e
删除u, k, w, d, d, h
粘贴y
历史r

关闭mac上按option输出字符MacOS中如何关闭或禁用Option(Alt)+ 键盘输出特殊字符(解决oh my zsh 使option f b 失效) 就是改下items2的key为+Esc

开启emacs入门

先安装再配置GNU Emacs Download & installation

然而brew没有--with-cocoa的参数, 当你用brew install emacs后, 用emacs --version却看到版本不对诶

因为mac自带emacs在/usr/bin/emacs
删不掉这个自带的emacs
Delete /usr/bin/emacs - Operation not permitted
Who installed this Emacs?

在使用brew安装完之后 更改系统自带旧版本

在~/.zshrc中添加alias emacs="/usr/local/Cellar/emacs/26.1_1/bin/emacs-26.1"

看教程学下基本的知识Fast and robust Emacs setup.

参考

操作定义
从零学习 vim 一个多月, 感觉最有用的三个教程
Learn Vimscript the Hard Way
Learn Vimscript the Hard Way 中文版

vim 入坑指南(二)— vim 的模式
mac-vim 按上下左右出现ABCD
插件
vim 入坑指南(五)插件 Vim-Plug
Minimalist Vim Plugin Manager
界面

集合
如何用Vim搭建IDE?
Vim - 配置IDE一般的python环境
spf13
超级强大的vim配置(vimplus)
Vim成长之路
把VIM配置成IDE开发环境
所需即所获:像 IDE 一样使用 vim
The Ultimate vimrc
简明 VIM 练级攻略
将你的Vim 打造成轻巧强大的IDE

ecmas
mac 终端光标的快捷键操作
一年成为 Emacs 高手 (像神一样使用编辑器)

npm简单入门

发表于 2019-03-15

npm简单入门

总结下

npm的命令

1
2
3
4
5
6
7
8
// 由于新版的nodejs已经集成了npm,所以之前npm也一并安装好了。同样可以通过输入 "npm -v" 来测试是否成功安装
npm -v

// 如果你安装的是旧版本的 npm,可以很容易得通过 npm 命令来升级,命令如下:
$ sudo npm install npm -g

// npm 安装 Node.js 模块语法格式如下:
$ npm install <Module Name>

npm 的包安装分为本地安装(local)、全局安装(global)两种,从敲的命令行来看,差别只是有没有-g而已,比如

1
2
3
4
5
6
7
npm install express         # 本地安装到./node_modules
npm install express -g # 全局安装到/usr/local 下或者你 node 的安装目录, nvm这种

npm install <package_name> --save # 本地安装后, 把包信息加入package.json的dependencies字段
npm install <package_name> --save-dev # 本地安装后, 把包信息加入package.json的devDependencies字段

npm install # 是根据package.json的dependencies和devDependencies信息, 根据不同的环境安装不同的包到./node_modules下

本地安装

  1. 将安装包放在 ./node_modules 下(运行 npm 命令时所在的目录),如果没有 node_modules 目录,会在当前执行 npm 命令的目录下生成 node_modules 目录。(并不会在package.json中填入)
  2. 可以通过 require() 来引入本地安装的包。

全局安装

  1. 将安装包放在 /usr/local 下或者你 node 的安装目录。
  2. 可以直接在命令行里使用。

而后面加参数 --save-dev 和 --save 表示的是dependencies和devDependencies,分别对应生产环境需要的安装包和开发环境需要的安装包。

同样在安装模块的时候,可以通过指定参数来修改package.json文件

查看安装信息

你可以使用以下命令来查看所有全局安装的模块:

$ npm list -g

如果要查看某个模块的版本号,可以使用命令如下:

$ npm list grunt

也可以npm list

卸载模块, 同理分3个地方

1
npm uninstall express

卸载后,你可以到 /node_modules/ 目录下查看包是否还存在,或者使用以下命令查看:

1
npm ls

更新模块
我们可以使用以下命令更新模块:

$ npm update express
搜索模块
使用以下来搜索模块:

$ npm search express

接下来我们可以使用以下命令在 npm 资源库中注册用户(使用邮箱注册):

$ npm adduser
接下来我们就用以下命令来发布模块:

$ npm publish

NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。

使用npm help <command>可查看某条命令的详细帮助,例如npm help install。

在package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。

使用npm update <package>可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。

使用npm update <package> -g可以把全局安装的对应命令行程序更新至最新版。

使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。

使用npm unpublish <package>@<version>可以撤销发布自己发布过的某个版本代码。

package.json

权限问题

当尝试全局安装某个包得时候,你可能会收到EACCES错误。这说明你没有权限写入npm用于存储全局包和命令的目录。

你可以用下面三种方法解决此问题:

  • 修改npm默认目录的权限;
  • 将npm默认目录定向到其他你具有读写权限的目录;
  • 使用某个包管理器来安装node,它会为你处理好权限问题。

知道有这么个就好了

1、找到npm的目录路径:

1
npm config get prefix

2、配置npm使用这个新目录:

1
npm config set prefix '~/.npm-global'

本地安装npm

安装npm包有两种方式:本地安装或全局安装。根据你想如何使用包,你可以选择安装方式。

如果你想要从你自己的模块中通过使用Node.js的require方法来依赖某个包,那你可以本地安装这个包,这是npm安装的默认行为。
另外,如果你想当做命令行工具使用它,比如grunt CLI,那你应该全局安装这个包。

1
npm install <package_name>

此命令将在你的当前目录创建node_modules目录(若还未安装任何包),并将下载此包到这个目录。

安装的是哪个版本的包?

如果在本地目录中没有package.json文件,那该包的最新版本会被安装了。

如果有package.json文件,那么在package.json中声明的满足semver(语义化版本)规则的最新版本会被安装。

使用已安装的包

一旦包被安装在node_modules,你就可以在你的代码中使用它了。比如,当你创建Node.js模块是,你可以引入它。

1
2
3
4
5
// index.js
var lodash = require('lodash');

var output = lodash.without([1, 2, 3], 1);
console.log(output);

5. 使用package.json配置文件

管理本地安装的包的最好方法是创建一个package.json文件。

package.json文件会给你提供很多好东西:

  • 它用作你的项目的包依赖管理文档。
  • 它允许你使用语义化版本管理规则,指定项目中能使用的包的版本。
  • 使你的构建版本可以重新生成,这意味着你可以更易于与其他开发者分享代码。

创建package.json文件

要创建package.json文件,运行以下命令:

1
npm init

此为初始化项目命令,会在你运行此命令的文件夹根目录下创建项目配置文件:package.json。同时每行会出现一个问题,你输入答案后会出来另一个问题。这些问题最终会记录到package.json文件中。

“—yes”标签
扩展的命令行问答式流程不是必须的。通常你可以轻松地使用package.json文件快速配置项目。

你可以运行带--yes或-y标签的npm init命令,来生成默认的package.json文件:

这样name是默认的文件夹名。其他的问题都是用默认值填充的:

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "testnpm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
  • name:默认为目录名字,除非在git目录中,它会是git仓库的名字;
  • version:版本号,刚初始化的项目总是1.0.0;
  • description - 包的描述。
  • main:总是index.js;
  • scripts:默认创建一行空的测试脚本;
  • keywords:关键字
  • author:作者
  • contributors - 包的其他贡献者姓名。
  • license:ISC开源证书
  • dependencies - 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下。
  • devDependencies
  • repository: will pull in info from the current directory, if present
  • bugs: will pull in info from the current directory, if present
  • homepage: 包的官网 url 。

你也可以通过set命令来设置一些配置项。比如下边的这些:

1
2
3
npm set init.author.email "xxx@xx.com"
npm set init.author.name "example user"
npm set init.license "MIT"

指定依赖包

  • “dependencies”: these packages are required by your application in production 这货线上
  • “devDependencies”: these packages are only needed for development and testing 这个本地开发和测试

--save 和 --save-dev安装标记

在命令行中使用这两个标记,是添加依赖到你的package.json文件的更简单(也更酷)的方式。

添加package.json依赖的入口:

1
npm install <package_name> --save

添加package.json开发环境依赖的入口:

1
npm install <package_name> --save-dev

管理依赖包的版本

npm使用语义化版本管理依赖包,也就是我们常说的“SemVer”。

如果在项目文件夹下有package.json文件,你在此文件夹下运行命令npm install,npm就会检查文件中列出的依赖包,并下载所有满足语义化规则的最新版本的依赖包。

一开始npm install <package_name>的时候会在package.json中的dependencies或devDependencies中填入相关信息, 然后再本路径目录下下载包到node_modules中
直接npm install 会读取package.json 中信息, 然后下载包到node_modules

6、更新本地包

很多时候,你需要升级项目中的依赖包,以获得上游源代码的更新。

运在package.json同级目录下,运行npm update命令,来实现依赖包的更新。

测试:运行npm outdated命令,包都是最新版的话应该不会有什么结果。

7、卸载本地包

通过npm uninstall <package>命令,你可以将node_modules目录下的某个依赖包移除:

1
npm uninstall lodash

要从package.json文件的依赖列表中移除,你需要使用--save标签:

1
npm uninstall --save lodash

注意:如果你是以开发依赖包(devDependency)的方式安装的(即安装时待--dave -dev标签),那用—save将无法从package.json中移除,你必须用--save -dev标签。

有3种方式哦

8、全局安装npm包

安装npm包有两种方法:本地安装和全局安装。如何选择安装方式取决于你想如何使用这些依赖包。

译注:所谓“本地安装”,就是在项目文件夹中安装npm依赖包,以便此项目调用;“全局安装”相反,安装之后可以供所有项目直接调用,可以免去重复安装的步骤和空间。

如果你期望将包当作命令行工具来使用,比如grunt命令行工具,那你就需要全局安装。
相反地,如果你期望在你自己的模块中引入依赖包,比如用Node的 require 方法,那么你需要将它本地安装。

要全局下载依赖包的话,添加-g标识符就好了哦,npm install -g <package>,像下面这样:

1
npm install -g jshint

如果出现EACCES错误,你就修复一下权限。逼不得已的时候,你也可以试试 sudo:

1
sudo npm install -g jshint

9、更新全局包

要更新全局包的话,那就再全局安装一下:npm install -g <package>:

1
npm install -g jshint

如果想要找出哪些包需要更新,你可以使用 npm outdated -g --depth=0 命令帮忙。

译注:

有时候,在项目文件夹中直接 npm install,通过package.json的依赖声明中重新安装所有包,给人感觉挺Low B的。这个时候就需要找出哪些包已经过时了,需要更新。
而且,有时候有些依赖包没被声明在package.json文件中,那npm install就对他不起作用了。
--depth=0 的意思是依赖包的深度,只检查顶层依赖包。(因为存在一个包依赖另一个包, 比如tslint)
更新所有全局包,你可以使用 npm update -g。(译注:这可能会很慢,因为你装了太多依赖了)
注意:npm版本低于2.6.1的话,此命令被建议用来更新所有过时的全局包。

10、卸载全局包

卸载全局包使用 npm uninstall -g <package> 命令:

1
npm uninstall -g jshint

11、创建Node.js模块

Node.js模块是一种可以发布到npm的包。当你创建一个新模块的时候,你将从 package.json 文件开始。

使用 npm init 命令创建 package.json 文件。命令行中将会弹出package.json字段中要你输入的值。两个必填字段:名称(name)和版本(version)。你可能也需要输入主文件字段(main),可以使用默认值 index.js。

如果你想为作者(author)字段添加信息,你可以使用以下格式(邮箱、网站都是选填的):

一旦package.json文件创建好了,你将想要创建模块的入口文件,如果使用默认值,他将会是 index.js。

在此文件中,添加一个函数,作为 exports 对象的一个属性。这样,require此文件之后,这个函数在其他代码中就可以使用了。

1
2
3
exports.printMsg = function() {
console.log("This is a message from the demo package");
}

测试:

将你的包发布到npm
在你的项目外新建一个目录,然后 cd 过去
运行 npm install <package>
创建一个test.js文件,require这个包,并调用此方法(函数)
运行 node test.js。终端将会输出:This is a message from the demo package
恭喜你,你的第一个npm包创建成功了。

12、发布npm包

你可以发布任何包含package.json文件的目录,比如Nodejs模块。

创建用户
发布包之前,你必须创建一个npm用户账号。如果还没有,你可以用npm adduser创建一个。如果已经注册了,使用npm login命令将账号信息存储到本地客户端。

测试:使用npm config ls确认账号信息已经存储到您的客户端。访问https://npmjs.com/~ 以确保信息正确。

发布包

使用npm publish来发布程序包。

注意,目录下的所有文件都将被包含进去,除非目录下有.gitignore 或 .npmignore 文件(详情请看npm-developers)将其排除。

同时,请确保npm上没有别的开发者提交的同名都包存在。

更新包

当你修改了你的包文件,你可以用npm version <update_type>更新它。
update_type是指语义化版本管理的发布类型的一种:补丁版本(patch)、次版本(minor)或主版本(major)。
此命令会更改package.json中的版本号。注意哦,如果你有此包的git仓库,那么此命令也会向git仓库添加此版本的一个标签。

更新版本号之后,你就可以再次 npm publish 了。

站点下的README文件是不会更新,除非你的包的新版本发布成功。所以你需要运行npm version patch和npm publish命令来修复站点下的文档。

13、语义化版本号

语义化版本控制是一种标准,许多项目都用它来标识新版本是何种更改。这是非常重要的,因为有时这些更改会破坏依赖这个包的代码。

语义化版本对于发布者

如果项目将要与他人分享,那它的版本应该始于1.0.0,尽管npm上有些项目不遵循此规则。
之后,所有更改应该按如下方法处理:

  • Bug修复和其他小版本修改:用Patch版本,增加最后的版本数,如:1.0.1;
  • 不会破坏已有特性的新特性:用Minor版本,增加中间的版本数,如:1.1.0;
  • 会破坏向后兼容的更改:用Majo版本,增加第一个版本数,如:2.0.0.

语义化版本对于使用者

如果你是包的使用者,你可以在package.json文件中指定你的app能接受哪种版本。

如果你用某包的1.0.4版本开始开发的,你可以按下面方式指定版本范围:

  • 补丁版本(Patch releases): 1.0 or 1.0.x or ~1.0.4
  • 次版本(Minor releases): 1 or 1.x or ^1.0.4
  • 主版本(Major releases): * or x

14、使用局部包, 私有包

对npm的包而言, scope就像一个命名空间, 如果一个包的名字前带有@, 那么这就是一个scoped package, scopes是在@和/之间的所有东西

1
@scope/project-name

每一个npm 用户都有他们自己的scope

1
@username/project-name

更新npm并登录

你的npm包版本要大于2.7.0的, 然后第一个使用scoped modules要先登录

1
2
sudo npm install -g npm
npm login

初始化局部包

只需要如下在一个包名前带上你的名字就好

1
2
3
{
"name": "@username/project-name"
}

如果你用npm init来初始化, 可以带一个--scope来

1
npm init --scope=username

如果你想一直用同一个scope, 可以在.npmrc中设置

1
npm config set scope username

发布局部包

scoped包默认是私有的, 要发布的话, 你得付费买一个私有模块账户

1
npm publish --access=public

使用局部包

一样的用法
In package.json:

1
2
3
4
5
{
"dependencies": {
"@username/project-name": "^1.0.0"
}
}

On the command line:

1
npm install @username/project-name --save

In a require statement:

1
var projectName = require("@username/project-name")

For information about using scoped private modules, visit npmjs.com/private-modules.

15、使用标签

为了更可读, 和git一样

添加标签(tag)

To add a tag to a specific version of your package, use npm dist-tag add @ []. See the CLI docs for more information.

使用标签(tag)发布

默认情况下, npm publish 给你的包打上 latest tag. 使用 --tag , 你可以指定当前发布的包的tag是beta

1
npm publish --tag beta

使用标签(tag)安装

Like npm publish, npm install will use the latest tag by default.
To override this behavior, use npm install @.

1
npm install somepkg@beta

Caveats 警告

Because dist-tags share the same namespace with semver, avoid using any tag names that may cause a conflict.
最好就是不要在打tag前加上v, 防止和semver语义化的冲突

参考文献

NPM文档

k8s集群的简单安装和使用

发表于 2019-03-06 | 分类于 后端教程

k8s集群的简单安装和使用

什么是kubernetes

Kubernetes 是一个平台

Kubernetes 提供了很多的功能,它可以简化应用程序的工作流,加快开发速度。通常,一个成功的应用编排系统需要有较强的自动化能力,这也是为什么 Kubernetes 被设计作为构建组件和工具的生态系统平台,以便更轻松地部署、扩展和管理应用程序(Kubernetes 是一个容器编排平台)。

编排的艺术| K8S 中的容器编排和应用编排 6666

在传统的单体式架构的应用中,我们开发、测试、交付、部署等都是针对单个组件,我们很少听到编排这个概念。而在云的时代,微服务和容器大行其道,除了为我们显示出了它们在敏捷性,可移植性等方面的巨大优势以外,也为我们的交付和运维带来了新的挑战:我们将单体式的架构拆分成越来越多细小的服务,运行在各自的容器中,那么该如何解决它们之间的依赖管理,服务发现,资源管理,高可用等问题呢?

在容器环境中,编排通常涉及到三个方面:

  • 资源编排 - 负责资源的分配,如限制 namespace 的可用资源,scheduler 针对资源的不同调度策略;
  • 工作负载编排 - 负责在资源之间共享工作负载,如 Kubernetes 通过不同的 controller 将 Pod 调度到合适的 node 上,并且负责管理它们的生命周期;
  • 服务编排 - 负责服务发现和高可用等,如 Kubernetes 中可用通过 Service 来对内暴露服务,通过 Ingress 来对外暴露服务。

在 Kubernetes 中有 5 种我们经常会用到的控制器来帮助我们进行容器编排,它们分别是 Deployment, StatefulSet, DaemonSet, CronJob, Job。

在这 5 种常见资源中

  • Deployment 经常被作为无状态实例控制器使用;
  • StatefulSet 是一个有状态实例控制器;
  • DaemonSet 可以指定在选定的 Node 上跑,每个 Node 上会跑一个副本,它有一个特点是它的 Pod 的调度不经过调度器,在 Pod 创建的时候就直接绑定 NodeName;
  • 最后一个是CronJob定时任务,它是一个上级控制器,和 Deployment 有些类似,当一个定时任务触发的时候,它会去创建一个 Job ,具体的任务实际上是由 Job 来负责执行的。

他们之间的关系如下图:

controllers.jpeg

一个简单的例子

我们来考虑这么一个简单的例子,一个需要使用到数据库的 API 服务在 Kubernetes 中应该如何表示:

客户端程序通过 Ingress 来访问到内部的 API Service, API Service 将流量导流到 API Server Deployment 管理的其中一个 Pod 中,这个 Server 还需要访问数据库服务,它通过 DB Service 来访问 DataBase StatefulSet 的有状态副本。由定时任务 CronJob 来定期备份数据库,通过 DaemonSet 的 Logging 来采集日志,Monitoring 来负责收集监控指标。

example1.jpeg

容器编排的困境

Kubernetes 为我们带来了什么?

通过上面的例子,我们发现 Kubernetes 已经为我们对大量常用的基础资源进行了抽象和封装,我们可以非常灵活地组合、使用这些资源来解决问题,同时它还提供了一系列自动化运维的机制:如 HPA, VPA, Rollback, Rolling Update 等帮助我们进行弹性伸缩和滚动更新,而且上述所有的功能都可以用 YAML 声明式进行部署。

困境

但是这些抽象还是在容器层面的,对于一个大型的应用而言,需要组合大量的 Kubernetes 原生资源,需要非常多的 Services, Deployments, StatefulSets 等,这里面用起来就会比较繁琐,而且其中服务之间的依赖关系需要用户自己解决,缺乏统一的依赖管理机制。

应用编排

什么是应用?

一个对外提供服务的应用,

  • 首先它需要一个能够与外部通讯的网络,
  • 其次还需要能运行这个服务的载体 (Pods),
  • 如果这个应用需要存储数据,这还需要配套的存储,

所以我们可以认为:

应用单元 = 网络 + 服务载体 +存储

service1.jpeg

那么我们很容易地可以将 Kubernetes 的资源联系起来,然后将他们划分为 4 种类型的应用:

  • 无状态应用 = Services + Volumes + Deployment
  • 有状态应用 = Services + Volumes + StatefulSet
  • 守护型应用 = Services + Volumes + DaemonSet
  • 批处理应用 = Services + Volumes + CronJob/Job

我们来重新审视一下之前的例子:

example2.jpeg

应用层面的四个问题

通过前面的探索,我们可以引出应用层面的四个问题:

  1. 应用包的定义
  2. 应用依赖管理
  3. 包存储
  4. 运行时管理

在社区中,这四个方面的问题分别由三个组件或者项目来解决:

  1. Helm Charts: 定义了应用包的结构以及依赖关系;
  2. Helm Registry: 解决了包存储;
  3. HelmTiller: 负责将包运行在 Kubernetes 集群中。

Helm是一个kubernetes应用的包管理工具. 但有很多问题, 也有很多年代替的东西.

k8s核心架构介绍

Kubernetes 编排系统 666

下面是kubernetes的架构图, 核心组件, 可以看个大概, 记住的话很有用.

framework2.png
framework1.png

Pod

Kubernetes的基本调度单元称为“pod”。它可以把更高级别的抽象内容增加到容器化组件。一个pod一般包含一个或多个容器,这样可以保证它们一直位于主机上,并且可以共享资源。Kubernetes中的每个pod都被分配一个唯一的(在集群内的)IP地址这样就可以允许应用程序使用端口,而不会有冲突的风险。

Pod可以定义一个卷,例如本地磁盘目录或网络磁盘,并将其暴露在pod中的一个容器之中。pod可以通过Kubernetes API手动管理,也可以委托给控制器来管理。

标签和选择器

标签和选择器是Kubernetes中的主要分组机制,用于确定操作适用的组件。

控制器

控制器是将实际集群状态转移到所需集群状态的对帐循环。它通过管理一组pod来实现。

其它控制器,是核心Kubernetes系统的一部分包括一个“DaemonSet控制器”为每一台机器(或机器的一些子集)上运行的恰好一个pod,和一个“作业控制器”用于运行pod运行到完成,例如作为批处理作业的一部分。控制器管理的一组pod由作为控制器定义的一部分的标签选择器确定。

服务

Kubernetes服务是一组协同工作的pod,就像多层架构应用中的一层。构成服务的pod组通过标签选择器来定义。

Kubernetes核心组件

再重复一遍核心组件架构图.

framework2.png
framework1.png

Kubernetes遵循master-slave architecture。Kubernetes的组件可以分为管理单个的 node 组件和控制平面的一部分的组件。

Kubernetes Master是集群的主要控制单元,用于管理其工作负载并指导整个系统的通信。
Kubernetes控制平面由各自的进程组成,每个组件都可以在单个主节点node上运行,也可以在支持high-availability clusters的多个主节点上运行。

Kubernetes主要由以下几个核心组件组成:如上图

组件名称 说明
etcd 保存了整个集群的状态;
apiserver 提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;
controller manager 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
scheduler 负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;
kubelet 负责维护容器的生命周期,同时也负责Volume(CVI)和网络(CNI)的管理;
Container runtime 负责镜像管理以及Pod和容器的真正运行(CRI);
kube-proxy 负责为Service提供cluster内部的服务发现和负载均衡;

除了核心组件,还有一些推荐的Add-ons:addons

组件名称 说明 备注
kube-dns 负责为整个集群提供DNS服务
Ingress Controller 为服务提供外网入口 有看过
Heapster 提供资源监控
Dashboard 提供GUI 有用到
Federation 提供跨可用区的集群
Fluentd-elasticsearch 提供集群日志采集、存储与查询

使用docker来安装单节点k8s集群

直接下载最新版的docker, 然后找到kubernetes选项, 勾上enable kubernetes等待安装上就好.

enablek8s.png

安装完会顺便自动安装上kubectl控制命令

运行kubectl version查看安装成功否.

部署kubernetes-dashboard服务, 方便查看k8s的配置

要想启动 Kubernetes Dashboard,还得在集群中部署一下 kubernetes-dashboard.yaml。

1
kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml

部署成功后,我们进行启动 proxy。

1
2
3
kubectl proxy

Starting to serve on 127.0.0.1:8001

这时候,打开浏览器,访问 Kubernetes Dashboard

dashboard1.webp

通过以下脚本,填写 kubeconfig 的 Token 信息(如果不操作这一步,就会提示 config 信息不全)。

1
2
3
#!/bin/bash
TOKEN=$(kubectl -n kube-system describe secret default| awk '$1=="token:"{print $2}')
kubectl config set-credentials docker-for-desktop --token="${TOKEN}"

选择 kubeconfig 文件,使用“shift + command + .”打开 $HOME 下隐藏目录文件 ./kube/config,点击“登录”,就可以认证成功,进入首页了。

home1.webp

介绍

  • 第一部分: dockerhe k8s, 如何设置k8s集群,以及运行一个小程序
  • 第二部分: 在k8s中运行应用必须理解的关键技术
  • 第三部分: 深入研究k8s内部, 介绍一些额外的概念

本来应用是开发给运维, 运维部署在监控.
微服务, 大型单体应用, 到微服务.大应用分解成小的,

k8s使开发者可以自主部署应用, 并控制部署的频率
抽象数据中心的硬件基础设施.

开发和运维在一起的一个团队中DevOps
kubernetes 使用linux容器技术来提供应用的隔离.

Docker:

  • 镜像
  • 镜像仓库
  • 容器

流程是:

  1. 先开发者操作docker构建和推送镜像,
  2. 开发机器上docker构建镜像
  3. 然后推送到镜像仓库中,开发者可以在生产机器中拉取
  4. 生产机器上拉取镜像中心的docker, 然后基于容器运行.

kubernetes是一个软件系统, 允许你在其上很容易部署和管理容器化的应用.

开发者开发开发了一个应用, 然后交给kubernetes master, 他会控制成为一个个工作节点. 架构

kubernetes分2种节点,

  • 主节点master: 控制和管理
  • 工作节点node: 运行用户实际部署的应用.

主节点中:

  • kubernetes API: 控制和其他控制面板组件都要和他通信.
  • scheduler: 调度应用
  • controller manager: 执行集群级别的功能, 如复制组件, 持续跟踪node, 处理节点失败
  • etcd开考的分布式数据存储, 持久化存储集群配置.

工作节点中node:

  • docker: 或其他容器
  • kubelet: 和API通信, 并管理它所在节点的容器.
  • kubernetes service proxy: 负责组件之间的负载均衡网络流量

跑应用的话要先把应用大包进一个或多个容器镜像, 再把这些镜像推动到镜像仓库中, 然后把应用的描述发布到kubernetes API中.
图1.10很不错:

APP descriptor描述了4个容器, 并分为3组(叫3个pod), 前2个pod中一个容器, 后一个2个容器.表示这两个容器要协同工作, 不要隔离.旁边的数字表示要运行每个pod的副本数量.

pod在node中

例子: 如何创建一个简单的应用, 并把它打包成容器镜像并在远端的kubernetes集群中或本地的单节点集群中运行

  1. 安装docker并运行一个hello容器
  2. 创建一个简单的node.js应用并部署在kubernetes中
  3. 把应用打包成可以独立运行的容器镜像
  4. 基于镜像运行容器
  5. 把镜像推送到docker hub中.

docker build看图2.2
镜像的构建不是在docker客户端而是在docker的守护进程daemon. 两者可以不要求在同一台机器上.
没有的镜像会从docker hub中下.
镜像是分层的, 只有本地没有的镜像才会去hub上下.
Dockerfile是每一条语句创建一个层.
最后一层也就是最上面一层标记为kubia:latest
构建完成后, 新的镜像存储在本地.

访问下
一些列docker操作后, 就可以push到docker hub了

然后是一些kubernetes的操作. 设置一个完整的多节点的kubernetes集群是很麻烦的额, 暂时就用docker的自带enable kubernetes. 最简单的 比用minikube还方便.

安装完后用kubectl cluster-info看集群状况. 下面还没有装dashboard

1
2
3
4
5
$ kubectl cluster-info
Kubernetes master is running at https://localhost:6443
KubeDNS is running at https://localhost:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

使用GKE托管 kubernetes 集群

暂时不用了, 自己看下好了

kubectl get nodes看节点的状况, kubectl get用来看kubernetes中对象情况

更详细的是用kubectl describe node docker-for-desktop 看节点的详情.

设置别名kubectl为k

超实用的不全, 在bash或zsh shell中

在kubernetes上运行第一个应用.

kubectl run来部署
运行一个前面推到 docker hub 的上那个. 本地的那个kubia

kubectl run kubia --image=ximage/kubia --port=8080 --generator=run/v1

kubia是kubernetes中的名字,
—image= 指定要运行的image,
—port= 指定kubernete应用监听哪个端口
—generator= 创建一个ReplicationController而不是Deployment. 以后不会用到这个命令.

pod的信息. 这里你会想是不是有一个kubectl的命令来看pod, 但是没有, kubernetes不是干这个活. 他不直接处理单个容器. 使用的是多容器共存的概念, 就是pod.

每个pod类似一个独立逻辑机器, 有自己的ip, 主机名这种, 运行一个独立的应用程序.
应用程序可以是单进程, 运行在单容器中, 也可以是一个主应用进程或其他支持进程.

容器, pod, node的关系看图2.5
kubectl get pods 然后等status为running才行.

ImagePullBackOff的情况会等一会才成功的. 具体就用describe看

那么如何访问正在运行的pod的呢

前面说过每个pod都有自己的IP地址, 但这个地址是集群内部的, 不能从外部访问, 所以为了能从外部访问, 需要用服务对象公开他. 需要创建一个特殊的LoadBalancer类型的服务. 如果是一个常规服务的话(一个ClusterIP服务)还是只能在内部访问.
LoadBalancer将创建一个外部的负载均衡, 通过负载均衡的公共IP来访问pod

开始创建一个服务对象.

告知kubernetes对外暴露之前创建的ReplicationController

1
2
$ kubectl expose rc kubia --type=LoadBalancer --name kubia-http
service "kubia-http" exposed
  • rc是名字ReplicationController的缩写, 不用写全称. pods是po, service是svc
  • kubia是前面kubectl run的名字,
  • —type=LoadBalancer是服务类型
  • —name kubia-http一个新的名字

列出服务

用kubectl get 到目前为止有nodes pods services

1
2
3
4
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1d
kubia-http LoadBalancer 10.110.207.73 localhost 8080:32391/TCP 3m

可以看到新创建的kubia-http并没有外部IP,是localhost, 如果是<none>的话,这是因为kubernetes创建负载均衡是要一段时间的, 过一会看.

kubectl get svc kubia-http 看

还是用curl localhost:8080来看, 其实在用docker ps可以看到运行的容器哦.

仔细看应用将pod的名字当做主机名.

1
2
3
4
5
6
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kubia-m9zfv 1/1 Running 0 32m

$ curl localhost:8080
You've hit kubia-m9zfv

我们只需要记得, 我单点访问master节点就好了.

ReplicationController和pod和服务是如何组合在一起的.

我们没有直接创建和使用容器, kubernetes的基本构建又是pod. 我们也没有真正直接创建pod, 是通过kubectl run 创建了一个ReplicationController, 这个是用来创建pod实例的. 为了能够外部访问, kubernetes将ReplicationController管理的所有pod有一个服务对外暴露.

图2.7看看

最终要你的组件是pod和它的容器

第一个组件是: pod的的容器是node进程,

第二个组件是: ReplicationController是用来确保始终存在一个运行中的pod实例, 通常ReplicationController用于复制pod并让他们保持运行.

第三个组件是: kubia-http服务. 为什么要有服务, 因为pod是短暂存在的, 或故障, 或误操作. 虽然ReplicationController会复制一个新的, 但和原来的pod有个一区别就是IP地址不一样, 解决不断变化的IP问题, 这就是需要服务的地方. 还有就是搞定一个IP和端口对上对外暴露多个pod.
当一个服务创建时, 他会得到一个静态的IP, 服务生命周期内这个IP地址都不会发生改变. 客户端是通过固定IP地址来连接到服务. 而不是直接连接pod

水平伸缩应用

有前面3个组件基础后, 搞事情咯

用kubernetes一个主要好处就是可以简单扩展部署, 例子🌰

把运行实例增加到3个.

现在是一个

1
2
3
$ kubectl get replicationcontrollers
NAME DESIRED CURRENT READY AGE
kubia 1 1 1 1h

名为kubia的单 ReplicationControllers . DESIRED表示希望保持的pod的副本数, CURRENT是当前的pod副本数.

增加期望副本数DESIRED

1
2
$ kubectl scale rc kubia --replicas=3
replicationcontroller "kubia" scaled

只是告诉kubernetes我期望的数量, kubernetes会自己去做.

1
2
3
$ kubectl get rc
NAME DESIRED CURRENT READY AGE
kubia 3 3 3 1h

同时看下pod

1
2
3
4
5
$ kubectl get po
NAME READY STATUS RESTARTS AGE
kubia-m9zfv 1/1 Running 0 1h
kubia-q89qr 1/1 Running 0 1m
kubia-w22k7 1/1 Running 0 1m

然后重新访问下哦,多试几次可以看到访问不同的主机, pod

1
2
3
4
5
6
7
8
$ curl localhost:8080
You've hit kubia-m9zfv

$ curl localhost:8080
You've hit kubia-q89qr

$ curl localhost:8080
You've hit kubia-w22k7

查看应用运行在哪个节点上

kubernetes中不需要管这个.

想要看就用加参数-o wide 多了2列

1
2
3
4
5
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
kubia-m9zfv 1/1 Running 0 1h 10.1.0.8 docker-for-desktop
kubia-q89qr 1/1 Running 0 6m 10.1.0.10 docker-for-desktop
kubia-w22k7 1/1 Running 0 6m 10.1.0.9 docker-for-desktop

还有使用dashboard

暂时不管了, 2步

pod这个kubernetes的核心

其他对象只是在管理, 暴露pod或被pod使用.

pod

pod是一组并置的容器, 代表kubernetes中基本构建模块. 实际应用中并不会单独部署容器, 而是针对一组pod中的容器进行部署和操作.
这并不意味这一个pod中总要包含多个容器.
pod也不会跨越多个工作节点.

由于不能将多个进程都聚集在一个单独的容器中, 所以产生pod, 对他们当做一个单元进行管理.

在docker中 知道容器间是隔离的, 但在pod中我们要共享, 所以kubernetes是配置docker让一个pod中的容器都在一个namespace中. 而不是每一个容器都一个命名空间.
所以他们也共享相同的主机名和网络接口, 一个namespace的好处,
由于现在pod中容器都共享相同的IP和端口, 所以要注意容器中的进程不能绑定到相同的port中. 这个只涉及一个pod中. 不同的pod间不会冲突.
同时一个pod中的所有容器都具有相同的loopback网络接口, 因此容器可以通过localhost与同一个pod的其他容器进行通信.

集群中的所有pod都在一个共享网络地址空间, 意味着每个pod可以和其他pod进行相互访问, 包括不同node间的pod, 不用NAT

通过pod合理管理容器, 就是前后端应用服务器, 数据库的都放不同的pod中.
还有 扩容是基于pod的
何时在一个pod中用多个容器呢, 主要是主进程和辅进程.

何时在pod中放多个容器

  • 他们需要一起运行还是可以在不同的主机上运行
  • 他们代表一个整体还是相互独立的组件
  • 他们必须一起进行扩容还是可以分别进行

图3.4哈哈哈

  • 前后端在一个容器,一个pod中
  • 前后端在不同容器, 但在一个pod中
  • 前后端不同容器, 不容pod中

以YAML或JSON格式描述文件来创建pod

前面的命令行只允许你配置一组有限的属性. 通过YAML可以利用版本控制系统哦.

使用kuberctl get po kubia-xxx -o yaml来查看这个pod的YAML格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
apiVersion: v1                                  # kubernetes API版本
kind: Pod # kubernetes对象/资源
metadata: # pod元数据(名称, 标签, 注解)
creationTimestamp: 2019-03-08T08:39:21Z
generateName: kubia-
labels:
run: kubia
name: kubia-m9zfv
namespace: default
ownerReferences:
- apiVersion: v1
blockOwnerDeletion: true
controller: true
kind: ReplicationController
name: kubia
uid: ab300f02-417d-11e9-ae46-025000000001
resourceVersion: "82915"
selfLink: /api/v1/namespaces/default/pods/kubia-m9zfv
uid: ab4e1d22-417d-11e9-ae46-025000000001
spec: # pod规格 / 内容(pod的容器列表, volume等)
containers:
- image: ximage/kubia
imagePullPolicy: Always
name: kubia
ports:
- containerPort: 8080
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-xgtvp
readOnly: true
dnsPolicy: ClusterFirst
nodeName: docker-for-desktop
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: default-token-xgtvp
secret:
defaultMode: 420
secretName: default-token-xgtvp
status: # pod及其内部容器的详细状态
conditions:
- lastProbeTime: null
lastTransitionTime: 2019-03-08T08:39:21Z
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: 2019-03-08T08:48:18Z
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: 2019-03-08T08:39:21Z
status: "True"
type: PodScheduled
containerStatuses:
- containerID: docker://14a88ed822997fea0d94d0a19366082c6cdaf648510f56b6221bd9bd7fc38c2e
image: kubia:latest
imageID: docker-pullable://ximage/kubia@sha256:11b82b25e898ed75b9436654a243198d1c4e1e133d930f1c9dffed7c22a80aa4
lastState: {}
name: kubia
ready: true
restartCount: 0
state:
running:
startedAt: 2019-03-08T08:48:18Z
hostIP: 192.168.65.3
phase: Running
podIP: 10.1.0.8
qosClass: BestEffort
startTime: 2019-03-08T08:39:21Z)
介绍pod定义的主要部分
  • 首先是YAML中使用的kubernetes API 版本和 YAML中用来描述的资源类型
  • 其次是几乎在所有kubernetes资源中都可以找到的3大重要部分
    • metadata 包括名称, 命名空间, 标签和关于该容器的其他信息
    • spec 包含pod内容的实际说明, 例如pod的容器, 卷和其他数据
    • status 包含运行中的pod的当前信息(新建时不需要的), 例如pod所处的条件, 每个容器的描述状态, 以及内部IP和其他基本信息
创建一个简答的pod的YAML描述文件

kubia-manual.yaml 可以在任意目录下哦

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1              # kubernetes API v1
kind: Pod # 是一个pod
metadata: #
name: kubia-manual # pod名称
spec: # pod规格 / 内容(pod的容器列表, volume等)
containers:
- image: ximage/kubia # 容器所用镜像
name: kubia # 容器名
ports:
- containerPort: 8080 # 监听端口
protocol: TCP

上面在pod中指定port是展示性的, 客户端能否通过端口链接到pod和这个并没有多大关系, 只不过这个明确指定很有用, 后面还可以用来允许你为每个端口指定一个名称.

可以用kubectl explain pod来查看怎么写. 对象含有那些属性. kubectl explain pod.spec

使用kuberctl create命令从YAML文件中创建pod

1
2
$ kubectl create -f kubia-manual.yaml 
pod "kubia-manual" created

kubectl create -f用户从YAML或JSON中创建任何资源(不只是pod)

然后再看创建好的pod的yaml格式

kubectl get po kubia-manual -o yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: 2019-03-08T12:13:05Z
name: kubia-manual
namespace: default
resourceVersion: "95408"
selfLink: /api/v1/namespaces/default/pods/kubia-manual
uid: 8767870d-419b-11e9-ae46-025000000001
spec:
containers:
- image: ximage/kubia
imagePullPolicy: Always
name: kubia
ports:
- containerPort: 8080
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-xgtvp
readOnly: true
dnsPolicy: ClusterFirst
nodeName: docker-for-desktop
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: default-token-xgtvp
secret:
defaultMode: 420
secretName: default-token-xgtvp
status:
conditions:
- lastProbeTime: null
lastTransitionTime: 2019-03-08T12:13:05Z
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: 2019-03-08T12:13:09Z
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: 2019-03-08T12:13:05Z
status: "True"
type: PodScheduled
containerStatuses:
- containerID: docker://2c04c4f7551ac2e2e862722a780a4befe28d9037df0b74b8d0116c66ff4e1352
image: kubia:latest
imageID: docker-pullable://ximage/kubia@sha256:11b82b25e898ed75b9436654a243198d1c4e1e133d930f1c9dffed7c22a80aa4
lastState: {}
name: kubia
ready: true
restartCount: 0
state:
running:
startedAt: 2019-03-08T12:13:08Z
hostIP: 192.168.65.3
phase: Running
podIP: 10.1.0.11
qosClass: BestEffort
startTime: 2019-03-08T12:13:05Z

虽然看kubectl get po可以知道pod运不运行, 但还有是需求, 通过与pod的时机通信来确定其正在运行. 后面讨论

现在看下应用的日志来检查错误.

查看应用程序日志

容器化的应用程序通常会把日志记录到标准输出和标准错误流, 而不是将其写入文件.

docker logs <cid> 这种可以用

在kubernetes中可以用更方便的

1
2
3
4
5
6
7
8
9
10
11
$ kubectl logs kubia-manual
Kubia server starting ......

$ kubectl logs kubia-m9zfv
Kubia server starting ......
Received request from::ffff:192.168.65.3
Received request from::ffff:192.168.65.3
Received request from::ffff:192.168.65.3
Received request from::ffff:192.168.65.3
Received request from::ffff:192.168.65.3
Received request from::ffff:192.168.65.3

在我们向nodejs中的程序发送web请求前, 日志只显示一条Kubia server starting ......

获取多容器pod的日志时指定容器名称要加-c <容器名> 这个名称不能用docker ps看到, 而是你YAML文件中的spec.containers.name

1
2
$ kubectl logs kubia-manual -c kubia       
Kubia server starting ......

注意我们只能获取到仍然存在的pod的日志, 当一个pod被删除时, 他的日志也会被删除. 如果希望pod在删除之后也能获取到日志, 那么我们需要设置中心化吗集群范围的日志系统.

向pod中发送请求

kubectl get和日志显示该pod正在运行, 但我们如何在实际操作用看到该状态呢.
前一章用kubectl expose创建一个service, 以便在外部访问pod.
还有其他链接到pod以进行测试和调试的方法. 其中之一就是端口转发

将本地网络端口转发到pod中的端口

不通过service, 用端口 kubectl port-forward来

如将本地端口8888转发到我们的kubia-manual pod中的8080端口:

1
2
3
4
5
6
7
8
9
10
11
$ kubectl port-forward kubia-manual 8888:8080
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080

$ curl localhost:8888
You've hit kubia-manual

$ kubectl port-forward kubia-manual 8888:8080
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
Handling connection for 8888

图3.5

是一种测试特定pod有效的方法.

用标签组织pod

在node中有很多pod的时候, 打标签分类就很有用了.

标签不仅可以用来组织pod, 也可以组织kubernetes的其他资源,

只要标签的key在资源内是唯一的, 一个资源就可以拥有多个标签. 通常在我们创建资源的时候就会将标签附加到资源上, 后面也可以再打标签上去.

比如每个pod有2个标签

  • app: 指定pod属于哪个应用, 组件或微服务
  • rel: 显示在pod中运行的应用程序版本是stable, beta还是canary

这样就可以对原来的pod组织成2个维度, 从app角度和从版本角度.
图3.7

例子 在创建yaml文件时给pod带上标签.

一个kubia-manual-with-labels.yaml

只是多了label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1              # kubernetes API v1
kind: Pod # 是一个pod
metadata: #
name: kubia-manual-v2 # pod名称
labels: # 这里是新增的labels
creation_method: manual
env: prod
spec: # pod规格 / 内容(pod的容器列表, volume等)
containers:
- image: ximage/kubia # 容器所用镜像
name: kubia # 容器名
ports:
- containerPort: 8080 # 监听端口
protocol: TCP

使用命令创建后

1
2
$ kubectl create -f kubia-manual-with-labels.yaml
pod "kubia-manual-v2" created

使用带上标签的可以看pod各自标签

1
2
3
4
5
6
7
$ kubectl get po --show-labels
NAME READY STATUS RESTARTS AGE LABELS
kubia-m9zfv 1/1 Running 0 17h run=kubia
kubia-manual 1/1 Running 0 14h <none>
kubia-manual-v2 1/1 Running 0 1m creation_method=manual,env=prod
kubia-q89qr 1/1 Running 0 16h run=kubia
kubia-w22k7 1/1 Running 0 16h run=kubia

用 -L 来显示指定标签列.

1
2
3
4
5
6
7
$ kubectl get po -L creation_method,env
NAME READY STATUS RESTARTS AGE CREATION_METHOD ENV
kubia-m9zfv 1/1 Running 0 17h
kubia-manual 1/1 Running 0 14h
kubia-manual-v2 1/1 Running 0 3m manual prod
kubia-q89qr 1/1 Running 0 16h
kubia-w22k7 1/1 Running 0 16h

修改现有pod的标签

给原来的 kubia-manual 添加上一个标签.

1
2
$ kubectl label po kubia-manual creation_method=manual
pod "kubia-manual" labeled

给 kubia-manual-v2 修改 env 的标签为 debug

就是比前面加标签多一个 --overwrite

1
2
3
4
5
6
7
8
9
10
$ kubectl label po kubia-manual-v2 end=debug --overwrite
pod "kubia-manual-v2" labeled

$ kubectl get po -L creation_method,env
NAME READY STATUS RESTARTS AGE CREATION_METHOD ENV
kubia-m9zfv 1/1 Running 0 17h
kubia-manual 1/1 Running 0 14h manual
kubia-manual-v2 1/1 Running 0 7m manual prod
kubia-q89qr 1/1 Running 0 16h
kubia-w22k7 1/1 Running 0 16h

前面只是看pod有啥标签, 刷选具体标签的值, 这里用标签选择器来过滤pod子集

标签要和标签选择器一起用哦, 刷选的条件如下(就是key 和 value)

  • 包含或不包含使用特定键的标签 (in notin)
  • 包含具有特定键和值的标签 =
  • 包含有特定key的, 但value和我们指定的不同 !=

使用下 ,列出pod

这里用get 的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ kubectl get po -l creation_method=manual
NAME READY STATUS RESTARTS AGE
kubia-manual 1/1 Running 0 14h
kubia-manual-v2 1/1 Running 0 12m

$ kubectl get po -l env
NAME READY STATUS RESTARTS AGE
kubia-manual-v2 1/1 Running 0 13m

$ kubectl get po -l '!env'
NAME READY STATUS RESTARTS AGE
kubia-m9zfv 1/1 Running 0 17h
kubia-manual 1/1 Running 0 14h
kubia-q89qr 1/1 Running 0 16h
kubia-w22k7 1/1 Running 0 16h

就是 -l 后面的值

  • creatin_method!=manual
  • env in (prod, devel)
  • env notin (prod, devel)

在标签中使用多个条件

使用逗号, 来分隔多个条件. 这个标签还能用来一次性删除多个pod

使用标签来约束调度pod

不约束正是kubernetes的正确方式. 但某些情况下你想要约束下, 比如垃圾的机器上跑小应用, 高级的机器上跑大应用.
GPU, CPU机器区别对待. 但还是不会特别说明这个pod去哪个node上.

使用标签分类工作节点node

比如新建一个node, 这个node是一个计算节点. 所以我们可以打label

用法还是同pod上打标签的.

1
kubectl label node <node name> gpu=true
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1              # kubernetes API v1
kind: Pod # 是一个pod
metadata: #
name: kubia-gpu # pod名称
spec: # pod规格 / 内容(pod的容器列表, volume等)
nodeSelector: # 添加节点选择器,
gpu: "true" # 要求pod部署到包含标签gpu=true的节点上
containers:
- image: ximage/kubia # 容器所用镜像
name: kubia # 容器名
ports:
- containerPort: 8080 # 监听端口
protocol: TCP

创建pod的时候, 调度器会只在包含标签gpu=true的节点node上找, 然后在里面部署pod

调度pod到某一个node

每一个node有一个唯一的标签, key是kubernetes.io/hostname value是节点的实际主机名

但我们不考虑单节点, 而是考虑一个逻辑上的节点组, 保证能够pod调度成功

注解pod

除了标签, 其他都可以注解. 也没有注解选择器哦.
kubernetes也会自动添加一些注解的, 比如新特性.

查看对象的注解

现在看不到了

1
kubectl get po kubia-manual -o yaml

添加和修改注解

和标签一样的操作, 创建时可以添加, 也可以在之后对现有的pod进行操作.

用kubectl annotate

1
2
$ kubectl annotate pod kubia-manual mycompany.com.someannotation='foo bar'
pod "kubia-manual" annotated

mycompany.com.someannotation=’foo bar’ 是一个key: value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ kubectl describe pod kubia-manual
Name: kubia-manual
Namespace: default
Node: docker-for-desktop/192.168.65.3
Start Time: Fri, 08 Mar 2019 20:13:05 +0800
Labels: creation_method=manual
Annotations: mycompany.com.someannotation=foo bar # 这条
Status: Running
IP: 10.1.0.11
Containers:
kubia:
Container ID: docker://2c04c4f7551ac2e2e862722a780a4befe28d9037df0b74b8d0116c66ff4e1352
Image: ximage/kubia
Image ID: docker-pullable://ximage/kubia@sha256:11b82b25e898ed75b9436654a243198d1c4e1e133d930f1c9dffed7c22a80aa4
Port: 8080/TCP
Host Port: 0/TCP
State: Running
Started: Fri, 08 Mar 2019 20:13:08 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-xgtvp (ro)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
default-token-xgtvp:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-xgtvp
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events: <none>

3.7 使用命名空间对资源进行分组

回到标签. 看到标签是如何将pod和其他对象组织成组的, 每个对象可以有多个标签, 当然可以分成多个组. 在集群中, 如果我们没有明确指定标签选择器, 那么能够看到所有对象.

当你想将对象分隔成完全独立但有不重叠的组时, kubernetes提供一个命名空间, 但这个和linux的命名空间不一样, kubernetes中的命名空间只是简单地为对象名称提供了一个作用域.

所以我们不会讲所有的资源都放在一个命名空间中, 而是多个命名空间中, 这样可以允许我们多次使用相同的资源名称.跨不同的命名空间.

可以将大组件大系统接着拆

列出集群中所有命名空间及其pod

1
2
3
4
5
6
$ kubectl get ns
NAME STATUS AGE
default Active 2d
docker Active 2d
kube-public Active 2d
kube-system Active 2d

目前为止只是在default命名空间中操作. 使用kubectl get的时候并没有明确指定命名空间, 所以默认是default命名空间

看下其他命令空间的pod --namespace/-n

1
2
3
4
5
6
7
8
$ kubectl get po --namespace kube-system
NAME READY STATUS RESTARTS AGE
etcd-docker-for-desktop 1/1 Running 0 2d
kube-apiserver-docker-for-desktop 1/1 Running 0 2d
kube-controller-manager-docker-for-desktop 1/1 Running 0 2d
kube-dns-86f4d74b45-46hx7 3/3 Running 0 2d
kube-proxy-ffc8d 1/1 Running 0 2d
kube-scheduler-docker-for-desktop 1/1 Running 0 2d

保持区分不同的pod

创建一个命名空间

1
2
3
4
apiVersion: v1              # kubernetes API v1
kind: Namespace # 表示定义一个namespace
metadata:
name: custom-namespace # 命名空间的名字

还是用原来的命令

1
2
$ kubectl create -f custom-namespace.yaml
namespace "custom-namespace" created

更方便的是使用命令咯 kubectl create namespace custom-namespace

只是要注意命名规范

管理其他命名空间中的对象

如果想在刚创建的命名空间中创建资源, 可以选择在yaml文件的metadata字段添加一个namespace: custom-namespace属性, 也可以使用kubectl create -f kubia-manual.yaml -n custom-namespace 指定

这事我们有两个pod, 一个在default命名空间, 一个在custom-namespace命名空间

kubectl config可以配置

命名空间的隔离

不提供,至少不是开箱即用/ 尽管命名空间将对象分隔到不同的组, 只允许你对属于特定命名空间的对象进行操作, 但实际上命名空间之间并不提供对正在运行的对象的任何隔离.

比如不同对象在不同命名空间中部署pod, 你觉得他们是隔离的, 但这个取决于kubernetes所使用的网络解决方案.

停止和移除pod

按名称删除

实际上是告诉kubernetes终止该pod中的所有容器.

1
2
$ kubectl delete po kubia-w22k7 // 还能通过空格删多个
pod "kubia-w22k7" deleted

使用标签选择器来删pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ kubectl get po --show-labels
NAME READY STATUS RESTARTS AGE LABELS
kubia-drw74 1/1 Running 0 1m run=kubia
kubia-m9zfv 1/1 Running 0 19h run=kubia
kubia-manual 1/1 Running 0 15h creation_method=manual
kubia-manual-v2 1/1 Running 0 1h creation_method=manual,end=debug,env=prod
kubia-q89qr 1/1 Running 0 18h run=kubia

$ kubectl delete po -l creation_method
pod "kubia-manual" deleted
pod "kubia-manual-v2" deleted

$ kubectl get po --show-labels // 状态变了, 正在停止
NAME READY STATUS RESTARTS AGE LABELS
kubia-drw74 1/1 Running 0 2m run=kubia
kubia-m9zfv 1/1 Running 0 19h run=kubia
kubia-manual 1/1 Terminating 0 15h creation_method=manual
kubia-manual-v2 1/1 Terminating 0 1h creation_method=manual,end=debug,env=prod
kubia-q89qr 1/1 Running 0 18h run=kubia

通过删除整个命名空间删pod

kubectl delete ns custom-namespace

删除命名空间所有pod, 但命名空间要留着

通过--all删除当前命名空间的素有pod

1
2
3
4
5
6
7
8
9
10
$ kubectl get po
NAME READY STATUS RESTARTS AGE
kubia-drw74 1/1 Running 0 4m
kubia-m9zfv 1/1 Running 0 19h
kubia-q89qr 1/1 Running 0 18h

$ kubectl delete po --all
pod "kubia-drw74" deleted
pod "kubia-m9zfv" deleted
pod "kubia-q89qr" deleted

但是鸡儿还有3个, 但不是原来的名字的pod, 看AGE还是新建的. 问题是一开始我们用ReplicationController的问题, 她会保持3个配额的. 所以删除整个ReplicationController先

1
2
3
4
5
$ kubectl get po
NAME READY STATUS RESTARTS AGE
kubia-7xgdg 1/1 Running 0 46s
kubia-rh2vj 1/1 Running 0 46s
kubia-xkm76 1/1 Running 0 46s

删除命名空间中几乎所有资源

直接是all -all

1
2
3
4
5
6
7
$ kubectl delete all --all
pod "kubia-7xgdg" deleted
pod "kubia-rh2vj" deleted
pod "kubia-xkm76" deleted
replicationcontroller "kubia" deleted
service "kubernetes" deleted
service "kubia-http" deleted

第一个all是所有资源类型
第二个all是所有资源实例(并不是完全删, 还有一些会剩下)

4 章 副本机制和其他控制器

kubernetes的主要好处就是可一个kubernetes一个容器列表来由其保持容器在集群中的运行.

只要将pod调度到某个节点, 该节点上的kubelet就会运行pod的容器, 从此只要该pod存在, 就会保持运行.

4.1 存活探针

kubernetes可以通过使用探针 liveness probe 检查容器是否还在运行. 可以为pod中的每个容器单独指定存活探针, 如果探测失败, kubernetes将定期执行探针并重新启动容器.

还支持就绪探针 readiness probe 不要搞混.

有3种探测容器的机制

  • HTTP GET 探针对容器的IP地址(你指定的端口和路径)执行HTTP GET请求.
  • TCP 套接字探针尝试与容器指定端口建立TCP连接.
  • Exec 探针在容器内执行任意命令, 并检查命令的退出状态码.
创建HTTP的存活探针
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1              # kubernetes API v1
kind: Pod # 是一个pod
metadata: #
name: kubia-liveness # pod名称
spec: # pod规格 / 内容(pod的容器列表, volume等)
containers:
- image: luksa/kubia-unhealthy # 容器所用镜像
name: kubia # 容器名
livenessProbe: # 一个存活探针
httpGet:
path: / # http请求路径
port: 8080

然后就是创建, 接着查看pod

还有查看log, 加上—previous可以看前一个容器的日志.

在用describe中可以看到错误码 Exit code 137这种. 还有附加信息显示. 在yaml中也可以配置
137是128+x

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1              # kubernetes API v1
kind: Pod # 是一个pod
metadata: #
name: kubia-liveness # pod名称
spec: # pod规格 / 内容(pod的容器列表, volume等)
containers:
- image: luksa/kubia-unhealthy # 容器所用镜像
name: kubia # 容器名
livenessProbe: # 一个存活探针
httpGet:
path: / # http请求路径
port: 8080
initialDelaySeconds: 15 # 会在第一次探测前等待15秒

一般都会设置延迟, 保证应用程序已经启动了running的状态

4.2 了解ReplicationController

ReplicationController是一种 kubernetes 资源, 可确保它的pod始终保持运行.

图4.1

ReplicationController的操作

ReplicationController不是根据pod类型来操作的, 而是根据pod是否匹配某个标签选择器
他的工作是确保pod的数量始终与其标签选择器匹配.

有3个部分:

  • label selector: 用于确定 ReplicationController 作用域中有哪些pod
  • replica count: 指定运行的pod数量
  • pod template: 用于创建新的pod副本模板

图4.3

都可以随时修改, 只有副本数会影响现有的pod, 更改标签和模板不会对于现有pod没有影响. 更改标签只是会使现有的pod脱离ReplicationController的范围, ReplicationController也不影响pod的内容, 在模板影响的是ReplicationController创建新的pod

使用ReplicationController的好处是:

  • 确保一个pod持续运行, 现有的pod丢失时会重启一个新的pod
  • 集群节点故障时, 会为故障节点上运行的所有pod创建替代副本
  • 能轻松实现pod的水平伸缩

4.2.2 创建一个ReplicationController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1              # kubernetes API v1
kind: ReplicationController # 是一个ReplicationController
metadata: #
name: kubia # 名称
spec:
replicas: 3
selector: # pod选择器决定了RC的操作对象
app: kubia
template:
metadata:
labels: # 这里的标签要和selector的对应
app: kubia
spec:
containers:
- name: kubia # 容器名
image: ximage/kubia # 容器所用镜像
ports:
- containerPort: 8080 # 监听端口

kubernetes会创建一个名为kubia的新的 ReplicationController , 确保符合标签选择器app: kubia的pod实例始终未3个.
模板中的pod标签显然必须和 ReplicationController 的标签选择器匹配, 否则会无休止地创建地新的容器. API服务也会校验的.
不需要指定pod选择器

4.2.3 使用 ReplicationController

1
2
3
4
5
6
7
kubectl get pods

kubectl delete pod <pod名> 看下ReplicationController生效没

kubectl get rc

kubectl describe rc kubia

控制器对删除操作的反应是新建一个, 但他没有对删除本身做出反应, 而是针对由此产生的状态 - pod数量不足.
虽然ReplicationController会立即收到删除pod的通知, 但这个不是他创建替代pod的原因, 改通知会触发控制器检查实际的pod数量并采取适当的措施.

模拟节点故障, 多个节点的情况下, minikube和docker不性, 用sudo ifconfig eht0 down关网卡

1
2
kubectl get node    # 看status NotReady 因为网断开了
kubectl get pods # 看status 变unknown 因为无法访问

4.2.4 将pod移入或移出ReplicationController的作用域

由ReplicationController创建的pod并不是绑定到ReplicationController, 而是管理与标签选择器匹配的pod, 所以通过selector

虽然一个pod没有绑定到ReplicationController, 但该pod的meta.ownerReference中引用了, 同个这个字段找pod属于哪个ReplicationController

在你改了一个pod的标签后, 就不归原来的ReplicationController管了, 只不过ReplicationController发现它少了一个pod后, 会重新启一个新的pod

1
2
3
4
5
kubectl label pod kubia-dmdck type=special

kubectl get pods --show-labels

kubectl label pod kubia-dmdck app=foo --overwrite

--overwrite是必要的, 防止你错改标签, 记住该标签只是第一步

如果直接改了ReplicationController的标签选择器呢, 相当于原来的pod都脱离控制, 但还是运行, 然后ReplicationController再创建副本数量的新的pod

4.2.5 修改pod模板

ReplicationController的pod模板可以随时修改, 更改pod的模板只会影响后面创建的pod, 不会影响原来的, 不要原来的你可以删掉.
图4.6

1
kubectl edit rc kubia     # 弹出一个yaml配置的, 修改

配置KUBE_EDITOR环境变量来告诉kubectl你要用的文本编辑器

4.2.6 水平缩放pod

扩容缩容的意思

1
2
3
4
5
kubectl scale rc kubia --replicas=10

kubectl edit rc kubia # 通过编辑定义 找到spec.replicas

kubectl get rc

4.2.7 删除

通过kubectl delete 删除ReplicationController时, pod也会被删除, 但由于由 ReplicationController 创建的pod不是 ReplicationController 的组成部分, 只是由其所管理, 因此可以只删除 ReplicationController 而保持pod运行.

在kubectl delete时增加--cascade=false来保持pod的运行.

1
kubectl delete rc kubia --cascade=false

4.3 使用ReplicaSet而不是ReplicationController

新一代的ReplicationController. 通常不会直接创建他, 而是在创建更高层级的Deployment资源时自动创建他们.

ReplicaSet的行为和ReplicationController完全相同, 但pod选择器的表达能力更强.
ReplicationController的标签选择器只允许包含某个标签的匹配pod, 但ReplicaSet的选择器还允许匹配缺少某个标签的pod, 或包含特定签名的pod, 不管其值如何.

如ReplicationController不能同时匹配env=1和env=2的, 只能2选1, 但ReplicaSet可以
ReplicationController不能基于标签名字来, 可以理解为env=*

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1beta2    # 新的版本号
kind: ReplicationSet # 是一个ReplicationSet
metadata: #
name: kubia # 名称
spec:
replicas: 3
selector:
matchLabels:
app: kubia # 使用了更简单的matchLabels选择器
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia # 容器名
image: ximage/kubia # 容器所用镜像
ports:
- containerPort: 8080 # 监听端口

注意这里不是v1 API的一部分, 因此在创建资源时要指定正确的apiVersion
还有唯一区别就是在选择器上, 不必在selector属性中直接列出pod需要的标签, 而是在selector.matchLabels下指定他们

关于API版本的属性
apiVersion属性指定两件事情

  • API组(apps)
  • 实际的API版本(v1beta2)

后面会看到某些kubernetes资源位于所谓的核心API组中, 改组不需要在APIVersion字段中指定
有好几个API组.

也是一样通过kubectl create创建后通过kubectl get rs和kubectl describe rs来检查

前面的matchLabels没啥区别, 用matchExpressions属性来重写选择器.

1
2
3
4
5
6
selector:
marchExpressions:
- key: app # 选择器要求该pod包含名为app的标签
operator: In
values: # 标签的值必须是kubia
- kubia

这里给选择器添加额外的表达式, 每个表达式必须包含一个key, 一个operator, 可能还有一个values(取决于运算符)
4个有效的运算符

  • In: label的值必须与其中一个指定的values匹配.
  • NotIn: label值与任何指定的values不匹配
  • Exists: pod必须包含一个指定名称的标签, 值不重要, 使用这个运算符时, 不应指定values字段
  • DoesNotExist: pod不得包含指定名称的标签, values也不能有.

指定了多个表达式时, 这些所有的运算符都得为true时才能使选择器和pod匹配.
如果同时指定了matchLabels和matchExpressions, 则所有标签必须要匹配, 表达式也要匹配.

删除也一样, kubectl delete rs kubia

4.4 使用DaemonSet在每一个节点上运行一个pod

ReplicaSet的行为和ReplicationController在kubernetes集群上运行部署特定数量的pod, 当你希望pod在每个节点上运行时.
这些情况包括pod执行系统级别的与基础结构相关的操作. 例如希望在每个节点上运行日志收集器和资源监控器, 另一个例子是kubernetes的kube-proxy进程
在kubernetes之外, 此类进程通常在节点启动期间, 通过系统初始化脚本或systemd守护进程启动. 当然可以在kubernetes节点上用systemd运行系统进程, 但这样就不能利用所有的kubernetes特性了.

用DaemonSet好了, 除了又DaemonSet创建的pod, 已经有一个指定的目标节点并跳过kubernetes调度程序.
DaemonSet确保能够创建足够的pod, 并在自己的节点上部署每个pod
图4.8

尽管ReplicaSet确保集群中存在期望数量的pod副本, 但DaemonSet并没有期望的副本数的概念, 他不需要, 因为他的工作是确保一个pod匹配它的选择器并在每个节点上运行.

如果节点下线, DaemonSet不会在其他地方重新创建pod, 如果新节点加入或删了pod, 那么会重新创建一个新的pod

4.4.2 使用DaemonSet只在特定节点上运行pod

通过在pod模板的nodeSelector属性上知道你个

后面可以设置节点为不可调度, 防止pod被部署到节点上. 但DaemonSet可以将节点部署到这些不可调度的节点上, 因为无法调度的属性只会被调度器使用, 而DaemonSet管理的pod则完全绕过调度器, 这是预期的, 因为DaemonSet的目的是运行系统服务, 即使是在不可调度的节点上, 系统服务通常也需要运行.

图4.9是一个ssd-monitor的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: apps/v1beta2    # 新的版本号
kind: DaemonSet # 是一个DaemonSet
metadata: #
name: ssd-monitor # 名称
spec:
selector:
matchLabels:
app: ssd-monitor
template:
metadata:
labels:
app: ssd-monitor
spec:
nodeSelector: # pod模板会包含一个节点选择器, 会选择有disk=ssd标签的节点
disk: ssd
containers:
- name: main # 容器名
image: luksa/ssd-monitor # 容器所用镜像

这个DaemonSet将运行一个基于luksa/ssd-monitor容器镜像的单容器pod, 该pod的实例在每个具有disk=ssd标签的节点上创建

1
2
3
4
5
kubectl create -f ssd-monitor.yaml

kubectl get ds

kubectl get po

这事并没有pod, 还需要给节点打上disk=ssd的标签, 打上标签后, DaemonSet将检测到节点的标签已经更改, 并将pod部署到有匹配标签的所有节点

1
kubectl label node minikube disk=ssd

删除节点, 就改下标签名

1
kubectl label node minikube disk=hdd --overwrite

删除DaemonSet也会删除pod的

目前为止的都是持续运行的pod, ReplicationController和ReplicaSet和DaemonSet都是持续运行, 永远不会达到完成状态, pod退出后再重新启动, 一个可完成任务是进程终止后不应该再重新启动的.

后面就是job资源, 允许你运行一种pod, 改pod在内部进程成功结束时, 不重启容器, 一旦任务完成, pod就被认为处于完成状态.
如果是异常退出, 可以按照ReplicaSet的pod方式重新安排到其他节点, 如果是进程本身异常退出, 错误退出码, 可以将job配置为重新启动容器
对临时任务很有用, 关键是任务要以正确的方式结束.

4.5 job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: batch/v1        # 新的版本号
kind: Job # 是一个DaemonSet
metadata: #
name: batch-job # 名称
spec:
template: # 没有指定pod选择器, 根据模板创建
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure # job不能使用Always为默认的重新启动策略, 三种还有Never
containers:
- name: main
image: luksa/batch-job
1
2
3
4
5
6
7
kubectl get jobs

kubectl get po

kubectl get po -a # --show-all 显示completed

kubectl logs batch-job-28

作业可以配置创建多个pod实例, 并可以串行或并行来运行他们, 通过配置completions和parallelism属性

顺序运行job pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: batch/v1        # 新的版本号
kind: Job # 是一个DaemonSet
metadata: #
name: multi-completino-batch-job # 名称
spec:
completions: 5 # 在这里指定次作业顺序运行5个pod
template:
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure # job不能使用Always为默认的重新启动策略, 三种还有Never
containers:
- name: main
image: luksa/batch-job

是指正确运行5个, 如果pod有失败的, 会重新启动

下面可以指定同时有2个可以并行, 一共完成5个

1
2
3
4
5
6
7
apiVersion: batch/v1        # 新的版本号
kind: Job # 是一个DaemonSet
metadata: #
name: multi-completino-batch-job # 名称
spec:
completions: 5 # 在这里指定次作业顺序运行5个pod
parallelism: 2 # 最多两个pod可以并行运行

缩放也是

1
kubectl scale job multi-completion-batch-job --replicas 3

设置完成时间 用activeDeadlineSeconds属性 设置超时时间, 并标记为失败

可以配置job manifest的spec.backoffLimit字段来配置失败前的重试次数, 默认6次

4.6 安排定期运行或在将来运行一次

job一般是在创建时立即运行pod, 有些批处理的, 需要在特定时间运行或在指定时间间隔内重复运行. 在linux系统内这些任务称为cron任务

kubernetes的cron任务通过CronJob资源进行配置. 用cron格式指定.
在配置时间时, kubernetes将根据在CronJob对象中配置的job模板创建job资源, 创建job资源时, 将根据任务的pod模板创建并启动一个或多个pod副本

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: batch/v1beta1         # 新的版本号
kind: CronJob # 是一个CronJob
metadata:
name: batch-job-every-fifteen-minutes # 名称
spec:
schedule: "0,15,30,45 * * * *" # 在每天每小时的15, 30, 45, 分钟运行
jobTemplate:
spec:
template:
metadata:
labels:
app: periodic-batch-job
spec:
restartPolicy: OnFailure # job不能使用Always为默认的重新启动策略, 三种还有Never
containers:
- name: main
image: luksa/batch-job

并不是特别复杂

cron时间格式表
从左到右包含5个

  • 分钟
  • 小时
  • 每月中的第几天
  • 月
  • 星期几

"0,15,30,45 * * * *" 看星号

在计划的时间内, CronJob资源会创建job资源, 然后job创建pod
可能发生job或pod创建并运行得相对较晚的情况, 可以有要求, 任务开始不能落后预定的时间过多, 通过startingDeadlineSeconds来指定截止日期

1
2
3
4
5
6
7
8
apiVersion: batch/v1beta1         # 新的版本号
kind: CronJob # 是一个CronJob
metadata:
name: batch-job-every-fifteen-minutes # 名称
spec:
schedule: "0,15,30,45 * * * *" # 在每天每小时的15, 30, 45, 分钟运行
startingDeadlineSeconds: 15 # pod最迟必须在预定时间后15秒开始运行
jobTemplate:

第5章:服务 让客户端发现pod并与之通信

  • 创建服务资源, 利用单个地址访问一组pod
  • 发现集群中的服务
  • 将服务公开给外部客户端
  • 从集群内部连接外部服务
  • 控制pod是与服务关联
  • 排除服务故障

现在已经学习过了pod, 以及如何通过ReplicaSet和类似资源部署运行.尽管特定的pod可以独立地应对外部刺激, 现在大多数应用都需要根据外部请求做出响应.例如就微服务而言, pod通常需要对来自集群内部其他pod,以及来自集群外部的客户端HTTP请求做出响应.

pod需要一种寻求其他pod的方法来使用其他pod提供的服务, 不像在没有kubernetes的世界, 没有那种指定IP地址的方法

  • pod是短暂的: 他们会随时启动或者关闭, 无论为了给其他pod提供空间而从节点中被移除 或者是减少了pod的数量, 又或者是因为集群中存在节点异常
  • kubernetes在pod启动前会给已经调度到节点上的pod分配IP地址: 因此客户端不能提前知道提供服务的pod的IP地址
  • 水平伸缩意味着多个pod可能会提供相同的服务: 每个pod都有自己的IP地址, 客户端无需关心后端提供服务pod的数量, 以及各自应对的IP地址, 他们无须记录每个pod的IP地址, 相反, 所有的pod可以通过一个单一的IP地址进行访问.

解决上述问题, kubernetes提供了一种资源, 叫服务service

服务是一种为一组功能相同的pod提供单一不变的接入点的资源. 当服务存在时, 它的IP地址和端口不会改变.
图5.1

服务的后端可以有不止一个pod, 服务的连接对所有的后端pod是负载均衡的.

使用kubectl expose来创建服务, 还可以用yaml文件

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- port: 80 # 该服务的可用端口
targetPort: 8080 # 服务将连接转发到容器端口
selector: # 具有app=kubia标签的pod都属于该服务
app: kubia

创建了一个叫kubia的服务, 他将在端口80接受请求并将连接路由到具有标签选择器是app=kubia的pod的8080端口上

用kubectl create创建后, kubectl get svc看下服务

会分配一个集群内的IP地址, 只能在集群内部可以被访问. 服务的主要目标就是使用集群内部的其他pod可以访问当前这组pod, 但通常也希望对外暴露服务.

从内部集群测试服务

  • 显而易见的方法是创建一个pod, 他将请求发送到服务的集群IP并记录响应, 可以通过查看pod日志检查服务的响应
  • 可使用ssh远程登录到其中一个kubernetes节点上, 然后使用curl命令
  • 可以通过kubectl exec命令在一个已经存在的pod中执行curl命令

使用kubectl get pod列出所有pod, 选择其中一个作为exec的执行目标

1
kubectl exec kubia-7nog -- curl -s http://10.111.249.154

--双横杠代表kubectl命令项的结束, 在两个横杠之后的内容是指在pod内部需要执行的命令.
-s表示需要连接一个不同的API服务器而不是默认的
图5.3

通常如果多次执行同样的命令, 每次调用执行应该在不同的pod上, 因为服务代理通常将每个连接随机指向选中的后端pod中的一个, 即使连接来自同一个客户端.
如果希望特定客户端产生的所有请求每次都指向同一个pod, 可以设置服务的sessionAffinity属性为ClientIP而不是None默认值

1
2
3
4
apiVersion: v1
kind: Service
spec:
sessionAffinity: ClientIP

kubernetes仅支持两种形式的会话亲和性服务: None和ClientIP, 不支持cookie的会话哦, 因为kubernetes不是在HTTP层面上.服务处理TCP和UDP包,并不关心其中的内容, cookie是HTTP的一部分, 所有服务并不知道他们.

创建一个服务可暴露一个端口, 也可以暴露多个端口. 通过一个集群IP, 使用一个服务就可以将多个端口全部暴露出来.

创建一个有多个端口的服务的时候, 必须给每个端口指定名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
app: kubia

标签选择器应用于整个服务, 不能对每个端口做单独的配置. 如果不同的pod有不同的端口映射关系, 需要创建两个服务.

使用命名的端口

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
spec:
containers:
- name: kubia
ports:
- name: http # 端口8080被命名为http
containerPort: 8080
- name: https
containerPort: 8443
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
spec:
ports:
- name: http
port: 80
targetPort: http # 将端口80映射到容器中被称为http的端口
- name: https
port: 443
targetPort: https
selector:
app: kubia

最大的好处就是即使更换端口号也无需更改服务spec

5.1.2服务发现

通过创建服务, 现在就可以通过一个单一稳定的IP地址访问到pod.
客户端pod如何知道服务的IP和端口, 是否需要先建服务, 然后手动, 不是, kubernetes还为客户端提供了发现服务的IP和端口的方式

通过环境变量发现服务: 在pod开始运行的时候, kubernetes会初始化一系列的环境变量指向现有存在的服务.如果你创建的服务早于客户端pod的创建, pod上的进程可以根据环境变量获得服务的IP地址和端口号.

在一个运行pod上检查环境, 去了解这些环境变量. 比如现在已经了解到了通过kubectl exec在pod上运行一个命令, 但由于服务的创建晚于pod的创建, 所有有关服务的变量并没有设置. 要解决这个问题.

在查看服务的环境变量之前, 首先需要删除所有的pod使ReplicationController创建全新的pod, 在无须知道pod的名字下删除所有pod

kubectl delete po --all

然后列出所有的新pod, 然后选择一个运行kubectl exec进入, 运行env列出所有的环境变量.

kubectl exec kubia-1ni env

会有

1
2
KUBIA_SERVICE_HOST=10.111.249.153   # 服务的集群IP
KUBIA_SERVICE_PORT=80 # 服务所在端口

通过kubectl get svc可以看有哪些服务, 用env看服务都是名字开头的, 代表KUBIA服务的IP地址和端口号.

比如前面的前端获取后端的, 就是靠这个知道访问的IP和端口.

服务是 下划线, 全大写.

环境变量是获得服务IP地址和端口号的一种方式, 还有DNS

在kube-system命名空间下列出所有的名称, 有一个pod被称为kube-dns, 这个pod就运行DNS服务, 在集群中的其他pod都被配置成使用其作为dns(可以通过修改每个容器的/etc/resolv.conf文件来实现)
运行在pod上的进程DNS查询都会被kubernetes自身的DNS服务器响应, 该服务器知道系统中运行的所有服务.

pod是否使用内部的DNS服务器是根据pod中spec的dnsPolicy属性来定义的.

通过FQDN连接服务

前端的pod可以通过打开以下FQDN的链接来访问后端数据库服务backend-database.default.svc.cluster.local
backend-database对应于服务名称, default表示服务在其中定义的名称空间, svc.cluster.local是在所有集群本地服务名称中使用的可配置集群域后缀.

注意, 客户端仍然必须知道服务的端口号, 如果服务使用标准端口号(比如HTTP 80, Postgres 5432),这样没问题, 如果并不是标准端口, 客户端可以从环境变量中获取端口号.

连接一个服务可能比这更简单, 如果前端pod和数据库pod在同一个命名空间下, 可以省略svc.cluster.local后缀, 甚至命名空间.因此可以使用backend-database来指代服务

在pod容器中运行shell

通过kubectl exec在一个pod容器上运行bash, 加上-it

kubectl exec -it kubia-3inly bash 进入后再使用curl http://kubia.default.svc.cluster.local

curl http://kubia.default, curl http://kubia

在请求的URL中, 可以将服务的名称作为主机名来访问服务, 因为根据每个pod容器DNS解析器配置的方法, 可以将命名空间和svc.cluster.local后缀省略掉, 看容器的/etc/resilv/conf文件

无法ping通服务IP的原因, curl是可以的, ping不行, 因为服务的集群IP是一个虚拟IP, 并且只有在于服务端口结合时才有意义.

5.2 连接集群外部的服务

希望通过kubernetes服务特性暴露外部服务的情况, 不要让服务将连接重定向到集群中的pod, 而是让他重定向到外部IP和端口.
这样做可以让你充分利用服务负载平衡和服务发现. 在集群中运行的客户端pod可以像连接到内部服务一样连接到外部服务.

5.2.1 服务endpoint

再说下服务, 服务并不是和pod直接相连的,相反, 有一种资源介于两者之间, 他就是endpoint, 用kubectl describe svc kubia可以看到有个Endpoints

Endpoint资源就是暴露一个服务的IP地址和端口列表, 和其他kubernetes资源一样, 都可以用kubectl info来看 kubectl get endpoint kubia
尽管在spec服务中定义了pod选择器, 但在重定向传入连接时不会直接使用它, 相反, 选择器用于构建IP和端口列表, 然后存储在Endpoint资源中. 当客户端连接到服务器时, 服务代理选择这些IP和端口对中的一个, 并将传入连接重定向到该位置监听的服务器.

5.2.2 手动配置服务的endpoint

服务的endpoint与服务解耦后, 可以分别手动配置和更新他们.

5.3 将服务暴露给外部客户端

3种

  • 将服务的类型设置成NodePort
  • 将服务的类型设置为LoadBalance
  • 创建一个Ingress资源

5.4 通过Ingress暴露服务

图5.9

要有Ingress控制器才能控制Ingress资源

kubectl get po --all-namespace

kubectl get ingresses

配置Ingress处理TLS传输 HTTPS

kubectl apply -f kubia-ingress-tls.yaml使用文件中指定内容来更新Ingress资源, 而不是通过删除并从新文件重新创建的方式.

就绪探针

参考

基于Docker for macOS的Kubernetes本地环境搭建与应用部署
Docker集群编排工具之Kubernetes(K8s)介绍、安装及使用
DOCKER FOR MAC WITH KUBERNETES
Kubernetes中文社区 | 中文文档

yarn的使用

发表于 2019-03-05 | 分类于 前端教程

yarn的使用

使用

初始化

初始化新项目

1
yarn init

添加依赖包

1
2
3
yarn add [package]
yarn add [package]@[version]
yarn add [package]@[tag]

将依赖项添加到不同依赖项类别

分别添加到 devDependencies、peerDependencies 和 optionalDependencies:

1
2
3
yarn add [package] --dev/-D
yarn add [package] --peer/-P
yarn add [package] --optional/-O

全局安装

1
yarn global add <package...>

注意:yarn add global <package...>会变成本地安装,注意顺序。

升级依赖包

1
2
3
yarn upgrade [package]
yarn upgrade [package]@[version]
yarn upgrade [package]@[tag]

移除依赖包

1
yarn remove [package]

安装项目的全部依赖

1
2
3
yarn
// 或者
yarn install

查看

yarn cache

运行 yarn cache dir会打印出当前的 yarn 全局缓存在哪里。

yarn cache list --pattern <pattern> 将列出匹配指定模式的已缓存的包。

示例:yarn cache list --pattern "gulp-(match|newer)"

yarn cache clean运行此命令将清除全局缓存。

将在下次运行 yarn 或 yarn install 时重新填充。

yarn list

yarn list [--depth] [--pattern]

默认情况下,所有包和它们的依赖会被显示。 要限制依赖的深度,你可以给 list 命令添加一个标志 --depth 所需的深度。
示例: yarn list --depth=0

运行

yarn run

yarn run [script] [<args>]

如果你已经在你的包里定义了 scripts,这个命令会运行指定的 [script]。例如:
运行这个命令会执行你的 package.json 里名为 "test" 的脚本。

参考

官网使用
yarn 常用命令
命令列表

travis配置文件的编写

发表于 2019-03-05 | 分类于 前端教程

travis配置文件的编写

介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
language: node_js
dist: trusty
sudo: required
matrix:
include:
- language: node_js
node_js:
- 8.15.0
env:
- TRAVIS_SECURE_ENV_VARS=false
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- mysql-5.7-trusty
packages:
branches:
only:
- master
- dev
before_install:
- echo -e "machine github.com\n login $CI_USER_TOKEN\n password x-oauth-basic" >> ~/.netrc
# Repo for Yarn
- sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg
- echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
- sudo apt-get update -qq
- sudo apt-get install -y -qq yarn=1.12.3-1
script:
- node -v
- yarn -v
- ./travis-script.sh
cache:
yarn: true
directories:
- $TRAVIS_BUILD_DIR/project-template/client/node_modules

sudo

需不需要sudo权限 一般是要的require

matrix

相当于就是一个几乘几的各种情况, 自动给你跑, 也可以include, 也可以exclude下.

exclude的要精确匹配 env 这种

addons

额外的软件包

env

就是环境咯, TRAVIS_SECURE_ENV_VARS=false不加密

branch

要进行CI的分支, 可以写safelistonly和blocklistexcept

在before_install下

第一条命令: echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc, 用来将登陆配置信息追加写入~/.netrc中, 方便以后登录不用填用户名密码.
travis API Token machine github.com\n

第二条命令: apt-key adv --fetch-keys will only fetch one key from the URL, and if the URL contains multiple keys, please use wget | apt-key add instead.

第三条命令: tee用来从标准输入中读, 然后便准输出. 就是一个复制粘贴的功能. 所以后面就是将 deb http://dl.yarnpkg.com/debian/ stable main 写入 yarn.list 中

第四第五条命令: 就是更新软件库表, 然后安装上 yarn

整个第2-5条命令就是在安装yarn https://yarnpkg.com/en/docs/install#debian-stable

script

就是这个travis要跑的脚本.

cache

就是cache, 看教程就是这么写的, $TRAVIS_BUILD_DIR就是当前.travis.yml所在的目录.

参考

持续集成服务 Travis CI 教程 阮一峰 666

Building a JavaScript and Node.js project

travis API Token machine github.com\n
.netrc文件简单使用
使用auth-source库读取Netrc文件中的用户名和密码
linux >和>>的区别

apt-get 命令详解(中文),以及实例
https://yarnpkg.com/en/docs/install#debian-stable

prettier代码格式美化

发表于 2019-02-20 | 分类于 vsc插件

prettier 代码格式美化

从EditorConfig到各种***Lint, 再到 prettier。 其实就是一起用

为什么要用 Prettier

用来替代*lint中的一些场景,比如说分号/tab 缩进/空格/引号,这些在 lint 工具检查出问题之后还需要手动修改,而通常这样的错误都是空格或者符号之类的,这样相对来说不太优雅,利用格式化工具自动生成省时省力。

搞起

建议过一遍 prettier 的官方文档, 也不用太多. 主要是 配置文件, 以及使用的参数

在项目中

为什么用 Prettier 66

安装完依赖, npm install --save-dev prettier后, 执行常见的.

1
2
3
node_module/.bin/prettier --single-quote --trailing-comma es5 --write "{app,__{tests,mocks}__}/**/*.js"
或
node_module/.bin/prettier -l --write 'src/**/*.{ts,tsx,less,css}' --no-semi --single-quote

当然常见的是写一个 npm script 在 package.json 中加上

1
2
3
"scripts": {
"fmt": "prettier -l --write 'src/**/*.{ts,tsx,less,css}' --no-semi --single-quote"
},

然后建议使用 prettier.config.js js 文件来配置, 不用 json 是因为我喜欢写点注释说明.

各种不同格式的 prettier 配置文件

官网上的prettier 文件

  • A .prettierrc file, written in YAML or JSON, with optional extensions: .yaml/.yml/.json.
  • A .prettierrc.toml file, written in TOML (the .toml extension is required).
  • A prettier.config.js or .prettierrc.js file that exports an object.
  • A "prettier" key in your package.json file.

常见的 4 种吧, 首先是.prettierrc, 你可以在里面用YAML or JSON格式来写, 其次是.prettierrc.toml, 用TOML格式来写 算一类吧, 我们常见的用JSON写
还有就是用prettier.config.js or .prettierrc.js这种导出一个对象来写, (推荐使用)
最后就是在package.json中的"prettier", 一般就是在script中写呗, 不会单独搞一个属性

类似其他的工具, 你也会常看到 .editorconfig, .babelrc, .eslintrc, .prettierrc 这种格式的文件.

读取顺序

配置完后你也可以修改下 vsc 格式化的快捷键
先从 vsc 的市场中可以看到, setting 会先从

  1. prettier 的配置文件读, 也就是前面说的 4 种
  2. 没有的话从.editorconfig这个小老鼠读
  3. 最后才是从默认的 vsc 的 prettier setting

下面链接中讲了为啥用 lint 和 format

这个讲了 lint 和 format Code Formatting 6666 各种配置

对于直接使用命令 CLI 的方式的话

建议只是用 2 种方式:

  • -l 会打印出 prettier 格式化前后不同的文件名.
  • --write 就地改正, 按设置的 prettier 的格式

prettierc 常用命令参数

基本命令就是

1
prettier [opts] [filename ...]

实际中常用

1
prettier --single-quote --trailing-comma es5 --write "{app,__{tests,mocks}__}/**/*.js"

默认是不会访问node_modules文件夹中的, 需要访问的话加参数--with-node-modules

CLI 的一些其他参数

讲的主要是比option多一点的东西.

--check或-c

就是 check 下文件是否格式化了, 也经常配合在pre-commit hook前使用,
如果是想讲这些没格式化的文件输出给下一个命令, 就用--list-different. 两者还是有区别的

--debug-check

如果你怕prettier改变你正确的代码, 那么用这个. 当然--write是不能配合这个用了.

--find-config-path 和 --config

当你经常去格式化那些指定目录的文件时, prettier会先尝试读取配置文件的信息, 造成性能有点缺失, 所以你可以用这个命令配置好, 先读第一次, 之后就重用第一次的配置.

1
2
prettier --find-config-path ./my/file.js
./my/.prettierrc // 这是配置文件

而这里是, 直接提供一个配置文件路径--config, 当然可以任意指定配置文件在哪了.

1
prettier --config ./my/.prettierrc --write ./my/file.js

不想要配置文件用--no-config, 默认配置就是不查找.

--ignore-path

用来忽略一些不需要格式化的目录中的文件, 比如 ./.prettierignore

--require-pragma

pragma 指需要一些特别的 comment.

1
2
3
/**
* @prettier // @prettier 或 @format
*/
--insert-pragma

在没有 pragma 的时候会插入@format这种 pragma 到格式化文件最顶端, 配合--require-pragma一起用

--list-different或-l

这是另一个常用的 flag, 会打印出 prettier 格式化前后不同的文件名.

1
prettier --single-quote --list-different "src/**/*.js"

也可以用--check, 这个打印出跟人性化的信息

--config-precedence

config file 文件中的配置优先级高还是 CLI options 中的高

  • config file 指的是配置文件
  • CLI options 指的是输入的命令参数

  • cli-override (default)

    • 默认 cli 中高, 也就是默认的 prettier 的配置高
  • file-override
    • config file 中的高
  • prefer-file
    • 如果配置文件找到了, 就按配置文件, 没找到就用 CLI 的

常用来整合编辑器中的, 比如用户定义了自己的配置, 但还是尊重项目的定的配置.

--no-editorconfig

不考虑小老鼠了, 具体看API中的信息

--with-node-modules

默认不会访问, 加了后就去访问格式化咯.

--write

就地改正, 按 prettier 的格式

--loglevel

改一下 CLI 的 log 等级

  • error
  • warn
  • log (default)
  • debug
  • silent
--stdin-filepath

prettier CLI 当做stdin的文件路径, 比如

abc.css

1
2
3
.name {
display: none;
}

shell

1
2
3
4
$ cat abc.css | prettier --stdin-filepath abc.css
.name {
display: none;
}

prettier 的默认配置

前面新建了一个 prettier.config.js 配置文件后可以参考这个设置

options.

默认的 option

prettier 的配置选项(参数)官网直译

总结下就是

  • 一行超过多少个字符换行, markdown 也需要强制换行么. 末尾加不加分号, 换行后>在哪
  • 对象字面量前后加空格么, 最后一个属性加逗号么, 单引号代替双引号么, 箭头寒素参数括号么.
  • 缩进用 tab 还是空格, 一个 tab 几个空格
  • html 默认, endOfLine 默认, parser, filePath, pragma. range 默认
  • 关注点还是末尾 js 加分号, tsx 不加,多一个—no-semi, 然后都用单引号 比 vsc 的配置多一点点
Print Width

指定一行不能超过多少长, 长了换行, 报错是 xxlint 的事

为了可读写, 建议不要超过 80 个字符的, 因为人阅读的时候一般不会超过 100-120 的, prettier的话倒是希望每行越长越好.

Default CLI Override API Override
80 --print-width <int> printWidth: <int>

如果你不想在markdown中限定换行, 可以用Prose Wrap来关闭它, 默认preserve是保持markdown的as-is

Tab Width

一个缩进等级代表几个空格

Default CLI Override API Override
2 --tab-width <int> tabWidth: <int>
Tabs

缩进的行用 tab 而不是用空格

Default CLI Override API Override
false --use-tabs useTabs: <bool>

tab 常用来缩进,但在 prettier 中 tab 是用来 align 的

Semicolons

每行后面带不带分号

Default CLI Override API Override
true --no-semi semi: <bool>

true 是在每行末尾加
false 是在每行开头加,但会引入 ASI 问题.

Quotes

使用单引号而不是双引号:

注意:

  • jsx 的会忽略这个配置, 用的是jsx-single-quote.
  • 如果在一个用字符串包上字符串的情况下, "I'm double quoted"变"I'm double quoted". "This \"example\" is single quoted"变'This "example" is single quoted'
  • 更多信息看strings rationale
Default CLI Override API Override
false --single-quote singleQuote: <bool>
Quote Props

当对象中的属性用引号包上的时候改变.

Valid options:

  • "as-needed" - 当没有严格要求时,禁止对象字面量属性名称使用引号
  • "consistent" - 要求对象字面量属性名称使用一致的引号,要么全部用引号,要么都不用
  • "preserve" - 想用就用
Default CLI Override API Override
“as-needed” —quote-props `<as-needed consistent preserve> quoteProps: “<as-needed consistent preserve>”`

eslint 要求对象字面量属性名称使用引号 (quote-props)

JSX Quotes

在 jsx 中用单引号代替双引号

Default CLI Override API Override
false --jsx-single-quote jsxSingleQuote: <bool>
Trailing Commas

多行的时候在末尾打印逗号. 单行数组是不会有末尾逗号的.
尾逗号[a,b,c,d,] 数组项 d 后面的逗号就是尾逗号

可选项:

  • none, 不加
  • es5: 在 es5 中某些加, 比如(objects, arrays, etc.)
  • all: 甚至在函数参数中也加
Default CLI Override API Override
“none” `—trailing-comma <none es5 all>` `trailingComma: “<none es5 all>”`
Bracket Spacing

在一个对象字面量中加空格

true - Example: { foo: bar }.
false - Example: {foo: bar}.

Default CLI Override API Override
true --no-bracket-spacing bracketSpacing: <bool>
JSX Brackets

对一个 jsx 元素而言,>是加在最后一样还是换新行

Valid options:

true - Example:

1
2
3
4
5
6
<button
className="prettier-class"
id="prettier-id"
onClick={this.handleClick}>
Click Here
</button>

false - Example:

1
2
3
4
5
6
7
<button
className="prettier-class"
id="prettier-id"
onClick={this.handleClick}
>
Click Here
</button>
Default CLI Override API Override
false --jsx-bracket-same-line jsxBracketSameLine: <bool>
Arrow Function Parentheses

箭头函数的参数, 在只有一个的情况下加不加圆括号()

Valid options:

"avoid" - 忽略. Example: x => x
"always" - 加上. Example: (x) => x

Default CLI Override API Override
“avoid” `—arrow-parens <avoid always>` `arrowParens: “<avoid always>”`
Range

只 format 文件中的一段, [) 选一个偏移范围

不能和cursorOffset一起用

Default CLI Override API Override
0 --range-start <int> rangeStart: <int>
Infinity --range-end <int> rangeEnd: <int>
Parser

指定用什么 parse, prettier会自动从输入的文件目录中读取, 你不需要配置这个.

但 babel 和 flow对一个 js 集来说是不同的两种, 所以可以选.

当然还要其他选项.

File Path

指定前面 parser 的路径

1
cat foo | prettier --stdin-filepath foo.css
Default CLI Override API Override
None --stdin-filepath <string> filepath: "<string>"
Require pragma

prettier可以按照这个标志来严格指定 format 文件, 只需要在每个文件前面加上pramga

例如文件中带上下面的参数后, 使用--require-pragma就会 format

1
2
3
/**
* @prettier
*/

or

1
2
3
/**
* @format
*/
Default CLI Override API Override
false --require-pragma requirePragma: <bool>
Insert Pragma

prettier 可以在用 prettier 进行 format 的时候在文件的开头插入@format, 如果已经有其他的docblock的时候会加入一行的

Default CLI Override API Override
false --insert-pragma insertPragma: <bool>
Prose Wrap

换行问题, 最开始的是超过多少字符提示, 但不换行的.

Valid options:

  • "always" - Wrap prose if it exceeds the print width.
  • "never" - Do not wrap prose.
  • "preserve" - Wrap prose as-is. First available in v1.9.0
Default CLI Override API Override
"preserve" `—prose-wrap <always never preserve> ` `proseWrap: “<always never preserve>”`
HTML Whitespace Sensitivity

HTML 文件全局空格敏感问题, 详细看whitespace-sensitive formatting

就是空格会影响布局, 就按 css 的 display 来

Valid options:

  • "css" - Respect the default value of CSS display property.
  • "strict" - Whitespaces are considered sensitive.
  • "ignore" - Whitespaces are considered insensitive.
Default CLI Override API Override
"css" `—html-whitespace-sensitivity <css strict ignore>` `htmlWhitespaceSensitivity: “<css strict ignore>”`
End of Line

历史原因, 有两种, That is \n (or LF for Line Feed) and \r\n (or CRLF for Carriage Return + Line Feed).

在 vscode 中

首先安装vscode的插件Prettier-Code formatter

安装成功后,编辑器默认的格式化处理就会被prettier代替, 默认快捷键是alt + shift + f

插件安装成功后,按cmd+,调出编辑器的配置,会出现prettier插件的相关配置节点,同时也能看到一些默认的配置信息, 在setting.json中也可以自己定义。

更多的配置方式
Configuration File

其他的 prettier

除了直接用prettier, 还有像tslint-config-prettier这种 prettier

typescript 配置 prettier

  • 文件保存时执行一次格式化
  • 迁移已有代码的格式
  • 代码提交前进行一次格式化

首先确认了 Prettier 对 TypeScript 有良好的支持.

保存时格式化

但我更喜欢保存时只是做一点比如去掉多余的空格, 按alt + shift + f才进行格式化.

在 vsc 中安装完插件后, 如果前面配置prettier.config.js 文件, 临时的配置比如:

1
2
3
4
{
"tabWidth": 4,
"useTabs": true
}

更多配置文件方式
Configuration File

然后cmd+,调出编辑器的配置, 设置editor.formatOnSave 选项.把值设置为 true.

已经有了 ESlint 下

Prettier 介绍与基本用法 6

使用 ESLint 运行 Prettier

如果你已经在你的项目中使用ESLint并且想要只通过单独一条命令来执行你的所有的代码检查的话,你可以使用ESLint来为你运行Prettier。

只需要使用eslint-plugin-prettier来添加Prettier作为ESLint的规则配置。

1
yarn add --dev prettier eslint-plugin-prettier

.eslintrc.json:

1
2
3
4
5
6
{
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}

关闭 ESLint 的格式规则

你是否通过ESLint来运行Prettier,又或者是单独运行两个工具,那你大概只想要每个格式问题只出现一次,而且你特别不想要ESLint仅仅是和Prettier有简单的不同和偏好而报出“问题”。

所以你大概想要禁用冲突的规则(当保留其他Prettier不关心的规则时)最简单的方式是使用eslint-config-prettier。它可以添加到任何ESLint配置上面。

vsc 中 (这个配置自定义不同语言设置不同选项的 prettier 我没设置好)

在 vsc 中多说下, 首先 vsc 有自己的格式方式, 按cmd+k m可以查看到许多语言, 然后是你可以自己配置Configure 'language_name' language based settings., 跳转到setting.json 中写这种(和你用cmd+shift+p在输入preferences: config这种快多了)

这里的保存后格式化是对当前文件吧, 而不是本项目中所有的.

1
2
3
4
5
6
// Set the default
"editor.formatOnSave": false,
// Enable per-language
"[javascript]": {
"editor.formatOnSave": true
}

在cmd+,中看setting.json 中可以自己直接改

vsc 的配置, workspace setting 在项目根目录.vscode中, 但我找不到. 一般我在里面写 debug 的文件, 比如 egg 的那个配置

对于我常用的配置

ts, tsx, js, jsx, md 这些文件, 在 setting.json 中可以单独配置 user 的, 还有 css, less 都可以

但是不能配合prettier用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  // js
"[javascript]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": false
},
// jsx
"[javascriptreact]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": false
},
// ts
"[typescript]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": false
},
// tsx
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": false
}
// md
"[markdown]": {},

下面链接中讲了详细使用 prettier

Code Formatting 6666 各种配置

读取顺序

先从 vsc 的市场中可以看到, setting 会先从

  1. prettier 的配置文件读, 也就是前面说的 4 种
  2. 没有的话从.editorconfig这个小老鼠读
  3. 最后才是从默认的 vsc 的 prettier setting

再说下一下默认在 vsc 中插件 prettier 设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 一行最大80个字符
prettier.printWidth (default: 80)

// 使用tab代表几个空格
prettier.tabWidth (default: 2)

// 是否使用单引号代替双引号
prettier.singleQuote (default: false)

// 末尾的逗号, 3个选项, "none"就是无, "es5"会在例如对象和数组, "all"是包括函数参数
prettier.trailingComma (default: 'none')

// 控制对象字面量里面的空格
prettier.bracketSpacing (default: true)

// 如果为true, 在多行的jsx元素中, 最后的 > 单独一行,而不是跟在最后一样的末尾
prettier.jsxBracketSameLine (default: false)

// 用什么 parser .只有 'flow' 和 'babylon'.
prettier.parser (default: 'babylon') - JavaScript only

// 在每行的末尾加上分号 true. false就是用ASI, 看链接参考
prettier.semi (default: true)

// 缩进用不用tab
prettier.useTabs (default: false)

// (Markdown) wrap prose over multiple lines.
prettier.proseWrap (default: 'preserve')

// 对只有一个参数的箭头函数的这个参数, 用不用圆括号包上
prettier.arrowParens (default: 'avoid')

// 在jsx中用单引号而不是双引号
prettier.jsxSingleQuote (default: false)

// 对html文件空格敏感, 有更多选项
prettier.htmlWhitespaceSensitivity (default: 'css')

// 对每行末尾通过prettier自己设定, 有更多选项
prettier.endOfLine (default: 'auto')

然后是 vsc 中特殊指定的, 通过setting.json可以改 User and Workspace Settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 prettier-eslint 代替 prettier.
prettier.eslintIntegration (default: false) - JavaScript and TypeScript only

// 使用 prettier-tslint 代替 prettier.
prettier.tslintIntegration (default: false) - JavaScript and TypeScript only

// 使用 prettier-stylelint 代替 prettier.
prettier.stylelintIntegration (default: false) - CSS, SCSS and LESS only

// 需要一个 a 'prettierconfig' 文件来格式化
prettier.requireConfig (default: false)

// 支持写一个 .gitignore 或 .prettierignore 这样的文件来忽略项目中哪些路径下的不进行格式化. 需要重启下vsc
prettier.ignorePath (default: .prettierignore)

// 对一些特定语言不进行用prettier的格式化. 在父目录下设置也会组织所有子目录下的配置
prettier.disableLanguages (default: ["vue"])

vsc 中的prettier插件也是会依赖项目中的本地依赖, 就是前面的优先级.

vsc 中的 setting

按cmd+,打开, 有两种不同的设定

  • User Settings - 这是全局的设定
  • Workspace Settings - 指定的 workspace 打开才会应用, 这个优先级高. 常用在小组中分享项目设定. 在.vscode文件中 de .settings.json, launch.json是用来 debug 的

改变会需要重启下 vsc, 也有不需要重启的, 自己点着试试, 先看 Commonly Userd => workbench 看下去

对于 workspace setting 在一个文件下当然可以加一个.vacode文件夹, 但当有一个根文件下有很多项目时呢. 比如 ava
那就在根目录下写一个Global Workspace settings 叫.code-workspace文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"folders": [
{
"path": "vscode"
},
{
"path": "vscode-docs"
},
{
"path": "vscode-generator-code"
}
],
"settings": {
"window.zoomLevel": 1,
"files.autoSave": "afterDelay"
}
}

比如 debugging 的东西就写在.vscode文件夹下的lounch.json中

然后就是语言指定配置, 这个在前面讲过cmd+k, m

参考

Visual Studio Code 入门(译)
VS Code 使用之基本设置与配置详解
使用 ESLint+Prettier 来统一前端代码风格
Prettier 插件为更漂亮快应用代码
使用 Prettier 美化 JavaScript 代码,让编程更舒心
vscode + prettier 专治代码洁癖(一)
笔记, TypeScript 配置 Prettier 6
Prettier 介绍与基本用法 6
为什么用 Prettier 66
我为什么推荐 Prettier 来统一代码风格 6
Code Formatting 6666 各种配置
What are the rules for JavaScript’s automatic semicolon insertion (ASI)?

js立即执行函数

发表于 2019-02-17 | 分类于 javascript教程

js立即执行函数表达式

IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。
第一部分是包围在 圆括号运算符() 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。

1
2
3
4
5
(function () {
var name = "Barry";
})();
// 外部不能访问变量 name
name // undefined

将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。

1
2
3
4
5
6
var result = (function () {
var name = "Barry";
return name;
})();
// IIFE 执行后返回的结果:
result; // "Barry"

描述

立即执行函数通常有下面两种写法:

1
2
3
4
5
6
(function(){ 
...
})();
(function(){
...
}());

在Javascript中,一对圆括号“()”是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。

这个写法和我们想象的写法不一样(知道的人当然已经习以为常)
很多人刚开始理解立即执行函数的时候,觉得应该是这样的:

1
2
3
4
5
function (){ ... }();

//或者

function fName(){ ... }();

然而事实却是这样:SyntaxError: Unexpected token (。这是为什么呢?

解释

要理解立即执行函数,需要先理解一些函数的基本概念:函数声明、函数表达式,因为我们定义一个函数通常都是通过这两种方式

函数声明 (function 语句):

1
2
3
function name([param[, param[, ... param]]]) {
statements
}

name:函数名;
param:被传入函数的参数的名称,一个函数最多可以有255个参数;
statements:这些语句组成了函数的函数体。

函数表达式 (function expression):

函数表达式和函数声明非常类似,它们甚至有相同的语法。

1
2
3
function [name]([param] [, param] [..., param]) {
statements
}

name:函数名,可以省略,省略函数名的话,该函数就成为了匿名函数;
param:被传入函数的参数的名称,一个函数最多可以有255个参数;
statements:这些语句组成了函数的函数体。

下面我们给出一些栗子说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明函数f1
function f1() {
console.log("f1");
}
// 通过()来调用此函数
f1();


//一个匿名函数的函数表达式,被赋值给变量f2:
var f2 = function() {
console.log("f2");
}
//通过()来调用此函数
f2();


//一个命名为f3的函数的函数表达式(这里的函数名可以随意命名,可以不必和变量f3重名),被赋值给变量f3:
var f3 = function f3() {
console.log("f3");
}
//通过()来调用此函数
f3();

上面所起的作用都差不多,但还是有一些差别

1、函数名和函数的变量存在着差别。函数名不能被改变,但函数的变量却能够被再分配。函数名只能在函数体内使用。倘若在函数体外使用函数名将会导致错误:

1
2
var y = function x() {};
alert(x); // throws an erro

2、函数声明定义的函数可以在它被声明之前使用

1
2
3
4
foo(); // alerts FOO!
function foo() {
alert('FOO!');
}

但函数声明非常容易(经常是意外地)转换为函数表达式。当它不再是一个函数声明:

  1. 成为表达式的一部分, 不单单是用()括号, 还有其他操作符和一些语句中.
  2. 不在是函数或者script自身的“源元素” (source element)。在script或者函数体内“源元素”并非是内嵌的语句(statement)有点难懂
1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 0;               // source element
if (x == 0) { // source element
x = 10; // 非source element
function boo() {} // 非 source element
}
function foo() { // source element
var y = 20; // source element
function bar() {} // source element
while (y == 10) { // source element
function blah() {} // 非 source element
y++; //非source element
}
}

🌰栗子:

简单点看就判断是不是function开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 函数声明
function foo() {}

// 函数表达式, 在括号中
(function bar() {})

// 函数表达式, 赋值语句
x = function hello() {}

// 在逻辑语句中
if (x) {
// 函数表达式
function world() {}
}


// 函数声明
function a() {
// 函数声明
function b() {}
if (0) {
//函数表达式
function c() {}
}
}

现在我们来解释上面的SyntaxError: Unexpected token (:

产生这个错误的原因是,Javascript引擎看到function关键字之后,认为后面跟的是函数定义语句,不应该以圆括号结尾。
解决方法就是让引擎知道,圆括号前面的部分不是函数定义语句,而是一个表达式,可以对此进行运算。所以应该这样写:

1
2
3
4
(function(){ /* code */ })();

// 或者
(function(){ /* code */ }()); // 这个是第二种吧

这两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称IIFE。

注意,上面的两种写法的结尾,都必须加上分号。

推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。

1
2
3
4
5
var i = function(){ return 10; }();

true && function(){ /* code */ }();

0, function(){ /* code */ }();

甚至像这样写:

1
2
3
4
5
6
7
!function(){ /* code */ }();

~function(){ /* code */ }();

-function(){ /* code */ }();

+function(){ /* code */ }();

new关键字也能达到这个效果:

1
2
3
new function(){ /* code */ }

new function(){ /* code */ }() // 只有传递参数时,才需要最后那个圆括号。

使用

那我们通常为什么使用函数立即表达式呢,以及我如何使用呢?

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。
它的目的有两个:

  • 一是不必为函数命名,避免了污染全局变量;
  • 二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
1
2
3
4
5
6
7
8
9
10
11
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 写法二
(function (){
var tmp = newData;
processData(tmp);
storeData(tmp);
}());

上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。

最后在举一个真实的栗子:在JavaScript的OOP中,我们可以通过IIFE来实现一个单例(关于单例的优化不再此处讨论)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 创建一个立即调用的匿名函数表达式
// return一个变量,其中这个变量里包含你要暴露的东西
// 返回的这个变量将赋值给counter,而不是外面声明的function自身

var counter = (function () {
var i = 0;

return {
get: function () {
return i;
},
set: function (val) {
i = val;
},
increment: function () {
return ++i;
}
};
} ());

// counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 因为i不是返回对象的属性
i; // 引用错误: i 没有定义(因为i只存在于闭包)

圆括号运算符

进击的 JavaScript(五) 之 立即执行函数与闭包 666

圆括号运算符也叫分组运算符,它有两种用法:如果表达式放在圆括号中,作用是求值;如果跟在函数后面,作用是调用函数

把表达式放在圆括号之中,将返回表达式的值

1
console.log((1+2)); // 3

将函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数,即对函数求值

1
2
3
4
5
console.log((function testa(){return 666;}));
// function testa(){return 666;}

console.log(function testa(){return 666;}());
// 666

注意:圆括号运算符不能为空,否则会报错

1
();//SyntaxError: Unexpected token )

由于圆括号的作用是求值,如果将语句放在圆括号之中,就会报错,因为语句没有返回值

1
2
(var a = function(){return 666});
// SyntaxError: Unexpected token var

IIFE

在 Javascript 中,圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。

有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。

1
2
function(){ /* code */ }();
// SyntaxError: Unexpected token (

产生这个错误的原因是,function这个关键字既可以当作语句,也可以当作表达式。

1
2
3
4
5
// 语句
function f() {}

// 表达式
var f = function f() {}

为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句。因此,JavaScript引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。

1
2
3
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。

注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。

1
2
3
// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())

上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。

推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。

1
2
3
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

自执行匿名函数和立即执行的函数表达式区别

深入理解JavaScript系列(4):立即调用的函数表达式 6666

在这篇帖子里,我们一直叫自执行函数,确切的说是自执行匿名函数(Self-executing anonymous function),但英文原文作者一直倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,我们来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这是一个自执行的函数,函数内部执行自身,递归
function foo() { foo(); }

// 这是一个自执行的匿名函数,因为没有标示名称
// 必须使用arguments.callee属性来执行自己
var foo = function () { arguments.callee(); };

// 这可能也是一个自执行的匿名函数,仅仅是foo标示名称引用它自身
// 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数
var foo = function () { foo(); };

// 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。
(function () { /* code */ } ());

// 为函数表达式添加一个标示名称,可以方便Debug
// 但一定命名了,这个函数就不再是匿名的了
(function foo() { /* code */ } ());

// 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了
(function () { arguments.callee(); } ());
(function foo() { foo(); } ());

// 另外,下面的代码在黑莓5里执行会出错,因为在一个命名的函数表达式里,他的名称是undefined
// 呵呵,奇怪
(function foo() { foo(); } ());

希望这里的一些例子,可以让大家明白,什么叫自执行,什么叫立即调用。

注:arguments.callee在ECMAScript 5 strict mode里被废弃了,所以在这个模式下,其实是不能用的。

参考

IIFE
JavaScript中的立即执行函数 666
阮一峰 IIFE 666666
[译] JavaScript:立即执行函数表达式(IIFE)666
进击的 JavaScript(五) 之 立即执行函数与闭包 666
深入理解JavaScript系列(4):立即调用的函数表达式 6666

再谈js作用域

发表于 2019-02-12 | 分类于 javascript教程

再谈 js 作用域

在上一篇js作用域链和闭包中讲的有点没头绪, 重新梳理下, 多看参考链接的第一篇, 搞清编译时和运行时的作用域链和执行上下文的区别.

看那 4 篇就够了

scope, scope chain, execution context, context 的区别

先解释下函数和作用域的关系

每一个JavaScript函数都被表示为object,进一步, as an instance of Function, 函数对象和其他对象一样, 拥有你可以编程访问的属性, 和一系列不能被访问, 但仅供JavaScript引擎使用的内部属性. 其中一个内部属性就是[[scope]].

这个内部属性[[scope]]包含一个代表作用域scope的对象集合, 这个集合是在函数被创建时产生的. 这个集合叫函数的作用域链, 他决定了函数能访问哪些数据. 这个集合中的每个对象叫做可变对象variable object. 当一个函数被创建后, 他的作用域链就有这些对象.

上面的创建指写好代码, 只是定义哦, 还没运行呢.

作用域 scope

作用域是你的代码在运行时(不运行时的预处理阶段也是可以产生的静态作用域链, 是非自己部分的哦),各个变量、函数和对象的可访问性。换句话说,作用域决定了你的代码里的变量和其他资源在各个区域中的可见性。

js 有 3 种作用域, 全局作用域(Global context: window/global), 局部作用域(Local Scope , 又称为函数作用域 Function context), 块级作用域{}和const let

上下文 context

上下文指的是在相同的作用域中的this的值, 这里this是在调用时确定的(本函数的this值), 而作用域scope也是运行时才有的. 所以没错.

例子 add()

主要是看这个链接
JavaScript 核心概念之作用域和闭包 666

1
2
3
4
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}

Scope Chain(作用域链)

作用域链的非自己部分在函数对象被建立(函数声明、函数表达式)的时候建立,而不需要等到执行,这部分作用域链是静态的;当函数执行时,建立一个自己当次执行的作用域,然后把这个作用域与前面的作用域链关联起来

所以, 当定义 add 函数后,其作用域链就创建了。函数所在的全局作用域的全局对象被放置到 add 函数作用域链([[scope]] 属性)中。我们可以从下图中看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如 this , window , document 以及全局对象中的 add 函数,也就是他自己。这也就是我们可以在全局作用域下的函数中访问 window(this),访问全局变量,访问函数自身的原因。
作用域链在稍后的执行函数时使用。当然还有函数作用域不是全局的情况,等会儿我们再讨论。

scope1.jpg

实例分析 JavaScript 作用域

上面这个链接讲了 JavaScript 的词法作用域

Execution Context(执行期上下文)

也分全局执行期上下文和函数执行期上下文

假设我们运行以下代码:

1
var total = add(5, 10);

执行该函数创建一个内部对象,称为 Execution Context(执行期上下文)。执行期上下文定义了一个函数正在执行时的作用域环境。

特别注意,执行期上下文和我们平常说的上下文不同,执行期上下文指的是作用域scope。平常说的上下文是this的取值指向。

执行期上下文和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象。分开的原因很简单,函数定义时的作用域链对象 [[scope]] 是固定的,而 执行期上下文 会根据不同的运行时环境变化。而且该函数每执行一次,都会创建单独的 执行期上下文,因此对同一函数调用多次,会导致创建多个执行期上下文。一旦函数执行完成,执行期上下文将被销毁。

函数定义时函数对象的属性是 [[scope]] ,而Execution Context(执行期上下文)的属性是scope chain

执行期上下文对象有自己的作用域链,当创建执期行上下文时,其作用域链将使用执行函数[[scope]]属性所包含的对象(即,函数定义时的作用域链对象)进行初始化。这些值按照它们在函数中出现的顺序复制到执行期上下文作用域链中。

Activation Object(激活对象)

随后,在执行其上下文中创建一个名为 Activation Object(激活对象)的新对象。 这个激活对象AO保存了函数中的所有形参,实参,局部变量,this 指针等函数执行时函数内部的数据情况。然后将这个激活对象推送到执行其上下文作用域链的顶部。

激活对象AO是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,执行期上下文将被销毁。也就会销毁Execution Context的作用域链,激活对象也同样被销毁。但如果存在闭包,激活对象就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。下图显示了执行上下文及其作用域链:

scope_execution.jpg

从左往右看,第一部分是函数执行时创建的执行期上下文,它有自己的作用域链,第二部分是作用域链中的对象,索引为1的对象是从[[scope]]作用域链中复制过来的,索引为0的对象是在函数执行时创建的激活对象AO,第三部分是作用域链中的对象的内容Activation Object(激活对象)和Global Object(全局对象)。

函数在执行时,每遇到一个变量,都会去执行期上下文的作用域链的顶部,执行函数的激活对象开始向下搜索,如果在第一个作用域链(即,Activation Object 激活对象)中找到了,那么就返回这个变量。如果没有找到,那么继续向下查找,直到找到为止。如果在整个执行期上下文中都没有找到这个变量,在这种情况下,该变量被认为是未定义的。这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量,以及 JavaScript 中各种看似怪异的、有趣的作用域问题的答案。

闭包

尤其是注意闭包的定义哦, 要使用到父函数的变量

这个看js作用域链和闭包中的闭包

再说下一道经典的题啊, 涉及闭包, 作用域, 内核线程, 事件队列, 进一步还可以考this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
a: 20,
getA: function() {
for(let i =1 ; i < 5; i++) { // let变为var的话是4个5
setTimeout(function() {
console.log(i)
}, 1000*i) // 注意这里不是1000, 改成1000的话会一秒后直接输出4个5
}
}
}

obj.getA();

输出
1
2
3
4

这道题我觉得应该首先看从输入URL到页面加载发生了什么 中多进程浏览器,和多线程内核, event loop. js立即执行函数(来形成作用域块)

尤其是内核和event loop, js 引擎和计时器是不同的线程, js 引擎是单线程的哦. 这样你就懂了为啥会这么输出.

在 4 个循环下, 在 js 栈中就会触发 4 次计时器, 等同步的执行完, 再会执行事件队列中的, 而各个计时器线程又不打扰. 所以栈中 for 执行完后, 等事件队列, 事件队列都是 1 秒后执行完. 然而这时的 i 就是 5, 所以大家都输出 5

解法是用
setTimeout 循环闭包的经典面试题 解法与探究

setTimeout 函数之循环和闭包 6 还可以
图例详解那道 setTimeout 与循环闭包的经典面试题 666
你所不知道的 setTimeout

JavaScript 的词法作用域

看完闭包后要看这个链接巩固, 在整体回顾.

实例分析 JavaScript 作用域

上面这个链接讲了 JavaScript 的词法作用域

简要说下:

如果一个文档流中包含多个 script 代码段(用 script 标签分隔的 js 代码或引入的 js 文件),它们的运行顺序是:

  1. 读入第一个代码段(js 执行引擎并非一行一行地分析程序,而是一段一段地分析执行的)
  2. 做词法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤 5
  3. 对var变量和function定义做“预解析“(永远不会报错的,因为只解析正确的声明)
  4. 执行代码段,有错则报错(比如变量未定义)
  5. 如果还有下一个代码段,则读入下一个代码段,重复步骤 2
  6. 完成

JavaScript 解析过程

从前面的例子 add()回顾下

JavaScript 中每个函数都都表示为一个函数对象(函数实例),函数对象有一个仅供 JavaScript 引擎使用的[[scope]] 属性。通过语法分析和预解析,将[[scope]] 属性指向函数定义时作用域中的所有对象集合。这个集合被称为函数的作用域链(scope chain),包含函数定义时作用域中所有可访问的数据。

对应的图是:

scope1.jpg

JavaScript 执行过程

执行具体的某个函数时,JS 引擎在执行每个函数实例时,都会创建一个执行期上下文(Execution Context)和激活对象(active Object)(它们属于宿主对象,与函数实例执行的生命周期保持一致,也就是函数执行完成,这些对象也就被销毁了,闭包例外。)

执行期上下文(Execution Context)定义了一个函数正在执行时的作用域环境。它使用函数[[scope]]属性进行初始化。

随后,执行期上下文 顶部 的会创建一个激活对象(active Object),这个激活对象保存了函数中的所有形参,实参,局部变量,this 指针等函数执行时函数内部的数据情况。这个时候激活对象中的那些属性并没有被赋值,执行函数内的赋值语句,这才会对变量集合中的变量进行赋值处理。也就是说 激活对象是一个可变对象,里面的数据随着函数执行时的数据变化而变化。

🌰
考虑一下下图中的代码:

lizi.png

分析过程:

  • 作用域 1 (绿色) :即全局作用域,包含变量foo;
  • 作用域 2 (黄色) :foo函数的作用域,包含变量a,bar,b
  • 作用域 3 (蓝色) :bar函数的作用域,包含变量c

bar 作用域里完整的包含了 foo 的作用域, 因为 bar 是定义在 foo 中的,产生嵌套作用域。值得注意的是,一个函数作用域只有可能存在于一个父级作用域中,不会同时存在两个父级作用域。还有诸如this , window , document等全局对象这里就不说了,避免混乱。

执行过程:

  • 语句console.log寻找变量a,b,c;
  • 其中c在自己的作用域中找到,
  • a,b在自己的作用域中找不到,于是向上级作用域中查找,在foo的作用域中找到,并且调用。

函数在执行时,每遇到一个变量,都会去执行期上下文的作用域链的顶部,也就是执行函数的激活对象开始搜索,如果在第一个作用域链(即,Activation Object 激活对象)中找到了,那么就返回这个变量。如果没有找到,那么继续向下查找,直到找到为止。如果在整个执行期上下文中都没有找到这个变量,在这种情况下,该变量被认为是未定义的。也就是说如果foo的作用域中也定义了c,但bar函数只调用自己作用域里的c。这就是我们说的变量取值。

关于形参, 实参, 同名局部变量的关系

1
2
3
4
5
6
7
8
function one(a,b,c) {
console.log(one.length);//形参数量 3
}
function two(a,b,c,d,e,f,g){
console.log(arguments.length);//实参数量 1
}
one(1)
two(1)
1
2
3
4
5
6
7
8
9
function DoSomething(a)
{
console.log(a); // 1
console.log(arguments[0]); // 1
var a = 2;
console.log(a); // 2
console.log(arguments[0]); // 2
}
DoSomething( 1 );

打印的结果是1,1,2,2。从上面的代码可以看到,参数a和局部变量a值是完全相同的,即使是局部变量a重新定义和赋值之后。这样就好理解了,参数和同名变量之间是 “引用” 关系,也就是说 JavaScript 引擎的处理参数和同名局部变量是都引用同一个内存地址。所以示例 5 中修改局部变量会影响到arguments的情况出现。

再展开, execution context 中有什么

看这个链接中的东西
了解 JavaScript 的执行上下文
由变量提升谈谈 JavaScript Execution Context

什么是执行上下文?

让我们将术语执行上下文想象为当前被执行代码的环境/作用域。说的够多了,现在让我们看一个包含全局global context和函数上下文execution context的代码例子

global_context.jpg

很简单的例子,我们有一个被紫色边框圈起来的全局上下文和三个分别被绿色,蓝色和橘色框起来的不同函数执行上下文。只有全局上下文(的变量)能被其他任何上下文访问。

你可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问。在上面的例子中,函数能访问当前上下文外面的变量声明,但在外部上下文不能访问内部的变量/函数声明。

执行上下文堆栈 这个看链接, 也可以看那个 event loop

浏览器里的JavaScript解释器被实现为单线程。这意味着同一时间只能发生一件事情,其他的行文或事件将会被放在叫做执行栈里面排队。下面的图是单线程栈的抽象视图:

stack1.jpg

有 5 个需要记住的关键点,关于执行栈(调用栈):

  • 单线程。
  • 同步执行。
  • 一个全局上下文。
  • 无限制函数上下文。
  • 每次函数被调用创建新的执行上下文,包括调用自己。

执行上下文的细节 666

我们现在已经知道每次调用函数,都会创建新的执行上下文。然而,在 JavaScript 解释器内部,每次调用执行上下文,分为两个阶段:

  1. 创建阶段【当函数被调用,但未执行任何其内部代码之前】:
    • 创建作用域链(Scope Chain)
    • 创建变量对象VO,内对应的 variables, functions 和 arguments。
    • 求”this“的值。 javascript中this指向由函数调用方式决定
  2. 激活/代码执行阶段:
    • 重新扫描一次代码,给变量赋值,然后执行代码。。

可以将每个执行上下文抽象为一个对象并有三个属性:

1
2
3
4
5
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters(函数实参/形参), inner variable and function declarations */ },
'this': {}
}

executionContextObj由函数调用时运行前创建,创建阶段arguments的参数会直接传入,函数内部定义的变量会初始化为undefined。执行阶段重新扫描一次代码,给变量赋值,然后执行代码。

下面是执行上下文期间 JS 引擎执行伪代码

这里和定义时(预处理时)的函数对象的属性[[scope]]不同哦

  1. 找到调用函数
  2. 执行函数代码前,创建execution context
  3. 进行创建阶段:
    • 初始化作用域链 Scope Chain
    • 创建 variable object:(全局下就是全局变量, 没有 arguments, AO下就是 4 种:函数的形参实参, 函数内声明的函数和变量)
      • 创建arguments对象,初始化该入参变量名和值(这个函数有, 全局的没有)
      • 扫描该执行上下文中声明的函数: (其实就是host提升, 看js作用域链和闭包中的提升)
        • 对于声明的函数,variable object中创建对应的变量名,其值指向该函数(函数是存在heap中的)
        • 如果函数名已经存在,用新的引用值覆盖已有的
      • 扫描上下文中声明的变量:(即:变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明)
        • 对于变量的声明,同样在variable object中创建对应的变量名,其值初始化为undefined
        • 如果变量的名字已经存在,则直接略过继续扫描
    • 决定上下文this的指向, 不要和作用域链, VO搞混
      • 用this的时候下一步就是用VO或scope chain中的变量咯

      • 调用的时候才确定this javascript中this指向由函数调用方式决定

      • 4 种: 直接调用(window或global), 方法调用(那个obj, 注意指向全局的那种调用方式, 从作用域链来看没错), new调用(就是创建的那个), 箭头(没有绑定this, 但使用this的话就是包含它的那个函数或表达式, 外面的父的this)

      • 即: this 永远指向最后调用它的那个对象

  4. 代码执行阶段:
    • 执行函数内的代码并给对应变量进行赋值(创建阶段为undefined的变量)

this结合上下文接着看
深入理解 JavaScript 系列(13):This? Yes,this! 666666
前端基础进阶(五):全方位解读 this 666

深入浅出 妙用 Javascript 中 apply、call、bind 6666

JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

JavaScript 中,某些函数的参数数量是不固定的,因此要说适用条件的话,
当你的参数是明确知道数量时用 call 。而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。

常用来转化为数组:

1
var args = Array.prototype.slice.call(arguments);

一个简单例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log(foo(22))
console.log(x);

var x = 'hello world';

function foo(i) {
var a = 'hello';
var b = function privateB() {

};

function c() {

}

console.log(i)
}

提升后是如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 提升函数声明
function foo(i) {
var a = 'hello';
var b = function privateB() {

};

function c() {

}

console.log(i)
}
// 提升变量的
var x

console.log(foo(22))
console.log(x);

x = 'hello world';

(a):代码首先进入到全局上下文的创建阶段。

  1. 初始化作用域链 Scope Chain
  2. 创建 variable object
    1. 创建arguments对象,初始化该入参变量名和值(这个函数有, 全局的没有)
    2. 函数声明和变量声明提升
  3. 决定上下文this的指向

得到如下的ExecutionContextGlobal

1
2
3
4
5
6
7
8
ExecutionContextGlobal = {
scopeChain: {...}, // 当成[]更贴切点
variableObject: {
x: undefined,
foo: pointer to function foo()
},
this: {...}
}

(b): 然后进入全局执行上下文的执行阶段。

这一阶段从上至下逐条执行代码,运行到console.log(foo(22))该行时,创建阶段已经为variableObject中的foo赋值了,因此执行时会执行foo(22)函数。
当执行foo(22)函数时,又将进入foo()的执行上下文,详见(c)阶段。
当执行到console.log(x)时,此时x在variableObject中赋值为undefined,因此打印出undefined,这也正是变量提升产生的结果。
当执行到var x = 'hello world';,variableObject中的 x 被赋值为hello world。
继续往下是foo函数的声明,因此什么也不做,执行阶段结束。下面是执行阶段完成后的ExecutionContextGlobal。

1
2
3
4
5
6
7
8
ExecutionContextGlobal = {
scopeChain: {...}, // 当成[]更贴切点
variableObject: {
x: 'hello world',
foo: pointer to function foo()
},
this: {...}
}

(c): 当 js 调用foo(22)时,进入到foo()函数的执行上下文,首先进行该上下文的创建阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ExecutionContextFoo = {
scopeChain: {...}, // 当成[]更贴切点
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: {...}
}

当执行阶段运行完后,ExecutionContextFoo如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fooExecutionContext = {
scopeChain: { ... }, // 当成[]更贴切点
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}

理清了 JS 中的执行上下文,就很容易明白变量提升具体是怎么回事了。
在代码执行前,执行上下文已经给对应的声明赋值,只不过变量是赋值为undefined,函数赋值为对应的引用,
而后在执行阶段再将对应值赋值给变量。

区分函数声明和函数表达式, 这个不再多说

在前面看到execution context中的是VO, 然后 AO是啥

Variable object(VO) :在全局作用域就是全局对象,而在其他作用域是活动对象AO。
Activation object(AO) :包含:函数的形式参数,函数的arguments对象,函数内声明的变量和内部函数 4 种(函数的形参实参, 函数内声明的函数和变量)。

其实AO是VO的一种情况。全局下是没有arguments这个对象的,所以全局对象不能称为活动对象。

未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。

全局上下文的变量对象

以浏览器中为例,全局对象为window。
全局上下文有一个特殊的地方,它的变量对象,就是window对象。而这个特殊,在this指向上也同样适用,this也是指向window。

1
2
3
4
5
6
7
// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
VO: Window,
scopeChain: {}, // 当成[]更贴切点
this: Window
}

除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。

另一种 VO 不是 this

js 中 执行环境(execution context) 和 作用域(scope) 的区别在哪里?

执行环境(Execution Context,简称Context)只是一个抽象概念,在具体JS Engine实现中,它对应很多内容,变量对象(Variable Object,简写VO)是其一,还有Scope Chain,this等,这些共同组成了执行环境这个概念。

VO不是指具体某个Object,而是指一类Object,所以也具有一定程度的抽象。

VO是JS Engine内部实现,用于identifier resolution,JS 代码层面是接触不到的, 但this是执行环境的一部分,所以不要与VO搞混.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var color = "blue";

function changeColor(){
var anotherColor = "red";

function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;

//color, anotherColor, and tempColor are all accessible here
}

//color and anotherColor are accessible here, but not tempColor
swapColors();
}

//only color is accessible here
changeColor();

书上(《Javascript 高级程序设计(第三版)》)的代码(如下)说得很清楚:global context对应一个VO(就是window!!), changeColor的local context对应一个VO,swapColors的local context对应一个VO。所以每个 context 都对应了一个VO。

如上所说,this也是执行环境的一部分,所以不要与VO搞混,VO是JS Engine内部实现,用于identifier resolution,JS 代码层面是接触不到的。参见 ES2016 规范:(而我们这里要访问到VO是通过this或者window来, 而不是直接访问VO)

Lexical Environments and Environment Record values are purely specification mechanisms and need not correspond to any specific artefact of an ECMAScript implementation. It is impossible for an ECMAScript program to directly access or manipulate such values.

当然如果你这个VO是global的话,比较特殊一点:

A global environment’s Environment Record may be prepopulated with identifier bindings and includes an associated global object whose properties provide some of the global environment’s identifier bindings. As ECMAScript code is executed, additional properties may be added to the global object and the initial properties may be modified.

这也是为什么前面提到说global context对应window。

同一本书专门讲 Function 一章有这样一句话:

The this object is bound at runtime based on the context in which a function is executed: when used inside global functions, this is equal to window in nonstrict mode and undefined in strict mode, whereas this is equal to the object when called as an object method.

简言之,this只是存了一个地址,要么指向window,要么指向调用该方法的那个object。
把上面代码改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var o = {color: "black"};

function changeColor(){
var color = "red";
console.log(this === window);
function swapColors(){
console.log(this === window);
console.log(this.color);
console.log(color);
}

swapColors();
}

changeColor(); // Output: true, true, undefined, red
o.changeColor = changeColor;
o.changeColor(); // Output: false, true, undefined, red

swapColors里的this,和swapColors的context对应的VO没什么关系,而是指向window。

说下全局上下文

深入理解 JavaScript 系列(12):变量对象(Variable Object)66666 看全局上下文中的变量对象这段

首先,我们要给全局对象一个明确的定义:

  • 全局对象(Global object) 是在进入任何执行上下文之前就已经创建了的对象;
  • 这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。

全局对象初始创建阶段将Math、String、Date、parseInt作为自身属性,等属性初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象自身)。例如,在DOM中,全局对象的window属性就可以引用全局对象自身(当然,并不是所有的具体实现都是这样):

1
2
3
4
5
6
7
global = {
Math: <...>,
String: <...>
...
...
window: global //引用自身, 就是上面的那个例如, 请注意这个对象哦
};

当访问全局对象的属性时通常会忽略掉前缀(global),这是因为全局对象是不能通过名称直接访问的。不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身。例如,DOM中的window, nodejs中的global。综上所述,代码可以简写为:

1
2
3
4
5
String(10); // 就是global.String(10);

// 带有前缀
this.b = 20; // global.b = 20; //通过this
window.a = 10; // === global.window.a = 10 === global.a = 10; // 通过递归引用自身

因此,回到全局上下文中的变量对象——在这里,变量对象就是全局对象自己:

1
VO(globalContext) === global;

非常有必要要理解上述结论,基于这个原理,在全局上下文中声明的对应,我们才可以间接通过全局对象的属性来访问它(例如,事先不知道变量名称)。

1
2
3
4
5
6
7
8
9
var a = new String('test');

alert(a); // 直接访问,在VO(globalContext)里找到:"test"

alert(window['a']); // 间接通过global访问:global === VO(globalContext): "test"
alert(a === this.a); // true

var aKey = 'a';
alert(window[aKey]); // 间接通过动态属性名称访问:"test"

函数上下文中的变量对象

在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为 AO)扮演VO的角色。

1
VO(functionContext) === AO;

活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象:

1
2
3
AO = {
arguments: <ArgO>
};

Arguments对象是活动对象的一个属性,它包括如下属性:

  1. callee — 指向当前函数的引用
  2. length — 真正传递的参数个数
  3. properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。
    例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function foo(x, y, z) {

// 形参, 声明的函数参数数量arguments (x, y, z)
console.log(foo.length); // 3

// 实参, 真正传进来的参数个数(only x, y)
console.log(arguments.length); // 2

// 参数的callee是函数自身
console.log(arguments.callee === foo); // true

// 形参实参, 参数共享. 还有同名形参和函数内变量这个看变量提升, 就是忽略
console.log(x === arguments[0]); // true
console.log(x); // 10

arguments[0] = 20;
console.log(x); // 20

x = 30;
console.log(arguments[0]); // 30

// 不过,没有传进来的参数z,和参数的第3个索引值是不共享的
z = 40;
console.log(arguments[2]); // undefined

arguments[2] = 50;
console.log(z); // 40

}

foo(10, 20);

这个例子的代码,在当前版本(71.0.3578.98 (Official Build) (64-bit))的Google Chrome浏览器里有一个 bug — 即使没有传递参数z,z和arguments[2]仍然是共享的。

处理上下文代码的 2 个阶段

现在我们终于到了本文的核心点了。执行上下文的代码被分成两个基本的阶段来处理:

  1. 进入执行上下文
  2. 执行代码

变量对象的修改变化与这两个阶段紧密相关。

注:这 2 个阶段的处理是一般行为,和上下文的类型无关(也就是说,在全局上下文和函数上下文中的表现是一样的)。

这里说下变量(以前认知中有个错误概念)

通常,各类文章和 JavaScript 相关的书籍都声称:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。请记住,这是错误的概念:

任何时候,变量只能通过使用var关键字才能声明。

赋值语句:

1
a = 10;

这仅仅是给全局对象创建了一个新属性(但它不是变量)。“不是变量”并不是说它不能被改变,而是指它不符合ECMAScript规范中的变量概念,所以它“不是变量”(它之所以能成为全局对象的属性,完全是因为VO(globalContext) === global,大家还记得这个吧?, 忽略了前缀)。

让我们通过下面的实例看看具体的区别吧:

1
2
3
4
5
console.log(a); // undefined
console.log(b); // Uncaught ReferenceError: b is not defined

b = 10;
var a = 20;

所有根源仍然是VO和进入上下文阶段和代码执行阶段:

进入上下文阶段:是这样的, 如果b是变量的话那么它也应该在VO中

1
2
3
4
VO = {
a: undefined
// 如果b是变量 那么也会存在 b: undefined, 但实际上报错, 所以并不存在这个变量b
};

我们可以看到,因为“b”不是一个变量,所以在这个阶段根本就没有“b”,“b”将只在代码执行阶段才会出现(但是在我们这个例子里,还没有到那就已经出错了)。

让我们改变一下例子代码:

1
2
3
4
5
6
7
console.log(a); // undefined, 这个大家都知道,

b = 10;
console.log(b); // 10, 代码执行阶段创建

var a = 20;
console.log(a); // 20, 代码执行阶段修改

关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性(attribute):{DontDelete},这个特性的含义就是不能用delete操作符直接删除变量属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
a = 10;
console.log(window.a); // 10

console.log(delete a); // true

console.log(window.a); // undefined

var b = 20;
console.log(window.b); // 20

console.log(delete b); // false

console.log(window.b); // still 20

但是这个规则在有个上下文里不起作用,那就是eval上下文,变量没有{DontDelete}特性。

1
2
3
4
5
6
eval('var a = 10;');
console.log(window.a); // 10

console.log(delete a); // true

console.log(window.a); // undefined

使用一些调试工具(例如:Firebug)的控制台测试该实例时,请注意,Firebug同样是使用eval来执行控制台里你的代码。因此,变量属性同样没有{DontDelete}特性,可以被删除。

特殊实现: __parent__ 属性

前面已经提到过,按标准规范,活动对象是不可能被直接访问到的。但是,一些具体实现并没有完全遵守这个规定,例如SpiderMonkey和Rhino;的实现中,函数有一个特殊的属性 __parent__,通过这个属性可以直接引用到活动对象(或全局变量对象),在此对象里创建了函数。

例如 (SpiderMonkey, Rhino):

1
2
3
4
5
6
7
8
9
10
11
var global = this;
var a = 10;

function foo() {}

console.log(foo.__parent__); // global

var VO = foo.__parent__;

console.log(VO.a); // 10
console.log(VO === global); // true

在上面的例子中我们可以看到,函数foo是在全局上下文中创建的,所以属性__parent__ 指向全局上下文的变量对象,即全局对象。

然而,在SpiderMonkey中用同样的方式访问活动对象是不可能的:在不同版本的SpiderMonkey中,内部函数的__parent__ 有时指向null ,有时指向全局对象。

在Rhino中,用同样的方式访问活动对象是完全可以的。

例如 (Rhino):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var global = this;
var x = 10;

(function foo() {

var y = 20;

// "foo"上下文里的活动对象
var AO = (function () {}).__parent__;

print(AO.y); // 20

// 当前活动对象的__parent__ 是已经存在的全局对象
// 变量对象的特殊链形成了
// 所以我们叫做作用域链
print(AO.__parent__ === global); // true

print(AO.__parent__.x); // 10

})();

再说下作用域与执行上下文

JavaScript 代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。
编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。
执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

process.webp

然后是前面说的执行上下文的生命周期

execution_context.webp

下面的链接更详细分为 4 个阶段, 整合了从global context开始

图解 JS 闭包形成的原因 666

程序执行的四个阶段

我以下面一段代码解释一下程序执行的几个阶段

1
2
3
4
5
6
7
8
var age = "21";
function myAge(){
var age = 0;
age++;
console.log(age);
}
myAge();
console.log(age);

第一阶段:在内存中创建执行执行环境栈、把全局对象window压入栈底、在window中声明变量

step1.jpeg

第二阶段:函数调用时
在执行环境中添加当前函数调用、为本次函数调用创建活动对象AO、根据scope指定运行期活动对象 AO 的上下文内部对象

step2.jpeg

第三阶段:函数调用后
函数调用从执行环境栈中出栈、函数作用域 AO 释放、函数作用域 AO 中的局部变量也一同被释放

step3.jpeg

函数调用栈与作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
var c = 100;
fn(); // 此处的保留的innerFoo的引用
}

foo();
bar();

这里打印c要看你的作用域链上能不能找到c,而不是说调用的时候, 前面有个c用过.

参考

《高性能 JavaScript》第 2 章

重点是这 4 篇文章, 然后看完就看下浏览器的机制, 再看下执行上下文的结构
JavaScript 核心概念之作用域和闭包 666
深入理解 JavaScript 中的作用域和上下文 666
实例分析 JavaScript 作用域 6666
JavaScript 中的 Hoisting (变量提升和函数声明提升) 666

从输入URL到页面加载发生了什么

了解 JavaScript 的执行上下文
由变量提升谈谈 JavaScript Execution Context

js 中 执行环境(execution context) 和 作用域(scope) 的区别在哪里?
js 中的活动对象 与 变量对象 什么区别?

讲清楚之 javascript 作用域 6
深入理解 JS 中声明提升、作用域(链)和this关键字
图解 JS 闭包形成的原因 666
深入理解 JavaScript 系列(12):变量对象(Variable Object)666666
深入理解 JavaScript 系列(13):This? Yes,this! 666666
深入浅出 妙用 Javascript 中 apply、call、bind 6666
闭包,是真的美 666

YAML语言入门

发表于 2019-02-12 | 分类于 YAML

YAML语言入门

YAML 是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便。

二 对象

对象的一组键值对,使用冒号结构表示。(记住这个是基本的结构)

1
animal: pets

转为 JavaScript 如下。

1
{ animal: 'pets' }

Yaml 也允许另一种写法,将所有键值对写成一个行内对象。

1
hash: { name: Steve, foo: bar }

转为 JavaScript 如下。

1
{ hash: { name: 'Steve', foo: 'bar' } }

然后是多个的

1
2
3
4
YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org

转为 JavaScript 如下。

1
2
3
4
5
6
{
YAML: 'yaml.org',
Ruby: 'ruby-lang.org',
Python: 'python.org',
Perl: 'use.perl.org'
}

三 数组

一组连词线开头的行,构成一个数组。

1
2
3
- Cat
- Dog
- Goldfish

转为 JavaScript 如下。

1
[ 'Cat', 'Dog', 'Goldfish' ]

数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。

1
2
3
4
-
- Cat
- Dog
- Goldfish

转为 JavaScript 如下。

1
[ [ 'Cat', 'Dog', 'Goldfish' ] ]

数组也可以采用行内表示法。

1
animal: [Cat, Dog]

转为 JavaScript 如下。

1
{ animal: [ 'Cat', 'Dog' ] }

四、复合结构

对象和数组可以结合使用,形成复合结构。

1
2
3
4
5
6
7
8
9
languages:
- Ruby
- Perl
- Python
websites:
YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org

转为 JavaScript 如下。

1
2
3
4
5
6
7
8
{ languages: [ 'Ruby', 'Perl', 'Python' ],
websites:
{ YAML: 'yaml.org',
Ruby: 'ruby-lang.org',
Python: 'python.org',
Perl: 'use.perl.org'
}
}

从上面可以得出写法, 从最里层往外看,就可以转成JavaScript的写法

五、纯量

纯量是最基本的、不可再分的值。以下数据类型都属于 JavaScript 的纯量。

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

null用~表示。

1
parent: ~

转为 JavaScript 如下。

1
{ parent: null }

时间采用 ISO8601 格式。

1
iso8601: 2001-12-14t21:59:43.10-05:00

转为 JavaScript 如下。

1
{ iso8601: new Date('2001-12-14t21:59:43.10-05:00') }

日期采用复合 iso8601 格式的年、月、日表示。

1
date: 1976-07-31

转为 JavaScript 如下。

1
{ date: new Date('1976-07-31') }

参考

YAML 语言教程

url的组成

发表于 2019-01-31 | 分类于 网络

url的组成

whatwg标准

whatwg

WHATWG 的 API 与遗留的 API 的区别如下。 在下图中,URL 'http://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash' 上方的是遗留的 url.parse() 返回的对象的属性。 下方的则是 WHATWG 的 URL 对象的属性。

WHATWG 的 origin 属性包括 protocol 和 host,但不包括 username 或 password。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ href │
├──────────┬──┬─────────────────────┬────────────────────────┬───────────────────────────┬───────┤
│ protocol │ │ auth │ host │ path │ hash │
│ │ │ ├─────────────────┬──────┼──────────┬────────────────┤ │
│ │ │ │ hostname │ port │ pathname │ search │ │
│ │ │ │ │ │ ├─┬──────────────┤ │
│ │ │ │ │ │ │ │ query │ │
" https: // user : pass @ sub.example.com : 8080 /p/a/t/h ? query=string #hash "
│ │ │ │ │ hostname │ port │ │ │ │
│ │ │ │ ├─────────────────┴──────┤ │ │ │
│ protocol │ │ username │ password │ host │ │ │ │
├──────────┴──┼──────────┴──────────┼────────────────────────┤ │ │ │
│ origin │ │ origin │ pathname │ search │ hash │
├─────────────┴─────────────────────┴────────────────────────┴──────────┴────────────────┴───────┤
│ href │
└────────────────────────────────────────────────────────────────────────────────────────────────┘

host是hostname + port
origin是protocol(这个有个:) + host
然后后面是pathname(这个有/), search(这个有?), hash(这个有#)

username:password这个是用来保护url的, 而不是用来登录的.

比如在ftp中, 你的浏览器登录后会使用anonymous, 所以你可以指定某一个username来, 只不过对于password不建议使用. 会被窃听到的, 况且还有https这个呢.

参考

nodejs中 URL 字符串与 URL 对象
Specifying username/password in a URL

1…567…14
Henry x

Henry x

this is description

133 日志
25 分类
135 标签
GitHub E-Mail
Links
  • weibo
© 2019 Henry x