使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目

webpack4.43.0的基础配置&高级配置

说到搭建一个vue项目,对于一个初级工程师来说最简单也是最快的方法就是使用vue-cli脚手架来搭建,当然搭建项目对于一线前端初级开发工程师来说是接触不到的,这些工作前端leader都已经做好了,你只需要往里面写业务逻辑即可,看上去没啥毛病,但对于有追求,有上进心的你来说,你也想知道一个项目在破土动工前,前端leader是怎么搭建一个前端项目的工作流的,如何去手动配置一个具体项目的webpack打包文件,包括后期的SSR,服务端渲染。这些都是你提升自己内功的砝码,也是初级前端和中级前端的区别所在。官方的vue-cli脚手架固然好用,但知其然也要知其所以然,所以呢,接下来我将给大家展示从零开始搭建一个渐进增强的脚手架。

git仓库地址

基础配置:https://github.com/alibaba4184/webpack4-single-page.git
高级配置及性能优化:https://github.com/alibaba4184/webpack4-advanced.git

前期环境准备

  • node 版本:12.14.0
  • webpack 版本:4.43.0
  • vscode

目录

  • webpack基础配置
  • webpack高级配置
  • webpack性能优化

目标

  • 掌握webpack的安装
  • 掌握webpack的基础配置
  • 掌握loader的配置
  • 掌握plugin的配置
  • 了解webpack性能优化

一、webpack基础配置

初始化 package.json:

npm init -y

1,webpack的安装:

  • 全局安装webpack

    npm i webpack webpack-cli -g

  • 项目中安装webpack (推荐)

    npm i webpack webpack-cli -D
    本次我们采用的webpack和webpack-cli的版本是:
    “webpack”: “^4.43.0”,
    “webpack-cli”: “^3.3.12”,

npm i [email protected] [email protected]  -D

2, webpack的使用
webpack-cli:
npm 5.2 以上的版本中提供了一个npx命令
npx 想要解决的主要问题,就是调用项目内部安装的模块,原理就是在node_modules下的.bin 目录中找到对应的命令执行使用webpack命令:npx webpack,webpack4.0之后可以实现0配置打包构建,0配置的特点就是限制较多,无法自定义很多配置开发中常用的还是使用webpack配置进行打包构建。
3,webpack配置
webpack有四大核心概念:

  • 入口(entry): 程序的入口js
  • 输出(output): 打包后存放的位置
  • loader: 用于对模块的源代码进行转换
  • 插件(plugins): 插件目的在于解决 loader无法实现的其他事情。
    (1)配置webpack.config.js文件(在根目录下建一个webpack.config.js文件)
    (2)运行npx webpack
const path = require('path')
module.exports = {
     
  // 入口文件配置
  entry: './src/index.js',
  // 出口文件配置项
  output: {
     
    // 输出的路径,webpack2起就规定必须是绝对路径
    path: path.join(__dirname, 'dist'),
    // 输出文件名字
    filename: 'bundle.js'
  },
  mode: 'development' // 默认为production, 可以手动设置为development, 区别就是是否进行压缩混淆
}

npx webpack命令配置到package.json的脚本中

  1. 配置package.json
  2. 运行`npm run build
{
     
  "name": "webpack4-single-page",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
     
    "build": "webpack"
  },
  "devDependencies": {
     
     "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12",
  }
}

开发时自动编译工具
每次要编译代码时,手动运行 npm run build 就会变得很麻烦。

webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:

  1. webpack’s Watch Mode
  2. webpack-dev-server
  3. webpack-dev-middleware

多数场景中,可能需要使用 webpack-dev-server,但是不妨探讨一下以上的所有选项。

watch:

webpack指令后面加上--watch参数即可

主要的作用就是监视本地项目文件的变化, 发现有修改的代码会自动编译打包, 生成输出文件

  1. 配置package.json的scripts"watch": "webpack --watch"

  2. 运行npm run watch

以上是cli的方式设置watch的参数

还可以通过配置文件对watch的参数进行修改:

const path = require('path')

// webpack的配置文件遵循着CommonJS规范
module.exports = {
     
  entry: './src/index.js',  //webpack4的默认入口文件是index.js
  output: {
     
    // path.resolve() : 解析当前相对路径的绝对路径
    // path: path.resolve('./dist/'),
    path: path.join(__dirname, './dist/'),
    filename: 'bundle.js'
  },
  mode: 'development',
  watch: true
}

webpack-dev-server (推荐)

  1. 安装devServer

    devServer需要依赖webpack,必须在项目依赖中安装webpack

    npm i [email protected] [email protected] -D

  2. index.html中修改

  3. 运行:npx webpack-dev-server

  4. 运行:npx webpack-dev-server --hot --open --port 8090

  5. 配置package.json的scripts:"dev": "webpack-dev-server --hot --open --port 8090"

  6. 运行npm run dev
    devServer会在内存中生成一个打包好的bundle.js,专供开发时使用,打包效率高,修改代码后会自动重新打包以及刷新浏览器,用户体验非常好,以上是cli的方式配置devServer的参数,还可以通过配置文件对devServer的参数进行修改(推荐);
    (1)修改webpack.config.js

const path = require('path')

module.exports = {
     
  // 入口文件配置
  entry: './src/index.js',
  // 出口文件配置项
  output: {
     
    // 输出的路径,webpack2起就规定必须是绝对路径
    path: path.join(__dirname, 'dist'),
    // 输出文件名字
    filename: 'bundle.js'
  },
  devServer: {
     
    port: 8090,
    open: true,
    hot: true
  },
  mode: 'development'
}

(2),修改package.json的scripts: "dev": "webpack-dev-server"
(3),运行npm run dev

注意:webpack和webpack-dev-server 版本相兼容的问题:安装插件的时候如果不指定对应的版本,会自动安装最新的版本,那么在实际的开发中会因为各版本的不兼容,导致运行时会报各种错误,本人已经亲自踩过,希望看这篇文章的小伙伴们不要再踩这种坑了!

版本不兼容会报错:
Error: Cannot find module 'webpack-cli/bin/config-yargs'

兼容的版本有:
webpack@3.12.0,webpack-dev-server@2.11.2
webpack@3.10.0,webpack-dev-server@2.9.7
webpack@3.4.1,webpack-dev-server@2.9.7
webpack@4.43.0,webpack-dev-server@3.11.0 
webpack-cli@3.3.12

使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第1张图片

webpack-dev-middleware

webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。
(1)特点

  • 它将打包后的文件直接写入内存.
  • 每次请求都将获得最新的打包结果.
  • 在监视模式(watch mode)下如果代码变化,middleware会马上停止提供旧版的 bundle 并且会延迟请求直到编译完成.
    注意:如果要使用webpack-dev-middleware,必须使用html-webpack-plugin插件,否则html文件无法正确的输出到express服务器的根目录。所以我们先来配置一下html-webpack-plugin插件
    html插件
  1. 安装html-webpack-plugin插件npm i [email protected] -D
  2. webpack.config.js中的plugins节点下配置
 const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
 // Single Page Application
       new HtmlWebpackPlugin({
     
          filename: 'index.html',  //打包输出后的文件名
          template: './src/index.html',  //打包的模板html文件
          minify: false,  //是否启动压缩
       }),
]

3,devServer会根据模板在express项目目录下生成html文件(类似于devServer生成内存中的bundle.js)
4,devServer自动引入bundle.js
5,打包时会自动生成index.html文件

(2)配置webpack-dev-middleware

1,安装express和webpack-dev-middleware**
npm i express [email protected] -D
2,根目录下新建server.js

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');

const app = express();
const compiler = webpack(config);

app.use(webpackDevMiddleware(compiler, {
     
 publicPath: '/'
}));
//webpack-dev-middleware 中间件将webpack编译好的代码托管到端口为3000的express服务器的根目录下

app.listen(3000, function () {
     
 console.log('http://localhost:3000');
});
  1. 配置package.json中的scripts:"server": "node server.js"
    **4. 运行: `npm run server

**小结:**只有在开发时才需要使用自动编译工具,例如webpack-dev-server,项目上线时都会直接使用webpack进行打包构建,不需要使用这些自动编译工具,自动编译工具只是为了提高开发体验

loader的使用

概念:loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 “load(加载)” 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!
1,处理css文件

 module: {
     
    rules: [
      // 配置的是用来解析.css文件的loader(style-loader和css-loader)
      {
     
        // 用正则匹配当前访问的文件的后缀名是  .css
        test: /\.css$/,
        use: ['style-loader', 'css-loader'] // webpack底层调用这些包的顺序是从右到左
      }
    ]
  }

loader的释义:

  • css-loader: 解析css文件
  • style-loader: 将解析出来的结果 放到html中, 使其生效

2,处理less 和 sass
npm i less less-loader sass-loader node-sass -D

  module: {
     
        rules: [
        {
      
         test: /\.less$/, 
         use: ['style-loader', 'css-loader', 'less-loader'] 
        },
      {
      
      test: /\.scss$/, 
      use: ['style-loader', 'css-loader', 'sass-loader'] 
      }
   ]}

3,处理图片和字体
npm i [email protected] [email protected] -D
url-loader封装了file-loader, 所以使用url-loader时需要安装file-loader
npm i bootstrap@3 -S 安装bootstrap
base64比原图大30%

webpack:使用html-withimg-loader对html中img标签引入的图片打包,打包后图片无法显示,路径上多出default对象
 // 解决办法:esModule: false  该项默认为true,改为false即可
        // esModule: true该配置项为图片打包后的默认路径,带default对象,
        // 默认为ture,在配置项里将此项改为false即可去掉多余的default对象
{
     
            test: /\.(jpg|jpeg|svg|png|gif|bmp)$/,
            use: [{
     
                loader: 'url-loader',
                options: {
     
                    // limit表示如果图片大于5KB,就以路径形式展示,小于的话就用base64格式展示
                    limit: 5 * 1024,
                    // 打包输出目录
                    outputPath: 'static',
                    esModule: false,
                    // 打包输出图片名称  ext扩展名
                    name: '[name]-[hash:4].[ext]'
                }
            }]
        },
        //对字体文件处理
        {
     
            test: /\.(woff|woff2|eot|svg|ttf)$/,
            use: 'url-loader',
        },

(1)处理html文件中引入的img静态资源,使用html-withimg-loader
npm i [email protected] -D
index.html 模板文件


<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Documenttitle>
    <script src="/bundle.js">script>
  head>
  <body>
    <ul>
      <li>1li>
      <li>2li>
      <li>3li>
      <li>
        <img
          style="width:60px"
          src="./assets/images/1585810599717.png"
          alt=""
        />
      li>
    ul>
    <script>
      const root = document.createElement("div");
      root.className = "red";
      root.innerHTML = "

这是一个标题

"
; document.body.appendChild(root);
script> body> html>

配置html-withimg-loader

 // 处理html文件中引入的img静态资源
        {
     
            test: /\.(htm|html)$/i,
            loader: 'html-withimg-loader'
        },

使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第2张图片
4,处理.vue页面的js,template,style-----(vue-loader)

  • vue-loader的作用是解析和转换.vue文件,提取出其中的逻辑代码script,样式代码style以及HTML模板template,再分别将它们交给对应的loader去处理。
  • 在webpack4中vue-loader已经更新到@15版本了,在使用vue-loader时,必须配合使用 Vue Loader 的插件:
    安装:
    npm i [email protected]
var VueLoaderPlugin = require('vue-loader/lib/plugin')
 {
     
     test: /\.vue$/,
     loader: 'vue-loader'
  },
// 插件
 plugins: [
   new VueLoaderPlugin()
 ]

5,babel

  1. npm i babel-loader @babel/core @babel/preset-env webpack -D
 {
       "@babel/polyfill": "^7.12.1",
   "@babel/runtime": "^7.12.5",
    "babel-loader": "^8.2.2",
     "@babel/core": "^7.12.10",
   "@babel/plugin-proposal-class-properties": "^7.12.1",
   "@babel/plugin-transform-runtime": "^7.12.10",
   "@babel/preset-env": "^7.12.11",
   }
  1. 如果需要支持更高级的ES6语法, 可以继续安装插件:

    npm i @babel/plugin-proposal-class-properties -D

    也可以根据需要在babel官网找插件进行安装

   {
       test: /\.js$/, 
    use: {
        
     loader: 'babel-loader',    
      //也可以 单独在 .babelrc文件中配置
     options: {
     
      presets: ['@babel/env'],   
     plugins: ['@babel/plugin-proposal-class-properties']    }  
     },
      exclude: /node_modules/,  //排除node-modules依赖包的js文件
      include: path.resolve(__dirname, '../src')  //包括src目录下的文件
     }

官方更建议的做法是在项目根目录下新建一个.babelrc的babel配置文件

{
     
 "presets": ["@babel/env"],
 "plugins": ["@babel/plugin-proposal-class-properties"]
}

如果需要使用generator,无法直接使用babel进行转换,因为会将generator转换为一个regeneratorRuntime,然后使用markwrap来实现generator,但由于babel并没有内置regeneratorRuntime,所以无法直接使用.

需要安装插件:
npm i @babel/plugin-transform-runtime -D
同时还需安装运行时依赖:
npm i @babel/runtime -S
.babelrc中添加插件:

{
     
  "presets": [
    "@babel/env"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-runtime"
  ]
}

如果需要使用ES6/7中对象原型提供的新方法,babel默认情况无法转换,即使用了transform-runtime的插件也不支持转换原型上的方法.
需要使用另一个模块:
npm i @babel/[email protected] -S
该模块需要在使用新方法的地方直接引入:(如果是一个大型的vue项目,一般推荐在入口文件index.js文件中引入即可)
import '@babel/polyfill

6,source map的使用

devtool

此选项控制是否生成,以及如何生成 source map。

使用 SourceMapDevToolPlugin 进行更细粒度的配置。查看 source-map-loader 来处理已有的 source map。

选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

带eval的souce-map不会额外的生成映射文件,且构建速度更快,其他的souce-map会生成一个映射文件

可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 来替代使用 devtool 选项,它有更多的选项,但是切勿同时使用 devtool 选项和 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 插件。因为devtool 选项在内部添加过这些插件,所以会应用两次插件。

devtool 构建速度 重新构建速度 生产环境 品质(quality)
(none) +++ +++ yes 打包后的代码
eval +++ +++ no 生成后的代码
cheap-eval-source-map + ++ no 转换过的代码(仅限行)
cheap-module-eval-source-map o ++ no 原始源代码(仅限行)
eval-source-map + no 原始源代码
cheap-source-map + o no 转换过的代码(仅限行)
cheap-module-source-map o - no 原始源代码(仅限行)
inline-cheap-source-map + o no 转换过的代码(仅限行)
inline-cheap-module-source-map o - no 原始源代码(仅限行)
source-map yes 原始源代码
inline-source-map no 原始源代码
hidden-source-map yes 原始源代码
nosources-source-map yes 无源代码内容

这么多模式用哪个好?

开发环境推荐:

cheap-module-eval-source-map

生产环境推荐:

none(不使用source map)

原因如下:

  1. 使用 cheap 模式可以大幅提高 soure map 生成的效率。大部分情况我们调试并不关心列信息,而且就算 source map 没有列,有些浏览器引擎(例如 v8) 也会给出列信息。
  2. 使用 module 可支持 babel 这种预编译工具,映射转换前的代码。
  3. 使用 eval 方式可大幅提高持续构建效率。官方文档提供的速度对比表格可以看到 eval 模式的重新构建速度都很快。
  4. 使用 eval-source-map 模式可以减少网络请求。这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大。

plugins的使用

clean-webpack-plugin

该插件在npm run build时自动清除dist目录后重新生成,非常方便

  1. 安装插件
    npm i [email protected] -D

  2. 引入插件

const {
      CleanWebpackPlugin } = require('clean-webpack-plugin') 
//注意在webpack4中需要解构出来CleanWebpackPlugin,否则拿不到该插件

  1. 使用插件, 在plugins中直接创建对象即可
plugins: [
   new CleanWebpackPlugin()
 ],

copy-webpack-plugin
1,安装插件
npm i [email protected] -D
2,引入插件

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

3,使用插件, 在plugins中插件对象并配置源和目标
from: 源, 从哪里拷贝, 可以是相对路径或绝对路径, 推荐绝对路径
to: 目标, 拷贝到哪里去, 相对于output的路径, 同样可以相对路径或绝对路径, 但更推荐相对路径(直接算相对dist目录即可)

plugins: [
    new HtmlWebpackPlugin({
     
      filename: 'index.html',
      template: './src/index.html'
    }),
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
     
      patterns:[
      {
     
        from: path.join(__dirname, 'assets'),
        to: 'assets'
      }
    ]
    })
    //使用copywebpackPlugin插件报错  
    //compilation.getCache is not a function  原因:该插件版本太高:
    解决方法:降低版本:报错的版本是7.0.0  ,降到6.0.3 就可以了
  ],

BannerPlugin
这是一个webpack的内置插件,用于给打包的JS文件加上版权注释信息

  1. 引入webpack
const webpack = require('webpack')
  1. 创建插件对象
plugins: [
    new HtmlWebpackPlugin({
     
      filename: 'index.html',
      template: './src/index.html'
    }),
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin([
      {
     
        from: path.join(__dirname, 'assets'),
        to: 'assets'
      }
    ]),
    new webpack.BannerPlugin('版权所有,盗版必究!')
  ],

二、webpack高级配置

多页应用打包

1,在webpack.config.js中修改入口和出口配置

 // 1. 修改为多入口
  entry: {
     
      main: './src/index.js',
      other: './src/other.js'
  },
  output: {
     
    path: path.join(__dirname, './dist/'),
    // filename: 'bundle.js',
    // 2. 多入口无法对应一个固定的出口, 所以修改filename为[name]变量
    filename: '[name].[hash:8].bundle.js',
    publicPath: '/'
  },
  plugins: [
      //new HtmlWebpackPlugin({
     
        //  template: './index.html',
         // filename: 'other.html',
          // chunks: ['other', 'index']   //两个入口使用同一个模板文件可以写在一个数组中
          //chunks: ['other']
      //})
         //3. 如果用了html插件,需要手动配置多入口对应的html文件,将指定其对应的输出文件
         new HtmlWebpackPlugin({
     
          filename: 'index.html',
          template: './public/index.html',
          minify: false,  //minify的意思是html不压缩处理。
          chunks: ['index']
        }),
        new HtmlWebpackPlugin({
     
            filename: 'other.html',
            template: './public/other.html',
            minify: false,
            chunks: ['other']
         }),
  ]

2,修改入口为对象,支持多个js入口,同时修改output输出的文件名为'[name].[hash:8].bundle.js'表示各自以入口文件名作为输出文件名,但是html-webpack-plugin不支持此功能,所以需要再拷贝一份插件,用于生成两个html页面,实现多页应用

第三方库的两种引入方式

可以通过expose-loader进行全局变量的注入,同时也可以使用内置插件webpack.ProvidePlugin对每个模块的闭包空间,注入一个变量,自动加载模块,而不必到处 importrequire

  • expose-loader 将库引入到全局作用域
  • 安装expose-loader
    npm i -D [email protected]
  • 配置loader
module: {
     
rules: [{
     
  test: require.resolve('jquery'),
  use: {
     
    loader: 'expose-loader',
     options: {
     
         exposes: '$'
       }
  }
}]
}

tips: require.resolve 用来获取模块的绝对路径。所以这里的loader只会作用于 jquery 模块。并且只在 bundle 中使用到它时,才进行处理。

  • webpack.ProvidePlugin 将库自动加载到每个模块
    1,引入webpack
const webpack = require('webpack')

2,创建插件对象
要自动加载jquery,我们可以将两个变量都执行对应的node模块

new webpack.ProvidePlugin({
     
  $: 'jquery',
  jQuery: 'jquery'
})
Development / Production不同配置文件打包

项目开发时一般需要使用两套配置文件,用于开发阶段打包(不压缩代码,不优化代码,增加效率)和上线阶段打包(压缩代码,优化代码,打包后直接上线使用)

抽取三个配置文件:

  • webpack.base.js

  • webpack.prod.js

  • webpack.dev.js

步骤如下:

  1. 将开发环境和生产环境公用的配置放入base中,不同的配置各自放入prod或dev文件中(例如:mode)

  2. 然后在dev和prod中使用webpack-merge把自己的配置与base的配置进行合并后导出

npm i -D [email protected]

const {
      merge } = require('webpack-merge')   //引入merge插件
  1. 将package.json中的脚本参数进行修改,通过--config手动指定特定的配置文件
    使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第3张图片

  2. 安装live-server,可以将打包后的dist目录托管到一个服务器上运行

npm i -D live-server

定义环境变量

除了区分不同的配置文件进行打包,还需要在开发时知道当前的环境是开发阶段或上线阶段,所以可以借助内置插件DefinePlugin来定义环境变量。最终可以实现开发阶段与上线阶段的api地址自动切换。

  1. 引入webpack
const webpack = require('webpack')

2.创建插件对象,并定义环境变量

new webpack.DefinePlugin({
     
 IS_DEV: 'false'
})

3.在src打包的代码环境下可以直接使用
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第4张图片

使用devServer解决跨域问题

在开发阶段很多时候需要使用到跨域,何为跨域?请看下图:
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第5张图片
开发阶段往往会遇到上面这种情况,也许将来上线后,前端项目会和后端项目部署在同一个服务器下,并不会有跨域问题,但是由于开发时会用到webpack-dev-server,所以一定会产生跨域的问题

目前解决跨域主要的方案有:

  1. jsonp(淘汰)
  2. cors
  3. http proxy

此处介绍的使用devServer解决跨域,其实原理就是http proxy

将所有ajax请求发送给devServer服务器,再由devServer服务器做一次转发,发送给数据接口服务器

由于ajax请求是发送给devServer服务器的,所以不存在跨域,而devServer由于是用node平台发送的http请求,自然也不涉及到跨域问题,可以完美解决!

使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第6张图片
2:cors方式,服务器代码(返回一段字符串即可):

// express的版本是 "express": "^4.17.1",
// npm i [email protected]
const express = require('express')
const app = express()
const cors = require('cors')
app.use(cors())
app.get('/api/getUserInfo', (req, res) => {
     
  res.send({
     
    name: '黑马儿',
    age: 13
  })
});

app.listen(9999, () => {
     
  console.log('http://localhost:9999!');
});

3,前端需要配置devServer的proxy功能,在webpack.dev.js中进行配置:

devServer: {
     
    open: true,
    hot: true,
    compress: true,
    port: 3000,
    // contentBase: './src'
    proxy: {
     
      '/api': 'http://localhost:9999'
    }
  },

意为前端请求/api的url时,webpack-dev-server会将请求转发给http://localhost:9999/api处,此时如果请求地址为http://localhost:9999/api/getUserInfo,只需要直接写/api/getUserInfo即可,代码如下:

axios.get('/api/getUserInfo').then(result => console.log(result))
HMR的使用

需要对某个模块进行热更新时,可以通过module.hot.accept方法进行文件监视,只要模块内容发生变化,就会触发回调函数,从而可以重新读取模块内容,做对应的操作.

if (module.hot) {
     
     module.hot.accept('./hotmodule.js', function() {
     
     console.log('hotmodule.js更新了');
     let str = require('./hotmodule.js')
     console.log(str)
  })
}

三、webpack性能优化

production模式打包自带优化
  • tree shaking

tree shaking 是一个术语,通常用于打包时移除 JavaScript 中的未引用的代码(dead-code),它依赖于 ES6 模块系统中 importexport静态结构特性。

开发时引入一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进bundle,其他没用到的功能都不会打包进来,可以实现最基础的优化

  • scope hoisting

scope hoisting的作用是将模块之间的关系进行结果推测, 可以让 Webpack 打包出来的代码文件更小、运行的更快

scope hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些至少被引用了一次的模块才能被合并。

由于 scope hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。
原因和tree shaking一样。
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第7张图片

  • 代码压缩

所有代码使用UglifyJsPlugin插件进行压缩、混淆

css优化
将css提取到独立的文件中

mini-css-extract-plugin是用于将CSS提取为独立的文件的插件,对每个包含css的js文件都会创建一个CSS文件,支持按需加载css和sourceMap

只能用在webpack4中,有如下优势:

  • 异步加载
  • 不重复编译,性能很好
  • 容易使用
  • 只针对CSS

使用方法:

  1. 安装
    npm i -D [email protected]

  2. 在webpack配置文件webpack.base.js中引入插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  1. 创建插件对象,配置抽离的css文件名,支持placeholder语法
new MiniCssExtractPlugin({
     
	filename: '[name].css'
})
  1. 将原来配置的所有style-loader替换为MiniCssExtractPlugin.loader
{
     
test: /\.css$/,
// webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理
// loader的执行顺序是从右到左以管道的方式链式调用
// css-loader: 解析css文件
// style-loader: 将解析出来的结果 放到html中, 使其生效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{
      test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },
// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
      test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
自动添加css前缀

使用postcss,需要用到postcss-loaderautoprefixer插件

  1. 安装
    npm i -D [email protected] [email protected]

  2. 修改webpack配置文件中的loader,将postcss-loader放置在css-loader的右边(调用链从右到左)

{
     
test: /\.css$/,
// webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理
// loader的执行顺序是从右到左以管道的方式链式调用
// css-loader: 解析css文件
// style-loader: 将解析出来的结果 放到html中, 使其生效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{
      test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] },
// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
      test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
  1. 项目根目录下添加postcss的配置文件:postcss.config.js

  2. postcss的配置文件中使用插件

module.exports = {
     
 plugins: [require('autoprefixer')]
}

注意:如果出现安装完对应的插件并运行项目后出现报错,那么很可能是版本不兼容的问题
Error: PostCSS plugin autoprefixer requires PostCSS 8.
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第8张图片
原因:该插件版本太高
降低autoprefixer版本,降低到8.0.0
扩展:cross-env
安装cross-env用于设置开发和生成环境不同的publicPath
npm install --save-dev cross-env@7

NOTE : Version 7 of cross-env only supports Node.js 10 and higher, to use it on Node.js 8 or lower install version 6 npm install --save-dev cross-env@6

开启css压缩

css的压缩我们一般在上生产环境的时候做,开发环境为了开发体验,一般不做css压缩,所以我们在安装完插件后再webpack.prod.js文件中进行引入并配置。

需要使用optimize-css-assets-webpack-plugin插件来完成css压缩
但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致JS代码无法压缩,所以还需要手动把JS代码压缩插件导入进来:`terser-webpack-plugin

1.安装
npm i -D [email protected] [email protected]
2.导入插件

const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

3.在webpack配置文件中添加配置节点

optimization: {
     
  minimizer: [new TerserJSPlugin({
     }), new OptimizeCSSAssetsPlugin({
     })],
},

tips: webpack4默认采用的JS压缩插件为:uglifyjs-webpack-plugin,在mini-css-extract-plugin上一个版本中还推荐使用该插件,但最新的v0.6中建议使用teser-webpack-plugin来完成js代码压缩,具体原因未在官网说明,我们就按照最新版的官方文档来做即可

teser-webpack-plugin插件和webpack版本不兼容问题

原因:https://blog.csdn.net/xianrenmodel/article/details/111029947
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第9张图片
需要安装teser插件4.2.3或3.0.8 ,导致报错的原因是webpack为4,而teser插件是5.1.1

js代码分离

Code Splitting是webpack打包时用到的重要的优化特性之一,此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

有三种常用的代码分离方法:

  • 入口起点(entry points):使用entry配置手动地分离代码。
  • 防止重复(prevent duplication):使用 SplitChunksPlugin去重和分离 chunk。
  • 动态导入(dynamic imports):通过模块的内联函数调用来分离代码。
1.手动配置多入口

(1)在webpack配置文件中配置多个入口

entry: {
     
  main: './src/index.js',
  other: './src/other.js'
},
output: {
     
  // path.resolve() : 解析当前相对路径的绝对路径
  // path: path.resolve('./dist/'),
  // path: path.resolve(__dirname, './dist/'),
  path: path.join(__dirname, '..', './dist/'),
  // filename: 'bundle.js',
  filename: '[name].bundle.js',
  publicPath: '/'
},

(2) 在index.js和other.js中都引入同一个模块,并使用其功能
index.js

import $ from 'jquery'

$(function() {
     
  $('
'
).html('main').appendTo('body') })

other.js

import $ from 'jquery'

$(function() {
     
  $('
'
).html('other').appendTo('body') })

(3)修改package.json的脚本,添加一个使用dev配置文件进行打包的脚本(目的是不压缩代码检查打包的bundle时更方便)

"scripts": {
     
    "build": "webpack --config ./build/webpack.prod.js",
    "dev-build": "webpack --config ./build/webpack.dev.js"
}

(4)运行npm run dev-build,进行打包
(5)查看打包后的结果,发现other.bundle.js和index.bundle.js都同时打包了jQuery源文件
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第10张图片
使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第11张图片
这种方法存在一些问题:

  • 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。
抽取公共代码

tips: Webpack v4以上使用的插件为SplitChunksPlugin,以前使用的CommonsChunkPlugin已经被移除了,最新版的webpack只需要在配置文件中的optimization节点下添加一个splitChunks属性即可进行相关配置

  1. 修改webpack配置文件(webpack.prod.js)
optimization: {
     
    splitChunks: {
     
      chunks: 'all'
    }
},
  1. 运行npm run dev-build重新打包
  2. 查看dist目录
    使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第12张图片
  3. 查看vendors~main~other.bundle.js,其实就是把都用到的jQuery打包到了一个单独的js中
    使用webpack4.43.0+vue2.6.12手动搭建一个webpack项目_第13张图片
动态导入 (懒加载)

webpack4默认是允许import语法动态导入的,但是需要babel的插件支持,最新版babel的插件包为:@babel/plugin-syntax-dynamic-import,以前老版本不是@babel开头,已经无法使用,需要注意

动态导入最大的好处是实现了懒加载,用到哪个模块才会加载哪个模块,可以提高SPA应用程序的首屏加载速度,Vue、React、Angular框架的路由懒加载原理一样

  1. 安装babel插件

npm install -D @babel/[email protected]

  1. 修改.babelrc配置文件,添加@babel/plugin-syntax-dynamic-import插件
  2. “@babel/plugin-syntax-dynamic-import”: “^7.8.3”,
{
     
"presets": ["@babel/env"],
"plugins": [
  "@babel/plugin-proposal-class-properties",
  "@babel/plugin-transform-runtime",
  "@babel/plugin-syntax-dynamic-import"
]
}
  1. 将jQuery模块进行动态导入
function getComponent() {
     
  return import('jquery').then(({
      default: $ }) => {
     
    return $('
'
).html('index') }) }
  1. 给某个按钮添加点击事件,点击后调用getComponent函数创建元素并添加到页面
window.onload = function () {
     
  document.getElementById('btn').onclick = function () {
     
    getComponent().then(item => {
     
      item.appendTo('body')
    })
  }
}
SplitChunksPlugin配置参数

webpack4之后,使用SplitChunksPlugin插件替代了以前CommonsChunkPluginSplitChunksPlugin的配置,只需要在webpack配置文件中的optimization节点下的splitChunks进行修改即可,如果没有任何修改,则会使用默认配置,默认的SplitChunksPlugin 配置适用于绝大多数用户。

webpack 会基于如下默认原则自动分割代码:

  • 公用代码块或来自 node_modules 文件夹的组件模块。
  • 打包的代码块大小超过 30k(最小化压缩之前)。
  • 按需加载代码块时,同时发送的请求最大数量不应该超过 5。
  • 页面初始化时,同时发送的请求最大数量不应该超过 3。

以下是SplitChunksPlugin的默认配置:

module.exports = {
     
 //...
 optimization: {
     
   splitChunks: {
     
     chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial
     minSize: 30000, // 模块最少大于30KB才拆分
     maxSize: 0,  // 模块大小无上限,只要大于30KB都拆分
     minChunks: 1, // 模块最少引用一次才会被拆分
     maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分
     maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分
     automaticNameDelimiter: '~', // 默认的连接符
     name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
     cacheGroups: {
      // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组
       vendors: {
      // 自定义缓存组名
         test: /[\\/]node_modules[\\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组
         priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20
       },
       default: {
      // 默认缓存组名
         minChunks: 2, // 最少引用两次才会被拆分
         priority: -20, // 权重-20
         reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次
       }
     }
   }
 }
};
noParse

在引入一些第三方模块时,例如jQuery、bootstrap等,我们知道其内部肯定不会依赖其他模块,因为最终我们用到的只是一个单独的js文件或css文件

所以此时如果webpack再去解析他们的内部依赖关系,其实是非常浪费时间的,我们需要阻止webpack浪费精力去解析这些明知道没有依赖的库

可以在webpack配置文件(webpack.base.js)的module节点下加上noParse,并配置正则来确定不需要解析依赖关系的模块

module: {
     
	noParse: /jquery|bootstrap/
}
IgnorePlugin

在引入一些第三方模块时,例如moment,内部会做i18n国际化处理,所以会包含很多语言包,而语言包打包时会比较占用空间,如果我们项目只需要用到中文,或者少数语言,可以忽略掉所有的语言包,然后按需引入语言包

从而使得构建效率更高,打包生成的文件更小

需要忽略第三方模块内部依赖的其他模块,只需要三步:

  1. 首先要找到moment依赖的语言包是什么
  2. 使用IgnorePlugin插件忽略其依赖
  3. 需要使用某些依赖时自行手动引入

具体实现如下:

  1. 通过查看moment的源码来分析:
function loadLocale(name) {
     
    var oldLocale = null;
    // TODO: Find a better way to register and load all the locales in Node
    if (!locales[name] && (typeof module !== 'undefined') &&
        module && module.exports) {
     
        try {
     
            oldLocale = globalLocale._abbr;
            var aliasedRequire = require;
            aliasedRequire('./locale/' + name);
            getSetGlobalLocale(oldLocale);
        } catch (e) {
     }
    }
    return locales[name];
}

  1. 观察上方代码,同时查看moment目录下确实有locale目录,其中放着所有国家的语言包,可以分析得出:locale目录就是moment所依赖的语言包目录

  2. 使用IgnorePlugin插件来忽略掉moment模块的locale目录

在webpack配置文件中安装插件,并传入配置项

参数1:表示要忽略的资源路径

参数2:要忽略的资源上下文(所在哪个目录)

两个参数都是正则对象

new webpack.IgnorePlugin(/\.\/locale/, /moment/)
  1. 使用moment时需要手动引入语言包,否则默认使用英文
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-CN')
console.log(moment().subtract(6, 'days').calendar())

总结:以上就是关于webpack4从零开始构建一个完整的项目的全过程,至于具体的实践,文章开头已经展示了项目的git仓库地址,有兴趣的读者可以克隆下来学习,下次我将给大家介绍另一个针对第三方库的构建优化(vue,react,angular等框架),我们需要借助一个插件–DllPlugin实现将这些框架作为一个个动态链接库,类似于windows中的dll文件。只需要构建一次,以后每次构建都只生成自己的业务代码,可以大大提高构建效率!

你可能感兴趣的:(webpack打包工具,webpack)