美文网首页汇总
基于 Webpack4 + Vue 的多页应用解决方案(一)

基于 Webpack4 + Vue 的多页应用解决方案(一)

作者: 柏丘君 | 来源:发表于2018-07-10 21:31 被阅读363次

    本项目 GitHub 仓库地址:https://github.com/charleylla/charley-vue-multi

    去年九月份,我写了一个使用 Webpack3 配置多页的系列文章,下面是文章地址:
    使用 webpack3 配置多页应用(一)
    使用 webpack3 配置多页应用(二)
    使用 webpack3 配置多页应用(三)
    使用 webpack3 配置多页应用(四)

    在这之前,我还写过一个使用 Webpack2 搭建 React 应用脚手架的文章:
    搭建基于 webpack2 的 react 脚手架

    现在,我去了新的公司,公司需要我搭建一个多页应用脚手架,作为公司多页应用的通用解决方案。由于公司的主要技术栈是 Vue,我就琢磨着使用 Webpack + Vue 搭建这样一个脚手架。
    对于 Vue 的多页应用方案,网上有很多文章,其中有一些是基于 vue-cli 生成的单页应用进行修改,我看了这些文章觉得有点麻烦,正好之前也搭过类似的脚手架,就决定再重复造个轮子。
    一年间,很多朋友阅读了我的 《使用 webpack3 配置多页应用》系列文章,并提供了一些宝贵的建议,我将这些建议应用到了新的多页应用脚手架中。新的脚手架,使用了 Webpack4。如果不打算使用 Zero Config(事实上使用自定义配置的灵活性更强,可以完成更多个性化的需求),Webpack4 和 Webpack3 没有太大的区别,但还是有部分配置发生了变化,我在写文章的时候会指出来。
    Webpack 的配置确实麻烦,从 Webpack2 到 Webpack4,每次配置都要重新学习一些知识,幸运的是每次学习的时间都比上一次要少。搭建 Webpack2 的脚手架,花了大概两个星期,搭建 Webpack3 的脚手架,花了不到一周,而搭建 Webpack4 的脚手架,花了大约两天时间。这说明 Webpack2 到 Webpack4 在配置层面变动并不大(内核层面变动很大)多配置几次,就慢慢熟悉了。
    建议大家先看 《使用 webpack3 配置多页应用》这系列的文章,可以对使用 Webpack 构建多页应用和常用 Loader 及 Babel 等工具的用法有个了解,本系列文章是在前面文章的基础上进行整合改进的结果,再包含了一些前端架构上的内容。

    项目目录一览

    下面是本项目的一个整体目录及说明:

    │  .babelrc
    │  .editorconfig
    │  .eslintrc.js
    │  .gitignore
    │  package-lock.json
    │  package.json
    │  postcss.config.js
    │  README.md
    │  webpack.config.js
    │
    ├─build                         —— Webpack 配置文件
    │      alias.js
    │      bundle.js
    │      externals.js
    │      loaders.js
    │      webpack.config.base.js
    │      webpack.config.dev.js
    │      webpack.config.prod.js
    │
    ├─doc                           —— 项目文档
    │      README.md
    │
    ├─src                           —— 项目源代码
    │  ├─assets                     —— 通用的库/图片/样式文件 
    │  │  ├─image
    │  │  ├─lib
    │  │  └─style
    │  │          atom.scss
    │  │          constant.scss
    │  │          func.scss
    │  │          main.scss
    │  │
    │  ├─component                  —— 组件
    │  │  └─test
    │  │          index.js
    │  │          style.scss
    │  │          template.vue
    │  │
    │  ├─model                      —— 数据,接口请求
    │  │      index.js
    │  │
    │  ├─page                       —— 页面
    │  │  ├─home                    —— 每个页面一个文件夹,文件夹名字为页面名字
    │  │  │      index.js
    │  │  │      style.scss
    │  │  │      template.vue
    │  │  │
    │  │  └─index
    │  │          index.js
    │  │          style.scss
    │  │          template.vue
    │  │
    │  ├─shared                     —— 公用配置,如 API,常量等
    │  │      api.js
    │  │      constant.js
    │  │
    │  └─util                       —— 帮助,封装通用的方法
    │          index.js
    │          request.js
    │
    └─template                      —— 项目模板文件
            config.js
            favicon.ico
            index.html
    

    基础配置

    下面介绍几个基础配置,主要是 Babel,ESLint 和 PostCSS 的配置内容。

    • .babelrc文件
    {
      "presets": [
        "env"
      ],
      "plugins": [
        [
          "transform-runtime",
          {
            "polyfill": false,
            "regenerator": true
          }
        ]
      ]
    }
    
    • postcss.config.js 文件
    module.exports = {
      plugins: {
        "autoprefixer": {
          browsers: ["last 5 version","Android >= 4.0"],
          //是否美化属性值 默认:true
          cascade: true,
          //是否去掉不必要的前缀 默认:true
          remove: true
        }
      }
    }
    
    • .eslintrc.js 文件
    module.exports = {
      "env": {
        "browser": true,
        "commonjs": true,
        "es6": true,
        "node": true,
      },
      "extends": [
        "eslint:recommended",
        "plugin:vue/essential"
      ],
      "parserOptions": {
        "ecmaVersion": 8,
        "sourceType": "module"
      },
      "rules": {
        "comma-dangle": ["warn", "always-multiline"],
        "indent": ["warn", 2],
        "linebreak-style": ["warn", "unix"],
        "quotes": ["warn", "double"],
        "semi": ["warn", "always"],
        "no-unused-vars": ["warn"],
        "no-console": "warn",
      },
    };
    
    • .editorconfig 文件
    root = true
    
    [*]
    charset = utf-8
    indent_style = space
    indent_size = 2
    end_of_line = lf
    insert_final_newline = true
    trim_trailing_whitespace = true
    
    

    Webpack 配置文件

    下面介绍脚手架 Webpack 的配置,也是本文的重点。

    入口文件

    使用根目录下的 webpack.config.js 作为 Webpack 配置的入口文件:

    const env = process.env.ENVIROMENT.trim();
    const option = process.env.OPTION ? process.env.OPTION.trim() : "";
    const webpackConfigFn = require(`./build/webpack.config.${env}`);
    module.exports = webpackConfigFn(env,{ option })
    

    首先,获取两个环境变量:ENVIROMENTOPTION,这两个环境变量在 package.json 中,通过 cross-env 传入:

    ...
    "scripts": {
      "dev": "cross-env ENVIROMENT=dev webpack-dev-server --open",
      "build": "cross-env ENVIROMENT=prod webpack",
      "build:report": "cross-env OPTION=report npm run build",
      "serve": "http-server ./dist -o"
    },
    ...
    

    使用 cross-env 是为了解决 Windows 和 Linux 下设置环境变量的方式不一致的问题,通过统一的方式设置环境变量。
    ENVIROMENT 环境变量用来标识生产或者开发环境,OPTION 环境变量用来进行一些选项的设置,后文进行介绍。
    ENVIROMENT 环境变量有两个:devprod,通过这个环境变量决定导入 webpack.config.dev.js 或者 webpack.config.prod.js。
    在 webpack.config.dev.js 和 webpack.config.prod.js 文件中,导出的是一个函数而不是一个配置对象,通过函数的方式具有更大的灵活性,可以接收参数,然后根据参数生成不同的配置文件。

    基础配置文件

    使用 webpack.config.base.js 作为 Webpack 的基础配置文件,包含一些公用的配置,该文件导出的也是一个函数,可以接收环境变量参数。
    下面是 webpack.config.base.js 文件的内容:

    const CleanWebpackPlugin = require("clean-webpack-plugin")
    const VueLoaderPlugin = require("vue-loader/lib/plugin")
    const CopyWebpackPlugin = require("copy-webpack-plugin");
    
    const { initConfig,resolve } = require("./bundle")
    const { initLoader } = require("./loaders")
    const config = {
      devtool: "cheap-module-source-map",
      // 加载器
      module: {
        rules: [],
      },
      resolve:{
        mainFields: ["jsnext:main", "browser", "main"]
      },
      plugins: [
        new CleanWebpackPlugin(["dist"], {
          root: resolve(""),
          verbose: true, //开启在控制台输出信息
          dry: false,
        }),
        new VueLoaderPlugin(),
        new CopyWebpackPlugin([{
                from: resolve("template"),
          to: resolve("dist"),
          ignore:["*.html"]
        }]),
      ],
    }
    
    module.exports = function(env){
      const {
        entry,
        output,
        alias,
        htmlPlugins
      } = initConfig(env)
      const loaders = initLoader(env);
    
      config.entry = entry;
      config.output = output;
      config.resolve.alias = alias;
      config.module.rules.push(...loaders);
      config.plugins.push(...htmlPlugins)
    
      return config;
    }
    

    可见,在 webpack.base.js 中,并没有直接对入口出口以及各种 Loader 以及 Alias 等进行配置,而是从 bundle.js 和 loaders.js 中导入了几个函数,通过为函数传参产生相应的配置,并添加到配置文件上。

    拆分配置文件

    通过 webpack.base.js 文件可以看到,我把一些配置写到 bundle.js 中了。该文件用来产生 Entry,Output,Alias 和 Plugins 的配置项,这样做的好处,首先是减少了 webpack.base.js 文件的大小,然后通过对配置文件进行拆分,使得后期修改配置文件很方便,不用直接和 webpack.base.js 打交道,只需要调整 bundle.js 中的函数即可,扩展性更好一点。
    下面就来看 bundle.js 文件的内容:

    const fs = require("fs")
    const path = require("path")
    const HTMLWebpackPlugin = require("html-webpack-plugin")
    const alias = require("./alias")
    const resolve = (p) => path.resolve(__dirname,"..",p)
    
    const entryDir = resolve("src/page")
    const outputDir = resolve("dist")
    const templatePath = resolve("template/index.html")
    const entryFiles = fs.readdirSync(entryDir)
    const
      entry = {},
      output = {}
      htmlPlugins = [];
    
    
    // Map alias
    function resolveAlias(){
      Object.keys(alias).forEach(attr => {
        const val = alias[attr]
        alias[attr] = resolve(val)
      })
    }
    
    // Handle Entry and Output of Webpack
    function resolveEntryAndOutput(env){
      entryFiles.forEach(dir => {
        entry[dir] = resolve(`${entryDir}/${dir}`)
        if(env === "dev"){
          output.filename = "js/[name].bundle.js";
        }else{
          output.filename = "js/[name].bundle.[hash].js";
        }
        output.path = outputDir;
      })
    }
    
    // Handle HTML Templates
    function combineHTMLWithTemplate(){
      entryFiles.forEach(dir => {
        const htmlPlugin = new HTMLWebpackPlugin({
          filename:`${dir}.html`,
          template:templatePath,
          chunks:[dir,"vendor"]
        })
        htmlPlugins.push(htmlPlugin)
      })
    }
    
    function initConfig(env){
      resolveAlias();
      resolveEntryAndOutput(env);
      combineHTMLWithTemplate();
      return{
        entry,
        output,
        alias,
        htmlPlugins
      }
    }
    
    exports.initConfig = initConfig;
    exports.resolve = resolve;
    

    resolve 方法用来解析路径,对 path.resolve 进行了一层包装。
    alias 是 Webpack 配置的别名,从 alias.js 中导入,用来提供快捷的文件导入,避免 ../../../../xxx 这样的情况。下面是 alias.js 文件的内容:

    const alias = {
      "@component":"src/component",
      "@util":"src/util",
      "@shared":"src/shared",
      "@model":"src/model",
      "@assets":"src/assets",
    }
    
    module.exports = alias;
    

    对于入口和出口文件的设置,首先通过 fs.readdirSync 方法读取 src/page 目录下的子目录,每个子目录都是一个页面,然后根据读取到的目录以及环境变量来设置 Webpack 的 Entry 和 Output。设置 Entry 和 Output 需要用到 resolveEntryAndOutput 方法。
    对于 html-webpack-plugin 插件的模板,也根据 src/page 下的子目录自动生成。
    最后导出一个 initConfig 函数,包含了基础的 Entry,Output,Alias 和 Plugins 配置项。
    然后导出了前面定义的 resolve 方法,供其他文件使用。

    拆分 Loaders

    loaders.js 中包含了各种 Loader,下面是 loaders.js 文件的内容:

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const { resolve } = require("./bundle")
    const vueLoader = {
      test: /\.vue$/,
      use: "vue-loader"
    }
    
    const cssLoader = {
      test: /\.css$/,
      exclude: /node_modules/,
      use: [
        "vue-style-loader",
        "css-loader",
        "postcss-loader"
      ]
    }
    
    const sassLoader = {
      test: /\.scss$/,
      exclude: /node_modules/,
      use: [
        "vue-style-loader",
        "css-loader",
        "sass-loader",
        "postcss-loader",
        {
          loader: "sass-resources-loader",
          options: {
            resources: resolve("src/assets/style/main.scss"),
          },
        }
      ]
    }
    
    const jsLoader = {
      test: /\.js$/,
      exclude: /node_modules/,
      use: ["babel-loader"]
    }
    
    const imgLoader = {
      test: /\.(png|svg|jpg|gif)$/,
      use: {
        loader: "file-loader",
        options: {
          name: "[name].[ext]",
          outputPath:"img"
        }
      }
    }
    
    const fontLoader = {
      test: /\.(woff|woff2|eot|ttf|otf)$/,
      use: {
        loader: "file-loader",
        options: {
          outputPath:"font"
        }
      }
    }
    
    const eslintLoader = {
      test: /\.(js|vue)$/,
      enforce: "pre",
      exclude: /node_modules/,
      loader: "eslint-loader",
      options: {
        fix:true,
        emitWarning:true,
      }
    }
    
    exports.initLoader = function(env){
      const loaders = [];
      if(env !== "dev"){
        cssLoader.use = [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader"
        ];
    
        sassLoader.use = [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "sass-loader",
          "postcss-loader",
          {
            loader: "sass-resources-loader",
            options: {
              resources: resolve("src/assets/style/main.scss"),
            },
          }
        ]
      }else{
        loaders.push(eslintLoader)
      }
    
      loaders.push(
        vueLoader,
        cssLoader,
        sassLoader,
        jsLoader,
        imgLoader,
        fontLoader
      );
    
      return loaders;
    }
    

    在 Webpack4 中,推荐使用 mini-css-extract-plugin 插件来提取 CSS 文件(老版本使用 extract-text-webpack-plugin 进行提取),在暴露的 initLoader 函数中,对环境变量进行了判断,如果是生产环境,就使用 mini-css-extract-plugin 对 CSS 进行提取。
    还有一个 sass-resources-loader,这个 Loader 用来为 SASS 提供全局的变量和函数,Mixins等功能。
    对于图片和字体文件,使用 file-loader 将他们提取到 img 和 font 目录。这里我选择 file-loader 而不是 url-loader 的原因是使用 url-loader 会将图片打包成 Base64,插入到 JS 和 CSS 文件中,导致打包后的 JS 和 CSS 文件过大。

    Externals 配置

    使用 Externals,可以让 Webpack 打包时忽略某些库,直接使用 CDN 上的资源,有效的减轻了打包文件的体积。
    Externals 配置放在 externals.js 文件中:

    exports.externals = {
      "vue": "Vue",
      "axios": "axios"
    }
    

    开发环境配置文件

    说完基础的配置文件,再看开发环境的配置文件就简单多了。开发环境的配置文件,主要是在基础配置文件的基础上,对 Webpack Dev Server 和热更新进行配置:

    const webpackMerge = require("webpack-merge");
    const webpack = require("webpack");
    const { resolve } = require("./bundle")
    const webpackBaseFn = require("./webpack.config.base");
    
    module.exports = function(env){
      const baseConfig = webpackBaseFn(env)
      return webpackMerge(baseConfig,{
        mode:"development",
        devServer:{
          contentBase:resolve("dist"),
          host:"0.0.0.0",
          useLocalIp: true,
          overlay:{
            errors:true,
            warnings:true
          },
          open:true,
          hot:true,
          historyApiFallback: true,
          inline: true,
          disableHostCheck: true,
          stats:{
            assets: false,
            chunks: false,
            chunkGroups: false,
            chunkModules: false,
            chunkOrigins: false,
            modules: false,
            moduleTrace: false,
            source: false,
            builtAt: false,
            children: false,
            hash:false,
          },
        },
        plugins:[
          //热更新
          new webpack.HotModuleReplacementPlugin(),
        ],
      });
    }
    

    下面对 devServer 配置进行一些说明:

    1. host
      host 指定为 "0.0.0.0",就可以通过 IP 地址来访问 Webpack Dev Server 提供的服务了,处于安全问题的考虑,Webpack Dev Server 默认禁止了通过 IP 地址访问服务。
    2. useLocalIp
      该配置项和 open 配置结合在一起使用,将useLocalIp 设置为 true 后,自动打开浏览器时将会通过 IP 地址访问服务,如果不设置 useLocalIp,自动打开浏览器将会打开 0.0.0.0:8080
    3. stat
      该配置项主要对控制台的输出进行一些清理工作,默认的控制台打印的日志很乱,通过对 stat 进行配置后,控制台输出的日志清爽多了。

    生产环境配置文件

    下面是生产环境的配置文件 webpack.config.prod.js:

    const webpackMerge = require("webpack-merge");
    const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
    const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
    
    const { resolve } = require("./bundle")
    const { externals } = require("./externals")
    const webpackBaseFn = require("./webpack.config.base");
    
    module.exports = function(env,{ option }){
      const baseConfig = webpackBaseFn(env)
      const reportOn = option === "report"
      const plugins = [
        new MiniCssExtractPlugin({
          filename: "css/[name].[hash].css",
        }),
      ];
      if(reportOn){
        plugins.push(new BundleAnalyzerPlugin())
      }
    
      return webpackMerge(baseConfig,{
        mode:"production",
        optimization:{
          splitChunks: {
            cacheGroups: {
              commons: {
                test: /[\\/]node_modules[\\/]/,
                name: "vendor",
                chunks: "all"
              }
            }
          },
          minimizer: [
            new UglifyJsPlugin({
              uglifyOptions: {
                compress: {
                  warnings: false,
                  drop_debugger: false,
                  drop_console: true
                }
              }
            }),
            new OptimizeCSSAssetsPlugin({
              cssProcessorOptions: {
                safe: true
              }
            })
          ]
        },
        stats:{
          chunkGroups: false,
          chunkModules: false,
          chunkOrigins: false,
          modules: false,
          moduleTrace: false,
          source: false,
          children: false,
        },
        externals,
        plugins
      });
    }
    

    生产环境的配置文件中,主要对公用的 JS 和 CSS 进行了压缩提取,例外提供了报表功能:当将 OPTION 环境变量设置为 report 时,将会在构建完成后自动打开打包报表分析,可以分析打包中出现的问题,以及各个 Thunk 的大小。
    在 Webpack4 中,代码分割和压缩 JS 以及 CSS 的配置放到了 optimization 选项中,splitChunks 用来提供代码分割功能,minimizer 用来提供压缩功能。

    package.json

    下面是 package.json 文件的内容:

    {
      "name": "CHARLEY_MUTLI_KIT",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "dev": "cross-env ENVIROMENT=dev webpack-dev-server",
        "build": "cross-env ENVIROMENT=prod webpack",
        "build:report": "cross-env OPTION=report npm run build",
        "serve": "http-server ./dist -o"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "autoprefixer": "^8.6.4",
        "babel-core": "^6.26.3",
        "babel-loader": "^7.1.4",
        "babel-plugin-transform-async-to-generator": "^6.24.1",
        "babel-plugin-transform-runtime": "^6.23.0",
        "babel-preset-env": "^1.7.0",
        "clean-webpack-plugin": "^0.1.19",
        "copy-webpack-plugin": "^4.5.2",
        "cross-env": "^5.2.0",
        "css-loader": "^0.28.11",
        "eslint": "^5.0.1",
        "eslint-loader": "^2.0.0",
        "eslint-plugin-vue": "^4.5.0",
        "file-loader": "^1.1.11",
        "friendly-errors-webpack-plugin": "^1.7.0",
        "html-webpack-plugin": "^3.2.0",
        "http-server": "^0.11.1",
        "mini-css-extract-plugin": "^0.4.1",
        "node-sass": "^4.9.0",
        "optimize-css-assets-webpack-plugin": "^4.0.3",
        "postcss-loader": "^2.1.5",
        "sass-loader": "^7.0.3",
        "sass-resources-loader": "^1.3.3",
        "style-loader": "^0.21.0",
        "uglifyjs-webpack-plugin": "^1.2.7",
        "url-loader": "^1.0.1",
        "vue-loader": "^15.2.4",
        "vue-style-loader": "^4.1.0",
        "vue-template-compiler": "^2.5.16",
        "webpack": "^4.14.0",
        "webpack-bundle-analyzer": "^2.13.1",
        "webpack-cli": "^3.0.8",
        "webpack-dev-server": "^3.1.4",
        "webpack-merge": "^4.1.3"
      },
      "dependencies": {
        "axios": "^0.18.0",
        "vue": "^2.5.16"
      }
    }
    

    配置总结

    本次在 Webpack 的配置中,我对配置文件中常用的功能进行了提取,将改动频率大的配置项单独出来,方便了扩展和维护,功能也更加清晰。此外,将 Node 和 Webpack 的配置结合的更加紧密,有了很大的灵活性,如果以后想做一个 React 的脚手架,只需要修改下 loaders.js 即可,不用动 Webpack 配置文件的主体。
    Webpack 的配置部分就到此结束,下篇文章我给大家介绍下项目中的模块划分。

    本项目 GitHub 仓库地址为:https://github.com/charleylla/charley-vue-multi,如果您觉得我的文章对您有帮助,欢迎帮我点个 Star,如果您有问题,欢迎提 Issue~

    完。

    相关文章

      网友评论

      • 40c2fd87783d:您好,请问页面跳转怎么做的,vue-router不能用了,用express吗?可以单页面和多页面混合来吗?
        柏丘君:@我是还不会游泳的鱼 可以的
        40c2fd87783d:谢谢,那个vuex的状态管理还可以使用吗?
        柏丘君:页面跳转使用链接跳转,或者 window.location 跳转。多页应用不需要使用 vue-router,自带 router 的,多页应用中可以嵌入单页应用

      本文标题:基于 Webpack4 + Vue 的多页应用解决方案(一)

      本文链接:https://www.haomeiwen.com/subject/txqyuftx.html