基于 vite 的前端工程化探索(7K字)


theme: condensed-night-purple

本文正在参加「金石计划 . 瓜分6万现金大奖

一直觉得自己在配置搭建方面很薄弱,于是下决心系统化学习一遍。但是理想和现实还是有很大的差距,从学习到写这篇文章断断续续花了一个多月的时间。中间还考虑过放弃,但是再阅读一遍的时候发现还是有点东西,于是还是决定写完它。由于自己能力有限,中间可能有细节忽略了,希望读者可以指出,不胜感激。

实现最简组件库

第一部分的源代码已经上传到 GitHub

使用 Monorepo 方式管理组件库生态

前置知识

为什么使用 pnpm 进行依赖包管理?当我们创建100个项目的时候,这些项目都会使用同一个依赖包,npm 或者 yarn 会将该依赖包的100份存放到磁盘中,而 pnpm 会将所有相同的依赖包存放到同一个位置。当然,如果该依赖包有多种版本,pnpm 会保存该依赖包的所有版本。在 pnpm 管理模式下,每个项目通过硬连接找到对应的依赖包。显然,pnpm 依赖包管理可以减少磁盘空间,提高依赖包的安装速度。

pnpm 内置支持 Monorepo ,可以创建一个 workspace 来联合所有项目到一个仓库,该 workspace 的根目录必须包含一个 pnpm-workspace.yaml 文件。

操作步骤
// a. 创建根目录 smart-admin

// b. 在根目录下初始化项目
pnpm init

// c. 修改 package.json,禁用 npm 和 yarn
"scripts": {
   "preinstall": "npx only-allow pnpm"
 }
 
// d. 创建 pnpm-workspace.yaml,声明所有软件包存放的目录
 packages:
  - "packages/**"

搭建 vite 开发环境

前置知识

vite 是一个开发服务器,而 index.html 是入口文件。vite 默认支持 .ts 文件,但 vite 仅支持转译工作,不进行类型检查

操作步骤
// a. 初始化环境,安装 vite 开发依赖包
// 目录: packages/smart-ui-vite
pnpm init -y
pnpm i -D vite

// b. 修改 package.json 文件,配置启动命令
// 目录: packages/smart-ui-vite
"scripts": {
    "dev": "vite"
},
  
// c. 生成 index.html 文件,运行命令 pnpm dev,浏览器查看运行结果
// 目录: packages/smart-ui-vite/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>title</title>
  </head>
  <body>
    <h1>My Vite</h1>
  </body>
</html>

image.png

// d. 在 src 目录中创建 index.ts 文件。
// 目录: packages/smart-ui-vite/src/index.ts
const s: string = "Hello Vite";
alert(s);

// e. 修改 index.html 文件,增加以下语句,导入创建的 index.ts 文件。运行成功后,浏览器出现“Hello Vite”字样的弹窗提示
// 目录: packages/smart-ui-vite/index.html
<script src="./src/index.ts" type="module"></script>

image.png

三种开发 vue 组件的方法

前置知识

安装 vue 依赖包后,可以直接编写使用渲染函数的 vue 组件。render 函数是模版字符串的一种替代形式,该方法可以通过 JavaScript 声明组件的渲染输出

单文件组件是框架指定的文件格式,需要 @vue/compiler-sfc 编译成 javascript 和 css ,这样就可以按照 es 模块的方式进行导入使用。目前 Vue3 已经内置该依赖,不过 vite 还需要安装依赖 @vitejs/plugin-vue 来处理该文件。

开发 jsx 组件,需要安装 @vitejs/plugin-vue-jsx 依赖。当使用 tsx 语法的时候,还需要在 tsconfig.json 文件中增加 "jsx": "preserve" 配置项,这样 typescript 才能保证 jsx 编译的完整性。

操作步骤
  • 使用渲染函数
// a. 安装依赖包
// 目录: packages/smart-ui-vite
pnpm i vue

// b. 通过渲染函数创建 vue 组件
// 目录: packages/smart-ui-vite/src/button/render-button.ts
import { defineComponent, h } from "vue";

export default defineComponent({
  name: "renderButton",
  render() {
    return h("button", null, "RenderButton");
  },
});

// c. 引入 RenderButton 组件
// 目录: packages/smart-ui-vite/src/index.ts
import { createApp } from "vue";
import RenderButton from "./button/render-button";

createApp(RenderButton).mount("#app");

// d. index.html 增加挂载点
// 目录: packages/smart-ui-vite/index.html
<div id="app"></div>
  • 使用单文件组件
// a. 安装插件
// 目录: packages/smart-ui-vite
pnpm i @vitejs/plugin-vue -D

// b. 增加 vite 配置项
// 目录: packages/smart-ui-vite/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
});

// c. 添加单文件组件
// 目录: packages/smart-ui-vite/src/button/sfc-button.vue
<template>
  <button>SFC Button</button>
</template>

<script lang="ts">
export default {
  name: "SFCButton",
};
</script>

// d. 修改 index.ts 文件,引入创建的 sfc-button 组件
// 目录: packages/smart-ui-vite/src/index.ts
import { createApp } from "vue";
// import RenderButton from "./button/render-button";
import SFCButton from "./button/sfc-button.vue";

createApp(SFCButton).mount("#app");

// e. 创建 shims-vue.d.ts 文件,解决 ts 文件中引入 vue 文件的报错问题
// 目录: packages/smart-ui-vite/src/shims-vue.d.ts
declare module "*.vue" {
  import { DefineComponent } from "vue";
  const component: DefineComponent<{}, {}, any>;
  export default component;
}
  • 使用 JSX 组件
// a. 安装 jsx 依赖包
// 目录: packages/smart-ui-vite
pnpm i @vitejs/plugin-vue-jsx -D

// b. 修改 vite.config.ts 文件
// 目录: packages/smart-ui-vite/vite.config.ts
import vueJsx from "@vitejs/plugin-vue-jsx";

export default defineConfig({
  plugins: [vue(), vueJsx()],
});

// c. 创建 jsx 组件
// 目录: packages/smart-ui-vite/src/button/jsx-button.tsx
import { defineComponent } from "vue";

export default defineComponent({
  name: "JSXButton",
  render() {
    return <button>JSX Button</button>;
  },
});

// d. 修改 index.ts 文件,导入 jsx 编写的组件
// 目录: packages/smart-ui-vite/src/index.ts
import JSXButton from "./button/jsx-button";

createApp(JSXButton).mount("#app");

// e. 新建 tsconfig.json 文件,解决 tsx 文件中“找不到 React 的报错”
// 目录: packages/smart-ui-vite/tsconfig.json
{
  "compilerOptions": {
    "declaration": true /* 生成相关的 '.d.ts' 文件。 */,
    "declarationDir": "./dist/types" /* '.d.ts' 文件输出目录 */,
    "jsx": "preserve"
  },
  "include": ["./**/*.*", "./shims-vue.d.ts"],
  "exclude": ["node_modules"],
  "esModuleInterop": true,
  "allowSyntheticDefaultImports": "true"
}

库文件封装

前置知识

组件库一般支持全局组件引入和单个组件引入,所以在做组件封装的时候提供了单个组件导出,以及通过插件的方式进行全局组件注入。

操作步骤
  • 文件封装
// a. 创建入口文件 /src/entry.ts,导出全部组件和单个组件
// 目录: packages/smart-ui-vite/src/entry.ts
import { App } from "vue";
import RenderButton from "./button/render-button";
import SFCButton from "./button/sfc-button.vue";
import JSXButton from "./button/jsx-button";

export { RenderButton, SFCButton, JSXButton };

export default {
  install(app: App): void {
    app.component(RenderButton.name, RenderButton);
    app.component(SFCButton.name, SFCButton);
    app.component(JSXButton.name, JSXButton);
  },
};

// b. 修改配置文件 vite.config.ts
// 目录: packages/smart-ui-vite/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

const rollupOptions = {
  external: ["vue"],
  output: {
    globals: {
      vue: "Vue",
    },
  },
};

export default defineConfig({
  plugins: [vue(), vueJsx()],
  build: {
    rollupOptions,
    minify: false,
    lib: {
      entry: "./src/entry.ts",
      name: "SmartUI",
      fileName: "smart-ui",
      formats: ["es", "umd", "iife"],
    },
  },
});

// c. 修改 package.json
// 目录: packages/smart-ui-vite/package.json
"scripts": { 
    "build": "vite build" 
 },

// d. 执行打包命令
// 目录: packages/smart-ui-vite
pnpm build
  • 验证结果
// a. 创建 demo/esm/index.html 文件,验证全局导入组件的方式
// 目录: packages/smart-ui-vite/demo/esm/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>全局引入</h1>
    <div id="app"></div>
    <script type="module">
      import { createApp } from "vue/dist/vue.esm-bundler.js";
      import SmartUI from "../../dist/smart-ui.mjs";

      createApp({
        template: `
      <RenderButton/>
      <TSXButton/>
      <SFCButton/>
    `,
      })
        .use(SmartUI)
        .mount("#app");
    </script>
  </body>
</html>

// b. 创建 demo/esm/button.html 文件,验证按需引入组件的情况
// 目录: packages/smart-ui-vite/demo/esm/button.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>按需引入</h1>
    <div id="app"></div>
    <script type="module">
      import { createApp } from "vue/dist/vue.esm-bundler.js";
      import {
        SFCButton,
        TSXButton,
        RenderButton,
      } from "../../dist/smart-ui.mjs";

      createApp({
        template: `
<RenderButton/>
<TSXButton/>
<SFCButton/>
`,
      })
        .component(SFCButton.name, SFCButton)
        .component(TSXButton.name, TSXButton)
        .component(RenderButton.name, RenderButton)
        .mount("#app");
    </script>
  </body>
</html>

image.png

image.png

  • 代码压缩和生成 sourcemap
// a. 安装依赖
// 目录: packages/smart-ui-vite
pnpm i terser -D

// b. 修改 vite.config.ts 配置文件,配置 terser 进行代码混淆,开启 sourcemap 可以在控制台看到源码
// 目录: packages/smart-ui-vite/vite.config.ts
export default defineConfig({
  build: {
    minify: "terser",
    sourcemap: true,
    ...
  },
});

观察打包后的文件即可看到配置是否生效

image.png

image.png

用 UnoCSS 实现原子化样式

前置知识点

UnoCSS 不是一个框架,而是一个引擎,它并没有提供核心工具类,而是引入预设和配置项来实现功能,详细介绍可见重新构想原子化CSS

safelist。当使用到 prop 动态引入样式的时候,类似这种形式 bg-${props.color}-500,组件无法正常加载相应的颜色。这是因为 unocss 是在编译的时候静态提取对应的样式,解决方式是配置一个 safelist,提前告知 uncoss 会动态加载那些样式。

操作步骤
// a. 安装依赖库,其中 @iconify-json/ic 是 json 形式的 icon 集合
// 目录: packages/smart-ui-vite
pnpm i -D unocss @iconify-json/ic

// b. 由于 Unocss 的配置比较多,所以另外新建一个文件 unocss.ts
// 目录: packages/smart-ui-vite/config/unocss.ts
import Unocss from "unocss/vite";
import { presetUno, presetAttributify, presetIcons } from "unocss";

const colors = ["red", "gray", "yellow"];
const safelist = [
  ...colors.map((v) => `bg-${v}-500`),
  ...colors.map((v) => `hover:bg-${v}-700`),
  ...["search", "edit", "check"].map((v) => `i-ic-baseline-${v}`),
];

export default () =>
  Unocss({
    safelist,
    presets: [presetUno(), presetAttributify(), presetIcons()],
  });

// c. vite.config.ts 导入 vite 配置
// 目录: packages/smart-ui-vite/vite.config.ts
import Unocss from "./config/unocss";

export default defineConfig({
  plugins: [Unocss()],
   build: {
    cssCodeSplit: true,  // 独立输出 css 样式
    ...
   }
})

// d. 新建文件 index.tsx, 创建提供 color 和 icon 这两个 props 的 jsx 组件。
// 目录: packages/smart-ui-vite/src/button/index.tsx
import { defineComponent, PropType } from "vue";
import "uno.css";

type IColor = "red" | "gray" | "yellow";
type IIcon = "search" | "edit" | "check";
const props = {
  color: {
    type: String as PropType<IColor>,
    default: "blue",
  },
  icon: {
    type: String as PropType<IIcon>,
  },
};
export default defineComponent({
  name: "UButton",
  props,
  setup(props, { slots }) {
    return () => (
      <button
        class={`py-2 bg-${props.color}-500 hover:bg-${props.color}-700 border-none`}
      >
        {!props.icon ? "" : <i class={`i-ic-baseline-${props.icon} p-3`}></i>}
        {slots.default ? slots.default() : ""}
      </button>
    );
  },
});

// e. 修改 index.ts 文件,测试组件使用不同 color 和 icon 的情况。
// 目录: packages/smart-ui-vite/src/index.ts
import UButton from "./button/index";
import { createApp } from "vue/dist/vue.esm-bundler.js";

createApp({
  template: `<div>
        <UButton color="red" icon="search">红色按钮</UButton>
        <UButton color="gray" icon="edit">绿色按钮</UButton>
        <UButton color="yellow" icon="check">绿色按钮</UButton>
    </div>`,
})
  .component(UButton.name, UButton)
  .mount("#app");

运行 pnpm build 之后就可以看到如图所示的效果

image.png

创建新项目验证 monoropo 思想

前置知识点

docs-vite 项目可以通过安装依赖包的形式引入项目 smart-ui-vite,并且依赖包是指向 workspace,这是 monorepo 的关键功能点。

操作步骤
// a. packages 目录下创建新的文件夹 docs-vite

// b. 初始化项目
// 目录: packages/docs-vite
pnpm init

// c. 在 workspace 也就是项目根目录中(如果 packages 中的子项目有安装该依赖,可以删除掉,使用 workspace 的 vite 依赖包)
// 项目根目录
pnpm i vite -w

// d. 安装 vue 和 smart-ui-vite
// 目录: packages/docs-vite
pnpm i vue
pnpm i smart-ui-vite

// e. 创建 index.html 文件,引入依赖进行测试
// 目录: packages/docs-vite/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import SmartUI from "smart-ui-vite/dist/smart-ui.mjs";
      import { createApp } from "vue/dist/vue.esm-bundler.js";
      import 'smart-ui-vite/dist/assets/entry.d2de1b65.css'  // 注意:生成的 css 文件默认会包含有 hash 值,需要根据实际情况进行引入

      createApp({
        template: `
          <UButton color="gray" icon="search">gray button</UButton>
          <UButton color="red" icon="edit">red button</UButton>
          <UButton color="yellow" icon="check">red button</UButton>
          `,
      })
        .use(SmartUI)
        .mount("#app");
    </script>
  </body>
</html>

// f. 配置 docs-vite 项目的启动命令,并启动 pnpm dev
// 目录: packages/docs-vite/package.json
{
  ...
  "scripts": {
    "dev": "vite"
  },
  ...
}

使用 monoropo 方式引入的依赖包

image.png
运行命令后即可在浏览器中查看到对应的效果

image.png

完善项目工程化

该部分的源代码已经上传到 GitHub

文档建设

前置知识

vitepress 是基于 vuepress,不同的是 vitepress 使用了 vite 进行构建,而不是 vuepress 的 webpack 构建,这明显提高了启动,更新和构建的速度,详情可查看官方中文网站

vitepress-theme-demoblock 是一款 vitepress 的主题插件,它增强了编写 vue 组件的能力。一方面解决了 vitepress 不能现实 script 和 style 部分代码的问题,另一方面解决了组件代码和代码示例相同却需要写两遍的问题。详情可查看 GitHub 的介绍

操作步骤
// a. 安装两个开发环境依赖包
// 目录: packages/smart-ui-vite
pnpm i vitepress vitepress-theme-demoblock -D

// b. 因为文档需要编写 jsx 代码,引入 unocss 调整样式,所以需要增加 vite.config.ts 文件
// 目录: packages/smart-ui-vite/docs/vite.config.ts
import { defineConfig } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import Unocss from "../config/unocss";

export default defineConfig({
  plugins: [vueJsx(), Unocss()],
});

//  c. 配置文档相关的脚本命令
// 目录: packages/smart-ui-vite/package.json
  "scripts": {
    ...
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs",
    "docs:serve": "vitepress serve docs"
  },
  
// d. 生成导航菜单,增加 markdown 插槽,可以同时显示源代码和界面
// 目录:packages/smart-ui-vite/docs/.vitepress/config.ts
const sidebar = {
  "/": [
    { text: "快速开始" },
    { text: "通用", children: [{ text: "Button 按钮", link: "/" }] },
    { text: "导航" },
  ],
};
const config = {
  themeConfig: {
    sidebar,
  },
  markdown: {
    config: (md) => {
      const { demoBlockPlugin } = require("vitepress-theme-demoblock");
      md.use(demoBlockPlugin);
    },
  },
};
export default config;

// e. enhanceApp 方法中引入 SmartUI 组件,另外注册 Demo 和 DemoBlock 组件
// 目录:packages/smart-ui-vite/docs/.vitepress/theme/index.ts
import Theme from "vitepress/dist/client/theme-default";
import SmartUI from "../../../src/entry";
import Demo from "vitepress-theme-demoblock/components/Demo.vue";
import DemoBlock from "vitepress-theme-demoblock/components/DemoBlock.vue";
import "vitepress-theme-demoblock/theme/styles/index.css";

export default {
  ...Theme,
  enhanceApp({ app }) {
    app.use(SmartUI);
    app.component("Demo", Demo);
    app.component("DemoBlock", DemoBlock);
  },
};

  • 按钮示例的核心代码

目录: packages/smart-ui-vite/docs/index.md

image.png

image.png

  • 最后的实现效果

image.png

文档建设

前置知识

当前选用了 vercel 作为文档部署的网站,它提供了图形化的操作界面,提供了简洁的部署服务。需要注意的是,由于 vercel 是国外网站,需要翻墙才能访问部署后的网站。

操作步骤
  • 配置静态页面部署服务器。

选用了 vercel 作为部署服务器,它可以通过 GitHub 账号免费注册登陆,然后可以选择需要部署的项目,如下图所示。其中包括5个项目配置项,包括构建工具,构建命令,构建文件存放的目录,安装依赖命令,部署项目所在的相对路径。配置完成后即可部署访问生成的网站,之后有代码提交后也会重新自动部署。

image.png

  • 配置 homepage
// 可以将 vercel 生成的静态网站地址放到 homepage
// 目录: package.json
 "homepage": "https://smart-admin-plus.vercel.app/",

单元测试

前置知识

vitest 是一款极速的单元测试框架,它可以和 vite 共享配置文件 vite.confit.ts。另外,它使用了和 Jest 相同的 API,可以很快的上手,也实现了单元测试最常见的模拟,快照及覆盖率功能。

代码在开发环境中验证通过后,一般需要部署到一个全新的系统环境去验证,这种机制就是持续集成服务。这里为了方便,使用了 GitHub Action 去实现持续集成服务。通俗来说,就是创建 workflow,通过在 GitHub 服务器运行单元测试来进行回归验证。

操作步骤
// a. 安装依赖,其中 happy-dom 可以在 node 环境下模拟 dom 环境,@vue/test-utils 是 vue 提供的测试工具库。
// 目录:packages/smart-ui-vite
pnpm i -D vitest happy-dom @vue/test-utils

// b. vite 增加 test 配置项
// 目录:packages/smart-ui-vite/vite.config.ts
/// <reference types="vitest"/>
import { defineConfig } from "vite";

export default defineConfig({
  ...
  test: {
    globals: true,
    environment: "happy-dom",
    transformMode: {
      web: [/.[tj]sx$/],
    },
  },
});

// c. 将 index.tsx 文件变成只处理组件导出功能,而原来的组件代码迁移到同目录的 button.tsx 文件中
// 目录:packages/smart-ui-vite/src/button/index.tsx
import Button from "./button";
export default Button;

// d. 编写测试用例,第一个测试按钮的文本内容,第二个测试组件的默认类名是否生效
// 目录:packages/smart-ui-vite/src/button/__tests__/button.spec.ts
import { describe, expect, test } from "vitest";
import { shallowMount } from "@vue/test-utils";
import Button from "../button";

describe("button", () => {
  test("mount", () => {
    const wrapper = shallowMount(Button, {
      slots: { default: "Button" },
    });
    expect(wrapper.text()).toBe("Button");
  });
});
describe("color", () => {
  test("default", () => {
    const wrapper = shallowMount(Button);
    expect(wrapper.classes().includes("bg-blue-500")).toBe(true);
  });
});

// e. 配置运行命令
// 目录:packages/smart-ui-vite/package.json
{
  ...
  "scripts": {
    ...
    "test": "vitest",
   }
}

// f. 在文件 main.yml 中创建 jobs,push 到 GitHub 后就会运行 CI 服务
// .github/workflows/main.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  #   Lint:
  UnitTest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: pnpm/action-setup@v2.1.0
        with:
          version: 7.2.1
      - name: Install modules
        run: pnpm install
      - name: Run Test
        run: cd packages/smart-ui-vite && pnpm run test:run

部署成功后,每次 push 都会触发。

image.png

可以通过 GitHub Action 右上角的 create status badge 生成徽章,然后将代码粘贴到 readme.md 中

image.png

// g. 增加 coverage 参数,用于生成测试覆盖率报告。安装对应的开发依赖包 @vitest/coverage-istanbul 和 @vitest/coverage-c8
// packages/smart-ui-vite/package.json
  "scripts": {
    "coverage": "vitest run --coverage"
  },

// h. 修改配置文件
// packages/smart-ui-vite/vite.config.ts
  test: {
    coverage: {
      provider: "istanbul",   // 指定覆盖引擎
      reporter: ["text", "json", "html"],  // 指定输出格式
    },
  },

运行 pnpm coverage 即可看到生成的测试报告目录,打开 index.html 文件即可看到对应的测试结果。

image.png

image.png

规范化

前置知识

eslint 和 prettier 常常结合起来,用于对代码语法和格式进行校验,其中 eslint 常用于语法校验,而 prettier 用于代码格式化

husky 是 git hooks 工具,可以在提交 git 操作的时候执行自定义操作

commitlint 可以约束 git commit 提交的内容,使其符合规范

步骤
  • eslint + prettier 代码检查工具
// a. 安装依赖
// 目录:packages/smart-ui-vite
pnpm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-formatter-pretty eslint-plugin-json eslint-plugin-prettier eslint-plugin-vue @vue/eslint-config-prettier babel-eslint prettier

// b. 增加 eslint 配置项
// 目录:packages/smart-ui-vite/.eslintrc.cjs
module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
    node: true,
    jest: true,
  },
  globals: {
    ga: true,
    chrome: true,
    __DEV__: true,
  },
  parser: "vue-eslint-parser",
  extends: [
    "plugin:json/recommended",
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/prettier",
  ],
  plugins: ["@typescript-eslint"],
  parserOptions: {
    parser: "@typescript-eslint/parser",
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
    "prettier/prettier": "error",
  },
};

// c. 配置不进行 eslint 检查的文件或目录
// 目录:packages/smart-ui-vite/.eslintignore
*.sh
node_modules
lib
coverage
*.md
*.scss
*.woff
*.ttf
src/index.ts
dist

// d. 配置校验和格式化命令
// 目录:packages/smart-ui-vite/package.json
{
"scripts": {
    "lint": "eslint --fix --ext .ts,.vue src",
    "format": "prettier --write \"src/**/*.ts\" \"src/**/*.vue\"",
},
}
  • husky 自动化提交验证。由于 .git 文件存放在项目根目录,所以 husky 相关的指令在 workspace 中进行
// a. 安装依赖
// 根目录
pnpm i husky -D

// b. 增加 prepare 命令,运行 pnpm run prepare 即可生成 .husky 目录
// 目录:package.json
{
  "scripts": {
    ...
    "prepare": "husky install"
  },
}

// c. 运行命令,增加 commit 之前的代码检查
// 根目录
npx husky add .husky/pre-commit "cd packages/smart-ui-vite && pnpm run lint"

// d. 增加测试命令,可以在不检视文件更改的条件下执行一次测试
// 目录:packages/smart-ui-vite/package.json
{
 "scripts": {
   ...
   "test:run": "vitest run",
 }
}

// e. 运行命令,提交代码前进行一次单元测试
// 根目录
npx husky add .husky/pre-push "cd packages/smart-ui-vite && pnpm test:run"
  • git commit 提交规范。类似于 husky,这个也是作用于 workspace 中
// a.  安装依赖
// 根目录
pnpm i -D @commitlint/config-conventional @commitlint/cli

// b. 增加 commitlint 配置文件
// 目录: commitlint.config.ts
module.exports = {
  extends: ["@commitlint/config-conventional"],
};

// c. 执行命令,git commit 的时候增加对 commit message 的内容校验
// 根目录
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

发布到 NPM 仓库

前置知识

确定依赖的版本号,就可以打上 tag 进行发布。首次发布的时候可以手动使用 npm 的命令进行操作,后续可以使用 GitHub Action 进行自动发布。

操作步骤
  • 确定依赖包的版本号,版本号的格式一般是“主版本号.次版本号.修订号”

image.png

  • 打上 tag
# 创建版本号对应的 git tag,并推送到远程仓库 
git tag 1.0.0
git push --tag
  • 首次发布
// a. 创建发布 npm 包用的 shell 脚本。发布的时候可能出现连接错误,可取消重试几次
// packages/smart-ui-vite/publish.sh
#!/usr/bin/env bash
npm config get registry
npm config set registry=https://registry.npmjs.org
echo "请进行登录操作"
npm login
echo "-------publishing------"
npm publish
npm config set registry=https://registry.npm.taobao.org
echo "发布完成"
exit

// b. 使用命令行,给发布脚本增加执行权限
chmod +x publish.sh

// c. 使用命令行运行发布命令
./publish.sh

发布的时候需要输入用户名,密码,邮箱地址,发送到邮箱的一次性密码

image.png

  • 使用 GitHub Action 自动发布

    首次发布的时候需要用户名和密码,之后就可以使用 github action 实现自动发布。

    a. 创建发布用的 NPM_PUBLISH_TOKEN。创建 npm token 的具体步骤可见截图。注意,创建新 token 的时候,type 需要选择 publish 类型;生成 token 之后需要注意保存,因为只有首次生成后可见,GitHub 生成的 token 也是如此。
    image.png

    b. 创建发布用的 ACCESS_TOKEN。创建 GitHub 的 access token,具体步骤可见截图。注意,创建的 token 需要有操作目标仓库的权限。
    image.png

    c. 将生成的 NPM_PUBLISH_TOKEN 和 ACCESS_TOKEN 放到 actions secrets 里面,相当于创建了两个保存 token 的变量,这样代码里面就可以直接使用而不会泄漏 token。
    image.png

    d. 创建发布分支,增加发布脚本。当发布分支增加内容后,就会触发发布脚本,将 smart-ui-vite 发布到 npm 仓库

// 创建分支
// 根目录
git checkout -b publish-smart-ui-vite 
git push --set-upstream origin publish-smart-ui-vite

// 编写发布脚本
// 目录:.github/workflows/publish.yml
name: Publish Smart-ui-vite To Npm
on:
 push:
   branches: [publish-smart-ui-vite]
jobs:
 publish:
   runs-on: ubuntu-latest
   name: "publish npm"
   environment: npm
   steps:
     - uses: actions/checkout@master
     - uses: pnpm/action-setup@v2.1.0
       with:
         version: 6.31.0
     - name: Install modules
       run: pnpm install
     - name: Build
       run: cd packages/smart-ui-vite && npm run build
     - name: "Publish to the npm registry"
       uses: primer/publish@3.0.0
       env:
         GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
         NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} # 使用上一步生成的变量
       with:
         default_branch: "publish"
         dir: "packages/smart-ui-vite"

e. 查看发布成功的 npm 包。需要注意的是,由于 npm 有严格的重名检测,所以这里使用的包名是 mock-ui-vite-plus

image.png

架构复用:使用 cli 工具提高研发效率

前置知识

脚手架是帮助开发过程的工具和环境配置的集合,现在的脚手架一般是通过 cli,也就是命令行界面进行封装的。

步骤
  • 创建模版项目

项目源码已经上传到 GitHub

// a. 这里直接参考 Vue3 官网创建一个模版项目,使用命令`npm init vue@latest`,后面跟着提示操作即可,之后的配置可以都选择 No。
// 接下来通过 `npm i mock-ui-vite-plus` 安装已经发布的 npm 包

// b. 修改 main.ts 文件,导入 npm 包提供的组件和样式
// 目录:src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import SmartUI from "mock-ui-vite-plus";     // 导入 npm 包
import "mock-ui-vite-plus/dist/assets/entry.css";  // 导入样式
import "./assets/main.css";

createApp(App).use(SmartUI).mount("#app");

// c. 修改 app.vue 文件,使用 mock-ui-vite-plus 提供的组件
// src/app.vue 
<script setup lang="ts"></script>

<template>
  <div style="margin-top: 50px">
    <UButton color="gray" icon="search">gray button</UButton>
    <UButton color="red" icon="edit">red button</UButton>
    <UButton color="yellow" icon="check">red button</UButton>
  </div>
</template>

d. 另外可以将背景色修改成白色,这样启动项目后方便看到如下图所示的效果。

image.png

// e. 将项目中的 package.json 文件修改为模版生成代码,这里只是将 name 变成变量,可以跟随项目名称进行变化。
// 目录: template/package.hbs.json
{
  "name": "{{ name }}",
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check build-only",
    "preview": "vite preview --port 4173",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit"
  },
  "dependencies": {
    "mock-ui-vite-plus": "0.0.0-4201a75",
    "vue": "^3.2.38"
  },
  "devDependencies": {
    "@types/node": "^16.11.56",
    "@vitejs/plugin-vue": "^3.0.3",
    "@vitejs/plugin-vue-jsx": "^2.0.1",
    "@vue/tsconfig": "^0.1.3",
    "npm-run-all": "^4.1.5",
    "typescript": "~4.7.4",
    "vite": "^3.0.9",
    "vue-tsc": "^0.40.7"
  }
}
  • 创建 cli 工具
// a. 在 packages 文件中创建 cli 项目 create-smart-app-cli,安装相关依赖
// 目录:packages/create-smart-app-cli
pnpm init
pnpm i chalk chalk-animation clear download-git-repo figlet handlebars inquirer ora

// b. 编写生成终端操作界面的代码,可以选择使用模版还是退出
// 目录:packages/create-smart-app-cli/bin/index.js
#!/usr/bin/env node
import { promisify } from "util";
import figlet from "figlet";
import clear from "clear";
import chalk from "chalk";
import inquirer from "inquirer";
import chalkAnimation from "chalk-animation";

const opt = {
  "SmartUI应用模版(Vite)": "smart-ui-vite",
  退出: "quit",
};

const question = [
  {
    type: "rawlist" /* 选择框 */,
    message: "请选择要创建的项目?",
    name: "operation",
    choices: Object.keys(opt),
  },
];

clear();
const logo = figlet.textSync("Smart UI!", { // 打印欢迎画面
  horizontalLayout: "default",
  verticalLayout: "default",
  width: 80,
  whitespaceBreak: true,
});
const rainbow = chalkAnimation.rainbow(logo);
setTimeout(() => {
  rainbow.stop(); // Animation stops
  query();
}, 500);

async function query() {
  const answer = await inquirer.prompt(question);
  if (answer.operation === "退出") return;
  const { default: op } = await import(`../lib/operations/${opt[answer.operation]}.js`);
  await op();
}

// c. 编写克隆项目 smart-ui-app-js-template 的代码
// 目录: packages/create-smart-app-cli/lib/operations/smart-ui-vite.js
import clone from "../utils/clone.js";
import inquirer from "inquirer";
import { resolve } from "path";
import fs from "fs";
import chalk from "chalk";
import handlebars from "handlebars";

const log = (...args) => console.log(chalk.green(...args));

export default async () => {
  const { name } = await inquirer.prompt([
    {
      type: "input" /* 选择框 */,
      message: "请输入项目的名称?",
      name: "name",
    },
  ]);

  log("创建项目:" + name);

  // 从github克隆项目到指定文件夹
  await clone("github:leedawn/smart-ui-app-js-template", name);

  // 生成路由定义
  compile(
    {
      name,
    },
    `./${name}/package.json`,
    `./${name}/template/package.hbs.json`
  );

  log(`
安装完成:
To get Start:
===========================
cd ${name}
npm i
npm run dev
===========================
            `);
};

// 编译模板文件
function compile(meta, filePath, templatePath) {
  if (fs.existsSync(templatePath)) {
    const content = fs.readFileSync(templatePath).toString();
    const result = handlebars.compile(content)(meta);
    fs.writeFileSync(filePath, result);
    log(`${filePath} 修改成功`);
  } else {
    log(`${filePath} 修改失败`);
  }
}

// d. 克隆项目通过 ora 库增加进度条
// 目录:packages/create-smart-app-cli/lib/utils/clone.js
import { promisify } from "util";
import download from "download-git-repo";
import ora from "ora";
export default async (repo, desc) => {
  const process = ora(`下载.....${repo}`);
  process.start();
  await promisify(download)(repo, desc);
  process.succeed();
};

// e. package.json 增加一个 bin 属性,声明一个可执行文件 create-smart 
// 目录:packages/create-smart-app-cli/package.json
{
  "name": "create-smart-app-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "bin": {
    "create-smart": "./bin/index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chalk": "^5.1.2",
    "chalk-animation": "^2.0.3",
    "clear": "^0.1.0",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.2",
    "handlebars": "^4.7.7",
    "inquirer": "^9.1.4",
    "ora": "^6.1.2"
  }
}

// f. 通过 npm link 模拟全局安装的效果
// 目录:packages/create-smart-app-cli
sudo npm link
create-smart

执行命令后,控制台出现如下界面,选择应用模版即可创建对应的项目

image.png

  • 发布 cli 工具

    现在 Vue3 推荐创建项目的方法是使用 npm init vue 命令,类似的,刚才创建的 create-smart-app-cli 项目(项目名称需要以“create-”开头)也可以使用这种方式进行创建。按照前面的步骤,只需将项目 create-smart-app-cli 发布到 npm,然后本地就可以通过 npm init smart-app-cli 命令创建项目。需要注意的是,从项目发布到本地使用存在一定的延迟,所以最好过几分钟再使用命令创建项目。

image.png

image.png

总结

本项目使用了多包单仓库的开发模式来实践前端工程化方案。首先使用三种方法编写了 vue 组件,UnoCSS 引擎添加样式;采用 vitepress 进行文档建设,vercel 进行线上的文档部署;采用 vitest 搭建单元测试环境,生成覆盖率测试报告;使用 eslint,prettier,husky,commitLint 实现代码规范化 ;使用 GitHub Action 实现持续集成服务。最后也实现了一个 mini 的 cli 工具,并且发布到了 npm 仓库。当然,这里面每个模块都可以进一步细化,都值得进一步探索。

本文参考《基于 vite 的组件库工程化实战

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容