整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          如何搭建組件庫的最小原型

          如何搭建組件庫的最小原型

          ear,大家好,我是“前端小鑫同學”,長期從事前端開發(fā),安卓開發(fā),熱衷技術(shù),在編程路上越走越遠~


          寫作背景:

          ???? 現(xiàn)在其實做得不錯的開源 UI 庫有很多,我還沒有真正的實踐過多造一個輪子也沒太大必要,但是學習編寫的思路和過程還是很有必要的,正好看到慕課的一個視頻就順便總結(jié)一下組件庫開發(fā)的流程,順便熟悉一個打包的配置和流程。

          搭建基礎(chǔ)結(jié)構(gòu):

          使用VueCli創(chuàng)建默認模板:

          1. 創(chuàng)建名為it200-ui的項目:vue create it200-ui
          2. 使用默認Vue2模板即可,我們只考慮搭建UI庫的思路不考慮版本的選擇;
          3. 按提示命令進入項目cd it200-ui,并啟動yarn serve

          調(diào)整目錄使適合UI庫開發(fā):

          1. 調(diào)整src/components層級到根目錄;
          2. 調(diào)整src為組件渲染示例examples
          3. 通過在 vue.config.js 配置pages節(jié)點來更改入口;

          創(chuàng)建第一個演示組件:

          ???? 目錄結(jié)構(gòu)如下,需按要求安裝開發(fā)依賴sass-loader,為了避免與 node-sass 的版本沖突造成的更多問題,我們不再安裝它而去添加一個名為sass包;

          components
          ├─lib
          |  ├─demo
          |  |  └index.vue
          ├─css
          |  └demo.scss
          

          在示例文件夾的 main.js 中導(dǎo)入并申明組件:

          import "../components/css/demo.scss";
          import Demo from "../components/lib/demo/index.vue";
          Vue.component("name", Demo);
          

          創(chuàng)建組件安裝腳本:

          ???? 通常在使用開源 UI 庫時并沒有使用 component 不是導(dǎo)入組件,而是使用的 use 進行安裝,所以我們在組件的同目錄創(chuàng)建一個組件的安裝腳本:

          import Demo from "./index.vue";
          
          Demo.install=function (Vue) {
            Vue.component(Demo.name, Demo);
          };
          
          export default Demo;
          

          使用組件安裝腳本注冊組件:

          import Demo from "../components/lib/demo/index.js";
          Vue.use(Demo);
          

          開發(fā)一個組件的生命周期:

          設(shè)計組件:

          ???? 組件的設(shè)計一定是為了滿足多處的復(fù)用而提出來的,站在各自的角度也可能都會有不一樣的答案,所以我們這里找了 Elementcard 組件中的一塊內(nèi)容來充當我們今天待設(shè)計組件的需求:

          組件設(shè)計稿:

          卡片組件需要滿足以下幾點要求,其他的要求暫不考慮:

          1. 支持通過 body-style 屬性來覆蓋默認的 body 區(qū)域?qū)傩裕?/span>
          2. 支持通過 shadow 屬性來設(shè)置陰影出現(xiàn)的時機;

          image.png

          組件提供的屬性:

          參數(shù) 說明 類型 可選值 默認值 body-style 設(shè)置 body 的樣式 object — { padding: '20px' } shadow 設(shè)置陰影顯示時機 string always / hover / never always

          編寫組件模板:

          創(chuàng)建card 組件的結(jié)構(gòu):

          components/
          ├─lib
          |  ├─card
          |  |  ├─index.js
          |  |  └index.vue
          ├─css
          |  └card.scss
          

          注冊并在 App.vue 中使用組件:

          import "../components/css/card.scss";
          import Card from "../components/lib/card/index.js";
          Vue.use(Card);
          

          按設(shè)計要求為組件添加屬性:

          通過 props 提供組件的上述基礎(chǔ)屬性。

          export default {
            name: "it-card",
            props: {
              bodyStyle: {
                type: Object,
                default: ()=> {
                  return { padding: "20px" };
                },
              },
              shadow: {
                type: String,
                default: "always",
              },
            },
          };
          

          編寫組件模板的框架:

          ???? 組件的大致結(jié)構(gòu)如下,通過三層 div 來設(shè)置卡片組件容器、陰影、內(nèi)容區(qū)的樣式,并提供默認插槽來設(shè)置具體內(nèi)容。

          <template>
            <div class="it-card">
              <div :class="`is-${shadow}-shadow`"></div>
              <div class="it-card__body" :style="bodyStyle">
                <slot></slot>
              </div>
            </div>
          </template>
          

          編寫組件的樣式:

          .it-card {
            border-radius: 4px;
            border: 1px solid #ebeef5;
            background-color: #fff;
            overflow: hidden;
            color: #303133;
            transition: 0.3s;
            .it-card__body {
              padding: 20px;
            }
            .is-always-shadow {
              box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
            }
            .is-hover-shadow:hover {
              box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
            }
            .is-never-shadow {
              box-shadow: none;
            }
          }
          

          在 App.vue 中完善卡片組件:

          在 app.vue 中完善卡片組件,并對比組件設(shè)計稿。

          <template>
            <div id="app">
              <h3>Card組件</h3>
              <it-card style="width: 300px" :body-style="{ padding: '0px' }">
                <img
                  src="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png"
                  class="image"
                />
                <div style="padding: 14px">
                  <span>好吃的漢堡</span>
                  <div class="bottom">
                    <time class="time">"2022-05-03T16:21:26.010Z"</time>
                  </div>
                </div>
              </it-card>
            </div>
          </template>
          
          <script>
          export default {
            name: "App",
          };
          </script>
          
          <style>
          // 這里的樣式使用 element card 使用的樣式
          </style>
          

          構(gòu)建 UMD 模塊:

          在前端模塊化的進程中,經(jīng)過了全局函數(shù)、命名空間,匿名函數(shù)自調(diào),文件模塊化方案,尤為常見的文件模塊化方案就是 CommonJs,ADM,UMD 了,下面來介紹一下各自的特點;

          CommonJs:

          1. 文件作用域:每個文件即為一個單獨的模塊,模塊中的內(nèi)容未主動暴露則對外不可見;
          2. 緩存:模塊的加載只發(fā)生在第一次導(dǎo)入,在之后的導(dǎo)入會優(yōu)先讀取緩存;
          3. 同步加載:同步加載能保證在使用時必定存在該模塊,但是并不適用于瀏覽器端,當同步加載慢的時候可能造成瀏覽器假死的狀態(tài)發(fā)生。

          結(jié)論:CommonJs 的模塊更適用于服務(wù)端應(yīng)用。

          AMD:

          1. 文件作用域:同 CommonJs,也是模塊化的主要產(chǎn)物;
          2. 異步加載:異步加載更好地適用于瀏覽器端,可以在異步加載后通過回調(diào)來執(zhí)行后續(xù)的腳本。

          結(jié)論:AMD 的模塊更適用于瀏覽器端應(yīng)用。

          UMD通用模塊:

          1. 同時滿足適用于瀏覽器和服務(wù)端的模塊化解決方案;
          2. 通過判斷是否包含毒素 exports 來確認是否支持 Node.js 模塊;
          3. 通過判斷是否包含 define 來確認是否支持 AMD 模塊;
          4. 上述兩個特點均不存在則將模塊掛載到全局(windowglobal)。

          使用 Webpack 來打包組件邏輯代碼:

          定義 webpack 打包配置文件webpack.components.js:

          1. 組件的打包我們使用多入口的方式分別處理,所以我們首先處理入口,通過遍歷組件 lib 目錄來得到一個以組件名和組件路徑組成的鍵值對。
          const glob=require("glob");
          
          let entrys={};
          
          async function makrList(dirPath, list) {
            const files=glob.sync(`${dirPath}/**/index.js`);
            for (let file of files) {
              const component=file.split(/[/.]/)[2];
              list[component]=`./${file}`;
            }
          }
          
          makrList("components/lib", entrys);
          
          1. 接下來我們處理出口的配置: 輸出文件名稱:使用入口的 key 來區(qū)分各個組件,并使用通過的 umd 作為組件輸出產(chǎn)物的標識;輸出目錄:這里需要注意使用絕對路徑來指定輸出文件的位置;libraryTarget和library有相互依賴的關(guān)系,主要用來指定模塊的暴露方式和模塊的別名,這一塊的描述我覺得 Rollup 中的描述將更清晰。
          const path=require("path");
          
          module.exports={
            entry: entrys,
            output: {
              filename: "[name].umd.js",
              path: path.resolve(__dirname, "dist"),
              library: "it200",
              libraryTarget: "umd",
            },
          };
          
          1. 最關(guān)鍵的是我們的 webpack 默認不認識.vue 的文件我們需要使用對應(yīng)的loader來處理,Vue 文件對應(yīng)的就是vue-loader,需要注意的是我們目前基于 Vue2 來構(gòu)建的項目,所以最新的vue-loader并不是特別適合我們可以降級到 **15** 版本來讓構(gòu)建正常進行。
          const { VueLoaderPlugin }=require("vue-loader");
          
          module.exports={
            plugins: [new VueLoaderPlugin()],
            module: {
              rules: [
                {
                  test: /\.vue$/,
                  use: [
                    {
                      loader: "vue-loader",
                    },
                  ],
                },
              ],
            },
          };
          
          1. 為了方便調(diào)用我們還是配置一下打包命令:
          "build:js": "webpack --config ./webpack.components.js"
          

          為了滿足全部導(dǎo)入的要求,我們還需要將組件整合:

          在 lib 目錄下新建一個index.js 文件將我們的組件統(tǒng)一導(dǎo)入后統(tǒng)一執(zhí)行組件掛載。

          import Demo from "./demo";
          import Card from "./card";
          
          const components={
            Demo,
            Card,
          };
          
          const install=function (Vue) {
            if (install.installed) return;
            Object.keys(components).forEach((key)=> {
              Vue.component(components[key].name, components[key]);
            });
          };
          
          export default {
            install,
          };
          

          使用Gulp 來打包組件的樣式代碼:

          gulp 主要通過定義任務(wù)并使用流式的處理方式使用不同的管道依次進行,我們主要處理 scss 文件內(nèi)容為 css 文件。

          需要用到的模塊如下:

          1. gulp-sass,因版本問題需要額外導(dǎo)入 sass 模塊。
          2. gulp-minify-css:主要用來對 css 文件進行壓縮。

          完整的打包配置如下:

          1. 配置文件指明了操作的文件入口為css 目錄下的 scss 結(jié)尾的文件;
          2. 文件輸出到 dist/css 目錄下;
          3. 方便執(zhí)行我們配一下打包命令:"build:css": "npx gulp sass"
          const gulp=require("gulp");
          const sass=require("gulp-sass")(require("sass"));
          const minifyCSS=require("gulp-minify-css");
          
          gulp.task("sass", async function () {
            return gulp
              .src("components/css/**/*.scss")
              .pipe(sass())
              .pipe(minifyCSS())
              .pipe(gulp.dest("dist/css"));
          });
          

          將模塊化的 scss 文件整合到一起,方便全部加載:

          在 css 目錄新建 index.scss 文件,并將各個組件需要的 scss 文件導(dǎo)入到此文件。

          @import "./card.scss";
          @import "./demo.scss";
          

          按需引入和全部引入:

          import "../dist/css/index.css";
          import IT200UI from "../dist/index.umd";
          Vue.use(IT200UI);
          
          import "../dist/css/card.css";
          import Card from "../dist/card.umd";
          Vue.use(Card);
          
          import "../dist/css/demo.css";
          import Demo from "../dist/demo.umd";
          Vue.use(Demo);
          

          發(fā)布組件庫到 NPM:

          1. 注冊 npm 用戶;
          2. 調(diào)整 package.json ;

          調(diào)整 package:

          1. 移除私有配置:private;
          2. 添加組件庫描述信息:description;
          3. 添加組件入口文件:main;
          4. 添加組件相關(guān)的關(guān)鍵詞:keywords;
          5. 添加作者名字:author;
          6. 添加組件庫發(fā)布的內(nèi)容:files;

          完成新增內(nèi)容如下:

          {
            "description": "IT200 組件庫,最小原型演示",
            "main": "dist/index.umd.js",
            "keywords": [
              "it200",
              "ui",
              "組件庫"
            ],
            "author": "fe-xiaoxin",
            "files": [
              "dist",
              "components"
            ]
          }
          

          調(diào)整組件庫的說明文檔:

          1. 包含組件庫的安裝方式;
          2. 包含組件庫的引用方式;

          快速開始

          如何安裝

          npm i it200-ui
          

          如何引入

          // 全部引入
          import 'it200-ui/dist/css/index.css';
          import IT200UI from 'it200-ui';
          Vue.use(IT200UI);
          
          // 按需引入
          import 'it200-ui/dist/css/cart.css';
          import { Card } from 'it200-ui';
          Vue.use(Card);
          

          正式開始發(fā)布:

          1. 確認 NPM 源為修改成其他鏡像地址,我這里使用 nrm 包進行源的管理,可以通過 nrm ls查詢和 nrm use 進行切換;
          2. 執(zhí)行 npm login 開始登陸,分別輸入用戶名、密碼、郵箱,開通動態(tài)驗證的話還需要輸入動態(tài)驗證碼,開通的方式可以翻我以前的文章;
          3. 執(zhí)行 npm publish 開始發(fā)布,開通動態(tài)驗證碼的話需要再次驗證動態(tài)驗證碼;

          image.png

          寫到最后:

          ???? 整個組件庫的開發(fā)我們省略了最后一步,因為版本的問題導(dǎo)致 vuepress 沒有成功的配置,在開發(fā)組件庫的過程中使用到的技術(shù)棧可以是五花八門但是通過本次總結(jié)到的我們開發(fā)組件庫的生命周期大致統(tǒng)一應(yīng)該是搭建結(jié)構(gòu)、設(shè)計組件、編寫組件、驗證組件、打包構(gòu)建、發(fā)布為主線,構(gòu)建組件庫文檔站點、編寫使用手冊、自動化構(gòu)建發(fā)布為支線同步進行。

          • 內(nèi)容設(shè)計的源碼地址:https://github.com/OSpoon/it200-ui;

          碼哥深入Webpack5等構(gòu)建工具(gulp/rollup/vite)|完結(jié)無秘

          來百度APP暢享高清圖片

          //下栽のke:chaoxingit.com/580/

          深入理解前端構(gòu)建工具:Webpack 5、Gulp、Rollup 和 Vite

          在現(xiàn)代前端開發(fā)中,構(gòu)建工具是不可或缺的一部分,它們能夠幫助開發(fā)者自動化各種任務(wù),如代碼壓縮、模塊打包、代碼轉(zhuǎn)換、靜態(tài)資源管理等。本文將深入介紹四種常用的前端構(gòu)建工具:Webpack 5、Gulp、Rollup 和 Vite,探討它們的特點、用途和適用場景。

          Webpack 5

          特點:

          1. 模塊打包器: Webpack 是一個模塊打包工具,能夠?qū)⑶岸隧椖恐械母鞣N資源(如 JavaScript、CSS、圖片等)視作模塊,并將其打包成靜態(tài)資源文件。
          2. 高度可配置: Webpack 提供了豐富的配置選項,可以靈活定制打包過程,滿足各種項目的需求。
          3. 代碼分割與懶加載: 支持代碼分割,可以將代碼拆分為多個 bundle,實現(xiàn)按需加載,提高頁面加載速度。
          4. 強大的生態(tài)系統(tǒng): Webpack 擁有龐大的社區(qū)和豐富的插件系統(tǒng),能夠滿足各種需求,擴展其功能。

          適用場景:

          • 適用于復(fù)雜的前端項目,需要處理多種類型資源和復(fù)雜依賴關(guān)系的場景。
          • 適用于需要高度定制化構(gòu)建流程的項目。

          Gulp

          特點:

          1. 基于任務(wù)的構(gòu)建工具: Gulp 是一個基于任務(wù)的前端構(gòu)建工具,通過定義一系列任務(wù)(Task),實現(xiàn)自動化處理文件的流程。
          2. 流式處理: 使用流式處理文件,可以有效提高處理效率和性能。
          3. 簡潔靈活的 API: Gulp 提供簡潔易懂的 API,易于上手和使用。
          4. 豐富的插件生態(tài)系統(tǒng): Gulp 擁有大量的插件,可以實現(xiàn)各種功能,如文件復(fù)制、壓縮、合并等。

          適用場景:

          • 適用于簡單的任務(wù)自動化和文件處理,如文件復(fù)制、壓縮、合并等。
          • 適用于需要處理大量文件或流式處理文件的項目。

          Rollup

          特點:

          1. ES6 模塊打包器: Rollup 是一個專注于 ES6 模塊的打包工具,能夠?qū)?ES6 模塊打包成適合生產(chǎn)環(huán)境使用的文件。
          2. Tree-shaking: Rollup 使用 Tree-shaking 技術(shù),可以剔除未使用的代碼,生成更小的包。
          3. 簡單易用: Rollup 的配置相對簡單,易于上手。
          4. 專注于庫的打包: Rollup 更適合用于開發(fā)庫或組件,專注于模塊的打包和優(yōu)化。

          適用場景:

          • 適用于開發(fā)庫或組件,專注于 ES6 模塊的打包和優(yōu)化。
          • 適用于需要生成輕量級、高性能 JavaScript 包的項目。

          Vite

          特點:

          1. 基于現(xiàn)代瀏覽器的開發(fā)服務(wù)器和構(gòu)建工具: Vite 是一個基于現(xiàn)代瀏覽器的開發(fā)服務(wù)器和構(gòu)建工具,旨在提供快速的開發(fā)體驗。
          2. ES 模塊原生支持: Vite 使用 ES 模塊的原生支持,無需打包即可直接運行。
          3. 熱更新: 支持熱更新(Hot Module Replacement),能夠在開發(fā)過程中實時更新修改的內(nèi)容。
          4. 針對現(xiàn)代瀏覽器優(yōu)化: Vite 針對現(xiàn)代瀏覽器進行優(yōu)化,構(gòu)建速度快。

          適用場景:

          • 適用于快速原型開發(fā),提供快速的開發(fā)環(huán)境和即時反饋。
          • 適用于現(xiàn)代瀏覽器,利用 ES 模塊的原生支持進行開發(fā)。

          這些工具各有特點,根據(jù)項目的需求和開發(fā)團隊的偏好,可以選擇合適的工具來進行前端構(gòu)建和開發(fā)。

          一深入Webpack5等構(gòu)建工具(gulp/rollup/vite)的知識和技能

          深入理解和掌握Webpack 5以及其他構(gòu)建工具(如gulp、rollup、vite)的知識和技能是前端開發(fā)中非常重要的一部分。以下是一些你可能需要掌握的知識和技能:

          Webpack 5:

          1. 模塊化概念: 理解模塊化開發(fā)的概念,以及Webpack是如何處理模塊的。
          2. 配置文件: 熟悉Webpack的配置文件,理解常用配置項,能夠根據(jù)項目需求進行配置。
          3. Loader和Plugin: 了解Loader和Plugin的概念,以及如何使用它們進行資源的加載、轉(zhuǎn)換和優(yōu)化。
          4. Code Splitting: 理解Webpack中的代碼拆分技術(shù),實現(xiàn)按需加載,提高應(yīng)用性能。
          5. DevServer: 使用Webpack DevServer進行本地開發(fā),熟悉熱模塊替換(HMR)的使用。
          6. 優(yōu)化: 學會進行Webpack構(gòu)建的性能優(yōu)化,包括代碼分割、懶加載、Tree-shaking等技術(shù)。
          7. Webpack生態(tài)系統(tǒng): 理解Webpack的生態(tài)系統(tǒng),熟悉一些常用的Webpack插件和工具。

          Gulp:

          1. 任務(wù)流程: 理解Gulp的基本工作原理,了解任務(wù)流程是如何定義和執(zhí)行的。
          2. Gulp API: 熟悉Gulp的API,能夠使用Gulp的方法創(chuàng)建和管理任務(wù)。
          3. 插件使用: 學會使用各種Gulp插件,實現(xiàn)文件的復(fù)制、壓縮、合并等任務(wù)。
          4. 文件流: 理解Gulp中的文件流概念,使用流式處理提高任務(wù)效率。
          5. 監(jiān)視文件變化: 使用Gulp的watch功能,實現(xiàn)文件變化時自動執(zhí)行相應(yīng)任務(wù)。

          Rollup:

          1. ES6模塊: 理解ES6模塊的概念,了解Rollup是如何專注于ES6模塊的打包和優(yōu)化的。
          2. Tree-shaking: 深入了解Tree-shaking技術(shù),學會如何剔除未使用的代碼。
          3. 配置文件: 熟悉Rollup的配置文件,了解其配置項,能夠根據(jù)項目需求進行配置。
          4. 插件系統(tǒng): 了解Rollup的插件系統(tǒng),學會使用插件完成一些特定的任務(wù)。

          Vite:

          1. 原生ES模塊支持: 理解Vite如何利用瀏覽器原生ES模塊的支持,實現(xiàn)無需打包即可運行。
          2. 熱更新: 了解Vite的熱更新機制,實現(xiàn)開發(fā)過程中的即時反饋。
          3. 插件開發(fā): 學會開發(fā)和使用Vite插件,擴展Vite的功能。
          4. 配置文件: 熟悉Vite的配置文件,了解其配置項,能夠根據(jù)項目需求進行配置。
          5. 優(yōu)化: 了解Vite的一些優(yōu)化策略,如代碼分割、按需加載等。

          其他通用技能:

          1. 調(diào)試工具: 使用瀏覽器開發(fā)者工具進行調(diào)試,了解Source Map的生成和使用。
          2. 版本控制: 熟悉Git,能夠使用版本控制工具管理代碼。
          3. 持續(xù)集成: 了解持續(xù)集成工具,將構(gòu)建工具整合到CI/CD流程中。
          4. 性能優(yōu)化: 學會使用各種工具進行性能分析和優(yōu)化,提高應(yīng)用的加載速度和性能。

          深入掌握這些知識和技能,將使你能夠更高效地構(gòu)建和維護復(fù)雜的前端項目。不同的項目可能使用不同的構(gòu)建工具,根據(jù)項目需求選擇合適的工具,并深入學習其使用和優(yōu)化技巧。

          一深入Webpack5等構(gòu)建工具(gulp/rollup/vite)的職責與挑戰(zhàn)

          深入理解和使用Webpack 5等構(gòu)建工具(如gulp、rollup、vite)需要面對一些職責和挑戰(zhàn):

          職責:

          1. 資源管理: 負責管理項目中的各種資源,包括JavaScript、CSS、圖片等,并在構(gòu)建過程中進行優(yōu)化和處理。
          2. 模塊打包: 將項目中的各個模塊打包成適合瀏覽器加載的形式,包括處理模塊間的依賴關(guān)系。
          3. 代碼轉(zhuǎn)換: 對不同類型的代碼進行轉(zhuǎn)換,如將ES6+代碼轉(zhuǎn)換成ES5,將Sass/LESS轉(zhuǎn)換成CSS等。
          4. 性能優(yōu)化: 通過代碼壓縮、文件合并、代碼拆分等手段提高應(yīng)用的性能和加載速度。
          5. 開發(fā)環(huán)境支持: 在開發(fā)環(huán)境中提供熱更新、代碼分析等功能,提高開發(fā)效率。
          6. 生產(chǎn)環(huán)境支持: 在生產(chǎn)環(huán)境中提供代碼壓縮、文件指紋、緩存管理等功能,保證應(yīng)用的穩(wěn)定性和性能。

          挑戰(zhàn):

          1. 學習曲線: 這些構(gòu)建工具都有一定的學習曲線,特別是對于初學者來說,需要花費時間來理解其工作原理和配置方式。
          2. 復(fù)雜配置: 隨著項目的復(fù)雜度增加,構(gòu)建工具的配置也會變得復(fù)雜,需要花費時間和精力來進行優(yōu)化和調(diào)試。
          3. 性能問題: 不恰當?shù)呐渲煤褪褂梅绞娇赡軙?dǎo)致構(gòu)建性能下降,甚至引發(fā)一些意想不到的問題,需要進行性能分析和優(yōu)化。
          4. 插件選擇: 在使用過程中需要選擇合適的插件來完成特定的任務(wù),需要對各種插件進行評估和選擇。
          5. 維護成本: 隨著項目的演進和變化,構(gòu)建工具的配置也需要不斷地進行維護和更新,增加了項目的維護成本。
          6. 生態(tài)變化: 這些構(gòu)建工具的生態(tài)系統(tǒng)都在不斷地變化和發(fā)展,需要及時跟進新的特性和最佳實踐。
          7. 集成問題: 在一些復(fù)雜的項目中,構(gòu)建工具可能需要和其他工具或框架進行集成,需要處理好各種依賴和兼容性問題。

          克服這些挑戰(zhàn)需要不斷地學習和實踐,積累經(jīng)驗和技能,同時也需要與社區(qū)和團隊保持良好的溝通和合作。

          一深入Webpack5等構(gòu)建工具(gulp/rollup/vite)的未來趨勢與總結(jié)

          未來,Webpack 5、Gulp、Rollup、Vite等構(gòu)建工具將繼續(xù)發(fā)展,面臨一些趨勢和變化:

          1. 性能優(yōu)化: 構(gòu)建工具的性能優(yōu)化將成為一個重要的趨勢。隨著前端項目的復(fù)雜性增加,構(gòu)建工具需要更快的構(gòu)建速度和更高的效率。優(yōu)化算法、增強緩存機制以及更好的并行處理將成為關(guān)注點。
          2. 模塊化開發(fā): 隨著模塊化開發(fā)的普及,構(gòu)建工具將更多地支持模塊化開發(fā)的需求。對模塊的處理、加載和分割將變得更加智能和高效。
          3. 開發(fā)體驗: 構(gòu)建工具將更多地關(guān)注開發(fā)者的體驗,提供更友好的命令行界面、詳細的錯誤提示和更好的文檔。熱更新、快速重構(gòu)、源代碼映射等功能將進一步改善開發(fā)體驗。
          4. 模塊化加載優(yōu)化: 隨著HTTP/2和HTTP/3的普及,模塊化加載優(yōu)化將成為一個重要的方向。構(gòu)建工具將更多地支持代碼分割、懶加載等技術(shù),以減少頁面加載時間和提升用戶體驗。
          5. 跨平臺支持: 構(gòu)建工具將更多地支持跨平臺開發(fā),包括Web、移動端、桌面端等。構(gòu)建工具的靈活性和可配置性將成為一個重要的考量因素。
          6. 生態(tài)整合: 構(gòu)建工具將更多地與周邊生態(tài)整合,包括與框架、庫、測試工具等的集成。更多的插件和擴展將為開發(fā)者提供更多的選擇和靈活性。

          總的來說,未來構(gòu)建工具將繼續(xù)朝著更快、更智能、更靈活的方向發(fā)展,以適應(yīng)日益復(fù)雜的前端開發(fā)需求。開發(fā)者需要不斷學習和掌握最新的技術(shù)和工具,保持對前端技術(shù)發(fā)展的敏感度和理解能力。

          者:glendonli,騰訊 PCG 前端開發(fā)工程師

          對于大型前端項目而言,構(gòu)建的穩(wěn)定性和易用性至關(guān)重要,騰訊文檔在迭代過程中,復(fù)雜的項目結(jié)構(gòu)和編譯帶來的問題日益增多,極大的增加了新人上手與日常搬磚的開銷。恰逢 Webpack5 上線,不如來一次徹底的魔改。

          1.前言

          騰訊文檔最近基于剛剛發(fā)布的 Webpack5 進行了一次編譯的大重構(gòu),作為一個多個倉庫共同構(gòu)成的大型項目,任意品類的代碼量都超過百萬。對于騰訊文檔這樣一個快速迭代,高度依賴自動化流水線,常年并行多個大型需求和無數(shù)小需求的項目來說,穩(wěn)定且快速的編譯對于開發(fā)效率至關(guān)重要。這篇文章,就是筆者最近進行重構(gòu),成功將日常開發(fā)優(yōu)化到 1s 的過程中,遇到的一些大型項目特有的問題和思考,希望能給大家在前端項目構(gòu)建的優(yōu)化中帶來一些參考和啟發(fā)。

          2.大型項目編譯之痛

          隨著項目體系的逐漸擴大,往往會遇到舊的編譯配置無法支持新特性,由于各種 config 文件自帶的閱讀 debuff,以及累累的技術(shù)債,大家總會趨于不去修改舊配置,而是試圖新增一些配置在外圍對編譯系統(tǒng)進行修正。也是這樣類似的原因,騰訊文檔過去的編譯編譯也并不優(yōu)雅:

          多級的子倉庫結(jié)構(gòu),復(fù)雜的編譯系統(tǒng)造成很高的理解和改動成本,也帶來了較高的編譯耗時,對于整個團隊的開發(fā)效率有著不小的影響。

          3.All in One

          為了解決編譯復(fù)雜和緩慢的問題,至關(guān)重要的,就是禁止套娃:多層級混合的系統(tǒng)必須廢除,統(tǒng)一的編譯才是王道。在所有編譯系統(tǒng)中,Webpack 在大項目的打包上具備很強優(yōu)勢,插件系統(tǒng)最為豐滿,并且 Webpack5 帶來了 Module Federation 新特性,因此筆者選擇了用 Webpack 來統(tǒng)合多個子倉庫的編譯。

          3.1.整合基于 lerna 的倉庫結(jié)構(gòu)

          騰訊文檔使用了 lerna 來管理倉庫中的子包,使用 lerna 的好處此處就不作展開了。不過 lerna 的通用用法也帶來了一定的問題,lerna 將一個倉庫變成了結(jié)構(gòu)上的多個倉庫,如果按照默認的使用方式,每個倉庫都會有自己的編譯配置,單個項目的編譯變成了多個項目的聯(lián)編聯(lián)調(diào),修改配置和增量優(yōu)化都會變得比較困難。

          雖然使用 lerna 的目的是使各個子包相對獨立,但是在整個項目的編譯調(diào)試中,往往需要的是所有包的集合,那么,筆者就可以忽略掉這個子包間的物理隔離,把子倉庫作為子目錄來看待。不依賴 lerna,筆者需要解決的,是子包間的引用問題:

          /** package/layout/src/xxx.ts **/
          import { Stream } from '@core/model';
          // do something
          

          事實上,筆者可以通過 webpack 配置中 resolve 的 alias 屬性來達到相應(yīng)效果:

          {
            resolve: {
              alias: {
                '@core/model': 'word/package/model/src/',
              }
            }
          }
          

          3.2.管理游離于打包系統(tǒng)之外的文件

          在大型項目中,有時會存在一些特殊的靜態(tài)代碼文件,它們往往并不參與到打包系統(tǒng)中,而是由其他方式直接引入 html,或者合并到最終的結(jié)果中。

          這樣的文件,一般分為如下幾類:

          1. 加載時機較早的外部 sdk 文件,本身以 minify 文件提供
          2. 外部文件依賴的其他框架文件,比如 jquery
          3. 一些 polyfill
          4. 一些特殊的必須早期運行的獨立邏輯,比如初始化 sdk 等

          由于 polyfill 和外部 sdk 往往直接通過掛全局變量運行的模式,項目中往往會通過直接寫入 html script 標簽的方式引用它們。不過,隨著此類文件的增多,直接利用標簽引用,對于版本管理和編譯流程都不友好,它們對應(yīng)的一些初始化邏輯,也無法添加到打包流程中來。這種情況,筆者建議手工的創(chuàng)建一個 js 入口文件,對以上文件進行引用,并作為 webpack 的一個入口。如此,就能通過代碼的方式,將這些散裝文件管理起來了:

          import 'jquery';
          
          import 'raven.min.js';
          import 'log.js';
          // ...
          

          但是,一些外部的 js 可能依賴于其他 sdk,比如 jQuery,但是打包系統(tǒng)并不知道它們之間的依賴關(guān)系,導(dǎo)致 jQuery 沒有及時暴露到全局中,該怎么辦呢?事實上,webpack 提供了很靈活的方案來處理這些問題,比如,筆者可以通過 expose-loader,將 jQuery 的暴露到全局,供第三方引用。在騰訊文檔中,還包含了一些對遠程 cdn 的 sdk 組件,這些 sdk 也需要引用一些庫,比如 jQuery 的。因此,筆者還通過 splitChunks 的配置,將 jQuery 重新分離出來,放在了較早的加載時機,保證基于 cdn 加載的 sdk 亦能正常初始化。

          通過代碼引用,一方面,可以很好地進行依賴文件的版本管理;另一方面,由于對應(yīng)文件的編譯也加入了打包流程,所有對應(yīng)文件的改動都可以被動態(tài)監(jiān)視到,有利于后續(xù)進行增量編譯。同時,由于 webpack 的封裝特點,每個庫都會被包含在一個 webpack_require 的特殊函數(shù)之中,全局變量的暴露數(shù)量也變得較為可控。

          3.3.定制化的 webpack 流程

          Webpack 提供了一個非常靈活的 html-webpack-plugin 來進行 html 生成,它支持模板和一眾的專屬插件,但是,仍然架不住項目有一些特殊的需求,通用的插件配置要么無法滿足這些需求,要么適配的結(jié)果就十分難懂。這也是騰訊文檔在最初使用了 gulp 來生成 html 的原因,在 gulp 配置中,有很多自定義流程來滿足騰訊文檔的發(fā)布要求。

          既然,gulp 可以自定義流程來實現(xiàn) html 生成,那么,筆者也可以單獨寫一個 webpack 插件來實現(xiàn)定制的流程。

          Webpack 本身是一個非常靈活的系統(tǒng),它是一個按照特定的流程執(zhí)行的框架,在每個流程的不同的階段提供了不同的鉤子,通過各種插件去實現(xiàn)這些鉤子的回調(diào),來完成代碼的打包,事實上,webpack 本身就是由無數(shù)原生插件組成的。在這整個流程中,筆者可以做各種不同的事情來定制它。

          對于生成 html 的場景,通過增加一個插件,在 webpack 處理生成文件的階段,將生成的 js、css 等資源文件,以及 ejs 模板和特殊配置整合到一起,再添加到 webpack 的 assets 集合中,便可以完成一次自定義的 html 生成。

          compilation.hooks.processAssets.tap({
            name: 'TemplateHtmlEmitPlugin',
            stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
          }, ()=> {
            // custom generation
            compilation.emitAsset(
              `${parsed.name}.html`,
              new sources.RawSource(source, true),
            );
            compilation.fileDependencies.addAll(dependencies);
          });
          

          在以上代碼中,大家可以留意到最后一句:compilation.fileDependencies.addAll(dependencies),通過這一句,筆者可以將所有被自定義生成所依賴的文件加入的 webpack 的依賴系統(tǒng)中,那么當這些文件發(fā)生變更的時候,webpack 能夠自動再次觸發(fā)對應(yīng)的生成流程。

          3.4.一鍵化的開發(fā)體驗

          至此,各路編譯都已經(jīng)統(tǒng)一化,筆者可以用一個 webpack 編譯整個項目了,watch 和 devServer 也可以一起 high 起來。不過,既然編譯可以統(tǒng)一,何不讓所有操作都整合起來呢?

          基于 node_modules 不應(yīng)該手工操作的假設(shè),筆者可以創(chuàng)建 package.json 中依賴的快照,每次根據(jù) package 的變化來判斷是否需要重新安裝,避免開發(fā)同學同步代碼后的手動判斷,跳過不必要的步驟。

          public async install(force=false) {
            const startTime=performance.now();
            const lastSnapshot=this.readSnapshot();
            const snapshot=this.createSnapshot();
          
            const runs=this.repoInfos.map((repo)=> {
              if (
                this.isRepoInstallMissing(repo.root)
                || (!repo.installed
                && (force || !isEqual(snapshot[repo.root], lastSnapshot[repo.root])))
              ) {
                // create and return install cmd
              }
              return undefined;
            }).filter(script=> !!script);
          
            const { info }=console;
            if (runs.length > 0) {
              try {
                // 執(zhí)行安裝并保存快照
                await Promise.all(runs.map(run=> this.exec(run!.cmd, run!.cwd, run!.name)));
                this.saveSnapshot(snapshot);
              } catch (e) {
                this.removeSnapshot();
                throw e;
              }
            } else {
              info(chalk.green('Skip install'));
            }
            info(chalk.bgGreen.black(`Install cost: ${TimeUtil.formatTime(performance.now() - startTime)}`));
          }
          

          同樣的,騰訊文檔的本地調(diào)試是基于特殊的測試環(huán)境,通過 whislte 進行代理,這樣的步驟也可以自動化,那么,對于開發(fā)來說,一切就很輕松了,一條命令,輕松搬磚~

          不過,作為是一個復(fù)雜的系統(tǒng),第一次使用,總需要初始化的吧,如果編譯系統(tǒng)的依賴尚未安裝,沒有雞,怎么生蛋呢?

          其實不然,筆者不妨在整套編譯系統(tǒng)的外層套個娃,做前端開發(fā),node 總會先安裝的吧?那么,在執(zhí)行正在的編譯命令之前,筆者執(zhí)行一個只依賴于 node 的腳本,這個腳本會嘗試執(zhí)行主要命令,如果主命令直接 crash,說明安裝環(huán)境尚未準備完畢,那么這個時候,對編譯系統(tǒng)進行初始化就 ok 了。如此,就真的可以做到一鍵開發(fā)了。

          const cmd='啟動編譯的命令';
          const main=extraArg=> childProcess.execSync(`${cmd} ${extraArg}`, { stdio: 'inherit', cwd: __dirname });
          
          try {
            main('');
          } catch (e) {
            // 初始化
            main('after-initialize');
          }
          

          3.5.編譯系統(tǒng)代碼化

          在這一次的重構(gòu)過程中,筆者將原本的編譯配置改為了由 ts 調(diào)用 webpack 的 nodeApi 來執(zhí)行編譯。代碼化的編譯系統(tǒng)有諸多好處:

          1. 使用 api 調(diào)用,可以享受 IDE 帶來的代碼提示,再也不會因為不小心在配置文件里面打了一個 typo 而調(diào)試一整天。
          2. 使用代碼 api,能夠更好的實現(xiàn)編譯的結(jié)構(gòu),特別是有多重輸出的時候,比起簡單的 config 文件組合,更好管理。
          3. 使用代碼化的編譯系統(tǒng),還有一個特別的作用,編譯系統(tǒng)也可以寫測試了!啥?編譯系統(tǒng)要寫測試?事實上,在騰訊文檔歷次的發(fā)布中,經(jīng)歷過數(shù)次莫名的 bug,在上線前的測試中,整個程序的表現(xiàn)突然就不正常了。相關(guān)代碼,并沒有任何改動,大家地毯式的排查了很久,才發(fā)現(xiàn)編譯的結(jié)果和以前有微小的不同。事實上,在系統(tǒng)測試環(huán)境生成的前五個小時,一個編譯所依賴的插件默默地更新了一個小版本,而筆者在 package.json 中對該插件使用的是默認的^xx.xx,流水線 install 到了最新的版本,導(dǎo)致了 crash。當時筆者得出了一個結(jié)論,編譯相關(guān)的庫需要鎖定版本。但是,鎖定版本并不能真正的解決問題,編譯所使用的組件,總有升級的一天,如果保證這個升級不會引起問題呢?這就是自動化測試的范疇了。事實上,如果大家看看 Webpack 的代碼,會發(fā)現(xiàn)他們也做了很多測試用例來編譯的一致性,但是,webpack 的插件五花八門,并不是每一個作者在質(zhì)量保障上都有足夠的投入,因此,用自動化測試保證編譯系統(tǒng)的穩(wěn)定性,也是一個可以深入研究的課題。

          4.編譯提速

          在涉及 typescript 編譯的項目中,基本的提速操作,就是異步的類型檢查,ts-loader 的 tranpsileOnly 參數(shù)和 fork-ts-checker 的組合拳百試不厭。不過,對于復(fù)雜的大型項目來說,這一套組合拳的啟用過程未必是一帆風順,不妨隨著筆者一起看看,在騰訊文檔中,啟用快速編譯的坎坷之路。

          4.1.消失的 enum

          在啟用 transpileOnly 參數(shù)后,編譯速度立即有了質(zhì)的提升,但是,結(jié)果并不樂觀。編譯后,頁面還沒打開,就 crash 了。根據(jù)報錯查下去,發(fā)現(xiàn)一個從依賴庫導(dǎo)入的對象變成了 undefined,從而引起程序崩潰。這個變?yōu)?undefined 的對象,是一個 enum,定義如下:

          export const enumScope{
            VAL1=0,
            VAL2=1,
          }
          

          為什么當筆者啟用了 transpileOnly 后它就為空了呢?這和它的特殊屬性有關(guān),它不是一個普通的 enum,它是一個 const enum。眾所周知,枚舉是 ts 的語法糖,每一個枚舉,對應(yīng)了 js 中的一個對象,所以,一個普通的枚舉,轉(zhuǎn)化為 js 之后,會變成這樣:

          // ts
          export enum Scope {
            VAL1=0,
            VAL2=1,
          }
          
          const a=Scope.VAL1;
          
          // js
          constScope={
            VAL1: 0,
            VAL2: 1,
            0: 'VAL1',
            1: 'VAL2',
          };
          
          const a=Scope.EDITOR;
          

          如果筆者給 Scope 加上一個 const 關(guān)鍵字呢?它會變成這樣:

          // ts
          export const enumScope{
            VAL1=0,
            VAL2=1,
          }
          
          const a=Scope.VAL1;
          
          // js
          const a=0;
          

          也就是說,const enum 就和宏是等效的,在翻譯成 js 之后,它就不存在了。可是,為何在關(guān)閉 transpileOnly 時,編譯結(jié)果可以正常運行呢?其實,仔細翻看外部庫的聲明文件.d.ts,就會發(fā)現(xiàn),在這個.d.ts 文件中,Scope 被原封不動地保留了下來。

          // .d.ts
          export const enumScope{
           VAL1=0,
           VAL2=1,
          }
          

          在正常的編譯流程下,tsc 會檢查.d.ts 文件,并且已經(jīng)預(yù)知了這一個定義,因此,它能夠正確的執(zhí)行宏轉(zhuǎn)換,而對于 transpileOnly 開啟的情況下,所有的類型被忽略,由于原本的庫模塊中已經(jīng)不存在 Scope 了,所以編譯結(jié)果無法正常執(zhí)行(PS:tsc 官方已經(jīng)表態(tài) transpile 模式下的編譯不解析.d.ts 是標準 feature,丟失了 const enum 不屬于 bug,所以等待官方支持是無果的)。既然得知了緣由,就可以修復(fù)了。四種方案:

          方案一,遵循官方指導(dǎo),對于不導(dǎo)出 const enum,只對內(nèi)部使用的枚舉 const 化,也就是說,需要修改依賴庫。當然,騰訊文檔本次 crash 所有依賴庫確實屬于自有的 sdk,但是如果是外部的庫引起了該問題呢?所以該方案并不保險。

          方案二,完美版,手動解析.d.ts 文件,尋找所有 const enum 并提取定義。但是,transpileOnly 獲取的編譯加速真是得益于忽略.d.ts 文件,如果筆者再去為了一個 enum 手工解析.d.ts,而.d.ts 文件可能存在復(fù)雜的引用鏈路,是極其耗時的。

          方案三,字符串替換,既然 const enum 是宏,那么筆者可以手工通過 string-replace-loader 達到類似效果。不過,字符串替換方式依舊過于暴力,如果使用了類似于 Scope['VAL1']的用法,可能就猝不及防的失效了。

          方案四,也是筆者最終所采取的方案,既然定義消失了,重新定義就好,通過 Webpack 的 DefinePlugin,筆者可以重新定義丟失的對象,保證編譯的正常解析。

          new DefinePlugin({
            Scope: {VAL1: 0,VAL2: 1 },
          })
          

          4.2.愛恨交加的 decorator 及依賴注入

          很不幸,僅僅是解決了編譯對象丟失的問題,代碼依舊無法運行。程序在初始化的時候,依舊迷之失敗了,經(jīng)過一番調(diào)試,發(fā)現(xiàn),初始化流程有一些微妙的不同。很明顯,transpileOnly 開啟的情況下,編譯的結(jié)果發(fā)生了變化。 要解決這個問題,就需要對 transpileOnly 模式的實現(xiàn)一探究竟了。transpileOnly 底層是基于 tsc 的 transpileModule 功能來實現(xiàn)的,transpileModule 的作用,是將每一個文件當做獨立的個體進行解析,每一個 import 都會被當做一個整體模塊來看待,編譯器不會再解析模塊導(dǎo)出與文件的具體關(guān)系,舉個例子:

          // src/base/a.ts
          export class A {}
          
          // src/base/b.ts
          export class B {}
          
          // src/base/index.ts
          export * from './a';
          export * from './b'
          
          // src/app.ts
          import { A } from './base';
          
          const a=new A();
          

          如上是常見的代碼寫法,我們往往會通過一個 index.ts 導(dǎo)出 base 中的模塊,這樣,在其他模塊中,筆者就不需要引用到文件了。在正常模式下,編輯器解析這段代碼,會附帶信息,告知 webpack,A 是由 a.ts 導(dǎo)出的,因此,webpack 在打包時,可以根據(jù)具體場景將 A、B 打包到不同的文件中。但是,在 transpileModule 模式下,webpack 所知道的,只有 base 模塊導(dǎo)出了 A,但是它并不知道 A 具體是由哪個文件導(dǎo)出的,因此,此時的 webpack 一定會將 A、B 打包到一個文件中,作為一整個模塊,提供給 App。對于騰訊文檔,這個情況發(fā)生了如下變化(模塊按照 1、2、3、4、5 的順序進行加載,模塊的視覺大小表示體積大小):

          可以看到,在 transpileOnly 開啟的情況下,大量的文件被打包到了模塊 1 中,被提前加載了。不過,一般情況下,模塊被打包到什么位置,并不應(yīng)該影響代碼的表現(xiàn),不是么?畢竟,關(guān)閉 code splitting,代碼是可以不拆包的。對于一般的情況而言,這樣理解并沒有錯。但是,對于使用了 decorator 的項目而言,就不適用了。在代碼普遍會轉(zhuǎn)為 es5 的時代,decorator 會被轉(zhuǎn)換為一個__decorator 函數(shù),這個函數(shù),是代碼加載時的一個自執(zhí)行函數(shù)。如果代碼打包的順序發(fā)生了變化,那么自執(zhí)行函數(shù)的執(zhí)行順序也就可能發(fā)生了變化。那么,這又如何導(dǎo)致了騰訊文檔無法正常啟動呢?這,就要從騰訊文檔全面引入依賴注入技術(shù)開始說起。

          在騰訊文檔中,每一個功能都是一個 feature,這個 feature 并不會手動初始化,而是通過一個特殊裝飾器,注入到騰訊文檔的 DI 框架中,然后,由注入框架進行統(tǒng)一的實例創(chuàng)建。舉個例子,在正常的變一下,由三個 Feature A、B、C,A、B 被編譯在模塊 1 中,C 被編譯到模塊 2 中。在模塊 1 加載時,workbench 會進行一輪實例創(chuàng)建和初始化,此時,F(xiàn)eatureA 的初始化帶來了某個副作用。然后,模塊 2 加載了,workbench 再次進行一輪實力創(chuàng)建和初始化,此時 FeatureC 的初始化依賴了 FeatureA 的副作用,但是第一輪初始化已經(jīng)結(jié)束,因此 C 順利實例化了。

          當 transpileOnly 被開啟式,一切變了樣,由于無法區(qū)分導(dǎo)出,F(xiàn)eature A、B、C 被打包到同一個模塊了。可想而知,在 Feature C 初始化時,由于副作用尚未發(fā)生,C 的初始化就失敗了。

          既然 transpileOnly 與依賴注入先天不兼容,那筆者就需要想辦法修復(fù)它。如果,筆者將 app 中的引用進行替換:

          // src/app.ts
          import { A } from './base/a';
          
          const a=new A();
          

          模塊導(dǎo)出的解析問題,是否就迎刃而解了?不過,這么多的代碼,改成這樣的引用,不但難看,反人類,工作量也很大。因此,讓筆者設(shè)計一個 plugin/loader 組合在編譯時來解決問題吧。在編譯的初始階段,筆者通過一個 plugin,對項目文件進行解析,將其中的 export 提取出來,找到每一個 export 和文件的對應(yīng)關(guān)系,并儲存起來(此處,可能大家會擔心 IO 讀寫對性能的影響,考慮到現(xiàn)在開發(fā)人均都是高速 SSD,這點 IO 吞吐真的不算什么,實測這個 export 解析<1s),然后在編譯過程中,筆者再通過一個自定義的 loader 將對應(yīng)的 import 語句進行替換,這樣,就可以實現(xiàn)在不影響正常寫代碼的情況下,保持 transpileOnly 解析的有效性了。

          經(jīng)過一番折騰,終于,成功地將騰訊文檔在高速編譯模式下運行了起來,達到了預(yù)定的編譯速度。

          5.Webpack5 升級之路

          5.1.一些兼容問題處理

          Webpack5 畢竟屬于一次非兼容的大升級,在騰訊文檔編譯系統(tǒng)重構(gòu)的過程中,也遇到諸多問題。

          5.1.1.SplitChunks 自定義 ChunkGroups 報錯

          如果你也是 splitChunks 的重度用戶,在升級 webpack5 的過程中,你可能會遇到如下警告:

          這個警告的說明并不是十分明確,用大白話來說,出現(xiàn)了這個提示,說明你的 chunkGroups 配置中,出現(xiàn)了 module 同時屬于 A、B 兩組(此處 A、B 是兩個 Entrypoint 或者兩個異步模塊),但是你明確指定了將模塊屬于 A 的情況。為何此時 Webpack5 會報出警告呢?因為從通常情況來說,module 分屬于兩個 Entrypoint 或者異步模塊,module 應(yīng)該被提取為公共模塊的,如果 module 被歸屬于 A,那么 B 模塊如果單獨加載,就無法成功了。

          不過,一般來說,出現(xiàn)這樣的指定,如果不是配置錯誤,那就是 A、B 之間已經(jīng)有明確的加載順序。但是這個加載順序,Webpack 并不知道。對于 entrypoint,webpack5 中,允許通過 dependOn 屬性,指定 entry 之間的依賴關(guān)系。但是對于異步模塊,則沒有這么遍歷的設(shè)置。當然,筆者也可以通過自定義插件,在 optimize 之前,對已有的模塊依賴關(guān)系以及修改,保證 webpack 能夠知曉額外的信息:

          compiler.hooks.thisCompilation.tap('DependOnPlugin', (compilation)=> {
            compilation.hooks.optimize.tap('DependOnPlugin', ()=> {
              forEach(this.dependencies, (parentNames, childName)=> {
                const child=compilation.namedChunkGroups.get(childName);
                if (child) {
                  parentNames.forEach((parentName)=> {
                  const parent=compilation.namedChunkGroups.get(parentName);
                  if (parent && !child.hasParent(parent)) {
                    parent.addChild(child);
                    child.addParent(parent);
                  }
                  });
                }
              });
            });
          });
          

          5.1.2.plugin 依賴的 api 已刪除

          Webpack5 發(fā)布后,各大主流 plugin 都已經(jīng)相繼適配,大家只要將插件更新到最新版本即可。不過,也有一些插件因為諸多緣由,一些插件并沒有及時更新。(PS:目前,沒有匹配的插件大多已經(jīng)比較小眾了。)總之,這個問題是比較無解的,不過可以適當?shù)却瑧?yīng)該在近期,大部分插件都會適配 webpack5,事實上 webpack5 也是用了不少改名大法,部分接口進行轉(zhuǎn)移,調(diào)用方式發(fā)生了改變,倒也沒有全部翻天覆地的變化,所以,實在等不及的小插件不妨試試自己 fork 修改一下。

          5.2.Module Federation 初體驗

          通常,對于一個大型項目來說,筆者會抽取很多公共的組件來提高項目間的模塊共享,但是,這些模塊之間,難免會有一些共同依賴,比如 React、ReactDOM,JQuery 之類的基礎(chǔ)庫。這樣,就容易造成一個問題,公共組件抽取后,項目體積膨脹了。隨著公共組件的增多,項目體積的膨脹變得十分可怕。在傳統(tǒng)打包模型上,筆者摸索出了一套簡單有效的方法,對于公共組件,筆者使用 external,將這些公共部分摳出來,變成一個殘疾的組件。

          但是,隨著組件的增多,共享組件的 Host 增多,這樣的方式帶來了一些問題:

          1. Component 需要為 Host 專門打包,它不是一個可以獨立運行的組件,每一個運行該 Component 的 Host 必須攜帶完整的運行時,否則 Component 就需要為不同的 Host 打出不同的殘疾包。
          2. Component 與 Component 之間如果存在較大的共享模塊,無法通過 external 解決。

          這個時候,Module Federation 出現(xiàn)了,它是 Webpack 從靜態(tài)打包到完整運行時的一個轉(zhuǎn)變,Module Federation 中,提出了 Host 和 Remote 的概念。Remote 中的內(nèi)容可以被 Host 消費,而在這個消費過程中,可以通過 webpack 的動態(tài)加載運行時,只加載其中需要的部分,對于已經(jīng)存在的部分,則不作二次加載。(下圖中,由于 host 中已經(jīng)包含了 jQuery、react 和 dui,Webpack 的運行時將只加載 Remote1 中的 Component1 和 Remote2 中的 Component2。)

          也就是說,公共組件作為一個 Remote,它包含了完整的運行時,Host 無需知道需要準備什么樣的運行時才可以運行 Remote,但是 Webpack 的加載器保證了共享的代碼不作加載。如此一來,就避免了傳統(tǒng) external 打包模式下的諸多問題。事實上,一個組件可以同時是 Host 和 Remote,也就是說,一個程序既可以作為主程運行,也可以作為一個在線的 sdk 倉庫。關(guān)于 Module Federation 的實現(xiàn)原理,此處不再贅述,大家感興趣可以參考探索 webpack5 新特性 Module federation 在騰訊文檔的應(yīng)用中的解析,也可以多多參考module-federation-examples這個倉庫中的實例。

          Webpack5 的 Module Federation 是依賴于其動態(tài)加載機制的,因此,在它的演示實例中,你都可以看到這樣的結(jié)構(gòu):

          // bootstrap.js
          import App from "./App";
          import React from "react";
          import ReactDOM from "react-dom";
          
          ReactDOM.render(<App />, document.getElementById("root"));
          
          // index.js
          import('./bootstrap.js');
          

          而 Webpack 的入口配置,都設(shè)置在了 index.js 上,這里,是因為所有的依賴都需要動態(tài)判定加載,如果不把入口變成一個異步的 chunk,那如何去保障依賴能夠按順序加載內(nèi)?畢竟實現(xiàn) Moudle Federation 的核心是 基于 webpack_require 的動態(tài)加載系統(tǒng)。由于 Module Federation 需要多個倉庫的聯(lián)動,它的推進必然是相對漫長的過程。那么筆者是否有必要將現(xiàn)有的項目直接改造為 index-bootstrap 結(jié)構(gòu)呢?事實上,筆者依然可以利用 Webpack 的插件機制,動態(tài)實現(xiàn)這一個異步化過程:

          private bootstrapEntry(entrypoint: string) {
          const parsed=path.parse(entrypoint);
          const bootstrapPath=path.join(parsed.dir, `${parsed.name}_bootstrap${parsed.ext}`);
          this.virtualModules[bootstrapPath]=`import('./${parsed.name}${parsed.ext}')`;
          return bootstrapPath;
          }
          

          在上述的 bootstrapEntry 方法中,筆者基于原本的 entrypoint 文件,創(chuàng)建一個虛擬文件,這個文件的內(nèi)容就是:

          import('./entrypoint.ts');
          

          再通過 webpack-virtual-modules 這個插件,在相同目錄生成一個虛擬文件,將原本的入口進行替換,就完成了 module-federation 的結(jié)構(gòu)轉(zhuǎn)換。這樣,配合一些其他的相應(yīng)配置,筆者就可以通過一個簡單參數(shù)開啟和關(guān)閉 module-federation,把項目變成一個 Webpack5 ready 的結(jié)構(gòu),當相關(guān)項目陸續(xù)適配成功,便可以一起歡樂的上線了。

          6.后記

          對編譯的大重構(gòu)是筆者蓄謀已久的事情,猶記得加入團隊之時,第一次接觸到編譯鏈路如此復(fù)雜的項目,深感自己的項目經(jīng)歷太淺,接觸的編譯和打包都如此簡單,如今親自操刀才知道,這中間除了許多技術(shù)難題,大項目必有的祖?zhèn)髋渲靡彩亲璧K項目進步的一大阻力。

          Webpack 5 的 Beta 周期很長,所以在 Webpack5 發(fā)布之后,兼容問題還真不如預(yù)想的那么多,不過 Webpack5 的文檔有些坑,如果不是使用 NodeApi 的時候,有類型聲明,筆者絕對無法發(fā)現(xiàn)官方不少文檔的給的參數(shù)還是 webpack4 的舊數(shù)據(jù),對不上號。于是不得不埋著頭調(diào)試源代碼,尋找正確的配置方式,不過也因此收獲良多。并且 Webpack5 還在持續(xù)迭代中,還存在一些 bug,比如 Module Federation 中使用了不恰當?shù)呐渲茫赡軙?dǎo)致奇怪的編譯結(jié)果,并且不會報錯。所以,遇到問題大家要大膽地提 issue。本次重構(gòu)的經(jīng)驗就暫時說到這兒,如有不當之處,歡迎斧正。


          主站蜘蛛池模板: 亚洲AV无码一区二区三区久久精品 | 国产美女口爆吞精一区二区| 中文字幕久久亚洲一区| 亚洲熟女综合色一区二区三区 | 国产成人一区二区动漫精品| 日本成人一区二区三区| 国产精品视频一区| 中文字幕AV一区二区三区| 国产亚洲一区二区精品| 精品国产一区二区三区2021| 日本一区二区三区在线视频观看免费| 一区二区三区高清| 国产精品第一区揄拍| 精品国产一区二区三区不卡| 亚洲丰满熟女一区二区v| 色婷婷一区二区三区四区成人网| 久久精品国产一区| 久久久久久综合一区中文字幕| 亚洲夜夜欢A∨一区二区三区| 国产激情一区二区三区 | 久久99精品免费一区二区| 精品伦精品一区二区三区视频| 夜夜精品无码一区二区三区| 亚洲中文字幕丝袜制服一区 | 一区二区在线视频观看| 日韩视频在线观看一区二区| 一区二区手机视频| 清纯唯美经典一区二区| 日本成人一区二区| 偷拍激情视频一区二区三区| 国产成人久久一区二区不卡三区| 日韩a无吗一区二区三区| 一区二区三区在线| 精品一区二区久久久久久久网站| 无码AV天堂一区二区三区| 天堂va在线高清一区 | 国产伦精品一区二区三区视频猫咪| 久久精品国产一区二区| 无码人妻一区二区三区在线视频 | 国产AV一区二区三区传媒| 精品国产一区二区三区四区|