37

1 Introduction

Hello everyone, my name is . Recently organized source code reading activity , interested parties can add me to 1617fb3b3a60f1 participate. It has been more than two months to exchange and learn together and make progress together.

I wanted to learn the source code, strongly recommended before I wrote "learning Source overall architecture series" contains jQuery , underscore , lodash , vuex , sentry , axios , redux , koa , vue-devtools , vuex4 , koa-compose , vue-next-release , vue-this , create-vue more than ten Source code articles.

Recently organized source code reading activity , everyone learns the source code together. So all kinds of searches are worth learning for us, and the source code with few lines of code.

In the vuejs organization , I found the "toy vite" written by You Yuxi a few years ago
vue-dev-server , I found 100 lines of code, it is worth learning. So there is this article.

After reading this article, you will learn:

1. 学会 vite 简单原理
2. 学会使用 VSCode 调试源码
3. 学会如何编译 Vue 单文件组件
4. 学会如何使用 recast 生成 ast 转换文件
5. 如何加载包文件
6. 等等

2. What is the principle of vue-dev-server

vue-dev-server#how-it-works
There are four sentences in English on the README

I found that the Google Translate is still relatively accurate, so I just moved it intact.

  • The browser requests the import to be imported as a native ES module-no bundling.
  • The server intercepts requests for *.vue files, compiles them on the fly, and then sends them back as JavaScript.
  • For libraries built with ES modules that work in the browser, you only need to import them directly from the CDN.
  • The npm package (only the package name) imported into the .js file will be rewritten on the fly to point to the locally installed file. Currently, only vue is supported as a special case. Other packages may need to be converted before they can be exposed as local browser target ES modules.

You can also take a look at the vitejs document to understand the principle. The pictures in the document are very good.

Native-ESM

After reading this article, I believe you will have a deeper understanding.

3. Preparation

3.1 Clone the project

This article warehouse vue-dev-server-analysis, ask for a star^_^

# 推荐克隆我的仓库
git clone https://github.com/lxchuan12/vue-dev-server-analysis.git
cd vue-dev-server-analysis/vue-dev-server
# npm i -g yarn
# 安装依赖
yarn

# 或者克隆官方仓库
git clone https://github.com/vuejs/vue-dev-server.git
cd vue-dev-server
# npm i -g yarn
# 安装依赖
yarn

Generally speaking, we look at the source code first from the package.json file:

// vue-dev-server/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 文件夹,再用 Node 执行 vue-dev-server 文件
    "test": "cd test && node ../bin/vue-dev-server.js"
  }
}

According to the scripts test command. Let's look at the test folder.

3.2 test folder

vue-dev-server/test folder, and the code is not long.

  • index.html
  • main.js
  • text.vue

As shown in the figure below.

test文件夹三个文件

Then we found the vue-dev-server/bin/vue-dev-server.js file, the code is not long.

3.3 vue-dev-server.js

// vue-dev-server/bin/vue-dev-server.js
#!/usr/bin/env node

const express = require('express')
const { vueMiddleware } = require('../middleware')

const app = express()
const root = process.cwd();

app.use(vueMiddleware())

app.use(express.static(root))

app.listen(3000, () => {
  console.log('server running at http://localhost:3000')
})

It turned out that express started the service 3000 The focus is on vueMiddleware middleware. Then let's debug this middleware.

Given that it is estimated that many small partners have not used VSCode debugging, here is how to debug the source code in detail. learning to debug the source code, the source code is not as difficult as .

3.4 Debug the project with VSCode

app.use(vueMiddleware()) vue-dev-server/bin/vue-dev-server.js file has a breakpoint.

Find vue-dev-server/package.json of scripts , move the mouse to the test command, there will be running script and debugging script commands. As shown in the figure below, select the debug script.

调试

VSCode 调试 Node.js 说明

Click the enter the function (F11) button to enter the vueMiddleware function. If you find that the breakpoint is in a file that is not in this project, and you don’t want to see it, you can quit or start over . can use the browser incognito (privacy) mode (shortcut Ctrl + Shift + N to prevent plug-in interference) to open http://localhost:3000 , you can continue to debug returned by the vueMiddleware function.

If your VSCode is not Chinese (not used to English), you can install the simplified Chinese plug-in .

If VSCode does not have this debugging function. It is recommended to update to the latest version VSCode (currently the latest version v1.61.2 ).

Then let's follow the debugging to learn the vueMiddleware source code. You can look at the main line first, and continue to breakpoint debugging where you think it is important.

4. vueMiddleware source code

4.1 Comparison with or without vueMiddleware

Without debugging, we can comment app.use(vueMiddleware()) vue-dev-server/bin/vue-dev-server.js file, execute npm run test open http://localhost:3000 .

没有执行 vueMiddleware 中间件的原始情况

After enabling the middleware again, as shown below.

执行了 vueMiddleware 中间文件变化

Looking at the picture, we probably know what the difference is.

4.2 Overview of vueMiddleware middleware

We can find vue-dev-server/middleware.js to see an overview of this middleware function.

// vue-dev-server/middleware.js

const vueMiddleware = (options = defaultOptions) => {
  // 省略
  return async (req, res, next) => {
    // 省略
    // 对 .vue 结尾的文件进行处理
    if (req.path.endsWith('.vue')) {
    // 对 .js 结尾的文件进行处理
    } else if (req.path.endsWith('.js')) {
    // 对 /__modules/ 开头的文件进行处理
    } else if (req.path.startsWith('/__modules/')) {
    } else {
      next()
    }
  }
}
exports.vueMiddleware = vueMiddleware

vueMiddleware finally returns a function. Four things are mainly done in this function:

  • .vue files ending with 0617fb3b3a66d3
  • .js files ending with 0617fb3b3a66ed
  • /__modules/ files starting with 0617fb3b3a6705
  • If it is not the above three cases, execute the next method and transfer the control to the next middleware

Then let's take a look at how to deal with it.

We can also breakpoint these important places to see the implementation. for example:

重要断点

4.3 Process the files ending in .vue

if (req.path.endsWith('.vue')) {
  const key = parseUrl(req).pathname
  let out = await tryCache(key)

  if (!out) {
    // Bundle Single-File Component
    const result = await bundleSFC(req)
    out = result
    cacheData(key, out, result.updateTime)
  }

  send(res, out.code, 'application/javascript')
}

4.3.1 BundleSFC compile single file components

This function @vue/component-compiler , and finally returns a file that the browser can recognize.

const vueCompiler = require('@vue/component-compiler')
async function bundleSFC (req) {
  const { filepath, source, updateTime } = await readSource(req)
  const descriptorResult = compiler.compileToDescriptor(filepath, source)
  const assembledResult = vueCompiler.assemble(compiler, filepath, {
    ...descriptorResult,
    script: injectSourceMapToScript(descriptorResult.script),
    styles: injectSourceMapsToStyles(descriptorResult.styles)
  })
  return { ...assembledResult, updateTime }
}

Then we look readSource implementation of the 0617fb3b3a67db function.

4.3.2 readSource read file resource

The main function of this function is to obtain file resources upon request. Return the file path filepath , resource source , and update time updateTime .

const path = require('path')
const fs = require('fs')
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
const parseUrl = require('parseurl')
const root = process.cwd()

async function readSource(req) {
  const { pathname } = parseUrl(req)
  const filepath = path.resolve(root, pathname.replace(/^\//, ''))
  return {
    filepath,
    source: await readFile(filepath, 'utf-8'),
    updateTime: (await stat(filepath)).mtime.getTime()
  }
}

exports.readSource = readSource

Then we look at the processing of .js files

4.4 Process the files ending in .js

if (req.path.endsWith('.js')) {
  const key = parseUrl(req).pathname
  let out = await tryCache(key)

  if (!out) {
    // transform import statements
    // 转换 import 语句 
    // import Vue from 'vue'
    // => import Vue from "/__modules/vue"
    const result = await readSource(req)
    out = transformModuleImports(result.source)
    cacheData(key, out, result.updateTime)
  }

  send(res, out, 'application/javascript')
}

For vue-dev-server/test/main.js conversion

import Vue from 'vue'
import App from './test.vue'

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

// 公众号:若川视野
// 加微信 ruochuan12
// 参加源码共读,一起学习源码
import Vue from "/__modules/vue"
import App from './test.vue'

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

// 公众号:若川视野
// 加微信 ruochuan12
// 参加源码共读,一起学习源码

4.4.1 transformModuleImports transform import introduction

recast

validate-npm-package-name

const recast = require('recast')
const isPkg = require('validate-npm-package-name')

function transformModuleImports(code) {
  const ast = recast.parse(code)
  recast.types.visit(ast, {
    visitImportDeclaration(path) {
      const source = path.node.source.value
      if (!/^\.\/?/.test(source) && isPkg(source)) {
        path.node.source = recast.types.builders.literal(`/__modules/${source}`)
      }
      this.traverse(path)
    }
  })
  return recast.print(ast).code
}

exports.transformModuleImports = transformModuleImports

That is, for the npm packet conversion. This is "/__modules/vue"

import Vue from 'vue' => import Vue from "/__modules/vue"

4.5 Process the files beginning with /__modules/

import Vue from "/__modules/vue"

What this code finally returns is to read the file under the vue-dev-server/node_modules/vue/dist/vue.esm.browser.js

if (req.path.startsWith('/__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')
}

4.5.1 loadPkg load package (only Vue files are supported here)

Currently, only Vue files are supported, that is, the files under the vue-dev-server/node_modules/vue/dist/vue.esm.browser.js

// vue-dev-server/loadPkg.js
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)

async function loadPkg(pkg) {
  if (pkg === 'vue') {
    // 路径
    // vue-dev-server/node_modules/vue/dist
    const dir = path.dirname(require.resolve('vue'))
    const filepath = path.join(dir, 'vue.esm.browser.js')
    return readFile(filepath)
  }
  else {
    // TODO
    // check if the package has a browser es module that can be used
    // otherwise bundle it with rollup on the fly?
    throw new Error('npm imports support are not ready yet.')
  }
}

exports.loadPkg = loadPkg

At this point, we have basically completed the analysis of the main file and some imported files. Have an understanding of the main process.

5. Summary

Finally, let's look at the two pictures above with or without vueMiddleware middleware to summarize:

没有执行 vueMiddleware 中间件的原始情况

After enabling the middleware, as shown below.

执行了 vueMiddleware 中间文件变化

The browser supports native type=module module request loading. vue-dev-server intercepts it and returns to the browser to support the content, because it does not need to be packaged and built, so the speed is very fast.

<script type="module">
    import './main.js'
</script>

5.1 import Vue from'vue' conversion

// vue-dev-server/test/main.js
import Vue from 'vue'
import App from './test.vue'

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

import statement in main.js
import Vue from 'vue'
Generate ast by recast convert it to import Vue from "/__modules/vue"
And finally returned to the browser is vue-dev-server/node_modules/vue/dist/vue.esm.browser.js

5.2 import App from'./test.vue' conversion

main.js in the introduction of .vue files, import App from './test.vue'
Then use @vue/component-compiler convert to a file supported by the browser.

5.3 What else can be done next?

In view of the limited length of the article, the cache tryCache is currently not analyzed. Simply put, node-lru-cache least recently used for caching (this algorithm is often tested). The source code of this warehouse should be analyzed in the follow-up, welcome to continue to pay attention to me @若川.

It is highly recommended that readers use VSCode debug the vue-dev-server source code according to the method in the article. There are still many details in the source code. Due to limited space, the article has not been fully described.

It is worth mentioning that this warehouse master branch , especially the rain the river two years ago was written, relative to this article would be more complicated, there is spare capacity readers can learn.

You can also go directly to see the vite source code.

After reading this article, you may find that there are more and more things that the front-end can do, and you can't help feeling: the front-end is unfathomable, only continuous learning.

Finally, welcome to add me on WeChat ruochuan12 , participate in the source code reading activity, everyone learn the source code together and make progress together.


若川
7k 声望3.2k 粉丝

你好,我是若川。写有 《学习源码整体架构系列》 20余篇。