爲何要用多頁面應用?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) }