捨棄 create-react-app 之餘還架了個 astro blog 昭告天下:打包與預覽

webpack 設定已經備妥,現在剩下啟動打包的指令、以及你可能需要一點小工具幫你把放在 ./public 中的靜態資源(比如 favicon)複製一份到輸出資料夾中。今天就來介紹個人慣用的腳本內容。

打包

Makefile 腳本

.PHONY: build
build:
	node -r esbuild-runner/register ./script/build.ts

script/build.ts 內容

個人基本上會將打包邏輯放在 ./script/build.ts 中,而這個檔案會處理以下三件事情:

  1. 啟動 webpack 進行打包
  2. ./public 中的靜態資源複製一份到打包完的目的資料夾
  3. (在專案內容比較簡單的時候)手動產生一份 sitemap.xml 並灌到打包完的資料夾中

先看看第一步「透過 webpack 進行打包」的功能:

import webpack from 'webpack';
import webpackProductionConfig from '@/config/webpack.config.production';

function webpackBuild(): Promise<void> {
  return new Promise((resolve, reject) => {
    const compiler = webpack(webpackProductionConfig);
    compiler.run((error, stats) => {
      if (error) {
        console.error(error);
        return reject(error);
      }
      if (stats) {
        // 將毫秒轉回秒
        console.info('build time: ', (stats.endTime - stats.startTime) / 1000);
        // 印出打包過程 log
        console.info('build log: ', stats.toString());
      }
      if (stats && stats.hasErrors()) {
        return reject(stats.compilation.errors);
      }
      compiler.close((closeError) => {
        if (closeError) {
          console.error(closeError);
          return reject(closeError);
        } else {
          return resolve();
        }
      });
    });
  });
}

webpackProductionConfig 傳入 webpack() 建立打包用的 compiler 並啟動之。在有錯誤發生時,執行 reject() 把錯誤拋出。中間讀取 stats.endTime / stats.startTime 來顯示打包時長。其他打包過程中出現的資訊則透過 stats.toString() 直接輸出到終端。

如果萬事順利,則進入 compiler.close() callback 中的的 return resolve(); 分之,結束 webpack 打包流程。


接著來將 ./public 中除了 index.html 以外的內容都複製一份到打包完畢的目的地資料夾:

import fs from 'fs-extra';
import { resolvePath } from '@/tool/resolvePath';
import env from '@/config/data/env';
import type { EnvForBuildApp } from '@/config/data/types';

const ENV_FOR_BUILD_APP = env['process.env'] as EnvForBuildApp;
const BUILD_DESTINATION_PATH = resolvePath(
  JSON.parse(ENV_FOR_BUILD_APP.BUILD_DESTINATION)
);

function copyPublicAsset() {
  fs.copySync(resolvePath('public'), BUILD_DESTINATION_PATH, {
    filter: (file) => file !== resolvePath('public/index.html'),
  });
  console.info('copy public folder done.');
}

透過 filter 跳過 index.html 是因為這份檔案已經透過 webpack 的 html-webpack-plugin 處理了,我們不需要在這裡再複製一份。關於 fx-extra 中的 copySync 詳細說明可參考官方文件


而在 React app 結構較簡單的情況下,個人會在處理打包時順便將 sitemap 一起做掉。

function generateSiteMap() {
  const content = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

<url>
<loc>https://example.com/</loc>
<lastmod>${new Date().toISOString()}</lastmod>
</url>

</urlset>`;
  fs.writeFileSync(`${BUILD_DESTINATION_PATH}/sitemap_index.xml`, content);
  console.log('sitemap.xml generated done.');
}

上方 xml 內容參考自 Google Search Center: Build and submit a sitemap

sitemap 的功能基本上就是在告訴搜尋引擎「這個網站包含哪些內容」。需要更近一步的介紹可看看 Google 的 Learn about sitemaps 一文。


最後,把這三個功能包起來執行即可:

async function buildProduction() {
  console.info('build start.');
  await webpackBuild();
  copyPublicAsset();
  generateSiteMap();
  console.info('build end.');
}

buildProduction();

預覽

個人習慣在打包完畢後,檢查包完的內容是否真的符合預期(比如檔案間互相引用的路徑在透過 webpack 處理後是否還正確、樣式是否能正常顯示)。以下是 Makefile 腳本:

# 預覽打包後的結果
.PHONY: preview
preview:
	npx http-server $(BUILD_DESTINATION) -p $(APP_PORT)

BUILD_DESTINATIONAPP_PORT 都是引用自 .evn 的內容,忘記這邊是怎麼來的可以回頭參考第 6 天內容)

在終端輸入 make preview 即可在本機試玩打包後的成果。


恭喜,你的專案已經交給 devOps 來準備上線了 (*゚ー゚)b