0%

关于使用ubuntu发行版和centos发行版的倾向(存目)

简单的ubuntu

镜像

实践日期:2019-12-29 ubuntu-18.04.3-live-server-amd64.iso

安装和分区(存目)

安装ssl

配置网卡

不知道怎么就安装了cloud image

参考Configure Ubuntu Server 18.04 to use a static IP address

在线安装Applications

配置apache

启用端口 /etc/apache2/ports.conf

配置/etc/sites-availiable/yoursite.conf
栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<VirtualHost *:90>
ServerName www.example.com

DirectoryIndex index.html
DocumentRoot /home/qqs/Workspace/csc_simulate/
<Directory /home/qqs/Workspace/csc_simulate>
Require all granted
</Directory>
Alias /viewer /home/qqs/Workspace/cs_meshviewer/build
<Directory /home/qqs/Workspace/cs_meshviewer/build>
Require all granted
</Directory>

<Location /viewer>
DirectoryIndex index.html
</Location>

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

ProxyPreserveHost On
ProxyPass /download http://10.196.98.58:3000/download
ProxyPassReverse /download http://10.196.98.58:3000/download
</VirtualHost>

注意 并不是唯一的,应根据需要定义路径的访问控制,Difference between Directory and Location
启用和禁用site
1
2
a2ensite yoursite.conf
a2dissite yoursite.conf

启用和禁用模块

1
2
3
4
5
6
7
8
// 关于反向代理
a2enmod proxy
a2enmod proxy_http
a2enmod proxy_balancer
a2enmod lbmethod_byrequests
// 关于虚拟路径
a2enmod alias
// 禁用:a2dismod

启用、停用和重启服务
1
systemctl start|stop|reload apache2

Issue 403 You don’t have permission to access this resource.

令人发狂的CentOS(Selinux)

镜像

实践日期:2019-12-29 CentOS-8-x86_64-1905-dvd1.iso

网卡设置

1
2
3
4
5
6
ip add
nmcli device show
nmcli device status
nmcli connection down enp0s3
nmcli connection up enp0s3
nmcli c reload

静态IP

/etc/sysconfig/network-scripts/ifcfg-enp0s3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none # 默认是dhcp,根据dhcp分配 改为none或static
NAME=enp0s3
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=enp0s3
UUID=0b813e18-6008-485c-ba8c-d19e259a847a
DEVICE=enp0s3
ONBOOT=yes # 开机启用该配置
IPADDR=10.196.98.99
GATEWAY=10.196.98.1
NETMASK=255.255.254.0
DNS1=10.192.0.100
PREFIX=24

防火墙

1
2
3
4
5
systemctl stop firewalld.service #关闭
systemctl start firewalld.service #启动

firewall-cmd --zone=public --permanent --add-port 22/tcp #开启端口
systemctl restart firewalld.service #重启

关闭selinux

1
2
sestatus // 查看状态
setenforce 0 // 临时关闭

禁用selinux

编辑/etc/selinux/config, set SELINUX=disabled。

概念

存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。

存储过程是为了完成特定功能的SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。

存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

  • 存储过程可封装,并隐藏复杂的商业逻辑。

    当dba与后台开发分离时,代码将以call xStoredProcedure 的形式访问数据库
  • 存储过程可以回传值,并可以接受参数。
  • 存储过程,往往定制化于特定的数据库上,因为支持的编程语言不同。当切换到其他厂商的数据库系统时,需要重写原有的存储过程。

    使用存储过程 or 使用 DAO SQL

    Stored Procedure|DAO SQL
    :——-|:——
    数据操作与底层业务解耦,以提供多方使用|不同服务做类似查询需要各自实现
    版本控制与代码分离|一致地版本控制
    仅需开发select和execuate权限|底层应用掌握较高地数据库权限
    增加了数据库运算压力|
    无法应对分表等业务扩展|扩展灵活
    数据库层面拼接查询或无法使用到索引|
    法国人说,使用存储过程封装数据操作主要是因为第一条地目的,然而私以为解耦数据操作和业务的话添加基础服务或中间件更好一些

    语法

    0 语句结构

    修改语句结束符,从分号 ; 临时改为两个 $$,使得过程体中使用的分号被直接传递到服务器,而不会被客户端(如mysql)解释。
    1
    2
    3
    DELIMITER $$

    DELIMITER //
    过程体,类似其他变成语言中{语句块}
    1
    BEGIN .... END    

    1 变量

    1
    2
    3
    4
    5
    -- 存储过程变量,只能在存储过程定义中使用
    DECLARE l_int int unsigned default 4000000;
    -- 用户变量,可以在会话任意位置使用
    SET @p_in=1
    SELECT 'Hello World' into @x;
    赋值多个变量
    1
    select col1, col2, col3 into a, b, c from t limit 1;

    2 函数

    1

    3 入参出参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    CREATE PROCEDURE 存储过程名([[IN |OUT |INOUT ] 参数名 数据类形...])

    set @p_in=1;
    -- 假设STOREDPROCEDURE1是带一个入参的存储过程
    call STOREDPROCEDURE1(@p_in);
    -- 入参可以是字面量
    call STOREDPROCEDURE1(1);


    条件分支

    1
    2
    3
    4
    5
    if a>b then
    select a;
    else
    select b;
    end if;
    1
    2
    3
    4
    5
    6
    7
    8
    case type
    when 0 then
    insert into t values(101);
    when 1 then
    insert into t values(11);
    else
    insert into t values(1);
    end case

    循环

    1
    2
    3
    while i<10 do
    insert into t values(i)
    end while
    1
    2
    3
    4
    5
    6
    7
    8
    set v=0;  
    LOOP_LABLE:loop
    insert into t values(v);
    set v=v+1;
    if v >=5 then
    leave LOOP_LABLE;
    end if;
    end loop;

    游标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    create procedure asset_owner_clean()
    begin
    declare _id int; -- asset list primary key
    declare targetid int; -- target user employee ID
    declare asset_owner varchar(80);
    declare asset_ownerid int;
    declare done int default false;
    -- set cursor traverse asset list whose owner is not null
    declare asset cursor for
    (select asset_id,owner,owner_id from all_asset where
    (owner is not null and owner <>'') or (owner_id is not null and owner_id <>''));

    declare continue handler for not found set done = true;
    open asset;
    checkloop: loop
    fetch asset into _id,asset_owner,asset_ownerid;
    SELECT _id, asset_owner, asset_ownerid;
    if done then
    leave checkloop;
    end if;
    -- loop body start
    <insert statements>
    -- loop body end
    end loop checkloop;
    close asset;
    commit;
    end
    注意:跳出循环的句柄是not found,意味着循环体中将empty set赋值给变量会跳出loop

    如 selete * into _list where 1=2;

SQL Server中调用存储过程的语法稍有不同, 见文章TransactSQL

  • https本身没有端口限制
  • https支持传输ssl证书以避免服务端仿冒
  • https报文使用对称加密 密钥包含在证书中

postman的”验证ssl证书”开关: settings -> General -> SSL certificate verification

ssl certificate warning

ssl warning

  • SSL证书不是来自公认的证书颁发机构(CA)
  • 数字证书信任链配置错误
  • 证书的域名匹配程度不完整
  • 证书已经过了有效期
  • 客户端不支持SNI协议
    由于第一条,开发环境的带自签名的https服务仅被本地认证,远程访问就会出现warning

生成自签名证书

信任证书

实现局域网访问HTTPS

数据分为持久化和非持久化两类。对于容器,非持久化数据从属其容器,生命周期与容器相同,删除容器时也会删除所有非持久化数据。如果希望自己的容器数据保存下来,需要将数据存储在卷上,卷与容器是解耦的,可以独立地创建并管理。

在主机上创建Docker的卷,实质上是创建一个目录,使之能且只能被Docker进程修改,除了‘卷’,可使用绑定装载的方式在主机和容器间共享文件

卷命令

1
2
3
4
5
docker volume create myvol
docker volume ls // 查看已创建地卷
docker volume inspect myvol // 查看指定卷详细信息
docker volume rm myvol // 删除指定卷
docker volume prune // 删除所有未装入容器使用的卷

装入容器

-v, —volume

1
docker run -d --name myjenkins -p 8080:8080 -p 50000:50000 -v /var/jenkins_home:/var/jenkins_home --restart always jenkins

用:分隔的3个field,卷名称 : 路径或文件 : 选项,通常只保留卷路径

ro 即 readonly

原文:

In the case of named volumes, the first field is the name of the volume, and is unique on a given host machine. For anonymous volumes, the first field is omitted.

The second field is the path where the file or directory will be mounted in the container.

The third field is optional, and is a comma-separated list of options, such as ro.

上例中,-v /var/jenkins_home:/var/jenkins_home 两个field前者是宿主路径,后者是容器路径,即将jenkins用户数据保存到容器的/var/jenkins_home同时持久化到本地位于/var/jenkins_home的卷中

引述 jenkins官网

-v $HOME/jenkins:/var/jenkins_home would map the container’s /var/jenkins_home directory to the jenkins subdirectory within the $HOME directory on your local machine, which would typically be /Users//jenkins or /home//jenkins.

—mount

1
2
docker run --name myjenkins -p 8080:8080 -p 50000:50000 \ 
--mount source=jenkins_home,target=/var/jenkins_home,driver=local --restart always jenkins

用 , 分隔三个字段,卷名称,目标路径或文件,驱动。

B站教程 RabbitMQ

意义

  • 异步处理
    例如 注册—保存成功—发送邮件 的流程,MQ接管非必要操作,拓展并行处理能力
  • 应用解耦
    使用发布订阅模式,消息上下游之间解除依赖关系
  • 流量削峰
    秒杀业务,前端请求塞到MQ,将筛选过的action选择性地交给biz层(业务层)
  • 日志处理

    Message Queue

    RabbitMQ is one of the most popular open source message brokers. RabbitMQ is lightweight and easy to deploy on premises and in the cloud. It supports multiple messaging protocols.

Erlang,具备函數語言特色的程序设计语言

installation

RPM Manual:
(Erlang依赖 + RabbitMQ)
参考官网 Tutorials

  • Before installing RabbitMQ, you must install a supported version of Erlang/OTP. Zero-dependency Erlang RPM for RabbitMQ : this is a (virtually) zero dependency 64-bit Erlang RPM package that provides just enough to run RabbitMQ. It may be easier to install than other Erlang RPMs in most environments. It may or may not be suitable for running other Erlang-based software or 3rd party RabbitMQ plugins.
  • other dependencies : socat, logrotate
  • download and install rabbit-server.rpm(https://www.rabbitmq.com/install-rpm.html#downloads)
1
2
3
4
5
cd /usr/local/src/
mkdir rabbitmq
cd rabbitmq


在ubuntu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/sh

sudo apt-get install curl gnupg debian-keyring debian-archive-keyring apt-transport-https -y

## Team RabbitMQ's main signing key
sudo apt-key adv --keyserver "hkps://keys.openpgp.org" --recv-keys "0x0A9AF2115F4687BD29803A206B73A36E6026DFCA"
## Cloudsmith: modern Erlang repository
curl -1sLf https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/gpg.E495BB49CC4BBE5B.key | sudo apt-key add -
## Cloudsmith: RabbitMQ repository
curl -1sLf https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/gpg.9F4587F226208342.key | sudo apt-key add -

## Add apt repositories maintained by Team RabbitMQ
sudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF
## Provides modern Erlang/OTP releases
##
deb https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu bionic main
deb-src https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu bionic main

## Provides RabbitMQ
##
deb https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu bionic main
deb-src https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu bionic main
EOF

## Update package indices
sudo apt-get update -y

## Install Erlang packages
sudo apt-get install -y erlang-base \
erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
erlang-runtime-tools erlang-snmp erlang-ssl \
erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl

## Install rabbitmq-server and its dependencies
sudo apt-get install rabbitmq-server -y --fix-missing

  • 启动/关闭 /sbin/service rabbitmq-server start/stop
  • 开机启动 chkconfig rabbitmq-server on

Docker Image

1
2
docker pull rabbitmq
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management

启用Management Plugin (网页管理界面)
1
2
3
4
5
sudo rabbitmq-plugins enable rabbitmq_management
sudo service rabbitmq-server restart
# 创建网页管理用户
sudo rabbitmqctl add_user QQs 123456
sudo rabbitmqctl set_user_tags QQs administrator

访问 http://{node-hostname}:15672/ 对RabbitMQ-Server进行可视化管理

授权,认证 以及 Access Control

参考官网 Tutorials

默认用户guest/guest只被允许本地登录

1
2
3
4
5
6
// 创建用户qqs/pass1234
sudo rabbitmqctl add_user qqs pass1234
// 用户列表
sudo rabbitmqctl list_users
// 授权管理员
sudo rabbitmqctl set_user_tags qqs administrator

vhost 是 RabbitMQ 控制权限的最小粒度
注意,/是默认的vhost,缺省了名称。
1
2
3
4
5
rabbitmqctl list_vhosts
rabbitmqctl add_vhost test_vhost
// set_permissions [-p <vhost>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p / qqs ".*" ".*" ".*"
rabbitmqctl set_permissions -p test_host qqs ".*" ".*" ".*"

client demos

见 Github rabbitmq tutorials

javascript client demo:

package

1
npm i amqplib

send.js
node
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

var amqp = require('amqplib/callback_api');

amqp.connect('amqp://qqs:pass1234@111.111.111.111:5672/', function(error0, connection) {
if (error0) {
throw error0;
}
connection.createChannel(function(error1, channel) {
if (error1) {
throw error1;
}

var queue = 'hello';
var msg = 'Hello World!';
// 订阅
channel.assertQueue(queue, {
durable: false
});
// 发布
channel.sendToQueue(queue, Buffer.from(msg));

console.log(" [x] Sent %s", msg);
});
setTimeout(function() {
connection.close();
process.exit(0);
}, 500);
});


receive.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env node

var amqp = require('amqplib/callback_api');

amqp.connect('amqp://qqs:pass1234@111.111.111.111:5672/', function(error0, connection) {
if (error0) {
throw error0;
}
connection.createChannel(function(error1, channel) {
if (error1) {
throw error1;
}

var queue = 'hello';

channel.assertQueue(queue, {
durable: false
});

console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);

channel.consume(queue, function(msg) {
console.log(" [x] Received %s", msg.content.toString());
}, {
noAck: true
});
});
});

定时任务

原理是:消息设置过期时间放入队列1,过期移入队列2,订阅者监听队列2

AMQP

高级消息队列协议(Advanced Message Queue Protocol) AMQP实体:队列、交换机、绑定

  • 默认交换机 根据消息携带的route key直接路由到同名的队列

案例

某年月日开发AMS系统,使用公司SSO服务提供的账号密码,用户在AMS前端登录页填写账号密码,AMS后台有记录post请求的requestbody的功能,于是我得到了用户的SSO账号密码。。。

对于本AMS系统,应视为使用第三方账号登录,类似使用wechat或github账号登录,点击入口后弹出第三方页面,通过彼系统认证返回客户端(browser)授权码,本系统凭此授权码访问与第三方有关的资源,通常可能只是从第三方获取用户名、头像、邮箱来填充本系统的个人信息识别.
stacklogin

gitlogin

此外在CS Scanflow登录Cloud地模块中,设计期望以账密登录后返回两个token,其中A是访问Cloud相关资源地凭据,B是保持登录(Remember me)需要token,当A过期时,调用接口,Cloud认证B合法,返回新的A,即可在后续使用A继续访问Cloud资源。

OpenID

OpenID 去中心化的身份识别框架,根据协议,任何网站可以作为identity provider,通过一串 URI 向某个网站证明用户的身份。

OAuth标准

OAuth在”客户端(受限资源/服务请求方)”与”服务提供商”之间,设置了一个授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,使用授权层颁发的令牌(token),访问服务提供商的资源。用户注册在服务端的账户密码,不会暴露给客户端。服务提供商可以自由限制授权层令牌的权限范围和有效期。

QQs:OAuth不强调认证,它是一个授权协议,实现的是支持由第三方提供授权访问的标准。

OIDC

OpenID Connect 基于OAuth 2.0协议之上的简单身份层,它允许客户端根据授权服务器的认证结果最终确认终端用户的身份,以及获取基本的用户信息;它支持包括Web、移动、JavaScript在内的所有客户端类型去请求和接收终端用户信息和身份认证会话信息;它是可扩展的协议,允许你使用某些可选功能,如身份数据加密、OpenID提供商发现、会话管理等。

授权方式

授权码(authorization code)

上文已提到的使用第三方登录的方式即授权码方式,授权码方式是最常用且靠谱的授权方式,相比之下,其余三种比较扯淡。

下图中A.com实为客户端,如某电商平台,B.com为用户信息持有方,如某社交平台(包含了受限数据服务以及授权服务),经过对接A.com可以通过如图的流程使用B.com的用户登录
authcodemode

  1. 跳转到B授权页
  2. 确认授权,重定向回A,并带回authorization code
  3. 向B请求访问受限数据,传递A身份,以及授权码
  4. 请求返回可访问受限数据令牌
    隐藏式(隐式授权)
    直接返回令牌,安全风险较高 注1
    密码式
    返回账号密码,A以此申请访问B的令牌,风险更高。(私以为,既然B予以这种信任,即将访问权限控制让与A,A可以直接以账密登录B而不用可能受限的token)
    凭据式
    对于没有前端的命令行应用,以get请求,用query parameters传参直接得到令牌。

    前后端分离的SSO

    互联网图片侵删

    关于OAuth, OpenId, OIDC什么的

    网上有文章说OAuth是authorization, OpenId是authentication,这听起来很谜。。
    OpenID是一个去中心化的网上身份认证系统。(维基百科)

所谓认证系统,解决的是”你是谁”的问题,用户在在identity provider(idp)的服务上注册,客户端登录即去idp获取OpenID标识对应的token,服务提供者校验身份,是拿客户端的token去idp确认。
OIDC(OpenID Connect)OpenID + OAuth2.0认证(授权访问)服务

access token 是客户端和资源服务器之间的凭据

那access token在资源服务器上是如何验证的呢?每个请求过来,资源服务器都会拿access token到SSO上去核对吗?

事实上是不一定的。以JWT为例。如果Access Token是JWT形式签发,资源服务可以使用验证签名的方式判断是否合法,只需要把签名密钥在资源服务同步一份即可。典型的是使用非对称加密(见加密篇),资源服务保留一份公钥,access token由授权服务使用私钥签发,资源服务是可以对其进行校验的。JWT允许携带一些信息,如用户,权限,有效期等,因此资源服务判断JWT合法之后可以继续根据携带信息来判断是否可访问资源。这样就有可以快速验证有效性,不需要频繁访问授权服务的优点,缺点是Access Token一旦签发,将很难收回,只能通过过期来失效。

access token, id token, refresh token

在Google,微博等认证门户登录成功后 颁发id token, 表示该账户通过认证, 是可以被信赖的, id token中包含用户的名字,邮箱等信息,可以个性化用户体验(personalize user experience)如在UI上显示用户姓名,在生日当天发送祝福消息等 总之与认证有关与授权无关

access token用作访问受限的资源,即身份认证授权服务器授权客户端访问某受保护的资源,为其颁发access token,在资源服务器(作为audience)上验证,access token不绑定客户端,因此可以copy出来使用,也就是客户端有责任保护自己的access token安全

可以说access token用于资源服务器,而不是客户端,与id token不同,它没有什么要告诉客户端的,包括账户是否已通过认证,事实上,账户退出,access token依然可以工作

refresh token用以刷新access token 这对于SPA可能不足以保证refresh token的安全,可以使用refresh token的轮换机制,即在access token刷新后更新refresh token,使原refresh token不会再被攻击者利用blabla
refresh token, what are they and when to use them

需求:导出列表

一个服务端分页的可检索列表,页面只缓存当前页的数据,导出功能无法完全在前端,点击export将现有检索条件传到后端,由后端查询数据库并生成excel文件,传到前端。

后端实现(Express.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
app.post('/list/export', (req,res)=>{
const data = getDataFromDB(req.body)
generateExcel(results, ()=>{
res.download(path.join(__dirname,'list.xlsx'));
})
}, err => {
res.json(err)
})
})

const columns = [
{ header: 'ID', key: 'id', width: 32 },
{ header: 'Name', key: 'name', width: 32 }];
const generateExcel = function(data, callback){
const excel = require('exceljs');
// create excel workbook
var options = {
filename:path.join(__dirname, 'list.xlsx'),
useStyles: true,
useSharedStrings: true
};
const workbook = new excel.stream.xlsx.WorkbookWriter(options);
workbook.creator = 'QQs';
workbook.created = new Date();
workbook.modified = new Date();
// views
workbook.views = [
{
x: 0, y: 0, width: 10000, height: 20000,
firstSheet: 0, activeTab: 1, visibility: 'visible'
}
]
// add worksheet
const sheet = workbook.addWorksheet('List');
// define columns
sheet.columns = columns;
// add rows
/*
数据量大的情况下考虑到nodejs内存分配瓶颈
*应限制每次select的条数分批addRow并且Row.commit
*/
data.forEach(record => {
const row = record;
// TODO data convertor
sheet.addRow(row).commit();
});
sheet.commit();
workbook.commit().then(callback);
}

生成Excel用到了第三方库exceljs,该库实现了流式写excel的方法,可以在数据量较大的情况下缓解IO压力(待考证)

response.download(filename)方法以Blob方式返回数据

前端实现(Angular8)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export() {
const postParams = new Object();
// TODO collect query parameters
this.httpClient
.post('list/export', postParams, {
responseType: 'blob',
headers: new HttpHeaders().append('Content-Type', 'application/json'),
})
.subscribe(res => {
this.downloadFile(res);
});
}
/**
* 创建blob对象,并利用浏览器打开url进行下载
* @param data 文件流数据
*/
downloadFile(data) {
// 下载类型 xls
const contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const blob = new Blob([data], { type: contentType });
const url = window.URL.createObjectURL(blob);
// 打开新窗口方式进行下载
// window.open(url);

// 以动态创建a标签进行下载
const a = document.createElement('a');
a.href = url;
// a.download = fileName;
a.download = 'list.xlsx';
a.click();
window.URL.revokeObjectURL(url);
}

接受请求必须设置response headers,否则默认设置无法取得返回值并进入next回调。

Express

集合式开发框架,自身已经集成了诸多常用中间件如,router, static, bodyparse等。然而随着开发需求的丰富,仍然不免要在github上调查采集。

某年月日,基于express.js的服务,使用了morgan格式化日志,如何记response body是个难题,因为一旦response.end()或者response.json(x),其发送的内容将不复存在。

express 的中间件机制是线性执行的

受限于当时的js标准,异步过程只能以回调的方式实现,如果顺位在后的中间件返回结果到前一个,必须在调用后者时就带上回调函数, 当中间件“链”很长时,就成了Callback Hell

于是作者(TJ Holowaychuk大神)又写了一个Koa,实现洋葱模型的中间件机制。

Koa

据说是轻量级、渐进式框架。

Egg

阿里基于Koa统一规范化常用服务及功能,给出较为详细的文档说明,势必免去开发者一些调查采集的工作。