webpack4 - 9.单页应用(Vue.js版)

单页应用简单说就是整个网站只有一个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






src/page/home.vue






src/page/login.vue






src/page/register.vue






然而,执行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


home.vue


解决方案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






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文件的