<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【工程化】刨根問底,揭開 Vue 中 Scope CSS 實(shí)現(xiàn)的幕后(原理)

          共 12198字,需瀏覽 25分鐘

           ·

          2021-03-25 14:29

          前言

          我想大家應(yīng)該都對 Vue 的 Scope CSS 耳熟能詳了,但是說起 Vue 的 Scope CSS 實(shí)現(xiàn)的原理,很多人應(yīng)該會(huì)說不就是給 HTML、CSS 添加屬性嗎 ????

          確實(shí)是這樣的,不過這只是最終 Scope CSS 呈現(xiàn)的結(jié)果。而這個(gè)過程又是如何實(shí)現(xiàn)的?我想能回答上一二的同學(xué)應(yīng)該不多。

          那么,回到今天本文,我將會(huì)圍繞以下 3 點(diǎn),和大家一起從 Vue 的 Scope CSS 的最終呈現(xiàn)結(jié)果出發(fā),深入淺出一番其實(shí)現(xiàn)的底層原理:

          • 什么是 Scope CSS
          • vue-loader 處理組件(.vue 文件)
          • Patch 階段應(yīng)用 ScopeId 生成 HTML 的屬性

          1 什么是 Scope CSS

          Scope CSS 即作用域 CSS,組件化所密不可分的一部分。Scope CSS 使得我們可以在各組件中定義的 CSS 不產(chǎn)生污染。例如,我們在 Vue 中定義一個(gè)組件:

          <!-- App.vue -->
          <template>
            <div class="box">scoped css</div>
          </template>
          <script>
            export default {};
          </script>
          <style scoped>
            .box {
              width200px;
              height200px;
              background#aff;
            }
          </style>

          通常情況下,在開發(fā)環(huán)境我們的組件會(huì)先經(jīng)過 vue-loader[1] 的處理,然后結(jié)合運(yùn)行時(shí)的框架代碼渲染到頁面上。相應(yīng)地,它們對應(yīng)的 HTML 和 CSS 分別會(huì)是這樣:

          HTML 部分:

          <div data-v-992092a6>scoped css</div>

          CSS 部分:

          .box[data-v-992092a6] {
            width200px;
            height200px;
            background#aff;
          }

          可以看到 Scope CSS 的本質(zhì)是基于 HTML 和 CSS 選擇器的屬性,通過分別給 HTML 標(biāo)簽和 CSS 選擇器添加 data-v-xxxx 屬性的方式實(shí)現(xiàn)。

          2 vue-loader 處理組件(.vue 文件)

          前面,我們也提及了在開發(fā)環(huán)境下一個(gè)組件(.vue 文件)會(huì)先由 vue-loader 來處理。那么,針對 Scope CSS 而言,vue-loader 會(huì)做這 3 件事:

          • 解析組件,提取出 template、scriptstyle 對應(yīng)的代碼塊
          • 構(gòu)造并導(dǎo)出 export 組件實(shí)例,在組件實(shí)例的選項(xiàng)上綁定 ScopId
          • style 的 CSS 代碼進(jìn)行編譯轉(zhuǎn)化,應(yīng)用 ScopId 生成選擇器的屬性

          注意,這里講的只是 vue-loader 對 .vue 文件的處理部分,不涉及 HMR、配合 Devtool 的邏輯,有興趣的同學(xué)可以自行了解~

          然而,之所以 vue-loader 有這么多的能力,主要是因?yàn)?vue-loader 的底層使用了 Vue 官方提供的包(package) @vue/component-compiler-utils[2],其提供了解析組件(.vue 文件)、編譯模版 template、編譯 style等 3 種能力。

          那么,下面我們就先來看一下 vue-loader 是如何使用 @vue/component-compiler-utils 來解析組件提取 template、script、style 的。

          2.1 提取 template、script、style

          vue-loader 提取 template、script、style 的過程主要是使用了 @vue/component-compiler-utilsparse 方法,這個(gè)過程對應(yīng)的代碼(偽代碼)會(huì)是這樣:

          // vue-loader/lib/index.js
          const { parse } = require("@vue/component-compiler-utils");

          module.exports = function (source{
            const loaderContext = this;
            const { sourceMap, rootContext, resourcePath } = loaderContext;
            const sourceRoot = path.dirname(path.relative(context, resourcePath));
            const descriptor = parse({
              source,
              compilerrequire("vue-template-compiler"),
              filename,
              sourceRoot,
              needMap: sourceMap,
            });
          };

          我們來逐點(diǎn)分析一下這段代碼,首先,會(huì)獲取當(dāng)前上下文 loaderContext,它會(huì)包含 webpack 打包過程核心對象 compilercompilation 等。

          其次,再構(gòu)建文件資源入口 sourceRoot,一般情況下它指的是 src 文件目錄,它主要用于構(gòu)建 source-map 使用。

          最后,則會(huì)使用 @vue/component-compiler-utils 提供的 parse 方法來解析 source(組件代碼)。這里,我們來看一下 parse 方法的幾個(gè)參數(shù):

          • soruce 源代碼塊,這里是組件對應(yīng)的代碼,即包含了 templatestyle、script
          • compiler 編譯核心對象,它是一個(gè) CommonJS 模塊(vue-template-compiler[3]),parse 方法內(nèi)部會(huì)使用它提供的 parseComponent 方法來解析組件
          • filename 當(dāng)前組件的文件名,例如 App.vue
          • sourceRoot 文件資源入口,用于構(gòu)建 source-map 使用
          • needMap 是否需要 source-map,parse 方法內(nèi)部會(huì)根據(jù) needMap 的值(truefalse,默認(rèn)為 true)來判斷是否生成 script、style 對應(yīng)的 source-map

          parse 方法的執(zhí)行則會(huì)返回一個(gè)對象給 desciptor,它會(huì)包含 templatestyle、script 分別對應(yīng)的代碼塊。

          那么,可以看到的是 vue-loader 解析組件的過程,幾乎外包給了 Vue 提供的工具包(package)。并且,我想這個(gè)時(shí)候肯定會(huì)有同學(xué)問:這些和 Vue 的 Scope CSS 有幾毛錢關(guān)系 ????

          有很大的關(guān)系!因?yàn)?Vue 的 Scope CSS 可不是無米之炊,它實(shí)現(xiàn)的前提是組件被解析了,然后再分別處理 templatestyle 部分的代碼!

          那么,顯然到這里我們已經(jīng)完成了對組件的解析。接著,則需要構(gòu)造和導(dǎo)出組件實(shí)例~

          2.2 構(gòu)造和導(dǎo)出組件實(shí)例

          vue-loader 在解析完組件后,會(huì)分別處理并生成 template、script、style 的導(dǎo)入 import 語句,再調(diào)用 normalizer 方法正?;╪ormalizer)組件,最后將它們拼接成代碼字符串:

          let templateImport = `var render, staticRenderFns`;
          if (descriptor.template) {
            // 構(gòu)造 template 的 import 語句
          }
          let scriptImport = `var script = {}`;
          if (descriptor.script) {
            // 構(gòu)造 script 的 import 語句
          }
          let stylesCode = ``;
          if (descriptor.styles.length) {
            // 構(gòu)造 style 的 import 語句
          }
          let code =
            `
          ${templateImport}
          ${scriptImport}
          ${stylesCode}

          import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
          var component = normalizer(
            script,
            render,
            staticRenderFns,
            ${hasFunctional ? `true` : `false`},
            ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
            ${hasScoped ? JSON.stringify(id) : `null`},
            ${isServer ? JSON.stringify(hash(request)) : `null`}
            ${isShadow ? `,true` : ``}
          )
            `
          .trim() + `\n`;

          其中,templateImport、scriptImport、stylesCode 等構(gòu)造好的 template、scriptstyle 部分的導(dǎo)入 import 語句看起來會(huì)是這樣:

          import {
            render,
            staticRenderFns,
          from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&";
          import script from "./App.vue?vue&type=script&lang=js&";
          // 兼容命名方式的導(dǎo)出
          export * from "./App.vue?vue&type=script&lang=js&";
          import style0 from "./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css&";

          不知道同學(xué)們注意 ?? 到?jīng)],templatestyle 的導(dǎo)入 import 語句都有這么一個(gè)共同的部分 id=7ba5bd90&scoped=true,這表示此時(shí)組件的 templatestyle 是需要 Scope CSS 的,并且 scopeId7ba5bd90。

          當(dāng)然,這僅僅是告知后續(xù)的 templatestyle 編譯時(shí)需要注意生成 Scope CSS,也是實(shí)現(xiàn) Scope CSS 的第一步!那么,接著則會(huì)調(diào)用 normalizer 方法來對該組件進(jìn)行正?;∟ormalizer)處理:

          import normalizer from "!../node_modules/vue-loader/lib/runtime/componentNormalizer.js";
          var component = normalizer(
            script,
            render,
            staticRenderFns,
            false,
            null,
            "7ba5bd90",
            null
          );
          export default component.exports;

          注意,normalizer 是重命名了原方法 normalizeComponent,以下統(tǒng)稱 normalizeComponent~

          我想同學(xué)們應(yīng)該都注意到了,此時(shí) scopeId 會(huì)作為參數(shù)傳給 normalizeComponent 方法,而傳給 normalizeComponent 的目的則是為了在組件實(shí)例的 options綁定 scopeId。那么,我們來看一下 normalizeComponent 方法(偽代碼):

          function normalizeComponent (
            scriptExports,
            render,
            staticRenderFns,
            functionalTemplate,
            injectStyles,
            scopeId,
            moduleIdentifier, /* server only */
            shadowMode /* vue-cli only */
          {
            ...
            var options = typeof scriptExports === 'function'
              ? scriptExports.options
              : scriptExports
            // scopedId
            if (scopeId) {
              options._scopeId = 'data-v-' + scopeId
            }
            ...
          }

          可以看到,這里的 options._scopeId 會(huì)等于 data-v-7ba5bd90,而它的作用主要是用于在 patch 的時(shí)候,為當(dāng)前組件的 HTML 標(biāo)簽添加名為 data-v-7ba5bd90 的屬性。因此,這也是 template 為什么會(huì)形成帶有 scopeId 的真正所在!

          2.3 編譯樣式 Style,應(yīng)用 ScopId 生成選擇器的屬性

          在構(gòu)造完 Style 對應(yīng)的導(dǎo)入語句后,由于此時(shí) import 語句中的 query 包含 vue,則會(huì)被 vue-loader 內(nèi)部的 Pitching Loader 處理。而 Pitching Loader 則會(huì)重寫 import 語句,拼接上內(nèi)聯(lián)(inline)的 Loader,這看起來會(huì)是這樣:

          export * from '
          "-!../node_modules/vue-style-loader/index.js??ref--6-oneOf-1-0
          !../node_modules/css-loader/dist/cjs.js??ref--6-oneOf-1-1
          !../node_modules/vue-loader/lib/loaders/stylePostLoader.js
          !../node_modules/postcss-loader/src/index.js??ref--6-oneOf-1-2
          !../node_modules/cache-loader/dist/cjs.js??ref--0-0
          !../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css&"
          '

          然后,webpack 會(huì)解析出模塊所需要的 Loader,顯然這里會(huì)解析出 6 個(gè) Loader:

          [
            { loader"vue-style-loader"options"?ref--6-oneOf-1-0" },
            { loader"css-loader"options"?ref--6-oneOf-1-1" },
            { loader"stylePostLoader"optionsundefined },
            { loader"postcss-loader"options"?ref--6-oneOf-1-2" },
            { loader"cache-loader"options"?ref--0-0" },
            { loader"vue-loader"options"?vue-loader-options" }
          ]

          那么,此時(shí) webpack 則會(huì)執(zhí)行這 6 個(gè) Loader(當(dāng)然還有解析模塊本身)。并且,這里會(huì)忽略 webpack.config.js 中符合規(guī)則的 Normal Loader(vue-style-loader 還會(huì)忽略前置 Loader)。

          不了解內(nèi)聯(lián) Loader 的同學(xué),可以看一下這篇文章【webpack進(jìn)階】你真的掌握了loader么?- loader十問[4]

          而對于 Scope CSS 而言,密切相關(guān)的就是 stylePostLoader。下面,我們來看一下 stylePostLoader 的定義:

          const { compileStyle } = require("@vue/component-compiler-utils");
          module.exports = function (source, inMap{
            const query = qs.parse(this.resourceQuery.slice(1));
            const { code, map, errors } = compileStyle({
              source,
              filenamethis.resourcePath,
              id`data-v-${query.id}`,
              map: inMap,
              scoped: !!query.scoped,
              trimtrue,
            });

            if (errors.length) {
              this.callback(errors[0]);
            } else {
              this.callback(null, code, map);
            }
          };

          從 stylePostLoader 的定義中,我們知道它是使用了 @vue/component-compiler-utils 提供的 compileStyle 方法來完成對組件 style 的編譯。并且,此時(shí)會(huì)傳入?yún)?shù) iddata-v-${query.id},即 data-v-7ba5bd90,而這也是 style 中聲明的選擇器的屬性為 scopeId 的關(guān)鍵點(diǎn)!

          而在 compileStyle 函數(shù)內(nèi)部,則是使用的我們所熟知 postcss[5] 來完成對 style 代碼的編譯和構(gòu)造選擇器的 scopeId 屬性。至于如何使用 postcss 完成這個(gè)過程,這里就不做過多介紹,有興趣的同學(xué)自行了解哈~

          3 Patch 階段應(yīng)用 ScopeId 生成 HTML 的屬性

          不知道同學(xué)們是否還記得在 2.2 構(gòu)造并導(dǎo)出組件實(shí)例的時(shí)候,我們講了在組件實(shí)例的 options 上綁定 _scopeId 是實(shí)現(xiàn) template 的 Scope 的關(guān)鍵點(diǎn)!但是,當(dāng)時(shí)我們并沒有介紹這個(gè) _scopeId 到底是如何應(yīng)用到 template 上的元素的 ???

          如果,你想在 vue-loader 或者 @vue/component-compiler-utils 的代碼中找到這個(gè)答案,我可以和你說找一萬年都找不到! 因?yàn)?,真正?yīng)用 _scopeId 的過程是發(fā)生在 Vue 運(yùn)行時(shí)的框架代碼中(沒想到吧 ??)。

          了解過 Vue 模版編譯過程的同學(xué),我想應(yīng)該都知道 template 會(huì)被編譯成 render 函數(shù),然后會(huì)根據(jù) render 函數(shù)創(chuàng)建對應(yīng)的 VNode,最后再根據(jù) VNode 渲染成真實(shí)的 DOM 在頁面上:

          而 VNode 到真實(shí) DOM 這個(gè)過程是由 patch 方法完成的。假設(shè),此時(shí)我們是第一次渲染 DOM,這在 patch 方法中會(huì)命中 isUndef(oldVnode)true 的邏輯:

          function patch (oldVnode, vnode, hydrating, removeOnly{
            if (isUndef(oldVnode)) {
              // empty mount (likely as component), create new root element
              isInitialPatch = true
              createElm(vnode, insertedVnodeQueue)
            } 
          }

          因?yàn)榈谝淮武秩?DOM,所以壓根不存在什么 oldVnode ??

          可以看到,此時(shí)會(huì)執(zhí)行 createElm 方法。而在 createElm 方法中則會(huì)創(chuàng)建 VNode 對應(yīng)的真實(shí) DOM,并且它還做了一件很重要的事,調(diào)用 setScope 方法應(yīng)用 _scopeId 在 DOM 上生成 data-v-xxx 的屬性!對應(yīng)的代碼(偽代碼):

          // packages/src/core/vdom/patch.js
          function createElm(
            vnode,
            insertedVnodeQueue,
            parentElm,
            refElm,
            nested,
            ownerArray,
            index
          {
            ...
            setScope(vnode);
            ...
          }

          setScope 方法中則會(huì)使用組件實(shí)例的 options._scopeId 作為屬性來添加到 DOM 上,從而生成了 template 中的 HTML 標(biāo)簽上名為 data-v-xxx 的屬性。并且,這個(gè)過程會(huì)由 Vue 封裝好的工具函數(shù) nodeOps.setStyleScope 完成,它的本質(zhì)是調(diào)用 DOM 對象的 setAttribute 方法:

          // src/platforms/web/runtime/node-ops.js
          export function setStyleScope (node: Element, scopeId: string{
            node.setAttribute(scopeId, '')
          }

          結(jié)語

          如果,有在網(wǎng)上查找過關(guān)于 vue-loader 和 Scope CSS 的文章的同學(xué)會(huì)發(fā)現(xiàn),很多文章都是說在 @vue/component-compiler-utilscompilerTemplate 方法中應(yīng)用 scopeId 生成了 template 中 HTML 標(biāo)簽的屬性。但是,通過閱讀本文,我們會(huì)發(fā)現(xiàn)這兩者壓根沒有任何關(guān)系(SSR 的情況除外)!

          并且,我想同學(xué)們也注意到了一點(diǎn),本文中提及的 Vue 運(yùn)行時(shí)框架的代碼是 Vue 2.x 版本的(不是 Vue3)。所以,有興趣的同學(xué)可以借助本文提供的路線推敲一下 Vue3 中 Scope CSS 的過程,相信你會(huì)收獲滿滿 ??。最后,如果本文中存在表達(dá)不當(dāng)或錯(cuò)誤的地方,歡迎各位同學(xué)提 Issue~

          點(diǎn)贊 ??、在看 ??

          通過閱讀本篇文章,如果有收獲的話,可以點(diǎn)個(gè)贊在看,這將會(huì)成為我持續(xù)分享的動(dòng)力,感謝~

          參考資料

          [1]

          vue-loader: https://github.com/vuejs/vue-loader

          [2]

          @vue/component-compiler-utils: https://github.com/vuejs/component-compiler-utils

          [3]

          vue-template-compiler: https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#readme

          [4]

          【webpack進(jìn)階】你真的掌握了loader么?- loader十問: https://juejin.cn/post/6844903693070909447

          [5]

          postcss: https://postcss.org/api/


          歡迎關(guān)注「前端雜貨鋪」,一個(gè)有溫度且致力于前端分享的雜貨鋪

          關(guān)注回復(fù)「加群」,可加入雜貨鋪一起交流學(xué)習(xí)成長


          瀏覽 49
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美高潮AAAAAA片 | 成人操碰视频 | 操逼网站无需下载在线观看 | 爽 好紧 别夹 喷水欧美 | 粉嫩小泬BBBB免费看 |