1.核心点

  • 本方案未采用同一套代码去响应各端的方式去开发,个人觉得兼容太麻烦、样式难以精确控制,遂采用 pc 和 移动端 两套代码、一套路由规则的方式去开发
  • 本方案适用于官网类、展示类的项目
  • 本方案在移动端中使用 vw 作为基本单位,使用 postcss-px-to-viewport 实现移动端 px 单位自动转为 vw

2.开撸

2.1 获取当前设备类型,共享给全局使用

  • vuex 中定义名为 device 的 state,用于全局共享当前设备(pc || 移动)
export default new Vuex.Store({
  state: {
    device: 'pc'
  },

  mutations: {
    setDevice (state, data) {
      state.device = data
    }
  }
})
  • App.vue 中使用像素来检测当前所处设备
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
import { throttle } from 'lodash-es'

export default {
  created () {
    // resize节流
    this.resizeChange = throttle(this.resizeChange, 200)
    this.resizeChange()
    window.addEventListener('resize', this.resizeChange, false)
  },

  methods: {
    ...mapMutations(['setDevice']),

    resizeChange () {
      // 判断是否是 pc 或者 移动端,也可以用机型之类的条件来判断,个人觉得用像素更简单快捷
      // 默认设置当屏幕宽度 > 1000 时,为PC端
      if (document.documentElement.clientWidth > 1000) {
        this.setDevice('pc')
      } else {
        // 默认设置当屏幕宽度 <= 1000 时,为移动端
        this.setDevice('m')
      }
    }
  },

  destroyed () {
    window.removeEventListener('resize', this.resizeChange, false)
  }
}
</script>

到目前为止,我们可以通过 vuex 中的 device 拿到当前的设备类型,pc 对应的是 pc 端,m 对应的是移动端

2.2 页面文件结构及关键代码

  • 页面文件结构
    在这里插入图片描述
  • 代码
// index.vue
<template>
  <div class="index">
    <component :is="$store.state.device"></component>
  </div>
</template>

<script>
import m from './device/m.vue'
import pc from './device/pc.vue'

export default {
  name: 'index',
  components: {
    pc,
    m
  }
}
</script>
// device 目录下的 m.vue 或 pc.vue
// m.vue
<template>
  <div class="index-m">
  	<p> 这里没有其他特殊之处,正常写 移动端 和 pc 端代码即可 </p>
  	<p> m.vue 和 pc.vue 没有本质差异,仅修改 类名 或 组件名称后缀为 -pc </p>
  </div>
</template>

<script>
export default {
  name: 'index-m'
}
</script>

<style lang="scss" scoped>
.index-m {
	// 如果是 m.vue,这里正常使用 px 作为基本单位
}
</style>

在首页 index.vue 中,将其所对应的 pc 端代码定义为 pc.vue,对应的 移动端 代码定义为 m.vue,并导入到 index.vue 中,分别命名为 pc 和 m,而后使用动态组件,并使用$store.state.device来控制当前所展示的页面为 pc端 还是 移动端。

2.3 使用 postcss-px-to-viewport 将项目中的 px 单位转换为 vw

截止到目前,我们已经实现了不同设备下(pc端 或 移动端)代码的按需展示,但是有一个问题亟待解决。

众所周知,移动端设备的屏幕大小不一、“千奇百怪”,如果继续使用 px 作为基本单位,最后呈现的效果必然不理想,经过对比,决定使用 vw 来作为移动端的基本单位。

但是我们都知道,一般情况下设计稿的尺寸是固定的 px 单位,在开发时需要将 px 单位转为 vw 来进行开发,但是这一过程略显繁琐,哪怕安装了 vs code 的单位转换插件依旧差强人意,这时,我们便可以请出 postcss-px-to-viewport 来帮我们解决这一问题。

  • 作用
    该插件可以让我们在写 css 代码的时候正常的使用 px,其会将 px 单位自动转换为视口单位 (vw, vh, vmin, vmax)

  • 文档

  • 安装

npm install postcss-px-to-viewport --save-dev
yarn add -D postcss-px-to-viewport
  • 配置
// .postcssrc.js
module.exports = {
  plugins: {
      // 用来给不同的浏览器自动添加相应前缀,如-webkit-,-moz-等等
      autoprefixer: {}, 
      "postcss-px-to-viewport": {
        // 需要转换的单位,默认为"px"
        unitToConvert: 'px',
        viewportWidth: 375,
        // 单位转换后保留的精度
        unitPrecision: 3,
        // 能转化为vw的属性列表
        propList: [
          '*'
        ],
        // 希望使用的视口单位
        viewportUnit: 'vw',
        // 字体使用的视口单位
        fontViewportUnit: 'vw',
        // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
        minPixelValue: 1,
        // 仅转换的文件,这里不要使用 \/ 去匹配文件,不生效😓
        // 这里还有一个坑,就是如果使用了 include 规则的话,正常的版本不生效,可以安装这个
        // "postcss-px-to-viewport": "github:evrone/postcss-px-to-viewport"
        include: [/device\\m.vue/]
      }
  }
};

配置include: [/device\\m.vue/]使我们移动端代码中的 px 单位自动转换为 vw,而 pc 端的代码不受影响。

2.4 使用 node 自动创建符合需求的页面

2.4.1 创建脚本文件
  • 根目录下创建 scripts 及其子文件
    在这里插入图片描述
  • template.js
module.exports = {
  viewTemplate: viewName => {
    return `
<template>
  <div class="${viewName}">
    <component :is="$store.state.device"></component>
  </div>
</template>

<script>
import m from './device/m.vue'
import pc from './device/pc.vue'

export default {
  name: '${viewName}',

  components: {
    pc,
    m
  }
}
</script>
`
  },

// 移动端 页面基本结构
  mTemplate: viewName => {
    return `
<template>
  <div class="${viewName}-m">
    
  </div>
</template>

<script>
export default {
  name: '${viewName}-m'
}
</script>

<style lang="scss" scoped>
.${viewName}-m {
  
}
</style>
`
  },

// pc端 页面基本结构
  pcTemplate: viewName => {
    return `
<template>
  <div class="${viewName}-pc">
    
  </div>
</template>

<script>
export default {
  name: '${viewName}-pc'
}
</script>

<style lang="scss" scoped>
.${viewName}-pc {
  
}
</style>
`
  }
}
  • generateView.js
// generateView.js
const chalk = require('chalk')
const path = require('path')
const fs = require('fs')
const log = message => console.log(chalk.green(`${message}`))
const successLog = message => console.log(chalk.blue(`${message}`))
const errorLog = error => console.log(chalk.red(`${error}`))
const { viewTemplate, mTemplate, pcTemplate } = require('./template')

const generateFile = (path, data) => {
  if (fs.existsSync(path)) {
    errorLog(`${path}文件已存在`)
    return
  }
  return new Promise((resolve, reject) => {
    fs.writeFile(path, data, 'utf8', err => {
      if (err) {
        errorLog(err.message)
        reject(err)
      } else {
        resolve(true)
      }
    })
  })
}

// 创建页面目录
const dotExistDirectoryCreate = (directory) => {
  return new Promise((resolve) => {
    mkdirs(directory, function () {
      resolve(true)
    })
  })
}

// 递归创建目录
const mkdirs = (directory, callback) => {
  var exists = fs.existsSync(directory)
  if (exists) {
    callback()
  } else {
    mkdirs(path.dirname(directory), function () {
      fs.mkdirSync(directory)
      callback()
    })
  }
}

// 获取页面名称
const getViewName = (viewPath) => {
  const arr = viewPath.split('\\')
  return arr[arr.length - 1]
}

log('请输入要生成的页面组件名称(无需添加.vue后缀)、支持深层目录解析(dirName/viewName)、页面将生成在 views/目录下')
let viewName = ''
process.stdin.on('data', async chunk => {
  const inputName = String(chunk).trim().toString()
  // Vue页面组件路径
  let viewPath = path.resolve(__dirname, '../src/views', inputName)
  viewName = getViewName(inputName)
  viewPath = path.resolve(viewPath, viewName + '.vue')
  const viewDirectory = path.dirname(viewPath)

  // 检查界面是否存在
  const hasViewExists = fs.existsSync(viewPath)
  if (hasViewExists) {
    errorLog(`${inputName}页面已存在,请重新输入`)
    return
  }

  try {
    // 1.生成页面目录
    log(`正在生成页面目录 ${viewDirectory}`)
    await dotExistDirectoryCreate(viewDirectory)

    // 2.生成页面子目录
    const sonViewDirectory = path.resolve(viewDirectory, './device')
    log(`正在生成页面子目录 ${sonViewDirectory}`)
    await dotExistDirectoryCreate(sonViewDirectory)

    // 3.生成 m.vue 页面
    const mViewPath = path.resolve(sonViewDirectory, './m.vue')
    log(`正在生成子目录子页面文件 ${mViewPath}`)
    await generateFile(mViewPath, mTemplate(viewName))

    // 4.生成 pc.vue 页面
    const pcViewPath = path.resolve(sonViewDirectory, './pc.vue')
    log(`正在生成子目录子页面文件 ${pcViewPath}`)
    await generateFile(pcViewPath, pcTemplate(viewName))

    // 5.生成页面
    log(`正在生成页面文件 ${viewPath}`)
    await generateFile(viewPath, viewTemplate(viewName))
    successLog('生成成功')
  } catch (e) {
    errorLog(e.message)
  }

  process.stdin.emit('end')
})

process.stdin.on('end', () => {
  log('exit')
  process.exit()
})
  • package.json 中 scripts 节点下添加如下代码
  "new:view": "node ./scripts/generateView"
2.4.2 使用脚本生成符合需求的页面
  • 打开终端,输入 npm run new:view
  • 根据提示,输入要生成的文件名称(支持深层目录解析)
  • 下图中我们创建了一个名为 about 的页面,可以看到,只需要一个指令及文件名,我们便得到了想要的页面结构,极大的提高了开发效率
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

3.持续优化

经过实践发现,pc端 和 移动端 的差别更多体现在页面元素的大小、位置、显隐上,而实际的业务逻辑变化并不大,上述方案中并未抽离页面的 js 代码,导致代码存在冗余,这里我们可以使用 mixins 来优化。

举例来讲,我们可以在首页 index 页面目录下添加一个 mixin.js 文件,将 device/pc.vue 和 device/m.vue 的公共业务逻辑抽离到该文件中,从而实现复用。

因项目较小,我也是后知后觉的才想到可以进一步优化,现如今项目已部署,因此就没有再调整,可以按照上述方案自行优化调整。

Logo

前往低代码交流专区

更多推荐