简介
本篇文章将重点介绍如何将一个灵感从构思到实际应用的过程,并以实现一个功能复杂的文字转图形应用 为例。阅读完后,你也可以根据类似的思路来开发您自己的开源产品。在线体验
示例图
更多示例图可在网站中查看
项目源码
在线体验:anyphoto.space
Github项目地址:anyphoto-web
背景
前段时间,在使用微信读书App应用的时候,遇到一些比较精彩的文案,想要分享给朋友。虽然应用本身已经提供了一些基础模板供用户使用,但是我发现好像很多东西都是固定的,比如说字体、背景颜色、边距等。而我本身作为前端开发者,是很想自定义一些样式和布局的
于是,我便做了一个可高度定制化的图片生成器,接下来要讲的是近两个月来,我的一些实践和思考及产出
注: 当下无论是Npm工具包(暂未发布)还是可视化在线网站都在不断地更新和维护,由于时间的原因,网站暂未集成所有配置项,但已支持大部分核心功能,可自行体验
核心npm工具库
前面我已经提到了,我的职业是一名前端开发者,所以最开始我想到的就是开发一个全局的npm工具库,供开发者可在本地全局安装,然后通过终端命令实现快速生成个性化图片
一段时间的折腾后,还算比较顺利,我完成了所有核心模块功能的实现(这其实也归功于我之前写过类似的应用)
如果你看过我之前的文章,你会知道我之前做过一个draw-md-keyword的工具,它的作用是能够快速地识别出一个markdown文件中的关键字,然后生成一张随机风格的关键词云图,它的主要使用场景是帮助那些喜欢写博客文章的同学快速地识别出一篇markdown文件中的核心关键字,然后在一张图片上呈现出来
再一个就是之前封装过一个上传图片至云对象存储的工具包,它将一切跟上传有关的逻辑封装在其中,用户不需要关注任何细节,只需要配置必要的字段,即可进行上传图片到云端
有了以上这些基础,才让我在这么短的时间内能够实现这样一个复杂的应用,至于为什么用复杂来形容这个应用,下文会详细展开描述
实现
写过脚手架应用的同学,应该都知道,开发一个可执行脚本命令工具,一般我们都会选择使用commander
而生成图片的核心能力依赖于node-canvas,这个工具库其实就是将浏览器端的canvas画布重新在node端实现了一遍
接下来简单聊下,我提供的工具支持的几个命令以及它们的作用
该npm包暂未发布到官方npm源上,虽然已经实现了所有功能,但是目前阶段正在不断调整,想要体验的朋友可以后续关注,也会在网站更新日志中提及
生成配置文件
如果你有了解使用过webpack或者gulp等这类库,你就会发现它们其实都是一个思路,先提供一个命令供用户生成配置文件,比如webpack init
命令生成webpack.config.js,接着用户可以根据自己的需要,在配置文件中按需进行配置
配置完成后,在webpack编译构建过程中,会读取用户的配置,然后打包与配置文件相匹配的静态资源
我实现的这个工具,亦是如此,首先,你需要执行以下脚本,生成配置文件anyphoto.config.js
shell
anyphoto init
但是为了让用户能够快速地生成预设模版的配置文件,以及指定在哪生成配置文件,该命令支持了相关选项和参数
shell
// /Users/xdzi8b/my-project 参数,指定了配置文件生成的路径
// --theme default 选项,指定了配置文件生成的主题模板
anyphoto init /Users/xdzi8b/my-project --theme default
对于选项,很好理解,就是为了方便大家生成预设的模版。也就是说我的应用中会有很多不同风格的图片风格模版,你想使用哪款,指定基于其生成即可。然后你只需要进行一些简单的配置,比如输入自定义头像、标题、内容,即可生成你想要的风格的图片
而指定参数在哪生成配置文件,是我觉得更值得一提的
考虑到每个人的使用场景和习惯都是不一样的,如果你希望生成的图片风格都是一致的,那你完全可以在系统最上层目录中生成配置文件,这样的好处就是以后所有项目中,不再需要生成配置文件,因为在应用程序中,它会逐层的向上寻找配置文件,找到最近的,然后基于该文件,生成图片
而对于希望在不同项目中使用时,生成不同风格图片的同学来说,则可以在不同项目中生成各自的配置文件。假设你是一个博主,经常需要写一些软文,根据平台差异,需要配置不同风格和要求的封面图,那可能你需要生成几套风格的图片来形成自己的IP,这个时候,你就可以在不同目录中生成对应的配置文件进行配置
生成图片
有了配置文件后且配置完成后,你只需要执行以下命令,即可生成图片
shell
anyphoto generate xxx // xxx是你的图片主题内容文案
其实在生成图片的过程中,有很多难点,有的至今还未想到好的解决方案,但在介绍它们之前,我们先来认识下该命令支持的参数及选项
javascript
program
.command('generate')
.description('Generate photo by inspiration')
.argument('[content]', 'Any content you like')
.option(
'-s, --separator <separator>',
'The Content separator,support with empty or space,If not specified, the default is to use space to split the content.'
)
.option('-a, --avatar <url>', 'The avatar address of the photo')
.option('-t, --title <name>', 'The title of the photo')
.option('-o, --outputDir <output dir>', 'The place where the photo will be generated')
.option('-n, --outputName <output name>', 'The name of the output photo')
.action((content, options) => {
generate({
content,
options
})
})
无论是选项还是参数,其实都可以不必在执行脚本命令时指定(前提是你已经在配置文件中进行了配置),但是为什么仍然还需要提供它们呢,主要是基于以下两个原因
- 不用多次修改配置文件就能生成具有不同内容的图片(只需在执行命令时传入不同的content参数即可)
- 生成的图片风格具有一致性,因为图片的生成同时依赖于配置文件和执行该命令时传入的参数和选项,即使传入了不同的内容,它们的基础配置项仍然相同,比如背景色、字体大小等
其实懂程序的开发者很好理解这件事,就是优先级的一个概念。比如我在配置文件中已经指定了生成图片中头像是一张小猫咪,那如果我在执行以下脚本时不指定avatar的话,那么生成的就是一张带有小猫咪头像的图片
shell
anyphoto generate 我是一个可爱的小猫咪,我的名字叫做小七 // 生成带有小猫咪🐱头像的图片
但某个时刻,当我在生成图片时,我想换个口味,生成一张带有小老虎头像的图片,此时的你有两种选择
1.修改配置文件中的avatar字段为一张小老虎的头像图片地址
2.通过在执行脚本时指定头像avatar选项为一张小老虎的头像图片地址
shell
anyphoto generate 我是一个可爱的小老虎,我的名字叫做小八 --avatar 'https://xxx.小老虎.png' // 生成带有小老虎🐯头像的图片
也就是说,命令行中指定的配置选项的优先级是要高于配置文件中的,它会覆盖配置文件中的对应选项
上述中列出的最重要的一个选项,是separator,它的值有两个,分别是space和empty
由于生成图片的主体内容的语言是多变的,比如生成中文、英文、韩语、日语等。而我需要根据你设置的图片宽度,及相关元素的字体大小和边距等,动态的计算出如何在哪里进行换行,那么我是如何计算出某行内容的宽度超过了最大计算出的宽度的呢?
其实原理很简单,就是通过你指定的分隔符,通过字符串的split进行分割后,给这些元素在设定的尺寸下进行动态计算布局而已
所以separator的设定值很重要,它直接会影响到你生成的图片中主体内容部分是否布局错乱,如果你发现生成的图片中主体内容换行有问题,那么大概率是该配置项你未能正确的设置
其实很好记忆和理解,像常见的中文、韩语、日语的书写风格一般是字符之间不带空格, 所以指定为empty即可,而像英语,两个单词之间是有个空格的,则指定为space即可
查看预设主题
因为未来会在该应用中集成一些比较有意思的精美的主题。这样可以减少用户使用和学习它的成本。用户只需要在生成配置文件时,指定需要的主题模板即可。然后只需在执行命令时或在配置文件中传入头像、内容等,即可生成用户预期的图片
所以为了方便大家知道有哪些预设主题模版,该工具提供了以下命令
shell
anyphoto show
以上命令虽然能够看到有哪些预设的主题模版,但是并不知道每个模板生成的图片是什么样子的,所以该命令仍然支持传入参数
shell
anyphoto show poem
执行该命令,则会自动在浏览器上打开对应主题模版的图片,供用户参考
其实这个原理很简单,通过node的fs模块读取主题目录中的文件名即可。所以如果在你使用该工具的过程中,若基于你的配置文件生成的图片精美,则你可以向我留言,或者提交pr,我会将其合并到工具库中,这样下次别人也可以直接使用你的模板配置文件了
上传图片
当用户生成图片后,可能想要将该图片上传到一些地方进行存储。对于开发者来说,最容易想到的就是上传到github上
所以该工具提供了一个方便的命令,支持用户将图片上传至指定github仓库中,前提是你需要配置好相关的token
shell
anyphoto github ./path/to/your/example.png
思考
接下来聊聊其中在生成图片过程中,一些重要且不可忽视的要点。
自定义字体
首先最想聊的是自定义字体,其实生成图片的核心在于使用到了canvas画布技术,然而这个canvas绘制的环境并不是在浏览器上,而是在node环境中,虽然其提供了注册自定义字体的接口,但是有个限制,那就是只能注册用户本地的字体
对于用户本地全局安装工具库后在终端中通过命令执行生成图片来说,我们完全可以将喜欢的字体下载到电脑上,在注册时通过绝对路径指定自定义字体即可。但是对于我想提供一个web服务供大家使用来说,你使用本地路径自定义字体是没有用的,因为该服务是运行于我的远程主机服务器上,是读不到你本地的字体文件的~
为了解决这样一个问题,我想到了一个变通的实现思路,那就是支持远程字体,也就是任何互联网上你喜欢的字体
读到这时你可能会疑惑,前面你不是说只能注册本地字体吗。没错,在应用程序中,做了很多兼容逻辑,如果我发现你传入的是一个有效的远程字体文件,那我则会在生成图片前,先将远程字体文件下载到本地,最后注册这个下载过的本地字体文件
在这一套流程中,我还发现了一个问题,由于网络环境的差异性,如果用户在多次生成图片时,都指定了同样一个字体,那我难道需要多次重复执行上述的下载过程吗?
很明显这样会带来两个问题,一是每次生成图片的时间会久一点,因为涉及到生成图片前下载字体的网络请求获取资源动作。再一个就是频繁的下载同样的字体,也是不合理的
所以为了避免这些问题,应用程序中也做了这样一个判断处理逻辑,那就是当应用程序在接受用户的输入生成图片前会先去看下用户指定的自定义字体是否已经存在于本地了,如果存在了,则不再下载,而是直接使用本地字体的绝对路径来注册字体
至于上述提及的自定义字体的下载注册行为,用户端几乎是无感知的。但是如果你是一个善于观察的人,你就会发现,第一次生成自定义线上字体文件的图片时,如果该文件的体积较大,是会稍微慢一点的。再往后同样指定该字体文件时,图片几乎是瞬间生成的。而首次和之后生成图片的时间差,正是第一次被应用程序识别到用户传入了一个本地不存在的字体,它需要去主动下载的过程它所耗费的时间
向下兼容
再一个我想提及的是关于程序中"向下兼容"的理解于实践。在生成的图片中,我们可以在指定位置绘制一张图片,使得图片更加丰富饱满,就拿头像来说吧
如果你全局下载了anyphoto,那么其实你在终端中执行命令生成图片这个过程中,所涉及的关于自定义字体或者头像等这类资源,都是同时支持传入本地或者线上资源的,这点我先想提及下。
对于像头像等这类图片元素,在绘制图片前会有很多判断逻辑
1.判断到用户并未配置头像字段,则我会降级使用默认头像
2.如果用户配置了头像字段。这里还会去判断用户指定的头像字段究竟是本地的,还是远程的图片资源文件。而无论是本地还是远程头像资源,我都需要先去判断用户传入的究竟是否是一个合格的图片地址
如何去判断呢,很简单
首先判断用户指定的头像资源文件地址是否以常见图片的后缀名结尾,比如png、jpg等。再来就是判断这个资源是否确实存在,而不是随便填写的一个"形似图片"的资源地址
javascript
const checkRemoteFileExists = async (remoteUrl, targetType = 'image') => {
try {
const response = await axios.head(remoteUrl)
return response.status === 200 && response.headers['content-type'].startsWith(`${targetType}/`)
} catch (error) {
return false
}
}
const isValidAvatar = path.isAbsolute(handleAvatar) ? true : await checkRemoteFileExists(handleAvatar)
可以看出这段代码使用axios库来发送http head请求到指定的remoteUrl,以获取文件的元数据而不下载整个文件内容。如果请求成功(状态码为200),并且响应头中的content-type和当前资源类型相匹配的话(例如,对于'image'目标类型,content-type可能是'image/png'或'image/jpeg'等),则表示远程文件存在
如果请求失败或者条件不满足,函数将返回false,表示远程文件不存在,那么我依然会降级使用默认头像。对于上述的关于自定义字体部分,该部分判断逻辑同样适用
小结
该工具库完全由前端编码实现,90%的代码都是易于阅读和理解的。最复杂的部分,我总结了以下两点
1.如何确保用户能够按照预期生成图片?
不管是什么程序或者工具,对于用户侧来说,他们最初的关注点仅仅是"我该如何使用它来为我所用",而不是"我该如何小心翼翼的使用它"
所以这就对开发者提出了一定的要求,因为我们的工具是面向用户的。而不同的用户群体,对于技术的感知能力是不一样的。说人话就是,并不是说有的用户都懂技术。即使懂技术,他们也不能确保在使用你的产品或者工具时,"输入"一定是能够正向匹配或者符合你的要求的
那么在这种情况下,我们该如何保证我们的服务高可用呢?
其实是需要我们在开发设计之初,就要考虑到这些点,处理更多的边界条件
就拿我这个应用来说,我并不会因为你有意或无意地指定了错误的自定义字体文件或是头像文件,就让程序崩溃,进而无法生成图片
而是在发现错误时,给予用户反馈(控制台会提示传入的资源是有问题的)的同时,仍然会"降级"使用默认配置来使得整个生成动作照样按照预期地运行下去,进而生成图片
其实这也是一个让用户学习和了解如何使用你的工具或服务的正向反馈,用户知道了哪里输入有问题后,会进而修改,使得程序能够更加准确地按照心里预期的方式执行下去...
2.如何让一切元素能够按照预期的方式进行布局
这部分功能实现也是在程序中最有意思的部分。因为通过计算机程序来在图片上绘制文本和内容,并不像我们用笔在纸上写字一样,你可以随意的进行绘画,计算机是需要你明确地告诉它每个元素应在哪个"像素点"进行绘制
如何在指定宽度的图片上,根据用户不同的输入(比如字体大小,各种边距的控制等),依然能够按照预期的设定生成一张精美的图片是最耐人寻味的
在该应用中,其实一切都不是固定不变的,它会根据用户的输入,动态的计算出每个元素的布局位置,进而进行绘制生成图片。彼此之间也都是有牵连和约束的,比如你设定了不同尺寸的头像尺寸,则必然会引起其他元素的位置调整,比如最主观的内容部分
这其中有大量的计算处理逻辑,这里我也不想展开叙述,有兴趣的朋友,可以自行阅读了解下
可视化网站
尽管我已经完成了一个核心的npm工具包(尚未发布),但我深刻意识到大多数用户即使有生成定制图片的需求,却可能不知道如何使用它,即便我提供了详细的文档
为了让所有人都能够快速上手并体验我的应用,我认为最直接的方法是创建一个可视化的网站。用户只需要动动手指,就可以轻松生成他们想要的图片,网站将提供直观的界面和交互,引导用户完成图片定制的过程
通过这种方式,用户可以通过可视化工具直接探索各种选项和设置,而无需深入研究文档。他们可以通过简单的点击和输入内容来实现他们的想法,并立即预览生成的图片,这无疑是降低了用户的使用成本,同时也让我的应用可以触达更多的用户
实现
由于实现网站的过程设计的技术点比较多,这里只是简单的一笔带过,后续会针对每个技术点,做一篇更加详细的介绍,理论上如果你在了解了搭建该网站的过程和涉及的技术点,那么你也可以做出一款属于自己的产品
前端部分
技术栈:react、material ui、typescript、react-router-dom、ahooks、oss、captcha
react
由于之前做的大部分业务和个人需求,都是使用vue进行开发的。所以这一次我计划使用react来练手下,所有的组件均采用函数式组件配合hooks实现,用起来还是挺爽的
再一个就是关于应用状态,因为该网站的设想是后期会做登录注册,这样可以使整个应用有更好的体验,比如说用户生成的图片是有用户属性的,而不单单是一张图片,这样用户下次登录后就可以看到之前的生成历史,同时我们也可以使用其他人生成的模板快速生成图片
所以我是需要有个可以帮我保存用户登录数据和状态的工具,最开始我是想使用zustand来实现的,但是后来我发现对于像这样一个不需要在多个页面之间有太多共享数据的网站来说,使用useContext配合useReducer即可实现状态的下发和保存
material ui
由于开发时间的限制,我选择了使用开源的UI框架来开发我的网站。这样我可以将更多的精力放在实现功能代码上,而不是花费时间在布局和样式编写上。使用UI框架另一个好处是能够确保网站各个页面之间的风格保持一致,从而提升用户体验
由于我之前开发业务需求时,都是使用的是Antd,但是我并不是太喜欢那套风格样式。经过比对,我使用了Material-UI来快速开发网站。最主要的原因,还是因为Material-UI有大量的现成网站模版,而其中正好有我想要的模板,这里给大家安利下这个免费的网站模板,你们也可以在此基础上快速接入自己的应用
captcha
验证码SDK,现在大部分网站,都会在注册登录模块使用人机验证码校验。网站可以有效地抵御恶意程序和自动化攻击。这可以防止自动化注册、暴力破解等非法行为。验证码的引入增加了攻击者获取网站资源的难度,提高了安全性和用户体验
这里其实有个小插曲,在官方文档中,并未直接提供给前端开发者在浏览器端直接调用的SDK。我采用了一种变通的方案进行了实现
后端部分
技术栈:Express、bcryptjs、crypto-js、cors、debug、dotenv、jsonwebtoken、xss、nodemon
下面简单列出每个工具库的作用和意义,有兴趣的自行了解
-
Express: 流行的 Node.js Web 应用程序框架,用于构建灵活和可扩展的 Web 应用程序,是开发个人练手学习后端服务的不二之选
-
bcryptjs: 用于密码哈希和验证的工具库,可安全地存储用户密码
-
crypto-js: 加密和解密工具库,提供了多种常用的加密算法,用于数据加密和解密操作
-
cors: 用于处理跨域资源共享(CORS)的中间件,允许在 Express 应用程序中配置和处理跨域请求
-
debug: 用于在开发过程中进行调试输出的工具库,可帮助追踪和排查问题
-
dotenv: 用于加载环境变量配置的工具库,从
.env
文件中读取变量并使其在应用程序中可用 -
jsonwebtoken: 用于生成和验证 JSON Web 令牌(JWT)的工具库,用于实现身份验证和授权功能
-
xss: 用于防止跨站脚本攻击(XSS)的工具库,提供了方法和过滤器,用于处理用户输入中的恶意脚本
-
nodemon: 用于开发环境的工具库,可以监视文件的变化并自动重启 Node.js 应用程序,提高开发效率
上述其实列出的每个工具库,在我们开发后端服务时,都是特别有用的。其中大部分的使用方式还是挺简单的,只要你可以静下心来阅读参考文档
其中,dotenv部分最让我感到惊艳,很多人都使用过它,作用就是往应用程序中注入环境变量。但其实更强大的是他的团队实现的dotenv-vault,它可以帮助我们已一种更加轻松姿态的在多环境及团队中维护环境变量,也确实解决了我在开发anyphoto-web服务时关于如何管理敏感环境变量的问题
关于它的使用方式,我已经写了一篇使用教程,后续会公开。强烈建议大家先去了解体验下
数据库部分
由于我的网站应用不仅仅是一个前端静态文件服务,它还涉及到数据层面的存储。因此,我选择了对于前端开发者来说比较友好的数据库 MongoDB,并结合 Mongoose 进行开发
读到这里,陌生的朋友可能对其有一定的抵触感,其实抵触大部分是来源于陌生,其实如果你上手使用过之后,你就会发现它是如此的易上手,对于前端同学来说简直不要太友好
这里我还是想吹捧下MongoDB这个团队,因为最打动我的是除了他们提供了一些配套的可视化数据库管理工具如MongoDB compass外,还提供了一个免费512M的云数据库MongoDB Atlas,对于开发个人学习项目来说其容量已完全足够。且当下他们的服务中已经集成了AI,除了可以使用AI工具帮助我们快速的筛选数据,还可以告诉我们该如何在程序中编写代码,效率起飞
从上图中可以看出,利用可视化数据管理工具以及内置AI功能,可以很方便的帮我们管理和检索数据,一切都是免费使用的
关于如何在本地安装和启动MongoDB数据库,我也已经总结了一些心得,后续会公开
Verdaccio
在上述中,我多次提及到关于AnyPhoto的核心Npm工具包并未发布,原因是因为目前还未到时机,很多代码依旧在不断调整
那你有没有考虑过一个问题?那就是我的后端服务中是如何安装使用它的呢(在server目录中)
这就是我想强烈推荐大家去了解、学习和使用的一个高效工具verdaccio
对于我的整个项目规划来说,它可以说是起到了一个至关重要的一点,那就是我可以不着急将AnyPhoto工具包发布到官方Npm源中,照样可以下载使用它
开发过Npm工具的同学,可能知道我们快速调试和使用一个本地开发包的方式是通过软连接,也就是npm link的方式建立映射
但是对于包未发布,我的网站却已发布(网站中后端服务部分用到了该包)这种情况,是无法通过npm link方式来实现的,而verdaccio正是解决这类问题的一个完美方案
它其实就是一个私有的Npm源,允许用户在本地启动一个服务,来便利地进行发布和下载包。同时它也会给你可视化的页面服务,有了它,我们就可以在正式向Npm官方发布工具包前,自己本地先发布和安装体验下即将要发布到全世界的工具库是否存在问题,如果有问题,我们就可以在修改后再次发布及安装进行验证
在Npm官方发过包的同学,应该大部分都遇到过一种常见,那就是心急发布的包,发现有问题想要修改和删除时,遇到的种种难题
其实除了本地启动一个Npm仓库服务,它还可以部署在你自己的服务器上,很多小公司就是这么玩的。而我的Npm工具包,目前也是发不到我自己云服务器上的verdaccio服务中,方便我下载安装使用
关于如何在本地和服务器端使用verdaccio,我也计划后续出一篇文章
Github Action
这部分我们来聊聊对于个人开发者来说,如何做CI/CD,也就是部署方面的工作最方便,如果你能够坚持读到这里,更能说明你需要它
当一切准备就绪的时候(前端、后端代码都开发完成)时,我们该如何将前端本地项目编译的静态文件资源推送到服务器上以及重启我远程服务器的后端服务呢?
几年前我写类似应用的时候,那时候采用的方式,是将前端webpack打包的静态资源文件和server目录下的后端代码通过FileZilla(是一个开源的FTP文件传输协议客户端应用程序,用于在计算机之间传输文件)手动推送到服务器上(其实这也是最原始的代码部署方式)
言归正传,目前我认为对于个人开发者来说,Github Action就是最方便的CI/CD利器,没有之一,它可以自动化你的工作流程,完成很多不可思议的工作
当提及Github Action时,开发者最为惊喜的地方在于它的综合性和广泛的适用性。Github Marketplace中提供了各种Actions,几乎涵盖了所有与CI/CD相关的工具和部署场景。这意味着无论你使用哪种工具或面对哪种部署需求,你都能在Github Action中找到合适的解决方案
这种综合性和全面性使得Github Action成为个人开发者的首选工具。无论你面对何种CI/CD需求,Github Action都能提供灵活、可靠的解决方案,减轻了繁琐的部署工作,让你专注于应用程序的开发和改进
就拿我这个网站应用部署来说,我希望理想的情况是,当我本地每次提交代码后,不用去做任何的手动部署工作,就可以在流水线构建成功后,在我的线上网站服务中体现出来,这样我就可以将精力聚焦于应用程序的开发
这正是Github Action的魅力所在。通过配置适当的工作流程,你可以实现完全自动化的部署过程。一旦你提交了代码,Github Action会自动触发流水线构建,并将构建成功的应用程序部署到线上环境中。这意味着你不再需要手动操作或进行繁重的部署工作,而是可以专注于不断改进和开发应用程序
对于没有接触过它的朋友,也不需要担心,因为你需要做的就是在项目中按照一定的规范和语法,使用YAML格式的配置文件来定义你的工作流程,这里我找了一个简单的例子供大家参考,它主要实现的功能,是在用户像github项目提交issue或者pr时,对别人进行消息回复
yaml
name: first-interaction # 工作流名称
on: # 触发工作流的时机
issues: # 在任意用户提交issues时触发该工作流
types: [opened]
pull_request: # 在任意用户提交pr时触发该工作流,且限定在提交到主分支main时才触发
branches: [main]
types: [opened]
jobs: # 使用jobs定义工作流的作业,一个工作流可以有多个作业,下方的check_for_first_interaction就是该工作流中的一个作业
check_for_first_interaction: # 作业名称
runs-on: ubuntu-latest # 作业运行的环境
steps: # 一个作业由多个步骤实现
- uses: actions/checkout@v3 # 指定该步骤中要使用的工具库(Github Action中有大量现成的库可以协助你完成各种工作)
- uses: actions/first-interaction@main # 使用该工具库实现当用户提交issue和pr时,自动回复用户的功能
with: # 使用该工具库,需要的一些参数,github action的secret可以很好的帮我们管理敏感数据
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
Hello! Thank you for filing an issue.
If this is a bug report, please include relevant logs to help us debug the problem.
pr-message: |
Hello! Thank you for your contribution.
If you are fixing a bug, please reference the issue number in the description.
上述是我找的一个简单的例子,旨在说明入门Github Action其实并没有那么难,无非是通过配置文件,告诉Github在我们项目发生一些变化时,无论是提交代码,还是被提交issue或者是pr时,执行一些操作完成某项任务而已,像一些常见的自动化测试、定时任务,代码审查工作,我们都可以利用它很轻松的集成到我们的项目中,更多有意思的东西需要大家自行学习研究,这里不再发散
回归到我的项目中来,其实我想要实现的功能很简单,那就是当我本地修改完代码进行推送后,我希望能够将前端部分的静态资源推送到我的服务器上,同时也将server目录中的代码推送上去并重启后端服务
这样一来,在流水线执行成功后,用户能够立马看到我的应用中发生的变化,它们可能是修复了某个错误的文案,或者是调整了一些样式布局等。这样一件事,对于一个个人开发者来说,简直不要太酷
下图是我配置的流水线完成的这样工作的几个步骤
这里大家可以发现,除了deploy-web和deploy-server步骤外,还有另外两个,简单说下作用
check-changed-files
最开始的时候,每次在我推送代码时,工作流中都会执行deploy-web和deploy-server这样两个工作作业。但是后来我发现其实是大可不必的,因为有的时候,我的代码只是改了简单的页面样式,无需再去执行deploy-server,因为它会增加流水线执行的时间,同时也没有什么实际意义
后来我就在执行这两步之前,添加了一个判断文件变动范围的步骤,也就是如果只是前端代码发生了改变,则仅执行deploy-web步骤(打包静态文件、推送至服务器),而在仅监听到server目录下的代码发生改变时,则仅执行deploy-server步骤(推送后端部分代码到服务器上,重新安装npm依赖,重启后端服务,为我的网站提供后端服务接口),而在发现前端和后端部分代码都发生变化时,则同时执行这两步骤
仅仅是添加了这样一个小的判断逻辑,就可以极大的缩短我的流水线工作时间。对于用户侧来说,他们能够更快的感知到我的网站发生的变化及调整
send-email
由于我没办法能够得知流水线工作的结果,所以往往我是需要不断地主动去观察本次流水线作业是否完成(或者是刷新网站,看修改的变化是否生效,但是这并不靠谱,有的变动并不能再页面中体现出来),后来我便给流水线加了一个发送邮件的功能,当流水线一切作业及步骤完成后,会主动通知给我发邮件,且在邮件中包含了触发本次流水线的模块是前端部分还是后端部分,以及具体变动的文件和对应步骤的执行结果
这样一来,我又再次解放了双手,往往是我修改完代码提交后,过几分钟就能收到邮件,我也就知会本次修改已经对用户侧可见了~
下图是我收到邮件的内容,可以很清楚的知道,哪些文件发生改变以及流水线执行的最终结果和本次git提交信息
至于提交信息,你同样可以在我的网站右下角的按钮中找到App Log部分,它同样能够让我知道当前看到的网站是哪次代码提交的"产物",避免像"我已经修复了某个问题,为什么页面没生效"的尴尬局面
服务器
至此前后端的工作基本已经全部实现,但是前面一直提及的服务器,以及如何部署前后端服务,一直可能困扰了一些对此不太明白的新手同学,简单总结下(后续会针对该部分工作做一次完善的记录)
不管是前端静态资源,还是后端服务代码,也就是server目录下的文件,都是存在于远程云主机服务上的。其实你可以简单的理解是另外一台虚拟世界的"电脑",这台"电脑"其实和你本地开发的机器没有异样,它也有ip,也有操作系统,你在本地终端中执行的哪些shell命令,比如ls、pwd、mkdir等,依然可以在这台云服务器上执行
你本地开发时,通过webpack启动的前端服务,以及通过node启动的后端服务,同样可以在代码被推送到云服务器这台"电脑"上后运行起来,这样说大家应该就能更好地理解了
但是对于前端部分,我们并不会这么做,因为我们无论是在哪个环境中执行build构建后,只需要将静态文件,也就是html,css,js,image这些资源放到服务器上即可,在我的应用中,亦是如此
下图是我的网站应用前端代码部分在github action流水线中的步骤(部分配置)
shell
# .github/workflows/deploy.yml 流水线配置文件
- name: Set Up Environment Variables # 设置环境变量
run: xxx
- name: Install Dependencies & Build # 安装依赖,进行打包构建
run: |
pnpm install
pnpm run build
- name: Deploy Web Static Files To Ubuntu # 上传静态文件至ubuntu服务器上
uses: wlixcc/SFTP-Deploy-Action@v1.2.4
with:
username: ${{ secrets.UBUNTU_USER }}
server: ${{ secrets.UBUNTU_HOST }}
password: ${{ secrets.UBUNTU_PASSWORD }}
local_path: './build/*'
remote_path: '/home/www/anyphoto-web'
delete_remote_files: true
接下来我们来重点说下后端服务(node服务)
PM2
PM2是一个流行的Node.js进程管理器,它可以帮助你简化和管理Node.js应用程序的部署和运行,提供了监控、日志管理、负载均衡等功能
而对于我们开发个人后端服务而言,无非是最依赖于它的"无痕重启"以及日志管理,所谓的无痕重启,也就是当我的后端服务代码发生改变时,我不需要重新启动后端服务即可生效,这样就可以确保我们写的后端服务一直"在线"
Nginx与域名
域名
无论我们自己购买的是什么云主机服务器,它都是有一个固定的公网IP,它就像我们每个人的身份证一样,能够通过这张"身份证"找到对应的服务器
前面我们已经能够将静态资源推送到云主机了,用户已经能够通过ip的形式访问到我们的静态资源文件,但如果我们想别人能够通过域名anyphoto.space的形式访问我们的应用的话,我们还是需要去购买域名以及进行备案的
所谓的备案,说白了就是证明这个域名的合法性以及归属性,一般两个礼拜周期即可,期间会有相关人士给你打电话进行相关咨询确认
有了域名之后,我们还需要进行解析,说人话就是将你的域名和你的云主机ip建立映射关系,比如我的应用解析是这样的
shell
anyphoto.space => 112.xxx.xxx.xxx
这样当别人就可以通过域名的形式访问我们的应用,是不是贼爽
Nginx
关于Nginx,其实我了解的并不多,虽然我知道它很强大。但是于我而言,我能够利用的也就是关于转发,也就是通过域名+路径访问我的服务器时,我让它定位到哪里去寻找资源,同时它也能够解决cors跨域的问题
emmm,这里我发现我好像无法很好的描述清楚它,索性不如看下关于我的应用其中的一部分nginx配置来的更加痛快
nginx
server {
listen 443 ssl; # 443端口通常是用于HTTPS(HTTP Secure)协议的默认端口
server_name www.anyphoto.space anyphoto.space; # 建立映射关系(前提你已经进行了域名解析)
# 开始https服务需要证书,在证书提供商那里下载完丢到你的服务器上即可,一般云厂商都有免费证书提供
ssl_certificate /home/www/ssl/www.anyphoto.space.pem;
ssl_certificate_key /home/www/ssl/www.anyphoto.space.key;
# 转发,当我通过https://anyphoto.space/api路径访问后端接口时,相当于在访问http://localhost:3001端口上的服务
location /api {
proxy_pass http://localhost:3001/api;
proxy_set_header Host $host;
}
# 转发,这个又要提到😂前端路由了,详细内容可查看我之前写的关于前端hash路由和history路由区别的文章,它的作用就是告诉用户
# 当我通过https://anyphoto.space/xxx访问资源时,你就去我的服务器上/home/www/anyphoto-web目录下寻找对应文件即可
location / {
root /home/www/anyphoto-web;
try_files $uri $uri/ /index.html;
index index.html index.htm index.php;
gzip_static on; # 资源过大时,一般我们后端服务都需要开启gzip压缩
}
}
同时,我上文提及到的私有Npm服务,也是部署在我的服务器上,可以让我在正式发布包前,愉快的玩耍
小结
拥有一台自己的服务器对于一个想要全面了解网站应用搭建的同学来说非常有必要,你可以深入学习和实践各个方面的网站搭建过程。同时别忘记购买域名且进行备案
很多朋友都对后端服务这块感到害怕,其实你并不是不会,只是这一块领域你比较陌生而已,折腾个一两次,包你如考科目三似的一次性过
感悟
回归到该件事本身,大家不难发现,即使是这样一个简单的创意灵感,从前期的设计规划到最后的实现,是需要我们付出很多的时间和精力的,你需要不断地接触一些陌生的知识和技术栈,踏足未知的领域。期间我也遇到了各种各样的困扰和难题,但我依然通过一些方法直面解决了它们
总之,想要挣钱,就别怕吃苦。还有就是"活到老,学到老"的一定是所有程序员所需要具备的精神
如今在AI这把"双刃剑"能力愈渐恐怖的趋势下,需要我们利用好它,反过来"征服"它,否则大概率我们是逃脱不了35岁定律的
前段时间看到一个结合Ai的技术screenshot-to-code,对AI技术在编程领域的"破坏性"无感知的同学可以看下
最后
由于篇幅、时间、精力有限,没有办法事无巨细的对每个模块做详细的阐述和解释。本篇文章主要还是从认知的角度展开,让大家了解前端工程师如何实现完整的前后端应用服务的思路,愿你能从本篇文章中有所收获
不可避免的,会有很多表达不清晰的地方,希望你可以提出来帮助我去改善它
文章中提及的一些技术实现和工具使用,后续也会有针性的单独写一篇文章展开叙述,帮助大家更好地理解和运用它们。对于其中一些不太了解的技术点,可以指出来,我会视情况进行更新
关于实现的npm核心工具包,暂未发布在npm官方源上,后续会在完善后,进行发布。目前也遇到了两个我个人解决不了的问题,希望有兴趣和想法的小伙伴可以提出自己的见解
- 如何能够让内容换行更加的人性化,而不是通过"手动敲空格"进行补全?
- 如果支持背景图片的话,应如何进行绘制,能够使得同一张背景图片,能够适应不同宽度及高度的容器?
在线生成定制化图片的网站正在不断地迭代更新中,可前往体验~,你也可以在anyphoto-web这里发现网站源码
在体验过程中,发现的任何问题,可在网站中对我进行留言,同时如果你基于它生成了令人满意的图片,别忘记将对应的配置文件分享给我,后续我会将它集成在npm工具库和网站中,供他人使用
同时希望能找到擅长小程序开发的朋友,我们可以一起合作,将该应用集成进小程序,再往前迈一步~