使用 koa2 搭建服务器
关于前后端路由区别:浅谈前后端路由与前后端渲染
目前,我在搭建一个包含前后端全套打包方案的工程时,碰到了一些问题,也学到了很多,在此记录下来,防止遗忘。
准备
初始化工程
npm init -y
我这里使用 koa 来搭建服务器,之后会尝试用原生 nodejs 去实现一遍。
安装 koa
npm install koa --save
搭建基本服务器
新建我们的服务器文件并写入以下内容:
server.js
const Koa = require('koa');
const app = new Koa();
const port = 3000;
app.use(async (ctx, next) => {
ctx.body = 'hello world!';
});
app.listen(3000);
console.log(`\nserver is start at port ${port}...\n`);
- app.use() 方法可以传入一个回调函数,所有请求都会经过 app.use 回调函数处理。这个回调函数也叫中间件,可以直接写在 app.use() 里,也可以拆分成一个独立函数传入 app.use()。
- ctx 是一个上下文对象,包含一次请求的所有信息,包括 response 和 request 对象。
- app.listen() 监听一个端口。
现在我们打开浏览器输入地址 ‘localhost:3000’,就可以看到我们的 ‘hello world’ 了。
静态资源访问
在单页应用中,我们希望前端去处理路由请求,即根据不同路由去渲染不同页面。当浏览器访问一个路径时,如果是前端路由,此时后端返回给前端的应该是 index.html,我们需要服务器支持这种请求。
安装 koa-static
npm install koa-static --save
server.js
const Koa = require('koa');
const path = require('path');
const serve = require('koa-static');
const app = new Koa();
const port = 3000;
const main = serve(path.join(__dirname), "dist"); // __dirname 指根目录
app.use(main);
app.listen(3000);
console.log(`\nserver is start at port ${port}...\n`);
注意:
这里有一个比较坑的地方,__dirname 是 nodejs 的一个变量,指的是根目录。我一直以为这个根目录指的是整个工程的根目录。
错!
这个根目录指的是被执行的文件所在的目录!
不一定是整个工程的的根目录!
我当前的目录结构为
nodejs-server-demo
├── backend
│ ├── server.js
│ dist
│ ├── index.html
│ └── bundle.js
└── package.json
根据我的目录结构,我的 server.js 中的 __dirname 指的就是 “/nodejs-server-demo/backend”,而不是 “/nodejs-server-demo”。
我的 index.html 文件放在 ./dist 文件夹下,所以这里我要修改路径为:
const serve = serve(path.join(__dirname, "../dist"));
现在,我们的服务器支持静态资源访问了,打开浏览器,输入 “localhost:3000” 和 “localhost:3000/bundle.js” 查看效果吧。
注意:
我们给 koa-static 中间件传入了一个目录,koa-static 中间件默认返回该目录下的 “index.html” 文件并返回。
如果想验证,我们可以将 ‘app.use(main)’ 注释掉,来看看效果,我们会发现 “localhost” 将返回 ‘not found’。
证明我们的想法是正确的。
路由
要实现路由 ,我们需要借助 “koa-router”。
安装
npm install koa-router --save
server.js
const Koa = require('koa');
const path = require('path');
const serve = require('koa-static');
const router = require('koa-router')(); // 注意 router 的引入方式
const app = new Koa();
const port = 3000;
app.use(serve(path.join(__dirname, "../dist")));
router.get('/about', async (ctx, next) => {
ctx.type = 'json';
ctx.body = {
status: 0,
data: {
name: "jaakko",
age: 21
}
};
await next();
}) // 处理 get 请求 '/about',返回 json 数据
app.use(router.routes()); // 注意 router 使用方式
app.listen(3000);
console.log(`\nserver is start at port ${port}...\n`);
我们现在引入了路由处理,新增了一个 ‘/about’ 的 get 请求路由。
打开浏览器,输入 ‘localhost:3000/about’,我们可以看到我们刚才设置的 json 数据。
区分前后端路由
这部分内容,参考 浅谈前后端路由与前后端渲染,这篇文章写的很详细。
路由歧义
想象这样一个场景:我们某个前端的页面的路由为 ‘/about’,我们还有一个 get 请求路由也为 ‘about’。那么当我们在浏览器地址栏输入 ‘localhost:3000/about’ 时,应该当成前端路由处理还是 get 请求处理?
为了避免这种歧义,我们应该将前后端路由区分开。
前端路由与前端渲染
此时,我们现在在浏览器输入 ‘localhost:3000/about’,会显示 not found。为什么呢?
浏览器访问 ‘localhost:3000/info’ 会发送一个 get 请求,我们的服务器现在没有 ‘/info’ 这个路由,所以返回 not found。
我们通过各种前端框架如 vue、react 的 router 库,去访问页面的路由(假定我们在 router 注册了这个页面,如 ‘localhost:3000/info’)时,为什么不会发送 get 请求呢。是因为它们的实现是通过 h5 history 实现的,可以往地址栏输入一个路径但是不真正地去访问它。这种情况是我们通过点击页面中某些按钮来跳转路由。
如果我们直接在地址栏输入 ‘localhost:3000/info’,还是会返回 ‘not found’,原因正如上文提到,我们通过地址栏去访问这个地址,实际上是发送了一个 get 请求,而服务器 并没有对这个路由进行处理,故返回 ‘not found’。通过 h5 history 去改变路由,可以允许我们改变地址但不发送请求。
服务端解决
我们要解决以上问题要保证两点:
- 前后端不要使用相同路由
- 后端在路由无响应时,返回给前端静态页面 ‘index.html’
1. 前后端不要使用相同路由
解决这个问题,我们的做法是:在向服务器请求数据的接口前统一加上 ‘/api’。
那我们刚才的 get 请求完整路径就应该为 ‘localhost:3000/api/about’
2. 后端路由无响应时返回 ‘index.html’
server.js
const Koa = require('koa');
const path = require('path');
const serve = require('koa-static'); // 静态资源操作
const router = require('koa-router')(); // 注意 router 的引入方式
const fs = require('fs'); // 文件操作
const app = new Koa();
const port = 3000;
const main = serve(path.join(__dirname, "../dist"));
app.use(main);
app.use(async (ctx, next) => {
// 如果路由以 '/api' 开头,进入路由匹配; 否则返回 'index.html'
if ((/^\/api/.test(ctx.url))) {
return next();
}
ctx.type = "html";
ctx.body = fs.createReadStream(path.join(__dirname, "../dist", "index.html"));
await next();
})
router.get('/api/about', async (ctx, next) => {
ctx.type = 'json';
ctx.body = {
status: 0,
data: {
name: "jaakko",
age: 21
}
};
await next();
})
app.use(router.routes()); // 注意 router 使用方式
app.listen(3000);
console.log(`\nserver is start at port ${port}...\n`);
至此,一个简单的 nodejs-koa 服务器就完成了,功能方面还有一些缺陷,我们现在来优化一下。
优化
koa-static
koa-static 方法传入第二个参数可以设置参数,具体选项参考 “这里”。我在这里介绍两个我认为较有用的选项。
1. index
index 选项设置默认返回的文件名,该选项默认值为 “index.html”。上文中我有提到 koa-static 默认返回 “index.html”,就是由于这个设置项的默认值。假如我们希望返回文件名不为 “index.html” 的默认文件,可以手动设置该选项的值。
2. defer
defer 选项设置是否延迟执行 koa-static 中间件。如果该选项为 true,服务器将不返回默认文件,而是先执行之后的中间件。
在我们的服务器中,如果该选项设置为 true,此时再访问就会出错。因为请求会跳过 koa-static 中间件,优先执行 koa-static 之后的中间件。
在 koa-static 之后的第一个中间件,就是下面这个中间件:
app.use(async (ctx, next) => {
// 如果路由以 '/api' 开头,进入路由匹配; 否则返回 'index.html'
if ((/^\/api/.test(ctx.url))) {
return next();
}
ctx.type = "html";
ctx.body = fs.createReadStream(path.join(__dirname, "../dist", "index.html"));
await next();
})
由上面这段代码我们可以看到,当路由不匹配后端路由(由 ‘/api’ 开头的路由)时,它会对所有的请求都返回 “index.html” 文件,这样肯定有问题。
正确的做法是:判断一下浏览器请求的文件类型,并返回相应文件,而不是统一返回 “index.html”。
server.js
const Koa = require('koa');
const path = require('path');
const serve = require('koa-static'); // 静态资源操作
const router = require('koa-router')(); // 注意 router 的引入方式
const fs = require('fs'); // 文件操作
const app = new Koa();
const port = 3000;
app.use(require('koa-static')('dist', { index: 'index.html', defer: true }));
app.use(async (ctx, next) => {
await next();
})
app.use(async (ctx, next) => {
// 如果路由以 '/api' 开头,进入路由匹配; 否则返回 'index.html'
if ((/^\/api/.test(ctx.url))) {
return next();
}
// 通过 ctx.url 我们可以获取浏览器请求的文件
const fileName = (/\.html|\.js$/.test(ctx.url));
// 如果浏览器请求的文件为 html、js 类型,就返回相应类型,否则返回 index.html
if (fileName) {
// 将路由前的 '/' 去掉,并返回 dist 目录下相应文件
ctx.body = fs.createReadStream(path.join(__dirname, "../dist", ctx.url.replace(/^\//, '')));
} else {
ctx.type = "html";
ctx.body = fs.createReadStream(path.join(__dirname, "../dist", "index.html"));
return next();
}
})
router.get('/api/about', async (ctx, next) => {
console.log('get in /about router');
ctx.type = 'json';
ctx.body = {
status: 0,
data: {
name: "jaakko",
age: 21
}
};
await next();
})
app.use(router.routes()); // 注意 router 使用方式
app.listen(3000);
console.log(`\nserver is start at port ${port}...\n`);
现在我们的服务器已经可以满足基本使用需求了,但是我们还可以进一步优化。
我们现在实现了根据不同类型文件请求返回相应的文件,但是当我们的服务器支持多种文件资源请求(如还有 jpg, png, gif 等)时,我们需要手动判断是否为类型,是在有些不便,那么,还有没有更优雅的方式来实现呢?当然,有空再说…
总结
通过搭建这个 nodejs 服务器,我对前后端交互过程有了更进一步了解,也终于弄清楚了前后端路由的区别。
学习是一个从渐悟到顿悟的过程,这个过程可能是痛苦的,但是一旦我们到达了顿悟的点,这一切的痛苦都是值得的。