美文网首页
前端技术分享

前端技术分享

作者: 斗伽 | 来源:发表于2021-11-24 17:21 被阅读0次

    vite

    • vite 是神马?
    image
    • Vite (法语意为 "快速的",发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

    • 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可以输出用于生产环境的优化过的静态资源。

     它有如下特点:
    
          光速启动
    
          按需编译
    
          热模块替换HMR
    
    

    接下来我们使用vue3+vite 搭建第一个 Vite 项目;

    开启 vscode

    • vite why?
    • <script type="module"></script> ES module

      • Vite利用了浏览器native ES module imports特性,使用ES方式组织代码,浏览器自动请求需要的文件,并在服务端按需编译返回,完全跳过了打包过程

    Vite vs webpack 参考链接

    • Webpack
      • webpack 是一个现代 JavaScript 应用程序的静态模块打包器。开发时启动本地开发服务器,实时预览。它通过解析应用程序中的每一个 import 和 require ,将整个应用程序构建成一个基于 JavaScript 的捆绑包,并在运行时转换文件,这都是在服务器端完成的,依赖的数量和改变后构建/重新构建的时间之间有一个大致的线性关系。需要对整个项目文件进行打包,开发服务器启动缓慢。
      • Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次,所以反应速度会慢一些。
    image
    • Vite
      • Vite 不捆绑应用服务器端。相反,它利用浏览器中的原生 ES Moudle 模块,在具体去请求某个文件的时候,才会在服务端编译这个文件。
      • vite 只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理,实现真正的按需加载。
      • 对于热更新问题, vite 采用立即编译当前修改文件的办法。同时 vite 还会使用缓存机制( http 缓存=> vite 内置缓存),加载更新后的文件内容。
    image

    vite优势

    • 不需要等待打包,所以冷启动的速度将会非常快。
    • 代码是按需编译的。只有你当前页面实际导入的模块才会被编译。你不需要等待整个应用程序被打包完才能够启动服务。这在巨型应用上体验差别更加巨大。(router demo)
    • 热替换HMR的性能将与模块的总数量无关。

    vite 工作原理

    vite 启动服务
    1\. vite 在启动时,内部会启一个 http server,用于拦截页面的脚本文件。
    
    处理.js/.vue文件,npm模块
    
    
    
    // 精简了热更新相关代码,如果想看完整版建议去 github
    // https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/server.ts
    import http, { Server } from 'http'
    import serve from 'serve-handler'
    
    import { vueMiddleware } from './vueCompiler'
    import { resolveModule } from './moduleResolver'
    import { rewrite } from './moduleRewriter'
    import { sendJS } from './utils'
    
    export async function createServer({
        port = 3000,
        cwd = process.cwd()
    }: ServerConfig = {}): Promise<Server> {
    const server = http.createServer(async (req, res) => {
        const pathname = url.parse(req.url!).pathname!
        if (pathname.startsWith('/__modules/')) {
            // 返回 import 的模块文件
            return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
        } else if (pathname.endsWith('.vue')) {
            // 解析 vue 文件
            return vueMiddleware(cwd, req, res)
        } else if (pathname.endsWith('.js')) {
            // 读取 js 文本内容,然后使用 rewrite 处理
            const filename = path.join(cwd, pathname.slice(1))
            const content = await fs.readFile(filename, 'utf-8')
            return sendJS(res, rewrite(content))
        }
    
        serve(req, res, {
            public: cwd,
            // 默认返回 index.html
            rewrites: [{ source: '**', destination: '/index.html' }]
        })
    })
    
    return new Promise((resolve, reject) => {
        server.on('listening', () => {
        console.log(`Running at http://localhost:${port}`)
        resolve(server)
        })
    
        server.listen(port)
    })
    }
    
    // 访问index.html
    
    
    解析js文件

    index.html 文件会请求 /src/main.js

    image
    if (pathname.endsWith('.js')) {
      // 读取 js 文本内容,然后使用 rewrite 处理
      const filename = path.join(cwd, pathname.slice(1))
      const content = await fs.readFile(filename, 'utf-8')
      return sendJS(res, rewrite(content))
    }
    
    // 精简了部分代码,如果想看完整版建议去 github
    // https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleRewriter.ts
    import { parse } from '@babel/parser'
    
    export function rewrite(source: string, asSFCScript = false) {
      // 通过 babel 解析,找到 import from、export default 相关代码
      const ast = parse(source, {
        sourceType: 'module',
        plugins: [
          'bigInt',
          'optionalChaining',
          'nullishCoalescingOperator'
        ]
      }).program.body
    
      let s = source
      ast.forEach((node) => {
        if (node.type === 'ImportDeclaration') {
          if (/^[^\.\/]/.test(node.source.value)) {
            // 在 import 模块名称前加上 /__modules/
            // import { foo } from 'vue' --> import { foo } from '/__modules/vue'
            s = s.slice(0, node.source.start) 
              + `"/__modules/${node.source.value}"`
                + s.slice(node.source.end) 
          }
        } else if (asSFCScript && node.type === 'ExportDefaultDeclaration') {
          // export default { xxx } -->
          // let __script; export default (__script = { xxx })
          s = s.slice(0, node.source.start)
            + `let __script; export default (__script = ${
                s.slice(node.source.start, node.declaration.start) 
                })`
            + s.slice(node.source.end) 
          s.overwrite(
            node.start!,
            node.declaration.start!,
            `let __script; export default (__script = `
          )
          s.appendRight(node.end!, `)`)
        }
      })
    
      return s.toString()
    }
    
    
    处理npm模块

    请求的文件如果是 /__modules/ 开头的话,表明是一个 npm 模块

    // 精简了部分代码,如果想看完整版建议去 github
    // https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleResolver.ts
    import path from 'path'
    import resolve from 'resolve-from'
    import { sendJSStream } from './utils'
    import { ServerResponse } from 'http'
    
    export function resolveModule(id: string, cwd: string, res: ServerResponse) {
      let modulePath: string
      modulePath = resolve(cwd, 'node_modules', `${id}/package.json`)
      if (id === 'vue') {
        // 如果是 vue 模块,返回 vue.runtime.esm-browser.js
        modulePath = path.join(
          path.dirname(modulePath),
          'dist/vue.runtime.esm-browser.js'
        )
      } else {
        // 通过 package.json 文件,找到需要返回的 js 文件
        const pkg = require(modulePath)
        modulePath = path.join(path.dirname(modulePath), pkg.module || pkg.main)
      }
    
      sendJSStream(res, modulePath)
    }
    
    
    处理 vue 文件
    // 精简了部分代码,如果想看完整版建议去 github
    // https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/vueCompiler.ts
    
    import url from 'url'
    import path from 'path'
    import { parse, SFCDescriptor } from '@vue/compiler-sfc'
    import { rewrite } from './moduleRewriter'
    
    export async function vueMiddleware(
      cwd: string, req, res
    ) {
      const { pathname, query } = url.parse(req.url, true)
      const filename = path.join(cwd, pathname.slice(1))
      const content = await fs.readFile(filename, 'utf-8')
      const { descriptor } = parse(content, { filename }) // vue 模板解析
      if (!query.type) {
        let code = ``
        if (descriptor.script) {
          code += rewrite(
            descriptor.script.content,
            true /* rewrite default export to `script` */
          )
        } else {
          code += `const __script = {}; export default __script`
        }
        if (descriptor.styles) {
          descriptor.styles.forEach((s, i) => {
            code += `\nimport ${JSON.stringify(
              pathname + `?type=style&index=${i}`
            )}`
          })
        }
        if (descriptor.template) {
          code += `\nimport { render as __render } from ${JSON.stringify(
            pathname + `?type=template`
          )}`
          code += `\n__script.render = __render`
        }
        sendJS(res, code)
        return
      }
      if (query.type === 'template') {
        // 返回模板
      }
      if (query.type === 'style') {
        // 返回样式
      }
    }
    
    

    经过解析,.vue 文件返回的时候会被拆分成三个部分:script、style、template。

    // 解析前
    <template>
      <div>
        <img alt="Vue logo" src="./assets/logo.png" />
        <HelloWorld msg="Hello Vue 3.0 + Vite" />
      </div>
    </template>
    
    <script>
    import HelloWorld from "./components/HelloWorld.vue";
    
    export default {
      name: "App",
      components: {
        HelloWorld
      }
    };
    </script>
    
    
    // 解析后
    import HelloWorld from "/src/components/HelloWorld.vue";
    
    let __script;
    export default (__script = {
        name: "App",
        components: {
            HelloWorld
        }
    })
    
    import {render as __render} from "/src/App.vue?type=template"
    __script.render = __render
    
    

    template 中的内容,会被 vue 解析成 render 方法。《Vue 模板编译原理》

    import {
      parse,
      SFCDescriptor,
      compileTemplate
    } from '@vue/compiler-sfc'
    
    export async function vueMiddleware(
      cwd: string, req, res
    ) {
      // ...
      if (query.type === 'template') {
        // 返回模板
        const { code } = compileTemplate({
          filename,
          source: template.content,
        })
        sendJS(res, code)
        return
      }
      if (query.type === 'style') {
        // 返回样式
      }
    }
    
    

    [图片上传失败...(image-aaf414-1637751525318)]

    而 template 的样式

    import {
      parse,
      SFCDescriptor,
      compileStyle,
      compileTemplate
    } from '@vue/compiler-sfc'
    
    export async function vueMiddleware(
      cwd: string, req, res
    ) {
      // ...
      if (query.type === 'style') {
        // 返回样式
        const index = Number(query.index)
        const style = descriptor.styles[index]
        const { code } = compileStyle({
          filename,
          source: style.content
        })
        sendJS(
          res,
          `
      const id = "vue-style-${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()
        )
      }
    }
    复制代码
    
    

    style 的处理也不复杂,拿到 style 标签的内容,然后 js 通过创建一个 style 标签,将样式添加到 head 标签中。

    小结

    通过上文解析了 vite 是如何拦截请求,然后返回需要的文件的过程;我们大概了解了vite是如何提高本地开发速度;接下来说一下 Vite 热更新的实现

    HMR 处理机制

    实现热更新,那么就需要浏览器和服务器建立某种通信机制,这样浏览器才能收到通知进行热更新。Vite 的是通过 WebSocket 来实现的热更新通信。

    客户端

    客户端的代码在 src/client/client.ts,主要是创建 WebSocket 客户端,监听来自服务端的 HMR 消息推送。

    Vite 的 WS 客户端目前监听这几种消息:

    • connected: WebSocket 连接成功

    • vue-reload: Vue 组件重新加载(当你修改了 script 里的内容时)

    • vue-rerender: Vue 组件重新渲染(当你修改了 template 里的内容时)

    • style-update: 样式更新

    • style-remove: 样式移除

    • js-update: js 文件更新

    • full-reload: fallback 机制,网页重刷新

    image
    服务端

    核心是监听项目文件的变更,然后根据不同文件类型(目前只有 vuejs)来做不同的处理:

    watcher.on('change', async (file) => {
      const timestamp = Date.now() // 更新时间戳
      if (file.endsWith('.vue')) {
        handleVueReload(file, timestamp)
      } else if (file.endsWith('.js')) {
        handleJSReload(file, timestamp)
      }
    })
    
    //  简单的源码分析如下:
    //  以 vue 文件处理
    async function handleVueReload(
        file: string,
        timestamp: number = Date.now(),
        content?: string
    ) {
      const publicPath = resolver.fileToRequest(file) // 获取文件的路径
      const cacheEntry = vueCache.get(file) // 获取缓存里的内容
    
      debugHmr(`busting Vue cache for ${file}`)
      vueCache.del(file) // 发生变动了因此之前的缓存可以删除
    
      const descriptor = await parseSFC(root, file, content) // 编译 Vue 文件
    
      const prevDescriptor = cacheEntry && cacheEntry.descriptor // 获取前一次的缓存
    
      if (!prevDescriptor) {
        // 这个文件之前从未被访问过(本次是第一次访问),也就没必要热更新
        return
      }
    
      // 设置两个标志位,用于判断是需要 reload 还是 rerender
      let needReload = false
      let needRerender = false
    
      // 如果 script 部分不同则需要 reload
      if (!isEqual(descriptor.script, prevDescriptor.script)) {
        needReload = true
      }
    
      // 如果 template 部分不同则需要 rerender
      if (!isEqual(descriptor.template, prevDescriptor.template)) {
        needRerender = true
      }
    
      const styleId = hash_sum(publicPath)
      // 获取之前的 style 以及下一次(或者说热更新)的 style
      const prevStyles = prevDescriptor.styles || []
      const nextStyles = descriptor.styles || []
    
      // 如果不需要 reload,则查看是否需要更新 style
      if (!needReload) {
        nextStyles.forEach((_, i) => {
          if (!prevStyles[i] || !isEqual(prevStyles[i], nextStyles[i])) {
            send({
              type: 'style-update',
              path: publicPath,
              index: i,
              id: `${styleId}-${i}`,
              timestamp
            })
          }
        })
      }
    
      // 如果 style 标签及内容删掉了,则需要发送 `style-remove` 的通知
      prevStyles.slice(nextStyles.length).forEach((_, i) => {
        send({
          type: 'style-remove',
          path: publicPath,
          id: `${styleId}-${i + nextStyles.length}`,
          timestamp
        })
      })
    
      // 如果需要 reload 发送 `vue-reload` 通知
      if (needReload) {
        send({
          type: 'vue-reload',
          path: publicPath,
          timestamp
        })
      } else if (needRerender) {
        // 否则发送 `vue-rerender` 通知
        send({
          type: 'vue-rerender',
          path: publicPath,
          timestamp
        })
      }
    }
    
    
    客户端逻辑注入

    代码里并没有引入 HRMclient 代码,Vite 是如何把 client 代码注入的呢??

    回到上面的一张图,Vite 重写 index.html 文件的内容并返回时:

    入口注入client.js文件

    image image

    app.vue引入css文件

    image

    vue文件 新增class变更后,热更新代码变更差异

    image

    热更新的具体怎么替换模块???

    到此,热更新的整体流程已经解析完毕

    后续

    依据原理手写实现自己的Vite -- lvite

    相关文章

      网友评论

          本文标题:前端技术分享

          本文链接:https://www.haomeiwen.com/subject/nrjemltx.html