RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)

news2024/11/27 16:33:00

继续聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。

写在前面

在上一篇文章《RSS Can:使用 Golang 实现更好的 RSS Hub 服务(一)》里,我们简单介绍了使用 Go 来获取传统网站的信息。

RSS Can(RSS 罐头)的相关代码已经开源在soulteary/RSS-Can 。

项目中的代码,将会伴随文章更新而更新,如果你觉得项目有趣,欢迎“一键三连”。当然,如果你觉得这个事情有价值,也有趣,也欢迎加入项目,一起折腾。

为什么需要动态化

Golang 是传统的“编译型选手”,本身的动态化能力很弱,姑且不讨论 Golang 应用是否应该做动态化的哲学问题,单就效率角度来看 ,存在太多问题了。

比如,当我们遇到目标网站改版、想要快速调整规则完善获取信息的时候,重复编译 Golang 程序,即使构建速度再快,也是一件反效率的事情,前后牵扯的七七八八的事情一箩筐。

但其实,我们的程序主体并没有修改调整,需要调整的内容只有一些细微的规则,所以,将这块经常变化的内容抽象出来单独维护,是一件有必要的事情;考虑到部署涉及到额外的测试、补停机切换等需要不少基础技术设施,我们没有必要为一个需求建立一座城堡。所以,将程序部分动态化或许是最简单的省事策略之一

为什么选择 JavaScript 作为动态化的 DSL

为什么考虑使用 JS 作为程序动态化的 DSL ,而不是使用 JSON、TOML、YAML 等传统的“静态”配置文件格式呢?

JavaScript x V8 x Golang

首先,动态语言相比“静态配置”对于程序要 “fancy” 不少。除了描述配置,还具备了和“程序实际运行的上下文交互”的能力,甚至在一些场景里,可以用 JavaScript 中现成的功能处理数据,而非一定要在 Golang 里做程序实现。

其次,在上一篇文章《RSS Can:使用 Golang 实现更好的 RSS Hub 服务(一)》里,我提到了我们面对的场景除了包含“静态的”服务端生成场景之外,还包含“动态的”客户端生成的内容,使用 “JavaScript”,可以更好的和动态内容“打成一片” (后面聊 CSR 的时候详细展开)。一套配置表达方式,可以在两套,甚至未来多套环境中运行,而不是一个环境玩一套,还是很符合个人的技术审美的。

最后,我挺喜欢 JavaScript 这门年轻但是充满活力的和表现力的语言的。在 Golang 生态里,虽然各种语言的运行时实现都有,但是不论是 V8 实现,还是 Quick JS 实现,都深得我心。

考虑到后面要我们展开的 CSR 部分的内容,项目这里就先选择使用 “V8” 实现,暂时不使用 Quick JS 啦。

我们先来聊聊如何在 Go 里调用 JavaScript 代码。

如何在 Go 里调用 JavaScript

想要在 Go 里调用 JavaScript 代码,在引入上文提到的 “v8” 之后,最简单的方式莫过于下面这样的简单代码示例:

// 创建一个用于运行代码的“容器”(虚拟机)
ctx := v8.NewContext()
// 全局执行代码
ctx.RunScript("const add = (a, b) => a + b", "math.js")
// 继续执行新代码,可以访问之前的代码
ctx.RunScript("const result = add(3, 4)", "main.js")
// 在 Go 里访问刚刚代码的执行结果
val, _ := ctx.RunScript("result", "value.js")
// 在 Go 里打印日子好,将结果打印出来
fmt.Printf("addition result: %s", val)

当然,如果我们想让 JavaScript 代码在 Go 里和 Go 的函数进行更多的交互,还需要做一些函数的调用绑定。当我们将代码放进项目里,执行 go run main.go,将得到下面符合预期的结果:

addition result: 7

不过,真的想在 Go 里放心的运行 JavaScript ,我们还需要对执行的方法做一些额外的处理,避免出现“意外”。

更安全的 JavaScript 沙箱环境

在引入 V8 之后,其实除了运行我们的动态配置、灵活的“小动态函数”之外,还能够运行来自三方的代码。不论是运行哪一种代码,都有可能出现等效下面的逻辑:

while (true) {
    // Loop forever
}

我们当然不希望程序整体,因为这类原因 “hang” 死,甚至影响同机器运行的其他服务。所以,对于调用 JavaScript 的 Golang 方法,需要做出一些改进:

const JS_EXECUTE_TIMEOUT = 200 * time.Millisecond
const JS_EXECUTE_THORTTLING = 2 * time.Second

func safeJsSandbox(ctx *v8.Context, unsafe string, fileName string) (*v8.Value, error) {
	vals := make(chan *v8.Value, 1)
	errs := make(chan error, 1)

	start := time.Now()
	go func() {
		val, err := ctx.RunScript(unsafe, fileName)
		if err != nil {
			errs <- err
			return
		}
		vals <- val
	}()

	duration := time.Since(start)
	select {
	case val := <-vals:
		fmt.Fprintf(os.Stderr, "cost time: %v\n", duration)
		return val, nil
	case err := <-errs:
		return nil, err
	case <-time.After(JS_EXECUTE_THORTTLING):
		vm := ctx.Isolate()
		vm.TerminateExecution()
		err := <-errs
		fmt.Fprintf(os.Stderr, "execution timeout: %v\n", duration)
		time.Sleep(JS_EXECUTE_TIMEOUT)
		return nil, err
	}
}

上面的程序将会保证我们想要执行的代码按照预期执行,当程序出现需要运行特别久的情况时(例子中是200毫秒),会自动停止运行代码,并休息 2s 避免潜在的重复调用造成系统负载飙升。

func main()
	ctx := v8.NewContext()

	safeJsSandbox(ctx, `
	while (true) {
	    // Loop forever
	}`, "loop.js")
}

当我们再次执行程序,会得到程序自动终止了运行时间过长的动态代码的日志提醒:

execution timeout: 12.206µs

使用 JavaScript 定义简单的动态配置

本篇文章,我们先不聊能够同时运行在 CSR、SSR 环境中的 JS SDK 的设计。先从一段简单的配置开始,只聊 Go 从 JavaScript 文件中获取配置并动态解析执行。

我们根据上一篇文章,不难梳理出消息列表中的每一条消息里,包含“标题、作者、分类、时间、描述、文章链接”的元素的信息,为了让我们的 Go 程序能够得到这个配置,我们需要将配置“包”在一个可执行函数或可访问的变量中。

function getConfig(){
    return {
        ListContainer: "#app .main-right .kr-home-main .kr-home-flow .kr-home-flow-list .kr-flow-article-item",
        Title: ".article-item-title",
        Author: ".kr-flow-bar-author",
        Category: ".kr-flow-bar-motif a",
        DateTime: ".kr-flow-bar-time",
        Description: ".article-item-description",
        Link: ".article-item-title",
    }
}

使用 Go 解析动态配置

如何在 Golang 中执行上面的 JavaScript 代码,并得到执行结果,其实也非常简单,我们可以借助上文中提到的能够“安全执行” JavaScript 代码的函数:

func main() {
	jsApp, _ := os.ReadFile("./config.js")
	inject := string(jsApp)
	ctx := v8.NewContext()
	safeJsSandbox(ctx, inject, "main.js")
	result, _ := ctx.RunScript("JSON.stringify(getConfig());", "config.js")
	jsonRaw := []byte(fmt.Sprintf("%s", result))
	fmt.Printf("addition result: %s", jsonRaw)
}

当我们执行完毕上面的代码后,将得到下面的结果:

cost time: 10.382µs
addition result: {"ListContainer":"#app .main-right .kr-home-main .kr-home-flow .kr-home-flow-list .kr-flow-article-item","Title":".article-item-title","Author":".kr-flow-bar-author","Category":".kr-flow-bar-motif a","DateTime":".kr-flow-bar-time","Description":".article-item-description","Link":".article-item-title"}

想要快速将上面的 “JSON” 格式的输出内容解析成 Go 的内存对象,我们可以使用 “JSON-to-Go” 来偷个懒,将上面的内容粘贴到网站的编辑器中,网页程序将自动转换出我们所需要的 Go Struct 定义。

JSON-to-GO 在线工具

简单调整得到的代码,不难写出下面的程序,来将上文中的 JSON 数据转换为程序需要的内存对象。

func main() {
...
	type Config struct {
		ListContainer string `json:"ListContainer"`
		Title         string `json:"Title"`
		Author        string `json:"Author"`
		Category      string `json:"Category"`
		DateTime      string `json:"DateTime"`
		Description   string `json:"Description"`
		Link          string `json:"Link"`
	}

	var jsonData Config
	err := json.Unmarshal(jsonRaw, &jsonData)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(jsonData)
}

调用动态配置获取网站数据

在上一篇程序里,我们的程序实现类似下面这样,是比较典型的 “hard code” 的代码。

func getFeeds() {
	// Request the HTML page.
	doc, err := getRemoteDocument("https://36kr.com/")
	if err != nil {
		log.Fatal(err)
	}

	// Find the article items
	doc.Find("#app .main-right .kr-home-main .kr-home-flow .kr-home-flow-list .kr-flow-article-item").Each(func(i int, s *goquery.Selection) {
		title := strings.TrimSpace(s.Find(".article-item-title").Text())
		time := strings.TrimSpace(s.Find(".kr-flow-bar-time").Text())
		fmt.Printf("Aritcle %d: %s (%s)\n", i+1, title, time)
	})
}

我们将动态配置和上面的程序进行结合,可以将程序简单调整为类似下面这样:

...

type Config struct {
	ListContainer string `json:"ListContainer"`
	Title         string `json:"Title"`
	Author        string `json:"Author"`
	Category      string `json:"Category"`
	DateTime      string `json:"DateTime"`
	Description   string `json:"Description"`
	Link          string `json:"Link"`
}

func getFeeds(config Config) {
	// Request the HTML page.
	doc, err := getRemoteDocument("https://36kr.com/")
	if err != nil {
		log.Fatal(err)
	}

	// Find the article items
	doc.Find(config.ListContainer).Each(func(i int, s *goquery.Selection) {
		title := strings.TrimSpace(s.Find(config.Title).Text())
		author := strings.TrimSpace(s.Find(config.Author).Text())
		time := strings.TrimSpace(s.Find(config.DateTime).Text())
		category := strings.TrimSpace(s.Find(config.Category).Text())
		description := strings.TrimSpace(s.Find(config.Description).Text())

		href, _ := s.Find(config.Link).Attr("href")
		link := strings.TrimSpace(href)

		fmt.Printf("Aritcle #%d\n", i+1)
		fmt.Printf("%s (%s)\n", title, time)
		fmt.Printf("[%s] , [%s]\n", author, category)
		fmt.Printf("> %s %s\n", description, link)
		fmt.Println()
	})
}

func main() {
	jsApp, _ := os.ReadFile("./config/config.js")
	inject := string(jsApp)
	ctx := v8.NewContext()
	safeJsSandbox(ctx, inject, "main.js")
	result, _ := ctx.RunScript("JSON.stringify(getConfig());", "config.js")

	var config Config
	err := json.Unmarshal([]byte(fmt.Sprintf("%s", result)), &config)
	if err != nil {
		fmt.Println(err)
		return
	}
	getFeeds(config)
}

当我们再次运行程序,Go 程序就会跟着 JavaScript 代码中定义的配置,来尝试解析页面中的信息啦。

Aritcle #1
动画市场迎来《三体》,然后呢? (1小时前)
[娱乐独角兽] , [文娱直播新动向]
> 内容生产需要向上走。 /p/2041078218796039

Aritcle #2
...

最后

接下来的内容里,我们继续聊聊,如何将这些信息源转换为 RSS 阅读器可以使用的信息源,以及如何针对不同类型的网站进行信息整理。

当然,也会继续聊聊之前系列文章中提到的有趣的技术点。

–EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。

喜欢折腾的小伙伴,欢迎阅读下面的内容,扫码添加好友。

  • 关于“交友”的一些建议和看法
  • 添加好友,请备注实名和公司或学校、注明来源和目的,否则不会通过审核。
  • 关于折腾群入群的那些事

本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2022年12月13日
统计字数: 7118字
阅读时间: 15分钟阅读
本文链接: https://soulteary.com/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/85795.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字孪生技术在智慧园区领域的实践

随着云计算、物联网、大数据、人工智能、5G 等为代表的技术迅速发展和 深入应用&#xff0c;“智慧园区”建设已成为全球园区发展的新趋势。近年来&#xff0c;党中央 和国务院更加注重智慧园区的建设与发展&#xff0c;相继出台了多项政策推动智慧园区的建设&#xff0c;智慧园…

数据镜像软件

什么是数据镜像 数据镜像是复制已传输到 USB 设备或在 USB 设备中修改的文件内容的过程。创建镜像副本后&#xff0c;可以在受密码保护的网络共享中维护这些副本。此共享文件夹最好保存在远程位置&#xff0c;以节省本地计算机上的空间。这样&#xff0c;只有管理员或受信任的…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java基于疫情防控下社区管理平台my3tu

对于即将毕业或者即将做课设的同学而言&#xff0c;由于经验的欠缺&#xff0c;面临的第一个难题就是选题&#xff0c;确定好题目之后便是开题报告&#xff0c;如果选题首先看自己学习那些技术&#xff0c;不同技术适合做不同的产品&#xff0c;比如自己会些简单的Java语言&…

Flutter自定义MultiChildRenderObjectWidget实现圆环布局效果

一、本文主要是学习巩固一下自定义RenderObject这一块内容&#xff0c;用所了解到的知识实现一个圆环布局效果 本篇文章主要参考了恋猫de小郭Flutter 完整开发实战详解&#xff08;十六、详解自定义布局实战&#xff09;文章&#xff0c;大家可以先看完这篇文章再来阅读本篇这…

生产制造企业用的ERP系统——流程管理

ERP的含义是企业资源计划&#xff0c;主要是对企业所拥有各种资源进行综合规划和优化管理&#xff0c;用以降低成本&#xff0c;提高效率&#xff0c;增加利润。 作为一个低代码开发平台&#xff0c;百数在办公领域已有10年历史&#xff0c;为企业信息化发展提供丰富的功能模块…

Java如何实现截取字符串

文章目录0 写在前面1 .substring()2 split()正则表达式3 StringUtils.substring()4 写在最后0 写在前面 在实际工作中有一些地方需要用到截取字符串的方法&#xff0c;所以在此记录下截取字符串的几种方法。 .substring() StringUtils.substring() split()正则表达式 1 .subs…

最高月薪15.5K,成大事不在于力量的大小,而在于能坚持多久~

在前几天的世界杯八强赛中&#xff0c;阿根廷通过点球战胜了荷兰&#xff0c;闯入了四强。 说起阿根廷&#xff0c;就不得不提起梅西&#xff0c;这个被众多球迷誉为天才的球王&#xff0c;可实际上&#xff0c;在他的父亲眼中&#xff0c;却从不称自己的儿子为天才&#xff0…

java 课设-超级玛丽游戏

题目&#xff1a; 本程序是针对超级玛丽小游戏的 JAVA 程序&#xff0c;进入游戏后首先用鼠标点击 GUI 窗口&#xff0c;然后开始游 戏&#xff0c;利用方向键来控制的马里奥的移动&#xff0c;同时检测马里奥与场景中的障碍物和敌人的碰撞&#xff0c;并判断马里 奥的可移动性…

.net 移动开发 MAUI

.net 移动开发 MAUI新建一个Hello World运行效果修改一下&#xff0c;做个加法运算吧运行效果创建新的内容页修改xaml内容添加逻辑内容主页面添加一个按钮并且增加路由按钮事件添加路由整体感受第一&#xff1a;WPF第二&#xff1a;小程序所有的页面跳转都是借助路由第三&#…

Win11怎么更改时区?

Win11可以自动检测配置用户的时区来显示正确的时间&#xff0c;如果由于一些特殊原因导致系统时区错误&#xff0c;显示时间也错误的话&#xff0c;这时候要怎么调整呢&#xff1f;下面小编就来教教大家更换Windows 11时区的方法。 方法一&#xff1a;在 Windows 11 上更改时区…

新旧电脑数据如何迁移?电脑数据导入到另一台电脑

新旧电脑数据如何迁移&#xff1f;换电脑是一件好事&#xff0c;换更新版本的电脑&#xff0c;还可以体验新功能。您需要找到将数据传输到新电脑的有效方法。电脑数据导入到另一台电脑&#xff0c;接下来的内容将为大家揭晓答案&#xff01; 方法1、使用专业的数据传输迁移数据…

[附源码]计算机毕业设计的玉石交易系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

cookie 和 session 的区别

1. 前言 cookie 和 session 的区别是什么&#xff1f;这个问题在面试中问到的频率非常高 cookie 和 session 的共同点: 都是用来记录用户访问浏览器时保存的数据&#xff0c;比如&#xff1a;用户的身份信息等 2. cookie 和 session 的工作原理 cookie 工作原理: 浏览器端第…

【自然语言处理】隐马尔科夫模型【Ⅲ】估计问题

有任何的书写错误、排版错误、概念错误等&#xff0c;希望大家包含指正。 由于字数限制&#xff0c;分成六篇博客。 【自然语言处理】隐马尔可夫模型【Ⅰ】马尔可夫模型 【自然语言处理】隐马尔科夫模型【Ⅱ】隐马尔科夫模型概述 【自然语言处理】隐马尔科夫模型【Ⅲ】估计问题…

Rsyslog+MariaDB搭建Rsyslog日志服务器

rsyslog是一个linux系统日志服务的工具&#xff0c;主要用来监控收集系统从开机运行之后所发生的所有日志&#xff0c;包括内核日志&#xff0c;服务日志&#xff0c;应用日志等等&#xff1b;记录的日志全部都写到/var/log下面&#xff0c;常用的有dmsg&#xff08;内核日志&a…

带你手把手实操一个RPC框架

前言&#xff1a; 这篇文章我们来聊一聊RPC框架&#xff0c;为什么要聊RPC呢 &#xff1f; 首先从个人成长角度&#xff0c;如果一个新时代码农能清楚的了解RPC框架所具备的要素&#xff0c;掌握RPC框架中涉及的服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信…

Linux 安装 Redis教程

1.切换到目录/use/local/src cd /use/local/src 2.下载文件 wget https://download.redis.io/releases/redis-6.2.6.tar.gz 3.文件解包 tar xzf redis-6.2.6.tar.gz 4.将解压后的包移动到/usr/local/redis目录下 mv redis-6.2.6 /usr/local/redis 5.切换到/usr/local/r…

Vue3这样子结合hook写弹窗组件更快更高效

为什么会有这个想法 在管理后台开发过程中&#xff0c;涉及到太多的弹窗业务弹窗&#xff0c;其中最多的就是“添加XX数据”&#xff0c;“编辑XX数据”&#xff0c;“查看XX详情数据”等弹窗类型最多。 这些弹窗组件的代码&#xff0c;很多都是相同的&#xff0c;例如组件状…

DBCO-PEG-NHS,二苯并环辛炔(DBCO)PEG-NHS衍生物,分子量MV 3.4K 5K

1、产品描述&#xff1a; 二苯并环辛炔&#xff08;DBCO&#xff09;PEG-NHS衍生物可以在不需要任何金属催化剂的情况下进行化学反应。菌株促进的环辛炔和叠氮化合物的1,3-偶极环加成反应&#xff0c;也称为无铜点击反应&#xff0c;是一种双正交反应&#xff0c;可使两个分子…

【Java版oj】day05统计回文

目录 一、原题再现 二、问题分析 三、完整代码 一、原题再现 统计回文_牛客题霸_牛客网 描述 “回文串”是一个正读和反读都一样的字符串&#xff0c;比如“level”或者“noon”等等就是回文串。花花非常喜欢这种拥有对称美的回文串&#xff0c;生日的时候她得到两个礼物分别…