单页应用简单说就是整个网站只有一个html页面,进入网站的时候浏览器加载这个页面,之后的所有操作都是在这个页面上通过js来实现前端路由,动态地控制页面元素的加载以实现页面“切换”的效果。
1.利用vue-router快速实现单页应用
接下来以wepack4整合vue.js为例,一步一步实现一个单页应用。这里需要说明的是,对于新手不建议直接使用vue-cli
这个官方提供的脚手架,因为它的配置过于复杂、繁琐,不利于我们了解项目配置的来龙去脉。
创建一个项目目录,并通过npm初始化
mkdir spa-vue && cd spa-vue && npm init -y
安装基本依赖,用于处理html、js、css及图片资源
npm install --save-dev webpack \
webpack-cli \
style-loader \
css-loader \
file-loader \
url-loader \
html-loader \
html-webpack-plugin \
mini-css-extract-plugin
安装vue.js、jquery及es6相关依赖
# 运行时依赖
npm install --save vue jquery
# 打包构建时时依赖
npm install --save-dev vue-loader \
vue-style-loader \
vue-template-compiler \
vue-hot-reload-api \
babel \
babel-loader \
babel-core \
babel-plugin-transform-runtime \
babel-preset-es2015 \
babel-runtime
安装vue-router来支持路由功能
npm i --save vue-router
最后安装webpack-dev-server方便开发调试
npm i --save-dev webpack-dev-server
完成所有的安装后,在package.json
中添加打包及开发调试脚本
{
"name": "spa-vue",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server --inline --progress --compress --hot --open --history-api-fallback --config webpack.config.js --host 127.0.0.1 --port 8888"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"babel-runtime": "^6.26.0",
"css-loader": "^1.0.0",
"file-loader": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.3",
"style-loader": "^0.23.0",
"url-loader": "^1.1.1",
"vue-hot-reload-api": "^2.3.1",
"vue-loader": "^15.4.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
"jquery": "^3.3.1",
"vue": "^2.5.17",
"vue-router": "^3.0.1"
}
}
完善项目代码,最终的文件结构如下
$ tree -I node_modules
.
├── package-lock.json
├── package.json
├── src
│ ├── app.vue
│ ├── css
│ ├── img
│ ├── index.html
│ ├── index.js
│ └── page
│ ├── home.vue
│ ├── login.vue
│ └── register.vue
└── webpack.config.js
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'production',
entry: {
index: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[hash:8].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015'],
plugins: ['transform-runtime']
}
}
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../' // 特别重要,否则css文件打包后其中引用的图片文件不正确
}
},
"css-loader"
]
},
{
test: /\.html$/,
use: {
loader: 'html-loader'
}
},
{
test: /\.(png|jpg|gif|jpeg|svg)$/,
use:[
{
loader: "url-loader",
options: {
name: "img/[name].[hash:5].[ext]",
limit: 1024, // size <= 1kib
// outputPath: "img",
publicPath: "../"
}
}
]
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[hash:8].css",
}),
new HtmlWebpackPlugin(
{
template: './src/index.html',
filename:'./index.html',
hash: false,
chunks: ['index']
}
),
new VueLoaderPlugin(),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Vue: ['vue/dist/vue.esm.js', 'default'],
VueRouter: ['vue-router/dist/vue-router.esm.js', 'default']
})
],
devServer: {
contentBase: path.resolve(__dirname, "../dist/"),
index:'./index.html'
}
};
src/index.html
Title
src/index.js
import App from './app.vue'
import Home from './page/home.vue'
import Login from './page/login.vue'
import Register from './page/register.vue'
Vue.use(VueRouter);
const routes = [
{ path: '/home', component: Home },
{ path: '/login', component: Login },
{ path: '/register', component: Register }
];
const router = new VueRouter({
mode: 'history',
routes: routes
});
new Vue({
el: '#app',
router: router,
components: { App }
});
这里有必要说明的是,设置mode为history会开启HTML5的History路由模式,通过/
设置路径。如果不设置mode,就会使用#
来设置路径。开启History路由,在生产环境web服务器必须进行配置,将所有路由指向index.html,或将index.html设为404页面,否则刷新页面时会出现404。
另外,webpack-dev-server也需要启用--history-api-fallback
参数来支持History模式。
src/app.vue
App
Home
Login
Register
src/page/home.vue
{{page}}
src/page/login.vue
{{page}}
src/page/register.vue
{{page}}
然而,执行npm run build
进行打包的时候,出现以下错误
Module build failed (from ./node_modules/babel-loader/lib/index.js):
Error: Cannot find module '@babel/core'
babel-loader@8 requires Babel 7.x (the package '@babel/core'). If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.
坑啊,版本不兼容,于是尝试安装babel-loader@7版本...
首先查看babel-loader的可用版本,
npm view babel-loader versions
发现@7的最高版本为7.1.5,于是卸载已安装的babel-loader,并安装该版本的[email protected]
npm uninstall babel-loader && npm install --save-dev [email protected]
执行npm run build
后,在dist
目录下生成以下文件
.
├── css
│ └── index.9803160c.css
├── index.html
└── js
└── index.9803160c.js
执行npm run dev
,打开页面后,点击链接能够切换页面并显示正常,we bpack整合vue.js实现的单页应用初步完成,当然到这一步还不够完善,需要进一步优化。
2.优化和完善
2.1 懒加载
从上面的打包结果可以看出,所有的.vue文件内容都打包进了一个.js和一个.css文件中,最终文件会变得非常大,index.html页面加载时间会很长,影响页面性能和用户体验。
vue-router支持懒加载,将index.js修改如下
import App from './app.vue'
Vue.use(VueRouter);
const Home = resolve => require(['./page/home.vue'], resolve);
const Login = resolve => require(['./page/login.vue'], resolve);
const Register = resolve => require(['./page/register.vue'], resolve);
const routes = [
{ path: '/home', component: Home },
{ path: '/login', component: Login },
{ path: '/register', component: Register }
];
const router = new VueRouter({
mode: 'history',
routes: routes
});
new Vue({
el: '#app',
router: router,
components: { App }
});
再次打包,结果如下:
.
├── css
│ ├── 1.c1ef77a7.css
│ ├── 2.c1ef77a7.css
│ ├── 3.c1ef77a7.css
│ └── index.c1ef77a7.css
├── index.html
└── js
├── 1.c1ef77a7.js
├── 2.c1ef77a7.js
├── 3.c1ef77a7.js
└── index.c1ef77a7.js
2 directories, 9 files
每个.vue中的内容都打包成了独立的文件,各个“页面”内容仅在被请求的时候才加载。
另外,vue组件懒加载还可以使用以下方式
const Home = () => import('./page/home.vue');
const Login = () => import('./page/login.vue');
const Register = () => import('./page/register.vue');
const Setting = () => import('./page/setting.vue');
这是es7的语法, 需要babel的babel-plugin-syntax-dynamic-import插件来支持
npm install --save-dev babel-plugin-syntax-dynamic-import
并且在webpack.config.js
中配置babel-loader
相关选项
...
module.exports = {
mode: 'production',
entry: {
index: './src/index.js'
},
output: {
...
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015'],
plugins: ['transform-runtime', 'syntax-dynamic-import']
}
}
},
...
]
},
plugins: [
...
],
devServer: {
...
}
};
2.2 vue-router的更多路由控制
默认路由
设置默认路由只需将path设为''
或者'/'
,另外还可以将path设为'*'
,匹配不到其它URL时则使用该路由。
const routes = [
{ path: '', component: Home},
{ path: '/', component: Home},
{ path: '*', component: Home}
];
路由重定向
将/h
重定向到/home
const routes = [
{ path: '/h', redirect: '/home'},
...
];
嵌套路由
路由可以嵌套,当使用嵌套路由时,子组件会被嵌套在父组件的
中。
对原来的几个.vue组件进行改造,将Login、Register作为子组件嵌套在Home中,另外再加一个Setting作为与Home同一层次的组件。
在嵌套路由中尝试使用懒加载,遇到一个问题,当请求至二级路由时,找不到相应的js及css文件,正常情况下js和css的请求路径为/js/xx.js
和/css/xx.css
,而使用懒加载时,请求路径则变成了/home/js/xx.js
和/home/css/xx.css
,被这个问题深深地坑了一把。
解决方案1:禁用懒加载
不使用懒加载的话所有的js和css都会合并到一起,影响首页加载速度,不是理想的方案。
index.js
import App from './app.vue'
Vue.use(VueRouter);
import Home from './page/home.vue';
import Login from './page/login.vue';
import Register from './page/register.vue';
import Setting from './page/setting.vue';
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Home,
children: [
{ path: 'login', component: Login },
{ path: 'register', component: Register }
]
},
{ path: '/setting', component: Setting }
];
const router = new VueRouter({
mode: 'hash',
routes: routes
});
new Vue({
el: '#app',
router: router,
components: { App }
});
app.vue
App
Home
Setting
home.vue
{{page}}
Login
Register
解决方案2:统一使用一级URI
此时使用/login
访问Login页面,而非/home/login
,如果对URI没有特别的要求可以使用这种方案。
app.vue
import App from './app.vue'
Vue.use(VueRouter);
// const Home = resolve => require(['./page/home.vue'], resolve);
// const Login = resolve => require(['./page/login.vue'], resolve);
// const Register = resolve => require(['./page/register.vue'], resolve);
// const Setting = resolve => require(['./page/setting.vue'], resolve);
const Home = () => import('./page/home.vue');
const Login = () => import('./page/login.vue');
const Register = () => import('./page/register.vue');
const Setting = () => import('./page/setting.vue');
// import Home from './page/home.vue';
// import Login from './page/login.vue';
// import Register from './page/register.vue';
// import Setting from './page/setting.vue';
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Home,
children: [
{ path: '/login', component: Login },
{ path: '/register', component: Register }
]
},
{ path: '/setting', component: Setting }
];
const router = new VueRouter({
mode: 'history',
routes: routes
});
new Vue({
el: '#app',
router: router,
components: { App }
});
home.vue
{{page}}
Login
Register
2.3 整合Bootstrap
首先安装bootstrap及其依赖,目前最新的是4.x版本
npm install --save bootstrap popper.js jquery
在入口index.js中全局引入bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js'
在index.html的
或任意.vue文件的标签内加上一段使用bootstrap样式的html代码# 来自官方文档示例
This is a primary alert—check it out!
This is a secondary alert—check it out!
This is a success alert—check it out!
刷新页面后,bootstrap样式正常显示,这样bootstrap就引入成功了。
关于Vue.js整合Bootstrap模板,请看我的另一篇文章:webpack4+vue2+nifty2.9构建单页应用
2.4 使用axios请求API
参考官方文档 https://www.npmjs.com/package/axios