第一次网站的搭建和优化经历。

前言

上个月接到个任务,需要开发一个代码提交网站,以在这个月的比赛中使用,届时会有将近千名考生提交代码。嗯当时的需求就是如此简洁明了。

设计

因为之前重构过一次网站,采用的是React+golang,于是这次也采用这样的前后端分离的技术。

于是花了一个星期,设计了前端样式,前后端数据传输的格式,数据库表结构,并且去github搜索了可行的技术框架,参考别人的一些代码。

虽然当时预想的是在比赛结束前30分钟或者比赛结束后10分钟开放提交。但考虑到考生应该对系统有熟悉的时间,于是设计时将比赛的active时间和open时间分开,也即考生可以登录的时间和可以提交的时间分开。这样利于考生提前登录熟悉环境,也有利于分散流量。

编码

在感觉一切确定好后,在某个周末开工,由于先前重构过网站,这两个网站有一定的相似性,和一些类似的demo,通过简单CV和更改,花了一天写完了前端,一天写完了后端。

  • 在前端方面,

不得不说react router的升级真的激进,v5v6的差别好大,好多函数组建都被改名或弃用了,导致网上参考的别人的代码(采用v5的)都失效了,只能去看官方文档。其余的,react引入的hook是真的香,不用再写一些类,定义一些有的无的东西,简化了代码,感觉非常舒心。不过目前也就只会用useStateuseEffect,对于表单form之类的,在数据处理方面写的有些繁琐,表单的每一项都要一个单独的handle函数,提交也是单独一个提交函数,感觉完全割裂了表单数据的整体性,不像之前看到的php写法。听说可以用useForm来解决,下次去看看。

前端UI库还是沿用了老一辈学长所使用的semantic UI。但这个库感觉过于古老了,以至于在使用上,尤其是错误消息提醒方面的效果和动画渐变效果比较难以实现,不像antd的错误效果有专门的函数,能够实现右上角、中间弹窗消失的效果。不过semantic唯一让我中意的就是table的样式,尤其是单元格的颜色对错时的绿色和红色。

  • 在后端方面,

由于要维持用户的登录状态,众所周知这需要cookie,但这个cookie存哪里呢我一直没找到什么解释。故我就采用最原始的存在数据库里并存着失效时间,但这样每次访问都要验证cookie的有效性而查询数据库,感觉效率会大大降低。网上搜到的一些方法如gin也就采用session的方式,但具体原理未能理解。

其次,在最初的设计中没有考虑后端代码的结构,虽然这次没有像第一次重构时所有代码写到一个文件里导致长达1000行,而是分了不同的文件,但在写的时候还是碰到了冗余代码的问题:在验证用户的权限、提交题目的权限等地方都有多次获取用户所在组等信息。原本设想的是不同地方获取的信息不同,如果统一写的话在不同地方会有冗余信息出来,但像这样,在提交题目时需要获取用户所在比赛的信息,在验证用户是否有权限访问某网站需要用户的身份的信息,特化在特定行为需要特定的信息,就为了那微不足道的性能优化降低了代码的可维护性,大大增加后续代码维护的成本。因此下次,设计时包括后端代码的结构组织,重复代码的提取,不要过早优化性能。

上周无意间发现了个不错的golang的脚手架singo。采用的MVC组织方式,将api的监听层和事务逻辑层分隔开,层次清晰,同时也学习到了GORM库,将与数据库交互的sql语句进行封装。也学习到了更优美的中间件的使用方式。忽然意识到去年10月用pythondjango也是采用这样的方式组织代码,有视图层,管理层,序列化层,看样子是一个很好的组织方式。

优化

写完后,忽然多了个需求想增加个通知界面,能够发布通知和试题压缩包链接之类的,于是很快啊,考虑到通知的增加需求,加了个简单页面,数据库加了个表。

然后啊,忽然说想让选手可以在整场比赛都可以提交,并且想提前一天开放登录,但不能提交,让他们熟悉下系统。还好之前考虑到这点,区分了这两种行为的时间,就不需要做改动了hhh。

就绪后,整了个服务器,然后把网站部署了下,测试测试,感觉一切功能良好,然后按了下F12,网络一览忽然发现传输的main.js文件多大1.8MB

一个人传输1.8MB,那1000个人就是2G了。这对于区区只有10M带宽的服务器怎么抵得住。

为了减少这个文件的大小,了解了webpack的作用,通过询问别人得知可以通过source-map-explorer插件查看这个js的内部结构。于是看到了为什么文件会这么大。

原来是代码高亮插件highlight如此之大,webpack把该插件中适用所有语言的高亮脚本都整进去了,而事实上我们允许提交的语言只有C++,因此只要保留这个语言的高亮脚本就好了。

明白了第一步的优化方向,剩下的就是如何做到这个。通过网上的搜索,由于我是用create-react-app构建的应用,修改webpack我不想eject内部配置,于是用react-app-rewiredcustomize-cra(实际这个没用到,因为没有写成模块化的形式)修改。通过如下代码:

config.plugins.push(
new webpack.ContextReplacementPlugin(/highlight\.js\/lib\/languages$/, new RegExp(`^./(cpp)$`))
);

但这个代码是在理解了webpack的配置形式才写出来的,相关的参考资料有:非常棒的了解的优化方向的

可以看见highlight插件的大小陡然减少了。

然后又对一些常用库比如reach, reach-router, axio, ace等库采用cdn引入,同时将原本用的关于时间的插件moment插件换成了更加轻量级的dateformat插件,也减少了体积。最终效果如下。

而关于cdn的,一开始采用的是https://www.bootcdn.cn,但在实际测试时网站有时加载比较慢,由于从这个网站获取js慢导致的,因此最后还是把这些js库放到了自己的服务器里,用腾讯云的cdn来缓存这些网页文件。新用户能有几十G的流量应该够用的。

杂项

本地开发时数据转发的问题,是解决cookies跨域(?)的问题,在src文件夹下创立setupProxy.js,内容如下

const {createProxyMiddleware} = require("http-proxy-middleware");
module.exports = function(app){
app.use(
createProxyMiddleware("/api",{
target:"http://127.0.0.1:9000",
changeOrigin:true,
})
)
}

关于用react-app-rewired进行webpack的配置的config-overrides.js文件,创立于与package.json同级目录,内容如下:

var webpack = require('webpack')
const cssminimizerplugin = require("css-minimizer-webpack-plugin")
// const minicssextractplugin = require("mini-css-extract-plugin");

module.exports = {
webpack: function(config, env) {
console.log('env', env);

config.plugins.push(
new webpack.contextreplacementplugin(/highlight\.js\/lib\/languages$/, new regexp(`^./(cpp)$`))
);

// config.plugins.push(
// new minicssextractplugin({
// filename: "[name].css",
// chunkfilename: "[id].css",
// }),
// );

// config.module.rules.push({
// test: /\.css$/,
// use: [minicssextractplugin.loader, "css-loader"],
// });

config.optimization.minimizer.push(
new cssminimizerplugin()
);

config.externals = {
'react': 'react',
'react-dom': 'reactdom',
// 'react-router-dom': 'reactrouterdom',
'ace-builds': 'ace',
'react-ace': 'reactace',
'axios': 'axios',
'highlight.js':'hljs',
'semantic-ui-react': 'semanticuireact',
}

return config;
}
}

其中引用cdn的写法,也即externals里面编写的形式,在上面提到的非常棒的的介绍里有提到。非常感谢这位作者。

简单来说,冒号前面是包名,后面是该脚本js最终赋值给window的全局变量名称。而这个名称通过查阅该js脚本就可以找到。一般就位于js脚本的开头或结尾。

结果

最终,网页访问压力都由腾讯的cdn承担了,后端api的访问由服务器承担。

在实际上线时,全程监测着服务器的cpu占用率和流量情况。由于前一天发现轻量级服务器不支持短时提升带宽,对当前服务器只有10M带宽感到担忧,但从最终结果来看,带宽高峰期也就出现在比赛开始前和比赛即将结束时,cdn的带宽高峰期有11M,只承担了后端api的服务器带宽高峰期也有9M(但感觉有点异常),可见cdn很好的缓解了流量高峰时服务器的压力,将前端后端访问分离开,才保证了本次比赛的顺利,因为两者加起来所需要的带宽超过了服务器本身的带宽。

总结

最后的最后,感谢你能看到最后。这里只是一些个人的碎碎念。

总而言之,在最初阶段,应该有充足的时间进行设计,规划好前端页面的组织,前后端数据格式,后端代码的结构。一个良好的设计能够大大降低后续代码的编写复杂度和维护难度,毕竟磨刀不误砍柴工。

在优化方面,前端涉及的领域是webpack,优化除了上述提到的js,实际还有css方面的优化。该网站虽然主要的js只有40kb,但css文件有500kb。可能是因为包括了semantic库的缘故。但由于后来在服务器的nginx开启了gzip压缩,传输的css只有100kb,因此也就没有太大的优化了。后端的由于不太熟悉,除了用高性能的框架之外,还暂时不知该如何优化。

如果有以后,UI库想试试mantine库。而golang用用singo