如何把一个qmake的Ubuntu手机应用打包为一个snap应用

我们知道Ubuntu平台提供了良好的融合(convergence)设计.通过融合设计,使得我们的同样一个应用在不需要修改任何代码的情况下,重新打包就可以运行到不同的屏幕尺寸的设备上.当然,Canonical公司最终的目的是实现snap应用运行到所有设备上,而不需要进行任何的重新打包的动作.目前Ubuntu手机上支持的应用打包格式是click包.在为了的Ubuntu SDK中,最终我们会把snap的支持加入到我们的SDK之中去.那么目前我们怎么把我们已经开发好的应用打包成为一个snap应用包,并可以成功部署到我们的电脑桌面(16.04)上呢?

如果大家对如何安装一个snap应用到16.04的桌面系统上的话,请参阅文章"安装snap应用到Ubuntu 16.4桌面系统".


1)通过Ubuntu SDK开发一个我们需要的手机应用


我们可以通过Ubuntu SDK来创建一个我们想要的项目.关于如何创建一个Ubuntu手机应用,这个不在我们的这个教程范围.如果你对如何利用Ubuntu SDK开发一个手机应用感兴趣的话,请参考我们的文章" Ubuntu 手机开发培训准备".这里将不再累述!

值得指出的是:在今天的教程中,我们将教大家如何把一个qmake的Ubuntu手机应用打包为一个snap的应用.这里,我们将利用之前我已经开发的一个项目作为例程来开始.我们在terminal下打入如下的命令:

$ git clone https://github.com/liu-xiao-guo/rssreader_snap

下载后的源码结构如下:

liuxg@liuxg:~/snappy/desktop/rssreader$ tree -L 2
.
├── snapcraft.yaml
└── src
    ├── manifest.json.in
    ├── po
    ├── rssreader
    └── rssreader.pro

从上面的结构上,我们可以看到:在src目录下的整个项目是有我们的Ubuntu SDK所创建的一个qmake项目,它有一个项目文件.pro文件.在手机上的运行情况如下:

  

同时,在我们项目的根目录下,我们发现了另外一个文件snapcraft.yaml.这个文件就是为了能够让我们把我们的qmake项目最终打包为snap应用的snap项目文件.


2)为我们的qmake项目打包


在上节中,我们已经提到了我们项目的snapcraft.yaml文件.现在我们把这个文件展示如下:

snapcraft.yaml

name: rssreader-app
version: 1.0
summary: A snap app from Ubuntu phone app
description: This is an exmaple showing how to convert a Ubuntu phone app to a desktop snap app
confinement: strict

apps:
  rssreader:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader
    plugs: [network,network-bind,network-manager,home,unity7,opengl]

parts:
  rssreader:
    source: src/
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    stage:
      - -usr/share/pkgconfig/xkeyboard-config.pc 
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]

初以乍看,这个文件和我们以往所看到的文件都不同.似乎很复杂!关于snapcraft.yaml的具体解释,我们可以参考我们的官方文档" snapcraft.yaml syntax".

在这里,我们做一个简单的解释:
  • name: 这是最终的包的名称.针对我们的情况,我们最终的snap包的名字为rssreader-app_1.0_amd64.snap
  • version: 这是我们包的版本信息.就像我们包的名称rssreader-app_1.0_amd64.snap所展示的那样.1.0是我们的版本信息
  • summary: 这是一个描述我们包信息的字符串.根据我们的设计,他只能最多长达79个字符
  • description:这是一个描述我们包的字符串.它可以比summary来得更长一些
  • confinement: 受限的种类:strict 或 devmode.当我们设置为devmode时,在安装时加上--devmode选项时,可以使得我们的应用不接受任何的安全的限制.就像我们以前在Ubuntu电脑上开发一样.我们可以随意地访问任何一个我们想要访问的目录等等
  • apps: 在这里定义我们的应用及其运行时所需要的命令.针对我们的情况,我们定义rssreader为我们的应用.当我们执行我们的应用时,我们需要使用<<包名>>.<<应用名>>来运行我们的应用.针对我们的情况,我们使用rssreader-app.rssreader来通过命令行来运行我们的应用
    • command:这是用来启动我们应用所需要的命令行.针对我们的情况:desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader.这里的desktop-launch来自我们下面的已经预先编译好的包 desktop/qt5
    • plugs:这一项定义了我们的snap应用所访问的权限.通过我们的设定,我们可以访问系统的$HOME目录及使用opengl等.更多细节请擦参阅Interfaces
  • parts: 每个part定义了我们软件所需要的部分.每个part就像一个mini的小项目.在我们编译时可以在parts的目录中分别找到对应的部分
    • rssreader: 我们定义的part的名称.它的名字可以是任何你所喜欢的名称
      • source: 定义part的源码.它可以在网站上的任何一个软件(bzr, git, tar)
      • plugin: 定义编译part所需要用到的plugin.开发者也可以拓展snapcraft的plugin.请参阅文章"Write your own plugins
      • qt-version:这个是针对Qt plugin来说的.定义Qt的版本
      • build-packages:定义在这里的每个包都是为了用来编译我们的项目的.需要安装的.它们将不会出现在最终的snap文件中
      • stage-packages:这些定义的包都要最终被打入到snap包中,并形成运行该应用所需要的文件.特别值得指出的是:我们加入了中文包ubuntu-defaults-zh-cn,从而使得我们的应用可以看见中文的显示.当然,我们包的大小也从100多兆增加到300多兆.注意这里的包都对应在我们通常ubuntu下的debian包
      • snap:定义了我们需要的或不需要的文件.在这里我们通过"-"来把一些不想要的文件剔除从而不打入到我们的包中
      • after:表明我们的这个part的编译必须是在desktop/qt5下载之后.对于有些项目,我们必须先得到一个part,并使用这个part来编译我们其它的part.在这种情况下,我们可以使用after来表明我们的先后顺序.在这里,我们也利用了其它人所开发的part desktop/qt5.我们可以在网址https://wiki.ubuntu.com/snapcraft/parts找到别人已经发布的parts.这些parts的描述也可以在地址https://wiki.ubuntu.com/Snappy/Parts找到.我们可以重复利用它们.在命令行中,我们也可以公共如下的命令来查找已经有的parts:
        • snapcraft update
        • snapcraft search
当然我们只做了一个简单的描述.更详细的关于snapcraft.yaml的描述,请参阅我们的文档" snapcraft.yaml syntax"或 文档

3)编译我们的snap应用


为了编译我们的snap应用,其实非常简单.我们直接进入到我们的项目的根目录下,并打入如下的命令:

$ snapcraft

这样,我们就可以编译我们的应用,并最终生产我们所需要的.snap文件:

312M 7月  13 12:25 rssreader-app_1.0_amd64.snap

就像我们上节中介绍的那样,由于我们加入了中文字体,所以我们的应用变得非常庞大.没有字体的snap包大约为141M.
如果我们想要清楚我们的打包过程中的中间文件,我们可以打入如下的命令:

$ snapcraft clean

它将清除在parts, stage及prime目录下的所有的文件.更多关于snapcraft的介绍,可以参阅我的文章" 安装snap应用到Ubuntu 16.4桌面系统".我们也可以通过如下的方式来得到它的帮助:

$ snapcraft --help


4)安装及运行我们的应用


我们可以通过如下的命令来安装我们的.snap文件:

$ sudo snap install rssreader-app_1.0_amd64.snap --force-dangerous

我们可以通过如下的命令来运行我们的应用:

$ rssreader-app.rssreader

运行时的画面如下:



从上面可以看出来.我们没有经过任何的修改,但是我们的手机应用也可以在16.04的桌面上运行得非常好.在本应用中,它也使用了融合(Convergence)技术,从而使得我们的应用在不同的屏幕尺寸上自动适配.大家可以参阅我的文章" 运用AdaptivePageLayout来做融合(convergence)设计以实现动态布局


5)安全调试


我们可以在我们的桌面系统中安装如下的软件:

$ snap install snappy-debug

如果在安装的过程中提示还需要安装其它的应用软件,我们按照提示安装即可.
接下来我们在一个terminal中打入如下的命令:

$ snappy-debug.security scanlog

根据 Interfaces文档介绍,log-observe是不能自动连接的,我们需要使用如下的命令来手动建立这种连接:

$ sudo snap connect snappy-debug:log-observe ubuntu-core:log-observe

这种方法也适用于我们建立其它需要手动链接的的情况.然后在另外一个terminal中打入如下的命令:

$ rssreader-app.rssreader

那么我们可以在这个窗口看到如下的信息:



显然在我们的应用运行时产生了一些安全的问题,并提示一些上面的输出信息.我们切换到另外一个运行命令"snappy-debug.security scanlog"的窗口:



显然在这个输出窗口也显示了一些"DENIED"安全错误信息.那么这些问题是怎么来的呢?显然,这可能是我们的应用没有设置相应的plug所致.我们可以参阅我们的官方文档 interfaces,并结合我们的应用.我们可以初步判断,我们可能需要network及network-bind plug.这是因为我们的应用是一个网路的应用,需要从网上抓数据.另外,在我们代码的 main.cpp中,我们使用了如下的代码:

QNetworkAccessManager *MyNetworkAccessManagerFactory::create(QObject *parent)
{
    QNetworkAccessManager *nam = new QNetworkAccessManager(parent);

    QString path = getCachePath();
    QNetworkDiskCache* cache = new QNetworkDiskCache(parent);
    cache->setCacheDirectory(path);
    nam->setCache(cache);

    return nam;
}

这段代码是为了设置一个cache,从而使得我们抓过来的照片能够得到cache,进而我们不需要浪费网路资源重复获取同样的一个照片.根据我们目前所有的plugs:

liuxg@liuxg:~$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app
:locale-control      -
:log-observe         snappy-debug
:modem-manager       -
:mount-observe       -
:network             -
:network-bind        -
:network-control     -
:network-manager     -
:network-observe     -
:opengl              rssreader-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app
:x11                 -

显然,netwrok-manager是我们所需要的plug.我们可以在我们的snapcraft.yaml加入上面所述的两个plug.修改过后的snapcraft.yaml文件如下:

snapcraft.yaml

name: rssreader-app
version: 1.0
summary: A snap app from Ubuntu phone app
description: This is an exmaple showing how to convert a Ubuntu phone app to a desktop snap app
confinement: strict

apps:
  rssreader:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader
    plugs: [network,network-bind,network-manager,home,unity7,opengl]

parts:
  rssreader:
    source: src/
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]
 

重新打包我们的应用并安装.我们可以利用:
$ snap interfaces
来显示我们应用所有的plug:

liuxg@liuxg:~/snappy/desktop/rssreader$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app
:locale-control      -
:log-observe         snappy-debug
:modem-manager       -
:mount-observe       -
:network             rssreader-app
:network-bind        rssreader-app
:network-control     -
:network-manager     -
:network-observe     -
:opengl              rssreader-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app
:x11                 -
-                    rssreader-app:network-manager

在上面的最后一行,我们可以看到:
-                    rssreader-app:network-manager
这是什么意思呢?我们来重新查看连接 interfaces,在那个页面里虽然目前还没有network-manager的介绍,但是我们可以看到诸如:

network-control

Can configure networking. This is restricted because it gives wide, privileged access to networking and should only be used with trusted apps.

Usage: reserved Auto-Connect: no

我们需要注意的是最下面的一句话:Auto-Connect: no.也就是说自动连接不存在.对于我们的networrk-manager的情况也是一样的.我们需要手动来连接.那么我们该如何来手动连接呢?

liuxg@liuxg:~$ snap --help
Usage:
  snap [OPTIONS] <command>

The snap tool interacts with the snapd daemon to control the snappy software platform.


Application Options:
      --version  print the version and exit

Help Options:
  -h, --help     Show this help message

Available commands:
  abort        Abort a pending change
  ack          Adds an assertion to the system
  change       List a change's tasks
  changes      List system changes
  connect      Connects a plug to a slot
  create-user  Creates a local system user
  disconnect   Disconnects a plug from a slot
  find         Finds packages to install
  help         Help
  install      Install a snap to the system
  interfaces   Lists interfaces in the system
  known        Shows known assertions of the provided type
  list         List installed snaps
  login        Authenticates on snapd and the store
  logout       Log out of the store
  refresh      Refresh a snap in the system
  remove       Remove a snap from the system
  run          Run the given snap command
  try          Try an unpacked snap in the system

我们通过上面的命令,我们可以知道snap有一个叫做connect的命令.进一步:

liuxg@liuxg:~$ snap connect -h
Usage:
  snap [OPTIONS] connect <snap>:<plug> <snap>:<slot>

The connect command connects a plug to a slot.
It may be called in the following ways:

$ snap connect <snap>:<plug> <snap>:<slot>

Connects the specific plug to the specific slot.

$ snap connect <snap>:<plug> <snap>

Connects the specific plug to the only slot in the provided snap that matches
the connected interface. If more than one potential slot exists, the command
fails.

$ snap connect <plug> <snap>[:<slot>]

Without a name for the snap offering the plug, the plug name is looked at in
the gadget snap, the kernel snap, and then the os snap, in that order. The
first of these snaps that has a matching plug name is used and the command
proceeds as above.

Application Options:
      --version            print the version and exit

Help Options:
  -h, --help               Show this help message

显然我们需要使用如下的命令来完成我们的connect工作:

$ snap connect <plug> <snap>[:<slot>]

针对我们的情况,我们使用如下的命令:

$ sudo snap connect rssreader-app:network-manager ubuntu-core:network-manager

当我们打入上面的命令后,我们重新来看我们的snap interfaces:
liuxg@liuxg:~$ snap interfaces
Slot                 Plug
:camera              -
:cups-control        -
:firewall-control    -
:gsettings           -
:home                rssreader-app
:locale-control      -
:log-observe         snappy-debug
:modem-manager       -
:mount-observe       -
:network             rssreader-app
:network-bind        rssreader-app
:network-control     -
:network-manager     rssreader-app
:network-observe     -
:opengl              rssreader-app
:optical-drive       -
:ppp                 -
:pulseaudio          -
:snapd-control       -
:system-observe      -
:timeserver-control  -
:timezone-control    -
:unity7              rssreader-app
:x11                 -

显然这一次,我们已经成功地把network-manager加入到我们的应用中.我们可以重新运行我们的应用:

liuxg@liuxg:~/snappy/desktop/rssreader$ rssreader-app.rssreader 

(process:9770): Gtk-WARNING **: Locale not supported by C library.
	Using the fallback 'C' locale.
Gtk-Message: Failed to load module "overlay-scrollbar"
Gtk-Message: Failed to load module "gail"
Gtk-Message: Failed to load module "atk-bridge"
Gtk-Message: Failed to load module "unity-gtk-module"
Gtk-Message: Failed to load module "canberra-gtk-module"
qml: columns: 1
qml: columns: 2
qml: currentIndex: 0
qml: index: 0
qml: adding page...
qml: sourcePage must be added to the view to add new page.
qml: going to add the page
qml: it is added: 958
XmbTextListToTextProperty result code -2
XmbTextListToTextProperty result code -2

显然我们的应用再也没有相应的错误提示了.



6)为我们的应用加上应用图标



到目前我们的应用虽然接近完美,但是我们还是不能够在我们的dash中启动我们的应用.为此,我们在我们的应用的根目录下创建了一个setup/gui目录.在该目录下,我们创建如下的两个文件:

liuxg@liuxg:~/snappy/desktop/rssreader/setup/gui$ ls -l
total 100
-rw-rw-r-- 1 liuxg liuxg   325 7月  14 13:18 rssreader.desktop
-rw-rw-r-- 1 liuxg liuxg 91353 7月  14 11:50 rssreader.png

加入这两个文件后的项目结构为:

liuxg@liuxg:~/snappy/desktop/rssreader$ tree -L 3
.
├── setup
│   └── gui
│       ├── rssreader.desktop
│       └── rssreader.png
├── snapcraft.yaml
└── src
    ├── manifest.json.in
    ├── po
    │   └── rssreader.liu-xiao-guo.pot
    ├── rssreader
    │   ├── components
    │   ├── main.cpp
    │   ├── Main.qml
    │   ├── rssreader.apparmor
    │   ├── rssreader.desktop
    │   ├── rssreader.png
    │   ├── rssreader.pro
    │   ├── rssreader.qrc
    │   └── tests
    └── rssreader.pro

有了上面的两个文件,我们再次重新打包并安装我们的snap应用.安装完后,在我们的dash里:


我们可以找到我们的RSS Reader应用了.


7)在不需要安装的情况下运行我们的应用



我们知道在开发的过程中,我们每次安装的时候都会产生一个新的版本.这个新的安装不仅花费很多的时间,而且占用系统的空间(每个版本占用的空间比较大).在开发时,我们能否不用安装而直接使用已经在打包过程中生产的文件呢?答案是肯定的.我们可以通过:

liuxg@liuxg:~$ snap --help
Usage:
  snap [OPTIONS] <command>

The snap tool interacts with the snapd daemon to control the snappy software platform.


Application Options:
      --version  print the version and exit

Help Options:
  -h, --help     Show this help message

Available commands:
  abort        Abort a pending change
  ack          Adds an assertion to the system
  change       List a change's tasks
  changes      List system changes
  connect      Connects a plug to a slot
  create-user  Creates a local system user
  disconnect   Disconnects a plug from a slot
  find         Finds packages to install
  help         Help
  install      Install a snap to the system
  interfaces   Lists interfaces in the system
  known        Shows known assertions of the provided type
  list         List installed snaps
  login        Authenticates on snapd and the store
  logout       Log out of the store
  refresh      Refresh a snap in the system
  remove       Remove a snap from the system
  run          Run the given snap command
  try          Try an unpacked snap in the system

我们在上面的命令中,发现一个叫做 try的命令.在我们运行完snapcraft命令后,snapcraft会帮我们生产相应的.snap文件.同时它也帮我们生产如下的目录:

liuxg@liuxg:~/snappy/desktop/rssreader$ ls -d */
parts/  prime/  setup/  src/  stage/

这其中有一个叫做prime的目录,其实它的里面就是我们安装snap后的文件.我们可以使用如下的方法:

$ sudo snap try prime/

通过上面的命令我们就可以把我们的snap安装到系统中.重新展示我们的snap安装目录:

liuxg@liuxg:/var/lib/snapd/snaps$ ls -al
total 287200
drwxr-xr-x 2 root root      4096 7月  15 11:13 .
drwxr-xr-x 7 root root      4096 7月  15 11:13 ..
-rw------- 1 root root  98439168 7月  14 13:10 mpv_x1.snap
-rw------- 1 root root 127705088 7月  14 17:30 photos-app_x1.snap
lrwxrwxrwx 1 root root        42 7月  15 11:13 rssreader-app_x1.snap -> /home/liuxg/snappy/desktop/rssreader/prime
-rw------- 1 root root     16384 7月  13 15:53 snappy-debug_22.snap
-rw------- 1 root root  67899392 7月  13 12:26 ubuntu-core_122.snap

我们看见其实就是一个软链接.通过这样的方法,我们很快地部署了我们的snap应用.同时避免每次安装时各个不同版本之间带来的不同安装.snap包实际上是一个使用squashfs打包而生产的.通过snap try,我们不需要创建一个squashfs文件.他的另外一个好处是我们可以随时修改这个snap的应用的内容,这是因为它本身是read-write的.我们可以甚至加上--devmode选项来取消应用对安全的限制.


8)利用devmode免除在开发时的安全考虑



我们知道在开发的过程中,我们有时不知道需要使用哪写plug来保证我们的软件正常运行.这个时候,我们可以在我们的snapcraft中定义修改我们的confinement使之成为devmode:

snapcraft.yaml


name: rssreader-app
version: 1.0
summary: A snap app from Ubuntu phone app
description: This is an exmaple showing how to convert a Ubuntu phone app to a desktop snap app
confinement: devmode

apps:
  rssreader:
    command: desktop-launch $SNAP/lib/x86_64-linux-gnu/bin/rssreader
    plugs: [network,network-bind,network-manager,home,unity7,opengl]

parts:
  rssreader:
    source: src/
    plugin: qmake
    qt-version: qt5
    build-packages:
      - cmake
      - gettext
      - intltool
      - ubuntu-touch-sounds
      - suru-icon-theme
      - qml-module-qttest
      - qml-module-qtsysteminfo
      - qml-module-qt-labs-settings
      - qtdeclarative5-u1db1.0
      - qtdeclarative5-qtmultimedia-plugin
      - qtdeclarative5-qtpositioning-plugin
      - qtdeclarative5-ubuntu-content1
      - qt5-default
      - qtbase5-dev
      - qtdeclarative5-dev
      - qtdeclarative5-dev-tools
      - qtdeclarative5-folderlistmodel-plugin
      - qtdeclarative5-ubuntu-ui-toolkit-plugin
      - xvfb
    stage-packages:
      - ubuntu-sdk-libs
      - qtubuntu-desktop
      - qml-module-qtsysteminfo
      - ubuntu-defaults-zh-cn
    snap:
      - -usr/share/doc
      - -usr/include
    after: [desktop/qt5]
 

请注意上面的这一行:

confinement: devmode

当我们这样设计后,plugs里定义的所有的plug将不起任何的作用(我们甚至可以把它们全部删除).在我们安装我们的snap时,我们使用如下的命令:

$ sudo snap install rssreader-app_1.0_amd64.snap --devmode

注意我们上面加入--devmode选项.它表明,我们的应用将不受任何的安全沙箱的限制.它可以做它任何喜欢做的事情.在运行的过程中将不再生产任何的安全错误的提示.这样的设计对于我们开发者来说,我们可以很快地开发出我们所需要的应用,而不用先考虑我们的安全问题.












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值