动手实践:用Vue.js和Element-ui创建前端项目的步骤
项目是干嘛的?
这是一个以小区为单位的业主与业主之间的共享闲置物品的网站,共享者通过共享自己的物品来得到相应的7豆,你们可以用7豆来在网站上兑换到生活用品。 项目我实现了可以在pc端和微信小程序登陆项目,实现数据的共用,但由于本来这项目是个人,由于微信登陆限制,导致pc端无法实现用微信账号登陆。
技术栈
-
Vue.js
-
Vuex
-
Vue-router
-
Element.js
-
moment
-
jsonp
-
axios
-
stylu
项目展示
线上网址: 7享网
测试账号:abc@163.com
密码:123456
欢迎各位大佬使用
目录结构
api文件是存放后台请求接口;
common文件是存放公共样式、字体文件、工具类JS;
components文件是项目使用的组件;
route文件是路由器;
store文件是vuex文件;
web文件是项目各个主页面;
App.vue是入口文件;
element.js用来element-ui按需引入组件一个汇总的地方;
main.js我觉得没啥好说,大家都懂的;
项目
安装
首先用vue的脚手架工具vue-cli 来建立一个项目,安装完毕之后,接下来就安装element-ui.由于考虑到项目打包后的体积,我就想按需的引入element-ui的组件,以达到减少打包后包的体积.
从cli3开始就没有来.babelrc
,只有babel.config.js
,把原来的babel.config.js
删掉,添加了.babelrc
,并增加一个element.js
文件来实行对导入组件的管理.
登陆
用户正确输入自己账号密码后,通过后台验证后将自己的令牌(也就是token)返回并放置在浏览器缓存中,利用令牌来当作唯一入口凭证作为请求头就能获取到后台任何数据.而这里,我是利用vuex 实行了对用户的状态管理,
// src/store/aciton.js
export const toLogin = function ({ commit }, info) {
return new Promise((resolve, reject) => {
login(info).then((res) => {
if (res.status === 200) {
let token = res.data.token
commit(types.TOKEN, token)
window.localStorage.setItem('token', token)
instance.defaults.headers.common['Authorization'] = `Bearer ` + token
resolve(token)
}
}).catch((error) => {
let responseText = JSON.parse(error.request.responseText)
reject(responseText)
})
})
}
export const getUser = function ({ commit }) {
return new Promise((resolve, reject) => {
getUserInfo().then((res) => {
if (res.status === 200) {
let userinfo = res.data
commit(types.USERINFO, userinfo)
resolve(userinfo)
}
}).catch((error) => {
let responseText = JSON.parse(error.request.responseText)
reject(responseText)
})
})
}
通过 toLogin
和 getUser
来实现注册和登陆,并把获取到的用户信息和令牌放到vuex和浏览器缓存中.
数据传输
在api方面我用的大伙们都熟悉都axios
插件, 根据后端api接口,我分别设置生产环境和开发环境两个不同baseUrl,这样在写接口的时候就方便了,同时由于我使用的是令牌作为唯一个凭证,我在请求头上也设置了直接读取本地token缓存,同时我也设置了axios的拦截器,以便可以在token过期或者不存在时,可以进行相应处理。不过在这里我做了axios
重复请求处理,以防止用户重复点击,经过查看axios
文档,其中有一个api接口可以取消用户的提交,
大概意思是可以使用 CancelToken
构造函数进行设置,来取消用户提交行为。
axios拦截器
let pendingList = []
const removePending = (config, c) => {
const arr = config.url
const flagUrl = arr + '&' + config.method // 将每次存储在请求队列中的元素关键值
if (pendingList.indexOf(flagUrl) !== -1) {
if (c) {
// cancelToken 实例化函数
c()
} else {
// cancelToken不存在时,把队列中删掉
pendingList.splice(pendingList.indexOf(flagUrl), 1)
}
} else {
// 如果请求没有在列表中,把他放入列表中
if (c) {
pendingList.push(flagUrl)
}
}
}
// reject拦截器 先
// You can also create a cancel token by passing an executor function to the CancelToken constructor:
const CancelToken = axios.CancelToken
instance.interceptors.request.use(
config => {
const token = window.localStorage.getItem('token')
if (token) { // 判断是否存在token,如果存在的话,则每个http header都加上token
config.headers.Authorization = token
}
if (config.method === 'post' && process.env.NODE_ENV === 'production') {
config.cancelToken = new CancelToken(function executor (c) {
// An executor function receives a cancel function as a parameter
removePending(config, c)
})
}
return config
},
err => {
return Promise.reject(err)
})
// respone拦截器 后
instance.interceptors.response.use(
response => {
if (response.config.method === 'post' && process.env.NODE_ENV === 'production') {
removePending(response.config)
}
return response
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
store.dispatch('logOut')
router.replace({
path: '/welcome',
query: { redirect: router.currentRoute.fullPath } // 将跳转的路由path作为参数,登录成功后跳转到该路由
})
}
}
return Promise.reject(error.response)
}
)
天气
当你输入完账号和密码的时候,进入的项目的主页,首先你会看到天气,这天气的地址获取是通过你客户端的ip地址来判断你目前所在哪个城市,这里我用的腾讯地图提供的ip地址获取城市的api接口.由于浏览器同源策略的限制,非同源下的请求,都会产生跨域问题,jsonp即是为了解决这个问题出现的一种简便解决方案.这里我直接用了jsonp这个插件 来解决我的跨域问题,并且成功获取到ip地址下省市。当在这里我,我使用濑加载的形式,把jsonp这个包分离出来。进一步减少主体的体积。
_getLocation () {
// 采用高德地图的ip定位,解决ipv6地址不能读取定位
const url = `https://restapi.amap.com/v3/ip?key=${AMAP_KEY}`
import(/* webpackChunkName: "jsonp" */'jsonp').then(({ default: _jsonp }) => {
_jsonp(url, null, (err, data) => {
if (err) {
this.normalAir('浙江省', '杭州市')
} else {
let city = data.city || '杭州市'
let province = data.province || '浙江省'
this.normalAir(province, city)
}
})
})
},
数据处理
在数据处理方面,由于后台返回的数据不一定就符合前端要求,所以我都对那些数据会做一些优化,已达到前端的要求.通常我的做法就是写成一个类,在类的方法里面写成各种类的方法,然后数据直接在类里面完成转换,提高代码的复用率. 附上pending.js
当然这样也还是不够完美的,因为在通过后台返回的数据虽然经过各自class
的处理后会漂亮些,但是这些class
也只是处理单一一个,假如遇到多个了呢?我们每次就要像如下那样处理我从后台拿到的数据.
_pending (page, type) {
pending(page, type).then((res) => {
this.pending = this.normalPending(res.data.data)
this.total = res.data.total
})
},
normalPending (data) {
let temp = []
data.forEach((d) => {
temp.push(createPending(d))
})
return temp
},
这看上去多麻烦,能不能让代码更加灵活和优美了呢?答案就是要对normalPending
写一个封装包装函数,
export function normalArray (fn) {
const wrapped = function (arg) {
let temp = []
arg.forEach((d) => {
temp.push(fn(d))
})
return temp
}
return wrapped
}
normalArray 本身是一个函数,它接受 fn 作为函数传入,返回一个新的函数 wrapped,当wrapped 执行的时候,通过传入的data,来自动执行wrapped函数
_pending (page, type) {
pending(page, type).then((res) => {
const normalPending = normallArray(createPending)
this.pending = normalPending(res.data.data)
this.total = res.data.total
})
},
也就这样每次都减少代码重复使用的次数,来让自己写起来更轻松.
瀑布流
以上是仿造瀑布流的图片展示,这里我只是设置了只有2行的效果,虽然不太好看,但是我想要的效果却是出来了.首先当放好第一行图片的时候,再去获取第一行图片中高度最小的图片,在第二行的图片时,先从刚刚找出第一行中最矮的图片,并以第一行最矮的图片的左边距离放置其下面,这样以此类推完成整个瀑布流的样式设置,这里我强调的是利用绝对定位来布置图片.
// 瀑布流的js设置
const GEP = 20
location () {
let fallParent = this.$refs.fall
let fallChildren = fallParent.children
let imgWidth = 215
let gep = GEP // 图片的宽度
let cols = Math.floor(fallParent.clientWidth / imgWidth) // 计算出多少列
let boxHeightArray = []
for (let i = 0; i < fallChildren.length; i++) {
if (i < cols) {
boxHeightArray[i] = fallChildren[i].offsetHeight
fallChildren[i].style.position = 'absolute'
fallChildren[i].style.top = 0 + 'px'
let currentGep = gep * i
fallChildren[i].style.left = imgWidth * i + currentGep + 'px'
} else {
let minHeight = Math.min.apply(null, boxHeightArray)
let minIndex = boxHeightArray.findIndex((item) => {
return item === minHeight
})
fallChildren[i].style.position = 'absolute'
fallChildren[i].style.top = minHeight + gep + 'px'
fallChildren[i].style.left = fallChildren[minIndex].offsetLeft + 'px'
boxHeightArray[minIndex] = boxHeightArray[minIndex] + fallChildren[i].offsetHeight + gep
}
}
this.initHight(boxHeightArray)
},
// 这是要边框有合适的高度
initHight (arr) {
let maxHeight = Math.max.apply(null, arr)
let finallyHeight = maxHeight + 100
this.$refs.fall.style.height = finallyHeight + 'px'
}
小球飞入购物车
在购物页面那里,我做了一个仿京东超市那样点击商品后小球由大变小那样飞进购物车的动画,这里面我用到vue里面提供动画的api和css3动画来完成整个动画效果.
const BALL_LEN = 10 // 设置球子的数量
function createBalls () {
let balls = []
for (let i = 0; i < BALL_LEN; i++) {
balls.push({
show: false
})
}
return balls
}
export default {
data () {
return {
balls: createBalls()
}
}
}
一开始我先形成10个隐藏的小球,并将其放在响应式变量balls中. 小球做好之后,下一步该让小球飞起来了.
addShopCart (item) {
// 获得球的高度
this.ballLeft = event.currentTarget.getBoundingClientRect().left + 115
this.ballTop = event.currentTarget.getBoundingClientRect().top
let shop = {
'id': item.id,
'name': item.name,
'price': item.price,
'num': 1
}
this.addShop(shop)
for (let i = 0; i < this.balls.length; i++) {
const ball = this.balls[i]
if (!ball.show) {
ball.show = true
ball.el = event.currentTarget
ball.url = item.image
this.dropBalls.push(ball)
return
}
}
},
通过点击某件商品,来获取当前物品的相对浏览器窗口左边和顶部的距离也就小球要开始起飞的位置,并将所点击的物品信息放在vuex中,点击的小球show状态修改为true,当监测小球的状态有变化,就执行以下动画了.
beforeDrop (el) {
// 初始化球的高度
const inner = el.getElementsByClassName(innerClsHook)[0]
el.style.display = ''
el.style.transform = `translate3d(0px,${this.ballTop + 12}px, 0)`
inner.style.transform = `translate3d(${this.ballLeft}px,0, 0)`
},
droping (el, done) {
this._reflow = document.body.offsetHeight // 重新计算小球高度
const shopping = this.$refs.shopping.$el.clientWidth
const shopCart = document.getElementById('shopping').getBoundingClientRect()
const shopCartLeft = shopCart.left + (shopping / 2)
const shopCartTop = shopCart.top + (shopping / 2)
const inner = el.getElementsByClassName(innerClsHook)[0]
// 小球滚动的行程
el.style.transform = `translate3d(0px,${Math.abs(shopCartTop)}px,0)`
inner.style.transform = `translate3d(${Math.abs(shopCartLeft)}px,0,0)`
// 小球动画的时间
el.addEventListener('transitionend', done)
},
afterDrop (el) {
const ball = this.dropBalls.shift() // 目的是小球回收,拿到第一个小球
if (ball) {
ball.show = false
el.style.display = 'none'
}
this.ballTop = 0
this.ballLeft = 0
},
动画执行完毕别忘了做小球的回收,以便动画能持续下去.而小球由大变小的过程就交由css3进行动画处理.
@keyframes shopImg
0%
transform: scale(2.5)
25%
transform: scale(1.8)
50%
transform: scale(1.2)
75%
transform: scale(0.6)
100%
transform: scale(0.3)
引导页
为了进一步提高用户使用体验,我在首页那里引入引导页面,来让第一次使用网站的朋友,知道各个模块的作用.一开始我是不知道如何下手,在大佬的推介下,我接触到introjs
和vue-introjs
这两个插件,但我发现打包后整个js包体积大了100KB,而这个我是无法接受的,另外一个就是他的样式和动画,而并不满足我自己想要的效果.于是我就分别看了两个包的源码,发现代码并不多,于是我决定仿照他们的写法来为自己的项目设计一个适合的插件.首先我是从引用文件开始看.
//intro.js
var introJs = function (targetElm) {
var instance;
// 初始化introJs
if (typeof (targetElm) === 'object') {
// 省略各种错误判断
} else {
instance = new IntroJs(document.body);
}
introJs.instances[ _stamp(instance, 'introjs-instance') ] = instance;
return instance;
};
introJs.version = VERSION;
introJs.instances = {};
//Prototype
introJs.fn = IntroJs.prototype = {
start: function (group) {
_introForElement.call(this, this._targetElement, group);
return this;
},
// 省略各类的api
}
首先我是从intro.js
文件进去找到入口函数introJs()
, 发现他默认作用于body
标签,我继续往下找,找到了启动函数start()
, 发现是使用_introForElement
函数来收集所有节点.那我继续顺藤摸瓜找到_introForElement
函数
var allIntroSteps = targetElm.querySelectorAll("*[data-intro]"),
看到这里,我顿时大概知道如何开始写这个插件
在这里并没有将allIntroSteps
这个变量写进响应式data
函数,好处减轻浏览器负担.接着start()
来运作整个插件,其用来初始化样式和收集所有html
标签,而样式的变化我是利用监测step的变化来改变样式,我这里并没有采用像intro.js
那样通过class来改变样式,就这样一个简单的适合我页面的引导页面就这样产生了.
guide.vue.
等待加载中页面
Vue.extend
按照官方文档解释是使用基础 Vue 构造器,创建一个“子类”.利用这个"子类",配合项目做一些命令式api的封装,来达到更方便的使用自定义组件目的,因此我做了一个等待加载中的组件.loading.vue.
// src/components/loading.vue
import Vue from 'vue'
const Loading = {
name: 'loading',
props: ['visible']
}
export default Loading
let instanceCache
export const loading = function (visible) {
const getInstance = () => {
const LoadingCtor = Vue.extend(Loading)
if (!instanceCache) {
instanceCache = new LoadingCtor()
instanceCache.$mount()
document.body.appendChild(instanceCache.$el)
}
return instanceCache
}
const instance = getInstance()
Vue.nextTick(() => {
instance.visible = visible
})
}
就这样,我就可以在想用的页面上通过类似api的形式来直接调用我的插件,提高生产效率.
// src/web/wish.vue
import { loading } from '../components/loading'
_getAllWish (page) {
loading(true)
getAllWish(page).then((res) => {
this.total = res.data.total
this.page = res.data.page
this.wishes = this.noramlWish(res.data.data)
loading(false)
})
},
优化
项目所使用的插件moment.js
在打包的时候会将其他不需要的地方语言也打包进来,其实项目中根本不会用到这些语言,我用的是ContextReplacementPlugin
插件,所以我想通过配置webpack配置来让不需要的语言不打包进去我的包来.
vue.config.js
module.exports = {
configureWebpack: {
plugins: [
new webpack.ContextReplacementPlugin(
/moment[/\\]locale$/,
/zh-cn/
)
]
}
}
考虑到现代主流的浏览器目前大部分都支持gzip压缩,为了进一步减少包的体积,提高用户的首屏加载速度,在这里我采用 CompressionPlugin
插件进行gzip的压缩,具体配置大家可以看官网文档.
module.exports = {
configureWebpack: {
plugins: [
new CompressionPlugin({
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
algorithm: 'gzip'
})
]
}
}
光配置webpack是不够,还需要在服务器的nginx也要进行配置,这样浏览器就知道会使用压缩后的文件来加载我的页面了.
当打包构建应用时,JavaScript
包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了,结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。而在设置懒加载时有点要注意的地方,那就是当 style
标签有 scoped
属性时,要注意该属性所使用的场景,避免打包后发现样式不能打包进来,这个属性要慎用。
const Home = () => import(/* webpackChunkName: "home" */ '../web/home')
const MyHome = () => import(/* webpackChunkName: "home" */ '../web/myHome')
const Shop = () => import(/* webpackChunkName: "shop" */ '../web/shop.vue')
const NotFound = () => import(/* webpackChunkName: "notFound" */ '../web/not-found')
const Help = () => import(/* webpackChunkName: "help" */'../web/help')
const HelpDetail = () => import(/* webpackChunkName: "help" */'../web/help-detail')
const Drift = () => import(/* webpackChunkName: "drift" */'../web/drift')
const Welcome = () => import(/* webpackChunkName: "welcome" */'../web/welcome')
const Hot = () => import(/* webpackChunkName: "hot" */'../web/hot')
const UserDetail = () => import(/* webpackChunkName: "hot" */'../web/user-detail')
const Wish = () => import(/* webpackChunkName: "wish" */ '../web/wish')
前端监控
前端监控的目的是:获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向。
前端监控可以分为三类:数据监控、性能监控和异常监控。
数据监控
数据监控,就是监听用户的行为,在这里我只是用了百度统计
来做来源分析
、访问分析
、流量分析
,来基本满足网页的自身需要。当然我可以在这里做得更多,通过埋点的方式来知道在某个页面停留的时间和在相应的页面触发的行为,但没做,这块我还不懂,在坐的各位大佬如果会的,麻烦在留言区跟我说下,或者提供好的文章让我学习学习。
性能监控
性能监控指的是监听前端的性能,主要包括监听网页或者说产品在用户端的体验等。性能监控这部分我使用JS提供的 window.performance
api接口来获取到测量网页和web应用程序的性能。performaceData.js
let performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance
// 如果浏览器不支持,直接不执行以下操作
if (performance === undefined) {
return false
}
let timinObj = performance.timing
let navigationObj = performance.navigation
对window.performance
接口做了兼容性的处理,并对不支持该接口的浏览器直接不执行以下操作。
if (navigationObj.type === 0) {
params.dns = Math.floor(timinObj.domainLookupEnd - timinObj.domainLookupStart) // dns查询时间
params.tcp = Math.floor(timinObj.connectEnd - timinObj.connectStart) // tcp链接时间
params.paint = Math.floor(timinObj.responseEnd - timinObj.responseStart) // request请求耗时
params.render = timinObj.domComplete - timinObj.domInteractive // dom树解释时间
params.domready = timinObj.domInteractive - timinObj.responseEnd // domready时间
params.load = timinObj.loadEventEnd - timinObj.fetchStart // 首屏时间
params.white = Math.floor(Date.now() - timinObj.navigationStart) // 白屏时间
let args = ''
// 拼成URL可以用的数
for (let i in params) {
if (args !== '') {
args += '$$'
}
args += `${i}=${params[i]}`
}
// 解决跨域的问题
let img = new Image(1, 1)
let baseUrl = process.env.NODE_ENV === 'production' ? 'https://www.ifenghua.top/v1' : 'http://127.0.0.1:5000/v1'
let src = baseUrl + '/performace' + `/${encodeURIComponent(args)}`
setTimeout(() => {
img.src = src
}, 100)
}
并不是所有的用户的行为都是需要做性能监控,我这里用到 performance.navigation
来区分用户行为,只有通过常规导航方式进来的就做性能监控。
- 0 : TYPE_NAVIGATE (用户通过常规导航方式访问页面,比如点一个链接,或者一般的get方式)
- 1 : TYPE_RELOAD (用户通过刷新,包括JS调用刷新接口等方式访问页面)
- 2 : TYPE_BACK_FORWARD (用户通过后退按钮访问本页面)
监控包括:
- dns查询时间
- tcp链接时间
- request请求耗时
- dom树解释时间
- domready时间
- 首屏时间
- 白屏时间
params.dns = Math.floor(timinObj.domainLookupEnd - timinObj.domainLookupStart) // dns查询时间
params.tcp = Math.floor(timinObj.connectEnd - timinObj.connectStart) // tcp链接时间
params.paint = Math.floor(timinObj.responseEnd - timinObj.responseStart) // request请求耗时
params.render = timinObj.domComplete - timinObj.domInteractive // dom树解释时间
params.domready = timinObj.domInteractive - timinObj.responseEnd // domready时间
params.load = timinObj.loadEventEnd - timinObj.fetchStart // 首屏时间
params.white = Math.floor(Date.now() - timinObj.navigationStart) // 白屏时间
这是我自己根据上图所示写出来的,有误的话,欢迎各位大佬指正出来。
最后,监控得来的数据我使用 new 一个图片对象,来解决数据派发出去的跨域原因。
异常监控
由于产品的前端代码在执行过程也会发生异常,特别是在生产环境的时候,当用户遇到问题时,如果没有异常监控的话,作为开发者的你不一定会知道,虽然大部分的异常可以通过try catch
能捕获得到,但是比如图片某些原因加载不了以及其他偶然性的异常是难以捕获的,在这里通过设置一个全局的异常监控来管理我的程序,error.js
const ERROR_RUNTIME = 1
const ERROR_SCRIPT = 2
const ERROR_STYLE = 3
const ERROR_IMAGE = 4
const ERROR_AUDIO = 5
const ERROR_VIDEO = 6
const ERROR_CONSOLE = 7
const ERROR_TRY_CATCH = 8
const LOAD_ERROR_TYPE = {
SCRIPT: ERROR_SCRIPT,
LINK: ERROR_STYLE,
IMG: ERROR_IMAGE,
AUDIO: ERROR_AUDIO,
VIDEO: ERROR_VIDEO,
CONSOLE: ERROR_CONSOLE,
TRY_CATCH: ERROR_TRY_CATCH
}
我先定义好错误的类型码,以便知道错误的类型。
export default class Error {
constructor () {
this.errorList = []
this.timer = ''
}
async init () {
await this.onError()
await this.auto()
}
onError () {
window.onerror = (message, source, lineno, colno, error) => {
this.handleError(this.formatRuntimerError.apply(null, [message, source, lineno, colno, error]))
}
// 监听资源加载错误
window.addEventListener('error', function (event) {
// 过滤 target 为 window 的异常,避免与上面的 onerror 重复
const errorTarget = event.target
if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]) {
this.handleError(this.formatLoadError(errorTarget))
}
}, true)
// 利用VUE实行全局错误监控,当监听资源加载错误以上的addEventListener不能使用的时候可以用这个
Vue.config.errorHandler = (err) => {
const errorTarget = err.target
if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]) {
this.handleError(this.formatLoadError(errorTarget))
}
}
}
}
一开始,首先初始化全局监控对象,通过 async await
异步方法来逐步执行,特别说说,由于我的项目好多组件并不是原生的标签,所以用window.addEventListener
和 window.onerror
会监控不到例如静态资源的异常情况,好在vue
提供一个全局配置的 errorHandler
异常监控的api来更好的对异常监控的支持。
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
'''
省略若干
'''
} else {
next()
}
// 异常数据发送
error.report()
})
关于异常数据上传,我利用框架中的路由守卫来监听每次的url变化来向服务器发送异常数据,但考虑到一旦长时间url没变化的时候,我就将数据上传行为改为为定时器发送,来保证错误能够及时上报服务器。
auto () {
this.timer = setInterval(() => {
this.report()
}, 20000)
}
report () {
if (this.errorList.length === 0) {
return
}
sendError(this.errorList).then((res) => {
this.errorList = []
})
}
部署
我的项目是部署到腾讯云上面,使用的nginx
+pm2
来部署我的web前端部分,通过pm2
来启动我的web项目 prod.server.js
以及管理我的项目
prod.server.js
const express = require('express')
const path = require('path')
const app = express()
const history = require('connect-history-api-fallback')
app.use(history({
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
}))
app.use(express.static(path.join(__dirname, './dist')))
module.exports = app.listen(9000, () => {
console.log('success')
})
有个地方需要注意一下,那就是由于我的路由使用的history模式,光在前端配置也是不行的,也需要后端进行相应的配置,而我的前端项目使用的是基于 Node.js 的 Express来启动,然后我就是使用来官方推介的connect-history-api-fallback
中间件来将把请求的位置更改为您指定的索引.
结束
项目代码仓: 我的github仓库
项目网址: 7享网