最近一直在研究使用vue做出来一些东西,但都是SPA的单页面应用,但实际工作中,单页面并不一定符合业务需求,所以这篇我就来说说怎么开发多页面的Vue应用,以及在这个过程会遇到的问题。
准备工作
在本地用vue-cli新建一个项目,这个步骤vue的官网上有,我就不再说了。
这里有一个地方需要改一下,在执行npm install命令之前,在package.json里添加一个依赖,后面会用到。
修改webpack配置
这里展示一下我的项目目录
1 ├── README.md 2 ├── build 3 │ ├── build.js 4 │ ├── check-versions.js 5 │ ├── dev-client.js 6 │ ├── dev-server.js 7 │ ├── utils.js 8 │ ├── vue-loader.conf.js 9 │ ├── webpack.base.conf.js 10 │ ├── webpack.dev.conf.js 11 │ └── webpack.prod.conf.js 12 ├── config 13 │ ├── dev.env.js 14 │ ├── index.js 15 │ └── prod.env.js 16 ├── package.json 17 ├── src 18 │ ├── assets 19 │ │ └── logo.png 20 │ ├── components 21 │ │ ├── Hello.vue 22 │ │ └── cell.vue 23 │ └── pages 24 │ ├── cell 25 │ │ ├── cell.html 26 │ │ ├── cell.js 27 │ │ └── cell.vue 28 │ └── index 29 │ ├── index.html 30 │ ├── index.js 31 │ ├── index.vue 32 │ └── router 33 │ └── index.js 34 └── static
在这一步里我们需要改动的文件都在build文件下,分别是:
- utils.js
- webpack.base.conf.js
- webpack.dev.conf.js
- webpack.prod.conf.js
我就按照顺序放出完整的文件内容,然后在做修改或添加的位置用注释符标注出来:
utils.js文件
1 // utils.js文件 2 3 var path = require(\'path\') 4 var config = require(\'../config\') 5 var ExtractTextPlugin = require(\'extract-text-webpack-plugin\') 6 7 exports.assetsPath = function (_path) { 8 var assetsSubDirectory = process.env.NODE_ENV === \'production\' ? 9 config.build.assetsSubDirectory : 10 config.dev.assetsSubDirectory 11 return path.posix.join(assetsSubDirectory, _path) 12 } 13 14 exports.cssLoaders = function (options) { 15 options = options || {} 16 17 var cssLoader = { 18 loader: \'css-loader\', 19 options: { 20 minimize: process.env.NODE_ENV === \'production\', 21 sourceMap: options.sourceMap 22 } 23 } 24 25 // generate loader string to be used with extract text plugin 26 function generateLoaders(loader, loaderOptions) { 27 var loaders = [cssLoader] 28 if (loader) { 29 loaders.push({ 30 loader: loader + \'-loader\', 31 options: Object.assign({}, loaderOptions, { 32 sourceMap: options.sourceMap 33 }) 34 }) 35 } 36 37 // Extract CSS when that option is specified 38 // (which is the case during production build) 39 if (options.extract) { 40 return ExtractTextPlugin.extract({ 41 use: loaders, 42 fallback: \'vue-style-loader\' 43 }) 44 } else { 45 return [\'vue-style-loader\'].concat(loaders) 46 } 47 } 48 49 // https://vue-loader.vuejs.org/en/configurations/extract-css.html 50 return { 51 css: generateLoaders(), 52 postcss: generateLoaders(), 53 less: generateLoaders(\'less\'), 54 sass: generateLoaders(\'sass\', { indentedSyntax: true }), 55 scss: generateLoaders(\'sass\'), 56 stylus: generateLoaders(\'stylus\'), 57 styl: generateLoaders(\'stylus\') 58 } 59 } 60 61 // Generate loaders for standalone style files (outside of .vue) 62 exports.styleLoaders = function (options) { 63 var output = [] 64 var loaders = exports.cssLoaders(options) 65 for (var extension in loaders) { 66 var loader = loaders[extension] 67 output.push({ 68 test: new RegExp(\'\\.\' + extension + \'$\'), 69 use: loader 70 }) 71 } 72 return output 73 } 74 75 /* 这里是添加的部分 ---------------------------- 开始 */ 76 77 // glob是webpack安装时依赖的一个第三方模块,还模块允许你使用 *等符号, 例如lib/*.js就是获取lib文件夹下的所有js后缀名的文件 78 var glob = require(\'glob\') 79 // 页面模板 80 var HtmlWebpackPlugin = require(\'html-webpack-plugin\') 81 // 取得相应的页面路径,因为之前的配置,所以是src文件夹下的pages文件夹 82 var PAGE_PATH = path.resolve(__dirname, \'../src/pages\') 83 // 用于做相应的merge处理 84 var merge = require(\'webpack-merge\') 85 86 87 //多入口配置 88 // 通过glob模块读取pages文件夹下的所有对应文件夹下的js后缀文件,如果该文件存在 89 // 那么就作为入口处理 90 exports.entries = function () { 91 var entryFiles = glob.sync(PAGE_PATH + \'/*/*.js\') 92 var map = {} 93 entryFiles.forEach((filePath) => { 94 var filename = filePath.substring(filePath.lastIndexOf(\'\/\') + 1, filePath.lastIndexOf(\'.\')) 95 map[filename] = filePath 96 }) 97 return map 98 } 99 100 //多页面输出配置 101 // 与上面的多页面入口配置相同,读取pages文件夹下的对应的html后缀文件,然后放入数组中 102 exports.htmlPlugin = function () { 103 let entryHtml = glob.sync(PAGE_PATH + \'/*/*.html\') 104 let arr = [] 105 entryHtml.forEach((filePath) => { 106 let filename = filePath.substring(filePath.lastIndexOf(\'\/\') + 1, filePath.lastIndexOf(\'.\')) 107 let conf = { 108 // 模板来源 109 template: filePath, 110 // 文件名称 111 filename: filename + \'.html\', 112 // 页面模板需要加对应的js脚本,如果不加这行则每个页面都会引入所有的js脚本 113 chunks: [\'manifest\', \'vendor\', filename], 114 inject: true 115 } 116 if (process.env.NODE_ENV === \'production\') { 117 conf = merge(conf, { 118 minify: { 119 removeComments: true, 120 collapseWhitespace: true, 121 removeAttributeQuotes: true 122 }, 123 chunksSortMode: \'dependency\' 124 }) 125 } 126 arr.push(new HtmlWebpackPlugin(conf)) 127 }) 128 return arr 129 } 130 /* 这里是添加的部分 ---------------------------- 结束 */
webpack.base.conf.js 文件
1 // webpack.base.conf.js 文件 2 3 var path = require(\'path\') 4 var utils = require(\'./utils\') 5 var config = require(\'../config\') 6 var vueLoaderConfig = require(\'./vue-loader.conf\') 7 8 function resolve(dir) { 9 return path.join(__dirname, \'..\', dir) 10 } 11 12 module.exports = { 13 /* 修改部分 ---------------- 开始 */ 14 entry: utils.entries(), 15 /* 修改部分 ---------------- 结束 */ 16 output: { 17 path: config.build.assetsRoot, 18 filename: \'[name].js\', 19 publicPath: process.env.NODE_ENV === \'production\' ? 20 config.build.assetsPublicPath : 21 config.dev.assetsPublicPath 22 }, 23 resolve: { 24 extensions: [\'.js\', \'.vue\', \'.json\'], 25 alias: { 26 \'vue$\': \'vue/dist/vue.esm.js\', 27 \'@\': resolve(\'src\'), 28 \'pages\': resolve(\'src/pages\'), 29 \'components\': resolve(\'src/components\') 30 } 31 }, 32 module: { 33 rules: [{ 34 test: /\.vue$/, 35 loader: \'vue-loader\', 36 options: vueLoaderConfig 37 }, 38 { 39 test: /\.js$/, 40 loader: \'babel-loader\', 41 include: [resolve(\'src\'), resolve(\'test\')] 42 }, 43 { 44 test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 45 loader: \'url-loader\', 46 options: { 47 limit: 10000, 48 name: utils.assetsPath(\'img/[name].[hash:7].[ext]\') 49 } 50 }, 51 { 52 test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 53 loader: \'url-loader\', 54 options: { 55 limit: 10000, 56 name: utils.assetsPath(\'fonts/[name].[hash:7].[ext]\') 57 } 58 } 59 ] 60 } 61 }
webpack.dev.conf.js 文件
1 var utils = require(\'./utils\') 2 var webpack = require(\'webpack\') 3 var config = require(\'../config\') 4 var merge = require(\'webpack-merge\') 5 var baseWebpackConfig = require(\'./webpack.base.conf\') 6 var HtmlWebpackPlugin = require(\'html-webpack-plugin\') 7 var FriendlyErrorsPlugin = require(\'friendly-errors-webpack-plugin\') 8 9 // add hot-reload related code to entry chunks 10 Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 baseWebpackConfig.entry[name] = [\'./build/dev-client\'].concat(baseWebpackConfig.entry[name]) 12 }) 13 14 module.exports = merge(baseWebpackConfig, { 15 module: { 16 rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 }, 18 // cheap-module-eval-source-map is faster for development 19 devtool: \'#cheap-module-eval-source-map\', 20 plugins: [ 21 new webpack.DefinePlugin({ 22 \'process.env\': config.dev.env 23 }), 24 // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 new webpack.HotModuleReplacementPlugin(), 26 new webpack.NoEmitOnErrorsPlugin(), 27 // https://github.com/ampedandwired/html-webpack-plugin 28 /* 注释这个区域的文件 ------------- 开始 */ 29 // new HtmlWebpackPlugin({ 30 // filename: \'index.html\', 31 // template: \'index.html\', 32 // inject: true 33 // }), 34 /* 注释这个区域的文件 ------------- 结束 */ 35 new FriendlyErrorsPlugin() 36 37 /* 添加 .concat(utils.htmlPlugin()) ------------------ */ 38 ].concat(utils.htmlPlugin()) 39 }) 40 webpack.prod.conf.js 文件 41 var path = require(\'path\') 42 var utils = require(\'./utils\') 43 var webpack = require(\'webpack\') 44 var config = require(\'../config\') 45 var merge = require(\'webpack-merge\') 46 var baseWebpackConfig = require(\'./webpack.base.conf\') 47 var CopyWebpackPlugin = require(\'copy-webpack-plugin\') 48 var HtmlWebpackPlugin = require(\'html-webpack-plugin\') 49 var ExtractTextPlugin = require(\'extract-text-webpack-plugin\') 50 var OptimizeCSSPlugin = require(\'optimize-css-assets-webpack-plugin\') 51 52 var env = config.build.env 53 54 var webpackConfig = merge(baseWebpackConfig, { 55 module: { 56 rules: utils.styleLoaders({ 57 sourceMap: config.build.productionSourceMap, 58 extract: true 59 }) 60 }, 61 devtool: config.build.productionSourceMap ? \'#source-map\' : false, 62 output: { 63 path: config.build.assetsRoot, 64 filename: utils.assetsPath(\'js/[name].[chunkhash].js\'), 65 chunkFilename: utils.assetsPath(\'js/[id].[chunkhash].js\') 66 }, 67 plugins: [ 68 // http://vuejs.github.io/vue-loader/en/workflow/production.html 69 new webpack.DefinePlugin({ 70 \'process.env\': env 71 }), 72 new webpack.optimize.UglifyJsPlugin({ 73 compress: { 74 warnings: false 75 }, 76 sourceMap: true 77 }), 78 // extract css into its own file 79 new ExtractTextPlugin({ 80 filename: utils.assetsPath(\'css/[name].[contenthash].css\') 81 }), 82 // Compress extracted CSS. We are using this plugin so that possible 83 // duplicated CSS from different components can be deduped. 84 new OptimizeCSSPlugin({ 85 cssProcessorOptions: { 86 safe: true 87 } 88 }), 89 // generate dist index.html with correct asset hash for caching. 90 // you can customize output by editing /index.html 91 // see https://github.com/ampedandwired/html-webpack-plugin 92 93 /* 注释这个区域的内容 ---------------------- 开始 */ 94 // new HtmlWebpackPlugin({ 95 // filename: config.build.index, 96 // template: \'index.html\', 97 // inject: true, 98 // minify: { 99 // removeComments: true, 100 // collapseWhitespace: true, 101 // removeAttributeQuotes: true 102 // // more options: 103 // // https://github.com/kangax/html-minifier#options-quick-reference 104 // }, 105 // // necessary to consistently work with multiple chunks via CommonsChunkPlugin 106 // chunksSortMode: \'dependency\' 107 // }), 108 /* 注释这个区域的内容 ---------------------- 结束 */ 109 110 // split vendor js into its own file 111 new webpack.optimize.CommonsChunkPlugin({ 112 name: \'vendor\', 113 minChunks: function (module, count) { 114 // any required modules inside node_modules are extracted to vendor 115 return ( 116 module.resource && 117 /\.js$/.test(module.resource) && 118 module.resource.indexOf( 119 path.join(__dirname, \'../node_modules\') 120 ) === 0 121 ) 122 } 123 }), 124 // extract webpack runtime and module manifest to its own file in order to 125 // prevent vendor hash from being updated whenever app bundle is updated 126 new webpack.optimize.CommonsChunkPlugin({ 127 name: \'manifest\', 128 chunks: [\'vendor\'] 129 }), 130 // copy custom static assets 131 new CopyWebpackPlugin([{ 132 from: path.resolve(__dirname, \'../static\'), 133 to: config.build.assetsSubDirectory, 134 ignore: [\'.*\'] 135 }]) 136 /* 该位置添加 .concat(utils.htmlPlugin()) ------------------- */ 137 ].concat(utils.htmlPlugin()) 138 }) 139 140 if (config.build.productionGzip) { 141 var CompressionWebpackPlugin = require(\'compression-webpack-plugin\') 142 143 webpackConfig.plugins.push( 144 new CompressionWebpackPlugin({ 145 asset: \'[path].gz[query]\', 146 algorithm: \'gzip\', 147 test: new RegExp( 148 \'\\.(\' + 149 config.build.productionGzipExtensions.join(\'|\') + 150 \')$\' 151 ), 152 threshold: 10240, 153 minRatio: 0.8 154 }) 155 ) 156 } 157 158 if (config.build.bundleAnalyzerReport) { 159 var BundleAnalyzerPlugin = require(\'webpack-bundle-analyzer\').BundleAnalyzerPlugin 160 webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 161 } 162 163 module.exports = webpackConfig
至此,webpack的配置就结束了。
但是还没完啦,下面继续。
文件结构
1 ├── src 2 │ ├── assets 3 │ │ └── logo.png 4 │ ├── components 5 │ │ ├── Hello.vue 6 │ │ └── cell.vue 7 │ └── pages 8 │ ├── cell 9 │ │ ├── cell.html 10 │ │ ├── cell.js 11 │ │ └── cell.vue 12 │ └── index 13 │ ├── index.html 14 │ ├── index.js 15 │ ├── index.vue 16 │ └── router 17 │ └── index.js
src就是我所使用的工程文件了,assets,components,pages分别是静态资源文件、组件文件、页面文件。
前两个就不多说,主要是页面文件里,我目前是按照项目的模块分的文件夹,你也可以按照你自己的需求调整。然后在每个模块里又有三个内容:vue文件,js文件和html文件。这三个文件的作用就相当于做spa单页面应用时,根目录的index.html页面模板,src文件下的main.js和app.vue的功能。
原先,入口文件只有一个main.js,但现在由于是多页面,因此入口页面多了,我目前就是两个:index和cell,之后如果打包,就会在dist文件下生成两个HTML文件:index.html和cell.html(可以参考一下单页面应用时,打包只会生成一个index.html,区别在这里)。
cell文件下的三个文件,就是一般模式的配置,参考index的就可以,但并不完全相同。
特别注意的地方
cell.js
在这个文件里,按照写法,应该是这样的吧:
1 import Vue from \'Vue\' 2 import cell from \'./cell.vue\' 3 4 new Vue({ 5 el:\'#app\',// 这里参考cell.html和cell.vue的根节点id,保持三者一致 6 teleplate:\'<cell/>\', 7 components:{ cell } 8 })
这个配置在运行时(npm run dev)会报错
1 [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build. 2 (found in <Root>)
网上的解释是这样的:
运行时构建不包含模板编译器,因此不支持 template 选项,只能用 render 选项,但即使使用运行时构建,在单文件组件中也依然可以写模板,因为单文件组件的模板会在构建时预编译为 render 函数。运行时构建比独立构建要轻量30%,只有 17.14 Kb min+gzip大小。
上面一段是官方api中的解释。就是说,如果我们想使用template,我们不能直接在客户端使用npm install之后的vue。
也给出了相应的修改方案:
1 resolve: { alias: { \'vue\': \'vue/dist/vue.js\' } }
这里是修改package.json的resolve下的vue的配置,很多人反应这样修改之后就好了,但是我按照这个方法修改之后依然报错。然后我就想到上面提到的render函数,因此我的修改是针对cell.js文件的。
1 import Vue from \'Vue\' 2 import cell from \'./cell.vue\' 3 4 /* eslint-disable no-new */ 5 new Vue({ 6 el: \'#app\', 7 render: h => h(cell) 8 })
这里面我用render函数取代了组件的写法,在运行就没问题了。
页面跳转
既然是多页面,肯定涉及页面之间的互相跳转,就按照我这个项目举例,从index.html文件点击a标签跳转到cell.html。
我最开始写的是:
1 <!-- index.html --> 2 <a href=\'../cell/cell.html\'></a>
但这样写,不论是在开发环境还是最后测试,都会报404,找不到这个页面。
改成这样既可:
1 <!-- index.html --> 2 <a href=\'cell.html\'></a>
这样他就会自己找cell.html这个文件。
打包后的资源路径
执行npm run build之后,打开相应的html文件你是看不到任何东西的,查看原因是找不到相应的js文件和css文件。
这时候的文件结构是这样的:
1 ├── dist 2 │ ├── js 3 │ ├── css 4 │ ├── index.html 5 │ └── cell.html
查看index.html文件之后会发现资源的引用路径是:
/dist/js.........
这样,如果你的dist文件不是在根目录下的,就根本找不到资源。
方法当然也有啦,如果你不嫌麻烦,就一个文件一个文件的修改路径咯,或者像我一样偷懒,修改config下的index.js文件。具体的做法是:
1 build: { 2 env: require(\'./prod.env\'), 3 index: path.resolve(__dirname, \'../dist/index.html\'), 4 assetsRoot: path.resolve(__dirname, \'../dist\'), 5 assetsSubDirectory: \'static\', 6 assetsPublicPath: \'/\', 7 productionSourceMap: true, 8 // Gzip off by default as many popular static hosts such as 9 // Surge or Netlify already gzip all static assets for you. 10 // Before setting to `true`, make sure to: 11 // npm install --save-dev compression-webpack-plugin 12 productionGzip: false, 13 productionGzipExtensions: [\'js\', \'css\'], 14 // Run the build command with an extra argument to 15 // View the bundle analyzer report after build finishes: 16 // `npm run build --report` 17 // Set to `true` or `false` to always turn it on or off 18 bundleAnalyzerReport: process.env.npm_config_report 19 },
将这里面的
1 assetsPublicPath: \'/\',
改成
1 assetsPublicPath: \'./\',
以上内容就是实际项目运用的,这就可以啦,在重新npm run build 试试看