将 Mybatis/MongoDB 集成到 SpringBoot 中的示例

前置要求

本文假设你已经了解或知道以下技能,尤其而且是勾选的内容。

  • Gradle
  • SpringBoot
  • Mybatis Plus
  • MongoDB
  • SpringBoot MongoDB Data
  • H2DB
  • SpringTest

场景

GitHub 项目, Blog 教程

需要同时使用 Mybatis-PlusMongoDB,所以就去了解了一下如何集成它们。

集成 Mybatis Plus

创建 SpringBoot 项目

使用 SpringIO 创建 SpringBoot 项目,初始依赖选择 web, h2 两个模块,gradle 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.rxliuli.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

注:数据库吾辈这里为了简单起见直接使用了 H2DB,真实项目中可能需要配置 MySQL 之类。为了简化项目依赖配置文件,所以使用了 Gradle 而非 Maven。

引入 Mybatis-Plus 和 MongoDB 依赖

build.gradle 中引入 mybatis-plus-boot-starter 依赖

1
2
3
dependencies {
implementation group: 'com.baomidou', name: 'mybatis-plus-boot-starter', version: '3.0.7.1'
}

添加测试数据库

src/resources 下创建两个 sql 文件 schema-h2.sqldata-h2.sql,简单的使用 H2DB 创建数据库/表并添加数据以供测试使用。

数据库结构:schema-h2.sql

1
2
3
4
5
6
7
8
9
create schema spring_boot_mybatis_plus_mongo;
use spring_boot_mybatis_plus_mongo;

create table user_info (
id bigint primary key not null,
name varchar(20) not null,
age tinyint not null,
sex bool not null
);

数据库测试数据:data-h2.sql

1
2
3
4
use spring_boot_mybatis_plus_mongo;

insert into user_info (id, name, age, sex) values (1, 'rx', 17, false);
insert into user_info (id, name, age, sex) values (2, '琉璃', 18, false);

配置 Mybatis Plus

application.yml 中添加数据源配置

1
2
3
4
5
6
7
# DataSource Config
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath*:db/schema-h2.sql
data: classpath*:db/data-h2.sql
url: jdbc:h2:mem:test

添加一些实体/Dao/Service

用户信息实体类:com.rxliuli.example.springbootmybatisplusmongo.entity.UserInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@TableName("user_info")
public class UserInfo implements Serializable {
@TableId
private Long id;
@TableField
private String name;
@TableField
private Integer age;
@TableField
private Boolean sex;

public UserInfo() {
}

public UserInfo(Long id, String name, Integer age, Boolean sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}

// getter()/setter()
}

用户信息 Dao:com.rxliuli.example.springbootmybatisplusmongo.dao.UserInfoDao

1
2
3
@Repository
public interface UserInfoDao extends BaseMapper<UserInfo> {
}

用户信息业务接口:com.rxliuli.example.springbootmybatisplusmongo.service.UserInfoService

1
2
public interface UserInfoService extends IService<UserInfo> {
}

用户信息业务接口实现类:com.rxliuli.example.springbootmybatisplusmongo.service.impl.UserInfoServiceImpl

1
2
3
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoDao, UserInfo> implements UserInfoService {
}

配置 Mybatis Plus 扫描的路径

在启动类配置 Mybatis Plus,这点非常重要,以致于吾辈要单独列出,可能会出现的问题参见 踩坑 部分

1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan("com.rxliuli.example.springbootmybatisplusmongo.**.dao.**")
public class SpringBootMybatisPlusMongoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisPlusMongoApplication.class, args);
}
}

测试使用 Mybatis Plus 的 UserInfoService

测试 Mybatis Plus 中 IService 接口的 list() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserInfoServiceTest {
@Autowired
private UserInfoService userInfoService;

@Test
public void list() {
final List<UserInfo> list = userInfoService.list();
assertThat(list)
.isNotEmpty();
}
}

集成 MongoDB

引入 MongoDB Boot Starter

build.gradle 中引入 spring-boot-starter-data-mongodb 依赖

1
2
3
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
}

配置 MongoDB

application.yml 中添加 MongoDB 的配置,现在 application.yaml 应该变成了下面这样

1
2
3
4
5
6
7
8
9
10
11
# DataSource Config
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath*:db/schema-h2.sql
data: classpath*:db/data-h2.sql
url: jdbc:h2:mem:test
data:
# Integration mongodb
mongodb:
uri: mongodb://XXX:XXX@XXX:XXX/XXX

添加 Repository

定义一些简单操作的 Dao 接口:com.rxliuli.example.springbootmybatisplusmongo.repository.UserInfoLogRepository

1
2
3
4
5
6
7
8
9
10
@Repository
public interface UserInfoLogRepository extends MongoRepository<UserInfoLog, Long>, CustomUserInfoLogRepository {
/**
* 根据 id 查询用户日志信息
*
* @param id 查询的 id
* @return 用户日志
*/
UserInfoLog findUserInfoLogByIdEquals(Long id);
}

自定义更加复杂需求的 Dao 接口:com.rxliuli.example.springbootmybatisplusmongo.repository.CustomUserInfoLogRepository

1
2
3
4
5
6
7
8
9
public interface CustomUserInfoLogRepository {
/**
* 根据一些参数查询用户信息列表
*
* @param userInfoLog 参数对象
* @return 用户信息列表
*/
List<UserInfoLog> listByParam(UserInfoLog userInfoLog);
}

具体的实现类:com.rxliuli.example.springbootmybatisplusmongo.repository.UserInfoLogRepositoryImpl

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
/**
* 数据仓库 {@link UserInfoLogRepository} 的实现类,但请务必注意,实现类继承的是 {@link CustomUserInfoLogRepository} 接口,而非本应该继承的接口
*/
public class UserInfoLogRepositoryImpl implements CustomUserInfoLogRepository {
@Autowired
private MongoOperations mongoOperations;

@Override
public List<UserInfoLog> listByParam(UserInfoLog userInfoLog) {
final Criteria criteria = new Criteria();
if (userInfoLog.getUserId() != null) {
criteria.and("userId")
.is(userInfoLog.getUserId());
}
if (userInfoLog.getLogTime() != null) {
criteria.and("logTime")
.gte(userInfoLog.getLogTime());
}
if (userInfoLog.getOperate() != null) {
criteria.and("operate")
.regex(userInfoLog.getOperate());
}
return mongoOperations.find(new Query(criteria), UserInfoLog.class);
}
}

配置 MongoDB 扫描的路径

修改启动类,添加 @EnableMongoRepositories 注解用以配置 MongoDB 扫描的 Repository 路径

1
2
3
4
5
6
7
8
@SpringBootApplication
@MapperScan("com.rxliuli.example.springbootmybatisplusmongo.**.dao.**")
@EnableMongoRepositories("com.rxliuli.example.springbootmybatisplusmongo.**.repository.**")
public class SpringBootMybatisPlusMongoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisPlusMongoApplication.class, args);
}
}

测试使用 MongoDB 的 UserInfoLogRepository

  1. 测试 UserInfoLogRepository 中由 MongoDB Data 自动实现的 findUserInfoLogByIdEquals() 方法
  2. 测试 CustomUserInfoLogRepository 中自定义复杂的 listByParam() 方法
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
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserInfoLogRepositoryTest {
@Autowired
private UserInfoLogRepository userInfoLogRepository;

/**
* 初始数据,最开始要运行一次
*/
@Test
public void insert() {
userInfoLogRepository.insert(Lists.newArrayList(
new UserInfoLog(1L, 1L, "登录", LocalDateTime.now()),
new UserInfoLog(2L, 1L, "退出", LocalDateTime.now()),
new UserInfoLog(3L, 2L, "登录", LocalDateTime.now()),
new UserInfoLog(4L, 3L, "退出", LocalDateTime.now())
));
}

@Test
public void findUserInfoLogByIdEquals() {
final UserInfoLog result = userInfoLogRepository.findUserInfoLogByIdEquals(1L);
assertThat(result)
.isNotNull();
}

@Test
public void listByParam() {
final UserInfoLog userInfoLog = new UserInfoLog(null, 1L, "登",
LocalDateTime.parse("2019-02-22T08:22:16.000Z", DateTimeFormatter.ISO_DATE_TIME));
final List<UserInfoLog> result = userInfoLogRepository.listByParam(userInfoLog);
assertThat(result)
.isNotEmpty()
.allMatch(log ->
Objects.equals(userInfoLog.getUserId(), log.getUserId())
&& log.getOperate().contains(userInfoLog.getOperate())
&& log.getLogTime().isAfter(userInfoLog.getLogTime())
);
}
}

同时使用 Mybatis Dao 和 MongoDB Repository

在 Service 中添加方法

用户信息业务接口:com.rxliuli.example.springbootmybatisplusmongo.service.UserInfoService

1
2
3
4
5
6
7
8
public interface UserInfoService extends IService<UserInfo> {
/**
* 获取用户信息与用户日志的映射表
*
* @return 以 {@link UserInfo} -> {@link List<UserInfoLog>} 形式的 {@link Map}
*/
Map<UserInfo, List<UserInfoLog>> listUserInfoAndLogMap();
}

用户信息业务接口实现类:com.rxliuli.example.springbootmybatisplusmongo.service.impl.UserInfoServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoDao, UserInfo> implements UserInfoService {
@Autowired
private UserInfoLogRepository userInfoLogRepository;

@Override
public Map<UserInfo, List<UserInfoLog>> listUserInfoAndLogMap() {
final List<UserInfo> userInfoList = list();
final List<UserInfoLog> userInfoLogList = userInfoLogRepository.findAll();
final Map<Long, List<UserInfoLog>> map = userInfoLogList.stream().collect(Collectors.groupingBy(UserInfoLog::getUserId));
return userInfoList.stream()
.collect(Collectors.toMap(user -> user, user -> map.getOrDefault(user.getId(), Collections.emptyList())));
}
}

添加简单的 RestAPI 进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/api/user-info")
public class UserInfoApi {
@Autowired
private UserInfoService userInfoService;

@GetMapping("/list")
public List<UserInfo> list() {
return userInfoService.list();
}

@GetMapping("/list-user-info-and-log-map")
public Map<String, List<UserInfoLog>> listUserInfoAndLogMap() {
return userInfoService.listUserInfoAndLogMap().entrySet().stream()
.collect(Collectors.toMap(kv -> JsonUtil.toJson(kv.getKey()), Map.Entry::getValue));
}
}

测试 RestAPI

现在,我们启动项目并打开浏览器,应当可以在以下地址看到对应的 JSON 数据

  • 用户信息列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [
    {
    "id": 1,
    "name": "rx",
    "age": 17,
    "sex": false
    },
    {
    "id": 2,
    "name": " 琉璃 ",
    "age": 18,
    "sex": false
    }
    ]
  • 用户信息及对应日志映射表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    "{\"id\":\"1\",\"name\":\"rx\",\"age\":17,\"sex\":false}": [
    {
    "id": 1,
    "userId": 1,
    "operate": " 登录 ",
    "logTime": "2019-02-22T16:22:16.099"
    },
    {
    "id": 2,
    "userId": 1,
    "operate": " 退出 ",
    "logTime": "2019-02-22T16:22:16.099"
    }
    ],
    "{\"id\":\"2\",\"name\":\"琉璃 \",\"age\":18,\"sex\":false}": [
    {
    "id": 3,
    "userId": 2,
    "operate": " 登录 ",
    "logTime": "2019-02-22T16:22:16.099"
    }
    ]
    }

踩坑

  1. Mybatis Plus 扫包范围
    使用 @MapperScan 限制 Mybatis Plus 扫描 Dao 的范围,注意不要扫到 MongoDB 的 Repository,否则会抛出异常

    1
    Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'userInfoLogRepository' defined in null: Cannot register bean definition [Root bean: class [org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] for bean 'userInfoLogRepository': There is already [Generic bean: class [org.mybatis.spring.mapper.MapperFactoryBean]; scope=singleton; abstract=false; lazyInit=false; autowireMode=2; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:\Text\spring-boot\spring-boot-mybatis-plus-mongo\out\production\classes\com\rxliuli\example\springbootmybatisplusmongo\repository\UserInfoLogRepository.class]] bound.

    原因是在 SpringMongoData 处理之前 Mybatis Plus 先扫描到并进行了代理,然后就会告诉你无法注册 SpringMongoData 相关的 Repository

  2. 使用 @EnableMongoRepositories 限制 SpringMongoData 扫描的范围

    既然说到限制,自然也不得不说一下 SpringMongoData 本身,如果你已经使用了 @MapperScan 扫描 Mybatis 需要处理的 Dao,那么添加与否并不重要。但是,吾辈要说但是了,但是,如果你先使用的 MongoDB,那么如果没有使用 @MapperScan 处理 Mybatis 的 Dao 的话,就会抛出以下异常,所以为了安全起见还是都定义了吧

    1
    Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userInfoServiceImpl': Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.rxliuli.example.springbootmybatisplusmongo.dao.UserInfoDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

    说的是自动注入 BaseMapper 失败,实际上是因为 Mybatis 的 Dao SpringMongoData 无法处理。

  3. 最好使用不同的后缀名区分 Mybatis MapperMongo Repository,或者放到不同的包
    也是为了避免扫描混乱,出现 Mybatis 扫描到 Mongo Repository 或是 Mongo 扫描到 Mybatis Mapper 的情况,出现上面的那两个错误。


那么,关于在 SpringBoot 中同时使用 Mybatis Plus 和 MongoDB 的搭建就到这里啦

基于 nodejs 的热更新 http 开发服务器

场景

之前一直在使用 http-server 作为本地快速启动静态 http 服务器的命令行工具,然而直到今天,吾辈实在难以忍受其在修改完 HTML 文件后,http-server 不会自动刷新浏览器重新渲染页面,而是需要手动刷新才行,真的是不厌其烦,所以吾辈开始找更好的工具。

注:http-server 其实也已经热更新到内存中了,只不过不会触发浏览器刷新页面。

期望

  • 零配置使用
  • 修改文件保存后将自动触发浏览器刷新页面
  • 基于 nodejs 开发
  • 允许特定的配置

结果

前端页面热更新

  • live-server: 自带热更新并启动即打开浏览器的 http 开发服务器
  • anywhere: 与上面的 live-server 类似(由国人开发,已经一年没有更新了)

nodejs 热更新

  • nodemon: 文件修改后自动重启 nodejs 程序
  • supervisor: nodejs 程序运行管理器,包含热更新功能(两年没有更新了)

VSCode 插件

  • LiveServer: VSCode 中的插件,可以将任何一个 HTML 当作 web 程序打开,并自带热更新

live-server

live-server 是一个 npm 包,全局安装之后可以很方便的使用,所以吾辈选择了这个。主要特点如下:

  • 零配置
  • 热插拔
  • 自动打开浏览器

下面来说一下如何使用

  1. 使用 npm 全局安装

    1
    npm i -g live-server
  2. 跳转到指定目录,然后使用 live-server 即可启动 http 服务器

    1
    live-server

Windows 上强制粘贴

场景

前面吾辈曾经写过一篇 Chrome 强制复制粘贴 的文章,然而那篇内容仅仅只是针对于 Chrome/Firefox 浏览器。对于 Windows 的客户端软件,例如 QQ、阿里旺旺之类,它们还是不允许粘贴密码。这点对于所有密码都是用密码管理器管理,随机生成的用户而言(吾辈),实在是太过讨厌了一点!

解决思路

QQ 这种客户端是如何屏蔽粘贴功能的呢?很显然,QQ 不仅仅是禁用右键/快捷键那么简单,或许是添加键盘驱动了也说不定。但不管怎样,我们都可以从根本的地方下手 – 模拟键盘输入,将剪切版的文字一个一个的输入进去!

解决方案

虽然不像 Linux 那样任何操作都可以使用脚本去控制(实际上也可以,只不过 Windows 的 cmd 脚本实在不怎么样),然而基于 Windows 丰富的生态,还是有人做出了第三方的脚本语言 – Autohotkey

我们首先去 官网 看一下,介绍只有简单的两句话。

Powerful. Easy to learn.
The ultimate automation scripting language for Windows.

翻译过来就是:

强大,简单易学
Windows 上的自动化脚本语言

我们可以写一个 Autohotkey 自动化的脚本,在检测到 QQ 运行并且按下 CS-V 时将剪切版的字符逐个输入进去。

具体实现

1
2
3
4
5
6
7
8
9
#IfWinActive ahk_exe QQ.exe
{
;热键为 Ctrl+Shift+V
^+v::
;发送剪切版的内容到输入
SendInput {Raw}%Clipboard%
Return
}
#IfWinActive

当然,如果不喜欢安装 Autohotkey 的话也没关系,吾辈转换了一个 .exe 可执行文件,也可以直接下载使用啦

使用效果

使用示例

在 Windows 上使用 FTP/SFTP 服务端

场景

最近在做 WebService 项目时遇到了定时上传统计报表的需求。协议是 FTP/SFTP,然而第三方服务暂时无法集成,所以只能在本地使用软件模拟出 FTP/SFTP 服务端,然后在代码中进行测试。

前言

吾辈并未使用 Windows 上大名鼎鼎的 FileZilla
谜之音:FileZilla 开源免费,而且 FTP/SFTP/FTPS 都能支持岂不美滋滋?
吾辈:然而安装完成直接启动就报错了
谜之音:报错就去查一下,这都觉得麻烦却是没办法了呢!

事实上这是很多开发者,尤其是 Linux 下的开发者,习惯了使用软件可能报错、可能有问题,对使用体验毫不在意。
所以吾辈滚了,滚去使用其他的软件了。

使用 freeFTPd

freeFTPd 官网

点击 下载链接 下载 freeFTPd,然后点击安装。第一次运行时会询问你 是否创建/使用私钥是否运行系统服务,全部选择 即可。

  1. 打开程序
    可以看到默认在 Status 标签,显示者 FTP 和 SFTP 服务都是关闭状态。
    首页
  2. 添加用户
    首先,我们需要添加一个用户,可以连接 FTP/SFTP 服务端的用户。
    1. 点击 Users 标签,然后点击 Add 添加用户
      Users 标签
    2. 设置用户信息
      添加用户信息
      依次
      1. 输入用户名
      2. 选择使用密码认证
      3. 输入密码
      4. 选择用户的服务端根目录
      5. 同时选择允许 FTP/SFTP 连接(默认选中)
      6. 点击 Apply 完成添加
  3. 启动 FTP 服务端
    启动 FTP
  4. 启动 SFTP 服务端
    启动 SFTP
  5. 查看 Status 状态页
    Status 选项页

测试 FTP/SFTP

如果仅仅是连接 FTP/SFTP 的话,我们确实可以使用 WinSCP 作为 FTP/SFTP 客户端。然而,作为开发者,连接 Linux 服务器也是家常便饭,所以我们选择 MobaXterm

下载页面 选择 MobaXterm Home Edition v11.1 (Portable edition) 下载免费便携版。下载完成得到一个压缩包,解压之,点击 MobaXterm_Personal_11.1.exe 运行程序。

MobaXterm 首页
MobaXterm 首页

  1. 点击 Session 添加会话
    连接 FTP
  2. 设置用户认证信息
    设置用户认证信息
  3. 选择用户认证信息
    选择用户认证信息
  4. 连接 FTP 成功
    连接 FTP 成功
  5. 同理添加 SFTP 连接
    同理添加 SFTP 连接
  6. 连接 SFTP 成功
    连接 SFTP 成功

最后,虽然概率很低,但如果在你的 PC 上按照该教程搭建失败,可以在文章底部进行评论告诉吾辈哦 (v^_^)v

Java8 函数式功能速查

场景

有时候使用 lambda 参数的时候忘记应该接口的名字,所以便在此写一下 Java8
function 包下原生的相关接口,方便快速查找。

lambda 接口

class 参数 返回值 Stream 示例 应用场景
Function <T> <R> map/flatMap 映射
Consumer <T> void forEach/peek 迭代
Predicate <T> boolean filter/anyMatch 过滤
Supplier <R> generate 生成
BiFunction <U,T> <T> reduce 归纳
UnaryOperator <T> <T> iterate 映射相同类型
BinaryOperator <T,T> <T> reduce 归纳相同类型
Comparator <T,T> <U> sort 比较

Stream 流

Stream 流为我们提供了一种简单的操作集合的方式,每个操作都具有原子性。

function 参数 返回值 功能
filter Predicate<T> Stream<T> 过滤
map Function<T,T> Stream<T> 映射
flatMap Function<T,Stream<T>> Stream<T> 压平映射
distinct Stream<T> 去重
sorted Stream<T> 排序, 要求 T 实现 Closeable
sorted Comparator<T> Stream<T> 排序
peek Consumer<T> Stream<T> 迭代,但有返回值
limit long Stream<T> 限制数量
skip long Stream<T> 从开头丢弃指定数量的元素
forEach Consumer<T> void 迭代
forEachOrdered Consumer<T> void 保证顺序的迭代
toArray Object[] 转换为数组
toArray IntFunction<T[]> T[] 转换为指定类型的数组
reduce BinaryOperator<T> Optional<T> 归纳为一个元素
collect Collector<T,A,R> R 将结果归集
min Comparator<T> Optional<T> 最小值
max Comparator<T> Optional<T> 最大值
count long 长度
anyMatch Predicate<T> boolean 是否存在匹配的元素
allMatch Predicate<T> boolean 是否所有元素都匹配
noneMatch Predicate<T> boolean 是否所有元素都不匹配
findFirst Optional<T> 查找第一个元素
findAny Optional<T> 查找任意一个元素
empty Stream<T> 获取一个空的流
of T... Stream<T> 将多个元素构造为流
iterate T,UnaryOperator<T> Stream<T> 构造无限有序流
generate Supplier Stream<T> 构造无限无序流
concat Stream<T>,Stream<T> Stream<T> 连接两个流
parallel Stream<T> 将流转换为并行模式(多线程)

Collectors

Collectors 是一个 Java8 增加的一个工具类,用于简单的构造 Collector 接口的实现,主要用于 Stream.collect() 中的参数。Stream.collect() 用于将流转换为其他的数据结构,包括但不限于 Collection, Map, String, Long 等等,并以此衍生出了许多有用的操作:分组,转换为 Map,归约(reduce 的另一种使用方式),连接(归约的特化形式)

function 功能 示例
toList 转换为 List
toSet 转换为 Set
toMap 转换为 Map
toCollection 转换为 Collection 的子类
joining 所有元素连接为 String, 可以指定分隔符/开头/结尾
mapping 在转换之前对每个元素进行映射,常用于分组
collectingAndThen 在转为之前对结果进行一些操作,例如构造不可变集合
counting 计算元素总数
minBy 最小值
maxBy 最大值
summingInt 计算总和(结果为 Integer
summingLong 计算总和(结果为 Long
summingDouble 计算总和(结果为 Double
averagingInt 计算平均值(结果为 Integer
averagingLong 计算平均值(结果为 Long
averagingDouble 计算平均值(结果为 Double
reducing 归纳, 与 Stream.reduce() 功能相同
groupingBy 分组, Collectors 独有
groupingByConcurrent 并发分组
partitioningBy 特化分组, 分成 truefalse
toMap 转换为 Map
toConcurrentMap 转换为并发 Map
summarizingInt 汇总信息并尽可能返回 Integer。注: summarizing* 的方法汇总的信息都是 数量/求和/平均值/最小值/最大值
summarizingLong 汇总信息并尽可能返回 Long
summarizingDouble 汇总信息并尽可能返回 Double

一些示例

常见 Stream 操作

1
2
3
4
5
6
7
8
9
10
final List<String> collect = Stream.of("1", "12", "", "123", "2", "12", "321", "")
//过滤
.filter(s -> !s.isEmpty())
//提取出组成字符串的字符
.flatMap(s -> Arrays.stream(s.split("")))
//去重
.distinct()
//转换为集合
.collect(Collectors.toList());
System.out.println(collect); //结果是 [1, 2, 3]

常见 Collectors 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final Map<Integer, List<List<String>>> collect = Stream.of("1", "12", "", "123", "2", "13", "321", "")
.collect(
//分组
Collectors.groupingBy(
//分组条件
String::length,
//分组之前对每个元素进行映射
Collectors.mapping(
//映射函数
s -> Arrays.asList(s.split("")),
//最后将 Stream 转换为 List
Collectors.toList()
)
)
);
System.out.println(collect); //结果是 {0=[[], []], 1=[[1], [2]], 2=[[1, 2], [1, 3]], 3=[[1, 2, 3], [3, 2, 1]]}

最后,Java8 有很多有趣的功能,或许我们所使用的不过是其中一个很小的子集,然而了解的越多越是觉得 Java8 的改进很多呢

注:本文并非 API 列表,并未包含全部的功能,所以如果找不到所需要的函数可以查看 JDK8 Oracle Documentation

Java8 时间格式化 DateTimeFormatter

场景

吾辈在使用 Java8 的 LocalDateTime 想要根据某种格式格式化字符串为日期时间,本以来会简单的事情,事实上却出乎预料!

问题

想要格式化一个字符串为日期时间。例如常见的 yyyy-MM-dd hh:mm:ss 格式的 2017-12-11 10:11:05,吾辈习惯性的写出以下代码

1
2
3
4
final String text = "2017-12-11 10:11:05";
final String pattern = "yyyy-MM-dd hh:mm:ss";
final LocalDateTime dateTime = LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern));
System.out.println(dateTime);

谜之音:JVM 不想理你,并抛给了你一个 Error

1
java.time.format.DateTimeParseException: Text '2017-12-11 10:11:05' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {MinuteOfHour=11, MilliOfSecond=0, MicroOfSecond=0, SecondOfMinute=5, HourOfAmPm=10, NanoOfSecond=0},ISO resolved to 2017-12-11 of type java.time.format.Parsed

大意便是无法解析,去 Google 了一下,在 StackOverflow 上发现了这个问题:DateTimeParseException: Text could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor

里面的答案说是使用 HH(每小时)而非 hh(每小时上午时钟),所以吾辈修改了代码,变成了下面这样

1
2
3
4
5
final String text = "2017-12-11 10:11:05";
// 只改了这里的格式
final String pattern = "yyyy-MM-dd HH:mm:ss";
final LocalDateTime dateTime = LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern));
System.out.println(dateTime);

然而确实能正常解析了!??**#黑人问号**

根源

去看了一下 Java8 的 DateTimeFormatter 日期时间格式化类,发现了 class 上一段有趣的注释

All letters ‘A’ to ‘Z’ and ‘a’ to ‘z’ are reserved as pattern letters. The
following pattern letters are defined:

Symbol Meaning Presentation Examples
G era text AD; Anno Domini; A
u year year 2004; 04
y year-of-era year 2004; 04
D day-of-year number 189
M/L month-of-year number/text 7; 07; Jul; July; J
d day-of-month number 10
Q/q quarter-of-year number/text 3; 03; Q3; 3rd quarter
Y week-based-year year 1996; 96
w week-of-week-based-year number 27
W week-of-month number 4
E day-of-week text Tue; Tuesday; T
e/c localized day-of-week number/text 2; 02; Tue; Tuesday; T
F week-of-month number 3
a am-pm-of-day text PM
h clock-hour-of-am-pm (1-12) number 12
K hour-of-am-pm (0-11) number 0
k clock-hour-of-am-pm (1-24) number 0
H hour-of-day (0-23) number 0
m minute-of-hour number 30
s second-of-minute number 55
S fraction-of-second fraction 978
A milli-of-day number 1234
n nano-of-second number 987654321
N nano-of-day number 1234000000
V time-zone ID zone-id America/Los_Angeles; Z; -08:30
z time-zone name zone-name Pacific Standard Time; PST
O localized zone-offset offset-O GMT+8; GMT+08:00; UTC-08:00;
X zone-offset ‘Z’ for zero offset-X Z; -08; -0830; -08:30; -083015; -08:30:15;
x zone-offset offset-x +0000; -08; -0830; -08:30; -083015; -08:30:15;
Z zone-offset offset-Z +0000; -0800; -08:00;
p pad next pad modifier 1
' escape for text delimiter
'' single quote literal ‘
[ optional section start
] optional section end
# reserved for future use
{ reserved for future use
} reserved for future use

是的,这是一个日期格式的说明,在这里确实可以看到 HH 才代表的是 24 小时,而 hh 则是将小时分为 am/pm(上午/下午)。

附:这里吐槽一下,Java 的格式化规则居然和标准的有偏差,uuuu 也能当作更好的 yyyy 使用(主要针对负的年份)

所以现在也只能将格式化时间的 pattern 修改为 uuuu-MM-dd HH:mm:ss 便能正常匹配了!

解决

那么,既然知道日期时间格式化模式的规则,那么接下来就可以直接写模式字符串了

  • 日期:uuuu-MM-dd
  • 时间:HH:mm:ss
  • 常见的日期时间:uuuu-MM-dd HH:mm:ss
  • 标准的日期时间:DateTimeFormatter.ISO_DATE_TIME(Java8 time 库中已存在)

将字符串转换为日期时间大致有两种方式

1
2
3
4
5
final String text = "2019-02-12T01:24:07.425Z";
// 先解析为时间在转换为具体的日期时间类
final LocalDateTime from = LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(text));
// 直接根据指定的格式解析字符串为具体的日期时间类
final LocalDateTime parse = LocalDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME);

那么,Java8 的踩坑之路还在继续,不知还有多少人在用 Java7- 呢?\(@ ̄ ∇  ̄@)/

Android 常用 App 清单

说明

该清单只是吾辈所用,使用工具因人而异,若是你对清单中的内容有何异议,可以在下方进行留言,吾辈会尽快阅读并回复!

附:列出的 Google Drive 链接是因为某些第三方 App 不在 Play Store 之中,而且在可预期的很长时间内都不可能在(Youtube 第三方客户端)

Google 全家桶

这里首先说明使用 Google 全家桶的原因:虽然 Google 最近声名狼藉,然而相比于 _国内肆无忌惮的获取数据,甚至百度明目张胆地说出“中国人就是喜欢拿隐私换方便”_,Google 还是显得像一朵 白百合。而且 Google 也确实不会明目张胆的找人要隐私,它只会偷偷摸摸的去做(#笑哭)。而多个的 App 都使用同一家公司的优势是巨大的:一切数据皆在云端,后台服务体贴之至。更何况 Google 的服务大多数都是全端跨平台的,对于同时使用 PC/Mobile 的使用者而言优势巨大。

Play Store:应用商店

Google 家的应用商店,主要可以强制替代国内系统自带的应用商店(垃圾商店,遍地广告),而且国内应用相对于国内版本要干净一些(例如 QQ/WeChat)。

Gmail:Google 家的电子邮件

跨平台的电子邮件。如果需要经常使用电子邮件(订阅内容/公司同事交流/GreasyFork 反馈/GitHub Issue/各种网络服务),那么 Gmail 是当之无愧的首选。在 Web 版本上在所有电子邮件中都是首屈一指的,在 Android 上的表现也是相当不错,毕竟是在自家的系统上。

Google Tasks:Google 家的任务管理

跨平台的任务管理。在 Gmail Web 版本中直接集成的三剑客之一(Calendar/Keep/Task),简单直白的任务管理,用于任务速记还算不错。

Google Photo:Google 家的相册

跨平台的网络相册。在 Web/Mobile 上都可以使用的网络相册,提供了多达 15G 的原画质照片的网络存储,对于经过压缩的照片更是无限量的进行云存储!

Google Analytics:网站数据分析

跨平台的网站数据分析。吾辈将之集成到了博客 https://blog.rxliuli.com 中,用于了解博客的访问记录,进行数据分析。当然,在 Mobile 上也有客户端可以快速查看,了解自己网站的浏览记录。

  • 指定时间段的访问量
  • 访问者行为流
  • 流量来源分析
  • 访问者行为分析
  • 搜索引擎数据分析
  • 网站实时监听

Google 通讯录:Google 家的通讯录

跨平台的联系人功能。不仅仅是电话号码,甚至于 Email/IM/网站 都能作为一个 联系人。更有趣的是在 Android 上通过联系人打电话,在 Web 上却可以通过邮件(Gmail)进行联系了呢!

注:Gmail 内置集成了通讯录。

Google 信息:Google 家的信息

跨平台的信息功能。人性化的界面设计,优秀的搜索功能,自带 Dark Mode(夜间模式,很重要),在 Web 上也可以查看/发送信息。

Google 翻译:目前最好的翻译

跨平台的多语言翻译。目前应该是最好的翻译了,Google Chrome 自带的翻译可是为 Google 积累了巨大的原始数据,对于翻译质量的提升也是理所当然的。

Google Drive:Google 家的云服务

跨平台的云服务功能。为什么说是云服务而非网络云盘呢?因为 Google 家的一些应用的同步功能实际上也在这里,Gmail 里的使用配额,Photo 里的原画质图片云存储限制,尽是存于此处。

其他

SSR:上网必须

官方

为了正常浏览网络折腾与支付一些代价都是微不足道的,貌似比 Shadowscoks 要稳定一点,不过据传闻作者最初并未打算将其开源。

附:最近网络盛传 V2Ray 更好,然而对于吾辈(使用者)而言,却是并未看到特别明显的优势,所以暂未切换过去。。。

Twitter:国外的网络社区

对于吾辈而言,中文推圈的乐趣逐渐减少,现今早已不比曾经了。之前的那么多有趣的人和事,如今却都已消逝,Eric/Neko/泉。。。太多人在乎的人离去了,水军也进入了这个小小的圈子,Twitter 在吾辈的心中也渐渐和 QQ 一个等级了

  • Eric:初入推圈最早接触的药娘之一,很早便已离开了
  • Neko:初入推圈最早接触的药娘之一,之后发生了理念之争(不分对错),互 B 之
  • 泉:初入推圈最早接触的药娘之一,后来其在面基时发生了一些事情,在吾辈心中药娘逐渐变得。。。

Telegram:安全私密的 IM

相比于 Twitter 是个社区,Telegram 则专注于用户之间的交流。开源(客户端)免费可端对端加密通讯,使得它受到许多推油的喜爱。Telegram 最新版的官方客户端已经做得足够好了,Plus/TelegramX 什么的基本上也不需要了呢

QQ:国内广泛使用的 IM

第三方

国内广泛使用的 IM,曾经的同学什么的都在这里了(虽然吾辈曾经为了转型 Twitter 而清空过 QQ 就是了 #中二病)。这里吾辈使用了第三方修改版,主要是为了去除 QQ 的广告以及功能增强。

  • 去除无用的侧边栏
  • 界面上稍微皮了一下
  • 破解撤回
  • 破解闪照
  • 破解口令红包

WeChat:国内不得不用联系工具

第三方

国内不得不用的 IM,名为微信,实为巨信。不仅安装包极其巨大,而且连基本的数据同步,都没有做好--以安全之名。然而 WeChat 究竟有没有保留原始数据,又有没有把数据交给政府审查,相信大家心里自然明白。
第三方修改功能

  • 不强制保留后台
  • 避免无止尽的更新弹窗
  • 破解撤回

注:该修改版本会被腾讯检测出来,所以使用上可能存在封号风险,利弊权衡皆由己定。

百度输入法:度娘的输入法

纯粹是使用习惯的问题,没什么特别好的地方。没有选择 Google 拼音的唯一原因就是其没有丰富的颜文字,而百度输入法满足了这个条件,而且也有 Android L 风格的皮肤,使用上还是极好的。

Via:精致小巧的壳浏览器

精致小巧的浏览器,还在使用的唯一原因是:不想使用 App 打开某个链接,例如百度网盘的链接就会 Firefox 会自动启动客户端。
如果不是浏览器的重度使用者,Via 基本够用。如果追求强大的扩展性,Android 上吾辈仅推荐 Firefox,它允许安装各种附加组件,Google Chrome 虽然强大,然而对于 Android 版本的可扩展性永远不会变好,因为它更喜欢 Android App。

Bmap:百度/高德第三方客户端

一款第三方地图应用,允许使用百度/高德两个地图源。功能的完善程度还算不错,而且重要的是没有各种讨厌的推广/广告!明明只想要一个地图,百度/阿里却给我们一个世界(全家桶系列)。。。

阅读:开源的小说阅读器

旧版, 书源地址, GitHub

开源的网络小说阅读器。虽然最近有流氓趋势(添加书源引导用户关注微信公众号),但矮子里面拔高个,它仍然是最好的一个了。它的主要优势是可以添加很多书源,聚合搜索多个不同网站的小说,相比于直接使用 Google 更加方便一点。

Firefox:Android 上扩展性最好的浏览器

在 PC 上,Google Chrome 是当之无愧的 Number One。然而,在 Android 上,Chrome for Android 并不支持插件。虽然 Google 宣称定位是 简洁高效的浏览体验,然而是不是为了推广使用 App 却并未可知,所以在 Android 上能使用附加组件(插件)的 Firefox 就是扩展性最好的浏览器。
常用 Plugin 列表

常用 UserJS 列表

条码扫描器:开源的条码扫描器

GitHub

开源的条码扫描器,比 WeChat 扫描安全一些,而且便于第三方集成(Firefox 搜索框)。

Nova 启动器:Nova 桌面

Pro 破解版

还不错的第三方桌面,在很早之前就已经出现了。有一些自定义手势相当方便,长按 Home 搜索应用,双击屏幕锁屏。并支持第三方图标库,例如下面的 Pixel Icon Pack。

Pixel Icon Pack:全而大 App 图标库

虽然吾辈最喜欢的是 material 风格,然而 material 主题的图标数量实在不够。而 Pixel 这款图标库的数量足够庞大,包含了 6910+ 个图标,覆盖了绝大多数的 App,统一了桌面 App 的图标。

OpenHub:GitHub 第三方客户端

GitHub

虽然 GitHub 是全世界最大的同性社交网站(代码托管平台),然而并没有官方的 Mobile 客户端。OpenHub 正是 GitHub 的一个第三方客户端,可以方便的在 Mobile 上使用 GitHub。

淘宝 Lite:淘宝海外版

你是否也曾厌恶淘宝的臃肿,是否讨厌淘宝的强制升级?现在,我们有了新的选择:淘宝 Lite,名副其实的精简版。主要面向国外用户,我们在地区中选择 全球 即可正常使用淘宝进行购物啦
主要优点

  • 没有各种广告
  • 没有各种看似强大然并卵的功能
  • 不强制升级
  • 没有强制索取权限
  • 包含完整的购物体验

支付宝:国内通用移动支付

国内广泛使用的移动支付工具,在一线城市(广州)基本上带个 Mobile 就能到处走了。相比于 WeChat,支付宝给吾辈的感觉更好。。。WeChat 总让吾辈觉得是只想在国内发展的一个毒瘤 App,而支付宝是有志于开拓世界的(无论影响好坏)。

LastPass:全端密码管理器

跨平台的密码管理器。基本上吾辈在 Google Chrome 上使用 Plugin,在 Mobile 上也使用它。虽然 Google Chrome 自带了密码管理/同步功能,然而对于某些两步验证的网站并未能很好的支持,而且也不支持跨浏览器!而 LastPass 基本上免费版本对于个人使用算是绰绰有余了,支持密码同步,跨平台/跨浏览器支持,复杂密码一键生成。

注:吾辈在 PC 上使用 Google Chrome,Mobile 上却使用 Mozilla Firefox。

PxView:Pixiv 第三方客户端

Pixiv 第三方客户端。相比于官方客户端有很多收费功能。
主要特点如下

  • 无广告
  • 开源
  • 支持黑暗模式
  • 允许查看排行
  • 保存图片

MXPlayer:本地视频播放器

Pro 破解版

MxPlayer 应该算是 Android 上最强的本地播放器了吧?拥有相当多的解码器,对绝大多数的视频都能正常播放,对字幕的支持也相当不错,同时也能当作本地音乐播放器使用。

百度网盘:国内一家独大的网盘

精简版

有时候要补番的时候需要通过百度网盘下载,所以 Mobile 上也安装了一个。嘛,虽然也不是官方客户端就是啦~~
主要修改内容

  • 不提示更新
  • 删除无用的元素,只保留核心的网盘功能
  • 或许有破解限速(吾辈开了会员所以没有尝试过)

静读天下:本地书籍阅读器

Pro 破解版

非常好用的电子书阅读器,对多种格式的文档都支持的非常好。
包括但不限于以下格式

  • Txt:传统纯文本小说格式
  • HTML:巨大网页小说,一般为二次导出
  • Epub:新的电子书籍标准
  • Mobi:亚马逊 Kindle 阅读器支持的专有格式
  • PDF:Adobe 发行的一种电子书籍格式
  • umd:常见的请小说格式
  • chm:常见的电子文档格式

交互友好,页面优雅,全能的本地阅读器。

有道词典:词典

精简版

国内的词典 App。虽然之前使用过欧陆词典(据说是 Android 系统上最好的词典),然而在同步这块终究差了一点。当然啦,官方的有道词典现在简直是个巨无霸,强行整合有道云课堂/笔记,国内的公司真真是做不好一款纯粹的 App!

Weawow:天气 App

一个天气应用,使用它纯粹是不喜欢系统自带的天气应用。简单纯粹,不强制弹窗提醒升级,桌面的小部件也很简洁,最重要的是天气预报还算准确!

Inoreader:RSS 阅读器

一个跨平台的 RSS 阅读器,相比于大名鼎鼎的 Feedly 有着更多的免费功能,而且对于用户的支持非常棒!

附:吾辈之前就有遇到过 RSS 只有一部分内容,需要在浏览器打开才能看到全文的情况,Inoreader 快速回答了呢!

YMusic:Youtube 的第三方音乐客户端

官方版

Youtube 虽然好用,但 Android 版的广告实在实在实在太多了!因为很重要,所以说三遍。吾辈日常使用 Youtube 听音乐,然而 Youtube 不能后台播放,不能下载到本地的特性导致单纯的听音乐真的很麻烦。
而 YMusic 不仅能下载 Youtube 上的内容,而且还能登录 Google 账号同步 Youtube 的内容。这,便是使用 YMusic 的理由!

Youtube Vanced:Youtube 的第三方视频客户端

YouTube Vanced, microG for YouTube Vanced

虽然吾辈经常用 Youtube 听音乐,然而有时候还是会想要看某个视频的,所以一个好用的客户端必不可少。Youtube Vanced 是国外发起的一个项目,就是为了解决 Youtube 不能后台播放/下载的问题。

如果你不需要登录 Google 账号可以使用 Newpipe,那个在作为音乐客户端方面更有优势。

ES 文件管理器:强大的文件管理器

精简版

ES 是相当老牌的一个文件管理器。有着相当好用的体验,可以多标签页查看文件,能够统计文件夹大小,自带近距离互传功能,深度搜索。
修改内容

  • 去除广告
  • 解锁主题
  • 显示系统隐藏文件
  • 禁止更新

潜伏之赤途:非常优秀的 AVG

Android 直装版, 知乎, 百度百科

毫无疑问的国产神作,当前国内最优秀的 AVG(文字冒险游戏)。当然,现在网络上这个游戏已经消失了,虽然名义上是侵犯版权,然而真实原因你也懂得。
下面是摘自百度百科上的描述

人性道义,民族气节,为国牺牲的精神,在利益和性命面前,人们所暴露出的丑陋或者真善美引人反省深思。本游戏是男性向 AVG 游戏神作,游戏很长,并且考验推理能力,最重要的是代入感非常强!想要真正体验一把地下党的生活吗?潜伏去吧,开始一场激烈的革命战斗!
这是一款谍战题材游戏,以抗战期间的上海滩为背景,你扮演一位潜伏日军情报机关的地下党,进行暗杀、窃取情报等一系列活动。作为一个地下党,需要冷静、睿智、果断,步步为营。在潜伏的路上,你能走得多远?从朝气蓬勃的勇往直前,到在许多次地看着被自己供出的同志倒下的时候真正的应征了那句 “搞情报工作的没一个有好下场”,你能否打破这句定言?
游戏中你叫方别,两年前还是上海一个慷慨激昂的爱国学生,在街上奔走疾呼 “抗日救亡”,甚至还秘密加入中国共产党,却因年少血气方刚,被抓进了监狱。释放后老师出钱将你送去日本留学为了把你塑造成亲日派的形象,两年来你当年的书生形象早已被人们遗忘,老师与组织决定,让你成为 “汉奸”,你要如何机智地扮演好这个 “碟中谍”?面对一个个艰难的选择是顾全大局还是心慈手软?
游戏中的人物设定都很鲜明。他们或是善良忠义,或是狡诈阴险,或是势力贪财,或是残忍报复。上到达官权贵,下到市井走卒都有着自己的标签。其中虽不乏稍微夸张成分存在,但也使得冲突变得更加地明显,为故事的戏剧性打下了很好的基础。

总结

那么,关于吾辈在 mobile 上使用的 App 清单便到此结束了。如果你有什么有趣的 App,也可以推荐给吾辈哦

为什么要用现代前端

背景

前端近两年来发展迅速,随着 nodejs 的广泛使用,大批 npm 的框架/库层出不穷,npm 上 JavaScript 库的数量甚至超过了 Maven 中央仓库
然而即便如此,仍然有很多公司固守在传统的前端切 UI,后端通过模板视图填充视图的技术。一方面固然是为了避免新技术踩坑,另一方面,居然有人在 deno 下说出了:求不要更新了,老子学不动了,并引发了大量讨论。

deno 是 nodejs 的作者开发的下一代 JavaScript 运行时。

现代前端

前端发展史

  1. 1996 年,样式表标准 CSS 第一版发布。
  2. 2001 年,微软公司时隔 5 年之后,发布了 IE 浏览器的下一个版本 Internet Explorer 6。这是当时最先进的浏览器,它后来统治了浏览器市场多年。
  3. 2002 年,Mozilla 项目发布了它的浏览器的第一版,后来起名为 Firefox
  4. 2003 年,苹果公司发布了 Safari 浏览器的第一版。
  5. 2004 年,Google 公司发布了 Gmail,促成了互联网应用程序(Web Application)这个概念的诞生。由于 Gmail 是在 4 月 1 日发布的,很多人起初以为这只是一个玩笑。
  6. 2004 年,WHATWG 组织成立,致力于加速 HTML 语言的标准化进程。
  7. 2005 年,Ajax 方法(Asynchronous JavaScript and XML)正式诞生,Jesse James Garrett 发明了这个词汇。它开始流行的标志是,2 月份发布的 Google Maps 项目大量采用该方法。它几乎成了新一代网站的标准做法,促成了 Web 2.0 时代的来临。
  8. 2006 年,jQuery 函数库诞生,作者为 John Resig。jQuery 为操作网页 DOM 结构提供了非常强大易用的接口,成为了使用最广泛的函数库,并且让 JavaScript 语言的应用难度大大降低,推动了这种语言的流行。
  9. 2008 年,V8 编译器诞生.
  10. 2009 年,Node.js 项目诞生,创始人为 Ryan Dahl,它标志着 JavaScript 可以用于 服务器端编程,从此网站的前端和后端可以使用同一种语言开发。并且,Node.js 可以承受很大的并发流量,使得开发某些互联网大规模的实时应用变得容易。
  11. 2013 年 5 月,Facebook 发布 UI 框架库 React,引入了新的 JSX 语法,使得 UI 层可以用组件开发。
  12. 2015 年 3 月,Facebook 公司发布了 React Native 项目,将 React 框架移植到了手机端,可以用来开发手机 App。它会将 JavaScript 代码转为 iOS 平台的 Objective-C 代码,或者 Android 平台的 Java 代码,从而为 JavaScript 语言开发高性能的原生 App 打开了一条道路。
  13. 2015 年 vuejs 发布 1.0 版本
  14. 2016 年 vuejs2.x 版本发布
  15. 新生事物仍在不断涌现…

上面就是前端的大概发展史,看完之后,不难发现,有一些关键的历史时刻,对前端开发产生了重大影响。例如 IE6 的发布(统治了浏览器市场很多年),JQuery 的诞生,Ajax 的流行。而现在,新的拐点出现了 – nodejs 的流行。现代前端仍然在快速发展中,前后端分离,SSR,PWA 都是近两年才出现的概念。如果没有上车,后面就再难追上了。例如像十年前不使用 Spring 开发的应用,在现代 Java Web 后端的环境中,没有 Spring 简直寸步难行。

上面说了一些现代前端的历史,那么使用它具体有什么好处呢?

JavaScript 模块化

仔细想想,我们的 HTML, CSS 和 JavaScript 是如何结合使用的?

是的,我们按照规范分离了 HTML, CSS 和 JavaScript,并在 HTML 中使用 <link /><scirpt></script> 标签引入 CSS 和 JavaScript。那么,不同的 JavaScript 之间如何交互呢?我们只能通过暴露顶级变量(window 作用域)来进行交互。
是呀,稍有经验的 JavaScript 开发者都会 抽取函数,然而一个 JavaScript 中太多的函数仍然容易产生混乱。

例如下面这段代码,点击不同的按钮,显示不同的面板。

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
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>JavaScript 避免使用 if-else</title>
</head>
<body>
<main>
<div id="tab">
<label>
<input type="radio" data-index="1" name="form-tab-radio" />
第一个选项卡
</label>
<label>
<input type="radio" data-index="2" name="form-tab-radio" />
第二个选项卡
</label>
<label>
<input type="radio" data-index="3" name="form-tab-radio" />
第三个选项卡
</label>
</div>
<form id="extends-form"></form>
</main>
<script src="./js/if-else.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// js/if-else.js
document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach(el => {
el.addEventListener('click', () => {
const index = el.dataset.index
const header = el.parentElement.innerText.trim()
// 如果为 1 就添加一个文本表单
if (index === '1') {
document.querySelector('#extends-form').innerHTML = `
<header><h2>${header}</h2></header>
<div>
<label for="name">姓名</label>
<input type="text" name="name" id="name" />
</div>
<div>
<label for="age">年龄</label>
<input type="number" name="age" id="age" />
</div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`
} else if (index === '2') {
document.querySelector('#extends-form').innerHTML = `
<header><h2>${header}</h2></header>
<div>
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" />
</div>
<div><img id="avatar-preview" src="" /></div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`
function readLocalFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.onload = event => {
resolve(event.target.result)
}
fr.onerror = error => {
reject(error)
}
fr.readAsDataURL(file)
})
}
document.querySelector('#avatar').addEventListener('change', evnet => {
const file = evnet.target.files[0]
if (!file) {
return
}
if (!file.type.includes('image')) {
return
}
readLocalFile(file).then(link => {
document.querySelector('#avatar-preview').src = link
})
})
} else if (index === '3') {
const initData = new Array(100).fill(0).map((v, i) => `第 ${i} 项内容`)
document.querySelector('#extends-form').innerHTML = `
<header><h2>${header}</h2></header>
<div>
<label for="search-text">搜索文本</label>
<input type="text" name="search-text" id="search-text" />
<ul id="search-result"></ul>
</div>
`
document
.querySelector('#search-text')
.addEventListener('input', evnet => {
const searchText = event.target.value
document.querySelector('#search-result').innerHTML = initData
.filter(v => v.includes(searchText))
.map(v => `<li>${v}</li>`)
.join()
})
}
})
})

使用现代前端的 JavaScript 模块化重构如下

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
// common.js
/**
* 状态机
* 用于避免使用 if-else 的一种方式
*/
class StateMachine {
static getBuilder() {
const clazzMap = new Map()
/**
* 状态注册器
* 更好的有限状态机,分离子类与构建的关系,无论子类如何增删该都不影响基类及工厂类
*/
return new class Builder {
// noinspection JSMethodCanBeStatic
/**
* 注册一个 class,创建子类时调用,用于记录每一个 [状态 => 子类] 对应
* @param state 作为键的状态
* @param clazz 对应的子类型
* @returns {*} 返回 clazz 本身
*/
register(state, clazz) {
clazzMap.set(state, clazz)
return clazz
}

// noinspection JSMethodCanBeStatic
/**
* 获取一个标签子类对象
* @param {Number} state 状态索引
* @returns {QuestionType} 子类对象
*/
getInstance(state) {
const clazz = clazzMap.get(state)
if (!clazz) {
return null
}
//构造函数的参数
return new clazz(...Array.from(arguments).slice(1))
}
}()
}
}

export StateMachine
1
2
// TabBuilder.js
export default StateMachine.getBuilder()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Tab.js
class Tab {
// 基类里面的初始化方法放一些通用的操作
init(header) {
const html = `
<header><h2>${header}</h2></header>
${this.initHTML()}
`
document.querySelector('#extends-form').innerHTML = html
}

// 给出一个方法让子类实现,以获得不同的 HTML 内容
initHTML() {}
}

export default Tab
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
// Tab1.js
import builder from './TabBuilder.js'
import Tab from './Tab'

const Tab1 = builder.register(
1,
class Tab1 extends Tab {
// 实现 initHTML,获得选项卡对应的 HTML
initHTML() {
return `
<div>
<label for="name">姓名</label>
<input type="text" name="name" id="name" />
</div>
<div>
<label for="age">年龄</label>
<input type="number" name="age" id="age" />
</div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`
}
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Tab2.js
import builder from './TabBuilder.js'
import Tab from './Tab'

const Tab2 = builder.register(
2,
class Tab2 extends Tab {
initHTML() {
return `
<div>
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" />
</div>
<div><img id="avatar-preview" src="" /></div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`
}
// 重写 init 初始化方法,并首先调用基类通用初始化的方法
init(header) {
super.init(header)
document.querySelector('#avatar').addEventListener('change', evnet => {
const file = evnet.target.files[0]
if (!file) {
return
}
if (!file.type.includes('image')) {
return
}
this.readLocalFile(file).then(link => {
document.querySelector('#avatar-preview').src = link
})
})
}
// 子类独有方法
readLocalFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.onload = event => {
resolve(event.target.result)
}
fr.onerror = error => {
reject(error)
}
fr.readAsDataURL(file)
})
}
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Tab2.js
import builder from './TabBuilder.js'
import Tab from './Tab'

const Tab3 = builder.register(
3,
class Tab3 extends Tab {
initHTML() {
return `
<div>
<label for="search-text">搜索文本</label>
<input type="text" name="search-text" id="search-text" />
<ul id="search-result" />
</div>
`
}
init(header) {
super.init(header)
const initData = new Array(100).fill(0).map((v, i) => `第 ${i} 项内容`)
document
.querySelector('#search-text')
.addEventListener('input', evnet => {
const searchText = event.target.value
document.querySelector('#search-result').innerHTML = initData
.filter(v => v.includes(searchText))
.map(v => `<li>${v}</li>`)
.join()
})
}
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.js
import builder from './TabBuilder.js'
import './Tab1'
import './Tab2'
import './Tab3'

document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach(el => {
el.addEventListener('click', () =>
// 调用方式不变
builder
.getInstance(Number.parseInt(el.dataset.index))
.init(el.parentElement.innerText.trim())
)
})

虽然看起来代码/文件变得更多了,然而实际上不同的状态区分更加明显,代码也更容易维护了。

兼容性

如果我们想要让传统前端项目兼容 IE11,那么恐怕不得不使用 JQuery 以及 ES5 以前的语法(ES5 也支持的不完全)。如果想要使用 ES6/ES7/ES8 的话恐怕不仅在 IE11 上无法保证兼容性,既便 Web 标准的前沿实现者 Google Chrome,它的旧版本对新特性的支持恐怕也不算好(Google Chrome 开发团队的实力毋庸置疑,然而如果一个标准是在浏览器发布之后才出现的话,旧版本浏览器却是不可能兼容了)。

附: 最近两年 JavaScript 的标准几乎是一年一个版本,不过都没有再像 ES6 如此激进了

那么,如果使用现代前端就能解决这个问题了么?是的,它现代前端项目基本上都会引入的一个库 – Babel

Babel 官网首页用一句话说明了 Babel 的定位

Babel is a JavaScript compiler.
Use next generation JavaScript, today.

意为:
Babel 是一个 JavaScript 编译器。
立刻使用下一代 JavaScript。

是的,你没听错,Babel 给自身的定义是 JavaScript 编译器。众所周知,JavaScript 是运行在浏览器上(现在也可以运行在 NodeJS)的解释型弱类型的脚本语言,是没有编译器的。而 Babel 就是帮我们将 ES6 之后的 JavaScript 代码编译成 ES5 的代码,以兼容较旧版本的浏览器。

例如下面的代码

1
2
3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}

会被转换成

1
2
3
4
5
6
for (var i = 0; i < 3; i++) {
;(_i => {
setTimeout(() => console.log(_i), 0)
i = _i
})(i)
}

当然,传统前端不能使用 Babel 了么?答案是可以的,然而因为是在浏览器端编译 JavaScript,所以速度比较慢,具体可以参考吾辈写的 在传统项目中使用 babel 编译 ES6

MVVM

Wiki

MVVMModel–view–viewmodel)是一种软件架构模式。

MVVM 有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或 GUI 代码实现的。MVVM 的视图模型是一个值转换器,这意味着视图模型负责从模型中暴露(转换)数据对象,以便轻松管理和呈现对象。在这方面,视图模型比视图做得更多,并且处理大部分视图的显示逻辑。视图模型可以实现中介者模式,组织对视图所支持的用例集的后端逻辑的访问。

说人话就是 MVVM 能让我们不再关心 DOM 的更改,专注于操作数据,DOM 会根据数据自动渲染,我们不再需要关心它。

事实上,我们的不同的代码虽然分离了,但逻辑上却不然,JavaScript 仍然需要操作 DOM 和 Style,而这项工作是非常繁琐而且易错的。
曾经我们使用 JQuery 来进行 DOM 交互,同时保证兼容性,以及更好的 Ajax 工具。现在,现代前端的很多框架就是为了解决数据与 DOM 同步的,不管是 ReactJSX,还是 VueJS单文件组件

JSX:React 的理念是 既然 JavaScript 能够操作 HTML/CSS,那就把所有的控制权交给 JavaScript 就好了,在 React JSX 中,一切都是 JavaScript,即便是 JSX 的 DSL 也只是一个看起来像 HTML 的 JavaScript 代码而已。像下面的代码,事实上就是 JavaScript,直接写 <div>Hello {this.props.name}</div> 只是语法糖,背后真正运行的还是 JavaScript。

1
2
3
4
5
6
7
class HelloMessage extends React.Component {
render() {
return <div>Hello {this.props.name}</div>
}
}

ReactDOM.render(<HelloMessage name="Taylor" />, mountNode)

假如使用 vuejs 的话写起来大概是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>Hello {name}</div>
</template>

<script>
export default {
name: 'HelloWord',
props: {
name: {
type: String
}
}
}
</script>
1
2
3
4
5
6
7
8
import Vue from 'vue'
import HelloMessage from './HelloMessage'

new Vue({
el: '#app',
components: { HelloMessage },
template: '<App/>'
})

它们之间的思想有许多共同之处,都推崇组件化开发,把 HTML/CSS/JavaScript 混合起来形成组件(类似于 Java 中将属性和函数封装为类),然后组合成更大的组件,形成组件树,并最终构成 WebApp。吾辈目前推荐先看 VueJS,毕竟是国人开发,中文文档最为完善,在三大前端框架中也属于最简单的一个(ReactJS 是最困难也是生态最好的一个)。

生态丰富

NPM 的生态相当丰富,现代前端几乎所有的库都通过 NPM 发布。至今,NPM 上已经有超过 70W+ 的包,在数量上甚至远超了 Maven 中央仓库。正是因为 NPM 发布包相当简单(吾辈都发布了几个),造成了如今无比繁荣的生态(想想 Maven 感觉都是泪。。。)

包管理器对比数据可以参考 http://www.modulecounts.com/

使用 NPM 安装和使用包相当简单,使用 npm i [package] 就能直接安装一个包,使用 ES6 import 语法就能在自己的 JavaScript 文件中快速引用一个包。

下面列出一些常用的 NPM 库

  • yarn: Facebook 家的前端包管理器
  • babel: 现代前端的 JQuery,解决兼容性
  • vuejs: 华人开发的前端 MVVM 框架
  • stylus: CSS 预处理器
  • eslint: 前端代码规范检查
  • webpack: 现代前端必备的打包工具
  • rollup: JavaScript SDK 打包工具
  • lodashjs: 流行的函数式工具库
  • axios: 符合 ES6 Promise 风格的 Ajax 库
  • vuetify: 基于 vuejs 的前端 material 风格的 UI 库
  • js-xlsx: 前端 Excel 处理工具
  • debug: debug 日志辅助工具
  • uglifyjs: JavaScript 压缩工具
  • http-server: 静态 http 服务器
  • hexo: 现代前端开发的博客系统
  • highcharts: 丰富强大的图表库
  • masonry: 无限滚动瀑布流
  • highlightjs: 代码高亮
  • rx-util: 写 Greasemonkey 脚本时自定义的工具库
  • 还有更多。。。

工程化

现代前端已经和后端类似,将原本混沌的 HTML/CSS/JavaScript 细分为了许多的内容。

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
├── dist // 打包后的静态文件
├── .editorconfig // 编辑器配置
├── .eslintrc.js // eslint 配置格式
├── .git // git 仓库
├── .gitignore // git 忽略文件
├── babel.config.js // babel 配置
├── LICENSE // 许可证
├── node_module // 项目依赖
├── package.json // npm 定义文件
├── public // 一些公共的资源
│ ├── favicon.ico
│ └── index.html
├── README.md // 项目说明
├── src // 源代码目录
│ ├── App.vue // 根组件
│ ├── main.js // 项目入口
│ ├── api // api 接口,和 views 中的文件夹对应
│ ├── components // 公共的组件
│ ├── plugins // vuejs 插件
│ │ └── vuetify.js
│ ├── router // vuejs 路由管理
│ │ └── index.js
│ ├── store // vuejs 状态管理
│ │ └── index.js
│ ├── utils // 工具函数
│ └── views // 各个页面
├── tests
│ └── unit // 单元测试
│ ├── .eslintrc.js
│ └── example.spec.js
├── vue.config.js // vuejs 的配置
└── yarn.lock // yarn 配置文件

当初第一次看到这个目录时真是被吓到了,使用 yarn 一下子 20000 个依赖文件就下载下来了。然而其实这只是将传统前端分的更细一点而已,对后期维护的好处也是不言而喻的。

总结

总而言之,现代前端流行之后,前后端分离已然是大势所趋,前端开发如果还仅仅是 切图仔 的话,迟早会因为跟不上时代而被淘汰。就吾辈而言,亦希望有更多人入坑现代前端,体会现代前端的强大!

附:吾辈个人而言认为现代前端主要的优势 模块化/工程化MVVM。前者使大型 WebApp 的开发变成可能,后者则改变了数据与 DOM 之间的交互方式。

关于提问与帮助

场景

不知从何时起,帮助别人似乎开始变成了一件吃力不讨好的事情。刚刚吾辈在 QQ 里面看到这样的几句发言,所以熬夜写了这篇内容。

受助者的发言

真以为别人给予帮助是一种义务了,告诉你怎么查就是在帮助你了。总不能把饭端到你面前,却还要嚷嚷着要别人喂你吃吧?
而且,帮助是要花费时间和精力的。很多人并不是总有时间来帮你找资料的,或许我们也只是在上班的时候想稍微休息一下,看到你的问题,就谈一下大概的解决方向而已。

思考

当你想要提问时,如何更加清晰的描述你的问题让别人更容易帮助你也是你的义务。不要觉得随便提问一个:xx 应该怎么做? 就会有人很快的回答你。吾辈个人认为提问之前最好了解下面几点

  • 不要提一些容易产生争端的问题
    例如 JavaPHP 哪个更好?
    这种问题不仅容易引战,更是毫无意义。不谈使用场合,比较则无意义。就连初中生都知道对比实验应该控制 环境变量,难道如此简单的事情你都不清楚,你是 巨婴 么?
  • 如果是纯粹知识性的问题最好先查询官网
    例如 Spring 怎么集成 Mybatis
    这种连官方文档都没过一遍就来问,就算说了也只能是鸡同鸭讲,对牛弹琴罢了。这种时候你需要的是 学习 而不是 提问
  • 大部分问题已经被解决过了
    虽然不想承认,但我们的所知所想所遇已然在这个世界上重复了无数次,善用搜索引擎很重要 —— 这里吾辈只推荐 Google,不推荐的只有 百度
  • 你遇到了非常冷门的问题
    那你要描述你的问题,让别人能简单的还原问题,才能更好的解决你的问题,最好附上一个可重现的 github 示例仓库。推荐提问的网站:国外 stack overflow,国内 segmentfault,提问之前可以看一下 提问的智慧

标准示例

下面是在 Segmentfault 提问时的简单规范

Segmentfault 简单规范

下面是一个简单的提问示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# JavaScript 中如何获取子类?

如题,使用 es6 class 定义的类,如何获取指定基类的所有子类呢?

例如下面定义了三个类 `A, B, C`

```js
class A {}
class B extends A {}
class C extends A {}
```

吾辈如何获取到 A 的所有子类呢?(=^-ω-^=)

---

有人说这个问题毫无意义,难道泥萌没有遇到过根据状态切换多种操作的情况么?难道一个一个的使用 if-else 判断会比使用 class 实现多态更优雅么?┐( ̄ヮ ̄)┌

渲染之后


JavaScript 中如何获取子类?

如题,使用 es6 class 定义的类,如何获取指定基类的所有子类呢?

例如下面定义了三个类 A, B, C

1
2
3
class A {}
class B extends A {}
class C extends A {}

吾辈如何获取到 A 的所有子类呢?(=^-ω-^=)


有人说这个问题毫无意义,难道泥萌没有遇到过根据状态切换多种操作的情况么?难道一个一个的使用 if-else 判断会比使用 class 实现多态更优雅么?┐( ̄ヮ ̄)┌


是的,这是由 Markdown 写出来的。作为提问者,让别人能更简单知道自己的问题是必要的,而 Markdown 天生的 写作语言。所以吾辈也建议使用 Markdown 进行提问,至少,能传递的信息要比纯文本丰富很多,不是么?

终末

最后,吾辈个人认为如果解决了问题之后,作为提问者也有必要将之分享出来,避免后人继续踩坑。分享的方式可以使用 博客微信公众号 之类的方式,如果能将一个问题讲明白给别人听,那才说明自己懂得了如何解决这个问题!

吾辈在 segementfault 上的提问 JavaScript 中如何获取子类? 以及之后写的一篇博客 JavaScript 避免使用 if-else 的方法

使用 heroku 免费部署 Shadowsocks

场景

感谢 heroku 提供的服务以及 mrluanma 提供的部署脚本。

现在看到这篇文章的各位是如何翻墙的呢?

  • 使用 vultr 自建 SSR
  • 购买 SSR/V2ray 服务
  • 使用免费的梯子
  • 其他?

之前提到过 heroku 可以搭建免费的 SS 服务,这里就来具体说明一下如何操作

附:免费的服务来之不易,请勿滥用 heroku 服务,避免对正常开发者使用造成影响

具体步骤

注册 heroku 账号

注册页面 填写一些信息就可以免费注册 heroku 帐号了。
免费账号有如下限制

  • 能够使用 512M 内存
  • 30min 无人访问后应用休眠
  • 应用每个月 500h 的免费活动时间

对于真正的项目这种配置当然不够,但如果只是部署一个 Shaodowsocks 的话还是绰绰有余的,而且也不可能无时无刻都在使用 Shadowsocks。

注:如果想要快速稳定的 SS 服务,请选择购买付费的 SS 服务。毕竟,某种意义上,免费的才是最贵的!

创建一个 Shadowsocks APP

部署到 heroku

设置 APP

需要设置的有 4 项,其中的密码必填!

  • APP 名字,也是之后 heroku 为你分配的子域名,默认为随机字符串
  • 选择服务器的位置,默认美国
  • 选择 Shadowsocks 连接密码
  • 选择加密算法,默认 aes-26-cfb

heroku app 设置

设置完成后点击 Deploy app,等待部署完成后,点击最下方的 View 按钮,如果在新标签页看到下面的这句话就代表部署成功了

1
Welcome to Heroku https://github.com/onplus/shadowsocks-heroku

使用客户端

Shadowsocks 客户端页面 下载对应平台的客户端,Windows 平台的链接是 https://github.com/onplus/shadowsocks-heroku/releases/download/0.9.10.1/ss-h-win64.zip

解压出来的文件

解压出来,可以看到 config.json 文件,我们需要修改一下配置

1
2
3
4
5
6
7
8
9
10
{
"server": "rxliuli-ss-demo.herokuapp.com", // Shadowsocks App 域名
"local_address": "127.0.0.1",
"scheme": "ws",
"local_port": "1080",
"remote_port": "80",
"password": "rxliuli-ss-demo-147258369", // Shadowsocks App 密码
"timeout": 600,
"method": "aes-256-cfb" // 加密方法,默认是 aes-256-cfb
}

现在,我们可以双击 ss-h.exe 启动 Shadowsocks,这种方式会打开一个命令行窗口。如果想后台运行可以使用 start.vbs 脚本。

浏览器设置

安装插件 Proxy SwitchyOmega,然后在 导入/导出 > 在线恢复 中输入 https://gist.githubusercontent.com/rxliuli/7447e51653a35e2a36a294f2b8ba9052/raw/57154aaa799f1c9d413500b63f38eb91fd1c075c/SwitchyOmegaBak,然后点击 恢复

设置 Proxy SwitchyOmega

访问 https://www.google.com/,嗯,现在还无法访问,我们选择 AutoSwitch 模式

选择 AutoSwitch 模式

好了,大功告成,我们以后可以正常在浏览器上网了!