前言
我们使用脚手架开发的时候,比如vue-cli webpack-dev-server vite,都会执行dev命令,启动一个服务器,点击显示的链接,就能打开页面进行开发
不例外,nuxt也有自己的脚手架nuxi,并且执行dev命令也会创建一个服务器,点击链接进入开发环境的页面
接下来,我们探究一下nuxt脚手架的页面服务器究竟是如何搭建的
阅读本文,你能学到:
- 原生node实现服务器
- nuxt服务器的相关工具包
- 手写一个类
nuxt的开发服务器
原生node实现服务器
说实话我没做过node后端开发,以下内容可能稍微欠缺火候,路过熟悉的大哥可以出来指正
我当前的node版本为:v18.14.2
首先创建一个index.mjs文件,并且指定端口为9999,根路径返回一个html
import { createServer } from 'node:http'
const server = createServer((req, res) => {
if (req.url === '/') {
res.setHeader('Content-Type', 'text/html')
res.end('<h1>hi, im http</h1>')
}
})
server.listen(9999, () => {
console.log('Server is running on: http://localhost:9999')
})
用node执行一下,打开链接就能看到效果
node index.mjs
打开http://localhost:9999,就能接收到Content-Type: text/html的内容
给服务器增加一个get接口请求,首先先改写页面内容,增加一个按钮,用于发起请求,并且给按钮绑定一个事件,点击的时候触发请求
const server = createServer((req, res) => {
if (req.url === '/') {
res.setHeader('Content-Type', 'text/html')
res.end(`\
<!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>Document</title>
</head>
<body>
<h1>hi, im http</h1>
<button id="button">get请求</button>
<script>
const button = document.getElementById('button')
button.addEventListener('click', () => {
const request = new XMLHttpRequest()
request.open('GET', 'http://localhost:9999/user')
request.send()
})
</script>
</body>
</html>
`)
}
// ...
})
然后添加get请求的路径,然后设定返回内容
const server = createServer((req, res) => {
// ...
if (req.url === '/user') {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
name: 'wu',
age: 18,
}))
}
})
我们运行一下看看
可以看到,页面多了一个get请求按钮,点击按钮的时候会触发get请求,并且返回设置好的响应内容
server也可以把请求进行分离,不需要在初始化server实例的时候把每个请求都列出来,我们添加一个post例子来尝试添加新请求处理
先添加post按钮和请求方法
const server = createServer((req, res) => {
if (req.url === '/') {
res.setHeader('Content-Type', 'text/html')
res.end(`\
<!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>Document</title>
</head>
<body>
<h1>hi, im http</h1>
<button id="get">get请求</button>
<button id="post">post请求</button>
<script>
const get = document.getElementById('get')
get.addEventListener('click', () => {
const request = new XMLHttpRequest()
request.open('GET', 'http://localhost:9999/user')
request.send()
})
const post = document.getElementById('post')
post.addEventListener('click', () => {
const request = new XMLHttpRequest()
request.open('POST', 'http://localhost:9999/post')
request.send(JSON.stringify({ // 这里带请求参数
name: 'wu',
}))
})
</script>
</body>
</html>
`)
}
// ...
})
然后为server添加post请求/post
server.on('request', (req, res) => {
if (req.url === '/post' && req.method === 'POST') {
const body = []
req.on('data', (chunk) => {
body.push(chunk)
}).on('end', () => {
console.log(Buffer.concat(body).toString()) // {"name":"wu"} 这里展示请求参数
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
name: 'wu',
age: 18,
}))
})
}
})
可以看到,我们页面添加了一个post请求按钮,并且为server实例添加了一个/post的接口
以上应该就是实现一个基础的原生node服务器的过程
nuxt服务器的相关工具包
看过这个系列的都知道,我讲解的nuxt拆包剖析,都离不开一个小团队unjs,这次也不例外地nuxt的开发服务器相关的工具包也来自unjs
unjs/h3unjs/listhen
unjs/h3
H3 is a minimal h(ttp) framework built for high performance and portability.
从介绍可以看出,h3就是一个高性能的http框架,nuxt的开发服务器就是使用该工具包实现
基本使用过程,首先当然是安装,我就不赘述了,新建一个文件h3.mjs
import { createServer } from 'node:http'
import { createApp, eventHandler, toNodeListener } from 'h3'
const app = createApp()
app.use('/', eventHandler(() => '<h1>hi, im h3</h1>'))
createServer(toNodeListener(app)).listen(8888, () => {
console.log('Server is running on: http://localhost:8888')
})
执行node h3.mjs后效果
个人认为相对于原生node写法,结合h3的写法还是优雅一点的
同样的,我们也添加get和post接口请求,我们会使用到h3的createRoute方法,方便管理路由
const app = createApp()
const route = createRouter()
route.get('/', eventHandler(() => `\
<!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>Document</title>
</head>
<body>
<h1>hi, im http</h1>
<button id="get">get请求</button>
<button id="idget">带有id的get请求</button>
<button id="post">post请求</button>
<script>
const get = document.getElementById('get')
get.addEventListener('click', () => {
const request = new XMLHttpRequest()
request.open('GET', 'http://localhost:8888/user')
request.send()
})
const idget = document.getElementById('idget')
idget.addEventListener('click', () => {
const request = new XMLHttpRequest()
request.open('GET', 'http://localhost:8888/user/111')
request.send()
})
const post = document.getElementById('post')
post.addEventListener('click', () => {
const request = new XMLHttpRequest()
request.open('POST', 'http://localhost:8888/post')
request.send(JSON.stringify({
name: 'wu',
}))
})
</script>
</body>
</html>
`)).get('/user', eventHandler(() => ({
name: 'wu',
age: 18,
}))).get('/user/:id', eventHandler(event => ({
name: 'wu',
age: 18,
id: event.context.params.id,
}))).post('/post', eventHandler(async (event) => {
const body = await readBody(event)
console.log(body) // { name: 'wu' }
return {
age: 18,
...body,
}
}))
app.use(route)
createServer(toNodeListener(app)).listen(8888, () => {
console.log('Server is running on: http://localhost:8888')
})
我们关注几个部分
- 使用
createRoute创建路由,路由实例可以链式调用方法注册路由 - 对比于原生node的返回响应数据,我们使用
eventHandler能直接将响应数据return,从而精简代码 - 我们可以通过
:xxx的形式设置动态入参,对应的获取参数,则是event.context.params.xxx - 对于
post请求,我们需要使用方法readBody获取body入参,响应数据也是直接return - 更多的
h3方法可以查看h3 JSDocs
看下运行效果
点击三个按钮发出对应的请求,并且能按照我们的设定返回数据
unjs/listhen
An elegant HTTP listener.
上一节我们构造了一个服务器,并且通过node的createServer运行起来,但输出的内容有点简略
unjs团队也提供了一个工具包unjs/listhen用来运行服务,并且功能还更齐全
我们使用上面的例子,改写一下使用listhen运行
import { listen } from 'listhen'
// ...
app.use(route)
// createServer(toNodeListener(app)).listen(8888, () => {
// console.log('Server is running on: http://localhost:8888')
// })
listen(toNodeListener(app), {
port: 8888,
showURL: true,
open: false,
clipboard: true,
})
这个图是node运行的样式
这个图是listhen运行的样式
对比一下可以看出,listhen运行的样式会更清晰,并且通过第二个参数可以进行一些控制,比如
- port:控制端口
- showURL: 控制是否展示链接
- open: 控制运行后是否直接打开浏览器
- clipboard: 控制运行后是否将url拷贝到剪切板
还有很多属性,可以在文档中查看
手写一个类nuxt的开发服务器
通过上文,其实我们已经知道如何做一个开发服务器,只需要做一些调整,就可以变成nuxt服务器
-
第一步,创建一个文件
nuxt-demo.mjs,我们的操作都在这里 -
第二步,需要一个
nuxt脚手架,因为脚手架不在本文的研究范围,我们先简单的处理一下,在package.json中添加
"scripts": {
"dev": "node nuxt-demo.mjs",
}
这样执行pnpm dev的时候,就会运行nuxt-demo.mjs文件
- 第三步,一个loading页面
我们在运行nuxt的时候,都会有一个loading页面,这个页面nuxt做成了一个集合包:nuxt/assets,其中loading页面的内容在这里
准备工作都好了,下面我先把完整代码贴出来
import { loading } from '@nuxt/ui-templates'
import { listen } from 'listhen'
import { createApp, eventHandler, toNodeListener } from 'h3'
// 第一部分
const app = createApp()
app.use('/', eventHandler(() => '<h1>hi, im nuxt</h1>'))
// 第二部分
const loadingListener = (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.statusCode = 503
res.end(loading({ loading: 'Starting' }))
}
let currentListener
const serverHandler = (req, res) => {
return currentListener ? currentListener(req, res) : loadingListener(req, res)
}
// 第三部分
const listhener = await listen(serverHandler, {
port: 8888,
showURL: false,
open: false,
clipboard: true,
})
// 第四部分
function initNuxt() {
const nuxtInstance = {
ready: () => {
// ! nuxt的准备过程,先用setTimeout模拟
setTimeout(() => {
// 创建真正的开发服务器
currentListener = toNodeListener(app)
}, 5000)
},
}
listhener.showURL() // 执行到这里,展示url
return nuxtInstance
}
// 这里进行nuxt初始化
const currentNuxt = initNuxt()
currentNuxt.ready()
第一部分,创建h3实例,绑定一个根路径,返回页面内容<h1>hi, im nuxt</h1>
第二部分,准备监听方法currentListener,如果currentListener为空,则使用loadingListener,其中loadingListener则直接返回nuxt/assets中的loading模板,也就是我们在启动nuxt时看到的页面
第三部分,创建listhen实例,注意我这里showURL属性是false,也就是在执行的时候并不会马上显示url
第四部分,我们创建一个initNuxt方法,用户创建nuxt实例,其实nuxt实例就是一个对象,里面有一个ready属性,用于做一些nuxt的准备工作,比如生成.nuxt目录等
在创建完nuxt实例之后,才展示url,调用listhener.showURL()方法,到这步才会显示url,这样做的目的应该是减少用户看到loading页的时间
在最后,执行currentNuxt.ready(),准备nuxt环境,比如说给当前监听方法currentListener赋值,读取工程目录文件生成.nuxt目录等操作,我们就使用一个setTimeout来模拟等待的过程
我们来看下执行效果
总结
文章简单地实现了原生node服务器,和使用unjs工具包实现服务器,最后再根据nuxt的处理,模拟了一个开发服务器
本文所有代码在github仓库
这篇文章写的有点久,主要是不熟悉服务器的开发步骤,并且unjs的文档写的比较简约,最后还是通过看nuxt和nitro源码才完成的,感觉还是一种进步吧