最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vite 源码解析最终章之组件渲染篇

    正文概述 掘金(yuuang)   2020-12-29   512

    为了更加完整舒适的阅读体验强烈建议跳转到以下地址进行阅读

    包含源码解析章节的完整目录:vite-design.surge.sh/guide/

    [

    ](link.zhihu.com/?target=htt…)

    如果觉得该文章对你有帮助,可以到原仓库点个 Star ✨

    github.com/zhangyuang/…

    组件渲染

    前面的章节我们说到了本地文件在发送给浏览器之前是会根据文件类型做不同的 transfrom 代码转换的。

    Vite 源码解析最终章之组件渲染篇

    观察一下我们实际在浏览器中加载的文件内容。可以看到表面我们加载的是一个 .vue 组件。但是文件的实际内容还是传统的 .js 文件,并且 Content-Type 也是 application/javascript; charset=utf-8 所以浏览器才能够直接运行该文件。并且我们可以发现不同的文件类型后面跟的 query type 参数也不一样。有 ?type=template ?type=import。下面让我们来具体分析一下一个 Vue 组件是如何在浏览器中被渲染的吧

    处理 css 文件

    浏览器是不支持直接 import 导入 .css 文件的。如果你配置过 webpack 来处理 css 文件,那么你应该清楚这类问题的解决方式要么是将 css 编译成 js 文件,要么是把组件中的 css 单独提取为 css文件通过 link 标签来进行加载。Vite 在本地开发时采用的是第一种方式,在生产环境构建时仍然是编译成独立的 css 文件进行加载。

    挂载样式

    Vite 使用 serverPluginCss 插件来处理形如 http://localhost:3000/src/index.css?import 这样的以.css为后缀结尾且 query 包含 import 的请求。

    // src/node/server/serverPluginCss.ts
    const id = JSON.stringify(hash_sum(ctx.path))
    if (isImportRequest(ctx)) {
        const { css, modules } = await processCss(root, ctx) // 这里主要对css文件做一些预处理之类的操作如 less->css, postcss之类的处理不在此处详细展开
        console.log(modules)
        ctx.type = 'js'
        ctx.body = codegenCss(id, css, modules)
    }
    export function codegenCss(
      id: string,
      css: string,
      modules?: Record<string, string>
    ): string {
      let code =
        `import { updateStyle } from "${clientPublicPath}"\n` +
        `const css = ${JSON.stringify(css)}\n` +
        `updateStyle(${JSON.stringify(id)}, css)\n`
      if (modules) {
        code += `export default ${JSON.stringify(modules)}`
      } else {
        code += `export default css`
      }
      return code
    }
    

    在上面的代码中,我们劫持了 css 文件的请求以及重写了请求的响应。将 css 文件改写为 esmodule 格式的js文件。如果启用了 css-modules 则我们导出一个具体对象。因为组件需要使用 :id=styles.xxx 的形式来引用。对于普通的 css 文件则无需导出具体有意义的对象。这里的核心方法是 updateStyle,让我们来看看这个方法到底干了什么。

    updateStyle

    http://localhost:3000/src/index.css?import 请求的详细响应信息可以看出。实质是 Vite 是通过 updateStyle 这个方法来将 css 字符串挂载到具体的 dom 元素上

    Vite 源码解析最终章之组件渲染篇

    export function updateStyle(id: string, content: string) {
      let style = sheetsMap.get(id)
      if (supportsConstructedSheet && !content.includes('@import')) {
        if (style && !(style instanceof CSSStyleSheet)) {
          removeStyle(id)
          style = undefined
        }
    
        if (!style) {
          style = new CSSStyleSheet()
          style.replaceSync(content)
          // @ts-ignore
          document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
        } else {
          style.replaceSync(content)
        }
      } else {
        if (style && !(style instanceof HTMLStyleElement)) {
          removeStyle(id)
          style = undefined
        }
    
        if (!style) {
          style = document.createElement('style')
          style.setAttribute('type', 'text/css')
          style.innerHTML = content
          document.head.appendChild(style)
        } else {
          style.innerHTML = content
        }
      }
      sheetsMap.set(id, style)
    }
    

    updateStyle 中用到的核心 API 是 CSSStyleSheet 首先我们在 supportsConstructedSheet 中判断了当前浏览器是否支持 CSSStyleSheet, 如果不支持则采用 style 标签插入的形式挂载样式。 如果支持则创建 CSSStyleSheet 实例。接着将编译后的 css 字符串传入 CSSStyleSheet 实例对象。再将该对象添加进 document.adoptedStyleSheet 就可以让我们的样式生效啦

    解析 Vue 文件

    Vite 在解析 .vue 文件主要做的还是 code transform 将 vue 组件解析成调用 compile 以及 render 方法的 js 文件。具体的 compile 以及 render 的核心逻辑还是在 vue3 的 api 中完成。

    // src/node/server/serverPluginVue.ts
    const descriptor = await parseSFC(root, filePath, ctx.body)
    

    首先用官方提供的库来将单文件组件编译成 descriptor。以 App.vue 为例这里我们摘出比较重要的信息省略 sourcemap信息。

    // src/App.vue
    {
      filename: '/Users/yuuang/Desktop/github/vite_test/src/App.vue',
      source: '<template>\n' +
        '  <img  src="./assets/logo.png" :class="style.big"/>\n' +
        '  <div class="small">\n' +
        '    small1\n' +
        '  </div>\n' +
        '  <HelloWorld msg="Hello Vue 3.0 + Vite" />\n' +
        '</template>\n' +
        '\n' +
        '<script>\n' +
        "import HelloWorld from './components/HelloWorld.vue'\n" +
        "import style from './index.module.css'\n" +
        '\n' +
        'export default {\n' +
        "  name: 'App',\n" +
        '  components: {\n' +
        '    HelloWorld\n' +
        '  },\n' +
        '  data() {\n' +
        '    return {\n' +
        '      style: style\n' +
        '    }\n' +
        '  },\n' +
        '  mounted () {\n' +
        "    console.log('mounted')\n" +
        '  }\n' +
        '}\n' +
        '</script>\n' +
        '\n' +
        '<style>\n' +
        '.small {\n' +
        '  width:21px\n' +
        '}\n' +
        '</style>\n' +
        '\n',
      template: {
        type: 'template',
        content: '\n' +
          '  <img  src="./assets/logo.png" :class="style.big"/>\n' +
          '  <div class="small">\n' +
          '    small1\n' +
          '  </div>\n' +
          '  <HelloWorld msg="Hello Vue 3.0 + Vite" />\n',
        loc: {
          source: '\n' +
            '  <img  src="./assets/logo.png" :class="style.big"/>\n' +
            '  <div class="small">\n' +
            '    small1\n' +
            '  </div>\n' +
            '  <HelloWorld msg="Hello Vue 3.0 + Vite" />\n',
          start: [Object],
          end: [Object]
        },
        attrs: {},
        map: xxx
      },
      script: {
        type: 'script',
        content: '\n' +
          "import HelloWorld from './components/HelloWorld.vue'\n" +
          "import style from './index.module.css'\n" +
          '\n' +
          'export default {\n' +
          "  name: 'App',\n" +
          '  components: {\n' +
          '    HelloWorld\n' +
          '  },\n' +
          '  data() {\n' +
          '    return {\n' +
          '      style: style\n' +
          '    }\n' +
          '  },\n' 
          '}\n',
        loc: {
          source: '\n' +
            "import HelloWorld from './components/HelloWorld.vue'\n" +
            "import style from './index.module.css'\n" +
            '\n' +
            'export default {\n' +
            "  name: 'App',\n" +
            '  components: {\n' +
            '    HelloWorld\n' +
            '  },\n' +
            '  data() {\n' +
            '    return {\n' +
            '      style: style\n' +
            '    }\n' +
            '  },\n' 
            '}\n',
          start: [Object],
          end: [Object]
        },
        attrs: {},
        map: xxx
      },
      scriptSetup: null,
      styles: [
        {
          type: 'style',
          content: '\n.small {\n  width:21px\n}\n',
          loc: [Object],
          attrs: {},
          map: [Object]
        }
      ],
      customBlocks: []
    }
    

    通过上述代码我们可以解析出一个组件的 descriptor。拿到解析结果后继续往下判断

    if (!query.type) {
          // watch potentially out of root vue file since we do a custom read here
          watchFileIfOutOfRoot(watcher, root, filePath)
    
          if (descriptor.script && descriptor.script.src) {
            filePath = await resolveSrcImport(
              root,
              descriptor.script,
              ctx,
              resolver
            )
          }
          ctx.type = 'js'
          const { code, map } = await compileSFCMain(
            descriptor,
            filePath,
            publicPath,
            root
          )
          ctx.body = code
          ctx.map = map
          return etagCacheCheck(ctx)
        }
    

    首先是针对没有特定 query type 的请求。我们首先判断 script 标签有没有 src 属性。例如 <script src="./app.js"></script> 将逻辑单独拆出一个文件进行维护。这种写法在 Vue 中比较少见也不推荐。Vue 推崇单文件组件的写法。接下来就是比较核心的 compileSFCMain 方法了

    if (script) {
      content = script.content
      map = script.map
      if (script.lang === 'ts') {
        const res = await transform(content, publicPath, {
          loader: 'ts'
        })
        content = res.code
        map = mergeSourceMap(map, JSON.parse(res.map!))
      }
    }
    /**
     * Utility for rewriting `export default` in a script block into a varaible
     * declaration so that we can inject things into it
     */
    code += rewriteDefault(content, '__script')
    

    首先判断如果是 ts 文件则先调用 esbuild 来将 ts 转换为 js。接着使用 rewriteDefault 来重写 export default 的内容到一个变量中。在这里是 __script。做完之后 code 的内容为

    import HelloWorld from './components/HelloWorld.vue'
    import style from './index.module.css'
    
    const __script = {
      name: 'App',
      components: {
        HelloWorld
      },
      data() {
        return {
          style: style
        }
      }
    }
    
    
    import HelloWorld from './components/HelloWorld.vue'
    import style from './index.module.css'
    
    const __script = {
      name: 'App',
      components: {
        HelloWorld
      },
      data() {
        return {
          style: style
        }
      }
    }
    
    import { render as __render } from "/src/App.vue?type=template"
    __script.render = __render
    __script.__hmrId = "/src/App.vue"
    __script.__file = "/Users/yuuang/Desktop/github/vite_test/src/App.vue"
    export default __script
    

    接着我们判断组件当中有没有 style 标签

    if (descriptor.styles) {
        descriptor.styles.forEach((s, i) => {
          const styleRequest = publicPath + `?type=style&index=${i}`
          if (s.scoped) hasScoped = true
          if (s.module) {
            if (!hasCSSModules) {
              code += `\nconst __cssModules = __script.__cssModules = {}`
              hasCSSModules = true
            }
            const styleVar = `__style${i}`
            const moduleName = typeof s.module === 'string' ? s.module : '$style'
            code += `\nimport ${styleVar} from ${JSON.stringify(
              styleRequest + '&module'
            )}`
            code += `\n__cssModules[${JSON.stringify(moduleName)}] = ${styleVar}`
          } else {
            code += `\nimport ${JSON.stringify(styleRequest)}`
          }
        })
        if (hasScoped) {
          code += `\n__script.__scopeId = "data-v-${id}"`
        }
      }
    

    首先判断有没有使用 css-modules 例如 <style module> xxx </style>。如果使用了则在 code 中注入 \nconst __cssModules = __script.__cssModules = {} 并且在 script 头部插入 import $style from './index.module.css' 这样的代码。这样我们就可以直接在 template 中使用 style.xxx来使用cssmodules。如果是普通的样式文件则直接注入\nimportstyle.xxx 来使用 css-modules。 如果是普通的样式文件则直接注入 `\nimport style.xxx来使用css−modules。如果是普通的样式文件则直接注入‘\nimport{JSON.stringify(styleRequest)}在 App.vue 中则是import "/src/App.vue?type=style&index=0"`

    if (descriptor.template) {
        const templateRequest = publicPath + `?type=template`
        code += `\nimport { render as __render } from ${JSON.stringify(
          templateRequest
        )}`
        code += `\n__script.render = __render`
      }
    

    接下来判断如果有 template 则注入 type 为 template 的 import 脚本用来发起请求。在 App.vue 中是 import {render as __render} from "/src/App.vue?type=template"
    最后注入一些辅助信息

    code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`
      code += `\n__script.__file = ${JSON.stringify(filePath)}`
      code += `\nexport default __script`
    

    最终的完整信息如下

    import HelloWorld from '/src/components/HelloWorld.vue'
    import style from '/src/index.module.css?import'
    
    const __script = {
        name: 'App',
        components: {
            HelloWorld
        },
        data() {
            return {
                style: style
            }
        }
    }
    
    import "/src/App.vue?type=style&index=0"
    import {render as __render} from "/src/App.vue?type=template"
    __script.render = __render
    __script.__hmrId = "/src/App.vue"
    __script.__file = "/Users/yuuang/Desktop/github/vite_test/src/App.vue"
    export default __script
    

    拿到 compileSFCMain 的返回结果后。返回给 ctx.body 渲染。接着由于我们在上面将请求类型划分为了 template, style 接下来看看我们如何处理各种类型的请求。

    if (query.type === 'template') {
          const templateBlock = descriptor.template!
          if (templateBlock.src) {
            filePath = await resolveSrcImport(root, templateBlock, ctx, resolver)
          }
          ctx.type = 'js'
          const cached = vueCache.get(filePath)
          const bindingMetadata = cached && cached.script && cached.script.bindings
          const vueSpecifier = resolveBareModuleRequest(
            root,
            'vue',
            publicPath,
            resolver
          )
          const { code, map } = compileSFCTemplate(
            root,
            templateBlock,
            filePath,
            publicPath,
            descriptor.styles.some((s) => s.scoped),
            bindingMetadata,
            vueSpecifier,
            config
          )
          ctx.body = code
          ctx.map = map
          return etagCacheCheck(ctx)
        }
    

    当 type 为 template 时,我们只需要取 descriptor 的 template 字段。首先判断有没有 src 属性来引用单独拆出来的文件。这种情况使用的也比较少。
    核心的方法调用是 compileSFCTemplate 来将 template 编译成官方提供的 h函数(render 函数)来生成 vnode 进行渲染。

    if (query.type === 'style') {
          const index = Number(query.index)
          const styleBlock = descriptor.styles[index]
          if (styleBlock.src) {
            filePath = await resolveSrcImport(root, styleBlock, ctx, resolver)
          }
          const id = hash_sum(publicPath)
          const result = await compileSFCStyle(
            root,
            styleBlock,
            index,
            filePath,
            publicPath,
            config
          )
          ctx.type = 'js'
          ctx.body = codegenCss(`${id}-${index}`, result.code, result.modules)
          return etagCacheCheck(ctx)
        }
    

    类型为 style 的请求类似。我们只需要使用 descriptor 的 styles 字段。使用 compileSFCStyle 来进行一些 @import 关键字的处理。最后使用 codegenCss 在外面包上一层调用客户端提供的 updateStyle 方法即可。


    起源地下载网 » Vite 源码解析最终章之组件渲染篇

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元