目录

  • ★☆★ 写在前面 ★☆★
  • ★☆★ 本系列文章 ★☆★
  • ★☆★ 开源网址 ★☆★
    • 一、给 Swing 加上 Spring
      • 0、前期努力
        • I. SpringBoot
        • II. SpringMVC
      • 1、开始搞起:搭建 spring 框架
      • 2、添加 Service 并使用
        • I. 准备
        • II. 使用
      • 3、异步 @Async
        • I. 准备
        • II. 使用
        • III. 涅槃重生
        • IV. 补充
    • 二、给项目打包成 exe
      • 1、打包
      • 2、转exe
    • 三、完

★☆★ 写在前面 ★☆★

请通过目录,选择感兴趣的部分阅读。

★☆★ 本系列文章 ★☆★

【java】本地客户端内嵌浏览器1 - Swing、SWT、DJNativeSwing、javaFX
【java】本地客户端内嵌浏览器2 - chrome/chromium/cef/jcef
【java】本地客户端内嵌浏览器3 - Swing 使用 Spring 框架 + 打包项目 + 转exe + 源码

★☆★ 开源网址 ★☆★

https://github.com/supsunc/swing-jcef-spring

一、给 Swing 加上 Spring

★ 这里说一下为什么使用 Spring,是因为本项目的一个功能:“搜寻仪器”,该功能调用了 dll 的方法,此方法至少要等待 7 - 8 秒才会返回结果,而正常写的话,因为是单线程,所以会导致 client 完全卡住,但不是 GG,在卡住期间,js正常运行,且在卡完之后,会直接表现当前 js 运行的状态,给人一种时间消失的感觉。
★ 因此,是打算将“搜寻仪器”扔给异步线程去做,而 spring 的 @Async 注解则正符合需求,于是我便跳进了一个深渊巨坑。

0、前期努力

I. SpringBoot

都说 SpringBoot 多么强大,然而也没真正接触过,在正式入坑之前,还请教了前辈:“SpringBoot只能构建web项目吗?”,哈哈,还是入坑了。

具体细节不再说了,最后成功了用 SpringBoot 搭建起来项目了,但是由于原来的项目依赖相关 dll,用 SpringBoot 打包之后的发布版本,怎么也弄不进去相关 dll,搞了一天,最后我放弃了 SpringBoot。

II. SpringMVC

★☆★ 最开始的想法:我们项目后台就是用 SpringMVC 啊,那么这个 client 能不能用呢。
★☆★ 然后迅速否定,SpringMVC 就是开发 JavaWeb 的,其中的 DispatcherServlet、getServletConfigClasses 等不适用于这种本地 client 啊。
★☆★ 然后转念一想,只用 Spring 不行么?

1、开始搞起:搭建 spring 框架

  1. 首先就是 Spring 的相关依赖 jar 包

下载地址:

  1. http://maven.springframework.org/release/org/springframework/spring/
  2. https://repo.spring.io/release/org/springframework/spring/

我这边主要使用了核心包:

spring 还需要 commons-logging.jar,下载地址:commons-logging

  1. 在项目中 lib 文件夹中创建 spring 文件夹,然后将 jar 包弄到里面,然后 Add as Library。

  1. 新建 package 叫做 my.spring.config,用来放置 spring 配置文件。
  2. my.spring.config 中创建 ApplicationContextXml.java,直接分享源代码:
package qpcr.spring.config;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;@Configuration
@ComponentScan(basePackages = {"my"})
public class ApplicationContextXml {}
  1. 给 idea 配上 spring 框架(此步不做也行,不影响程序 Run)
  • 打开 Project Structure,点击 Fact,然后点击“加号”,然后点击“spring”。

  • 选择 Module,点击 OK。

  • 点击右侧的“加号”。

  • 选中后点击 OK。

  • Apply、OK 关闭窗口即可。
  1. 在包 my.spring.main 中创建 UI.java,然后将 Main.java 中的 init() 方法移动到这个 UI.java 中。让 UI.java 实现一个接口 org.springframework.beans.factory.InitializingBean,并重写 afterPropertiesSet() 方法,执行 init()
package my.client.main;import my.client.browser.MyBrowser;
import my.client.handler.DownloadHandler;
import my.client.handler.MenuHandler;
import my.client.handler.MessageRouterHandler;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.browser.CefMessageRouter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;@Component
public class UI implements InitializingBean {private void init() {EventQueue.invokeLater(() -> {JFrame jFrame = new JFrame("MyBrowser");jFrame.setMinimumSize(new Dimension(1366, 738));    // 设置最小窗口大小jFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);    // 默认窗口全屏jFrame.setIconImage(Toolkit.getDefaultToolkit().getImage(jFrame.getClass().getResource("/images/icon.png")));if (!CefApp.startup()) {    // 初始化失败JLabel error = new JLabel("<html><body>&nbsp;&nbsp;&nbsp;&nbsp;在启动这个应用程序时,发生了一些错误,请关闭并重启这个应用程序。<br>There is something wrong when this APP start up, please close and restart it.</body></html>");error.setFont(new Font("宋体/Arial", Font.PLAIN, 28));error.setIcon(new ImageIcon(jFrame.getClass().getResource("/images/error.png")));error.setForeground(Color.red);error.setHorizontalAlignment(SwingConstants.CENTER);jFrame.getContentPane().setBackground(Color.white);jFrame.getContentPane().add(error, BorderLayout.CENTER);jFrame.setVisible(true);return;}MyBrowser myBrowser = new MyBrowser("https://www.baidu.com", false, false);CefClient client = myBrowser.getClient();// 绑定 MessageRouter 使前端可以执行 js 到 java 中CefMessageRouter cmr = CefMessageRouter.create(new CefMessageRouter.CefMessageRouterConfig("cef", "cefCancel"));cmr.addHandler(new MessageRouterHandler(), true);client.addMessageRouter(cmr);// 绑定 ContextMenuHandler 实现右键菜单client.addContextMenuHandler(new MenuHandler(jFrame));// 绑定 DownloadHandler 实现下载功能client.addDownloadHandler(new DownloadHandler());jFrame.getContentPane().add(myBrowser.getBrowserUI(), BorderLayout.CENTER);jFrame.setVisible(true);jFrame.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {int i;String language = "en-us";if (language.equals("en-us"))i = JOptionPane.showOptionDialog(null, "Do you really want to quit this software?", "Exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"Yes", "No"}, "Yes");else if (language.equals("zh-cn"))i = JOptionPane.showOptionDialog(null, "你真的想退出这个软件吗?", "Exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"是的", "不"}, "是的");elsei = JOptionPane.showOptionDialog(null, "你真的想退出这个软件吗?\nDo you really want to quit this software?", "Exit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"是的(Yes)", "不(No)"}, "是的(Yes)");if (i == JOptionPane.YES_OPTION) {myBrowser.getCefApp().dispose();jFrame.dispose();System.exit(0);}}});});}@Overridepublic void afterPropertiesSet() throws Exception {init();}
}
  1. 修改 main 方法。

★ 此处才是最坑的,我这边用的全是注解开发,没有一个 xml 。
★ 然而网上搜索怎么启动 spring,全是 ClassPathXmlApplicationContextFileSystemXmlApplicationContext 两个实例化方法,然后再 getBean() 之类的。

全注解开发的正确代码应该这么写:

package my.client.main;import my.spring.config.ApplicationContextXml;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class Main {public static void main(String[] args) {new AnnotationConfigApplicationContext(ApplicationContextXml.class);}
}

2、添加 Service 并使用

I. 准备
  1. 新建两个 package,分别是 my.client.interfacesmy.client.impl
  2. my.client.interfaces 中新建一个 interface 叫做 MyService。
package my.client.interfaces;public interface MyService {String doSomething();
}
  1. my.client.impl 中新建一个 class 叫做 MyServiceImpl,实现 MyService 接口,并加上 @Service 注解。
package my.client.impl;import my.client.interfaces.MyService;
import org.springframework.stereotype.Service;@Service
public class MyServiceImpl implements MyService {@Overridepublic String doSomething() {System.out.println("This is method 'doSomething'.");return "doSomething";}
}
II. 使用
  1. 给 UI.java 注入 MyService。
@Component
public class UI implements InitializingBean {private MyService myService;public UI(MyService myService) {this.myService = myService;}private void init() {...}@Overridepublic void afterPropertiesSet() throws Exception {init();}
}
  1. 将 myService 传给 MessageRouterHandler 构造函数。
// 绑定 MessageRouter 使前端可以执行 js 到 java 中
CefMessageRouter cmr = CefMessageRouter.create(new CefMessageRouter.CefMessageRouterConfig("cef", "cefCancel"));
cmr.addHandler(new MessageRouterHandler(myService), true);
client.addMessageRouter(cmr);
  1. 修改 MessageRouterHandler 构造函数,将 MyService 对象存起来。
public class MessageRouterHandler extends CefMessageRouterHandlerAdapter {private MyService myService;public MessageRouterHandler(MyService myService) {this.myService = myService;}@Overridepublic boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request, boolean persistent, CefQueryCallback callback) {...}@Overridepublic void onQueryCanceled(CefBrowser browser, CefFrame frame, long query_id) {}
}
  1. 在 onQuery 方法中,使用 myService.doSomething()。
if (request.indexOf("doSomething") == 0) {callback.success(myService.doSomething());return true;
}

3、异步 @Async

I. 准备
  1. my.spring.config 中,创建一个 class 叫做 TaskExecutorConfig,实现 AsyncConfigurer 接口。
  2. 配置线程池,重写 getAsyncExecutor() 方法。
package my.spring.config;import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;@Configuration
public class TaskExecutorConfig implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// Set up the ExecutorService.executor.initialize();// 线程池核心线程数,核心线程会一直存活,即使没有任务需要处理。// 当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。// 核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。// 默认是 1// CPU 核心数 Runtime.getRuntime().availableProcessors();executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);// 当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。// 如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。// 默认时是 Integer.MAX_VALUE// executor.setMaxPoolSize(10);// 任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。// 默认时是 Integer.MAX_VALUEexecutor.setQueueCapacity(1000);/*  keepAliveTime: 当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。*  默认时是 60*  executor.setKeepAliveSeconds(10);*/// allowCoreThreadTimeout: 是否允许核心线程空闲退出,默认值为false。// 如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。// executor.setAllowCoreThreadTimeOut(true);return executor;}
}
II. 使用
  1. my.client.interfaces 中新建一个 interface 叫做 AsyncService。
package my.client.interfaces;import java.util.concurrent.Future;public interface AsyncService {Future<String> asyncMethod();
}
  1. my.client.impl 中新建一个 class 叫做 AsyncServiceImpl,实现 AsyncService 接口,并加上 @Service 注解。重写 asyncMethod 方法,写一个 Thread.sleep(5000); 代替耗时操作。
package my.client.impl;import my.client.interfaces.AsyncService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;import java.util.concurrent.Future;@Service
public class AsyncServicesImpl implements AsyncService {@Override@Asyncpublic Future<String> asyncMethod() {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}return new AsyncResult<>("I am finished.");}
}
  1. MyServiceImpl 中注入 AsyncService
package my.client.impl;import my.client.interfaces.AsyncService;
import my.client.interfaces.MyService;
import org.springframework.stereotype.Service;@Service
public class MyServiceImpl implements MyService {private AsyncService asyncService;public MyServiceImpl(AsyncService asyncService) {this.asyncService = asyncService;}@Overridepublic String doSomething() {System.out.println("This is method 'doSomething'.");return "doSomething";}
}
  1. 重写 doSomething() 方法,使用 asyncServiceasyncMethod 方法。

★ 这是网上提供的异步结果的获取方法。
★ 等等,这个异步线程不还是在主线程用一个 while 去等待结果么?这算哪门子异步啊。

@Override
public String doSomething() {Future<String> futureAsyncMethod= asyncService.asyncMethod();String result = "";while (!futureAsyncMethod.isDone()) {try {result = futureAsyncMethod.get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}return result;
}
III. 涅槃重生

在 spring 章节部分开头,我说明了为什么要使用 spring。

其直接原因就是 client 内嵌浏览器client 发送请求,然后请求不响应的时候,client 就会卡住。
那么解决办法就很简单了:

  • 把耗时操作扔给异步线程去操作,没有 Done 则返回 “doing”,前端接收响应数据为 “doing”,则再次发请求。
  • 判断是否正在进行那个耗时操作,如果在进行,则判断 isDone,没有 Done 则返回 “doing”,重复上一步操作。
  • 如果 Done 了,则正常返回数据。
  1. 首先修改前端网页部分,如果响应数据为 “doing”,则再次发请求。(当然如果你正确返回结果就有可能是 doing 的话,那就把这个字符串换一个)
function doSomething() {// 这里的 cef 就是 client 创建 CefMessageRouter 对象的入参涉及到的字符串window.cef({request: 'doSomething',onSuccess(response) {if(response === "doing"){setTimeout(doSomething, 0); // 将任务加到新队列中,避免网页卡住}else{// 正确得到响应数据}},onFailure(error_code, error_message) {console.log(error_code, error_message);}});
}
  1. 由于 Spring 组件默认就是单例的,所以可以这么写,直接分享源代码:
package my.client.impl;import my.client.interfaces.AsyncService;
import my.client.interfaces.MyService;
import org.springframework.stereotype.Service;import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;@Service
public class MyServiceImpl implements MyService {private AsyncService asyncService;public MyServiceImpl(AsyncService asyncService) {this.asyncService = asyncService;}private Future<String> futureAsyncMethod = null;@Overridepublic String doSomething() {if (futureAsyncMethod == null)futureAsyncMethod = asyncService.asyncMethod();if (futureAsyncMethod.isDone()) {String result = "";try {result = futureAsyncMethod.get();} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}futureAsyncMethod = null;return result;} else {return "doing";}}
}
IV. 补充

如果你和我发生了一样的事情:

  1. 报错:Bean 'my.spring.config.TaskExecutorConfig' of type [XXXX] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
  2. @Async 根本没生效。
  1. 请参考这个链接:【小家Spring】注意BeanPostProcessor启动时对依赖Bean的“误伤”陷阱(is not eligible for getting processed by all...)
  2. 不过我并没有从这个链接中直接找到解决办法。
  3. 我的解决办法是,给 TaskExecutorConfig 类加上 BeanPostProcessor 的接口:
@Configuration
@EnableAsync
public class TaskExecutorConfig implements AsyncConfigurer, BeanPostProcessor {// BeanPostProcessor 接口的目的是使当前 Configuration 先加载// 可能是吧,不太清楚,请参考上面的链接@Overridepublic Executor getAsyncExecutor() {...}
}

二、给项目打包成 exe

1、打包

  1. 按图所示。

  1. 按图所示。

  1. 按图所示创建文件夹 bin。

  1. 按图所示,在 bin 中创建文件夹 jcef 和 spring,将对应依赖移进去,在 jcef 中创建 lib 文件夹。

  1. 右键单击 lib,或点击上面的“加号”,选择 Directory Content。

  1. 选择 lib 下面 jcef 里面的 lib\win64。

  1. 点击 jcef.jar 之后,点击下面的 class path 后面的展开。

  1. 编辑完了之后,Build Artifacts。

  1. 打开 Artifacts Build 之后的地方:E:\idea\jcef\out\artifacts\jcef_jar。

  1. 我们写一个 bat 文件命令行,或用 cmd cd 到此路径,然后执行命令行:java -Djava.library.path=.\bin\jcef\lib -jar jcef.jar

如果不写 -Djava.library.path=.\bin\jcef\lib 则会报之前提到过的错:no chrome_elf in java.library.path

2、转exe

E:\idea\jcef\out\artifacts\jcef_jarjcef_jar 改名为 app

  1. 下载工具:exe4j,激活过程我就不说了。
  2. 打开 exe4j,第一个页面:Welcome,直接 Next 即可。

  1. 第二个页面:Project type,默认选择 Regular mode 即可,必须选择这个,网上大部分教程全是选择 "JAR in EXE" mode,导致后面步骤完全不一样,真坑,前进的道路真曲折。

  1. 第三个页面:Application info,三个填空:
  • 第一个为应用程序名字;
  • 第二个为导出地址;
  • 第三个为 exe 地址,写一个 . 即可。

  1. 第四个页面:Executable info,输入 exe 名字,视情况勾选 Allow only a single running instance of the application,可以在 Advanced Options 中设置一些其他信息。(默认是32-bit,如果用64位jre,则需要到那里设置 Generate 64-bit executable

  1. 第五个页面:Java invocation
  • 点击那个“加号”。

  • 选择 Archive,然后选择那个 jar 包。

  • 再次点击那个“加号”,然后选择 Directory,选择 jcef 和 spring 文件夹。

  • 点击下面 Main class from 后面的"更多":

  • 点击 Advanced Options 里面的 Native libraries。

  • 点击“加号”后,选择 jcef 里面的 lib 文件夹。

  1. 第六个页面:JRE,可以设置 Minimum version,也可以在 Advanced Options 中设置一些其他信息。

  1. 第七个页面:Splash screen,第八个页面:Messages,默认即可。

  1. 第九个页面:Compile executable,等待自动完成。

  1. 第十个页面:Finished,可以点击 Save As 将配置存起来,下次直接 open 这个配置。

  1. 点击 Click Here to Start the Application ,可以直接启动 exe,或到指定路径下,双击打开。

三、完

本博客写了 4 天,写之前研究这些全部内容,用了两个星期。
本博客于 2019-10-31 18:38 首发于 CSDN博客
累死我啦!!!

【java】本地客户端内嵌浏览器3 - Swing 使用 Spring 框架 + 打包项目 + 转exe + 源码相关推荐

  1. 【java】本地客户端内嵌浏览器2 - chrome/chromium/cef/jcef

    目录 ★☆★ 写在前面 ★☆★ ★☆★ 本系列文章 ★☆★ ★☆★ 开源网址 ★☆★ 一.发现新大陆 - CEF/JCEF 0.前言 1.使用 jcef.jar 搭建项目 2.启动包含 jcef.ja ...

  2. C# WPF使用CefSharp客户端内嵌浏览器做一个开小差工具

    前言 CefSharp是一个C#客户端内嵌入chromium开源项目浏览器的工具,方便在客户端中自然的访问网页内容,十分好用.当然,网上有很多使用CefSharp的教程了,怎么使用都很详尽.我这里只是 ...

  3. java gui 嵌入浏览器_DJNativeSwing-SWT组件-Java GUI中内嵌浏览器

    Java项目中经常需要在GUI程序中嵌入浏览器,而Swing自带的组件对CSS.JS的支持不是很好,网上也有很多组件,参考 但是由于对各个平台的支持不是很好,笔者是在Mac系统下进行开发,很多组件只支 ...

  4. html微信内置浏览器点击图片放大,双指缩放,附源码(自测可用)

    本人用的是vue框架 引用微信的JS <script type="text/javascript" src="http://res.wx.qq.com/open/j ...

  5. java使用swing实现内嵌浏览器

    java使用swing实现内嵌浏览器 1.使用swing内嵌浏览器需要导入3个jar包,第3个根据电脑版本选择 dj-native-swing-swt.jar     dj-native-swing. ...

  6. Java swing 做一个传统Web项目的桌面程序启动器(内嵌浏览器)

    背景:公司有个老项目,web项目,但是使用者都想要一个桌面应用程序.实际上,是web程序的启动较为麻烦.这里每次都需要启动Tomcat和浏览器. 想法:重写一个项目太麻烦,想想成本,人间不值得.于是我 ...

  7. Java实现内嵌浏览器

    创建项目 ---->   导入需要的jar ---->  代码实现 需要的jar: https://pan.baidu.com/s/1MEZ1S0LnKSMGQm24QWgmCw 代码: ...

  8. PC游戏中用CEF3制作内嵌浏览器

    因为项目需要,需要将游戏手机助手中的朋友圈给移植到PC游戏中,而以前游戏中的内嵌浏览器采用的是IE6内核,满足不了我们的需求,于是决定把Cef3内嵌到游戏中,在完成正常工作之余,利用闲散时间不断地查找 ...

  9. java selenium div内嵌滚动条 网页长截图发邮件

    java selenium 网页内嵌滚动条截图发邮件 主要问题 下面展开说 由于公司要求做一个接口,请求这个接口进行网页截图并发送邮件的功能,本来前期是用python写好了,but似乎不太符合要求,那 ...

最新文章

  1. Datawahle文化衫来了!
  2. JSP与servlets的区别
  3. 在objective-c / cocoa中抛出异常
  4. 用CSS的float属性创建三栏布局网页的方法
  5. 多进程多线程GDB调试 (转)
  6. HDOJ 2546饭卡(01背包问题)
  7. 打印5列五颗星_13个Excel快捷打印技巧,让你熟练掌握打印机操作
  8. 基于Web用户控件的Portal
  9. mysql集群fuzhi_MySQL集群 和MySQL主从复制的不同
  10. vscode 文件高亮插件_vscode中的vue文件不高亮,但是已经安装了vetur插件了,到底为什么???...
  11. spring boot进行上传文件
  12. openGauss与PostgreSQL分区策略语法测试
  13. Java基础-Collection集合接口(List及Set)
  14. 【图像直线拟合】基于matlab最小二乘法图像直线拟合【含Matlab源码 100期】
  15. java catch中throw_Java的catch块中throw e和throw new Exception(e)有什么区别?
  16. 搜索命令:whereis/which/locate/find/grep
  17. 一加nfc门禁卡录入_一加7t怎么开启NFC 模拟门禁卡方法介绍
  18. 解决pyspark的 Added jobs for time问题
  19. 一份完整的聚合支付设计方案,喜欢就拿去用吧!
  20. 解决vue项目路由出现message: “Navigating to current location (XXX) is not allowed“的问题

热门文章

  1. 基于卷积神经网络的人脸表情识别应用--AR川剧变脸(一)
  2. 数据分析师培训机构告诉你,如何成为优秀的数据分析师
  3. selenium淘宝爬虫
  4. 【错误记录】Visual Studio 2019 中运行 Unity C# 脚本时报错 ( 根据解决方案, 可能需要安装额外的组件才能获得 | .NET 桌面开发 | 使用 Unity 的游戏开发 )
  5. .easyui(DataGrid数据查询)
  6. python前端框架实例_Python数据可视化:PyQt5 + ECharts框架实例
  7. php脚本的执行过程(编译与执行相分离)
  8. 《微信小程序跳转页面安卓闪现两次》
  9. 什么是外观检测系统?外观检测系统的功能有哪些?
  10. Python怎么启动打开Windows的应用程序