Node-web05-07-02服务器相关配置及使用

阿里云机器使用

购买服务器

  • 登录 https://www.aliyun.com/
    • 该登录登录,该注册注册,填好身份信息,充值110块,因为必须有超过100块才能买按时付费的服务器
  • 进入云服务器 ECS购买






  • 然后根据提示到 管理控制台,就能看到你买的机器

登录你的服务器

1
2
3
4
5
6
# 你本地命令行里
ssh root@[你的机器公网IP]
# 然后询问你是否连接,回答yes
Are you sure you want to continue connecting (yes/no)? yes
# 然后输入你的秘密即可, 就是刚刚的 123456Abc 然后回车
# 此时不出意外登录成功

使用 ssh登录,避免用密码

  • 如果你用过 github 那么默认应该又一个 ssh key,每台机子有一个 ssh key 就够了,所以你不用创建了
    • 如果你不知道怎么创建 ssh key ,可以参考这个git入门
  • 上传你的ssh key
    1
    2
    3
    4
    5
    6
    7
    8
    # 运行
    > ssh-copy-id root@[你的IP]
    # 然后让你输入密码 123456Abc
    # 成功后
    Number of key(s) added: 1

    # 此后你在登录的时候就不需要密码了
    root@[你的ip]

root密码的安全性考虑

  • 因为本教程里我的密码让你们知道了,这样你就可以登录我的机器了
  • 于是你可能胡作非为
  • 所以应该再次修改密码为一个随机数,或者你不知道的密码
  • google linux change password for root
  • 得到答案 passwd root
1
2
# 修改root用户密码,这样 你的机器就相对安全了
passwd root

其他人如何访问你的机器

1
2
3
4
5
6
7
8
9
10
# 第一步你先登录你的机器

# 第二步
root@dog:~# cd ~/.ssh
root@dog:~/.ssh# ls
# 这里 authorized_keys 就是所有能登录人的 public key
authorized_keys
# 你可以看看是啥
root@dog:~/.ssh# cat authorized_keys
# 这里就是登录人的 公钥,就是你本机的 ~/.ssh/id_rsa.pub 里的内容

服务器用户不要用 root

  • 因为root的权限太大了

每次记不住你的 机器ip咋办

  • 编辑你的 host文件

    • windows 自己修改吧,我的是mac
      1
      2
      3
      4
      5
      # mac,如果权限不够你就 sudo
      sudo vi /etc/hosts

      # 在末尾添加一行
      你的机器ip dev1
  • 此时你就可以 ssh root@dev1 登录,是不是非常方便

添加用户

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
ssh root@dev1

# 添加用户
> root@dog:~# adduser blog

Adding user `blog' ...
Adding new group `blog' (1000) ...
Adding new user `blog' (1000) with group `blog' ...
Creating home directory `/home/blog' ...
Copying files from `/etc/skel' ...
# 输入你的密码,我这里是 123456Abc
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for blog
Enter the new value, or press ENTER for the default
# 一路回车
Full Name []:
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n]

# 切换用户
root@dog:~# su - blog
blog@dog:~$ pwd
# 我们所有文件都应该在这儿
/home/blog

# 使用 blog用户 登录,然后输入你刚才设置的密码
ssh blog@dev1
# 这样 blog 就只能在它的辖区内操作,就很安全

# 解决 blog用户登录输 密码问题,
ssh-copy-id blog@dev1

# 然后你用 blog登录 就可以这样,而且不用输入密码
ssh blog@dev1

# 最后安全起见,修改 blog的密码,这样就安全了

root安装docker

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
# step01 卸载旧版 docker
sudo apt-get remove docker docker-engine docker.io containerd runc

# step02 更新源
sudo apt-get update

# step03 按照提示 运行
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common

# step04 Add Docker’s official GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# step05
sudo apt-key fingerprint 0EBFCD88

# step06
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"

# step07
sudo apt-get update

# step08
sudo apt-get install docker-ce docker-ce-cli containerd.io

安装限定版本的 docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看列表
apt-cache madison docker-ce

# 安装限定版本
sudo apt-get install docker-ce=<VERSION_STRING> docker-ce-cli=<VERSION_STRING> containerd.io

# 我这里是版本是
docker-ce (5:20.10.3~3-0~ubuntu-bionic)
docker-ce-cli (5:20.10.3~3-0~ubuntu-bionic)
containerd.io (1.4.3-1)

# 所以为了以后跟我同步,你应该这样
sudo apt-get install docker-ce=5:20.10.3~3-0~ubuntu-bionic docker-ce-cli=5:20.10.3~3-0~ubuntu-bionic containerd.io=1.4.3-1

# 测试 docker ,运行它的例子
docker run hello-world

docker设置镜像源头[不设置后面会卡在这]

1
2
3
4
5
6
7
8
9
10
11
# 登录服务器
ssh root@dev1

# 新建文件 /etc/docker/daemon.json
# 内容如下
{
"registry-mirrors": ["https://y0qd3iq.mirror.aliyuncs.com"]
}

# 重启docker
service docker restart

将blog用户加到docker分组

  • 我们是 root安装的 docker 所以其他用户没有权限的
  • 将 blog 用户添加到 docker 分组
  • 搜索 linux list all groups
  • 运行 查看分组名 cat /etc/group
  • 继续搜索 linux add user to group
  • 例子 usermod -a -G [组名] [用户名]
  • 运行 usermod -a -G docker blog

git拉代码

  • 登录 ssh blog@dev1
  • 查看有木有 git which git
  • 有就行,然后拉代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# blog用户登录,默认在你的 home目录
blog@dog:~$ pwd
/home/blog

# 创建我们放代码的地方,进入这个目录
mkdir app
cd app

# 拷贝代码,你会发现失败了,因为我们用的 ssh 方式
git clone git@github.com:slTrust/next-demo-2.git

# 你需要添加ssh key 才行
- 参考这个 https://sltrust.github.io/2017/10/05/N004_1_git%E5%85%A5%E9%97%A8/
# 具体操作如下
ssh-keygen -t rsa -b 4096 -C “你的邮箱”
# 一路回车
# 复制公钥信息
cat ~/.ssh/id_rsa.pub

# 把上面的内容 在 github 里 add key

# 再次 clone代码
git clone git@github.com:slTrust/next-demo-2.git
# 代码拉取成功~

启动数据库:因为我们一会要连接

  • 接续上面的内容
  • 启动 pg
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # blog用户登录
    cd ~/
    # 创建 pg数据持久化目录
    mkdir blog-data

    # 运行命令
    docker run -v /home/blog/blog-data:/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2

    # 创建数据库

    # 创建数据库命令模版
    CREATE DATABASE [数据库名] ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';

    # 进入数据库
    docker exec -it id bash
    # 登录数据库
    psql -U blog -W
    # 创建我们需要的数据库,注意是生产环境的 “blog_production”

    CREATE DATABASE blog_production ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';

项目运行准备工作

服务器上 yarn build,所以需要安装node:安装node12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Using Ubuntu
curl -fsSL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs

# 根据提示 安装c++ / yarn
sudo apt-get install gcc g++ make

curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -

echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

sudo apt-get update && sudo apt-get install yarn
# 注意为了和我一致,尽量保持 yarn的版本 是 1.22.xx版


yarn config get registry
# 淘宝
yarn config set registry https://registry.npm.taobao.org
# 还原
yarn config set registry https://registry.yarnpkg.com

# yarn设置镜像 参考
https://www.cnblogs.com/momozjm/p/10635941.html

服务器安全组设置

  • 让我们的自己的电脑能访问它
  • 找到对应机器,安全组开放3000端口

Node-web05-07-01博客系统部署

博客部署阿里云

代码

我们先build代码

  • yarn build 结果报错了,说找不到 Post
  • 原因是 next-env.d.ts ,一旦有 import 这个声明就不是全局的了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /// <reference types="next" />
    /// <reference types="next/types/global" />
    import * as next from 'next';

    declare module "*.png" {
    const value: string;
    export default value;
    }


    declare module 'next' {
    import {Session} from 'next-iron-session';

    interface NextApiRequest {
    session: Session
    }
    }
  • 新建 custom.d.ts 在项目根目录,里面放置全局的类型声明

1
2
3
4
5
6
7
type Post = {
id: string;
date: string;
title: string;
content: string;
htmlContent: string;
}
  • 此时再次 yarn build 构建成功
  • 去看 .next/BUILD_ID 这个是每次 build产生的id
  • 然后 yarn start 运行,部署代码也是这样运行

docker化应用

  • google docker nodejs 得到 Dockerizing a Node.js web app
  • 所有的英文都不看,直接看例子
  • 创建 Dockerfile touch Dockerfile
  • 内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM node:12
# 在 linux 的 /usr/src/app 目录工作
WORKDIR /usr/src/app
COPY package.json ./
# 因为我们用的是 yarn 不是 npm
COPY yarn.lock ./
# run 开头 代表运行 命令
RUN yarn install
# 这个意思是 把你项目根目录的内容 拷贝到 工作目录
COPY . .
# 暴露端口
EXPOSE 3000
# 最后一步你要运行的代码
CMD [ "yarn", "start" ]
  • 再次创建一个忽略文件 .dockerignore

    • .gitignore
      1
      2
      node_modules
      *.log
  • 继续看文档

    1
    2
    3
    4
    5
    6
    7
    # 看文档
    # 构建你的 docker应用
    # 构建成功会生成一个镜像
    docker build -t <your username>/node-web-app .

    # 如何运行镜像呢?
    docker run -p 3000:3000 -d <your username>/node-web-app
  • 我的构建命令如下

1
2
3
docker build -t my-node-app/node-web-app . 

docker run -p 3000:3000 -d my-node-app/node-web-app

构建过程出错了

1
2
3
4
5
6
7
8
9
10
11
12
13
# 第一个问题 yarn install 一直卡在 "Fetch packages..."
yarn cache clean & yarn install

# 第二个问题 yarn install --verbose 显示它的进度,发现卡在这里
Browserslist: caniuse-lite is outdated. Please run: npx browserslist@latest --update-db

- 解决办法
- 删除 node_modules 文件夹及 package-lock.json 文件

结果不管事
再次添加参数

RUN yarn cache clean & yarn install --verbose --network-timeout 60000
  • 历经千辛万苦,终于build成功了
  • 运行docker run -p 3000:3000 -d my-node-app/node-web-app 打开浏览器 http://localhost:3000/
  • 发现报错, 通过log看运行日志
  • docker logs 你的应用id
  • 应该是 数据库连接问题,数据库需要一个 pg 容器,并配置好localhost
  • 修改 ormconfig.json 里的host为你的本机ip
    • 因为 容器内的 localhost 不是你本机的 localhost

Node-web05-06博客系统后端分页

博客分页

代码

未登录的处理

  • 后端发现没登录返回401
  • 前端发现401也提示登录,同时在url后面 附上 returnTo 参数
  • 前端登录成功后,根据 returnTo 回跳页面
  • 后端发现登录了,就可以 根据携带的cookie 拿到session里的 currentUser

功能点

  • 使用 query-string 处理参数
  • returnTo后面的路径 可能也包含参数
    • encodeURIComponent 处理传递的路径转译问题
      1
      sign_in?returnTo=${encodeURIComponent(window.location.pathname)}

分页如何做

  • 1) /posts or /posts?page=1
  • 2) /posts?page=100咋办
    • 实际没有100页 ,前端瞎写的
  • 3) 后端获取 page (查询字符串)
  • 4) typeorm 的 page 功能
  • 5) 输出JSON给前端,同时告诉前端这是第几页

大型网站的微博咋写的?

  • 参考新浪
  • 你访问 /posts?page=1 看了,然后在看第二页
  • 此时访问的是 /posts?page=2,但是新浪微博的用户量很大,这期间可能 更新了10来条 你看到可能跟刚才的存在重叠,或者完美错过
  • 正确姿势
  • /posts?page=2&id=上一页最后一个id&offset=10
  • 后端获取id小于此 id的数据,最多10条

Node-web05-05博客系统创建博客

创建博客

代码

如何在代码里隐藏密码/密钥

  • 绝对不能写在代码里,一旦泄漏很危险
  • 必须放到环境变量里,或者被 .gitignore 的文件里

正确姿势

  • 第一种:环境变量(不够方便)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 命令行里
    export SECRET=你的密钥

    # js程序里
    password:process.env.SECRET

    # 操作流程
    export SECRET=你的密钥
    # 重启你的应用即可,开发用这个
    yarn dev
    # 如果是部署在生产环境,你就登录到这个机器设置环境变量,然后运行你的 “程序”
  • 第二种:仅限于next.js 因为提供了支持

1
2
3
4
5
6
7
8
# next.js的项目里新建 .env.local
# 在这里设置环境变量
# 内容如下
SECRET=你的密钥

# 最重要的一步,将这个文件 添加到 .gitignore

# 这样的好处是,每个人有自己的 密钥,相互不知道

Entity 循环引用 bug

Node-web05-04博客系统登录

登录功能

代码

实现逻辑

  • 创建登录页面 sign_in
  • 创建 posts/sessions API
  • 使用 SignIn Model 校验数据
  • 使用 session 记录登录状态

用 session记录登录状态

  • google 搜索 next.js session
  • 没找到标准答案
  • 搜仓库 发现 example 里有个例子 with-iron-session
  • lib/withSession.tsx
  • 一定不能把密钥写在代码里,测试可以这样,但是生产环境一定不能这样
1
2
3
4
5
6
7
8
9
10
11
12
import {withIronSession} from 'next-iron-session';
import {NextApiHandler} from 'next';

export function withSession(handler: NextApiHandler) {
return withIronSession(handler, {
// password: process.env.SECRET_COOKIE_PASSWORD,
password: 'c2a85490-cc60-4f21-94e8-8dc5dd3220da', //密钥,这个应该用环境变量
cookieName: 'blog',
// 这个选项如果不设置 你本地开发是http的 就会种不下 cookie 获取 user 会是 undefined
cookieOptions: {secure: false}
});
}
  • 然后修改 api/v1/sessions.tsx
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
import {NextApiHandler} from 'next';
import {SignIn} from '../../../src/model/SignIn';
import {withSession} from "../../../lib/withSession";

const Sessions: NextApiHandler = async (req, res) => {
const {username, password} = req.body;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const signIn = new SignIn();
signIn.username = username;
signIn.password = password;
await signIn.validate();
if (signIn.hasErrors()) {
res.statusCode = 422;
res.end(JSON.stringify(signIn.errors));
} else {
req.session.set('currentUser', signIn.user);
await req.session.save()
res.statusCode = 200;
res.end(JSON.stringify(signIn.user));
}
};

// 包一层
export default withSession(Sessions);

// 但是遇到了问题 req.session 点不出来,因为没类型,ts无法识别
// 除非我们能对 NextApiRequest 进行一个扩展,添加 session 选项

如何声明对已有类型进行扩展

1
2
3
4
5
6
7
8
9
首先引入 这个类型,然后在这个类上加属性

import * as [原有类型] from '[原有类型]';

declare module '[原有类型]' {
interface MyAddon {
[你的属性]:any
}
}
  • next-env.d.ts 修改如下
1
2
3
4
5
6
7
import * as next from 'next';
declare module 'next' {
import {Session} from 'next-iron-session';
interface NextApiRequest {
session: Session
}
}
  • 然后去登录 http://localhost:3000/sign_in
  • 此时登录成功 会响应一个 set-cookie 头,这个东西对应服务器里的一小块内存。这个session里记录了用户的信息

Node-web05-03博客系统注册

注册功能

代码

注册 sign_up

  • 新建pages/sign_up.tsx
  • 代码

验证用户名唯一性

  • yarn m:create -n AddUniqueUsernameToUsers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class AddUniqueUsernameToUsers1613632711836 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createIndex('users',
new TableIndex({
name: 'users_username', columnNames: ['username'],
isUnique: true
}));
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('users', 'users_username');
}
}

此时在创建同名 user的时候 就会报错

数据校验

  • 数据库层校验
    • 返回数据库报错msg
    • 兜底,可以不做,不过会导致数据混乱
  • 后台应用层校验

    • 必须做,这里存在问题是存在并发,多个用户 注册时候 found分别为 null就会跳过,所以还是要做 数据库层的校验
      1
      2
      3
      4
      const found = connection.manager.find(User,{username})
      if (found) {
      errors.username.push('已存在,不能重复注册');
      }
  • 前端校验

    • 有的时候做不了

小知识点 mac查看端口占用

1
2
3
4
5
6
7
lsof -i tcp:8080 

该命令会显示占用8080端口的进程,有其 pid ,可以通过pid关掉该进程

杀死进程

kill pid

将校验逻辑放到 entity 里

加密密码操作放到 User里

  • typeorm 钩子函数
1
2
3
4
@BeforeInsert()
generatePasswordDigest() {
this.passwordDigest = md5(this.password);
}

隐藏 JSON 中的部分字段

  • 使用 “lodash” 的 omit功能,过滤不要序列化的字段
1
2
3
toJSON() {
return _.omit(this, ['password', 'passwordConfirmation', 'passwordDigest', 'errors']);
}

Node-web05-02博客系统数据显示

博客系统数据显示

代码

在Next.js里 创建 connection

  • lib/getDatabaseConnection.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {createConnection} from 'typeorm';

const promise = (async function () {
console.log('创建connection.')
/*
立即执行函数,引用的时候被执行,始终返回 promise的结果
1。 createConnection 是一个 返回promise的 所以想要他必须包裹在 async 的匿名函数里 代号xx
2。 然后立即执行 xx
3。 promise 这个函数就代表 我去执行 createConnection 这个异步操作
*/
return await createConnection();
})();


export const getDatabaseConnection = async () => {
/*
别人引用的时候 第一次得到 connection
第二次依然得到 connection,因为 promise只执行了一次
promise 之后不管重复引入多少次都不会重新执行
*/
return promise;
};

pages/index.tsx里

1
2
3
4
5
6
7
8
9
10
export const getServerSideProps: GetServerSideProps = async (context) => {
const connect = await getDatabaseConnection()
console.log('connect');
const ua = context.req.headers['user-agent'];
return {
props: {
browser: ua
}
};
};
  • 遇到问题了 如果是第一次运行yarn dev 这里连接是没问题的
  • 但是如果中途 修改代码 ctrl+s 后 会导致热启动,然后重新执行 getDatabaseConnection() 但是我们的连接已经存在过一次了。所以会报错
  • 这个问题是 next.js 本地开发 修改文件自动加载导致的
  • 只会出现在开发环境,就是你本地yarn dev 然后修改代码的时候触发
1
2
Server Error
AlreadyHasActiveConnectionError: Cannot create a new connection named "default", because connection with such name already exist and it now has an active connection session.

解决 connection 开发环境修改代码重复执行问题

  • 使用 getConnectionManager

  • 修改lib/getDatabaseConnection.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {createConnection,getConnectionManager} from 'typeorm';

const promise = (async function () {
const manager = getConnectionManager();
if (!manager.has('default')) {
return createConnection();
} else {
const current = manager.get('default');
if(current.isConnected){
return current
}else{
return createConnection();
}
}
})();

export const getDatabaseConnection = async () => {
return promise;
};

疑问:我们能不能自己写一个 manager

  • 可以,但是会有一个问题
  • 我们的代码是在项目里,一旦引入,如果在开发环境下,还是会存在重复执行的问题
  • 而 typeorm 的 manager为啥没有被重新执行,因为他在 node_modules 里,nextjs发现 如果你的文件是 node_modules 里的就不会去刷新

复述这个问题:开发中遇到最难的bug

  • 一开始我们在 nextjs里 直接 getConnection 会报错,因为 还没初始化过
  • 我们用 createConnecion,为了防止执行多次
  • 我们把它放在 立即执行函数里返回,保证多次 import 都始终执行一次
  • 尝试 create + get 判空的方式
    • 又遇到问题 ctrl+s 修改代码导致 重新加载 每次都触发 create,重复了
  • 最后我们使用typeorm的 getConnectionManager 来管理
    • 因为 typeorm 的代码 处于 node_modules里,nextjs修改代码保存的时候 不会重新加载
  • 自己写 manager 没用,因为 nextjs 里 ctrl+s 热重启导致 你写的 manager 重新加载重新执行。即又触发了 create

获取Post内容

  • 前提你通过 node dist/seed.js 创造好数据
  • pages/index.tsx里获取 posts
1
2
3
4
5
6
7
8
9
10
11
12
13
export const getServerSideProps:GetServerSideProps = async (context)=>{
const connection = await getDatabaseConnection()// 第一次链接能不能用 get
const posts = await connection.manager.find(Post)

console.log('posts');
console.log(posts)
return {
props: {
aaa:'aaadaa'
}
}

}
  • 报错 No metadata for "Post" was found.
  • 原因是 connection 是换成 js之后执行的,他不知道这个 post 有那些列
    • 为啥 seed.js知道,因为他引入了 reflect-metadata
    • reflect-metadata 可以通过你引入的 entity 类,反向推出它的元数据
    • 而我们 nextjs 里没有引入它
  • createConnecion,构造的时候可以传递参数,告知它的 entity和 config信息
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
import {createConnection,getConnectionManager} from 'typeorm';
import 'reflect-metadata';
import {Post} from 'src/entity/Post';
import {User} from 'src/entity/User';
import {Comment} from 'src/entity/Comment';
import config from 'ormconfig.json';

const create = async () => {
// @ts-ignore
return createConnection({
...config,
entities: [Post, User, Comment]
});
};

const promise = (async function () {
const manager = getConnectionManager();
if (!manager.has('default')) {
return create();
} else {
const current = manager.get('default');
if(current.isConnected){
return current
}else{
return create();
}
}
})();


export const getDatabaseConnection = async () => {
return promise;
};

BUG:no metadata for Post 再次出现

  • 我们展示 posts的时候,ctrl+s 导致一个问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise = (async function () {
const manager = getConnectionManager();
if (!manager.has('default')) {
return create();
} else {
const current = manager.get('default');
if(current.isConnected){
// ctrl+s会走进这里,如果走进这里 no metadata for Post 就会触发
return current
}else{
return create();
}
}
})();
  • 修改,current.isConnected 就关闭连接,再次 create()
1
2
3
4
5
6
const promise = (async function () {
const manager = getConnectionManager();
const current = manager.has('default') && manager.get('default');
if (current) {await current.close();}
return create()
})();

Node-web05-01博客系统构建

博客系统构建

CRUD最佳实践

  • CRUD小王子
  • 数据库封装员

增删改次难在哪里?

  • 第一: 开发效率,5年CRUD效率没有任何提升
  • 第二:代码质量
    • 屎山
    • 单元测试技术
    • 公司以结果为导向,敏捷开发
      • 而真正的敏捷开发最重要的就是——单元测试
    • 因为他们只想敏捷,不想开发。
    • 真正的敏捷开发是,用最少的时间作出质量最高的程序
      • 而不是需求想变就变,改了又改。
  • 第三:前后端联调
    • 沟通成本
  • 第四:伸缩性Scale
    • 增删改查数据量变大了咋办
    • 100万数据 变 1亿,机器扛得住?
  • 第五:高并发 C10k问题
    • 10000个连接同时进来,怎么保证服务质量
  • 第六:安全和稳定
    • 防拖库、MD5碰撞?
    • XSS、CSRF、Replay?
    • 怎么备份?怎么双活?
    • 经典案例:CSDN被拖库

一些重要的原则

  • 过早优化乃万恶之源
    • 你没办法量化性能就别尝试优化
  • 开发效率>可读性>运行效率
    • 创业公司目标是活下去
    • 纠结 ++i 和 i++的效率提升 有意义吗
  • 可用性 > 易用性 > 美观
    • 不要一开始就在易用性和美观上浪费时间
  • 永远不要删除数据
    • 软删除、删除前确认

博客系统需求分析:做毁它

功能点

  • 可登录、注销,但不可以重置密码(人工解决)
  • 重置密码联系管理员
  • 用户可以对博客CRUD
  • 用户可以对博客进行“评论”,但不能修改评论(从简)
  • 用户不可以编辑用户名、密码、姓名、头像(从简)

可用性需求

  • 手机上也能完成操作

其他要求

  • 对搜索引擎优化

思路

需求

  • 简单CRUD
  • 三个表 users/posts/comments

主要数据

  • users(id/username/password_digest)
  • posts(id/user_id/title/content)
  • comments(id/user_id/post_id/content)

其他

  • 手机适配:一开始就设计两套界面PC+mobile
  • SEO:多用 SSG或者SSR,少用BSR

代码

开始写代码

step01 创建表

代码基于这个commit

我们用的docker创建的 pgsql数据库

  • 自行安装docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 项目根目录创建持久化数据目录
mkdir blog-data

# 启动pg
docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2

# 进入 pg容器 内部
docker exec -it 容器id bash

# 登陆数据库
psql -U blog

# 查看数据库
\l

# 删除数据库
drop database 数据库名

# 创建数据库
CREATE DATABASE blog_development ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';

# 连接数据库
\c 数据库名称
  • migration
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
# package.json里添加脚本
"m:create": "typeorm migration:create",

# 创建表
yarn m:create -n CreateUsers
这样会生成 src/migration/xxx-CreateUsers.ts
# 修改内容
import {MigrationInterface, QueryRunner, Table} from "typeorm";

export class CreateUsers1613578095866 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
return await queryRunner.createTable(new Table({
name:'users',
columns:[
{name:'id',isGenerated:true,type:'int',generationStrategy:'increment',isPrimary:true},
{name:'username',type:'varchar'},
{name:'password_digest',type:'varchar'},
]
}))
}

public async down(queryRunner: QueryRunner): Promise<void> {
return await queryRunner.dropTable('users');
}
}

# 然后运行 数据库表就创建好了
yarn m:run

# 同步创建 posts\comments

return await queryRunner.createTable(new Table({
name:'posts',
columns:[
{name:'id',isGenerated:true,type:'int',generationStrategy:'increment',isPrimary:true},
{name:'title',type:'varchar'},
{name:'content',type:'varchar'},
{name:'author_id',type:'int'}
]
}))

return await queryRunner.createTable(new Table({
name:'comments',
columns:[
{name:'id',isGenerated:true,type:'int',generationStrategy:'increment',isPrimary:true},
{name:'user_id',type:'int'},
{name:'post_id',type:'int'},
{name:'content',type:'text'}
]
}))

# 我们的表需要有时间列, 那样你就得冲头修改表的结构
# 我们再次创建一个 migration
yarn m:create -n AddCreateAtAndUpdatedAt

注意 await
注意 await
注意 await

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns('users', [
new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}),
new TableColumn({name: 'updatedAt', type: 'time', isNullable: false, default: 'now()'})
]);
await queryRunner.addColumns('posts', [
new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}),
new TableColumn({name: 'updatedAt', type: 'time', isNullable: false, default: 'now()'})
]);
await queryRunner.addColumns('comments', [
new TableColumn({name: 'createdAt', type: 'time', isNullable: false, default: 'now()'}),
new TableColumn({name: 'updatedAt', type: 'time', isNullable: false, default: 'now()'})
]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'createdAt');
await queryRunner.dropColumn('users', 'updatedAt');
await queryRunner.dropColumn('posts', 'createdAt');
await queryRunner.dropColumn('posts', 'updatedAt');
await queryRunner.dropColumn('comments', 'createdAt');
await queryRunner.dropColumn('comments', 'updatedAt');
}

统一命名风格:规范列名

  • 驼峰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yarn m:create -n RenameColumns

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.renameColumn('users', 'password_digest', 'passwordDigest');
await queryRunner.renameColumn('posts', 'author_id', 'authorId');
await queryRunner.renameColumn('comments', 'user_id', 'userId');
await queryRunner.renameColumn('comments', 'post_id', 'postId');
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.renameColumn('users', 'passwordDigest', 'password_digest');
await queryRunner.renameColumn('posts', 'authorId', 'author_id');
await queryRunner.renameColumn('comments', 'userId', 'user_id');
await queryRunner.renameColumn('comments', 'postId', 'post_id');
}

# migration
yarn m:run

# 查看表结构
\d 表名

step02 创建关联

1
2
3
4
5
6
7
8
9
10
11
users
one user has many posts
one has many comments

posts
a post to one user
a post has many comments

comments
a comments to one user
a comments to one post
  • 修改package.json 脚本 "e:create": "typeorm entity:create"
  • 创建三个实体
    1
    2
    3
    yarn e:create -n User
    yarn e:create -n Post
    yarn e:create -n Comment

step03 填充数据

  • seed尝试录入数据,修改 src/seed.ts
1
2
3
4
5
6
7
8
9
10
11
12
import "reflect-metadata";
import {createConnection} from "typeorm";
import {User} from "./entity/User";

createConnection().then(async connection => {
const {manager} = connection;
const u1 = new User();
u1.username = 'aaa';
u1.passwordDigest = 'xxx';
await manager.save(u1);
console.log(u1.id);
}).catch(error => console.log(error));
  • 运行node dist/seed.js 报错
1
Error: Column createdAt of Entity Comment does not support length property.
  • 点击 src/entity/User 里的 @CreateDateColumn
  • 会跳到对应的ts源码
  • 但是还看不懂 于是 webstorm 项目目录 有个 jump to source 点击 定位到位置
  • 然后 node_modules/typeorm 目录点击 findinpath 搜索 “createdAt” 发现 psql 对应会创建 timestamp

step04 创建页面

step05 创建API

  • /api/v1/sign_up
  • /api/v1/sign_in

step06 约定前后端接口

  • RESTful
  • 约定错误码
  • 约定资源格式

step07 单元测试

Node-web04-01TypeORM

TypeORM使用

  • 基于这次commit
  • 完整代码

docker容器启动 pg

1
2
3
4
5
6
7
# 本项目根目录
docker run -v "$PWD/blog-data":/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2

# 如果需要密码 请这样设置
替换
-e POSTGRES_HOST_AUTH_METHOD=trust 为
-e POSTGRES_HOST_PASSWORD=123456
  • 其他命令
1
2
3
4
5
# 查看容器
docker ps -a

# 查看容器启动日志
docker logs 容器id

进入docker容器

1
docker exec -it 容器id bash

进入pg命令行

  • psql -U blog -W 登陆数据库
1
2
3
4
5
6
7
8
9
10
11
# 查看有那些数据库
\l

# 连接 blog数据库
\c blog

# 查看数据库里的表
\dt

# 丢弃表
drop table 表名

创建数据库

1
2
CREATE DATABASE [数据库名] ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';
# 分别创建三个 blog_development、test、production

安装TypeORM

1
2
3
4
5
6
7
# 安装如下依赖

"reflect-metadata": "^0.1.13"
"typeorm": "^0.2.25"

devDependencies
"@types/node": "^14.0.6"
  • tsconfig.json 里添加两个配置
1
2
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
  • 做到这步先提交代码

typeorm使用

1
2
3
4
5
6
7
8
npx typeorm init --database postgres

# 此时会生成很多文件,他会修改我们的一些东西
# 因为 typeorm 推荐使用ts-node编译(没有内置)
# babel 和 ts-node 对TS的支持并非完全一致
# 但是 Next.js 默认用babel把ts变成js,所以 package.json的改动要撤销
# 所以我们要统一用 babel处理
# 还要撤销 .gitignore的改动

babel处理 typeorm并运行

原因已经说了,typeorm需要ts-node运行,但是nextJS使用babel转译ts的,所以我们只能统一用babel

  • 安装 @babel/cli

    1
    yarn add @babel/cli
  • 编译 typeorm的 ts文件

1
2
3
4
5
6
7
8
9
10
11
12
npx babel ./src --out-dir dist --extensions ".ts,.tsx"

# 报错 can not found @babel/core
yarn add @babel/core

# 继续运行那个命令
# 结果又报错了
SyntaxError: /Users/hjx/Desktop/demo/next-demos/next-demo-2/src/entity/User.ts: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (3:1):

# google 搜索 Support for the experimental syntax 'decorators-legacy' isn't currently enabled (3:1):
# 得到答案
https://stackoverflow.com/questions/52262084/syntax-error-support-for-the-experimental-syntax-decorators-legacy-isnt-cur

新建.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"presets": [
"next/babel"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}

typeorm默认要 ts-node支持

  • 但是 nextjs 是用 babel转译 ts
  • 所以我们要统一 使用 babel
  • 所以 typeorm的文件要编译成js运行

操作步骤

1
2
3
npx babel ./src --out-dir dist --extensions ".ts,.tsx"

node /dist/index.js

重要步骤

  • 一定一定一定要改成 false
  • 如果为true 连接数据库的时候,typeorm会自动根据 entity 目录修改数据库,
    • 假如你改了 entity 多了一些列,就会同步到表里
    • 如果是生产环境,数据要么丢了,要么脏了,就很麻烦
  • 一开始就要杜绝 sync
1
2
# 修改 ormconfig.json 的配置
"synchronize": false,

创建表

  • 创建 posts 表
  • npx typeorm migration:create -n CreatePost
    • 得到 src/migrations/{TIMESTAMP}-CreatePost.ts
    • 编写对应的 up/down函数
  • 运行 migration
    • npx babel .src --out-dir dist --extensions ".ts,.tsx"
    • npx typeorm migration:run 创建表 up操作
    • npx typeorm migration:revert drop表 down操作

解决不同平台运行多个命令问题

  • 如下命令只支持 mac/linux 用户
1
"dev": "next dev & babel -w ./src --out-dir dist --extensions .ts,.tsx"

windwos咋办

concurrently

  • 支持运行多个命令,而且支持 windows
1
"dev": "concurrently \"next dev\" \"babel -w ./src --out-dir dist --extensions .ts,.tsx\"",

创建实体

package.json

1
"entity:create": "typeorm entity:create"
  • 运行 yarn entity:create -n Post
  • 会生成 src/entity/Post.ts
  • 通过 Post 类,修改 post 表
    • @Column() 修饰器 修饰对应的列,需要你自己填写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm';

@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('increment')
id: number;
@Column('varchar')
title: string;
@Column('text')
content: string;

constructor(attributes: Partial<Post>) {
Object.assign(this, attributes);
}
}

EntityManager API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
await connection.manager.find(User,{name:'查询的名字'})
await connection.manager.create(User,{name:'', attr...})

await connection.manager.save(u1)

await connection.manager.save([u1,u2,u3])

await connection.manager.remove(u1)

await connection.manager.update(User,1,{name:'xxx'})

await connection.manager.delete(User,1)

await connection.manager.findOne(User,1)

实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "reflect-metadata";
import {createConnection} from "typeorm";
import {Post} from "./entity/Post";

createConnection().then(async connection => {
const posts = await connection.manager.find(Post)
console.log(posts)
const p = new Post();
p.title = 'Post 1'
p.content = '我的第一篇文章'
await connection.manager.save(p);

const post2 = await connection.manager.find(Post)
console.log(post2)

connection.close()
}).catch(error => console.log(error));

总结

  • migration 用来数据库迁移
  • entity 用 类和对象操作数据库表和行
  • connection 数据库连接
  • manager/repository
    • 两种风格的API,操作实体

使用 seed 填充数据

  • 前置操作 参考 01docker.md 内容,新建好容器,配置好数据库连接

  • 开第一个终端 yarn dev

    • 可以编译我们的 typeorm代码
  • 开第二个终端执行数据库迁移

1
2
3
4
# 创建数据库表
yarn migration:run
# revert操作
yarn migration:revert
  • 填充数据
1
2
# 项目根目录
node dist/seed.js
  • 进入docker容器内部查询
1
select * from posts;

代码

HTTP06密码学到https流程

密码学相关

  • 窃听,服务器经过很多环节。每个环节都能看到报文内容
  • 篡改,插入广告。 运营商,路由器,或中间某个环节插入的
  • 伪装,用你的身份信息登录
  • 否认,做过了xx操作,死不承认

对称加密

一个密钥加密解密,速度快

  • DES
  • AES

非对称

两个密钥。公钥加密,私钥解密;私钥加密,公钥解密; 速度慢

  • RSA

https加密流程