likes
comments
collection
share

技术缝合怪 小程序架构方案(pnpm + tailwindcss + eslint + vite + commitlint + uniapp)

作者站长头像
站长
· 阅读数 36

缝合怪

采用多种技术框架,搭建的小程序解决方案,降低开发阶段的心智成本,提高研发效率。

monorepo

项目结构

  • miniprogram-workspace
    • icon (具体实现可以使用字体图标库、或者使用svg,建议封装成vue组件)
    • miniprogram(小程序主项目)
    • mui(公共组件库)
    • navigate(路由导航,支持页面间以promise的方式传递数据,支持拦截器)
    • plugins(vite相关插件)
    • request(网络请求封装,支持拦截器,支持文件上传,并发上传)
    • utils(公共函数库)

创建uniapp项目

使用@vue/cli创建,模版选择使用vite+tsnpx degit dcloudio/uni-preset-vue#vite-ts 项目名称 ,创建模版的命令,名称暂用miniprogram代替,下载完毕后需要修改地方。

// package.json
{
	"type": "module"
}
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import type { Plugin } from 'vite'

// vite.config.ts 
export default defineConfig({
  plugins: [(uni as unknown as { default: () => Plugin[] }).default()],
  // 按理不应该这样处理,估计@dcloudio/vite-plugin-uni打包有问题,看官方是否会修复这个问题
});

此项目是依赖monorepo 设计的,需要将uniapp项目作为子目录,pnpm-workspace.yaml 加入配置即可。(主目录暂用miniprogram-workspace (工程目录)代替)。

插件配置

vitePluginMiniPages

这个插件的作用是自动生成pages.json文件,支持每个页面独立的json配置文件。插件源码查看

// index.jso,和index.vue 同级目录
{
  "style": {
    "navigationBarTitleText": "测试"
  },
  "order": -1, // 排序 生序
  "name": "路由名称"
}

可以在指定的路径生成router.ts文件,包含所有页面的路由信息,路由跳转具体页面可以使用这个枚举。

export const router = {
  /** /pages/index/index */
  INDEX: '/pages/index/index',
  /** /subpagesB/pages/user/user */
  SUBPAGESB_USER: '/subpagesB/pages/user/user',
}

export enum Router {
  /** /pages/index/index */
  INDEX = '/pages/index/index',
  /** /subpagesB/pages/user/user */
  SUBPAGESB_USER = '/subpagesB/pages/user/user',
}

插件配置项,添加到miniprogram 项目vite.config.ts内。

import { vitePluginMiniPages } from '@miniprogram/plugins'

vitePluginMiniPages({
  paths: path.join(__dirname, 'src/pages'), // 主包路径
  subPaths: glob.globSync(path.join(__dirname, 'src/sub*')), // 分包路径
  pagesFile: path.join(__dirname, 'src/pages.json'), // pages.json文件路径
  routerFile: path.join(__dirname, 'src/router.ts'), // router.ts文件路径
  rootPath: path.join(__dirname, 'src'), // 监听根目录
})

vitePluginOssAssets

这个插件的作用是自动把assets资源引用的资源文件自动上传到ali-oss,打包后的路径替换成oss资源地址,自动删除上传的资源文件,插件配置项,添加到miniprogram 项目vite.config.ts内。插件源码查看

// vite.config.ts

import { vitePluginOssAssets } from '@miniprogram/plugins'

{
  plugins: [
	  vitePluginOssAssets({
        accessKeyId: '*********************************',
        accessKeySecret: '*****************************',
        bucket: 'bucket', // 桶
        bucketPath: 'dev/public/', // 子目录,主要区分环境路径
        includes: /\.svg$|\.png$|\.jpg$/, // 需要处理的资源类型
        assetsPath: path.join(__dirname, 'src/assets'), // 资源文件路径
        version: true, // 是否自动添加文件版本(默认添加文件hash),boolean | string
        enable: true, // 是否启动插件 
        replace(url: string) {
          // 将本地文件路径替换成oss地址
          return {
            name: url,
            uri: new URL(url, `${'https://www.aliyuncs.com/'}${'dev/public/'}`).href,
          }
        },
      })
  ],
	resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
}

<image
  class="w-[200rpx] h-[200rpx] mb-[50rpx] mx-auto"
  src="@/assets/logo.png"
/>

vitePluginMiniCiUpload

这个插件的作用是打包自动发布小程序到体验。具体官方文档查看这里。插件源码查看

// vite.config.ts

import { vitePluginMiniCiUpload } from '@miniprogram/plugins'

{
	plugins: [
		vitePluginMiniCiUpload({
        enable: result.mode === 'production',
        appid: '***************************',
        privateKeyPath: path.join(__dirname, 'ci/private.key'), // 私钥文件路径,小程序的私钥文件
        version: '0.0.1',
        setting: {
          es6: false,
          minify: true,
        },
      })
	]
}

@miniprogram/navigate 说明文档

源码查看

export declare class Nav<T extends string> {
    switchTab(options: SwitchTabOptions<T>): Promise<any>;
    reLaunch(options: ReLaunchOptions<T>): Promise<any>;
    redirectTo(options: RedirectToOptions<T>): Promise<any>;
    navigateTo(options: NavigateToOptions<T>): Promise<UniApp.NavigateToSuccessOptions>;
    navigateBack(options?: UniApp.NavigateBackOptions): Promise<any>;
    back(result?: NavigateResultObject): void;
    navigate(options: NavigateToOptions<T>, data?: NavigateResultObject): Promise<NavigateResultObject>;
    beforeEach(fn: Handlers<T>['fulfilled']): void;
}
export declare function onNavigate(query?: AnyObject, fn?: (result?: NavigateResultObject) => void): void;
export declare function navigatePlugin(): {
    install(app: App): void;
};
export declare function currentPage(): Page.PageInstance<AnyObject, {}> | null;
export declare function currentInstance(): any;

switchTab, reLaunch, redirectTo, navigateTo,重写了options参数,url类型为T范型,新增query参数,会自动拼接到地址url上

初始化Nav类,传入范型Router 类达到约束url的目的。

import { Nav } from '@miniprogram/navigate'
import { Router } from '@/router'

export const nav = new Nav<Router>()

// 拦截器用法
nav.beforeEach(async (to, from) => {
	  // 1、重定向路由,继续下一个拦截器
    return {
       url: Router.SUBPAGESA_HOME,
       query: { a: 1 }
    }
    // 2、继续下一个拦截器
    return true
    
    // 3、终止 之后的拦截器都不执行
    return false
})

navigate、back、onNavigate方法配套使用

// index.vue

console.info('我进去了 home')
const data = await nav.navigate(
	  {
	    url: Router.HOME,
	    query: { key: '1' },
	  },
	  {
	    test: 1,
	  },
)
console.info('我出来了 home', data) // { a: 1 }
// home.vue

onLoad((query) => {
  onNavigate(query, (event) => {
    console.info(event) // { test: 1 }
  })
})

function onBack() {
  nav.back({ a: 1 })
}

为了处理不是通过back出栈的操作,防止内存溢出,需要在app实例使用插件navigatePlugin

import { navigatePlugin } from '@miniprogram/navigate'

app.use(navigatePlugin())

// 监听页面卸载事件,来处理上个页面的promise

@miniprogram/request 文档说明

源码查看

export declare class Request {
    static create(option?: BaseOption): Request;
    interceptors: Interceptors<UniApp.RequestOptions, UniApp.RequestSuccessCallbackResult, UniApp.UploadFileOption, UniApp.UploadFileSuccessCallbackResult>;
    request(options: RequestOptions): Promise<UniApp.RequestSuccessCallbackResult>;
    get(options: RequestOptions): Promise<UniApp.RequestSuccessCallbackResult>;
    post(options: RequestOptions): Promise<UniApp.RequestSuccessCallbackResult>;
    upload(options: UploadOptions): Promise<UniApp.UploadFileSuccessCallbackResult>;
    uploadQueue(files: string | string[], options: Omit<UploadOptions, 'filePath'>, queue?: number): Promise<UniApp.UploadFileSuccessCallbackResult[]>;
}

参考axios 实现方式封装uni.request

import { AbortController, Request } from '@miniprogram/request'

// 初始化请求实例
const request = Request.create({
  baseURL: 'https://www.baidu.com',
})

// 定义请求拦截器
request.interceptors.request.use((config) => {
  return config
})
// 定义响应拦截器
request.interceptors.response.use(
  (result) => {
    if (result.statusCode === 404) {
      return Promise.reject(404)
    }
    return result
  },
  (error) => {
    return Promise.reject(error)
  },
)
// 定义响应拦截器
request.interceptors.response.use(
  (result) => {
    return result
  },
  (error) => {
    return Promise.reject(error)
  },
)

// 定义上传响应拦截器
request.interceptors.fileResponse.use(
  (result) => {
    if (result.statusCode === 404) {
      return Promise.reject(404)
    }
    return result
  },
  (error) => {
    return Promise.reject(error)
  },
)

// 中断请求
const abortController = new AbortController()
const data = await request.get({
  url: '/s?wd=a',
  signal: abortController.signal,
})
abortController.abort()

// 上传文件,并发上传
uni.chooseImage({
  count: 9,
  success: (res) => {
	  // 单个文件上传
    request.upload({
      url: '/upload',
      name: 'file',
      filePath: res.tempFilePaths[0]
    })
    
    // 并发上传 
    request.uploadQueue(res.tempFilePaths, {
      url: '/upload',
      name: 'file',
    }, 9)
  }
})

引入tailwindcss

直接采用@uni-helper/vite-plugin-uni-tailwind解决方案。

// vite.config.ts

import { defineConfig } from 'vite'
import nested from 'tailwindcss/nesting'
import tailwindcss from 'tailwindcss'
import tailwindcssConfig from './tailwind.config'
import postcssPresetEnv from 'postcss-preset-env'
import uni from '@dcloudio/vite-plugin-uni'
import uniTailwind from '@uni-helper/vite-plugin-uni-tailwind'

export default defineConfig({
  css: {
    postcss: {
      plugins: [
        nested(),
        tailwindcss({
          config: tailwindcssConfig,
        }),
        postcssPresetEnv({
          stage: 3,
          features: { 'nesting-rules': false },
        }),
      ],
    },
  },
  plugins: [
    uni(),
    uniTailwind(),
  ],
})

// tailwind.config.ts

/** @type {import('tailwindcss').Config} */
import type { Config } from 'tailwindcss'
import plugin from 'tailwindcss/plugin'

import { basePreset, elementPlusPreset, miniprogramBasePreset } from 'tailwind-extensions'
import { isMp, isQuickapp } from '@uni-helper/uni-env'

const presets: Config['presets'] = [basePreset]
if (isMp || isQuickapp) {
  presets.push(
    elementPlusPreset({
      baseSelectors: [':root', 'page'],
    }),
    miniprogramBasePreset,
  )
}
else {
  presets.push(elementPlusPreset())
}

const theme: Config['theme'] = {}
if (isMp || isQuickapp)
  theme.screens = {}

const config: Config = {
  content: [
    'src/**/*.{vue,ts}',
    'src/main.ts',
    'src/App.vue',
    './index.html',
  ],
  corePlugins: {
    preflight: false,
    space: false,
    divideWidth: false,
  },
  divideColor: false,
  divideStyle: false,
  divideOpacity: false,
  presets,
  theme,
  plugins: [],
}

export default config

添加tailwind.css文件

// tailwind.css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

在App.vue导入tailwind.css

<style lang="scss">
@import "./styles/tailwind.css";
</style>

引入Eslint

npm init @eslint/config
# 选择 To check syntax and find problems
# 选择 JavaScript modules (import/export)
# 选择 Vue
# 选择 Does your project use TypeScript? yes
# 选择 Where does your code run? Browser
# 选择 Would you like to install them now? yes
# 选择 Which package manager do you want to use? pnpm

打开eslint.config.js文件

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";

/**
 * @type { import("eslint").Linter.Config[]}
 */
export default [
  {languageOptions: { globals: globals.browser }},
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  ...pluginVue.configs['flat/recommended'], // 更为严格的模式
  // ...pluginVue.configs["flat/essential"],
  {
    rules: {
      "vue/multi-word-component-names": "off",
      "no-undef": "off",
    },
	  // 添加自定义规则
  },
  {
	  ignores: [
      "**/dist/**/*.js",
      "**/dist/**/*.d.ts",
      "**/build/**/*.js",
      "**/build/**/*.d.ts",
      "**/node_modules/**/*.js",
      "**/node_modules/**/*.d.ts"
    ],
  }
];

eslint-plugin-vue 内包含多种预设项,具体请查看这里

修改miniprogram-workspace 下的package.json,添加两个命令后执行看是否有效果。

{
	"scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  },
}

主流编辑器vscode配置eslint ,参考github.com/antfu/eslin…

// .code-workspace 模式
{
	"settings": {
		"editor.formatOnSave": true,
		"eslint.experimental.useFlatConfig": true,
		"eslint.format.enable": true,
		"editor.codeActionsOnSave": {
			"source.fixAll.eslint": "explicit",
			"source.organizeImports": "never"
		},
		"prettier.enable": false,
		"eslint.rules.customizations": [
			{ "rule": "style/*", "severity": "off" },
			{ "rule": "format/*", "severity": "off" },
			{ "rule": "*-indent", "severity": "off" },
			{ "rule": "*-spacing", "severity": "off" },
			{ "rule": "*-spaces", "severity": "off" },
			{ "rule": "*-order", "severity": "off" },
			{ "rule": "*-dangle", "severity": "off" },
			{ "rule": "*-newline", "severity": "off" },
			{ "rule": "*quotes", "severity": "off" },
			{ "rule": "*semi", "severity": "off" }
		],
		"eslint.validate": [
			"javascript",
			"javascriptreact",
			"typescript",
			"typescriptreact",
			"vue",
			"html",
			"markdown",
			"json",
			"jsonc",
			"yaml",
			"toml",
			"gql",
			"graphql"
		],
	}
}

// .vscode/settings.json 模式
{
  "eslint.experimental.useFlatConfig": true,
  "prettier.enable": false,
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },
  "eslint.rules.customizations": [
    { "rule": "style/*", "severity": "off" },
    { "rule": "format/*", "severity": "off" },
    { "rule": "*-indent", "severity": "off" },
    { "rule": "*-spacing", "severity": "off" },
    { "rule": "*-spaces", "severity": "off" },
    { "rule": "*-order", "severity": "off" },
    { "rule": "*-dangle", "severity": "off" },
    { "rule": "*-newline", "severity": "off" },
    { "rule": "*quotes", "severity": "off" },
    { "rule": "*semi", "severity": "off" }
  ],
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml",
    "toml",
    "gql",
    "graphql",
    "astro"
  ]
}

使用commitlint规范提交

约束git commit提交规范,工程目录执行以下命令。有需要了解husky这里查看。

pnpm install -wD @commitlint/cli @commitlint/config-conventional husky

package.json 添加脚本

// package.json 添加脚本
{
	"scripts": {
	  "preinstall": "npx only-allow pnpm", // 强制使用pnpm
	  "postinstall": "husky install", // 初始化 husky
	  "commitlint": "commitlint --edit"
	}
}

创建commitlint.config.js配置文件

/**
 * @type {import('@commitlint/types').UserConfig}
 */
export default {
    extends: ['@commitlint/config-conventional'],
};

然后执行以下命令,husky 添加commit-msg 钩子。

echo "pnpm commitlint \${1}" > .husky/commit-msg 

控制台执行 git commit -m 'test' ,会发现无法提交成功。

由于添加提交规范,使用起来会比较麻烦,所以需要引入commitizen ,来简化git commit操作。

工程目录执行以下命令。

pnpm install -wD cz-conventional-changelog commitizen

package.json 添加脚本

// package.json 添加脚本
{
	"scripts": {
	  "commit": "cz"
	},
	"config": {
	  "commitizen": {
	    "path": "cz-conventional-changelog"
	  }
	}
}

工程目录执行以下命令。

echo "exec < /dev/tty && pnpm commit --hook || true" > .husky/prepare-commit-msg

控制台执行 git commit ,就会出现可交互的命令行操作。

确保提交的代码风格一致,提交前用eslint检查。

pnpm install -wD lint-staged
// package.json 添加脚本

{
	"scripts": {
	  "lint": "eslint .",
    "lint:fix": "eslint . --fix",
	  "lint-staged": "lint-staged"
	},
	"lint-staged": {
	  "**/*.{js,ts,jsx,tsx,vue}": [
	    "eslint --fix"
	  ]
	}
}
echo 'pnpm lint-staged' > .husky/pre-commit

命令行运行pnpm lint-staged ,只会检测需提交的文件。如果需要全量检测,执行pnpm lint:fix

写了这么多,都是一步一步实践来的,也算是对工作上对技术的总结吧!希望大家使用这个模版开发可以增加研发效率,有更多的时间去专研自己喜欢的东西。

这个模版不限于小程序,可以扩展到其他项目中,主要学习的是编程思维,让技术为自己所用。

也感谢大佬们的开源精神,我也奉上我这套模版的 github.com/gaoyuan1011… 地址,有需求的可以通过 github.com/gaoyuan1011… 联系我,欢迎一起交流技术。

转载自:https://juejin.cn/post/7367554378733879332
评论
请登录