Code Monkey home page Code Monkey logo

blog's Issues

好用的webpack插件:webpack-oss-upload-plugin

该 Webpack 插件用于在本地打包完成后,将打包后的文件上传至 阿里云OSS,并提供上传完成的回调


使用

安装 webpack-oss-upload-plugin

npm install webpack-oss-upload-plugin -D

在 webpack config 中使用

const prefix = `${dir}/${projectName}/${version}/`;

{
  output:{
    publicPath: `http://e-package.oss-cn-shanghai.aliyuncs.com/${prefix}`
  },
  plugins: [
    new WebpackOssUploadPlugin({
      // oss 的配置
      oss: {
        region: 'region',
        endpoint: 'endpoint',
        accessKeyId: 'accessKeyId',
        accessKeySecret: 'accessKeySecret',
        bucket: 'bucket'
      },
      // 上传后的文件路径为:publicPath/{prefix}/your-file.js
      prefix,
      // 上传完成后会调用该回调
      onComplete: (complication) => {
        
      }
    })
  ]
}

onComplete 的参数暴露了 complication 对象,里面包含当前打包的信息,你可以合理使用它

选项说明

  • oss: 阿里 oss 配置,region、endpoint、accessKeyId、accessKeySecret、bucket 这些参数是必须的
  • dir:可选项,默认为空数组,数组中的每一项表示上传至 oss 形成的目录名。比如 prefix: a/c/c/,那么上传后你的文件位置是:publicPath/a/b/c/your-file.js
  • onComplete: 可选项,当 OSS 将所有需要上传的文件上传完成后,会被调用,该方法参数为 complication 对象,里面包含当前打包的信息,你可以合理使用它

项目地址

github 地址:https://github.com/Vibing/webpack-oss-plugin

其他

如果你有其他需求或好的建议,请在 issue 中提给我

gRPC的简单使用

gRPC 是谷歌开源的一套多语言 RPC 框架,用官网的一句话来概括就是:

A high-performance, open-source universal RPC framework

翻译过来就是:一个高性能、开源通用的 RPC 框架。gRPC 也是一个遵循server/client模型的框架

gRPC 使用

先安装 grpc 和 @grpc/proto-loader

yarn add grpc @grpc/proto-loader

由于 gRPC 使用谷歌特有的 Protocol Buffer,用于序列化结构化数据的自动化过程,只需要定义如何组织你的结构化数据一次,就可以使用 protoc 轻松的根据这个定义生成语言相关的源代码(支持多种语言),以便于读写结构化数据。

这里通过 @grpc/proto-loader 来解析 proto 文件

关于 protoc buffer ,可以参考 Protocol Buffer是什么? 这篇文章

定义 hello.proto

protoBuffer 目前有 2 和 3 两个版本,这里我们用版本 3

// 使用 proto3
syntax = "proto3";

// 定义包名
package helloworld;

// 定义Hello服务
service Hello {
    // 定义 sayHello 方法
    rpc sayHello (HelloReq) returns (HelloRes);
}

// 定义 sayHello 方法的传值
message HelloReq {
    // 传入 name,1表示第一个参数
    string name = 1;
    // 传入 age,2表示第二个参数
    int32 age = 2;
    // 传入 job
    string job = 3;
}

message HelloRes {
    string message = 1;
}

protobuf 书写较为严格,不要忘记分号。

需要注意的是,以 string name = 1;为例,这里的 1 表示第一个参数,其实在JSON格式里表示第一个 key ,比如:

message HelloReq {
    // 传入 name,1表示第一个参数
    string name = 1;
    // 传入 age,2表示第二个参数
    int32 age = 2;
}

// 对应的 JSON, 第一个 key 必须是 name ,第二个是 age
{
	name: '',
	age: 20
}

Server

RPC 分为 Server 端和 Client 端,我们先写 Server 端代码

首先就是引入 grpc 和 @grpc/proto-loader,先使用 proto-loader 加载 proto 文件生成对应的 package,然后通过 grpc 加载这个包,并通过.helloworld来返回 helloword 这个包(对应的是 proto 文件里的 package helloworld;

const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

// 加载 proto 文件并配置
const packageDefinition = protoLoader.loadSync(
  path.resolve(__dirname, '../proto/hello.proto'),
  {
    // 保留现场大小写而不是转换为驼峰格式
    keepCase: true,
    // 长转换类型。有效值为String和Number(全局类型)。默认情况下将复制当前值,这是一个不安全的数字,如果不带{@link Long}且带有长库的话。
    longs: String,
    // 枚举值转换类型。唯一有效的值是`String`(全局类型)。默认情况下复制当前值,即数字ID。
    enums: String,
    // 在结果对象上设置默认值
    defaults: true,
    // 包括设置为当前字段名称的虚拟oneof属性(如果有的话)
    oneofs: true
  }
)

// 使用 grpc 加载包
const helloProto = grpc.loadPackageDefinition(packageDefinition).helloworld

上面这部分仅仅是对 proto 文件的处理,由于 Server 端和 Client 都用同一份 proto,所以这部分代码是服务端和客户端都要用到的。

现在 proto 已经解析好了,我们接着上面的代码开始写一个服务:

// 创建 server
const server = new grpc.Server()

// 添加服务, 这里的服务名叫Hello
server.addService(helloProto.Hello.service, {
  // 实现sayHello方法
  sayHello
})

// sayHello 方法,call 用来获取请求信息,callback 用来向客户端返回信息
function sayHello(call, callback) {
  try {
    // 获取 name 和 age
    const { name, age, job } = call.request
    console.log('收到客户端传值:', name, age, job)
    // 按 proto 约定传值,返回`我叫${name},年龄${age}`
    callback && callback(null, { message: `我叫${name},年龄${age}` })
  } catch (error) {
    console.log('服务出错', error)
    callback && callback(error)
  }
}

// 异步启动
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
    server.start()
    console.log('server start...')
})

打开控制台,输入 node server/index.js运行该服务

Client

因为 Client 也需要解析 proto,上文已经有相关代码,这里只写 Client 余下的代码:

// 创建客户端
const client = new helloProto.Hello(
  'localhost:50051',
  grpc.credentials.createInsecure()
)

// 调用 sayHello 方法
client.sayHello({ name: '张三', age: 30, job: 'teacher' }, (err, response) => {
  if (err) {
    console.log(err)
    return
  }
  const { message } = response
  console.log(message)
})

客户端就这么简单。

现在运行 node client/index.js

客户端调用 sayHello 方法,并传入{ name: '张三', age: 30, job: 'teacher' } ,服务端接收到参数后通过 sayHello 处理,并返回给客户端 我叫张三,年龄30

完整代码见:https://github.com/Vibing/node-grpc

至此,gRPC 一个简单的调用就完成了,就是这么简单。

理解JSX和虚拟DOM

前言

react的火热程度已经达到了94.5k个start,本系列文章主要用简单的代码来实现一个react,来了解JSX、虚拟DOM、diff算法以及state和setState的设计。

提到react,当然少不了vue,vue的api设计十分简单 上手也非常容易,但黑魔法很多,使用起来有点虚, 而react没有过多的api,它的深度体现在设计**上,使用react开发则让人比较踏实、能拿捏的住,这也是我喜欢react的原因之一。

JSX

react怎么少的了JSXJSX是什么,让我来看个例子
现在有下面这段代码:

const el = <h3 className="title">Hello Javascript</h3>

这样的js代码如果不经过处理会报错,jsx是语法糖,它让这段代码合法化,通过babel转化后是这样的:

const el = React.createElement(
    'h3',
    { className: 'title' },
    'Hello Javascript'
)

这种例子官网首页也有demo

准备开始

开始编码之前,先介绍两个东西:parcelbabel-plugin-transform-jsx,等会我们用parcel搭建一个开发工程,babel-plugin-transform-jsxbabel的一个插件,它可以将jsx语法转成React.createElement(...)

下面我们开始

简单的搭建

parcel这里就不介绍了,一句话概况就是为你生成一个零配置的开发环境。

  1. yarn global add parcel-bundlernpm install -g parcel-bundler
  2. 新建项目文件夹,这里取名为simple-react
  3. simple-react中执行 yarn init -ynpm init -y 生成package.json
  4. 创建一个index.html
  5. 创建src文件夹 再在src下创建index.js 然后再index.html中引入index.js

如果你先麻烦,可以直接下载源码修改。

以上步骤完可能不完整,最好参考parcel里的内容。以上工作完成后,我们需要安装babel-plugin-transform-jsx

npm insatll babel-plugin-transform-jsx --save-dev
或者
yarn add babel-plugin-transform-jsx --dev

然后添加.babelrc文件,并在该文件中加入下面这段代码:

{
  "presets": ["env"],
  "plugins": [["transform-jsx", { "function": "React.createElement" }]]
}

上面代码的意思是 使用transform-jsx插件,并配置为使用React.createElement方法来解析JSX,当然你也可以不用React.createElement和自定义方法,比如preact使用的h方法。

React.createElement()

现在我们在index.js里开始编码。
首先写入代码:

const el = <h3 className="title">Hello Javascript</h3>;
console.log(el);

我们在什么都不写的情况下,打印看看el是什么。
react-1

打印报错:React没有定义。 这是因为在.babelrc文件中,我们使用的这段代码起了作用:

["transform-jsx", { "function": "React.createElement" }]

上面说过,它会通过React.createElement方法来转译JSX,那么我们就给出这个方法:
我们把刚才那段代码改变一下:

const React = {
  createElement: function(...args) {
    return args[0];
  }
};

const el = <h3 className="title">Hello Javascript</h3>;

console.log(el);

上面代码添加了一个React对象,并在其中添加一个createElement方法,现在再执行一下看看打印出什么:
react-7

由打印结果可以看出,jsx在使用React.createElement方法转译时,createElement方法应该是这样的:

createElement({ elementName, attributes, children });
  • elementName: dom对象的标签名 比如div、span等等
  • attributes: 当前dom对象的属性集合 比如class、id等等
  • children: 所有子节点

现在我们改写一下createElement方法,让key的名称简单一点:

const React = {
  createElement: function({ elementName, attributes, children }) {
    return {
      tag: elementName,
      attrs: attributes,
      children
    };
  }
};

现在可以看到打印结果是:
react-3

我们再打印个复杂点的DOM结构:

const el = (
  <div style="color:red;">
    Hello <span className="title">JavaScript</span>
  </div>
);

console.log(el);

react-4

和我们想要的结构一样。
其实上面打印出来的就是虚拟DOM,现在我们要做的就是如何把虚拟DOM转成真正的DOM对象并显示在浏览器上。

ReactDOM.render()

要想将虚拟dom转成真实dom并渲染到页面上,就需要调用ReactDOM.render,比如:

ReactDOM.render(<h1>Hello World</h1>, document.getElementById('root'));

这段代码转换后的样子:

ReactDOM.render(
  React.createElement('h1', null, 'Hello World'),
  document.getElementById('root')
);

这时,react会将<h1>Hello World</h1>挂载到id为root的dom下,从而在页面上显示出来。

现在我们实现render方法:

function render(vnode, container) {
  const dom = createDom(vnode); //将vnode转成真实DOM
  container.appendChild(dom);
}

上面代码中先调用createDom将虚拟dom转成真实DOM然后挂载到container下。

我们来实现createDom方法:

function createDom(vnode) {
  if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
    vnode = '';
  }

  if (typeof vnode === 'string' || typeof vnode === 'number') {
    return document.createTextNode(String(vnode));
  }

  const dom = document.createElement(vnode.tag);

    //设置属性
  if (vnode.attrs) {
    for (let key in vnode.attrs) {
      const value = vnode.attrs[key];
      setAttribute(dom, key, value);
    }
  }
    //递归render子节点
  vnode.children.forEach(child => render(child, dom));
  return dom;
}

由于属性的种类比较多,我们抽出一个setAttribute方法来设置属性:

function setAttribute(dom, key, value) {
  //className
  if (key === 'className') {
    dom.setAttribute('class', value);

    //事件
  } else if (/on\w+/.test(key)) {
    key = key.toLowerCase();
    dom[key] = value || '';
    //style
  } else if (key === 'style') {
    if (typeof value === 'string') {
      dom.style.cssText = value || '';
    } else if (typeof value === 'object') {
      // {width:'',height:20}
      for (let name in value) {
      //如果是数字可以忽略px
        dom.style[name] =
          typeof value[name] === 'number' ? value[name] + 'px' : value[name];
      }
    }

    //其他
  } else {
    dom.setAttribute(key, value);
  }
}

现在render方法已经完整的实现了,我们将创建ReactDOM对象,将render方法挂上去:

const ReactDOM = {
  render: function(vnode, container) {
    container.innerHTML = '';
    render(vnode, container);
  }
};

这里在调用render之前加了一句container.innerHTML = '',就不解释了,相信大家都明白。

那么万事具备,我们来测试一下,直接上一个比较复杂的dom结构并加上属性:

const element = (
  <div
    className="Hello"
    onClick={() => alert(1)}
    style={{ color: 'red', fontSize: 30 }}
  >
    Hello <span style={{ color: 'blue' }}>javascript!</span>
  </div>
);

ReactDOM.render(element, document.getElementById('root'));

打开页面,是我们想要的结果:

react-5

再看看控制台的dom:

react-6

很完美,这是我们想要的东西

mysql常用命令

Mysql常用命令

用户

使用root登录

mysql -h 主机名(默认为localhost) -u 用户名(root) -p

显示当前用户下所有数据库

mysql> show databases;

退出登录

mysql> exit;

创建用户(登录root后)

CREATE USER '用户名’@'主机名' IDENTIFIED BY ‘密码’;

查看当前登录的用户

select current_user();

查看mysql下所有用户

select user from mysql.user; 

查看mysql用户的所有字段

desc mysql.user;

销毁用户

drop user 用户名@主机名

分配权限

给某个用户分配权限
例如:grant all privileges on 数据库名称.* to wanghao@localhost; (为wanghao@localhost分配某个数据库下所有表的所有权限)

GRANT 权限  ON 数据库/表 TO ‘用户'@'主机名' [IDENTIFED BY '密码’]; 

让权限生效

flush privileges;

查看某用户拥有的权限

show grants for 用户名@主机名;

吊销权限

revoke 权限(多个权限逗号分开) on 数据库.表名 from ‘用户'@'主机名’;

重置某个用户登录密码

set password for ‘用户名’@‘主机名’ = password(‘新密码’);

数据库操作

以下内容可以参考:MySQL 教程 | 菜鸟教程

创建数据库

create database 数据库名称;

查看数据库

show databases;

删除数据库

drop database <数据库名>;

表操作

进入(使用)某个数据库

use 数据库名称;

查看当前数据库下的表

show tables;

查看某个表的所有字段

show columns from 表名;

查看某个表的描述

describe 表名称;

添加字段到第一个位置(默认添加到最后)

alter table 表名 add 字段名 INT(10) first;

修改表名

alter table 表名1 rename  表名2;

删除表内某字段

alter table 表名 drop 字段名;

修改表字段

alter table 表名 change 字段名1 字段名2 INT(10);

删除表

drop table 表名;

学Node必须掌握的Buffer和Stream

本文并不介绍 Buffer 和 Stream 使用的api,而是把对 Buffer 和 Stream 的理解带给大家。

之前发了篇文章《Nodejs核心模块简介》,笼统的介绍了下 Events模块、fs模块、stream的使用、http模块。

文章也在我的 github 博客上,欢迎订阅。

因为想学好 node 这些东西几乎是必须掌握的。这篇文章来说一下在 node 中几乎无处不在的 Buffer 和 Stream,什么是 Buffer 以及它和 Stream 到底什么关系? 马上揭晓。

Buffer

Buffer 是个类数组的对象,可以把它当做数组更好理解些,只不过里面存的是二进制数据。

先创建个 buffer 来看看它打印出来的样子:

const str = 'hello';
const buf = Buffer.from(str);

console.log(buf); // <Buffer 68 65 6c 6c 6f>

buf 里装的数据是字符串 hello,而 buf 的长度为 5 ,hello 的长度也是 5,所以 Buffer 中每个元素占一个字节(英文每个字母是一个字节)。

Buffer 是什么

从代码使用来看,Buffer 是类数组对象。
从内存角度看,Buffer 是在内存中开辟的一块区域。

Buffer 翻译过来是缓冲器,它主要用来暂存数据。

为了更好理解,用大白话把上面哪句翻译一下:Buffer 就是我们常坐的公交车就是数据,人上车就表示在 Buffer 中输入数据, 到站了人就下车,Buffer 里的数据就会输出

比如我们填写完表单,发送 http 请求到服务端,我们的数据就会暂存在 Buffer 中,服务端取的数据就是从暂存的 Buffer 里取的

Buffer 里装的是什么

废话!装的当然是数据了!没错 是数据...

<Buffer 68 65 6c 6c 6f>, 从刚才打印结果看,Buffer 显示的每个元素都是十六进制,但这只是为了方面查看,在控制台显示时是十六进制而已。。。

这里问问大家,数据在内存中是什么? 没错 是二进制,就是类似010101这样的东西。
为什么以二进制存在?因为电脑读写的数据都是电信号! 而电信号就是 01

好在我们可以把二进制、十进制、十六进制等进行转换,所以 Buffer 的每个元素看上去是十六进制,其实内存里存的都是二进制。

Buffer 的每个元素取值范围是多少呢?

  • 00 - ff(十六进制)
  • 0 - 255(十进制)
  • 00000000 - 11111111 (二进制)

255 这个数字肯定见过不少,比如 css 中的 rgba 每个值的范围是 0-255

而 255 其实跟 ASCII码 紧紧相连,回顾一下上面代码中打印 hello 的 buffer :<Buffer 68 65 6c 6c 6f>,然后对照下面这个 ASCII 表

3
再对照 buffer 里的每个元素:
2

是不是一下就明白了, 原来数据就是这样纸的呀。
当然,上面的 hello 使用的是国际统一码,是 0 - 127,后128个(128—255)称为扩展ASCII码,目前许多基于x86的系统都支持使用扩展(或“高”)ASCII。

Stream 与 Buffer

亲! 先把进制问题和ASCII码放一边,现在你的脑子里只有公交车

Stream 翻译过来就是流,流动的意思,《Nodejs核心模块简介》里也简单的介绍了这块内容,有兴趣可以看看。

既然 Stream 是流动的,那它跟 Buffer 到底有啥联系?

现在回到 人和公交车 的问题,上面说 是数据,公交车本身是 Buffer,Buffer 里有没有数据 取决于在不在里面。

注意,我上面说的是公交车本身,没有说公交车有没有在跑。

所以,聪明的你猜到了,跑着的公交车就是 Stream。

流的原则是:有源头、有终点、源头流向终点。
公交车就是这样,从起点发车,载着数据(人)往终点跑。

光说不行,来看一段代码:

const fileReadStream = fs.createReadStream('./logs/hello.log');
const fileWriteStream = fs.createWriteStream('./logs/hello2.log');

fileReadStream.on('data', chunk => {
  console.log(chunk);
  fileWriteStream.write(chunk); 
});

打印结果如下:
1

从图看出,hello.log数据很多,一个 buffer(公交车) 装不完, 打印了好几次的 chunk 才完成写入,每个 chunk 都是一个buffer,都填满了数据(人)。 流可以看成是公交司机,流的作用就是将 buffer 从一个地方(起点)运送的另一个地方(终点)。

总结

现在捋一下:

  1. Buffer 就是在内存中开辟一段空间,用来装数据的
  2. 数据都是二进制的,记住电信号(010101)
  3. Stream 的三大原则:有源头、有终点、源头流向终点。
  4. Stream 就像司机,它的作用就是将装着数据的 Buffer 开向终点

观察者模式

这是一种至关重要的行为设计模式,它定义了对象之间的一对多依赖关系,以便当一个对象(发布者)更改其状态时,所有其他依赖对象(订阅者)都将得到通知并自动更新。这也称为PubSub(发布者/订阅者)或Event Dispatcher / Listeners Pattern。发布者有时称为主题,订阅者有时称为观察者。

如果您已经使用addEventListener或jQuery编写事件处理代码,那么您可能已经有点熟悉此模式了。它也对反应式编程(RxJS)有影响。

在示例中,我们创建了一个简单的Subject类,该类具有用于Observer从订户集合中添加和删除类的对象的方法。另外,一种fireSubject类对象中的任何更改传播到订阅的Observers的方法。的Observer类,在另一方面,有其内部状态和基于从传播的改变更新其内部状态的方法Subject它已经预订。

Observer.js

class Subject {
  constructor() {
    this._observers = [];
  }

  subscribe(observer) {
    this._observers.push(observer);
  }

  unsubscribe(observer) {
    this._observers = this._observers.filter(obs => observer !== obs);
  }

  fire(change) {
    this._observers.forEach(observer => {
      observer.update(change);
    });
  }
}

class Observer {
  constructor(state) {
    this.state = state;
    this.initialState = state;
  }

  update(change) {
    let state = this.state;
    switch (change) {
      case 'INC':
        this.state = ++state;
        break;
      case 'DEC':
        this.state = --state;
        break;
      default:
        this.state = this.initialState;
    }
  }
}

// usage
const sub = new Subject();

const obs1 = new Observer(1);
const obs2 = new Observer(19);

sub.subscribe(obs1);
sub.subscribe(obs2);

sub.fire('INC');

console.log(obs1.state); // 2
console.log(obs2.state); // 20

npm常用命令

常用命令

npm i --registry=http://10.21.200.55:7001  #单次使用私有源

npm list -g --depth=0    #查看全局包列表,不考虑依赖

npm config ls -l    #查看npm配置

npm i [email protected]    #安装指定版本的包

npm update node-sass    #更新包

npm search node-sass    #搜索一个包是否存在

npm cache clean    #清理本地包缓存

npm init --yes    #快速创建一个package.json

npm install -g npm    #更新npm

npm publish <本地路径>    #发布包

npm outdated     #查看包的版本状态

npm root -g #查看全局包位置

常用工具

  • nrm 来管理镜像源
  • npm-check 来检测更新包

npm script

  • 通过npm script可以直接调用本地可执行文件
  • & 同时执行多个script; && 依次执行多个script
  • 钩子:
prebuild    build的前置钩子  
build  
postbuild    build的后置钩子

.npmrc

  • npm i 的时候会去读取本地项目的rc文件,没有就读~目录
  • 通过rc来解决安装包慢的问题
registry=https://registry.npm.taobao.org #设置淘宝镜像
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ #设置sass来源
phantomjs_cdnurl=http://npm.taobao.org/mirrors/phantomjs  
electron_mirror=http://npm.taobao.org/mirrors/electron/

npm link的使用

  • 在本地项目package1 下运行 npm link, 将本地包关联为全局包
  • 在本地项目package1 下运行 npm unlink, 取消关联为全局包
  • 在本地项目project 下运行 npm link package1,关联全局包刚刚link的package1
  • 在project中,可以通过require('package1')来使用未发布的本地包

npm设置代理

假设你的梯子在你本地机器上开启了一个第三方服务器 127.0.0.1:1080,只需按照下面的方法配置一下就能正常安装 node-sass 了

npm config set proxy http://127.0.0.1:1080
npm i node-sass

下载完成后删除 http 代理

npm config delete proxy

这样下来就能正常安装了

Web Workers 使用

单线程的JavaScript

从我接触 js 的时候,经常听到一句话:js 是单线程的。
单线程意味着 js 代码在执行时,只能按编码顺序从上到下执行(暂时抛开异步方法),如果遇到计算量大、耗时长的任务,用户就能感觉到卡顿。

JavaScript 的主线程主要作用是服务与 UI 构建,如果遇到繁重任务阻塞了 UI 主线程,就会感觉到卡。

一般我们解决的方法有两个:异步、使用Web Worker

异步暂时不讨论,这里主要说 Web Worker

Web Worker

既然主线程用于构建 UI,那么为了不阻塞 UI 构建,我们将繁重任务从主线程剥离出来放到其他线程里执行,不就OK了?

使用 Web Worker 可以将 js 运行在后台线程中,由于它独立于主线程,所以不会阻塞 UI 的构建

专用线程 和 共享线程

专用线程(Dedicated Web Worker) 和共享线程(Shared Web Worker)。
专用线程只能由创建它的单个脚本使用,共享线程可以由多个脚本使用。

需要注意的点

  1. 有同源限制
  2. 无法使用 window 对象
  3. 无法访问 DOM 节点

浏览器支持情况

目前统计,目前约有 97.48% 的浏览器支持专用线程

而共享线程只有大约 36.75% 的浏览器支持

所以我们在使用它们时,不要忘记判断浏览器是否支持:

if (Worker) {
    //...
}
if (ShareWorker) {
    //...
}

使用

由于共享线程浏览器支持情况较差,本章我们只介绍专用线程。

我们创建一个文件夹,并在里面创建 index.html 和 worker.js
目录如下:

.
├── index.html
└── worker.js

index.html 代码:

<input type="text" id="ipt" value="" />
<div id="result"></div>

<script>
    const ipt = document.querySelector('#ipt');
    const worker = new Worker('worker.js');
    
    ipt.onchange = function() {
      // 通过postMessage发送消息
      worker.postMessage({ number: this.value });
    };
    
    // 通过onmessage接收消息
    worker.onmessage = function(e) {
      document.querySelector('#result').innerHTML = e.data;
    };
</script>

worker.js 代码:

// 这里的 self 类似主线程中的 window
self.onmessage = function(e) {
  self.postMessage(e.data.number * 2);
};

处理错误

在主线程中处理错误:

    // 主线程
    worker.onerror = function () {
        // ...
    }
    
    // 主线程使用专用线程
    worker.onmessageerror = function () {
        // ...
    }

在专用线程中处理错误:

    // worker 线程
    onerror = function () {
    
    }

加载外部脚本

Web Worker 提供了 importScripts() 方法,能够将外部脚本文件加载到 Wroker 中。

importScript('script1.js')
importScript('script2.js')

// 上面写法等同于
importScript('script1.js','script2.js')

子线程

Worker 可以生成子 Worker,但有两点要注意:

  • 子 Worker 必须与父网页同源
  • 子 Worker 中的 URI 相对于父 Worker 所在的位置进行解析

嵌入式 Worker

目前没有一类标签可以使 Worker 的代码像 <script> 元素一样嵌入网页中,但我们可以通过 Blob() 将页面中的 Worker 代码进行解析。

<script id="worker" type="javascript/worker">
// 这段代码不会被 JS 引擎直接解析,因为类型是 'javascript/worker'

// 在这里写 Worker 线程的逻辑
</script>
<script>
    var workerScript = document.querySelector('#worker').textContent
    var blob = new Blob(workerScript, {type: "text/javascript"})
    var worker = new Worker(window.URL.createObjectURL(blob))
</script>

相关链接

koa+jwt实现token验证与刷新

JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

本文只讲Koa2 + jwt的使用,不了解JWT的话请到这里进行了解。

koa环境

要使用koa2+jwt需要先有个koa的空环境,搭环境比较麻烦,我直接使用koa起手式,这是我使用koa+typescript搭建的空环境,如果你也经常用koa写写小demo,可以点个star,方便~

安装koa-jwt

koa-jwt主要作用是控制哪些路由需要jwt验证,哪些接口不需要验证:

import  *  as  koaJwt  from  'koa-jwt';

//路由权限控制 除了path里的路径不需要验证token 其他都要
app.use(
	koaJwt({
		secret:  secret.sign
	}).unless({
		path: [/^\/login/, /^\/register/]
	})
);

上面代码中,除了登录、注册接口不需要jwt验证,其他请求都需要。

使用jsonwebtoken生成、验证token

执行npm install jsonwebtoken安装jsonwebtoken
相关代码:

import  *  as  jwt  from  'jsonwebtoken';

const secret = 'my_app_secret';
const payload = {user_name:'Jack', id:3, email: '[email protected]'};
const token = jwt.sign(payload, secret, { expiresIn:  '1h' });

上面代码中通过jwt.sign来生成一个token,
参数意义:

  • payload:载体,一般把用户信息作为载体来生成token
  • secret:秘钥,可以是字符串也可以是文件
  • expiresIn:过期时间 1h表示一小时

在登录中返回token

import * as crypto from 'crypto';
import * as jwt from 'jsonwebtoken';

async login(ctx){

  //从数据库中查找对应用户
  const user = await userRespository.findOne({
    where: {
      name: user.name
    }
  });

  //密码加密
  const psdMd5 = crypto
    .createHash('md5')
    .update(user.password)
    .digest('hex');

  //比较密码的md5值是否一致 若一致则生成token并返回给前端
  if (user.password === psdMd5) {
    //生成token
    token = jwt.sign(user, secret, { expiresIn:  '1h' });
    //响应到前端
    ctx.body = {
      token
    }
  }

}

前端拦截器

前端通过登录拿到返回过来的token,可以将它存在localStorage里,然后再以后的请求中把token放在请求头的Authorization里带给服务端。
这里以axios请求为例,在发送请求时,通过请求拦截器把token塞到header里:

//请求拦截器
axios.interceptors.request.use(function(config) {
    //从localStorage里取出token
    const token = localStorage.getItem('tokenName');
    //把token塞入Authorization里
    config.headers.Authorization = `Bearer ${token}`;
    
    return config;
  },
  function(error) {
    // Do something with request error
    return Promise.reject(error);
  }
);

服务端处理前端发送过来的Token

前端发送请求携带token,后端需要判断以下几点:

  1. token是否正确,不正确则返回错误
  2. token是否过期,过期则刷新token 或返回401表示需要从新登录

关于上面两点,需要在后端写一个中间件来完成:

app.use((ctx, next) => {
  if (ctx.header && ctx.header.authorization) {
    const parts = ctx.header.authorization.split(' ');
    if (parts.length === 2) {
      //取出token
      const scheme = parts[0];
      const token = parts[1];
      
      if (/^Bearer$/i.test(scheme)) {
        try {
          //jwt.verify方法验证token是否有效
          jwt.verify(token, secret.sign, {
            complete: true
          });
        } catch (error) {
          //token过期 生成新的token
          const newToken = getToken(user);
          //将新token放入Authorization中返回给前端
          ctx.res.setHeader('Authorization', newToken);
        }
      }
    }
  }

  return next().catch(err => {
    if (err.status === 401) {
      ctx.status = 401;
      ctx.body =
        'Protected resource, use Authorization header to get access\n';
    } else {
      throw err;
    }});
 });   

上面中间件是需要验证token时都需要走这里,可以理解为拦截器,在这个拦截器中处理判断token是否正确及是否过期,并作出相应处理。

后端刷新token 前端需要更新token

后端更换新token后,前端也需要获取新token 这样请求才不会报错。
由于后端更新的token是在响应头里,所以前端需要在响应拦截器中获取新token。
依然以axios为例:

//响应拦截器
axios.interceptors.response.use(function(response) {
    //获取更新的token
    const { authorization } = response.headers;
    //如果token存在则存在localStorage
    authorization && localStorage.setItem('tokenName', authorization);
    return response;
  },
  function(error) {
    if (error.response) {
      const { status } = error.response;
      //如果401或405则到登录页
      if (status == 401 || status == 405) {
        history.push('/login');
      }
    }
    return Promise.reject(error);
  }
);

Mysql入门第六课《一对一、一对多、多对多》

原在我的 Github 上,欢迎订阅。

其他文章:

前言

数据源于生活,数据之间的关系也是从生活里映射过来的。

比如:一个老师可以教很多学生,一个学校有很多老师,一个人只能有一个身份证等等。

总结下来,所有的数据之间有三种关系:

  • 一对一
  • 一对多
  • 多对多

一对一

一对一就是,我只有你,你只有我。

比如:

  • 人与身份证的关系。
  • 商品与商品信息的关系。
  • QQ号与QQ空间的关系。

即使是一对一也要明确主从关系,比如人与身份证的关系,人是主,身份证是从,因为没有人哪里来的身份证呢?再比如 没有商品哪来的商品信息呢?

这里用人与身份证的关系来举例,我们新建 person 表:

CREATE TABLE person
	id INT UNSIGNED PRIMARY KEY auto_increment,
	name CHAR(30) DEFAULT NULL
)

再建id_card表,并通过外键person_id与主表关联:

CREATE TABLE id_card(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	card_no VARCHAR(20) NOT NULL,
	person_id INT UNSIGNED,
	CONSTRAINT id_card_person FOREIGN KEY (person_id) REFERENCES person(id)
)

然后添加点数据:
person 表:

id name
1 小A
2 小B
3 小C

id_card 表:

id card_no person_id
1 34xxxxxxxxxxxx0912 1
2 34xxxxxxxxxxxx1108 2
2 34xxxxxxxxxxxx0422 3

然后查询所有人的姓名和对应的身份证号:

SELECT a.name,b.card_no 
FROM person a LEFT JOIN id_card b
ON a.id=b.person_id;

查询结果:

name card_no
小A 34xxxxxxxxxxxx0912
小B 34xxxxxxxxxxxx1108
小C 34xxxxxxxxxxxx0422

一对多

一对多,即主表的一个数据可以有多个从表的数据。
举个例子:班级和学生,一个班级有多个学生,主表是班级,从表是学生。

我们创建班级表class:

CREATE TABLE class(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	class_name VARCHAR(30) COMMENT '班级名'
);

再创建学生表student

CREATE TABLE student(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	student_name CHAR(30) COMMENT '学生名',
	class_id INT UNSIGNED DEFAULT NULL COMMENT '班级id',
	CONSTRAINT student_class FOREIGN KEY (class_id) REFERENCES class(id)
)

创建学生表时,每个学生都有一个班级,我们用class_id作为表示,然后和class表建立外键约束(这一步也可不要,看开发情况而定)。

添加数据后

class 表:

id class_name
1 一班
2 二班
3 三班

student 表:

id student_name class_id
1 李安安 1
2 陈小帅 1
3 张力克 3

然后查一下所有班级和班级的学生数量:

SELECT c.class_name ,COUNT(s.student_name) student_num 
FROM class c LEFT JOIN student s ON c.id=s.class_id 
GROUP BY c.class_name;

结果:

id student_num
一班 2
三班 1
二班 0

多对多

三种关系里,多对多是最复杂的。
多对多举例:一篇文章有可以有多种分类,一种分类可以有多篇文章。

由于多对多的关系比较复杂,我们一般会添加一张中间表专门来记录他们的关系。

新建tag(文章分类)表:

CREATE TABLE tag(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	tag_name VARCHAR(50) NOT NULL
)

新建article(文章)表:

CREATE TABLE article(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	title VARCHAR(100) NOT NULL
)

再建立tagarticle的关系表:

CREATE TABLE tag_article(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	tag_id INT UNSIGNED DEFAULT NULL,
	article_id INT UNSIGNED DEFAULT NULL,
	FOREIGN KEY(tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
	FOREIGN KEY(article_id) REFERENCES article(id) ON DELETE CASCADE ON UPDATE CASCADE,
	UNIQUE(tag_id,article_id)
)

然后添加了些数据后:
tag表:

id tag_name
1 文学
2 科技
3 编程

article表:

id title
1 青青河边草
2 我国在航空领域取得重大成就
3 PHP是世界上最好的语言

tag_article关系表:

id tag_id article_id
1 1 1
2 2 2
3 3 3
4 1 2

开发中通过tag_article关系表来进行查询
比如查询tag_id=1的所有文章:

SELECT a.title 
FROM article a INNER JOIN tag_article t 
ON a.id=t.article_id 
WHERE tag_id=1

查询结果:

title
青青河边草
我国在航空领域取得重大成就

总结

一对一、一对多、多对多的关系很好理解,在开发中只要分清关系类型,分清主表从表就能清晰的对数据有很好的了解。

其实学到这里,基本已经算入门了,后面我会对mysql里的常用函数和一些字句进行学习,也会写成文章分享给大家。

React和Immutable天生的一对

前言

我们慢慢脱离了jQuery的**,迎接有ReactVue新Angular的时代。

React的出现改了前端的革命。组件化、虚拟dom等**在它身上体现的淋漓尽致。

我们在使用任何框架的时,避免不了出现优化的问题,毕竟框架为我们提供的是方便,在方便的同时如何使你的项目性能更好,效率更高是我们程序员一生解决不完的bug。

Immutable

Immutable也是Facebook旗下和React同时期出现的一个库,只是React太火了,导致当时没多少人了解Immutable。

Immutable:Immutable collections for JavaScript

翻译过来就是:JavaScript的不可变集合

而JavaScript里的对象正好相反,都是可变的(Mutable)

下面我们来解释一下不可变

现在我们有一个数据是这样的:

const data = {
    name: 'Jack',
    age: 25
}

我们修改它的属性时一般是这样:

data.name = 'Tom'

这时你会发现,data(数据源)被改变了,因为JavaScript使用的是引用赋值,这样做虽然可以节省内存,但是当应用变得复杂后,会存在很大的隐患。
为了不让data改变,一般会使用shallowCopy(浅拷贝)或 deepCopy(深拷贝)创建副本来避免被修改,但这样做造成了 CPU 和内存的浪费。

Immutable可以解决这个问题。

Immutable原理

刚才说Immutable可以解决这个问题,意思是:Immutable可以创建新的数据副本,并且不会造成CPU和内存的浪费。

那么Immutable是通过什么玩意来实现这么牛叉的功能的?

这里先写个列子:

import { Map } from 'immutable';

const data1 = Map({
    name: 'Jack',
    age: 25
});

//将name修改为Tom
const data2 = data1.set('name', 'Tom');

代码中,我们使用ImmutableMap创建一个Immutable Data,然后修改 data1 中的 name = 'Tom',此时打印 data1 却还是之前的数据。

Immutable Data 一旦创建,就不能再被修改,对Immutable Data的任何操作(增删改)都会创建一个新的Immutable对象,它的实现原理是持久化数据结构(Persistent Data Structure),为了避免深拷贝把所有节点都复制一遍带来的性能消耗,Immutable 使用了结构共享(Structural Sharing)。

结构共享: 即数据的对象树中,一个节点的数据发生变化后,只修改这个节点本身和受它影响的父级节点,其他节点仍然共享,通过下面这个图可以了解的比较清楚
image

Immutable 的数据不可变性,给经常产生变化的对象带了福音,第一眼想到的是 React 的 state,毕竟我们在使用React开发项目时,最最最经常操作的不就是状态吗?

React优化

React官方也建议把 this.state当成 immutable的,那么我们来写个简单的例子:

使用 Immutable 之前:

class Demo extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            data: {
                count: 0
            }
        }
    }
    
    handerAdd = () => {
        this.setState({
            count: this.state.data.count+1
        })
    }
    
    //...
}

使用Immutable之后:

import { Map } from 'immutable'
    
    //...
    constructor(props){
        super(props);
        this.state = {
            data: Map({
                count: 0
            })
        }
    }

    handerAdd = () =>{
        this.setState(({data})=>({
            data: data.updata('count',c => c+1)
        }))
    }

优化React的render机制

React 组件更新时,会调用 componentShouldUpdate 声明周期方法,这个方法默认会返回true,即使你的state和props没有发生变化也会重新render,这往往会带来比较大的开销。

React 提供了一个 PureComponent,会在componentShouldUpdate里执行一次浅比较来减少不必要的render(其实卵用不大)。

现在有了 Immutable,我们使用它来帮我们彻底解决这个问题。 Immutable提供了 is 方法,来比较两个 Immutable对象是否完全相同,我们用这个来实现:

import { is } from 'immutable';

shouldComponentUpdate(nextProps, nextState){
    return !(this.props === nextProps || is(this.props, nextProps)) ||
         !(this.state === nextState || is(this.state, nextState));
}

使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:

image

这样就可以大大的减少组件不必要的 render 啦~

结束语

本文简单介绍了Immutable 的使用,以及在 React 中使用 Immutable 如何做优化,还有其他功能没有介绍,比如 Cursor 等,Immutable 的应用还有很多,只要是跟数据操作有关的,都可以使用它来提高性能。

状态模式

介绍

这是一种行为设计模式,它允许对象根据对其内部状态的更改来更改其行为。状态模式类返回的对象似乎更改了其类。它为一组有限的对象提供特定于状态的逻辑,其中每种对象类型代表一种特定的状态。

状态模式的核心是:

  • 一个对象有状态变化
  • 每次状态变化都会触发一个逻辑

示例

状态模式需要一个主题类 Context 用来作为状态的载体,这个类记用于 get 和 set 状态;
状态模式需要状态类 State ,里面包含具体的状态变化的逻辑;

在生活中,红绿灯就是状态模式的体现,灯颜色的变化会触发汽车或行人的行为
先定义 State 类,它用于红绿灯颜色变化和相应的逻辑处理:

class State {
  constructor(color){
    this.color = color;
  }
  
  /**
   * 用于处理切换逻辑
   */
  handle(context){
    // 逻辑处理
    console.log(`跳到:${this.color} 灯`)
    // 调用 Context 的 setState 来改变状态
    context.setState(this)
  }
}

再看看 Context 类,它抽象出来 用于获取和设置当前 state

class Context {
  constructor() {
    this.state = null;
  }

  getState() {
    return this.state
  }

  setState(state) {
    this.state = state;
  }
}

将状态抽离出来保存在 Context 中,具体的状态逻辑在 State 中,来看看调用:

// use
const context = new Context();

// 红灯
const redLight = new State('红灯')
redLight.handle(context) // 跳到红灯了

// 绿灯
const redLight = new State('绿灯')
redLight.handle(context) // 跳到绿灯了

// 黄灯
const redLight = new State('黄灯')
redLight.handle(context) // 跳到黄灯了

Object.defineProperty 与 Proxy

Object.definedProperty

在 vue2.x 版本中使用 Object.definedProperty 来劫持数据,实现数据双向绑定。我们来实现一个简单的数据劫持:

function observer(obj) {
  if (typeof obj === 'object') {
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        listener(obj, key);
      }
    }
  }
}

function listener(obj, key) {
  let curValue = obj[key];
  observer(curValue); // 如果curValue是对象,进入递归

  Object.defineProperty(obj, key, {
    get() {
      console.log('get-->', curValue);
      return curValue;
    },
    set(newVal) {
      console.log('set-->', newVal);
      curValue = newVal;
    }
  });
}

上面写了个简单版的数据劫持,现在我们来测试一下:

const obj = {
  name: 'Jack',
  age: 20
};

observer(obj)

现在我们将 obj.name 改为 Tom:

灰常好,set 方法执行了,我们在更改数据前,劫持了数据。

我们再看看获取 name:

灰常好,get 方法也执行了,我们在获取数据前,劫持了数据。

看上去很美好,但 Object.definedProperty 并不是完美的,它在有些情况下无法劫持数据:

  • 对删除和新增的属性无法监听到
  • 数组的变化无法监听,虽然可以触发 get,但无法触发 set 的劫持
  • 如果对象过大、层级过深,那么遍历的时间会更久,引发性能问题

Proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

语法

const p  = new Proxy(target, handler)
  • target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

还记得 Object.defineProperty 的语法吗:

Object.defineProperty(obj, key, options);

从语法上就能发现一个最大的不同点:Object.defineProperty 监听的是对象的属性,而 Proxy 监听的是整个对象

所以我们不需要遍历对象,而是直接监听对象:

const obj = {
  arr: []
}

const handler = {
  get(target, key, receiver){
    console.log('get->', target[key])
    if(typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return target[key]
  },
  set(target, key, value){
    console.log('set->', key, value)
    return Reflect.set(target, key, value)
  }
}
const p = new Proxy(obj, handler)

上面我们监听的是一个空的对象,我们直接添加属性看看:

  p.name = 'Jack';

这里注意,我们代理的是 obj,返回的是 p,所以要对 p 进行操作:

真好,set 触发了。

我们再看看获取 name:

真好,get 也触发了。

我们再看看数组:

都可以非常好的监听到 get 、set,太强大了

关于 Proxy 的更多用法和说明,请看 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

ES6设计模式

es6-design-patterns

在EcmaScript 6中实现的软件设计模式


在这个项目中,我们介绍EcmaScript 6中的软件设计模式。

图像由StarUMLstaruml-design-patterns项目中的.mdj文件生成。

内容

创作模式

抽象工厂

抽象工厂

'use strict';

class AbstractFactory {
    constructor() {
    }

    createProductA (product) {
    }

    createProductB (product) {
    }
}

class ConcreteFactory1 extends AbstractFactory {
    constructor() {
        super()
        facade.log("ConcreteFactory1 class created");
    }

    createProductA (product) {
        facade.log('ConcreteFactory1 createProductA')
        return new ProductA1()
    }

    createProductB (product) {
        facade.log('ConcreteFactory1 createProductB')
        return new ProductB1()
    }
}

class ConcreteFactory2 extends AbstractFactory {
    constructor() {
        super()
        facade.log("ConcreteFactory2 class created");
    }

    createProductA (product) {
        facade.log('ConcreteFactory2 createProductA')
        return new ProductA2()
    }

    createProductB (product) {
        facade.log('ConcreteFactory2 createProductB')
        return new ProductB2()
    }
}

class AbstractProductA {
    constructor() {
    }
}

class AbstractProductB {
    constructor() {
    }
}


class ProductA1 extends AbstractProductA {
    constructor() {
        super()
        facade.log('ProductA1 created')
    }
}

class ProductA2 extends AbstractProductA {
    constructor() {
        super()
        facade.log('ProductA2 created')
    }
}

class ProductB1 extends AbstractProductB {
    constructor() {
        super()
        facade.log('ProductB1 created')
    }
}

class ProductB2 extends AbstractProductB {
    constructor() {
        super()
        facade.log('ProductB2 created')
    }
}

function init_AbstractFactory() {
    var factory1 = new ConcreteFactory1()
    var productB1 = factory1.createProductB()
    
    var factory2 = new ConcreteFactory2()
    var productA2 = factory2.createProductA()
}

建造者

建造者

建造者

'use strict';

class Director {
    constructor() {
        this.structure = ['Maze','Wall','Door'];
        facade.log("Director class created");
    }

    Construct (){
        for(var all in this.structure){
            let builder = new ConcreteBuilder()
            builder.BuildPart(this.structure[all]);
            builder.GetResult()
        }
    }
}

class Builder {
    constructor() {
    }

    BuildPart (){
    }
}

class ConcreteBuilder extends Builder {
    constructor() {
        super()
        facade.log("ConcreteBuilder class created");
    }

    BuildPart (rawmaterial){
        facade.log("ConcreteBuilder BuildPart()");
        var material = rawmaterial
        this.product = new Product(material)
    }

    GetResult (){
        facade.log(JSON.stringify(this.product))
        return this.product
    }
}

class Product {
    constructor(material) {
        facade.log("Product class created");
        this.data = material
    }
}

function init_Builder() {
    let director = new Director()
    director.Construct()
}

工厂方法

工厂方法

'use strict';

class Productt {
    constructor() {
    }
}

class ConcreteProduct extends Productt {
    constructor() {
        super()
        facade.log('ConcreteProduct created')
    }
}

class Creator {
    constructor() {
    }

    FactoryMethod (){

    }

    AnOperation (){
        facade.log("AnOperation()")
        this.product = this.FactoryMethod()
        facade.log(this.product instanceof ConcreteProduct)
    }
}

class ConcreteCreator extends Creator {

    constructor() {
        super()
        facade.log('ConcreteCreator created')
    }

    FactoryMethod (){
        return new ConcreteProduct();
    }
}

function init_FactoryMethod() {
    var factory = new ConcreteCreator()
    factory.AnOperation()
}

原型模式

原型

'use strict';

class Prototype {
    constructor(prototype) {
    }

    Clone (){
    }
}

class ConcretePrototype1 extends Prototype {
    constructor() {
        facade.log("ConcretePrototype1 created");
        super()
        this.feature = "feature 1"
    }

    setFeature(key, val) {
        this[key] = val
    }

    Clone (){
        facade.log('custom cloning function')
        let clone = new ConcretePrototype1()
        let keys = Object.keys(this)

        keys.forEach(k => clone.setFeature(k, this[k]))

        facade.log("ConcretePrototype1 cloned");
        return clone;
    }
}

class ConcretePrototype2 extends Prototype {
    constructor() {
        facade.log("ConcretePrototype2 created");
        super()
    }

    Clone (){
        facade.log("ConcretePrototype2 cloned");
        return clone;
    }
}

function init_Prototype () {
    var proto1 = new ConcretePrototype1()
    proto1.setFeature('feature', "feature 22")
    var clone1 = proto1.Clone()
    facade.log(clone1.feature)
}

单例模式

辛格尔顿

'use strict';

let _singleton = null

class Singleton {
    constructor (data) {
        if(!_singleton) {
            this.data = data
            _singleton = this
        }
        else
            return _singleton
        facade.log("Singleton class created")
    }

    SingletonOperation () {
        facade.log('SingletonOperation')
    }

    GetSingletonData () {
        return this.data
    }
}

function init_Singleton() {
    var singleton1 = new Singleton("data1")
    var singleton2 = new Singleton("data2")
    facade.log(singleton1.GetSingletonData())
    facade.log(singleton2.GetSingletonData())
    facade.log(singleton1 instanceof Singleton)
    facade.log(singleton2 instanceof Singleton)
    facade.log(singleton1 === singleton2)
}

结构模式

适配器模式

适配器多重继承

'use strict';

class Target {
    constructor(type) {
        let result

        switch(type) {
            case 'adapter':
                result = new Adapter()
                break
            default:
                result = null
        }
        return result
    }

    Request() {
    }
}


class Adaptee {
    constructor() {
        facade.log('Adaptee created')
    }

    SpecificRequest () {
        facade.log('Adaptee request')
    }
}


class Adapter extends Adaptee {

    constructor() {
        super()
        facade.log('Adapter created')
    }

    Request (){
        return this.SpecificRequest()
    }
}


function init_Adapter() {
    var f = new Target("adapter")
    f.Request()
}

桥接模式

桥

'use strict';

class Abstraction {
    constructor() {
    }

    Operation (){
        this.imp.OperationImp();
    }
}


class RefinedAbstraction extends Abstraction {
    constructor() {
        super()
        facade.log('RefinedAbstraction created')
    }

    setImp (imp) {
        this.imp = imp
    }

}


class Implementor {
    constructor() {
    }

    OperationImp (){
    }
}


class ConcreteImplementorA extends Implementor {
    constructor() {
        super()
        facade.log('ConcreteImplementorA created')
    }

    OperationImp (){
        facade.log('ConcreteImplementorA OperationImp')
    }
}

class ConcreteImplementorB extends Implementor {
    constructor() {
        super()
        facade.log('ConcreteImplementorB created')
    }

    OperationImp (){
        facade.log('ConcreteImplementorB OperationImp')
    }
}

function init_Bridge() {
    var abstraction = new RefinedAbstraction()
    var state = Math.floor(Math.random()*2)
    if(state)
        abstraction.setImp(new ConcreteImplementorA())
    else
        abstraction.setImp(new ConcreteImplementorB())

    abstraction.Operation()
}

组合模式

Composite

'use strict';

class Component {
    constructor() {
    }

    Operation (){
    }

    Add (Component){
    }

    Remove (Component){
    }

    GetChild (key){
    }
}

class Leaf extends Component {
    constructor(name) {
        super()
        this.name = name
        facade.log('Leaf created')
    }

    Operation (){
        facade.log(this.name)
    }
}

class Composite extends Component {
    constructor(name) {
        super()
        this.name = name
        this.children = []
        facade.log('Composite created')
    }

    Operation (){
        facade.log('Composite Operation for: ' + this.name)
        for(var i in this.children)
            this.children[i].Operation()
    }

    Add (Component){
        this.children.push(Component)
    }

    Remove (Component){
        for(var i in this.children)
            if(this.children[i] === Component)
                this.children.splice(i, 1)
    }

    GetChild (key){
        return this.children[key]
    }
}

function init_Composite() {
    var composite1 = new Composite('C1')
    composite1.Add(new Leaf('L1'))
    composite1.Add(new Leaf('L2'))
    var composite2 = new Composite('C2')
    composite2.Add(composite1)
    composite1.GetChild(1).Operation()
    composite2.Operation()
}

装饰器模式

Decorator

'use strict';

class Componentt {
    constructor() {
    }

    Operation (){
    }
}

class ConcreteComponent extends Componentt {
    constructor() {
        super()
        facade.log('ConcreteComponent created')
    }

    Operation (){
        facade.log('o o')
    }
}

class Decorator extends Componentt {
    constructor(component) {
        super()
        this.component = component
        facade.log('Decorator created')
    }

    Operation (){
        this.component.Operation()
    }
}

class ConcreteDecoratorA extends Decorator {
    constructor(component, sign) {
        super(component)
        this.addedState = sign
        facade.log('ConcreteDecoratorA created')
    }

    Operation (){
        super.Operation()
        facade.log(this.addedState)
    }
}

class ConcreteDecoratorB extends Decorator {
    constructor(component, sign) {
        super(component)
        this.addedState = sign
        facade.log('ConcreteDecoratorA created')
    }

    Operation (){
        super.Operation()
        facade.log(this.addedState + this.addedState + this.addedState + this.addedState + this.addedState)
    }

    AddedBehavior  (){
        this.Operation()
        facade.log('|........|')
    }
}

function init_Decorator() {
    var component = new ConcreteComponent()
    var decoratorA = new ConcreteDecoratorA(component, '!!!')
    var decoratorB = new ConcreteDecoratorB(component, '.')
    facade.log('component: ')
    component.Operation()
    facade.log('decoratorA: ')
    decoratorA.Operation()
    facade.log('decoratorB: ')
    decoratorB.AddedBehavior()
}

外观模式

正面

'use strict';

class Facade {
    constructor () {
        this.log("Facade class created");
        this.htmlid = null;
    }

    log (text) {
        if(typeof this.htmlid === null){
            console.log(text);
        }
        else{
            $('#'+this.htmlid).append(text+'</br>');
        }
    }

    erase () {
        $("#"+this.htmlid).html('');
    }

    test_dp (dp) {
        switch(dp){
            case "Facade":
                this.htmlid = "test_Facade"
                this.erase()
                this.log("This is the Facade")
                break
            case "AbstractFactory": 
                this.htmlid = "test_AbstractFactory"
                this.erase()
                init_AbstractFactory()
                break
            case "Builder":
                this.htmlid = "test_Builder"
                this.erase()
                init_Builder()
                break;
            case "Factory":
                this.htmlid = "test_Factory"
                this.erase()
                init_FactoryMethod()
                break
            case "Prototype":
                this.htmlid = "test_Prototype"
                this.erase()
                init_Prototype()
                break
            case "Singleton":
                this.htmlid = "test_Singleton"
                this.erase()
                init_Singleton()
                break
            case "Adapter":
                this.htmlid = "test_Adapter"
                this.erase()
                init_Adapter()
                break
            case "Bridge":
                this.htmlid = "test_Bridge"
                this.erase()
                init_Bridge()
                break
            case "Composite":
                this.htmlid = "test_Composite"
                this.erase()
                init_Composite()
                break
            case "Decorator":
                this.htmlid = "test_Decorator"
                this.erase()
                init_Decorator()
                break
            case "Flyweight":
                this.htmlid = "test_Flyweight"
                this.erase()
                init_Flyweight()
                break
            case "Proxy":
                this.htmlid = "test_Proxy"
                this.erase()
                init_Proxy()
                break
            case "ChainofResponsibility":
                this.htmlid = "test_ChainofResponsibility"
                this.erase()
                init_ChainofResponsibility()
                break
            case "Command":
                this.htmlid = "test_Command"
                this.erase()
                init_Command()
                break
            case "Interpreter":
                this.htmlid = "test_Interpreter"
                this.erase()
                init_Interpreter()
                break
            case "Iterator":
                this.htmlid = "test_Iterator"
                this.erase()
                init_Iterator()
                break
            case "Mediator":
                this.htmlid = "test_Mediator"
                this.erase()
                init_Mediator()
                break
            case "Memento":
                this.htmlid = "test_Memento"
                this.erase()
                init_Memento()
                break
            case "Observer":
                this.htmlid = "test_Observer"
                this.erase()
                init_Observer()
                break
            case "State":
                this.htmlid = "test_State"
                this.erase()
                init_State()
                break
            case "Strategy":
                this.htmlid = "test_Strategy"
                this.erase()
                init_Strategy()
                break
            case "TemplateMethod":
                this.htmlid = "test_TemplateMethod"
                this.erase()
                init_TemplateMethod()
                break
            case "Visitor":
                this.htmlid = "test_Visitor";
                this.erase();
                init_Visitor()
                break;
            default:
                console.log("nothing to test");
        }
    }
}

享元模式

轻量级

'use strict';

class FlyweightFactory {
    constructor() {
        this.flyweights = {};
        facade.log('FlyweightFactory created')
    }

    GetFlyweight (key){
        if(this.flyweights[key]){
            return this.flyweights[key];
        }
        else{
            this.flyweights[key] = new ConcreteFlyweight(key);
            return this.flyweights[key];
        }
    }

    CreateGibberish (keys) {
        return new UnsharedConcreteFlyweight(keys, this)
    }
}

class Flyweight {
    constructor() {
    }

    Operation (extrinsicState){
    }
}


class ConcreteFlyweight extends Flyweight {
   constructor(key) {
        super()
        this.intrinsicState = key
        facade.log('ConcreteFlyweight created')
    }

    Operation (extrinsicState){
        return extrinsicState + this.intrinsicState
    }
}

class UnsharedConcreteFlyweight extends Flyweight {
    constructor(keys, flyweights) {
        super()
        this.flyweights = flyweights
        this.keys = keys
        facade.log('UnsharedConcreteFlyweight created')
    }

    Operation (extrinsicState){
        var key, word = ''

        for(var i = 0; i < extrinsicState; i++) {
            //random key
            key = this.keys[Math.floor(Math.random() * (this.keys.length))]
            word = this.flyweights.GetFlyweight(key).Operation(word)
        }
        facade.log('UnsharedConcreteFlyweight Operation: ')
        facade.log(word)
    }
}

function init_Flyweight() {
    var flyweights = new FlyweightFactory()
    var gibberish = flyweights.CreateGibberish(['-', '+', '*'])
    gibberish.Operation(5)
    gibberish.Operation(10)
}

代理模式

Proxy

'use strict';

class Subject {
    constructor() {
    }

    Request (){
    }
}

class RealSubject extends Subject {
    constructor() {
        super()
        facade.log('RealSubject created')
    }

    Request (){
        facade.log('RealSubject handles request')
    }
}

class Proxy extends Subject {
    constructor() {
        super()
        facade.log('Proxy created')
    }

    Request (){
        this.realSubject = new RealSubject();
        this.realSubject.Request();
    }
}

function init_Proxy() {
    var proxy = new Proxy()
    proxy.Request()
}

行为模式

责任链模式

责任链

'use strict';
class Handler {
    constructor() {
    }
    HandleRequest() {
    }
}

class ConcreteHandler1 extends Handler {
    constructor() {
        super()
        facade.log('ConcreteHandler1 created')
    }

    setSuccessor (successor) {
        this.successor = successor
    }

    HandleRequest(request) {
        if (request === 'run')
            facade.log('ConcreteHandler1 has handled the request')
        else {
            facade.log('ConcreteHandler1 calls his successor')
            this.successor.HandleRequest(request)
        }
    }
}

class ConcreteHandler2 extends Handler {
    constructor() {
        super()
        facade.log('ConcreteHandler2 created')
    }

    HandleRequest(request) {
        facade.log('ConcreteHandler2 has handled the request')
    }
}

function init_ChainofResponsibility() {
    let handle1 = new ConcreteHandler1()
    let handle2 = new ConcreteHandler2()
    handle1.setSuccessor(handle2)
    handle1.HandleRequest('run')
    handle1.HandleRequest('stay')

}

命令模式

Command

Command Sequence

'use strict';

class Invoker {
    constructor() {
        facade.log('Invoker created')
    }

    StoreCommand(command) {
        this.command = command
    }
}

class Command {
    constructor() {
    }

    Execute() {
    }
}

class ConcreteCommand extends Command {
    constructor(receiver, state) {
        super()
        this.receiver = receiver
        facade.log('ConcreteCommand created')
    }
    
    Execute() {
        facade.log('ConcreteCommand Execute')
        this.receiver.Action();
    }
}

class Receiver {
    constructor() {
        facade.log('Receiver created')
    }

    Action() {
        facade.log('Receiver Action')
    }
}


function init_Command() {
    var invoker = new Invoker()
    var receiver = new Receiver()
    var command = new ConcreteCommand(receiver)
    invoker.StoreCommand(command)
    invoker.command.Execute()
}

解释器模式

口译员

'use strict';

class Context {
    constructor(input) {
        this.input = input
        this.index = 0
        this.output = null
    }

    Lookup(expr) {
        //return this.
    }
}

class AbstractExpression {
    constructor() {
    }

    Interpret (context){
    }
}

class TerminalExpression extends AbstractExpression {
    constructor(name) {
        super()
        this.name = name
        facade.log('TerminalExpression created')
    }

	Interpret (context){
    }
}

class NonterminalExpression extends AbstractExpression {
    constructor() {
        super()
        this.name = '+'
        facade.log('NonterminalExpression created')
    }

	Interpret (context){

        return terminal1.Interpret() + terminal2
    }
}

function init_Interpreter() {
    //var context = new Context('A+B+A')
    facade.log('Not implemented')
}

迭代器模式

Iterator

'use strict';

class Iterator {
    constructor() {
    }

    First (){
    }

    Next (){
    }

    IsDone (){
    }

    CurrentItem (){
    }
}

class ConcreteIterator extends Iterator {
    constructor(aggregate) {
        super()
        facade.log('ConcreteIterator created')
        this.index = 0
        this.aggregate = aggregate
    }

    First (){
        return this.aggregate.list[0]
    }

    Next (){
        this.index += 2
        return this.aggregate.list[this.index]
    }

    CurrentItem (){
        return this.aggregate.list[this.index]
    }
}

class Aggregate {
    constructor() {
    }

    CreateIterator (){
    }
}

class ConcreteAggregate extends Aggregate {
    constructor(list) {
        super()
        this.list = list
        facade.log('ConcreteAggregate created')
    }

	CreateIterator (){
		this.iterator = new ConcreteIterator(this);
    }
}

function init_Iterator() {
    var aggregate = new ConcreteAggregate([0,1,2,3,4,5,6,7])
    aggregate.CreateIterator()
    facade.log(aggregate.iterator.First())
    facade.log(aggregate.iterator.Next())
    facade.log(aggregate.iterator.CurrentItem())
}

中介者模式

Mediator

'use strict';

class Mediator {
    constructor() {
    }

    ColleagueChanged(colleague) {

    }
}

class ConcreteMediator extends Mediator {
    constructor() {
        super()
        facade.log('ConcreteMediator created')
        this.colleague1 = new ConcreteColleague1(this)
        this.colleague2 = new ConcreteColleague2(this)
    }

    ColleagueChanged(colleague) {
        switch(colleague) {
            case this.colleague1:
                facade.log('ConcreteColleague1 has Changed -> change ConcreteColleague2.feature: ')
                this.colleague2.setFeature('new feature 2')
                break
            case this.colleague2:
                facade.log('ConcreteColleague2 has Changed, but do nothing')
                break
            default:
                facade.log('Do nothing')
        }
    }
}

class Colleague {
    constructor() {
    }

    Changed() {
        this.mediator.ColleagueChanged(this)
    }
}

class ConcreteColleague1 extends Colleague {
    constructor(mediator) {
        super()
        facade.log('ConcreteColleague1 created')
        this.mediator = mediator
        this.feature = "feature 1"
    }

    setFeature(feature) {
        facade.log('ConcreteColleague1 Feature has changed from ' + this.feature + ' to ' + feature)
        this.feature = feature
        this.Changed()
    }
}
class ConcreteColleague2 extends Colleague {
    constructor(mediator) {
        super()
        facade.log('ConcreteColleague2 created')
        this.mediator = mediator
        this.feature = "feature 2"
    }

    setFeature(feature) {
        facade.log('ConcreteColleague2 Feature has changed from ' + this.feature + ' to ' + feature)
        this.feature = feature
        this.Changed()
    }
}


function init_Mediator() {
    var mediator = new ConcreteMediator()
    mediator.colleague1.setFeature("new feature 1")
}

备忘录模式

Memento

Memento

'use strict';

class Originator {
    constructor() {
        facade.log('Originator created')
        this.state = 'a';
        facade.log('State= ' + this.state)
    }

    SetMemento (Memento){
        this.state = Memento.GetState()
        facade.log('State= ' + this.state)
    }

    CreateMemento (state){
        return new Memento(state);
    }
}

class Memento {
    constructor(state) {
        this.state = state
        facade.log('Memento created. State= ' + this.state)
    }

    GetState (){
        return this.state;
    }

    SetState (state){
        this.state = state;
    }
}

class Caretaker {
    constructor() {
        facade.log('Caretaker created')
        this.mementos = []
    }

    AddMemento(memento) {
        facade.log('Caretaker AddMemento')
        this.mementos.push(memento)
    }

    SetMemento() {
        return this.mementos[this.mementos.length-1]
    }
}

function init_Memento() {
    let caretaker = new Caretaker()
    let originator = new Originator()
    caretaker.AddMemento(originator.CreateMemento('b'))
    originator.SetMemento(caretaker.SetMemento())
    facade.log(originator.state)
}

观察者模式

Observer

Observer Sequence

'use strict';

class Subjectt {
    constructor() {
    }

    Attach (Observer){
        this.observers.push(Observer);
        facade.log('Observer attached')
    }

    Dettach (Observer){
        for(var i in this.observers)
            if(this.observers[i] === Observer)
                this.observers.splice(i, 1)
    }

    Notify (){
        facade.log('Subject Notify')
        for(var i in this.observers){
            this.observers[i].Update(this);
        }
    }
}

class ConcreteSubject extends Subjectt {
    constructor() {
        super()
        this.subjectState = null
        this.observers = []
        facade.log('ConcreteSubject created')
    }

    GetState() {
        return this.subjectState;
    }

    SetState(state) {
        this.subjectState = state;
        this.Notify()
    }
}

class Observer {
    constructor() {
    }

    Update (){
    }
}

class ConcreteObserver extends Observer {
    constructor() {
        super()
        this.observerState = '';
        facade.log('ConcreteObserver created')
    }

    Update (Subject){
        this.observerState = Subject.GetState();
        facade.log('Observer new state: ' + this.observerState)
    }
}

function init_Observer() {
    var observer1 = new ConcreteObserver()
    var observer2 = new ConcreteObserver()
    var subject = new ConcreteSubject()
    subject.Attach(observer1)
    subject.Attach(observer2)
    subject.SetState('state 1')
}

状态模式

State

'use strict';

class Contextt {
    constructor(state) {
        switch(state) {
            case "A":
                this.state = new ConcreteStateA()
                break
            case "B":
                this.state = new ConcreteStateB()
                break
            default:
                this.state = new ConcreteStateA()
        }
    }

    Request (){
        this.state.Handle(this);
    }
}

class State {
    constructor() {
    }

    Handle (){
    }
}

class ConcreteStateA extends State {
    constructor() {
        super()
        facade.log('ConcreteStateA created')
    }

    Handle (context){
        facade.log('ConcreteStateA handle')
    }
}

class ConcreteStateB extends State {
    constructor() {
        super()
        facade.log('ConcreteStateB created')
    }

    Handle (context){
        facade.log('ConcreteStateB handle')
    }
}

function init_State() {
    let context = new Contextt("A")
    context.Request()
}

策略模式

Strategy

'use strict';

class Contexttt {
    constructor(type){
        switch(type) {
            case "A":
                this.strategy = new ConcreteStrategyA()
                break
            case "B":
                this.strategy = new ConcreteStrategyB()
                break
            default:
                this.strategy = new ConcreteStrategyA()
        }
    }

    ContextInterface (){
        this.strategy.AlgorithmInterface()
    }
}

class Strategy {
    constructor() {
    }

    AlgorithmInterface (){
    }
}

class ConcreteStrategyA extends Strategy{
    constructor() {
        super()
        facade.log('ConcreteStrategyA created')
    }

    AlgorithmInterface (){
        facade.log('ConcreteStrategyA algorithm')
    }
}

class ConcreteStrategyB extends Strategy{
    constructor() {
        super()
        facade.log('ConcreteStrategyB created')
    }

    AlgorithmInterface (){
        facade.log('ConcreteStrategyB algorithm')
    }
}

function init_Strategy() {
    let contextA = new Contexttt("A")
    contextA.ContextInterface()
    let contextB = new Contexttt("B")
    contextB.ContextInterface()
}

模板方法模式

Template Method

'use strict';

class AbstractClass {
    constructor() {
    }

    TemplateMethod (){
        this.PrimitiveOperation1();
        this.PrimitiveOperation2();
    }

    PrimitiveOperation1 (){
    }

    PrimitiveOperation2 (){
    }  
}

class ConcreteClass extends AbstractClass {
    constructor() {
        super()
        facade.log("ConcreteClass created")
    }

    PrimitiveOperation1 (){
        facade.log('ConcreteClass PrimitiveOperation1')
    }

    PrimitiveOperation2 (){
        facade.log('ConcreteClass PrimitiveOperation2')
    }  
}

function init_TemplateMethod() {
    let class1 = new ConcreteClass()
    class1.TemplateMethod()
}  

访问者模式

Visitor

Visitor

'use strict';

class Visitor {
    constructor() {
    }

    VisitConcreteElementA (ConcreteElementA){
    }

    VisitConcreteElementB (ConcreteElementB){
    }  
}

class ConcreteVisitor1 extends Visitor {
    constructor() {
        super()
        facade.log("ConcreteVisitor1 created");
    }

    VisitConcreteElementA (ConcreteElementA){
        facade.log("ConcreteVisitor1 visited ConcreteElementA");
    }

    VisitConcreteElementB (ConcreteElementB){
        facade.log("ConcreteVisitor1 visited ConcreteElementB");
    }  
}

class ConcreteVisitor2 extends Visitor {
    constructor() {
        super()
        facade.log("ConcreteVisitor2 created");
    }

    VisitConcreteElementA (ConcreteElementA){
        facade.log("ConcreteVisitor2 visited ConcreteElementA");
    }

    VisitConcreteElementB (ConcreteElementB){
        facade.log("ConcreteVisitor2 visited ConcreteElementB");
    }  
}

class ObjectStructure {
    constructor() {
        facade.log("ObjectStructure created");
    }
}

class Element {
    constructor() {
    }

    Accept (visitor){
    }
}

class ConcreteElementA extends Element {
    constructor() {
        super()
        facade.log("ConcreteElementA created");
    }

    Accept (visitor){
        visitor.VisitConcreteElementA(this);
    }

    OperationA (){
        facade.log("ConcreteElementA OperationA");  
    }
}

class ConcreteElementB extends Element {
    constructor() {
        super()
        facade.log("ConcreteElementB created");
    }

    Accept (visitor){
        visitor.VisitConcreteElementB(this);
    }

    OperationB (){
        facade.log("ConcreteElementB OperationB");  
    }
}


function init_Visitor() {
    let visitor1 = new ConcreteVisitor1();
    let visitor2 = new ConcreteVisitor2();
    let elementA = new ConcreteElementA();
    let elementB = new ConcreteElementB();
    elementA.Accept(visitor1);
    elementB.Accept(visitor2);
}

Mysql入门第四课《查询数据》

原文在我的 Github 中,欢迎订阅。

前言

前几篇文章

之所以把数据查询单拉一个文章,是因为查询牵扯的知识点比较多,可以说在增删改查里,查的复杂度也是最高的。

之前已经了解一点像WHERE id=2 这种非常简单的条件语句。

单表查询非常简单,但开发中更多的是多表查询,那我们以多表查询来说道说道。

热热身

我们在处理数据时通过某个字段来查另一个跟它有关的信息,除了在数据库中经常这样操作,在前端也有类似情况。

先看一段前端经常遇到的数据:

{
    province:'江苏省',
    citys:[
        '南京市',
        '苏州市',
        '无锡市'
    ]
}

上面是把省市都揉到一起了,只嵌套了两层,但如果嵌套个四五层,就像这样:

{
   province:'江苏省',
   children:[
    {
        name:'城市1',
        children:[
            name:'江宁区',
            children:[
                name:'XX小区'
            ]
        ]
    },
    {
        name:'城市2',
        children:[
            name:'AA区',
            children:[
                name:'BB小区'
            ]
        ]
    }
   ]
}

这种数据解析起来会疯。

我们一直说数据扁平化,来 我们扁平一把:

// 省
const provice = [
    {
        province:'江苏省',
        province_id: 1001
    },
    {
        province:'浙江省',
        province_id: 1002
    },
    ...
]

// 市
const citys = [
    {
      name:'南京市',
      province_id: 1001
    },
    {
      name:'苏州市',
      province_id: 1001
    },
    
    {
      name:'杭州市',
      province_id: 1002
    },
    {
      name:'嘉兴市',
      province_id: 1002
    },
    ...
]

//找到江苏省下所有的城市
const result = citys.filter(i => i.province_id === 1001);

数据扁平化的好处就是,当不需要找城市的时候,citys 数据跟我无关,只需关心 province 就可以了,而且在查找性能上更快(有时候能免了递归)。

上面的例子引出下面这句话:在数据库中,通过某些字段将表与表关联起来,这就是关系型数据库的核心。

准备几张表

在图中可以看到 student 表里有 class_id,这样 学生班级 通过 class_id就有了关联,在开发中,我们可以通过它来查找class信息。

查询

我们通过上面几个表来查询几个需求:

  1. 查询成绩大于 60 分的学生,显示学生的姓名和成绩
  2. 查询姓的老师的个数
  3. 查询没有学过马上来老师课的学生姓名
  4. 查询所有学生的姓名、选课数量、成绩总和

我们一个一个来并分析。

查询成绩大于 60 分的学生,显示学生的姓名和成绩

SELECT t1.student_name, t2.number FROM 
student t1 LEFT JOIN score t2 ON t1.id=t2.student_id 
WHERE t2.number>60;

先看结果:

得到了正确数据。

分析语句:
t1t2分别是 student 和 score 的别名。
细心的同学能看出,我把上面的 sql 语句用三行来显示,这是有寓意的哟:

  1. 第一行:要查询的字段,这个非常好理解
  2. 第二行:其实它的结果是个临时表!即对应查询语句里的 table_name !
  3. 第三行:通俗易通的WHERE条件语句

也就是说,它依然是符合通用语法:

SELECT column_name,column_name
FROM table_name
[WHERE Clause]
[LIMIT N][ OFFSET M]

只不过第二行生成了一个临时表。

这里牵扯到了 JOIN ON 语法,我会在后面的章节中专门细说,这里推荐几篇相关文章:

查询姓马的老师的个数

SELECT COUNT(id) AS teacher_num FROM teacher WHERE teacher_name LIKE '马%';

解析:

  • COUNT(fieldName): COUNT 函数用于统计某字段数量
  • AS: 取别名
  • LIKE:一般与%使用,模糊搜索,如果不用%相当于精确搜索。
  • %:表示任意字符,类似于正则表达式里的*

查询所有学生的姓名、选课数量、成绩总和

这个查询比较复杂,我们先上 sql :

SELECT 
t1.student_name, 
IFNULL(t2.course_num,0) AS course_num, 
IFNULL(t2.sum_number,0) AS sum_number FROM 
student t1 
LEFT JOIN 
(SELECT student_id,count(id) course_num, SUM(number) AS sum_number FROM score GROUP BY student_id) t2 
ON t1.id=t2.student_id;

再看下结果:

先!不!要!慌! 我们一点一点来解析。

现在你脑海里应该先浮现出通用查询语句:

SELECT column_name,column_name
FROM table_name
[WHERE Clause]
[LIMIT N][ OFFSET M]

而图中的查询语句翻译过来就是:

SELECT 学生名, 选课数量, 成绩总和 FROM 表;

然后我们来拆分上图中的查询:
先看 SELECT student_id,count(id) course_num, SUM(number) AS sum_number FROM score GROUP BY student_id,我们单独来执行这句看看结果:

这条语句为我们生成了一个表,它显示了 学生id、选课数、总成绩,所以这张表示核心,但需求是让我们展示所有的学生,所以我们必须依赖student查。

如果把上图中查出来的结果 命名为t2,就会变成:

SELECT 
t1.student_name, IFNULL(t2.course_num,0) AS course_num, IFNULL(t2.sum_number,0) AS sum_number 
FROM student t1 LEFT JOIN t2 
ON t1.id=t2.student_id;

再去掉些“多余”的部分:

SELECT 
t1.student_name, t2.course_num, t2.sum_number 
FROM student t1 LEFT JOIN t2 
ON t1.id=t2.student_id;

哈哈,是不是一下就看懂了呢?

这里再介绍下语句里没见过的东东:

  1. IFNULL(a,b):类似常见的 if 语句,判断 a 是否为 null,如果是则显示 b。
  2. COUNT():对读取的数据中的某字段计算出个数,一般用于查询出数据的条数。
  3. SUM():求和,对读取数据中的某个字段求和。
  4. GROUP BY:通过 GROUP BY 可以设定通过哪些字段对读取的数据进行分组排序(默认升序),需要注意的是,GROUP BY 有分组聚合功能。

关于GROUP BY有几篇文章可以看看:

附建表语句

下面是几个表的建表语句:

-- 班级表
CREATE TABLE class(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	caption VARCHAR(30) COMMENT '班级名'
);

-- 学生表
CREATE TABLE student(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	student_name CHAR(30) COMMENT '学生名',
	gender CHAR(30) DEFAULT NULL	COMMENT '学生性别',
	class_id INT DEFAULT NULL COMMENT '班级id'
);

-- 老师表
CREATE TABLE teacher(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	teacher_name CHAR(30) COMMENT '教师名'
);

-- 课程表
CREATE TABLE course(
 id INT UNSIGNED PRIMARY KEY auto_increment,
 course_name CHAR(30) COMMENT '课程名',
 teacher_id INT DEFAULT NULL COMMENT'教师id'
);

-- 成绩表
CREATE TABLE score(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	number INT DEFAULT NULL COMMENT '分数',
	student_id INT DEFAULT NULL COMMENT '学生id',
	course_id INT DEFAULT NULL COMMENT '课程id'
);

总结

这篇文章主要了解查询,然而这也只是一个练习而已,实际开发中比这难的查询有很多,需要自己平常没事多练习。

今天工作比价忙,文章写的可能有点糙,如果有哪里不正确的地方欢迎指正。

微前端(singleSpa + React )试玩

前言

我们团队正在做一个XX系统,技术栈是React,目前该系统日渐庞大,开发及维护成本加大,且每次必须把整个项目一起打包,费时费力。经考虑后决定将其拆分成多个项目,由它们组合成一个完整系统,微前端架构是非常好的选择。

微前端差不多有以下几个好处:

  1. 单项目维护:比如将商品模块单拉出来形成一个项目,它可以由一个小组单独维护,实现良好解耦
  2. 复杂度降低:不需要在整个集成式的庞大系统内开发,避免巨大的代码量,开发时编译速度快,提高开发效率
  3. 容错性:单独项目发生错误不会影响整个系统
  4. 技术栈灵活:vue、react、angular 等包括其他前端技术栈都可以使用,会 vue 的不需要再学 react

对我们来说最大的好处是单项目维护

展示

UI示例图

我们将整个微前端分为两个部分:

  1. 主项目(Main):红色框部分,作为整个项目的父级,负责展示菜单模块、头部模块
  2. 子项目(Sub-apps):蓝色框部分,子项目的作用是具体的业务展示

动图展示

注意看地址栏变化,其中包含 /app1/xxx/app2/xxx,乍一看这是一个项目中两个页面的切换,实际上是来自两个独立的项目,app1 和 app2 来自不同的 git 仓库。

微前端架构图

整个流程大概为:用户访问 index.html, 此时运行模块加载器Js,加载器会根据整个系统的配置文件(project.config) 去注册各个项目,系统会先加载主项目(Main),然后会根据路由前缀动态加载对应的子项目

我们这个架构也参考了网上很多好的文章,其中核心文章可参考 https://alili.tech/archive/11052bf4/

关于 project.config

大概如下

[
 {
    isBase: false,
    name: 'app1',
    version: '1.0.0',
    //通过该路由前缀匹配加载当前入口文件
    hashPrefix: '/app1',
    //入口文件
    entry: 'http://www.xxxx.com/app1/dist/singleSpaEntry.js',
    //顶级Store
    store: 'http://www.xxxx.com/main/dist/store.js'
  }
  ......
]

技术细节

single-spa

我们找了些实现微前端的仓库,对比后决定使用single-spa

我们技术栈是 react,在子项目入口中需要使用 single-spa-react 来构建,关键代码如下:

import singleSpaReact from 'single-spa-react';

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  domElementGetter
});

export function bootstrap(props) {
  return reactLifecycles.bootstrap(props);
}

export function mount(props) {
  return reactLifecycles.mount(props);
}

export function unmount(props) {
  return reactLifecycles.unmount(props);
}

如果你使用 vue,可以使用 single-spa-vue

然后在系统入口文件中,把所有的项目注册进来:

import * as singleSpa from 'single-spa';

singleSpa.registerApplication(
    'app1',
    () => SystemJS.import('app1-entry.js'),
    () => location.hash.startsWith(`#/app1`),
    props
  );

具体可参考 single-spa 官网 https://single-spa.js.org 这里有很多例子

Webpack 与 SystemJs

我们使用的 lerna 统一管理所有项目的依赖包,所有依赖包的版本统一,这样非常方便维护。

使用 webpack 的 dll 功能,将所有项目的公用依赖包抽离,比如 react、react-dom、react-router、mobx等

为了方便项目动态加载,我们也参考网上大佬的想法,使用了systemjs,只不过我们使用的是 0.20.19 版本,配合 systemjs ,在 Webpack 中需要改一下 libraryTarget:

output: {
    publicPath: 'http://www.xxxxx.com/',
    filename: '[name].js',
    chunkFilename: '[name].[chunkhash:8].js',
    path: path.resolve(__dirname, 'release'),
    libraryTarget: 'amd', //注意 这里使用 amd 的规范
    library: 'app1'
  },

我们没有使用 umd 规范,也没有使用 systemjs 里的 Import Maps
功能,而是直接通过 project.config 来动态加载模块入口。

app之间通信

关于这个也看了一些大佬的方案,大概就是所有的项目里有个 store,在注册入口时将所有 store 放进队列,需要更新 store 里的状态时,调用 dispatch 将所有 store 同步。

我的做法和传统单页应用一样,一个系统应该只有一个顶级 Store,由于顶级 Store 里存的一般是整个系统的公用状态 比如菜单、用户信息等,我把它放在 Main项目里,但打包时这个Store是单独抽离的:

entry: {
    singleSpaEntry: './src/singleSpaEntry.js',
    store: './src/store' //单独一个入口
  },

在注册时,将这个 Store 传入每个项目中:

//顶级Store
const mainStore = await SystemJS.import(storeURL);

singleSpa.registerApplication(
    'app1',
    () => SystemJS.import('http://www.x.com/app1/entry.js'),
    hashPrefix('/app1'),
    { mainStore }
);
singleSpa.registerApplication(
    'app2',
    () => SystemJS.import('http://www.x.com/app2/entry.js'),
    hashPrefix('/app1'),
    { mainStore }
);

这样就可以达到只管理这一个 Store 就可以,非常方便。
注意:我使用的是 Mobx 作为状态管理

前端部署

我们部署的方式非常简单,我自己写了一个 webpack 插件用于把打包后的 dist 传到 OSS 然后将项目信息传给服务端,服务端根据我传入的项目信息组织成 project.config,然后用户在访问 index.html 时会获取 project.config,此时 single-spa 根据配置注册所有项目,然后根据路由来拉取对应的项目入口文件js文件。

把子项目的挂载 DOM 放在 Main 项目里

我们的需求是 Main 作为整个项目的 Layout,其中子项目的挂载 Dom 也在 Main项目里,这就必须等到 Main 项目完全渲染完成后,才能挂载子项目。我参考了网上有些微前端的实现,把 domElementGetter 方法借鉴了过来:

function domElementGetter() {
  let el = document.getElementById('sub-module-wrap');
  if (!el) {
    el = document.createElement('div');
    el.id = 'sub-module-wrap';
  }
  let timer = null;
  timer = setInterval(() => {
    if (document.querySelector('#content-wrap')) {
      document.querySelector('#content-wrap').appendChild(el);
      clearInterval(timer);
    }
  }, 100);

  return el;
}

demo

demo地址:https://github.com/Vibing/micro-frontend

结束语

这是我们第一次玩微前端,可能有很多地方不完美,还望各位大佬多多包涵

docker简单实操

忽略 docker 安装

以 nginx 为例:

从 docker hub 仓库拉取最新的 nginx 镜像:docker pull nginx:latest

下载完成后通过 docker images查看本地镜像:

启动该镜像

docker run -d -p 8080:80 --name mynginx nginx

-d:后台运行,不要阻塞shell指令创建窗口
-p:指定内外端口映射,-p 8080:80 宿主机为8080 容器为80
--name: 为当前启动的容器命名

通过 docker ps 查看当前启动的容器:

现在在浏览器打开:127.0.0.1:8080,可以看到 nginx 的访问页面:

进入容器内部

我们进入 nginx 容器内部,修改刚才访问的页面试试。

通过docker exec -i -t mynginx bash 在容器 mynginx 中开启一个交互模式的终端:

  • -i: 即使没有附加也保持STDIN 打开
  • -t: 分配一个伪终端

nginx 的默认页面在 /usr/share/nginx/html/ 文件夹内:

我们看一下 index.html

然后通过echo hello nginx > index.html修改 index.html 里的内容:

修改完成后,再次访问127.0.0.1:8080:

完美

前端也要学Docker啊!

Docker这两年非常火热,也是各大厂必用的好东西,这两天没事玩了一下感觉很不错,学起来也不难 写下此文共勉学习。

关于Docker

Docker 可理解为跑在宿主机上的非常精简、小巧、高度浓缩的虚拟机。 它可以将容器里的进程安稳的在宿主机上运行。

Docker重要的三个概念必须要知道:

  • Image: 镜像
  • Container: 容器
  • Repository: 镜像仓库

为了好理解 我们从 Docker的 Logo 入手:

WX20190615-112209@2x

图片是一条鲸鱼游在海里 身上载着N个集装箱,下面是Docker字样。OK 图片描述完毕

图片给出的信息:

  1. 海:宿主机
  2. 集装箱:Docker容器
  3. 鲸鱼+集装箱:Docker技术
也就是说:Docker容器(集装箱)里可以存放着我们写的代码,然后 Docker 载着代码在大海(宿主机)里运行

之所以用鲸鱼,可能是它在海里没什么天敌 体型又巨大而且游泳速度很快,毕竟Docker使用GO语言写的呢。

镜像(Image)、容器(Container)、仓库(Repository)

上文中只说了Container,而ImageContainer的关系 就像实例的关系:

var p1 = new Person(); 

即:p1是容器、Person是镜像。 至于仓库嘛 就相当于github的代码仓库,github是存代码的仓库,相应的 Docker 仓库就是存放镜像的。

只有理解上面的镜像(Image)、容器(Container)、仓库(Repository)才能破解下面的图:

WX20190615-102950

上图分了三个块:

  • Client(客户端 命令终端)
  • DOCKER_HOST(Docker daemon)
  • Resistry(镜像仓库)

从左往右看,Client 中执行了几个命令,这些命令都与 Docker daemon(Docker的守护进程) 有交互,然后 Docker daemon 会根据相应命令做对应的动作。

  1. docker build:表示创建了一个 Image,这是一条虚线 ,虚线从开始到结束指向了中间的Images框里。
  2. docker pull:表示从仓库中拉取 Image,就像 github 里 pull 代码一样。docker daemon 接收到 pull 指令,从 Registry(远程镜像仓库) 里找到对应镜像(这里是Nginx) 然后拉倒本地的 Images 中。
  3. docker run:向 daemon 发出运行指令,daemon 收到指令后去本地的 Images 中找对应镜像,如果能找到就会使用该镜像生成一个容器,如果没找到则会默认执行 docker pull 从仓库里下载,然后再生成容器,如果容器中运行着我们的代码,那么当容器运行后 代码也跟着 run 起来了

Docker安装

Docker分社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)
社区版是免费的,所以我们用CE版就可以了。
Docker CE具体安装参考官网文档:CentOSMacOSWindows

下载完成后 打开终端运行:docker run hello-world 成功运行则表示安装成功了。

下篇文章《使用Docker部署NodeJs应用》会说Docker常用的命令及使用Docker部署NodeJs
代码并让它运行起来,敬请期待

nestjs模块

要想使用 nest 开发,你必须了解 module ,否则无法下手,也搞不懂模块间的引用逻辑。

如果你不懂 nest 里的模块,可以这样理解:它类似组件化概念里的组件,比如在写 React 时,我们有一个根组件,一个 React 应用由多个组件构成的; 在 nest 应用中,也有一个根模块( app.module, 即上图中的 Application Module ),整个 nest 应用由多个模块和一个根模块构成;

来看一个模块:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  imports: [],
  controllers: [CatsController],
  providers: [CatsService],
  exports: []
})
export class CatsModule {}

仔细看,当一个类被 @module 装饰时,那么这个类就是一个模块类。@module 参数中有 imports、controllers、providers、exports 四个参数,这几个参数的内容,就是该模块的所有。比如 CatsController 和 CatsService 属于同一个应用程序域,那么就应该把它们都归属到一个模块下。

  • imports: 引入的模块,当前模块需要使用其他模块的功能,就需要在这里将其他模块引进来
  • constrollers: 当前模块中的控制器
  • providers: 提供者就是为当前模块提供的功能,一般是 Service 或被 @Injectable() 装饰的类,这些类可以通过 constructor 注入依赖关系,只有 provider 可以在模块中被共享
  • exports: 向外部导出 providers 里的提供者(子集),否则即使外部模块引入了当前模块也无法使用providers里的功能

总结一下,一个完整的 Module 由 imports, controllers, providers, exports 构成,模块可以引入其他模块,从而使用跟多功能,也可以导出当前模块的提供者,为其他模块提供服务

React Hooks优化

React Hooks

react hooks 的使用需要在 function component 组件中,本文讲述在使用 react hooks 中你需要注意的一些事情

状态每次改变,整个 function 都会重新执行

可能导致:函数的每次执行,其内部定义的变量和方法都会重新创建,也就是说会从新给它们分配内存,这会导致性能受到影响

看下面这个例子:

import React, { useState, ReactElement } from 'react'
import { Button } from 'antd'

let num = 0; // 用于记录当前组件执行次数

export default (): ReactElement => {
  console.log('render num: ', ++num) // 打印执行次数

  let [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(++count)
  }

  return (
    <>
      <p>count: {count}</p>
      <Button type="primary" onClick={handleClick}>
        Button
      </Button>
    </>
  )
}

初始化时执行了一次:

现在我点三次按钮,让 count 状态改变:

可见,每改变一次 count, 该组件对应的整个 function 会重新执行,其内部变量和方法会重新创建,从而影响性能。

解决方法:

  • 变量尽量放在函数外部
  • 方法使用 useCallback 包裹起来

使用方法:

const handleClick = useCallback(()=>{
    // 业务代码
},[ count ])

useCallback 的作用:组件初始化时,将第一个参数函数“缓存”起来,只有在第二个参数(数组中的值)有变化时,被包裹的函数才会重新被创建,否则不会重新创建。

总结:变量尽量放在组件外部定义,函数使用 useCallback 包裹起来,避免组件 render 时重复创建。

父组件更新,子组件也跟着执行

再看个例子,我们把上面例子作为父组件,在里面添加一个子组件.

父组件:

export default (): ReactElement => {
  let [count, setCount] = useState(0)

  const handleClick = useCallback(() => {
    setCount(++count)
  }, [count])

  return (
    <>
      <p>count: {count}</p>
      
      {/* 这里添加一个子组件 */}
      <ChildrenComponent />

      <Button type="primary" onClick={handleClick}>
        Button
      </Button>
    </>
  )
}

子组件代码:

export default (): ReactElement => {
  console.log('children render')

  return <div>children component</div>
}

现在我再点三次按钮,让父组件 render 三次:

大爷的,子组件打印三次,表示执行了三次。

这肯定不是我想要的,我想要的是子组件需要被渲染的时候再去执行,那么如何解决?

答:使用 React.memo

React.memo 类似 class 组件里的 PureComponent , 能帮助我们控制合适重新渲染组件。

注意:说它类似,但不完全一样,它更像是 PureComponent + shouldComponentUpdate 的结合。
PureComponent 通过 props 和 state 的浅比较来判断要不要重新渲染组件。

那么在 react hooks 里如何去写呢

我们把子组件加上 React.memo :

export default React.memo(
  (): ReactElement => {
    console.log('children render')

    return <div>children component</div>
  },
)

现在再点三次按钮:

可见,子组件不再打印,也就是不再执行了。

React.memo 也提供了 shouldComponentUpdate 功能,用于自定义比较来决定是否渲染:

React.memo(MyComponent, (prevProps, nextProps)=>{
 // 如果传递 nextProps 渲染会返回与传递 prevProps 渲染相同的结果,则返回 true,否则返回 false.
 
 // return true:不渲染  return false:渲染
})

总结

  • 使用 useCallback 缓存定义的函数
  • 使用 React.memo 避免不必要的 render

如果有更好的建议,请留言,多谢

Mysql入门第一课《建表、改表、删表》

前言

本人想学数据库了,于是有了这个Mysql系列。

本系列主要用于本人学习Mysql的记录,我把它当做学习笔记。
没有从安装数据库及用户新增和权限分配等知识开始,而是侧重于Mysql表操作、数据增删改查及其他相关知识。

为学习方便,以下将以student(学生表)、class(班级表)、lesson(课程表) 为导向进行学习。

另外,本人所用可视化工具是Navicat Premium

还有一点:该篇文章用到一些简单的数据类型字段,如:INT、TINYINT、VARCHAR 只需知道它是数字类型和字符串类型。
关于数据类型请阅读《数据类型》

建表

建表通用语句:

CREATE TABLE table_name (column_name column_type);

翻译过来就是:CREATE TABLE 表名 ( 字段名 字段类型等 );

建立 student 表

使用上面通用语句来建学生表:

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
    student_name VARCHAR(30) COMMENT '学生姓名',
	age TINYINT DEFAULT 0 COMMENT '年龄',
	sex CHAR(5) NOT NULL DEFAULT '0' COMMENT '性别', 
    create_time timestamp DEFAULT CURRENT_TIMESTAMP()
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

运行上面 sql 后,建表成功:

分析一下建表语句,先看除字段以外的部分:

CREATE TABLE student (
   ...
) ENGINE=InnoDB DEFAULT CHARACTER=utf8;

ENGINE=InnoDB DEFAULT CHARACTER=utf8;是数据库默认的可以不用写,但作为新手应该知道,这句是指:数据库引擎使用的是InnoDB, 默认的字符编码是utf8

下面再来看看字段定义部分:

id INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
student_name VARCHAR(30) COMMENT '学生姓名',
age TINYINT DEFAULT 0 COMMENT '年龄',
sex CHAR(5) NOT NULL DEFAULT '0' COMMENT '性别', 
create_time timestamp DEFAULT CURRENT_TIMESTAMP()
  • id: 字段名称
  • INT: 字段数据类型
  • PRIMARY KEY: 将该字段设为主键
  • AUTO_INCREMENT:自增
  • COMMENT:为该字段添加注释,后面的字符串为注释内容
  • DEFAULT: 默认值
  • CURRENT_TIMESTAMP(): 当前时间

总结一下公式大概就是:
字段名称 + 字段类型 + [ 默认值、主键设置、自增、注释...... ]
[ ]内为可选项

以上是建表时常用的命令,[]内的......表示还有其他命令,但上面对我这个入门者已经够了。

注意:建表时还需要考虑表之间的关联和 foreign key(外键) ,这里暂时不介绍,后面会有章节专门来说这块。

改表

表是建好了,但随着开发进行 之前建好的表很可能不满足未来需求,所以对表的修改是必须的

同样,修改表也有通用语句:

ALTER TABLE <表名> [修改选项]

下面是修改选项语法

添加字段:

ADD COLUMN <列名> <类型> ...

修改字段名:

CHANGE COLUMN <旧列名> <新列名> <新列类型> ...

优化(修改)字段类型

MODIFY COLUMN <列名> <类型> ...

删除字段

DROP COLUMN <列名> ...

修改表名

RENAME TO <新表名>

对于 MODIFYCHANGE可能有疑问,这里说明下:MODIFY主要用于修改字段类型等,不能修改字段名称,而CHANGE是把旧字段换成新字段 当然也可以修改字段类型。

简单来说MODIFY是对原有字段做类型修改,CHANGE是直接将整个字段换掉 包括类型等

添加字段

下面动手操作一把,首先是对表添加字段:

ALTER TABLE student ADD COLUMN hobby VARCHAR(100);

上面语句为 student 表添加了一个字段hobby(爱好) ,该字段数据类型是字符串(100字符)。

下图表示 hobby 字段添加成功:

使用 CHANGE 修改字段

ALTER TABLE student CHANGE COLUMN hobby hobby_num TINYINT;

上面语句将旧字段 hobby 替换成新字段 hobby_num 字段类型为数字类型;

执行完后的结果如下:

使用 MODIFY 修改字段类型

上面说过,MODIFY不能修改字段名,一般用于修改字段类型等操作,下面我们把刚才的hobby_num从数字类型改为字符串类型:

ALTER TABLE student MODIFY COLUMN hobby_num VARCHAR(30);


可以看到,类型已经成功修改为VARCHAR类型。

删除字段

删除字段非常简单,这里我们删除 hobby_num 字段:

ALTER TABLE student DROP COLUMN hobby_num;

执行成功,下图中 hobby_num 已经被删除:

修改表名

修改表名的操作频率非常低,但还是要知道一下。
我们把 student 表名改为 students:

ALTER TABLE student RENAME students;

执行后可以看到,表名已经修改成功:

删表

删表的操作除了在学习中常用到,真正在开发中操作频率也非常低。
删表语句如下:

DROP TABLE table_name;

我们把 students 表给删了:

DROP TABLE students;

看下结果:

OK,表没了...没了...了...

总结

本篇学习了:

  1. 如何创建表
  2. 对表名进行更改、表字段进行增删改操作
  3. 对表进行删除操作

可能的疑惑:
建表的时候用了很多数据类型,光数字类型就出现了INTTINYINT,字符串类型出现了CHARVARCHAR

所以下篇文章我们来学习《数据类型》来了解它们。

Mysql入门第二课《数据类型》

前言

本文接着上篇 Mysql入门第一课《建表、改表、删表》 继续学习。

要建一个优秀的表,选择合适的数据类型非常重要,如果数据类型选择不当,不仅开发起来给自己找麻烦,而且还会造成数据库性能低下。

比如给student(学生表)添加age字段,选择TINYINT类型就够了,它的范围是 0-255(无符号) 比较适合,如果使用 INT 也可以满足条件,但INT占 4 个字节,而TINYINT只占 1 个字节,相比较当然TINYINT性能更好。

刚才提到了UNSIGNED(无符号),我会在下文说明。

数据类型

Mysql支持多种类型,大致分为三类:数值、字符串、日期/时间类型。

我们各个击破

数值类型

类型 大小 范围(有符号) 范围(无符号) 用途
TINYINT 1字节 (-128, 127) (0, 255) 小整数值
SMALLINT 2字节 (-32768, 32767) (0, 65535) 大整数值
MEDIUMINT 3字节 (-8 388 608,8 388 607) (0,16 777 215) 大整数值
INT或INTEGER 4字节 (-2 147 483 648,2 147 483 647) (0,4 294 967 295) 大整数值
BIGINT 8字节 (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) (0,18 446 744 073 709 551 615) 极大整数值
FLOAT 4字节 (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) 0,(1.175 494 351 E-38,3.402 823 466 E+38) 单精度浮点数值
DOUBLE 8 字节 (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),
0,
(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)
0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) 双精度浮点数值
DECIMAL 对DECIMAL(M,D) ,如果M>D,
为M+2否则为D+2
依赖于M和D的值 依赖于M和D的值 小数值

这里解释下上面提到的有符号无符号

  • 有符号:默认为有符号,其实就是从负数到正数的取值范围
  • 无符号(UNSIGNED):没有负数,最低从 0 开始

对于平常开发来说,整数类型其实到 INT 的数值范围已经很大了。

建表时经常看到类似 INT(5) 后面有个 5,它表示显示宽度(M),M 的值不能大于取值范围长度。
举个例子: 如果age字段类型是INT(5) UNSIGNED ZEROFILL,插入一条数据age为99,最后显示为:00099

UNSIGNED 为无符号, ZEROFILL 的作用是用 0 填充没有数字的位置。

我问过一些同事,在开发时为了方便,很多字段应该用数字类型 他们选择用字符串类型。这句话看看就好

字符串类型

字符串类型是建表时最最最常用的,下面看下它有哪些类型:

类型 大小 用途
CHAR 0-255字节 定长字符串
VARCHAR 0-65535 字节 变长字符串
TINYBLOB 0-255字节 不超过 255 个字符的二进制字符串
TINYTEXT 0-255字节 短文本字符串
BLOB 0-65 535字节 二进制形式的长文本数据
TEXT 0-65 535字节 长文本数据
MEDIUMBLOB 0-16 777 215字节 二进制形式的中等长度文本数据
MEDIUMTEXT 0-16 777 215字节 中等长度文本数据
LONGBLOB 0-4 294 967 295字节 二进制形式的极大文本数据
LONGTEXT 0-4 294 967 295字节 极大文本数据

通常情况下,二进制的数据用的很少,一般像图片、音频都是存在 CDN 或 云服务器里,用的比较多的就是CHARVARCHARTEXT了。

光看表格没啥概念,但可以知道字符串主要以字节来提现大小,我们开发中用的字符串一般就是英文字母和汉字,那就需要知道字节与它们的关系:

在 Mysql 的UTF8编码下:

  • 1 个英文字母(包括大小写)占 1 个字节
  • 1 个汉字占 3 个字节

所以当我们存名称、简介和文章时,可以通过占用字节数选择合适的类型了,完美。

日期/时间类型

这个类型我们用的也很多,像生日、创建时间、修改时间等等都需要它。

类型 大小 范围 格式 用途
DATE 3字节 1000-01-01/9999-12-31 YYYY-MM-DD 日期值
TIME 3字节 '-838:59:59'/'838:59:59' HH:MM:SS 时间值或持续时间
YEAR 1字节 1901/2155 YYYY 年份值
DATETIME 8字节 1000-01-01 00:00:00/9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值
TIMESTAMP 4字节 1970-01-01 00:00:00/2038 (结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07) YYYYMMDD HHMMSS 混合日期和时间值,时间戳

在开发中,常用的是 DATETIMETIMESTAMP 也有使用INT来记录时间,下面从可读性、存储空间、操作性上来分析:

  1. 可读性,INT 可读性最差,显示时需要使用代码进行格式转换,没有 DATETIMETIMESTAMP 直观。
  2. 存储空间,INT 和 TIMESTAMP 最小 都是 4 个字节,DATETIME 占 8 个字节。
  3. 操作性,在平时,我们对日期的操作有读、写、比较、计算。 读写大家都一样,没啥区别;在比较和计算上INT要方便很多,可以直接比较,加减等运算,其余两种需要利用代码工具进行计算和比较,此时性能最好的是INT

综合考虑,个人比较偏向TIMESTAMP,占用空间小,可读性强,如果对性能不是非常苛刻,在代码帮助下操作也很简单,但使用时要考虑它的时间范围!

番外

结合 Mysql入门第一课《建表、改表、删表》 和本篇文章,有几处出现了约束条件,这里有必要说一下:

  • UNSIGNED :无符号,值从0开始,无负数
  • ZEROFILL:零填充,当数据的显示长度不够的时候可以使用前补0的效果填充至指定长度,字段会自动添加UNSIGNED
  • NOT NULL:非空约束,表示该字段的值不能为空
  • DEFAULT:表示如果插入数据时没有给该字段赋值,那么就使用默认值
  • PRIMARY KEY:主键约束,表示唯一标识,不能为空,且一个表只能有一个主键。一般都是用来约束id
  • AUTO_INCREMENT:自增长,只能用于数值列,而且配合索引使用,默认起始值从1开始,每次增长1
  • UNIQUE KEY:唯一值,表示该字段下的值不能重复,null除外。比如身份证号是一人一号的,一般都会用这个进行约束
  • FOREIGN KEY:外键约束,目的是为了保证数据的完成性和唯一性,以及实现一对一或一对多关系

总结

本篇文章主要介绍数据类型 以及在开发中 如何使用合适的数据类型,然后在番外中介绍了下建表时出现的条件约束

下篇文章将开始 Mysql入门第三课《数据的增删改》 欢迎阅读。

React Hooks Immutable

默认渲染行为

React 的渲染主要分两种:首次渲染和重渲染。
首次渲染就是第一次渲染,这是无法避免的就不讨论了,重复渲染是指由于状态改变或 props 改变等原因造成的渲染。

React 默认的渲染特性:当父组件渲染时,会递归渲染下面所有的子组件(让人诟病的特性)

如下:

const App = () => {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState('Tom');

  React.useEffect(() => {
    setInterval(() => {
      setCount(s => s + 1);
    }, 1000);
  }, []);

  return (
    <>
      <h3>count: {count}</h3>
      <Child name={name} />
    </>
  );
};

function Child(props: { name: string }) {
  console.log('child render', props.name);
  return <div>name: {props.name}</div>;
}

React 对这种行为可以说是不负责任,不管你三七二十八,只要父组件渲染,所有它下面的子组件都会渲染,这种方式简单而粗暴。

既然如此,那么这块只能我们这些负责任的开发者去优化了。

浅比较优化

React 把优化问题抛给开发者,它给我们提供了三个用于性能优化的 API。

React.PurComponent

用于 class 写法的组件,它会对传入组件的 props 进行浅比较,如果比较的结果相同,则不会去渲染组件

React.memo

与 React.PurComponent 一样,用于函数组件

浅比较

在 js 中,数据主要分两种:

  • 基本类型(字符串、数字、undefined...),
  • 引用类型,即对象(JSON、Array、Function、Regex...)

基本类型的数据属于原始值(primitive value),它直接存储在栈内存中,
引用类型的数据存在于堆内存中,通过存在栈内存中的指针来调用它

  • 原始数据是 immutable 的,引用类型(object)一般是 mutable 的
  • 原始数据比较直接通过值比较,而 object 则通过引用比较
var a = 1;
var b = 1;
a === b // true

var a = {};
var b = {};
a === b // false 比较的是引用 所以不相等 

对于对象,不仅有引用比较,也有深比较和浅比较

const a = {num: 1};
const b = {num: 1};
a === b // false 引用不相等
a.num === b.num // true 对象的一级相等

const a = { p: {num: 1}, t: 2 }
const b = { p: {num: 1}, t: 2 }
a === b // false 引用不相等
shallowEqual(a, b) // false a.p === b.p 引用不等 浅比较为false
deepEqual(a, b) // true 深比较相等

如果对象的一级属性中存在引用比较,则不相等。
对象的深比较跳过了引用比较,仅仅是比较相同层级下的属性值。

  • 由于对象存在于堆内存中 通过栈内存中的引用来调用,所以如果对象的值不变,对象的引用变了,就会导致 React 组件缓存失败,造成组件无必要的渲染,进而会造成性能问题
  • 如果对象值变,引用不变,React 则不触发渲染,导致界面与数据不一致

React.memo 可以让 props 在变化时,该组件才会发生有意义的重新渲染
我们将子组件用 React.memo 包起来,只要 props 不变,在父组件更新时,子组件也不会重渲染

const Child = React.memo((props: { name: string }) => {
  console.log('child render', props.name);
  return <div>name: {props.name}</div>;
});

看起来问题解决了,React.memo 将前后的 props 进行浅比较,这基本能解决大多数问题。但如果 props 中含有对象数据,在浅比较时比较的是引用,这种方式就行不通了,好在 React.memo 提供了第二个参数,可以自定义比较前后的 props

浅比较行不通,那么深比较呢

const Child = React.memo((props) => {
  return <div>{props.name}</div>;
}, (prev, next) => {
    // 深比较
  return deepEqual(prev,next)
});

这样的确可以达到效果,但如果是比较复杂的对象,就会存在较大的性能问题,甚至直接挂掉,因此不建议使用深比较去进行性能优化

还有一种方式:如果能保证对象的值相等,再保证对象的引用相等,就可以保证子组件在 props 没变的情况下不会渲染。

React.useRef 的返回值是固定的常量,我们可以使用它来做为对象的引用

const Child = React.memo(({ obj }) => {
  console.log('child render', obj, obj.name);
  return <div>name: {obj.name}</div>;
});

const App = () => {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    setInterval(() => {
      setCount(s => s + 1);
    }, 1000);
  }, []);

  const obj = React.useRef({
    name: 'Jack'
  });

  return (
    <>
      <h3>count: {count}</h3>
      <Child obj={obj.current} />
    </>
  );
};

这样还是存在一个严重的问题:如果 name 改变了,但 obj 没有变,导致子组件不会重新渲染,数据与UI界面不一致。 看来 useRef 只能用于常量

那我们只要保证 name 不变的时候 obj 和上次一样, name 才让子组件更新就可以了。没错,就是 useMemo。

useMemo 的特性就是保证依项不变时,对应的对象也不会变,只有依赖项变化时,对应的对象才会变

const App = () => {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState('');

  React.useEffect(() => {
    setInterval(() => {
      setCount(s => s + 1);
    }, 1000);
  }, []);

  // 只有当 name 变化时,obj才会变化
  const obj = React.useMemo(
    () => ({
      name
    }),
    [name]
  );

  return (
    <>
      <h3>count: {count}</h3>
      <input
        type="text"
        value={name}
        onChange={e => {
          setName(e.target.value);
        }}
      />
      <Child obj={obj} />
    </>
  );
};

immutable

上面的方式算是一种解决方案,现在我们来看看其他的东西。

我们在 class 组件中更新状态,只需要一个 setState 就行,不管你传入什么 state,组件都会刷新

// class 的 setState 传什么都会更新组件
this.setState({
    name: 'Jack'
})

但在 hooks 里, 如果前后两次的引用相等,就不会更新组件

const [state, setState] = useState({})

// 同一个引用,不会更新
setState(s => {
    s.name = 'Tom'
    return s
})

// 生成新应用,可以更新
setState(s => {
    const newState = {
        ...s,
        name: 'Tom'
    };
    return newState;
})

在 hooks 中,如果你想修改状态对象,必须保证前后修改的对象引用不等。这就要求我们不能直接更新老的 state,而是要保持老的 state 不变,去生成一个新的 state,也就是 immutable 方式。
老的 state 保持不变,也就意味着该 state 应该是 immutable obj

const state = [
    {
        name: 'Tom',
        age: 20
    },
    {
        name: 'Jack',
        age: 30
    }
]

state[0].name = 'Alen'

// hooks中需要如下写法
const newState = [
    {
        ...state[0],
        name: 'Alen'
    },
    ...state
]

immutable 的写法过于繁琐,这不是我们想要的

其实,综上来说,我们的需求很简单:

  • 需要改变状态
  • 改变状态后和之前的状态引用不相等

第一个冲上脑门的答案就是:先深拷贝,然后做 mutable 修改就可以了

const state = [
    {
        name: 'Tom',
        age: 20
    },
    {
        name: 'Jack',
        age: 30
    }
]

const newState = deepClone(state);
newState[0].name = 'Alen'

深拷贝有两个缺点:

  • 拷贝的性能问题
  • 对于循环引用的处理
    虽然市面上有些库支持高性能的深拷贝,但会对参考物对等(reference equality )造成了破坏
const state = [
    {
        name: 'Tom',
        age: 20
    },
    {
        name: 'Jack',
        age: 30
    }
]

const newState = lodash.cloneDeep(state)

state === newState // false
state[0] === newState[0] // false

可以发现,所有对象的结构都被破坏,在 React 中使用这种方式,即使对象属性没有任何变化,也会导致没有意义的重新渲染,仍会导致比较严重的性能问题

深拷贝是非常糟糕的

这么看来,更新状态还要其他的需求。
我们将 oldState 视为一个属性树,改变其中某节点时,能返回一个新的对象

  • 当前节点及其组件节点的引用在新老 state 中不相等,这样能保证UI组件即时刷新
  • 非当前节点及其祖先节点的引用在新老 state 中保持引用相等,这能保证状态不变时组件不重渲染

遗憾的是,JavaScript 并没有内置这种对 immutable 数据的支持,更不用说对 immutable 数据更新了,但可以使用一些三方库来解决这个问题,比如:immer 和 immutablejs

import Immutable from 'immutable'

var state = Immutable.Map({
    a: 1,
    b: 2
})

var newState = state.set('a', 3) 

Immutable 数据使用结构共享的方式,只更新修改了子节点的引用,不会去修改未更改的子节点引用,达到我们想要的需求

总结

  • 默认情况下,React 组件更新会触发其下面所有的子组件递归渲染
  • 通过 React.memo 的浅比较 props,来保证 props 不变的情况下,组件不会刷新
  • 浅比较只对基本类型(primitive)生效,对于对象无效,即使对象中的值不变,也会引发重渲染
  • 通过使用 useRef 和 useMemo 来缓存对象,在对象值没变时不用引发重渲染
  • 不能通过深拷贝的方式修改 state,不仅导致性能问题,还会引起即使 state 内的值没有变化,但引用发生变化,从而造成无意义渲染,引发严重性能问题
  • 这要求我们使用 immutable 的方式更新 state,来保证引用和缓存
  • 可以通过第三方库:immer、immutablejs 来简化 immutable 的 state 更新写法

React中的事件函数为什么要bind this

我们平常写React是这样的:

class HelloMessage extends React.Component {

  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); //绑定this
  }
  
  handleClick(){
    console.log( this )
  }

  render() {
    const { count } = this.state;
    
    return (
      <button onClick={ this.handleClick } >Hello</button>
    );
  }
}

ReactDOM.render(
  <HelloMessage/>,document.getElementById('root')
);

上面代码可以完好运行,handleClick 绑定好 this 后,打印如下:

HelloMessage {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternalFiber: Hr, …}
    context: {}
    props: {}
    refs: {}
    state: null
    updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
    _reactInternalFiber: Hr {tag: 1, key: null, elementType: ƒ, type: ƒ, stateNode: HelloMessage, …}
    __proto__: b

我们把.bind(this)去掉,再打印一次:

undefined

OK,果然this就没有任何东西了。

但是如果把 handleClick 改为箭头函数的写法,就不需要 bind(this) 了:

 handleClick = () => {
    console.log( this )
 }

更改后,this 也能正常打印出来。

我们先看一下render方法中被Babel编译后的样子:

  render() {
    return React.createElement(
      'button',
      { onClick: this.handleClick },
      'Hello'
    );
  }

React 通过 React.createElement 方法模拟 document.createElement 来创建 DOM(当然React创建的是虚拟DOM)。 属性中 onClick 指向 this.handleClick 方法,看起来都没有问题。

下面我们不用 React 来实现上面 this 的现象:

// 创建DOM
function createElement(dom, params) {
  var domObj = document.createElement(dom);
  domObj.onclick = params.onclick;
  domObj.innerHTML = 'Hello';
  document.body.appendChild(domObj);
}

// Demo类
class Demo {

  handleClick() {
    console.log(this);
  }

  render() {
    createElement('button', {
      onclick: this.handleClick
    });
  }
}

// 执行render
new Demo().render();

小伙伴们猜猜,这里的 this 指向什么(手动滑稽)?

来打印结果:

<button>Hello</button>

啊哈哈哈啊,没错 this 并不是 undefined,也没有指向 Demo 对象,而是创建的 button DOM对象本身。

现在再把 handleClick 改为箭头函数:

// Demo类
class Demo {

  handleClick = () => {
    console.log(this);
  }

  render() {
    createElement('button', {
      onclick: this.handleClick
    });
  }
}

// 执行render
new Demo().render();

来看看这次打印的 this :

Demo {handleClick: ƒ}
    handleClick: () => { console.log(this); }
    __proto__: Object

卧槽,居然指向了 Demo 对象,这样就可以在 handleClick 里随意访问 Demo 里的属性和方法了!

说明

其实,如果你 JavaScript 基础够扎实,我想你是不会点进来看这篇文章的。
这里的问题无非就牵扯到两个点:

  1. this 指向问题
  2. 箭头函数的特性

this 指向问题

我打赌,上面的 Demo 实例很多人看的云里雾里的,所有我把它转成 ES5 的写法:

function createElement(dom, params) {
  var domObj = document.createElement(dom);
  domObj.onclick = params.onclick;
  domObj.innerHTML = 'Hello';
  document.body.appendChild(domObj);
}

// 创建 Demo 类,相当于 class Demo {} 的写法
function Demo() {

}

// 在原型上挂载 handleClick 方法
Demo.prototype.handleClick = function() {
   console.log(this);
};

Demo.prototype.render = function() {
  createElement('button', {
    onclick: this.handleClick
  });
};

new Demo().render(); // 运行 render 方法

打印结果:

<button>Hello</button>

**严重注意:在ES6 class 内定义方法时,如果不是箭头函数,方法是挂载在 prototype 原型对象上的! **

那么下面,把 handleClick 用箭头函数的方式写出来:

function createElement(dom, params) {
  var domObj = document.createElement(dom);
  domObj.onclick = params.onclick;
  domObj.innerHTML = 'Hello';
  document.body.appendChild(domObj);
}

// 创建 Demo 类,相当于 class Demo {} 的写法
function Demo() {
    // 相当于箭头函数 不了解的同学请恶补一下吧
    var _this = this;
    this.handleClick = function(){
        console.log( _this );
    }
}

Demo.prototype.render = function() {
  createElement('button', {
    onclick: this.handleClick
  });
};

new Demo().render(); // 运行 render 方法

这时候再打印:

Demo {handleClick: ƒ}
    handleClick: ƒ ()
    __proto__: Object

厉害了哈~ this 指向了想要的对象环境中。

总结

通过以上代码解析,能知道 React 中,在不适用箭头函数时,需要通过 bind 将函数内 this 指向当前对象才能正常访问。

而使用箭头函数时,由于箭头函数的特性,函数内的 this 就是当前对象上下文,所以不需要 bind 来指向。

如果哪里有不对的地方,欢迎指正,感谢!

React优化:竭尽全力的减少render渲染

前言

玩过React的同学都知道,render()方法除了第一次组件被实例化,其他情况绝大多数是state改变触发的。

而render方法的执行,所带来的负担就是重新对比Virtual DOM(虚拟DOM树),也就是重新执行Diff算法,然后把要修改的的DOM重新update。

而我们总是在开发过程中会产生非常多不必要的重新渲染

如何减少render的触发,是提升项目性能的关键之一。

入手点

减少render的入口在哪?当然是要知道哪些情况会触发render啦~

根据个人了解,触发render有以下两种情况:

  1. 组件实例化
  2. state变化、props变化

那么我们从这两点入手

优化

Immutable 和 componentShouldUpdate

《React和Immutable》中,提到了使用Immutable配合React的componentShouldUpdate生命周期函数来减少不必要的render,效果非常明显,这里就不说了,建议大家一定去看一下

除了使用 Immutable ,还有其他方法来避免render执行吗?

先看看正常开发中,使用setState的情况:

constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    console.log('render');
    return (
      <div>
        <input onChange={this.change} />
        <button onClick={this.submit}>提交</button>
      </div>
    );
  }

 change = e => {
    this.setState({
      value: e.target.value
    });
  };

  submit = () => {
    console.log(this.state.value);
  };

执行上面代码,然后在input里输入123,看看控制台上发生了什么:

4

对没错,执行了3次render,我们想象一下,用户在登录界面输入账号和密码时,render 会执行多少次啊啊啊 简直要命。

那有什么方法可以解决呢?

使用防抖来减少render

防抖函数应用在高频率操作时,能避免无意义的代码执行,从而提高性能。

constructor(props) {
    super(props);
    this.state = {
      inputVal: ''
    };
  }

  render() {
    console.log('render');
    return (
      <div style={{ margin: 50 }}>
        <input onChange={this.change} />
        <button onClick={this.submit}>提交</button>
      </div>
    );
  }

  timer = null;
  change = e => {
    //获取inputVal
    const inputVal = e.target.value;

    //防抖处理
    clearTimeout(this.timer);
    this.timer = setTimeout(() => {
      //set值
      this.setState({
        inputVal
      });
    }, 200);
  };

  submit = () => {
    console.log(this.state.inputVal);
  };

我们依然将inputVal挂在state上面,由于onChange事件可以频繁的触发setState,所以我们从这里下手,在onChange事件里使用防抖来避免频繁触发setState,从而避免多次render带来的性能消耗。

我们在文本框里输入值,然后看看控制台打印的结果:

7

Yes! 可以看到 我们输入 Hello,控制台只打印出了一次 render ,非常好!

在开发中可以将防抖函数放在工具函数中方便调用哟~

减少setState 使用私有属性

现在我们改变一种写法:不使用setState而是换成私有属性this.inputVal来代替:

constructor(props) {
    super(props);
    this.inputVal;
  }

  render() {
    console.log('render');
    return (
      <div style={{ margin: 50 }}>
        <input onChange={this.change} />
        <button onClick={this.submit}>提交</button>
      </div>
    );
  }

  change = e => {
    this.inputVal = e.target.value;
  };

  submit = () => {
    console.log(this.inputVal);
  };

我们仍然输入123,然后看看控制台:

5

哇塞~~ render一次都没执行,现在点击提交,打印结果是 123,这正是我们想要的。

缺点

聪明的同学能发现一个问题,其实这样的优化有一个缺点:不能使用双向绑定

在开发中,真正使用双向绑定的表单元素,一般会有其他效果必须依赖该表单元素的数据,比如省市区级联,市区的变化必须依赖省的数据。

所以如果没有必要使用双向绑定的,请尽量避免使用。

总结

减少render的执行次数,可以使用:

  • Immutable 配合 shouldComponentUpdate 具体请看这里
  • 使用防抖函数来减少不必要的setState,从而减少render次数
  • 使用私有属性来替代setState

Mysql入门第三课《数据的增删改》

原文在我的Github里,欢迎订阅。

之前已经学习了Mysql入门第一课《建表、改表、删表》Mysql入门第二课《数据类型》,今天继续学习 如果对表数据进行增加、修改和删除的操作。

依然以 student 表为例。

执行以下 sql 新建一个空的 student表:

CREATE TABLE student(
	id INT UNSIGNED PRIMARY KEY auto_increment,
	name VARCHAR(10),
	age TINYINT(3)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

新增数据

先看下新增数据通用语法:

INSERT INTO table_name ( field1, field2,...fieldN ) VALUES ( value1, value2,...valueN );

field为字段名称,value是要插入的值,把它看做给变量赋值就行了。

下面为student表插入一条数据:

INSERT INTO student (name, age) VALUES ('赵云', 26);

上面是每次插入一条数据,在开发中往往会遇到批量新增数据的情况。
下面我们一次性新增 5 条数据:

INSERT INTO student (name, age) VALUES ('张飞', 30),('刘备', 32),('关羽', 33),('马超', 28),('诸葛亮', 35)

执行上面语句后,看下结果:

修改数据

修改数据通用语法:

UPDATE table_name SET field1=new-value1, field2=new-value2 [WHERE Clause]

修改数据要注意一个前提:对谁修改

上面语法中的[WHERE Clause]就是条件语句,用来控制要修改的哪些数据。

下面来实操一把。

现在表里的三国人物都是男的,我们把张飞改成貂蝉,让其他男的 happy 一下(邪恶脸):

UPDATE student SET name='貂蝉', age=18 WHERE id=2;

之前张飞那条数据的 id 是 2,我们使用 WHERE 语句找到了id=2 的数据,然后把nameage都修改了。

这下把赵云高兴坏了,因为貂蝉在他下面。

OK,刚才可以看出,要对谁修改,是看条件语句怎么写。
现在我要把刘备和关羽都变成小乔

UPDATE student SET name='小乔', age=16 WHERE id=3 OR id=4;

上面都是通过 WHERE 来确定对某些数据修改,那如不写条件语句会怎么样?

UPDATE student set name='王昭君', age=17;

没错!如果不加条件语句,会把整个表都修改了!一定要注意!

删除数据

通用语法:

DELETE FROM table_name [WHERE Clause]

删除数据跟 UPDATE 有点像,是根据条件语句来删除对应数据。

比如我要删除 id=1 的 王昭君。

DELETE FROM student WHERE id=1;

看下结果,id=1 的王昭君不见了,好桑心:

至于批量删除跟 WHERE 语句有关,比如删除 id>3 的王昭君:

DELETE FROM student WHERE id>3;

更多王昭君不见了,更伤心了:

现在不加条件语句:

DELETE FROM student;

很好,全删了,眼不见为净!

总结

本篇介绍了如何对数据表进行增加数据、修改数据、删除数据。

下面文章介绍 查询数据~

敬请期待

JavaScript常用设计模式之单例模式

什么是单例模式

一句话概括:提供唯一一个对象,可以供外界访问。

举例一

我们在封装一些工具方法时,喜欢用这种方式:

const util = {
   ajax: function(){},
   random: function(){},
   ...
}

最简单的就是使用字面量的创建一个对象,里面包含一些属性和方法,util暴露给外界访问。

举例二

上面的代码有个问题,如果有私有属性或方法不想暴露出来那就不行了,我们改一下:

const utilSpace = function() {

    //私有变量
    const _privateName = 'hello';
    
    //私有方法
    function _showPrivateName() {
        console.log( _privateName )
    }
    
    //暴露给外界的对象
    return {
        showName: function(){
            _showPrivateName();
        },
        
        publicVar: "hello, I'm Tom"
    }
}

const single = utilSpace(); //向外界暴露single即可

single.showName(); // hello
single.publicVar; // hello, I'm Tom

其实上面的代码已经够用了,如果你想用更优雅或更合理的写法可以这样:

(function(){

    var instance; // 该变量用于缓存实例
    
    // 定义 Util 类
    var Util = function() {
    
        if( instance ) return instance;
        
        instance = this;
        
        //其他属性或方法
        this.xxx = 0;
        this.name = 'Tom';
    }
    
    window.Util = Util;
    
})();

var a = new Util();
var b = new Util();

console.log( a === b ) //true

上面代码中,定义一个Util类, 并用 instance 变量将 Util 实例缓存起来,当使用 new Util 时,如果 instance 已经缓存了实例,则直接将该实例返回,这样就保证不管 new 多少次,始终只会返回一个实例,达到单例的目的。

结尾

前端开发中我们遇到的单例模式有很多,比如:Dialog、Tips、Toast 等都是可以通过单例模式来搞定的。

时间分片(Time Slicing)

时间分片

W3C性能工作组规定:将执行时间超过50ms任务定义为长任务(Long Task)。

长任务由于长时间阻塞主线程,会让用户感觉到卡顿。

而解决长任务的方式大致有两种:

  • 使用Web Worker,将长任务放在 Worker 线程中执行,缺点是无法访问 window 对象和 操作 DOM
  • 时间切片(Time Slicing)

什么是时间分片

时间分片并不是某个 api,而是一种技术方案,它可以把长任务分割成若干个小任务执行,并在执行小任务的间隔中把主线程的控制权让出来,这样就不会导致UI卡顿。

React 的 Fiber 技术核心**也是时间分片,Vue 2.x 也用了时间分片,只不过是以组件为单位来实施分片操作,由于收益不高 Vue 3 把时间分片移除了。

使用时间分片

在早期,时间分片充分利用了“异步”来实现,例如:

btn.onclick = function (){
    someTask(); //50ms
    setTimeout(function() {
        otherTask(); //50ms
    })
}

上面代码,本来应该执行 100ms 的长任务,被拆分成了两个 50ms 的任务。

使用 Generator 函数

Generator是 ES6 里的语法,它提供了一个生成器函数来生成迭代器对象,我们利用 Generator 函数提供的 yield 关键字来让函数暂停,通过使用迭代器对象的 next 方法让函数继续执行。

如果我们用 Generator 函数,则可以这么写:

btn.onclick = ts(function* (){
    someTask();
    yield;
    otherTask();
})

这样就可以通过 yield 把一个长任务拆分成两个短任务。
我们也可以将 yield 关键字放在循环里:

btn.onclick = ts(function* (){
    while (true) {
        someTask();
        yield;
    }
})

上面虽然是个死循环,但依然不会阻塞主线程,所以浏览器不会卡死。

基于 Generator 函数实现 ts 方法

基于 Generator 函数的执行特性,我们很容易使用它来实现一个时间分片函数:

function ts(gen) {
  if (typeof gen === 'function') gen = gen();
  if (!gen || typeof gen.next !== 'function') return;
  return function next() {
    const res = gen.next();
    if (res.done) return;
    setTimeout(next);
  };
}

代码核心**:通过 yield 关键字可以将任务暂停执行,并让出主线程的控制权;通过setTimeout将未完成的任务重新放在任务队列中执行。

演示

为了好理解,先写段长任务代码,将主线程阻塞一段时间:

const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {}
console.log('done!');

该段脚本霸占主线产长达 1s 的时间,如果把这段长任务分解成多个小任务执行呢。

我们通过这种方式来看一下,将长任务使用时间分片来处理:

ts(function* (){
    const start = performance.now();
    let count = 0;
    while (performance.now() - start < 1000) {
        yield;
    }
    console.log('done!');
})()

从图里看到,一个长任务虽然切成了诺干个小任务,但时间颗粒度过小,这样会导致执行任务的总时长增加,而W3C定义超过 50ms 为长任务,所以我们要控制一下任务时长,让它在一个合理的时间内,这样不会导致任务总时长过长。

继续优化

为了保证切割的任务接近 50ms,可以在 ts 函数中根据任务的指向时间判断是否应该一次性执行多个任务。
修改一下 ts 函数:

function ts(gen) {
  if (typeof gen === 'function') gen = gen();
  if (!gen || typeof gen.next !== 'function') return;
  return function next() {
    const start = performance.now();
    const res = null;
    do {
      res = gen.next();
    } while (!res.done && performance.now() - start < 25);
    if (res.done) return;
    setTimeout(next);
  };
}

上面代码中,做了一个 do while:如果当前任务执行时间低于 25ms 则多个任务一起执行,否则作为一个任务执行,一直到 res.done = true 为止。

现在,我们再测试一下看看结果:

可以看到,时间切片的颗粒度变的正常了,总时间也会相应缩短,完美!

总结

时间分片的概念以及技术方案让长任务分割成多个短任务,并且将控制权放给主线程,不会造成主线程卡顿

通过使用 Generator 函数特性,很方便的实现了 ts 函数。

使用Docker部署Node应用

上篇《前端也要学Docker啊!》介绍了Docker及它的三个主要概念:Image(镜像)、Container
(容器)、Registry(仓库) 以及Docker安装。

本篇我们来动手实践:在本地创建一个自己的镜像(Node应用),使用该镜像创建容器并执行容器中的Node应用。

创建一个Node项目

在根目录创建index.js

//index.js
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello Docker O(∩_∩)O~~';
});

app.listen(3000);

创建 Docker 镜像需要用到 docker build命令,而docker build命令又是根据 Dockerfile 配置文件来构建镜像,所以我们要在项目根目录创建一个 Dockerfile 文件:

#Dockerfile
FROM node:10.13-alpine #项目的基础依赖
MAINTAINER chenLong #项目维护者
COPY . . #将本机根目录所有文件拷贝到容器的根目录下 这个可以根据喜好调节路径
EXPOSE 3000 #容器对外暴露的端口
RUN npm i #安装node依赖
CMD npm start #在容器环境里执行的命令

你可以到 Docker 官网查看详细的Dockfile说明

构建镜像

上面 Node 代码已经完成了,我们使用 yarn init -ynpm init -y 完成package.json初始化,然后安装一个koa依赖:执行yarn add koanpm i koa

然后我们在本地跑一下 node 程序:node index.js,打开浏览器输入 localhost:3000 ,可以看到浏览器中成功显示了 Hello Docker O(∩_∩)O~~ 。

WX20190618-140213

程序没问题,我们开始构建这个镜像,执行命令:docker build -t docker-demo/hello-docker:v1 . (注意最后有个 "." 是必须的)

  • -t: --tag简写,镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。

上面的 docker-demo/hello-docker是我们定义的镜像名称,v1是标签名称(类似版本号)

WX20190618-121212@2x

图中蓝色框表示 Dockerfile 的执行步骤。此时一个名为docker-demo/hello-docker的镜像已经创建完成了,现在我们执行docker images查看一下:

WX20190618-112816@2x

表示本地的镜像列表中已经有了我们刚才创建的docker-demo/hello-docker

让Node程序在Docker中跑起来

上面已经创建好了镜像,里面包含着我们写的代码,现在我们需要把代码运行起来。
非常简单,我们使用docker run命令使用镜像创建一个容器实例(此刻脑海中浮现 var p1 = new Person() )。

我们执行命令: docker run -i -t -p 8080:3000 docker-demo/hello-docker:v1

  • -i: 以交互模式运行容器,通常与 -t 同时使用;
  • -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
  • -p: 指定端口映射,格式为:主机(宿主)端口:容器端口,这里将容器的3000端口与宿主机的8080端口映射

WX20190618-121238@2x

打开浏览器,运行localhost:8080:
WX20190618-121254

完美,容器里的代码已经跑起来了!

总结

  1. 在项目根目录创建 Dockerfile 并配置
  2. 使用 docker build 命令创建Docker镜像,该命令会根据 Dockerfile 里的配置来构建镜像
  3. 使用 docker run 命令根据镜像创建对应的容器实例并运行

谈谈代码拆分,聊聊基于路由拆分 VS 基于组件拆分

代码拆分与动态导入

当项目越做越大时,体积过大导致加载速度过慢,性能问题直接影响用户体验。

这时我们会考虑将代码拆分

拆分,顾名思义就是将一个大的东西拆分成N个小的东西,用公式表示就是:Sum = n * Sub

代码拆分基于动态导入

什么是动态导入?就是我需要什么,你给我什么,我不需要的时候,你别给我,我嫌重。

动态导入可以将模块分离成一个单独的文件 在需要的时候加载进来。

对于动态导入,webpack 提供了两个类似的技术。

  • 第一种,也是优先选择的方式是,使用符合 ECMAScript 提案 的 import() 语法。
  • 第二种,则是使用 webpack 特定的 require.ensure

从webpack 2以后,一般使用第一种。

react-loadable

由于import()方法返回的是Promise对象,我们为了能方便的返回组件,
这里推荐使用react-loadable插件

例子代码:

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

代码里有熟悉的 import() 方法。react-loadable 使用 webpack 的动态导入,调用Loadable方法可以方便的返回要使用的组件。

下面我将以我本人的项目经历,来讲解代码拆分(code splitting)

代码拆分前

当初还是小白的我,一开始哪知道有代码拆分这个技术啊,就一个人负责一个小项目,一开始项目不大,跑起来也是嗖嗖的,这里先贴一下路由代码:

import Home from './home';
import Page1 from './page1';
import Page2 from './page2';

<Route exact path="/" component={Home}/>
<Route path="/page1" component={Page1}/>
<Route path="/page2" component={Page2}/>

懂行的人一看就明白,这里没有使用动态导入,而是直接将所有页面静态引入进来,然后赋到对应路由上。
这么做的坏处就是:打包时,整个项目所有的页面都会打包成一个文件中,随着页面增多,这个文件也越来越大,最后我看了一下,达到了近25M(我吓得打开度娘...)。

如果用一张图来表示的话,这张图在适合不过了:

p1

哈哈,整个一坨有没有。所有路由在这一坨红色里,看着真特么憋屈啊

基于路由的代码拆分

打开度娘的我脸色渐渐有了好转,通过搜索,看到了webpack有个code splitting功能(代码拆分),
前面说过,代码拆分其实就是使用动态导入的技术实现的,那么我们就使用动态导入来优化一把之前的路由:

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const loadComponent = path =>
  Loadable({
    loader: () => import(`${path}`),
    loading: Loading
  });

<Route exact path="/" render={this.loadComponent('./home') } />
<Route path="/page1" render={this.loadComponent('./page1') } />
<Route path="/page2" render={this.loadComponent('./page2') } />

我们不再使用 import module from 'url' 来静态引入模块,而是使用 loadComponent 来动态导入,它返回的是Loadable的结果,也就是我们想要的组件,我们把再把组件给对应的路由,这就完成了基于路由的代码拆分。

使用以后,鄙人怀着激动的心情开始打包项目,当我看到控制台的打包日志时,我的表情是这样的:
p4

咳咳,这种好事情当然要分享一下啦,你要的结果:

p5

可以看到,webpack打包时已经将之前的一个臃肿文件按路由拆分成了三个文件,当我们点击一个路由时,会动态加载对应的文件。

比如我点击home页面的路由时:

p6

我再点击page1时:

p7

嗯,是按照路由来拆分的代码,完美~

这样看来,我们需要将之前的那张图改成这样的:

p2

看着项目加载速度变快了,心里真特么高兴 p8

基于模块拆分

其实基于路由的代码拆分已经可以满足绝大多数项目了,再大的项目也能满足。

但随着项目做的多了,慢慢的发现了一个问题:代码浪费

比如我要做一个Tab切换的功能,像酱紫的:

p9

对应的代码大概是酱紫的:

import { Tabs } from 'antd';
import TabOne from './component/tab1';
import TabTwo from './component/tab2';
import TabThree from './component/tab3';

const TabPane = Tabs.TabPane;

export default class Home extends Component {
  render() {
    return (
      <Tabs defaultActiveKey="1">
        <TabPane tab="Tab 1" key="1">
          <TabOne />
        </TabPane>
        <TabPane tab="Tab 2" key="2">
          <TabTwo />
        </TabPane>
        <TabPane tab="Tab 3" key="3">
          <TabThree />
        </TabPane>
      </Tabs>
    );
  }
}

Tab切换,每个前端小伙伴都做过,其实说白了,就是显示隐藏的效果。

但是在这个页面中,已经把每个Tab里的代码都加载进来了,如果用户只看第一个Tab,其他Tab不点击,就造成了代码浪费

如何解决这个问题呢?还是那句话:我需要什么,你给我什么,我不需要的时候,你别给我,我嫌重。

我们使用动态导入的方式改造一下代码:

import { Tabs } from 'antd';
import Loadable from 'react-loadable';
import TabOne from './component/tab1';
import Loading from './component/loading';

const TabPane = Tabs.TabPane;

const loadComponent = path =>
  Loadable({
    loader: () => import(`${path}`),
    loading: Loading
  });

const Tab2 = loadComponent('./component/tab2.tsx');
const Tab3 = loadComponent('./component/tab3.tsx');

export default class Home extends Component {
  render() {
    return (
      <Tabs defaultActiveKey="1">
        <TabPane tab="Tab 1" key="1">
          <TabOne />
        </TabPane>
        <TabPane tab="Tab 2" key="2">
          <Tab2 />
        </TabPane>
        <TabPane tab="Tab 3" key="3">
          <Tab3 />
        </TabPane>
      </Tabs>
    );
  }
}

同样 我们不再使用import module from 'url'的方式,而是使用 loadComponent 方法动态导入。

由于TabOne是第一个默认显示的,所以没必要动态导入。

现在我们来点击Tab 2看看效果:

p8

非常棒,正是我们想要的。

再点击Tab 3 :

p9

简直完美!😄

到目前为止,我们基于模块的代码拆分就完成了,我们把之前的拆分图再改一下:

p3

看上去爽朗了很多啊!

总结

基于路由的代码拆分可以很大程度上减轻代码的臃肿,但依然会存在不会被使用的组件被import进来,导致代码浪费。

本人认为,既然是组件化时代,那么就应以组件为核心,将动态导入颗粒化到组件而不是路由,将会带来更合理,性能更高的项目优化。

nestjs身份验证

一般业务流程是:验证用户登录信息没问题后,会签发一个 token 给用户 用于之后的接口请求。

给用户签发 JWT

nest 中使用 @nestjs/jwt 来给用户签发 jwt

yarn add @nestjs/jwt

在 auth 模块中引入 jwt 模块

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    UsersModule,
    // 引入 Jwt 模块并配置秘钥和有效时长
    JwtModule.register({
      secretOrPrivateKey: 'll@feifei',
      signOptions: { expiresIn: '60s' }
    }),
  ],
  providers: [AuthService],
  exports: [AuthService],
  controllers: [AuthController]
})
export class AuthModule {}

在 auth.controller 中新建一个 login 路由用于用户登录

import { Body, Controller, Post, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './auth.dto'

@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('/login')
  async getHello(@Body() data: LoginDto) {
    return await this.authService.login(data);
  }
}

再看看 service 中怎么使用 jwt

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
     // 引入 JwtService
    private readonly jwtService: JwtService,
  ) {}

  async login(data) {
    const { username, password } = data;
    const user = (await this.usersService.findOne(username))[0];
    if (!user) {
      throw new UnauthorizedException('用户不存在');
    }

    if (user.password !== password) {
      throw new UnauthorizedException('密码不匹配');
    }

    const { id } = user;
    const payload = { id, username };
    // 生成token
    const token = this.signToken(payload);

    return {
      ...payload,
      token,
    };
  }

	signToken(data) {
    return this.jwtService.sign(data);
  }
}

现在使用请求localhost:3000/login

正常返回了当期登录信息 token,

接下来,按照登录流程,成功发放了 token 给前端后,前端在请求其他数据时需要把 token 给后端,后端经过审核 token 有效才会正常返回接口数据。

使用 Jwt 审核 token

一般情况下,前端把 token 放在请求头的 Authorization 字段中,使用 Authorization = 'Bearer tokenString'的方式请求数据

passport 是一个非常好的处理 jwt 的包,在 nestjs 中使用 passport-jwt 策略来完成 token 的安检,现在我们来添加这个策略:

在 auth/jwt.strategy.ts 中添加 JwtStrategy 策略,需要继承 PassportStrategy(Strategy) ,注意:Strategy 是 passport-jwt 包里的

import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt';
import { SECRET } from './secret';

export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // 配置从头信息里获取token
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // 忽略过期: false
      ignoreExpiration: false,
      // secret必须与签发jwt的secret一样
      secretOrKey: SECRET,
    });
  }

  // 实现 validate,在该方法中验证 token 是否合法
  async validate(payload: any) {
    console.log('payload:', payload);
    return payload;
  }
}

写好 jwt 策略后,需要在模块中引入 PassportModule,在 providers 中加入 JwtStrategy,否则无法使用策略:

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { SECRET } from './secret';

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      secret: SECRET,
      signOptions: { expiresIn: '60s' },
    }),
    // 引入并配置PassportModule
    PassportModule.register({
      defaultStrategy: 'jwt',
    }),
  ],
  controllers: [AuthController],
  // 引入JwtStrategy
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

现在添加一个路由来验证一下

import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async getHello(@Body() data) {
    return await this.authService.login(data);
  }

  @Get('test')
  // 使用路由级守卫
  @UseGuards(AuthGuard())
  async test() {
    return 'test';
  }
}

需要注意的是,在路由中需要添加 @nestjs/passport 中的 AuthGuard 守卫,否则会直接请求到控制器里面,需要 AuthGuard 守卫来检测 jwt 是否合法,如果合法会放行到控制器中

看看结果:

请求成功了,控制台也成功打印了 payload 信息:

payload: { id: 2, username: 'admin', iat: 1619766245, exp: 1619766305 }

使用全局守卫处理 JWT

上文使用的是路由级别的守卫 使用 AuthGuard 来处理 JWT,但一般项目中,绝大多数接口都是要处理 JWT 的,如果每个接口都写上一遍无疑是一个较大的工程

所以我要使用全局的 AuthGuard 来完成这个功能,但也有些接口不需要 jwt 验证(比如 login、register),所以我们不直接使用全局的 AuthGuard,而是创建一个新的守卫:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

新建的守卫需要实现 CanActivate,这就遇到一个麻烦,怎么把 AuthGuard 拿进来使用呢?

仔细一想, AuthGuard 也是守卫,它内部已经实现了 CanActivate,现在我要用 AuthGuard 的功能,只需要让 JwtAuthGuard 来继承 AuthGuard() 就好了

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();

    const whitelist = ['/login'];

    if (whitelist.find((url) => url === request.url)) {
      return true;
    }

    return super.canActivate(context);
  }
}

代码中通过 request 拿到当前请求的 url,通过与白名单(whitelist)对比达到排除不需要 jwt 认证的接口,非白名单内的接口仍需要通过 super.canActivate(context)。 ps: 这里白名单的处理不全面,建议配合 path-to-regexp 来使用

全局使用 JwtAuthGuard 有两种方式,一种在 app.module.ts 中通过 providers 注册全局提供者:

providers: [    {      provide: APP_GUARD,      useClass: JwtAuthGuard,    }]

还有一种是在 main.js 中添加全局使用:

app.useGlobalGuards(new JwtAuthGuard());

对于两种方式,官网上是这么说的:

相关链接:https://docs.nestjs.com/guards

这里我们随便用哪种方式都能满足 , Ps: 记得把路由级别的 AuthGuard 去掉~

Mysql入门第五课《外键约束》

其他文章:

前言

外键约束是mysql提供的表与表之间的关联,使用它可以保证数据的一致性和完整性。

但是我问过同事,他们现在开发中不会使用外键约束,主要原因是数据量大或请求很频繁时会导致一些性能问题,实际开发中是通过业务代码来代替外键约束的功能。

但这不代表不需要学它,如果你的项目数据量不是很大,用外键约束还是非常方便的。

什么是外键

通过之前学习和练习,外键其实已经出现过,比如在student表中:

学生表中包含一个class_id,它指向class(班级表)的id,于是我们可以通过class_id来查找这个学生的班级信息。

此时,对于student表来说class_id就是它的外键,外键字面意思就是指向外部的某个键,这里就是指向外部class表的id

外键约束

多了“约束”两个字。 有约束代表着不能对含有外键关联的表随意删除和修改了,举个例子:

学生表 和 班级表 相互关联并有约束条件,如果有一天,你要删除某个班级,那班级里的学生怎么办呢?默认数据库会不让你删,因为班里还有学生存在。

OK,我们实际操作一把,现在把学生表和班级表加个外键约束:

ALTER TABLE student ADD CONSTRAINT student_class FOREIGN KEY (class_id) REFERENCES class(id);

解析一下上面的语句:

  • ALTER TABLE student :对 student 表进行操作
  • ADD CONSTRAINT student_class:添加约束,名称为 student_class
  • FOREIGN KEY (class_id):指定外键是 class_id
  • REFERENCES class(id):关联(参考) class 表的 id

看下结果:

为了好理解再看下 ER 图:

ON DELETE 和 ON UPDATE

ON DELETE 和 ON UPDATE 表示删除时 和 更新时 要处理的方式。

上面图里有删除时更新时,这是数据删除和更新时的处理方式:

  • NO ACTION 或 RESTRICT:对父表删除或更新时,必须把子表处理完才能删除或更新主表数据
  • CASCADE:对父表删除或更新时,子表同时删除或更新
  • SET NULL:对父表删除或更新时,子表设置为NULL
    默认为 RESTRICT 。

NO ACTION 或 RESTRICT

来看下设置为 NO ACTION 或 RESTRICT 时,我们删除和更新数据试试:

id=1 的班级表中有对应的学生,我们来删除这个班级:

DELETE FROM class WHERE id=1;

更新也是一样:

UPDATE class SET id=10	WHERE id=1;

CASCADE

CASCADE: 对父表删除或更新时,子表同时删除或更新

我们来试一把。
先修改外键约束:

ALTER TABLE student DROP FOREIGN KEY student_class;

ALTER TABLE student ADD 
CONSTRAINT student_class 
FOREIGN KEY (class_id) 
REFERENCES class(id)
ON DELETE CASCADE
ON UPDATE CASCADE;


可以看到已经改为CASCADE了。
需要注意的是,要先删除外键约束 然后再重新生成。

现在我们来删除id=2的班级:

DELETE FROM class WHERE id=2;

然后来看看班级和学生:

班级表中 id=2 的数据已经没有了

再看看学生表:

class_id=2 的学生也一起删了

现在修改一下试试,我们把id=1 的班级修改为 id=10:

UPDATE class SET id=10 WHERE id=1;

修改成功:

再看看原本class_id=1的学生怎么样了:

class_id 也变成 10

SET NULL

SET NULL:对父表删除或更新时,子表设置为NULL

先把外键约束改成 SET NULL 的处理方式:

ALTER TABLE student DROP FOREIGN KEY student_class;

ALTER TABLE student 
ADD CONSTRAINT student_class 
FOREIGN KEY (class_id) 
REFERENCES class(id) 
ON DELETE SET NULL
ON UPDATE SET NULL;

为了演示方便,在student表中添加一条数据:

现在我们删除 id=3 的班级:

id=3的班级已经被删除,再看看 class_id=3 的学生怎么样了:

从图中看到,由于class_id=3这个班级被删除,这个班里的学生没有了班级,所以class_idNULL了。

我们再看下更新:

UPDATE class SET id=100 WHERE id=10;

上面语句是把id=10的班级改为id=100

班级修改成功了,再看看这个班级下的学生:

原本 class_id=10 的学生现在也没有了班级,class_id 也置为 NULL 了。

总结

本篇学习了什么是外键约束,以及外键约束的几个处理方式:

  • NO ACTION 或 RESTRICT:对父表删除或更新时,必须把子表处理完才能删除或更新主表数据
  • CASCADE:对父表删除或更新时,子表同时删除或更新
  • SET NULL:对父表删除或更新时,子表设置为NULL
    默认为 RESTRICT 。

RPC入门理解

RPC 指远程过程调用

常见调用,比如 web 端和远程服务器交互,这是客户端与服务器之间的交互

RPC 是指服务器和服务器之间的调用

服务器之间的交互形式

依赖中间件做数据交互,比如 MySQL、RabbitMQ、Redis 等作为中间依赖的调用方式,A 服务器向 B 服务器发送请求时 需要经过中间件,这类交互形式不需要 B 服务器立刻给出处理结果,一般适合处理数据挤压类型的架构,比如双十一订单量巨大,B 服务器处理能力有限,先把订单数据积压在中间件中,然后 B 服务器慢慢去处理。

直接交互,没有中间件,通过 HTTP、RPC、WS 等方式进行通信,这类架构的特点就是快速相应,A 服务器发送请求到 B 服务器,并会一直等待 B 服务器响应。

RPC 有 Server(一般叫Provider) 和 Client(一般叫Consumer) 的概念,只不过两者都是服务器, Client 服务器属于服务消费者,Server 属于服务提供者

RPC 可以理解为:服务器可以像调用本地方法一样调用远程方法

框架对比

除了 Google 的 gRpc 和 Facebook 的 thrift 支持多语言,其他框架都只支持 Java

gRpc 和 thrift 都是通过借助代码生成工具,根据服务的描述文件来生成不同语言的 Client 和 Server 的代码,gRpc 和 thrift 都用自家的 protobuf 和 thrift 格式的文件来序列化。

核心原理(整体架构)

RPC 整个架构分为三块:

  • RPC Server:暴露服务,服务提供方
  • Client: 服务消费,调用远程服务
  • Registry: 服务注册与发现

RPC 的调用过程是这样的

  1. 第一步,Server 会把它需要暴露的服务以及地址信息等注册到 Registry (注册中心),如果 Server 信息改变会再次注册到注册中心
  2. 第二部,Client 会订阅注册中心,也就是会从 Registry 中一直关注它需要的服务,当 Registry 有 Client 需要的服务时,会通知给 Client,这样 Client 就会有 Server 的服务的信息
  3. 第三部,Client 拿到 Server 后,就可以进行调用了

PS:注册中心并不是必须的模块,Client 也可以把 Server 的信息直接写死,然后直接调用 Server;

也就是说,RPC 中最关键的是调用这一环。

调用的过程

这个过程就是图中的 1-10 ,视频解释

  1. client 要去调接口里的方法(也就是存根里的方法,此时这个方法在远程服务器中,所以需要网络传输才能调用)
  2. 需要把传输的对象转成网络传输需要的二级制数据,这一过程叫序列化
  3. 通过网络将数据传给 server
  4. server 拿到数据,需要把数据反序列化成对象,对象中包含了客户端想要调用的服务端的信息(接口、方法、参数等等)也就是客户端要调用的存根
  5. 根据客户端过来的调用信息,去寻找具体的实现加以调用,方法调用完成后就会拿到调用的结果
  6. 拿到结果后,又一次将结果序列化成二进制
  7. 将二进制数据就给网络
  8. 通过网络传输响应给 client
  9. client 拿到数据后反序列化成结果对象
  10. 返回结果,完成一个闭环

所以,整个 RPC 调用,一般由 客户端、存根代理、服务端、网络传输、序列换与反序列化 这几个模块构成

总结:RPC 架构本质就是服务器调用另外一个服务器上的方法,文章通篇是对 RPC 架构的大致理解,后面会抽时间使用 Google 的 gRPC + Nodejs 来完成一个简单的 rpc 调用

Nodejs文件上传、监听上传进度

带有进度条的文件上传

前言

文件上传如果加上进度条会有更好的用户体验(尤其是中大型文件),本文使用Nodejs配合前端完成这个功能。

前端我们使用 FormData 来作为载体发送数据。

效果

前端部分

HTML 部分 和 Js 部分

<input type="file" id="file" />
<!-- 进度条 -->
<progress id="progress" value="0" max="100"></progress>
// 获取 input file 的 dom 对象
const inputFile = document.querySelector('#file');

// 监听 change 事件
inputFile.addEventListener('change', function() {
    // 使用 formData 装载 file
    const formData = new FormData();
    formData.append('file', this.files[0]);
    
    // 上传文件
    upload(formData);
})

下面我们实现upload 方法。

使用 XMLHttpRequest 的方式

const upload = ( formData ) => {
    const xhr = new XMLHttpRequest();
    // 监听文件上传进度
    xhr.upload.addEventListener('progress', function(e) {
      if (e.lengthComputable) {
        // 获取进度
        const progress = Math.round((e.loaded * 100) / e.total);
        
        document.querySelector('#progress').setAttribute('value', progress);
      }
    },false);
    
    // 监听上传完成事件
    xhr.addEventListener('load', ()=>{
        console.log('😄上传完成')
    }, false);
    
    xhr.open('post', 'http://127.0.0.1:3000/upload');
    xhr.send(formData);   
}

使用 jQuery 的 ajax 上传

jQuery 目前的使用量依然庞大,那么使用 jQuery 的 ajax 如何监听文件上传进度呢:

const upload = ( formData ) => {
    $.ajax({
        type: 'post',
        url: 'http://127.0.0.1:3000/upload',
        data: formData,
        // 不进行数据处理和内容处理
        processData: false,
        contentType: false,
        // 监听 xhr
        xhr: function() {
          const xhr = $.ajaxSettings.xhr();
          if (xhr.upload) {
            xhr.upload.addEventListener('progress', e => {
                const { loaded, total } = e;
                var progress = (loaded / total) * 100;
                document.querySelector('#progress').setAttribute('value', progress);
              },
              false
            );
            return xhr;
          }
        },
        success: function(response) {
          console.log('上传成功');
        }
      });
}

使用 axios 上传并监听进度

axios 使用量非常大,用它监听文件上传更简单,代码如下:

const upload = async ( formData ) => {

    let config = {
        // 注意要把 contentType 设置为 multipart/form-data
        headers: {
          'Content-Type': 'multipart/form-data'
        },
        
        // 监听 onUploadProgress 事件
        onUploadProgress: e => {
            const {loaded, total} = e;
            // 使用本地 progress 事件
            if (e.lengthComputable) {
                let progress = loaded / total * 100;
                document.querySelector('#progress').setAttribute('value', progress);
            }
        }
      };

      const { status } = await axios.post('http://127.0.0.1:3000/upload', formData, config);
      if (res.status === 200) {
          console.log('上传完成😀');
      }
}

Nodejs 部分

这部分比较简单,其实就是单纯的文件上传,我们用 Koa 来实现.

环境搭建及依赖包安装

这里使用 koa2,安装以下依赖包:

  • koa
  • @koa/router: koa 的路由
  • @koa/cors:用于跨域
  • koa-body: 解析 body 数据
  • nodemon: 使用它启动服务,带有热更新

代码部分

const Koa = require('koa');
const Router = require('@koa/router');
const koaBody = require('koa-body');
const path = require('path');
const fs = require('fs');
const cors = require('@koa/cors');

const app = new Koa();
const router = new Router();

router.all('/upload', async ctx => {
  // 处理文件上传    
  const res = await dealFile(ctx);

  res && (ctx.body = {
      status: 200,
      msg: 'complete'
    });
});

// 中间件部分
app.use(cors());
app.use(
  koaBody({
    multipart: true,
    formidable: {
      maxFileSize: 2000 * 1024 * 1024 //最大2G
    }
  })
);
app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000);

dealFile 方法处理上传的文件

出于性能考虑,我们操作file 毫无疑问要使用stream
我们要监听end事件,没办法在事件回调里返回响应,因为会报 404,所以需要使用 Promise 来封装一下,然后用 async、await

const dealFile = ctx => {
  const { file } = ctx.request.files;

  const reader = fs.createReadStream(file.path);
  const writer = fs.createWriteStream(
    path.resolve(__dirname, './image', file.name)
  );

  return new Promise(resove => {
    reader.pipe(writer);
    reader.on('end', () => {
      resove(true);
    });
  });
};

到这里就全部完成了。

注意:前端监听文件进度不需要后端有什么特殊处理,后端仅仅是做了文件流的写入而已。

Jenkins

使用 docker 的 jenkins/jenkins 镜像

拉取镜像

docker pull jenkins/jenkins:2.249.3-lts-centos7

这里用的2.249.3-lts-centos7版本

运行容器

在 run 镜像之前,需要修改下目录权限, 因为当映射本地数据卷时,/www/jenkins_home目录的拥有者为 root 用户,而容器中 jenkins user 的 uid 为 1000

执行下面命令

sudo chown -R 1000:1000 /www/jenkins_home

然后 run jenkins 镜像

docker run -p 8080:8080 -p 50000:50000 -p 3333:3333 -d -v /www/jenkins_home:/var/jenkins_home jenkins/jenkins:2.249.3-lts-centos7
  • 8080 是浏览器访问 Jenkins 的端口
  • 3333 是需要部署的 node 应用使用的端口
  • -v 是将容器中的/var/jenkins_home与宿主机的/www/jenkins_home映射
  • -d 表示后台运行该容器

初始化配置

启动容器后即可在浏览器访问 Jenkins,初始化需要填写一个秘钥(秘钥在命令工具中会给你,复制一下就ok)。

登录进去后,安装推荐的插件,然后在 系统管理->插件管理安装Nodejs插件,用于跑 nodejs 项目。

然后在系统管理->全局工具配置中安装 nodejs :

新建任务

image-20201126154608627

  1. 输入一个任务名称
  2. 选择构建一个自由风格的软件项目

点击确定后是这样的页面:

image-20201126154905239

输入当前的任务描述,然后在源码管理中选择Git

image-20201126155116587

这里使用 sshkey 的方式来拉去代码

初始化时,Credentials(证书) 没有选项,它是用来验证 git 权限的,需要添加一个私钥,点击添加:

image-20201126155659056

对于如何生成秘钥请看:Git 目录下的 git ssh秘钥这篇文章。

然后将 git 拉下来的代码检出到本地子目录,这里写的是sub_dir

构建环境

image-20201126160054699

这里是 Nodejs 环境,配置一下就好

构建

image-20201126160139651

上面配置完了 git 代码的拉取、nodejs 的运行环境,下面执行一些 shell ,这些 shell 主要用来定义环境变量、安装 nodejs 项目依赖包、启动 nodejs 项目。

最后点击保存,到首页即可点击构建,就开始执行对应的任务。任务执行的细节可以通过控制台输出来查看。

策略模式

它属于行为设计模式,定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换

一个基于策略模式的程序至少由两部分组成:

  • 一组策略类,策略类封装了具体的算法,并负责具体的计算过程
  • 环境类 Context, Context 接受客户的请求,随后把请求委托给某一个策略类

要做到这点,说明 Context 中要维持对某个策略对象的引用;

示例

条条大路通罗马,现在人们出去旅游会选择各种方式,这里通过策略模式来完成不同需求下的旅游

先建立几组策略类,这些策略是旅游出行的方式,有的人有时间选择徒步,有的人旅游地方远或时间紧就选飞机,有的人享受自驾:

/**
 * 定义多个策略类 每个策略算法不同
 */
class Foot {
  trigger() {
    console.log('时间很长 徒步旅行');
  }
}

class Car {
  trigger() {
    console.log('开车自驾游')
  }
}

class Plain {
  trigger() {
    console.log('地方远时间紧 飞机旅游')
  }
}

具体的策略有了,现在需要一个环境类 Context 来接收策略,根据不同场景委托给对应的策略:

/**
 * 定义环境类 接收请求 把请求委托给策略类 让策略类去处理
 * @class Context
 */
class Context {
  constructor(stragegieInstance) {
    // 传入具体某个策略实例
    this.stragegieInstance = stragegieInstance;
  }

  handle() {
    this.stragegieInstance.trigger();
  }
}

此时根据不同请求来委托不同的策略:

// Context根据不同请求 来使用不同的策略
// 案例1:我有很长时间,想徒步看世界 则使用 Foot 策略
const FootInstance = new Foot();
const context = new Context(FootInstance);
context.handle();

// 案例2:我想自驾旅游 则使用 Car 策略
const CarInstance = new Car();
const context = new Context(CarInstance);
context.handle();

// 案例3:旅游地方远时间也紧 则使用 Plain 策略
const PlainInstance = new Plain();
const context = new Context(PlainInstance);
context.handle();

总结

策略模式的核心是:通过一个 Context 类接收请求,将请求委托给具体的策略类

实际开发中使用策略模式的地方很多,比如表单验证有很多验证策略,比如页面要根据不同的用户显示不同的内容,这也是策略模式

JavaScript的内存模型

内存的生命周期

JavaScript作为一门高级编程语言,不像其他语言(例如C语言)需要开发者手动的去管理内存,在 JavaScript 中,系统会自动为你分配内存,在几乎任何一种语言中,内存的生命周期主要分三个阶段:

  • 内存分配:一般由操作系统分配内存,在有的语言中需要开发者手动操作
  • 使用内存:获得操作系统分配的内存后,在内存中发生读和写的操作
  • 释放内存:在程序使用完内存后,会将这部分内存释放掉供其他程序使用,在 JavaScript 中这一步由垃圾回收机制自动释放

栈内存和堆内存

JavaScript数据类型分两大类:基本类型、引用类型。
字符串、数字、布尔值等属于基本类型,对象类型都属于引用类型。

是一种先进后出、后进先出的数据结构,
举个例子,乒乓球盒子(先进后出,后进先出):

是一种树的结构,所以它是“无序”的,可以从任何地方将数据取出来,比如书架里的书

// 基本类型
const a = 'hello world';

// 引用类型
const obj = {}

栈内存中存的数据大小必须是固定的,所以基本类型都会存储在栈内存中,这种存储方式属于简单存储,也叫静态内存,所以它的效率是很高的。

堆内存中存的是引用类型的对象数据,比如 JSON、Function、Array 等等,因为引用类型数据没有固定大小,所以不能存在于栈内存,在运行时的访问方式是通过存在栈内存里的指针(地址)去堆里寻找对应的对象数据,也称它为动态内存,堆内存的效率是比栈内存要低。

我们在使用引用类型数据时,系统会在栈内存中存一个地址,该地址指向堆内存空间中你需要找的对象。

内存泄漏

常听说内存泄漏,到底什么是内存泄漏? 从本质上说,内存泄漏可以定义为:不再被应用程序所需要的内存,出于某种原因,它不会被GC回收

意想不到的全局变量

在 JavaScript 中,对于没有声明的变量,则会在全局范围中创建一个新的变量并对其进行引用,在浏览器中,全局对象是 window,例如:

function foo(arg) {
    bar = "some text";
    this.book = 'JavaScript';
}

等价于:

function foo(arg) {
    window.bar = "some text";
    window.book = 'JavaScript'
}

如果这种意外的全局变量过多,则会导致内存溢出

你可以使用更严格的use strict模式来避免它,或者你必须能确保将其指定为null

被遗忘的定时器和回调

常用的定时器,比如setInterval

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每五秒会执行一次

定时器里有对 id 为 renderer 的 DOM 进行引用,试想如果未来在某处将该 DOM 删掉,会导致定时器内部的代码变得不再需要,但由于定时器仍在运行,所以导致里面的处理程序所占内存不能被 GC 回收,也意味着 serverData 不能被回收。

所以我们在不适用定时器的时候,务必使用clearInterval将该定时器清除,断开其内部代码的引用,可以让 GC 收集和回收。

闭包的使用

闭包是内部函数使用外部函数的变量

let thing = null;

const fun1 = function () {
    const oThing = thing;
    const unused = function () {
        if(oThing) // 引用了oThing
            console.log('hi')
    }
    
    thing = {
        longStr: new Array(10000).join('*'),
        someMethod: function (){
            console.log('message')
        }
    }
}

setInterval(fun1, 1000)

unused函数内引用了外部的oThing,在fun1中调用了thing
thing中,为 someMethod 创建的作用域可以被 unused 共享。

该闭包的形成,阻止了oThing的回收。

而且在定时器中被循环执行,thing的内存大小将会稳定增长,而且每个作用域都间接引用了一个longStr这个大数组,造成了相当大的内存泄漏。

DOM的引用

const elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
}

function setSrc() {
    elements.image.src = 'http://exmaple.com/image_name.jpg'
}

function removeImage(){
    // image是body元素的直接子元素
    document.body.removeChild( document.getElementById('image') )
    // 此时,虽然 image 元素被删了,但全局对象中仍然引用#image,仍在内存中,GC无法收集它
}

git ssh秘钥

使用ssh-keygen命令生成 ssh 秘钥:

➜  ~ ssh-keygen

Generating public/private rsa key pair.
Enter file in which to save the key (/Users/aa/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/aa/.ssh/id_rsa.
Your public key has been saved in /Users/aa/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:vJ43VKHgqjiQVorBY4tZL6f2wXX6V8l6F/DjN8iLJ8o aa@AAdeMacBook-Pro
The key's randomart image is:
+---[RSA 3072]----+
|                 |
|        .   .    |
|.      . . . .   |
|.+..   .. . o    |
|+==.  ..S  o +   |
|*+..o..o .. + +  |
|.. =o.. .. o...o |
|  = .. o..=.o+o..|
| . o.   +E.+oo...|
+----[SHA256]-----+

查看刚才生成的秘钥:

➜  ~ ls ~/.ssh
id_rsa      id_rsa.pub  known_hosts
  • id_rsa: 密钥文件
  • is_rsa.pub: 公钥文件

一般 ssh 需要公钥,查看公钥:

➜  ~ cat ~/.ssh/id_rsa.pub

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJthVQyVKP+4gGIyTH9zrd/0Ak2n0PAnHED71VZx/HgdOue/HfzXVgfO2pLnq/rzO9W/GUEOetdOwyhDRsB/x1bFGvYRn6dKXQh8I949pc4uj5jHG6s0eiKG3e4Kahckxj8y0LxQ1VG+erYQ05FeXo/27iFXRYe4At13GS2QHQ+a3Lbh6O0whl2qITzOWO8IIH3g1HLkjlPL8cmOz1gpgcqh1LQ/HjERIlxDiFtn1Y8J/G9R1RGh21lNaRbXX7rzGUfuaToB1kifNcz8VzDbpNZgXl8BFuFLfHMcpay113Y1kg588/ESh166MPDQngHp1YjkZLNgDwQsXJ+qyTjnfi3kOo56loCr+y653BtJqSa86f7n/tTdKw7lCicRhDzrFHRNV9lBApIp7dAfdIV03HuKHJ5JRBtCKNRN/mziWaxi0oE1IZdPK76Ehzh0FuPRpe8fbTc8qir6xYnMGOthmMcOtwAoWi+Lcb4JIekRcLsJo9F05xUzvNj3KxwSteehU= aa@AAdeMacBook-Pro

以 github 为例,进入用户-> settings -> SSH and GPG keys,新建一个 ssh key,将刚才的 ssh 公钥复制进去即可

Nodejs核心模块简介

学习nodejs必须要掌握其核心,就像学JavaScript必须掌握函数、对象、数据类型、BOM、DOM等。

nodejs核心也不少,这里介绍几个核心:Events模块、fs模块、stream的使用、http模块。

Events

事件驱动、非阻塞异步IO是nodejs的特点,所以Events是非常重要的模块。并且node中绝大多数模块都继承了Events

事件是发布订阅模式的实现。在浏览器中,比如click事件

$('button').on('click',()=>{
    //处理click响应
})

当然你也可以自定义事件:

//自定义事件
$('div').bind('hello',()=>{
    alert('hello')
});

//触发事件
$('div').trigger('hello');

那么在 node 中如何使用 Events 模块的呢?
我们定义一个类,让它继承 Events

const EventEmit = require('events');

//定义一个播放器类
class Player extends EventEmit { }

const player = new Player();

//定义播放事件
player.on('play', ( param )=>{
    console.log(`播放器播放《${param}》`)
})

play.emit('play','海阔天空'); // 播放器播放《海阔天空》
play.emit('play','七里香');   // 播放器播放《七里香》

如果你想让事件只执行一次,可以使用 once :

//定义播放事件
player.once('play', ( param )=>{
    console.log(`播放器播放《${param}》`)
})

play.emit('play','海阔天空'); 
play.emit('play','七里香');  

此时 只会打印出 播放器播放《海阔天空》

上面也说了 node 中绝大多数模块都继承了 Events,比如 stream、fs、http等等,它们就像浏览器里的 click,是原生就有的,如果你接着往下看 会对发现很多用到事件的地方。

fs文件系统模块

fs 全拼是 file system文件系统
既然是文件系统,它的主要作用就是操作文件,比如文件的新增、修改内容、读写文件内容等。

fs.stat 获取文件夹及文件相关信息

查看文件夹信息

fs.stat('./logs', (err, stats) => {
  if (err) {
    console.log(err);
    return;
  }

  console.log('目录:', stats.isDirectory());
  console.log('文件:', stats.isFile());
  console.log('大小:', stats.size);
});

//打印结果
目录: true
文件: false
大小: 160

查看文件信息

fs.stat('./fs.js', (err, stats) => {
  if (err) {
    console.log(err);
    return;
  }

  console.log('目录:', stats.isDirectory());
  console.log('文件:', stats.isFile());
  console.log('大小:', stats.size);
});

//打印结果
目录: false
文件: true
大小: 2087

fs.mkdir 创建文件夹

// 创建目录
fs.mkdir("./logs", err => {
  if (err) {
    console.log(err);
    return;
  }
  console.log("logs目录创建成功");
});

fs.mkdir 是异步方法,如果你想同步创建可以使用 fs.mkdirSync

fs.mkdirSync('./logs2'); //同步创建文件夹

fs.writeFile 写入内容

fs.writeFile('./logs2/hello.log', '你好~', err => {
  if (err) {
    console.log(err);
    return;
  }
  console.log('写入成功');
});

注意: 若文件不存在则创建文件 若文件中有内容则覆盖

有时候我们不希望内容被覆盖,而是追加,那么可以使用 appendFile 方法。

fs.appendFile("./logs/hello.log", "hello~\n我是程序员", err => {
  if (err) {
    console.log(err);
    return;
  }
  console.log("写入成功");
});

fs.readFile 读取文件内容

// 读取文件内容
fs.readFile("./logs/hello.log", "utf8", (err, stats) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log(stats);
});

fs.readdir 读取文件夹

// 读取文件夹
fs.readdir("./logs", (err, files) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log(files); //返回一个包含所有文件名称的数组
});

//打印结果
[ 'data-write.json', 'data.json', 'traking.log' ]

fs.rename 修改文件名称

// 修改名称 把hello.log修改为tranking.log
fs.rename("./logs/hello.log", "./logs/traking.log", err => {
  if (err) {
    console.log(err);
    return;
  }

  console.log("改名成功");
});

删除目录下所有文件

// 删除目录下文件
fs.readdirSync("./logs").map(file => {
  // 删除文件unlink
  fs.unlink(`./logs/${file}`, err => {
    if (err) {
      console.log(err);
      return;
    }
    console.log("文件删除成功");
  });
});

删除文件夹

// 只能删除空目录 若目录里不为空则会报错 所以要先删除里面的文件 再删除文件夹
fs.rmdir("./logs", err => {
  if (err) {
    console.log(err);
    return;
  }
  console.log("目录删除成功");
});

Stream 流

流,可理解为水流。只不过这里是数据流。
流的意义在于三点:

  1. 有源头
  2. 有终点
  3. 从源头流到终点

使用 stream 读写文件

const fs = require('fs');

const fileReadStream = fs.createReadStream('./logs/data.json');
const fileWriteStream = fs.createWriteStream('./logs/data-write.json');


/* 通过文件流的事件方式 */
fileReadStream.on('data', chunk => {
  fileWriteStream.write(chunk); // 可写流写入文件 如果文件不存在则创建文件
});

fileReadStream.on('error', err => {
  console.log('错误:', err);
});

fileReadStream.on('end', () => {
  console.log('结束');
});

上面代码中建立了一个流:可读流--->可写流。它满足了上面说的三点:有源头(可读流)、有终点(可写流)、从源头到终点(一个文件里的数据流到了另一个文件里)。

代码中也能看出,Stream 其实也继承了 Events,它含有data、error、end等事件。

使用 pipe

我们把上面代码改一下

// 监听pipe事件
fileWriteStream.on('pipe', source => {
  console.log(source);
});

/* 通过pipe方式 */
fileReadStream.pipe(fileWriteStream);

pipe 可理解为水管,在可读流和可写流之间连接了水管,不需要再监听 data 事件,使用起来很方便 能达到同样的效果。

Http 模块

http 模块主要用于搭建 http 服务,处理用户请求信息等

使用 http 请求

const http = require('http');

// 使用发送http请求
const options = {
  protocol: 'http:',
  hostname: 'www.baidu.com',
  port: '80',
  method: 'GET',
  path: '/img/baidu_85beaf5496f291521eb75ba38eacbd87.svg'
};

let responseData = '';

const request = http.request(options, response => {
  console.log(response.statusCode); // 获取链接请求的状态码

  response.setEncoding('utf8');

  response.on('data', chunk => {
    responseData += chunk;
  });

  response.on('end', () => {
    console.log(responseData);
  });
});

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

request.end();

使用 http 创建服务

// 使用http创建服务器
const port = 3000;
const host = '127.0.0.1';

const server = http.createServer();

server.on('request', (request, response) => {
  response.writeHead(200, {
    'Content-Type': 'text/plain'
  });
  response.end('Hello World\n');
});

server.listen(port, host, () => {
  console.log(`Server running at http://${host}:${port}/`);
});

关于 Node 核心模块还有很多,比如 Buffer、crypto加密、global全局变量、net网络、os操作系统等等,只不过上面介绍的是使用频率非常高的模块。

后面文章会继续介绍,详情的api使用请参考官网

@babel/preset-env与@babel/plugin-transform-runtime

babel 在转译时,会将源码分为两个部分来处理,分别是: syntax 和 api

  • syntax:类似对象展开、optional chain、let、const 等语法
  • api:类似数组的 includes 等函数、方法

@babel/preset-env

{
    "presets": [ ["@babel/preset-env"] ]
}

preset-env可以使用最新的 JavaScript Api ,它是许多 preset 的集合(es2015+),通过配置来智能使用 JavaScript。

但随着 ECMA 的发展会一直更新和增加里面的内容,比如今年(2021年)它包含的预设由:es2020、es2019、... es2015。到了明年,它的 preset 可能就多包含一个 es2021

默认情况下,preset-env 跟 babel-preset-latest 是等同的;

在发中,如果需要支持特定的浏览器,可以通过 targets 来配置,preset-env 会根据配置生成相符合的代码:

{
    "presets":[
        ["@babel/preset-env", {
            "targets": {
                "chrome": 88
            }
        }]
    ]
}

合理的配置,能减少很多无用的代码;

对于 preset-env ,syntax 语法很容易就转好了,但 api 不会做任务处理,比如:

const 属于 syntax 语法,但 includes 并没有被转译。如果运行在不支持 includes 的浏览器中就会报错。

core-js

babel 使用 polyfill 来处理 api,@babel/preset-env 中有个配置选项 useBuiltIns,用来告诉 babel 如何处理 api,它的默认值是 false,即默认不处理任何 api。

{
    "presets":[
        ["@babel/preset-env",{
            "useBuiltIns": "usage"
        }]
    ]
}

useBuiltIns 还有一个选项是entry, 即在项目入口处把整个 polyfill 引入,这样会导致包非常大,而我们需要的仅仅是能支持 includes 而已。所以不用 entry 而使用 usage ,它会根据使用 api 的情况来按需加载需要用到的 polyfill 。这里的 polyfill 来自 core-js这个库,所以完整配置如下:

{
    "presets":[
        ["@babel/preset-env",{
            "useBuiltIns": "usage",
            "corejs" 3
        }]
    ]
}

@babel/plugin-transform-runtime

babel 在转译 syntax 时,会经常使用一些辅助函数来帮忙转译,比如 class 语法中,babel 自定义了 _classCallCheck 这个辅助函数;typeof 则被重新自定义了一个 _typeof 辅助函数。这些函数叫做 helpers,一个项目中如果每个文件都有这些函数,显然会不合理。

@babel/plugin-transform-runtime就是为了解决这个问题:

  • api 从之前的直接修改原型改为从一个统一模块中引入,避免全局变量和原型的污染
  • heplers 从之前在当前文件中定义,改为从一个统一的模块中引入,这样打包结果中每个 hepler 只会存在一个

使用 @babel/plugin-transform-runtime

yarn add @babel/plugin-transform-runtime @babel/runtime -D

然后配置一下

"plugins":[
        ["@babel/preset-env",{
            "useBuiltIns": "usage",
            "corejs" 3
        }]
]

总结

  1. babel 在转译过程中,对 syntax 语法的处里非常好,但有很多 api 是不转译的,比如数组的 includes 方法

  2. preset-env 转译 JavaScript ,可以通过 useBuiltIns 来设置 core-js ,用于解决 api 的 polyfill

  3. babel 转译时,会自定义一些 helpers 函数,可以通过 @babel/plugin-transform-runtime 来抽离这些 heplers 统一导入

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.