vue-code-view是一個基于 vue 2.x、輕量級的代碼交互組件,在網頁中實時編輯運行代碼、預覽效果的代碼交互組件。
使用此組件, 不論 vue 頁面還是 Markdown 文檔中的示例代碼,效果如下:
當項目中頁面或者 Markdown 文檔包含大量代碼時,使用 highlight.js 進行代碼高亮后極大的增大了閱讀性,但是當我們閱讀時想要對當前代碼進行編輯調試時,只能打開本地開發環境或者跳轉至 codepen codesandbox等在線項目示例。即使是很簡單的代碼示例仍然避免不了上述場景的繁瑣步驟!如果遇到網絡不好,或者本地開發環境沒有安裝配置的情況,那就很遺憾了!
目前大多開源項目的 Markdown 文檔示例大多支持了示例代碼的實時渲染,可以在文檔頁面中看到源碼的運行效果,提供了在線項目的跳轉功能。當需要調試代碼時,還是需要重復上述步驟,體驗不是太友好。
那么能不能有這么一個組件能支持在頁面中編輯代碼,實時運行預覽效果?在網絡找了好久,沒有找到 vue 版本,只看到了 react-code-view,受其啟發,自已編寫了一個 vue 版本組件 vue-code-view !
目前組件已實現的主要功能特性:
參數 | 說明 | 類型 | 默認值 |
theme | theme mode,支持 light / dark | light | dark | dark |
showCode | 是否顯示代碼編輯器 | boolean | false |
source | 示例代碼 | string | - |
renderToolbar | 自定義工具欄展示 | function | - |
errorHandler | 錯誤處理函數 | function | - |
debounceDelay | 錯誤處理防抖延遲(ms) | number | 300 |
使用 npm 或 yarn 安裝組件包。
npm i vue-code-view
# or
yarn add vue-code-view
組件使用包含運行時編譯器的 Vue 構建版本,所以需要單獨配置下。
若使用 vue cli,需要在vue.config.js文件進行如下配置:
module.exports={
runtimeCompiler: true,
// or
chainWebpack: (config)=> {
config.resolve.alias
.set("vue$", "vue/dist/vue.esm.js");
},
};
在項目的入口文件 main.js 中引入組件及樣式,注冊組件。
import Vue from "vue";
import App from "./App.vue";
import CodeView from "vue-code-view";
import "vue-code-view/lib/vue-code-viewer.css";
...
Vue.use(CodeView);
...
使用組件的source屬性傳入示例代碼。
示例代碼格式支持 <template> <script> <style>,<template>不能為空;暫不支持JSX 語法。
<template>
<div id="app">
<code-viewer :source="code_example"></code-viewer>
</div>
</template>
<script>
const code_source=`
<template>
<div id="app">
<img alt="Vue logo" class="logo" src="https://cn.vuejs.org/images/logo.svg" />
<h1>Welcome to Vue.js {{version}} !</h1>
</div>
</template>
<script>
export default {
data() {
return {
version: '2.x'
};
},
};
<\/script>
<style>
.logo {
width:66px;
}
</style> `,
export default {
data() {
return {
code_example: code_source
};
},
};
</script>
組件 JSX 語法使用方式。
<script>
const code_example=`<template>
<div id="app">
<img alt="Vue logo" class="logo" src="https://cn.vuejs.org/images/logo.svg" />
<h1>Welcome to Vue.js !</h1>
</div>
</template> `;
export default {
name: "demo",
render() {
return (
<div >
<code-viewer source={code_example}
showCode={false}
></code-viewer>
</div>
);
},
};
</script>
項目引入其他組件庫后,組件的示例源代碼中直接使用即可,實現預覽調試功能。
組件內置了錯誤預處理,目前支持代碼為空、代碼格式錯誤(<template>內容不存在)等,以文字的形式顯示在示例區域,也提供了自定義錯誤方式 errorHandler(使用 Notice 組件進行信息告知)。
render() {
return (
<div >
<code-viewer
source={code_example}
showCode={false}
errorHandler={(errorMsg)=> {
this.$notify.error({
title: "Info",
message: errorMsg,
});
}}
></code-viewer>
</div>
)
}
示例使用了antd vue 的 notify組件進行消息提醒,效果如下:
具體示例效果詳見 組件Markdown說明文檔
后續功能持續迭代中!激情期待。
歡迎點贊+轉發+關注!大家的支持是我分享最大的動力!!!
當今信息爆炸的時代,數據成為了一種寶貴的資源。Flyscrape,一個現代的網絡爬蟲工具包,提供了一種快速、簡便的方式來構建自定義的網絡爬蟲。
Flyscrape 是一個獨立的網絡爬蟲工具,具有以下特點:
在 Mac、Linux 或 WSL 上,通過以下命令安裝 Flyscrape:
curl -fsSL https://flyscrape.com/install | bash
使用 new 命令創建一個新的抓取腳本:
flyscrape new hackernews.js
在腳本中定義抓取的配置:
export const config={
url: "https://hackernews.com",
// 更多配置...
};
編寫數據提取邏輯,使用類似于 jQuery 或 cheerio 的 API:
export default function({ doc, absoluteURL }) {
// 數據提取代碼...
};
使用 dev 命令啟動開發模式:
flyscrape dev hackernews.js
使用 run 命令執行爬蟲:
flyscrape run hackernews.js
爬蟲將輸出一個 JSON 數組,包含所有抓取的頁面數據。
Flyscrape 為用戶提供了一個高效、便捷的數據抓取解決方案。如果你需要快速構建自定義爬蟲,Flyscrape 是一個值得嘗試的選擇。
靜態站點生成SSG - Static Site Generation是一種在構建時生成靜態HTML等文件資源的方法,其可以完全不需要服務端的運行,通過預先生成靜態文件,實現快速的內容加載和高度的安全性。由于其生成的是純靜態資源,便可以利用CDN等方案以更低的成本和更高的效率來構建和發布網站,在博客、知識庫、API文檔等場景有著廣泛應用。
在前段時間遇到了一個比較麻煩的問題,我們是主要做文檔業務的團隊,而由于對外的產品文檔涉及到全球很多地域的用戶,因此在CN以外地域的網站訪問速度就成了比較大的問題。雖然我們有多區域部署的機房,但是每個地域機房的數據都是相互隔離的,而實際上很多產品并不會做很多特異化的定制,因此文檔實際上是可以通用的,特別是提供了多語言文檔支持的情況下,各地域共用一份文檔也變得合理了起來。而即使對于CN和海外地區有著特異化的定制,但在海外本身的訪問也會有比較大的局限,例如假設機房部署在US,那么在SG的訪問速度同樣也會成為一件棘手的事情。
那么問題來了,如果我們需要做到各地域訪問的高效性,那么就必須要在各個地域的主要機房部署服務,而各個地域又存在數據隔離的要求,那么在這種情況下我們可能需要手動將文檔復制到各個機房部署的服務上去,這必然就是一件很低效的事情,即使某個產品的文檔不會經常更新,但是這種人工處理的方式依然是會耗費大量精力的,顯然是不可取的。而且由于我們的業務是管理各個產品的文檔,在加上在海外業務不斷擴展的情況下,這類的反饋需求必然也會越來越多,那么解決這個問題就變成了比較重要的事情。
那么在這種情況下,我就忽然想到了我的博客站點的構建方式,為了方便我會將博客直接通過gh-pages分支部署在GitHub Pages上,而GitHub Pages本身是不支持服務端部署的,也就是說我的博客站全部都是靜態資源。由此可以想到在業務中我們的文檔站也可以用類似的方式來實現,也就是在發布文檔的時候通過SSG編譯的方式來生成靜態資源,那么在全部的內容都是靜態資源的情況下,我們就可以很輕松地基于CDN來實現跨地域訪問的高效性。此外除了調度CDN的分發方式,我們還可以通過將靜態資源發布到業務方申請的代碼倉庫中,然后業務方就可以自行部署服務與資源了,通過多機房部署同樣可以解決跨地域訪問的問題。
當然,因為要考慮到各種問題以及現有部署方式的兼容,在我們的業務中通過SSG來單獨部署實現跨地域的高效訪問并不太現實,最終大概率還是要走合規的各地域數據同步方案來保證數據的一致性與高效訪問。但是在思考通過SSG來作為這個問題的解決方案時,我還是很好奇如何在React的基礎上來實現SSG渲染的,畢竟我的博客就可以算是基于Mdx的SSG渲染。最開始我把這個問題想的特別復雜,但是在實現的時候發現只是實現基本原理的話還是很粗暴的解決方案,在渲染的時候并沒有想象中要處理得那么精細,當然實際上要做完整的方案特別是要實現一個框架也不是那么容易的事情,對于數據的處理與渲染要做很多方面的考量。
在我們正式開始聊SSG的基本原理前,我們可以先來看一下通過SSG實現靜態站點的特點:
那么同樣的,通過SSG生成的靜態資源站點也有一些局限性:
綜上所述,SSG更適用于生成內容較為固定、不需要頻繁更新、且對于數據延遲敏感較低的的項目,并且實際上我們可能也只是選取部分能力來優化首屏等場景,最終還是會落到CSR來實現服務能力。因此當我們要選擇渲染方式的時候,還是要充分考慮到業務場景,由此來確定究竟是CSR - Client Side Render、SSR - Server Side Render、SSG - Static Site Generation更適合我們的業務場景,甚至在一些需要額外優化的場景下,ISR - Incremental Static Regeneration、DPR - Distributed Persistent Rendering、ESR - Edge Side Rendering等也可以考慮作為業務上的選擇。
當然,回到最初我們提到的問題上,假如我們只是為了靜態資源的同步,通過CDN來解決全球跨地域訪問的問題,那么實際上并不是一定需要完全的SSG來解決問題。將CSR完全轉變為SSR畢竟是一件改造范圍比較大的事情,而我們的目標僅僅是一處生產、多處消費,因此我們可以轉過來想一想實際上JSON文件也是屬于靜態資源的一種類型,我們可以直接在前端發起請求將JSON文件作為靜態資源請求到瀏覽器并且借助SDK渲染即可,至于一些交互行為例如點贊等功能的速度問題我們也是可以接受的,文檔站最的主要行為還是閱讀文檔。此外對于md文件我們同樣可以如此處理,例如docsify就是通過動態請求,但是同樣的對于搜索引擎來說這些需要執行Js來動態請求的內容并沒有那么容易抓取,所以如果想比較好地實現這部分能力還是需要不斷優化迭代。
那么接下來我們就從基本原理開始,優化組件編譯的方式,進而基于模版渲染生成SSG,文中相關API的調用基于React的17.0.2版本實現,內容相關的DEMO地址為https://github.com/WindrunnerMax/webpack-simple-environment/tree/master/packages/react-render-ssg。
通常當我們使用React進行客戶端渲染CSR時,只需要在入口的index.html文件中置入<div id="root"></div>的獨立DOM節點,然后在引入的xxx.js文件中通過ReactDOM.render方法將React組件渲染到這個DOM節點上即可。將內容渲染完成之后,我們就會在某些生命周期或者Hooks中發起請求,用以動態請求數據并且渲染到頁面上,此時便完成了組件的渲染流程。
那么在前邊我們已經聊了比較多的SSG內容,那么可以明確對于渲染的主要內容而言我們需要將其離線化,因此在這里就需要先解決第一個問題,如何將數據離線化,而不是在瀏覽器渲染頁面之后再動態獲取。很明顯在前邊我們提到的將數據從數據庫請求出來之后寫入json文件就是個可選的方式,我們可以在代碼構建的時候請求數據,在此時將其寫入文件,在最后一并上傳到CDN即可。
在我們的離線數據請求問題解決后,我們就需要來看渲染問題了,前邊也提到了類似的問題,如果依舊按照之前的渲染思路,而僅僅是將數據請求的地址從服務端接口替換成了靜態資源地址,那么我們就無法做到SEO以及更快的首屏體驗。其實說到這里還有一個比較有趣的事情,當我們用SSR的時候,假如我們的組件是dynamic引用的,那么Next在輸出HTML的時候會將數據打到HTML的<script />標簽里,在這種情況下實際上首屏的效率還是不錯的,并且Google進行索引的時候是能夠正常將動態執行Js渲染后的數據抓取,對于我們來說也可以算作一種離線化的渲染方案。
那么這種方式雖然可行但是并不是很好的方案,我們依然需要繼續解決問題,那么接下來我們需要正常地來渲染完整的HTML結構。在ReactDOM的Server API中存在存在兩個相關的API,分別是renderToStaticMarkup與renderToString,這兩個API都可以將React組件輸出HTML標簽的結構,只是區別是renderToStaticMarkup渲染的是不帶data-reactid的純HTML結構,當客戶端進行React渲染時會完全重建DOM結構,因此可能會存在閃爍的情況,renderToString則渲染了帶標記的HTML結構,React在客戶端不會重新渲染DOM結構,那么在我們的場景下時需要通過renderToString來輸出HTML結構的。
// packages/react-render-ssg/src/basic/index.ts
import ReactDOMServer from "react-dom/server";
const App=React.createElement(
React.Fragment,
null,
React.createElement("div", null, "React HTML Render"),
React.createElement(
"button",
{
onClick: ()=> alert("On Click"),
},
"Button"
)
);
const HTML=ReactDOMServer.renderToString(App);
// <div data-reactroot="">React HTML Render</div><button data-reactroot="">Button</button>
當前我們已經得到組件渲染過后的完整HTML結構,緊接著從輸出的內容我們可以看出來一個問題,我們定義的onClick函數并沒有在渲染過后的HTML結構中體現出來,此時在我們的HTML結構中只是一些完整的標簽,并沒有任何事件的處理。當然這也是很合理的情況,我們是用React框架實現的事件處理,其并不太可能直接完整地映射到輸出的HTML中,特別是在復雜應用中我們還是需要通過React來做后續事件交互處理的,那么很顯然我們依舊需要在客戶端處理相關的事件。
那么在React中我們常用的處理客戶端渲染函數就是ReactDOM.render,那么當前我們實際上已經處理好了HTML結構,而并不需要再次將內容完整地渲染出來,或者換句話說我們現在需要的是將事件掛在相關DOM上來處理交互行為,將React附加到在服務端環境中已經由React渲染的現有HTML上,由React來接管有關的DOM的處理。那么對于我們來說,我們需要將同樣的React組件在客戶端一并定義,然后將其輸出到頁面的Js中,也就是說這部分內容是需要在客戶端中執行的。
// packages/react-render-ssg/src/basic/index.ts
const PRESET=`
const App=React.createElement(
React.Fragment,
null,
React.createElement("div", null, "React HTML Render"),
React.createElement(
"button",
{
onClick: ()=> alert("On Click"),
},
"Button"
)
);
const _default=App;
ReactDOM.hydrate(_default, document.getElementById("root"));
`;
await fs.writeFile(`dist/${jsPathName}`, PRESET);
實際上這部分代碼都是在服務端生成的,我們此時并沒有在客戶端運行的內容,或者說這是我們的編譯過程,還沒有到達運行時,所以我們生成的一系列內容都是在服務端執行的,那么很明顯我們是需要拼裝HTML等靜態資源文件的。因此在這里我們可以通過預先定義一個HTML文件的模版,然后將構建過程中產生的內容放到模版以及新生成的文件里,產生的所有內容都將隨著構建一并上傳到CDN上并分發。
<!-- packages/react-render-ssg/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... Meta -->
<title>Template</title>
<!-- INJECT STYLE -->
</head>
<body>
<div id="root">
<!-- INJECT HTML -->
</div>
<!-- ... React Library -->
<!-- INJECT SCRIPT -->
</body>
</html>
// packages/react-render-ssg/src/basic/index.ts
const template=await fs.readFile("./public/index.html", "utf-8");
await fs.mkdir("dist", { recursive: true });
const random=Math.random().toString(16).substring(7);
const jsPathName=`${random}.js`;
const html=template
.replace(/<!-- INJECT HTML -->/, HTML)
.replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsPathName}"></script>`);
await fs.writeFile(`dist/${jsPathName}`, PRESET);
await fs.writeFile(`dist/index.html`, html);
至此我們完成了最基本的SSG構建流程,接下來就可以通過靜態服務器訪問資源了,在這部分DEMO可以直接通過ts-node構建以及anywhere預覽靜態資源地址。實際上當前很多開源的靜態站點搭建框架例如VitePress、RsPress等等都是采用類似的原理,都是在服務端生成HTML、Js、CSS等等靜態文件,然后在客戶端由各自的框架重新接管DOM的行為,當然這些框架的集成度很高,對于相關庫的復用程度也更高。而針對于更復雜的應用場景,還可以考慮Next、Gatsby等框架實現,這些框架在SSG的基礎上還提供了更多的能力,對于更復雜的應用場景也有著更好的支持。
雖然在前邊我們已經實現了最基本的SSG原理,但是很明顯我們為了最簡化地實現原理人工處理了很多方面的內容,例如在上述我們輸出到Js文件的代碼中是通過PRESET變量定義的純字符串實現的代碼,而且我們對于同一個組件定義了兩遍,相當于在服務端和客戶端分開定義了運行的代碼,那么很明顯這樣的方式并不太合理,接下來我們就需要解決這個問題。
那么我們首先需要定義一個公共的App組件,在該組件的代碼實現中與前邊的基本原理中一致,這個組件會共享在服務端的HTML生成和客戶端的React Hydrate,而且為了方便外部的模塊導入組件,我們通常都是通過export default的方式默認導出整個組件。
// packages/react-render-ssg/src/rollup/app.tsx
import React from "react";
const App=()=> (
<React.Fragment>
<div>React Render SSG</div>
<button onClick={()=> alert("On Click")}>Button</button>
</React.Fragment>
);
export default App;
緊接著我們先來處理客戶端的React Hydrate,在先前我們是通過人工維護的編輯的字符串來定義的,而實際上我們同樣可以打包工具在Node端將組建編譯出來,以此來輸出Js代碼文件。在這里我們選擇使用Rollup來打包Hydrate內容,我們以app.tsx作為入口,將整個組件作為iife打包,然后將輸出的內容寫入APP_NAME,然后將實際的hydrate置入footer,就可以完成在客戶端的React接管DOM執行了。
// packages/react-render-ssg/rollup.config.js
const APP_NAME="ReactSSG";
const random=Math.random().toString(16).substring(7);
export default async ()=> {
return {
input: "./src/rollup/app.tsx",
output: {
name: APP_NAME,
file: `./dist/${random}.js`,
format: "iife",
globals: {
"react": "React",
"react-dom": "ReactDOM",
},
footer: `ReactDOM.hydrate(React.createElement(${APP_NAME}), document.getElementById("root"));`,
},
plugins: [
// ...
],
external: ["react", "react-dom"],
};
};
接下來我們來處理服務端的HTML文件生成與資源的引用,這里的邏輯與先前的基本原理中服務端生成邏輯差別并不大,只是多了通過終端調用Rollup打包的邏輯,同樣也是將HTML輸出,并且將Js文件引入到HTML中,這里需要特殊關注的是我們的Rollup打包時的輸出文件路徑是在這里由--file參數覆蓋原本的rollup.config.js內置的配置。
// packages/react-render-ssg/src/rollup/index.ts
const exec=promisify(child.exec);
(async ()=> {
const HTML=ReactDOMServer.renderToString(React.createElement(App));
const template=await fs.readFile("./public/index.html", "utf-8");
const random=Math.random().toString(16).substring(7);
const path="./dist/";
const { stdout }=await exec(`npx rollup -c --file=${path + random}.js`);
console.log("Client Compile Complete", stdout);
const jsFileName=`${random}.js`;
const html=template
.replace(/<!-- INJECT HTML -->/, HTML)
.replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsFileName}"></script>`);
await fs.writeFile(`${path}index.html`, html);
})();
當前我們已經復用了組件的定義,并且通過Rollup打包了需要在客戶端運行的Js文件,不需要再人工維護輸出到客戶端的內容。那么場景再復雜一些,假如此時我們的組件有著更加復雜的內容,例如引用了組件庫來構建視圖,以及引用了一些CSS樣式預處理器來構建樣式,那么我們的服務端輸出HTML的程序就會變得更加復雜。
繼續沿著前邊的處理思路,我們在服務端的處理程序僅僅是需要將App組件的HTML內容渲染出來,那么假設此時我們的組件引用了@arco-design組件庫,并且通常我們還需要引用其中的less文件或者css文件。
import "@arco-design/web-react/dist/css/arco.css";
import { Button } from "@arco-design/web-react";
// OR
import "@arco-design/web-react/es/Button/style/index";
import { Button } from "@arco-design/web-react/es/Button";
那么需要關注的是,當前我們運行組件的時候是在服務端環境中,那么在Node環境中顯然我們是不認識.less文件以及.css文件的,實際上先不說這些樣式文件,import語法本身在Node環境中也是不支持的,只不過我們通常是使用ts-node來執行整個運行程序,暫時這點不需要關注,那么對于樣式文件我們在這里實際上是不需要的,所以我們就需要配置Node環境來處理這些樣式文件的引用。
require.extensions[".css"]=()=> undefined;
require.extensions[".less"]=()=> undefined;
但是即使這樣問題顯然沒有結束,熟悉arco-design的打包同學可能會清楚,當我們引入的樣式文件是Button/style/index時,實際上是引入了一個js文件而不是.less文件,如果需要明確引入.less文件的話是需要明確Button/style/index.less文件指向的。那么此時如果我們是引入的.less文件,那么并不會出現什么問題,但是此時我們引用的是.js文件,而這個.js文件中內部的引用方式是import,因為此時我們是通過es而不是lib部分明確引用的,即使在tsconfig中配置了相關解析方式為commonjs也是沒有用的。
{
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true
}
}
}
因此我們可以看到,如果僅僅用ts-node來解析或者說執行服務端的數據生成是不夠的,會導致我們平時實現組件的時候有著諸多限制,例如我們不能隨便引用es的實現而需要借助包本身的package.json聲明的內容來引入內容,如果包不能處理commonjs的引用那么還會束手無策。那么在這種情況下我們還是需要引入打包工具來打包commonjs的代碼,然后再通過Node來執行輸出HTML。通過打包工具,我們能夠做的事情就很多了,在這里我們將資源文件例如.less、.svg都通過null-loader加載,且相關的配置輸出都以commonjs為基準,此時我們輸出的文件為node-side-entry.js。
// packages/react-render-ssg/rspack.server.ts
const config: Configuration={
context: __dirname,
entry: {
index: "./src/rspack/app.tsx",
},
externals: externals,
externalsType: "commonjs",
externalsPresets: {
node: true,
},
// ...
module: {
rules: [
{ test: /\.svg$/, use: "null-loader" },
{ test: /\.less$/, use: "null-loader" },
],
},
devtool: false,
output: {
iife: false,
libraryTarget: "commonjs",
publicPath: isDev ? "" : "./",
path: path.resolve(__dirname, ".temp"),
filename: "node-side-entry.js",
},
};
當前我們已經得到了可以在Node環境中運行的組件,那么緊接著,考慮到輸出SSG時我們通常都需要預置靜態數據,例如我們要渲染文檔的話就需要首先在數據庫中將相關數據表達查詢出來,然后作為靜態數據傳入到組件中,然后在預輸出的HTML中將內容直接渲染出來,那么此時我們的App組件的定義就需要多一個getStaticProps函數聲明,并且我們還引用了一些樣式文件。
// packages/react-render-ssg/src/rspack/app.tsx
import "./app.less";
import { Button } from "@arco-design/web-react";
import React from "react";
const App: React.FC<{ name: string }>=props=> (
<React.Fragment>
<div>React Render SSG With {props.name}</div>
<Button style={{ marginTop: 10 }} type="primary" onClick={()=> alert("On Click")}>
Button
</Button>
</React.Fragment>
);
export const getStaticProps=()=> {
return Promise.resolve({
name: "Static Props",
});
};
export default App;
/* packages/react-render-ssg/src/rspack/app.less */
body {
padding: 20px;
}
同樣的,我們也需要為客戶端運行的Js文件打包,只不過在這里由于我們需要處理預置的靜態數據,我們在打包的時候同樣就需要預先生成模版代碼,當我們在服務端執行打包功能的時候,就需要將從數據庫查詢或者從文件讀取的數據放置于生成的模版文件中,然后以該文件為入口去再打包客戶端執行的React Hydrate能力。在這里因為希望將模版文件看起來更加清晰,我們使用了JSON.parse來處理預置數據,實際上這里只需要將占位預留好,數據在編譯的時候經過stringify直接寫入到模版文件中即可。
// packages/react-render-ssg/src/rspack/entry.tsx
/* eslint-disable @typescript-eslint/no-var-requires */
const Index=require(`<index placeholder>`);
const props=JSON.parse(`<props placeholder>`);
ReactDOM.hydrate(React.createElement(Index.default, { ...props }), document.getElementById("root"));
在模版文件生成好之后,我們就需要以這個文件作為入口調度客戶端資源文件的打包了,這里由于我們還引用了組件庫,輸出的內容自然不光是Js文件,還需要將CSS文件一并輸出,并且我們還需要配置一些通過參數名可以控制的文件名生成、externals等等。這里需要注意的是,此處我們不需要使用html-plugin將HTML文件輸出,這部分調度我們會在最后統一處理。
// packages/react-render-ssg/rspack.config.ts
const args=process.argv.slice(2);
const map=args.reduce((acc, arg)=> {
const [key, value]=arg.split("=");
acc[key]=value || "";
return acc;
}, {} as Record<string, string>);
const outputFileName=map["--output-filename"];
const config: Configuration={
context: __dirname,
entry: {
index: "./.temp/client-side-entry.tsx",
},
externals: {
"react": "React",
"react-dom": "ReactDOM",
},
// ...
builtins: {
// ...
pluginImport: [
{
libraryName: "@arco-design/web-react",
customName: "@arco-design/web-react/es/{{ member }}",
style: true,
},
{
libraryName: "@arco-design/web-react/icon",
customName: "@arco-design/web-react/icon/react-icon/{{ member }}",
style: false,
},
],
},
// ...
output: {
chunkLoading: "jsonp",
chunkFormat: "array-push",
publicPath: isDev ? "" : "./",
path: path.resolve(__dirname, "dist"),
filename: isDev
? "[name].bundle.js"
: outputFileName
? outputFileName + ".js"
: "[name].[contenthash].js",
// ...
},
};
那么此時我們就需要調度所有文件的打包過程了,首先我們需要創建需要的輸出和臨時文件夾,然后啟動服務端commonjs打包的流程,輸出node-side-entry.js文件,并且讀取其中定義的App組件以及預設數據讀取方法,緊接著我們需要創建客戶端入口的模版文件,并且通過調度預設數據讀取方法將數據寫入到入口模版文件中,此時我們就可以通過打包的commonjs組件執行并且輸出HTML了,并且客戶端運行的React Hydrate代碼也可以在這里一并打包出來,最后將各類資源文件的引入一并在HTML中替換并且寫入到輸出文件中就可以了。至此當我們打包完成輸出文件后,就可以使用靜態資源服務器啟動SSG的頁面預覽了。
const appPath=path.resolve(__dirname, "./app.tsx");
const entryPath=path.resolve(__dirname, "./entry.tsx");
require.extensions[".less"]=()=> undefined;
(async ()=> {
const distPath=path.resolve("./dist");
const tempPath=path.resolve("./.temp");
await fs.mkdir(distPath, { recursive: true });
await fs.mkdir(tempPath, { recursive: true });
const { stdout: serverStdout }=await exec(`npx rspack -c ./rspack.server.ts`);
console.log("Server Compile", serverStdout);
const nodeSideAppPath=path.resolve(tempPath, "node-side-entry.js");
const nodeSideApp=require(nodeSideAppPath);
const App=nodeSideApp.default;
const getStaticProps=nodeSideApp.getStaticProps;
let defaultProps={};
if (getStaticProps) {
defaultProps=await getStaticProps();
}
const entry=await fs.readFile(entryPath, "utf-8");
const tempEntry=entry
.replace("<props placeholder>", JSON.stringify(defaultProps))
.replace("<index placeholder>", appPath);
await fs.writeFile(path.resolve(tempPath, "client-side-entry.tsx"), tempEntry);
const HTML=ReactDOMServer.renderToString(React.createElement(App, defaultProps));
const template=await fs.readFile("./public/index.html", "utf-8");
const random=Math.random().toString(16).substring(7);
const { stdout: clientStdout }=await exec(`npx rspack build -- --output-filename=${random}`);
console.log("Client Compile", clientStdout);
const jsFileName=`${random}.js`;
const html=template
.replace(/<!-- INJECT HTML -->/, HTML)
.replace(/<!-- INJECT STYLE -->/, `<link rel="stylesheet" href="${random}.css">`)
.replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsFileName}"></script>`);
await fs.writeFile(path.resolve(distPath, "index.html"), html);
})();
作者:WindrunnerMax
鏈接:https://juejin.cn/post/7375426024705769482
來源:稀土掘金
*請認真填寫需求信息,我們會在24小時內與您取得聯系。