webpack基础篇(一)

327 阅读7分钟

webpack中文官网

webpack英文官网

https://webpack.docschina.org/

目标:

  1. 彻底学会webpack
  2. 理解webpack作用及原理
  3. 上手项目的打包过程配置
  4. 拥有工程化的前端思维

一、认识webpack

1. webpack是什么

webpack其实就是个模块化打包工具(module bundler)。

官网解释: 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle。

符合ES Module的模块引入方式 除了ES Module还有nodejs使用的CommonJS,还有CMDAMD,webpack可以识别任何模块引入的语法。

CommonJS
// 导出
module.exports = xxx
// 使用
let xxx = require('./**.js')

模块化参考文档: webpack.js.org/concepts/mo…

webpack.docschina.org/concepts/mo…

模块化方法:webpack.docschina.org/api/module-…

3. 搭建webpack环境

  1. webpack是基于nodejs开发的模块打包工具,本质上是由node实现的。
  2. 新版本的nodejs会提升webpack的打包速度。保持webpack版本和node版本尽量的新就会提升打包速度。高版本的webpack会利用node中的特性来提升打包速度。
// 查看node版本号
node -v 
// 查看npm版本号
npm -v
// 创建webpack-demo文件夹
mkdir webpack-demo
// 进入webpack-demo
cd webpack-demo
// 执行npm init,帮助我们以node规范的形式创建一个项目(或包文件),生成package.json
npm init
// package.json配置
"private": true, // 代表为私有仓库,不会被发布到npm的线上仓库中
"main": "index.js",  // 此项目不会向外暴露,因此删除此配置。

webpack 安装

// 不推荐全局安装,最好单独项目安装
npm install webpack webpack-cli -g

// 查看webpack版本号
webpack -v

// 卸载webpack(全局)
npm uninstall webpack webpack-cli -g

// 在项目中安装webpack (此时node_modules中会有webpack依赖的包)
npm install webpack webpack-cli --save-dev (-D)

webpack -v // command not found (nodejs会默认全局寻找webpack,因此无法找到,因为未安装到全局)

// node提供了npx命令,会帮助我们在当前这个项目的node_modules文件夹里找webpack
npx webpack -v

// 查看webpack相关版本信息
npm info webpack 
// 安装指定版本的webpack
npm install webpack@4.***

4. 使用webpack的配置文件

文件名称:webpack.config.js

const path = require('path')
module.exports = {
    // 解释:打包index.js文件,输出到bundle文件夹下,生成的名字为bundle.js
    entry: 'index.js', // 打包文件
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'bundle',)'' // 打包后的文件指定的文件夹(默认为dist文件夹),需要是绝对路径
    }
}

// 运行 npx webpack ,查看编译结果
npx webpack
// 将webpack.config.js该名为webpackconfig.js
npx webpack --config webpackconfig.js  // 代表webpack以webpackconfig.js为配置文件进行打包

// 一般项目上线配置 package.json 如下
"build": "webpack --config ./build/webpack.prod.js"

package.json

"scripts": {
    "bundle": "webpack",
}

// 运行 和上面的运行效果一样,这样就不用npx了
npm run bundle

以上共三种运行webpack的方式

global 全局
webpack index.js

local 当前项目
npx webpack index.js

npm scripts
npm run bundle -> webpack

webpack-cli的作用:使我们在命令行中能正确运行webpack这个命令,不安装则无法使用npx webpack 或 webpack命令

参考文档: webpack.docschina.org/guides/gett…

5. webpack打包输出内容

执行 `npm run build` 在控制台输出

Hash:5*****1**2**a* // 每次打包对应唯一一个hash值
Version:webpack 4.**.* // 打包对应webpack版本
Time:236ms Built at:2019-8-11 12:21:21 // 本次打包耗时,及打包的时间
Asset     Size     Chunks    Chunk Names //打包后的文件名,大小,id(每个文件都有自己的id值),入口js文件名
bundle.js 2.66 KiB 0      [emitted] main 
Entrypoint main=bundle.js
[0]./src/index.js 1068 bytes {0}[built]
mode: 'production' // 'development' 开发环境则代码不会被压缩,'production'则会被压缩

二、webpack核心概念

三问:

  1. webpack是什么

  2. 模块是什么

  3. 配置文件的作用是什么

1. loader是什么

webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。你可以使用 Node.js 来很简单地编写自己的 loader

webpack不能识别非js结尾的模块,此时就需要在module中配置相应的loader

配置项为module

module.exports =  {
    module: {
        rules: [
        {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            use: {
               loader: 'file-loader'  // 使用file-loader来处理图片
            }
        },
        {
        test: /\.(j|t)sx?$/,
        exclude: NODE_ENV === 'development' ? /(node_modules)/ : [],
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
            }
          }
        ]
      },
        ]
    }
}

2. 使用loader打包静态资源(图片)

webpack.docschina.org/loaders/fil…

webpack.docschina.org/loaders/url…

// file-loader
module.exports =  {
    module: {
        rules: [
        {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            use: {
               loader: 'file-loader',  // 使用file-loader来处理图片
               option: {
                   // placeholder 占位符
                   name: '[name]_[hash:8].[ext]', // [name] 原始文件名字,[ext]原始文件后缀,生成和原文件名一样的图片
                   // 当遇到 png|jpe?g|gif|svg 这几种类型的文件的时候,输出到images文件夹里(或图片资源服务器)
                   outputPath: 'images/'
               }
            }
        },
        ]
    }
}

// url-loader 有一部分功能和 file-loader 一致

module.exports =  {
    module: {
        rules: [
        {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            use: {
               loader: 'url-loader', 
               option: {
                   name: '[name]_[hash:8].[ext]',
                   outputPath: 'images/',
                   limit: 2048, // 图片超过2048字节(2KB)时,则打包到images/目录下。否则转化为base64,打包到js中
               }
            }
        },
        ]
    }
}
编译之后,发现图片无法找到,而是以base64的形式打包到了js中
url-loader将图片转化为base64字符串
url-loader最佳使用方式,图片比较小的时候,比较合适,节省http请求,图片大时则不合适,实现这种最佳实践的参数就是limit
也就是说url-loader比file-loader功能更强大

3. 使用loader打包静态资源(scss、css)

  • css-loader: 分析出多个css之间的关系,并将这些css合并为一个css文件
  • style-loader:在拿到css-loader生成的css文件后,将这段内容挂载到html的head中,插入<style>标签
  • sass-loader: 解析scss文件
  • postcss-loader:添加浏览厂商前缀,参考文档:webpack.docschina.org/loaders/pos…
module.exports =  {
    module: {
        rules: [
        {
            test: /\.css$/,
            use:  [
                 'style-loader',
                 'css-loader',
               ]
        },
        ]
    }
}

scsslessstyles等文件时,参考文档:webpack.docschina.org/loaders/sas…

module.exports =  {
    module: {
        rules: [
        {
            test: /\.scss$/,
            use: [
                 'style-loader',
                 'css-loader',
                 'sass-loader',
                 {
                    loader: 'postcss-loader',
                    options: {
                    ident: 'postcss',
                    plugins: (e) => {
                      const _plugins = [
                         PostcssFlexbugsFixes,
                         PostcssPresetEnv({
                          autoprefixer: {
                          flexbox: 'no-2009'
                         },
                         stage: 3
                        })
                     ]
                // mobile 文件夹下面的文件 px 自动转 rem
                if (e.context.includes('/mobile')) {
                  _plugins.push(adaptive({ remUnit: 72, autoRem: true }))
                }
                return _plugins
              }
            }
          }
               ]
        },
        ]
    }
}

在webpack的配置里,loader是有先后执行顺序的,从下到上,从右到左。

npm install sass-loader node-sass webpack --save-dev

npm i -D postcss-loader
npm install autoprefixer -D

4. 使用loader打包静态资源(scss、css)

  • css-loader 常用配置项
module.exports =  {
    module: {
        rules: [
        {
            test: /\.css$/,
            use:  [
                 'style-loader',
                 {
                   loader: 'css-loader',
                   option: {
                       importLoaders: 2, // 多层import引入的scss文件,也要走sass-loader和postcss-loader
                   }
                 },
                 'sass-loader',
                 'postcss-loader',
               ]
        },
        ]
    }
}

如果直接以import './index.scss;'这种方式引入css文件,会作用于所有相同的类名,相当于全局类名,很容易出现样式冲突的问题。

module.exports =  {
    module: {
        rules: [
        {
            test: /\.css$/,
            use:  [
                 'style-loader',
                 {
                   loader: 'css-loader',
                   option: {
                       importLoaders: 2, // 多层import引入的scss文件,也要走sass-loader和postcss-loader
                       modules: true,
                   }
                 },
                 'sass-loader',
                 'postcss-loader',
               ]
        },
        ]
    }
}

新增 modules: true,引入方式改为import style from './index.scss;',使用方式为style.className,如:style.header。这样样式就避免了耦合。

  • 处理字体文件(打包svg、ttf、eot、ttf等)
module.exports =  {
    module: {
        rules: [
        {
            test: /\.(eot|ttf|svg)$/,
            use: {
               loader: 'file-loader', 
            }
        },
        ]
    }
}

参考文档:

  1. webpack.docschina.org/guides/asse… 讲解了css文件、图片、字体文件、数据文件(csv等)的打包方案,打包的好处和使用技巧。

  2. loaders: webpack.docschina.org/loaders/ css-loader、style-loader、sass-loader、postcss-loader

5. 使用plugins让打包更便捷

plugin可以在webpack运行到某个时刻的时候,帮你做一些事情。很像生命周期函数

当需要使用plugin时,搜一下就可以了,太多了...

参考文档: webpack.docschina.org/plugins/htm…

配置文档: github.com/jantimon/ht…

HtmlWebpackPlugin作用:会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html中。

npm install --save-dev html-webpack-plugin
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html', // 以src下的index.html作为模板生成html
  })],
  output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),  // 输出到dist文件夹下
  }
}

在输入html、css、js等文件的时候,会生成在同一级目录下,但如果你想将html文件单独放在某个问题夹下,如下图所示:

dist/
  index.html
  assets/
      index.js
      index.css

我们可以做如下配置:new HtmlWebpackPlugin({template: 'index.html', filename: "../index.html"}), 原文链接:github.com/jantimon/ht… 通过filename配置项的../html文件提出到上一级目录中。

clean-webpack-plugin:每次重新打包的时候,自动删除上一次打包的dist文件中的内容。

npm install clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html', // 以src下的index.html作为模板生成html
  }),
  new CleanWebpackPlugin(['dist']) // 删除dist文件夹下的内容
  ],
  output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),  // 输出到dist文件夹下
  }
}

loader与plugin的区别:loader是用来预处理文件的,而plugin是在webpack运行到某个时刻的时候,会帮你做一些事情。

6. Entry与Output的基础配置

module.exports = {
  entry: 'index.js', // 默认输出是main.js,在output中filename修改为bundle.js
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html', // 以src下的index.html作为模板生成html
  }),
  new CleanWebpackPlugin(['dist']) // 删除dist文件夹下的内容
  ],
  output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),  // 输出到dist文件夹下
  }
}

多入口文件时,我们可以在output中用占位符来解决,如:[name] [hash]

module.exports = {
  entry: {
    main:  './src/index.js',
    sub: './src/index.js',
  }, 
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html', // 以src下的index.html作为模板生成html
  }),
  new CleanWebpackPlugin(['dist']) // 删除dist文件夹下的内容
  ],
  output: {
      publicPath: 'https://cdn.com', // 对生成文件添加前缀,js发布到cdn时可以使用
      filename: '[name].js',
      path: path.resolve(__dirname, 'dist'),  // 输出到dist文件夹下
  }
}

以上写法解决了多入口,都合并到index.html的问题,在html中生成两个script标签。还有多入口,多js,打到多html中的问题需要解决。

参考文档:

  1. webpack.docschina.org/concepts/ou…
  2. webpack.docschina.org/configurati…
  3. webpack.docschina.org/configurati…
  4. webpack.docschina.org/guides/outp… 这个文档需要认真阅读

7. SourceMap的配置

sourceMap是一个映射关系,他知道打包出错代码的位置对应的实际的开发中源代码错误的位置。做源代码和目标生成代码的映射。

参考文档: webpack.docschina.org/configurati…

devtool: 'source-map ' // 配置之后,打包后的代码中会自动生成 **.js.map 文件,表示文件映射关系。其实是一个VLQ的编码集合,对源代码和打包代码做了一个映射关系。

devtool: 'inline-source-map ' // inline 将**.js.map以dataURL的方式直接写在打包后的js中,不会再有**.js.map,精确到会告诉你哪一行的哪一列出错了,这样的映射会非常耗费性能。

devtool: 'cheap-inline-source-map ' // cheap 则会只告诉哪一行出错了,打包性能会得到提升,可以从devtool的对照表看出构建速度的变化。只要有cheap的都会有较大变化。

devtool: 'cheap-module-inline-source-map' // module还管第三方引入模块等的错误

devtool: 'eval' // 打包最快的方法,eval的js执行形式来生成sourceMap的对应关系,性能最好,但对复杂代码,提示并不全面。

// 最佳实践(开发)
devtool: 'cheap-module-eval-source-map'
// 最佳实践(线上)
devtool: 'cheap-module-source-map'

sourceMap的原理是什么?

重要参考文档:

segmentfault.com/a/119000000… www.html5rocks.com/en/tutorial… www.ruanyifeng.com/blog/2013/0… www.youtube.com/watch?v=NkV…

8. 使用WebpackDevServer提升开发效率

参考文档:webpack.docschina.org/configurati…

// package.json

"scripts": {
    "dev": "webpack --watch" // --watch监听打包文件,只要发生变化,就会重新打包。只要有这个参数就生效。
}

但我们想要更丰富的功能:执行npm run dev就会自动打包,并自动打开浏览器,同时可以模拟一些服务器上的特性,此时就要借助WebpackDevServer来实现。

devServer:{
    contentBase: './dist' // 服务器起在哪个文件夹下。WebpackDevServer会帮助我们在这个文件夹下起一个服务器
}
npm install webpack-dev-server -D

// package.json
"scripts": {
    "dev": "webpack --watch"
    "start": "webpack-dev-server" // 自动重新刷新浏览器
}
npm run start

细化配置

参考文档:

webpack.docschina.org/configurati… webpack.docschina.org/configurati…

devServer:{
    port: 8080, // 默认8080
    contentBase: './dist',
    open: true, // 自动打开浏览器,并访问服务器地址。 file协议不行,不能发送ajax请求
    proxy: {
        './api': 'http://localhost:3000' // 用户访问 /api 这个路径会被转发到 http://localhost:3000,支持跨域代理
    }
}

扩充

// package.json
"scripts": {
    "dev": "webpack --watch",
    "start": "webpack-dev-server" ,
    "middleware": "node server.js" // 自己写一个服务器
}
server.js

npm install express webpack-dev-middle-ware -D

const express = require('express')
const webpack = require('webpack')
const webpacDevMiddelware = require('webpack-dev-middle-ware')
const config = require('./webpack.config.js')
// 在node中使用webpack
const complier = webpack(config) // 做编译,返回一个编译器,可随时对代码进行编译

const app = express()

app.use(webpacDevMiddelware(complier, {
  publicPath: config.output.publicPath // 打包生成路径
}))

app.listen(3000, () => {
  console.log('server is running')  
})

运行

npm run server
访问
localhost:3000
module.exports = {
  output: {
      publicPath: '/', // 对生成文件添加前缀
      filename: '[name].js',
      path: path.resolve(__dirname, 'dist'),  // 输出到dist文件夹下
  }
}

参考文档:

webpack.docschina.org/api/cli/ 文档中怎么写一些执行语法

webpack <entry> [<entry>] -o <output>

webpack index.js -o bundle.js  // -o 命令:入口index.js,出口bundle.js,-o为output的简写 

webpack.docschina.org/api/node/ 想在nodejs中运行webpack做一些事情,具体参数可参考此文档

本节参考文档:

webpack.docschina.org/guides/deve…

webpack.docschina.org/configurati…

9. Hot Module Replacement 热模块更新

热模块替换HMR,只更新你修改的内容,不刷新页面。从google的devTool来查看是否文件刷新。

// package.json
"scripts": {
    "start": "webpack-dev-server" ,
}

webpack-dev-server打包后的dist中的内容放到了内存中,加快访问速度

const webpack = require('webpack')

module.exports =  {
    devServer:{
        port: 8080, // 默认8080
        contentBase: './dist',
        open: true, 
        hot: true, // 让webpack-dev-server开启Hot Module Replacement功能
        hotOnly: true, // 即使HMR功能没有生效,也不让浏览器自动刷新,
    },
    module: {
        rules: [
        {
            test: /\.css$/,
            use:  [
                 'style-loader',
                 'css-loader',
                 'postcss-loader',
               ]
        },
        ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: 'src/index.html',
      }),
      new CleanWebpackPlugin(['dist']), // 开发环境不需要此配置
      new webpack.HotModuleReplacementPlugin() // 使用webpack插件,可用于开发环境
   ],
}

作用:

  1. 此时修改css之后,不会影响js的变更,只会重新替换css。
  2. 修改某个模块之后,只更新这个模块的代码,其他模块不受影响。
先判断当前项目是否开启HMR
// 只要***文件发生了变化,就会执行f()
if (module.hot) { // css-loader已经帮你实现了,因此自己不用写。VUE/REACT也已经帮助你实现了。
    module.hot.accept('./***', () => {
        f() // 要更新的方法
    })
}

判断判断当前项目是否开启HMR,css-loader已经帮你实现了,因此自己不用写。VUE/REACT也已经帮助你实现了。因此不用写上面的这段代码。

参考文档:

  1. webpack.docschina.org/guides/hot-…
  2. webpack.docschina.org/api/hot-mod…
  3. webpack.docschina.org/concepts/ho… HMR底层webpack实现原理

10. 使用Babel处理ES6语法

babel官网: babeljs.io/

babeljs.io/setup#insta…

npm install --save-dev babel-loader @babel/core  // @babel/core是babel的核心库,让babel识别js中的内容,把js转换为AST抽象语法树,再把抽象语法树编译转化为新的语法出来。
module: {
  rules: [
    { 
      test: /\.js$/,  // 如果为js文件,则使用babel-loader分析js语法
      exclude: /node_modules/, // node_modules 文件夹除外
      loader: "babel-loader", 
      option: {
        "presets": ["@babel/preset-env"] // 可以这样配置也可以在.babelrc下配置  
      }
    }
  ]
}
npm install @babel/preset-env --save-dev  // @babel/preset-env,其中babel-loader只是和webpack通信的一个桥梁,babel-loader并不会转化语法,还需要借助 @babel/preset-env来翻译相应语法。
.babelrc

{
  "presets": ["@babel/preset-env"]
}

此时只做了语法翻译,但这样还缺少对象或函数,如promise,在低版本浏览器还是没有的,此时还需要把这些缺失的对象或函数补充到低版本浏览器中,此时需要借助babel-polyfill来实现。babel-polyfill实际上就是在window对象上挂载了方法,绑定了全局变量。 babeljs.io/docs/en/bab…

npm install --save @babel/polyfill

@babel/polyfill 会让你打包生成的包变的巨大无比。
import "@babel/polyfill";  // 这种写法会把所有的语法都打包的包里,而不是用了哪些,打包哪些。
module: {
  rules: [
    { 
      test: /\.js$/,  // 如果为js文件,则使用babel-loader分析js语法
      exclude: /node_modules/, // node_modules 文件夹除外
      loader: 'babel-loader', 
      option: {
        "presets": [['@babel/preset-env', { // 这种是解决业务代码的使用场景,但要写组件库的话就不适合了,这种写法会污染全局环境,因此就换成了下面的runtime的形式
            "targets": { // 项目打包运行的浏览器,符合以下这些浏览器版本以上就可以,如:"chrome": "67",是chrome的67版本以上
              "edge": "17",
              "firefox": "60",
              "chrome": "67",
              "safari": "11.1",
            },
            useBuiltIns: 'usage'  // 根据业务代码来觉得加哪些polyfill,打包就会小很多
        }]] 
      }
    }
  ]
}

babeljs.io/docs/en/bab…

npm install --save-dev @babel/plugin-transform-runtime
module: {
  rules: [
    { 
      test: /\.js$/,  // 如果为js文件,则使用babel-loader分析js语法
      exclude: /node_modules/, // node_modules 文件夹除外
      loader: 'babel-loader', 
      option: {
        "plugins": [
            [
              "@babel/plugin-transform-runtime", // 使用plugin的好处,可以避免使用presets时污染全局环境,会以闭包的形式注入或引入对应内容,不会污染环境,在写类库是会更好。
              {
                "absoluteRuntime": false,
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false,
                "version": "7.0.0-beta.0"
              }
            ]
          ]
      }
    }
  ]
}

npm install --save @babel/runtime-corejs2

babel文档已有变更

import "@babel/polyfill";

在webpack4等较新的版本上,如果在webpack.config.js中配置了babel-loader相关内容,那么在.babelrc文件下,如果对@babel/present-env设置了useBuiltIns: 'usage',这样代码就不需要引入import "@babel/polyfill",会被自动引入,编译时的提示信息如下:

when setting `useBuiltIns: 'usage'`,polyfills are automatically imported when needed.
please remove the `import "@babel/polyfill"` call or use `useBuiltIns: 'usage'` instead.

11. webpack实现对React框架代码的打包

参考文档: babeljs.io/docs/en/bab…

npm install --save-dev @babel/preset-react // 解析jsx语法
.babelrc
执行顺序:从下往上,从右往左
{
    "presets": [
        ['@babel/preset-env', {
            "targets": {
              "edge": "17",
              "firefox": "60",
              "chrome": "67",
              "safari": "11.1",
            },
            useBuiltIns: 'usage'
          }
        ],
        "@babel/preset-react"
    ]
}