前言
本文仅作为学习经验分享,本人才疏学浅,如有不正确之处,欢迎批评指正。
今日在学习前端工程化的知识时,认识了SPA(单页面应用),MPA(多页面应用)这两种前端架构模式。其实这两种网站的模式很好理解。mpa就是传统的多页面应用,通过文档之间的跳转进行页面切换。spa就是单个html的应用,通过动态更新DOM组件实现界面切换。在使用react,vue这样的前端框架时,构建的项目也都是spa,其中控制页面切换的重要部分就是前端的路由管理。比如react-router,vue-router。
于是我就在想,能不能使用原生的技术实现一个类似前端框架一样的SPA呢。结论是可以的! 即使我没有看过主流框架的源代码,我仍然照葫芦画瓢实现了一个简单的版本。
正文
先列举一下整个的项目结构
project----
|-----Pages-----
|----home.js
|----user.js
|----about.js
|----contact.js
|-----index.html
|-----logic.js
|-----system.js
其中index.html相当于SPA的骨架,logic.js是主要的逻辑文件,维护路由页面跳转,system.js里面用来存放一些api函数。Pages目录存放页面组件,每个组件使用一个.js文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>csr-spa-demo</title>
</head>
<body>
<div id="container"></div>
<!---渲染页面内容--->
<script type="module" src="logic.js" ></script>
<!---路由管理--->
</body>
</html>
实现思路:
采用window.location.hash作为路由,当hash改变时,触发window的hashchange事件,根据不同的路由,将不同的组件(dom元素)appendChild到id为container的div中。
const container = document.querySelector('#container');
import HomePage from './Pages/home.js';
import AboutPage from './Pages/about.js';
import ContactPage from './Pages/contact.js';
import UserPage from './Pages/user.js';
//初始化界面默认home
window.addEventListener('load', () => {
window.location.hash = '#home';
container.appendChild(HomePage());
});
//路由管理
window.addEventListener('hashchange', () => {
const hash = window.location.hash;
let router,param;
console.log(hash);
if(hash.includes('/')){
[router,param]=hash.slice(1).split('/');
}else{
router=hash.slice(1);
};
container.innerHTML = '';
switch (router) {
case 'home':
container.appendChild(HomePage());
break;
case 'about':
container.appendChild(AboutPage());
break;
case 'contact':
container.appendChild(ContactPage());
break;
case 'user':
container.appendChild(UserPage(param));
break;
}
});
路由参数通过分解hash获得,比如用户访问user/123这个路由,就将123解析出来,传给User(123);
每个组件返回的是一个编辑好内容的div元素。拿home.js举例
import {navigate} from '../system.js';
export default function Home() {
const container=document.createElement('div');
container.innerHTML=`
<nav>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</nav>
<div class="content" id="content">Welcome to the Home page!</div>
<span>Input user's id</span>
<input type='text'/>
<button>Click Me</button>
`;
container.style.backgroundColor='orange';
container.querySelector('button').addEventListener('click',()=>{
let userId=container.querySelector('input').value;
if(!userId){
userId=0;
}
navigate('user/'+userId);
});
return container;
}
我们通过手动编写该组件的innerHTML,style还有内部的逻辑实现组件化。
这里的navigate函数就是system.js中定义的路由跳转函数。
export function navigate(router){
window.location.hash='#'+router;
}
在react中,有一个useEffect()的状态函数,可以实现组件初次加载后执行一些逻辑,比如页面组件加载后,进行ajax请求获取数据,然后渲染页面。
我在user.js中也需要使用这个功能,于是我经过查询,采用MutationObserver对象来监听指定父组件的childList变化,当变化时,触发传入的callback函数,然后解除监听。我在system.js中封装成了onLoad函数.
export function onLoad(node, callback) {
const config = {
childList: true
};
const oberver = new MutationObserver((mutationsList, observer)=>{
callback();//执行加载后逻辑
oberver.disconnect();
});
oberver.observe(node,config);
}
然后我在user.js中调用onLoad实现了user.js初次加载后的ajax
export default function User(userid) {
const container=document.createElement('div');
container.innerHTML=`
<nav>
<a href="#home">Back to Home</a>
</nav>
<div class="content" id="content"></div>
<button>Click Me</button>
`;
container.style.backgroundColor='lightGreen';
container.querySelector('button').addEventListener('click',()=>{
alert('User');
});
onLoad(document.querySelector('#container'),async()=>{
//该组件加载后触发
const response = await fetch('http://localhost:3000/user/'+userid, {
method: 'GET'
});
if(response.ok){
const data=await response.json();
container.querySelector('#content').textContent=data.userid;
}
});
return container;
}
由于onLoad中的监听绑定是先于logic.js中的appendChild,所以当组件加载到父组件(页面组件的父组件是index.html的#container)后,就会触发callback,这样就实现了一个组件初次加载后立即执行逻辑。
该SPA的效果如下
页面初始为#home
点击对应的
a标签或者触发navigate都会修改hash,达到界面跳转的效果
当指定了路由参数时,会解析参数然后传给组件,
user组件使用onload在加载后发送带有userid参数的GET请求给服务器,服务器返回对应的用户信息(这里为了简化服务器只返回userid),客户端接收后进行动态更新。
结语
至此,一个简单的组件化的,客户端渲染的SPA就实现了。
本次实践收获良多,也加深了我对前端工程化概念的理解。
感谢您的阅读。