Vue
和
React
开发的同学对于
ajax
请求数据前端渲染应该是不陌生的,这里先从
node+koa
后端模板引擎渲染
.html
说起,可以更好的理解如何从后端渲染过渡到现在的主流
ajax
请求+前端框架的开发形式的转变,更能理解
ajax
请求的好处到底在哪,毕竟
ajax
是后期发展起来的,它并不是一开始就有的,既然发展起来并得到了如此广泛的应用,一定是因为它解决了重要的难题。
npm init -y
// app.js
const Koa = require('koa') // 包装过的http
const KoaStaticCache = require('koa-static-cache') // 静态资源中间件
const Router = require('koa-router')
const co = require('co')
const Render = require('koa-swig') // 模板引擎
const path = require('path')
const bodyParser = require('koa-bodyparser') // 处理post请求
const app = new Koa() // 创建服务器
/* 处理静态资源:任何请求,首先通过 KoaStaticCache 中间件处理看是否是静态资源请求 */
app.use( KoaStaticCache(__dirname + '/static', {
prefix: '/public' // 如果当前请求 url 是以 /public 开始的,则作为静态资源请求,映射到我服务器上的 /static 路径中的文件
}) )
/* 处理请求正文中的数据(处理post请求参数)*/
app.use(bodyParser())
/* 服务器重启会重置数据,所以可以写成一个本地文件,对文件进行增删改查(类似数据库的功能了) */
let datas = {
maxId: 3,
appName: 'TodoList',
skin: 'index.css',
tasks: [
{id: 1, title: '测试任务1', done: true},
{id: 2, title: '学习koa', done: false},
{id: 3, title: '学习sql', done: false},
]
}
/* 设置模板引擎 */
app.context.render = co.wrap(Render({
root: path.join(__dirname, 'views'),
autoescape: true, // 数据是否编码(html)
cache: false,
// cache: 'memory', // 内存中缓存模板,下次访问就直接从内存中读取模板
ext: 'html'
}))
/* router.routes() 也相当于一个中间件,请求 url 会经过我们定义的路由进行过滤 */
const router = new Router()
/* 首页,模板引擎中设置了文件内容为views文件夹,所以 / 会去找 views/index.html文件 */
router.get('/', async ctx => {
ctx.body = await ctx.render('index.html', {datas})
})
/* 添加 */
router.get('/add', async ctx => {
ctx.body = await ctx.render('add.html', {datas})
})
router.post('/posttask', async ctx => {
let cur_title = ctx.request.body.title || ''
if(cur_title) {
datas.tasks.unshift({
id: ++datas.maxId,
title: cur_title,
done: false
})
ctx.body = await ctx.render('message', {
msg: '添加成功',
href: '/'
})
} else {
ctx.body = await ctx.render('message', {
msg: '请输入任务标题',
href: 'javascript:history.back()'
})
}
})
/* 改变 */
router.get('/change/:id', ctx => {
let cur_id = ctx.params.id
datas.tasks.forEach(task => {
if(task.id * 1 === cur_id * 1) {
task.done = !task.done
}
})
ctx.response.redirect('/')
})
/* 删除 */
router.get('/remove/:id', async ctx => {
let cur_id = ctx.params.id
datas.tasks = datas.tasks.filter(task => task.id * 1 !== cur_id * 1)
ctx.body = await ctx.render('message', {
msg: '删除成功',
href: '/'
})
})
app.use(router.routes())
app.listen(80, () => {
console.log('启动成功...运行在http://localhost:80')
})
DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/public/{{datas.skin}}">
head>
<body>
<h1>{{datas.appName}}h1>
<a href="/add">添加新任务a>
<hr />
<ul>
{% for task in datas.tasks %}
{% if task.done %}
<li class="done">
<input type="checkbox" checked onclick="change({{task.id}})" />
[{{task.id}}] - {{task.title}}
<a href="/remove/{{task.id}}">删除a>
li>
{% else %}
<li>
<input type="checkbox" onclick="change({{task.id}})" />
[{{task.id}}] - {{task.title}}
<a href="/remove/{{task.id}}">删除a>
li>
{% endif %}
{% endfor %}
ul>
<script>
function change(id) {
window.location.href = '/change/' + id
}
script>
body>
html>
添加任务的模板就是原生表单
<form action="/posttask" method="POST">
<input type="text" name="title" />
<button>添加button>
form>
模板引擎中引入了样式文件,通过服务端的配置,会去 static
路径下去查找
// app.js
app.use( KoaStaticCache(__dirname + '/static', {
prefix: '/public' // 如果当前请求 url 是以 /public 开始的,则作为静态资源请求,映射到我服务器上的 /static 路径中的文件
}) )
开发 node 程序时,调试过程中每修改一次代码都需要重新启动服务才生效,这是因为 NodeJS 只有在第一次引用到某部分时才会去解析脚本文件,以后都会直接访问内存,避免重复载入和解析,这种设计有利于提高性能,但是却并不利于开发调试,为了每次修改后都能实时生效,安装一个辅助依赖 supervisor
会监视代码的改动重启NodeJS服务。
npm i supervisor
// package.json
"scripts": {
"supervisor": ".\\node_modules\\.bin\\supervisor app"
},
npm run supervisor
启动成功后,访问 http://localhost/
以上就是通过后端模板引擎来渲染页面的方式,个人认为明显的缺陷有:
下面尝试使用 Vue + ajax
的形式改写以上功能
// app.js
const Koa = require('koa')
const KoaStaticCache = require('koa-static-cache')
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const fs = require('fs')
const app = new Koa()
// 这里的数据改用本地读取的方式来mock
let datas = JSON.parse(fs.readFileSync('./data/data.json'))
/* 静态资源托管 */
app.use( KoaStaticCache(__dirname + '/static', {
prefix: '/public',
gzip: true
}) )
/* 处理body解析 */
app.use(bodyParser())
/* 路由 */
const router = new Router()
router.get('/', async ctx => {
ctx.body = 'hello, this is a test!'
})
router.get('/todos', async ctx => {
ctx.body = {
code: 0,
result: datas.todos
}
})
router.post('/add', async ctx => {
let title = ctx.request.body.title || ''
if(!title) {
ctx.body = {
code: 1,
result: '请传入任务标题'
}
} else {
let newTask = {
id: ++datas._id,
title,
done: false
}
datas.todos.unshift(newTask)
ctx.body = {
code: 0,
result: newTask
}
fs.writeFileSync('./data/data.json', JSON.stringify(datas))
}
})
router.post('/toggle', async ctx => {
let id = ctx.request.body.id * 1 || 0
if(!id) {
ctx.body = {
code: 1,
result: '请传入id'
}
} else {
let todo = datas.todos.find(todo => todo.id * 1 === id)
todo.done = !todo.done
ctx.body = {
code: 0,
result: '修改成功'
}
fs.writeFileSync('./data/data.json', JSON.stringify(datas))
}
})
router.post('/remove', async ctx => {
let id = ctx.request.body.id * 1 || 0
if(!id) {
ctx.body = {
code: 1,
result: '请传入id'
}
} else {
datas.todos = datas.todos.filter(todo => todo.id * 1 !== id)
ctx.body = {
code: 0,
result: '删除成功'
}
fs.writeFileSync('./data/data.json', JSON.stringify(datas))
}
})
app.use(router.routes())
app.listen(80, () => {
console.log('启动成功...')
})
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<link rel="stylesheet" href="/public/css/index.css">
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14">script>
head>
<body>
<div id="app">
<h1>TotoListh1>
<div>
<input type="text" v-model="newTask" />
<button @click="add">提交button>
div>
<hr />
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" :checked="todo.done" @click.prevent="toggle(todo.id)" />
<span>{{todo.title}}span>
<button @click="remove(todo.id)">删除button>
li>
ul>
div>
<script>
new Vue({
el: '#app',
data: {
newTask: '',
todos: []
},
created() {
fetch('/todos').then(r => {
return r.json()
}).then(res => {
let {code, result} = res
if(code * 1 === 0) {
this.todos = result
}
})
},
methods: {
remove(id) {
fetch('/remove', {
method: 'post',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({id})
}).then(r => {
return r.json()
}).then(res => {
let {code, result} = res
if(code * 1 === 0) {
this.todos = this.todos.filter(todo => todo.id * 1 !== id * 1)
} else {
alert(result)
}
})
},
add() {
fetch('/add', {
method: 'post',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({title: this.newTask})
}).then(r => {
return r.json()
}).then(res => {
let {code, result} = res
if(code * 1 === 0) {
this.todos.unshift(result)
this.newTask = ''
} else {
alert(result)
}
})
},
toggle(id) {
fetch('/toggle', {
method: 'post',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({id})
}).then(r => {
return r.json()
}).then(res => {
let {code, result} = res
if(code * 1 === 0) {
let todo = this.todos.find(todo => todo.id * 1 === id * 1)
todo.done = !todo.done
alert(result)
} else {
alert(result)
}
})
}
}
})
script>
body>
html>
是不是很熟悉了?Vue+ajax
就这样应用了
跟上面的不同,这里的index.html
不再是需要后端渲染的模板引擎了,可以直接丢到服务器上运行的文件
node app.js
启动成功后,访问 http://localhost/public/index.html,静态资源托管还是没变,访问 /public
会映射到 /static
目录下。
路径是没变的,对数据的操作都是通过 ajax
请求局部改变数据部分内容的,不需要对整个页面重刷新。
以上内容代码可以去这里自行clone。