从零开始学习uniapp:进阶篇

发表时间: 2023-02-08 11:11

前面我们已经创的了我们的项目 uniapp-demo —— 具体操作查看 uniapp入门到进阶 一 1、如何使用uniapp创建项目工程 ;接下来我们要在此基础上继续完善我们的工程。

一、uniapp-demo工程文件结构调整及创建

1、配置vite开发环境

在根目录创建build文件夹,用来存放我们的vite插件配置

首先为了方便我们管理我们的pages.json文件,不再每次创建页面时手动新增路由,我们采用动态自动生成pages.json文件

/** * @Author: Mr Chen  陈晓  * @Date: 2023-01-16 14:12:05 * @LastEditTime: 2023-02-08 09:52:03 * @Description: 自动生成路由 pages * @RelyOn: htmlparser2 fs deep-list-dir normalize-path *					picocolors @types/lodash * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import { Parser } from 'htmlparser2'import fs from 'fs'import { deepListDirSync } from 'deep-list-dir'import path from 'path'import c from 'picocolors'import { merge, transform, isObject } from 'lodash'import normallize from 'normalize-path'import AppConfig from '../src/app.config'export const defaultPagesRE = /src[\/\]pages[\/\]((?!.+(component(s)?|static).+).)*\.vue$/export const defaultMetaRE = /\<meta(.|\s)*?(\/\>|\/meta\>)/imexport function MetaParser(str, alias, preset): Record<string, any> {  let attr  let parser = new Parser(    { onopentag: (_, attributes) => (attr = attributes) },    { lowerCaseAttributeNames: false },  )  parser.write(str)  parser.end()  if (!attr) return {}  return replaceKeysDeep(    Object.entries(attr).reduce((style, e: [string, string]) => {      let [name, platform] = e[0].split(':')      const add = o => merge(style, o)      // eslint-disable-next-line no-eval      if (!name) return add({ [platform]: (0, eval)('str =' + e[1]) })       if (!e[1]) return add(preset?.[name] || {})       if (platform) return add({ [platform]: { [name]: e[1] } })       if (name) return add({ [name]: e[1] })    }, {}),    alias,  )}export interface Options {  pagesRE: RegExp  metaRE: RegExp  pagesBasePath: string  configPath: string  alias: { [unikey: string]: string }  pluginName: string  DEBUG: boolean}export default function (options: Partial<Options> = {}) {  let {    pagesRE = defaultPagesRE,    metaRE = defaultMetaRE,    pagesBasePath = 'src/pages',    alias = {},    pluginName = 'uni-meta',    DEBUG = process.env.DEBUG,  } = options  alias = {    weChat: 'mp-weixin',    app: 'app-plus',    title: 'navigationBarTitleText',    ...alias,  }  let pageMeta: { [key: string]: string }  function generateMeta() {    pageMeta = Object.fromEntries(      deepListDirSync(pagesBasePath, { pattern: pagesRE, base: '/' }).map(file => [        normalizePagePath(file),        getMeta(fs.readFileSync(file, 'utf-8')) || '{}',      ]),    )    debug(`pageMeta:`, pageMeta)    const basePath = 'pages'    const META = {      pages: [],      subPackages: [],    }    Object.entries(pageMeta).forEach(([path, style]) => {      style = JSON.parse(style)      let [packageName, ...pageName]: any = path.split('/')      pageName = pageName.join('/')      if (packageName == 'template') {        META['pages'][pageName == 'main/index' ? 'unshift' : 'push']({          path: [basePath, packageName, pageName].join('/'),          style,        })      } else {        const packagePath = [basePath, packageName].join('/')        const sub = META['subPackages'].find(item => item.root == packagePath)        if (!sub)          META['subPackages'].push({ root: packagePath, pages: [{ path: pageName, style }] })        else sub['pages'].push({ path: pageName, style })      }    })    fs.writeFileSync(      'src/pages.json',      '// ⛔ 本文件为了做菜单动态处理动态生成,请勿手动配置,防止手动配置被覆盖,请勿手欠,否则后果自负\n' +        '\n' +        JSON.stringify(merge(META, replaceKeysDeep(AppConfig.pages, alias) || {}), null, 2),    )    debug(`META:`, META)  }  generateMeta()  let server  return [    {      name: 'vite-plugin-' + pluginName,      enforce: 'pre',      configureServer(_server) {        server = _server      },      transform(code, id) {        if (pagesRE.test(normalizePagePathFromBase(id))) {          if (pageMeta[normalizePagePath(id)]) {            let meta            try {              meta = getMeta(code)            } catch (error) {              log(c.red(` ${normalizePagePath(id)} meta信息错误\n` + c.red(error)))            }            if (pageMeta[normalizePagePath(id)] !== (meta || '{}')) {              debug(pageMeta[normalizePagePath(id)], meta)              if (server) server.restart()              else generateMeta()            }          }          code = removeMeta(code)        }        return { code, map: null }      },    },  ]  //移除meta  function removeMeta(code) {    return code.replace(metaRE, '')  }  //获取meta  function getMeta(code) {    let str = code.match(metaRE)?.[0]    if (!str) return    return JSON.stringify(MetaParser(str, alias, AppConfig['preset']))  }  function normalizePagePath(file) {    return normallize(path.relative(pagesBasePath, file)).replace(/\.vue$/, '')  }  function normalizePagePathFromBase(file) {    return normallize(path.relative(process.cwd(), file))  }	  //输出日志  function log(...args) {      }    //debug调试  function debug(...args) {    DEBUG &&      console.log(        console.log("deBug")      )  }}function replaceKeysDeep(obj, keysMap) {  return transform(obj, function (result, value, key) {       let currentKey = keysMap[key] || key     result[currentKey] = isObject(value) ? replaceKeysDeep(value, keysMap) : value   })}

其次:git 版本管理

/** * @Author: Mr Chen  陈晓  * @Date: 2023-01-16 14:28:22 * @LastEditTime: 2023-02-08 10:52:03 * @Description: 打包文件输出版本到public目录 * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import { execSync } from 'child_process'export default function createGitVersion(viteEnv, options = { fileName: 'version.json' }) {  const { fileName } = options  const { VITE_APP_ENV } = viteEnv  return {    name: 'generateGitVersion',    generateBundle() {      try {        const command = 'git log -1 --pretty=format:'        const commandContent = 'git log -3 --pretty=format:%s'        const Branch = execSync(`${command}%d`).toString().trim()        const Hash = execSync(`${command}%H`).toString().trim()        const author = execSync(`${command}%cn`).toString().trim()        const email = execSync(`${command}%ce`).toString().trim()        const content = execSync(`${commandContent}`).toString().trim()        let getNowTime = function () {          let dateTime          let yy = new Date().getFullYear()          let mm = new Date().getMonth() + 1          let dd = new Date().getDate()          let hh = new Date().getHours()          let mf =            new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() : new Date().getMinutes()          let ss =            new Date().getSeconds() < 10 ? '0' + new Date().getSeconds() : new Date().getSeconds()          dateTime = yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mf + ':' + ss          return dateTime        }        const jsonStr = {          name: '模版--' + VITE_APP_ENV,          testurl: '',          url: '',          Branch: `${Branch}`,          Hash: `${Hash}`,          CommitUser: `${author}(${email})`,          CommitContent: `${content}`,          time: getNowTime(),        }        this.emitFile({          type: 'asset',          fileName: fileName,          source: JSON.stringify(jsonStr, null, '\t'),        })      } catch (error) {        console.log(error)      }    },  }}

再次: 重新生成路由页面

/** * @Author: Mr Chen  陈晓  * @Date: 2023-01-16 14:28:22 * @LastEditTime: 2023-02-08 10:12:58 * @Description: 自动生成默认插件模板 * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import path from 'path'import c from 'picocolors'import normallize from 'normalize-path'import { defaultPagesRE } from './vite-plugin-uni-meta'export interface Options {  pagesRE: RegExp  name: string  configFile: string  pagesBasePath: string  configPath: string  pluginName: string  DEBUG: boolean}export default function (options: Partial<Options> = {}) {  let {    pagesRE = defaultPagesRE,    name = 'view',    configFile = 'vite.config.js',    pagesBasePath = 'src/pages',    pluginName = 'uni-provider',    DEBUG = process.env.DEBUG,  } = options  return {    name: 'vite-plugin-' + pluginName,    enforce: 'pre',    transform(code, id) {      id = normalizePagePathFromBase(id)      if (pagesRE.test(normalizePagePathFromBase(id))) {        let startTag = new RegExp(`\<${name}`).test(code)        let endTag = new RegExp(`\<\/${name}`).test(code)        if (startTag && !endTag)          code = code            .replace(new RegExp(`(?<=\<${name}.*?)(\/\>|>.*?\<\/${name}\>)`), '>')            .replace(/([\s\S]*)(\<\/template\>)/, `$1</${name}>\n</template>`)        if (!startTag && !endTag)          code = code            .replace('<template>', `<template>\n<${name}>`)            .replace(/([\s\S]*)(\<\/template\>)/, `$1</${name}>\n</template>`)        debug(c.yellow(id), `startTag: ${startTag}`, `endTag: ${endTag}`)      }      return { code, map: null }    },  }  function normalizePagePath(file) {    return normallize(path.relative(pagesBasePath, file))  }  function normalizePagePathFromBase(file) {    return normallize(path.relative(process.cwd(), file))  }  function log(...args) {    console.log(c.dim(new Date().toLocaleTimeString()), c.bold(c.red(`[${pluginName}]`)), ...args)  }  function debug(...args) {    DEBUG &&      console.log(        c.dim(new Date().toLocaleTimeString()),        c.bold(c.red(`[debug:${pluginName}]`)),        ...args,      )  }}

第四:setup扩展管理

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 10:45:23 * @Description: setup扩展 * @RelyOn: vite-plugin-vue-setup-extend * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import setupExtend from 'vite-plugin-vue-setup-extend'export default function createSetupExtend() {  return setupExtend()}

第五:模块转换

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 10:58:25 * @Description: 生成 vite 插件模板 * @RelyOn: picocolors normalize-path * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import path from 'path'import c from 'picocolors'import normallize from 'normalize-path'export interface Options {  pagesBasePath: string  pluginName: string  excludedFiles: string[]  includedFiles: RegExp[]  excludedAttributes: string[]  excludedTags: string[]  strippedPrefixes: string[]  DEBUG: boolean}const htmlRE = /<template>[\s\S]*<\/template>/gconst elementRE = /<\w[\w:\.$-]*\s((?:'[\s\S]*?'|"[\s\S]*?"|`[\s\S]*?`|\{[\s\S]*?\}|[\s\S]*?)*?)>/gconst valuedAttributeRE =  /([?]|(?!\d|-uniapp|-\d)[a-zA-Z0-9\u00A0-\uFFFF-@_:%-]+)(?:=(["'])([^]*?))?/gconst PREFIX = 'data-'export default function (options: Partial<Options> = {}) {  if (!process.env.UNI_PLATFORM?.startsWith('mp')) return []  let {    pagesBasePath = 'src/pages',    pluginName = 'mp-attr-fix',    excludedFiles = ['node_modules', 'index.html'],    excludedAttributes = ['style', 'class', 'id', 'src', 'v-'],    excludedTags = [      'script',      'style',      'link',      'meta',      'title',      'head',      'html',      'body',      'template',      'Meta',      'sys',      'u-',      'wt-',    ],    includedFiles = [/.vue/],    strippedPrefixes = ['v-bind:', ':'],    DEBUG = process.env.DEBUG,  } = options  return {    name: 'vite-plugin-' + pluginName,    enforce: 'pre',    transform(code: string, id) {      let map = {}      if (excludedFiles.some(e => id.includes(e)) || !includedFiles.some(e => e.test(id)))        return null      let aaa = Array.from(code.match(htmlRE)?.[0]?.matchAll(elementRE) || []).flatMap(match =>        Array.from(          (excludedTags.some(e => match[0].startsWith('<' + e)) ? '' : match[1]).matchAll(            valuedAttributeRE,          ),        ),      )      aaa.forEach(([s, name, _, content = '']) => {        if (          excludedAttributes.some(e => name.includes(e)) ||          name.startsWith(PREFIX) ||          name.startsWith('@') ||          content.includes('/') ||          content.includes('.') ||          content.includes('(')        )          return []        map[s] = content      })      Object.entries(map).forEach(([s, v]) => {        let sPrefix = strippedPrefixes.find(e => s.startsWith(e))        if (sPrefix) return        let suffix = v ? '' : '=""'        debug(s, '=>', `${sPrefix || ''}data-${s.replace(/:/g, '-').replace(sPrefix, '')}${suffix}`)        code = code.replace(          new RegExp(`(?<=\\s)${s}(?=\s|>)`, 'g'),          `${s.replace(/hover:/g, 'hover-')} data-${s            .replace(/hover:/g, 'hover-')            .replace(sPrefix, '')}${suffix}`,        )      })      //   console.log(code)      return { code, map: null }    },  }  function normalizePagePath(file) {    return normallize(path.relative(pagesBasePath, file))  }  function normalizePagePathFromBase(file) {    return normallize(path.relative(process.cwd(), file))  }  function log(...args) {    console.log(c.dim(new Date().toLocaleTimeString()), c.bold(c.red(`[${pluginName}]`)), ...args)  }  function debug(...args) {    DEBUG &&      console.log(        c.dim(new Date().toLocaleTimeString()),        c.bold(c.red(`[debug:${pluginName}]`)),        ...args,      )  }}

第六:单元测

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 11:22:12 * @Description: 单元测试 * @RelyOn: espower-source * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import espower from 'espower-source'import path from 'path'export default function (options = {} as any) {  let { pluginName = 'espower', DEBUG = process.env.DEBUG } = options  return {    name: 'vite-plugin-' + pluginName,    enforce: 'post',    transform(code: string, id) {      if (process.env.NODE_ENV !== 'test') return      if (!/(\/__tests__\/.*|(\.|\/)(test|spec))\.[jt]sx?$/.test(id)) return      code = code.replace('import.meta.hot.accept', 'importmetahotaccept')      code = espower(code, id, {        patterns: [          'assert(value, [message])',          'assert.ok(value, [message])',          'assert.equal(actual, expected, [message])',          'assert.notEqual(actual, expected, [message])',          'assert.strictEqual(actual, expected, [message])',          'assert.notStrictEqual(actual, expected, [message])',          'assert.deepEqual(actual, expected, [message])',          'assert.notDeepEqual(actual, expected, [message])',          'assert.deepStrictEqual(actual, expected, [message])',          'assert.notDeepStrictEqual(actual, expected, [message])',        ],      })      code = code.replace('importmetahotaccept', 'import.meta.hot.accept')      return { code }    },  }}

第七:vite 默认配置

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 11:45:03 * @Description: vite默认配置 * @RelyOn: normalize-path picocolors * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import path from 'path'import c from 'picocolors'import normallize from 'normalize-path'export interface Options {  name: string  configFile: string  pagesBasePath: string  configPath: string  pluginName: string  DEBUG: boolean  map: Record<string, (code: string, id: string) => string | func>}export default function (options: Partial<Options> = {}) {  let {    configFile = 'vite.config.js',    pagesBasePath = 'src/pages',    pluginName = 'define',    map = {},    DEBUG = process.env.DEBUG,  } = options  map = {    [['__', 'filename'].join('')]: (_, id) => `"${path.relative(process.cwd(), id)}"`,    [['__', 'dirname'].join('')]: (_, id) => `"${path.dirname(path.relative(process.cwd(), id))}"`,    ...map,  }  let keys = Object.keys(map).join('|')  return {    name: 'vite-plugin-' + pluginName,    enforce: 'post',    transform: (code: string, id) => {      let arr: func[] = []      code = code.replace(new RegExp(`(${keys})`, 'g'), (org, k) => {        let r = map[k](code, id)        if (typeof r === 'string') return r        else arr.push(r)        return org      })      arr.forEach(e => (code = e(code)))      return code    },  }  function normalizePagePath(file) {    return normallize(path.relative(pagesBasePath, file))  }  function normalizePagePathFromBase(file) {    return normallize(path.relative(process.cwd(), file))  }  function log(...args) {    console.log(c.dim(new Date().toLocaleTimeString()), c.bold(c.red(`[${pluginName}]`)), ...args)  }  function debug(...args) {    DEBUG &&      console.log(        c.dim(new Date().toLocaleTimeString()),        c.bold(c.red(`[debug:${pluginName}]`)),        ...args,      )  }}

第八:插件管理中心。新建index.ts,导入以上插件

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 11:55:16 * @Description: 管理插件中心 * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import uni from '@dcloudio/vite-plugin-uni'import ViteRestart from 'vite-plugin-restart'import AutoImport from 'unplugin-auto-import/vite'import Inspect from 'vite-plugin-inspect'import Unocss from 'unocss/vite'import UniMeta from './vite-plugin-uni-meta'import UniProvider from './vite-plugin-uni-provider'import Espower from './vite-plugin-espower'import GitVersion from './vite-plugin-version'import SetupExtend from './vite-plugin-setup-extend'import ImportsConfig from './imports.config'import Define from './vite-plugin-define'import { visualizer } from 'rollup-plugin-visualizer'function isTest() {  return process.env.NODE_ENV === 'test'}export default function createVitePlugins(viteEnv, isBuild = false) {  const { VITE_APP_ENV } = viteEnv  const vitePlugins = [    Inspect(), //vite分析工具    UniMeta(), //自动生成页面meta信息和路由并注册pages.json    UniProvider(), //自动注册页面全局组件    SetupExtend(), //setup扩展    Unocss(),    ViteRestart({      restart: ['src/pages.js', 'src/app.config.ts'],    }),    AutoImport(ImportsConfig),    isTest() || uni({ vueOptions: { reactivityTransform: true } }),    isTest() && Espower(),    Define(), //添加一些全局变量    visualizer(), //可视化依赖关系  ]  VITE_APP_ENV === 'development' && vitePlugins.push(Espower())  isBuild && vitePlugins.push(GitVersion(viteEnv))  return vitePlugins}

第九:插件入口配置 imports.config

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 11:59:24 * @Description: 插件入口 * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import AutoImport from 'unplugin-auto-import/vite'import * as Hooks from '../src/hooks'const Config: Parameters<typeof AutoImport>[0] = {  imports: [    'vue',    'uni-app',    { '@/app/index': ['app'] },    getImports('@/hooks', Hooks),    { 'power-assert': [['default', 'assert']] },  ],  dts: 'declare/auto-imports.d.ts',}function getImports(path: string, o: Record<string, any>) {  return { [path]: Object.keys(o) }}export default Config

第十:在vite.config.ts引入我们的vite插件

/** * @Author: Mr Chen  陈晓  * @Date: 2023-1-16 14:32:19 * @LastEditTime: 2023-01-08 12:00:00 * @Description: vite.config.ts * 任何傻子都能写出电脑能识别的代码,但我能写出任何人都能看懂的代码 **/import { defineConfig, loadEnv } from 'vite'import path from 'path'//引入vite插件import createVitePlugins from './build/index'export default defineConfig(({ mode, command }) => {  const env = loadEnv(mode, process.cwd())  return {    base: './',    resolve: {      alias: {        '@': path.resolve(__dirname, 'src'),        '@root': path.resolve(__dirname),        lodash: 'lodash-es',      },      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],    },    // vite 代理处理跨域    server: {      port: 9527,      host: true,      open: true,      proxy: {        '/dev-api': {          target: '',           changeOrigin: true,          rewrite: p => p.replace(/^\/dev-api/, ''),        },        '/dataV-api': {          target: '',           changeOrigin: true,          rewrite: p => p.replace(/^\/dataV-api/, ''),        },      },      watch: { ignored: ['**/dist/**'] },    },    plugins: createVitePlugins(env, command === 'build'),    esbuild: { keepNames: true },    optimizeDeps: { exclude: ['lodash-es'] },    test: { globals: true, environment: 'jsdom', deps: { inline: ['@vue'] } },  }})

欢迎大家收藏、点赞和转发