REMARK.md 32 KB

REMARK.md

  • 配置和参数详细说明文档
  • 使用详见 README.md

--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--

使用篇

传值

  • 方法作为传递属性: 子组件可以传入一个 method 作为属性, 若未在 props 声明, 可通过 $attrs 访问. 多层级组件可使用 $listener 来抛出事件
  • 子组件中 v-model 优雅地绑定父组件属性: .syncupdate: 结合计算属性. .sync 其实是语法糖, 父组件会自动同步在子组件的 update: 抛出的值

    1. 父组件使用 .sync 来自动接收子组件抛出的 update:. 子组件重新定义一个带有 set / get 的计算属性: set 抛出更新触发 .sync; getprops
    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: 若骨架屏注入, 提示资源加载异常, 将 .envVUE_APP_SKELETON 设置为空字符串即可. (index.html 引入 skeleton.css 可不注释)
  • vue add quasar 添加 quasar 组件库, 选择兼容 IE 11, 依赖和配置会自动注入

  • 底部导航栏体验优化 计算布局容器避免使用 margin

    1. 前言: 钉钉布局底部会留出操作安全区域, 当页面超过一屏时, 底部 TabBar 上下滑动时会移动, 且 web 页面自身的滑动和 webview 自带的滑动体验感极差, 尤其添加了下拉刷新
    2. 方案: 基于 quasarQScrollArea 组件, 其还支持自定义滚动条. 计算出固定位置高度后, 余下部分留给使用 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";
    });
  },
  1. 传值: 若父组件是在 created 内使用 $nextTick,其内部执行会晚于子组件的 mounted 方法。如在 router-view 嵌套的二级路由页面 mounted 晚于父组件 $nextTick
  2. 效果: 页面部分区域滚动, 自定义滚动条, 兼容下拉刷新效果. 高度锁定了 100%, 因此页面滑动 tabBar 不会出现上下移动体验差情况. 若有可能, 通过各平台 jsApi 关闭 webview 的弹性效果
  import { ui as ddUI } from "dingtalk-jsapi";
  mounted() {
    // 锁定滑动, 自定义滑动区域
    ddUI.webViewBounce.disable();
  },

--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--

配置篇

plop

  • 模板库在根目录下 plop-templates, 包含了组件, 页面和 vuex. vue 配置同于 vue.jsonsnippets. vuex 同于其 modules 下配置
  • 配置 plopfile.js 文件, 并在 package.jsonscripts 配置 new, 运行 npm run new 即可选择对应魔板和文件路径名称, 即可生成. 自动添加组件 name

二级路由

  1. mob 配置了 tabBar (使用路由模式: 不需要绑定 active. vant 跳转仅使用二级路由 path 即可), admin 配置了 sideBar, 由 admin.js 路由文件 reduce 处理后加载
  2. 两个方式显示二级路由: childrenpath 前不添加 /, 则使用时需要 一级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. .envstring 化的值, 非合法 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, "/"));
  }
  1. 调用封装的 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-sasssass-laoder 开发依赖, 新建一个后缀为 .scss 而不是 .sass 的文件, 在 main.js 引入, 不是 App.vue
    4. vant-ui 使用 less, 需要下载 lessless-laoder 开发依赖, 配置: loaderOptions 自动导入、main.js 导入主题、.babel.config.js 减少包体积
    5. vant-uimain.jsimport "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.stylquasar 通过配置后删除)

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 添加了 MOBADMINUNI 编译条件, 通过 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 无效. colrow 可用, 但不支持分设备

  • 通过 vuex 配置了 config 文件, vuex 状态同步更新, 支持响应式动态绑定, 在 App.vue 全局进行响应

    1. native 是否移动端设备: 在 flexRemresize 响应式更新. 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>
  1. 在设置通用样式控制现显示与隐藏, 若需要为指定自定义 @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
  1. 若需要为指定自定义 @media 重置样式, 将样式写入对应类下即可

    1. 若在指定设备重置样式, 样式重置没有 onlynot, 不同设备类下进行重置, 多个设备取并集即可. 注意权重, 建议直接复制然后修改对应值
    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 classSymbol. Unicode 下若图标需要在 data 传递到标签, 将 &#xe606; 替换为 \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 整理
    2. 方式:不要使用 npm i style-resources-loader —save-dev,使用 vue add style-resources-loader 命令, 按提示选择 css 预编译语言, 配置路径
    3. 注意: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")]
  }
},

--~---~---~---~---~---~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~---~---~---~---~---~--~-----~---~-----~---~---~--