木灵鱼儿
阅读:2163
koa框架23 服务端渲染、jade、ejs、koa服务端渲染、内容静态化
服务端渲染
全称的 server side rendering 也就是所谓的ssr了,经常看到vue的一个关于seo优化的一个方案或者是防止首页白屏,一般选用ssr方案。
优点:
- seo友好
- 无兼容问题
- 安全性高
- 代码精简
相对客户端渲染 client side rendering 客户端有两个优点:
- 降低服务端,带宽压力
- 用户体验好
js运行速度快,生成html比服务端一个来回快很多。
服务端渲染框架
pug(jade)
以前叫jade,现在叫pug,改名了。
使用
npm i pug
const pug=require('pug');
pug.renderFile(
//模板
'./template/1.pug',
//数据
{
pretty: true, //用来测试
title: '试试pug这个东西',
users: [
{name: 'blue', age: 18},
{name: 'zhangsan', age: 25, vip: true},
{name: 'lisi', age: 26},
]
},
(err, html)=>{
if(err){
console.log('渲染失败');
console.log(err);
}else{
console.log(html);
}
});
pug.render()这是渲染手打的pug代码,renderFile则是渲染一个模板文件,所以renderFile的第一个参数为模板文件地址。
第二个参数是模板使用的参数+pug自身的设置选项,他合并到一个对象里面了,pretty: true,
用于美化输出的,原来是做一行压缩的
第三个则是回调,err为错误,html为解析完毕后渲染好的html文档。
模板
html
head
meta(charset="utf-8")
meta(name="copyright",content="zhinengshe.com")
link(rel="stylesheet",href="/main.css")
style.
#logo {width:200px;height:150px;}
.box {width:200px;height:200px;}
body
h1#logo.box.active= title
ul.list
for user in users
if user.vip
li.item.vip
span.name= user.name
span.age= user.age
else
li.item
span.name= user.name
span.age= user.age
pug的语法,层级的关系是通过他的缩进来判断的,行内属性写在圆括号后,然后由于id和class比较常用,于是又对应的缩写形式:h1#logo.box.active
如果是元素的内容,在标签书写完毕后空一格接内容:style #logo{xxxx}
如果想要换行写,那么就在标签后面加个点.
,然后回车,记得缩进,不然不会识别为内容
如果要使用变量,在前面加个等号=
然后就是一些for循环了。
ejs
ejs相对于pug,他不会破坏html,他类似于php那种渲染方式。
npm i ejs
使用
const ejs=require('ejs');
ejs.renderFile(
//1.模板
'./template/1.ejs',
//2.数据
{
name: 'blue',
arr: [12,5,8,34]
},
(err, html)=>{
if(err){
console.log(err);
}else{
console.log(html);
}
}
);
ejs也是三个参数,和pug差不多,但是ejs第二个参数没有配置参数
模板
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<%-include('header.inc.ejs')%>
姓名:<%=name%>
<ul>
<%arr.forEach(item=>{%>
<li><%=item%></li>
<%})%>
</ul>
</body>
</html>
ejs实际上就是一个html+变量语法
<%=name%>
name是变量,=符号用于输出,这个输出是转义的,如果变量里面有html的尖括号那些特殊字符,会被转义
<%-name%>
使用减号表示不转义输出
include
是ejs的一个引入ejs文件的一个方法,用于多个文件共用一部分相同的内容,比如header,footer这些。
然后include本身只是引用,所以还需要输出,如果使用=等号,引入的ejs文件会被转义,所以要使用减号
js部分,ejs里面可以直接使用原生js,js的使用,如果不需要输出,不要等号和减号就行了,所以<%arr.forEach(item=>{%>
用<%%>
直接包裹js语法就行了。
koa渲染
koa对ejs也有也插件
npm i koa-ejs
const Koa = require("koa");
const ejs = require("koa-ejs");
const path = require("path");
let server = new Koa();
ejs(server, {
root: path.resolve(__dirname, "template"), //模板目录
layout: false, //布局目录,子目录
viewExt: "ejs", //模板扩展名
cache: false, //缓存
debug: false, //debug模式,一般用不到
});
server.use(async ctx => {
await ctx.render("index", {
title: "木灵鱼儿"
})
});
server.listen(8080);
koa-ejs的使用时将server作为参数传入,然后接一个配置参数,root为模板根目录,layout暂时不管,false就行了,然后就是viewExt,设置了扩展名后,我们在后面调用模板就不用再写后缀了,缓存的话,如果开启了,会缓存渲染后数据,方便下次调用,自己调试的时候就关闭,debug是作者自己用的,我们一般用不着
使用了通过ctx.render方法,可以去渲染指定模板文件,第一个是文件名,第二个是参数。
配合数据库
const Koa = require('koa');
const ejs = require('koa-ejs');
const path = require('path');
const Router = require('koa-router');
const mysql = require('promise-mysql');
const opn = require('opn');
const {
time2date
} = require('./libs/common');
let server = new Koa();
ejs(server, {
//模板目录
root: path.resolve(__dirname, 'template'),
//布局目录(子目录)
layout: false,
//模板扩展名
viewExt: 'ejs',
//缓存
cache: false,
debug: false
});
//路由
let router = new Router();
//首页
router.get('/', async ctx => {
ctx.redirect(
router.url('list', 1)
);
});
//列表
router.get('list', '/list/:page', async ctx => {
const {
page
} = ctx.params;
const SIZE = 10;
//本页数据
let data = await ctx.db.query("SELECT ID,title,time,channel FROM news_table LIMIT ?,?", [(page - 1) * SIZE, SIZE]);
data.forEach(news => {
news.time = time2date(news.time * 1000);
});
//总数据条数
let rows = await ctx.db.query("SELECT count(*) AS total FROM news_table");
await ctx.render('list', {
list: data,
page: parseInt(page),
page_count: Math.ceil(rows[0].total / SIZE),
channels: {
'index': '热门',
'guonei': '国内',
'guoji': '国际',
'war': '军事',
'hangkong': '航空'
}
});
});
//详情
router.get('/news/:id', async ctx => {
const {
id
} = ctx.params;
let rows = await ctx.db.query("SELECT title,content FROM news_table WHERE ID=?", [id]);
let data = rows[0];
if (!data) {
ctx.body = '新闻没有找到,可能已经删除或尚未审核';
} else {
await ctx.render('news', data);
}
});
server.use(router.routes());
(async () => {
server.context.db = await mysql.createPool({
host: 'localhost',
user: 'root',
password: '123456',
database: 'news163'
});
server.listen(8080);
opn('http://localhost:8080/');
})();
内容静态化
所谓额内容静态化就是将已渲染的东西缓存起来,下次使用相同的东西就使用缓存,已加快速度。
原理
有缓存 ---- 读取缓存 --- 返回
无缓存 ---- 渲染----服务器返回个浏览器渲染内容 --- 将渲染缓存
无非就这个逻辑,于是几种方式
内存缓存
//缓存对象
let cache = {};
//静态化
router.use(async (ctx, next) => {
if (cache[ctx.url]) {
console.log(`from cache: ${ctx.url}`);
ctx.body = cache[ctx.url];
} else {
console.log(`from render: ${ctx.url}`);
await next();
cache[ctx.url] = ctx.body;
}
});
通过use,每个路由都需要经过他,我们就就判断嘛,有缓存用缓存,没缓存先渲染丢给服务器后再保存到缓存对象。
文件缓存
将缓存保存为本地文件: 创建一个cache文件夹
const fs = require('promise-fs');
//静态化
router.use(async (ctx, next) => {
let name = ctx.url.replace(/\//g, '_');
let cachepath = path.resolve(__dirname, 'cache', name);
try {
await fs.stat(cachepath);
ctx.set('content-type', 'text/html; utf-8');
ctx.body = fs.createReadStream(cachepath);
console.log(`from cache: ${ctx.url}`);
} catch (e) {
await next();
await fs.writeFile(cachepath, ctx.body);
console.log(`from render: ${ctx.url}`);
}
});
由于我们使用路径作为文件名,但是路径是带下划线的,我们需要进行修改,改为下划线。
然后我们先生成缓存的文件路径,因为这时我们已经有访问路径了嘛,自然知道缓存的文件名了。
由于需要判断文件是否存在,通过stat方法查找是否有这个文件,有就继续运行代码,没有就会报错,所以我们用try-catch包裹
有这个文件,我们通过createReadStream方法,读取一段返回一段,但是这是文件流的形式,也就是文件下载的形式,这就导致文件变成了下载而不是网页,但是我们可以通过修改ctx的头信息将这个流额形式改为html读取。
如果没有这个文件,我们就先渲染,也就是next下一步,渲染自然是通过下一个对应路由去做了,渲染完毕后再保存本地。
radis缓存
使用文件的话,io的消耗很大,我们又可以使用radis
const redis = require('redis');
const bluebird = require('bluebird');
let client = redis.createClient({
host: 'localhost'
});
bluebird.promisifyAll(redis.RedisClient.prototype);
//静态化
router.use(async (ctx, next) => {
let name = ctx.url.replace(/\//g, '_');
let cache = await client.getAsync(name);
if (cache) {
ctx.body = cache;
console.log(`from cache: ${ctx.url}`);
} else {
await next();
await client.psetexAsync(name, 30 * 86400 * 1000, ctx.body);
console.log(`from render: ${ctx.url}`);
}
});
连接好radis,并且通过bluebird将方法转为async后
一样先改下路径名字作为缓存对象的key,然后通过getAsync直接去获取这个key,如果这个key存在的话,radis是返回字符的,不像mysql返回数组对象额。
我们直接if判断这个对象存不存在就行了,存在就用,不存在就先next,再存储,这里我们也使用了过期时间,这样radis会自动在到期后删除内容。
版权申明
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。
相关推荐
koa教程2 HttpBearerAuth 传递token
前端发送token到后端,一般有三种方式:链接中携带token,query参数post这种请求body中携带token头信息携带token但是事实上http已经为我们提供了传token的方式,是一个相对来说比较安全的方式,就是HttpBearerAuth 其实他也是一个头信息来着,只是相对来说多了一点操作,在apipost软件中,我们可以直接可视化设置如果是axios,则是需要添加一个头信息:axios({ method: 'get', url: url, responseType: 'blob', headers: { 'Authoriz...

koa教程2 微信openid登录
微信的openid,前端登录时,会发送一个code码到后端,后端接收到这个code,然后配合appid、appsecret参数,像wx发送一个get请求。wx的请求地址:GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code具体查看文档:微信官方文档-服务端可以看到这段get请求有四个参数:属性类型默认值必填说明appidstring 是小程序 appIdsecretstring 是...
koa教程2 权限分级控制
在之前的代码中,我们使用了一个scope属性来表示用户的权限,但是scope是一个数字,他的阅读性是非常差的,当其他人开发的时候,可能并不知道你这个数字是什么意思?所以,我们需要将它转义成文本。为了方便判断,我们在enum.js中增加一个用户等级的对象enum.js://通用判断是否存在该类型 function isThisType(val) { let flag = false; Object.keys(this).find(key => { if (this[key] === val) { return flag = true; } })...
koa教程2 token鉴权
之前我们在token中传入我们的自定义参数,现在我们要做一个token鉴权,很简单,如果他的token合法,就可以访问这个路由,如果不合法,就打回。这个也不能说是真正意义上的鉴权,因为他还没有判断scope值,所以只能说是简单判断。做这个判断我们就可以通过中间件的形式,于是我们在middlewares目录下创建一个auth.js文件auth.js:const parseBearerToken = require("@utils/paresBearerToken"); const jwt = require("jsonwebtoken"); cons...
koa教程2 用户登录
登录类型用户注册之后,自然是用户登录,登录的时候我们需要考虑到登录的方式,往往在正式的项目里面,登录的方式可能会有很多种:账号密码登录手机号登录小程序一键登录那么我们登录的时候就要进行判断,设置一个type的属性,用于在登录时判断用户是什么类型的登录,这个由前端那边设定参数。type:1为账号密码登录,2为手机号登录,3为小程序一键登录作为判断,我们可以写一个查询表(枚举),在项目根目录创建一个lib目录,在里面创建一个enum.js文件。enum.js://通用判断是否存在该类型 function isThisType(val) { let flag = false; Obje...
koa教程2 密码加密
默认情况下,我们通过User.create({ email, password, nickname });模型传入的数据,是无加密的,也就是说,在数据库中密码是明文显示的,这样肯定是不行,我们需要使用bcrypt插件进行加密yarn add bcryptjs --dev打开models/user.jsuser.js:const bcrypt = require("bcryptjs"); const { sequelize } = require(`${process.cwd()}/core/db`); const { Sequelize, Model, DataTy...

koa教程2 使用sequelize连接数据库,创建用户模型
数据库的连接处理,我们需要使用sequelize插件。这个插件还有对应的数据库驱动,如果是使用mysql,那么驱动是mysql2,具体可以查看中文版的入门教程:入门yarn add sequelize mysql2 --dev安装完成后我们先需要创建数据库连接。连接数据库在根目录/core目录**下创建一个db.js文件用于连接数据库。在连接数据库之前,我们需要一些数据库的参数,如账号,密码,端口,数据库名字,数据库地址,为了方便管理配置,我们在根目录创建一个config目录,里面存放一config.js文件 ,这个之前的教程我们就已经创建好了。config.js:module.expo...
koa教程2 用户注册及Joi校验、成功信息返回
用户注册用户数据表搭建完毕后,我们肯定是需要注册用户的,那么首先我们需要创建一个路由api了。在根目录:/app/router/v1目录下创建一个user.js文件。user.js:const Router = require("koa-router"); const router = new Router({ prefix: "/v1/user" }); const { User } = require(`${process.cwd()}/models/user`); //注册用户 router.post("/register&q...

koa教程2 错误处理
错误捕获在koa中,我们的错误捕获是通过try...catch实现的,捕获的是next函数,在中间件中next就是下一个中间件触发回调,如果next运行出错了,那么就可以被try...catch捕获。但是,这种捕获,只能捕获同步形式的代码,如果有异步处理,那么try...catch就无法捕获到了。比如:try { setTimeout(() => { throw new Error("出错了") }, 1000) } catch (error) { console.log("error") };按道理,如果能捕获错误,那么...

koa教程2 参数 + 参数校验
传递参数的方式目前有四种传递参数的方式:请求地址段?xx=xx这种query参数body中携带参数header头信息携带参数那么koa是如何获取这四种参数呢?koa获取参数由于body参数获取比较复杂,我们需要安装一个中间件进行处理。yarn add koa-bodyparser --dev然后在app.js中引入并激活const Koa = require("koa"); const InitManager = require("./core/init"); const bodyParser = require("koa-bodypar...