• Vue3,Vite,TypeScript,Monorepo,qiankun...... Buff叠满,BUG没有


    背景

    之前不是说了吗,公司准备升到 Vue3。后面又因为一些需求把项目结构改成了 qiankun 和 Monorepo,所以这篇文章就是一个建设模板的讲解。

    老话说的好,~打 BOSS 前要把 Buff 加满~,不是,是工欲善其事,必先利其器。在其它小伙伴过来写代码之前需要把重复工作的模板搭好,开发效率才能更高。

    所以我们的目标就是,ESLintPrettierTypeScript之类的工具配置好,可以用命令行启动整个项目,可以用命令行新建微应用。可以让其他人零配置直接上手写代码。

    开始

    本项目默认包管理器使用 pnpm,IDE 使用 VSCode

    pnpm create vite 
    
    • 1

    image.png

    TypeScript

    看下依赖,只有 vue-tsc 不熟,查下文档。(注意下图红框)

    image.png

    看起来是一个检查 Vue SFC 文件 TS 类型的命令行工具,比如在打包或者在 merge 代码前检查 TS 类型是否正确,而在 dev 环境下使用 Volar 插件来验证。

    这样看来,需要推荐安装 Volar 插件。

    在根目录新建 .vscode 目录,添加

    {
      "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
    } 
    
    • 1
    • 2
    • 3

    这样其他用 VSCode 打开这个项目的人就会弹出推荐安装该插件。

    插件的名字可以在详情查看

    image.png

    这样开发时就有 TS 类型约束了。

    image.png

    而且安装这两个插件之后,也不需要原来的 *.vue 的类型声明了。

    ESLint

    pnpm install eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser -D 
    
    • 1

    需要注意的 @typescript-eslint/parser@typescript-eslint/eslint-plugin 版本号必需一致

    添加 .eslintrc.js.eslintignore 文件

    pnpm install eslint-define-config -D 
    
    • 1

    .eslintrc.js

    // @ts-check
    const { defineConfig } = require('eslint-define-config');
    
    module.exports = defineConfig({
      root: true,
      rules: {
        // rules...
      },
    }); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这样 eslintrc 也有类型约束了

    image.png

    同样,VSCode 的 ESLint 插件也添加到推荐 JSON 里。下面不在提示了。

    Prettier

    VolarESLint 都有格式化功能,那为什么还要选择 Prettier 呢?

    代码风格最不好管理的地方在哪里? 就是所有人都有自己的习惯,无法讨论出一个让所有人都满意的方案

    Prettier 的方案是 opinionated,就是给出最小的配置,要不你就用我的风格,要不你就别用。不容许随便改。(所以也不用吵了。。。)

    image.png

    注意代码风格语法检查的区别,代码风格是要不要加空格,分号,逗号等等。语法检查是是否声明变量,声明未使用等等。

    pnpm install -D eslint-config-prettier eslint-plugin-prettier prettier 
    
    • 1

    修改 .eslintrc.js 配置

    {
      "extends": [
        "some-other-config-you-use",
        "prettier"
      ],
      plugins: ["prettier"],
      rules: {
        "other-rules",
        "prettier/prettier": "error"
      }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在 VSCode 安装 Prettier 插件,把格式化方式改为 Prettire,测试格式化正常。

    image.png

    Stylelint

    pnpm install --D stylelint stylelint-config-recommended-vue stylelint-config-prettier stylelint-config-recess-order postcss-html 
    
    • 1

    新建 .stylelintrc.json

    {
      "extends": ["stylelint-config-recommended-vue", "stylelint-config-prettier", "stylelint-config-recess-order"]
    } 
    
    • 1
    • 2
    • 3

    需要注意几个问题

    1. 需要关闭 VSCode 自己的 css 验证,否则会有两个验证报错

    settings.json

    "css.validate": false,
    "less.validate": false,
    "scss.validate": false 
    
    • 1
    • 2
    • 3
    1. 如果 .stylelintrc 配置文件是用 .json 结尾的,不要加注释。不是 JSON5,会导致拉取配置无法编译。
    2. stylelint-config-standard 默认规则只对 css 生效,无法解析 Vue
    3. 如果使用预编译器,需要替换对应的规则集

    比如要使用scss

    pnpm install stylelint-config-standard-scss -D 
    
    • 1

    .stylelintrc.json 配置改为

    {
      "extends": [
        "stylelint-config-standard-scss",
        "stylelint-config-recommended-vue/scss",
        "stylelint-config-prettier",
        "stylelint-config-recess-order"
      ]
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Windi CSS

    pnpm i -D vite-plugin-windicss windicss 
    
    • 1

    然后,在你的 Vite 配置中添加插件:

    vite.config.js

    import WindiCSS from 'vite-plugin-windicss'
    
    export default {
      plugins: [
        WindiCSS(),
      ],
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    最后,在你的 Vite 入口文件中导入 virtual:windi.css

    main.js

    import 'virtual:windi.css' 
    
    • 1

    安装 VSCode 插件,在 settings.json 里添加

     "editor.quickSuggestions": {
        "strings": true
      }, 
    
    • 1
    • 2
    • 3

    这样就会有代码提示悬停预览等等功能

    image.png

    windi.config.ts 中配置

    import { defineConfig } from 'windicss/helpers'
    
    export default defineConfig({
      extract: {
        include: [
          'src/**/*.{vue,jsx,tsx,svelte}',
          'shared/**/*.{vue,ts}',
        ],
      },
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后,在 VSCode 中 Ctrl+Shift+P , 运行命令:Windi CSS: Run & Open Analysis 可以打开 windicss 的可视化分析。

    生产依赖

    演示就用 antd 了

    pnpm install @ant-design/icons-vue ant-design-vue @vueuse/core pinia vue-router 
    
    • 1

    业务相关代码,比如布局啊,全局主题啊不是这次的关键,就不详细写了。

    monorepo

    pnpm 使用 monorepo 非常简单,只需要加个文件 pnpm-workspace.yaml,然后新建一个packages 文件夹

    prefer-workspace-packages: true
    packages:
      - 'packages/**' 
    
    • 1
    • 2
    • 3

    这样 packages 里就是单独的 repo,而各种配置和依赖可以共用全局的。

    qiankun

    为了让共享配置层级清晰,qiankun 主应用(基座)和子应用都放到 packages 中。

    pnpm install -W qiankun 
    
    • 1

    全局依赖需要加 -W

    主应用几个需要改到的地方。

    1. 路由,需要把子应用注册路由加进来
    // 获取微服务路由
    const microRoutes = []
    microApps.forEach(micro => {
      microRoutes.push({
        path: `${micro.activeRule}/:morePath*`,
        component: Layout
      })
    })
    
    // 创建路由实例
    export const router: Router = createRouter({
      history: createWebHistory(),
      routes: microRoutes.concat([
        {
          path: "/",
          component: Layout,
          redirect: "/app1"
        }
      ])
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. main.ts 入口处,添加注册信息

    microApps.ts

    type microApp = {
      name: string
      entry: string
      container: "#view-main"
      activeRule: string
      port: number | string
    }
    
    const apps: microApp[] = [
      {
        name: "app1",
        entry: "//localhost:10002",
        container: "#view-main",
        activeRule: "/app1",
        port: "10002"
      }
    ]
    export default apps 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    import { start as startQianKun, registerMicroApps, initGlobalState, MicroAppStateActions } from "qiankun"
    
    // 微应用的信息
    import apps from "./microApps"
    
    /**
     * 注册微应用
     * 第一个参数 - 微应用的注册信息
     * 第二个参数 - 全局生命周期钩子
     */
    registerMicroApps(apps, {
      // qiankun 生命周期钩子 - 微应用加载前
      beforeLoad: (app: any) => {
        // 加载微应用前,加载进度条
        // eslint-disable-next-line no-console
        console.log("before load app.name====>>>>>", app.name)
        return Promise.resolve(app)
      },
      beforeMount: (app: any) => {
        // eslint-disable-next-line no-console
        console.log("[LifeCycle] before mount %c%s", "color: green;", app.name)
        return Promise.resolve(app)
      },
      // qiankun 生命周期钩子 - 微应用挂载后
      afterMount: (app: any) => {
        // 加载微应用前,进度条加载完成
        // eslint-disable-next-line no-console
        console.log("[LifeCycle] after mount %c%s", "color: green;", app.name)
        return Promise.resolve(app)
      }
    })
    
    /**
     * qiankun 通信实例
     */
    const initialState = {}
    export const actions: MicroAppStateActions = initGlobalState(initialState)
    
    // 注册 qiankun
    startQianKun({ sandbox: { strictStyleIsolation: true } })
    // qiankun 通信
    actions.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prevState: 变更前的状态
      // console.log(state, prevState)
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    1. 布局处添加子应用挂载的入口
     <a-layout>
        <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
          <div class="logo" />
          <Menu></Menu>
        </a-layout-sider>
        <a-layout>
          <a-layout-header style="padding: 0; background: #fff">
            <menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
            <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
          </a-layout-header>
          <a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
            <!-- 子应用容器 -->
            <div id="view-main" />
            <!-- 主应用自己的路由渲染 -->
            <router-view></router-view>
          </a-layout-content>
        </a-layout>
      </a-layout> 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    子应用需要注意的位置

    1. package.jsonname 需要和主应用注册的 name 一致
    2. vite.config.tsserver.port 需要改为主应用注册的 entry 端口一致
    3. 子应用路由
    export const router: Router = createRouter({
      history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? `/${packageConfig.name}` : "/"),
    }) 
    
    • 1
    • 2
    • 3
    1. vite.config.ts 添加插件
    import packageConfig from "./package.json"
    import qiankun from "vite-plugin-qiankun"
    const useDevMode = true // 如果是在主应用中加载子应用 vite, 必须打开这个, 否则vite加载不成功, 单独运行没影响
    
    plugins: [
      qiankun(`${packageConfig.name}`, { useDevMode })
    ] 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. main.ts 改为
    import { createApp } from "vue"
    import { createPinia } from "pinia"
    import router from "./router"
    import { renderWithQiankun, qiankunWindow, QiankunProps } from "vite-plugin-qiankun/dist/helper"
    import { MicroAppStateActions } from "qiankun"
    
    import Antd from "ant-design-vue"
    import "ant-design-vue/dist/antd.variable.min.css"
    import "virtual:windi.css" // windi Css
    
    import App from "./App.vue"
    let instance = null
    
    type AppProps = Partial<MicroAppStateActions & QiankunProps>
    function render(props: AppProps) {
      const { container } = props
      if (container) {
        // 注册主,微应用全局通信
        props.onGlobalStateChange((state, prevState) => {
          // useTheme().theme({ state, prevState, container })
        })
      }
      instance = createApp(App)
      instance.use(router).use(createPinia).use(Antd)
      instance.mount(container ? container.querySelector("#app") : document.getElementById("app"))
      if (qiankunWindow.__POWERED_BY_QIANKUN__) {
        // console.log('我正在作为子应用运行')
      }
    }
    
    renderWithQiankun({
      /**
       * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
       * @param props
       */
      mount(props: MicroAppStateActions & QiankunProps) {
        render(props)
      },
      /**
       * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子
       */
      bootstrap() {
        // 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
      },
      /**
       * 应用每次切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
       */
      unmount() {
        instance.unmount()
        instance._container.innerHTML = ""
        instance = null
      }
    })
    
    if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
      render({})
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    在全局 package.json 添加启动命令

     "scripts": {
        "dev": "pnpm -r --filter ./packages --parallel run dev",
        "build": "pnpm -r --filter ./packages --parallel run build",
        "preview": "pnpm -r --filter ./packages --parallel run preview",
        "clean": "rm -rf node_modules **/*/node_modules"
      } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样,整个项目就部署好了。

    整合配置

    但是,现在所有的配置还是分散的,需要整合下。

    ESLintPrettierrStylelint 用全局的一份配置就行。

    packages.json 主子应用都有依赖放到全局,子应用单独用的放到自己文件内。

    • 主应用全局依赖 pnpm install xxxx -W 其他命令和原命令一致,多添加一个 -W
    • 子应用依赖安装 pnpm i xxxx --filter ${微应用pwd} 比如 pnpm uninstall uuid --filter app1 会在 app1 子应用下安装 uuid 依赖,当然也可以去子应用目录下直接安装

    tsconfig.json 使用 extends 继承全局配置,其中 paths 会匹配到本应用。比如这个 @ 就是该子应用的 src,不是全局的。

    {
      "extends": "../../tsconfig.json",
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@/*": ["src/*"],
          "common/*": ["../../common/*"]
        }
      },
      "exclude": ["dist", "**/*.js"]
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    vite.config.ts 使用 loadConfigFromFilemergeConfig,当然其实读取文件,手动合并也是可以的。

    import { defineConfig, loadConfigFromFile, mergeConfig } from "vite"
    import AutoImport from "unplugin-auto-import/vite"
    import packageConfig from "./package.json"
    import qiankun from "vite-plugin-qiankun"
    import { resolve } from "path"
    
    const pathResolve = (dir: string): string => {
      return resolve(__dirname, ".", dir)
    }
    
    const useDevMode = true // 如果是在主应用中加载子应用 vite, 必须打开这个, 否则vite加载不成功, 单独运行没影响
    // https://vitejs.dev/config/
    export default defineConfig(async ({ command, mode }) => {
      const base = (await loadConfigFromFile({ command, mode }, pathResolve("../../vite.config.ts"))).config
      base.plugins.shift()
      const config = {
        resolve: {
          alias: {
            "@": pathResolve("src"),
            common: pathResolve("../../common")
          }
        },
        server: { port: 10002 },
        plugins: [
          qiankun(`${packageConfig.name}`, { useDevMode }),
          AutoImport({
            // vue函数的自动导入
            imports: ["vue", "vue-router", "pinia", "@vueuse/core"],
            dts: false
          })
        ]
      }
      return mergeConfig(base, config)
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    windi.config.ts 使用 presets 继承全局配置。

    import { defineConfig } from "windicss/helpers"
    
    export default defineConfig({
      presets: [require("../../windi.config.ts").default]
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用命令行生成子应用

    虽然创建子应用不是一个高频操作,但还是有很多重复操作,而且,每个子应用的模板也不好统一。

    所以我们要设计一个命令行可以自动生成子应用。那么大概需要几步

    1. 根据 nameport 注册子应用
    2. 根据 namepackages 下新建项目,从 git 仓库上拉一个模板下来,修改 nameport 之类的配置。
    3. pnpm install 并提示
    pnpm i -W -D download-git-repo fs-extra inquirer 
    
    • 1
    • download-git-repo 从 git clone 仓库到本地
    • fs-extra 一个扩展的 fs
    • inquirer 可以根据预设收集用户输入,这里用来确认子应用的 name, port

    创建提示用户输入

     const apps = await readAppsFile()
      // console.log(apps)
      const meta = await inquirer.prompt([
        {
          type: "input",
          message: "请输入你要新建微应用名字(英文或者数字):",
          name: "appName",
          validate(answer) {
            console.log(answer)
            const done = this.async()
            const validateRes = RegxMap.IS_APP_NAME.test(answer)
            if (!validateRes) {
              done("请按要求输入正确的微应用名字!")
              return
            }
            if (apps.find(item => item.name === answer)) {
              done("已存在同名微应用,请确认后更换名字再重试。")
              return
            }
            done(null, true)
          }
        },
        {
          type: "input",
          message: "请输入你要新建的微应用的端口(数字,推荐 10000 - 10010):",
          name: "port",
          validate(answer) {
            const done = this.async()
            const validateRes = RegxMap.IS_PORT.test(answer)
            if (!validateRes) {
              done("只能输入数字!")
              return
            }
            // eslint-disable-next-line eqeqeq
            if (apps.find(item => item.port == answer)) {
              done("已存在相同微应用端口,请确认后更换再重试。")
              return
            }
            done(null, true)
          }
        }
      ])
      return meta 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    用户输入成功之后,从 git 下载模板,正则匹配对应的关键字,替换为用户输入字段

    module.exports = async meta => {
     await cloneRepo(meta)
     await replaceName(meta)
     const newApps = await writeAppsFile(meta)
     await writeMicroApps(newApps)
     const workerProcess = exec(`pnpm install`)
     workerProcess.stdout.on("data", data => {
       console.log("stdout: " + data)
     })
    
     workerProcess.stderr.on("data", data => {
       console.log("stderr: " + data)
     })
     console.log(`微服务创建成功,请在 pnpm install 完成之后,前往 packages/${meta.appName} 目录进行开发`)
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    下面是修改注册子应用信息的代码,其他修改大同小异

    // 修改 modules/microApps.ts
    const writeMicroApps = async newApps => {
      const reg = /const\sapps:\smicroApp\[\]\s=\s\[(.|\s)*export\sdefault\sapps/
      const filePath = `../../common/modules/microApps.ts`
      const microAppsFile = fs.readFileSync(resolve(__dirname, filePath), "utf-8")
      const microAppsContent = microAppsFile.replace(reg, `const apps: microApp[] = ${newApps} \r\n export default apps`)
      fs.writeFile(resolve(__dirname, filePath), microAppsContent, err => {
        if (err) console.log(err)
      })
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我这里是用一个 JSON 记录了所有子应用信息,当然也可以用正则去匹配 fs 读取的注册信息

    添加命令,测试一下。

    "gen": "node ./scripts/genNewMicroApp/index.js" 
    
    • 1

    image.png

    image.png

    因为 Menu 的逻辑没有写,手动改下路由看下。

    image.png

    其它问题

    代码仓库: github.com/zhl1232/qia…
    模板仓库:github.com/zhl1232/qia…

    1. 一些业务逻辑没有写,比如 Menu 逻辑,全局主题之类的
    2. 现在子应用模板只有 Vue
    3. 只有开发环境,没有部署配置
    4. 仓库行尾是用的 linux 配置,windows 环境自行更改 Prettier 下的 endOfLine

    等等吧,如果有需求的话,后面再补吧。

    image.png

    推荐

    VSCode 开发的话,可以试下这个插件 GitHub Copilot

    就是最近很火的那个 AI 写代码的。挺有意思的,一方面,简单的代码可以靠提示就行,另一方面,如果思路卡住了,他可能还真能给你一些提示。

    Code Spell Checker
    一个单词拼写检查。语法,风格都有检查的,但实际上单词拼错了也会影响项目质量。不信你装上看一下,基本每个页面都有拼错的单词。。。

  • 相关阅读:
    python try-except捕获异常的方法
    服务器被入侵怎么办
    hive升级HiveException java. lang. RuntimeException
    【BOOST C++ 19 应用库】(5)序列数据封装和优化
    SQL Server全套教程(基于SQL语句----预览版)
    蓝桥杯KMP总结
    uniapp 获取地理位置(uni#getLocation和高德sdk获取中文地址)
    机器视觉公司怎么可能养我这闲人,连软件加密狗都用不起,项目都用盗版,为什么​?
    Vue计算属性computed和监听watch
    【刷爆LeetCode】七月算法集训(20)二叉搜索树
  • 原文地址:https://blog.csdn.net/web220507/article/details/125387377