{"id":22375906,"url":"https://github.com/ynzy/vue3-h5-template","last_synced_at":"2025-10-11T16:42:44.454Z","repository":{"id":39347378,"uuid":"340041358","full_name":"ynzy/vue3-h5-template","owner":"ynzy","description":"🎉基于Vue3+TypeScript+ Vue-Cli4.0 ，构建手机端模板脚手架","archived":false,"fork":false,"pushed_at":"2022-01-10T15:26:47.000Z","size":1591,"stargazers_count":211,"open_issues_count":4,"forks_count":84,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-04T20:30:04.313Z","etag":null,"topics":["rem","typescript","typescript-vue","vant","vant-ui","vue-h5","vue3","vue3-h5","wx"],"latest_commit_sha":null,"homepage":"https://vue3-h5-template.vercel.app/","language":"Less","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ynzy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-02-18T12:20:43.000Z","updated_at":"2025-07-29T09:03:26.000Z","dependencies_parsed_at":"2022-07-11T21:31:06.442Z","dependency_job_id":null,"html_url":"https://github.com/ynzy/vue3-h5-template","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ynzy/vue3-h5-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ynzy%2Fvue3-h5-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ynzy%2Fvue3-h5-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ynzy%2Fvue3-h5-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ynzy%2Fvue3-h5-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ynzy","download_url":"https://codeload.github.com/ynzy/vue3-h5-template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ynzy%2Fvue3-h5-template/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279007973,"owners_count":26084369,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-11T02:00:06.511Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rem","typescript","typescript-vue","vant","vant-ui","vue-h5","vue3","vue3-h5","wx"],"created_at":"2024-12-04T21:28:12.098Z","updated_at":"2025-10-11T16:42:44.435Z","avatar_url":"https://github.com/ynzy.png","language":"Less","funding_links":[],"categories":[],"sub_categories":[],"readme":"# vue3-h5-template\n\n基于 Vue3+TypeScript+ Vue-Cli4.0 + vant ui + sass+ rem 适配方案+axios 封装 + jssdk 配置 + vconsole 移动端调试，构建手机端模板脚手架\n\n* 项目地址：[github](https://github.com/ynzy/vue3-h5-template)\n* 掘金地址：[掘金](https://juejin.cn/post/6931630327211229198)\n* 简书地址：[简书](https://www.jianshu.com/p/adb0983830f6)\n\n[查看 demo](https://vue3-h5-template.vercel.app/) 建议手机端查看\n\n### Node 版本要求\n\n`Vue CLI` 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。你可以使用 [nvm](https://github.com/nvm-sh/nvm) 或 [nvm-windows](https://github.com/coreybutler/nvm-windows) 在同一台电脑中管理多个 Node 版本。\n\n本示例 Node.js 12.14.0\n### 项目结构\n```js\nvue-h5-template -- UI 主目录  \n├── public -- 静态资源  \n├ ├── favicon.ico -- 图标  \n├ └── index.html -- 首页  \n├── src -- 源码目录  \n├ ├── api -- 后端交互的接口  \n├ ├── assets -- 静态资源目录\n├ ├ ├── css\n├ ├ ├── index.scss -- 全局通用样式\n├ ├ ├── mixin.scss -- 全局 mixin\n├ ├ └── variables.scss -- 全局变量  \n├ ├── components -- 封装的组件  \n├ ├── config -- 环境配置  \n├ ├── hooks -- vue3 Hooks\n├ ├── model -- 类型声明文件\n├ ├── const -- 放 vue 页面的配置常量  \n├ ├── plugins -- 插件  \n├ ├── route -- VUE 路由  \n├ ├ ├── index -- 路由入口  \n├ ├ └── router.config.js -- 路由表  \n├ ├── store -- VUEX  \n├ ├── utils -- 工具包  \n├ ├ ├── request.js -- axios 封装\n├ ├ └── storage.js -- 本地存储封装\n├ ├── views -- 业务上的 vue 页面  \n├ ├ ├── layouts -- 路由布局页面(是否缓存页面)\n├ ├ ├── tabBar -- 底部菜单页面\n├ ├ └── orther -- 其他页面\n├ ├── App.vue -- 根组件  \n├ ├── main.ts -- 入口 ts  \n├ ├── shims-axios.d.ts -- axios 声明文件  \n├ └── shims-vue.d.ts -- vue 组件声明文件\n├── .env.development -- 开发环境  \n├── .env.production -- 生产环境  \n├── .env.staging -- 测试环境  \n├── .eslintrc.js -- ESLint 配置  \n├── .gitignore -- git 忽略  \n├── .postcssrc.js -- CSS 预处理配置(rem 适配)  \n├── babel.config.js -- barbel 配置入口  \n├── tsconfig.json -- vscode 路径引入配置\n├── package.json -- 依赖管理  \n└── vue.config.js -- vue cli4 的 webpack 配置\n```\n### 启动项目\n\n```bash\n\ngit clone https://github.com/ynzy/vue3-h5-template.git\n\ncd vue3-h5-template\n\nnpm install\n\nnpm run serve\n```\n\n## \u003cspan id=\"top\"\u003e目录\u003c/span\u003e\n* [√配置多环境变量](#env)\n* [√rem 适配方案](#rem)\n* [√VantUI 组件按需加载](#vant)\n* [√Sass 全局样式](#sass)\n* [√适配苹果底部安全距离](#phonex)\n* [√使用 Mock 数据](#mock)\n* [√Axios 封装及接口管理](#axios)\n* [√Vuex 状态管理](#vuex)\n* [√Vue-router](#router)\n* [√Webpack 4 vue.config.js 基础配置](#base)\n* [√配置 alias 别名](#alias)\n* [√配置 proxy 跨域](#proxy)\n* [√配置 打包分析](#bundle)\n* [√externals 引入 cdn 资源](#externals)\n* [√去掉 console.log](#console)\n* [√splitChunks 单独打包第三方模块](#chunks)\n* [√gzip 压缩](#gzip)\n* [√uglifyjs 压缩](#uglifyjs)\n* [√vconsole 移动端调试](#vconsole)\n* [√动态设置 title](#dyntitle)\n* [√本地存储 storage 封装](#storage)\n* [√配置 Jssdk](#jssdk)\n* [√Eslint + Pettier 统一开发规范](#pettier)\n\n### \u003cspan id=\"env\"\u003e✅ 配置多环境变量 \u003c/span\u003e\n\n`package.json` 里的 `scripts` 配置 `serve` `stage` `build`，通过 `--mode xxx` 来执行不同环境\n\n- 通过 `npm run serve` 启动本地 , 执行 `development`\n- 通过 `npm run stage` 启动测试 , 执行 `development`\n- 通过 `npm run prod` 启动开发 , 执行 `development`\n- 通过 `npm run stageBuild` 打包测试 , 执行 `staging`\n- 通过 `npm run build` 打包正式 , 执行 `production`\n\n```javascript\n\"scripts\": {\n  \"serve\": \"vue-cli-service serve --open\",\n  \"stage\": \"cross-env NODE_ENV=dev vue-cli-service serve --mode staging\",\n  \"prod\": \"cross-env NODE_ENV=dev vue-cli-service serve --mode production\",\n  \"stageBuild\": \"vue-cli-service build --mode staging\",\n  \"build\": \"vue-cli-service build\",\n}\n```\n\n##### 配置介绍\n\n\u0026emsp;\u0026emsp;以 `VUE_APP_` 开头的变量，在代码中可以通过 `process.env.VUE_APP_` 访问。  \n\u0026emsp;\u0026emsp;比如,`VUE_APP_ENV = 'development'` 通过`process.env.VUE_APP_ENV` 访问。  \n\u0026emsp;\u0026emsp;除了 `VUE_APP_*` 变量之外，在你的应用代码中始终可用的还有两个特殊的变量`NODE_ENV` 和`BASE_URL`\n在项目根目录中新建`.env.*`\n\n- .env.development 本地开发环境配置\n\n```bash\nNODE_ENV='development'\n# must start with VUE_APP_\nVUE_APP_ENV = 'development'\n\n```\n\n- .env.staging 测试环境配置\n\n```bash\nNODE_ENV='production'\n# must start with VUE_APP_\nVUE_APP_ENV = 'staging'\n```\n\n- .env.production 正式环境配置\n\n```bash\n NODE_ENV='production'\n# must start with VUE_APP_\nVUE_APP_ENV = 'production'\n```\n\n这里我们并没有定义很多变量，只定义了基础的 VUE_APP_ENV `development` `staging` `production`  \n变量我们统一在 `src/config/env.*.ts` 里进行管理。\n\n这里有个问题，既然这里有了根据不同环境设置变量的文件，为什么还要去 config 下新建三个对应的文件呢？  \n**修改起来方便，不需要重启项目，符合开发习惯。**\n\nconfig/index.js\n\n```javascript\nexport interface IConfig {\n\tenv?: string // 开发环境\n\ttitle?: string // 项目title\n\tbaseUrl?: string // 项目地址\n\tbaseApi?: string // api请求地址\n\tAPPID?: string // 公众号appId  一般放在服务器端\n\tAPPSECRET?: string // 公众号appScript 一般放在服务器端\n\t$cdn: string // cdn公共资源路径\n}\n\n// 根据环境引入不同配置 process.env.NODE_ENV\nconst config = require('./env.' + process.env.VUE_APP_ENV)\nmodule.exports = config\n```\n\n并且定义了接口类型，方便我们调用的时候可以自动识别参数\n\n配置对应环境的变量，拿本地环境文件 `env.development.js` 举例，用户可以根据需求修改\n\n```javascript\n// 本地环境配置\nmodule.exports = {\n\ttitle: 'vue-h5-template',\n\tbaseUrl: 'http://localhost:9018', // 项目地址\n\tbaseApi: 'https://test.xxx.com/api', // 本地api请求地址\n\tAPPID: 'xxx',\n\tAPPSECRET: 'xxx'\n}\n```\n\n##### 调用 config\n\n```js\nimport config from '@/config/index'\nsetup() {\n  console.log('环境配置', config)\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"rem\"\u003e✅ rem 适配方案 \u003c/span\u003e\n\n不用担心，项目已经配置好了 `rem` 适配, 下面仅做介绍：\n\nVant 中的样式默认使用`px`作为单位，如果需要使用`rem`单位，推荐使用以下两个工具:\n\n- [postcss-pxtorem](https://github.com/cuth/postcss-pxtorem) 是一款 `postcss` 插件，用于将单位转化为 `rem`\n- [amfe-flexible](https://github.com/cuth/postcss-pxtorem) 用于设置 `rem` 基准值\n\n```js\nyarn add postcss-pxtorem --dev\nyarn add amfe-flexible --save\n```\n\n##### PostCSS 配置\n\n下面提供了一份基本的 `postcss` 配置，可以在此配置的基础上根据项目需求进行修改\n\n```javascript\n// https://github.com/michael-ciniawsky/postcss-load-config\nmodule.exports = {\n\tplugins: {\n\t\tautoprefixer: {\n\t\t\toverrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome \u003e 31', 'ff \u003e 31', 'ie \u003e= 8']\n\t\t},\n\t\t'postcss-pxtorem': {\n\t\t\trootValue: 37.5,\n\t\t\tpropList: ['*']\n\t\t}\n\t}\n}\n```\n\n我采用了`amfe-flexible`进行设置 rem，看 Github 上说这个更好一些，使用哪个自行参考\n\n```js\n// main.ts\n// 移动端适配\nimport 'amfe-flexible'\n```\n\n更多详细信息： [vant](https://youzan.github.io/vant/#/zh-CN/quickstart#jin-jie-yong-fa)\n\n**新手必看，老鸟跳过**\n\n很多小伙伴会问我，适配的问题。\n\n我们知道 `1rem` 等于`html` 根元素设定的 `font-size` 的 `px` 值。Vant UI 设置 `rootValue: 37.5`,你可以看到在 iPhone 6 下\n看到 （`1rem 等于 37.5px`）：\n\n```html\n\u003chtml data-dpr=\"1\" style=\"font-size: 37.5px;\"\u003e\u003c/html\u003e\n```\n\n切换不同的机型，根元素可能会有不同的`font-size`。当你写 css px 样式时，会被程序换算成 `rem` 达到适配。\n\n因为我们用了 Vant 的组件，需要按照 `rootValue: 37.5` 来写样式。\n\n举个例子：设计给了你一张 750px \\* 1334px 图片，在 iPhone6 上铺满屏幕,其他机型适配。\n\n- 当`rootValue: 70` , 样式 `width: 750px;height: 1334px;` 图片会撑满 iPhone6 屏幕，这个时候切换其他机型，图片也会跟着撑\n  满。\n- 当`rootValue: 37.5` 的时候，样式 `width: 375px;height: 667px;` 图片会撑满 iPhone6 屏幕。\n\n也就是 iphone 6 下 375px 宽度写 CSS。其他的你就可以根据你设计图，去写对应的样式就可以了。\n\n当然，想要撑满屏幕你可以使用 100%，这里只是举例说明。\n\n```html\n\u003cimg class=\"image\" src=\"https://imgs.solui.cn/weapp/logo.png\" /\u003e\n\n\u003cstyle\u003e\n\t/* rootValue: 75 */\n\t.image {\n\t\twidth: 750px;\n\t\theight: 1334px;\n\t}\n\t/* rootValue: 37.5 */\n\t.image {\n\t\twidth: 375px;\n\t\theight: 667px;\n\t}\n\u003c/style\u003e\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"vant\"\u003e✅ VantUI 组件按需加载 \u003c/span\u003e\n\n项目采用[Vant 自动按需引入组件 (推荐)](https://youzan.github.io/vant/v3/#/zh-CN/quickstart)下\n面安装插件介绍：\n\n一般来说 ts 使用的是方案二，但是我在用的过程中有一些问题，所以采用了方案一\n\n方案一：\n\n[babel-plugin-import](https://github.com/ant-design/babel-plugin-import) 是一款 `babel` 插件，它会在编译过程中将\n`import` 的写法自动转换为按需引入的方式\n\n#### 安装插件\n\n```bash\nnpm i babel-plugin-import -D\n```\n\n在`babel.config.js` 设置\n\n```javascript\n// 对于使用 babel7 的用户，可以在 babel.config.js 中配置\nconst plugins = [\n\t[\n\t\t'import',\n\t\t{\n\t\t\tlibraryName: 'vant',\n\t\t\tlibraryDirectory: 'es',\n\t\t\tstyle: true\n\t\t},\n\t\t'vant'\n\t]\n]\nmodule.exports = {\n\tpresets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage', corejs: 3 }]],\n\tplugins\n}\n```\n\n方案二：\n[ts-import-plugin](https://github.com/Brooooooklyn/ts-import-plugin)用于 TypeScript 的模块化导入插件\n\n`yarn add ts-import-plugin --dev` 然后在 vue.config.js 中加入\n\n```js\nconst merge = require('webpack-merge')\nconst tsImportPluginFactory = require('ts-import-plugin')\n// * 三方ui在ts下按需加载的实现\nconst mergeConfig = config =\u003e {\n\tconfig.module\n\t\t.rule('ts')\n\t\t.use('ts-loader')\n\t\t.tap(options =\u003e {\n\t\t\toptions = merge(options, {\n\t\t\t\ttranspileOnly: true,\n\t\t\t\tgetCustomTransformers: () =\u003e ({\n\t\t\t\t\tbefore: [\n\t\t\t\t\t\ttsImportPluginFactory({\n\t\t\t\t\t\t\tlibraryName: 'vant',\n\t\t\t\t\t\t\tlibraryDirectory: 'es',\n\t\t\t\t\t\t\tstyle: true\n\t\t\t\t\t\t})\n\t\t\t\t\t]\n\t\t\t\t}),\n\t\t\t\tcompilerOptions: {\n\t\t\t\t\tmodule: 'es2015'\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn options\n\t\t})\n}\n```\n\n#### 使用组件\n\n项目在 `src/plugins/vant.js` 下统一管理组件，用哪个引入哪个，无需在页面里重复引用\n\n```javascript\n// 按需全局引入 vant组件\nimport { App as VM } from 'vue'\nimport { Button, Cell, CellGroup, Icon } from 'vant'\n\nconst plugins = [Button, Icon, Cell, CellGroup]\n\nexport const vantPlugins = {\n\tinstall: function(vm: VM) {\n\t\tplugins.forEach(item =\u003e {\n\t\t\tvm.component(item.name, item)\n\t\t})\n\t}\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"sass\"\u003e✅ Sass 全局样式\u003c/span\u003e\n\n使用`dart-sass`, 安装速度比较快，大概率不会出现安装不成功\n\n每个页面自己对应的样式都写在自己的 .vue 文件之中 `scoped` 它顾名思义给 css 加了一个域的概念。\n\n```html\n\u003cstyle lang=\"scss\"\u003e\n\t/* global styles */\n\u003c/style\u003e\n\n\u003cstyle lang=\"scss\" scoped\u003e\n\t/* local styles */\n\u003c/style\u003e\n```\n\n#### 目录结构\n\nvue-h5-template 所有全局样式都在 `@/src/assets/css` 目录下设置\n\n```bash\n├── assets\n│   ├── css\n│   │   ├── index.scss               # 全局通用样式\n│   │   ├── reset.scss               # 清除浏览器默认样式\n│   │   ├── mixin.scss               # 全局mixin\n│   │   └── variables.scss           # 全局变量\n```\n\nvue.config.js 添加全局样式配置\n\n```json\ncss: {\n\tloaderOptions: {\n\t\tscss: {\n\t\t\t// 向全局sass样式传入共享的全局变量, $src可以配置图片cdn前缀\n\t\t\t// 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders\n\t\t\tprependData: `\n\t\t\t\t@import \"assets/css/mixin.scss\";\n\t\t\t\t@import \"assets/css/variables.scss\";\n\t\t\t\t`\n\t\t\t// $cdn: \"${defaultSettings.$cdn}\";\n\t\t}\n\t}\n},\n```\n\n设置 js 中可以访问 `$cdn`,`.vue` 文件中使用`this.$cdn`访问\n\n```javascript\n// 引入全局样式\nimport '@/assets/css/index.scss'\n\n// 设置 js中可以访问 $cdn\n// 引入cdn\nimport { $cdn } from '@/config'\nVue.prototype.$cdn = $cdn\n```\n\n在 css 和 js 使用\n\n```html\n\u003cscript\u003e\n\tconsole.log(this.$cdn)\n\u003c/script\u003e\n\u003cstyle lang=\"scss\" scoped\u003e\n\t.logo {\n\t\twidth: 120px;\n\t\theight: 120px;\n\t\tbackground: url($cdn+'/weapp/logo.png') center / contain no-repeat;\n\t}\n\u003c/style\u003e\n```\n\n[▲ 回顶部](#top)\n\n#### 自定义 vant-ui 样式\n\n现在我们来说说怎么重写 `vant-ui` 样式。由于 `vant-ui` 的样式我们是在全局引入的，所以你想在某个页面里面覆盖它的样式就不能\n加 `scoped`，但你又想只覆盖这个页面的 `vant` 样式，你就可在它的父级加一个 `class`，用命名空间来解决问题。\n\n```css\n.about-container {\n\t/* 你的命名空间 */\n\t.van-button {\n\t\t/* vant-ui 元素*/\n\t\tmargin-right: 0px;\n\t}\n}\n```\n\n#### 父组件改变子组件样式 深度选择器\n\n当你子组件使用了 `scoped` 但在父组件又想修改子组件的样式可以 通过 `::v-deep` 来实现：\n\n```scss\n\u003cstyle scoped\u003e\n::v-deep .a {\n\t.b { /* ... */ }\n}\n\u003c/style\u003e\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"phonex\"\u003e✅ 适配苹果底部安全距离\u003c/span\u003e\n\nindex.html 的 meta 指定了 viewport-fit=cover\n\n[vant 中自带底部安全距离参数](https://youzan.github.io/vant/v3/#/zh-CN/advanced-usage)\n\n```js\n\u003c!-- 在 head 标签中添加 meta 标签，并设置 viewport-fit=cover 值 --\u003e\n\u003cmeta\n  name=\"viewport\"\n  content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover\"\n/\u003e\n\u003c!-- 开启顶部安全区适配 --\u003e\n\u003cvan-nav-bar safe-area-inset-top /\u003e\n\n\u003c!-- 开启底部安全区适配 --\u003e\n\u003cvan-number-keyboard safe-area-inset-bottom /\u003e\n```\n\n如果不用 vant 中的适配，也可以自己写，我在 scss 中写了通用样式\n\n```scss\n.fixIphonex {\n\tpadding-bottom: $safe-bottom !important;\n\t\u0026::after {\n\t\tcontent: '';\n\t\tposition: fixed;\n\t\tbottom: 0 !important;\n\t\tleft: 0;\n\t\theight: calc(#{$safe-bottom} + 1px);\n\t\twidth: 100%;\n\t\tbackground: #ffffff;\n\t}\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"mock\"\u003e✅ 使用 Mock 数据 \u003c/span\u003e\n\nmock 请求的封装采用的是 [vue-element-admin 的 mock 请求封装](https://panjiachen.github.io/vue-element-admin-site/zh/guide/essentials/mock-api.html#swagger)，直接拿来用就可以了\n\n- mock.js\n\n```js\nconst Mock = require('mockjs')\n\nconst user = require('./user')\n// const role = require('./role')\n// const article = require('./article')\n// const search = require('./remote-search')\n\n// const mocks = [...user, ...role, ...article, ...search]\nconst mocks = [...user]\n// for front mock\n// please use it cautiously, it will redefine XMLHttpRequest,\n// which will cause many of your third-party libraries to be invalidated(like progress event).\nfunction mockXHR() {\n\t// mock patch\n\t// https://github.com/nuysoft/Mock/issues/300\n\tMock.XHR.prototype.proxy_send = Mock.XHR.prototype.send\n\tMock.XHR.prototype.send = function() {\n\t\tif (this.custom.xhr) {\n\t\t\tthis.custom.xhr.withCredentials = this.withCredentials || false\n\n\t\t\tif (this.responseType) {\n\t\t\t\tthis.custom.xhr.responseType = this.responseType\n\t\t\t}\n\t\t}\n\t\tthis.proxy_send(...arguments)\n\t}\n\n\tfunction XHR2ExpressReqWrap(respond) {\n\t\treturn function(options) {\n\t\t\tlet result = null\n\t\t\tif (respond instanceof Function) {\n\t\t\t\tconst { body, type, url } = options\n\t\t\t\t// https://expressjs.com/en/4x/api.html#req\n\t\t\t\tresult = respond({\n\t\t\t\t\tmethod: type,\n\t\t\t\t\tbody: JSON.parse(body),\n\t\t\t\t\tquery: url\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tresult = respond\n\t\t\t}\n\t\t\treturn Mock.mock(result)\n\t\t}\n\t}\n\n\tfor (const i of mocks) {\n\t\tMock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))\n\t}\n}\n\nmodule.exports = {\n\tmocks,\n\tmockXHR\n}\n```\n\n- user.js\n\n```js\nconst tokens = {\n\tadmin: {\n\t\ttoken: 'admin-token'\n\t},\n\teditor: {\n\t\ttoken: 'editor-token'\n\t}\n}\n\nconst users = {\n\t'admin-token': {\n\t\troles: ['admin'],\n\t\tintroduction: 'I am a super administrator',\n\t\tavatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',\n\t\tname: 'Super Admin'\n\t},\n\t'editor-token': {\n\t\troles: ['editor'],\n\t\tintroduction: 'I am an editor',\n\t\tavatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',\n\t\tname: 'Normal Editor'\n\t}\n}\n\nmodule.exports = [\n\t// user login\n\t{\n\t\turl: '/vue-h5/user/login',\n\t\ttype: 'post',\n\t\tresponse: config =\u003e {\n\t\t\tconst { username } = config.body\n\t\t\tconst token = tokens[username]\n\n\t\t\t// mock error\n\t\t\t// if (!token) {\n\t\t\t// \treturn {\n\t\t\t// \t\tcode: 60204,\n\t\t\t// \t\tmessage: 'Account and password are incorrect.'\n\t\t\t// \t}\n\t\t\t// }\n\n\t\t\treturn {\n\t\t\t\tcode: 20000,\n\t\t\t\tdata: token,\n\t\t\t\tmsg: '登录成功'\n\t\t\t}\n\t\t}\n\t},\n\n\t// get user info\n\t{\n\t\turl: '/vue-h5/user/info.*',\n\t\ttype: 'get',\n\t\tresponse: config =\u003e {\n\t\t\tconst { token } = config.query\n\t\t\tconst info = users['admin-token']\n\t\t\t// mock error\n\t\t\t// if (!info) {\n\t\t\t// \treturn {\n\t\t\t// \t\tcode: 50008,\n\t\t\t// \t\tmessage: 'Login failed, unable to get user details.'\n\t\t\t// \t}\n\t\t\t// }\n\n\t\t\treturn {\n\t\t\t\tcode: 20000,\n\t\t\t\tdata: info,\n\t\t\t\tmsg: '登录成功'\n\t\t\t}\n\t\t}\n\t},\n\n\t// user logout\n\t{\n\t\turl: '/vue-h5/user/logout',\n\t\ttype: 'post',\n\t\tresponse: _ =\u003e {\n\t\t\treturn {\n\t\t\t\tcode: 20000,\n\t\t\t\tdata: 'success'\n\t\t\t}\n\t\t}\n\t}\n]\n```\n\n- main.js\n  如果不需要使用，去除掉这段代码就可以了\n\n```js\n// 使用mock数据\nif (config.mock) {\n\tconst { mockXHR } = require('../mock')\n\tmockXHR()\n}\n```\n\n- 接口请求\n\n```js\nonMounted(() =\u003e {\n\taxios\n\t\t.get('/vue-h5/user/info')\n\t\t.then(res =\u003e {\n\t\t\tconsole.log(res)\n\t\t})\n\t\t.catch(err =\u003e {\n\t\t\tconsole.error(err)\n\t\t})\n})\n```\n\n### \u003cspan id=\"axios\"\u003e✅ Axios 封装及接口管理\u003c/span\u003e\n\n`utils/request.js` 封装 axios ,开发者需要根据后台接口做修改。\n\n- `service.interceptors.request.use` 里可以设置请求头，比如设置 `token`\n- `config.hideloading` 是在 api 文件夹下的接口参数里设置，下文会讲\n- `service.interceptors.response.use` 里可以对接口返回数据处理，比如 401 删除本地信息，重新登录\n\n```ts\n/**\n * @description [ axios 请求封装]\n */\nimport store from '@/store'\nimport axios, { AxiosResponse, AxiosRequestConfig } from 'axios'\n// import { Message, Modal } from 'view-design' // UI组件库\nimport { Dialog, Toast } from 'vant'\nimport router from '@/router'\n// 根据环境不同引入不同api地址\nimport config from '@/config'\n\nconst service = axios.create({\n\tbaseURL: config.baseApi + '/vue-h5', // url = base url + request url\n\ttimeout: 5000,\n\twithCredentials: false // send cookies when cross-domain requests\n\t// headers: {\n\t// \t// clear cors\n\t// \t'Cache-Control': 'no-cache',\n\t// \tPragma: 'no-cache'\n\t// }\n})\n\n// Request interceptors\nservice.interceptors.request.use(\n\t(config: AxiosRequestConfig) =\u003e {\n\t\t// 加载动画\n\t\tif (config.loading) {\n\t\t\tToast.loading({\n\t\t\t\tmessage: '加载中...',\n\t\t\t\tforbidClick: true\n\t\t\t})\n\t\t}\n\t\t// 在此处添加请求头等，如添加 token\n\t\t// if (store.state.token) {\n\t\t// config.headers['Authorization'] = `Bearer ${store.state.token}`\n\t\t// }\n\t\treturn config\n\t},\n\t(error: any) =\u003e {\n\t\tPromise.reject(error)\n\t}\n)\n\n// Response interceptors\nservice.interceptors.response.use(\n\tasync (response: AxiosResponse) =\u003e {\n\t\t// await new Promise(resovle =\u003e setTimeout(resovle, 3000))\n\t\tToast.clear()\n\t\tconst res = response.data\n\t\tif (res.code !== 0) {\n\t\t\t// token 过期\n\t\t\tif (res.code === 401) {\n\t\t\t\t// 警告提示窗\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (res.code == 403) {\n\t\t\t\tDialog.alert({\n\t\t\t\t\ttitle: '警告',\n\t\t\t\t\tmessage: res.msg\n\t\t\t\t}).then(() =\u003e {})\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// 若后台返回错误值，此处返回对应错误对象，下面 error 就会接收\n\t\t\treturn Promise.reject(new Error(res.msg || 'Error'))\n\t\t} else {\n\t\t\t// 注意返回值\n\t\t\treturn response.data\n\t\t}\n\t},\n\t(error: any) =\u003e {\n\t\tToast.clear()\n\t\tif (error \u0026\u0026 error.response) {\n\t\t\tswitch (error.response.status) {\n\t\t\t\tcase 400:\n\t\t\t\t\terror.message = '请求错误(400)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 401:\n\t\t\t\t\terror.message = '未授权,请登录(401)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 403:\n\t\t\t\t\terror.message = '拒绝访问(403)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 404:\n\t\t\t\t\terror.message = `请求地址出错: ${error.response.config.url}`\n\t\t\t\t\tbreak\n\t\t\t\tcase 405:\n\t\t\t\t\terror.message = '请求方法未允许(405)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 408:\n\t\t\t\t\terror.message = '请求超时(408)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 500:\n\t\t\t\t\terror.message = '服务器内部错误(500)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 501:\n\t\t\t\t\terror.message = '服务未实现(501)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 502:\n\t\t\t\t\terror.message = '网络错误(502)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 503:\n\t\t\t\t\terror.message = '服务不可用(503)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 504:\n\t\t\t\t\terror.message = '网络超时(504)'\n\t\t\t\t\tbreak\n\t\t\t\tcase 505:\n\t\t\t\t\terror.message = 'HTTP版本不受支持(505)'\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\terror.message = `连接错误: ${error.message}`\n\t\t\t}\n\t\t} else {\n\t\t\tif (error.message == 'Network Error') {\n\t\t\t\terror.message == '网络异常，请检查后重试！'\n\t\t\t}\n\t\t\terror.message = '连接到服务器失败，请联系管理员'\n\t\t}\n\t\tToast(error.message)\n\t\t// store.auth.clearAuth()\n\t\tstore.dispatch('clearAuth')\n\t\treturn Promise.reject(error)\n\t}\n)\n\nexport default service\n```\n\n#### 接口管理\n\n在`src/api` 文件夹下统一管理接口\n\n- 你可以建立多个模块对接接口, 比如 `home.ts` 里是首页的接口这里讲解 `authController.ts`\n- `url` 接口地址，请求的时候会拼接上 `config` 下的 `baseApi`\n- `method` 请求方法\n- `data` 请求参数 `qs.stringify(params)` 是对数据系列化操作\n- `loading` 默认 `false`,设置为 `true` 后，显示 loading ui 交互中有些接口需要让用户感知\n\n```ts\nimport request from '@/utils/request'\nexport interface IResponseType\u003cP = {}\u003e {\n\tcode: number\n\tmsg: string\n\tdata: P\n}\ninterface IUserInfo {\n\tid: string\n\tavator: string\n}\ninterface IError {\n\tcode: string\n}\nexport const fetchUserInfo = () =\u003e {\n\treturn request\u003cIResponseType\u003cIUserInfo\u003e\u003e({\n\t\turl: '/user/info',\n\t\tmethod: 'get',\n\t\tloading: true\n\t})\n}\n```\n\n#### 如何调用\n\n由于`awaitWrap`类型推导很麻烦，所以还是采用 try catch 来捕获错误，既能捕获接口错误，也能捕获业务逻辑错误\n\n```js\nonMounted(async () =\u003e {\n\ttry {\n\t\tlet res = await fetchUserInfo()\n\t\tconsole.log(res)\n\t} catch (error) {\n\t\tconsole.log(error)\n\t}\n})\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"vuex\"\u003e✅ Vuex 状态管理\u003c/span\u003e\n\n目录结构\n\n```bash\n├── store\n│   ├── modules\n│   ├── |── Auth\n│   ├── ├── ├── index.ts\n│   ├── ├── ├── interface.ts\n│   ├── ├── └── types.ts\n│   ├── index.ts\n│   ├── getters.ts\n```\n\n类型定义\n\n- 模块类型\n\ninterface.ts\n\n```ts\nimport { IUserInfo } from '@/api/interface'\n\n/**\n * 用户信息\n */\nexport interface IAuthState {\n\tuserInfo: IUserInfo\n}\n```\n\nindex.ts\n\n```ts\nimport { Module } from 'vuex'\nimport { IGlobalState } from '@/store/index'\nimport { IAuthState } from '@/store/modules/Auth/interface'\nimport * as Types from '@/store/modules/Auth/types'\n\nconst state: IAuthState = {\n\tuserInfo: {}\n}\n\nconst login: Module\u003cIAuthState, IGlobalState\u003e = {\n\tnamespaced: true,\n\tstate,\n\tmutations: {\n\t\t[Types.SAVE_USER_INFO](state, data) {\n\t\t\tstate.userInfo = data\n\t\t}\n\t},\n\tactions: {\n\t\tasync [Types.SAVE_USER_INFO]({ commit }, data) {\n\t\t\treturn commit(Types.SAVE_USER_INFO, data)\n\t\t}\n\t}\n}\n\nexport default login\n```\n\n- 全局 store 类型\n\n将模块类型导入到 index.ts,定义全局类型\n\n```ts\nimport { IAuthState } from './modules/Auth/interface'\n\nexport interface IGlobalState {\n\tauth: IAuthState\n}\n\nconst store = createStore\u003cIGlobalState\u003e({\n\tgetters,\n\tmodules: {\n\t\tauth\n\t}\n})\n\nexport default store\n```\n\n`main.ts` 引入\n\n```javascript\nimport { createApp } from 'vue'\nimport store from './store'\n\nconst app = createApp(App)\napp.use(store)\napp.mount('#app')\n```\n\n使用\n\n```ts\nimport { fetchUserInfo } from '@/api/authController.ts'\nimport { useStore } from 'vuex'\nimport * as Types from '@/store/modules/Auth/types'\nimport { IGlobalState } from '@/store'\n\nexport default defineComponent({\n\tname: 'about',\n\tprops: {},\n\tsetup(props) {\n\t\tconst store = useStore\u003cIGlobalState\u003e()\n\t\tconst userInfo = computed(() =\u003e {\n\t\t\treturn store.state.auth.userInfo\n\t\t})\n\t\tonMounted(async () =\u003e {\n\t\t\ttry {\n\t\t\t\tlet res = await fetchUserInfo()\n\t\t\t\tif (res.code !== 0) return new Error(res.msg)\n\t\t\t\t// Action 通过 store.dispatch 方法触发\n\t\t\t\tstore.dispatch(`auth/${Types.SAVE_USER_INFO}`, res.data)\n\t\t\t} catch (error) {\n\t\t\t\tconsole.log(error)\n\t\t\t}\n\t\t})\n\t\treturn {\n\t\t\tuserInfo\n\t\t}\n\t}\n})\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"router\"\u003e✅ Vue-router \u003c/span\u003e\n\n本案例主要采用 `history` 模式，开发者根据需求修改 `mode` `base`\n\n前往:[vue.config.js 基础配置](#base)\n\n```ts\nimport { createRouter, createWebHistory } from 'vue-router'\nimport { constantRouterMap } from './router.config'\n\nconst router = createRouter({\n\thistory: createWebHistory(process.env.BASE_URL),\n\t// 在按下 后退/前进 按钮时，就会像浏览器的原生表现那样\n\tscrollBehavior(to, from, savedPosition) {\n\t\tif (savedPosition) {\n\t\t\treturn savedPosition\n\t\t} else {\n\t\t\treturn { top: 0 }\n\t\t}\n\t},\n\troutes: constantRouterMap\n})\n\nexport default router\n```\n\n```ts\nimport { RouteRecordRaw } from 'vue-router'\n\nexport const constantRouterMap: Array\u003cRouteRecordRaw\u003e = [\n\t{\n\t\tpath: '/',\n\t\tname: 'Home',\n\t\tcomponent: () =\u003e import('@/views/layouts/index.vue'),\n\t\tredirect: '/home',\n\t\tmeta: {\n\t\t\ttitle: '首页',\n\t\t\tkeepAlive: false\n\t\t},\n\t\tchildren: [\n\t\t\t{\n\t\t\t\tpath: '/home',\n\t\t\t\tname: 'Home',\n\t\t\t\tcomponent: () =\u003e import(/* webpackChunkName: \"tabbar\" */ '@/views/tabBar/home/index.vue'),\n\t\t\t\tmeta: { title: '首页', keepAlive: false, showTab: true }\n\t\t\t},\n\t\t\t{\n\t\t\t\tpath: '/demo',\n\t\t\t\tname: 'Dome',\n\t\t\t\tcomponent: () =\u003e import(/* webpackChunkName: \"tabbar\" */ '@/views/tabBar/dome/index.vue'),\n\t\t\t\tmeta: { title: '首页', keepAlive: false, showTab: true }\n\t\t\t},\n\t\t\t{\n\t\t\t\tpath: '/about',\n\t\t\t\tname: 'About',\n\t\t\t\tcomponent: () =\u003e import(/* webpackChunkName: \"tabbar\" */ '@/views/tabBar/about/index.vue'),\n\t\t\t\tmeta: { title: '关于我', keepAlive: false, showTab: true }\n\t\t\t}\n\t\t]\n\t}\n]\n```\n\n更多:[Vue Router](https://next.router.vuejs.org/zh/index.html)\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"base\"\u003e✅ Webpack 4 vue.config.js 基础配置 \u003c/span\u003e\n\n如果你的 `Vue Router` 模式是 hash\n\n```javascript\npublicPath: './',\n```\n\n如果你的 `Vue Router` 模式是 history 这里的 publicPath 和你的 `Vue Router` `base` **保持一直**\n\n```javascript\npublicPath: '/app/',\n```\n\n```javascript\nconst IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)\n\nmodule.exports = {\n\t// publicPath: './', // 署应用包时的基本 URL。 vue-router hash 模式使用\n\tpublicPath: '/app/', // 署应用包时的基本 URL。  vue-router history模式使用\n\toutputDir: 'dist', //  生产环境构建文件的目录\n\tassetsDir: 'static', //  outputDir的静态资源(js、css、img、fonts)目录\n\tlintOnSave: !IS_PROD,\n\tproductionSourceMap: false, // 如果你不需要生产环境的 source map，可以将其设置为 false 以加速生产环境构建。\n\tdevServer: {\n\t\tport: 9020, // 端口号\n\t\topen: false, // 启动后打开浏览器\n\t\toverlay: {\n\t\t\t//  当出现编译器错误或警告时，在浏览器中显示全屏覆盖层\n\t\t\twarnings: false,\n\t\t\terrors: true\n\t\t}\n\t\t// ...\n\t}\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"alias\"\u003e✅ 配置 alias 别名 \u003c/span\u003e\n\n```javascript\nconst path = require('path')\nconst resolve = dir =\u003e path.join(__dirname, dir)\nconst IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)\n\nmodule.exports = {\n\tchainWebpack: config =\u003e {\n\t\t// 添加别名\n\t\tconfig.resolve.alias\n\t\t\t.set('@', resolve('src'))\n\t\t\t.set('assets', resolve('src/assets'))\n\t\t\t.set('api', resolve('src/api'))\n\t\t\t.set('views', resolve('src/views'))\n\t\t\t.set('components', resolve('src/components'))\n\t}\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"proxy\"\u003e✅ 配置 proxy 跨域 \u003c/span\u003e\n\n如果你的项目需要跨域设置，你需要打来 `vue.config.js` `proxy` 注释 并且配置相应参数\n\n\u003cu\u003e**!!!注意：你还需要将 `src/config/env.development.js` 里的 `baseApi` 设置成 '/'**\u003c/u\u003e\n\n```javascript\nmodule.exports = {\n\tdevServer: {\n\t\t// ....\n\t\tproxy: {\n\t\t\t//配置跨域\n\t\t\t'/api': {\n\t\t\t\ttarget: 'https://test.xxx.com', // 接口的域名\n\t\t\t\t// ws: true, // 是否启用websockets\n\t\t\t\tchangOrigin: true, // 开启代理，在本地创建一个虚拟服务端\n\t\t\t\tpathRewrite: {\n\t\t\t\t\t'^/api': '/'\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n使用 例如: `src/api/home.js`\n\n```javascript\nexport function getUserInfo(params) {\n\treturn request({\n\t\turl: '/api/userinfo',\n\t\tmethod: 'post',\n\t\tdata: qs.stringify(params)\n\t})\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"bundle\"\u003e✅ 配置 打包分析 \u003c/span\u003e\n\n```javascript\nconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin\n\nmodule.exports = {\n\tchainWebpack: config =\u003e {\n\t\t// 打包分析\n\t\tif (IS_PROD) {\n\t\t\tconfig.plugin('webpack-report').use(BundleAnalyzerPlugin, [\n\t\t\t\t{\n\t\t\t\t\tanalyzerMode: 'static'\n\t\t\t\t}\n\t\t\t])\n\t\t}\n\t}\n}\n```\n\n```bash\nnpm run build\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"externals\"\u003e✅ 配置 externals 引入 cdn 资源 \u003c/span\u003e\n\n这个版本 CDN 不再引入，我测试了一下使用引入 CDN 和不使用,不使用会比使用时间少。网上不少文章测试 CDN 速度块，这个开发者可\n以实际测试一下。\n\n另外项目中使用的是公共 CDN 不稳定，域名解析也是需要时间的（如果你要使用请尽量使用同一个域名）\n\n因为页面每次遇到`\u003cscript\u003e`标签都会停下来解析执行，所以应该尽可能减少`\u003cscript\u003e`标签的数量 `HTTP`请求存在一定的开销，100K\n的文件比 5 个 20K 的文件下载的更快，所以较少脚本数量也是很有必要的\n\n暂时还没有研究放到自己的 cdn 服务器上。\n\n```javascript\nconst defaultSettings = require('./src/config/index.js')\nconst name = defaultSettings.title || 'vue mobile template'\nconst IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)\n\n// externals\nconst externals = {\n\tvue: 'Vue',\n\t'vue-router': 'VueRouter',\n\tvuex: 'Vuex',\n\tvant: 'vant',\n\taxios: 'axios'\n}\n// CDN外链，会插入到index.html中\nconst cdn = {\n\t// 开发环境\n\tdev: {\n\t\tcss: [],\n\t\tjs: []\n\t},\n\t// 生产环境\n\tbuild: {\n\t\tcss: ['https://cdn.jsdelivr.net/npm/vant@2.4.7/lib/index.css'],\n\t\tjs: [\n\t\t\t'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',\n\t\t\t'https://cdn.jsdelivr.net/npm/vue-router@3.1.5/dist/vue-router.min.js',\n\t\t\t'https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js',\n\t\t\t'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js',\n\t\t\t'https://cdn.jsdelivr.net/npm/vant@2.4.7/lib/index.min.js'\n\t\t]\n\t}\n}\nmodule.exports = {\n\tconfigureWebpack: config =\u003e {\n\t\tconfig.name = name\n\t\t// 为生产环境修改配置...\n\t\tif (IS_PROD) {\n\t\t\t// externals\n\t\t\tconfig.externals = externals\n\t\t}\n\t},\n\tchainWebpack: config =\u003e {\n\t\t/**\n\t\t * 添加CDN参数到htmlWebpackPlugin配置中\n\t\t */\n\t\tconfig.plugin('html').tap(args =\u003e {\n\t\t\tif (IS_PROD) {\n\t\t\t\targs[0].cdn = cdn.build\n\t\t\t} else {\n\t\t\t\targs[0].cdn = cdn.dev\n\t\t\t}\n\t\t\treturn args\n\t\t})\n\t}\n}\n```\n\n在 public/index.html 中添加\n\n```javascript\n    \u003c!-- 使用CDN的CSS文件 --\u003e\n    \u003c% for (var i in\n      htmlWebpackPlugin.options.cdn\u0026\u0026htmlWebpackPlugin.options.cdn.css) { %\u003e\n      \u003clink href=\"\u003c%= htmlWebpackPlugin.options.cdn.css[i] %\u003e\" rel=\"preload\" as=\"style\" /\u003e\n      \u003clink href=\"\u003c%= htmlWebpackPlugin.options.cdn.css[i] %\u003e\" rel=\"stylesheet\" /\u003e\n    \u003c% } %\u003e\n     \u003c!-- 使用CDN加速的JS文件，配置在vue.config.js下 --\u003e\n    \u003c% for (var i in\n      htmlWebpackPlugin.options.cdn\u0026\u0026htmlWebpackPlugin.options.cdn.js) { %\u003e\n      \u003cscript src=\"\u003c%= htmlWebpackPlugin.options.cdn.js[i] %\u003e\"\u003e\u003c/script\u003e\n    \u003c% } %\u003e\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"console\"\u003e✅ 去掉 console.log \u003c/span\u003e\n\n保留了测试环境和本地环境的 `console.log`\n\n```bash\nnpm i -D babel-plugin-transform-remove-console\n```\n\n在 babel.config.js 中配置\n\n```javascript\n// 获取 VUE_APP_ENV 非 NODE_ENV，测试环境依然 console\nconst IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV)\nconst plugins = [\n\t[\n\t\t'import',\n\t\t{\n\t\t\tlibraryName: 'vant',\n\t\t\tlibraryDirectory: 'es',\n\t\t\tstyle: true\n\t\t},\n\t\t'vant'\n\t]\n]\n// 去除 console.log\nif (IS_PROD) {\n\tplugins.push('transform-remove-console')\n}\n\nmodule.exports = {\n\tpresets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'entry' }]],\n\tplugins\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"chunks\"\u003e✅ splitChunks 单独打包第三方模块\u003c/span\u003e\n\n```javascript\nmodule.exports = {\n\tchainWebpack: config =\u003e {\n\t\tconfig.when(IS_PROD, config =\u003e {\n\t\t\tconfig\n\t\t\t\t.plugin('ScriptExtHtmlWebpackPlugin')\n\t\t\t\t.after('html')\n\t\t\t\t.use('script-ext-html-webpack-plugin', [\n\t\t\t\t\t{\n\t\t\t\t\t\t// 将 runtime 作为内联引入不单独存在\n\t\t\t\t\t\tinline: /runtime\\..*\\.js$/\n\t\t\t\t\t}\n\t\t\t\t])\n\t\t\t\t.end()\n\t\t\tconfig.optimization.splitChunks({\n\t\t\t\tchunks: 'all',\n\t\t\t\tcacheGroups: {\n\t\t\t\t\t// cacheGroups 下可以可以配置多个组，每个组根据test设置条件，符合test条件的模块\n\t\t\t\t\tcommons: {\n\t\t\t\t\t\tname: 'chunk-commons',\n\t\t\t\t\t\ttest: resolve('src/components'),\n\t\t\t\t\t\tminChunks: 3, //  被至少用三次以上打包分离\n\t\t\t\t\t\tpriority: 5, // 优先级\n\t\t\t\t\t\treuseExistingChunk: true // 表示是否使用已有的 chunk，如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了，那么将不会重新生成新的。\n\t\t\t\t\t},\n\t\t\t\t\tnode_vendors: {\n\t\t\t\t\t\tname: 'chunk-libs',\n\t\t\t\t\t\tchunks: 'initial', // 只打包初始时依赖的第三方\n\t\t\t\t\t\ttest: /[\\\\/]node_modules[\\\\/]/,\n\t\t\t\t\t\tpriority: 10\n\t\t\t\t\t},\n\t\t\t\t\tvantUI: {\n\t\t\t\t\t\tname: 'chunk-vantUI', // 单独将 vantUI 拆包\n\t\t\t\t\t\tpriority: 20, // 数字大权重到，满足多个 cacheGroups 的条件时候分到权重高的\n\t\t\t\t\t\ttest: /[\\\\/]node_modules[\\\\/]_?vant(.*)/\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t\tconfig.optimization.runtimeChunk('single')\n\t\t})\n\t}\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"gzip\"\u003e✅ gzip 压缩\u003c/span\u003e\n\n可能会报错，安装低版本\n参考地址[https://www.cnblogs.com/wuzhiquan/p/14179388.html](https://www.cnblogs.com/wuzhiquan/p/14179388.html)\n\n```js\n// * 打包gzip\nconst assetsGzip = config =\u003e {\n\tconfig.plugin('compression-webpack-plugin').use(require('compression-webpack-plugin'), [\n\t\t{\n\t\t\tfilename: '[path].gz[query]',\n\t\t\talgorithm: 'gzip',\n\t\t\ttest: /\\.js$|\\.html$|\\.json$|\\.css/,\n\t\t\tthreshold: 10240, // 只有大小大于该值的资源会被处理 10240\n\t\t\tminRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理\n\t\t\tdeleteOriginalAssets: true // 删除原文件\n\t\t}\n\t])\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"uglifyjs\"\u003e✅ uglifyjs 压缩\u003c/span\u003e\n\n需要注意，使用此插件，需要把 es6 代码转成 es5 代码，此项目没有使用\n\n```js\n// * 代码压缩\nconst codeUglify = config =\u003e {\n\tconfig.plugin('uglifyjs-webpack-plugin').use(require('uglifyjs-webpack-plugin'), [\n\t\t{\n\t\t\tuglifyOptions: {\n\t\t\t\t//生产环境自动删除console\n\t\t\t\tcompress: {\n\t\t\t\t\tdrop_debugger: true,\n\t\t\t\t\tdrop_console: false,\n\t\t\t\t\tpure_funcs: ['console.log']\n\t\t\t\t}\n\t\t\t},\n\t\t\tsourceMap: false,\n\t\t\tparallel: true\n\t\t}\n\t])\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"vconsole\"\u003e✅ vconsole 移动端调试 \u003c/span\u003e\n\n参考地址：https://github.com/AlloyTeam/AlloyLever\n参考地址：https://www.cnblogs.com/liyinSakura/p/9883777.html\n\n```ts\n\u003c!-- MobileConsole --\u003e\n\u003ctemplate\u003e\n\t\u003cteleport to=\"#vconsole\"\u003e\n\t\t\u003cdiv class=\"vc-tigger\" @click=\"toggleVc\"\u003e\u003c/div\u003e\n\t\u003c/teleport\u003e\n\u003c/template\u003e\n\u003cscript lang=\"ts\"\u003e\nimport { defineComponent, onUnmounted, reactive } from 'vue'\nimport VConsole from 'vconsole'\nimport config from '@/config'\nimport { useDOMCreate } from '@/hooks/useDOMCreate'\ninterface IState {\n\tlastClickTime: number\n\tcount: number\n\tlimit: number\n\tvConsole: any\n}\nexport default defineComponent({\n\tname: 'MobileConsole',\n\tprops: {},\n\tsetup() {\n\t\tuseDOMCreate('vconsole')\n\t\tconst state = reactive\u003cIState\u003e({\n\t\t\tlastClickTime: 0,\n\t\t\tcount: 0,\n\t\t\tlimit: ['production', 'prod'].includes(config.env || '') ? 5 : 0,\n\t\t\tvConsole: null\n\t\t})\n\t\tconst hasClass = (obj: HTMLElement | null, cls: string) =\u003e {\n\t\t\treturn obj?.className.match(new RegExp('(\\\\s|^)' + cls + '(\\\\s|$)'))\n\t\t}\n\t\tconst addClass = (obj: HTMLElement | null, cls: string) =\u003e {\n\t\t\tif (!hasClass(obj, cls)) obj?.classList.add(cls)\n\t\t}\n\t\tconst removeClass = (obj: HTMLElement | null, cls: string) =\u003e {\n\t\t\tif (hasClass(obj, cls)) {\n\t\t\t\tobj?.classList.remove(cls)\n\t\t\t}\n\t\t}\n\t\tconst toggleClass = (obj: HTMLElement | null, cls: string) =\u003e {\n\t\t\tif (hasClass(obj, cls)) {\n\t\t\t\tremoveClass(obj, cls)\n\t\t\t} else {\n\t\t\t\taddClass(obj, cls)\n\t\t\t}\n\t\t}\n\t\tconst toggleVc = () =\u003e {\n\t\t\tconst nowTime = new Date().getTime()\n\t\t\tif (nowTime - state.lastClickTime \u003c 3000) {\n\t\t\t\tstate.count++\n\t\t\t} else {\n\t\t\t\tstate.count = 0\n\t\t\t}\n\t\t\tstate.lastClickTime = nowTime\n\t\t\tif (state.count \u003e= state.limit) {\n\t\t\t\tif (!state.vConsole) {\n\t\t\t\t\tstate.vConsole = new VConsole()\n\t\t\t\t}\n\t\t\t\tlet vconDom = document.getElementById('__vconsole')\n\t\t\t\ttoggleClass(vconDom, 'vconsole_show')\n\t\t\t\tstate.count = 0\n\t\t\t}\n\t\t}\n\t\tonUnmounted(() =\u003e {\n\t\t\tstate.vConsole = null\n\t\t})\n\t\treturn {\n\t\t\ttoggleVc\n\t\t}\n\t}\n})\n\u003c/script\u003e\n\u003cstyle lang=\"scss\" scoped\u003e\n.vc-tigger {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: 20px;\n\theight: 20px;\n\tbackground: red;\n}\n\u003c/style\u003e\n\n```\n\n- 在组件中设置暗门，点击几次显示 vconsole\n  - 在 app.vue 中通过 limit 进行设置\n  - 开发测试环境点击一次就可显示\n  - 生产环境点击 5 次\n\n#### teleport\n\n官方文档:[https://v3.cn.vuejs.org/guide/teleport.html](https://v3.cn.vuejs.org/guide/teleport.html)\n\n以前的弹框之类的组件哪里引用，dom 元素就在哪里，它可以帮助我们把这些代码从组件代码中分离开，方便我们更好查看 dom 元素组成\n\nuseDOMCreate 可以帮助我们便捷创建 dom 元素，这样就不需要在 index.html 去创建 teleport 需要的 dom 元素了\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"dyntitle\"\u003e✅ 动态设置 title \u003c/span\u003e\n\n```js\nexport const useDocumentTitle = (title: string) =\u003e {\n\tdocument.title = title\n}\n```\n\nrouter/index.ts 使用\n\n```ts\nrouter.beforeEach((to, from, next) =\u003e {\n\tuseDocumentTitle(to.meta.title)\n\tnext()\n})\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"storage\"\u003e✅ 本地存储 storage 封装 \u003c/span\u003e\n\n案例在：dome/storage/index.vue 下\n\n引用：\n\n```js\nimport { storage } from '@/utils/storage'\n```\n\n调用：\n\n```js\nstorage.set('data', originalData.value)\nstorageData.value = storage.get('data')\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"jssdk\"\u003e✅ 配置 Jssdk \u003c/span\u003e\n\nTODO： 待更新\n\n安装：\n\n```bash\nyarn add weixin-js-sdk\n```\n\n类型声明写在了 model/weixin-js-sdk.d.ts\n\n由于苹果浏览器只识别第一次进入的路由，所以需要先处理下配置使用的 url\n\n- router.ts\n此处的jssdk配置仅供演示，正常业务逻辑需要配合后端去写\n```ts\nimport { isWeChat } from '../utils/index'\nimport { fetchWeChatAuth } from '@/api/WxController'\nimport { getQueryParams, phoneModel } from '@/utils'\nimport store from '@/store'\n\n// 路由开始进入\nrouter.beforeEach((to, from, next) =\u003e {\n  //! 解决ios微信下，分享签名不成功的问题,将第一次的进入的url缓存起来。\n  if (window.entryUrl === undefined) {\n    window.entryUrl = location.href.split('#')[0]\n  }\n\tconst { code } = getQueryParams\u003cIQueryParams\u003e()\n\t\t// 微信浏览器内微信授权登陆\n\t\t// \u0026\u0026 !store.state.auth.userInfo.name\n\t\tif (isWeChat()) {\n\t\t\tif (code) {\n\t\t\t\tstore.commit('auth/STE_ISAUTH', true)\n\t\t\t\tstore.commit('auth/STE_CODE', code)\n\t\t\t}\n\t\t\tif (!store.state.auth.isAuth) {\n\t\t\t\tlocation.href = fetchWeChatAuth()\n\t\t\t}\n\t\t}\n  next()\n})\nrouter.afterEach((to, from, next) =\u003e {\n  let url\n  if (phoneModel() === 'ios') {\n    url = window.entryUrl\n  } else {\n    url = window.location.href\n  }\n\t// 保存url\n  store.commit('link/SET_INIT_LINK', url)\n})\n```\n\nstore/Link\n```ts\nimport { Module } from 'vuex'\nimport { IGlobalState } from '@/store/index'\nimport { ILinkState } from '@/store/modules/Link/interface'\n\nconst state: ILinkState = {\n  initLink: ''\n}\n\nconst login: Module\u003cILinkState, IGlobalState\u003e = {\n  namespaced: true,\n  state,\n  mutations: {\n    ['SET_INIT_LINK'](state, data) {\n      console.log(data)\n      state.initLink = data\n    }\n  },\n  actions: {}\n}\n\nexport default login\n```\n由于window没有entryUrl变量，需要声明文件进行声明\n\ntypings.ts\n```ts\ndeclare interface Window {\n  entryUrl: any\n}\n```\n\n创建 hooks 函数\n\nhooks/useWxJsSdk.ts\n\n每个页面使用jssdk，都需要调用一次useWxJsSdk,然后再使用其他封装的函数\n\n调用：\n\n```ts\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"pettier\"\u003e✅ Eslint + Pettier 统一开发规范 \u003c/span\u003e\n\n参考Typescript的[代码检查](https://ts.xcatliu.com/engineering/lint.html)\n\nVScode 安装 `eslint` `prettier` `vetur` 插件\n\n在文件 `.prettierrc` 里写 属于你的 pettier 规则\n或者`prettier.config.js`\n\n```js\nmodule.exports =  {\n  \"wrap_line_length\": 120,\n  \"wrap_attributes\": \"auto\",\n  \"eslintIntegration\":true,\n  \"overrides\": [\n    {\n      \"files\": \".prettierrc\",\n      \"options\": {\n        \"parser\": \"json\"\n      }\n    }\n  ],\n\t// 一行最多 100 字符\n\tprintWidth: 100,\n\t// 使用 4 个空格缩进\n\ttabWidth: 2,\n\t// 不使用缩进符，而使用空格\n\tuseTabs: false,\n\t// 行尾需要有分号\n\tsemi: true,\n\t// 使用单引号\n\tsingleQuote: true,\n\t// 对象的 key 仅在必要时用引号\n\tquoteProps: 'as-needed',\n\t// jsx 不使用单引号，而使用双引号\n\tjsxSingleQuote: false,\n\t// 末尾不需要逗号\n\ttrailingComma: 'none',\n\t// 大括号内的首尾需要空格\n\tbracketSpacing: true,\n\t// jsx 标签的反尖括号需要换行\n\tjsxBracketSameLine: false,\n\t// 箭头函数，只有一个参数的时候，也需要括号 avoid\n\tarrowParens: 'always',\n\t// 每个文件格式化的范围是文件的全部内容\n\trangeStart: 0,\n\trangeEnd: Infinity,\n\t// 不需要写文件开头的 @prettier\n\trequirePragma: false,\n\t// 不需要自动在文件开头插入 @prettier\n\tinsertPragma: false,\n\t// 使用默认的折行标准 always\n\tproseWrap: 'preserve',\n\t// 根据显示样式决定 html 要不要折行\n\thtmlWhitespaceSensitivity: 'css',\n\t// 换行符使用 lf auto\n\tendOfLine: 'lf'\n}\n```\n.eslintrc.js 配置\n```js\nmodule.exports = {\n  root: true,\n  env: {\n    browser: true,\n    node: true,\n    es6: true\n  },\n  extends: [\n    'plugin:vue/vue3-essential',\n    'eslint:recommended',\n    '@vue/typescript/recommended',\n    '@vue/prettier',\n    '@vue/prettier/@typescript-eslint'\n  ],\n  parserOptions: {\n    ecmaVersion: 2020\n  },\n  rules: {\n    // 禁止使用 var\n    'no-var': 'error',\n    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',\n    '@typescript-eslint/no-empty-function': 0,\n    '@typescript-eslint/no-var-requires': 0,\n    '@typescript-eslint/interface-name-prefix': 0,\n    '@typescript-eslint/no-explicit-any': 0 // TODO\n  }\n};\n\n```\n\nVscode setting.json 设置\n\n```json\n{\n  \"[vue]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[tavascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  // 保存时用eslint格式化\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": true\n  },\n  // 两者会在格式化js时冲突，所以需要关闭默认js格式化程序\n  \"javascript.format.enable\": false,\n  \"typescript.format.enable\": false,\n  \"vetur.format.defaultFormatter.html\": \"none\",\n  // js/ts程序用eslint，防止vetur中的prettier与eslint格式化冲突\n  \"vetur.format.defaultFormatter.js\": \"none\",\n  \"vetur.format.defaultFormatter.ts\": \"none\",\n  \"files.eol\": \"\\n\",\n  \"editor.tabSize\": 2,\n  \"editor.formatOnSave\": true,\n  // \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"eslint.autoFixOnSave\": true,\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    {\n      \"language\": \"typescript\",\n      \"autoFix\": true\n    }\n  ],\n  \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n```\n\n[▲ 回顶部](#top)\n\n# 鸣谢 ​\n\n[vue-h5-template](https://github.com/sunniejs/vue-h5-template)\n[vue-cli4-config](https://github.com/staven630/vue-cli4-config)\n[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fynzy%2Fvue3-h5-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fynzy%2Fvue3-h5-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fynzy%2Fvue3-h5-template/lists"}