不用React Vue,只用原生JS,如何开发单页面应用(SPA)?
theme: condensed-night-purple
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情。
背景
之前我发布了一篇文章,介绍自己做的小游戏《Dice Crush》。
本文分享一项技术方案,正是我开发上述游戏时用到的:不用React Vue,只用原生JS,如何开发单页面应用?
什么是单页面应用
单页面应用(Single-Page Application)是个相对古老的传统的多页面应用(Multi-Page Application)的名词。
以前我们访问网页,每个页面是一个html文件。点击某个超链接,就跳转到新的html页面。每次浏览器访问html时,需要重新下载整个html文档、JS和CSS依赖,才能展现出整个页面。这个效率很低。
随着异步请求AJAX等技术的兴起、HTML5规范的出现,开发者有了更优秀的页面加载方案:一个网站的所有页面,都是同一份html文档,用JS判断路由,并动态展示内容。通过预加载等方式,把整个网站的页面都下载到内存中。每当用户点击超链接,准备切换页面时,通过history API使浏览器更新URL而不必重新下载html文档,然后JS只要把现有的页面卸载(隐藏),再把内存中的东西展示出来即可。这个过程完全避免了网络请求,极大提高了网站用户体验。
采用上述方案实现的Web应用就是单页面应用。
React和Vue开发的基本都是单页面应用
现代Web开发,大多数网站是用React或Vue开发的,它们基本都是单页面应用。
开发者可以很方便的使用React、Vue开发单页面应用,是因为React Router和Vue Router帮开发者实现了单页面应用的核心逻辑。所以开发者不必关心细节,只要会用React Router和Vue Router即可。
这就导致一个问题:如果我们不用React或Vue(例如我的游戏《Dice Crush》是用原生JS实现),没有React Router和Vue Router的能力,该怎么开发单页面应用呢?
开发单页面应用,有哪些难题
在聊怎么实现之前,我们要先想明白:开发单页面应用,需要解决哪些难题?
- 多个页面如何定义?
- 页面切换时,不可以使用location.replace('新的网址')或document.href = '新的网址',因为它会使浏览器下载html文档。我们需要用HTML5的History API,修改网址。
- \标签导航时,不能使用原生的href属性,因为它会使浏览器下载html文档。我们需要监听onclick事件,在里面调用History API修改网址。
- 使用History API修改网址后,页面不会有任何变化,只是浏览器URL变了。我们需要手动操控当前页面DOM的销毁、新页面DOM的生成。
- 使用History API修改网址后,当用户点击浏览器的「返回」、「前进」时,页面不会刷新,只是浏览器URL变了。我们需要监听事件onpopstate,即监听用户点击浏览器的「返回」、「前进」,然后操控当前页面DOM的销毁、新页面DOM的生成。
以上是一些最基本的难题,如果你要追求极致用户体验,还需要解决下面的难题:
- \标签导航,需要借助href属性,给予用户在新窗口打开链接的权利。
- 当用户切换路由时,如果发生了临界事件,要能够做好兼容。例如,用户点击了链接,准备渲染新页面,此时立马点击了旧页面某个按钮,要执行旧页面某个按钮的回调函数。这可能有超出预期的结果。我们需要在切换路由后,就禁止旧页面的一切事件回调。
1、定义多个页面
每个页面是由HTML+JS+CSS组成的。每个页面需要对应一个路由。
我说一下我在游戏《Dice Crush》中的做法。
它有3个页面:主页、选择关卡页面、游戏页面。如下图:
我给每个页面定义了一个template.js,用于存放html字符串。比如:
``js
const template =
Dice Crush
之后渲染页面时,只需要document.body.innerHtml = template,就可以把该页面的模板渲染到html文档上了。当然,渲染页面时,还需要给button绑定click事件。
因此,我们给每个页面声明一个template,再声明一个用于渲染该页面的函数(功能主要是给document.body.innerHtml赋值、给button添加click事件),就可以了。之后需要渲染哪个页面,就调用哪个页面的渲染方法。
2、页面切换,使用History API切换URL
需要切换页面时,我们需要使用history.pushState(null, '', '新的页面URL')来修改浏览器URL,同时调用上述渲染页面方法,把页面渲染在浏览器中。
3、a标签的问题
我们需要注意,如果给\标签添加了href,最好给它绑定这样的click事件:
js
linkElement.onclick = function (event) {
if (event.button !== 0) return;
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
window.history.pushState(null, '', 'new-page.html');
// 手动渲染新的页面
};
event.button表示按下的是鼠标哪个按键(0是主按键,通常指鼠标左键或默认值)。如果用户是鼠标中键按下a标签、或者用户同时按下了Ctrl(Windos)、Command(Mac)、Shift,那么他应该期望是在新窗口打开,我们使用href原生行为即可。如果用户同时按下了Option,那么他应该期望是打开菜单栏,我们也执行原生行为。其它情况,都表明用户要在本页面点开那个网址,我们拦截原生的href,通过history.pushState实现,并手动渲染新的页面。
4、手动加载新页面、卸载旧页面
由于我们页面渲染是通过document.body.innerHtml实现的,所以会在加载新页面时自动卸载旧页面。
当然,如果你的旧页面在window上添加了一些事件监听器、计时器,也要记得手动卸载掉。做好清除工作,不然会出问题。
5、页面初次加载与监听事件onpopstate
页面初次加载时,我们需要根据路由渲染一个页面,示例代码如下:
js
const init = () => {
if (window.location.pathname.includes('play')) {
renderGame();
} else if (window.location.pathname.includes('select')) {
renderSelect();
} else {
renderHome();
}
};
init();
同样,当页面onpopstate时,即用户点击了「返回」、「前进」,依然停留在本页面时,我们也需要重新根据当前路由渲染一下页面。需要执行如下逻辑:
js
window.onpopstate = init;
至此,我们手写的一个单页面应用就开发完成啦~这也是我在游戏《Dice Crush》中使用的方案,你学会了吗?
写在最后
我是HullQin,独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》、《极致用户体验》。
- 一定给Button加上这个CSS,否则在用户设定的字体或字号下,文案就换行了!
- [Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁
- 一行代码,让网页变为黑白配色
- 联机象棋发布!打开URL就能联机对战!观战!单机演练!分享残局!
- 开发个「英雄杀记牌器」
- [教你做小游戏] 太卷了!开发象棋,为了减少40%存储空间,我学了下Huffman Coding
- [教你做小游戏] 极致压缩:用2至5位二进制表示17种可能性
- [教你做小游戏] 车炮能移动17个位置,针对90种出发点,如何建立0-16和目标点的映射?
- 不用React Vue,只用原生JS,如何开发单页面应用(SPA)?
- 火爆全网的 Evil.js 源码解读
- 在互联网,摸爬滚打了几年,我悟了。面对如今经济形势,普通打工人如何应对?
- [Go WebSocket] 你的第二个Go WebSocket服务: 聊天室
- 我们用48h,合作创造了一款Web游戏:Dice Crush,参加国际赛事
- [JS真好玩] 掘金前端你好:作者榜出bug啦,我们一起看怎么修。顺便分享创意技巧:改EventListener
- [教你做小游戏] 展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!
- [JS入门到进阶] 手写裁剪图片的工具,并部署。一键裁剪掘金文章封面
- [JS入门到进阶] 手写解析URL参数的工具,并部署。用起来又快又爽!
- 你必须要会uvloop!让Python asyncio异步编程性能直逼Go协程性能
- [JS入门到进阶] 7条关于 async await 的使用口诀,新学 async await?背10遍,以后要考!快收藏
- [JS真好玩] 大招!用JS找到:哪 个 小 坏 蛋 给 我 连 点 2 次 赞 ?