|
@@ -0,0 +1,772 @@
|
|
|
+# REMARK.md
|
|
|
+
|
|
|
+- 配置和参数详细说明文档
|
|
|
+- 使用详见 README.md
|
|
|
+
|
|
|
+--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--
|
|
|
+
|
|
|
+_使用篇_
|
|
|
+
|
|
|
+# 传值
|
|
|
+
|
|
|
+- 方法作为传递属性: 子组件可以传入一个 `method` 作为属性, 若未在 `props` 声明, 可通过 `$attrs` 访问. 多层级组件可使用 `$listener` 来抛出事件
|
|
|
+- 子组件中 `v-model` 优雅地绑定父组件属性: `.sync` 和 `update:` 结合计算属性. `.sync` 其实是语法糖, 父组件会自动同步在子组件的 `update:` 抛出的值
|
|
|
+
|
|
|
+ 1. 父组件使用 `.sync` 来自动接收子组件抛出的 `update:`. 子组件重新定义一个带有 `set / get` 的计算属性: `set` 抛出更新触发 `.sync`; `get` 取 `props` 值
|
|
|
+ 2. 需要注意的是父组件必须要传入且用 `.sync` 修饰, 否则子组件 `update:` 没有接收, 链条就断了.
|
|
|
+
|
|
|
+ ```
|
|
|
+ // 通过计算属性优雅地在子组件使用 `v-model` 绑定父组件的值
|
|
|
+ currentPage: {
|
|
|
+ get() {
|
|
|
+ return this.page
|
|
|
+ },
|
|
|
+ set(val) {
|
|
|
+ this.$emit('update:page', val)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+- 若父组件是在 `created` 内使用 `this.$nextTick(() => { })`,其内部执行会晚于子组件的 `mounted` 方法。如在 `router-view` 嵌套的二级路由页面 `mounted` 晚于父组件 `$nextTick`
|
|
|
+
|
|
|
+- `.sync` 处理对象更新
|
|
|
+
|
|
|
+ 1. vue 不检测在子组件内更新对象的属性 (_封装 form 尤其有用_). 注意两个前提, 传入对象需要是 data 下第一层, 对象内部字段需要在父组件中进行声明
|
|
|
+ 2. 可以在子组件抛出一个对象或者集合, 如对 `table` 的封装, 监听多选抛出对选集合数据: _父组件传入空集合, 子组件在多选选择事件内进行抛出_
|
|
|
+
|
|
|
+- 路由声明式传参:`params` 下页面刷新不会丢失,被存储在路径中,并且变量名提前声明了. _路由无参数会进入 404, 且 push 推入路由页面需要使用 name 属性_
|
|
|
+
|
|
|
+- 过滤器支持多参: 首参默认是数据源, 不能修改. 如 _全局过滤器_, 时间格式化和解析, 默认值: `YYYY-MM-DD HH:mm:ss`, 也可自定义传入 `fecha` 支持的格式
|
|
|
+- 计算属性和 watch: 计算属性监听的值没有变化不会重新计算, 且可以同时监听多个属性, 唯一的问题是, 她需要被使用才会触发: 如果有这个需求可以使用 watch,一个 watch 只能监听一个属性
|
|
|
+
|
|
|
+# 使用
|
|
|
+
|
|
|
+- table 组件化: _动态作用域插槽和具名插槽, 分页计算属性和 props 的优雅结合_. 配置表单字段映射, 传入数据源和表头即可实现表单 (使用混入)
|
|
|
+- navBar 组件化: 引入导航头, 自动计算偏移量, 将页面通过插槽载入到 navBar 组件内. 主容器设置 100% height 不设置 overflow, 若需要设置高度 100%以填充背景色, 设置其容器 height 100%即可
|
|
|
+- index 入口: 由 `process.env.VUE_APP_PLATFORM` 集合的值决定: 若仅有 mob 入口为移动端, 否则为 admin. _若兼容两端则 index 根据设备自动跳转对应路由_
|
|
|
+
|
|
|
+# 进阶
|
|
|
+
|
|
|
+- bus
|
|
|
+
|
|
|
+ 1. 配置了事件载体和事件触发 id, _使用 bus 将 key 声明为常量,便于追踪和管理_, 若有频繁触发使用节流控制(并抛出), 如 `flex-rem` 中的 `window.onresize`
|
|
|
+ 2. 使用 `bus.$vm.$on`, 需要在 `destroyed() { bus.$vm.$off(bus.$sel.window_resize); }` 关闭监听. 若仅仅监听一次使用 `bus.$vm.$off`, 抛出事件 `bus.$vm.$emit`
|
|
|
+
|
|
|
+```
|
|
|
+// vue 支持 vue-hooks
|
|
|
+created() {
|
|
|
+ bus.$vm.$on(bus.$sel.window_resize, () => { });
|
|
|
+ // vue 支持 vue-hooks
|
|
|
+ this.$on("hook:destroyed", () => {
|
|
|
+ bus.$vm.$off(bus.$sel.window_resize);
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 优化
|
|
|
+
|
|
|
+ 1. **当数据赋值后不需要响应式, 使用 Object.freeze 冻结**: `this.item = Object.freeze(Object.assign({}, this.item))` _滤掉无用字段可用封装 `optimize` 库_
|
|
|
+ 2. 一次性响应事件使用 v-once
|
|
|
+ 3. 优先 v-show, 尽量不使用 v-if
|
|
|
+ 4. 使用 keep-alive: 需要组件有 name, `plop` 方式下自动生成
|
|
|
+ 5. v-if 不要和 v-for 同级: 若真是存在这个情况, 改用计算属性
|
|
|
+
|
|
|
+- js 和 css 变量共享
|
|
|
+
|
|
|
+ 1. css 到 js: 通过 `css-modules :export` 来实现, 如 `variables.styl` 导出 `:export { colorTheme: $-color-theme }`. 在 js 内 import 文件即可. _css 也可用_
|
|
|
+ 2. js 到 css 最简单方式就是 vue 动态样式: 若需要实时响应, 可和 vuex 进行关联. 如 `App.vue` 内, _通过 vuex 实现了高分屏兼容 @media 布局: 控制显示设备_. **IE 不支持 `css var()`**
|
|
|
+
|
|
|
+# 兼容
|
|
|
+
|
|
|
+- `swiper` 最新版 `^5.3.8` 不支持 `IE10`, 选择兼容版本 `^3.4.2` 版本. 匹配 `vue-awesome-swiper` 版本 `^2.6.7"` (各个 swiper 版本间配置稍有不同)
|
|
|
+- `skeleton`: 若骨架屏注入, 提示资源加载异常, 将 `.env` 中 `VUE_APP_SKELETON` 设置为空字符串即可. (_index.html_ 引入 `skeleton.css` 可不注释)
|
|
|
+- `vue add quasar` 添加 `quasar` 组件库, 选择兼容 `IE 11`, 依赖和配置会自动注入
|
|
|
+
|
|
|
+- 底部导航栏体验优化 **计算布局容器避免使用 `margin`**
|
|
|
+
|
|
|
+ 1. 前言: 钉钉布局底部会留出操作安全区域, 当页面超过一屏时, 底部 `TabBar` 上下滑动时会移动, 且 `web` 页面自身的滑动和 `webview` 自带的滑动体验感极差, _尤其添加了下拉刷新_
|
|
|
+ 2. 方案: 基于 `quasar` 的 `QScrollArea` 组件, 其还支持自定义滚动条. 计算出固定位置高度后, 余下部分留给使用 `QScrollArea` 进行包裹, 是兼容 `QPullToRefresh` 下拉刷新的
|
|
|
+
|
|
|
+ ```
|
|
|
+ <!-- 滚动标签 -->
|
|
|
+ q-scroll-area.main-area(:thumb-style="thumbStyle" :bar-style="barStyle" :style="{height: areaH}")
|
|
|
+ <!-- 高度属性 -->
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ areaH: 0 // 自定义滑动区域, 优化体验
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 自定义滚动区域 Bar 样式
|
|
|
+ thumbStyle() {
|
|
|
+ return {
|
|
|
+ right: "4px",
|
|
|
+ borderRadius: "5px",
|
|
|
+ backgroundColor: "#027be3",
|
|
|
+ width: "5px",
|
|
|
+ opacity: 0.75
|
|
|
+ };
|
|
|
+ },
|
|
|
+ barStyle() {
|
|
|
+ return {
|
|
|
+ right: "2px",
|
|
|
+ borderRadius: "9px",
|
|
|
+ backgroundColor: "#027be3",
|
|
|
+ width: "9px",
|
|
|
+ opacity: 0.2
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async created() {
|
|
|
+ // 因为父组件内使用 `this.$nextTick(() => { })`
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.areaH =
|
|
|
+ document.body.clientHeight -
|
|
|
+ this.tabH -
|
|
|
+ this.$refs.dateDom.clientHeight +
|
|
|
+ "px";
|
|
|
+ });
|
|
|
+ },
|
|
|
+ ```
|
|
|
+
|
|
|
+ 3. 传值: 若父组件是在 `created` 内使用 `$nextTick`,其内部执行会晚于子组件的 `mounted` 方法。如在 `router-view` 嵌套的二级路由页面 `mounted` 晚于父组件 `$nextTick`
|
|
|
+ 4. 效果: 页面部分区域滚动, 自定义滚动条, 兼容下拉刷新效果. 高度锁定了 `100%`, 因此页面滑动 `tabBar` 不会出现上下移动体验差情况. _若有可能, 通过各平台 jsApi 关闭 webview 的弹性效果_
|
|
|
+
|
|
|
+ ```
|
|
|
+ import { ui as ddUI } from "dingtalk-jsapi";
|
|
|
+ mounted() {
|
|
|
+ // 锁定滑动, 自定义滑动区域
|
|
|
+ ddUI.webViewBounce.disable();
|
|
|
+ },
|
|
|
+ ```
|
|
|
+
|
|
|
+--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--
|
|
|
+
|
|
|
+_配置篇_
|
|
|
+
|
|
|
+# plop
|
|
|
+
|
|
|
+- 模板库在根目录下 `plop-templates`, 包含了组件, 页面和 `vuex`. `vue` 配置同于 `vue.json` 的 `snippets`. `vuex` 同于其 `modules` 下配置
|
|
|
+- 配置 `plopfile.js` 文件, 并在 `package.json` 的 `scripts` 配置 `new`, 运行 `npm run new` 即可选择对应魔板和文件路径名称, 即可生成. _自动添加组件 `name`_
|
|
|
+
|
|
|
+# 二级路由
|
|
|
+
|
|
|
+1. `mob` 配置了 `tabBar` (使用路由模式: 不需要绑定 `active`. `vant` 跳转仅使用二级路由 `path` 即可), `admin` 配置了 `sideBar`, 由 `admin.js` 路由文件 reduce 处理后加载
|
|
|
+2. 两个方式显示二级路由: `children` 内 `path` 前不添加 `/`, 则使用时需要 `一级path/二级路由path` 的格式; 若添加了 `/`, 则可直接使用其二级路由的 `path`,
|
|
|
+3. `tabBar.js` 配合 `vant` 使用路由模式: 不需要绑定 `active`. `vant` 跳转仅使用二级路由 `path` 即可. `tab` 的活跃取决于当前绑定的路由, 不使用 `v-model`
|
|
|
+
|
|
|
+# 关于 alias
|
|
|
+
|
|
|
+1. 不为 src 下一级文件夹配置别名: 无太大实际意义; 为按需引入文件和文件夹配置, 如 `@import "~mixin"`, `m-background-image('~image/background-admin.png')`, ...
|
|
|
+2. 关于路径前需要添加 `~` 标识说明: 配置了 webpack 输出路径为 `./`, _样式和图片文件_ 实质上是从编译包读取, 因此在 css 内引入需要添加, **注意: hmtl 内不能使用别名, 未进入编译**
|
|
|
+
|
|
|
+# 关于环境变量
|
|
|
+
|
|
|
+- 前言
|
|
|
+
|
|
|
+ 1. 前言: `webpack` 运行会自动读取环境变量, 基于 `NodeJs`. `vue.config.js` 内使用 _ES5_ 语法不能通过 `import` 读取外部文件, 可用 `require`. (_process 不支持解构取值_)
|
|
|
+ 2. 新建: 在 **根目录** 新建 `.env` 为全局环境变量, 运行 `package.json - scripts` 对应环境命令时, 先加载全局 .env, 再加载对应环境下变量, 同名变量后覆盖前
|
|
|
+ 3. 命令: `serve / dev` 默认读取 `.env.development`, `build` 读取 `env.prod`. 若需要运行自定义环境, 在对应命令后追加 `--mode 对应环境配置文件名称`
|
|
|
+ 4. 高级: `.local` 也可以加在指定模式的环境文件上,比如 .env.development.local 将会在 development 模式下被载入,且被 git 忽略 (配置秘钥会很有用)
|
|
|
+
|
|
|
+- 使用
|
|
|
+
|
|
|
+ 1. 格式: 变量命名格式为 `VUE_APP_*`, 取值通过 `process.evn.VUE_APP_*` 取对应的 key 即可. 修改变量需要重新 run 项目, webpack 重新读取, 否则不生效
|
|
|
+ 2. 说明: 若变量只是在编译期间使用, 不限制格式. 如 vue.config.js 中 `ISANALYZ`. 若需要打到 dist 包内, 则必须符合 `VUE_APP_*`格式
|
|
|
+ 3. 高级: 不同环境文件命令互不能加载: 配置代理 key 时, 需要所有在 build 下文件中赋值 `"", 空字符串`.
|
|
|
+ 1. _未定义变量值为 undefined, 不为空字符串, 支持对象 (BOOL 不会转换, 会以字符串出现)_
|
|
|
+ 2. `.env`是 `string` 化的值, 非合法 `json: string` 可使用 `includes` 函数, 单独效验使用正则
|
|
|
+
|
|
|
+# 关于 proxy
|
|
|
+
|
|
|
+- 配置: 如需要多个代理, `proxy` 为对象类型, 继续添加对应环境变量, 同理配置即可实现
|
|
|
+
|
|
|
+```
|
|
|
+// 本地代理
|
|
|
+devServer: {
|
|
|
+ proxy: {
|
|
|
+ [env.VUE_APP_DEV_PROXY]: {
|
|
|
+ // target字段为空编译报错
|
|
|
+ target: env.VUE_APP_BASE_API,
|
|
|
+ changeOrigin: true,
|
|
|
+ pathRewrite: {
|
|
|
+ ["^" + env.VUE_APP_DEV_PROXY]: ""
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ disableHostCheck: true
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+- 说明: 请求 target 和代理 key 配置在环境变量文件内, 若有关键不提交仓库信息, 可放入对应环境变量的 `.local` 文件内. _只需要修改开发环境文件中 `VUE_APP_DEV_PROXY` 即可_
|
|
|
+
|
|
|
+```
|
|
|
+// 开发代理
|
|
|
+function _devProxy(url) {
|
|
|
+ return process.env.VUE_APP_DEV_PROXY + url;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+# 关于 axios
|
|
|
+
|
|
|
+不直接使用对 `axios` 的封装 `request`, 进行网络请求: thunk 包装调用, 在实际请求文件中导入 `request`. _对 axios 的封装是 create 新的对象_
|
|
|
+
|
|
|
+- 使用
|
|
|
+
|
|
|
+ 1. 请求格式 form/json 取决于 Content-Type:data 在 body, `详见 @/service/request:`. 发起请求不直接引用 `request`, 通过 `thunk` 包装调用
|
|
|
+ 2. service 网络请求, 分文件模块化以管理不同业务请求. `request` 中新建了 axios 对象, 使用通过对应业务文件按需加载 (_不使用 `vue-router` 插件, 不在原型链添加 axios 对象_)
|
|
|
+
|
|
|
+- 拦截器
|
|
|
+
|
|
|
+ 1. 返回 reject: 未被捕获下会在控制台抛出报错: `Uncaught (in promise) Error: 错误信息`, _阻断代码继续执行, 不需要添加额外条件处理_
|
|
|
+ 2. 捕捉 reject: 使用 `try...catch` 语法; 或在调用方法后追加 `.catch(err => {})` 进行捕获 (_被捕获后则控制台不会报错, 注意后续代码执行, 不会阻断代码继续执行_)
|
|
|
+ 3. 实现 catch 捕获: 要注意添加 if (!res) return; (catch 后 await 得到的就是其返回的值, 没有返回即为 undefined). _res 的判断条件取决于 catch 返回抛出的值_
|
|
|
+
|
|
|
+ ```
|
|
|
+ // 当前月事件分布 - 实现 catch 要注意添加 if (!res) return; (catch 后 await 得到的就是其返回的值, 没有返回即为 undefined)
|
|
|
+ async refreshMonthMark(params) {
|
|
|
+ const res = await monthRecordList(params).catch(() => false);
|
|
|
+ if (!res) return;
|
|
|
+ this.dateEvents = res.list.map(item => item.date.replace(/-/gi, "/"));
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+ 4. 调用封装的 `async` 方法, 内部没有返回 `Promise`, 是不支持 `await` 的: 如 `reLogin` 的实现, 内部返回 `Promise`, 通过 `bus` 实现监听. _在 `progress` 重置登录中 `bus` 抛出_
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+ async created () {
|
|
|
+ await this.loginAuth(); // 登录验证: 返回Promise
|
|
|
+ this.inputCalendar(this.today) // 调用封装的async方法, 内部没有返回Promise, 是不支持await的: 若成功执行到reLogin后被挂起
|
|
|
+ await this.relogin() // 重试登录: 返回Promise => 每次登录失效的请求都会触发: 内部做一个throttle, 避免多个连续登录请求
|
|
|
+ this.inputCalendar(this.today) // 若收到登录失效回调: 刷新所有请求接口. 若正常请求成功, 被reLogin的await挂起
|
|
|
+ },
|
|
|
+ destroyed() {
|
|
|
+ bus.$vm.$off(bus.$sel.login_timeout);
|
|
|
+ }
|
|
|
+ // 登录过期重新登录: 每次登录失效的请求都会触发. throttle, 避免多个连续登录请求
|
|
|
+ relogin () {
|
|
|
+ return new Promise(resolve => {
|
|
|
+ const _reLogin = throttle(() => this._ddingLoginAuth(this, resolve), 300);
|
|
|
+ bus.$vm.$on(bus.$sel.login_timeout, () => _reLogin()); // 响应抛出登录失效
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 钉钉免登
|
|
|
+ loginAuth () {
|
|
|
+ if (this.token) return Promise.resolve();
|
|
|
+ return new Promise(resolve => {
|
|
|
+ const _this = this;
|
|
|
+ ready(function () {
|
|
|
+ _this._ddingLoginAuth(_this, resolve)
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 钉钉登录授权
|
|
|
+ _ddingLoginAuth (_this, resolve) {
|
|
|
+ runtime.permission.requestAuthCode({
|
|
|
+ corpId: process.env.VUE_APP_CorpId,
|
|
|
+ onSuccess: function (result) {
|
|
|
+ _this.loginIn({ authCode: result.code }).then(() => resolve());
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ ```
|
|
|
+
|
|
|
+- 提示效果 `@service/progres.js`
|
|
|
+
|
|
|
+ 1. 「服务于通用而不是组件内部 」: 分平台全局提示 UI
|
|
|
+ 2. 组件库优先于平台和设备: quasar 通用最优, 其次是优先移动端
|
|
|
+
|
|
|
+```
|
|
|
+import store from "@/store";
|
|
|
+
|
|
|
+const progress = {};
|
|
|
+const env = process.env;
|
|
|
+const _this = Vue.prototype;
|
|
|
+
|
|
|
+// 组件库优先于平台和设备: quasar 通用最优, 其次是优先移动端
|
|
|
+const quasar = env.VUE_APP_PLATFORM.includes(env.VUE_APP_UI_QUASAR);
|
|
|
+const vant = env.VUE_APP_PRIORITY !== "admin" && store?.state?.config?.native;
|
|
|
+```
|
|
|
+
|
|
|
+# 按需加载
|
|
|
+
|
|
|
+- 配置差异和说明
|
|
|
+
|
|
|
+ 1. 注意: cli4 配置 .babelrc 不识别, 需要在 babel.config.js 配置. _按需引入不需要全局引入 css: 配置成功引入会报错_
|
|
|
+ 2. 编译: `presets`配置使用 cli 自带插件, 不需要使用 `es2015`. 若使用 `npm i babel-preset-es2015 -D` 依赖 (_-D 为 --save-dev, -S 进入 `dependencies`_)
|
|
|
+ 3. 依赖: `element-ui` 依赖于 `babel-plugin-component`, `vant-ui` 依赖于 `babel-plugin-import`. _都是开发依赖_
|
|
|
+ 4. 自动: `quasar` 通过 `vue add quasar` 自动添加. 关于 `quasar.variables.styl` 不能修改文件名称和路径: 因为 `loader` 对路径做了绑定和优化, 需要引入才会生效
|
|
|
+
|
|
|
+**babel.config.js**
|
|
|
+
|
|
|
+```
|
|
|
+module.exports = {
|
|
|
+ presets: ["@vue/cli-plugin-babel/preset"],
|
|
|
+ plugins: [
|
|
|
+ "@babel/syntax-dynamic-import" /*异步加载*/,
|
|
|
+ [
|
|
|
+ "component",
|
|
|
+ {
|
|
|
+ libraryName: "element-ui",
|
|
|
+ styleLibraryName: "theme-chalk"
|
|
|
+ },
|
|
|
+ "element-ui"
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ "import",
|
|
|
+ {
|
|
|
+ libraryName: "vant",
|
|
|
+ libraryDirectory: "es",
|
|
|
+ style: false // 主题和样式重置设置 false 减少包体积
|
|
|
+ },
|
|
|
+ "vant"
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ "transform-imports",
|
|
|
+ {
|
|
|
+ quasar: {
|
|
|
+ transform: "quasar/dist/babel-transforms/imports.js",
|
|
|
+ preventFullImport: true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ ]
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+- 重置主题和样式
|
|
|
+
|
|
|
+ 1. 全局样式: 在 `App.vue` 中导入通用和样式重置: `@import '~@/styles/loader/common'`, `@import '~@/styles/loader/browser.reset'`
|
|
|
+ 2. 移动端使用: `navBar` 组件导入移动端重置: 禁用长按复制粘贴: 在 `navBar` 载入 `@import '~@/styles/loader/navBar.reset'`
|
|
|
+ 3. `element-ui` 使用 `sass`, 需要下载 `node-sass` 和 `sass-laoder` 开发依赖, 新建一个后缀为 _.scss 而不是 .sass 的文件_, 在 main.js 引入, 不是 App.vue
|
|
|
+ 4. `vant-ui` 使用 `less`, 需要下载 `less` 和 `less-laoder` 开发依赖, _配置: `loaderOptions` 自动导入、`main.js` 导入主题、`.babel.config.js` 减少包体积_
|
|
|
+ 5. `vant-ui`在`main.js`中`import "vant/lib/index.less";`引入主题. 且在`vue.config.js`配置`loaderOptions`进行样式重置; 在`babel.config.js`配置`style: false`以减少包体积
|
|
|
+ 6. `quasar` 通过 `vue add quasar` 添加 `quasar` 组件库, 配置手动加载组件, `styles` 预编译, `icons` 使用 `recommend`, 选择兼容 `IE 11`, 依赖和配置会自动注入
|
|
|
+ 7. `quasar` _因不能修改 `quasar.variables.styl` 文件名和路径_, 因此配置主题重置于此文件, 并在 `mian,js` 添加编译以引入 (`quasar.styl` 和 `quasar` 通过配置后删除)
|
|
|
+
|
|
|
+**main.js**
|
|
|
+
|
|
|
+```
|
|
|
+// 按需引入组件库 & 主题和样式重置
|
|
|
+
|
|
|
+/* IFTRUE_ELEMENT */
|
|
|
+import "@/config/loader/element.ui";
|
|
|
+import "@/styles/loader/element.variables"; // 主题和样式重置
|
|
|
+/* FITRUE_ELEMENT */
|
|
|
+
|
|
|
+/* IFTRUE_VANT */
|
|
|
+import "@/config/loader/vant.ui";
|
|
|
+import "vant/lib/index.less"; // 需引入主题: 且在 vue.config.js 配置 loaderOptions 进行样式重置; babel.config.js 配置 style: false 以减少包体积
|
|
|
+/* FITRUE_VANT */
|
|
|
+
|
|
|
+/* IFTRUE_QUASAR */
|
|
|
+import "@/config/loader/quasar.ui";
|
|
|
+import "@/styles/quasar.variables"; // 备注: 不能修改文件名称和路径: loader对路径做了绑定和优化, 需要引入才会生效 (vue add quasar 自动添加)
|
|
|
+/* FITRUE_QUASAR */
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+**vue.config.js**
|
|
|
+
|
|
|
+```
|
|
|
+// vant-ui 主题覆盖 less (条件编译) //
|
|
|
+let vantVariables = {};
|
|
|
+/* IFTRUE_MOB */
|
|
|
+vantVariables = {
|
|
|
+ modifyVars: {
|
|
|
+ hack: `true; @import "~@/styles/loader/vant.variables";`
|
|
|
+ }
|
|
|
+};
|
|
|
+/* FITRUE_MOB */
|
|
|
+
|
|
|
+// es5语法, 导出一份拷贝: 修改需要重新运行
|
|
|
+module.exports = {
|
|
|
+ // css配置: 自动加载 loaderOptions 对 stylus 和 less 都无效
|
|
|
+ css: {
|
|
|
+ less: vantVariables
|
|
|
+ },
|
|
|
+ configureWebpack: config => {
|
|
|
+ // quasar: vue add quasar 自动添加
|
|
|
+ pluginOptions: {
|
|
|
+ quasar: {
|
|
|
+ importStrategy: "manual",
|
|
|
+ rtlSupport: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ transpileDependencies: ["quasar"]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+# 骨架屏
|
|
|
+
|
|
|
+- 依赖 `vue-skeleton-webpack-plugin` 注入骨架屏. 环境变量 `VUE_APP_SKELETON` 控制是否加载骨架屏
|
|
|
+
|
|
|
+```
|
|
|
+// vue骨架屏插件配置
|
|
|
+if (env.VUE_APP_SKELETON) {
|
|
|
+ config.plugins.push(
|
|
|
+ new SkeletonWebpackPlugin({
|
|
|
+ webpackConfig: {
|
|
|
+ entry: {
|
|
|
+ app: resolve("src/config/skeleton/conf")
|
|
|
+ }
|
|
|
+ },
|
|
|
+ minimize: true,
|
|
|
+ quiet: true
|
|
|
+ })
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 可通过 `https://github.com/famanoder/dps` 插件生成骨架屏, 将页面导入到 `@/config/skeleton` 即可; 或使用 `vue-content-loader` 组件库, _mob 可使用 vant 组件_
|
|
|
+
|
|
|
+- 更建议使用全局 loading gif 替换, 将图片打包为 base64 注入到代码: 骨架屏是打包注入, 此时不能访问 window. _根据条件编译, 可选择优先平台. 移动端端优先_
|
|
|
+
|
|
|
+```
|
|
|
+// 骨架屏是打包注入, 此时不能访问 window: 移动端优先
|
|
|
+const compile = process.env.VUE_APP_PRIORITY === "mob";
|
|
|
+
|
|
|
+// 骨架屏: 全局记录挂在方法
|
|
|
+window.mountApp = () => {
|
|
|
+ app.$mount("#app");
|
|
|
+ window.mountApp = null;
|
|
|
+};
|
|
|
+
|
|
|
+// 骨架屏:当js晚于css加载完成,那直接执行渲染
|
|
|
+if (window.STYLE_READY || process.env.VUE_APP_SKELETON) {
|
|
|
+ window.mountApp?.();
|
|
|
+}
|
|
|
+
|
|
|
+/* 骨架屏 (触发器): 服务于 index.hml, 触发自动挂载 vue 对象 -- skeleton.css */
|
|
|
+
|
|
|
+<!-- 骨架屏结束触发自动挂载 vue 对象 -->
|
|
|
+<link rel="preload" href="./skeleton.css" as="style" onload="rel='stylesheet';this.onload=null;loadSkeleton?.();">
|
|
|
+<script>
|
|
|
+ function loadSkeleton() {
|
|
|
+ window.STYLE_READY = true;
|
|
|
+ window.mountApp && window.mountApp();
|
|
|
+ loadSkeleton = null
|
|
|
+ }
|
|
|
+ // Firefox 不支持 preload
|
|
|
+ if (navigator.userAgent.indexOf("Firefox") > 0) {
|
|
|
+ loadSkeleton && loadSkeleton()
|
|
|
+ }
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+# 条件编译
|
|
|
+
|
|
|
+- 前言: `require` 支持 if, `import` 需要使用 `const x = () => import('') / import('').then()`, 返回 `Promise`, 且返回值被 `default` 包裹 . _引入就被 build, 因此更优是条件编译_
|
|
|
+
|
|
|
+- 初衷: 不同平台按需加载对应模块, 定制化情况下减少包体积 (没有条件编译会打入包)
|
|
|
+
|
|
|
+```
|
|
|
+// 配置条件编译: js-conditional-compile-loader 下 npm 和 cnpm 不能混用
|
|
|
+config.module.rules.push({
|
|
|
+ test: /\.js\$/,
|
|
|
+ include: [resolve("src"), resolve("test")],
|
|
|
+ use: [{
|
|
|
+ loader: "js-conditional-compile-loader",
|
|
|
+ options: {
|
|
|
+ isDebug: process.env.NODE_ENV == "development", // optional, this is default
|
|
|
+ // 自定义 flag: process.env.npm_config_flag
|
|
|
+ VANT: env.VUE_APP_PLATFORM.includes(env.VUE_APP_UI_VANT),
|
|
|
+ ELEMENT: env.VUE_APP_PLATFORM.includes(env.VUE_APP_UI_ELEMENT),
|
|
|
+ QUASAR: env.VUE_APP_PLATFORM.includes(env.VUE_APP_UI_QUASAR),
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- 使用: 默认配置了 DEBUG. 在 `vue.config.js` 添加了 `MOB` 和 `ADMIN` 和 `UNI` 编译条件, 通过 `VUE_APP_PLATFORM` 集合控制加载不同组件库 (_配置见 `按需加载`_)
|
|
|
+
|
|
|
+```
|
|
|
+/* IFDEBUG // 开发环境下远程调试vconsole
|
|
|
+import "@/config/loader/vconsole";
|
|
|
+FIDEBUG */
|
|
|
+```
|
|
|
+
|
|
|
+- 路由: 结合路由处理, 可为不同编译条件下包路由和体积不同, 做到真正的按需加载, 保密及性能
|
|
|
+
|
|
|
+```
|
|
|
+const indexAuto = "/index"; // 若有重定向, 使用目标页面, 避免由redirect触发router切换后报错
|
|
|
+const indexMob = "/loginIn";
|
|
|
+const indexAdmin = "/login";
|
|
|
+
|
|
|
+let index = indexAuto;
|
|
|
+
|
|
|
+import mob from "./modules/mob";
|
|
|
+import admin from "./modules/admin";
|
|
|
+import universal from "./modules/universal";
|
|
|
+
|
|
|
+const compile = process.env.VUE_APP_PRIORITY;
|
|
|
+const routes = [];
|
|
|
+
|
|
|
+if (compile === "mob") {
|
|
|
+ index = indexMob;
|
|
|
+ routes.push(...mob);
|
|
|
+} else if (compile === "admin") {
|
|
|
+ index = indexAdmin;
|
|
|
+ routes.push(...admin);
|
|
|
+} else {
|
|
|
+ if (compile === "universal") {
|
|
|
+ routes.push(...universal);
|
|
|
+ } else {
|
|
|
+ routes.push(...mob, ...admin, ...universal);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+# 代码处理
|
|
|
+
|
|
|
+- 前言: npm i uglifyjs-webpack-plugin@1 需要使用低版本, 新版本不识别 const 关键字 - 报 npm audit fix
|
|
|
+- 方案: 使用 TerserPlugin 插件, 配置代码压缩和去除注释和打印
|
|
|
+
|
|
|
+```
|
|
|
+// 代码压缩去除打印: npm i uglifyjs-webpack-plugin@1 需要使用低版本, 新版本不识别 const 关键字 - 报 npm audit fix
|
|
|
+minimizer: [
|
|
|
+ new TerserPlugin({
|
|
|
+ cache: false,
|
|
|
+ sourceMap: false,
|
|
|
+ parallel: true, // 使用多进程并行运行来提高构建速度。默认并发运行数:os.cpus().length - 1。
|
|
|
+ terserOptions: {
|
|
|
+ compress: {
|
|
|
+ drop_debugger: true, // 去除 debugger
|
|
|
+ drop_console: true, // 生产环境自动删除 console
|
|
|
+ dead_code: true // 去除不可达代码: 如 if (false) { ... }
|
|
|
+ },
|
|
|
+ warnings: false
|
|
|
+ }
|
|
|
+ })
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--
|
|
|
+
|
|
|
+_样式篇_
|
|
|
+
|
|
|
+# 关于高分屏导致@media 和栅格布局异常说明和处理
|
|
|
+
|
|
|
+- css 颜色透明度:rgba(0, 0, 0, 0.6) / rgba("#000", 0.6)
|
|
|
+
|
|
|
+- 因为高分屏 `flexRem.js` 设置了 `viewport` 高分屏缩放比例方案. 导致 `@media` 无效, `-none、-xs、-sm、-md、-lg、-xl` 无效. `col` 和 `row` 可用, 但不支持分设备
|
|
|
+
|
|
|
+- **通过 `vuex` 配置了 `config` 文件, `vuex` 状态同步更新, 支持响应式动态绑定, 在 `App.vue` 全局进行响应**
|
|
|
+
|
|
|
+ 1. `native` 是否移动端设备: 在 `flexRem` 的 `resize` 响应式更新. flexRem 窗口 resize 同步更新 `store.commit("config/SET_IS_NATIVE", isNative());`
|
|
|
+ 2. `realWid` 还原 `document.body.clienWidth` 真实的宽度. 在 `setFlexRem` 中同步 `store.commit("config/SET_REAL_WID", document.body.clientWidth * scale);`
|
|
|
+ 3. 通过 `getters` 缓存和响应需要监听变更窗口的临界点, _实现了类 @media 效果_, 响应式尺寸配置型
|
|
|
+
|
|
|
+ ```
|
|
|
+ // computed
|
|
|
+ const getters = {
|
|
|
+ // 缓存realWid是否需要变更布局
|
|
|
+ media: state => {
|
|
|
+ const docWid = state.config.realWid;
|
|
|
+ return {
|
|
|
+ mob: docWid <= 734,
|
|
|
+ pad: docWid > 734 && docWid <= 1080,
|
|
|
+ pc: docWid > 1080
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+ ```
|
|
|
+
|
|
|
+- 通过绑定动态 `class` 对象, 实现类似 `@media` 的效果
|
|
|
+
|
|
|
+ 1. 在页面顶层容器 `App.vue` 上绑定动态样式 `div(id="app" :class="classMedia"). 容器包含关系, 样式自然全局有效
|
|
|
+
|
|
|
+ 2. 将动态样式绑定在计算属性 `computed` 内, 和 `App.vue` 容器绑定. 如下监听 `this.$store.getters` 对于 `@media`计算属性的变化
|
|
|
+
|
|
|
+ ```
|
|
|
+ <script>
|
|
|
+ export default {
|
|
|
+ computed: {
|
|
|
+ // 实现了@media功能, 兼容高分屏方案
|
|
|
+ classMedia() {
|
|
|
+ const { mob, pad, pc } = this.$store.getters.media;
|
|
|
+ const native = this.$store.state.config.native;
|
|
|
+ return {
|
|
|
+ "media-mob": mob,
|
|
|
+ "media-pad": pad,
|
|
|
+ "media-pc": pc,
|
|
|
+ // 移动设备, 监听运行环境是否移动设备
|
|
|
+ "device-native": native,
|
|
|
+ "device-web": !native
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ </script>
|
|
|
+ ```
|
|
|
+
|
|
|
+ 3. 在设置通用样式控制现显示与隐藏, 若需要为指定自定义 `@media` 重置样式, 将样式写入对应类下即可, **注意权重, 建议直接复制修改对应值**
|
|
|
+
|
|
|
+ ```
|
|
|
+ // 通过vuex实现了高分屏兼容@media布局: 控制显示设备, only 当前设备显示, not 当前设备隐藏(_注意理解 `only` 的含义_)
|
|
|
+ .media-mob
|
|
|
+ .only-pad, .only-pc, .not-mob
|
|
|
+ display none
|
|
|
+ .media-pad
|
|
|
+ .only-mob, .only-pc, .not-pad
|
|
|
+ display none
|
|
|
+ .media-pc
|
|
|
+ .only-pad, .only-mob, .not-pc
|
|
|
+ display none
|
|
|
+ // 移动设备, 监听运行环境是否移动设备
|
|
|
+ .device-native
|
|
|
+ .only-web, .not-native
|
|
|
+ display none
|
|
|
+ .device-web
|
|
|
+ .only-native, .not-web
|
|
|
+ display none
|
|
|
+ ```
|
|
|
+
|
|
|
+ 4. 若需要为指定自定义 `@media` 重置样式, 将样式写入对应类下即可
|
|
|
+
|
|
|
+ 1. 若在指定设备重置样式, 样式重置没有 `only` 和 `not`, 不同设备类下进行重置, 多个设备取并集即可. **注意权重, 建议直接复制然后修改对应值**
|
|
|
+ 2. 仅仅在 `mob` 显示: `q-menu.only-mob`, 仅不在 `mob` 显示: `q-tabs.not-mob`. 不要取并集如 `q-tabs.only-pad.only-pc` 可能两个设备下都不会显示. _注意理解 `only` 的含义_
|
|
|
+
|
|
|
+ ```
|
|
|
+ // 通过 vuex 实现了高分屏兼容@media 布局
|
|
|
+ .media-pc, .media-pad
|
|
|
+ .layout
|
|
|
+ &-content
|
|
|
+ &:first-child
|
|
|
+ left 25%
|
|
|
+ &:last-child
|
|
|
+ right 25%
|
|
|
+ .media-mob
|
|
|
+ .layout
|
|
|
+ &-content
|
|
|
+ width 100%
|
|
|
+ ```
|
|
|
+
|
|
|
+ 4. `device-web` 和 `device-native` 检测. 也同样添加了 `only` 和 `not` 处理. 样式重置可以取并集. _参考上面第2 和第1 的说明_
|
|
|
+
|
|
|
+ ```
|
|
|
+ // 仅web环境下添加: q-btn 的 hover 效果
|
|
|
+ .device-web
|
|
|
+ .q-btn
|
|
|
+ transition all 0.5s
|
|
|
+ position relative
|
|
|
+ overflow hidden
|
|
|
+ .q-btn:before
|
|
|
+ content ''
|
|
|
+ position absolute
|
|
|
+ top 0
|
|
|
+ left 0
|
|
|
+ width 100%
|
|
|
+ height 100%
|
|
|
+ z-index 1
|
|
|
+ transition all 0.5s
|
|
|
+ opacity 1
|
|
|
+ transform translate(-105%, 0)
|
|
|
+ border-right-width 1px
|
|
|
+ border-right-style solid
|
|
|
+ border-right-color rgba(0, 0, 0, 1)
|
|
|
+ background-color rgba(0, 0, 0, 0.25)
|
|
|
+ .q-btn:hover:before
|
|
|
+ opacity 0
|
|
|
+ transform translate(0, 0)
|
|
|
+ ```
|
|
|
+
|
|
|
+# 高分屏方案和 rem 适配
|
|
|
+
|
|
|
+1. 针对不同移动机型做高分屏方案, 兼容 pc 端 dpr 为 1, 兼容@media 无效问题
|
|
|
+2. 通过 px2rem 自动将 px 转为 rem. `基准值 100, 基准字号 14`, 可根据设计图实际调整, 配置在 `.env` 文件
|
|
|
+3. 响应 `onsize` 方法动态更新尺寸, 更新方法 `setFlexRem` 做了防抖和节流
|
|
|
+
|
|
|
+```
|
|
|
+// setFlexRem 防抖和节流
|
|
|
+import { debounce, throttle } from "@/utils/optimize";
|
|
|
+import bus from "@/config/event-bus";
|
|
|
+
|
|
|
+// 节流: 初始化和 onresize 加载时
|
|
|
+const initLayout = throttle(isEmit => {
|
|
|
+ setFlexRem();
|
|
|
+ if (isEmit) bus.$vm.$emit(bus.$sel.window_resize); // 响应式更新
|
|
|
+}, 400);
|
|
|
+
|
|
|
+// 初始化
|
|
|
+initLayout();
|
|
|
+// 兼容 onresize 改变窗口全局调用: 防抖
|
|
|
+window.onresize = debounce(() => initLayout(true), 200);
|
|
|
+// 响应式需要调用resize事件以响应加载时更新
|
|
|
+window.onload = () => {
|
|
|
+ bus.$vm.$emit(bus.$sel.window_onload);
|
|
|
+ bus.$vm.$emit(bus.$sel.window_resize);
|
|
|
+};
|
|
|
+
|
|
|
+// bus.$vm.$on(bus.$sel.window_resize, () => { }); // 响应resize
|
|
|
+// bus.$vm.$on(bus.$sel.window_onload, () => { }); // 响应onload
|
|
|
+```
|
|
|
+
|
|
|
+# 关于 iconfont
|
|
|
+
|
|
|
+- iconfont 地址:: 注意在 css 后缀下**format**会被单独格式化为一行, stylus 导入后不识别这个语法 (less OK), 因此不独立配置 iconfont.css 文件
|
|
|
+
|
|
|
+# 关于 stylus @import
|
|
|
+
|
|
|
+1. 存在 css 预编译时,@impor 导入 css 不需要添加 css-loader; **使用 css loaderOptions 而不是 style-resources-loader 实现自动加载**
|
|
|
+2. _使用 stylus 引用绝对路径需要在路径前添加 ~ ,支持别名. 否则路径不识别_ (不建议使用相对路径); **按需加载, 主题已自动加载 (`初次不生效多刷新下`)**
|
|
|
+3. 按需引入 `@import '~styles/extend'`, 若使用 `@extend` 时, _导入必须在 `@extend` 之前_. mixin 不受影响. **@extend 导入后可直接使用已定义样式**
|
|
|
+4. _转义_: `Unicode` 模式下多色图标是无效的, 若有需求可使用 `Font class` 或 `Symbol`. **`Unicode` 下若图标需要在 `data` 传递到标签, 将 `` 替换为 `\ue606` 即可**
|
|
|
+
|
|
|
+# 关于 css loaderOptions
|
|
|
+
|
|
|
+- 配置文件
|
|
|
+
|
|
|
+```
|
|
|
+// css 配置: 自动加载 loaderOptions 对 stylus 和 less 都无效
|
|
|
+css: {
|
|
|
+ extract: true,
|
|
|
+ sourceMap: false,
|
|
|
+ loaderOptions: {
|
|
|
+ // 自动加载仅导入主题: less sass stylus 方式不相同 - 细节详见 README.md
|
|
|
+ stylus: {
|
|
|
+ import: "~@styles/loader/variables.styl"
|
|
|
+ }
|
|
|
+ }
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+# style-resources-loader
|
|
|
+
|
|
|
+- 前言
|
|
|
+
|
|
|
+ 1. 前言:css 配置 `loaderOptions` 对, less sass stylus 方式不相同. 以下是使用 `style-resource-loader`自动加载 css 整理
|
|
|
+ 1. 方式:不要使用 npm i style-resources-loader —save-dev,使用 vue add style-resources-loader 命令, 按提示选择 css 预编译语言, 配置路径
|
|
|
+ 1. 注意:style-resources-loader 是兼容加载 css 的,当作对应的文件传入方式即可(在 css 预编译文件内,支持直接 @import .css 文件,_不需要添加 css-loader_)
|
|
|
+
|
|
|
+- 说明
|
|
|
+
|
|
|
+ 1. css 做了 chunk, 每次打包都会更新, 每一个页面 css 文件都是独立的
|
|
|
+ 2. 在 style 标签内导入非 .css 文件, 是不能直接使用的, 因为 @import 仅仅是导入, 没有预编译, 也不能触发 loader 进行转译.
|
|
|
+ 3. css 预编译 @import 是不能直接使用的, 需要通过继承/混入为当前文件下的类. 或通过 style-resource-loader 自动加载, 对应的 loader 后转译可以使用: _仅用于导入配置变量_
|
|
|
+ 4. 若 vue 页面有 scoped 和非 scoped 的 style 标签, loader 私有化后打入到文件会因为 scoped 原因带出现有 id 和不带 id 的两套样式, 导致 chunk 文件过大, 且大多是无效重复样式
|
|
|
+
|
|
|
+- 使用
|
|
|
+
|
|
|
+ 1. 全局样式写为 css 文件, 如 resetcss 通过 App.vue 内引入, 不加 scoped. 打包仅产生一个 app.css 文件, 且是全局有效的. _reset 其它组件库也使用这种方式_
|
|
|
+ 2. _css 预编译文件按需引入_, 通过混合和继承产生类后使用.
|
|
|
+ 3. 主题即值类型通过 style-resources-loader 自动加载引入, _主题即变量不会触发打包 chunk_
|
|
|
+ 4. 不要在页面内 scoped 和非 scoped 的 style 标签. _最根本的解决方案还是: 1. 自动加载仅导入主题; 2. 全局样式写为.css 在 App.vue 导入; 3. 预编译文件按需要引入_
|
|
|
+
|
|
|
+- 配置
|
|
|
+
|
|
|
+```
|
|
|
+// 自动加载主题: 使用 vue add style-resources-loader 添加插件方式
|
|
|
+pluginOptions: {
|
|
|
+"style-resources-loader": {
|
|
|
+ preProcessor: "stylus",
|
|
|
+ // 自动加载仅导入主题: 支持 .css 文件,不需要添加 css-loader - 细节详见 README.md
|
|
|
+ patterns: [path.resolve(__dirname, "src/theme/index.styl")]
|
|
|
+ }
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--
|