看到web component这么火爆,抽空写一篇关于template的博文,怎样创建一个简单的SPA应用。
什么是Template
在web component 中,template和它的名字一样,是一个模板标签,在它创建的上下文中,浏览器是不会去解析的,甚至不会去加载里面的任何资源,浏览器dom解析到template标签就跳过了,比如我们这样写 :
<template>
<img src='http://localhost:9999/imgs/icon.png' />
</template>
这段代码是要求发送一个请求至http://localhost:9999/imgs/icon.png,得到一张图片,而事实并不像我们想的这样,因为它被包含于Template的上下文中,浏览器根本就不会去理会。
那么这样做的好处在哪儿,我们为什么需要Template标签。
因为template标签的特性,我们甚至可以创建一大堆的视图模板,根据需求渲染相应的视图模板,而不必担心浏览器渲染的性能问题,因为Template是不会被浏览器渲染的,我们可以写一个js脚本来让它根据需求进行渲染,实现一个懒加载的效果。
如何根据template的特性创建一个简单的SPA应用
现在来进入今天的正题,以上简单的带过了一下template,可能大家对Template已经有了一个大概的轮廓,别急我们这就来进入实战,趁热打铁吧。
在实际的开发中,我们很多情况是需要多个视图模板的,这里作为demo我创建了两个视图模板,同时还有两个按钮,点击相应的按钮加载相应的视图模板。
整个demo大概是这样的
用一个h1标签显示index表示这是主页,随后有两个按钮,zhangsan 和 lisi
点击zhangsan,显示zhangsan的相关信息
点击lisi则显示lisi的相关信息
这样的一个简单的应用用的是传统pjax,即history API + Ajax 应用,不过源于作为演示,并没有搭建服务器后端,因此主要作用于history API。
现在贴上html部分源码
<template id="zhangsanTpl">
<h1>i'm ZhangSan</h1>
<ul>
<li>姓名:ZhangSan</li>
<li>年龄:20</li>
<li>性别: 男</li>
</ul>
</template>
<template id="lisiTpl">
<h1>i'm Lisi</h1>
<ul>
<li>姓名:Lisi</li>
<li>年龄:18</li>
<li>性别: 男</li>
</ul>
</template>
<div class="target">
<h1>Index</h1>
</div>
<div>
<button onclick="targetZhangsan()">zhangsan</button>
<button onclick="targetLisi()">lisi</button>
</div>
这里用了两个template表示两个视图模板,分别封装了zhangsan和lisi的相关信息,介于template上下文中的节点在浏览器中是不会被渲染出来的,所以,我们还需要一个js脚本来帮我们做这些事情。
let targetDiv = document.querySelector(".target");
//清除显示容器
let cleanTargetDiv = () => targetDiv.innerHTML = "";
//添加模板到容器
let appendTpl = tpl => targetDiv.appendChild(document.importNode(tpl.content,true));
//改变路由
let updateUrl = url => {
if(Object.prototype.toString.call(url) == '[object String]'){
history.pushState({},url,`/${url}`);
}
else {
throw new Error('url must be a string');
}
}
//初始化路由
updateUrl("host");
//zhangsan点击回调
let targetZhangsan = () => {
cleanTargetDiv();
updateUrl('zhangsan');
appendTpl(document.querySelector('#zhangsanTpl'));
}
//lisi点击回调
let targetLisi = () => {
cleanTargetDiv();
updateUrl('lisi');
appendTpl(document.querySelector('#lisiTpl'));
}
有点懵?别急,我一步一步的来说
首先我们需要拿到的是目标div的DOM对象,也就是我们需要渲染的容器
let targetDiv = document.querySelector(".target");
随后我们这里定义了两个方法用于,清除容器当前内容和添加视图模板到容器(可以简单的理解为显示当前视图模板)
//清除显示容器
let cleanTargetDiv = () => targetDiv.innerHTML = "";
//添加模板到容器
let appendTpl = tpl => targetDiv.appendChild(document.importNode(tpl.content,true));
接下来的不可少的一步是更新路由,我们需要实现点击相关按钮,更新容器内容的同时还要更新路由。
比如我们上面点击zhangsan,路由就变为http://127.0.0.1:8020/zhangsan
点击lisi就变为http://127.0.0.1:8020/lisi
//改变路由
let updateUrl = url => {
if(Object.prototype.toString.call(url) == '[object String]'){
history.pushState({},url,`/${url}`);
}
else {
throw new Error('url must be a string');
}
}
这个方法接收一个参数,这里参数的命名用url表现的有点唐突,因为这里我们需要传入的仅仅只是一个相关路由,而不是整个url,但是由于时间关系,没有做修改,没事将就看。
在方法内部,我们做了一个判断,判断传入参数的类型是否是一个字符串,添加这个限制的原因,是由于传入错误类型导致整个应用崩溃,这“多此一举”主要归功于js的鸭子类型,但是为什么不用TS来写?有类型限制,类型声明不是更好?但是,由于本篇博文讲解的是用原生js来实现,因此这里避开使用TS吧。
如果参数类型不是一个字符串则抛出一个异常。异常信息为’url must be a string’。
//初始化路由
updateUrl("host");
上面这句话是表明刚进入应用时的路由变更为http://127.0.0.1:8020/host,更加具有语义性。
//zhangsan点击回调
let targetZhangsan = () => {
cleanTargetDiv();
updateUrl('zhangsan');
appendTpl(document.querySelector('#zhangsanTpl'));
}
//lisi点击回调
let targetLisi = () => {
cleanTargetDiv();
updateUrl('lisi');
appendTpl(document.querySelector('#lisiTpl'));
}
上面是为了给两个按钮绑定监听事件,并在元素上进行声明绑定
<button onclick="targetZhangsan()">zhangsan</button>
<button onclick="targetLisi()">lisi</button>
回调函数里的三条语句并不难,调用清除当前容器的方法和更新路由的方法。
重点在第三句。
//lisi
appendTpl(document.querySelector('#lisiTpl'));
//zhangsan
appendTpl(document.querySelector('#zhangsanTpl'));
现在回头看看上面的方法声明
//添加模板到容器
let appendTpl = tpl => targetDiv.appendChild(document.importNode(tpl.content,true));
这个方法首先拿到了目标容器(.target)的DOM对象,随后向用appendChild添加子元素,而添加的子元素是
document.importNode(tpl.content,true)
document.importNode 用于递归import子节点,第一个参数为需要import的节点,第二个参数为一个boolean值,表示是否需要递归import所有的子节点。
tpl.content
tpl是该方法传入进来的参数,它是一个需要显示的视图模板的DOM对象,该DOM对象有一个content 属性表示该template下所有的子节点。
整个方法可以理解为,将要显示的template里的子节点都复制一份拿出来,然后append到目标div容器里进行显示。
现在就初步的完成一个简单的SPA应用,基于template。
下面贴上该demo完整的源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<template id="zhangsanTpl">
<h1>i'm ZhangSan</h1>
<ul>
<li>姓名:ZhangSan</li>
<li>年龄:20</li>
<li>性别: 男</li>
</ul>
</template>
<template id="lisiTpl">
<h1>i'm Lisi</h1>
<ul>
<li>姓名:Lisi</li>
<li>年龄:18</li>
<li>性别: 男</li>
</ul>
</template>
<div class="target">
<h1>Index</h1>
</div>
<div>
<button onclick="targetZhangsan()">zhangsan</button>
<button onclick="targetLisi()">lisi</button>
</div>
<script type="text/javascript">
let targetDiv = document.querySelector(".target");
//清除显示容器
let cleanTargetDiv = () => targetDiv.innerHTML = "";
//添加模板到容器
let appendTpl = tpl => targetDiv.appendChild(document.importNode(tpl.content,true));
//改变路由
let updateUrl = url => {
if(Object.prototype.toString.call(url) == '[object String]'){
history.pushState({},url,`/${url}`);
}
else {
throw new Error('url must be a string');
}
}
//初始化路由
updateUrl("host");
//zhangsan点击回调
let targetZhangsan = () => {
cleanTargetDiv();
updateUrl('zhangsan');
appendTpl(document.querySelector('#zhangsanTpl'));
}
//lisi点击回调
let targetLisi = () => {
cleanTargetDiv();
updateUrl('lisi');
appendTpl(document.querySelector('#lisiTpl'));
}
</script>
</body>
</html>
如果有错误或者有疑问请在下面回复给我吧,我会及时的改正或答复。
这里附上本文的源码。在我的GitHub 上,欢迎大家学习下载
https://github.com/HaoDaWang/SPA-for-Template