Node-web03_02Next

next深入

使用 TypeScript 开发 Next

1
2
3
4
5
# 全局安装 tsx
yarn global add typescript@3.9.2
# 安装成功后会有两个命令
- tsc
- tsserver
  • 创建 tsconfig.json tsc --init
  • 把 jsconfig.json内容 合并到 tsconfig.json

    1
    2
    3
    4
    5
    {
    "compilerOptions": {
    "baseUrl": "."
    }
    }
  • 安装类型声明文件 yarn add --dev typescript @types/react @types/react-dom @types/node

    • 你就可以用ts开发 react和 node了
  • 重启 yarn dev
  • 结果报错了,找不 xx.png
  • 修改类型声明文件next-env.d.ts
1
2
3
4
declare module "*.png" {
const value: string;
export default value;
}
  • 把js文件都修改为 tsx后缀

强烈建议加上一句配置 tsconfig.json里

1
2
# 禁用隐式 any
noImplicitAny:"true"

Next.js API

如何返回 JSON

  • 新建 pages/api/v1/posts.tsx
1
2
3
4
5
6
7
8
9
10
import { NextApiRequest, NextApiResponse } from "next";

const Posts = (req: NextApiRequest, res: NextApiResponse) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify({ name: 'hjx' }))
res.end()
}

export default Posts

你还可以这样写

1
2
3
4
5
6
7
8
9
10
import { NextApiHandler } from "next";

const Posts:NextApiHandler = (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify({ name: 'hjx' }))
res.end()
}

export default Posts

读取markdown文件

  • step01 在 markdown/ 目录建立几个文章 ,形如
1
2
3
4
5
6
---
title: hjx的博客
date: 2021-01-30
---

hjx发布的博客 内容1111
  • step02 安装 matter yarn add gray-matter

  • step03 新建功能函数 getPosts lib/posts.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import fs, { promises as fsPromise } from 'fs';
import path from "path";
import matter from 'gray-matter';

const getPosts = async () => {
const markdownDir = path.join(process.cwd(), 'markdown')
const fileNames = await fsPromise.readdir(markdownDir)
const posts = fileNames.map(fileName => {
const fullPath = path.join(markdownDir, fileName)
const id = fileName.replace(/\.md$/g, '');
const text = fs.readFileSync(fullPath, 'utf-8');
const { data: { title, date }, content } = matter(text);
return { id, title, date, content }
})
return posts;
}

export default getPosts;
  • step04 新建 pages/api/v1/posts2.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
import getPosts from "lib/posts";
import { NextApiHandler } from "next";

const Posts: NextApiHandler = async (req, res) => {
const posts = await getPosts()
console.log(posts)
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify(posts))
res.end()
}

export default Posts

Next.js的三种渲染方式

  • 客户端渲染 BSR
    • 只在浏览器上执行的渲染
  • 静态页面生成 SSG
    • Static Site Generation,解决白屏问题,SEO问题
    • 无法生成用户相关内容(所有用户请求的结果都一样)
  • 服务器渲染 SSR
    • 解决白屏问题,SEO问题
    • 可以生成用户相关的内容(不同用户不同结果)

SSG和 SSR都属于预渲染 Pre-rendering

三种方式实际以前就有

  • BSR —— 客户端渲染 vue/react创建 html
  • SSG —— 页面静态化 把PHP提前渲染成 HTML
  • SSR —— 大部分后台语言java,python 如JSP

不同点

Next.js 的预渲染可以和前端 React无缝对接

客户端渲染 BSR

  • 安装 axios yarn add axios
  • yarn add --dev @types/axios
  • pages/posts/index.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { NextPage } from "next";
import axios from 'axios';
import { useEffect, useState } from "react";

type Posts = {
id: string;
date: string;
title: string;
content: string;
}
const PostsIndex: NextPage = () => {
const [posts, setPosts] = useState<Posts[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
useEffect(() => {
setIsLoading(true)
axios.get('/api/v1/posts2').then(response => {
setTimeout(() => {
setPosts(response.data)
if (response.data.length === 0) {
setIsEmpty(true);
}
setIsLoading(false)
}, 3000);
})
}, [])
return (
<div>
Post Index
{isLoading ? <div>加载中...</div> :
isEmpty ? <div>没有文章!</div> :
posts.map(post => {
return (<div key={post.id}>
{post.id}
</div>)
})
}
</div>
)
}

export default PostsIndex;

优化代码:自定义Hooks

  • hooks/usePosts.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
28
29
30
31
32
33
34
35
import { useState, useEffect } from "react";
import axios from 'axios';

type Posts = {
id: string;
date: string;
title: string;
content: string;
}

export const usePosts = () => {
const [posts, setPosts] = useState<Posts[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
useEffect(() => {
setIsLoading(true)
axios.get('/api/v1/posts2').then(response => {
setTimeout(() => {
setPosts(response.data)
if (response.data.length === 0) {
setIsEmpty(true);
}
setIsLoading(false)
}, 3000);
})
}, [])
return {
posts,
setPosts,
isLoading,
setIsLoading,
isEmpty,
setIsEmpty
}
}

pages/posts/index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { usePosts } from "hooks/usePosts";
import { NextPage } from "next";

const PostsIndex: NextPage = () => {
const { isLoading, isEmpty, posts } = usePosts();
return (
<div>
Post Index
{isLoading ? <div>加载中...</div> :
isEmpty ? <div>没有文章!</div> :
posts.map(post => {
return (<div key={post.id}>
{post.id}
</div>)
})
}
</div>
)
}

export default PostsIndex;

客户端渲染的缺点

  • 白屏
    • ajax请求结果之前,显示的是 loading
  • SEO不友好
    • 搜索引擎不友好,看不到 posts数据
    • 因为搜索引擎不会执行JS只会看到HTML

问题分析,上面的渲染方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { usePosts } from "hooks/usePosts";
import { NextPage } from "next";

const PostsIndex: NextPage = () => {
const { isLoading, isEmpty, posts } = usePosts();
return (
<div>
Post Index
{isLoading ? <div>加载中...</div> :
isEmpty ? <div>没有文章!</div> :
posts.map(post => {
return (<div key={post.id}>
{post.id}
</div>)
})
}
</div>
)
}

export default PostsIndex;
  • 是服务器渲染的还是客户端渲染的?
  • 渲染了几次?一次还是两次?
参考 React SSR的官网
  • 推荐 在后端renderToString()在前端 hydrate()
  • hydrate() 混合 ,会保留HTML附上事件监听
  • 后端渲染 html,前端比对一下,有监听,前端添加监听事件
  • 前端也会渲染一次,用以确保前后端渲染结果一致

SSG 页面静态生成

  • 之前的BSR 每个人访问文章列表都是一样的
    • 每次访问 BSR 页面都会发 ajax请求 一样的数据,然后由前端渲染
  • 为什么? 不在后端渲染好,然后发给每个人
  • N次渲染变成了一次
  • N次ajax请求没了
  • N次客户端渲染变成了一次静态页面生成
  • 这个过程 动态内容静态化

  • pages/posts/index_ssg.tsx

    • 新建 getStaticProps 函数,返回 {props:{posts:post}}]}
1
2
3
4
5
6
7
8
9
10
11
//格式要求,函数名称必须是 getStaticProps
// 而且必须 export 导出
export const getStaticProps = async () => {
const posts = await getPosts() // 你的内容
// 返回格式
return {
props: {
posts: JSON.parse(JSON.stringify(posts))
}
}
}
  • pages/posts/index_ssg.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
28
29
30
31
32
33
import { Post } from "hooks/usePosts";
import getPosts from "lib/posts";
import { NextPage } from "next";

interface Props {
posts: Post[]
}
const PostsIndex: NextPage<Props> = (props) => {
// 这个 posts是 前端/后端 都可以拿到的
const { posts } = props;
console.log(posts)
return (
<div>
Post Index(SSG)
{posts.map(p => {
return (
<div key={p.id}>{p.id}</div>
)
})}
</div>
)
}

export default PostsIndex;

export const getStaticProps = async () => {
const posts = await getPosts()
return {
props: {
posts: JSON.parse(JSON.stringify(posts))
}
}
}
1
2
// 你会发现页面会有个如下标签把数据封装了
<script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"posts":[{"id":"第一篇博客","title":"hjx的博客","date":"2021-01-30T00:00:00.000Z","content":" \n\nhjx发布的博客 内容1111"},{"id":"第二篇博客","title":"hjx的博客2","date":"2021-01-30T00:00:00.000Z","content":" \n\nhjx发布的博客2 内容22222"}]},"__N_SSG":true},"page":"/posts/index_ssg","query":{},"buildId":"development","nextExport":false,"isFallback":false,"gsp":true}</script>

惊叹的发现

  • 前端不用ajax也能拿到数据 posts
  • 这就是 SSG的好处:后端数据直接给前端
  • 前端 JSON.parse 就可以得到 posts

为啥 java/php/python做不到?

  • 也可以做到,思路一样
  • 但是他们不支持JSX,很难和React无缝对接
  • 而且他们的对象不能直接给JS用,需要类型转换

生产环境静态化示例

  • yarn build
  • 项目根目录 .next/server就是 我们静态化的内容
  • 如果运行 build后的页面 yarn start

解读

1
2
3
λ  (Server) SSR 不能自动创建HTML
○ (Static) 自动创建 HTML
● (SSG) 自动创建 HTML JS JSON

三种文件类型

  • posts.html含有静态内容,用于客户直接访问
  • posts.js 含有静态内容,用于 快速导航
    <Link href="/posts/index_bsr">posts BSR</Link>
    <Link href="/posts/index_ssg">posts SSG</Link>
    
    • 就会预先加载 快速导航里页面对应的js index_bsr.js 和 index_ssg.js
  • posts.json 含有数据, 和 posts.js结合得到页面

为什么不直接把 数据 放入 posts.js 呢?

SSG总结

  • 动态内容静态化
  • 如果动态内容和用户无关,可以提前静态化
  • 通过 getStaticProps 可以获取数据
  • 静态内容(JS) + 数据(JSON) 就得到完整页面
  • 代替了之前 BSR的 静态内容 + 动态内容 (AJAX获取)

时机

静态化是在 yarn build 时候实现的

优点

  • 生产环境直接给出完整页面
  • 首屏不会有白屏
  • 有利于搜索引擎(SEO)

Node-web03_01Next

使用Next.js

创建项目

1
2
3
4
5
6
7
8
9
npm init next-app xxx-demo-1


安装好后
cd xxx-demo-1

运行
yarn dev
http://localhost:3000/

CRM开始 改内容

创建页面

  • pages/posts/first-post.js
1
2
3
4
5
6
import React from 'react';
export default function x() {
return (
<div>First Post</div>
)
}
  • 为了更好的类型提示 ,安装类型声明文件
1
yarn add --dev @types/react @types/react-dom

总结

  • 约定大于配置

Link快速导航

1
2
3
4
5
// 第一步
import Link from 'next/link'

// 第二步
<Link href="/posts/first-post"> Link标签 去 first-post</Link>
  • Link跳转的页面不会刷新,用ajax请求页面内容 (first-post.js)
  • 不会请求重复的HTML/JS/CSS
  • 自动在页面插入新内容,删除旧内容
  • 因为省了很多请求和解析过程,所以速度很快

借鉴了 以前就有的技术 客户端导航 如 pjax技术

同构代码

代码同时运行在两端

  • 组件里 console.log(1)
  • 你会发现 Node控制台/浏览器 都会输出 console.log(1)

差异

  • 不是所有代码,都会运行,有些需要用户触发 比如 btn.click
  • 不是所有API都能用,比如 window 在 Node里报错

参考 pages/posts/first-post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useCallback } from 'react';
import Link from 'next/link'
console.log('组件执行了')
// console.log(window) // 放开注视回报错,因为Node里没有 window
export default function x() {
const clickMe = useCallback(() => {
console.log('btn click,只在浏览器执行,不再Node控制台显示')
console.log(window)
}, [])
return (
<div>First Post
<hr />
回到首页
<hr />
<a href="/">a标签回首页,会重新请求</a>
<hr />
<Link href="/">Link回首页</Link>
<button onClick={clickMe}>btn</button>
</div>
)
}

全局配置

  • 自定义 Head
  • 全局 CSS
  • 局部 CSS

自定义 Head

  • 每个组件内你可以这样
1
2
3
4
<Head>
<title>我的Blog</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover" />
</Head>

如何修改全局 title 和 meta

  • 修改 pages/_app.js
  • 添加 <Head> 如果你想覆盖全局的 head,则在你的页面写head即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import '../styles/globals.css'
import Head from 'next/head'

function App({ Component, pageProps }) {
return (
<div className="hjx">
<Head>
<title>全局配置的title</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover" />
</Head>
<Component {...pageProps} />
</div>)
}
export default App
  • export default function App 是每个组件的根组件
  • 页面切换 App不会销毁,App里面的组件回销毁
  • 可用 App 保留全局状态

修改了 _app.js需要重启才能生效

全局CSS

  • styles/globals.css 里设置全局样式
  • pages/_app.js里引入 import '../styles/globals.css'
  • 其他地方不能 引入全局 css ,会报错

局部css

  • 可以引入 import styles from '../styles/Home.module.css'
  • 还可以jsx 写 css
1
2
3
4
5
6
7
8
9
10
11
12
13
// 类似 vue的 scoped
<style jsx>{`
a{
color:red;
}
`}</style>

// 修改全局的 css
<style jsx global>{`
body{
background:pink;
}
`}</style>

index.js

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
import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css'

export default function Home() {
return (
<div className={styles.container}>
<main className={styles.main}>
<a href="/posts/first-post">打开first-post,a标签回去first-post,会重新请求</a>
<br />
<Link href="/posts/first-post"> Link标签 去 first-post</Link>
</main>
<style jsx>{`
a{
color:red;
}
`}</style>
<style jsx global>{`
body{
background:pink;
}
`}</style>
</div>
)
}

模块化的css

  • 新建 first-post.module.css
1
2
3
4
.content{
background: pink;
border:1px solid red;
}
  • 修改 pages/posts/first-post.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import styles from 'styles/first-post.module.css'
export default function x() {
return (
<>
<div>First Post
<hr />
<div className={styles.content}>1111</div>
</div>
</>

)
}

如何引入文件不是相对路径

1
2
3
4
5
// pages/_app.js 里
import '../styles/globals.css'

// 如何 不写相对路径,变成这样
import 'styles/globals.css'
  • 项目根目录 新建 jsconfig.js
1
2
3
4
5
{
"compilerOptions": {
"baseUrl": "."
}
}

使用 sass

  • yarn add sass
  • 直接 .css文件修改为 .scss即可

静态资源

next推荐放在 public/

不推荐资源放在 public里

  • public/logo.png
  • 页面里 <img src="/logo.png" />
  • 但是这样无法被缓存

正确姿势

  • 项目根目录新建 assets/images
  • 你不能直接这样,会报错
1
2
3
import png from 'assets/003.png'

<img src={png} />
  • 你需要修改配置 next.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
webpack: (config, options) => {
config.module.rules.push({
test: /\.png$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[contenthash].[ext]',
publicPath: '/_next/static',
outputPath: 'static',
}
}

]
})
return config;
}
}

配置一个img那么麻烦, next-images 插件

1
yarn add next-images

修改配置 next.config.js

1
2
3
4
5
6
const withImages = require('next-images')
module.exports = withImages({
webpack(config, options) {
return config
}
})

代码仓库

Node-web01_01Express入门

Express

为什么有web框架

功能

  • 更方便的处理 HTTP 请求和响应
  • 更方便的链接数据库、Redis
  • 更方便的路由
  • 其他:HTML模版

理念

  • web框架主流思想都是 MVC
  • Model 处理数据相关逻辑
  • View 处理视图相关逻辑,前后端分离之后,view不重要
  • Controller 负责其他逻辑

CRM 来开始 hello world

  • 创建项目 express-demo-1
  • git init
  • yarn init -y
  • .gitignore 添加忽略文件
  • express官网 抄代码
  • 添加依赖 yarn add express
  • 新建 app.js
1
2
3
4
5
6
7
8
9
10
var express = require('express');
var app = express();

app.get('/', function (req, res) {
res.send('Hello World!');
});

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
  • 项目根目录运行 node app.js
  • 然后开始做修改 如果你想热更新
  • npm i node-dev -g
  • node-dev app.js 这样修改代码就会自动生效

给 Express 加入 TS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 安装全局工具
yarn global add typescript ts-node
# 安装类型支持
yarn add @types/express

# 项目根目录运行,初始化 ts配置文件
tsc --init

# 修改 tsconfig的 target 和 nolmplicitAny
{
"target": "es2015",
"noImplicitAny": true
}

# 修改 require 为 import
import express from 'express';
  • 新建 app2.ts
1
2
3
4
5
6
7
8
9
10
import express from 'express';
const app = express();

app.get('/', function (req, res) {
res.send('Hello World!');
});

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
  • 运行 ts-node app2.ts 成功即可

app的类型

由于我们用的是ts所以可以看代码

  • 点击 const app = express(); 中的 express
  • 看到 declare function e(): core.Express; 继续点击 core.Express
  • 看到 export interface Express extends Application { ... } 点击 Application
  • 看到 export interface Application extends EventEmitter, IRouter, Express.Application {...}
    • EventEmitter 是个事件触发器
    • 点击 IRouter
      • 包含了 get/post/put 等方法
  • 有了 TS 都不用看文档了

代码仓库

express 脚手架

  • 安装 yarn global add express-generator

使用 express-generator

  • express -h 查看帮助
  • mkdir express-demo-2
  • cd express-demo-2
  • express --view=ejs .
  • yarn install 安装依赖
  • yarn start 运行项目后 可访问 http://localhost:3000

将项目升级为TS

  • app.js 复制到 app.ts
  • yarn add @types/node --dev 这样才能用 require
  • yarn add @types/express --dev
  • 你可以用 import 代替 require
  • 你可以用 const 代替 var
  • 需 RequestHandler 和 ResponseHandler 断言
  • 将 bin/www 里的入口改为 app.ts
  • 添加 start:ts 脚本,将 node 改为 ts-node

疑问

  • 为什么ts-node 可以运行js
    • 本来就可以啊,只是添加了 ts 的支持
  • 不要在生产环境里这样用

Express核心概念

中间件

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
const express = require('express');
const app = express();

app.get('/', function (req, res, next) {
console.log('中间件1')
next();
});

app.get('/', function (req, res, next) {
console.log('中间件2')
next();
});

app.get('/', function (req, res) {
res.send('Hello World!');
});

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});

/*
访问 http://localhost:3000/
// 运行结果
中间件1
中间件2
*/

优点

  • 模块化
  • app.use 将这个函数整合起来

路由

1
2
3
4
5
6
7
8
9
10
11
12
app.use('/', function (req, res, next) {
if(req.path == '/xxx'){
res.write('home');
}
next();
});


app.get
app.post
app.use
// ....

异常处理

  • next()会进入下一个中间件
  • next(error) 会直接进入 errorHandler,不执行后面的中间件
  • next('route')
    • 这是一个特殊参数

express 知识点请参考这个

Node全解10_01child_process

child_process 子进程

进程Process

场景

  • notepad.exe是一个程序,不是进程
  • 双击 notepad.exe时,操作系统开启了一个进程

定义

  • 进程是程序的执行实例
  • 程序在CPU上执行时的活动叫做进程
  • 实际上并没有明确的定义,只有一些规则

特点

  • 一个进程可以创建另一个进程(父进程和子进程)
  • 通过任务管理器可以查看到进程

了解 CPU

特点

  • 一个单核CPU,在一个时刻,只能做一件事
  • 那么如何让用户同时看电影,听歌,写代码呢?
  • 答案是在不同进程中快速切换

多进程并发执行

  • 指多个程序在宏观上并行,微观上串行
  • 每个进程会出现 “执行-暂停-执行”的规律
  • 多个进程之前会出现抢资源(如打印机)的现象

阻塞

等待执行的进程中

  • 都是非运行态
  • 一些(A)在等待 CPU资源
  • 另一些(B)在等待 I/O完成 (如文件读取)
  • 如果这个时候把 CPU分配给 B 进程, B还是在等 I/O
  • 我们把这个 B 叫做阻塞进程
  • 因此,分派程序只会把 CPU 分配给非阻塞进程

进程三个状态

1
2
3
就绪     ==》    运行 
| |
《== 阻塞 《==

线程 Thread 引入

  • 以前: 面向进程设计的系统中,进程是程序的基本执行实体
  • 后来: 面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器

引入原因

  • 进程是执行的基本实体,也是资源分配的基本实体
  • 导致进程的创建、切换、销毁太消耗CPU时间了
  • 于是引入了线程,线程作为执行的基本实体
  • 而进程作为资源分配的基本实例

线程 Thread

  • 最新的操作系统里:线程才是CPU调度和执行的最小单元
  • 一个进程中至少有一个线程,可以有多个线程
  • 一个进程中的线程共享该进程的所有资源
  • 进程的第一个线程叫做初始化线程
  • 线程的调度可以由操作系统负责,也可以由用户自己负责

举例

  • 浏览器进程有渲染引擎、V8引擎、存储模块、网络模块、用户界面模块等
  • 每个模块都可以放在一个线程里

分析

  • 子进程 VS 线程
    • 优先使用 线程

child_process

Node.js操作子进程

使用目的

  • 子进程的运行结果存储在系统缓存之中(最大200KB)
  • 等到子进程运行结束之后,主进程再用回调函数读取子进程的运行结果
1
2
3
4
5
6
7
8
9
const child_process = require('child_process');

const { exec } = child_process;

exec('ls', (error, stdout, stderr) => {
console.log(error);
console.log(stdout);
console.log(stderr);
})

exec API

exec(cmd,options,fn)

  • execute 的缩写,用于执行 bash命令
  • 同步版本:execSync

  • 返回一个流
1
2
3
4
5
const streams = exec('ls');
// 不用回调 用事件
streams.stdout.on('data', (chunk) => {
console.log(chunk)
})

Promise

  • 可以使其 Promise 化(util.promisify)
1
2
3
4
5
6
7
8
9
const util = require('util');
const child_process = require('child_process');
const { exec } = child_process;

const exec2 = util.promisify(exec);

exec2('ls').then((data) => {
console.log(data.stdout);
})

有漏洞

  • 如果cmd 被注入了,可能执行意外的代码
  • 推荐使用 execFile
1
2
3
4
5
6
// 类似 sql 那种 “;” 结束上一个命令 然后注入它的命令
exec('ls; pwd', (error, stdout, stderr) => {
console.log(error);
console.log(stdout);
console.log(stderr);
})

推荐使用 execFile 而不是 exec

execFile API

  • 执行特定的程序
  • 命令行参数用数组的形式传入,无法注入
  • 同步版本: execFileSync
1
2
3
4
5
6
7
8
9
10
const child_process = require('child_process');

const { execFile } = child_process;

const user_input = '. && pwd';

execFile('ls', ['-la', user_input], (error, stdout, stderr) => {
console.log(error);
console.log(stdout);
})
  • 同样也支持流

options 参数

  • cwd
  • env
  • shell
  • maxBuffer 最大缓存 默认 1024*1024 字节
1
2
3
4
5
6
7
8
9
10
11
12
13
const child_process = require('child_process');

const { execFile } = child_process;

execFile('ls', ['-la'], {
cwd: '/', // 当前命令执行的工作目录
env: { NODE_ENV: 'development' },// 环境变量
maxBuffer: 1024 * 1024,// 设置最大缓存
// shell: 设置用什么 shell
}, (error, stdout) => {
console.log(error)
console.log(stdout)
})

spawn

  • 用法跟 execFile 几乎一样
  • 没有回调函数,只能通过流事件获取结果
  • 没有最大 200KB 的限制(因为是流)

经验

  • 能用 spawn 的时候就不要用 execFile
1
2
3
4
5
6
7
8
9
10
const child_process = require('child_process');
const { spawn } = child_process;

const streams = spawn('ls', ['-la'], {
cwd: '/'
});

streams.stdout.on('data', (chunk) => {
console.log(chunk.toString())
})

fork

  • 创建一个子进程,执行Node脚本
  • fork(‘./child.js’) 相当于 spawn('node',['./child.js'])

特点

  • 会多出一个 message 事件,用于父子通信
  • 会多出一个 send 方法

父.js

1
2
3
4
5
6
7
const child_process = require('child_process');

var n = child_process.fork('./child.js');
n.on('message', function (m) {
console.log('父进程得到了值:', m);
});
n.send({ hello: 'world' });

child.js

1
2
3
4
5
6
7
setTimeout(() => {
process.send({ foo: 'bar' });
}, 2000);

process.on('message', function (m) {
console.log('子进程得到消息:', m);
});

操作线程

一些历史

  • child_process.exec
    • v0.1.90 加入 Node.js
  • new Worker
    • v10.5.0 加入 Node.js
    • v11.7.0 之前需要 –experimental-worker开启
  • 这个线程API太新了
    • 所以我们应该不会经常用到
  • 效率
    • 目前效率并不算很高, 文档中文 自己都写了

worker_threads API

  • isMainThread
  • new Worker(filename)
  • parentPort 线程间通信

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const { Worker, isMainThread, parentPort } = require('worker_threads');

    if (isMainThread) {
    const worker = new Worker(__filename);
    worker.once('message', (message) => {
    console.log(message); // Prints 'Hello, world!'.
    });
    worker.postMessage('Hello, world!');
    } else {
    // When a message from the parent thread is received, send it back:
    parentPort.once('message', (message) => {
    parentPort.postMessage(message);
    });
    }
  • postMessage

事件列表

  • message
  • exit

总结

  • 如果你对进程,线程感兴趣推荐学习任意一本关于操作系统的教科书
  • 代码仓库

Node全解09_02Stream自定义

自定义Stream

Writable Stream

1
2
3
4
5
6
7
8
9
10
11
const { Writable } = require("stream");

const outStream = new Writable({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
}
});

// process.stdin 就是标准输入
process.stdin.pipe(outStream);

Readable Stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { Readable } = require('stream')

const inStream = new Readable()

inStream.push('ABCDEFGHIJKLM')
inStream.push('NOPQRSTUVWXYZ')

inStream.push(null) // No more data

inStream.on('data', (chunk) => {
process.stdout.write(chunk)
console.log('写数据了')
})

// 把所有数据都 push进去了 然后在 pipe

对方调用 read 我们才提供数据

  • 如果你想创建一个可读的流应该是 等别人 read在推 而不是上面 那样提前push好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { Readable } = require("stream");

const inStream = new Readable({
read(size) {
const char = String.fromCharCode(this.currentCharCode++)
this.push(char);
console.log(`推了 ${char}`)
if (this.currentCharCode > 90) { // Z
this.push(null);
}
}
})

inStream.currentCharCode = 65 // A

inStream.pipe(process.stdout)

Duplex 流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { Duplex } = require("stream");
const inoutStream = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
},
read(size) {
this.push(String.fromCharCode(this.currentCharCode++));
if (this.currentCharCode > 90) {
this.push(null);
}
}
});

inoutStream.currentCharCode = 65;
process.stdin.pipe(inoutStream).pipe(process.stdout);

Transform

1
2
3
4
5
6
7
8
const { Transform } = require("stream");
const upperCaseTr = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
process.stdin.pipe(upperCaseTr).pipe(process.stdout);

内置的 Transform Stream

1
2
3
4
5
6
7
const fs = require("fs");
const zlib = require("zlib");
const file = process.argv[2]; // 文件路径

fs.createReadStream(file) // 创建流 一点一点的读
.pipe(zlib.createGzip()) // 每次读就传给 gzip 压缩
.pipe(fs.createWriteStream(file + ".gz")); // 将压缩后的 gzip流 传给一个可写的流

加密 / 压缩 / 打印进入 / 完成提示

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 fs = require("fs");
const zlib = require("zlib");
const file = process.argv[2];
const crypto = require("crypto");


const { Transform } = require("stream");

const reportProgress = new Transform({
transform(chunk, encoding, callback) {
process.stdout.write(".");
callback(null, chunk);
}
});


fs.createReadStream(file)
.pipe(crypto.createCipher("aes192", "123456")) // 加密
.pipe(zlib.createGzip()) // 压缩
.pipe(reportProgress) // 变换流 每次传入数据打一个点
.pipe(fs.createWriteStream(file + ".gz"))
.on("finish", () => console.log("Done")); // 完成后的后续

// 注意调用顺序
// 一定要 先加密 在 gzip 反之无法打开文件

Stream 的用途在 Node.js 里应用非常广泛

Readable Stream

  • HTTP Response
  • HTTP Request
  • fs read stream
  • zlib stream
  • TCP sockets
  • child process stdout & stderr
  • process.stdin
  • 其他

Writable Stream

  • HTTP Response
  • HTTP Request
  • fs write stream
  • zlib stream
  • TCP sockets
  • child process stdin
  • process.stdout, process.stderr
  • 其他

数据流的积压问题 Back Pressure

一定要看

highWaterMark 是干什么的?

参考

Node.js Stream 文档

面试题

例子代码仓库

Node全解09_01stream

Stream

第一个 Stream 例子

1
2
3
4
5
6
7
const fs = require('fs')
const stream = fs.createWriteStream('./big_file.txt')
for (let i = 0; i < 1000000; i++) {
stream.write(`这是第${i}行内容,我们需要很多内容,要不停的写文件啊啊啊啊啊啊啊回车\n`)
}
stream.end() // 别忘了关闭 stream
console.log('done')

分析

  • 打开流,多次往里面塞内容,关闭流
  • 看起来就是可以写多次嘛,没什么大不了
  • 最终我们得到一个 100M 左右的文件

释义

  • stream 是流,但默认没有水,
  • stream.write 可以让水流中有水(数据)
  • 每次写的小数据叫做 chunk (块)
  • 产生数据的一段叫做 source (源头)
  • 得到数据的一段叫做 sink (水池) 上面例子的水池就是big_file.txt

第二个 stream 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require('fs');
const http = require('http');
const server = http.createServer()
server.on('request', (request, response) => {
fs.readFile('./big_file.txt', (error,
data) => {
if (error) throw error
response.end(data)
console.log('done')
})
})
server.listen(8888)
console.log(8888)

运行这个例子 并在浏览器打开 http://localhost:8888/

打开活动监视器 / 任务管理器 搜索 node 查看内存占用 一下飙升到110M左右

这个例子的结论

  • 如果用户请求过来,由于big_file.txt 的大小是 100M左右,再加上 Node.js 本身占用的几兆,所以Node.js 总体占用的内存是 110兆内存
  • 一个用户占用 100M左右 10个呢? 服务器内存是不是就不够用了

第三个例子

用 stream 改写

1
2
3
4
5
6
7
8
9
const fs = require('fs');
const http = require('http');
const server = http.createServer()
server.on('request', (request, response) => {
const stream =
fs.createReadStream('./big_file.txt')
stream.pipe(response)
})
server.listen(8888)
  • 查看 Node.js 内存占用,基本不会超过 30MB
  • 文件 steam 和 response stream 是通过管道相连的
  • 流 可以使你的内存降的非常低

管道 Pipe

两个流可以用一个管道相连
stream1 的末尾接上 stream2 的开端
只要 stream1 有数据,就会流到 stream2

常用代码

1
stream1.pipe(stream2)

链式调用

1
2
3
4
5
a.pipe(b).pipe(c)

// 等价于
a.pipe(b)
b.pipe(c)

管道可以通过事件实现

1
2
3
4
5
6
7
8
// stream1 一有数据就塞给 stream2
stream1.on('data', (chunk)=>{
stream2.write(chunk)
})
// stream1 停了 ,就停了 stream2
stream1.on('end', ()=>{
stream2.end()
})

Stream 对象的原型链

s = fs.createReadStream(path)

  • 那么它的对象层级为
  • 自身属性(fs.createReadStream 构造)
  • 原型: stream.Readable.prototype
  • 二级原型: stream.Stream.prototype
  • 三级原型: events.EventEmitter.prototype
  • 四级原型: Object.prototype

Stream对象 都继承了 EventEmitter

如何查看它的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');
const stream = require('stream');
const events = require('events');
const s = fs.createReadStream('./big_file.txt')

console.log(s)

console.log(stream.Readable.prototype);

console.log(events.EventEmitter.prototype);

// 运行技巧
node --inspect-brk xxx.js
// 打开浏览器 点击 step over

支持的事件和方法

Readable Stream

事件

  • data,end
  • error,close,readable

方法

  • pipe()
  • unpipe()
  • wrap()
  • destroy()
  • read()
  • unshift()
  • resume() / pause()
  • isPaused()
  • setEncoding()

Writeable Stream

事件

  • drain 面试爱问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 假设 stream1 开始 一秒写 10M数据 后来 stream1 一秒写 1000M数据
    // 这样就会堵车 , 所有有了 drain 不堵车了
    stream1.on('data', (chunk)=>{
    const flag = stream2.write(chunk)
    if(flag === false){
    // done write 堵车了,你别写了
    }
    // 不堵车了
    stream2.on('drain',()=>{
    // go on write
    })

    })
    // stream1 停了 ,就停了 stream2
    stream1.on('end', ()=>{
    stream2.end()
    })
  • finish

  • error,close,pipe,unpipe

方法

  • write(),destroy(),end(),cork(),uncork(),setDefaultcoding()

Stream 分类

  • Readable 可读
  • Writeable 可写
  • Duplex 可读可写(双向) 同时读和写,注意 其实就是两个管道 但是两个管道没有交叉点
  • Transform 可读可写(变化) 例子 webpack 你写的是ES6 读到的是 ES5

Readable Stream

静止态 paused 和 流动态 flowing

  • 默认处于 paused 态
  • 添加 data 事件监听,它就变成 flowing
  • 删掉 data 事件监听, 它就变成 paused
  • paused() 可以让它 变为 paused
  • resume() 可以让它 变为 flowing
1
2
3
4
5
6
7
const fs = require('fs');
const stream = fs.createReadStream('./big_file.txt')
stream.pipe(response)
stream.pause();
setTimeout(()=>{
stream.resume
},3000)

Writeable Stream

drain 流干了事件

  • 表示可以加水了
  • 我们调用 stream.write(chunk)的时候,可能会得到 false
  • false 代表写的太快了,数据积压了
  • 这个时候不能在 write 了,要监听 drain
  • 等 drain 事件触发了,才能继续 write
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
const fs = require('fs')

function writeOneMillionTimes(writer, data) {
let i = 1000000
write()

function write() {
let ok = true
do {
i--
if (i === 0) {
// Last time!
writer.write(data)
} else {
// See if we should continue, or wait.
// Don't pass the callback, because we're not done yet.
ok = writer.write(data)
if (ok === false) {
console.log('不能再写了')
}
}
} while (i > 0 && ok)
if (i > 0) {
// Had to stop early!
// Write some more once it drains.
writer.once('drain', () => {
console.log('干涸了')
write()
})
}
}
}

const writer = fs.createWriteStream('./big_file.txt')
writeOneMillionTimes(writer, 'hello world')

finish 事件

  • 调用 steam.end() 之后 而且
  • 缓冲区数据已经传给底层系统之后
  • 触发 finish 事件

Node全解08_01sql进阶

sql进阶

sql 必会的内容

缓存字段

假设一个博客包含多个评论 comments

如何获取评论数

  • select count(id)form comments where blob_id = 9 这样太慢了
  • 可以在 blob 表添加一个 comment_count 字段
  • 每次添加 comment 则 +1
  • 每次删除 comment 则 -1

事务

1
2
3
start transaction;
语句1;语句2;语句3;
commit;

只要一句出错,则全部不生效

MySQL 存储引擎

  • SHOW ENGINEA;

常见的

  • InnoDB 默认
  • MyISAM 拥有较高的插入,查询速度 不支持事务
  • Memory 在内存中,快速访问数据
  • Archive 只支持 insert 和 select

InnoDB

  • 它是事务型数据库的首选,支持事务,遵循ACID、支持行锁和外键

索引

语法

  • create unique index index1 on users(name(100))
    • 唯一性的加 unique 反之去掉
    • users(name(100)) 表名(字段名(长度))
  • show index in users;

用途

  • 提高搜索效率
  • where xxx>100 那么我们可以创建 xxx的索引
  • where xxx>100 and yyy>200 创建 xxx,yyy的索引

Node全解07_01docker_mysql

docker 启动 mysql

  • docker 安装 不说了 自行解决
  • 进入 Docker 上面的 mysql 主页
  • 选择版本 如 5.7.27 或者 8.0.18
  • 使用docker run 容器的名字
  • name 是容器的名字
  • MYSQL_ROOT_PASSWORD 是密码
  • tag是 版本号,我们选 5.7.27
  • 再加一个端口映射 -p 3306:3306
  • 最终命令
1
2
3
4
5
6
7
8
docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7.27

# 查看容器
docker ps -a
# 删除容器
docker rm -f id
# 删除镜像
docker rmi -f id

注意

  • docker启动的容器默认不会持久化
  • 容器删掉了,数据就没了
  • 如果需要持久化,自行搜索 [docker mysql 数据目录]
  • 目前我们不需要持久化

连接 docker mysql

1
2
3
docker exec -it 镜像名称 bash
这句话就是进入容器内部,容器内是一个 linux系统
然后你就可以在这个系统里运行 mysql

mysql 命令

1
2
3
4
5
6
7
mysql -u root -p 回车 输入密码 123456
命令 show databases; 可以查看所有的数据库
如果你手抖就 ctrl + c 重新来
命令 use xxx; 选择使用的数据库
show tables; 查看对应库下的所有表

select * from user; 查看表内容

什么是数据库

数据库Database

  • 将大量数据保存起来,通过计算机加工而成的可以运行高效访问的数据集合称为数据库
  • 根据保存格式不同,数据库一般分为
    • 关系型数据库 使用最广泛
    • 面向对象数据库、xml数据库、键值对数据库、层次数据库

数据库管理系统 DBMS

  • 用来管理数据库的系统称之为数据库管理系统
  • 如 MySQL 、PostgreSQL、SQL Server、DB2、Oracle

mysql 一个bug

  • mysql不要使用 utf-8 记住永远不要再 mysql里使用 utf-8 而是用 utf-8mb4

Node.js 访问数据库

  • mysqljs 里面是例子代码
  • 新建目录 node-mysql
  • yarn init -y
  • 抄文档 yarn add mysql
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
var mysql = require('mysql');
var connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',

});

connection.connect();
/*
CREATE DATABASE IF NOT EXISTS fang
CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
*/
// 创建库
connection.query('CREATE DATABASE IF NOT EXISTS fang CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;', function (error, results, fields) {
if (error) throw error;
console.log(results);
});
connection.query('use fang;');
// 创建表
connection.query(`CREATE TABLE IF NOT EXISTS user(
name text,
age int
)`, function (error, results, fields) {
if (error) throw error;
console.log(results);
});

connection.end();

事务

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
var mysql = require('mysql');
var dbConfig = {
host: 'localhost',
user: 'root',
password: '123456',
};

const fn = (error, results, fields) => {
if (error) throw error;
console.log(results);
};
transaction([
`insert into user values('eee',${222})`,
`insert into user values('eee',${333})`,
`insert into user values('eee',${444})`,
], [
fn, fn, fn
])

async function transaction(queries, queryValues) {
if (queries.length !== queryValues.length) {
return Promise.reject(
'Number of provided queries did not match the number of provided query values arrays'
)
}
const connection = await mysql.createConnection(dbConfig);
connection.connect();
connection.query('use test_blog;')
try {
await connection.beginTransaction()
const queryPromises = []

queries.forEach((query, index) => {
queryPromises.push(connection.query(query, queryValues[index]))
})
const results = await Promise.all(queryPromises)
await connection.commit()
await connection.end()
return results
} catch (err) {
await connection.rollback()
await connection.end()
return Promise.reject(err)
}
}

CRM学习法 搞定mysql

  • 了解如何连接 mysql
  • 如何创建 数据库
  • 如何创建表
  • 对表 CRUD

如果你是新手千万不要写update 和 delete语句

推荐你用这个解决sql语法

  • devdocs.io 开启 postgresql 文档
  • 菜鸟教程

mysql 数据类型

五大类

  • 数字
    • bit
    • tinyint
    • bool,boolean
    • smallint
    • int
    • bigint
    • decimal
    • float
    • double
    • serial
    • 太多了
  • 字符串
    • char(100) 固定占用100字符,即使你存一个 字符串1
    • varchar(100) 变长存储 省空间
    • binary(1024)
    • varbinary(1024)
    • blob
    • text 存博客这种
    • enum(‘v1’,’v2’)
    • set(‘v1’,’v2’)
    • 具体看文档
  • 时间和日期类型
    • date 年月日
    • time 某事某分某秒
    • datetime 年月日 时分秒
    • timestamp 时间戳
    • year 只存年
    • 关注一下 ISO 8601
      • 很多程序员处理不好日期数据,就是因为不了解它
      • 如何把日期处理为 ISO 8601格式
  • JSON类型 5.7.8以上
  • 其他特殊类型

参考文档

  • dev.mysql.com
  • 菜鸟教程