上上一期连接——也就是本文的基础,参考KOA,5步手写一款粗糙的web框架html
上一期连接——有关Router的实现思路,这份Koa的简易Router手敲指南请收下git
本文参考仓库:点我github
上一期科普了Router,咱们能够为每一张页面配置一个路由,可是咱们不可能每一个router.get(path,(ctx,next)=>{ctx.body=...})
都直接写html
,这样代码也太难维护了。因而出现了模版这个东西,模版主要是用来管理页面的。每个html
都放入一个单独的文件中,这样不管是调用仍是复用都很方便。这里我用了ejs的语法,来写这个模版引擎的中间件。web
那么,咱们从最简单的静态页面开始吧~正则表达式
调用文件不是一件难事,只须要读取,而后赋值给ctx.body
便可:数组
const fs=require("fs")
const path=require("path")
let indexTPL=fs.readFileSync(path.join(__dirname,"/pages/template.ejs"),"utf-8")
ctx.body=indexTPL;
复制代码
这里我先以逻辑为主,因此我用了readFileSync
这个同步方法,而没有用异步读取的方法。bash
这里,咱们新建立一个名为View中间件,专门用于模板嵌套。app
const fs=require("fs")
const path=require("path")
function View(path){
let tpl="";
return async (ctx,next)=>{
tpl = fs.readFileSync(path.join(__dirname,path),"utf-8")
ctx.body= tpl
await next();
}
}
复制代码
而后咱们就能够直接在项目中应用这个中间件了。框架
let view=require("./Views")
let router=new Router()
router.get("/",view("/pages/template.ejs"))
复制代码
或者异步
app.use(view("/pages/template.ejs"))
复制代码
都是可行的,由于我建立的是标准的中间件啊~
咱们为何要用模板!固然是为了动态页啊!因此咱们须要替换模板标签<%=参数名%>
为咱们须要值。同时模板也须要支持一些函数,好比数组循环填充列表。
那么第一步,咱们须要的就是将这个标签提取出来,而后替换成咱们特有的标签<!--operator 1-->
这个能够自定义一个特别的标签用于占位符。
你们没听错,提取,替换!因此正则表达式
是躲不过了,他已经在虐个人路上了……
由于单纯的赋值和执行函数差异比较大,因此我把他们分开识别。若是你们有更好的方法,记得推荐给我。(正则渣渣瑟瑟发抖)
let allTags=[];
function getTags(){
//先取出须要执行的函数,也就是不带"="的一对标签,放入数组,而且,将执行函数这一块替换成占位符。
let operators = tpl.match(/<%(?!=)([\s\S]*?)%>([\s\S]*?)<%(?!=)([\s\S]*?)%>/ig)||[]
operators.forEach((element,index )=> {
tpl=tpl.replace(element,`<!--operator ${index}-->`)
});
//再取出含有“=”的专门的赋值标签,怕和执行函数中的赋值标签搞混,因此这边我分开执行了
let tags=tpl.match(/<%=([\s\S]*?)%>/ig)||[]
tags.forEach((element,index) => {
tpl=tpl.replace(element,`<!--operator ${index+operators.length}-->`)
});
//给我一个整套的待替换数组
allTags=[...operators,...tags];
}
复制代码
重头戏来了,如今咱们要进行模板替换了,要换成咱们传入的值。这里须要注意的就是咱们将allTags
逐个替换成可执行的js文本,而后执行js,生成的字符串暂存于数组之中。等执行完毕,再将以前的<!--operator 1-->
占位符替换掉。
这里须要注意的是,咱们先把赋值的标签<%=%>
去除,变成${}
,就像下方这样:
let str="let tmpl=`<p>字符串模板:${test}</p> <ul> <li>for循环</li> <% for(let user of users){ %> <li>${user}</li> <% } %> </ul>` return tmpl"
复制代码
而后咱们再把可执行的函数的<%%>去除,首尾加上```闭合字符串,就像下方这样:
let str="let tmpl=`<p>字符串模板:${test}</p> <ul> <li>for循环</li>` for(let user of users){ tmpl+=`<li>${user}</li>` } `</ul>` return tmpl"
复制代码
可是这是字符串啊,这个时候咱们要借助一个方法Function 构造函数
咱们能够new一个Function,而后将字符串变成能够执行的js。
Function的语法是这样的new Function ([arg1[, arg2[, ...argN]],] functionBody)
,再字符串以前能够声明无数个参数,那么咱们就借助...
三个帮咱们把Object
变成单个参数放进去就能够了。
举个例子:
let data={
test:"admin",
users:[1,2,3]
}
复制代码
上方对象,咱们用Object.keys(data)
,提取字段名,而后利用三点扩展运算符...
,变成test,users
new Function(...Object.keys(data),方法字符串)
复制代码
也就等同于
new Function(test,users,方法字符串)
复制代码
咱们合并下上方的字符串,这个可执行的模板js就是这样的,怎么样是否是好理解了?
function xxx(test,users){
let tmpl=`<p>字符串模板:${test}</p>
<ul>
<li>for循环</li>`
for(let user of users){
tmpl+=`<li>${user}</li>`
}
`</ul>`
return tmpl;
}
复制代码
感受要变成可执行的js,原理不难,就是拼合起来很复杂。
下方是完整的执行代码:
function render(){
//获取标签
getTags();
//开始组合每一个标签中的内容,而后将文本变成可执行的js
allTags=allTags.map((e,i)=>{
let str = `let tmpl=''\r\n`;
str += 'tmpl+=`\r\n';
str += e
//先替换赋值标签
str = str.replace(/<%=([\s\S]*?)%>/ig,function () {
return '${'+arguments[1]+'}'
})
//再替换函数方法,记得别忘了首位的"`"这个闭合标签
str = str.replace(/<%([\s\S]*?)%>/ig,function () {
return '`\r\n'+arguments[1] +"\r\ntmpl+=`"
})
str += '`\r\n return tmpl';
//提取object的key值,用于function的参数
let keys=Object.keys(data);
let fnStr = new Function(...keys,str);
return fnStr(...keys.map((k)=>data[k]));
})
allTags.forEach((element,index )=> {
tpl=tpl.replace(`<!--operator ${index}-->`,element)
});
}
复制代码
将readFile
变成一个Promise
,而后放入中间件中await
一下,这样就能够实现异步了~
若是不了解async/await,科普传送门。
const util=require("util")
const fs=require("fs")
const path=require("path")
let readFile=util.promisify(fs.readFile)
function view(p,data){
let tpl="";
let allTags=[];
function getTags(){
//略
}
function render(){
//略
}
return async (ctx,next)=>{
tpl = await readFile(path.join(__dirname,p),"utf-8")
//别忘了运行render(),替换模板标签
render();
ctx.body=tpl;
await next();
}
}
复制代码