Vue3中怎么使用pnpm搭建monorepo开发环境

2023-06-12 16:38:40 开发 环境 搭建

这篇文章主要介绍了Vue3中怎么使用pnpm搭建monorepo开发环境的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue3中怎么使用pnpm搭建monorepo开发环境文章都会有所收获,下面我们一起来看看吧。

    Pnpm 和 Monorepo

    Pnpm
    是新一代的
    nodejs
    包管理工具。第一个
    “P”
    意为
    Performance
    ,代表着更佳的性能。

    它的主要优点有两个,一是采用了

    hard-link
    机制,避免了包的重复安装,节省了空间,同时能提高项目依赖的安装速度。二是对
    monorepo
    的支持非常友好,只需要一条配置就能实现。

    Monorepo
    是一种新的仓库管理方式。过去的项目,大多采用一个仓库维护一个项目的方案。对于一个庞大复杂的项目,哪怕只进行一处小小的修改,影响的也是整体。而采用
    monorepo
    的形式,我们可以在一个仓库中管理多个包。每个包都可以单独发布和使用,就好像是一个仓库中又有若干个小仓库。

    Vue3 源码采用 monorepo 方式进行管理,将众多模块拆分到 packages 目录中。

    Vue3中怎么使用pnpm搭建monorepo开发环境

    这带来的最直观的好处,就是方便管理和维护。而且,它不像

    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
    的确方便。
    Vue3
    Element 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
    :作为开发依赖安装

    -w
    monorepo
    环境默认会认为应该将依赖安装到具体的
    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('模块信息:
    ', entry, '
    ', format, '
    ', outputFormat, '
    ', 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 ...')
    })

    使用该脚本,会使用

    esbuild
    packages
    下的包进行构建,打包的结果放到各个包的
    dist
    目录下。

    在开发阶段,我们默认打包成

    IIFE
    格式,方便在浏览器中使用
    html
    文件进行测试。在生产阶段,会分别打包成
    CommonJS
    ES Module
    IIFE
    的格式。

    完成第一次调试

    给项目增加一条

    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-learnpackageseactivitysrcindex.ts
     global
     iife
     D:demo3vue3-learnpackageseactivitydisteactivity.global.js
    watching ...

    编写一个

    html
    文件进行测试:

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

    打开浏览器控制台:

    Vue3中怎么使用pnpm搭建monorepo开发环境

    相关文章