# vue 购物 WebApp

# 项目概述

# 简介

  项目名 蘑菇购 (opens new window),与一般购物WebApp类似,包括首页、分类、购物车、个人中心、详情。

  项目基于vuevue-routervue-cli3api请求相关部分采用axios,数据部分并非来自服务器,而是本地基于express启动相关数据服务。原因一是网络接口更新快、数据变化大、依赖性高,二是项目本身不大,基于项目启动本机服务灵活性较高,代码安装依赖即可运行,故最终考虑express爬取相关接口数据保存本地。

  状态管理未使用vuex,仅仅是少部分使用vuex功能显得多余,项目大才完全有必要。基于vue.observable能实现部分vuex功能。图片加载部分异步更新DOM采用事件总线进行组件通讯。

  项目第三方开源组件包括better-scroll滚动插件、vue-awesome-swiperswiper轮播组件、normalize.css初始化样式、vue-lazyload懒加载、移动端click300ms延时采用fastclick

  项目难度不高,适合新手练手,此篇仅是练习组件化封装和目录配置的相关记录。

# 预览地址

  ;GitHub (opens new window) / Gitee (opens new window)

# 示例图

# 文件目录配置

├── public
│   ├── favicon.ico
│   ├── index.html
├── server
│   ├── static
│   │   ├── image
│   ├── app.js
│   ├── db.js
│   ├── router.js
├── src
│   ├── api
│   │   ├── home.js
│   │   ├── category.js
│   ├── assets
│   │   ├── iconfont
│   │   ├── img
│   │   ├── placeholder.png
│   ├── components
│   │   ├── BetterScroll
│   │   ├── CheckButton
│   │   ├── IndexBar
│   │   ├── Message
│   │   │   ├── Message.vue
│   │   │   ├── index.js
│   │   ├── Navbar
│   │   ├── Swiper
│   │   ├── SwiperSlide
│   │   ├── Tabbar
│   │   ├── TabbarItem
│   ├── layout
│   │   ├── Tabbar
│   ├── router
│   │   ├── index.js
│   │   ├── routes.js
│   ├── store
│   │   ├── index.js
│   │   ├── vuex.js
│   ├── styles
│   │   ├── index.less
│   ├── utils
│   │   ├── index.js
│   │   ├── request.js
│   ├── views
│   │   ├── home
│   │   ├── category
│   │   ├── cart
│   │   ├── profile
│   │   ├── detail
│   ├── App.vue
│   ├── main.js
│   ├── .env.development
│   ├── package.json
│   ├── README.md
│   ├── vue.config.js

# 初始化

# 脚手架初始化

  初始空脚手架vue-cli3仅配置BabelRouterCSS Pre-processorsless),删除其余业务不相关部分,文件夹部分通过需求逐步新建。

# Tabbar 组件、路由

  项目目前正常运行为空白,先搭建路由相关部分,抽离routes静态数据,同级目录下新增routes.js导出静态数据,index.js引入静态数据。

// router -> index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'hash',
  routes,
})

export default router

// router -> routes.js
export default [
  {
    path: '/',
    redirect: '/home',
  },
  {
    path: '/home',
    name: 'home',
    component: () => import('views/home'),
  },
  ...
]

  项目启动,会发生路由路径加载错误,需要配置文件夹别名,空脚手架不含vue.config.js,需要手动新增。路径src/views修改别名views,其余别名后续会用到,全部配置。

// vue.config.js
const path = require('path')

function resolve(dir) {
  return path.join(__dirname, dir)
}

module.exports = {
  chainWebpack: config => {
    config.resolve.alias
      .set('@', resolve('src'))
      .set('views', resolve('src/views'))
      ...
  },
}

  文件夹别名配置后,懒加载路径下并没有文件。views新增home文件夹,其下新增index.vue,其余文件同理,重启运行。

  此时项目依旧空白,但是homeindex.vue已被重定向,接下来封装TabbarTabbar较为公共,components下新建TabbarTabbarItem,文件夹下均新增index.vueTabbar一般高度49px最为舒适,同时定位屏幕底部,层级高于其他组件。TabbarItem内部引入router-link,组件接收参数参考 vant-ui (opens new window) 并做了部分修改,通过当前routes.path参数和计算属性配置高亮。

// Tabbar -> index.vue
<div class="tabbar">
  <slot />
</div>

// TabbarItem -> index.vue
<div class="tabbar-item">
  <router-link :to="to" tag="div">
    <div class="tabbar-item-icon" :style="{ color: to === path ? activeColor : inactiveColor }">
      <slot name="icon" />
    </div>
    
    <div class="tabbar-item-text" :style="{ color: to === path ? activeColor : inactiveColor }">
      <slot name="text" />
    </div>
  </router-link>
</div>

export default {
  props: {
    to: {
      type: String,
      default: '',
    },
    activeColor: {
      type: String,
      default: '',
    },
    inactiveColor: {
      type: String,
      default: '',
    },
  },
  computed: {
    path() {
      return this.$route.path
    },
  },
}

  公共组件Tabbar封装完成,项目相关Tabbar还未封装及引用。由于项目相关Tabbar有关于项目页面布局。故src下新增layout文件夹,相关布局组件不会太多,不用文件夹下再放index.vue形式。

// layout -> Tabbar.vue
<common-tabbar>
  <common-tabbar-item to="/home" active-color="#ff8198" inactive-color="#555555">
    <i slot="icon" class="iconfont icon-home"></i>
    <span slot="text">首页</span>
  </common-tabbar-item>
  ...
</common-tabbar>

import CommonTabbar from 'components/Tabbar'
import CommonTabbarItem from 'components/TabbarItem'

export default {
  components: {
    CommonTabbar,
    CommonTabbarItem,
  },
}

  图标采用 iconfont (opens new window),官网选择合适的Tabbar图标,下载压缩包解压。assets文件夹下新建iconfont文件夹,引入解压的全部文件。其中demo_index.html关于字体图标使用方式做了详细阐述,iconfont.css需要手动引入,iconfont也是一种字体,最终归结为css样式。src下新建styles文件夹,创建index.lessindex.less放置公共初始化样式,main.js最终引入index.less

├── iconfont
│   ├── demo_index.html
│   ├── demo.css
│   ├── iconfont.css
...

// index.less
@import '~assets/iconfont/iconfont.css';

// main.js
import 'styles/index.less'

  ;Tabbar业务组件封装完成,App.vue引入,项目运行下Tabbar展示屏幕底部,点击Tabbar发生路由跳转和URL更新。

// App.vue
<div id="app">
  <tabbar />

  <router-view />
</div>

import Tabbar from 'layout/Tabbar'

export default {
  components: { Tabbar },
}

# 样式、页面标题、图标初始化

  路由重定向至home页面,发现body存在margin,安装normalize.cssmian.js引入。

// 安装
npm i normalize.css --save

// main.js
import 'normalize.css'

  ;styles文件夹下index.less,初始化htmlbody#app高度,删除App.vue相关样式。

html,
body,
#app {
  height: 100%;
}

  路由添加导航守卫router.beforeEach,用于初始化页面标题,但是目标路由to并不含有meta.title,修改routes.js,其余同理,正确运行页面标题切换。替换publicfavicon.ico,项目刷新显示图标。

// router -> index.js
router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  next()
})

// router -> routes.js
{
  path: '/home',
  name: 'home',
  meta: {
    title: '首页',
  },
  component: () => import('views/home'),
}

  ;NavBar也是一般较通用组件,components新建NavBar,组件开放插槽,一般高度44px最为舒适,路由页面、详情均使用,组件传值background-colorhome页引入,页面引入组件顺序遵循引入公共组件、定制组件、公共js、定制js

# 数据服务

# Express

  项目目前可实现路由跳转,相关api以及数据还未准备。网络接口常更新、数据不稳定,采用expresssuperagent爬取保存接口数据,爬虫crawler参考其他文章。大致拆分项目需要用到的后端接口,首页轮播图、特色、推荐、详情、列表、分类等,项目新建serve文件夹。image保存数据图片。

├── serve
│   ├── static
│   │   ├── image
│   ├── app.js
│   ├── db.js
│   ├── router.js
│   ├── const.js
│   ├── utils.js

  ;app.js启动数据服务,开放静态static文件夹,映射/staticserve/static

app.use('/static/', express.static('./serve/static/'))

  ;db.js本地数据库,baseURL为本机局域网ip,便于移动端访问本机数据,也方便调试。项目之前使用ipconfig手动输入的方式,这种方式不免显得繁琐,数据服务一启动便与项目没有实际关联性,没有必要再去修改一次ip地址。故使用os模块动态获取本机局域网ip,当然此种方式如若PC端访问图片失败,大概率是动态获取ip部分有误,注释相关代码,通过上一种方式修改ip即可。

const os = require('os')
const interfaces = os.networkInterfaces()
const port = 3000
const baseURL = 'http://127.0.0.1:3000'

for (const key of Object.keys(interfaces)) {
  const el = interfaces[key].find(el => el.family === 'IPv4' && el.address !== '127.0.0.1')

  el && (baseURL = `http://${el.address}:${port}`)
}

  ;router.js后端路由部分,由于业务相关接口不是特别多,不用router再去分级,也不存在post相关请求,不需要额外安装body-parser

const db = require('./db')
const { SUCCESS_CODE } = require('./const')

router.get('/api/getBann', (req, res) => {
  res.json({
    code: SUCCESS_CODE,
    content: db.banner,
    msg: 'success',
  })
})
...

  ;package.json配置快速启动命令。

// 安装
npm i nodemon --save-dev

"scripts": {
  "serve": "nodemon serve/app.js",
},

# axios、api 封装

  项目使用axios第三方插件,安装步骤参考 axios (opens new window)

├── src
│   ├── api
│   │   ├── home.js
│   ├── utils
│   │   ├── request.js
├── .env.development
├── vue.config.js

  ;request.js放置在utils工具类函数文件夹下。

const server = axios.create({
  timeout: 5000,
})

  开发与产品URL一般不一致,通常是配置环境变量,根目录创建.env.development文件,后期需要添加.env.production配置产品环境变量。

VUE_APP_BASE_API = '/api'

  项目下尝试访问express请求通常情况会发生跨域报错,服务端可设置跨域部分,或者项目设置代理。

// vue.config.js
devServer: {
  port: 8000,
  open: true,
  proxy: {
    [process.env.VUE_APP_BASE_API]: {
      target: 'http://127.0.0.1:3000/',
      ws: false,
      changeOrigin: true,
    },
  },
},

  引入request.js,设置请求url、请求方式,页面引用。

// api.js => home.js
import request from 'utils/request'

export function getBann() {
  return request({
    url: '/api/getBann',
    method: 'get',
  })
}

// 引用页面
import { getBann } from 'api/home'

getBann()
  .then(res => { ... })
  .catch(err => { ... })

# BetterScroll、vue-awesome-swiper

  项目涉及第三方组件主要是BetterScrollvue-awesome-swiperBetterScroll也是公共组件,components新建BetterScroll文件夹,详细步骤参考 better-scroll (opens new window)swiper也是较为公共的组件,components新建SwiperSwiperSlidevue-awesome-swiper版本造成的坑比较多,主要由于vue-awesome-swiperswiper的版本不适应造成,建议使用4.1.15.2.0,详细步骤参考 vue-awesome-swiper (opens new window)

# 页面

# 首页

  目前基础架子基本搭建完成,vuex状态管理部分暂不考虑,实际用到的时候自然带入。首页组件已含有NavBar,调整首页目录结构,组件命名尽量语义化,后期维护非常方便。

├── home
│   ├── index.vue
│   ├── components
│   │   ├── RecommendView.vue
│   │   ├── FeatureView.vue
│   │   ├── CardList.vue
│   │   ├── CardListItem.vue

  首页组件树结构,浏览器安装devtools工具非常直观。

<Home>
  <NavBar><BetterScroll><Swiper>
      <SwiperSlide>
    <RecommendView>
    <FeatureView>
    <IndexBar><CardList>
      <CardListItem>

  ;NavBar默认fiexed定位屏幕顶部,会导致遮住better-scrollhome使用伪元素before规避。且better-scroll外层wrapper需要指定高度,尽量加上相对定位。

// styles -> index.less
.m-home::before {
  content: '';
  display: block;
  height: 44px;
  width: 100%;
}

// home -> index.vue
.scroll {
  height: calc(100vh - 93px);
  overflow: hidden;
  position: relative;
}

  轮播图获取等数据接口,页面调用都需要api文件夹文件声明接口再引入。

// api -> home.js
export function getRecom() {
  return request( ... )
}

// home -> index.vue
import { getBann } from 'api/home'

  ;IndexBar也是公共组件,components新建IndexBar,组件参数传递数组,存在高亮切换和点击事件的抛出,同时含默认高亮,则将IndexBar封装v-model形式。props增加组件可复用性,不仅仅只依赖于data内数据label-value对形式,传递props可依赖多种形式。modelprops.data是封装自定义组件v-model必备,具体步骤参考官方 v-model (opens new window)index-bar-item点击调用change,实现v-model

// IndexBar -> index.vue
<div
  v-for="item in data"
  :key="item[props.value]"
  class="index-bar-item"
  :class="{ active: value === item[props.value] }"
  @click="itemClick(item)"
>
  ...
</div>

export default {
  model: {
    value: 'value',
    event: 'change',
  },
  props: {
    data: {
      type: Array,
      default: () => [],
    },
    value: {
      type: String,
      default: '',
    },
    props: {
      type: Object,
      default: () => ({
        label: 'label',
        value: 'value',
      }),
    },
  },
  methods: {
    itemClick(item) {
      item[this.props.value] !== this.value && this.$emit('change', item[this.props.value])
    },
  },
}

// home -> index.vue
<index-bar v-model="currentBar" :data="indexBars" @change="onChange" />

data: {
  indexBars: [
    {
      label: '流行',
      value: '0',
    },
    ...
  ],
  currentBar: '0',
},

  列表数据接口,传递参数包括点击currentTypepageNumpageSize。图片异步加载必然导致better-scroll高度计算失误,每张图片加载完毕都要重新计算高度才合理,故CardListItem内图片load完毕需要抛出给首页,再调用scroll组件内refresh方法。首页与CardListItem组件之间的关系薄弱,或者说没有关系,组件间事件通信可采用EventBus事件总线的方式。

// mian.js
Vue.prototype.$bus = new Vue()

// CardListItem 发出
onLoad() {
  this.$bus.$emit('imageLoad')
},

// home -> index.vue 监听
this.$bus.$on('imageLoad', () => {
  this.$refs.scroll.refresh()
})

  但是对于图片较多的列表,会导致调用refresh方法频繁,需要添加防抖函数。utilsindex.jstimer作为了闭包函数debounce的私有变量,首页引入函数debounce

export function debounce(func, delay = 20) {
  var timer = null
  return function (...arg) {
    if (timer) clearTimeout(timer)

    timer = setTimeout(() => {
      func.apply(this, arg)
    }, delay)
  }
}

  当组件scroll实例完全创建完毕才有必要生成防抖函数,实例未创建完毕$ref.scroll.refresh不存在,生成的防抖函数实际也不生效,短路运算&&更加保证refresh非函数则不执行。如此fresh就是一个保存有私有变量timer的防抖函数,图片加载小于20ms只执行最后一次。

// home -> index.vue
<scroll @load="onLoad" />

onLoad() {
  this.refresh = debounce(this.$refs.scroll.refresh, 20)
},

mounted() {
  this.$bus.$on('imageLoad', () => {
    this.refresh && this.refresh()
  })
},

  ;indexBar吸顶,通过使better-scroll下的InddexBar fixed定位不可取,better-scroll使用translate会导致内部定位元素非理想状态,解决办法最好是NavBar同级再添加组件IndexBar fixed定位,scroll未到吸顶距离隐藏,吸顶距离则显示。showTop用于返回顶部,滚动距离高于一屏则显示返回顶部按钮。

// home -> index.vue
scroll({ y }) {
  this.$nextTick(() => {
    this.showSticky = this.$refs.indexBar && -y > this.$refs.indexBar.$el.offsetTop
  })

  this.showTop = -y > document.body.clientHeight
},

  上拉加载、下拉刷新、IndexBar切换,下拉重新调用接口,上拉当前pageNum++,再获取数据,list数据使用concat拼接,或者使用push(...array)方式,indexBar切换重新获取数据,scroll滚动至IndexBar位置。

this.$nextTick(() => {
  this.showSticky && this.$refs.scroll.scrollTo(0, -this.$refs.indexBar.$el.offsetTop, 0)
})

  首页需要keep-active缓存,保存页面状态。

// App.vue
<keep-alive>
  <router-view />
</keep-alive>

# 详情

  路由routes.js新增详情路由。

// router -> routes.js
{
  path: '/detail/:id',
  name: 'detail',
  meta: {
    title: '详情',
  },
  component: () => import('views/detail'),
},

// home -> index.vue
this.$router.push({ path: `/detail/${id}` })

  目录结构。

├── Detail
│   ├── index.vue
│   ├── components
│   │   ├── GoodsInfo.vue
│   │   ├── StoreInfo.vue
│   │   ├── ClothList.vue
│   │   ├── ParamsInfo.vue
│   │   ├── CommentList.vue
│   │   ├── RecommendList.vue
│   │   ├── NavBar.vue
│   │   ├── SubmitBar.vue

  组件树结构。

<Detail>
  <NavBar><BetterScroll><Swiper>
      <SwiperSlide>
    <GoodsInfo>
    <StoreInfo>
    <ClothList>
    <ParamsInfo>
    <CommentList>
    <RecommendList>
  <SubmitBar>

  组件大致同首页一致,NavBar差别较大,NavBar对公共组件的NavBar进行封装,组件自定义v-model,抛出change事件,点击实现类似锚点的功能,同时伴随高亮。大致原理点击获取元素的value值,value值查询navbars对应的refName,获取对应组件的offsetTop实现锚点。

navbars: [
  {
    label: '商品',
    value: '0',
    refName: 'swiper',
  },
],

this.$refs.scroll.scrollTo(0, -this.$refs[refName].$el.offsetTop)

  ;scroll滚动过程中高亮伴随切换,在scroll事件中获取滚动距离,遍历navbars设置currentBar的值,同时v-model双向绑定currentBar,从而实现滚动高亮。

this.navbars.forEach(el => {
  if (this.$refs[el.refName] && -y >= this.$refs[el.refName].$el.offsetTop) {
    this.currentBar = el.value
  }
})

  添加购物车需要vuex状态管理,需要用到的部分实质只有购物车的商品列表,故使用vuex显得大材小用,况且不用vuex也能实现迷你版状态管理。为了保留与vuex一致性,store下新增index.jsvuex.jsvuex.js声明Store类,构造函数默认观察state数据。

import Vue from 'vue'

class Store {
  constructor({ state, mutations }) {
    Object.assign(this, {
      state: Vue.observable(state || {}),
      mutations,
    })
  }
  commit(type, arg) {
    this.mutations[type](this.state, arg)
  }
}

export default { Store }

  ;index.js与一般状态管理基本一致。

import Vuex from './vuex'

export default new Vuex.Store({
  state: {
    goods: [],
  },
  mutations: {
    ADD_GOODS(state, arg) { ... },

    ALL_CHECKED(state, val) { ... },
  },
})

  页面实现this.$store方式调用还要将导出实例放置原型上,至此迷你版vuex调用方式与vuex趋于一致,actionsgutters暂时用不上。

import store from './store'

Vue.prototype.$store = store

  添加购物车按钮点击,调用mutations方法。

this.$store.commit('ADD_GOODS', { ... })

  详情页面点击不同首页商品,只会请求同一商品,原因keep-active缓存了当前详情页,不会再次触发created,调整App.vue

<keep-alive exclude="Detail"></keep-alive>

  此时详情页Tabbar还存在,类比keep-active,组件传值exclude

// layout -> Tabbar.vue
export default {
  props: {
    exclude: {
      type: String,
      default: '',
    },
  },
  computed: {
    show() {
      const excludes = this.exclude.split(',')

      return !excludes.includes(this.$route.name)
    },
  },
}

// App.vue
<tabbar exclude="detail" />

  ;Message消息提示组件封装,根据开源组件库 element-ui (opens new window),封装一个简单版的Messagecomponents下新建Message,新建main.vueindex.jsmain.vue内部mounted之后,固定延时关闭Message,同时执行关闭回调。

<transition v-if="visible" name="fade">
  <div class="message">{{ message }}</div>
</transition>

export default {
  data() {
    return {
      visible: true,
      message: '',
      duration: 2000,
      onClose: null,
    }
  },
  mounted() {
    setTimeout(() => {
      this.visible = false

      this.onClose && this.onClose()
    }, this.duration)
  },
}

  ;index.js内部引入Vue,同时引入组件Message,创建组件构造器,通过new构造器创建组件实例,$mount挂载当前实例同时渲染为真实DOM,再追加至body内部,对外抛出install方法。

import Vue from 'vue'
import main from './main.vue'

const MessageConstructor = Vue.extend(main)

const Message = function (options) {
  if (typeof options === 'string') {
    options = {
      message: options,
    }
  }

  const instance = new MessageConstructor({
    data: options,
  })

  instance.$mount()

  document.body.appendChild(instance.$el)
}

export default {
  install() {
    Vue.prototype.$message = Message
  },
}

  ;main.js引入组件,Vue.use()调用内部install方法,Message被置于Vue.prototype上。

// mian.js
import Message from 'components/Message'
Vue.use(Message)

// detail -> index.vue
this.$message('商品添加成功!')

# 购物车

  购物车页面商品多,存在滚动情况,使用better-scroll,页面列表依赖storestate

computed: {
  data() {
    return this.$store.state.goods
  },
},

  目录结构。

├── cart
│   ├── index.vue
│   ├── components
│   │   ├── GoodsList.vue
│   │   ├── TotalBar.vue

  组件树结构。

<Cart>
  <NavBar><BetterScroll><GoodsList>
      <CheckButton><TotalBar>
    <CheckButton>

  ;CheckButton即公共选中按钮,components下新建CheckButton,内部实现v-model,内部通过切换背景色实现选中和取消,且内部点击事件阻止冒泡。可能存在当外部调用CheckButton时,带有CheckButton的整个卡片点击则CheckButton取消或者选中,此时修改v-model绑定值即可。但是当点击CheckButton时,由于本身CheckButton被点击时会切换,加上事件冒泡,外层卡片也会触发点击事件,再次修改v-model值,出现预期之外的结果,最好的办法就是阻止事件的冒泡。

@click.stop="$emit('change', !value)"

  ;TotalBar内部计算属性依赖storestate,根据state商品数量动态计算价格、总量。全选按钮点击商品全部选中,再次点击全部取消。全选点击则调用storemutations遍历修改商品checked属性。但是点击CheckButton,由于内部冒泡的阻止,触发不了外部点击事件。此时伪元素after就又能派上用场了,定位一个空盒子在全选按钮上,点击事件的触发元素一直是这个after伪元素。

.check {
  position: relative;

  &::after {
    content: '';
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
  }
}

  由于keep-active的缓存机制,导致列表无法下拉,主要由于初始情况scroll计算高度错误导致。解决办法一,添加activated事件,页面活动时,调用组件内部refresh事件更新高度。

activated() {
  this.$nextTick(() => {
    this.$refs.scroll.refresh()
  })
},

  解决办法二,keep-active不缓存cart页面。

<keep-alive exclude="Cart,Detail"></keep-alive>

# 分类

  目录结构。

├── category
│   ├── index.vue
│   ├── components
│   │   ├── CatesList.vue

  组件树结构。

<Category>
  <NavBar>
  <CatesList>

# 个人信息

  目录结构。

├── profile
│   ├── index.vue
│   ├── components
│   │   ├── UserInfo.vue
│   │   ├── CountInfo.vue
│   │   ├── OptionList.vue

  组件树结构。

<Profile>
  <NavBar>
  <UserInfo>
  <CountInfo>
  <OptionList>

# 优化部分

# 图片懒加载

  首页商品懒加载,assets文件夹添加懒加载填充图。

// 安装
npm i vue-lazyload --save

// main.js
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
  loading: require('assets/placeholder.png'),
})

# 移动端点击

  移动端300ms点击。

// 安装
npm i fastclick --save

// main.js
import FastClick from 'fastclick'
FastClick.attach(document.body)

# px 转换 vw

  ;pxvw视口单位,相关插件 postcss-px-to-viewport (opens new window),根目录需要新建postcss.config.js配置文件,相关配置参数官方文档很详尽了,唯一需要注意的就是px单位避免存在于行内/内联样式,minPixelValue最小转换数值一般为1,可能有部分边框需要1px显示。

// 安装
npm install postcss-px-to-viewport --save-dev

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px',
      viewportWidth: 375,
      unitPrecision: 6,
      propList: ['*'],
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: [],
      minPixelValue: 1,
      replace: true,
      exclude: undefined,
      include: undefined,
      landscapeUnit: 'vw',
    },
  },
}

# windows nginx 部署

  ;nginx (opens new window) 选择Stable version稳定版nginx/Windows-x.xx.x,下载压缩包解压,根目录执行命令启动nginx

// 查看 nginx 版本号
nginx -v

// 启动
start nginx

// 强制停止或关闭 nginx
nginx -s stop

// 正常停止或关闭 nginx (处理完所有请求后再停止服务)
nginx -s quit

// 修改配置后重新加载
nginx -s reload

// 测试配置文件是否正确
nginx -t

  浏览器输入http://localhost/,正常访问为Welcome to nginx!nginx默认访问html/index.html,可修改配置文件conf/nginx.conf更改默认路径,运行重新加载命令。

├── dist
│   ├── index.html
│   ├── ...
├── html
│   ├── index.html
│   ├── 50x.html
...

location / {
  root   dist;
  index  index.html index.htm;
}

# 后记

  项目基本思路均梳理大半,部分思路可能未提及,项目 GitHub (opens new window) 开放,可以克隆或者下载压缩包,仓库内存稍大,大约464M,压缩包下载1分钟左右,原因主要由于脱离网络接口,数据保存本地导致,详细情况开头已细致说明。整个项目非常适用新手练手,服务端数据服务只需要npm run serve即可开启。

  由于express动态获取本机内网ip,所以完全可以手机访问cli-service启动的Network地址,实现手机浏览器也可预览的效果。

# 更新日志

# 2020/11/13 10:31

  图片存放项目中首次下载或克隆耗时太长,express也是获取本机局域网ip实现移动访问,项目显得比较冗余。倘若有一个图床,express负责返回不同图片地址,问题会得到根本程度的解决。于是利用GitHub,手动造一个图床,了解原理不用网上的PicGo也能实现。

  实质就是开辟一个GitHub公开仓库,提交图片文件即可。仓库内部预览图片,点击原始数据获取图片原始URL,图片根目录一般是https://github.com/用户名/仓库名/raw/master,剩余部分则是图片在项目中的路径。

注意GitHub图床不太稳定,图片经常会挂掉,加速地址访问也会挂掉。项目内目前使用的是Gitee图床,相对会稳定很多。

  删除掉原项目动态获取局域网ipapp.js内静态文件关闭,删除static文件夹。图片详情推荐随机数生成可能存在相同情况优化。

# 2022/03/05 17:55

  项目有云服务器是可以实现访问并预览的,但是小项目练手没有必要,GitHub开放了静态网页预览功能,可以调整部分代码实现。ajax部分去掉,api内不引入request工具函数,直接引入servedb.js,合并router.jsapi下函数。

  实现浏览器静态网页的代码提交在browser分支,静态Page部署在GitHub,预览较慢,镜像页面也可以在 Gitee (opens new window) 访问。

# 🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 3/6/2022, 9:06:37 PM