整合營(yíng)銷(xiāo)服務(wù)商

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

          免費(fèi)咨詢熱線:

          騰訊文檔在線編輯怎么使用?

          訊文檔是一款在線協(xié)作編輯工具,可用于多人實(shí)時(shí)編輯文檔、表格和幻燈片等。以下是使用騰訊文檔進(jìn)行在線編輯的基本步驟:

          1、登錄:在瀏覽器中打開(kāi)騰訊文檔的網(wǎng)頁(yè)(搜索“騰訊文檔”就能找到地址),使用騰訊賬號(hào)登錄或者使用微信掃碼登錄。

          2、創(chuàng)建文檔:登錄后,點(diǎn)擊左上角的“新建”按鈕,選擇創(chuàng)建的文檔類(lèi)型(文檔、表格或幻燈片)。

          3、編輯文檔:在創(chuàng)建的文檔中,可以像使用常規(guī)編輯器一樣進(jìn)行文字輸入、格式設(shè)置、插入圖片和鏈接等操作。

          4、評(píng)論和批注:在編輯過(guò)程中,可以通過(guò)選中文字、點(diǎn)擊右側(cè)出現(xiàn)的插入批注按鈕,在文檔中添加評(píng)論批注或者@他人,方便與其他人進(jìn)行交流和協(xié)作。

          5、共享和協(xié)作:點(diǎn)擊右上角的“分享”按鈕,可以將文檔鏈接發(fā)送給其他人共享和協(xié)作。也可以設(shè)置權(quán)限,指定其他人可查看、編輯或評(píng)論文檔。

          右上會(huì)出現(xiàn)正在查看的人頭像圖標(biāo)。多人編輯時(shí),可以看到其他人的光標(biāo)位置和實(shí)時(shí)編輯內(nèi)容。

          6、版本控制:騰訊文檔支持文件版本控制功能,可以在編輯過(guò)程中保存文檔的不同版本,并在需要時(shí)進(jìn)行查看和恢復(fù)。

          點(diǎn)擊右上角的“三橫杠”圖標(biāo)。點(diǎn)擊“版本歷史記錄”。

          在版本歷史記錄頁(yè)面,即可查看文檔的“所有創(chuàng)建人、修改時(shí)間和修改內(nèi)容”。

          7、導(dǎo)出和下載:完成編輯后,點(diǎn)擊右上角的“三橫杠”圖標(biāo),可以將文檔導(dǎo)出為Word、PDF、圖片、HTML等格式,直接下載到本地計(jì)算機(jī)。

          以上是騰訊文檔在線編輯的基本使用方法,具體操作步驟可能會(huì)有一些微小的差異,以實(shí)際使用為準(zhǔn)。

          本次操作演示使用的操作環(huán)境如下:

          硬件型號(hào):ThinkPad T14

          系統(tǒng)版本:Windows 10

          APP版本:騰訊文檔在線版 3.0

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

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

          1.前言

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

          2.大型項(xiàng)目編譯之痛

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

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

          3.All in One

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

          3.1.整合基于 lerna 的倉(cāng)庫(kù)結(jié)構(gòu)

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

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

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

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

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

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

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

          這樣的文件,一般分為如下幾類(lèi):

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

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

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

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

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

          3.3.定制化的 webpack 流程

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

          既然,gulp 可以自定義流程來(lái)實(shí)現(xiàn) html 生成,那么,筆者也可以單獨(dú)寫(xiě)一個(gè) webpack 插件來(lái)實(shí)現(xiàn)定制的流程。

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

          對(duì)于生成 html 的場(chǎng)景,通過(guò)增加一個(gè)插件,在 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),通過(guò)這一句,筆者可以將所有被自定義生成所依賴的文件加入的 webpack 的依賴系統(tǒng)中,那么當(dāng)這些文件發(fā)生變更的時(shí)候,webpack 能夠自動(dòng)再次觸發(fā)對(duì)應(yīng)的生成流程。

          3.4.一鍵化的開(kāi)發(fā)體驗(yàn)

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

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

          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)試是基于特殊的測(cè)試環(huán)境,通過(guò) whislte 進(jìn)行代理,這樣的步驟也可以自動(dòng)化,那么,對(duì)于開(kāi)發(fā)來(lái)說(shuō),一切就很輕松了,一條命令,輕松搬磚~

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

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

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

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

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

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

          4.編譯提速

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

          4.1.消失的 enum

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

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

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

          // 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 加上一個(gè) const 關(guān)鍵字呢?它會(huì)變成這樣:

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

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

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

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

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

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

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

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

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

          4.2.愛(ài)恨交加的 decorator 及依賴注入

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

          // 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();
          

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

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

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

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

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

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

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

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

          5.Webpack5 升級(jí)之路

          5.1.一些兼容問(wèn)題處理

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

          5.1.1.SplitChunks 自定義 ChunkGroups 報(bào)錯(cuò)

          如果你也是 splitChunks 的重度用戶,在升級(jí) webpack5 的過(guò)程中,你可能會(huì)遇到如下警告:

          這個(gè)警告的說(shuō)明并不是十分明確,用大白話來(lái)說(shuō),出現(xiàn)了這個(gè)提示,說(shuō)明你的 chunkGroups 配置中,出現(xiàn)了 module 同時(shí)屬于 A、B 兩組(此處 A、B 是兩個(gè) Entrypoint 或者兩個(gè)異步模塊),但是你明確指定了將模塊屬于 A 的情況。為何此時(shí) Webpack5 會(huì)報(bào)出警告呢?因?yàn)閺耐ǔG闆r來(lái)說(shuō),module 分屬于兩個(gè) Entrypoint 或者異步模塊,module 應(yīng)該被提取為公共模塊的,如果 module 被歸屬于 A,那么 B 模塊如果單獨(dú)加載,就無(wú)法成功了。

          不過(guò),一般來(lái)說(shuō),出現(xiàn)這樣的指定,如果不是配置錯(cuò)誤,那就是 A、B 之間已經(jīng)有明確的加載順序。但是這個(gè)加載順序,Webpack 并不知道。對(duì)于 entrypoint,webpack5 中,允許通過(guò) dependOn 屬性,指定 entry 之間的依賴關(guān)系。但是對(duì)于異步模塊,則沒(méi)有這么遍歷的設(shè)置。當(dāng)然,筆者也可以通過(guò)自定義插件,在 optimize 之前,對(duì)已有的模塊依賴關(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)相繼適配,大家只要將插件更新到最新版本即可。不過(guò),也有一些插件因?yàn)橹T多緣由,一些插件并沒(méi)有及時(shí)更新。(PS:目前,沒(méi)有匹配的插件大多已經(jīng)比較小眾了。)總之,這個(gè)問(wèn)題是比較無(wú)解的,不過(guò)可以適當(dāng)?shù)却瑧?yīng)該在近期,大部分插件都會(huì)適配 webpack5,事實(shí)上 webpack5 也是用了不少改名大法,部分接口進(jìn)行轉(zhuǎn)移,調(diào)用方式發(fā)生了改變,倒也沒(méi)有全部翻天覆地的變化,所以,實(shí)在等不及的小插件不妨試試自己 fork 修改一下。

          5.2.Module Federation 初體驗(yàn)

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

          但是,隨著組件的增多,共享組件的 Host 增多,這樣的方式帶來(lái)了一些問(wèn)題:

          1. Component 需要為 Host 專(zhuān)門(mén)打包,它不是一個(gè)可以獨(dú)立運(yùn)行的組件,每一個(gè)運(yùn)行該 Component 的 Host 必須攜帶完整的運(yùn)行時(shí),否則 Component 就需要為不同的 Host 打出不同的殘疾包。
          2. Component 與 Component 之間如果存在較大的共享模塊,無(wú)法通過(guò) external 解決。

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

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

          Webpack5 的 Module Federation 是依賴于其動(dòng)態(tài)加載機(jī)制的,因此,在它的演示實(shí)例中,你都可以看到這樣的結(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 上,這里,是因?yàn)樗械囊蕾嚩夹枰獎(jiǎng)討B(tài)判定加載,如果不把入口變成一個(gè)異步的 chunk,那如何去保障依賴能夠按順序加載內(nèi)?畢竟實(shí)現(xiàn) Moudle Federation 的核心是 基于 webpack_require 的動(dòng)態(tài)加載系統(tǒng)。由于 Module Federation 需要多個(gè)倉(cāng)庫(kù)的聯(lián)動(dòng),它的推進(jìn)必然是相對(duì)漫長(zhǎng)的過(guò)程。那么筆者是否有必要將現(xiàn)有的項(xiàng)目直接改造為 index-bootstrap 結(jié)構(gòu)呢?事實(shí)上,筆者依然可以利用 Webpack 的插件機(jī)制,動(dòng)態(tài)實(shí)現(xiàn)這一個(gè)異步化過(guò)程:

          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)建一個(gè)虛擬文件,這個(gè)文件的內(nèi)容就是:

          import('./entrypoint.ts');
          

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

          6.后記

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

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


          主站蜘蛛池模板: 国产伦精品一区二区三区免费迷| 国产一区二区三区在线观看免费| 日本v片免费一区二区三区| 无码视频一区二区三区在线观看 | 亚洲一区二区三区高清| 久久精品一区二区三区四区| 99精品国产一区二区三区不卡| 欧美一区内射最近更新| 日本视频一区二区三区| 国产福利视频一区二区 | 精品国产一区二区三区AV | 国产不卡视频一区二区三区| 亚洲国产精品乱码一区二区| 亚洲AV成人一区二区三区观看| 无码精品人妻一区二区三区AV| 久久精品国产亚洲一区二区| 亚洲日韩国产欧美一区二区三区| 一区二区三区91| 中文字幕一区二区三区在线观看 | 国产精品一区二区无线| 国产一区玩具在线观看| 五月婷婷一区二区| 亚洲成av人片一区二区三区| 亚洲无圣光一区二区| 亚洲国产一区二区视频网站| 久久精品黄AA片一区二区三区| 国产成人一区二区三区高清| 日韩人妻精品无码一区二区三区 | 在线观看亚洲一区二区| 无遮挡免费一区二区三区| 无码免费一区二区三区免费播放| 国产精品一区二区AV麻豆| 日韩精品乱码AV一区二区| 国产第一区二区三区在线观看 | 91香蕉福利一区二区三区| 一区二区三区www| 精品亚洲av无码一区二区柚蜜| 丝袜无码一区二区三区| 亚洲色精品三区二区一区| 国产成人精品无码一区二区| 精品一区二区三区在线播放|