從零單排:基於vite+vue3實現多入口打包外掛

語言: CN / TW / HK

我正在參加「掘金·啟航計劃」

前言

本文為從零單排系列的第三篇,通過本文我們能夠基於vite實現一個多頁面(多入口)的打包外掛,推薦先閱讀本系列文章的先導片從零單排:前端進階之路、第一篇文章從零單排:使用pnpm建立monorepo和第二篇文章從零單排:基於vite+vue3搭建一個多入口的移動端專案(支援單入口、多入口和全部入口的打包)以獲得更好的體驗

這個專案我們在之前建立好的build專案中實現

初始化

初始化package.json

在build資料夾下,執行pnpm init命令 pnpm init

初始化ts

本專案我們使用typescript進行開發,先全域性安裝ts,在build資料夾下再執行tsc --init命令初始化 tsc --init

修改package.json

  • type - 宣告遵循的模組化規範,我們這裡設定為module,即採用ESModule規範
  • bin - 將可執行檔案載入到全域性環境中,那麼我們的包釋出後,專案安裝這個包就會自動連結到專案的node_module/.bin目錄中,就可以使用別名(我們這裡設定的別名是adv-build)執行相應的命令
  • script - 指令碼,我們這裡宣告兩個指令碼,dev實時編譯ts,build清除編譯後的檔案重新編譯,清除我們使用的rimraf,所以需要提前安裝 pnpm add rimraf json // package.json { "name": "@advance/build", "version": "1.0.0", "private": false, "description": "", "type": "module", "main": "./bin.js", "bin": { "adv-build": "./bin.js" }, "scripts": { "dev": "tsc --watch", "build": "rimraf ./lib && tsc" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@types/node": "^18.7.18", "@vitejs/plugin-vue": "^3.1.0", "commander": "^9.4.0", "del": "^7.0.0", "rimraf": "^3.0.2", "vite": "^3.1.0", "vite-plugin-html": "^3.2.0" } }

修改tsconfig.json

  • include - 指定哪些檔案需要編譯,我們設定為編譯src資料夾下的所有ts檔案
  • outDir - 編譯後的檔案存放位置
  • module - 指定要使用模組化的規範 json // tsconfig.json { "compilerOptions": { "target": "ES2019", "module": "ESNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./lib", "declaration": true, "moduleResolution": "Node" }, "include": [ "src/**/*" ] }

實現

在build資料夾下新建src資料夾和bin.js入口檔案,不難發現我們上面的package.json中的bin設定的執行檔案就是bin.js,相當於我們執行adv-build時就會去執行bin.js

我們主要的程式碼都在src下面編寫,在src下新建commands資料夾和common資料夾,commands資料夾用於存放命令相關程式碼,common資料夾存放公共程式碼。在commands資料夾下新加dev.ts檔案和build.ts檔案,對應的dev命令和build命令相關的處理程式碼。在common資料夾下新建constant.ts檔案用於存放公共變數,新建utils.ts用於存放工具方法,目錄如下 xml . ├── src # 主目錄 │ ├── common # 公共程式碼邏輯 | | |── constant.ts # 公共變數 | | |── utils.ts # 公共方法 │ ├── commands # 命令相關程式碼 | | |── dev.ts # dev命令相關 | | |── build.ts # build命令相關

bin.js編碼

程式碼的第一行 #!/usr/bin/env node 是告訴系統使用node執行此檔案,入口檔案我們沒有做過多的操作只是引入了編譯後的cli.js ```js

!/usr/bin/env node

import './lib/cli.js' ```

cli.ts編碼

這個檔案相當於指揮官,根據不同的命令呼叫對應的處理方法,我們使用commander這個庫來幫助我們去解析命令和引數,commander.js是node.js命令列介面的完整解決方案,大家可以先自行去閱讀一下官方文件,這裡主要說下我們會使用到的: + version - 設定版本號 + command - 新增命令名稱 + description - 對該命令的描述 + option - 定義選項 + action - 命令的回撥函式 ```ts // src/cli.ts

import {Command} from 'commander'

const program = new Command() program.version('@advance/build 1.0.0')

program .command('dev') // 註冊dev命令 .description('Run dev server') // 對dev命令的描述 .option('--open ', 'auto open page of url', false) // 定義open選項,預設是false,用於指定啟動dev-server時自動開啟哪個頁面 .action(async ({open}) => { // 當用戶輸入dev命令時的回撥,它會把上面定義的option注入到這個回撥函式中 const {dev} = await import('./commands/dev.js') // 我們在這裡非同步引入dev函式,執行 dev(open) });

program .command('build') // 註冊build命令 .description('Compile pages in production mode') // 對build命令的描述 .option('--all', 'build all page', false) // 定義all選項,預設是false,用於指定是否打包全部的頁面 .option('--pages ', 'build page list') // 定義pages選項,它是可變長引數,最終會將我們輸入的引數解析成陣列,用於指定需要打包的頁面 .action(async (options) => { // 當用戶輸入build命令時的回撥,它會把上面定義的option注入到這個回撥函式中 const {build} = await import('./commands/build.js') // 我們在這裡非同步引入build函式,執行 build(options) })

program.parse() ```

utils.ts編碼

這個檔案存放公共方法 ```ts import fs from 'fs'

// 判斷檔案是否存在 const isExist = (path: string) => { return fs.existsSync(path) }

export { isExist } ```

constant.ts編碼

這個檔案存放變數 ```ts // src/common/constant.ts

import {resolve} from 'path' import {isExist} from './utils.js' // 當前Node.js程序執行時的資料夾地址 const CWD = process.cwd() // 存放多頁面的路徑 const PAGES_PATH = resolve(CWD, 'src/pages') // 需要注入的js程式碼,解決在ios12以下機型在dev server白屏問題 const INJECTSCRIPT = <script >if (globalThis === undefined) { var globalThis = window; }</script>

// 在根目錄下查詢是否有vite的配置檔案 const configFileOfTs = resolve(CWD, 'vite.config.ts') const configFileOfJs = resolve(CWD, 'vite.config.js') let configFile: string | false = false if(isExist(configFileOfTs)) { configFile= configFileOfTs } else if(isExist(configFileOfJs)) { configFile= configFileOfJs }

export { CWD, PAGES_PATH, INJECTSCRIPT, configFile // vite配置檔案的路徑 } ```

dev.ts編碼

這個檔案通過呼叫vite的createServer API啟動dev-server,dev函式接受一個open引數,用於指定自動開啟的頁面,同時我們會去尋找業務專案(我們的是第二篇文章中的H5專案)的vite的配置檔案,並傳遞給createServer函式,這樣就可以自己根據業務的需要設定相應的vite配置了 ```ts import {createServer} from 'vite' import vue from '@vitejs/plugin-vue' import {PAGES_PATH, INJECTSCRIPT, configFile} from '../common/constant.js' import {resolve} from 'path' import fs from 'fs' import {createHtmlPlugin} from 'vite-plugin-html' import { isExist } from '../common/utils.js'

export async function dev(open: string | false) { try { let openPath: string | false = false if(open && isExist(resolve(PAGES_PATH, ./${open}/index.html))) { openPath = /${open}/index.html } const server = await createServer({ configFile, root: PAGES_PATH, plugins: [ vue(), createHtmlPlugin({ pages: fs.readdirSync(PAGES_PATH).map(page => { return { entry: /${page}/main.ts, filename: ${page}.html, template: src/pages/${page}/index.html, injectOptions: { data: { injectScript: INJECTSCRIPT } } } }) }) ], server: { open: openPath } }) await server.listen(); server.printUrls(); } catch(err) { console.log(err); } } ```

build.ts編碼

這個檔案通過呼叫vite的build方法進行打包,為防止和我們的build函式衝突,給vite的build方法設定別名為viteBuild,我們的build函式根據引數來進行全部頁面打包或部分頁面打包。跟上面的dev.ts一樣,我們會去找專案的vite配置檔案並傳遞給vite提供的build函式中,利用遞迴實現按順序打包 ```ts import { build as viteBuild } from 'vite' import path from 'path' import { CWD, INJECTSCRIPT, PAGES_PATH, configFile } from '../common/constant.js' import fs from 'fs' import vue from '@vitejs/plugin-vue' import { createHtmlPlugin } from 'vite-plugin-html' import { deleteSync } from 'del' import { isExist } from '../common/utils.js'

const compile = (page: string) => { return new Promise(async (resolve, reject) => { try { // 不是資料夾的直接跳過 if (!fs.statSync(path.resolve(PAGES_PATH, ./${page})).isDirectory()) { reject() return } console.log(開始打包${page}); const entry = path.resolve(PAGES_PATH, ./${page}/index.html) // 判斷入口檔案是否存在 if (!isExist(entry)) { console.log(${page}的入口檔案不存在); reject() return } const outDir = path.resolve(CWD, ./dist/${page}) // 刪除舊的打包資源 deleteSync(outDir) await viteBuild({ configFile, root: path.resolve(PAGES_PATH, page), base: './', plugins: [ vue(), createHtmlPlugin({ entry: '/main.ts', template: 'index.html', inject: { data: { injectScript: INJECTSCRIPT } } }) ], build: { outDir } }) console.log(${page}打包成功); resolve(${page}打包成功) } catch (err) { console.log('err====>', err);

  reject(err)
}

}) }

export async function build({ all, pages }: { all?: boolean, pages?: string[] }) { const buildPages = all ? fs.readdirSync(PAGES_PATH) : pages if (!Array.isArray(buildPages)) { console.log('請輸入要打包的頁面'); return } // 遞迴實現按順序打包 const runner = async () => { if (!buildPages || !buildPages.length) return const page = buildPages.shift() as string try { await compile(page) } catch (error) { } runner() } runner() }

```

注意

通過上面的程式碼,我們會發現,在引入本地自己寫的ts檔案時,我們加上了.js的字尾,你可能會疑惑為什麼這樣做,因為tsc在將ts檔案編譯時,不會給引用的檔案加上字尾,而我們又在package.json設定了type為module,這樣導致,node執行編譯後js檔案會提示找不到對應的引入檔案會報錯,所以我們在ts編碼時需要引入自己寫的ts檔案就加上.js為字尾

本文章的程式碼是將上一篇文章中的打包部分程式碼給抽離出來,使用ts編碼,增加了一些異常錯誤的處理和優化部分程式碼,但是核心邏輯並沒有改變,對優化部分程式碼都做了解釋,對沒有修改的部分未解釋的很清楚,如果你有看著模糊的地方可以閱讀上一篇文章的實現按需打包章節裡面有詳細的解釋

測試

準備工作

在build資料夾下執行dev命令編譯程式碼 pnpm dev 修改multi-page-app專案的package.json的scripts json "scripts": { "dev": "adv-build dev", "build": "adv-build build", },

測試本地服務

在multi-page-app資料夾下執行dev命令 + 無open引數 pnpm dev 可以通過 http://127.0.0.1:5173/page-1/index.html#/ 訪問到頁面 + 有open引數 pnpm dev --open page-1 自動在瀏覽器開啟page-1的頁面

測試打包

在multi-page-app資料夾下執行build命令 + 無引數 pnpm build 提示輸入打包頁面,退出打包 + 有all引數 pnpm build --all 打包pages下面的所有子專案,並輸出到dist目錄 + 有pages引數 pnpm build --page page-1 打包page-1,並輸出到dist目錄 + 有all引數和pages引數 pnpm build --all --page page-1 打包pages下面的所有子專案,並輸出到dist目錄

釋出

打包專案

在build資料夾下執行build命令 pnpm build

註冊npm賬號

npm官網註冊賬號

登入

npm login

釋出

注意源需要設定成官方的,如果是淘寶源需要切換到官方源,否則會發布失敗,一般包名不能一樣(釋出前先去官網搜尋這個包名是否存在),我這裡在package.json修改name為adv-build npm publish

總結

我們的目標是:搞事,搞事,還是TM的搞事

本系列的程式碼都已上傳到github,如有需要可自行下載

如果你覺得文章不錯,不妨: + 點贊-讓更多人也能夠看到這篇文章 + 關注-防止找不到我了。。。

文件

從零單排:前端進階之路系列全部文章 1. 從零單排:使用pnpm建立monorepo 2. 從零單排:基於vite+vue3搭建一個多入口的移動端專案(支援單入口、多入口和全部入口的打包) 3. 從零單排:基於vite+vue3實現多入口打包外掛 4. 從零單排:搭建一個屬於自己的腳手架---敬請期待

打個廣告 + 移動端相容性問題及解決方案彙總 + 基於vue2.x+better-scroll實現的下拉重新整理上拉載入元件 + webpack5學習指南-入門篇 + 從零建立vue3+vite+ts專案 + 如何“優雅”的實現自定義樣式彈幕功能