不用React Vue,只用原生JS,如何开发单页面应用(SPA)?

语言: CN / TW / HK

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的能力,该怎么开发单页面应用呢?

开发单页面应用,有哪些难题

在聊怎么实现之前,我们要先想明白:开发单页面应用,需要解决哪些难题?

  1. 多个页面如何定义?
  2. 页面切换时,不可以使用location.replace('新的网址')或document.href = '新的网址',因为它会使浏览器下载html文档。我们需要用HTML5的History API,修改网址。
  3. \标签导航时,不能使用原生的href属性,因为它会使浏览器下载html文档。我们需要监听onclick事件,在里面调用History API修改网址。
  4. 使用History API修改网址后,页面不会有任何变化,只是浏览器URL变了。我们需要手动操控当前页面DOM的销毁、新页面DOM的生成。
  5. 使用History API修改网址后,当用户点击浏览器的「返回」、「前进」时,页面不会刷新,只是浏览器URL变了。我们需要监听事件onpopstate,即监听用户点击浏览器的「返回」、「前进」,然后操控当前页面DOM的销毁、新页面DOM的生成。

以上是一些最基本的难题,如果你要追求极致用户体验,还需要解决下面的难题:

  1. \标签导航,需要借助href属性,给予用户在新窗口打开链接的权利。
  2. 当用户切换路由时,如果发生了临界事件,要能够做好兼容。例如,用户点击了链接,准备渲染新页面,此时立马点击了旧页面某个按钮,要执行旧页面某个按钮的回调函数。这可能有超出预期的结果。我们需要在切换路由后,就禁止旧页面的一切事件回调。

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个专栏里分享:《教你做小游戏》《极致用户体验》

「其他文章」