Webpack是什么

Webpack是JavaScript应用程序的前端资源模块化管理和静态模块打包器

它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等。

简单来说,它可以将我们项目中的所有js、图片、css等资源,根据其入口文件的依赖关系,打包成一个能被浏览器识别的js文件。能够帮助前端开发将打包的过程更智能化和自动化。

一个基本的webpack配置:

1
2
3
4
5
6
7
8
module.exports={
entry:, // 设置入口文件
output:{}, // 设置输出文件
module:{ // 设置loader,即一些资源转换工具
rules:[]
},
plugins:[], // 设置插件,插件能做到很多loader做不到的事情
}
  • entry是设置入口文件,webpack会根据其入口文件的依赖关系,将所有依赖关系的资源打包在一起。
  • output是设置打包后文件的输出,例如:打包后的文件名和输出路径等。
  • module是设置loader,即设置一些资源转换工具,例如将stylus、less、sass进行处理,转换成浏览器能够识别的css资源。
  • plugins是设置插件的地方,官方也提供了很多插件,插件能够帮助我们做很多事情,例如压缩打包等

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const HtmlWebPackPlugin = require("html-webpack-plugin")
const path = require("path")
const webpack = require("webpack")

module.exports = {
resolve: {
extensions: [".js", ".jsx", ".json"] // 带有此类后缀的文件,可以不写后缀
},
entry: path.resolve(__dirname, "src/index.jsx"),
module: {
rules: [
{
test: /\.jsx?/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
babelrc: false, // 不使用.babelrc文件
presets: [
require.resolve("@babel/preset-react"),
[require.resolve("@babel/preset-env",{module:false})]
]
}
}
}
]
},
devServer: {
hot: true
},
plugins: [
new HtmlWebPackPlugin({
template: path.resolve(__dirname, "src/index.html"),
filename: "index.html"
}),
new webpack.HotModuleReplacementPlugin()
]
}

作用域

全局变量是挂在的在全局对象上的

  • 在windows的全局对象是windows
  • 在Mac的全局对象是brower
1
2
3
var a = 1

window.a // 这里就能拿到

我们在页面上引入的script文件是全局的,因此文件里面的变量也是全局的,这样就有可能造成作用域被污染,变量发生冲突

1
2
3
<script src="/.moduleA.js"></script>
<script src="/.moduleB.js"></script>
<script src="/.moduleC.js"></script>

这时可以考虑使用闭包

1
2
3
4
5
6
7
8
9
10
11
12
// moduleA.js
// 此时name和sex已经被存缓好了,可以被return的function访问
// 这样name和age就被保护(隐藏)起来了,因为这样外部就访问不到他们了
var moduleA= (function(){
var name = "susan"
var sex = "nv"
return {
tell: function(){
console.log("my name is ",name)
}
}
})()

第二种写法:

1
2
3
4
5
6
7
8
(function(){
var name = "susan"
var sex ="nv"
function tell(){
console.log("my name is ",name)
}
window.susanModule = { tell }
})(window)

模块导出机制的演变历史

AMD

asynchronous Module Definition:异步模块定义

1
2
3
4
5
6
7
8
// 第一参数:模块的名字
// 第二参数:模块的依赖
// 第三参数:模块的导出
define("getSum", ["math"], function(math){
return function(a, b){
console.log("sum:", math.sum(a,b))
}
})

CommonJS

1
2
3
4
5
6
7
// 通过require函数来引入
const math = require("./math")

// 通过exports将其导出
exports.getSum = function(a, b){
return a + b
}

ES6 Module

1
2
3
4
5
6
7
// import 导入
import math from "./math"

// export 导出
export function sum(a, b) {
return a + b
}

快速入门

开始前的准备

第一步安装webpack

首先我们可以通过npm全局安装一下webpack,

1
2
//全局安装
npm install -g webpack

接着我们新建一个文件夹,命名为:webpack-demo。

将终端切换到webpack-demo下执行下面的语句,将webpack安装到webpack-demo目录下。

1
npm install --save-dev webpack

第二步创建package.json文件

通过终端进入webpack-demo目录下,然后执行npm init命令,终端会咨询你一些npm的配置,例如项目名称、作者等。在这里,我们仅仅为了演示和学习,一路回车即可。

这时候我们就自动生成了一个webpack.json的文件,我们可以通过这个文件看到当前的自动化脚本和项目的依赖等信息。

第三步编写webpack.config.js

在编写webpack.config.js之前,为了方便学习演示,我们先创建几个文件夹:

  • build-用来存放打包后的资源
  • html-用来存放html文件
  • js-用来存放js文件

我们在html文件夹下,创建一个index.html文件,在这个文件中我们要引用稍后通过webpack打包好的js资源,并进行效果的展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello Webpack</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="../build/bundle.js"></script>
</body>
</html>
  • 这里面我们在body中编写了一个id为root的div,稍后我们会通过js代码,在root下添一个div标签,其内容是:Hello Webpack!
  • 紧接着,我们引入了一个script标签,它的资源引用指向build文件夹下的bundle.js文件(我们会在webpack输出配置中配置输出的文件名,这个稍后就会讲)。

接着我们在js文件夹下面新建一个index.js文件:

1
2
3
4
5
// index.js
var domDiv=document.createElement("div");
domDiv.textContent="Hello Webpack!"
var root=document.querySelector("#root");
root.appendChild(domDiv);

在这里,我们在id为root的标签下,添加了一个内容为:Hello Webpack!的div标签。到这里,我们终于把前期工作都准备完毕了。

我们在webpack-demo的文件夹下,创建webpack.config.js的文件,

1
2
3
4
5
6
7
8
9
10
11
// `webpack.config.js`
module.exports={
/**
* 单入口文件打包
*/
entry:__dirname+"/js/index.js", //入口文件,即webpack开始打包的入口
output:{
path:__dirname+"/build", //打包出口文件路径
filename:"bundle.js" //打包后文件名,这里我们将打包后的文件名设置成bundle,即index.html中引用的文件名
}
}

在这里通过CommonJS语法规范,导出了一个对象。

  • entry:主要用来设置打包文件的入口,即webpack从哪个文件开始进行打包,webpack会不断寻找其依赖关系,将所有依赖的文件打包在一起。

    • 在这里我们将入口文件设置成:__dirname+”/js/index.js”,即项目目录下的js文件夹下的index.js文件,对它进行打包。
    • entry还可以配置多入口文件,多入口文件的情况。
  • output:主要用来设置文件输出的路径和文件名,我们通过path设置了文件打包输出的目录,即项目目录下的build文件夹。通过filename设置了文件输出的名字,即bundle.js,这也是我们在index.js下等待引用的文件名。

开始打包

完成了最简单的webpack配置,接着我们在终端输入webpack进行打包。

在build文件下面多了一个bundle.js文件,即webpack替我们打包后生产的文件。

然后我们在浏览器中打开index.html即可发现我们浏览器中显示了:Hello Webpack!

多入口文件打包

有时候我们的入口文件可能不止一个,例如我们在js目录下,再新建一个greeter.js的文件。

1
2
3
4
5
// greeter.js
var domGreet=document.createElement("div")
domGreet.textContent="Welcome to learn Webpack";
var root=document.querySelector("#root");
root.appendChild(domGreet);

依旧是在root下增加一个子元素,子元素的内容为Welcome to learn Webpack

接着我们修改index.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello Webpack</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="../build/bundle.js"></script>
<script type="text/javascript" src="../build/greeter.js"></script>
</body>
</html>

我们在index.html中多引入了一个名为greeter.js的资源。

接着,我们修改webpack.config.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports={
/**
* 多入口文件打包
*/
entry:{
bundle:__dirname+"/js/index.js", //入口文件,即webpack开始打包的入口
greeter:__dirname+"/js/greeter.js"
},
output:{
path:__dirname+"/build", //打包出口文件路径
filename:"[name].js" //打包后文件名,如果是多入口,引用[name],会将entry的对象中的属性作为文件名
}
}

这时候entry的值是一个对象,通过属性和值的方式定义多个入口文件,我们在outputfilename值中引用[name],就会把entry中的属性名作为文件的名字,打包输出。

这时候我们的build文件下就会存在两个打包后的js文件:bundle.jsgreeter.js文件。

这时候我们再打开index.html文件

Loader

  • Loader主要是用来对文件资源进行转换打包,
  • 例如:我们js中运用了es6的语法,而有的浏览器不支持,我们就可以用babel将es6的语法转换成浏览器支持的es5语法。

在这里我们主要通过运用url-loader对图片资源进行打包,演示一下loader的作用。

新建一个文件夹,命名为image。将图片放在这个目录下。

我们修改index.js文件,新建一个img标签,显示该图片,并将标签加到id为root的div下。

1
2
3
4
5
6
7
8
var domDiv=document.createElement("div");
domDiv.textContent="Hello Webpack!"
var root=document.querySelector("#root");
root.appendChild(domDiv);

var image=document.createElement("img");
image.src=require('../image/pic01.jpg');
root.appendChild(image)

接着我们修改我们的webpack.config.js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module.exports={
/**
* 单入口文件打包
*/
// entry:__dirname+"/js/index.js", //入口文件,即webpack开始打包的入口
/**
* 多入口文件打包
*/
entry:{
bundle:__dirname+"/js/index.js", //入口文件,即webpack开始打包的入口
greeter:__dirname+"/js/greeter.js"
},
output:{
path:__dirname+"/build", //打包出口文件路径
filename:"[name].js", //打包后文件名,如果是多入口,引用[name],会将entry的对象中的key作为文件名
publicPath:"./"
},
module:{
rules:[
{
test: /\.(png|jpg)?$/,
use:[
{
loader:"url-loader",
}
]
},
]
}
}

在这里,我们增加了module,在这里我们在rules数组中添加我们的规则,例如在这里我们添加了一个规则:

在遇到png或jpg结尾的文件后缀时,我们loader运用url-loader去转换。

好了,写完这个后,我们接着利用npm去安装一下url-loader,我们可以修改packge.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"url-loader":"1.0.1"
}
}

然后在终端运行npm install,npm就会自动帮我安装url-loader。接着我们通过webpack命令进行打包。

打包完成后,我们运行index.html文件,我们可以看到图片就被加载到页面中了

Plugins

  • Plugins我们可以通过它,给项目配置很多的插件。
  • webpack也为我提供了完整的插件文档:
  • 在这里我们主要使用BannerPlugin给每个打包后的js文件添加banner文字头,来举例学习Plugins的作用。

根据文档我们发现使用BannerPlugin很简单,我们修改webpack.config.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// webpack.config.js
module.exports={
/**
* 单入口文件打包
*/
// entry:__dirname+"/js/index.js", //入口文件,即webpack开始打包的入口
/**
* 多入口文件打包
*/
entry:{
bundle:__dirname+"/js/index.js", //入口文件,即webpack开始打包的入口
greeter:__dirname+"/js/greeter.js"
},
output:{
path:__dirname+"/build", //打包出口文件路径
filename:"[name].js", //打包后文件名,如果是多入口,引用[name],会将entry的对象中的key作为文件名
publicPath:"./"
},
module:{
rules:[
{
test: /\.(png|jpg)$/,
use:[
{
loader:"url-loader",
options:{
limit:8194,
publicPath:"../build"
}
}
]
},
]
},
plugins: [
new webpack.BannerPlugin('大麦所有,盗版必究')
]
}

我们添加了plugins属性,该属性的值是一个数组,我们可以在该数组中不断的new 插件的实例对象,通过这样我们就可以改webpack设置插件。

是不是很简单?接着我们打包文件,在终端输入webpack打包。

查看我们的打包后的文件,以bundle.js为例:

1
2
// bundle.js
/*! 大麦所有,盗版必究 */!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="./",n(n.s=0)}([function(e,t,n){var r=document.createElement("div");r.textContent="Hello Webpack!";var o=document.querySelector("#root");o.appendChild(r);var u=document.createElement("img");u.src=n(1),o.appendChild(u)},function(e,t){e.exports="../build/a2d8df5678e4b3f4b3d686a173544500.jpg"}]);

我们可以看到我们的打包后的文件头部就被添加了文字Banner。

模块

  • 作用域封装
  • 重用性
  • 接触耦合

Webpack的打包逻辑

  • 从入口文件开始,分析整个应用的依赖树
  • 将每个依赖模块包装起来,放到一个数组中 等待调用
  • 实现模块加载方法,并把它放到模块执行的环境中,确保模块间可以互相调用
  • 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数

npm与包管理器

1
2
// 初始化包
npm init
1
2
3
4
5
6
7
8
9
10
11
12
// package.json
{
"name":"demo", // 包名称
"version":"1.1.0",
"description":"",
"main":"index.js" // 执行入口
"scripts": {
"test":"each \"Error :no test spified\" && exit 1"
}, // 自定义脚本
"author":"",
"license":"ISC"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 设置淘宝镜像仓库
npm config set registry https://npm.taobao.org

# 安装
# save的功能:自动将下载的包写入package.json的dependencies字段
npm install lodash -s
# or
npm install lodash --save

# 写入package.json的devDependencies字段
npm install lodash --save-dev

# 默认-s参数,所以会同时写入dependencies字段和devDependencies字段
# 如果想写入一个的话,使用only
npm insall --only=dev

## 删除所有的包,然后重装
rm -rf node_modules && npm install

版本语义化

  • 1.2.3:大版本为1,中版本为2,小版本为3

  • ^version:中版本中最新的版本

    • ^1.0.1:对于所有形为1.x.x版本中,1.0.1是最新的版本
  • ~version:小版本中最新的版本

    • ~1.0.1:对于所有形为1.0.x版本中,1.0.1是最新的版本

npm install的过程

  1. 寻找包版本信息文件(package.json),依照他进行安装
  2. 查package.json中的依赖,并检查项目中其他的版本信息文件
  3. 如果发现了新包,就更新版本信息文件

模块系统

  • 当今越来越多的网站已经从网页模式进化到了 Webapp 模式。
  • webapp通常是一个单页面应用,每一个视图通过异步的方式加载,这导致页面初始化和使用过程中会加载越来越多的 JavaScript 代码,这给前端开发的流程和资源组织带来了巨大的挑战。
  • 在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统。
  • webpack默认的入口就是src/indesx.js,默认的出口文件就是dist/main.js
  • 如果想要修改的话,就需要修改webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require("path")

module.exports = {
entry: "./app.js", // entry 为打包的入口
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js" // filename 为打包出口
},
devServer: {
port: 3000, // 服务端口
pubilcPath: "/dist"
}
}

webpack-dev-server

监听工程目录的改动,动态的打包和更新

webpack中一切皆资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js

const path = require("path")
module.exports = {
entry: "./src/app.js",
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
module:{
rules: [
{
test: /\.css$/,
use: [
"style-loader",
"css-loader"
]
}
]
}
}
1
2
3
4
import "./style/mycss.css"

console.log("1232")
document.write("123123213asdasda")
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello Webpack</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="../dist/bundle.js"></script>
</body>
</html>

webpack构建react工程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
npm init -y

npm install react react-dom
npm install redux react-redux
npm install webpack webpack-cli -D

# 核心包和命令行包
npm install @babel/core @babel/cli
# bebel的转换规则(preset-env负责ES版本的转换,preset-react负责解析jsx)
npm install @babel/preset-env @babel/preset-react
# plugin-proposal-class-properties 负责解析static PropsType的静态写法
npm install @babel/plugin-proposal-class-properties
npm install babel-loader

npm install html-webpack-plugin -d
  1. 创建src目录,在目录里创建App.jsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // App.jsx
    import React from "react"
    import ReactDom from "react-dom"

    const App = () => {
    return <div>
    <h1>hello world</h1>
    </div>
    }

    export default App

    ReactDom.render(<App></App>,document.getElementById("root"))
  2. 创建index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello Webpack</title>
    </head>
    <body>
    <div id="root"></div>
    </body>
    </html>
  3. 创建index.jsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import App from "./App"

    if(module.hot) {
    module.hot.accept(err => {
    if(err) {
    console.log("热替换出错")
    }
    })
    }
  4. 创建babel的配置

    • 方案一:在package.json里面填写

      1
      2
      3
      4
      5
      6
      7
      8
      9
      {
      "name": "reactdomo",
      "version": "1.0.0",
      "description": "",
      ...
      "babel": {
      "presets": ["@babel/presets-env"]
      }
      }
    • 方案二:创建.babelrc文件(优先级比较高)

      1
      2
      3
      {
      "preset": ["@babel/presets-env"]
      }
  5. 编写webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    const HtmlWebPackPlugin = require("html-webpack-plugin")
    const path = require("path")
    const webpack = require("webpack")

    module.exports = {
    resolve: {
    extensions: [".js", ".jsx", ".json"] // 带有此类后缀的文件,可以不写后缀
    },
    entry: path.resolve(__dirname, "src/index.jsx"),
    module: {
    rules: [
    {
    test: /\.jsx?/,
    exclude: /node_modules/,
    use: {
    loader: "babel-loader",
    options: {
    babelrc: false, // 不使用.babelrc文件
    presets: [
    require.resolve("@babel/preset-react"),
    [require.resolve("@babel/preset-env",{module:false})]
    ]
    }
    }
    }
    ]
    },
    devServer: {
    hot: true
    },
    plugins: [
    new HtmlWebPackPlugin({
    template: path.resolve(__dirname, "src/index.html"),
    filename: "index.html"
    }),
    new webpack.HotModuleReplacementPlugin()
    ]
    }
  6. 执行webpack --mode devlepoment , 执行webpack-dev-server --open

或者:直接填写package.json,然后npm install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"name": "todolist_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/cli": "^7.10.3",
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"html-webpack-plugin": "^4.3.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.1",
"redux": "^4.0.5"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4",
"webpack": "^4.44.0",
"webpack-cli": "^3.3.12"
}
}

reference