首頁 > 軟體

Vue3中使用pnpm搭建monorepo開發環境

2022-08-19 22:00:07

前言

Vue3 原始碼閱讀系列,計劃從環境搭建開始,將 Vue3 的響應式模組,執行時模組和編譯器模組,以及狀態庫 Pinia、路由庫 Vue-Router的核心原理做一個梳理。這大概是一個漫長的過程。祝自己不要爛尾,祝大家有所收穫。

Pnpm 和 Monorepo

Pnpm 是新一代的 nodejs 包管理工具。第一個 “P”意為 Performance,代表著更佳的效能。

它的主要優點有兩個,一是採用了 hard-link 機制,避免了包的重複安裝,節省了空間,同時能提高專案依賴的安裝速度。二是對monorepo 的支援非常友好,只需要一條設定就能實現。

Monorepo 是一種新的倉庫管理方式。過去的專案,大多采用一個倉庫維護一個專案的方案。對於一個龐大複雜的專案,哪怕只進行一處小小的修改,影響的也是整體。而採用 monorepo 的形式,我們可以在一個倉庫中管理多個包。每個包都可以單獨釋出和使用,就好像是一個倉庫中又有若干個小倉庫。

Vue3 原始碼採用 monorepo 方式進行管理,將眾多模組拆分到 packages 目錄中。

這帶來的最直觀的好處,就是方便管理和維護。而且,它不像 Vue2 那樣將原始碼整體打包對外暴露。Vue3的這種組織形式,方便的實現了 Tree-shaking,需要哪個功能就引入對應的模組,能大大減少打包後專案的體積。

搭建開發環境

建立專案

首先全域性安裝 pnpm

npm install -g pnpm

新建一個目錄並進行初始化:

mkdir vue3-learn
cd vue3-learn
pnpm init
mkdir packages

設定 monorepo

在專案根目錄下新建 pnpm-workspace.yaml 檔案:

packages:
  - 'packages/*'

意思是,將 packages 目錄下所有的目錄都作為單獨的包進行管理。

通過這樣一個簡單的設定,Monorepo 開發環境搭建好了。

如果大家之前接觸過 lerna + yarn workspace的方案,就會深有體會,使用 pnpm 的確方便。Vue3Element Plus以前採用的方案就是前者,現在都已經改用後者了。

安裝依賴

如果你使用過 Vite,就一定體驗過它的快。因為 Vite 內建了 esbuild 作為開發階段的構建工具。esbuild 的特點就是快。

Vue3 採用了和 vite 一致的選擇,開發階段使用 esbuild 作為構建工具,在生產階段採用 rollup 進行打包。

我們先安裝一些依賴:

# 原始碼採用 typescript 編寫
pnpm add  -D -w typescript
# 構建工具,命令列引數解析工具
pnpm add -D -w esbuild rollup rollup-plugin-typescript2 @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-commonjs minimist execa 

說明:

-D:作為開發依賴安裝

-wmonorepo 環境預設會認為應該將依賴安裝到具體的 package中。使用 -w 引數,告訴 pnpm 將依賴安裝到 workspace-root,也就是專案的根目錄。

依賴說明:

依賴描述
typescript專案使用 typescript 進行開發
esbuild開發階段的構建工具
rollup生產階段的構建工具
rollup-plugin-typescript2rollup 編譯 ts 的外掛
@rollup/plugin-jsonrollup 預設採用 esm 方式解析模組,該外掛將 json 解析為 esm 供 rollup 處理
@rollup/plugin-node-resolverollup 預設採用 esm 方式解析模組,該外掛可以解析安裝在 node_modules 下的第三方模組
@rollup/plugin-commonjs將 commonjs 模組 轉化為 esm 模組
minimist解析命令列引數
execa生產階段開啟子程序

初始化Typescript

pnpm tsc --init

pnpm 的使用基本和 npm 一致。這裡的用法就相當於 npm 中的 npx

npx tsc --init

意思是,去 node_modules 下的 .bin 目錄中找到tsc 命令,並執行它。

執行完該命令,會在專案根目錄生成一個 tsconfig.json 檔案,進行一些設定:

{
  "compilerOptions": {
    "outDir": "dist", // 輸出的目錄
    "sourceMap": true, // 開啟 sourcemap
    "target": "es2016", // 轉譯的目標語法
    "module": "esnext", // 模組格式
    "moduleResolution": "node", // 模組解析方式
    "strict": false, // 關閉嚴格模式,就能使用 any 了
    "resolveJsonModule": true, // 解析 json 模組
    "esModuleInterop": true, // 允許通過 es6 語法引入 commonjs 模組
    "jsx": "preserve", // jsx 不跳脫
    "lib": ["esnext", "dom"], // 支援的類庫 esnext及dom
    "baseUrl": ".",  // 當前目錄,即專案根目錄作為基礎目錄
    "paths": { // 路徑別名設定
      "@my-vue/*": ["packages/*/src"]  // 當引入 @my-vue/時,去 packages/*/src中找
    },
  }
}

準備兩個模組

我們先在 packages 目錄下新建兩個模組,分別是 reactivity 響應式模組 和 shared 工具庫模組。然後編寫構建指令碼進行第一次的開發偵錯。

shared

packages 下新建 shared 目錄,並初始化:

pnpm init

然後修改 package.json

{
  "name": "@my-vue/shared",
  "version": "1.0.0",
  "description": "@my-vue/shared",
  "main": "dist/shared.cjs.js",
  "module": "dist/shared.esm-bundler.js"
}

注意 name 欄位的值,我們使用了一個 @scope 作用域,它相當於 npm 包的名稱空間,可以使專案結構更加清晰,也能減少包的重名。

編寫該模組的入口檔案:

// src/index.ts
/**
 * 判斷物件
 */
export const isObject = (value) =>{
    return typeof value === 'object' && value !== null
}
/**
 * 判斷函數
 */
export const isFunction= (value) =>{
    return typeof value === 'function'
}
/**
 * 判斷字串
 */
export const isString = (value) => {
    return typeof value === 'string'
}
/**
 * 判斷數位
 */
export const isNumber =(value)=>{
    return typeof value === 'number'
}
/**
 * 判斷陣列
 */
export const isArray = Array.isArray

reactivity

packages 下新建 reactivity 目錄,並初始化:

pnpm init

然後修改 package.json

{
  "name": "@my-vue/reactivity",
  "version": "1.0.0",
  "description": "@my-vue/reactivity",
  "main": "dist/reactivity.cjs.js",
  "module": "dist/reactivity.esm-bundler.js",
  "buildOptions": {
    "name": "VueReactivity"
  }
}

在瀏覽器中以 IIFE 格式使用響應式模組時,需要給模組指定一個全域性變數名字,通過 buildOptions.name 進行指定,將來打包時會作為設定使用。

main 指定的檔案支援 commonjs 規範進行匯入,也就是說在nodejs 環境中,通過 require 方法匯入該模組時,會匯入 main 指定的檔案。

同理,module 指定的是使用 ES Module 規範匯入模組時的入口檔案。

編寫該模組的入口檔案:

// src/index.ts
import { isObject } from '@my-vue/shared'
const obj = {name: 'Vue3'}
console.log(isObject(obj))

reactivity 包中用到了另一個包 shared ,需要安裝才能使用:

pnpm add @my-vue/shared@workspace --filter @my-vue/reactivity

意思是,將本地 workspace 內的 @my-vue/shared 包,安裝到 @my-vue/reactivity包中去。

此時,檢視 reactivity 包的依賴資訊:

"dependencies": {
   "@my-vue/shared": "workspace:^1.0.0"
}

編寫構建指令碼

在根目錄下新建 scripts 目錄,存放專案構建的指令碼。

新建 dev.js,作為開發階段的構建指令碼。

// scripts/dev.js
// 使用 minimist 解析命令列引數
const args = require('minimist')(process.argv.slice(2))
const path = require('path')
// 使用 esbuild 作為構建工具
const { build } = require('esbuild')
// 需要打包的模組。預設打包 reactivity 模組
const target = args._[0] || 'reactivity'
// 打包的格式。預設為 global,即打包成 IIFE 格式,在瀏覽器中使用
const format = args.f || 'global'
// 打包的入口檔案。每個模組的 src/index.ts 作為該模組的入口檔案
const entry = path.resolve(__dirname, `../packages/${target}/src/index.ts`)
// 打包檔案的輸出格式
const outputFormat = format.startsWith('global') ? 'iife' : format === 'cjs' ? 'cjs' : 'esm'
// 檔案輸出路徑。輸出到模組目錄下的 dist 目錄下,並以各自的模組規範為字尾名作為區分
const outfile = path.resolve(__dirname, `../packages/${target}/dist/${target}.${format}.js`)
// 讀取模組的 package.json,它包含了一些打包時需要用到的設定資訊
const pkg = require(path.resolve(__dirname, `../packages/${target}/package.json`))
// buildOptions.name 是模組打包為 IIFE 格式時的全域性變數名字
const pgkGlobalName = pkg?.buildOptions?.name
console.log('模組資訊:n', entry, 'n', format, 'n', outputFormat, 'n', outfile)
// 使用 esbuild 打包
build({
  // 打包入口檔案,是一個陣列或者物件
  entryPoints: [entry], 
  // 輸入檔案路徑
  outfile, 
  // 將依賴的檔案遞迴的打包到一個檔案中,預設不會進行打包
  bundle: true, 
  // 開啟 sourceMap
  sourcemap: true,
  // 打包檔案的輸出格式,值有三種:iife、cjs 和 esm
  format: outputFormat, 
  // 如果輸出格式為 IIFE,需要為其指定一個全域性變數名字
  globalName: pgkGlobalName, 
  // 預設情況下,esbuild 構建會生成用於瀏覽器的程式碼。如果打包的檔案是在 node 環境執行,需要將平臺設定為node
  platform: format === 'cjs' ? 'node' : 'browser',
  // 監聽檔案變化,進行重新構建
  watch: {
   onRebuild (error, result) {
       if (error) {
           console.error('build 失敗:', error)
       } else {
           console.log('build 成功:', result) 
       }
    }
  }
}).then(() => {
  console.log('watching ...')
})

使用該指令碼,會使用 esbuildpackages 下的包進行構建,打包的結果放到各個包的 dist 目錄下。

在開發階段,我們預設打包成 IIFE 格式,方便在瀏覽器中使用 html 檔案進行測試。在生產階段,會分別打包成 CommonJSES ModuleIIFE 的格式。

完成第一次偵錯

給專案增加一條 scripts 命令:

// package.json
"scripts": {
    "dev": "node scripts/dev.js reactivity -f global"
}

意思是,以 IIFE 的格式,打包 reactivity 模組,打包後的檔案可以執行在瀏覽器中。

在終端中執行:

pnpm dev

輸出:

PS D:vue3-learn> pnpm dev
> vue3-learn@1.0.0 dev D:vue3-learn
> node scripts/dev.js reactivity -f global
模組資訊:
 D:vue3-learnpackagesreactivitysrcindex.ts
 global
 iife
 D:demo3vue3-learnpackagesreactivitydistreactivity.global.js
watching ...

編寫一個 html 檔案進行測試:

// packages/reactivity/test/index.html
<body>
    <div id="app"></div>
    <script src="../dist/reactivity.global.js"></script>
</body>

開啟瀏覽器控制檯:

小結

到此,一個基本的 monorepo 開發環境就搭建完畢了。

程式碼已上傳至 Github點選存取

更多關於Vue3 pnpm搭建monorepo的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com