65

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

 5 years ago
source link: http://www.10tiao.com/html/399/201806/2651496145/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

vue-cli 到多页应用

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

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

第一步:cli 一个 vue 项目

新建一个 vue 项目 官网:

  1. 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. constExtractTextPlugin=require('extract-text-webpack-plugin')

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

  6. //add

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

  8. constHtmlWebpackPlugin=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.      returnExtractTextPlugin.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:newRegExp('\\.'+ 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]= process.env.NODE_ENV ==='development'?

  105.        [

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

  107.          entry

  108.        ]:[entry];

  109.    }

  110.  });

  111.  return entries;

  112. };

  113. //end

  114. //add 新增出口文件

  115. exports.createHtmlWebpackPlugin =(publicModule)=>{

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

  117.  let entries = exports.createEntry();

  118.  let plugins =[];

  119.  let conf;

  120.  let basename;

  121.  let foldername;

  122.  publicModule = publicModule ||[];

  123.  files.forEach(file =>{

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

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

  126.    if(basename === foldername){

  127.      conf ={

  128.        template: file,

  129.        filename: basename +'.html',

  130.        inject:true,

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

  132.      };

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

  134.        conf.chunksSortMode ='dependency';

  135.        conf.minify ={

  136.          removeComments:true,

  137.          collapseWhitespace:true,

  138.          removeAttributeQuotes:true

  139.        };

  140.        // 在构建生产环境时,需要指定共用模块

  141.        conf.chunks =[...publicModule,...conf.chunks];

  142.      }

  143.      plugins.push(newHtmlWebpackPlugin(conf));

  144.    }

  145.  });

  146.  return plugins;

  147. };

  148. //end

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

从 express 新建私服并配置 (build 文件夹下新建 我这里叫 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. //把原来写死的

  2. entry:{

  3.    app:'./src/index.js'

  4.  },

  5. //改为:

  6. entry: utils.createEntry(),

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. constCopyWebpackPlugin=require('copy-webpack-plugin')

  9. constHtmlWebpackPlugin=require('html-webpack-plugin')

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

  11. const portfinder =require('portfinder')

  12. process.env.NODE_ENV ='development';

  13. const HOST = process.env.HOST

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

  15. const devWebpackConfig = merge(baseWebpackConfig,{

  16.  module:{

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

  18.  },

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

  20.  devtool: config.dev.devtool,

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

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

  23.  // devServer: {

  24.  //   clientLogLevel: 'warning',

  25.  //   historyApiFallback: {

  26.  //     rewrites: [

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

  28.  //     ],

  29.  //   },

  30.  //   hot: true,

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

  32.  //   compress: true,

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

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

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

  36.  //   overlay: config.dev.errorOverlay

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

  38.  //     : false,

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

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

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

  42.  //   watchOptions: {

  43.  //     poll: config.dev.poll,

  44.  //   }

  45.  // },

  46.  //end

  47.  plugins:[

  48.    new webpack.DefinePlugin({

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

  50.    }),

  51.    new webpack.HotModuleReplacementPlugin(),

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

  53.    new webpack.NoEmitOnErrorsPlugin(),

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

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

  56.    // new HtmlWebpackPlugin({

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

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

  59.    //   inject: true

  60.    // }),

  61.    //end

  62.    // copy custom static assets

  63.    newCopyWebpackPlugin([

  64.      {

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

  66.        to: config.dev.assetsSubDirectory,

  67.        ignore:['.*']

  68.      }

  69.    ])

  70.  ]

  71.  //add

  72.    .concat(utils.createHtmlWebpackPlugin())

  73.  //end

  74. })

  75. //del

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

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

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

  79. //     if (err) {

  80. //       reject(err)

  81. //     } else {

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

  83. //       process.env.PORT = port

  84. //       // add port to devServer config

  85. //       devWebpackConfig.devServer.port = port

  86. //

  87. //       // Add FriendlyErrorsPlugin

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

  89. //         compilationSuccessInfo: {

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

  91. //         },

  92. //         onErrors: config.dev.notifyOnErrors

  93. //         ? utils.createNotifierCallback()

  94. //         : undefined

  95. //       }))

  96. //

  97. //       resolve(devWebpackConfig)

  98. //     }

  99. //   })

  100. // })

  101. //end

  102. module.exports = devWebpackConfig;

webpack.prod.conf.js

plugins 最后加上. concat(utils.createHtmlWebpackPlugin(['manifest', 'vendor'])) 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 代表多页 每页单独一个文件夹 文件夹下建对应文件

打包改为相对路径 config/index.js build 下面

  1. assetsPublicPath:'/',   =>   assetsPublicPath:'./',

最后,npm run dev 或者 npm run build

测试环境自己配 跟 生产环境差不多,就几个配置参数不一样

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

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'),

还是版本问题

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 一下运行没问题搞定~ 建议而已,你怎么打人,呜呜呜~


原文:https://segmentfault.com/a/1190000015113584 作者:吃葡萄不吐番茄皮



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK