从零开始学习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'] } }, }})
欢迎大家收藏、点赞和转发