Content Table

Vue-Cli 3 创建多页项目

Vue-Cli 3 创建单页项目 一文中介绍了创建单页项目, 现实中复杂一点的项目使用多页项目更合适一些, Vue-Cli 3 创建多页项目很容易, 创建的项目结构如下:

下面就简要的介绍下使用 Vue-Cli 3 创建多页项目的步骤:

  1. Vue-Cli 3 创建单页项目

  2. 在 src 目录下创建目录 pages/page1, pages/page2 (每个目录表示一个单页)

  3. 把 views 目录, App.vue, main.js, router.js, store.js 等都分别复制一份到 page1 和 page2 目录 (参考上面的项目结构图)

  4. 修改 vue.config.js 中的 pages, 配置每个单页的入口 (page1, page2 就是单页访问地址):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    devServer: {
    port: 8888,
    },
    pages: {
    page1: 'src/pages/page1/main.js',
    page2: 'src/pages/page2/main.js',
    },
    };
  5. 启动项目: yarn serve

  6. 访问页面:

  7. 打包项目: yarn build

  8. 自定义单页的入口模板

    有眼神好的同学可能看到了 public/index.htmlpublic/page2.html, 但是发现没有 public/page1.html, 这是因为 Vue-Cli 在 public 目录下发现页面对应的 ${subpage}.html 文件 (pages 下配置的 page1, page2), 就使用它生成这个页面的入口 html 文件, 如果没有则会使用 public/index.html 作为模板生成这个页面的入口 html 文件.

    由于不同页面引入的 js, css 或者第三方库等都可能不一样, 所以为页面定制自己的入口模板文件也是有必要的.

vue.config.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
process.env.VUE_APP_VERSION = new Date().getTime();

module.exports = {
devServer: {
port: 8888,
proxy: 'http://localhost:8080'
},

// 多页的页面
pages: {
sample: 'src/pages/sample/main.js',
teacher: 'src/pages/teacher/main.js',
'admin-school': 'src/pages/admin-school/main.js',
'admin-system': 'src/pages/admin-system/main.js',
},

// yarn build 的输出目录
outputDir: '../ebag-web-app/src/main/webapp/WEB-INF/page-vue',
assetsDir: 'static',

css: {
loaderOptions: {
sass: {
// SCSS 全局变量
data: `
@import "@/../public/static/css/variables.scss";
`
}
}
}
};

页面的 HTML 中引入自定义 JS 和 CSS 文件时最好也加上打包时的时间戳 process.env.VUE_APP_VERSION, 避免缓存引起的问题:

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
<!DOCTYPE html>
<html>

<% hash = VUE_APP_VERSION %>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>老师</title>

<link rel="stylesheet" type="text/css" href="/static/css/global.css?hash=<%= hash %>">
</head>

<body>

<div id="app"></div>
<!-- 库文件 -->
<script src="/static/lib/jquery.min.js"></script>
<script src="/static/lib/jquery.rest.js"></script>

<!-- 项目的 JS: 增加了 hash 使得更新后不用刷新缓存就能使用最新的 JS -->
<script src="/static/lib/tio/Message.js?hash=<%= hash %>"></script>
<script src="/static/js/urls.js?hash=<%= hash %>"></script>

<!-- built files will be auto injected -->
</body>
</html>

ZooKeeper 实现分布式锁

在 Java 中使用 ZooKeeper 实现分布式锁可按以下几步进行:

  1. 下载安装 ZooKeeper 3.4.13,参考 本机安装 ZooKeeper 集群 进行安装单机版 ZooKeeper,有必要的时候再安装集群
  2. 引入 Curator 的依赖,它实现了 ZooKeeper 的分布式锁
  3. Java 测试程序

下载安装

参考 本机安装 ZooKeeper 集群

引入 Curator 的依赖

1
compile group: 'org.apache.curator', name: 'curator-recipes', version: '2.12.0'

注意: Curator 和 ZooKeeper 的版本需要对应,否则会报错

Curator 2.x.x: compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x

Curator 3.x.x: compatible only with ZooKeeper 3.5.x and includes support for new features such as dynamic reconfiguration, etc.

Java 测试程序

官方文档: http://curator.apache.org/getting-started.html,先演示 Curator 连接 ZooKeeper 并使用分布式锁 InterProcessMutex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void testDistributedLock() throws Exception {
// [1] This will create a connection to a ZooKeeper cluster using default values.
// The only thing that you need to specify is the retry policy. For most cases, you should use:
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(10000, 3));

// [2] The client must be started (and closed when no longer needed).
client.start();

InterProcessMutex lock = new InterProcessMutex(client, "/ebag/lock");

// [3] 获取全局锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// [4] 业务代码
System.out.println("Do something");
} finally {
// [5] 释放全局锁
lock.release();
}
}

// [6] Close the client
client.close();
}

多线程测试分布式锁,结果 sn 是按顺序输出的,说明锁生效了:

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
public static void testZooKeeperThread() {
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(10000, 3));
client.start();
List<Thread> threads = new LinkedList<>();

for (int i = 0; i < 100; ++i) {
threads.add(new Thread(new ZooKeeperRunnable(client, "/ebag/lock")));
}

for (Thread thread : threads) {
thread.start();
}

// client.close();
}

class ZooKeeperRunnable implements Runnable {
public static int sn = 0;
private InterProcessMutex lock;

public ZooKeeperRunnable(CuratorFramework client, String path) {
lock = new InterProcessMutex(client, path);
}

public void run() {
try {
// 获取全局锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// Thread.sleep(100);
sn++;
System.out.println(sn);
} finally {
// 释放全局锁
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

提示: 上面的多线程是同一个 JVM 中的,可以很简单的修改为在多个 JVM 中运行,演示真正的分布式锁的效果,共享资源 sn 可以保存到 Redis,数据库等中。

ZooKeeper 常用命令

  • 创建: create path data: create /foo "Hello"
  • 修改: set path data: set /foo "Vox"
  • 获取: get path: get /foo
  • 删除: delete path: delete /foo
  • 查看: ls path: ls /foo

CentOS7 安装 LibreOffice

使用 CentOS7 无界面版本作为服务器的操作系统,在里面安装 LibreOffice 用于转换各种文档到 PDF,LibreOffice 的安装步骤如下:

  1. 下载:

    1. 访问 https://www.libreoffice.org/download/download/
    2. 选择 Linux x86_64(rpm) 的版本
    3. 下载得到 LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz (目前最新版为 6.0.5)
  2. 安装:

    1. 删除: 在安装之前,先删除已经安装的 LibreOffice: yum remove libreoffice*
    2. 解压: tar -xvf LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz
    3. 安装:
      1. cd LibreOffice_6.0.5.2_Linux_x86-64_rpm/RPMS
      2. yum localinstall *.rpm
    4. 查看:
      1. which libreoffice6.0 看到路径为 /usr/bin/libreoffice6.0
      2. ll /usr/bin/libreoffice6.0 得到 /opt/libreoffice6.0/program/soffice,说明安装到了 /opt/libreoffice6.0
  3. 依赖:

    执行 libreoffice6.0 可能会提示库文件找不到,如 libcairo.so.2,libcups.so.2,libSM.so.6 等,执行下面几条命令安装需要的库:

    • yum install cairo -y
    • yum install cups-libs -y
    • yum install libSM -y

安装 LibreOffice 可参考 https://www.tecmint.com/install-libreoffice-on-rhel-centos-fedora-debian-ubuntu-linux-mint/

Java 中可使用 JODConverter 调用 LibreOffice 进行文件格式转换,可参考 Office 文档转为 PDF 和 HTML

QLineEdit 中增加按钮

下图为 Safari 的地址栏,在输入框右边有一个刷新按钮:

在输入框中增加按钮的设计是比较常见的,例如 Chrome 的地址栏、Firefox 的搜索框、Tim 的搜索框等,这种控件 Qt 没有提供,那应该怎么实现呢?下面提供 2 种思路:

  • QLineEdit + QPushButton 使用 QHBoxLayout 布局到一个 QWidget 中,去掉 QLineEdit 得到焦点时的高亮效果,此时应该高亮它的父控件,失去焦点时取消它的父控件的高亮效果
  • QLineEdit 作为一个普通的 QWidget,也就是它能够使用 QLayout 把 QPushButton 作为子控件布局到它里面

思路有了,第一种方式需要写很多代码进行控制,实现比较麻烦,下面只介绍第二种思路的实现。

模型视图编程

模型视图控制器 (MVC) 编程哪家讲的好,莫非是 Qt 帮助文档里的 Model/View Programming 了,可惜没有中文的,看过不少书里这方面的内容都是从这篇文档里抄的,我们就不要再花心思去创新了,直接翻译吧。

模型视图编程简介

Qt contains a set of item view classes that use a model/view architecture to manage the relationship between data and the way it is presented to the user. The separation of functionality introduced by this architecture gives developers greater flexibility to customize the presentation of items, and provides a standard model interface to allow a wide range of data sources to be used with existing item views. In this document, we give a brief introduction to the model/view paradigm, outline the concepts involved, and describe the architecture of the item view system. Each of the components in the architecture is explained, and examples are given that show how to use the classes provided.

Qt 中有几个 item view 的类 (视图),使用模型/视图架构来管理和显示数据。这种分离的设计给开发者很高的灵活性,可以自定义数据的显示方式,视图通过模型提供的标准接口可以支持各种各样的数据源。本文涉及到模型/视图编程范例、简要的概念介绍以及描述视图系统的架构。每一个部分都会进行解释以及给出相关代码展示怎么使用。

模型视图架构

Model-View-Controller (MVC) is a design pattern originating from Smalltalk that is often used when building user interfaces. In Design Patterns, Gamma et al. write:

MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input. Before MVC, user interface designs tended to lump these objects together. MVC decouples them to increase flexibility and reuse.

Smalltalk 经常用来构建用户界面,模型-视图-控制器 (MVC) 这种设计模式就是从 Smalltalk 借鉴而来的。Gamma et al. 在设计模式中写到:

MVC 由 3 种对象组成:Model 是应用的数据,View 显示数据,Controller 定义了用户界面对用户输入的响应。在使用 MVC 之前,用户界面的设计常常把这些对象耦合在一起。MVC 能够解耦它们从而提供更高的灵活性和重用性。

The model/view architecture The model communicates with a source of data, providing an interface for the other components in the architecture. The nature of the communication depends on the type of data source, and the way the model is implemented.

The view obtains model indexes from the model; these are references to items of data. By supplying model indexes to the model, the view can retrieve items of data from the data source.
In standard views, a delegate renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes.

在模型/视图架构中,模型访问数据源中的数据,视图等控件通过模型提供的接口访问数据。通信的方式取决于数据源的类型和模型实现的方式。 数据由`数据项`组成,index 是数据项的引用,视图从模型中获得 index。视图通过访问模型中的 index 就能够访问数据源中的数据了。 在标准的视图中,`代理`用来渲染数据项。当数据项被编辑时,代理使用模型的 index 和模型交互。

Generally, the model/view classes can be separated into the three groups described above: models, views, and delegates. Each of these components is defined by abstract classes that provide common interfaces and, in some cases, default implementations of features. Abstract classes are meant to be subclassed in order to provide the full set of functionality expected by other components; this also allows specialized components to be written.

Models, views, and delegates communicate with each other using signals and slots:

  • Signals from the model inform the view about changes to the data held by the data source.
  • Signals from the view provide information about the user’s interaction with the items being displayed.
  • Signals from the delegate are used during editing to tell the model and view about the state of the editor.

根据上面的介绍,模型/视图的类可以分为三类:模型、视图和代理,它们都有相应的抽象类提供通用的接口和某些功能的默认实现。抽象类就意味着要被其他类继承,根据需求提供对应的实现。

模型、视图和代理之间使用信号槽进行通信:

  • 数据源中的数据发生变化时,模型发射信号通知视图
  • 用户和视图交互时视图会发射信号,例如点击 view item,信号的参数包含了被交互的 view item 的信息
  • 编辑数据项的时候代理会把编辑器的状态通过信号通知模型和视图

模型

All item models are based on the QAbstractItemModel class. This class defines an interface that is used by views and delegates to access data. The data itself does not have to be stored in the model; it can be held in a data structure or repository provided by a separate class, a file, a database, or some other application component.

The basic concepts surrounding models are presented in the section on Model Classes.

QAbstractItemModel provides an interface to data that is flexible enough to handle views that represent data in the form of tables, lists, and trees. However, when implementing new models for list and table-like data structures, the QAbstractListModel and QAbstractTableModel classes are better starting points because they provide appropriate default implementations of common functions. Each of these classes can be subclassed to provide models that support specialized kinds of lists and tables.

The process of subclassing models is discussed in the section on Creating New Models.

所有的 item 模型都是基于类 QAbstractItemModel 实现的。视图和代理使用 QAbstractItemModel 定义的接口访问数据。数据不一定是保存在模型中,也可以保存在其他类、文件、数据库或者其他应用中。

模型相关的概念在模型的类一节中进行介绍。

模型类 QAbstractItemModel 定义的访问数据接口 (函数) 是很灵活的,能够满足表格、列表和树用来显示模型的数据。然而,当给列表和表格自定义新的模型时,继承 QAbstractListModel 或者 QAbstractTableModel 是个很好的选择,因为他们提供了很多通用操作的默认实现,就不需要我们再重复实现。

实现自定义模型在创建新的模型类一节中进行介绍。

Qt provides some ready-made models that can be used to handle items of data:

  • QStringListModel is used to store a simple list of QString items.
  • QStandardItemModel manages more complex tree structures of items, each of which can contain arbitrary data.
  • QFileSystemModel provides information about files and directories in the local filing system.
  • QSqlQueryModel, QSqlTableModel, and QSqlRelationalTableModel are used to access databases using model/view conventions.

If these standard models do not meet your requirements, you can subclass QAbstractItemModel, QAbstractListModel, or QAbstractTableModel to create your own custom models.

Qt 已经提供了一些可用于处理数据项的模型:

  • QStringListModel 用于存储简单的列表数据,数据项为 QString
  • QStandardItemModel 用于管理有树结构关系的数据项,每个数据项可以包含任意的数据
  • QFileSystemModel 访问本地文件系统的文件和文件夹
  • QSqlQueryModel, QSqlTableModel 和 QSqlRelationalTableModel 使用模型视图的方式访问数据库

如果这些标准的模型还不能满足我们的需求,可以继承 QAbstractItemModel, QAbstractListModel, or QAbstractTableModel 实现自定义的模型类。

分组布局

在进行界面布局的时候,常把控件根据功能分组放在一起,最常用的就是使用 QGroupBox 来放置一组控件。QGroupBox 虽然使用起来很方便,但就是有点丑,在要求较高的设计中,还得使用控件组合加自定义绘图或者 QSS 等才能实现,例如下面这个软件界面,直接使用 Qt 提供的控件是满足不了的:

上图中分组的布局没有使用 QGroupBox,而是用几个控件组合起来实现的,设计如下:

异形按钮组

不少软件里看到过如下的按钮组,有 5 个按钮,中间 1 个,上下左右各一个:

可以通过绘图的方式实现:计算每一个按钮的位置、大小、图片、点击的时候判断点击到了哪个按钮然后刷新绘制它的样式,并调用相应的函数执行点击操作,难度还是相当大的。

操作图像像素,实现各种效果

Qt 中图像相关的类主要是 QPixmap 和 QImage,QPixmap 没有提供访问图像像素数据的接口,访问图像的像素数据需要使用 QImage,主要的函数有 (相关重载函数没有列出来):

1
2
3
4
5
6
7
8
// 获取图像的像素数据
QRgb pixel(int x, int y) const
QColor pixelColor(int x, int y) const
uchar* scanLine(int i)

// 设置图像的像素数据
void setPixel(int x, int y, uint index_or_rgb)
void setPixelColor(int x, int y, const QColor &color)

下面把一个图像转为灰度图为例介绍怎么操作图像的像素:

  1. 取得图像的宽、高
  2. 根据宽、高遍历每一个像素
  3. 得到每一个像素的 RGBA 颜色分量
  4. 对得到的颜色分量 RGBA 进行灰度计算得到新的颜色
  5. 使用计算得到的颜色设置对应像素

自定义按钮组

如下的按钮组相信大家都看到过,最左和最右的按钮是圆角的,中间的按钮是矩形的,同时只能有一个按钮是选中状态:

按钮的样式使用 QSS 实现,使用 setProperty 设置按钮的 class 属性为 GroupButton,就可以利用类选择器 .GroupButton 选择按钮组的按钮,避免它们的样式影响到普通按钮。为了单独设置最左和最右按钮的样式,使用 setProperty 为其设置一个属性 position,最左按钮的为 first,最右按钮的为 last,然后就能使用属性选择器 .GroupButton[position="first"].GroupButton[position="last"] 选择它们了。