微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

手把手教你 vue-cli 单页到多页应用

2018-06-03 21:07 前端大全

(点击上方公众号,可快速关注)


作者:吃葡萄不吐番茄皮

segmentfault.com/a/1190000015113584


前言

我有一个cli创建的vue项目,但是我想做成多页应用,怎么办,废话不多说,直接开撸~

约定:新增代码部分在//add和//end中间 删除(注释)代码部分在//del和//end中间,很多东西都写在注释里

第一步:cli一个vue项目

新建一个vue项目,官网:

vue init webpack demo

cli默认使用webpack的dev-server服务,这个服务是做不了单页的,需要手动建一个私服叫啥你随意 一般叫dev.server或者dev.client。

第二步:添加两个方法处理出口入口文件(SPA默认写死的)

进入刚刚创建vue项目:

 
           
  1. cd demo

在目录下面找到 ·build/utils.js· 文件,修改部分 utils.js

 
           
  1. 'use strict'

  2. const path = require('path')

  3. const config = require('../config')

  4. const ExtractTextPlugin = require('extract-text-webpack-plugin')

  5. const packageConfig = require('../package.json')

  6. //add

  7. const glob = require('glob');

  8. const HtmlWebpackPlugin = require('html-webpack-plugin');   //功能:生成html文件及js文件并把js引入html

  9. const pagePath = path.resolve(__dirname, '../src/views/');  //页面的路径,比如这里我用的views,那么后面私服加入的文件监控器就会从src下面的views下面开始监控文件

  10. //end

  11. exports.assetsPath = function (_path) {

  12.  const assetsSubDirectory = process.env.NODE_ENV === 'production'

  13.    ? config.build.assetsSubDirectory

  14.    : config.dev.assetsSubDirectory

  15.  return path.posix.join(assetsSubDirectory, _path)

  16. }

  17. exports.cssLoaders = function (options) {

  18.  options = options || {}

  19.  const cssLoader = {

  20.    loader: 'css-loader',

  21.    options: {

  22.      sourceMap: options.sourceMap

  23.    }

  24.  }

  25.  const postcssLoader = {

  26.    loader: 'postcss-loader',

  27.    options: {

  28.      sourceMap: options.sourceMap

  29.    }

  30.  }

  31.  // generate loader string to be used with extract text plugin

  32.  function generateLoaders (loader, loaderOptions) {

  33.    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

  34.    if (loader) {

  35.      loaders.push({

  36.        loader: loader + '-loader',

  37.        options: Object.assign({}, loaderOptions, {

  38.          sourceMap: options.sourceMap

  39.        })

  40.      })

  41.    }

  42.    // Extract CSS when that option is specified

  43.    // (which is the case during production build)

  44.    if (options.extract) {

  45.      return ExtractTextPlugin.extract({

  46.        use: loaders,

  47.        fallback: 'vue-style-loader'

  48.      })

  49.    } else {

  50.      return ['vue-style-loader'].concat(loaders)

  51.    }

  52.  }

  53.  // https://vue-loader.vuejs.org/en/configurations/extract-css.html

  54.  return {

  55.    css: generateLoaders(),

  56.    postcss: generateLoaders(),

  57.    less: generateLoaders('less'),

  58.    sass: generateLoaders('sass', { indentedSyntax: true }),

  59.    scss: generateLoaders('sass'),

  60.    stylus: generateLoaders('stylus'),

  61.    styl: generateLoaders('stylus')

  62.  }

  63. }

  64. // Generate loaders for standalone style files (outside of .vue)

  65. exports.styleLoaders = function (options) {

  66.  const output = []

  67.  const loaders = exports.cssLoaders(options)

  68.  for (const extension in loaders) {

  69.    const loader = loaders[extension]

  70.    output.push({

  71.      test: new RegExp('\\.' + extension + '$'),

  72.      use: loader

  73.    })

  74.  }

  75.  return output

  76. }

  77. exports.createNotifierCallback = () => {

  78.  const notifier = require('node-notifier')

  79.  return (severity, errors) => {

  80.    if (severity !== 'error') return

  81.    const error = errors[0]

  82.    const filename = error.file && error.file.split('!').pop()

  83.    notifier.notify({

  84.      title: packageConfig.name,

  85.      message: severity + ': ' + error.name,

  86.      subtitle: filename || '',

  87.      icon: path.join(__dirname, 'logo.png')

  88.    })

  89.  }

  90. }

  91. //add  新增一个方法处理入口文件(单页应用的入口都是写死,到时候替换成这个方法)

  92. exports.createEntry = () => {

  93.  let files = glob.sync(pagePath + '/**/*.js');

  94.  let entries = {};

  95.  let basename;

  96.  let foldername;

  97.  files.forEach(entry => {

  98.    // Filter the router.js

  99.    basename = path.basename(entry, path.extname(entry), 'router.js');

  100.    foldername = path.dirname(entry).split('/').splice(-1)[0];

  101.    // If foldername not equal basename, doing nothing

  102.    // The folder maybe contain more js files, but only the same name is main

  103.    if (basename === foldername) {

  104.      entries[basename] = [

  105.        'webpack-hot-middleware/client?noInfo=true&reload=true&path=/__webpack_hmr&timeout=20000',

  106.        entry];

  107.    }

  108.  });

  109.  return entries;

  110. };

  111. //end

  112. //add 新增出口文件

  113. exports.createHtmlWebpackPlugin = () => {

  114.  let files = glob.sync(pagePath + '/**/*.html', {matchBase: true});

  115.  let entries = exports.createEntry();

  116.  let plugins = [];

  117.  let conf;

  118.  let basename;

  119.  let foldername;

  120.  files.forEach(file => {

  121.    basename = path.basename(file, path.extname(file));

  122.    foldername = path.dirname(file).split('/').splice(-1).join('');

  123.    if (basename === foldername) {

  124.      conf = {

  125.        template: file,

  126.        filename: basename + '.html',

  127.        inject: true,

  128.        chunks: entries[basename] ? [basename] : []

  129.      };

  130.      if (process.env.NODE_ENV !== 'development') {

  131.        conf.chunksSortMode = 'dependency';

  132.        conf.minify = {

  133.          removeComments: true,

  134.          collapseWhitespace: true,

  135.          removeAttributeQuotes: true

  136.        };

  137.      }

  138.      plugins.push(new HtmlWebpackPlugin(conf));

  139.    }

  140.  });

  141.  return plugins;

  142. };

  143. //end

第三步:创建私服(不使用dev-server服务,自己建一个)

从express新建私服并配置(build文件夹下新建,我这里叫webpack.dev.client.js)webpack.dev.client.js:

 
           
  1. /**

  2. * created by qbyu2 on 2018-05-30

  3. * express 私服

  4. * */

  5. 'use strict';

  6. const fs = require('fs');

  7. const path = require('path');

  8. const express = require('express');

  9. const webpack = require('webpack');

  10. const webpackDevMiddleware = require('webpack-dev-middleware');   //文件监控(前面配置了从views下面监控)

  11. const webpackHotMiddleware = require('webpack-hot-middleware');   //热加载

  12. const config = require('../config');

  13. const devWebpackConfig = require('./webpack.dev.conf');

  14. const proxyMiddleware = require('http-proxy-middleware');   //跨域

  15. const proxyTable = config.dev.proxyTable;

  16. const PORT = config.dev.port;

  17. const HOST = config.dev.host;

  18. const assetsRoot = config.dev.assetsRoot;

  19. const app = express();

  20. const router = express.Router();

  21. const compiler = webpack(devWebpackConfig);

  22. let devMiddleware  = webpackDevMiddleware(compiler, {

  23.  publicPath: devWebpackConfig.output.publicPath,

  24.  quiet: true,

  25.  stats: {

  26.    colors: true,

  27.    chunks: false

  28.  }

  29. });

  30. let hotMiddleware = webpackHotMiddleware(compiler, {

  31.  path: '/__webpack_hmr',

  32.  heartbeat: 2000

  33. });

  34. app.use(hotMiddleware);

  35. app.use(devMiddleware);

  36. Object.keys(proxyTable).forEach(function (context) {

  37.  let options = proxyTable[context];

  38.  if (typeof options === 'string') {

  39.    options = {

  40.      target: options

  41.    };

  42.  }

  43.  app.use(proxyMiddleware(context, options));

  44. });

  45. //双路由   私服一层控制私服路由    vue的路由控制该页面下的路由

  46. app.use(router)

  47. app.use('/static', express.static(path.join(assetsRoot, 'static')));

  48. let sendFile = (viewname, response, next) => {

  49.  compiler.outputFileSystem.readFile(viewname, (err, result) => {

  50.    if (err) {

  51.      return (next(err));

  52.    }

  53.    response.set('content-type', 'text/html');

  54.    response.send(result);

  55.    response.end();

  56.  });

  57. };

  58. //拼接方法

  59. function pathJoin(patz) {

  60.  return path.join(assetsRoot, patz);

  61. }

  62. /**

  63. * 定义路由(私服路由 非vue路由)

  64. * */

  65. // favicon

  66. router.get('/favicon.ico', (req, res, next) => {

  67.  res.end();

  68. });

  69. // http://localhost:8080/

  70. router.get('/', (req, res, next)=>{

  71.  sendFile(pathJoin('index.html'), res, next);

  72. });

  73. // http://localhost:8080/home

  74. router.get('/:home', (req, res, next) => {

  75.  sendFile(pathJoin(req.params.home + '.html'), res, next);

  76. });

  77. // http://localhost:8080/index

  78. router.get('/:index', (req, res, next) => {

  79.  sendFile(pathJoin(req.params.index + '.html'), res, next);

  80. });

  81. module.exports = app.listen(PORT, err => {

  82.  if (err){

  83.    return

  84.  }

  85.  console.log(`Listening at http://${HOST}:${PORT}\n`);

  86. })

私服创建好了,安装下依赖。有坑。。。

webpack和热加载版本太高太低都不行

 
           
  1. npm install webpack@3.10.0 --save-dev

  2. npm install webpack-dev-middleware --save-dev

  3. npm install webpack-hot-middleware@2.21.0 --save-dev

  4. npm install http-proxy-middleware --save-dev

第四步:修改配置

webpack.base.conf.js:

 
           
  1. 'use strict'

  2. const utils = require('./utils')

  3. const webpack = require('webpack')

  4. const config = require('../config')

  5. const merge = require('webpack-merge')

  6. const path = require('path')

  7. const baseWebpackConfig = require('./webpack.base.conf')

  8. const CopyWebpackPlugin = require('copy-webpack-plugin')

  9. const HtmlWebpackPlugin = require('html-webpack-plugin')

  10. const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

  11. const portfinder = require('portfinder')

  12. const HOST = process.env.HOST

  13. const PORT = process.env.PORT && Number(process.env.PORT)

  14. const devWebpackConfig = merge(baseWebpackConfig, {

  15.  module: {

  16.    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })

  17.  },

  18.  // cheap-module-eval-source-map is faster for development

  19.  devtool: config.dev.devtool,

  20.  // these devServer options should be customized in /config/index.js

  21.  devServer: {

  22.    clientLogLevel: 'warning',

  23.    historyApiFallback: {

  24.      rewrites: [

  25.        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },

  26.      ],

  27.    },

  28.    hot: true,

  29.    contentBase: false, // since we use CopyWebpackPlugin.

  30.    compress: true,

  31.    host: HOST || config.dev.host,

  32.    port: PORT || config.dev.port,

  33.    open: config.dev.autoOpenBrowser,

  34.    overlay: config.dev.errorOverlay

  35.      ? { warnings: false, errors: true }

  36.      : false,

  37.    publicPath: config.dev.assetsPublicPath,

  38.    proxy: config.dev.proxyTable,

  39.    quiet: true, // necessary for FriendlyErrorsPlugin

  40.    watchOptions: {

  41.      poll: config.dev.poll,

  42.    }

  43.  },

  44.  plugins: [

  45.    new webpack.DefinePlugin({

  46.      'process.env': require('../config/dev.env')

  47.    }),

  48.    new webpack.HotModuleReplacementPlugin(),

  49.    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.

  50.    new webpack.NoEmitOnErrorsPlugin(),

  51.    // https://github.com/ampedandwired/html-webpack-plugin

  52.    //del   注释掉spa固定的单页出口  末尾动态配上出口

  53.    // new HtmlWebpackPlugin({

  54.    //   filename: 'index.html',

  55.    //   template: 'index.html',

  56.    //   inject: true

  57.    // }),

  58.    //end

  59.    // copy custom static assets

  60.    new CopyWebpackPlugin([

  61.      {

  62.        from: path.resolve(__dirname, '../static'),

  63.        to: config.dev.assetsSubDirectory,

  64.        ignore: ['.*']

  65.      }

  66.    ])

  67.  ]

  68.  //add

  69.    .concat(utils.createHtmlWebpackPlugin())

  70.  //end

  71. })

  72. //del

  73. // module.exports = new Promise((resolve, reject) => {

  74. //   portfinder.basePort = process.env.PORT || config.dev.port

  75. //   portfinder.getPort((err, port) => {

  76. //     if (err) {

  77. //       reject(err)

  78. //     } else {

  79. //       // publish the new Port, necessary for e2e tests

  80. //       process.env.PORT = port

  81. //       // add port to devServer config

  82. //       devWebpackConfig.devServer.port = port

  83. //

  84. //       // Add FriendlyErrorsPlugin

  85. //       devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({

  86. //         compilationSuccessInfo: {

  87. //           messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],

  88. //         },

  89. //         onErrors: config.dev.notifyOnErrors

  90. //         ? utils.createNotifierCallback()

  91. //         : undefined

  92. //       }))

  93. //

  94. //       resolve(devWebpackConfig)

  95. //     }

  96. //   })

  97. // })

  98. //end

webpack.dev.conf.js:

 
           
  1. 'use strict'

  2. const utils = require('./utils')

  3. const webpack = require('webpack')

  4. const config = require('../config')

  5. const merge = require('webpack-merge')

  6. const path = require('path')

  7. const baseWebpackConfig = require('./webpack.base.conf')

  8. const CopyWebpackPlugin = require('copy-webpack-plugin')

  9. const HtmlWebpackPlugin = require('html-webpack-plugin')

  10. const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

  11. const portfinder = require('portfinder')

  12. const HOST = process.env.HOST

  13. const PORT = process.env.PORT && Number(process.env.PORT)

  14. const devWebpackConfig = merge(baseWebpackConfig, {

  15.  module: {

  16.    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })

  17.  },

  18.  // cheap-module-eval-source-map is faster for development

  19.  devtool: config.dev.devtool,

  20.  // these devServer options should be customized in /config/index.js

  21.  //del  注掉SPA的服务器

  22.  // devServer: {

  23.  //   clientLogLevel: 'warning',

  24.  //   historyApiFallback: {

  25.  //     rewrites: [

  26.  //       { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },

  27.  //     ],

  28.  //   },

  29.  //   hot: true,

  30.  //   contentBase: false, // since we use CopyWebpackPlugin.

  31.  //   compress: true,

  32.  //   host: HOST || config.dev.host,

  33.  //   port: PORT || config.dev.port,

  34.  //   open: config.dev.autoOpenBrowser,

  35.  //   overlay: config.dev.errorOverlay

  36.  //     ? { warnings: false, errors: true }

  37.  //     : false,

  38.  //   publicPath: config.dev.assetsPublicPath,

  39.  //   proxy: config.dev.proxyTable,

  40.  //   quiet: true, // necessary for FriendlyErrorsPlugin

  41.  //   watchOptions: {

  42.  //     poll: config.dev.poll,

  43.  //   }

  44.  // },

  45.  //end

  46.  plugins: [

  47.    new webpack.DefinePlugin({

  48.      'process.env': require('../config/dev.env')

  49.    }),

  50.    new webpack.HotModuleReplacementPlugin(),

  51.    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.

  52.    new webpack.NoEmitOnErrorsPlugin(),

  53.    // https://github.com/ampedandwired/html-webpack-plugin

  54.    //del   注释掉spa固定的单页出口  末尾动态配上出口

  55.    // new HtmlWebpackPlugin({

  56.    //   filename: 'index.html',

  57.    //   template: 'index.html',

  58.    //   inject: true

  59.    // }),

  60.    //end

  61.    // copy custom static assets

  62.    new CopyWebpackPlugin([

  63.      {

  64.        from: path.resolve(__dirname, '../static'),

  65.        to: config.dev.assetsSubDirectory,

  66.        ignore: ['.*']

  67.      }

  68.    ])

  69.  ]

  70.  //add

  71.    .concat(utils.createHtmlWebpackPlugin())

  72.  //end

  73. })

  74. //del

  75. // module.exports = new Promise((resolve, reject) => {

  76. //   portfinder.basePort = process.env.PORT || config.dev.port

  77. //   portfinder.getPort((err, port) => {

  78. //     if (err) {

  79. //       reject(err)

  80. //     } else {

  81. //       // publish the new Port, necessary for e2e tests

  82. //       process.env.PORT = port

  83. //       // add port to devServer config

  84. //       devWebpackConfig.devServer.port = port

  85. //

  86. //       // Add FriendlyErrorsPlugin

  87. //       devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({

  88. //         compilationSuccessInfo: {

  89. //           messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],

  90. //         },

  91. //         onErrors: config.dev.notifyOnErrors

  92. //         ? utils.createNotifierCallback()

  93. //         : undefined

  94. //       }))

  95. //

  96. //       resolve(devWebpackConfig)

  97. //     }

  98. //   })

  99. // })

  100. //end

  101. module.exports = devWebpackConfig;

webpack.prod.conf.js:plugins最后加上 .concat(utils.createHtmlWebpackPlugin())

test环境一样

第五步:修改package.json 指令配置

scripts下面 dev,这样执行的时候就不会走默认的dev-server而走你的私服了。

 
           
  1. "scripts": {

  2.    "dev": "node build/webpack.dev.client.js",

  3.    "start": "npm run dev",

  4.    "build": "node build/build.js"

  5.  },

第六步:创建测试文件

src目录下新建views文件夹(代码注释里有,当时配的目录跟这个一致就可以,随便你命名,遵循命名规范就行),views文件夹下新建两个文件夹index和home,代表多页,每页单独一个文件夹,文件夹下建对应文件。

最后,

 
           
  1. npm run dev

这个时候你会发现,特么的什么鬼文章,报错了啊。稍安勿躁~两个地方:

1、webpack.dev.client.js

 
           
  1. //双路由   私服一层控制私服路由    vue的路由控制该页面下的路由

  2. app.use(router)

  3. app.use('/static', express.static(path.join(assetsRoot, 'static')));

这个assetsRoot cli创建的时候是没有的 在config/index.js 下面找到dev加上

 
           
  1. assetsRoot: path.resolve(__dirname, '../dist'),

2、还是版本问题

webpack-dev-middleware默认是3.1.3版本但是会报错,具体哪个版本不报错我也不知道。

 
           
  1. context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid);

找不到invalid,源码里面是有的。卸载webpack-dev-middleware:

 
           
  1. npm uninstall webpack-dev-middleware

使用dev-server自带的webpack-dev-middleware(cli单页应用是有热加载的),重新install dev-server:

 
           
  1. npm install webpack-dev-server@2.10.0 --save-dev

 
           
  1. npm run dev

总结

核心点就在创建并配置私服和修改出口入口配置,坑就在版本不兼容。建议:cli一个vue的demo项目,从头撸一遍,再在实际项目里使用,而不是copy一下运行没问题搞定~

建议而已,你怎么打人,呜呜呜~


觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

 
前端大全 更多文章 没学过线代也能读懂的CSS3 matrix Web 前端面试指南与高频考题解析小册介绍 拿 Proxy 可以做哪些有意思的事儿 Firefox 开始支持 Web 组件技术 页面可视化搭建工具的前生今世
猜您喜欢 当数学家遇上大数据 Ruby:《Ruby 元编程》读书笔记(三) 性能分析的基本概念:QPS\/TPS 如何做数据分析挖掘—以电信行业为例 再议携程Android动态加载框架DynamicAPK