前言
我们使用脚手架开发的时候,比如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')})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') })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.mjsnode index.mjsnode 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>`)}// ...})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> `) } // ... })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,}))}})const server = createServer((req, res) => { // ... if (req.url === '/user') { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ name: 'wu', age: 18, })) } })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>`)}// ...})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> `) } // ... })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,}))})}})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, })) }) } })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/h3
unjs/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')})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') })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')})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') })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,})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, })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",}"scripts": { "dev": "node nuxt-demo.mjs", }"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 = 503res.end(loading({ loading: 'Starting' }))}let currentListenerconst 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() // 执行到这里,展示urlreturn nuxtInstance}// 这里进行nuxt初始化const currentNuxt = initNuxt()currentNuxt.ready()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()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
源码才完成的,感觉还是一种进步吧