基于赫尔辛基大学《深入浅出现代Web编程》的web笔记

基础知识

html

浏览器在获取到http文档后,根据所需要的资源依次发送请求
响应头Response headers告诉我们,例如,响应的字节大小,以及响应的确切时间
文档对象模型Document Object Model,或DOM,是一个应用编程接口(API),它能够对与网页相对应的元素树进行程序化修改
一个HTML文档的DOM树的最顶端节点被称为document对象。我们可以使用DOM-API在网页上执行各种操作。你可以通过在控制台标签中输入document来访问document对象
利用js在控制台更改页面元素的内容是非持久的,重新加载依旧是原有页面

css

CSS属性可以在控制台的_elements_标签中检查
形如:

1
2
3
4
5
6
7
8
.container {
padding: 10px;
border: 1px solid;
}

.notes {
color: blue;
}

的css样式称为class selectors类选择器;一个类选择器的定义总是以句号开始,并包含类的名称。代码块中的这两个选择器用于选择页面的某些部分,并对它们定义样式规则,来装饰它们
这些类是属性,它可以被添加到HTML元素中

react

React组件的布局大多是用JSX编写的。底层上,由React组件返回的JSX被编译成JavaScript。
JSX是"XML-like"语言,每个标签都需要被关闭。JSX很像HTML,区别在于使用JSX,你可以通过在大括号内编写适当的JavaScript来轻松嵌入动态内容。

  • React组件的内容(通常)需要包含一个根元素,在DOM树中有 "额外的 "div-elements。这可以通过使用fragments来避免,即用一个空元素来包装组件要返回的元素
  • React组件名称必须大写

js

1
2
3
4
5
6
7
8
9
const x = 1
let y = 5

console.log(x, y) // 1 5 are printed
y += 10
console.log(x, y) // 1 15 are printed
y = 'sometext'
console.log(x, y) // 1 sometext are printed
x = 4 // causes an error

const和let是最近才在ES6版本中加入的。JavaScript变量--你应该使用let、var还是const? on Medium
普通而言推荐let(var的使用范围可能溢出你想使用的范围)

1
2
3
4
5
6
7
8
9
10
const t = [1, -1, 3]

t.push(5)

console.log(t.length) // 4 is printed
console.log(t[1]) // -1 is printed

t.forEach(value => {
console.log(value) // numbers 1, -1, 3, 5 are printed, each to own line
})

声明为const的数组也可以修改,遍历函数forEach接受一个单参数的函数

1
2
3
4
5
6
const t = [1, -1, 3]

const t2 = t.concat(5)

console.log(t) // [1, -1, 3] is printed
console.log(t2) // [1, -1, 3, 5] is printed

在使用React时,经常使用函数式编程的技术。函数式编程范式的一个特点是使用不可变的数据结构。在React代码中,最好使用concat方法,该方法不会将项目添加到数组中,而是创建一个新的数组,其中同时包含旧数组和新项目的内容

1
2
3
4
5
6
7
8
9
10
//其他用法
const t = [1, 2, 3]
const m1 = t.map(value => value * 2)
const m2 = t.map(value => '<li>' + value + '</li>')

//解构赋值
const t = [1, 2, 3, 4, 5]
const [first, second, ...rest] = t
console.log(first, second) // 1 2 is printed
console.log(rest) // [3, 4, 5] is printed

定义对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
const object3 = {
name: {
first: 'Dan',
last: 'Abramov',
},
grades: [2, 3, 5, 3],
department: 'Stanford University',
}

//可以通过使用点符号或方括号来为一个对象即时添加属性
object1.address = 'Helsinki'
object1['secret number'] = 12341

函数:

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
//匿名函数
const sum = (p1, p2) => {
console.log(p1)
console.log(p2)
return p1 + p2
}

//如果只有一个参数,可以在定义中排除括号。
const square = p => {
console.log(p)
return p * p
}

//如果函数只包含一个表达式,那么大括号就不需要了
const square = p => p * p

function product(a, b) {
return a * b
}

//函数表达式
const average = function(a, b) {
return (a + b) / 2
}

匿名函数也是版本ES6新增的
函数作为一个对象可在对象内部声明,或者在之后附加给对象,并可以使用外部变量引用对象内部的函数 与其他语言相反,在JavaScript中,this的值是根据方法的调用方式定义的。当通过引用调用方法时,this的值就变成了所谓的全局对象,最终的结果往往不是开发者最初的意图。
可以使用bind方法强行将This绑定到某个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const arto = {
name: 'Arto Hellas',
age: 35,
education: 'PhD',
greet: function() {
console.log('hello, my name is ' + this.name)
},

doAddition: function(a, b) {
console.log(a + b)
},
}

arto.doAddition(1, 4) // 5 is printed

const referenceToAddition = arto.doAddition
referenceToAddition(10, 15) // 25 is printed

arto.greet() // "hello, my name is Arto Hellas" gets printed

const referenceToGreet = arto.greet
referenceToGreet() // prints "hello, my name is undefined"

类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
greet() {
console.log('hello, my name is ' + this.name)
}
}

const adam = new Person('Adam Ondra', 35)
adam.greet()

const janja = new Person('Janja Garnbret', 22)
janja.greet()

js中的类依旧是Object(JavaScript基本上只定义了Boolean, Null, Undefined, Number, String, Symbol, BigInt, and Object)

拓展资料:
JavaScript再认识(JS教程)

typescript

由微软开发的开源类型化的JavaScript超集,可以编译成普通JavaScript。换句话说,所有现有的JavaScript代码实际上是有效的TypeScript。
微软的Azure管理门户(120万行代码)和Visual Studio代码(30万行代码)都是用TypeScript编写的
TypeScript提供了诸如更好的开发时工具、静态代码分析、编译时类型检查和代码级文档等功能。

TypeScript由语言,编译器,语言服务三个部分组成。

  • 语言由语法、关键字和类型注释组成。
  • 编译器负责类型信息的清除(即删除类型信息)和代码转换。使TypeScript代码被转译成可执行的JavaScript。所有与类型相关的东西都在编译时被移除,所以TypeScript实际上并不是真正的静态类型代码。这不是传统意义的编译,可能称为翻译更合适
  • 语言服务从源代码中收集类型信息。开发工具可以使用这些类型信息来提供智能提示、类型提示和可能的重构替代方案。

TypeScript中的类型注解是一种轻量级的方式来记录函数或变量的预期契约。

1
2
3
4
5
6
7
8
const birthdayGreeter = (name: string, age: number): string => {
return `Happy birthday ${name}, you are now ${age} years old!`;
};

const birthdayHero = "Jane User";
const age = 22;

console.log(birthdayGreeter(birthdayHero, age));

  • Structural typing:TypeScript是一种结构化类型的语言。在结构化类型中,如果第一个元素的类型中的每一个特征,在第二个元素的类型中存在一个相应的和相同的特征,那么两个元素被认为是彼此兼容的。如果两个类型相互兼容,则被认为是相同的
  • Type inference:如果没有指定类型,TypeScript编译器可以尝试推断出类型信息。变量的类型可以根据它的分配值和它的用途来推断。类型推断发生在初始化变量和成员、设置参数默认值和确定函数返回类型时
  • TypeScript在编译过程中删除了所有类型系统结构,即在运行时没有类型信息
  • TypeScript提供类型检查和静态代码分析,代码中的类型注释可以作为一种类型的代码级文档发挥作用(原生js可使用类似java注释的jsdoc来实现类似功能,但还是没那么直观)
  • ts的类型系统无法规避运行时问题,且类型声明,推理之间可能存在问题,有时会需要手动转换

TypeScript关于类型断言类型守卫的文档。

工具

CI/CD

在协作开发场景中,如何协调开发和部署关系?
Git允许代码的多个副本、流或版本共存而不互相覆盖。每个提交都可以附上不同的签名,是分布式开发的标准解决方案之一。
在GitHub中,将一个分支合并到软件的主干分支,通常是通过一个叫做pull request的机制实现的,在这个机制中,做了一些修改的开发者要求将这些修改合并到主干分支。一旦提出了拉动请求,也就是通常所说的PR,或者打开了,另一个开发者就会检查是否一切正常,然后合并PR。
部署指的是把软件放在最终用户需要使用的地方,就静态网站来说,只需要使用nginx之类的工具放到服务器并开放一个端口用于访问。这个部署的过程能否自动化完成呢?

CI(持续集成Continuous integration)的严格定义和该术语在业界的使用方式有很大不同。严格来说,CI指的是经常将开发人员的修改合并到主分支中,并构建和运行自动测试来验证开发人员的更改。这通常是正确的,但是当我们在行业中提到CI时,我们通常在谈论实际合并发生后的情况。
这个过程可以这样描述:

  • 提示:保持我们的代码清洁和可维护。
  • 构建:将我们所有的代码整合成软件
  • 测试:以确保我们不会破坏现有的功能
  • 打包。把它全部放在一个容易移动的批次中
  • 上传/部署。将它提供给全世界

按定义看,部署似乎是CD(持续部署 Continuous Delivery)的工作,即通过生产管道的所有阶段的每一个更改都会向您的客户发布。只有失败的测试能阻止将更改部署到应用中
实际生产中,两者界限非常模糊,例如部署总是需要保证内容是可靠的,这就包括了测试。因此常常用CI替代整个过程,或者合为CI/CD,常见方法:

  • 不允许直接提交到主分支上
  • 让CI流程在所有针对主分支的拉动请求(PR)上运行,只有在满足我们所需的条件时才允许合并,如测试通过。
  • 在CI系统的已知环境中构建包

ci/cd的目的是更好地开发和发布,需要面向以下问题:

  • 如何确保在所有将要部署的代码上运行测试?
  • 如何确保主分支在任何时候都是可部署的?
  • 如何确保构建是一致的,并且总是在它要部署的平台上工作?
  • 如何确保这些变化不会相互覆盖?

虽然构建ci/cd系统似乎也很麻烦,但相比每次部署都要手动测试并构建还是好多了,老话说的好“懒惰是第一生产力”
行为准则:

  • 保证每个错误有足够的日志信息,让开发者能第一时间知道
  • 每个pr都应该有合适的标签,也就是说如果你临时又改了想法,例如加了个新功能,最后的标签应该包括这个改动
  • 老笑话说,错误只是一个 "未记录的功能",因此安全起见,对意料之外的行为应该构建失败并通知开发者
  • 测试是针对主干分支的副本进行的,测试标准应该保持一致
  • 为修复bug方便起见,主分支和开发分支不应该有太大差异

github action

免费,实用,直接集成于github,将托管仓库和ci/cd合一,这可能是中小型项目最好的ci/cd方案之一
使用GitHub Actions创建CI/CD管道的核心组件是一个叫做工作流的东西。工作流是你可以在你的版本库中设置的流程,以运行自动化任务,如构建、测试、刷新、发布和部署
每个工作流必须指定至少一个作业,其中包含一组步骤来执行单个任务。作业将被并行运行,每个作业中的步骤将被按顺序执行 。步骤可以包括自定义行为或者github提供的命令
为了让GitHub识别工作流,它们必须被指定在仓库的.github/workflows文件夹中。每个工作流都是它自己的独立文件,需要使用YAML数据序列化语言进行配置。
YAML是 "YAML Ain't Markup Language(YAML不是一种标记语言)"的递归首字母缩写。正如它的名字所暗示的那样,它的目标是人类可读的,它通常被用于配置文件。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: Hello World!

on://触发器是推送到主分支
push:
branches:
- master

jobs:
simple_deployment_pipeline:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4//指定了一个版本(`@v4`),以避免在动作被更新时可能出现的破坏性变化,从git检查项目的源代码
- uses: actions/setup-node@v2
with:
node-version: '20'
- name: npm install
run: npm install

- name: lint
run: npm run dev


实际在部署中,还需要保证只有推送到main分支时才运行检查等操作,可通过一个条件实现if: ${{ github.event_name == 'push' }},相关信息可在GitHub context中找到
此外,安全起见,应该对主分支设置保护,如pr合并前必须通过所有测试

部署时需要考虑的意外:

  • 如果我的电脑在部署过程中崩溃或挂起怎么办?
    • 我连接到服务器并通过互联网进行部署,如果我的互联网连接中断了会怎样?
    • 如果我的部署脚本/系统中的任何特定指令失败会怎样?
    • 如果由于某种原因,我的软件在我部署的服务器上不能按预期工作,会发生什么?我可以回滚到以前的版本吗?
  • 如果一个用户在我们进行部署之前对我们的软件做了一个HTTP请求(我们没有时间向用户发送响应)会发生什么?

我们可以定义良好的部署系统如下:

  • 部署系统应该永远不会让我们的软件处于崩溃状态。
  • 部署系统应该让我们知道何时发生了故障。通知失败比通知成功更重要。
  • 部署系统应该允许我们回滚到以前的部署。 与全面部署相比,这种回滚最好更容易做到,而且不容易失败。 当然,最好的选择是在部署失败的情况下自动回滚
  • 部署系统应该处理用户在部署之前/期间发出HTTP请求的情况。
  • 部署系统应该确保我们正在部署的软件符合我们为此设定的要求(例如,如果没有运行测试就不要部署)。

容器

容器将你的应用封装成一个单一的包。然后,这个包将包括所有与应用有关的依赖性。因此,每个容器可以与其他容器隔离运行。容器防止里面的应用访问设备的文件和资源。开发人员可以给所包含的应用访问文件的权限,并指定可用的资源。
准确地说,容器是操作系统级的虚拟化,器可作为操作系统的一个进程运行,相对虚拟机更轻量级。

docker

目前常用的容器
一个docker容器是一个镜像(快照)的运行时实例。镜像包括包括所有的代码、依赖性和关于如何运行应用的指示,容器只是一个运行时概念。镜像被创造后无法更改,只可以增加新的层,由于加层的开销较大,每一层应该做尽可能多的事
例如,运行docker container run hello-world时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker container run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/
Docker daemon是一个后端服务,它确保了容器的运行,我们使用Docker client来与daemon交互。
常用操作:

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
$ docker container run --help

Usage: docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]
Run a command in a new container

Options:
...
-i, --interactive Keep STDIN open even if not attached
-t, --tty Allocate a pseudo-TTY
...

$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
b8548b9faec3 ubuntu "bash" 3 minutes ago Exited (0) 6 seconds ago hopeful_clarke

$ docker start hopeful_clarke
hopeful_clarke

$ docker kill hopeful_clarke
hopeful_clarke

$ docker start -i hopeful_clarke
root@b8548b9faec3:/#
root@b8548b9faec3:/# apt-get update
root@b8548b9faec3:/# apt-get -y install vim
root@b8548b9faec3:/# vim /usr/src/app/index.js

$ docker commit hopeful_clarke hello-node-world

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-node-world latest eef776183732 9 minutes ago 252MB
ubuntu latest 1318b700e415 2 weeks ago 72.8MB
hello-world latest d1165f221234 5 months ago 13.3kB


$ docker container cp ./index.js hello-node:/usr/src/app/index.js

容器可以用哈希标识符寻址,但大多数命令都接受容器的名字作为一种更人性化的工作方法。
docker kill向进程发送一个信号SIGKILL,迫使它退出,这将导致容器的停止
commit将创建一个新的镜像,包括我们所做的修改。你可以在这样做之前使用container diff来检查原始镜像和容器之间的变化。
当我们在这个终端的容器内时,打开另一个终端,使用container cp命令可以将文件从你自己的机器复制到容器内。

Dockerfile

Dockerfile是一个简单的文本文件,包含创建镜像的所有指令。

1
2
3
4
5
6
7
8
9
10
FROM node:16

WORKDIR /usr/src/app

COPY ./index.js ./index.js

ENV DEBUG=playground:*

USER node
CMD node index.js

FROM指令将告诉Docker,镜像的基础应该是node:16。COPY指令将把主机上的文件index.js复制到镜像中的同名文件。CMD指令讲述了使用docker run时的情况。CMD是默认的指令,然后可以用镜像名称后给出的参数来覆盖。如果你忘记了,请看docker run --help。
WORKDIR指令是为了确保我们不干扰镜像的内容而悄悄加入的。它将保证所有下面的命令都将/usr/src/app设置为工作目录。如果该目录不存在于基本镜像中,它将被自动创建。如果不指定一个WORKDIR,我们就有可能意外地覆盖重要的文件。
ENV指令用于设置环境变量,也可以用命令实现CMD DEBUG=playground:* npm start 运行构建,参数-t帮助我们命名镜像:

1
2
3
$ docker build -t fs-hello-world .
[+] Building 3.9s (8/8) FINISHED
...
节点/表达式容器化的10个最佳实践

Docker-compose是另一个神奇的工具,它可以帮助我们管理容器,通过docker-compose.yml文件
绑定挂载是将主机上的文件与容器中的文件绑定的行为。我们可以用container run添加一个-v标志。语法是-v FILE-IN-HOST:FILE-IN-CONTAINER。绑定挂载在docker-compose的volumes键下声明。格式是一样的,先是主机,然后是容器。
绑定挂载的结果是,主机的mongo文件夹中的文件mongo-init.js与容器的/docker-entrypoint-initdb.d目录中的mongo-init.js文件相同。对任何一个文件的修改都可以在另一个文件中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
mongo:
image: mongo
ports:
- 3456:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_DATABASE: the_database
volumes:
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
- ./mongo_data:/data/db

volumes:
mongo_data:

./mongo_data:/data/db将在你的本地文件系统中创建一个名为mongo/data的目录,并将其映射到容器中的/data/db。这意味着/data/db中的数据被存储在容器之外,但仍然可以被容器访问!只要记得把这个目录添加到.gitignore中。
volumes: mongo_data: 创建一个被docker管理的卷。启动应用后,你可以用docker volume ls列出卷,用docker volume inspect检查其中一个,甚至用docker volume rm删除它们。
多阶段构建,可用来抛弃不必要的中间产物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# The first FROM is now a stage called build-stage

FROM node:16 AS build-stage
WORKDIR /usr/src/app

COPY . .

RUN npm ci

RUN npm run build

# This is a new stage, everything before this is gone, except the files we want to COPY

FROM nginx:1.20-alpine
# COPY the directory build from build-stage to /usr/share/nginx/html
# The target location here was found from the docker hub page

COPY --from=build-stage /usr/src/app/build /usr/share/nginx/html

docker中的cmd 详解

想开发时使用docker,构建代码很简单,如何用vscode之类的编辑器访问呢
用标志-v做一次试运行,如果成功就把配置移到docker-compose文件中。为了使用-v,我们将需要告诉它当前的目录。命令pwd你输出当前目录的路径。我们可以用它作为-v的左侧,将当前目录映射到容器的内部,或者你可以使用完整的目录路径。

1
2
3
$ docker run -p 3000:3000 -v "$(pwd):/usr/src/app/" hello-front-dev
Compiled successfully!
You can now view hello-front in the browser.
接下来,让我们把配置移到docker-compose.yml。这个文件也应该在项目的根目录下。
1
2
3
4
5
6
7
8
9
10
11
services:
app:
image: hello-front-dev
build:
context: . # The context will pick this directory as the "build context"
dockerfile: dev.Dockerfile # This will simply tell which dockerfile to read
volumes:
- ./:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
ports:
- 3000:3000
container_name: hello-front-dev # This will name the container hello-front-dev
有了这个配置,docker-compose up可以在开发模式下运行应用。你甚至不需要安装Node来开发它!稍微有点麻烦的是,安装新一来也需要在运行中的容器安装并保存

网络:
docker每个容器都有自己的独立端口号,localhost指向各个容器的网络
跨容器网络:
运行docker-compose up时,docker-compose设置了一个网络。它还将docker-compose.yml中的所有容器添加到网络中。一个DNS确保我们可以找到另一个容器。容器被赋予两个名字:服务名和容器名。例如对服务名app,http://app:3000是一个有效网络地址
选项depend_on,可确保一个容器容器在依赖启动前不会启动。depends_on并不保证被依赖的容器中的服务已经准备好了,它只是确保该容器已经被启动(并且相应的条目被添加到DNS中)。如果一个服务需要等待另一个服务在启动前做好准备,应该使用其他解决方案

有很多比docker-compose更强大的工具可以在生产中运行容器。像Kubernetes这样的重量级容器编排工具使我们能够在一个全新的水平上管理容器。这些工具隐藏了物理机器,让开发者不用担心基础设施。
更多课程:

DevOps with Docker
DevOps with Kubernetes