Howdyjs组件库迁移Vue3设计与总结

Howdyjs组件库迁移Vue3设计与总结

将个人组件库 Howdyjs 使用 Vue + Typescript 进行重构,使用 Vite 构建开发站点,Rollup 进行组件打包并分包发布 NPM。

主要变更

  1. 新版全面采用Typescript
  2. Vue 组件部分将使用Vue3重构,不向下兼容,有 Vue2.X 需求的请使用旧版
  3. 因各组件内的关联性不强,新版的组件库将进行分包发布,可便于按需加载
  4. 旧版多数包都将功能封装成 Vue 指令,并默认导出的是 Vue 指令,现在新版将默认导出原生构造函数,便于跨框架或原生使用,但同时保留了 Vue 指令封装的使用方式
  5. 使用lerna进行分包管理
  6. 使用Rollup进行组件打包
  7. 展示站点使用Vite搭建

架构说明

Vite

综合对比之后,发现采用 Vite 基本可以实现当前展示站点的所有功能,而且在开发环境下热更新速度极快,所以新版项目采用了 Vite 构建开发站点。

由于项目有导入.md 文件的需求,而 Vite 并不能直接使用 Webpack 的 markdown-loader,所以暂时自己写一个简单的 markdown-plugin 在 Vite 中使用

// vite.config.ts
const markdownPlugin = (options: any) => {
  return {
    name: "markdown",
    transform(code: string, id: string) {
      if (!/\.md/.test(id)) {
        return;
      }
      const result = marked(code, options);
      return `export default ${JSON.stringify(result)}`;
    },
  };
};
export default {
  plugins: [
    vue(),
    markdownPlugin({
      highlight: (code: string) => {
        if (code.includes("template")) {
          return hljs.highlight("html", code).value;
        } else if (code.includes('lang="ts"')) {
          return hljs.highlight("typescript", code).value;
        } else {
          return hljs.highlightAuto(code).value;
        }
      },
    }),
  ],
};

Vite 新版文档地址: https://vitejs.dev/

Lerna

Lerna 是一个项目内包管理工具,虽然当前项目内的组件关联性不强,但也提前先引入了 Lerna 进行分包管理。

  • 执行npm run bootstrap命令进行项目初始化.
  • 执行npm run publish命令可快速发包

Rollup 打包

组件使用 Rollup 进行打包,执行npm run build:pkg打包各 Packages,包含 cjs、es 和其 d.ts 文件。

使用 nodejs 执行 rollup 打包,代码位于/scripts 下,build.js 为打包初始模板,一个组件会被打包出 3 种格式:cjs/esm/umd,格式说明参考格式

Vue 路由自动生成

使用 Vite 打包时,Vue 路由懒加载是基于 Rollup 的动态引入插件的,它对我原站点的格式不太适用。而由于展示站点中,各个路由格式是具有一定通用性的,所有采用了一种读取文件目录自动生成路由文件的方式。

// scripts/gen-route.js
// 自动生成路由文件
const fs = require("fs");
const packagesDirs = fs.readdirSync("./src/pages");
const packagesMap = {};
packagesDirs.map((package) => {
  const exampleDirs = fs.readdirSync(`./src/pages/${package}/example`);
  const exampleNum = exampleDirs.length;
  packagesMap[package] = exampleNum;
});
const packages = Object.keys(packagesMap).map((key) => {
  return {
    name: key,
    exampleNum: packagesMap[key],
  };
});
const routes = [
  {
    path: "/",
    name: "home",
    component: "i(../views/home.vue)",
  },
  ...packages.map((pkg) => {
    const { name, exampleNum } = pkg;
    return {
      path: `/${name}`,
      name: `${name}`,
      redirect: `/${name}/readme`,
      component: `i(../pages/${name}/index.vue)`,
      children: [
        {
          path: `/${name}/readme`,
          name: `${name}-readme`,
          component: "i(../components/PageReadme.vue)",
        },
        ...Array.from({ length: exampleNum }, (_, exampleIndex) => {
          return {
            path: `/${name}/example${exampleIndex + 1}`,
            name: `${name}-example${exampleIndex + 1}`,
            component: `i(../pages/${name}/example/example${
              exampleIndex + 1
            }.vue)`,
          };
        }),
      ],
    };
  }),
];

const reg = /"i\((.*?)\)"/g;
const routesStr = JSON.stringify(routes, null, 2).replace(
  reg,
  (...arg) => `() => import("${arg[1]}")`
);

const output = `
/* eslint-disable */
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
  history: createWebHistory('/howdy/'),
  routes: ${routesStr}
});

export default router;
`;

fs.writeFileSync("./src/router/index.ts", output);

执行命令npm run gen-router后,会自动读取/packages 下的包文件,然后生成出对应的路由。

说明