winney

It is never too old to learn.

0%
winney

疫情实时大数据报告—React项目

React Dom Examples

React Examples

Create React App 中文文档

Create React App

VSCode扩展

生成react代码片段-ES7

在VSCode中搜’ES7’,选择‘ES7 React/Redux/GraphQL/React-Native snippets’

在新建的.jsx/.js文件中

  1. 输入rcc,快速生成class component:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import React, { Component } from 'react'

    export default class App extends Component {
    render() {
    return (
    <div>

    </div>
    )
    }
    }

  2. 输入rfc,快速生成function component:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import React from 'react'

    export default function App() {
    return (
    <div>

    </div>
    )
    }

  3. 输入rconst,快速生成constrctor:

    1
    2
    3
    4
    5
    6
    7
    constructor(props) {
    super(props)

    this.state = {

    }
    }
  4. 输入rcredux,快速生成redux模版:

    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 React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import { connect } from 'react-redux'

    export class test extends Component {
    static propTypes = {
    prop: PropTypes
    }

    render() {
    return (
    <div>

    </div>
    )
    }
    }

    const mapStateToProps = (state) => ({

    })

    const mapDispatchToProps = {

    }

    export default connect(mapStateToProps, mapDispatchToProps)(test)

生产环境的配置

react中引入css的方式有哪几种

快速注释

在vscode中,.jsx文件中,选中要注释的文字,按 CTRL + shift + /

1
{/* 提取组件 */}

setState

  1. 箭头函数
1
2
3
this.setState(state => ({
isShowWarn: !state.isShowWarn
}))

事件绑定

  1. bind()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    constructor(props) {
    super(props)

    this.state = {
    isShowWarn: false
    }

    this.handleClick = this.handleClick.bind(this)
    }


    handleClick() {
    this.setState(state => ({
    isShowWarn: !state.isShowWarn
    }))
    }

    <button onClick={this.handleClick}>点击</button>
  2. 箭头函数

    1
    2
    3
    4
    5
    6
    7
    handleClick = ()=>{
    this.setState(state => ({
    isShowWarn: !state.isShowWarn
    }))
    }

    <button onClick={this.handleClick}>点击</button>
  3. 箭头函数写在元素上

    1
    2
    3
    4
    5
    6
    7
    handleClick() {
    this.setState(state => ({
    isShowWarn: !state.isShowWarn
    }))
    }

    <button onClick={ (e)=> this.handleClick(e) }>点击</button>

key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值

1
2
3
4
<Post
key={post.id}
id={post.id}
title={post.title} />

组件中的return

1
return <div>内容</div>

返回多个html标签,换行写更清晰,使用return()

1
2
3
4
5
6
7
return(
<ul>
<li></li>
<li></li>
<li></li>
</ul>
)

组件,含有return,会返回react或null

函数组件,可以从参数props中拿到属性

class组件,可以从this.props中拿到属性

props的属性名==组件传进来的属性名

{}中可以写JavaScript代码,如果是对象,需要{{a:1,b:2}}

map函数,自带return

key放在组件<ListItems/>上,而不是放在<li><li/>

key在某个循环中保证唯一性就好,不用在整个页面中保证唯一性

避免key使用索引index,特别是反序操作的情况。 因为会重新渲染,导致性能变差。使用id等唯一性属性

map() 方法中的元素需要设置 key 属性

key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值:

1
2
3
4
5
6
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);

上面例子中,Post 组件可以读出 props.id,但是不能读出 props.key

四、引入路由

依赖包分间接依赖包和直接依赖包

1
cnpm i react-router-dom

在pages目录中,创建路由组件

React Router—看这里的最新用法

react-router—–Github

React Router中文文档

react-router-dom使用指南

v6文档:https://reactrouter.com

v5文档:https://v5.reactrouter.com/web/guides/quick-start

1
2
3
4
5
6
7
8
9
10
11
12
// 入口文件index.js
import React from "react";
import ReactDOM from "react-dom"
import { BrowserRouter } from "react-router-dom";
import App from './App'

ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 应用的根组件App.js
import React, {Component} from "react";
import {Routes, Route } from "react-router-dom";
import "./App.less";

import Login from "./pages/login/Login";
import Admin from "./pages/admin/Admin";

export default class App extends Component {
render() {
return (
<Routes>
<Route path="/login" element={<Login />}></Route>
<Route path="/" element={<Admin />}></Route>
</Routes>
)
}
}

重定向

1
2
3
4
5
6
import { Navigate } from 'react-router-dom';
function A(){
return (
<Navigate to="/b" />
)
}

引入antd样式

App.css

1
@import '~antd/dist/antd.css';

React中StrictMode严格模式

StrictMode 是一个用来检查项目中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

StrictMode 目前有助于:

1、识别不安全的生命周期
2、关于使用过时字符串 ref API 的警告
3、关于使用废弃的 findDOMNode 方法的警告
4、检测意外的副作用
5、检测过时的 context API

React Router

API接口

词汇表

获取 URL 参数

比如你访问 /foo?bar=baz,你可以通过访问 this.props.location.query.bar 从 Route 组件中获得 "baz" 的值。

1
2
3
4
5
6
7
8
9
10
React.render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
), document.body)

通过上面的配置,这个应用知道如何渲染下面四个 URL:

URL 组件
/ App
/about App -> About
/inbox App -> Inbox
/inbox/messages/:id App -> Inbox -> Message

createRoot

1
2
3
4
5
6
7
8
9
10
// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App tab="home" />);

轮播图-swiper

1
2
3
4
5
6
7
8
9
npm install swiper


// import Swiper JS
import Swiper from 'swiper';
// import Swiper styles
import 'swiper/css';

const swiper = new Swiper(...);

Swiper React Components

react移动端

Ant Design Mobile

ant-design-mobile——GitHub

1
npm install --save antd-mobile

国际化

textarea 标签

而在 React 中,<textarea> 使用 value 属性代替。这样,可以使得使用 <textarea> 的表单和使用单行 input 的表单非常类似:

1
<textarea value={this.state.value} onChange={this.handleChange} />

select 标签

由于 selected 属性的缘故,椰子选项默认被选中。React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。例如:

react打包正式环境-相对路径

1
npm run build

打包后,会有静态资源获取不到的报错

解决:

package.json文件中,加入homepage属性

1
"homepage": "."

react 项目打包路径问题

create-react-app打包上线页面空白的问题

1.项目用的是 BrowserRouter , BrowserRouter 一般是用于服务端渲染,所以服务端也需要相应的配置。要不然 网关不知道你有哪些路由,怎么给你转发。

解决:

  1. BrowserRouter 换成 HashRouter
    打包后,发现在本地开启web服务器预览后,正常,但是放在服务器上后,依然为空白,提示静态资源找不到。
  2. 解决:
    配置 package.json 中的 homepage:'./'
    这样可以使打包后的静态资源,采用相对路径。

react根据不同环境配置不同接口

三分钟教你搞定 React 项目多环境配置

React实现复制功能

1
npm i --save copy-to-clipboard
1
2
3
4
5
6
7
8
9
import copy from 'copy-to-clipboard';


handleClick = (e) => {
copy(e.target.value)
}


<input placeholder='请输入' onClick={this.handleClick}/>

Lazy和Suspense

1、React.lazy

React.lazy函数能让你像渲染常规组件一样处理动态引入(的组件)。

什么意思呢?其实就是懒加载。

(1)为什么代码要分割

当你的程序越来越大,代码量越来越多。一个页面上堆积了很多功能,也许有些功能很可能都用不到,但是一样下载加载到页面上,所以这里面肯定有优化空间。就如图片懒加载的理论。

(2)实现原理

当Webpack 解析到该语法时,它会自动地开始进行代码分割(Code Splitting),分割成一个文件,当使用到这个文件的时候会这段代码才会被异步加载。

(3)解决方案

React.lazy和常用的三方包react-loadable,都是使用了这个原理,然后配合webpack进行代码打包拆分达到异步加载,这样首屏渲染的速度将大大的提高。

由于React.lazy不支持服务端渲染,所以这时候react-loadable就是不错的选择。

2、如何使用React.lazy

下面示例代码使用create-react-app脚手架搭建

富文本编辑器

react-draft-wysiwyg——GitHub

1
npm install --save react-draft-wysiwyg draft-js
1
2
3
4
5
6
7
8
9
import { Editor } from "react-draft-wysiwyg";
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
<Editor
editorState={editorState}
toolbarClassName="toolbarClassName"
wrapperClassName="wrapperClassName"
editorClassName="editorClassName"
onEditorStateChange={this.onEditorStateChange}
/>;

react-draft-wysiwyg-文档

enzyme——GitHub

用于 React 的 JS 测试工具

React测试框架之enzyme

Enzyme学习笔记

React 测试利器之 Enzyme

全网最细:Jest+Enzyme测试React组件(包含交互、DOM、样式测试)

React官网

Create React App

创建应用

1
2
3
npx create-react-app my-app
cd my-app
npm start

react使用Apifox的项目案例,可参考Gitee/ad_manage_react项目

json-server的使用,可参考全球新闻发布系统项目

单页面应用

与页面或后续页面的任何交互,都不再需要往返 server 加载资源,即页面不会重新加载

编译器

Babel 是 React 最常用的 compiler

打包工具

常用的打包 React 应用的工具有 webpackBrowserify

管理工具

npmYarn 是两个常用的管理 React 应用依赖的 package 管理工具

JSX

React DOM 使用 camelCase(驼峰式命名)来定义属性的名称

组件

组件名称应该始终以大写字母开头(<Wrapper/> 而不是 <wrapper/>

props

props 是只读的。不应以任何方式修改它们

组合 vs 继承

props.children

每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容

1
<Welcome>Hello world!</Welcome>

少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}

function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}

在 React 中,我们也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title} </h1>
<p className="Dialog-message">
{props.message} </p>
</FancyBorder>
);
}

function WelcomeDialog() {
return (
<Dialog title="Welcome" message="Thank you for visiting our spacecraft!" /> );
}
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
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}

class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}

render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}

handleChange(e) {
this.setState({login: e.target.value});
}

handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}

state

当组件中的一些数据在某些时刻发生变化时,这时就需要使用 state 来跟踪状态

stateprops 之间最重要的区别是:props 由父组件传入,而 state 由组件本身管理。组件不能修改 props,但它可以修改 state

生命周期方法

受控组件 vs 非受控组件

Ref

状态提升

应该在 React 组件的哪个生命周期函数中发起 AJAX 请求?

componentDidMount 这个生命周期函数中发起 AJAX 请求

工具链

JSX 防止注入攻击

所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击

JSX 表示对象

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用

React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:

1
2
3
4
5
6
7
8
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};

这些对象被称为 “React 元素”。

仅使用 React 构建的应用通常只有单一的根 DOM 节点。如果你在将 React 集成进一个已有应用,那么你可以在应用中包含任意多的独立根 DOM 节点。

React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的 UI。

组件

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。

1
2
3
4
5
6
7
8
9
10
<Avatar user={props.author} />

function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改

不要直接修改 State

应该使用 setState()

构造函数是唯一可以给 this.state 赋值的地方

State 的更新可能是异步的

1
2
3
4
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
1
2
3
4
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

条件渲染

元素变量

1
2
3
4
5
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}

与运算符 &&

true && expression 总是会返回 expression, 而 false && expression 总是会返回 false

注意:下面示例中,render 方法的返回值是 <div>0</div>

1
{ count && <h1>Messages: {count}</h1>}

三目运算符

1
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
1
2
3
4
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}

阻止组件渲染

render 方法直接返回 null,而不进行任何渲染

受控组件

表单元素(如<input><textarea><select>)通常自己维护 state

文件 input 标签

因为它的 value 只读,所以它是 React 中的一个非受控组件

处理多个输入

当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。

1
2
3
4
5
6
7
8
9
handleInputChange(event) {
const target = event.target;
const value = target.name === 'isGoing' ? target.checked : target.value;
const name = target.name;

this.setState({
[name]: value
});
}

等同 ES5:

1
2
var partialState = {};
partialState[name] = value;this.setState(partialState);

提交事件

1
2
<form onSubmit={this.handleSubmit}>
</form>

组合 vs 继承

包含关系

有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。

我们建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:

1
{props.children}

少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop

1
2
3
4
5
6
7
8
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>

将设计好的 UI 划分为组件层级

编写组件步骤

  1. 第一步:将设计好的 UI 划分为组件层级
  2. 第二步:用 React 创建一个静态版本
  3. 第三步:确定 UI state 的最小(且完整)表示
  4. 第四步:确定 state 放置的位置
  5. 第五步:添加反向数据流

通过问自己以下三个问题,你可以逐个检查相应数据是否属于 state:

  1. 该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。
  2. 该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。
  3. 你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。

包含所有产品的原始列表是经由 props 传入的,所以它不是 state;搜索词和复选框的值应该是 state,因为它们随时间会发生改变且无法由其他数据计算而来;经过搜索筛选的产品列表不是 state,因为它的结果可以由产品的原始列表根据搜索词和复选框的选择计算出来。

综上所述,属于 state 的有:

  • 用户输入的搜索词
  • 复选框是否选中的值

哪个组件应该拥有某个 state

  • 找到他们的共同所有者(common owner)组件(在组件层级上高于所有需要该 state 的组件)。

state 只能由拥有它们的组件进行更改

state在哪个组件,修改state的方法就在哪个组件

生命周期图谱

组件的生命周期

React权限菜单设计

React项目配置6(前后端分离如何控制用户权限)

(源码开放) React + webpack3 多页面应用 及 常见问题解答

10分钟快速搭建React权限菜单设计
React 组件权限控制的实现

React的React.FC与React.Component的初步认识

useMemo和useEffect有什么区别?怎么使用useMemo

useMemo 使用指南

react-hooks中的一些懵逼点

useEffect只能在DOM更新后再触发再去控制

memo是在DOM更新前触发的,就像官方所说的,类比生命周期就是shouldComponentUpdate

在前端开发的过程中,我们需要缓存一些内容,以避免在需渲染过程中因大量不必要的耗时计算而导致的性能问题。为此 React 提供了一些方法可以帮助我们去实现数据的缓存,useMemo 就是其中之一

React Hooks

React Hooks 解析(上):基础

React Hooks 解析(下):进阶

useCallback

解释这个 Hook 之前先理解下什么是副作用。网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的。正常情况下,在Function Component的函数体中,是不建议写副作用代码的,否则容易出 bug。

在绝大多数情况下,useEffectHook 是更好的选择。唯一例外的就是需要根据新的 UI 来进行 DOM 操作的场景。useLayoutEffect会保证在页面渲染前执行,也就是说页面渲染出来的是最终的效果。如果使用useEffect,页面很可能因为渲染了 2 次而出现抖动。

context

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const ThemeContext = React.createContext('light');

<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}

class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

Context.Consumer

消费多个 Context

注意事项

因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染

为了防止这种情况,将 value 状态提升到父节点的 state 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}

render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
useContext
1
2
3
4
5
6
7
8
9
10
11
function HeaderBar() {
const user = useContext(CurrentUser);
const notifications = useContext(Notifications);

return (
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
);
}
useReducer

useReducer的用法跟 Redux 非常相似,当 state 的计算逻辑比较复杂又或者需要根据以前的值来计算时,使用这个 Hook 比useState会更好。

useCallback
1
2
3
4
5
6
7
8
function Foo() {
const [count, setCount] = useState(0);

const memoizedHandleClick = useCallback(
() => console.log(`Click happened with dependency: ${count}`), [count],
);
return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}

useCallback缓存的是方法的引用,而useMemo缓存的则是方法的返回值。使用场景是减少不必要的子组件渲染:

useRef
1
2
3
4
5
6
7
8
9
function() {
const myRef = useRef(null);

useEffect(() => {
myRef.current.focus();
}, [])

return <input ref={myRef} type="text" />;
}

useRef返回一个普通 JS 对象,可以将任意数据存到current属性里面,就像使用实例化对象的this一样。另外一个使用场景是获取 previous props 或 previous state

自定义 Hook

自定义 Hook 的命名有讲究,必须以use开头,在里面可以调用其它的 Hook。入参和返回值都可以根据需要自定义,没有特殊的约定。使用也像普通的函数调用一样,Hook 里面其它的 Hook(如useEffect)会自动在合适的时候调用

Effect Hook

在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。(我们会在使用 Effect Hook 里展示对比 useEffect 和这些方法的例子。)

Hook概览

懒加载-React.lazy

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

异常捕获边界

基于路由的代码分割

错误边界(Error Boundaries)

Refs 转发

1
2
3
4
5
6
7
8
9
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

出于同样的原因,当 React.forwardRef 存在时有条件地使用它也是不推荐的:它改变了你的库的行为,并在升级 React 自身时破坏用户的应用。

高阶组件

高阶组件是参数为组件,返回值为新组件的函数。

组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。

深入 JSX

if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。但是,你可以用在 JSX 以外的代码中

字符串字面量

如下两个 JSX 表达式是等价的:

1
2
3
<MyComponent message="hello world" />

<MyComponent message={'hello world'} />

当你将字符串字面量赋值给 prop 时,它的值是未转义的。所以,以下两个 JSX 表达式是等价的:

1
2
3
<MyComponent message="&lt;3" />

<MyComponent message={'<3'} />
Props 默认值为 “True”

以下两个 JSX 表达式是等价的

1
2
3
<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />
属性展开

以下两个组件是等价的:

1
2
3
4
5
6
7
8
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}

你还可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};

const App = () => {
return (
<div>
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
);
};
字符串字面量

你可以将字符串放在开始和结束标签之间,此时 props.children 就只是该字符串。这对于很多内置的 HTML 元素很有用。例如:

1
<MyComponent>Hello world!</MyComponent>

React 组件也能够返回存储在数组中的一组元素:

1
2
3
4
5
6
7
8
9
render() {
// 不需要用额外的元素包裹列表元素!
return [
// 不要忘记设置 key :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}

JavaScript 表达式可以被包裹在 {} 中作为子元素。例如,以下表达式是等价的:

1
2
3
<MyComponent>foo</MyComponent>

<MyComponent>{'foo'}</MyComponent>
布尔类型、Null 以及 Undefined 将会忽略
1
2
3
4
5
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>

如果你想渲染 falsetruenullundefined 等值,你需要先将它们转换为字符串

1
2
3
<div>
My JavaScript variable is {String(myVariable)}.
</div>

性能优化

Brunch

通过安装 terser-brunch 插件,来获得最高效的 Brunch 生产构建:

避免调停
1
2
3
shouldComponentUpdate(nextProps, nextState) {
return true;
}

如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。

如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

1
2
3
4
5
6
7
8
9
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate

不可变数据的力量

1
2
3
4
5
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
1
2
3
4
5
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};

为了不改变原本的对象,我们可以使用 Object.assign 方法:

1
2
3
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}

添加对象扩展属性以使得更新不可变对象变得更方便:

1
2
3
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}

Polyfill

HTML5 Cross Browser Polyfills

Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

1
ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

1
2
3
4
5
6
7
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode );
}

一个 portal 的典型用例是当父组件有 overflow: hiddenz-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

Profiler

Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

不使用 ES6的使用方法

无论是函数组件还是 class 组件,都拥有 defaultProps 属性:

1
2
3
Greeting.defaultProps = {
name: 'Mary'
};

如果使用 createReactClass() 方法创建组件,那就需要在组件中定义 getDefaultProps() 函数:

1
2
3
4
5
getDefaultProps: function() {
return {
name: 'Mary'
};
},

初始化 State

自动绑定

为了保险起见,以下三种做法都是可以的:

  • 在 constructor 中绑定方法。
  • 使用箭头函数,比如:onClick={(e) => this.handleClick(e)}
  • 继续使用 createReactClass

Render Props

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
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}

class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>

{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}

class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 如果你出于某种原因真的想要 HOC,那么你可以轻松实现
// 使用具有 render prop 的普通组件创建一个!
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}

render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

移动端事件使用onTouchMove

1
2
3
4
5
6
7
onTouchMove={handleMouseMove}

// 使用event.touches[0].clientX
// this.setState({
// x: event.touches[0].clientX - 30,
// y: event.touches[0].clientY - 30
// });

静态类型检查

建议在大型代码库中使用 Flow 或 TypeScript 来代替 PropTypes

flow
TypeScript
1
npx create-react-app my-app --template typescript

将 TypeScript 添加到现有的 Create React App 项目中,请参考此文档.

配置 TypeScript 编译器

1
yarn run tsc --init

如果你使用 npm,执行:

1
npx tsc --init

tsconfig.json 文件中,有许多配置项用于配置编译器。查看所有配置项的的详细说明,请参考此文档

1
2
3
4
5
6
7
8
9
10
// tsconfig.json

{
"compilerOptions": {
// ...
"rootDir": "src",
"outDir": "build"
// ...
},
}

TypeScript React Starter 提供了一套默认的 tsconfig.json 帮助你快速上手。

在 React 中,你的组件文件大多数使用 .js 作为扩展名。在 TypeScript 中,提供两种文件扩展名:.ts 是默认的文件扩展名,而 .tsx 是一个用于包含 JSX 代码的特殊扩展名。

运行 TypeScript

如果你按照上面的说明操作,现在应该能运行 TypeScript 了。

1
yarn build

如果你使用 npm,执行:

1
npm run build

如果你没有看到输出信息,这意味着它编译成功了

严格模式

你可以为应用程序的任何部分启用严格模式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';

function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
);
}

StrictMode 目前有助于:

渲染阶段的生命周期包括以下 class 组件方法:

  • constructor
  • componentWillMount (or UNSAFE_componentWillMount)
  • componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
  • componentWillUpdate (or UNSAFE_componentWillUpdate)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • setState 更新函数(第一个参数)

使用 PropTypes 进行类型检查

1
2
3
4
5
6
7
8
9
10
11
12
13
import PropTypes from 'prop-types';

class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}

Greeting.propTypes = {
name: PropTypes.string
};

出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

默认 Prop 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}

// 指定 props 的默认值:
Greeting.defaultProps = {
name: 'Stranger'
};

// 渲染出 "Hello, Stranger":
ReactDOM.render(
<Greeting />,
document.getElementById('example')
);

defaultProps 用于确保 this.props.name 在父组件没有指定其值时,有一个默认值。

非受控组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}

handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}

如果你还是不清楚在某个特殊场景中应该使用哪种组件,那么 这篇关于受控和非受控输入组件的文章 会很有帮助。

默认值
1
2
3
4
<input
defaultValue="Bob"
type="text"
ref={this.input} />

希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value

同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

文件输入

在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。

1
<input type="file" />

在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

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
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(
`Selected file - ${this.fileInput.current.files[0].name}`
);
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}

Web Components

React 顶层 API

转换元素

React 提供了几个用于操作元素的 API:

Fragments

React 还提供了用于减少不必要嵌套的组件。

Refs
Suspense

Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。目前,Suspense 仅支持的使用场景是:通过 React.lazy 动态加载组件。它将在未来支持其它使用场景,如数据获取等。

Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。Hook 拥有专属文档章节和单独的 API 参考文档:

React.PureComponent

React.PureComponentReact.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

注意

React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 props 和 state 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。

React.memo
1
2
3
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});

React.memo高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

1
2
3
4
5
6
7
8
9
10
11
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);

此方法仅作为**性能优化**的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。

createElement()
1
2
3
4
5
React.createElement(
type,
[props],
[...children]
)
cloneElement()
1
2
3
4
5
React.cloneElement(
element,
[props],
[...children]
)
isValidElement()
1
React.isValidElement(object)

验证对象是否为 React 元素,返回值为 truefalse

React.Children

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

React.createRef
React.forwardRef
React.lazy
1
2
// 这个组件是动态加载的
const SomeComponent = React.lazy(() => import('./SomeComponent'));
React.Suspense

React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。目前,懒加载组件是 <React.Suspense> 支持的唯一用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
// 显示 <Spinner> 组件直至 OtherComponent 加载完成
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
);
}

React.Component

组件的生命周期

生命周期图谱

shouldComponentUpdate()

此方法仅作为**性能优化的方式**而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。

constructor()
1
constructor(props)

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

setState()
1
2
3
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
forceUpdate()
defaultProps
state

State & 生命周期

DOM 元素

合成事件

React 术语词汇表

Hook 简介

自定义 Hook

useContext

useReducer

useState

1
2
3
4
5
6
7
8
9
10
11
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}

与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象

1
2
3
4
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});

useEffect

需要清除的 effect

清除 effect
1
2
3
4
5
6
7
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});

effect 的执行时机

useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

Hook 规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook

自定义 Hook

Hook API 索引

useContext

即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

把如下代码与 Context.Provider 放在一起

useReducer
1
const [state, dispatch] = useReducer(reducer, initialArg, init);
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
用 reducer 重写 useState 一节的计数器示例:

const initialState = {count: 0};

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
惰性初始化
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 init(initialCount) {
return {count: initialCount};
}

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}

function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useCallback

每次渲染,函数会生成一个新的引用地址(引用类型,跟对象/数组一样)

  • 保持一个函数的不变性
  • 可以说是useMemo的一个语法糖,专门处理函数

The useCallback hook

下面两个代码是等价的:

useMemo

1
2
3
4
5
const handleMegaBoost = React.useMemo(() => {
return function() {
setCount((currentValue) => currentValue + 1234);
}
}, []);

useCallback

1
2
3
const handleMegaBoost = React.useCallback(() => {
setCount((currentValue) => currentValue + 1234);
}, []);
1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useMemo

Understanding useMemo and useCallback

课程

  • 做快照,减少大量运算
  • 保持一个值的不变性

为了减少一些大量计算(需要计算的数据没有改变,但是其他数据改变,重新渲染,会引起大量计算)

useMemo takes two arguments:(两个参数)

  1. A chunk of work to be performed, wrapped up in a function(大量计算的函数)
  2. A list of dependencies(依赖)

**useMemo is essentially like a lil’ cache, and the dependencies are the cache invalidation strategy.**(像缓存)

This is commonly known as memoization, and it’s why this hook is called “useMemo”.(记忆化,备忘)

消耗性能的案例(每秒time变化,都会进行一次大量计算)
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 React, {useState, useEffect} from 'react'

export default function Customevent() {
const [num, setNum] = useState(10);
const [time, setTime] = useState(new Date())

useEffect(() => {
setTimeout(() => {
setTime(new Date())
}, 1000);
},[time])

// 大量计算
const heavyComputations = () => {
console.log('正在进行大量计算')
return num;
}

return (
<div>
<h1>{time.toLocaleString()}</h1>
<input type="text" value={num} onChange={ (e) => {setNum(e.target.value)}}/>

<div>{heavyComputations()}</div>
</div>
)
}
使用usememo之后,只有num变化时,才进行大量的计算
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
import React, {useState, useEffect, useMemo} from 'react'

export default function Customevent() {
const [num, setNum] = useState(10);
const [time, setTime] = useState(new Date())

useEffect(() => {
setTimeout(() => {
setTime(new Date())
}, 1000);
},[time])

// 大量计算
const heavyComputations = () => {
console.log('正在进行大量计算')
return num;
}

const result = useMemo(() => {
return heavyComputations();
}, [num]);

return (
<div>
<h1>{time.toLocaleString()}</h1>
<input type="text" value={num} onChange={ (e) => {setNum(e.target.value)}}/>

<div>{result}</div>
</div>
)
}

I’ve extracted two new components, Clock and PrimeCalculator. By branching off from App, these two components each manage their own state. A re-render in one component won’t affect the other.(可以将它们分成两个组件,各自管理它们自己的状态,互不影响)

React.memo wraps around our component and protects it from unrelated updates. Our PurePrimeCalculator will only re-render when it receives new data, or when its internal state changes.(可以使用 React.memo包裹组件,让它成为一个纯组件【输入不改变的时候,输出不改变】)

改为独立组件

这样也是每秒都执行一次大量计算函数

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 React, {useState, useEffect} from 'react'

export default function Customevent() {
const [time, setTime] = useState(new Date())

useEffect(() => {
setTimeout(() => {
setTime(new Date())
}, 1000);
},[time])

return (
<div>
<h1>{time.toLocaleString()}</h1>
<ShowInput/>
</div>
)
}

function ShowInput() {
const [num, setNum] = useState(10);
// 大量计算
const heavyComputations = () => {
console.log('正在进行大量计算')
return num;
}

return (
<div>
<input type="text" value={num} onChange={ (e) => {setNum(e.target.value)}}/>

<div>{heavyComputations()}</div>
</div>
)
}

解决:使用React.memo将ShowInput组件改为纯函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {useState, useEffect} from 'react'
const PureShowInput = React.memo(ShowInput);

export default function Customevent() {
const [time, setTime] = useState(new Date())

useEffect(() => {
setTimeout(() => {
setTime(new Date())
}, 1000);
},[time])

return (
<div>
<h1>{time.toLocaleString()}</h1>
<PureShowInput/>
</div>
)
}

function ShowInput() {
.....
}

也可以在导出的时候,直接将其导出为纯组件

1
2
3
4
5
// PrimeCalculator.js
function PrimeCalculator() {
/* Component stuff here */
}
export default React.memo(PrimeCalculator);
使用纯组件(React.memo)的方法,还是会进行大量计算

Boxes.js

1
export default React.memo(Boxes);

App.js

1
2
3
4
5
6
7
const boxes = [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];

<Boxes boxes={boxes} />

原因:every time React re-renders, we’re producing a brand new array. They’re equivalent in terms of value, but not in terms of reference.(组件每次重新渲染,会生成一个全新的数组,引用类型,地址就发生了改变,所以Boxes会重新渲染)

解决方法:使用useMemo,依赖boxWidth,当boxWidth改变时才更新

1
2
3
4
5
6
7
const boxes = React.useMemo(() => {
return [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
}, [boxWidth]);

当使用context时,父组件的值改变的时候,会引起子组件的更新。如果不想子组件每次都更新,可以使用useMemo,只有当依赖user, status, forgotPwLink发生改变时,再更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const AuthContext = React.createContext({});
function AuthProvider({ user, status, forgotPwLink, children }){
const memoizedValue = React.useMemo(() => {
return {
user,
status,
forgotPwLink,
};
}, [user, status, forgotPwLink]);
return (
<AuthContext.Provider value={memoizedValue}>
{children}
</AuthContext.Provider>
);
}
useRef
useImperativeHandle
useLayoutEffect
useDebugValue

Hooks FAQ

测试概览

Jest

AJAX and APIs

比如社区比较流行的 AxiosjQuery AJAX,或者是浏览器内置的 window.fetch

componentDidMount 这个生命周期函数中发起 AJAX 请求

在组件中使用事件处理函数

Yehuda Katz 的文章详细解释了什么是绑定,以及函数在 JavaScript 中怎么起作用。

传递参数

1
<button onClick={() => this.handleClick(id)} />
1
<button onClick={this.handleClick.bind(this, id)} />
通过箭头函数传递参数
1
2
3
4
5
6
7
<ul>
{this.state.letters.map(letter =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
通过 data-attributes 传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
handleClick(e) {
this.setState({
justClicked: e.target.dataset.letter
});
}

<ul>
{this.state.letters.map(letter =>
<li key={letter} data-letter={letter} onClick={this.handleClick}>
{letter}
</li>
)}
</ul>
怎样阻止函数被调用太快或者太多次?

如果你有一个 onClick 或者 onScroll 这样的事件处理器,想要阻止回调被触发的太快,那么可以限制执行回调的速度,可以通过以下几种方式做到这点:

  • 节流:基于时间的频率来进行抽样更改 (例如 _.throttle)
  • 防抖:一段时间的不活动之后发布更改 (例如 _.debounce)
  • requestAnimationFrame 节流:基于 requestAnimationFrame 的抽样更改 (例如 raf-schd)

可以看这个比较 throttle 和 debounce 的可视化页面

节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import throttle from 'lodash.throttle';

class LoadMoreButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickThrottled = throttle(this.handleClick, 1000);
}

componentWillUnmount() {
this.handleClickThrottled.cancel();
}

render() {
return <button onClick={this.handleClickThrottled}>Load More</button>;
}

handleClick() {
this.props.loadMore();
}
}

防抖

防抖确保函数不会在上一次被调用之后一定量的时间内被执行。当必须进行一些费时的计算来响应快速派发的事件时(比如鼠标滚动或键盘事件时),防抖是非常有用的。下面这个例子以 250ms 的延迟来改变文本输入。

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 debounce from 'lodash.debounce';

class Searchbox extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.emitChangeDebounced = debounce(this.emitChange, 250);
}

componentWillUnmount() {
this.emitChangeDebounced.cancel();
}

render() {
return (
<input
type="text"
onChange={this.handleChange}
placeholder="Search..."
defaultValue={this.props.value}
/>
);
}

handleChange(e) {
this.emitChangeDebounced(e.target.value);
}

emitChange(value) {
this.props.onChange(value);
}
}

组件状态

课程链接:nodejs教程_2020年最新NodeJs+Express+Mongodb+Mongoose入门实战教程

判断是否安装成功

1
node -v

Vscode快速生成代码块

在vscode里面安装node-snippets,会有node代码的相关代码提示。安装完之后,重启vscode即可生效

http模块-基本使用

例如,在app.js中输入nodehttp,会有node-http-server等语句的提示,选择node-http-server,会生成对应的代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//表示引入http模块
var http = require('http');
/*
request 获取客户端传过来的信息
response 给浏览器响应信息
*/
http.createServer(function (request, response) {
//设置响应头
response.writeHead(200, {'Content-Type': 'text/plain'});
//表示给我们页面上面输出一句话并且结束响应
response.end('Hello World');
}).listen(8081); // 端口

console.log('Server running at http://127.0.0.1:8081/');

运行:

1
node app.js

在控制台显示:

1
Server running at http://127.0.0.1:8081/

浏览器访问http://127.0.0.1:8081/,可看到:

1
Hello World

如果修改了app.js里面的内容,需要重新运行node app.js

http模块-基本使用2

1
2
3
4
5
6
7
8
9
10
11
12
13
const http = require('http');

http.createServer((req, res) =>{
console.log(req.url);

// 设置响应头
// 状态码 200, 文件类型是htm1,字符集是utf-8
res.writeHead(200, {"Content-type": "text/html;charset='utf-8'"}); // 解决乱码

res.write('你好 nodejs');

res.end(); // 一定要写end
}).listen(3001)

浏览器访问:http://127.0.0.1:3001/,显示:

1
浣犲ソ nodejs

加上以下代码,解决乱码:

1
res.write('<head> <meta charset="UTF-8"></head>');  // 解决乱码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http');

http.createServer((req, res) =>{
console.log(req.url);

// 设置响应头
// 状态码 200, 文件类型是htm1,字符集是utf-8
res.writeHead(200, {"Content-type": "text/html;charset='utf-8'"}); // 解决乱码

res.write('<head> <meta charset="UTF-8"></head>'); // 解决乱码

res.write('你好 nodejs');

res.write('<h2>你好 nodejs</h2>');

res.end(); // 一定要写end
}).listen(3001)

重新启动

1
node app.js

url模块-基本使用

nodejs的内置模块

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

var api = 'http://www.itying.com?name=zhangsan&age=20';

var params = url.parse(api, true).query;

console.log(`姓名: ${params.name}--年龄: ${params.age}`)

控制台输出:

1
姓名: zhangsan--年龄: 20

http+url模块的基本使用

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
const http = require('http');
const url = require('url');

http.createServer((req, res) =>{
console.log(req.url);

// http://127.0.0.1:3001/?name=zhangsan&age=20

// 设置响应头
// 状态码 200, 文件类型是htm1,字符集是utf-8
res.writeHead(200, {"Content-type": "text/html;charset='utf-8'"}); // 解决乱码

res.write('<head> <meta charset="UTF-8"></head>'); // 解决乱码

if(req.url != '/favicon.ico') {
var userinfo = url.parse(req.url, true).query;
console.log(`姓名: ${userinfo.name}--年龄: ${userinfo.age}`)
}


res.write('你好 nodejs');

res.write('<h2>你好 nodejs</h2>');

res.end(); // 一定要写end
}).listen(3001)

在浏览器访问:http://127.0.0.1:3001/?name=zhangsan&age=20

控制台显示:姓名: zhangsan--年龄: 20

Nodejs自启动工具supervisor

  1. 安装supervisor

    1
    npm i -g supervisor
  2. 使用supervisor代替node命令启动应用

    1
    supervisor app.js         

    注意:如果在vscode中,识别不了supervisor命令,需要重启vscode

CommonJs 和Nodejs模块、自定义模块

Nodejs中的模块化

Node应用由模块组成,采用CommonJS模块规范。

在nodejs中,模块分为两类:

  • 核心模块(node提供的模块,如:http模块、url模块、fs模块都是nodejs内置的核心模块,可以直接引入使用)
  • 文件模块(用户编写的模块)

module/tools.js

自定义模块
1
2
3
4
5
6
7
8
9
10
/*
1.我们可以把公共的功能抽离成为一个单独的js 文件作为一个模块,默认情况下面这个模块里面的方法或者属性,外面是没法访问的。如果要让外部可以访问模块里面的方法或者属性,就必须在模块里面通过 exports或者module.exports 暴露属性或者方法。
2.在需要使用这些模块的文件中,通过require的方式引入这个模块。这个时候就可以使用模块里面暴露的属性和方法。
*/

function formatApi(api) {
return 'http://www.itying.com/' + api
}

exports.formatApi = formatApi;
外部引用
1
2
3
4
5
const tools = require('./module/tools')

var api = tools.formatApi('api/focus');

res.write(api);

在浏览器看到的输出:

1
http://www.itying.com/api/focus

exports的使用

request.js

1
2
3
4
5
6
7
8
9
10
var obj = {
get:function() {
console.log('从服务器获取数据')
},
post: function(){
console.log('提交数据')
}
}

exports.xxxx = obj;

app.js

1
2
3
4
5
const request = require('./module/request');

console.log(request);

request.xxxx.get();

运行:

1
2
3
4
5
node app.js

// 控制台输出:
{ xxxx: { get: [Function: get], post: [Function: post] } }
从服务器获取数据

module.export的使用

request.js

1
2
3
4
5
6
7
8
9
10
var obj = {
get:function() {
console.log('从服务器获取数据')
},
post: function(){
console.log('提交数据')
}
}

module.exports = obj;

app.js

1
2
3
4
5
const request = require('./module/request');

console.log(request);

request.get();

运行:

1
2
3
4
5
node app.js

// 控制台输出:
{ get: [Function: get], post: [Function: post] }
从服务器获取数据

exports单独导出

request.js

1
2
3
4
5
6
7
8
// 单独导出
exports.get = function() {
console.log('从服务器获取数据')
}

exports.post = function() {
console.log('提交数据')
}

app.js

1
2
3
4
5
const request = require('./module/request');

console.log(request);

request.get();

运行:

1
2
3
4
5
node app.js

// 控制台输出:
{ get: [Function: get], post: [Function: post] }
从服务器获取数据

node_modules目录的模块使用

在项目目录中,新建node_modules目录,创建axios目录,新建index.js文件

node_modules\axios\index.js

1
2
3
4
5
6
7
exports.get =  function() {
console.log('从服务器获取数据')
}

exports.post = function() {
console.log('提交数据')
}

app.js

1
2
3
4
5
6
7
const axios = require('./node_modules/axios/index.js');

console.log(axios);

axios.get();

axios.post();

运行:

1
2
3
4
5
6
node app.js

// 控制台输出:
{ get: [Function (anonymous)], post: [Function (anonymous)] }
从服务器获取数据
提交数据
node_modules目录的模块–引用时的简写

nodejs里面,node_modules目录里面定义的模块,可以省略node_modules路径

1
const axios = require('axios/index.js');

index.js文件为默认执行文件,也可以省略写

1
const axios = require('axios');
node_modules目录的模块(非index.js文化)–引用

node_modules\db\db.js

1
2
3
4
5
6
7
exports.find =  function() {
console.log('查找数据')
}

exports.add = function() {
console.log('增加')
}

app.js

1
2
const db = require('db');
db.add();

运行:

1
2
3
4
5
6
node app.js

// 控制台输出:
throw err;
^
Error: Cannot find module 'db'

因为db里面的db.js不是index.js

解决方案:

切换到node_modules\db目录,执行以下代码:

1
2
3
npm init --yes

npm init -y

package.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "db",
"version": "1.0.0",
"description": "",
"main": "db.js", // 入口文件是db.js
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

重新运行:

1
2
3
4
node app.js

// 控制台输出:
增加

Nodejs中的包、npm 、第三方模块、package.json以及cnpm

Nodejs中除了它自己提供的核心模块外,我们可以自定义模块,也可以使用第三方的模块。Nodejs 中第三方模块由包组成,可以通过包来对一组具有相互依赖关系的模块进行统一管理。

包

完全符合CommonJs规范的包目录一般包含如下这些文件。
  • package.json:包描述文件。
  • bin:用于存放可执行二进制文件的目录。
  • lib:用于存放JavaScript代码的目录。
  • doc:用于存放文档的目录。

在 NodeJs中通过NPM命令来下载第三方的模块(包)。

npm

npm是世界上最大的开放源代码的生态系统。我们可以通过npm下载各种各样的包,这些源代码(包)我们可以在 https://www.npmjs.com找到。

npm是随同NodeJS一起安装的包管理工具,能解决NodeJS 代码部署上的很多问题,常见的使用场景有以下几种:

  • 允许用户从NPM服务器下载别人编写的第三方包到本地使用。(silly-datetime)
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序(工具)到本地使用。(supervisor)
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。
–save

安装依赖包的时候,加上--save,这样在package.json文件中的dependencies中就有包的相关信息,当我们将package.json分享给别人的时候,别人直接执行npm icnpm i就可以安装项目中的依赖包。可以根据package.json来安装对应的依赖包

package.json

创建package.json:

1
npm init  或者  npm init -yes

npm i / cnpm i表示安装dependencies对应的包 如果删掉node_modules可以通过此命令找到 package.json对应的所有的包信息

1
2
3
4
5
"dependencies" : {
"ejs":"^2.3.4",
"express": "^4.13.3",
"formidable": "^1.0.17"
}
  • ^表示第一位版本号不变,后面两位取最新的
  • ~表示前两位不变,最后一个取最新
  • *表示全部取最新

如果要指定版本安装,将版本号前面的^/~/*去掉即可

淘宝镜像

淘宝NPM镜像是一个完整npmjs.org镜像,你可以用此代替官方版本(只读),同步频率目前为10分钟一次以保证尽量与官方服务同步。

我们可以使用我们定制的cnpm (gzip压缩支持)命令行工具代替默认的npm:

1
npm install -g cnpm --registry=https://registry.npm.taobao.org

Nodejs中的fs模块的使用

  1. fs.stat检测是文件还是目录
  2. fs.mkdir创建目录
  3. fs.writeFile创建写入文件
  4. fs.appendFile追加文件
  5. fs.readFile读取文件
  6. fs.readdir读取目录
  7. fs.rename重命名
  8. fs.rmdir删除目录
  9. fs.unlink删除文件

创建package.json

1
npm init
1
2
3
4
5
6
7
8
9
10
11
{
"name": "02",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

检测是文件还是目录

app.js

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

fs.stat('./package.json', (err, data) => {
if(err) {
console.log(err);
return;
}

console.log(`是文件:${data.isFile()}`);
console.log(`是目录:${data.isDirectory()}`);
})

运行:

1
2
3
4
5
node app.js

// 控制台输出:
是文件:true
是目录:false

创建目录

app.js

1
2
3
4
5
6
7
8
fs.mkdir('./css', err => {
if(err) {
console.log(err);
return;
}

console.log('创建成功')
})

运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
node app.js

// 控制台显示:
创建成功

node app.js

// 控制台显示:
[Error: EEXIST: file already exists, mkdir 'H:\Gitee\nodejs\node_demo\02\css'] {
errno: -4075,
code: 'EEXIST',
syscall: 'mkdir',
path: 'H:\\Gitee\\nodejs\\node_demo\\02\\css'
}

创建写入文件

如果不存在文件,就直接创建文件且写入内容;如果存在文件,就直接写入内容,替换旧的

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
// app.js
fs.writeFile('./html/index.html', '你好nodejs', err => {
if(err) {
console.log(err);
return;
}

console.log('创建写入文件成功');
})

node app.js
// 控制台显示:
创建写入文件成功

// app.js
fs.writeFile('./html/index.html', '你好nodejs---写入文件', err => {
if(err) {
console.log(err);
return;
}

console.log('创建写入文件成功');
})

node app.js
// 控制台显示:
创建写入文件成功

参数说明:

1
2
3
4
5
6
7
filename        (String)                文件名称
data (String | Buffer) 将要写入的内容,可以使字符串或buffer数据。
options (object) option数组对象,包含:
· encoding (String) 可选值,默认‘utf8',当data使buffer时,该值应该为ignore
· mode (Number) 文件读写权限,默认值438
· flag (String) 默认值‘w’
callback {Function} 回调,传递一个异常参数err。

追加文件

如果不存在文件,就直接创建文件且写入内容;如果存在文件,就直接追加添加的内容到后面

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
fs.appendFile('./css/base.css', 'body{color:red}\n', err => {
if(err) {
console.log(err);
return;
}

console.log('追加文件成功');
})

node app.js
// 控制台显示:
追加文件成功

fs.appendFile('./css/base.css', 'h3{color:red}\n', err => {
if(err) {
console.log(err);
return;
}

console.log('追加文件成功');
})

node app.js
// 控制台显示:
追加文件成功

./css/base.css

1
2
3
body{color:red}
h3{color:red}

读取文件

1
2
3
4
5
6
7
8
9
fs.readFile('./html/index.html', (err, data) => {
if(err) {
console.log(err);
return;
}

console.log(data);
console.log(data.toString()); // 把Buffer转化成string类型
})
1
2
3
4
node app.js
// 控制台输出:
<Buffer e4 bd a0 e5 a5 bd 6e 6f 64 65 6a 73 2d 2d 2d 2d 2d e5 86 99 e5 85 a5 e6 96 87 e4 bb b6>
你好nodejs-----写入文件

读取目录

1
2
3
4
5
html
│ index.html
│ news.html

└─js
1
2
3
4
5
6
7
8
fs.readdir('./html', (err, data) => {
if(err) {
console.log(err);
return;
}

console.log(data);
})

读取该目录下的目录以及文件

1
2
3
node app.js
// 控制台输出:
[ 'index.html', 'js', 'news.html' ]

重命名·移动文件

功能:1、表示重命名2、移动文件

重命名
1
2
3
4
5
6
7
8
fs.rename('./css/aaa.css', './css/index.css', err => {
if(err) {
console.log(err);
return;
}

console.log('重命名成功');
})
移动文件
1
2
3
4
5
6
7
8
fs.rename('./css/index.css', './html/index.css', err => {
if(err) {
console.log(err);
return;
}

console.log('移动文件成功');
})

删除目录

如果目录里面有文件,直接删除目录,会删除失败

aaa目录里面有index.html文件

1
2
3
4
5
6
7
8
fs.rmdir('./aaa', err => {
if(err) {
console.log(err);
return;
}

console.log('删除目录成功');
})
1
2
3
4
5
6
7
8
node app.js
// 控制台输出:
[Error: ENOTEMPTY: directory not empty, rmdir 'H:\Gitee\nodejs\node_demo\02\aaa'] {
errno: -4051,
code: 'ENOTEMPTY',
syscall: 'rmdir',
path: 'H:\\Gitee\\nodejs\\node_demo\\02\\aaa'
}

所以要先删除该目录中的文件

删除文件

1
2
3
4
5
6
7
8
fs.unlink('./aaa/index.html', err => {
if(err) {
console.log(err);
return;
}

console.log('删除文件成功');
})
1
2
3
node app.js
// 控制台输出:
删除文件成功

然后再执行删除目录的命令

1
2
3
4
5
6
7
8
fs.rmdir('./aaa', err => {
if(err) {
console.log(err);
return;
}

console.log('删除目录成功');
})
1
2
3
node app.js
// 控制台输出:
删除目录成功

案例

判断服务器上面有没有upload目录。如果没有创建这个目录,如果有的话不做操作。(图片上传)

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');
var path = './upload';

fs.stat(path, (err, data)=> {
if(err) {
console.log(err);
mkdir(path);
return;
}

if(data.isDirectory()){
console.log('目录已经存在')
} else {
// 如果存在upload文件,首先删除文件,再去执行创建目录
fs.unlink(path, err =>{
if(err) {
console.log('请检测传入的数据是否正确');
} else {
mkdir(path);
}
})
}
})

// 创建目录的函数
function mkdir(path) {
fs.mkdir(path, err => {
if(err) {
console.log(err);
return;
}

console.log('创建目录成功');
})
}

mkdirp的使用

1
npm i mkdirp --save

创建目录

1
2
3
4
const mkdirp = require('mkdirp');
mkdirp('./uploadDir').then(made =>
console.log(`made directories, starting with ${made}`)
)

一次生成多级目录

1
2
3
const mkdirp = require('mkdirp');

mkdirp('./upload/img/xxxx');

案例2

wwwroot文件夹下面有images css js 以及index.html,找出wwwroot目录下面的所有的目录,然后放在一个数组中

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

fs.readdir('./wwwroot', (err, data) => {
if(err) {
console.log(err);
return;
}

console.log(data)
})
1
2
// 结果:
[ 'css', 'images', 'index.html', 'js' ] 包含文件

注意:fs里面的方法是异步的

fs.stat是异步方法,把fs.stat放在for循环里面执行,不起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 错误的写法  注意:fs里面的方法是异步的
var path = './wwwroot';
var dirArr = []; // 所以目录的数组

fs.readdir(path, (err, data) => {
if(err) {
console.log(err);
return;
}

for(var i = 0; i< data.length; i++) {
fs.stat(path + '/' + data[i], (err, stats) => {
if(stats.isDirectory()) {
dirArr.push(data[i])
}
})
}

console.log(dirArr);
})
console.log(dirArr);

for循环执行得比较快,100毫秒内已经执行完了这个循环

1
2
3
4
5
6
7
8
9
for(var i = 0; i<3; i++) {
setTimeout(function(){
console.log(i);
},100)
}
// 控制台显示:
3
3
3
解决方案
  1. 改造for循环 递归实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var path = './wwwroot';
    var dirArr = []; // 所以目录的数组

    fs.readdir(path, (err, data) => {
    if(err) {
    console.log(err);
    return;
    }

    (function getDir(i) {
    if(i === data.length) { // 执行完成
    console.log(dirArr);
    return;
    }
    fs.stat(path + '/' + data[i], (err, stats) => {
    if(stats.isDirectory()) {
    dirArr.push(data[i])
    }
    getDir(i+1);
    })
    })(0)
    })
    1
    2
    // 结果:
    [ 'css', 'images', 'js' ]
  2. nodejs里面的新特性 async await

Nodejs新特性async await的使用以及使用async await处理异步

使用回调函数,获取异步的值

1
2
3
4
5
6
7
8
9
10
11
function getData(callback){
// ajax
setTimeout(function(){
var name = '张三';
callback(name);
},1000);
}

getData(function(name){
console.log(name);
})
1
2
3
node app.js
// 控制台显示:
张三
Async、Await和Promise的使用

async是“异步”的简写,而 await可以认为是 async wait 的简写。所以应该很好理解 async用于申明一个异步的 function ,而await用于等待一个异步方法执行完成。

简单理解:

async:让方法变成异步

await:等待异步方法执行完成

使用promise获取异步的值

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
var p = new Promise((resolve, reject) => {
setTimeout(function(){
var name = '张三222';
resolve(name);
},1000);
})

/* p.then(function(res){
console.log(res);
}) */

p.then((res) =>{
console.log(res);
})

// 封装函数:
// 封装函数
function getData(resolve, reject){
setTimeout(function(){
var name = '张三222';
resolve(name);
},1000);
}

var p = new Promise(getData);
p.then(res => {
console.log(res);
})

async与await

1
2
3
4
5
6
7
8
9
10
async function test(){
return '学习nodejs';
}

async function main(){
var data = await test(); // 获取异步方法里面的数据 要与async配合使用
console.log(data);
}

main();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回promise
async function test(){
return new Promise((resolve, reject) =>{
setTimeout(function(){
var name = '张三333';
resolve(name);
},1000);
})
}

async function main(){
var data = await test(); // 获取异步方法里面的数据 要与async配合使用
console.log(data);
}

main();

案例2使用async和await的解决

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
const fs = require('fs');
// 1、定义一个isDir的方法判断一个资源到底是目录还是文件
async function isDir(path){
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if(err) {
console.log(err);
reject(err);
return;
}
if(stats.isDirectory()){
resolve(true);
}else {
resolve(false);
}
})
})
}

// 2、获取wwwroot里面的所有资源循环遍历
function main(){
var path = './wwwroot';
var dirArr = [];
fs.readdir(path, async (err, data) => {
if(err) {
console.log(err);
return;
}

for(var i = 0; i< data.length; i++) {
if(await isDir(path + '/' + data[i])){
dirArr.push(data[i]);
}
}

console.log(dirArr);
})
}

main();

Nodejs fs中的流以及管道流

fs.createReadStream从文件流中读取数据

以流的方式读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');

var readStream = fs.createReadStream('./input.txt');

var count = 0;
var str = '';

readStream.on('data', (chunk) => {
console.log(`${++count} 接收到:${chunk.length}`);
str+=chunk;
})

readStream.on('end', () => {
console.log(str);
console.log(count);
})

readStream.on('error', (err) => {
console.log(err);
})

fs.createWriteStream写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const fs = require('fs');
var str = '';
for(var i = 0; i< 500; i++) {
str+= '我是从数据库获取的数据,要保存起来\n';
}
// 创建一个可以写入的流,写入到文件output.txt中
var writeSteam = fs.createWriteStream('./output.txt');

// 使用utf8编码写入数据
// writeSteam.write(str, 'UTF8')

writeSteam.write(str);

// 标记写入完成
writeSteam.end();
// 要执行writeSteam.end(),才可以调用finish;不执行writeSteam.end(),也会写入完成,只是finish事件监听不到
// 异步
// 处理流事件——> finish事件
writeSteam.on('finish', () => { // finish--所有数据已被写入到底层系统时触发
console.log('写入完成'); // 控制台后打印
})

console.log('程序执行完毕'); // 控制台先打印

管道读写操作

复制文件,从一个流到另一个流

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

// 创建一个可读流
var readStream = fs.createReadStream('./aaa.jpg');
// 创建一个可写流
var writeSteam = fs.createWriteStream('./data/aaa.jpg');

// 管道读写操作
// 读取aaa.jpg文件,并写入到data中
readStream.pipe(writeSteam);

还可以重命名

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

// 创建一个可读流
var readStream = fs.createReadStream('./app.js');
// 创建一个可写流
var writeSteam = fs.createWriteStream('./data/test.js');

// 管道读写操作
// 读取aaa.js的文件内容,并将内容写入到data/test.js文件中
readStream.pipe(writeSteam);

利用HTTP模块Url模块Path模块Fs模块创建一个静态 WEB服务器

tree /f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
demo
│ app.js
│ package.json

└─static
│ 404.html
│ index.html
│ login.html

├─css
│ index.css

├─images
│ blog-head.jpg
│ favicon.ico

├─js
│ index.js

└─json
data.json

http模块

1
2
3
4
5
6
7
const http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

根据访问路径,返回对应的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const http = require('http');
const fs = require('fs');

http.createServer(function (req, res) {
// 1.获取地址
var pathname = req.url;
pathname = pathname == '/' ? '/index.html' : pathname;
// 2. 通过fs模块读取文件
if(pathname != '/favicon.ico') {
fs.readFile('./static' + pathname, (err, data) =>{
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404');
}

res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
}
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

存在问题,css文件访问格式不对

自定义一个模块,根据不同的文件类型(文件后缀名),返回不同的Content-Type

module/common.js

1
2
3
4
5
6
7
8
9
10
11
12
13
exports.getMime = function(extname) {
switch(extname) {
case '.html':
return 'text/html';
case '.css':
return 'text/css';
case '.js':
return 'text/javascript';
default:
return 'text/html';
}

}

css文件访问格式不对:

使用path.extname()获取后缀名

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 http = require('http');
const fs = require('fs');
const path = require('path');
const common = require('./module/common.js');

http.createServer(function (req, res) {
// 1.获取地址
var pathname = req.url;
pathname = pathname == '/' ? '/index.html' : pathname;
// path.extname()获取后缀名
let extname = path.extname(pathname);
// 2. 通过fs模块读取文件
if(pathname != '/favicon.ico') {
fs.readFile('./static' + pathname, (err, data) =>{
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404');
}
let mime = common.getMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
})
}
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

解决获取json文件时有get参数时,页面显示404

使用url模块的url.parse()去掉get传值后的参数

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
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const common = require('./module/common.js');

http.createServer(function (req, res) {
// 1.获取地址
// var pathname = req.url;
var pathname = url.parse(req.url).pathname; // 去掉get传值后的参数
pathname = pathname == '/' ? '/index.html' : pathname;
// path.extname()获取后缀名
let extname = path.extname(pathname);
// 2. 通过fs模块读取文件
if(pathname != '/favicon.ico') {
fs.readFile('./static' + pathname, (err, data) =>{
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404,这个页面不存在');
}
let mime = common.getMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
})
}
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

module/common.js

使用mime.json动态获取content-type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');
exports.getFileMime = function(extname) {
return new Promise((resolve, reject) => {
fs.readFile('./static/data/mime.json', (err, data) => {
if(err) {
console.log(err);
reject(err);
}
// console.log(data); // <Buffer类型
// console.log(data.toString());
// console.log(JSON.parse(data.toString()));

var mimeObj = JSON.parse(data.toString());
console.log(mimeObj[extname])
resolve(mimeObj[extname]);
})
})
}

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
....
if(pathname != '/favicon.ico') {
fs.readFile('./static' + pathname, async (err, data) =>{
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404,这个页面不存在');
}
// let mime = common.getMime(extname);
let mime =await common.getFileMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
})
}

.....

在网页中的network中,可以看到不同类型的文件请求的content-type不一样了

module/common.js

使用同步的方法

1
2
3
4
5
6
7
exports.getFileMime = function(extname) {
var data = fs.readFileSync('./static/data/mime.json'); // 同步方法

var mimeObj = JSON.parse(data.toString());
// console.log(mimeObj[extname]);
return mimeObj[extname];
}

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
....
if(pathname != '/favicon.ico') {
fs.readFile('./static' + pathname, (err, data) =>{
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404,这个页面不存在');
}
let mime = common.getFileMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
})
}

.....

NodeJs 封装静态WEB服务、路由、EJS模板引擎、GET、POST

module/routes.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
26
27
28
29
30
31
32
33
const fs = require('fs');
const path = require('path');
const url = require('url');

let getFileMime = function(extname) {
var data = fs.readFileSync('./static/data/mime.json'); // 同步方法

var mimeObj = JSON.parse(data.toString());
// console.log(mimeObj[extname]);
return mimeObj[extname];
}

// 封装-创建静态web服务
exports.static = function(req, res, staticPath) {
// 1.获取地址
var pathname = url.parse(req.url).pathname; // 去掉get传值后的参数
pathname = pathname == '/' ? '/index.html' : pathname;
// path.extname()获取后缀名
let extname = path.extname(pathname);
// 2. 通过fs模块读取文件
if(pathname != '/favicon.ico') {
fs.readFile('./' + staticPath + pathname,(err, data) =>{
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404,这个页面不存在');
}
let mime = getFileMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
})
}
}

app.js

1
2
3
4
5
6
7
8
9
const http = require('http');
const routes = require('./module/routes.js');

http.createServer(function (req, res) {
// 创建静态web服务
routes.static(req, res, 'static');
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');
路由

路由(Routing〉是由一个URL(或者叫路径)和一个特定的 HTTP方法(GET、POST等)组成的,涉及到应用如何响应客户端对基个网站节点的访问。

通俗的说:
路由指的就是针对不同请求的URL,处理不同的业务逻辑。

访问路径,加上路由的处理:

【匹配不到对应文件(index.html、login.html等),继续往下匹配路由,再处理404的问题】

因为fs.readFile是异步的,所以要改为同步的, 不然routes.static(req, res, 'static');会匹配不到,就到匹配404页面了

module/routes.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
26
27
28
29
30
31
32
33
34
const fs = require('fs');
const path = require('path');
const url = require('url');

let getFileMime = function(extname) {
var data = fs.readFileSync('./static/data/mime.json'); // 同步方法

var mimeObj = JSON.parse(data.toString());
// console.log(mimeObj[extname]);
return mimeObj[extname];
}

// 封装-创建静态web服务
exports.static = function(req, res, staticPath) {
// 1.获取地址
var pathname = url.parse(req.url).pathname; // 去掉get传值后的参数
pathname = pathname == '/' ? '/index.html' : pathname;
// path.extname()获取后缀名
let extname = path.extname(pathname);
// 2. 通过fs模块读取文件
// 同步的方法
if(pathname != '/favicon.ico') {
try {
let data = fs.readFileSync('./' + staticPath + pathname);
if(data) {
let mime = getFileMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
}
} catch (error) {

}
}
}

app.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const http = require('http');
const fs = require('fs');
const routes = require('./module/routes.js');
const url = require('url');

http.createServer(function (req, res) {
// 创建静态web服务
routes.static(req, res, 'static'); // static里面的fs.readFile要改为fs.readFileSync
// 路由
var pathname = url.parse(req.url).pathname;

if(pathname == '/login') {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('执行登录');
} else if(pathname == '/register') {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('执行注册');
} else if(pathname == '/admin') {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('处理后的业务逻辑');
} else {
fs.readFile('./static' + pathname, (err, data) => {
if(err) {
// console.log(err);
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404, 页面不存在');
}
})
}

// 老师的代码是直接使用下面这段,但是这样,我页面访问index.html会获取不到图片,样式和js文件,访问这些文件的路径会进去else里
// else {
// res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
// res.end('页面不存在');
// }

}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

初识EJS模块引擎

我们学的EJS是后台模板,可以把我们数据库和文件读取的数据显示到Html页面上面。它是一个第三方模块,需要通过npm安装

1
npm install ejs --save

app.js

1
2
3
4
5
6
7
8
9
10
const ejs = require('ejs');
....
if(pathname == '/login') {
let msg = '从数据库中获取到的数据';
ejs.renderFile('./views/login.ejs', {msg:msg},(err, data) => {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
}
....

./views/login.ejs

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ejs模块</title>
</head>
<body>
<h3><%= msg %></h3>
</body>
</html>

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
if(pathname == '/news') {
let list = [
{id:1001, title: '新闻1111'},
{id:1002, title: '新闻2222'},
{id:1003, title: '新闻3333'},
{id:1004, title: '新闻4444'},
{id:1005, title: '新闻5555'},
]
ejs.renderFile('./views/news.ejs', {newList:list}, {}, (err, data) => {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
}

./views/news.ejs

1
2
3
4
5
<ul>
<%for(var i =0; i< newList.length;i++){%>
<li><%=newList[i].title%></li>
<%}%>
</ul>

js代码写在 <% %>里面

GET、POST

超文本传输协议(HTTP)的设计目的是保证客户端机器与服务器之间的通信。
在客户端和服务器之间进行请求-响应时,两种最常被用到的方法是: GET 和 POST。

GET -从指定的资源请求数据。(一般用于获取数据)
POST -向指定的资源提交要被处瑾的数据。(一般用于提交数据〉

获取请求类型:

1
req.method

获取GET传值:

1
2
var urlinfo = url.parse(req.url, true);
urlinfo.query

案例:

1
2
3
4
5
6
// http://127.0.0.1:8081/news?page=2&id=1
var query = url.parse(req.url, true).query;
console.log(query);

// 控制台显示:
[Object: null prototype] { page: '2', id: '1' }

获取POST传值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const qs = require('qs');

var postData = '';
// 数据块接收中
req.on('data', function(postDataChunk){
postData += postDataChunk;
});
// 数据接收完毕,执行回调函数
req.on('end', function () {
try{
JSON.parse(postData)
} catch(e) {}

req.query = postData;
console.log(req.query);
console.log(qs.parse(postData)) // 将获取的参数转为对象
res.end(postData);
})

案例:

./views/register.ejs

1
2
3
4
5
<form action="/doRegister" method="post">
姓名:<input type="text" name="username">
年龄:<input type="password" name="password">
<input type="submit" value="提交">
</form>

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(pathname == '/register') {
// 注册页面
ejs.renderFile('./views/register.ejs',{}, (err, data) => {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
} else if(pathname == '/doRegister') {
// 注册操作之后
var postData = '';
// 数据块接收中
req.on('data', function(postDataChunk){
postData += postDataChunk;
});
// 数据接收完毕,执行回调函数
req.on('end',()=> {
res.end(postData);
})
}

Nodejs路由封装-封装一个类似express的路由

将处理路由的方法统一封装起来

module/routes.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
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
87
88
89
90
91
const fs = require('fs');
const path = require('path');
const url = require('url');
const ejs = require('ejs');
const qs = require('qs');

let getFileMime = function(extname) {
var data = fs.readFileSync('./static/data/mime.json'); // 同步方法

var mimeObj = JSON.parse(data.toString());
// console.log(mimeObj[extname]);
return mimeObj[extname];
}

let app = {
static: (req, res, staticPath) => {
var pathname = url.parse(req.url).pathname; // 去掉get传值后的参数
pathname = pathname == '/' ? '/index.html' : pathname;
// path.extname()获取后缀名
let extname = path.extname(pathname);

// 同步的方法
if(pathname != '/favicon.ico') {
try {
let data = fs.readFileSync('./' + staticPath + pathname);
if(data) {
let mime = getFileMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
}

} catch (error) {

}
}
},
login: (req, res) => {
let msg = '从数据库中获取到的数据';
ejs.renderFile('./views/login.ejs', {msg:msg}, {}, (err, data) => {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
},
news: (req, res) => {
let list = [
{id:1001, title: '新闻1111'},
{id:1002, title: '新闻2222'},
{id:1003, title: '新闻3333'},
{id:1004, title: '新闻4444'},
{id:1005, title: '新闻5555'},
];
console.log(req.method)
// http://127.0.0.1:8081/news?page=2&id=1
var query = url.parse(req.url, true).query;
console.log(query);

ejs.renderFile('./views/news.ejs', {newList:list}, {}, (err, data) => {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
},
register: (req, res) => {
ejs.renderFile('./views/register.ejs',{}, (err, data) => {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
})
},
doRegister: (req, res) => {
var postData = '';
// 数据块接收中
req.on('data', function(postDataChunk){
postData += postDataChunk;
});
// 数据接收完毕,执行回调函数
req.on('end',()=> {
try{
JSON.parse(postData)
} catch(e) {}

req.query = postData;
console.log(req.query);
console.log(qs.parse(postData))
res.end(postData);
})
},
error: (req, res) => {
res.end('error')
},
}

module.exports = app;

app.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
26
27
28
29
const http = require('http');
const fs = require('fs');
const url = require('url');
const app = require('./module/routes.js');

http.createServer(function (req, res) {
// 创建静态web服务
app.static(req, res, 'static'); // static里面的fs.readFile要改为fs.readFileSync
// 路由
var pathname = url.parse(req.url).pathname.replace('/',''); // replace('/','')将pathname中的/去掉
// http://127.0.0.1:8081/login pathname = login
// http://127.0.0.1:8081/news pathname = news
// http://127.0.0.1:8081/doRegister pathname = doRegister

try {
app[pathname](req, res);
} catch (error) {
fs.readFile('./static' + pathname, (err, data) => {
if(err) {
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404, 页面不存在');
} else {
app['error'](req, res);
}
})
}
}).listen(8081);

console.log('Server running at http://127.0.0.1:8081/');

封装app.get()配置路由

module/route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const url = require('url');

let G = {};

let app = function(req, res) {
let pathname = url.parse(req.url).pathname;

if(G[pathname]) {
G[pathname](req, res); // 执行方法
} else {
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('页面不存在');
}
}

app.get = function(str, cb) {
// 注册方法
G[str] = cb;
}

module.exports = app;

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const http = require('http');
const app = require('./module/route');

// 注册web服务
http.createServer(app).listen(8081);

// 配置路由
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('首页');
})

// 配置路由
app.get('/login', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('login');
})

// 配置路由
app.get('/news', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('news');
})

封装post以及通过req.body获取post的数据

module/route.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
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
const url = require('url');

function changeRes(res){
res.send = (data) =>{
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
}
}

let sever = () => {
let G = {};
// 将get和post的方法分开 避免接口名一致,后面的请求的会替换前面的方法
G._get = {};
G._post = {};

let app = function(req, res) {
// 扩展res的方法
changeRes(res);

let pathname = url.parse(req.url).pathname;
// 获取请求类型
let method = req.method.toLowerCase();
if(G['_' + method][pathname]) {
if(method == 'get') {
G._get[pathname](req, res); // 执行方法
} else{
// post 获取post的数据 把它绑定到req.body
var postData = '';
req.on('data', function(chunk){
postData += chunk;
});
// 数据接收完毕,执行回调函数
req.on('end',()=> {
req.body = postData;
G._post[pathname](req, res); // 执行方法
})
}
} else {
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('页面不存在');
}
}

app.get = function(str, cb) {
// 注册方法
G._get[str] = cb;
}
app.post = function(str, cb) {
// 注册方法
G._post[str] = cb;
}

return app;
}

module.exports = sever(); // 注意:要调用它

app.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
26
27
28
29
30
31
32
33
34
35
const http = require('http');
const app = require('./module/route');
const ejs = require('ejs');

// 注册web服务
http.createServer(app).listen(8081);

// 配置路由
app.get('/', function(req, res) {
res.send('首页');
})

// 配置路由
app.get('/login', function(req, res) {
ejs.renderFile('./views/login.ejs',{}, (err, data) => {
res.send(data);
})
})

// 配置路由
app.get('/news', function(req, res) {
res.send('news');
})

// login.ejs中的action="/doLogin"
// 使用这种写法(推荐)
app.post('/doLogin', function(req, res) {
res.send(req.body)
})

// login.ejs中的action="/login"
// 虽然URL中的路径是/login跟get一样的,但渲染页面的的内容不一样。(不推荐)
app.post('/login', function(req, res) {
res.send(req.body)
})

封装静态web服务

访问静态文件,如css,js等文件

在ejs文件中引入css文件,报错

http://127.0.0.1:8081/static/css/style.css net::ERR_ABORTED 404 (Not Found)

module/route.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
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const fs = require('fs');
const path = require('path');
const url = require('url');

function changeRes(res){
res.send = (data) =>{
res.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
res.end(data);
}
}
// 根据后缀名获取文件类型
function getFileMime(extname) {
var data = fs.readFileSync('./data/mime.json'); // 同步方法

var mimeObj = JSON.parse(data.toString());
// console.log(mimeObj[extname]);
return mimeObj[extname];
}

// 静态web服务的方法
function initStatic(req, res, staticPath) {
// 1.获取地址
var pathname = url.parse(req.url).pathname; // 去掉get传值后的参数
pathname = pathname == '/' ? '/index.html' : pathname;
// path.extname()获取后缀名
let extname = path.extname(pathname);
// 2. 通过fs模块读取文件
// 同步的方法
try {
let data = fs.readFileSync('./' + staticPath + pathname);
if(data) {
let mime = getFileMime(extname);
res.writeHead(200, {'Content-Type': '' + mime + ';charset="utf-8"'});
res.end(data);
}
} catch (error) {

}
}

let sever = () => {
let G = {
_get: {},
_post: {},
staticPath: 'static' // 设置默认静态web目录
};
// 将get和post的方法分开 避免接口名一致,后面的请求的会替换前面的方法

let app = function(req, res) {
// 扩展res的方法
changeRes(res);
// 配置静态web服务
initStatic(req, res, G.staticPath);

let pathname = url.parse(req.url).pathname;
// 获取请求类型
let method = req.method.toLowerCase();
if(G['_' + method][pathname]) {
if(method == 'get') {
G._get[pathname](req, res); // 执行方法
} else{
// post 获取post的数据 把它绑定到req.body
var postData = '';
req.on('data', function(chunk){
postData += chunk;
});
// 数据接收完毕,执行回调函数
req.on('end',()=> {
req.body = postData;
G._post[pathname](req, res); // 执行方法
})
}
} else {
fs.readFile(pathname, (err, data) => {
if(err) {
res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
res.end('404, 页面不存在');
}
})
// 只写这个会报错
// res.writeHead(404, {'Content-Type': 'text/html;charset="utf-8"'});
// res.end('页面不存在');
}
}
// get 请求
app.get = function(str, cb) {
// 注册方法
G._get[str] = cb;
}
// post 请求
app.post = function(str, cb) {
// 注册方法
G._post[str] = cb;
}
// 配置静态Web服务目录
app.static = function(staticPath) {
G.staticPath = staticPath;
}

return app;
}

module.exports = sever(); // 注意:要调用它

app.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
26
27
28
29
30
31
32
33
34
35
36
37
const http = require('http');
const app = require('./module/route');
const ejs = require('ejs');

// 注册web服务
http.createServer(app).listen(8081);

// app.static('public'); // 修改默认静态web目录

// 配置路由
app.get('/', function(req, res) {
res.send('首页');
})

// 配置路由
app.get('/login', function(req, res) {
ejs.renderFile('./views/login.ejs',{}, (err, data) => {
res.send(data);
})
})

// 配置路由
app.get('/news', function(req, res) {
res.send('news');
})

// login.ejs中的action="/doLogin"
// 使用这种写法(推荐)
app.post('/doLogin', function(req, res) {
res.send(req.body)
})

// login.ejs中的action="/login"
// 虽然URL中的路径是/login跟get一样的,但渲染页面的的内容不一样。(不推荐)
app.post('/login', function(req, res) {
res.send(req.body)
})

views/login.ejs

1
<link rel="stylesheet" href="./css/style.css">    // 配置了静态web服务,才能访问

MongoDB数据库介绍、安装、使用

MongoDB介绍

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的NoSql数据库。他支持的数据结构非常松散,是类似json的 bson格式,因此可以存储比较复杂的数据类型。Mongodb最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。它的特点是高性能、易部署、易使用,存储数据非常方便

MongoDB安装

注意:配置db目录和log目录

配置环境变量:将安装有MongoDB的bin目录,添加到系统的环境变量中

测试是否成功:在控制台输入mongo

连接数据库:

1
mongo

查看数据库:

1
show dbs

MongoDB数据库创建、删除、表(集合)、创建删除、数据的增、删、改、查

test(数据库)

  • user
  • admin
  • article

表(集合):user、admin、article

1、使用数据库、创建数据库
1
user test

如果真的想把这个数据库创建成功,那么必须插入一个数据

数据库中不能直接插入数据,只能往集合(collections)中插入数据。下面命令表示给 test数据库的user表中插入数据。

1
db.user.insert({"name":"zhangsan","age":20});
2、查看数据库
1
show dbs
3、显示当前的数据集合(mysql中叫表)
1
show collections
4、删除集合,删除指定的集合 删除表
1
2
3
删除集合  db.COLLECTION_NAME.DROP()

db.user.drop();
5、删除数据库,删除当前所在的数据库
1
db.dropDatabase();
三、插入(增加)数据

插入数据,随着数据的插入,数据库创建成功了,集合也创建成功了

1
db.表名.insert({"name":"zhangsan","age":20});
四、查找数据

1、查找所有记录

1
db.user.find();

相当于:select * from user;

1
2
3
4
5
6
> db.user.find();
{ "_id" : ObjectId("6409aade3631637f8390508f"), "name" : "zhangsan", "age" : 20 }
{ "_id" : ObjectId("6409ab7e3631637f83905090"), "name" : "zhangsan", "age" : 30 }
{ "_id" : ObjectId("6409ab883631637f83905091"), "name" : "zhangsan", "age" : 40 }
{ "_id" : ObjectId("6409ab903631637f83905092"), "name" : "zhangsan", "age" : 20 }
{ "_id" : ObjectId("6409ac5105d810a057af99e4"), "name" : "zhangsan", "age" : 22 }

2、查询去掉后的当前聚集集合中的某列的重复数据

1
db.user.distinct("name");

会过滤掉name中的相同数据

相当于:select distinct name from user;

1
2
> db.user.distinct("name");
[ "zhangsan" ]

3、查询age=22的记录

1
db.user.find({"age":22})

相当于:select * from user where age = 22;

1
2
> db.user.find({"age":22})
{ "_id" : ObjectId("6409ac9005d810a057af99e6"), "name" : "zhangsan", "age" : 22 }

4、查询age>22的记录

1
db.user.find({age:{$gt:22}});

相当于:select * from user where age >22;

5、查询age<22的记录

1
db.user.find({age:{$lt:22}});

相当于:select * from user where age <22;

6、查询age>=25

1
db.user.find({age:{$gte:25}});

相当于:select * from user where age >=25;

7、查询age<=25的记录

1
db.user.find({age:{$lte:25}});

8、查询age>=23并且age <=26的记录 (注意书写格式)

1
db.user.find({age:{$gte:23,$lte:26}});

9、查询name中包含mongo的数据 (模糊查询用于搜索)z

1
db.user.find({name:/mongo/});

//相当于%%

select * from user where name like '%mongo%';

10、查询name中以mongo开头的, ^符号

1
db.user.find({name:/^mongo/});

select * from user where name like 'mongo%';

查询name中以mongo结束的, $符号

1
db.user.find({name:/mongo$/});

11、查询指定列name、age数据

1
db.user.find({},{name:1, age:1});
1
db.user.find({"age":{$gt:20}},{age:1});
1
db.user.find({"age":{$gt:20}},{name:1});
1
db.user.find({"age":{$gt:20}},{name:1, age:1});

相当于:select name, age from user;

当然name也可以用truefalse,当用true的情况下和 name:1效果一样,如果用false就是排除name,显示name以外的列信息。

12、查询指定列name、age数据,age>25

1
db.user.find({"age":{$gt:25}},{name:1, age:1});

相当于:select name, age from user where age >25;

13、按照年龄排序 1 升序 -1 降序

升序:

1
db.user.find().sort({age:1});

降序:

1
db.user.find().sort({age:-1});

14、查询name = zhangsan, age = 22的数据

查询数据库的条件,可以写多个

1
db.user.find({name:'zhangsan', age:22});

15、查询前5条数据

1
db.user.find().limit(5);

相当于:select top 5 * from user;

新增100条数据:(类似js代码)

1
2
3
4
> for(var i = 1; i<100; i++) {
...
... db.admin.insert({"name":"zhang" + i, "age": i})
... };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> db.admin.find()
{ "_id" : ObjectId("640aa65d3ac7e1046e8f888c"), "name" : "zhang1", "age" : 1 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f888d"), "name" : "zhang2", "age" : 2 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f888e"), "name" : "zhang3", "age" : 3 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f888f"), "name" : "zhang4", "age" : 4 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8890"), "name" : "zhang5", "age" : 5 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8891"), "name" : "zhang6", "age" : 6 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8892"), "name" : "zhang7", "age" : 7 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8893"), "name" : "zhang8", "age" : 8 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8894"), "name" : "zhang9", "age" : 9 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8895"), "name" : "zhang10", "age" : 10 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8896"), "name" : "zhang11", "age" : 11 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8897"), "name" : "zhang12", "age" : 12 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8898"), "name" : "zhang13", "age" : 13 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8899"), "name" : "zhang14", "age" : 14 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f889a"), "name" : "zhang15", "age" : 15 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f889b"), "name" : "zhang16", "age" : 16 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f889c"), "name" : "zhang17", "age" : 17 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f889d"), "name" : "zhang18", "age" : 18 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f889e"), "name" : "zhang19", "age" : 19 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f889f"), "name" : "zhang20", "age" : 20 }
Type "it" for more

16、查询10条以后的数据

1
db.admin.find().skip(10);

17、查询在5~10之间的数据

1
2
3
4
5
6
> db.admin.find().limit(4).skip(5);
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8891"), "name" : "zhang6", "age" : 6 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8892"), "name" : "zhang7", "age" : 7 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8893"), "name" : "zhang8", "age" : 8 }
{ "_id" : ObjectId("640aa65d3ac7e1046e8f8894"), "name" : "zhang9", "age" : 9 }
>
1
db.admin.find().limit(10).skip(5);

可用于分页,limit是pageSize,skip是第几页减1*pageSize

pageSize:每页显示的条数

1
2
3
db.admin.find().limit(10).skip(0);    		// 第一页   (n-1)*10
db.admin.find().limit(10).skip(10); // 第二页
db.admin.find().limit(10).skip(20); // 第三页

18、or与查询

1
db.user.find({$or:[{age:22},{age:25}]});

相当于:select * from user where age = 22 or age =25;

19、findOne查询第一条数据

1
db.user.findOne();

相当于:select top 1* from user;

1
db.user.find().limit(1);

20、查询某个结果集的记录条数 统计数量

1
db.user.find().count();
1
db.user.find({age:{$gte:25}}).count();

相当于:select count(*) from user where age >=20;

如果要返回限制之后的记录数量,要使用count(true)或者count(非0)

1
db.user.find().skip(10).limit(5).count(true);

四、修改数据

1
db.student.insert({"name":"liming", "age":20, sex:"男", "score":{"shuxue":70, "yuwen":80, "yingyu":86}});
1
2
3
4
5
6
7
> db.student.find()                                                                      
{ "_id" : ObjectId("640acb7319e76fb47ee3df38"), "name" : "小明", "age" : 33, "score" : { "shuxue" : 70, "yuwen" : 80, "yingyu" : 86 }, "sex" : "男" }
{ "_id" : ObjectId("640acebb19e76fb47ee3df39"), "name" : "xiaoming", "age" : 20, "sex" : "男", "score" : { "shuxue" : 70, "yuwen" : 80, "yingyu" : 86 } }
{ "_id" : ObjectId("640acedf19e76fb47ee3df3a"), "name" : "Lisi", "age" : 20, "sex" : "女", "score" : { "shuxue" : 70, "yuwen" : 80, "yingyu" : 86 } }
{ "_id" : ObjectId("640aceea19e76fb47ee3df3b"), "name" : "lisa", "age" : 20, "sex" : "女", "score" : { "shuxue" : 70, "yuwen" : 80, "yingyu" : 86 } }
{ "_id" : ObjectId("640aceff19e76fb47ee3df3c"), "name" : "liming", "age" : 20, "sex" : "男", "score" : { "shuxue" : 70, "yuwen" : 80, "yingyu" : 86 } }
>

注意:要加上$set,如果不加上,会直接替换整条数据

修改里面还有查询条件。你要改谁,要告诉mongo。

查找名字叫做小明的,把年龄更改为16岁:

1
2
3
db.student.update({"name":"小明"}, {$set:{age:16}})

db.student.update({"name":"小明"}, {$set:{"sex":"男"}})

查找数学成绩是70,把年龄更改为33岁:

1
db.student.update({"score.shuxue":70}, {$set:{age:33}})

更改所有匹配项目:{multi: true}
By default, the update() method updates a single document. To update multiple documents, use the multi option in the update() method.

1
db.student.update({"sex":"男"},{$set:{"age":33}}, {multi: true});

完整替换,不出现$set关键字了注意

1
2
3
4
5
6
db.student.update({"name":"小明"}, {"name":"大明","age":16}); 

// 原数据:
{ "_id" : ObjectId("640acb7319e76fb47ee3df38"), "name" : "小明", "age" : 33, "score" : { "shuxue" : 70, "yuwen" : 80, "yingyu" : 86 }, "sex" : "男" }
// 直接替换为:
{ "_id" : ObjectId("640acb7319e76fb47ee3df38"), "name" : "大明", "age" : 16 }

db.users.update({name: "Lisi"},{$inc: {age: 50]}, false, true);

相当于: update users set age = age + 50 where name = 'Lisi';

db.users.update({name: 'Lisi'}, {$inc: {age: 50}, $set: {name: 'hoho'}}, false, true);

相当于:update users set age = age + 50, name = 'hoho' where name = 'Lisi';

五、删除数据

1
2
db.collectionsNames.remove({"borough":"Manhattan"});
db.users.remove({age: 132});

By default, the remove() method removes all documents that match the remove condition. Use the justOne option to limit the remove operation to only one of fhe matching documents.

{justOne: true}

1
db.restaurants.remove({"borough": "Queens"}, {justOne: true})
1
db.student.remove({"sex":"男"},{justOne:true})

删除数据,如果不加条件,是全部都删除

1
db.admin.remove({});      // 全部删除

MongoDb大数据查询优化、MongoDB索引、复合索引、唯一索引、explain分析查询速度

一、索引基础

​ 索引是对数据库表中一列或多列的值进行排序的一种结构,可以让我们查询数据库变得更快。MongoDB的索引几乎与传统的关系型数据库一模一样,这其中也包括一些基本的查询优化技巧。

创建索引:

1
db.user.ensureIndex({"username":1});

获取当前集合的索引:

1
db.user.getIndexes();

删除索引的命令:

1
db.user.dropIndex({"username":1})

在MongoDB中,我们同样可以创建复合索引,如:

数字1表示username键的索引按升序存储,-1表示age键的索引按照降序方式存储

1
2
3
db.user.ensureIndex({"username":1,"age": -1});

db.user.dropIndex({"username":1,"age": -1}); // 这样删除

​ 该索引被创建后,基于username和age 的查询将会用到该索引,或者是基于username的查询也会用到该索引,但是只是基于age的查询将不会用到该复合索引。因此可以说,如果想用到复合索引,必须在查询条件中包含复合索引中的前N个索引列。然而如果查询条件中的键值顺序和复合索引中的创建顺序不一致的话,MongoDB可以智能的帮助我们调整该顺序,以便使复合索引可以为查询所用。如:

1
2
3
db.user.find({"age":30, "username": "stephen"})

db.user.find({"age":10, "name": "zhang10"})

​ 对于上面示例中的查询条件,MongoDB在检索之前将会动态的调整查询条件文档的顺序,以使该查询可以用到刚刚创建的复合索引。
​ 对于上面创建的索引,MongoDB都会根据索引的 keyname和索引方向为新创建的索引自动分配一个索引名,下面的命令可以在创建索引时为其指定索引名,如:

1
db.user.ensureIndex({"usename": 1}, {"name": "useindex"})

随着集合的增长,需要针对查询中大量的排序做索引。如果没有对索引的键调用sort,MongoDB需要将所有数据提取到内存并排序。因此在做无索引排序时,如果数据量过大以致无法在内存中进行排序,此时MongoDB将会报错。

二、唯一索引

在缺省情况下创建的索引均不是唯一索引。下面的示例将创建唯一索引,如:

1
db.user.ensureIndex({"userid":1}, {"unique": true});

如果再次插入userid重复的文档时,MongoDB将报错,以提示插入重复键,如:

1
2
db.user.insert({"userid": 5});
db.user.insert({"userid": 5});

E11000 duplicate key error index: user.user.$userid_1 dup key: {: 5.0}

如果插入的文档中不包含userid键,那么该文档中该键的值为null,如果多次插入类似的文档,MongoDB将会报出同样的错误,如:

1
2
db.user.insert({"age": 5});
db.user.insert({"age": 5});

E11000 duplicate key error index: user.user.$userid_1 dup key: {:null }

如果在创建唯一索引时已经存在了重复项,我们可以通过下面的命令帮助我们在创建唯一索引时消除重复文档,仅保留发现的第一个文档,如:
先删除刚刚创建的唯一索引。

五、explain executionStats查询具体的执行时间
1
db.tablename.find().explain("executionStats")
1
db.user.find().explain("executionStats")

关注输出的如下数值:explain.executionStats.executionTimeMillis

例子:

查询6万多条数据,不加索引,查询时间500多毫秒;加上索引,10毫秒左右,再执行一次,0毫秒

连接远程的数据库

1
mongo 192.168.0.8:27017

Mongodb4.x的使用以及 Mongodb账户权限配置

查看mongo版本

1
mongo -version

Mongodb账户权限配置

1、第一步创建超级管理用户

1
2
3
4
5
6
use admin
db.createUser({
user: 'admin',
pwd: '123456',
roles:[{role:'root', db:'admin'}]
})

2、第二步修改Mongodb数据库配置文件

查看mongodb安装路径的方法:1、win环境下,使用【win+r】快捷键打开运行窗口,输入“services.msc”指令打开服务列表,右键MongoDB服务选择“属性”查看mongodb安装路径;2、linux环境下,打开linux终端,输入“find / -name mongodb”、“locate mongodb”、“whereis mongodb”、“which mongodb”其中一个命令查看mongodb安装路径即可。

1
2
3
4
5
路径: G:\Install\WEB\mongodb\bin\mongod.cfg

配置:
security:
authorization: enabled

注:修改配置文件前,最好先备份

3、第三步重启mongodb服务

  1. win+R ——> services.msc
  2. 找到MongoDB Server, 右键重启

在终端输入mongo连接之后,输入show dbs,不显示内容

4、第四步用超级管理员账户连接数据库

1
2
3
mongo admin -u 用户名 -p 密码

mongo 192.168.1.200:27017/test -u user -p password // test:数据库名
1
mongo admin -u admin -p 123456      // 连接之后,再输入show dbs,会显示所有数据库

5、第五步给test数据库创建一个用户, 只能访问test不能访问其他数据库

1
2
3
4
5
6
7
use test

db.createUser({
user: 'testadmin',
pwd: '123456',
roles:[{role:'dbOwner', db:'test'}]
})
1
2
3
4
5
use test

show users

db.dropUser('testadmin') // 删除用户

使用testadmin账号

1
mongo test -u testadmin -p 123456
1
2
> show dbs
test 0.000GB // 只能看到test数据库

Mongodb账户权限配置中常用的命令

  1. 查看当前库下的用户

    1
    show users
  2. 删除用户

    1
    db.dropUser('testadmin')
  3. 修改用户密码

    1
    db.updateUser('admin', {pwd: 'password'})
  4. 密码认证

    1
    db.auth('admin', 'password')

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    mongo test         // test:数据库名称
    > show dbs
    // 终端不显示内容

    > db.auth("testadmin", "123456") // test数据库的用户
    1
    > show dbs
    test 0.000GB

Mongodb数据库角色

  1. 数据库用户角色:read、readWrite;
  2. 数据库管理角色:dbAdmin、dbOwner、userAdmin;
  3. 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager;
  4. 备份恢复角色:backup、 restore;
  5. 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase
  6. 超级用户角色:root

参考:MongoDB 内置角色

连接数据库的时候需要配置账户密码

1
const url = 'mongodb://admin:123456@localhost:27017/';

基本操作

查看mongodb的数据库存放位置/日志存放位置:

bin目录中的配置文件:G:\Install\WEB\mongodb\bin\mongod.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Where and how to store data.
storage:
dbPath: G:\Install\WEB\mongodb\data
journal:
enabled: true
# engine:
# mmapv1:
# wiredTiger:

# where to write logging data.
systemLog:
destination: file
logAppend: true
path: G:\Install\WEB\mongodb\log\mongod.log
查看/修改mongodb的端口:

bin目录中的配置文件:G:\Install\WEB\mongodb\bin\mongod.cfg

1
2
3
4
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1

关系型数据库表(集合)与表(集合)之间的几种关系

一对一:一个身份证对应一个驾驶证

一对多:一个商品分类对应多个商品,一个班级对应多名学生

多对多:例如:一个学生可以选多门课程,而同一门课程可以被多个学生选修,彼此的对应关系即是多对多关系

一个用户可以关注多个商品,一个商品可以被多个用户关注

MongoDB 的高级查询aggregate聚合管道

一、MongoDB聚合管道(Aggregate Pipeline)

使用聚合管道可以对集合中的文档进行变换和组合。

实际项目中:表关联查询、数据的统计。

MongoDB中使用db.COLLECTION_NAME.aggregate([{<stage>},...])方法来构建和使用聚合管道。先看下官网给的实例,感受一下聚合管道的用法。

二、MongoDB Aggregation 管道操作符与表达式
管道操作符 Description
$project 增加、删除、重命名字段
$match 条件匹配、只满足条件的文档才能进入下一阶段
$limit 限制结果的数量
$skip 跳过文档的数量
$sort 条件排序
$group 条件组合结果 统计
$lookup $lookup 操作符 用以引入其它集合的数据 (表关联查询)
SQL和NOSQL对比
WHERE $match
GROUP BY $group
HAVING $match
SELECT $project
ORDER BY $sort
LIMIT $limit
SUM() $sum
COUNT() $sum
join $lookup
三、数据模拟
1
2
3
4
5
6
7
8
9
10
11
12
db.order.insert({"order_id": "1", "uid": 10, "trade_no":"111", "all_price":100, "all_num": 2})
db.order.insert({"order_id": "2", "uid": 7, "trade_no":"222", "all_price":90, "all_num": 2})
db.order.insert({"order_id": "3", "uid": 9, "trade_no":"333", "all_price":20, "all_num": 6})

db.order_item.insert({"order_id": "1", "title": "商品鼠标1", "price":50, "num":1})
db.order_item.insert({"order_id": "1", "title": "商品键盘2", "price":50, "num":1})

db.order_item.insert({"order_id": "2", "title": "牛奶", "price":50, "num":1})
db.order_item.insert({"order_id": "2", "title": "酸奶", "price":40, "num":1})

db.order_item.insert({"order_id": "3", "title": "矿泉水", "price":2, "num":5})
db.order_item.insert({"order_id": "3", "title": "毛巾", "price":10, "num":1})
四、$project

修改文档的结构,可以用来重命名、增加或删除文档中的字段。

要求查找order 只返回文档中trade_no和all_price字段

1
2
3
4
5
db.order.aggregate([
{
$project:{trade_no: 1, all_price:1}
}
])

结果:

1
2
3
{ "_id" : ObjectId("64116235c9443120dbcb913e"), "trade_no" : "111", "all_price" : 100 }
{ "_id" : ObjectId("6411623fc9443120dbcb913f"), "trade_no" : "333", "all_price" : 20 }
{ "_id" : ObjectId("641162b7c9443120dbcb9146"), "trade_no" : "222", "all_price" : 90 }
五、$match

作用

用于过滤文档。用法类似于 find()方法中的参数。

1
2
3
4
5
6
7
8
db.order.aggregate([
{
$project:{trade_no: 1, all_price:1}
},
{
$match:{"all_price": {$gte:90}}
}
])

结果:

1
2
{ "_id" : ObjectId("64116235c9443120dbcb913e"), "trade_no" : "111", "all_price" : 100 }
{ "_id" : ObjectId("641162b7c9443120dbcb9146"), "trade_no" : "222", "all_price" : 90 }
六、$group

将集合中的文档进行分组,可用于统计结果。
统计每个订单的订单数量,按照订单号分组。

1
2
3
4
5
db.order_item.aggregate([
{
$group:{_id: "$order_id", total:{$sum :1}}
}
])

结果:

1
2
3
{ "_id" : "2", "total" : 2 }
{ "_id" : "1", "total" : 2 }
{ "_id" : "3", "total" : 2 }
1
2
3
4
5
db.order_item.aggregate([
{
$group:{_id: "$order_id", total:{$sum: "$num"}}
}
])

结果:

1
2
3
{ "_id" : "2", "total" : 2 }
{ "_id" : "1", "total" : 2 }
{ "_id" : "3", "total" : 6 }
1
2
3
4
5
db.order_item.aggregate([
{
$group:{_id: "$order_id", total:{$sum: "$price"}}
}
])

结果:

1
2
3
{ "_id" : "3", "total" : 12 }
{ "_id" : "1", "total" : 100 }
{ "_id" : "2", "total" : 90 }
七、$sort

将集合中的文档进行排序

1
2
3
4
5
6
7
8
9
10
11
db.order.aggregate([
{
$project:{trade_no: 1, all_price:1}
},
{
$match:{"all_price": {$gte:90}}
},
{
$sort: {"all_price": -1}
}
])

结果:

1
2
{ "_id" : ObjectId("64116235c9443120dbcb913e"), "trade_no" : "111", "all_price" : 100 }
{ "_id" : ObjectId("641162b7c9443120dbcb9146"), "trade_no" : "222", "all_price" : 90 }

八、$limit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.order.aggregate([
{
$project:{trade_no: 1, all_price:1}
},
{
$match:{"all_price": {$gte:90}}
},
{
$sort: {"all_price": -1}
},
{
$limit: 1
}
])

结果:

1
{ "_id" : ObjectId("64116235c9443120dbcb913e"), "trade_no" : "111", "all_price" : 100 }
九、$skip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.order.aggregate([
{
$project:{trade_no: 1, all_price:1}
},
{
$match:{"all_price": {$gte:90}}
},
{
$sort: {"all_price": -1}
},
{
$skip: 1
}
])

结果:

1
{ "_id" : ObjectId("641162b7c9443120dbcb9146"), "trade_no" : "222", "all_price" : 90 }
十、$lookup表关联

localField: "order_id"(order表哪个字段)与foreignField: "order_id"(order_item表的哪个字段关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.order.aggregate([
{
$lookup:
{
from:"order_item",
localField: "order_id",
foreignField: "order_id",
as: "items"
}
}
]);

from:"order_item", // 关联的表
localField: "order_id", // 原表(order表)
foreignField: "order_id", // 关联表(order_item表)

格式化之后的结果:

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
{
"_id": ObjectId("64116235c9443120dbcb913e"),
"order_id": "1",
"uid": 10,
"trade_no": "111",
"all_price": 100,
"all_num": 2,
"items": [{
"_id": ObjectId("64116245c9443120dbcb9140"),
"order_id": "1",
"title": "商品鼠标1",
"price": 50,
"num": 1
}, {
"_id": ObjectId("6411624bc9443120dbcb9141"),
"order_id": "1",
"title": "商品键盘2",
"price": 50,
"num": 1
}]
} {
"_id": ObjectId("6411623fc9443120dbcb913f"),
"order_id": "3",
"uid": 9,
"trade_no": "333",
"all_price": 20,
"all_num": 6,
"items": [{
"_id": ObjectId("6411625dc9443120dbcb9144"),
"order_id": "3",
"title": "矿泉水",
"price": 2,
"num": 5
}, {
"_id": ObjectId("64116262c9443120dbcb9145"),
"order_id": "3",
"title": "毛巾",
"price": 10,
"num": 1
}]
} {
"_id": ObjectId("641162b7c9443120dbcb9146"),
"order_id": "2",
"uid": 7,
"trade_no": "222",
"all_price": 90,
"all_num": 2,
"items": [{
"_id": ObjectId("64116251c9443120dbcb9142"),
"order_id": "2",
"title": "牛奶",
"price": 50,
"num": 1
}, {
"_id": ObjectId("64116256c9443120dbcb9143"),
"order_id": "2",
"title": "酸奶",
"price": 40,
"num": 1
}]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
db.order.aggregate([
{
$lookup:
{
from:"order_item",
localField: "order_id",
foreignField: "order_id",
as: "items"
}
},
{
$project:{trade_no: 1, all_price:1, items :1}
},
{
$match:{"all_price": {$gte:90}}
},
{
$sort:{"all_price": -1}
}
]);

格式化之后的结果:

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
{
"_id": ObjectId("64116235c9443120dbcb913e"),
"trade_no": "111",
"all_price": 100,
"items": [{
"_id": ObjectId("64116245c9443120dbcb9140"),
"order_id": "1",
"title": "商品鼠标1",
"price": 50,
"num": 1
}, {
"_id": ObjectId("6411624bc9443120dbcb9141"),
"order_id": "1",
"title": "商品键盘2",
"price": 50,
"num": 1
}]
} {
"_id": ObjectId("641162b7c9443120dbcb9146"),
"trade_no": "222",
"all_price": 90,
"items": [{
"_id": ObjectId("64116251c9443120dbcb9142"),
"order_id": "2",
"title": "牛奶",
"price": 50,
"num": 1
}, {
"_id": ObjectId("64116256c9443120dbcb9143"),
"order_id": "2",
"title": "酸奶",
"price": 40,
"num": 1
}]
}

Nodejs 操作MongoDB数据库

Nodejs操作mongodb数据库官方文档:

MongoDB Node.js Driver

一、Create a Project Directory
1
mkdir node_quickstart
1
cd node_quickstart
1
npm init -y
1
npm i mongodb --save
二、Connect to MongoDB

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
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
87
88
89
90
91
92
93
94
95
96
97
//引入mongodb
const { MongoClient } = require("mongodb");
//定义数据库连接的地址
const url = 'mongodb://127.0.0.1:27017';
//定义要操作的数据库
const dbName = 'test';

//实例化Mongoclient传入数据库连接地址
const client = new MongoClient(url,{useUnifiedTopology : true });
//连接数据库
client.connect((err)=>{
if(err){
console.log(err);
return;
}

console.log('数据库连接成功');
let db = client.db(dbName);

// 1、查找数据
db.collection('user').find({}).toArray((err,result)=>{
if(err){
console.log(err);
return;
}
console.log(result);

// 操作数据库完毕以后一定要 关闭数据库连接
client.close();
})

// db.collection('user').find({"age": 13}).toArray((err,result)=>{
// if(err){
// console.log(err);
// return;
// }
// console.log(result);
// })

// 2、增加数据
db.collection('user').insertOne({
"name": "张三",
"age": 13
},(err,result)=>{
if(err){
console.log(err);
return;
}
console.log('增加成功');
console.log(result);

// 操作数据库完毕以后一定要 关闭数据库连接
client.close();
})

// 3、更新数据
db.collection('user').updateOne({
name: "张三",
},{$set: {"age": 13}},(err,result)=>{
if(err){
console.log(err);
return;
}
console.log('更新成功');
console.log(result);
// 操作数据库完毕以后一定要 关闭数据库连接
client.close();
})

// 4、删除一条数据
db.collection('user').deleteOne({
name: "张三"
},(err,result)=>{
if(err){
console.log(err);
return;
}
console.log('删除一条数据成功');
console.log(result);
// 操作数据库完毕以后一定要 关闭数据库连接
client.close();
})

// 5、删除多条数据
db.collection('user').deleteMany({
name: "zhangsan"
},(err,result)=>{
if(err){
console.log(err);
return;
}
console.log('删除多条数据成功');
console.log(result);
// 操作数据库完毕以后一定要 关闭数据库连接
client.close();
})
})

NodeJs操作MongoDb数据库查询数据通过ejs显示列表以及通过表单增加数据

Express,动态路由,get传值

1
2
$ mkdir myapp
$ cd myapp
1
npm init
1
npm install express --save

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express')
const app = express()
const port = 5000

// 配置路由
app.get('/', (req, res) => {
res.send('Hello World!')
})

// 监听端口
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
1
node app.js
配置路由、获取get传值
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
const express = require('express')
const app = express()
const port = 5000

// 配置路由
app.get('/', (req, res) => {
res.send('首页')
})

app.get('/login', (req, res) => {
res.send('登录')
})

app.get('/register', (req, res) => {
res.send('注册')
})

app.get('/article', (req, res) => { // get:获取数据(显示数据
res.send('文章')
})

app.post('/doLogin', (req, res) => { // post:增加数据
console.log('执行登录')
res.send('执行登录')
})

app.put('/editUser', (req, res) => { // put:修改数据
console.log('修改用户')
res.send('修改用户')
})

app.delete('/deleteUser', (req, res) => { // delete:删除数据
console.log('删除用户')
res.send('删除用户')
})

// 动态路由 配置路由的时候也要注意顺序
app.get('/article/add', (req, res) => {
res.send('article add')
})

app.get('/article/:id', (req, res) => {
var id = req.params.id;
res.send('动态路由' + id)
})

// 如果这个路由写在后面, id就是add
// app.get('/article/add', (req, res) => {
// res.send('article add')
// })

// get 传值
// http://localhost:5000/product?id=125454&cid=2
app.get('/product', (req, res) => {
var id = req.params.id;
let query = req.query;
console.log(query) // { id: '125454', cid: '2' }
res.send('product')
})
// 监听端口
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

Express框架中ejs的安装使用

1
npm install ejs --save
1
2
// 2、配置模板引擎
app.set('view engine', 'ejs');
1
2
3
4
// 3、使用   默认加载模板引擎的文件夹是views
res.render('index',{ // views/index.ejs 不需要写views {}里面是传数据,没有传空对象

})

使用supervisor app.js启动,就不需要每次修改了app.js都要重启一次

1
npm i -g supervisor 
绑定数据
1
2
<%=data%>
<%=obj.name%>
1
<p><%-article%></p>         // 渲染包含HTML标签的内容
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 express = require('express');
const app = express();
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
let title = '你好'
res.render('index',{
title:title
})
});

app.get('/news', (req, res) => {
let userinfo = {
name:'张三',
age:18
}
let article = '<h3>内容包含HTML标签</h3>'
res.render('news',{
userinfo:userinfo,
article:article
})
});

// 监听端口 端口号建议写成3000以上
app.listen(5000);
1
<p><%=title%></p>
1
2
3
<p><%=userinfo.name%>——————<%=userinfo.age%></p>

<p><%-article%></p>
条件判断

js代码要写在<%%>里面

1
2
3
4
5
6
7
8
9
<%if(flag ===true){%>
<strong>flag ===true</strong>
<%}%>

<%if(score >=60){%>
及格
<%}else{%>
不及格
<%}%>
循环遍历
1
2
3
4
5
<ul>
<%for(var i = 0; i<list.length; i++){%>
<li><%=list[i]%></li>
<%}%>
</ul>
在模板中引入公共模板
1
<%- include("footer.ejs") %>

将模板文件设置为.html

  1. 在 app.js 定义ejs,代码如下:

    1
    const ejs = require('ejs');
  2. 注册html模板引擎,代码如下:

    1
    app.engine('html', ejs.__express);
  3. 将模板引擎换成html,代码如下:

    1
    app.set('view engine', 'html');
  4. 修改模板文件的后缀为.htmlindex.ejs——>index.html

app.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
26
27
28
29
30
31
32
33
34
const express = require('express');
const ejs = require('ejs');
const app = express();

// 1.定义ejs
app.engine('html', ejs.__express);
// 2.注册html模板引擎
app.set('view engine', 'html');

app.get('/', (req, res) => {
let title = '你好'
res.render('index',{
title:title
})
});

app.get('/news', (req, res) => {
let userinfo = {
name:'张三',
age:18
}
let article = '<h3>内容包含HTML标签</h3>'
let list = [1,3,4,5,6,7,8,9,10,11,12]
res.render('news',{
userinfo:userinfo,
article:article,
flag: true,
score: 60,
list:list
})
});

// 监听端口 端口号建议写成3000以上
app.listen(5000);

指定模板位置,默认模板位置在views

1
app.set('views', __dirname + 'views');

利用Express.static托管静态文件

1、如果你的静态资源存放在多个目录下面,你可以多次调用express.static中间件:

1
2
// 配置静态web目录
app.use(express.static('static'));

重启服务,static目录下面的文件就可以访问

static/css/base.css

index.htmlnews.html中引入样式:

1
<link rel="stylesheet" href="/css/base.css">

可以配置多个静态资源存放目录

public/js/base.js:

1
2
let a = 10;
console.log(a);

app.js

1
2
// 配置第二个静态web目录
app.use(express.static('public'));

重启服务,public目录下面的文件就可以访问

news.html中引入js:

1
2
<script src="/js/base.js"></script>
// 可以看到在控制台输出 10

2、如果你希望所有通过express.static访问的文件都存放在一个“虚拟(virtual)”目录(即目录根本不存在)下面,可以通过为静态资源目录指定一个挂载路径的方式来实现,如下所示:

1
app.use('/static', express.static('public'));

现在,你就可以通过带有"/static"前缀的地址来访问 public目录下面的文件了。

例子:

app.js

1
2
// 配置虚拟目录
app.use('/static2', express.static('public'));

重启服务,即可访问http://localhost:5000/static2/images/logo.jpg`

views/news.html

1
<img src="/static2/images/logo.jpg" alt="logo">

实际上不存在static2目录,是指向了public目录

Express中间件——body-paser中间件

通俗的讲:中间件就是匹配路由之前或者匹配路由完成做的一系列的操作。中间件中如果想往下匹配的话,那么需要写next()

Express应用可使用如下几种中间件:
  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

Express中间件是一种函数,可以在HTTP请求的处理过程中进行拦截和处理,以实现某些特定的功能。中间件可以用来处理请求和响应对象,调用下一个中间件函数,终止请求-响应周期,以及执行任何其他操作。

应用级中间件

应用级别的中间件使用app.use()方法来注册,可以匹配任何路由,也可以设置在路由之前或之后执行

app.js

1
2
3
4
5
6
7
8
9
10
11
12
// 应用级中间件            
app.use((req, res, next) => { // 在请求路由之前就执行
console.log(new Date()); // 访问http://localhost:5000/在控制台输出当前时间
next();
})

app.get('/', (req, res) => {
let title = '你好'
res.render('index',{
title:title
})
});
路由级中间件

路由级别的中间件使用app.METHOD()方法来注册,其中METHOD表示HTTP请求方法(如GET、POST等),可以针对特定的路由进行匹配

1
2
3
4
5
6
7
8
9
// 路由中间件(用得比较少)
app.get('/news/add', (req, res, next) => {
console.log('执行增加新闻'); // 在控制台输出 执行增加新闻
next(); // 继续向下匹配路由
});

app.get('/news/:id', (req, res) => {
res.send('新闻动态路由') // 在页面输出 新闻动态路由
});
错误处理中间件

错误处理中间件通常用于处理在请求处理过程中发生的错误

1
2
3
4
//  3. 错误处理中间件
app.use(( req, res, next) => {
res.status(404).send('404')
})

示例:

1
2
3
4
5
app.use((err, req, res, next) => {
console.log(new Date());
console.log(err);
res.status(500).send('服务器错误')
})
内置中间件
1
2
// 4. 内置中间件
app.use(express.static('static'))
1
<link rel="stylesheet" href="/css/base.css">
第三方中间件

body-parser中间件

body-parser是一个处理HTTP请求体的中间件,可以解析JSON、Raw、文本(Text)、URL-encoded格式的请求体。在Express框架中,使用body-parser做为请求体解析中间件[3]。使用body-parser可以把请求主体解析为req.body属性下可用的对象形式,方便后续处理[1]

  1. 安装

    1
    npm i body-parser --save
  2. 引入

    1
    const bodyParser = require('body-parser');
  3. 设置中间件

    1
    2
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));

    处理form表单的中间件 form表单提交的数据

  4. 接收post数据:req.body 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.get('/login', (req, res) => {
    res.render('login',{})
    })

    app.post('/doLogin', (req, res) => {
    var body = req.body;
    console.log(body); // 控制台输出:{ username: 'admin', password: '123456' }
    res.send(body.username + ' ' + body.password);
    })

    index.ejs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <form action="/doLogin" method="post">
    <div class="form-group">
    <label for="username">用户名</label>
    <input type="text" class="form-control" id="username" name="username">
    </div>

    <div class="form-group">
    <label for="password">密码</label>
    <input type="password" class="form-control" id="password" name="password">
    </div>

    <div>
    <input type="submit" value="提交">
    </div>
    </form>

Express中间件 cookie的基本使用

  • cookie是存储于访问者的计算机中的变量。可以让我们用同一个浏览器访问同一个域名的时候共享数据。

  • HTTP是无状态协议。简单地说,当你浏览了一个页面,然后转到同一个网站的另一个页面,服务器无法认识到这是同一个浏览器在访问同一个网站。每一次的访问,都是没有任何关系的。

  • Cookie是一个简单到爆的想法:当访问一个页面的时候,服务器在下行HTTP报文中,命令浏览器存储一个字符串;浏览器再访问同一个域的时候,将把这个字符串携带到上行HTTP请求中。第一次访问一个服务器,不可能携带cookie。必须是服务器得到这次请求,在下行响应报头中,携带cookie 信息,此后每一次浏览器往这个服务器发出的请求,都会携带这个cookie。

要加密,不允许修改,不加密,在客户端可以直接修改

  1. 安装

    1
    npm i cookie-parser --save
  2. 引入

    1
    const cookieParser = require('cookie-parser');
  3. 设置中间件

    1
    app.use(cookieParser());
  4. 设置cookie

    res.cookie('name','test', {maxAge: 1000*60*60});

    1
    2
    3
    4
    5
    6
    app.get('/', (req, res) => {
    // 设置cookie,如果cookie没有过期的话,关闭浏览器后重新打开cookie,cookie不会销毁
    res.cookie('name','test', {maxAge: 1000*60*60});

    res.send('Cookie的使用');
    });
  5. 获取cookie

    let name = req.cookies.name;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    app.get('/article', (req, res) => {
    // 获取cookie
    let name = req.cookies.name;
    res.send('文章页面----' + name);
    });

    app.get('/user', (req, res) => {
    // 获取cookie
    let name = req.cookies.name;
    res.send('用户页面----' + name);
    })

    在访问/时设置了cookie,在这个域(Domain)下不同的页面之间可以共享这个cookie

path:指定 cookie 的路径,默认为当前路径。

1
2
// 指定cookie路径
res.cookie('name','test', {maxAge: 1000*60*60, path: '/article'});

设置了之后,在文章页面可以获取到该cookie,在用户页面获取不了该cookie

domain:指定 cookie 的域名,默认为当前域名

  1. 在host中添加两个域

    1
    2
    127.0.0.1 aaa.test.com
    127.0.0.1 bbb.test.com
  2. http://localhost:5000/的访问,可以使用http://aaa.test.com:5000/http://bbb.test.com:5000/访问

  3. aaa.test.com中设置的cookie,在bbb.test.com中无法获取——【默认为当前域名】

    1
    2
    3
    4
    app.get('/', (req, res) => {
    res.cookie('name','test', {maxAge: 1000*60*60});
    res.send('Cookie的使用');
    });
    1. 先访问http://aaa.test.com:5000/进行设置cookie
    2. 访问http://aaa.test.com:5000/user,可以获取到cookie
    3. 访问http://bbb.test.com:5000/user,无法获取到cookie
  4. 设置多个域名共享cookie

    1
    2
    // 设置多个域名共享cookie, aaa.test.com   bbb.test.com
    res.cookie('name','test22', {maxAge: 1000*60*60, domain: '.test.com'});

    aaa.test.com和bbb.test.com共享cookie test.com二级域名都可以获取cookie

中文cookie
1
res.cookie('name','张三', {maxAge: 1000*60*60});

cookie可以在客户端修改,如果像这种中文编码后的值被修改后,就会乱码。所以要对cookie进行加密

加密cookie

防止cookie被恶意篡改

  1. 在初始化 cookie-parser 中间件时,需要传入一个 secret 参数,这个参数就是用来对 cookie 进行签名的密钥。

    1
    2
    // 1. 配置中间件的时候需要传入加密的参数
    app.use(cookieParser('test'));
  2. 将 signed 参数设置为 true

    1
    2
    // 2.将 signed 参数设置为 true
    res.cookie('name','test33', {maxAge: 1000*60*60, signed: true});
  3. 获取签名后的cookie值

    1
    let signedName = req.signedCookies.name; 

加了签名后的cookie,如果被修改了,无法获取到cookie的值,会默认返回false/undefined

Express Session的基本使用

一、Session的简单介绍

session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而session保存在服务器上。

​ Cookie数据存放在客户的浏览器上,Session数据放在服务器上。Session相比 Cookie要更安全一些。由于Session保存到服务器上,所以当访问量增多的时候,会比较占用服务器的性能。单个cookie保存的数据大小不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。Session没有这方面的限制。Session是基于Cookie进行工作的。

二、Session的工作流程

​ 当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key, value的键值对,然后将key (cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key (cookie),找到对应的session(value)。

三、express-session的使用

https://www.npmjs.com/package/express-session

  1. 安装

    1
    npm i express-session --save
  2. 引入

    1
    const session = require('express-session');
  3. 配置session的中间件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 配置session的中间件
    app.use(session({
    secret: 'keyboard cat', // 服务器端生成session的签名
    resave: false, // 强制保存 session 即使它并没有变化
    saveUninitialized: true, // 强制将未初始化的session存储
    cookie: {
    maxAge: 1000*60,
    secure: false // true表示只有https协议才能访问cookie
    },
    rolling: true // 在每次请求时强行设置cookie,这将重置cookie过期时间(默认是false)
    }))
  4. 设置session

    1
    2
    3
    4
    5
    6
    app.get('/login', (req, res) => {
    // 设置session
    req.session.username = '张三';
    req.session.age = 28;
    res.send('执行登录!');
    });
  5. 获取session

    1
    2
    3
    4
    5
    6
    7
    8
    app.get('/', (req, res) => {
    // 获取session
    if(req.session.username || req.session.age) {
    res.send('欢迎--'+ req.session.username + ' '+ req.session.age +'--!');
    }else {
    res.send('请先登录!');
    }
    });
  6. 销毁session

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    app.get('/loginOut', (req, res) => {
    // 删除session
    // 1.设置session的过期时间为0(它会把所有的session值都销毁)
    // req.session.cookie.maxAge = 0;
    // 2. 销毁指定session
    // req.session.username = '';
    // 3. 销毁session
    req.session.destroy(); // 全部销毁

    // req.session.destroy(function(err){
    //
    // });
    res.send('退出登录!');
    });

负载均衡配置session,把session保存到数据库

多服务器负载均衡

  1. 安装

    1
    npm i express-session connect-mongo --save
  2. 引入模块

    connect-mongo

    1
    2
    3
    4
    const express = require('express');
    const session = require('express-session');
    const MongoStore = require('connect-mongo');
    const app = express();
  3. 配置session中间件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 配置session的中间件
    app.use(session({
    secret: 'keyboard cat', // 服务器端生成session的签名
    name:'test', // 修改session对应cookie的名称
    resave: false, // 强制保存 session 即使它并没有变化
    saveUninitialized: true, // 强制将未初始化的session存储
    store: MongoStore.create({
    mongoUrl:'mongodb://localhost:27017/sessionDB', // 数据库连接地址
    touchAfter: 24 * 3600 // session自动更新时间(不管发出了多少次请求,在24小时内只更新一次session,除非你改变了)
    }),
    cookie: {
    maxAge: 1000*60, // session过期时间
    secure: false // true表示只有https协议才能访问cookie
    },
    rolling: true // 在每次请求时强行设置cookie,这将重置cookie过期时间(默认是false)
    }))
  4. 在mongoDB中查看数据

    1
    2
    3
    4
    5
    6
    7
    > use sessionDB
    switched to db sessionDB
    > show collections
    sessions
    > db.sessions.find()
    { "_id" : "e0DHVlNpyZoixD7MT-A_Kr1EqMQno6My", "expires" : ISODate("2023-03-29T06:19:29.579Z"), "lastModified" : ISODate("2023-03-29T06:18:29.579Z"), "session" : "{\"cookie\":{\"originalMaxAge\":60000,\"expires\":\"2023-03-29T06:19:29.579Z\",\"secure\":false,\"httpOnly\":true,\"path\":\"/\"},\"username\":\"张三\",\"age\":28}" }
    >
其他存储方式的插件
connect-redis
connect-mysql

Express路由模块化以及 Express应用程序生成器

http://expressjs.com/en/guide/routing.html 中的express.Router

Express中允许我们通过express.Router创建模块化的、可挂载的路由处理程序。

  1. 新建login.js;将登录相关模块的api都写在login.js中,便于管理,也便于多人同时协作开发

  2. 在app.js中

    1
    2
    3
    4
    // 1.引入login模块
    const login = require('./routes/login');
    // 2.挂载login模块
    app.use('/login', login);
  3. 访问http://localhost:5000/login/*,即可正常访问

routes/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express');
const router = express.Router();

router.get('/', function(req, res) {
res.render('login',{});
})

router.post('/doLogin', function(req, res) {
var body = req.body
console.log(body);
res.send('执行提交' + body.username);
})

module.exports = router;

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<form action="/login/doLogin" method="post">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" name="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="form-group">
<input type="submit" value="提交">
</div>
</form>

app.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
26
27
28
29
30
const express = require('express');
const ejs = require('ejs');
const bodyParser = require('body-parser');
const app = express();

// 引入外部login模块
const login = require('./routes/login');

// 配置模板引擎
app.engine('html', ejs.__express)
app.set('view engine', 'html');

// 配置静态资源目录
app.use(express.static('public'));

// 配置第三方中间件
app.use(bodyParser.urlencoded({ extended:false}));
app.use(bodyParser.json());

// 挂载login模块
app.use('/login', login);

app.get('/', (req, res) => {
res.send('首页!');
});

app.listen(5000, () => {
console.log('Example app listening on port 5000!');
});

新建user.js模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express');
const router = express.Router();

router.get('/', function(req, res) {
res.send('用户列表');
});
router.get('/add', function(req, res) {
res.send('添加用户');
});
router.get('/edit', function(req, res) {
res.send('编辑用户');
});

module.exports = router;
挂载user模块
1
2
3
4
const user = require('./routes/user')

// 挂载user模块
app.use('/user', user);

访问http://localhost:5000/user/*即可

Express应用程序生成器-express-generator

安装

1
npm install -g express-generator

验证是否安装成功

1
express -h

创建项目

1
express --view=ejs myapp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 执行以上命令,会创建myapp目录,在里面自动生成以下目录及文件:
myapp
│ app.js
│ package.json

├─bin
│ www

├─public
│ ├─images
│ ├─javascripts
│ └─stylesheets
│ style.css

├─routes
│ index.js
│ users.js

└─views
error.ejs
index.ejs
1
2
3
cd myapp
npm i
npm start 或 node ./bin/www
根据不同模块创建路由
  1. 路由分模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    routes
    │ admin.js
    │ api.js
    │ index.js

    └─admin
    login.js
    nav.js
    user.js
  2. 页面分模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    views
    │ error.ejs
    │ index.ejs

    └─admin
    ├─login
    │ index.ejs

    ├─nav
    │ add.ejs

    └─user
    add.ejs
  3. 路由配置

    routes/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var express = require('express');
    var router = express.Router();

    /* GET home page. */
    router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
    });

    module.exports = router;

    routes/api.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var express = require('express');
    var router = express.Router();

    /* GET API listing. */
    router.get('/', function(req, res, next) {
    res.send('API');
    });

    module.exports = router;

    routes/admin.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var express = require('express');
    var router = express.Router();

    // 引入模块
    const user = require('./admin/user');
    const login = require('./admin/login');
    const nav = require('./admin/nav');

    /* GET Admin listing. 管理后台相关的*/
    router.get('/', function(req, res, next) {
    res.send('后台管理中心');
    });

    // 挂载路由
    router.use('/user', user);
    router.use('/login', login);
    router.use('/nav', nav);

    module.exports = router;

    routes/admin/user.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const express = require('express');
    const router = express.Router();

    router.get('/', function(req, res) {
    res.send('用户列表');
    });
    router.get('/add', function(req, res) {
    res.render('admin/user/add');
    });
    router.get('/edit', function(req, res) {
    res.send('编辑用户');
    });

    router.post('/doAdd', function(req, res) {
    var body = req.body;
    res.send(body);
    });

    router.post('/doEdit', function(req, res) {
    res.send('执行编辑');
    });

    module.exports = router;

    routes/admin/nav.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const express = require('express');
    const router = express.Router();

    router.get('/', function(req, res) {
    res.send('导航列表');
    });
    router.get('/add', function(req, res) {
    res.render('admin/nav/add');
    });
    router.get('/edit', function(req, res) {
    res.send('编辑用户');
    });

    router.post('/doAdd', function(req, res) {
    var body = req.body;
    res.send(body);
    });

    router.post('/doEdit', function(req, res) {
    res.send('执行编辑');
    });

    module.exports = router;

    routes/admin/login.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const express = require('express');
    const router = express.Router();

    router.get('/', function(req, res) {
    res.render('admin/login/index',{});
    })

    router.post('/doLogin', function(req, res) {
    var body = req.body
    console.log(body);
    res.send('执行提交' + body.username);
    })

    module.exports = router;
  4. 页面

    views/admin/user.ejs

    1
    2
    3
    4
    5
    <form action="/admin/user/doAdd" method="post">
    <input type="text" name="username" placeholder="用户名"> <br/> <br/>
    <input type="password" name="password" id="password"placeholder="密码"> <br/> <br/>
    <input type="submit" value="提交">
    </form>
  5. app.js配置

    1
    2
    3
    4
    5
    6
    7
    const admin = require('./routes/admin');
    const index = require('./routes/index');
    const api = require('./routes/api');

    app.use('/admin', admin);
    app.use('/api', api);
    app.use('/', index);

Express结合multer 上传图片

Multer是一个node.js中间件,用于处理multipart/form-data类型的表单数据,它主要用于上传文件。它是写在 busboy 之上非常高效。

注意: Multer不会处理任何非multipart/form-data类型的表单数据。

  1. 安装依赖

    1
    npm i express ejs multer --save
  2. 在根目录新建static/uploads目录, 上传之前目录必须存在

  3. app.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
    26
    const express = require('express');
    var path = require('path');
    const multer = require('multer');
    const upload = multer({ dest: 'static/uploads'}); // uploads这个目录必须要在上传文件前存在
    const app = express();

    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'ejs');

    app.get('/', (req, res) =>{
    res.send('hello world');
    });

    app.get('/upload', (req, res) =>{
    res.render('upload');
    });

    // pic是文件框的name值
    app.post('/doUpload', upload.single('pic'), (req, res) => {
    console.log(req.file);
    res.send(req.file);
    });

    app.listen(5000, () => {
    console.log('server is running at port 5000');
    });
  4. views/upload.ejs

    注意:form 要一定加上 enctype="multipart/form-data"属性

    1
    2
    3
    4
    <form action="/doUpload" method="post" enctype="multipart/form-data">
    <input type="file" name="pic">
    <input type="submit" value="上传">
    </form>
  5. 上传一张图片之后,可以在static/uploads下多了一个文件名随机生成的文件,但是没有后缀名,打开不了

自定义上传后的文件名以及设置文件后缀名

app.js中将const upload = multer({ dest: 'static/uploads'});替换为以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const storage = multer.diskStorage({
// 配置上传的目录
destination: function (req, file, cb) {
cb(null, 'static/uploads') // 上传之前目录必须存在
},
// 修改上传后的文件名
filename: function (req, file, cb) {
// 1. 获取后缀名
const extname = path.extname(file.originalname);
// 2. 获取文件名
const basename = path.basename(file.originalname, extname);
// 3. 生成新的文件名
cb(null, basename + Date.now() + extname)

// cb(null, file.originalname)
}
})
const upload = multer({ storage: storage })
将文件上传的这个功能封装到tools.js

model/tools.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
26
27
28
29
const path = require('path');
const multer = require('multer');

let tools = {
multer(){
const storage = multer.diskStorage({
// 配置上传的目录
destination: function (req, file, cb) {
cb(null, 'static/uploads') // 上传之前目录必须存在
},
// 修改上传后的文件名
filename: function (req, file, cb) {
// 1. 获取后缀名
const extname = path.extname(file.originalname);
// 2. 获取文件名
const basename = path.basename(file.originalname, extname);
// 3. 生成新的文件名
cb(null, basename + Date.now() + extname)

// cb(null, file.originalname)
}
})
const upload = multer({ storage: storage })

return upload;
}
}

module.exports = tools;

app.js

1
2
3
4
5
6
7
8
const tools = require('./model/tools');


// pic是文件框的name值
app.post('/doUpload', tools.multer().single('pic'), (req, res) => {
console.log(req.file);
res.send(req.file);
});

Express按照日期生成上传文件目录

安装依赖

1
npm i silly-datetime --save

model/tools.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const path = require('path');
const multer = require('multer');
const { mkdirp } = require('mkdirp');
var sd = require('silly-datetime');
let tools = {
multer(){
const storage = multer.diskStorage({
// 配置上传的目录
destination: async (req, file, cb)=>{
// 1. 获取当前日期 20200703
const day = sd.format(new Date(), 'YYYYMMDD');
// 2. 拼接目录:static/upload/20200703
let dir = path.join('static/uploads', day);
// 3. 按照日期生成图片存储目录 mkdirp是一个异步方法 要使用 async 和 await
await mkdirp(dir);
// 4. 生成新的文件名
cb(null, dir) // 上传之前目录必须存在
},
// 修改上传后的文件名
filename: function (req, file, cb) {
// 1. 获取后缀名
const extname = path.extname(file.originalname);
// 2. 获取文件名
const basename = path.basename(file.originalname, extname);
// 3. 生成新的文件名
// 原文件名
cb(null, basename + extname);

// 带日期
// const day = sd.format(new Date(), 'YYYYMMDD');
// cb(null, basename + '_' + day + extname)
}
})
const upload = multer({ storage: storage })

return upload;
}
}

module.exports = tools;

多文件上传

app.js

1
2
3
4
5
6
7
8
9
10
11
12
// 这里tools.multer() == upload
var cpUpload = tools.multer().fields([
{ name: 'pic1', maxCount: 1 },
{ name: 'pic2', maxCount: 1 },
])

// pic是文件框的name值
app.post('/doUpload', cpUpload, (req, res) => {
// console.log(req.file);
// res.send(req.file);
res.send(req.files); // 多文件上传,要用files
});

上传文件的文件名为中文的处理

解决multer上传文件乱码 文件上传

fileFilter(Function)——Multer的option

1
2
3
4
5
fileFilter(req, file, callback) {
// 解决中文名乱码的问题 latin1 是一种编码格式
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf8");
callback(null, true);
},
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
const path = require('path');
const multer = require('multer');
const { mkdirp } = require('mkdirp');
var sd = require('silly-datetime');
let tools = {
multer(){
const storage = multer.diskStorage({
// 配置上传的目录
destination: async (req, file, cb)=>{
// 1. 获取当前日期 20200703
const day = sd.format(new Date(), 'YYYYMMDD');
// 2. 拼接目录:static/upload/20200703
let dir = path.join('static/uploads', day);
// 3. 按照日期生成图片存储目录 mkdirp是一个异步方法 要使用 async 和 await
await mkdirp(dir);
// 4. 生成新的文件名
cb(null, dir) // 上传之前目录必须存在
},
// 修改上传后的文件名
filename: function (req, file, cb) {
// // 1. 获取后缀名
// const extname = path.extname(file.originalname);
// // 2. 获取文件名
// const basename = path.basename(file.originalname, extname);
// 3. 生成新的文件名
// 原文件名
// cb(null, decodeURI(file.originalname));
cb(null, file.originalname);

// 带日期
// const day = sd.format(new Date(), 'YYYYMMDD');
// cb(null, basename + '_' + day + extname)
}
})
const upload = multer({
fileFilter(req, file, callback) {
// 解决中文名乱码的问题 latin1 是一种编码格式
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf8");
callback(null, true);
},
storage: storage
})

return upload;
}
}

module.exports = tools;

一个文件选择框进行多选——可以使用upload.array()

1
<input type="file" name="pic1" multiple>
1
2
3
4
app.post('/doUpload', upload.array('pic1', 2), (req, res) => {
console.log(req.files);
res.send('Files uploaded successfully.');
});

多个文件上传框,每个文件上传框多选的报错处理

报错信息:MulterError: Unexpected field

原来代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// upload.ejs:
<form action="/doUpload" method="post" enctype="multipart/form-data">
<input type="file" name="pic1" multiple>
<input type="file" name="pic2" multiple>
<input type="submit" value="上传">
</form>

// app.js:
var cpUpload = tools.multer().fields([
{ name: 'pic1', maxCount: 2 },
{ name: 'pic2', maxCount: 8 },
])

app.post('/doUpload', cpUpload, (req, res) => {
res.send(req.files); // 多文件上传,要用files
});

Node Multer unexpected field中看到以下解答:

2022-2023 Answer

When using FormData and working with arrays, the multer variable name must also include the ‘[]‘, this did not use to be the case.

Old code:

1
2
3
const multerConfig = upload.fields([
{name: 'photos', maxCount: 20}
])

2022+ code:

1
2
3
const multerConfig = upload.fields([
{name: 'photos[]', maxCount: 20}
])

修改后的代码:

将name值修改为[]形式,可解决

1
2
<input type="file" name="pic1[]" multiple>
<input type="file" name="pic2[]" multiple>
1
2
3
4
var cpUpload = tools.multer().fields([
{ name: 'pic1[]', maxCount: 2 },
{ name: 'pic2[]', maxCount: 8 },
])

mongoose 入门以及mongoose 实现数据的增、删、改、查

Mongoose是在node.js异步环境下对mongodb进行便捷操作的对象模型工具。Mongoose是NodeS的驱动,不能作为其他语言的驱动。

Mongoose有两个特点:

1、通过关系型数据库的思想来设计非关系型数据库
2、基于mongodb驱动,简化操作

使用

  1. 安装

    1
    npm i mongoose --save
  2. 引入

    1
    const mongoose = require('mongoose');
  3. 连接数据库

    1
    mongoose.connect('mongodb://127.0.0.1:27017/test');     // test是数据库名

    如果有账户密码需要采用下面的连接方式:

    1
    mongoose.connect('mongodb://admin:123456@127.0.0.1:27017/test');   //账号:密码@
  4. 定义Schema, 字段名称必须要与数据库保持一致

    数据库中的Schema,为数据库对象的集合。schema是 mongoose里会用到的一种数据模式,可以理解为表结构的定义;每个schema 会映射到mongodb中的一个collection,它不具备操作数据库的能力

    1
    2
    3
    4
    5
    6
    7
    const { Schema } = mongoose;

    var UserSchema = new Schema({
    name: String,
    age: Number,
    sex: String
    });
  5. 定义Model(创建数据模型)

    定义好了Schema,接下就是生成Model。model是由schema生成的模型,可以对数据库的操作。
    注意:mongoose.model里面可以传入两个参数也可以传入三个参数

    mongoose.model(参数1:模型名称(首字母大写),参数2:Schema,参数3:数据库集合名称)
    如果传入2个参数的话:这个模型会和模型名称相同的复数的数据库建立连接:如通过下面方法创建模型,那么这个模型将会操作users这个集合。
    如果传入3个参数的话:模型默认操作第三个参数定义的集合名称

    1
    var User = mongoose.model('User', UserSchema);      // 操作users表——User则对应users表
    1
    var User = mongoose.model('User', UserSchema, 'user');  // 操作user表
  6. 查询数据

    1
    2
    3
    4
    5
    6
    7
    User.find({}, function (err, users) {
    if (err) {
    console.log(err);
    } else {
    console.log(users);
    }
    });

    报错处理throw new MongooseError('Model.find() no longer accepts a callback');

    因为在Mongoose 6.0版本中,Model.find()方法不再接受回调函数,而是返回一个Promise。如果你在Mongoose 6.0或更高版本中继续使用回调函数调用Model.find(),就会出现这个错误。

    修改后:

    1
    2
    3
    4
    5
    6
    7
    User.find({})
    .then((users) => {
    console.log(users);
    })
    .catch((error) => {
    console.log(error);
    });
  7. 增加数据(插入数据)

    Model.save()方法不再接受回调函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var user = new User({
    name: '张三AAAAAA',
    age: 18,
    sex: '男'
    });

    user.save()
    .then((users) => {
    console.log(users);
    })
    .catch((error) => {
    console.log(error);
    });
  8. 更新数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    User.updateOne({
    name: '张三AAAAAA'
    }, {
    $set: {
    name: '张三333',
    age: 28,
    sex: '女'
    }
    })
    .then((users) => {
    console.log(users);
    })
    .catch((error) => {
    console.log(error);
    });
  9. 删除数据

    1
    2
    3
    4
    5
    6
    7
    User.deleteOne({'_id':'642538f4a1e6c5b49ac14d57'}) // id直接写字符串就行,mongoose会处理
    .then((res) => {
    console.log(res);
    })
    .catch((error) => {
    console.log(error);
    });

    控制台输出:

    1
    { acknowledged: true, deletedCount: 1 }

mongoose默认参数、mongoose模块化、mongoose性能疑问

mongoose 默认参数:增加数据的时候,如果不传入数据会使用默认配置的数据

mongoose 预定义模式修饰符GettersSetters修饰符

https://www.bilibili.com/video/av38925557

https://www.bilibili.com/video/av41033371