为何要用多页面应用?javascript
研究了下vue搭建多页面应用,我的感受多页面应用的优点主要就是在打包的时候每一个页面都是单独打包,除了公用的vendor/mainfest文件,每一个页面都只引用本身的样式表和脚本文件。在一个模块更新的时候,也不会影响到其余模块(以下图)。即提升了页面加载速度,也有利于运营维护。另外有SEO需求的话,单页面应用的改造是比较麻烦的,多页面入口则容易的多。关于单页面和多页面详细对比分析详见:https://www.cnblogs.com/xyyt/p/9116827.htmlcss
如上图,只是修改了index模块下的页面,从新打包,只有index相关的css/js文件及mainfest,其余文件都没有改动。html
上边的说明还很差理解?vue
现实生活中的集团和子公司的例子来模拟说明多页面应用是再形象不过——一个集团至关于一个大项目,集团下边每一个子公司都负责一起独立的业务(至关于每一个页面入口)。集团提供了办公场地、办公设备、人员招聘等公共资源,每一个子公司又能够根据本身的须要调整本身内部组织结构,置办本身独有的一些内部资源。每一个子公司都相互独立运行,一个子公司的变更不会影响到其余子公司。就算后边某个子公司发展足够好能够从集团独立出来,或者经营不善面临解散,也只是子公司内部及集团层面有调整,对其余子公司没任何影响,多页面应用的优点也就是这样。java
怎么搭建多页面应用?node
首先,你须要会使用vue-cli脚手架建立一个基于webpack模板的单页面应用项目,须要注意的地方请留意下面代码后边的注释:webpack
# 全局安装 vue-cli npm install --global vue-cli # 建立一个基于 webpack 模板的新项目 vue init webpack my-project # 这里须要进行一些配置,默认回车便可 This will install Vue 2.x version of the template. For Vue 1.x use: vue init webpack#1.0 my-project ? Project name my-project ? Project description A Vue.js project ? Author runoob <test@runoob.com> ? Vue build standalone ? Use ESLint to lint your code? Yes//如不须要eslint,此处输入N回车 ? Pick an ESLint preset Standard//如不须要eslint,此处输入N回车 ? Setup unit tests with Karma + Mocha? Yes//如不须要单元测试,此处输入N回车 ? Setup e2e tests with Nightwatch? Yes//如不须要端对端测试,此处输入N回车 vue-cli · Generated "my-project". To get started: cd my-project npm install npm run dev Documentation can be found at https://vuejs-templates.github.io/webpack
而后,进入项目,安装并运行,确保单页面应用能正常访问:git
cd my-project npm install npm run dev DONE Compiled successfully in 4388ms > Listening at http://localhost:8080
注意:目前最新的模板是没有开启自动打开浏览器访问的,能够去config/index.js中开启——autoOpenBrowser: truegithub
最后,就要进入正题了,开始进行多页面改造了。web
单页面应用改造多页面应用:
1.准备工做_安装glob组件:
glob是webpack安装时依赖的一个第三方模块,该模块容许你使用 *等符号, 例如lib/*.js就是获取lib文件夹下的全部js后缀名的文件。
npm install glob -save-dev
2. 配置文件改造:
须要改动的文件以下:
下面就按照顺序贴出完整的代码内容,在作修改或者添加代码的位置作了中文注释。
utils.js——修改1处
'use strict' const path = require('path') const config = require('../config') const ExtractTextPlugin = require('extract-text-webpack-plugin') const packageConfig = require('../package.json') exports.assetsPath = function (_path) { const assetsSubDirectory = process.env.NODE_ENV === 'production' ? config.build.assetsSubDirectory : config.dev.assetsSubDirectory return path.posix.join(assetsSubDirectory, _path) } exports.cssLoaders = function (options) { options = options || {} const cssLoader = { loader: 'css-loader', options: { minimize: process.env.NODE_ENV === 'production', sourceMap: options.sourceMap } } const postcssLoader = { loader: 'postcss-loader', options: { sourceMap: options.sourceMap } } // generate loader string to be used with extract text plugin function generateLoaders (loader, loaderOptions) { const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] if (loader) { loaders.push({ loader: loader + '-loader', options: Object.assign({}, loaderOptions, { sourceMap: options.sourceMap }) }) } // Extract CSS when that option is specified // (which is the case during production build) if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: 'vue-style-loader' }) } else { return ['vue-style-loader'].concat(loaders) } } // https://vue-loader.vuejs.org/en/configurations/extract-css.html return { css: generateLoaders(), postcss: generateLoaders(), less: generateLoaders('less'), sass: generateLoaders('sass', { indentedSyntax: true }), scss: generateLoaders('sass'), stylus: generateLoaders('stylus'), styl: generateLoaders('stylus') } } // Generate loaders for standalone style files (outside of .vue) exports.styleLoaders = function (options) { const output = [] const loaders = exports.cssLoaders(options) for (const extension in loaders) { const loader = loaders[extension] output.push({ test: new RegExp('\\.' + extension + '$'), use: loader }) } return output } /* 这里是添加的部分 ---------------------------- 开始 */ // glob是webpack安装时依赖的一个第三方模块,还模块容许你使用 *等符号, 例如lib/*.js就是获取lib文件夹下的全部js后缀名的文件 var glob = require('glob') // 页面模板 var HtmlWebpackPlugin = require('html-webpack-plugin') // 取得相应的页面路径,由于以前的配置,因此是src文件夹下的pages文件夹 var PAGE_PATH = path.resolve(__dirname, '../src/pages') // 用于作相应的merge处理 var merge = require('webpack-merge') //多入口配置 // 经过glob模块读取pages文件夹下的全部对应文件夹下的js后缀文件,若是该文件存在 // 那么就做为入口处理 exports.entries = function () { var entryFiles = glob.sync(PAGE_PATH + '/*/*.js') var map = {} entryFiles.forEach((filePath) => { var filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.')) map[filename] = filePath }) return map } //多页面输出配置 // 与上面的多页面入口配置相同,读取pages文件夹下的对应的html后缀文件,而后放入数组中 exports.htmlPlugin = function () { let entryHtml = glob.sync(PAGE_PATH + '/*/*.html') let arr = [] entryHtml.forEach((filePath) => { let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.')) let conf = { // 模板来源 template: filePath, // 文件名称 filename: filename + '.html', // 页面模板须要加对应的js脚本,若是不加这行则每一个页面都会引入全部的js脚本 chunks: ['manifest', 'vendor', filename], inject: true } if (process.env.NODE_ENV === 'production') { conf = merge(conf, { minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true }, chunksSortMode: 'dependency' }) } arr.push(new HtmlWebpackPlugin(conf)) }) return arr } /* 这里是添加的部分 ---------------------------- 结束 */ exports.createNotifierCallback = () => { const notifier = require('node-notifier') return (severity, errors) => { if (severity !== 'error') return const error = errors[0] const filename = error.file && error.file.split('!').pop() notifier.notify({ title: packageConfig.name, message: severity + ': ' + error.name, subtitle: filename || '', icon: path.join(__dirname, 'logo.png') }) } }
webpack.base.conf.js——修改1处
'use strict' const path = require('path') const utils = require('./utils') const config = require('../config') const vueLoaderConfig = require('./vue-loader.conf') function resolve (dir) { return path.join(__dirname, '..', dir) } module.exports = { context: path.resolve(__dirname, '../'), /*修改部分*/ entry:utils.entries(), /*修改部分end*/ output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), '@comp': resolve('src/components'), '@static': resolve('static'), } }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } ] }, node: { // prevent webpack from injecting useless setImmediate polyfill because Vue // source contains it (although only uses it if it's native). setImmediate: false, // prevent webpack from injecting mocks to Node native modules // that does not make sense for the client dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty' } }
webpack.dev.conf.js——修改2处
'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin /*注释掉的文件*/ // new HtmlWebpackPlugin({ // filename: 'index.html', // template: 'index.html', // inject: true // }), /*注释掉的文件end*/ // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ].concat(utils.htmlPlugin())//追加的代码 }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) })
webpack.prod.conf.js——修改2处
'use strict' const path = require('path') const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const ExtractTextPlugin = require('extract-text-webpack-plugin') const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const env = require('../config/prod.env') const webpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true, usePostCSS: true }) }, devtool: config.build.productionSourceMap ? config.build.devtool : false, output: { path: config.build.assetsRoot, filename: utils.assetsPath('js/[name].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') }, plugins: [ // http://vuejs.github.io/vue-loader/en/workflow/production.html new webpack.DefinePlugin({ 'process.env': env }), new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false } }, sourceMap: config.build.productionSourceMap, parallel: true }), // extract css into its own file new ExtractTextPlugin({ filename: utils.assetsPath('css/[name].[contenthash].css'), // Setting the following option to `false` will not extract CSS from codesplit chunks. // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 allChunks: true, }), // Compress extracted CSS. We are using this plugin so that possible // duplicated CSS from different components can be deduped. new OptimizeCSSPlugin({ cssProcessorOptions: config.build.productionSourceMap ? { safe: true, map: { inline: false } } : { safe: true } }), // generate dist index.html with correct asset hash for caching. // you can customize output by editing /index.html // see https://github.com/ampedandwired/html-webpack-plugin /*注释的代码*/ // new HtmlWebpackPlugin({ // filename: config.build.index, // template: 'index.html', // inject: true, // minify: { // removeComments: true, // collapseWhitespace: true, // removeAttributeQuotes: true // // more options: // // https://github.com/kangax/html-minifier#options-quick-reference // }, // // necessary to consistently work with multiple chunks via CommonsChunkPlugin // chunksSortMode: 'dependency' // }), /*注释的代码end*/ // keep module.id stable when vendor modules does not change new webpack.HashedModuleIdsPlugin(), // enable scope hoisting new webpack.optimize.ModuleConcatenationPlugin(), // split vendor js into its own file new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks (module) { // any required modules inside node_modules are extracted to vendor return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ) } }), // extract webpack runtime and module manifest to its own file in order to // prevent vendor hash from being updated whenever app bundle is updated new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', minChunks: Infinity }), // This instance extracts shared chunks from code splitted chunks and bundles them // in a separate chunk, similar to the vendor chunk // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk new webpack.optimize.CommonsChunkPlugin({ name: 'app', async: 'vendor-async', children: true, minChunks: 3 }), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.build.assetsSubDirectory, ignore: ['.*'] } ]) ].concat(utils.htmlPlugin())//追加的代码 }) if (config.build.productionGzip) { const CompressionWebpackPlugin = require('compression-webpack-plugin') webpackConfig.plugins.push( new CompressionWebpackPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' ), threshold: 10240, minRatio: 0.8 }) ) } if (config.build.bundleAnalyzerReport) { const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin webpackConfig.plugins.push(new BundleAnalyzerPlugin()) } module.exports = webpackConfig
3. 页面改造:
1)src目录下新建pages文件夹,用来存放页面。
2)新建index文件夹做为主页(也能够定义为home).
3)将src目录下的App.vue和main.js以及根目录下的index.html文件通通放到index文件夹中,并将main.js改成index.js(html文件即为打包好直接访问的页面模板,决定了访问的文件路径,最好跟页面文件夹一致,js文件为页面入口,须要与html文件一致;vue文件名不限制,只须要在js文件中正确引入就能够了)。
一个页面文件夹中文件的格式分别以下:
App.vue
<template> <div id="app"> login </div> </template> <script> export default { name: 'App' } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>首页</title> </head> <body> <div id="app"></div> <!-- built files will be auto injected --> <script src="../../../static/zepto.min.js"></script> </body> </html>
index.js
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', components: { App }, template: '<App/>' })
4)其余页面能够直接复用index页面目录下的全部文件,改下名称就好:
4.页面访问及跳转:
从新运行项目,直接访问:
首页:http://localhost:8080/index.html
用户中心:http://localhost:8080/user.html
登录:http://localhost:8080/login.html
页面跳转能够这样写:
<ul> <li> <a href="/login.html"> 登录 </a> </li> <li> <a href="/user.html"> 用户中心 </a> </li> </ul>
4.结语:
到此,一个简单的多页面应用框架已经搭建完成,有兴趣的小伙伴能够尝试下了,建议按照上边的代码本身敲一遍,即便不能彻底明白,也至少知道发生了什么。
git地址:https://github.com/xyytwz/my-vue-multipage
如遇项目编译异常或打包异常,请检查配置文件修改是否按照以上文档修改的地方所有都修改了,每一个文件后边都备注修改处数量了。
后续异常:
1)多页面打包后css中引用的背景图片不显示问题:
找到build/utils.js文件
修改为为以下所示内容:
if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, publicPath:"../../",//添加代码 fallback: 'vue-style-loader' }) } else { return ['vue-style-loader'].concat(loaders) }