support use chrome
This commit is contained in:
parent
ea04fd82c8
commit
bf65f6ccfa
80
chrome.go
Normal file
80
chrome.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"apigo.cc/gojs"
|
||||||
|
"apigo.cc/gojs/goja"
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/launcher"
|
||||||
|
"github.com/ssgo/log"
|
||||||
|
"github.com/ssgo/u"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Chrome struct {
|
||||||
|
id string
|
||||||
|
launcher *launcher.Launcher
|
||||||
|
browser *rod.Browser
|
||||||
|
}
|
||||||
|
|
||||||
|
var chromes = map[string]*Chrome{}
|
||||||
|
var chromesLock sync.Mutex
|
||||||
|
|
||||||
|
func (ch *Chrome) Close() {
|
||||||
|
if ch.browser != nil {
|
||||||
|
ch.browser.Close()
|
||||||
|
ch.browser = nil
|
||||||
|
}
|
||||||
|
if ch.launcher != nil {
|
||||||
|
ch.launcher.Cleanup()
|
||||||
|
ch.launcher = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chromesLock.Lock()
|
||||||
|
delete(chromes, ch.id)
|
||||||
|
chromesLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseAllChrome() {
|
||||||
|
n := len(chromes)
|
||||||
|
if n > 0 {
|
||||||
|
for _, ch := range chromes {
|
||||||
|
ch.Close()
|
||||||
|
}
|
||||||
|
log.DefaultLogger.Info("关闭所有 Chrome 浏览器", "count", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartChrome(showWindow *bool, vm *goja.Runtime) (*Chrome, error) {
|
||||||
|
logger := gojs.GetLogger(vm)
|
||||||
|
ch := &Chrome{}
|
||||||
|
ch.browser = rod.New()
|
||||||
|
if localBrowserPath, hasLocalBrowser := launcher.LookPath(); hasLocalBrowser {
|
||||||
|
ch.launcher = launcher.New().Bin(localBrowserPath).Headless(!u.Bool(showWindow)).Set("no-sandbox").Set("disable-gpu").Set("disable-dev-shm-usage").Set("single-process")
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
ch.launcher.Set("disable-setuid-sandbox")
|
||||||
|
case "windows":
|
||||||
|
ch.launcher.Set("disable-features=RendererCodeIntegrity")
|
||||||
|
}
|
||||||
|
localChromeURL, err := ch.launcher.Launch()
|
||||||
|
if err != nil {
|
||||||
|
ch.Close()
|
||||||
|
return nil, gojs.Err(err)
|
||||||
|
}
|
||||||
|
logger.Info("启动本地 Chrome 浏览器", "url", localChromeURL, "path", localBrowserPath)
|
||||||
|
ch.browser.ControlURL(localChromeURL)
|
||||||
|
}
|
||||||
|
if err := ch.browser.Connect(); err != nil {
|
||||||
|
ch.Close()
|
||||||
|
return nil, gojs.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.id = u.UniqueId()
|
||||||
|
chromesLock.Lock()
|
||||||
|
chromes[ch.id] = ch
|
||||||
|
chromesLock.Unlock()
|
||||||
|
return ch, nil
|
||||||
|
|
||||||
|
}
|
26
chrome_test.go
Normal file
26
chrome_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package http_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"apigo.cc/gojs"
|
||||||
|
_ "apigo.cc/gojs/console"
|
||||||
|
_ "apigo.cc/gojs/file"
|
||||||
|
_ "apigo.cc/gojs/http"
|
||||||
|
_ "apigo.cc/gojs/util"
|
||||||
|
"github.com/ssgo/u"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChrome(t *testing.T) {
|
||||||
|
gojs.ExportForDev()
|
||||||
|
r, err := gojs.RunFile("chrome_test.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(u.Red("chrome_test"), u.BRed(err.Error()))
|
||||||
|
} else if r != true {
|
||||||
|
t.Fatal(u.Red("chrome_test"), u.BRed(u.JsonP(r)))
|
||||||
|
} else {
|
||||||
|
fmt.Println(u.Green("chrome_test"), u.BGreen("test succeess"))
|
||||||
|
}
|
||||||
|
gojs.WaitAll()
|
||||||
|
}
|
199
chrome_test.js
Normal file
199
chrome_test.js
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import http from 'apigo.cc/gojs/http'
|
||||||
|
import co from 'apigo.cc/gojs/console'
|
||||||
|
import u from 'apigo.cc/gojs/util'
|
||||||
|
import fs from 'apigo.cc/gojs/file'
|
||||||
|
|
||||||
|
let t1, t2
|
||||||
|
function printOK(...testResult) {
|
||||||
|
t2 = u.timestampMS()
|
||||||
|
co.info('. >> 成功', '耗时[' + (t2 - t1) + ']', !testResult ? '' : testResult.join(' '))
|
||||||
|
t1 = t2
|
||||||
|
}
|
||||||
|
|
||||||
|
co.log("开始测试...")
|
||||||
|
t1 = u.timestampMS()
|
||||||
|
|
||||||
|
co.log("启动Chrome浏览器...")
|
||||||
|
let ch = http.startChrome()
|
||||||
|
printOK("Chrome启动成功")
|
||||||
|
|
||||||
|
var page
|
||||||
|
|
||||||
|
co.log("打开z20首页...")
|
||||||
|
page = ch.open("https://z20.cc/")
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("设置视口大小...")
|
||||||
|
page.setViewport(1280, 720)
|
||||||
|
printOK("1280×720")
|
||||||
|
|
||||||
|
co.log("获取当前URL...")
|
||||||
|
let url = page.url()
|
||||||
|
printOK(url)
|
||||||
|
|
||||||
|
co.log("获取页面标题...")
|
||||||
|
const title = page.title()
|
||||||
|
printOK(title.substring(0, 15) + (title.length > 15 ? "..." : ""))
|
||||||
|
|
||||||
|
co.log("导航到Apigo.cc首页...")
|
||||||
|
page.navigate("https://apigo.cc")
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("获取当前URL...")
|
||||||
|
url = page.url()
|
||||||
|
printOK(url)
|
||||||
|
|
||||||
|
co.log("获取资源列表...")
|
||||||
|
let res = page.getResList()
|
||||||
|
printOK(`资源列表(${res.length})获取成功`)
|
||||||
|
|
||||||
|
co.log("获取资源列表(按类型script)...")
|
||||||
|
res = page.getResListByType("script")
|
||||||
|
printOK(`资源列表(${res.length})获取成功`)
|
||||||
|
|
||||||
|
co.log("获取资源列表(按MimeType)...")
|
||||||
|
res = page.getResListByMimeType("text/*")
|
||||||
|
printOK(`资源列表(${res.length})获取成功`)
|
||||||
|
|
||||||
|
co.log("获取资源内容...")
|
||||||
|
let content = page.getResContent(res[0].url)
|
||||||
|
printOK(`资源(${res[0].url})内容(${content.length}/${res[0].size})获取成功`)
|
||||||
|
|
||||||
|
co.log("执行后退操作...")
|
||||||
|
page.back()
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("获取当前URL...")
|
||||||
|
url = page.url()
|
||||||
|
printOK(url)
|
||||||
|
|
||||||
|
co.log("执行前进操作...")
|
||||||
|
page.forward()
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("获取当前URL...")
|
||||||
|
url = page.url()
|
||||||
|
printOK(url)
|
||||||
|
|
||||||
|
co.log("滚动页面...")
|
||||||
|
page.scrollTo(0, 500)
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("查找Logo...")
|
||||||
|
const logo = page.find(".logo")
|
||||||
|
printOK("已找到")
|
||||||
|
|
||||||
|
co.log("检查Logo可见性...")
|
||||||
|
printOK(logo.isVisible())
|
||||||
|
|
||||||
|
co.log("获取Logo 尺寸...")
|
||||||
|
printOK(logo.getAttribute("width"), logo.getAttribute("height"))
|
||||||
|
|
||||||
|
co.log("获取Logo alt...")
|
||||||
|
printOK(logo.getAttribute("alt"))
|
||||||
|
|
||||||
|
co.log("获取Logo URL...")
|
||||||
|
printOK(logo.getAttribute("src"))
|
||||||
|
|
||||||
|
co.log("获取Logo位置信息...")
|
||||||
|
const rect = logo.getBoundingRect()
|
||||||
|
printOK(`X:${rect.x} Y:${rect.y} W:${rect.width} H:${rect.height}`)
|
||||||
|
|
||||||
|
co.log("导航到Apigo.cc探索页...")
|
||||||
|
// page = ch.open("https://apigo.cc/explore/repos")
|
||||||
|
page.navigate("https://apigo.cc/explore/repos")
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("获取当前URL...")
|
||||||
|
url = page.url()
|
||||||
|
printOK(url)
|
||||||
|
|
||||||
|
co.log("查找搜索框...")
|
||||||
|
var searchInput = page.findN("input[type=search]")
|
||||||
|
if (!searchInput) {
|
||||||
|
return "搜索框不存在"
|
||||||
|
}
|
||||||
|
printOK("存在")
|
||||||
|
|
||||||
|
co.log("查找搜索框父...")
|
||||||
|
const parent = searchInput.parent()
|
||||||
|
printOK("存在", parent.getAttribute('class'))
|
||||||
|
|
||||||
|
co.log("查找搜索按钮...")
|
||||||
|
const searchBtn = parent.find("button")
|
||||||
|
printOK("存在")
|
||||||
|
printOK("存在", searchBtn.getAttribute('class') == searchBtn.getProperty('className') ? 'class检查通过' : 'class检查失败')
|
||||||
|
|
||||||
|
co.log("查找搜索按钮(findChild)...")
|
||||||
|
parent.findChild("button")
|
||||||
|
printOK("成功")
|
||||||
|
|
||||||
|
co.log("查找搜索按钮(findX)...")
|
||||||
|
parent.findX("./button")
|
||||||
|
printOK("成功")
|
||||||
|
|
||||||
|
co.log("查找表单...")
|
||||||
|
const form = searchInput.findParent('form')
|
||||||
|
printOK("存在", form.getAttribute('id'))
|
||||||
|
|
||||||
|
co.log("检查搜索框可交互性...")
|
||||||
|
printOK(searchInput.isInteractable())
|
||||||
|
|
||||||
|
co.log("设置搜索框值...")
|
||||||
|
searchInput.setValue("ag")
|
||||||
|
printOK("值已设置")
|
||||||
|
|
||||||
|
co.log("等待跳转搜索结果...")
|
||||||
|
page.waitPageLoad()
|
||||||
|
searchInput = page.find("input[type=search]")
|
||||||
|
printOK("完成", page.url())
|
||||||
|
|
||||||
|
co.log("获取搜索框值...")
|
||||||
|
printOK(searchInput.getValue())
|
||||||
|
|
||||||
|
co.log("查找所有结果链接...")
|
||||||
|
var list = page.findAll(".flex-list > .flex-item")
|
||||||
|
printOK(`找到 ${list.length} 个链接`)
|
||||||
|
|
||||||
|
co.log("查找第二个项目的仓库连接...")
|
||||||
|
const link = list[0].find("a:nth-child(2)")
|
||||||
|
printOK(link.getAttribute("href"))
|
||||||
|
|
||||||
|
co.log("计算按钮中心位置...")
|
||||||
|
const linkCenter = link.getCenter()
|
||||||
|
printOK(`X:${linkCenter.x.toFixed(1)} Y:${linkCenter.y.toFixed(1)}`)
|
||||||
|
|
||||||
|
co.log("移动鼠标到按钮中心...")
|
||||||
|
page.mouseMoveTo(linkCenter.x, linkCenter.y)
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("按下鼠标左键...")
|
||||||
|
page.mouseClick()
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("等待搜索结果稳定...")
|
||||||
|
page.waitPageLoad()
|
||||||
|
printOK()
|
||||||
|
|
||||||
|
co.log("获取当前URL...")
|
||||||
|
url = page.url()
|
||||||
|
printOK(url)
|
||||||
|
|
||||||
|
co.log("截取页面截图...")
|
||||||
|
const screenshot1 = page.screenshot()
|
||||||
|
printOK(`size:${screenshot1.length}`)
|
||||||
|
// fs.write('./screenshot1.png', screenshot1)
|
||||||
|
|
||||||
|
co.log("截取页面截图(全部)...")
|
||||||
|
const screenshot2 = page.screenshotFullPage()
|
||||||
|
printOK(`size:${screenshot2.length}`)
|
||||||
|
// fs.write('./screenshot2.png', screenshot2)
|
||||||
|
|
||||||
|
co.log("生成PDF...")
|
||||||
|
const pdf = page.pDF()
|
||||||
|
printOK(`size:${pdf.length}`)
|
||||||
|
// fs.write('./page.pdf', pdf)
|
||||||
|
|
||||||
|
co.log("所有测试步骤完成!")
|
||||||
|
|
||||||
|
return true
|
57
go.mod
57
go.mod
@ -1,41 +1,48 @@
|
|||||||
module apigo.cc/gojs/http
|
module apigo.cc/gojs/http
|
||||||
|
|
||||||
go 1.18
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/gojs v0.0.1
|
apigo.cc/gojs v0.0.21
|
||||||
|
apigo.cc/gojs/console v0.0.2
|
||||||
|
apigo.cc/gojs/file v0.0.4
|
||||||
|
apigo.cc/gojs/util v0.0.12
|
||||||
|
github.com/go-rod/rod v0.112.8
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/ssgo/config v1.7.9
|
||||||
github.com/ssgo/httpclient v1.7.8
|
github.com/ssgo/httpclient v1.7.8
|
||||||
github.com/ssgo/u v1.7.9
|
github.com/ssgo/log v1.7.7
|
||||||
|
github.com/ssgo/s v1.7.24
|
||||||
|
github.com/ssgo/u v1.7.21
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/gojs/console v0.0.1 // indirect
|
github.com/ZZMarquis/gm v1.3.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
|
github.com/emmansun/gmsm v0.30.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/gomodule/redigo v1.9.2 // indirect
|
github.com/gomodule/redigo v1.9.2 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
|
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/ssgo/discover v1.7.8 // indirect
|
github.com/ssgo/discover v1.7.10 // indirect
|
||||||
github.com/ssgo/redis v1.7.7 // indirect
|
github.com/ssgo/redis v1.7.8 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
|
||||||
github.com/gorilla/websocket v1.5.3
|
|
||||||
github.com/ssgo/config v1.7.7 // indirect
|
|
||||||
github.com/ssgo/log v1.7.7
|
|
||||||
github.com/ssgo/s v1.7.13
|
|
||||||
github.com/ssgo/standard v1.7.7 // indirect
|
github.com/ssgo/standard v1.7.7 // indirect
|
||||||
github.com/ssgo/tool v0.4.27 // indirect
|
github.com/ssgo/tool v0.4.29 // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
github.com/ysmood/goob v0.4.0 // indirect
|
||||||
|
github.com/ysmood/gson v0.7.3 // indirect
|
||||||
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.27.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
27
http.go
27
http.go
@ -12,6 +12,7 @@ import (
|
|||||||
"apigo.cc/gojs"
|
"apigo.cc/gojs"
|
||||||
"apigo.cc/gojs/goja"
|
"apigo.cc/gojs/goja"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/ssgo/config"
|
||||||
"github.com/ssgo/httpclient"
|
"github.com/ssgo/httpclient"
|
||||||
"github.com/ssgo/log"
|
"github.com/ssgo/log"
|
||||||
"github.com/ssgo/u"
|
"github.com/ssgo/u"
|
||||||
@ -31,8 +32,13 @@ var defaultHttp = &Http{
|
|||||||
globalHeaders: make(map[string]string),
|
globalHeaders: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO ws
|
var conf = struct {
|
||||||
|
Timeout int
|
||||||
|
ChromePath string
|
||||||
|
}{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
config.LoadConfig("http", &conf)
|
||||||
obj := map[string]any{
|
obj := map[string]any{
|
||||||
"new": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
"new": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||||
return newClient("HTTP", argsIn, vm)
|
return newClient("HTTP", argsIn, vm)
|
||||||
@ -51,11 +57,28 @@ func init() {
|
|||||||
"connect": defaultHttp.Connect,
|
"connect": defaultHttp.Connect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
obj1 := map[string]any{
|
||||||
|
"startChrome": StartChrome,
|
||||||
|
"closeAllChrome": CloseAllChrome,
|
||||||
|
}
|
||||||
|
tsCode := gojs.MakeTSCode(obj1)
|
||||||
|
|
||||||
|
mappedObj := gojs.ToMap(obj1)
|
||||||
|
for k, v := range mappedObj {
|
||||||
|
obj[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
gojs.Register("apigo.cc/gojs/http", gojs.Module{
|
gojs.Register("apigo.cc/gojs/http", gojs.Module{
|
||||||
Object: obj,
|
Object: obj,
|
||||||
TsCode: httpTS,
|
// TsCode: httpTS,
|
||||||
|
TsCodeMaker: func() string {
|
||||||
|
a := strings.SplitN(tsCode, "export default {", 2)
|
||||||
|
exportPart1 := "export default {\n" + strings.TrimRight(a[1], "}")
|
||||||
|
return strings.Replace(httpTS, "export default {", exportPart1, 1) + "\n" + a[0]
|
||||||
|
},
|
||||||
Desc: "",
|
Desc: "",
|
||||||
Example: "",
|
Example: "",
|
||||||
|
WaitForStop: CloseAllChrome,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
http.ts
3
http.ts
@ -26,6 +26,9 @@ function upload(url: string, form: Object, files: Object, headers?: Object): Res
|
|||||||
function download(filename: string, url: string, callback?: (finished: number, total: number) => void, headers?: Object): Result { return null as any }
|
function download(filename: string, url: string, callback?: (finished: number, total: number) => void, headers?: Object): Result { return null as any }
|
||||||
function connect(url: string, config?: WSConfig): WS { return null as any }
|
function connect(url: string, config?: WSConfig): WS { return null as any }
|
||||||
|
|
||||||
|
interface Chrome {
|
||||||
|
}
|
||||||
|
|
||||||
interface Client {
|
interface Client {
|
||||||
get(url: string, headers?: Object): Result
|
get(url: string, headers?: Object): Result
|
||||||
head(url: string, headers?: Object): Result
|
head(url: string, headers?: Object): Result
|
||||||
|
Loading…
x
Reference in New Issue
Block a user