项目名 蘑菇购,与一般购物WebApp
类似,包括首页、分类、购物车、个人中心、详情。
项目基于vue
、vue-router
、vue-cli3
,api
请求相关部分采用axios
,数据部分并非来自服务器,而是本地基于express
启动相关数据服务。原因一是网络接口更新快、数据变化大、依赖性高,二是项目本身不大,基于项目启动本机服务灵活性较高,代码安装依赖即可运行,故最终考虑express
爬取相关接口数据保存本地。
状态管理未使用vuex
,仅仅是少部分使用vuex
功能显得多余,项目大才完全有必要。基于vue.observable
能实现部分vuex
功能。图片加载部分异步更新DOM
采用事件总线进行组件通讯。
项目第三方开源组件包括better-scroll
滚动插件、vue-awesome-swiper
和swiper
轮播组件、normalize.css
初始化样式、vue-lazyload
懒加载、移动端click300ms
延时采用fastclick
。
项目难度不高,适合新手练手,此篇仅是练习组件化封装和目录配置的相关记录。
蘑菇购
├── 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
仅配置Babel
、Router
、CSS Pre-processors
(less
),删除其余业务不相关部分,文件夹部分通过需求逐步新建。
项目目前正常运行为空白,先搭建路由相关部分,抽离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: 'history',
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
,其余文件同理,重启运行。
此时项目依旧空白,但是home
下index.vue
已被重定向,接下来封装Tabbar
,Tabbar
较为公共,components
下新建Tabbar
、TabbarItem
,文件夹下均新增index.vue
。Tabbar
一般高度49px
最为舒适,同时定位屏幕底部,层级高于其他组件。TabbarItem
内部引入router-link
,组件接收参数参考 vant-ui 并做了部分修改,通过当前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: String,
activeColor: String,
inactiveColor: String
},
computed: {
path() {
return this.$route.path
}
}
}
公共组件Tabbar
封装完成,项目相关Tabbar
还未封装及引用。由于项目相关Tabbar
有关于项目页面布局。故src
下新增layout
文件夹,相关布局组件不会太多,不用文件夹下再放index.vue
形式。
// layout -> Tabbar.vue
<m-tabbar>
<m-tabbar-item to="/home" active-color="#ff8198" inactive-color="#555555">
<i slot="icon" class="iconfont icon-home"></i>
<span slot="text">首页</span>
</m-tabbar-item>
...
</m-tabbar>
import Tabbar from "components/Tabbar"
import TabbarItem from "components/TabbarItem"
export default {
components: {
MTabbar: Tabbar,
MTabbarItem: TabbarItem
}
图标采用 iconfont,官网选择合适的Tabbar
图标,下载压缩包解压。assets
文件夹下新建iconfont
文件夹,引入解压的全部文件。其中demo_index.html
关于字体图标使用方式做了详细阐述,iconfont.css
需要手动引入,iconfont
也是一种字体,最终归结为css
样式。src
下新建styles
文件夹,创建index.less
,index.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.css
,mian.js
引入。
// 安装
cnpm i normalize.css --save
// main.js
import 'normalize.css'
styles
文件夹下index.less
,初始化html
、body
、#app
高度,删除App.vue
相关样式。
html,
body,
#app {
height: 100%;
}
路由添加导航守卫router.beforeEach
,用于初始化页面标题,但是目标路由to
并不含有meta.title
,修改routes.js
,其余同理,正确运行页面标题切换。替换public
下favicon.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-color
。home
页引入,页面引入组件顺序遵循引入公共组件、定制组件、公共js
、定制js
。
项目目前可实现路由跳转,相关api
以及数据还未准备。网络接口常更新、数据不稳定,采用express
、superagent
爬取保存接口数据,爬虫crawler
参考其他文章。大致拆分项目需要用到的后端接口,首页轮播图、特色、推荐、详情、列表、分类等,项目新建serve
文件夹。image
保存数据图片。
├── serve
│ ├── static
│ │ ├── image
│ ├── app.js
│ ├── db.js
│ ├── router.js
app.js
启动数据服务,开放静态static
文件夹,映射/static
到serve/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
var 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')
router.get('/api/getBann', function (req, res) {
res.send({
message: "success",
result: db.banner,
status: "0",
success: true
})
})
...
package.json
配置快速启动命令。
// 安装
cnpm i nodemon --save-dev
scripts: {
serve: "nodemon serve/app.js"
}
项目使用axios
第三方插件,安装步骤参考 axios。
├── src
│ ├── api
│ │ ├── home.js
│ ├── utils
│ │ ├── request.js
├── .env.development
├── vue.config.js
request.js
内baseURL
独立出来,使用环境变量,放置utils
工具类函数文件夹下。
const server = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000,
})
开发与产品URL
一般不一致,通常是配置环境变量,根目录创建.env.development
文件,后期需要添加.env.production
配置产品环境变量。
VUE_APP_BASE_API = "/api"
项目下尝试访问express
请求通常情况会发生跨域报错,服务端可设置跨域部分,或者项目设置代理。
// vue.config.js
devServer: {
port: 8000,
proxy: {
[process.env.VUE_APP_BASE_API]: {
target: 'http://127.0.0.1:3000/',
ws: false,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
},
}
引入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
,BetterScroll
也是公共组件,components
新建BetterScroll
文件夹,详细步骤参考 better-scroll。swiper
也是较为公共的组件,components
新建Swiper
、SwiperSlide
,vue-awesome-swiper
版本造成的坑比较多,主要由于vue-awesome-swiper
与swiper
的版本不适应造成,建议使用4.1.1
和5.2.0
,详细步骤参考 vue-awesome-swiper。
目前基础架子基本搭建完成,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-scroll
,home
使用伪元素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
可依赖多种形式。model
、props.data
是封装自定义组件v-model
必备,具体步骤参考官方 v-model,index-bar-item
点击调用change
,实现v-model
。
// IndexBar -> index.vue
<div
class="index-bar-item"
:class="{ active: value === item[props.value] }"
@click="itemClick(item)"
v-for="item in data"
:key="item[props.value]"
>...</div>
export default {
props: {
data: {
type: Array,
default: () => [],
},
value: {},
props: {
type: Object,
default: () => ({
label: "label",
value: "value",
}),
},
},
model: {
value: "value",
event: "change",
},
methods: {
itemClick(item) {
item[this.props.value] !== this.value &&
this.$emit("change", item[this.props.value])
},
}
}
// home -> index.vue
<index-bar
:data="indexBars"
v-model="currentBar"
@change="onChange"
/>
data:{
indexBars: [
{
label: "流行",
value: "0"
}
...
],
currentBar: "0"
}
列表数据接口,传递参数包括点击currentType
、pageNum
、pageSize
。图片异步加载必然导致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
方法频繁,需要添加防抖函数。utils
下index.js
,timer
作为了闭包函数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.js
、vuex.js
,vuex.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
趋于一致,actions
、gutters
暂时用不上。
import store from "./store"
Vue.prototype.$store = store
添加购物车按钮点击,调用mutations
方法。
this.$store.commit("ADD_GOODS", {...})
详情页面点击不同首页商品,只会请求同一商品,原因keep-active
缓存了当前详情页,不会再次触发created
,调整App.vue
。
<keep-alive exclude="detail">
此时详情页Tabbar
还存在,类比keep-active
,组件传值exclude
。
// layout -> Tabbar.vue
export default {
props: {
exclude: String,
},
computed: {
show() {
const excludes = this.exclude.split(",")
return !excludes.includes(this.$route.name)
}
}
}
// App.vue
<tabbar exclude="detail" />
Message
消息提示组件封装,根据开源组件库 element-ui,封装一个简单版的Message
,components
下新建Message
,新建main.vue
、index.js
,main.vue
内部mounted
之后,固定延时关闭Message
,同时执行关闭回调。
<transition name="fade" v-if="visible">
<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
,页面列表依赖store
内state
。
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
内部计算属性依赖store
内state
,根据state
商品数量动态计算价格、总量。全选按钮点击商品全部选中,再次点击全部取消。全选点击则调用store
下mutations
遍历修改商品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="detail,cart">
目录结构。
├── 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
文件夹添加懒加载填充图。
// 安装
cnpm i vue-lazyload --save
// main.js
import VueLazyload from "vue-lazyload"
Vue.use(VueLazyload, {
loading: require('assets/placeholder.png')
})
移动端300ms
点击。
// 安装
cnpm i fastclick --save
// main.js
import FastClick from 'fastclick'
FastClick.attach(document.body)
px
转vw
视口单位,相关插件 postcss-px-to-viewport,根目录需要新建postcss.config.js
配置文件,相关配置参数官方文档很详尽了,唯一需要注意的就是px
单位避免存在于行内/内联样式,minPixelValue
最小转换数值一般为1
,可能有部分边框需要1px
显示。
// 安装
cnpm 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'
}
}
}
nginx 选择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 开放,可以克隆或者下载压缩包,仓库内存稍大,大约464M
,压缩包下载1
分钟左右,原因主要由于脱离网络接口,数据保存本地导致,详细情况开头已细致说明。整个项目非常适用新手练手,服务端数据服务只需要npm run serve
即可开启。
由于express
动态获取本机内网ip
,所以完全可以手机访问cli-service
启动的Network
地址,实现手机浏览器也可预览的效果。
图片存放项目中首次下载或克隆耗时太长,express
也是获取本机局域网ip
实现移动访问,项目显得比较冗余。倘若有一个图床,express
负责返回不同图片地址,问题会得到根本程度的解决。于是利用Github
,手动造一个图床,了解原理不用网上的PicGo
也能实现。
实质就是开辟一个Github
公开仓库,提交图片文件即可。仓库内部预览图片,点击原始数据获取图片原始URL
,图片根目录一般是https://github.com/用户名/仓库名/raw/master
,剩余部分则是图片在项目中的路径。
注意
Github
图床不太稳定,图片经常会挂掉,加速地址访问也会挂掉。项目内目前使用的是Gitee
图床,相对会稳定很多。
删除掉原项目动态获取局域网ip
,调整baseURL
,app.js
内静态文件关闭,删除static
文件夹。图片详情推荐随机数生成可能存在相同情况优化。
项目有云服务器是可以实现访问并预览的,但是小项目练手没有必要,github
开放了静态网页预览功能,可以调整部分代码实现。ajax
部分去掉,api
内不引入request
工具函数,直接引入serve
下db.js
,合并router.js
和api
下函数。
vue.config.js
新增publicPath
,由于静态网页预览history
模式刷新报错404
,router
model
删除history
模式,使用默认hash
模式,并提交在noajax
分支。
伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞或 Star ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正~
你的支持就是我更新的最大动力~
GitHub、Blog、掘金、CSDN 同步更新,欢迎关注~