首页 vite preview 实现原理
文章
取消

vite preview 实现原理

最近在查看vue3源码的时候,突发奇想,想去了解一下 vue代码的运行环境,具体怎么执行的,vite dev\build\preview 都做了什么,由于 preview是预览 build打包好的项目文件的,考虑到会比较简单一点,所以先从vite preview 指令开始,看它的运行原理

我先不看代码,大概设想一下,是个本地开的静态服务,和我本地的nginx 类似的。接着看源码,

  1. 查看package.json文件,找到 bin 属性,看到指令执行文件是 bin/vite.js

然后vite.js代码我发现接着的指令生成文件 vite/src/node/cli.ts

  1. vite指令的是由cac实现的. 搜索preview查看到vite 发现preview代码内部实现文件在 vite/src/node/preview.ts,

看源码要尽量学会化繁为简,抽离到最小系统去看哈

在preview.ts文件的 preview 函数里面,就是vite preview的内部实现逻辑,我发现,它是基于connect.js 中间件框架实现的,

1
2
3
4
5
6
7
  const app = connect();
  // app: function app(req, res, next){ app.handle(req, res, next); } 
  const httpServer = await resolveHttpServer(
    config.preview,
    app,
    await resolveHttpsConfig(config.preview?.https),
  )

化繁为简哈,由于我们直接 vite preview 就开启了预览服务,看代码我们也不加任何参数,看基本实现。
app是中间件应用,是一个函数哈,httpServer 是写在另一个文件里面的,文件多,看着就很绕,不必要的代码忽视他们,按最基本的代码逻辑去看。不知道 connect 的可以去了解了解,很简单哈,

Connect是一个可扩展的HTTP服务器框架,用于使用被称为中间件的“插件”的节点。

不要想的太复杂哈,接着我们看httpServer 所在的文件,哦,是resolveHttpServer,在http.ts文件里,如下:

看到了吧,我们还是按照最基本的逻辑去看,其他多余的逻辑不看,resolveHttpServer里面,使用的是node的api,createServer; 参数是 app, 我们回头复习一下,createServer的参数是一个function ,现在是 app这个变量,即connect(),

1
2
3
4
5
6
const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html');
  res.setHeader('X-Foo', 'bar');
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('ok');
}); 

接着我们再回到preview.ts主文件接着往下看:

看到了吧,connect的use方法,中间件一个一个的去连接使用,这就像装饰器,这就像链式调用,这就像错误拦截,一层一层的,每一个中间件都执行了对响应的一种功能要求和处理逻辑,通过next()连接下去。corsMiddleware proxyMiddleware compression viteAssetMiddleware htmlFallbackMiddleware 等等。 具体connect 可看 connect

我们还是化繁为简,我们只看最基础的 preview怎么实现的,我们接着看到最后,按照connect的逻辑,找到server.listen, 在preview.ts文件中的 preview指令最后调用了 httpServerStart方法,该方法也在 http.ts文件里面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export async function httpServerStart(
  httpServer: HttpServer,
  serverOptions: {
    port: number
    strictPort: boolean | undefined
    host: string | undefined
    logger: Logger
  },
): Promise<number> {
  let { port, strictPort, host, logger } = serverOptions

  return new Promise((resolve, reject) => {
    const onError = (e: Error & { code?: string }) => {
      if (e.code === 'EADDRINUSE') {
        if (strictPort) {
          httpServer.removeListener('error', onError)
          reject(new Error(`Port ${port} is already in use`))
        } else {
          logger.info(`Port ${port} is in use, trying another one...`)
          httpServer.listen(++port, host)
        }
      } else {
        httpServer.removeListener('error', onError)
        reject(e)
      }
    }

    httpServer.on('error', onError)

    httpServer.listen(port, host, () => {
      httpServer.removeListener('error', onError)
      resolve(port)
    })
  })
}

如上所示,我们看完了整个preview内部实现流程,其实就是一个node服务,基于connect中间件框架,中间加了各种服务请求处理的中间件,最后监听服务,然后打开浏览器访问。

那我们是怎么指定dist文件并访问打包的项目的呢?

在vite preview里面使用了一个关键的中间件 viteAssetMiddleware它指向的是 sirv(xx…):

sirv这个单词不好理解,我的理解是 serve,谐音词,sirv.js 执行之后返回的也是一个 function mid(req,res,next){} 中间件,它的作用是加载静态资源的,访问静态资源,相关的各种全面的响应处理, sirv。具体实现可以预览如下:

代码里可以看到,核心到最后是 send(req, res, data.abs, data.stats, data.headers);我们再看一下 send方法里面的实现逻辑:

最终它的逻辑还是回到了
res.writeHead(code, headers);
fs.createReadStream(file, opts).pipe(res);

综上所述,vite preview 就是开了一个node createServer ,基于connect.js中间件框架,使用了sirv静态服务处理中间件,然后监听端口,访问页面。中间还加了各种之上的中间件。

下面是我简化使用的代码:

1.vite里面简化的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import http from 'node:http'
import fs from 'node:fs';
import connect from 'connect'
import sirv from 'sirv'

const distDir = ('./sirv-test-dist')
const si = sirv(distDir, {
  etag: true,
  dev: true,
  extensions: [],
  ignores: false,
})

console.log(String(si),'si---')
const app = connect();
const server = http.createServer(app)
app.use(si)

server.listen('3032')
console.log(server,'server--','http://127.0.0.1:3032')

2.使用最原始的createServer 模仿一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 import http from 'node:http'
import fs from 'node:fs';
import sirv from 'sirv'
import { parse } from '@polka/url';

const distDir = ('./sirv-test-dist')
const si = sirv(distDir, {
  etag: true,
  dev: true,
  extensions: [],
  ignores: false,
})
const server = http.createServer((req, res) => {
  // one  中间件可以在createServer里面直接执行的,可以不携带next,
  //在connect里面是当中间件使用,在这里直接使用也是可以通过的
  // si(req,res);


  // two
  if(req.method=='GET'&&req.url=='/index.html'){
    console.log(req)
    res.writeHead(200, { 'Content-Type': 'text/html' });

    fs.createReadStream('./dist/index.html').pipe(res)
  }else if(req.method=='GET'){
    let mimeType = req.url.split('.')[1]
    let ms={
      js:'javascript',
      css:'css',
      ico:'ico'
    }
    console.log(mimeType,'mmmm====')
    if(mimeType){
      res.writeHead(200,{ 'Content-Type': `text/${ms[mimeType]}` });
      fs.createReadStream('./dist'+req.url).pipe(res) 
    }
  }
});

server.listen(3033)

console.log(server,'server--','http://127.0.0.1:3033')

all in all,以上基本阅读了vite preview 的实现逻辑,也自行模仿了一下,换个原始方式写了一下,
但是模仿的毕竟考虑的综合细节因素不全面,还是使用人家写好的比较好。

查看源码就像 开锁 破解密码 拆解飞机构造等等,有的简单,有的非常难,这不是代码本身的,而是里面的逻辑很复杂,而且代码各种抽离,你要翻山越岭,俄罗斯套娃一般去理清逻辑,挺不简单的,有的时候适当浅尝辄止也好的,毕竟人家花费几年时间、精力才写好的,交付给应用开发人员的阶段性产品,我们可以按文档使用,可以窥探源码实现原理,但是还要量力而行,以实际组合应用为主,社会分工各个环节不同,没精力掌握所有环节。本人最近在换工作,求机会哈