由于几个现代前端框架的流行,许多前端开发人员逐渐失去了在不使用任何框架或库的情况下构建整个应用程序的感觉。
基于my previous article,我决定尝试仅使用JavaScript和自定义元素创建单个页面应用程序(SPA)的感觉。因此,我最终创建了一家“迷你”电子商务商店。
即使我无法完成它,它也提供了一些我发现有趣的见解。
在我们继续之前,您可以check out the web page here或查看the repo here。
single-page application(Spa)是一个Web应用程序或网站,通过使用Web服务器的新数据动态重写当前网页,而不是Web浏览器加载整个新页面的默认方法。 P>
水疗中心中的一个流行概念是 Component Driven Development ,它基本上是将代码分开为较小的可重复使用的组件的一种方法,这些组件提供单个/简单功能。
在发布an article on custom elements之后,它提供了一种使用Vanilla JavaScript创建可重复使用的组件的方法;出于好奇心,我一直在思考和询问我们可以在没有我们喜欢的框架/库,也许只使用自定义元素的情况下,在JavaScript中可以推动自定义元素多远?当我在社交媒体上问这个问题时,一些反应很有趣,但它们引起了我的兴趣。我决定牺牲一些时间来选择压力(正如一个人所描述的那样),并找出可能造成的压力。事实证明,这是很大的压力 - 但没有我想象的那么多。
有三个区分水疗中心的主要概念:
- 结构:按结构,我的意思是文件夹/代码结构,布局,组件等
- 路由:这当然非常重要,因为路由在前端进行了处理
- 国家管理:,即使这并不是SPA独有的,但国家管理在SPA中至关重要
基于这三个概念,我已经将本文分为三个部分,以根据我在我的小冒险中与他们接触的方式讨论这些主题。
文件夹结构
由于自定义元素,这方面也许是项目的最简单方面。正如我在有关自定义元素的文章中所解释的那样,您可以轻松地使用JavaScript创建可重复使用的HTML组件。因此,正如您所期望的那样,我故意努力使文件夹结构看起来与您在React应用程序中的结构相似。这是扩展文件夹结构的屏幕截图:
是的,很多文件,我知道 - 目前甚至没有使用其中一些文件 - 但这是对水疗文件夹结构的讽刺模仿ð
如果您很好奇组件的片段,这是navbar
:
路由
水疗中心中的基本路由非常简单,相对易于实现。例如,React Router works under the hood的方式是生成一个包含所有定义路由的routes
对象,然后在用户尝试访问不属于预定义路由的路由时丢弃404
错误。
为了模仿这个,我刚刚创建了一个看起来像这样的routes
对象:
// routes.js
// all available routes
const routes = {
'?about': () => {
return about()
},
'?home': function () {
return product()
},
'': async () => {
return await home('')
},
404: () => {
error404()
},
}
export default routes
我知道它看起来有些混乱,但是有一个理由...
在上面的摘要中,我以route
为键创建了一个对象,并且一个函数返回另一个函数作为值。使用Higher Order Functions的原因是因为我希望能够将参数传递给模板函数(创建“动态”路由),这似乎是最简单的方法。
请注意,我使用了koude5(?
),而不是常规路由(/
) - 很快就会提供更多信息。
// app.js
// import the routes object
import routes from './api/routes.js'
// Main container
const main = document.querySelector('.root')
// switch route without reloading the page (default behavior of links)
function handleRoute(event) {
event = event || window.event
event.preventDefault()
window.history.pushState({}, '', event.target.href)
handleLocation()
}
// check if the current path (url) is part of the preconfigured routes, else display the 404 page
async function handleLocation() {
const path = window.location.search
const templateFunc = routes[path] || routes[404]
const html = await templateFunc()
main.innerHTML = html
}
// make handleRoute() a method on the window object
window.handleRoute = handleRoute
// when user clicks back/forward, trigger the location function
window.onpopstate = handleLocation
// call the location function on first page load
handleLocation()
// export the function so it can be used within custom elements
export { handleRoute }
上面的摘要中的评论是不言自明的;首先,我们导入routes
对象,然后创建一个handleRoute
函数,将其添加到我们的链接中以跨页来进行路由。该函数采用event
参数(用户单击链接时会触发它),防止事件的默认行为(阻止页面重新加载),并调用koude11将用户“按”链接中指定的URL,之后,我们称之为函数:handleLocation
。
handlelocation
用于获取用户所打开的当前URL(在这种情况下,搜索参数 - 这是我们在路由对象中使用的)使用window.location.search
,请检查该路由是否在routes
对象中。如果是这样,我们返回路由的内容,否则我们返回404
路线,然后最终调用route
返回的功能,然后将其内容附加到主页包装中。
配置路线后,我们现在可以创建“软链接”来链接不同的页面而无需重新加载浏览器:
<button class="nav-btn"><a href="/" onclick="handleRoute()">Home</a></button>
做同一件事的不同方式
在水疗中心中实现路由有不同的方法。让我们看一下其中的三个:
-
常规路由(/):这是通常用于路由库的标准方法,例如React Router等。此方法使您可以使用前向斜杠(/)创建以单独的页面出现的路由。要使用此方法,我们需要修改
handleLocation
函数以使用window.location.pathname
而不是window.location.search
,
// app.js
// check if the current path (url) is part of the preconfigured routes, else display the 404 page
async function handleLocation() {
const path = window.location.pathname
const templateFunc = routes[path] || routes[404]
const html = await templateFunc()
main.innerHTML = html
}
然后修改路由对象:
// routes.js
// all available routes
const routes = {
'/about': () => {
return about()
},
'/home': function () {
return product()
},
'/': async () => {
return await home('')
},
404: () => {
error404()
},
}
export default routes
但是,使用此方法时,您还需要配置服务器以返回用户输入的所有路由的单个HTML页面。如果没有这样做(由于HTML总是在JavaScript之前解析),浏览器试图访问与现有HTML文件的路由的HTML文件时,浏览器会引发错误。当然,如果您有兴趣,则可以轻松地使用Express或任何类似工具来实现此目标。
- 查询参数:这是我上面使用的方法,因此我认为您已经明白了。当然,这(让我选择它)的主要优点是,浏览器已经考虑了带有查询参数的URL作为基本URL的一部分,因此所有内容都可以“作为单个页面”,而无需任何服务器端配置。
- hash url: this - 正如您所期望的那样,类似于使用查询参数。唯一的区别是它使用与查询参数一起使用的popstate event的hashchange event Intate。您可以查看此视频以获取更多信息:
动态路线呢?
创建动态路线的第一种方法是创建一个接受path
和template
的函数(在这种情况下,是返回模板的函数),并将其附加到routes
对象。
// programmatically create a new route and append it to the routes object
async function createRoute(path, template) {
routes[path] = await template
}
不幸的是,这没有起作用,我还无法完全想象为什么(也许您可以提供帮助)。
因此,我必须继续将props
传递给路线模板功能的最初想法。完整的route
对象最终看起来像这样:
// route.js
// all available routes
const routes = {
'?about': () => {
return about()
},
'?home': function () {
return product()
},
'': async () => {
return await home('')
},
'?category=jewelery': async () => {
return await home('jewelery')
},
'?category=electronics': async () => {
return await home('electronics')
},
'?category=men%27s%20clothing': async () => {
return await home("men's clothing")
},
'?category=women%27s%20clothing': async () => {
return await home("women's clothing")
},
404: () => {
error404()
},
}
注意:我使用encodeURI格式化了
prop
,然后再添加到端点以获取产品。
国家管理
国家管理可以说是当今前端发展中最关键,令人难以置信的,也许是最困难的方面,尤其是在水疗中心;当您尝试在不使用任何专业工具(状态管理库/软件包)的情况下实现相同的功能时,这个困难就会升级。
虽然在组件内具有立即发挥副作用的事件相对容易,但是如果您想重新发明轮子,则在组件中实时共享和维持状态几乎是一场噩梦。例如,我针对产品类别的最初计划是创建一个单一路由,然后根据所选类别获取并显示内容。为此,我在自定义元素的koude27方法中添加了一个事件听众,以根据所选类别获取和显示产品,然后使用递归重新渲染组件以反映状态更改:
// home.js
// event listener to render a selected product category
connectedCallback() {
this.shadowRoot.querySelectorAll('.category')
.forEach((navLink) => navLink.addEventListener('click', async (e) => {
let selectedCategory = e.target.textContent
// update the selected product category
category = await fetchProductCategory(selectedCategory)
// call the home function to cause a re-render
home()
}))
}
但是,我注意到的一种怪异的行为是,即使是重新渲染的组件,它仍然保持相同的陈旧数据 - 基本上,显示默认类别而不是新类别。如前所述,这导致了routes
对象的混乱。
诚然,可能还有其他更好的替代方法或方法可以使用普通的旧JavaScript在单个页面应用程序中有效管理状态,但我尚未遇到任何问题。在我的研究中,我遇到了像this one一样的useState
钩的几个投机实施,但是它们都无法解决挑战。因此,我犹豫了一下,我放弃了。
结论
是的,从技术上讲,可以使用Web组件创建单页应用程序。但是,这种方法本质地适用于所有SPA的基本问题之一是运送到浏览器的JavaScript量。上述项目中JavaScript的总未介绍的大小约为2MB,对于这样一个小应用程序来说,这是很多JavaScript。当然,使用模块捆绑器将大大降低此尺寸,但是,这并不能超越用户对初始负载的性能影响。
这一小挑战的目的 - 甚至本文不一定是鼓励开发人员停止使用框架或库,这首先发明了这些技术的主要原因是解决了在该技术中的不同挑战软件开发过程;因此,其中大多数绝对有用且重要。但是,我的看法是,开发人员应该非常好奇,以真正了解这些工具的工作方式以及使用它们时所涉及的权衡。
我希望这对您来说是一个有趣的书,不要忘记在custom elements上查看主要文章。