<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>

          【總結(jié)】1259- Vite 插件開發(fā)實踐:微前端的資源處理

          共 5799字,需瀏覽 12分鐘

           ·

          2022-03-16 02:23

          最近實現(xiàn)的簡單、透明、組件化微前端方案總體感覺不錯,也收到了很多人的反饋,很具有學習參考價值。

          但有不少朋友使用該方案打包配置出現(xiàn)了一些問題,做事應有始有終,挖的坑總得完善一下。今天分享一下 Vite 針對微應用方案插件開發(fā)歷程。

          通過文章你可以學到:

          1. 寫一個 Vite 插件
          2. 通過 rollup 編譯生成一個單獨的資源文件
          3. import 資源路徑處理
          4. rollup 一些配置含義
          5. 一些解決問題的思路

          問題點

          總結(jié)下來,在 Vite 中使用該微前端方案會遇到如下問題:

          1. Vite 打包后的資源默認是以 HTML 為入口,我們的微前端方案需要以 JS 為入口
          2. JS 為入口方案打包導出代碼被移除掉了
          3. import.meta 語句打包被轉(zhuǎn)譯成 {} 空對象了
          4. chunk 分離后的 CSS 文件,Vite 默認以 document.head.appendChild 處理
          5. 打包后的 CSS 文件默認在 main.js 中沒有引用
          6. 資源路徑手動寫 new URL(image, import.meta.url) 太繁瑣

          通過配置解決問題

          首先前三個問題可以通過 Vite 解決。Vite 兼容了 rollup 的配置

          問題一,修改 JS 入口則需要修改 Vite 配置,設置 build.rollupOptions.inputsrc/main.tsx,這樣 Vite 會默認以自定義配置的 main.tsx 為入口文件做打包處理,不再生成 index.html

          問題二,rollup 的一個特性默認會清理掉入口文件的導出模塊,可以配置 preserveEntrySignatures: 'allow-extension' 來保證打包之后 export 的模塊不被移除掉。

          問題三,看了 ViteIssue,很多人遇到了這個問題,最初以為是 Vite 默認對它做了處理,后面看了 Vite 源碼也沒有發(fā)現(xiàn)處理的邏輯所在,應該是被 esbuild 做了轉(zhuǎn)譯。因此將 build.target 設置為 esnext 即可解決問題,即 import.meta 屬于 es2020,設置為具體的 es2020 也行。

          配置:

          export default defineConfig({
          build: {
          // es2020 支持 import.meta 語法
          target: 'es2020',
          rollupOptions: {
          // 用于控制 Rollup 嘗試確保入口塊與基礎入口模塊具有相同的導出
          preserveEntrySignatures: 'allow-extension',
          // 入口文件
          input: 'src/main.tsx',
          },
          },
          });

          寫 Vite 插件

          我們可以寫一個插件將上面的配置封裝。

          一個普通的 Vite 插件很簡單

          defineConfig({
          plugins: [
          {
          // 可以使用 Vite 和 rollup 提供的鉤子
          },
          ],
          });

          插件可以做很多事情,通過 Viterollup 提供的鉤子對代碼解析、編譯、打包、輸出的整體流程進行自定義處理。

          插件一般不直接寫在 vite.config.ts 中,可以定義一個方法導出這個插件,這里可以用 config 這個鉤子來提供默認的 Vite 配置,將自定義的配置進行封裝:

          export function microWebPlugin(): Plugin {
          // 插件鉤子
          return {
          name: 'vite-plugin-micro-web',
          config() {
          return {
          build: {
          target: 'es2020',
          rollupOptions: {
          preserveEntrySignatures: 'allow-extension',
          input: 'src/main.tsx',
          },
          },
          };
          },
          };
          }

          這樣一個簡單的插件就完成了。

          Vite 獨有鉤子

          • config - 在解析 Vite 配置前調(diào)用,它可以返回一個將被深度合并到現(xiàn)有配置中的部分配置對象,或者直接改變配置
          • configResolved - 在解析 Vite 配置后調(diào)用,使用這個鉤子讀取和存儲最終解析的配置
          • configureServer - 是用于配置開發(fā)服務器的鉤子
          • transformIndexHtml - 轉(zhuǎn)換 index.html 的專用鉤子。鉤子接收當前的 HTML 字符串和轉(zhuǎn)換上下文
          • handleHotUpdate - 執(zhí)行自定義 HMR 更新處理。

          rollup 鉤子

          rollup 鉤子非常多,一共分兩個階段

          編譯階段:


          輸出階段:

          這里我們會用到的鉤子有:

          • transform - 用于轉(zhuǎn)換已加載的模塊內(nèi)容
          • generateBundle - 已經(jīng)編譯過的代碼塊生成階段

          樣式插入節(jié)點處理

          問題四,document.head.appendChild 處理

          1. 使用 transform 鉤子,替換 Vite 默認的 document.head.appendChild 為自定義節(jié)點
          2. cssCodeSplit 打包為一個 CSS 文件

          我們默認采用 cssCodeSplit 打包為一個 CSS 文件,免去了用插件 transform 修改 Vite 的邏輯。

          問題五,即打包后的 CSS 沒有引用的問題,獲取這個帶 hashCSS 我們可以有多種解決方案

          1. 使用 HTML 打包模式,抽取 index.html 中的 JSCSS 文件再單獨處理
          2. 不添加樣式文件名 hash ,通過約定固定該樣式名稱
          3. 通過鉤子提取文件名處理

          權(quán)衡之下,最終采用 generateBundle 階段提取 Vite 編譯生成的 CSS 文件名,通過修改入口代碼將其插入。但 generateBundle 已經(jīng)在輸出階段,不會再走 transform 鉤子。

          發(fā)現(xiàn)一個兩全其美的辦法:創(chuàng)建極小的入口文件 main.js,還可以配合 hash 和主應用時間戳緩存處理。

          async generateBundle(options, bundle) {
          // 主入口文件
          let entry: string | undefined;
          // 所有的 CSS 模塊
          const cssChunks: string[] = [];
          // 找出入口文件和 CSS 文件
          for (const chunkName of Object.keys(bundle)) {
          if (chunkName.includes('main') && chunkName.endsWith('.js')) {
          entry = chunkName;
          }
          if (chunkName.endsWith('.css')) {
          // 使用相對路徑,避免后續(xù) ESM 無法解析模塊
          cssChunks.push(`./${chunkName}`);
          }
          }
          // 接下面代碼
          },

          生成新的入口文件

          通過 bundle 提取可以獲取到帶 hashJSCSS 入口文件了。現(xiàn)在需要寫入一個新的文件 main.jsrollup 中有個 API emitFile 可以觸發(fā)創(chuàng)建一個資源文件。

          接下來對它進行處理:

          // 接上面代碼
          if (entry) {
          const cssChunksStr = JSON.stringify(cssChunks);

          // 創(chuàng)建極小的入口文件,配合 hash 和主應用時間戳緩存處理
          this.emitFile({
          fileName: 'main.js',
          type: 'asset',
          source: `
          // 帶上 microAppEnv 參數(shù),使用相對路徑避免報錯
          import defineApp from './${entry}?microAppEnv';

          // 創(chuàng)建 link 標簽
          function createLink(href) {
          const link = document.createElement('link');
          link.rel = 'stylesheet';
          link.href = href;
          return link;
          }

          // 入口文件導出一個方法,將打包的 CSS 文件通過 link 的方式插入到對應的節(jié)點中
          defineApp.styleInject = (parentNode) => {
          ${cssChunksStr}.forEach((css) => {
          // import.meta.url 讓路徑保持正確,中括號取值避免被 rollup 轉(zhuǎn)換掉
          const link = createLink(new URL(css, import.meta['url']));
          parentNode.prepend(link);
          });
          };

          export default defineApp;
          `
          ,
          });
          }

          插件需要應用入口配合導出一個 styleInject 方法提供樣式插入,我們通過封裝入口方法得以解決。

          封裝一個方法給應用入口調(diào)用:

          export function defineMicroApp(callback) {
          const defineApp = (container) => {
          const appConfig = callback(container);
          // 處理樣式局部插入
          const mountFn = appConfig.mount;
          // 獲取到插件中的方法
          const inject = defineApp.styleInject;
          if (mountFn && inject) {
          appConfig.mount = (props) => {
          mountFn(props);
          // 裝載完畢后,插入樣式
          inject(container);
          };
          }
          return appConfig;
          };

          return defineApp;
          }

          現(xiàn)在 build 之后會生成一個不帶 hashmain.js 文件,主應用可以正常加載打包后的資源了。

          進一步優(yōu)化,main.js 的壓縮混淆,可以用 Vite 導出 transformWithEsbuild 進行編譯:

          const result = await transformWithEsbuild(customCode, 'main.js', {
          minify: true,
          });

          this.emitFile({
          fileName: 'main.js',
          type: 'asset',
          source: result.code,
          });

          子應用路徑問題

          之前我們需要手動添加 new URL(image, import.meta.url) 來修復子應用路徑問題。通過 transform 鉤子自動處理該邏輯。

          在這個插件之前,Vite 會將所有的資源文件轉(zhuǎn)換為路徑

          import logo from './logo.svg';

          // 轉(zhuǎn)換為:

          export default '/src/logo.svg';

          因此,我們只需要將 export default "資源路徑" 替換為 export default new URL("資源路徑", import.meta['url']).href 就可以了。

          const imagesRE = new RegExp(`\\.(png|webp|jpg|gif|jpeg|tiff|svg|bmp)($|\\?)`);

          transform(code, id) {
          // 修正圖片資源使用絕對地址
          if (imagesRE.test(id)) {
          return {
          code: code.replace(
          /(export\s+default)\s+(".+")/,
          `$1 new URL($2, import.meta['url']).href`
          ),
          map: null,
          };
          }
          return undefined;
          },

          完成,一個比較完善的 Vite 微應用方案由此而生。

          看看效果:

          更多

          有了插件,可以發(fā)揮出意想不到的事情。本微前端方案沒有實現(xiàn)以下的隔離方式,不保證后續(xù)會實現(xiàn),大家可以發(fā)揮更多的想象力。

          CSS 樣式隔離

          通過插件將主應用節(jié)點中的 id 添加并修改 CSS

          .name {
          color: red;
          }

          /* 轉(zhuǎn)換為 */

          #id .name {
          color: red;
          }

          但前提是需要為每個 設置一個唯一的 id。并且樣式性能會受到影響,CSSModules 方案會更好。

          JS 沙箱

          雖然在 ESM 中做運行時沙箱目前沒有現(xiàn)成的方案,但運行時沙箱性能非常差。換個思路,可以從編譯時沙箱入手。用 transform 鉤子將應用所有的 window 轉(zhuǎn)譯為沙箱fakeWindow,從而達到隔離效果。

          代碼示例

          大家可以 clone 下來學習

          插件倉庫:https://github.com/MinJieLiu/micro-app/tree/main/packages/micro-vite-plugin

          微前端示例:https://github.com/MinJieLiu/micro-app-demo

          瀏覽 81
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  天天爽日日爽 | 北条麻妃 无码 在线 视频 | 亚洲第一无码天堂精品 | 日本在线播放 | 无码免费毛片一区二区三区古代 |