11期之一 - Vue-dev-server源码解析(一)

156 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

本文是对Vue-dev-server的学习整理,源码仓库地址:vue-dev-server

该仓库已由所有者存档。 它现在是只读的。

vue-dev-server 是什么

一个 POC(proof of concept) 开发服务器,允许您通过原生 ES 模块导入来导入 `*.vue` 文件。

This is a proof of concept.

Imagine you can import Vue single-file components natively in your browser... without a build step.

这是一个概念证明。

 想象一下,您可以在浏览器中本地导入 Vue 单文件组件……无需构建步骤。

怎么运作

  • 浏览器请求导入作为原生 ES 模块导入 - 没有捆绑。 
  •  服务器拦截对 *.vue 文件的请求,即时编译它们,然后将它们作为 JavaScript 发送回来。
  •  对于提供在浏览器中工作的 ES 模块构建的库,只需直接从 CDN 导入它们。
  •  对 .js 文件(仅包名)内的 npm 包的导入会动态重写,以指向本地安装的文件。 目前,仅支持 vue 作为特例。 其他包可能需要转换为以原生浏览器为目标的 ES 模块。

对于上面一段话,可以不用完全懂,可以先结合代码理解

package.json

{
  "name": "@vue/dev-server",  "version": "0.1.1",  "description": "Instant dev server for Vue single file components",  "main": "middleware.js",  "bin": {    "vue-dev-server": "./bin/vue-dev-server.js"  },  "scripts": {    "test": "cd test && node ../bin/vue-dev-server.js"  },  "author": "Evan You",  "license": "MIT",  "dependencies": {    "@vue/component-compiler": "^3.6.0",    "express": "^4.16.4",    "lru-cache": "^5.1.1",    "recast": "^0.17.3",    "validate-npm-package-name": "^3.0.0",    "vue": "^2.6.8",    "vue-template-compiler": "^2.6.8"  }}

package.json常用的配置属性:

main

"main": "middleware.js",

定义了npm包的入口文件middleware.js

bin

"bin": { "vue-dev-server": "./bin/vue-dev-server.js" },

bin属性用来将可执行文件加载到全局环境中,指定了bin字段的npm包,一旦在全局安装,就会被加载到全局环境中,可以通过别名来执行该文件。

比如@vue/dev-server的npm包:

"bin": {    "vue-dev-server": "./bin/vue-dev-server.js"},

一旦在全局安装了@vue/dev-server,就可以直接通过vue-dev-server来执行相应的命令,比如

npm i @vue/dev-server
npx vue-dev-server

如果非全局安装,那么会自动连接到项目的node_module/.bin目录中。

script

"scripts": {    "test": "cd test && node ../bin/vue-dev-server.js"},

npm 允许在package.json文件里面,使用scripts字段定义脚本命令。

上面代码是package.json文件的一个片段,里面的scripts字段是一个对象。它的每一个属性,对应一段脚本。比如,test命令对应的脚本是cd test && node ../bin/vue-dev-server.js。

命令行下使用npm run 或者yarn 命令,就可以执行这段脚本。

yarn test
# 等同于
cd test && node ../bin/vue-dev-server.js

怎么使用?

在一个目录中,创建一个 index.html:

<div id="app"></div>
<script type="module">
import Vue from 'https://unpkg.com/vue/dist/vue.esm.browser.js'
import App from './App.vue'

new Vue({
  render: h => h(App)
}).$mount('#app')
</script>

在 App.vue 中:

<template>
  <div>{{ msg }}</div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hi from the Vue file!'
    }
  }
}
</script>

<style scoped>
div {
  color: red;
}
</style>

然后:

npm i @vue/dev-server
npx vue-dev-server

vue-dev-server.js

#!/usr/bin/env node
const express = require('express')
const { vueMiddleware } = require('../middleware')
// 创建一个 Express 应用程序
const app = express()
// 当前工作目录
const root = process.cwd();// 中间件 这是处理文件的核心
app.use(vueMiddleware())
// 传递一个包含静态资源的目录给express.static中间件用于立即开始提供文件
// 这里的root是test目录 
app.use(express.static(root))// 我们的服务http://localhost:3000会打到test的index.html中app.listen(3000, () => {  console.log('server running at http://localhost:3000')})

middleware.js

这里我直接copy了另外一篇文章,【若川视野 x 源码共读】第11期 | vue-dev-server的源码解读

接着,我们可以看到vueMiddleware这个中间件,这个是核心,对请求进行拦截,然后对具体请求的文件进行解析返回。后面再详细讲解。

大概的流程如下所示。

  1. client请求server
  2. server将请求会经过中间件。
  3. Middleware会根据文件类型进行解析并响应。

image.png

vueMiddleware function

核心代码

const vueMiddleware = (options = defaultOptions) => {
  return async (req, res, next) => {
    // vue 文件类型处理
    if (req.path.endsWith('.vue')) {
      // 拿到请求的path 生成key
      const key = parseUrl(req).pathname
      // 查看是否有缓存
      let out = await tryCache(key)
      // 没有缓存,则生成
      if (!out) {
        // Bundle Single-File Component        
        const result = await bundleSFC(req)
        out = result
        // LRU 设置缓存信息
        cacheData(key, out, result.updateTime)
      }      
      // 返回
      send(res, out.code, 'application/javascript')
    } else if (req.path.endsWith('.js')) { // .js 文件类型处理
      // 拿到请求的path 生成key
      const key = parseUrl(req).pathname
      // 查看是否有缓存
      let out = await tryCache(key)
      // 没有缓存,则生成
      if (!out) {
        // transform import statements
        // result : { filepath, source, updateTime } 读取js source
        const result = await readSource(req)
        // 对import的模块语句进行转换
        // 比如,import Vue from 'vue'; 转换:import Vue from '/__modules/vue';
        out = transformModuleImports(result.source)
        // LRU 设置缓存信息
        cacheData(key, out, result.updateTime)
      }
      // 返回      
      send(res, out, 'application/javascript')
    } else if (req.path.startsWith('/__modules/')) { // 对/__modules/开头的文件进行处理
      const key = parseUrl(req).pathname
      const pkg = req.path.replace(/^\/__modules\//, '')
      let out = await tryCache(key, false)
      // Do not outdate modules
      if (!out) {
        out = (await loadPkg(pkg)).toString()
        cacheData(key, out, false)
        // Do not outdate modules
      }
      send(res, out, 'application/javascript')
    } else {
      next()
    }
  }
}

总结

本文只对vue-dev-server梳理了下流程,核心代码vueMiddleware没有做过深解析,了解了中间件对请求处理,

通过package.json,了解了bin, script等属性,通过vue-dev-server.js源码,了解怎么搭建express应用程序实例,express.js中间件,并且现学现卖,最近把一个N年前的项目,用express跑起来了,很有成就感。

通过对middleware.js源码阅读,了解前端构建思想。