2、工程编译打包 -- 渐进式vue3的组件库通关秘籍

948 阅读8分钟

本节是渐进式vue3的组件库通关秘籍的第二节 -- 基于webpack和babel完成打包组件库的流程,本节假设你已经完成了第一节的内容:1、工程搭建 -- 渐进式vue3的组件库通关秘籍

1、预备知识

Node.js提供了丰富的开发工具和构建工具,可以帮助前端开发人员进行代码编译、打包、压缩、模块化管理等工作。例如,Webpack、Gulp、Grunt等工具都是基于Node.js构建的,它们可以与前端框架结合使用,提高开发效率和代码质量。

也就是说,在我们的前端组件库工程中,node.js扮演的角色就是使用其自身能力和周边工具,将我们的前端组件库代码进行编译、打包成浏览器或者其它node工程能够识别和引用的模块。

因此在开始本节之前,需要你预先了解一下的关于node工程打包构建的一些基础知识,其中本节涉及到的相关知识点也会做适当的讲解。

2、package.json

package.json文件是Node.js项目中的重要文件之一,它包含了项目的元数据和依赖项信息。下面是package.json文件中的一些重要字段及其作用:

  1. name: 项目的名称。必须是唯一的,通常采用小写字母、连字符和下划线的组合。
  2. version: 项目的版本号。遵循语义化版本规范(Semantic Versioning),即MAJOR.MINOR.PATCH格式。
  3. description: 项目的简要描述。
  4. main: 指定了项目的入口文件。当其他模块引入该项目时,默认导入的文件。
  5. scripts: 用于定义一些脚本命令,比如启动项目、运行测试等。这些脚本可以通过npm运行。
  6. dependencies: 项目的生产依赖项。列出了项目运行时必须的依赖模块及其版本信息。
  7. devDependencies: 项目的开发依赖项。列出了在开发过程中需要用到的依赖模块,如测试框架、构建工具等。
  8. keywords: 关键字数组,用于描述项目的特征,方便搜索引擎检索。
  9. author: 项目的作者信息。
  10. license: 项目的许可证信息。
  11. repository: 项目的代码仓库信息。
  12. engines: 指定了项目运行所需的Node.js版本范围。
  13. config: 用于定义一些项目配置信息,比如端口号、数据库连接等。

package.json文件的这些字段可以帮助我们管理项目的依赖项、版本控制、项目信息等,是Node.js项目中必不可少的配置文件之一。

接下来我们修改package.json的部分内容如下:

{
  ...
  "main": "lib/index.js",
  "module": "es/index.js",
  "unpkg": "fakeui.min.js",
  ...
}

main: lib/index.js:

  • 这个配置指定了项目的主入口文件路径。当其他模块引入该项目时,默认导入的文件就是这个指定的主入口文件。在这个例子中,主入口文件的路径为 lib/index.js

module: es/index.js:

  • 这个配置指定了项目的 ES Module 入口文件路径。在一些支持 ES Module 的环境中,会根据这个配置来导入项目的模块。在这个例子中,ES Module 的入口文件路径为 es/index.js

unpkg: fakeui.min.js:

  • 这个配置是为了支持 unpkg 这种 CDN 服务的配置。当用户通过 unpkg 访问项目时,会加载指定的文件。在这个例子中,unpkg 会加载 dist/fakeui.min.js 这个文件。

3、webpack配置

本工程使用webpack5来进行编译打包。

首先安装webpack的相关依赖:

npm i webpack webpack-cli --save-dev
npm i babel-loader vue-loader --save-dev

然后我们新建一个config文件夹用来存放webpack的配置文件。

新建 config/webpack.base.config.js,这些是基础的配置,开发环境和生产环境都是通用的,其中我们使用babel-loader编译 jsx,js,ts 文件。

关于 output 的配置,我们都知道,webpack可以将不同的模块化方式(commonjs, AMD, CMD, ES6 Module)的代码打包。那我们打出来的代码包其实也可以按不同的模块化方式生成,所以:

  • library 中 name 就是 webpack 打包内容的名字, type: umd 的意思是所有的模块下都可以运行。
  • 当输出为 library 时,尤其是当 libraryTarget'umd'时,此选项将决定使用哪个全局对象来挂载 library。为了使 UMD 构建在浏览器和 Node.js 上均可用,应将 output.globalObject 选项设置为 'this'。对于类似 web 的目标,默认为 self
const path = require("node:path");
​
module.exports = {
  // 指定哪些模块是外部模块,不需要被打包进输出的bundle中
  externals: [
    {
      vue: {
        root: "Vue", //表示在浏览器环境中,全局变量Vue可以直接访问
        commonjs2: "vue", // 表示在CommonJS环境的模块引入方式,通过require(‘vue’)来引入Vue模块。
        commonjs: "vue", // 同样表示在CommonJS环境的模块引入方式,通过require(‘vue’)来引入Vue模块。
        amd: "vue", // 表示在AMD(Asynchronous Module Definition)环境下的模块引入方式。
        module: "vue", // 表示ES Module(ES6模块)的引入方式。
      },
    },
  ],
  // 指定webpack在解析模块时的行为,在这个配置中,modules数组指定了webpack在解析模块时应该搜索的目录顺序
  // extensions数组指定了webpack在尝试解析导入模块时应该尝试的文件后缀顺序,例如.ts、.tsx、.js、.jsx。这些配置有助于webpack更方便地找到对应的模块文件
  resolve: {
    modules: ["node_modules", path.join(__dirname, "./node_modules")],
    extensions: [".ts", ".tsx", ".js", ".jsx"],
  },
  // 如何处理不同类型的模块
  module: {
    rules: [
      {
        test: /.(jsx|tsx|js|ts)$/,
        loader: "babel-loader",
        exclude: /node_modules/, // 排除此目录,三方包已经是编译好的了,不需要再进行编译
      },
      {
        test: /.vue$/,
        loader: "vue-loader",
        exclude: /node_modules/,
      },
    ],
  },
  output: {
    filename: "[name].js"
  },
};
​

新建 config/webpack.prod.config.js, 用于生产环境编译打包的配置,分别为 commonjs 和 esmodule 分别准备两个配置文件。

const path = require("path");
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.config");
const distfilename = "fakeui";
const resolveDir = (dir) => path.join(__dirname, `../${dir}`);
const es = merge(baseConfig, {
  mode: "production",
  entry: {
    [distfilename]: ["./index.esm"],
  },
  experiments: {
    outputModule: true,
  },
  output: {
    path: resolveDir("dist/es"),
    library: {
      type: "module",
    },
  },
});
const cjs = merge(baseConfig, {
  mode: "production",
  entry: {
    [distfilename]: ["./index.js"],
  },
  output: {
    path: resolveDir("dist/lib"),
    library: {
      name: distfilename,
      type: "umd",
    },
    globalObject: "this",
  },
});
​
module.exports = [es, cjs];
​

在 package.json 里面添加一条构建命令,使用生产环境的配置。

{
  ...
  "scripts": {
  ...
    "build": "webpack -c ./config/webpack.prod.config.js"
  },
  ...
}
​
  

在根目录的 index.js(打包入口文件) 里面添加以下内容:

module.exports = require("./components");

在根目录的 index.esm.js (esm打包入口) 里面添加以下内容:

export * from "./components";

执行构建命令:

npm run build

此时不会构建成功,我们可以看到以下报错。

image-20240507141819978

这是因为babel无法解析 jsx, 接下来我们在根目录新增一个 babel.config.js,用以配置对 jsx 的支持。

module.exports = {
  presets: [
    // 根据你在配置中指定的目标环境(比如浏览器版本、Node.js 版本等),
    // 自动确定需要的转换规则,例如将 ES6+ 的代码转换为向下兼容的 ES5 代码,以确保你的代码在目标环境中能够正确运行。
    ["@babel/preset-env", { targets: { node: "current" } }],

    // babel官方提供的解析ts的预设
    ["@babel/preset-typescript"],
  ],
  plugins: [
    "@vue/babel-plugin-jsx", // 解析基于vue的jsx
    "@babel/plugin-syntax-dynamic-import", // 懒加载语法解析,如果使用了 import() 语法
    [
      "@babel/plugin-proposal-decorators", // 装饰器语法解析
      {
        version: "2023-05",
      },
    ],
    [
      "@babel/plugin-transform-runtime", // 一个插件,可以重用 Babel 的注入帮助代码以节省代码大小。
      {
        corejs: false,
        helpers: true,
        regenerator: true,
      },
    ],
  ],
};

在根目录下新建 tsconfig.js,增加对vue的jsx的解析支持,重点是 "jsx": "preserve", "jsxImportSource": "vue",

{
  "compilerOptions": {
    "baseUrl": "./",
    "lib": ["DOM", "ESNext"],
    "strictNullChecks": false,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "jsx": "preserve",
    "jsxImportSource": "vue",
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noImplicitAny": false,
    "target": "ESNext",
    "module": "ESNext",
    "skipLibCheck": true,
    "allowJs": true,
    "stripInternal": true,
    "resolveJsonModule": true,
    "forceConsistentCasingInFileNames": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "noImplicitThis": true
  },
  "include": ["components/**/*"],
  "exclude": ["node_modules"]
}

安装所有babel需要的依赖:

npm i @vue/babel-plugin-jsx @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-decorators @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript --save-dev

最后执行构建命令

npm run build

不出意外的话可以在 dist 文件夹下看到输出的两个不同环境的打包文件。

image-20240507161530078

接下来我们在 docs 的 hello world 组件中引入打包后的文件试一试

修改 docs/components/hello-world.md

---
outline: deep
---

# hello world

say hello to the world!

## 代码演示

<script setup>
import { HelloWorld } from '../../dist/es/fakeui.js' // 修改了这里

</script>

<HelloWorld />

```vue
<script setup>
import { HelloWorld } from "../../dist/es/fakeui.js"; // 修改了这里
</script>

<HelloWorld />
```

保存预览一下,可以看到组件按照预期的工作了。

commonjs的包的使用大家可以自行测试使用。

4、代码风格统一

上面我们已经完成了对组件库组件的部分基建,除了还没有加入样式的打包逻辑,生产优化等需要后续逐步添加的功能,目前已经具备了多人协作开发的能力,那么我们就需要进行代码风格的统一,这里我们使用 prettier 进行代码格式化

根目录下添加 .prettierrc 文件

{
  "singleQuote": true,
  "trailingComma": "all",
  "endOfLine": "lf",
  "printWidth": 100,
  "proseWrap": "never",
  "arrowParens": "avoid",
  "htmlWhitespaceSensitivity": "ignore",
  "overrides": [
    {
      "files": ".prettierrc",
      "options": {
        "parser": "json"
      }
    }
  ]
}

添加 .prettierignore 文件,配置忽略哪些文件

dist
.*

在 package.json 里面配置格式化命令

{
	...
  "scripts": {
   	...
    "prettier": "prettier -c --write **/*"
  },
  ...
}

执行格式化命令,对全局的文件进行格式化

npm run prettier

如果没有 prettier ,需要先全局安装一下

npm i prettier -g

关于eslint的配置,本节先不引入,感兴趣的可以先尝试一下配置 eslint

5、小结

本节完成了对此组件库工程的打包配置,支持vue和jsx。了解了webpack的相关打包的知识、babel在node工程中扮演的角色和基本的配置、了解前端和node的发展过程中演化出来的不同的模块化方案。在没有项目脚手架的时候也能够独立的配置出满足项目要求的构建方案。

本节代码:feature_1.1_package

下一节:样式系统

欢迎点赞、欢迎star、欢迎讨论、一起学习。