All Projects → Kingbultsea → vite-analysis

Kingbultsea / vite-analysis

Licence: other
出最新版本的全部源码分析中...(换了m1芯片arm64架构的电脑,旧版本无法运行,如esbuild Chromium)

Programming Languages

rust
11053 projects

Projects that are alternatives of or similar to vite-analysis

codemirror-editor-vue3
CodeMirror component for Vue3
Stars: ✭ 22 (-84.06%)
Mutual labels:  vite, vue-next
element-plus-admin
基于vite+ts+elementPlus
Stars: ✭ 361 (+161.59%)
Mutual labels:  vite, vue-next
electron-vue-next
A starter template for using vue-next with the electron.
Stars: ✭ 189 (+36.96%)
Mutual labels:  vite, vue-next
vue3.0-template-admin
本项目基于vue3+ElementPlus+Typescript+Vite搭建一套通用的后台管理模板;并基于常见业务场景,抽象出常见功能组件;包括动态菜单,菜单权限、登录、主题切换、国际化、个人中心、表单页、列表页、复制文本、二维码分享等等
Stars: ✭ 500 (+262.32%)
Mutual labels:  vite, vue-next
bpmn-vue-activiti
基于Vue3.x + Vite + bpmn-js + element-plus + tsx 实现的Activiti流程设计器(Activiti process designer based on Vue3.x + Vite + BPMN-JS + Element-Plus + TSX implementation)
Stars: ✭ 345 (+150%)
Mutual labels:  vite, vue-next
rakkasjs
Lightning fast Next.js alternative powered by Vite
Stars: ✭ 527 (+281.88%)
Mutual labels:  vite
Hajime
Hajime - Competition software management for Fencing Martial Arts
Stars: ✭ 13 (-90.58%)
Mutual labels:  vite
element-shuang
🎉基于 Vue3、element-plus、vite封装的后台管理系统模板,https://shuangda1018.github.io/element-shuang/#/table
Stars: ✭ 37 (-73.19%)
Mutual labels:  vite
vite-plugin-inspect
Inspect the intermediate state of Vite plugins
Stars: ✭ 491 (+255.8%)
Mutual labels:  vite
vite-ts-tailwind-starter
Opinionated Vite + Vue 3 + TypeScript + Tailwind CSS starter template w/ tests and CI.
Stars: ✭ 228 (+65.22%)
Mutual labels:  vite
electron-vue-vite-boilerplate
Electron Vue Vite Boilerplate for you next project
Stars: ✭ 26 (-81.16%)
Mutual labels:  vite
vue2-timeago
🙌 A vue component used to format date with time ago statement. 💬
Stars: ✭ 76 (-44.93%)
Mutual labels:  vite
vite-rollup-plugins
A compatibility list of rollup plugins for Vite 2 and test playground
Stars: ✭ 76 (-44.93%)
Mutual labels:  vite
vitesome
A simple opinionated Vue3 Starter Template with Vite.js
Stars: ✭ 124 (-10.14%)
Mutual labels:  vite
vite-example
Todo app with vite/vue3/vue-router4
Stars: ✭ 22 (-84.06%)
Mutual labels:  vite
vite-wiki
📄The Vite Wiki
Stars: ✭ 37 (-73.19%)
Mutual labels:  vite
vite-vue-admin
🎉🎉使用Vite + Vue3 + TypeScript + Element-plus + Mock开发的后台管理系统🎉🎉
Stars: ✭ 97 (-29.71%)
Mutual labels:  vite
rapide
WIP! do not use just yet - Opinionated Vite + Alpine.js starter PWA template
Stars: ✭ 15 (-89.13%)
Mutual labels:  vite
tov-template
vite + vue3 + ts 开箱即用现代开发模板
Stars: ✭ 251 (+81.88%)
Mutual labels:  vite
nuxt3-app
Nuxt3 (Nuxt 3) best starter repo, Tailwindcss, Sass, Headless UI, Vue, Pinia, Vite
Stars: ✭ 252 (+82.61%)
Mutual labels:  vite

1 - 820c2cf 根基

npm bin的知识点

我们写包的时候,可以在package.json中添加bin字段。

{
  "bin": {
    "vds": "bin/vds.js"
  }
}

当用户安装我们写的包,用户可以使用vds来执行 bin/vds.js

npm为我们在./bin目录下面放了一个叫做vds的软连接,所以在目录下面执行命令行vds,就会执行bin/vds.js文件

Upgrade-Insecure-Requests请求头

HTTP Upgrade-Insecure-Requests 请求头向服务器发送一个客户端对HTTPS加密和认证响应良好,并且可以成功处理的信号,可以请求所属网站所有的HTTPS资源。 在https页面中,如果调用了http资源,那么浏览器就会抛出一些错误。为了改变成这一状况,chrome(谷歌浏览器)会在http请求中加入 Upgrade-Insecure-Requests: 1 ,服务器收到请求后会返回 Content-Security-Policy: upgrade-insecure-requests 头,告诉浏览器,可以把所属本站的所有 http 连接升级为 https 连接。

是如何监听文件变动的

利用工具chokidar,文件改动保存,即可触发(其实按下ctrl + s就已经触发了)

const chokidar = require('chokidar')
const fileWatcher = chokidar.watch(process.cwd(), {
    ignored: [/node_modules/]
  })

fileWatcher.on('change', (file) => {
    if (file.endsWith('.vue')) {
        // do something
    }
})

流程

  1. 浏览器请求文件,服务端发送经过@vue/compiler-sfc处理,转换成Vue版本的AST。

  2. 服务器把AST整理成js,其中把template转换成render函数

    // http://localhost:3000/file-vue/test.vue
    
        const script = {
          name: "123",
          data() {
            return {
              a: 123
            }
          }
        }
    
    export default script
    import { render } from "/file-vue/test.vue?type=template"
    script.render = render
    script.__hmrId = "/file-vue/test.vue"

准备工作

  1. hmrProxy.js文件: 目的是给浏览器运行ws,与服务器进行双向交流,使用esmimport,请求本地vue文件,进行上面说的流程。

    比如,文件变动(变动也分类型,比如现在是对比出旧文件和新文件的AST语法树中的script.content内容字符串有变动,事件名称reloadtemplate的改动事件名称为rerender),通知前端事件 reload,使用import异步引入parse后的内容(如上面的代码),再重新运行(这块还没写,预备位置)。

2.对于vue文件的style还未处理,停留在vueAST上。

3.准备前端事件update-stylefull-reload

// 这个文件运行在客户端上

const socket = new WebSocket(`ws://${location.host}`)

// 监听服务器发送的事件
socket.addEventListener('message', ({ data }) => {
  const { type, path, index } = JSON.parse(data)
  switch (type) {
    case 'connected':
      console.log(`[vds] connected.`)
      break
    case 'reload':
      import(`${path}?t=${Date.now()}`).then(m => {
        __VUE_HMR_RUNTIME__.reload(path, m.default) // m为AST转换成的js文件内容
        console.log(`[vds][hmr] ${path} reloaded.`)
      })
      break
    case 'rerender':
      import(`${path}?type=template&t=${Date.now()}`).then(m => {
        __VUE_HMR_RUNTIME__.rerender(path, m.render)
        console.log(`[vds][hmr] ${path} template updated.`)
      })
      break
    case 'update-style':
      import(`${path}?type=style&index=${index}&t=${Date.now()}`).then(m => {
        // TODO style hmr  留坑
      })
      break
    case 'full-reload':
      location.reload() // 刷新浏览器
  }
})

// ping server 如果服务器ws连接中断,进行每秒的重新链接
socket.addEventListener('close', () => {
  console.log(`[vds] server connection lost. polling for restart...`)
  setInterval(() => {
    new WebSocket(`ws://${location.host}`).addEventListener('open', () => {
      location.reload() // 这里有BUG 需要把setInterval给删除
    })
  }, 1000)
})

2 - 4a04d81

server.js 新增serve-handler

利用这个包,改写除了__hmrClient和.vue后缀的文件的路径,比如访问localhost:3000/a会自动修正为localhost:3030/index.html

parseSFC.js 处理SFC文件时,增加可以配置的缓存

if (saveCache) { // 新增是否保存缓存
    cache.set(filename, descriptor)
}

server.js 修复BUG

// 提升 以下这段代码上一个层级
const fileWatcher = chokidar.watch(process.cwd(), {
    ignored: [/node_modules/]
})

3 - 33488fe 新增+优化

moduleRewriter.js

使用magic-string重写AST语法树输出script中的export script

server.js新增访问__modules与.js文件

if (pathname.startsWith('/__modules/')) {
    return moduleMiddleware(pathname.replace('/__modules/', ''), res)
  } else if (pathname.endsWith('.vue')) {
    return vue(req, res)  // 发送vue文件 需要vue中间件 (@vue/compiler-sfc)
  } else if (pathname.endsWith('.js')) { // 发送JS
    const filename = path.join(process.cwd(), pathname.slice(1))
    if (fs.existsSync(filename)) {
      const content = rewrite(fs.readFileSync(filename, 'utf-8'))
      return sendJS(res, content)
    }
 }

moduleMiddleware.js

当访问localhost:3000/__modules/abc,通过require.resolve('abc')拿到绝对路径,把文件内容放进response响应体中。关于require.resolve

require.resolve是如何寻找文件的

处理好的文件中的变量增加下划线标识

___script __render

引入hmrClient.js

import "/__hmrClient" // 新增,这是一个客户端client ws 文件,在commit-1中就存在
// __hmrClient 会自动访问 hmrClient.js

    let __script; export default (__script = {
      name: "123",
      data() {
        return {
          a: 123
        }
      }
    })

import { render as __render } from "/file-vue/test.vue?type=template"
__script.render = __render
__script.__hmrId = "/file-vue/test.vue"

4 - bfb4b91 重构

parseSFC.js阅读文件,修改成promise的方法

曾经是同步读取文件,会阻塞线程,现在改用promises,去除需要等待IO处理的时间。

// ./lib/hmrWatcher.js
const [descriptor, prevDescriptor] = await parseSFC(file)

moduleMiddleware.js

  1. 处理__modulesmoduleMiddleware.js曾经使用require.resolve('abc')确定路径,现在改为使用resolve-cwd模块。不同之处在于resolve-cwd会在命令行当前的路径开始寻找,其他行为与require.resolve一致。 http://localhost:3000/__modules/vue -->命令行当前目录/node_modules/vue/index.js 如果是用require.resolve,且当前工作目录下没有vue模块(当然更没有当前vue包,详情看文章),则会在当前命令行目录的上级路径查找模块,直到没有为止。

    require.resolve('diff')
    // 寻找的路径["Users/当前工作目录/node_modules", "/Users/node_modules", "/node_modules"]

    可以看到是往上寻找的。

const resolve = require('resolve-cwd')
modulePath = resolve(id)
  1. 曾经使用sendJS改为sendJSStream,在发送文件的时候,新增Transfer-Encoding: chunkedresponse header。

    数据以一系列分块的形式进行发送。 Content-Length 首部在这种情况下不被发送。在每一个分块的开头需要添加当前分块的长度,以十六进制的形式表示,后面紧跟着 '\r\n' ,之后是分块本身,后面也是'\r\n' 。终止块是一个常规的分块,不同之处在于其长度为0。终止块后面是一个挂载(trailer),由一系列(或者为空)的实体消息首部构成。

function send(res, source, mime) {
  res.setHeader('Content-Type', mime)
  res.end(source)
}

function sendJS(res, source) {
  send(res, source, 'application/javascript')
}

function sendJSStream(res, file) {
  res.setHeader('Content-Type', 'application/javascript')
  const stream = fs.createReadStream(file)
  stream.on('open', () => {
    stream.pipe(res)
  })
  stream.on('error', (err) => {
    res.end(err)
  })
}

server.js expose createAPI

封装server

#!/usr/bin/env node
const { createServer } = require('../lib/server')

// TODO pass cli args
createServer()
// 封装后 就不直接像之前 require('')文件直接使用了,而且可以配置port,默认3000

package.json添加main字段,指向./lib/server.js

  "main": "lib/server.js"

5 - 91d76bf 使用TS重构内容

lint-staged

{
    "gitHooks": { // yorkie 不知道的 去搜索下git仓库 yorkie
      "pre-commit": "lint-staged"
     },
     "lint-staged": {
       "*.js": [
         "prettier --write",
         "git add"
       ],
       "*.ts": [
         "prettier --parser=typescript --write",
         "git add"
       ]
     }
}

假如你改动了index.ts文件,并且git add 把文件丢进暂存区。

// ./src/server/index.ts
export { createServer, ServerConfig } from "./server"
+ let a = 'a'

改动.prettierrc,把单括号模式关闭(这个改动加不加都没所谓,执行的是你现在本身的文件内容)

semi: false
- singleQuote: true
+ singleQuote: false
printWidth: 80
trailingComma: none

git commit,识别到是commit行为,通过钩子执行lint-staged命令,改动的是js文件,执行prettier --parser=typescript --write,你会发现你的代码从单括号变成双括号了,并且提交到本地仓库的也是双括号。

// ./src/server/index.ts
export { createServer, ServerConfig } from "./server"
let a = "a"

TS相关

客户端运行的代码在src/client,服务端运行的代码在src/server

关于从js ---> ts的重构文件源与目标

  • ./lib/hmrClient.js ---> ./src/client/client.ts
  • ./lib/hmrWatcher.js ---> ./src/server/hmrWatcher.ts
  • ./bin/vds.js ---> ./src/server/index.ts // "main": "dist/server/index.js"
  • ./lib/moduleMiddleware.js ---> ./src/server/moduleMiddleware.ts
  • ./lib/moduleRewriter.js ---> ./src/server/moduleRewriter.ts
  • ./lib/parseSFC.js ---> ./src/server/parseSFC.ts
  • ./lib/server.js ---> ./src/server/server.ts
  • ./lib/util.js ---> ./src/server/util.ts
  • ./lib/vueMiddleware.js ---> ./src/server/vueMiddleware.ts####

tsconfig.base.json

server与client公用(使用extends: "../../tsconfig.base.json")。

{
  "compilerOptions": {
    "sourceMap": false,
    "target": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "declaration": true,
    "allowJs": false,
    "allowSyntheticDefaultImports": true,
    "noUnusedLocals": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "removeComments": false,
    "lib": [
      "esnext",
      "DOM"
    ]
  }
}

./src/client/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "outDir": "../../dist/client", // 输出位置
    "module": "esnext", // 生成输出支持esnext的代码
    "lib": ["ESNext", "DOM"] // 需要注入的库 DOM 与 esnext(就是语法啦)
  },
  "include": ["./"]
}

./src/client/tsconfig,json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "outDir": "../../dist/server",
    "module": "commonjs", // 生成cjs的语法 可以说是es5了
    "lib": ["ESNext"]
  },
  "include": ["./"]
}

client的区别是,server不需要使用esm/node,客户端必须使用esm,因为那是灵魂。

命令

"scripts": {
    "dev": "run-p dev-client dev-server",
    "dev-client": "tsc -w --p src/client",
    "dev-server": "tsc -w --p src/server",
    "build": "tsc -p src/client && tsc -p src/server",
    "lint": "prettier --write --parser typescript \"src/**/*.ts\"",
    "prepublishOnly": "tsc"
  }
  • dev:run-p为npm-run-all包的命令(前面有介绍过package.json中的main字段),利用这个工具,可以同时运行两个命令,并非顺序运行。
  • dev-client: typescript的编译命令tsc,--w 监听文件变动,使得文件自动编译。--p 后接文件路径。
  • dev-server: 同上。
  • build: server and client编译(采用-p src/client 的tsconfig / -p src/server 的 tsconfig)。
  • lint: 格式src/**/*.ts路径下的ts文件, --parser表示需要格式的文件是typescript文件。
  • prepublishOnly: 无效的命令... 不知道为什么而准备

更新cli文件

- const { createServer } = require('../lib/server')
+ const { createServer } = reuqire('../dist/server')

6 - e718bd5 添加测试

server/server.ts

给promise,返回ws(如果已连接)本身。

async function  createServer({ port = 3000 }: ServerConfig = {}): Promise<Server> {
    // do something
}

添加新命令test

{
    "script": {
+        "test": "yarn build && jest"
    },
+   "jest": {
+        "watchPathIgnorePatterns": ["<rootDir>/test/temp"] // 告诉jest不需要运行这个目录下的.test
+   }
}

观察测试得出的结论

execa包: 创建子进程运行./bin/vds.js,并且等待进程在控制台打印输出Running后,进行测试,在所有测试完成后终止其进程。

puppeteer: 可以通过API来控制Chrome/Chromium,很多操作浏览器的行为都可以通过puppeteer来实现。

流程查看(后续再画出流程图指)

这里的流程查看,可以使用单元测试运行,但是还有一种形式。

1.进入 USER/test/fixtures目录,运行USER/bin/vds.js。 2.打开localhost:3000。 3.可以观察流程,修改fixtures文件夹下的组件(注意要恢复,因为该文件会影响测试结果,导致后续jest测试失败),查看ws发送的通知。

HMR文件是何时植入的(就是client.ts是什么时候给到浏览器进行ws的建立的)?

#index.html
<div id="app"></div>
<script type="module" src="/main.js"></script>

#main.js http://localhost:3000/main.js
// 经过server/server.ts
// 识别到请求的是(注意哈,当前工作目录在fixtures目录下)main.js,js文件不进行分块发送(commit-4中有提到)
// 直接发送main.js
import { createApp } from 'vue' // 等待加载完毕文件
import Comp from './Comp.vue' // 才会运行js,别忘了esm的特性是静态的(除了import.resolve())
createApp(Comp).mount('#app')

#server/moduleMiddleware.ts的拦截(http://localhost:3000/__modules/vue)
// 拦截后这里直接发送一整个 'dist/vue.runtime.esm-browser.js'(怎么寻找的?看commit-4)

# server/vueMiddleware.ts (http://localhost:3000/Comp.vue)
if (!query.type) { // 并没有type参数,什么参数都没
    // inject hmr client 植入hmr
    let code = `import "/__hmrClient"\n`
    if (descriptor.script) {
      code += rewrite(
        descriptor.script.content,
        true /* rewrite default export to `script` */
      )
    } else {
      code += `const __script = {}; export default __script`
    }
    if (descriptor.template) {
      code += `\nimport { render as __render } from ${JSON.stringify(
        parsed.pathname + `?type=template${query.t ? `&t=${query.t}` : ``}`
      )}`
      code += `\n__script.render = __render`
    }
    if (descriptor.style) { // 还没做
      // TODO
    }
    code += `\n__script.__hmrId = ${JSON.stringify(parsed.pathname)}`
    return sendJS(res, code)
}

# http://localhost:3000/Comp.vue
import "/__hmrClient"
import Child from './Child.vue'
let __script; export default (__script = {
  components: { Child },
  setup() {
    return {
      count: 0
    }
  }
})
import { render as __render } from "/Comp.vue?type=template"
__script.render = __render
__script.__hmrId = "/Comp.vue"
// (这块其实就是你写的vue组件,帮你设置好输出罢了)

# http://localhost:3000/Child.vue
import "/__hmrClient" // 二次导入为什么不会进行两次ws的连接?(因为import只进行一次的,esm它不同amd cmd cjs)
const __script = {}; export default __script
import { render as __render } from "/Child.vue?type=template"
__script.render = __render
__script.__hmrId = "/Child.vue"
// 与Comp一样

为什么render要分开引入?

import "/Child.vue?type=template"的好处分析:

关于reload事件

这里如果你的vue组件改变,server/hmrWatcher中会对比新旧AST语法树中的script字符,如果是script改变,那么发送reload事件,给用户端。客户端识别到reload事件,他将使用esm的异步引入模块import().then()

# client/client.ts (我看的是未经编译的ts,实际是js,这里涉及到的其实都是js,为了方便我用了ts)
case 'reload':
      import(`${path}?t=${Date.now()}`).then((m) => {
        __VUE_HMR_RUNTIME__.reload(path, m.default) // path资源路径
        console.log(`[vds] ${path} reloaded.`)
      })
      break

__VUE_HMR_RUNTIME__:

先介绍一下vue-next的知识点。

# vue-next/runtime-core/mountComponent: Function
// 这个在vue组件开始渲染的时候触发,在mountComponent Component类型组件才会有
if (__DEV__ && instance.type.__hmrId) { // 可以看到需要有__hmrId这个文件路径才会帮你注册
      registerHMR(instance) // 注册hmrID 其实就是路径
}

# vue-next/runtime-core/unmountComponent: Function
// 删除注册
if (__DEV__ && instance.type.__hmrId) {
      unregisterHMR(instance)
}

# vue-next/runtime-core/hmr.ts
export function registerHMR(instance: ComponentInternalInstance) {
  const id = instance.type.__hmrId!
  let record = map.get(id)
  if (!record) {
    createRecord(id) // map中存放set, key: hmrId, value: new Set()
    record = map.get(id)!
  }
  record.add(instance) // 把instance 存进set 一个组件可能会被多处地方使用,所以会存在多个instance
}
    
export function unregisterHMR(instance: ComponentInternalInstance) {
  map.get(instance.type.__hmrId!)!.delete(instance)
}

这里使用到了的功能是reload:

export const hmrDirtyComponents = new Set<Component>()

function reload(id: string, newComp: ComponentOptions) {
  const record = map.get(id)
  if (!record) return
  Array.from(record).forEach(instance => {
    const comp = instance.type // instance.type是旧的newComp
    if (!hmrDirtyComponents.has(comp)) {
      // 1. Update existing comp definition to match new one
      extend(comp, newComp)
      for (const key in comp) {
        if (!(key in newComp)) {
          delete (comp as any)[key]
        }
      }
        
      // 2. Mark component dirty. This forces the renderer to replace the component
      // on patch.
      hmrDirtyComponents.add(comp)
      // 3. Make sure to unmark the component after the reload.
      queuePostFlushCb(() => {
        hmrDirtyComponents.delete(comp)
      })
    }

    if (instance.parent) {
      // 4. Force the parent instance to re-render. This will cause all updated
      // components to be unmounted and re-mounted. Queue the update so that we
      // don't end up forcing the same parent to re-render multiple times.
      queueJob(instance.parent.update)
    } else if (instance.appContext.reload) {
      // root instance mounted via createApp() has a reload method
      instance.appContext.reload()
    } else if (typeof window !== 'undefined') {
      // root instance inside tree created via raw render(). Force reload.
      window.location.reload() //
    } else {
      console.warn(
        '[HMR] Root or manually mounted instance modified. Full reload required.'
      )
    }
  })
}

这块检测了组件字段的改变。如果当前组件拥有父组件,调用父组件的instance.update方法:image-20210402102925225

(这里真心建议大家看一下vue-next的渲染流程,不然我也表达不出它原本意思)

vue-next组件渲染流程图

调用这个方法,目标就是让子组件重新走一次流程,可以让render重新更新,让视图重新抓取effect,大体意思就是渲染子组件。

调用这个方法,目标就是让子组件重新走一次流程,可以让render重新更新,让视图重新抓取effect,大体意思就是渲染子组件。

如果没有子组件,那么调用instance.appContext.reload

export interface AppContext {
  config: AppConfig
  mixins: ComponentOptions[]
  components: Record<string, PublicAPIComponent>
  directives: Record<string, Directive>
  provides: Record<string | symbol, any>
  reload?: () => void // HMR only
}

// main.js(我们vue项目的入口文件) 中的 createApp(Comp).mount('#app')
function mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(Comp as Component, rootProps)
          vnode.appContext = context
          // HMR root reload for reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer)
            }
          }
}

所以看到,本质上就是调用render,这和一整个vue渲染流程是一样的。

render

如果也没有reload,执行window.loaction.reload()(没有分析出什么环境才会触发,毕竟dev环境下,必有reload,留个坑,暂时认为是vue-next的保守行为)

关于rerender事件

文件改动到的仅仅是模板(template),像对比script那样,如果有变动server端则发送一个rerender事件到client客户端。client客户端收到rerender,触发__VUE_HMR_RUNTIME__.rerender

# client/client.ts    
case 'rerender':
      import(`${path}?type=template&t=${Date.now()}`).then((m) => {
        __VUE_HMR_RUNTIME__.rerender(path, m.render)
        console.log(`[vds] ${path} template updated.`)
      })
      break

接着查看vue-nextrerender方法:

# vue-next/runtime-core/hmr.ts
function rerender(id: string, newRender?: Function) { // id为path路径
  const record = map.get(id)
  if (!record) return
  // Array.from creates a snapshot which avoids the set being mutated during
  // updates
  Array.from(record).forEach(instance => {
    if (newRender) {
      instance.render = newRender as InternalRenderFunction // 更新vnode
    }
    instance.renderCache = []
    // this flag forces child components with slot content to update
    isHmrUpdating = true
    instance.update() // 调用自身update
    isHmrUpdating = false
  })
}

结论

rerender用在template,使用instance.update

reload用在script,使用instance.parent.update/render

script包含了template的更新,分开的做法,好处在template更新的时候,只更新组件自身的树,script是从父树开始的。

为什么script需要从parent更新?

查看组件渲染的三大块:

1.createComponentInstance 创建instance,一个组件相关的Object

2.setupComponent,对attrs的一些处理 还有setup的处理instance.type中的所有关键词字段的处理。

3.setupRenderEffect,2xOptionsAPI与render的处理。

因为script的改变,会有可能变动到功能点2,但是调用父组件的更新?不是只会走updateComponent的参数更新功能?

# vue-next/runtime-core/vnode.ts
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as Component) // reload: 已经在渲染前运行
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

可以看到不走updateComponent,强行走渲染123的流程。

所以可以得出:这是为了优化template,不需要走上级的更新。

为什么要走重新渲染呢?不是万物皆render?

不是这样说的,template的改变,只会影响到你的参数,而实际上render又是script变动的一部分,在手写render的情况下,是走重新渲染的(不探讨为什么手写render也要走重新渲染,我目前觉得不需要重新渲染,vite可以优化一下)。

假如你的setup的内容改变,需要重新运行,那么就要经过2的处理,所以就需要重新渲染。

7 - 3e64a74 去除git add

更新package.json

{
  "lint-staged": {
    "*.js": [
-     "git add", 
      "prettier --write"
    ],
    "*.ts": [
-     "git add", 
      "prettier --parser=typescript --write"
    ]
  }
}

去除git add,现在把改动文件丢进暂存区,将不会把你的文件格式化了。在commit后,你会发现进行了格式化。 可以自己尝试一下把某个单括号改成双括号,你会发现commit提交后自动变成单括号了。

8 - 93286b9 优化cwd

优化process.cwd()

提取成为一个参数,作为输入。

更改名称与整合功能

处理模块中间件的文件moduleMiddleware.ts修改名称为moduleResolve.ts,并且新增寻找module路径与模块名称的键值对。为source map做准备。

// TODO support custom imports map e.g. for snowpack web_modules
const fileToIdMap = new Map()

  if (id.endsWith('.map')) {
    sourceMapPath = id
    id = fileToIdMap.get(id.replace(/\.map$/, ''))
    if (!id) {
      res.statusCode = 404
      res.end()
      return
    }
  }

try {
    modulePath = resolve(cwd, `${id}/package.json`)
    if (id === 'vue') {
    } else {
      fileToIdMap.set(path.basename(modulePath), id)
    }
    sendJSStream(res, modulePath)
  } catch (e) {
  }

更改带有处理vue文件并生成语法树功能的parseSFC.ts名称为vueCompiler.ts

修改监听功能hmrWatcher.ts文件名称为watcher.ts

更改resolve-cwd为resolve-from

moduleResolver,中会寻找包。

resolve-cwd: 从当前命令行路径开始寻找。

resolve-from: 从给出的路径开始寻找。

以上两个寻找行为与require.resolve一致,仅仅是入口方式不同。

9 - f4382f1 整合优化

整理vueCompiler.ts

把处理templatescript的代码,提取出来。

function compileSFCTemplate and function compileSFCMain

watcher.ts

如果没有AST语法树,则不做任何处理。

if (file.endsWith('.vue')) {
      // check which part of the file changed
      const [descriptor, prevDescriptor] = await parseSFC(file)
-      if (!prevDescriptor) {
+      if (!descriptor || !prevDescriptor) {
        // the file has never been accessed yet
        return
      }
}

10 - 140f2b2 style HMR

新增事件

原有事件: reload<script>的更新,会触发此事件(拥有scoped的样式也会触发,这里只留下坑位) rerender<template>的更新,会触发此事件。

新增: style-update:发送给client更新css的消息。

nextStyles.forEach((_, i) => {
        if (!prevStyles[i] || !isEqual(prevStyles[i], nextStyles[i])) {
          send({
            type: 'style-update',
            path: resourcePath,
            index: i
          })
        }
})

style-remove:发送给client删除css的消息

hash-sum

生成hash,例如hash_sum(resourcePath)style-remove事件需要用到,识别对应vue组件的id

watcher.ts

整合代码,包括前面说的新增两个事件。

client.ts

新增事件,同上。

vueCompiler.ts

新增对style的处理:

if (descriptor.styles) {
    descriptor.styles.forEach((s, i) => {
      if (s.scoped) hasScoped = true
      code += `\nimport ${JSON.stringify(
        pathname + `?type=style&index=${i}${timestamp}`
      )}`
    })
    if (hasScoped) {
      code += `\n__script.__scopeId = "data-v-${hash(pathname)}"`
    }
}


function compileSFCStyle(
  res: ServerResponse,
  style: SFCStyleBlock,
  index: string,
  filename: string,
  pathname: string
) {
  const id = hash(pathname)
  const { code, errors } = compileStyle({
    source: style.content,
    filename,
    id: `data-v-${id}`,
    scoped: style.scoped != null
  })
  // TODO css modules

  if (errors) {
    // TODO
  }
  sendJS(
    res,
    `
const id = "vue-style-${id}-${index}"
let style = document.getElementById(id)
if (!style) {
  style = document.createElement('style')
  style.id = id
  document.head.appendChild(style)
}
style.textContent = ${JSON.stringify(code)}
  `.trim()
  )
}

遇到vue的ASTstyle,检测<style>是否添加了scoped(因为一个组件可以写多个<style>,所以用了forEach),如果有则:

import hash from 'hash-sum'
if (hasScoped) {
      code += `\n__script.__scopeId = "data-v-${hash(pathname)}"`
}

以上style功能还没有测试用例完善。

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].