文章目录
1.前言
上一篇文章说了一篇文章带你玩转前端所有模块化,有了模块化,那么肯定需要模块化打包工具,那么从这一篇文章开始,我将开始来讨论下webpack模块化打包工具,Webpack有一定基础的读者可以选择略过,对于零基础的同学,跟着我一起学,后面我会持续更新,看完的话会受益很多!废话不多说,进入正题!
2.Webpack是什么?有什么用?
Webpack是一个开源的JavaScript模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个JS文件(有时会有多个,这里讨论的只是最基本的情况),这个过程就叫作模块打包。你可以把Webpack理解为一个模块处理工厂。我们把源代码交给Webpack,由它去进行加工、拼装处理,产出最终的资源文件。
3.为什么选择Webpack?webpack的优势在哪?
webpack默认支持多种模块标准,包括AMD、CommonJS,以及最新的ES6模块,而其他工具大多只能支持一到两种。这对于一些同时使用多种模块标准的工程非常有用,Webpack会帮我们处理好不同类型模块之间的依赖关系。
webpack有完备的代码分割(code splitting)解决方案。从字面意思去理解,它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态地加载。这对于资源体积较大的应用来说尤为重要,可以有效地减小资源体积,提升首页渲染速度。
webpack可以处理各种类型的资源。除了javaScript以外,webpack还可以处理样式、模板,甚至图片等,而开发者需要做的仅仅是导入它们。比如你可以从JavaScript文件导入一个CSS或者PNG,而这一切最终都可以用loader来处理。
webpack拥有庞大的社区支持。除了webpack核心库以外,还有无数开发者来为它编写周边插件和工具,绝大多数的需求你都可以直接找到已有解决方案,甚至会有多个解决方案供你挑选。
4. 如何安装 webpack?
Webpack对Node.js的版本是有一定要求的,推荐使用Node.js的LTS(长期维护),首先我们需要安装node.js ,安装完成后,打开命令行并执行node–v,显示当前Node.js的版本号,代表已经安装成功。
我电脑之前安装过,所以版本不是最新的版本!接下来我们需要使用Node.js的包管理器npm来安装Webpack。使用过npm的应该知道,安装模块的方式有两种:一种是全局安装,一种是本地安装。全局安装Webpack的好处是npm会帮我们绑定一个命令行环境变量,一次安装、处处运行;本地安装则会添加其成为项目中的依赖,只能在项目内部使用。这里建议使用本地安装的方式(如果采用全局安装,那么在与他人进行项目协作的时候,由于每个人系统中的Webpack版本不同,可能会导致输出结果不一致)。
首先新建一个工程目录:
npm init
接着按顺序输入项目名,版本 描述,作者等这些信息!
那么此时你项目里面有个package.json文件,它相当于npm项目的说明书,里面记录了项目名称、版本、仓库地址等信息(刚才上面输入的信息)。
接下来执行安装webpack,webpack-cli,webpack是核心模块,webpack-cli是命令行工具,你装了它才能用命令执行webpack,安装结束之后,在命令行执行npx webpack-v以及npx webpack-cli-v,可显示各自的版本号,即证明安装成功(我是之前安装的,不是最新版本,只是为了截图给大家看)!
5. entry,chunk,bundle是什么?
5.1 entry
对于webpack来说每个文件都是个模块(module),简单的来说就是入口文件(entry),在一切流程的最开始,我们需要指定一个或多个入口(entry),也就是告诉webpack具体从源码目录下的哪个文件开始打包。如果把工程中各个模块的依赖关系当作一棵树,那么入口就是这棵依赖树的根,看下图:
5.2 chunk
chunk字面的意思是代码块,在webpack中可以理解成被抽象和包装过后的一些模块。它就像一个装着很多文件的文件袋,里面的文件就是各个模块,webpack在外面加了一层包裹,从而形成了chunk,webpack会从入口文件开始检索,并将具有依赖关系的模块生成一棵依赖树,最终得到一个chunk。根据具体配置不同,一个工程打包时可能会产生一个或多个chunk,看下图:
5.3 bundle
由这个chunk得到的打包产物我们一般称之为bundle!在工程中可以定义多个入口,每一个入口都会产生一个结果资源。比如我们工程中有两个入口src/index.js和src/vendor.js,在一般情形下会打包生成dist/index.js和dist/vendor.js,因此可以说,entry与bundle存在着对应关系看下图:
6.资源入口
webpack其实通过context和entry这两个配置项来共同决定入口文件的路径。在配置入口时,实际上做了两件事:确定入口模块位置,告诉webpack从哪里开始进行打包。定义chunk name。如果工程只有一个入口,那么默认其chunk name为“main”(上面的例子就一个路口);如果工程有多个入口,我们需要为每个入口定义chunk name,来作为该chunk的唯一标识,entry的配置可以有多种形式:字符串、数组、对象、函数。可以根据不同的需求场景来选择!
1.字符串类型入口:后面接的全是字符串。
entry: "./src/index.js",
2.数组类型入口:传入一个数组的作用是将多个资源预先合并,在打包时Webpack会将数组中的最后一个元素作为实际的入口路径。
entry: ["/src/b.js","./src/index.js"],
这样写就相当在./src/index.js文件里面引入了b.js文件。
//index.js
import "/src/b.js"
3.对象类型入口:对象的属性名(key)是chunkname,属性值(value)是入口路径。
entry: {
//chunk name为one,路口文件为"./src/index.js"
one: "./src/index.js",
//chunk name为two,入口文件为"./src/b.js"
two: "./src/b.js"
}
//还可以下面这样写!
entry: {
one: ["/src/b.js","./src/index.js"],
two: "./src/c.js"
}
4.函数类型入口:用函数定义入口时,只要返回上面介绍的任何配置形式即可
entry:()=> "./src/index.js",
entry:()=> {
one: ["/src/b.js","./src/index.js"],
two: "./src/c.js"
}
传入一个函数的优点在于我们可以在函数体里添加一些动态的逻辑来获取工程的入口。另外,函数也支持返回一个Promise对象来进行异步操作。
entry:new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve( "./src/index.js")
},2000)
})
7. webpack入门应用
准备两个js文件
b.js:
export function moduleB() {
document.write("我是B模块.")
}
a.js:
import {moduleB} from "./b";
document.write("我是A模块.<br>");
moduleB();
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack </title>
</head>
<script src="./dist/bundle.js"></script>
<body>
</body>
</html>
先用webpack-cli的命令行来执行打包
npx webpack --entry=./a.js --output-filename=bundle.js --mode=development
命令行的第1个参数entry是资源打包的入口。webpack从这里开始进行模块依赖的查找,得到项目中包含a.js和b.js两个模块,并通过它们来生成最终产物。
命令行的第2个参数output-filename是输出资源名。你会发现打包完成后工程中出现了一个dist目录,其中包含的bundle.js就是webpack的打包结果。
命令行的第3个参数mode指的是打包模式。webpack为开发者提供了development、production、none三种模式。当置于development和production模式下时,它会自动添加适合于当前模式的一系列配置,减少了人为的工作量。在开发环境下,一般设置为development模式就可以了。
项目中的a.js和b.js现在已经成为了budnle.js,被页面加载和执行,并输出了各自的内容。看下图:
我们每进行一次打包都要输入一段冗长的命令,这样做不仅耗时而且容易出错。为了使命令行指令更加简洁,我们可以在package.json中scripts添加一个build脚本命令!scripts是npm提供的脚本命令功能,在这里我们可以直接使用由模块所添加的指令(比如用“webpack”取代之前的“npx webpack”)。
{
"name": "webpacktest",
"version": "1.0.0",
"description": "webpack demo",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build":"webpack --entry=./a.js --output-filename=bundle.js --mode=development"
},
"author": "ws9029",
"license": "ISC"
}
现在打包命令就简单多了!
npm run build
上面的a.js,b.js是放在根目录下的,而通常情况下我们会分别设置源码目录与资源输出目录。工程源代码放在/src中,输出资源放在/dist中,webpack默认的源代码入口就是src/index.js,那么我们只需要把a.js改成index.js,因此现在可以省略掉entry的配置了。看下面目录结构:
修改package.json scripts build配置 :
"build":"webpack --output-filename=bundle.js --mode=development"
当项目需要越来越多的配置时,就要往命令中添加更多的参数,那么到后期维护起来就会相当困难。为了解决这个问题,可以把这些参数改为对象的形式专门放在一个配置文件里,在webpack每次打包的时候读取该配置文件即可,webpack的默认配置文件为webpack.config.js,也可以使用其他文件名,需要使用命令行参数指定,比如你想把配置文件改成webpack.test.config.js,那么package.json的scripts中的build就要改成:
"build":"webpack --config webpack.test.config.js"
为了方便,我们在工程根目录下创建webpack.config.js,并添加如下代码:
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
},
mode: "development"
}
因为它也是个模块,所以通过module.exports导出了一个对象,也就是打包时被Webpack接收的配置对象。先前在命令行中输入的一大串参数就都要改为key-value的形式放在这个对象中。执行npm run build,Webpack就会预先读取webpack.config.js,然后进行打包。我们修改下index.js文件:
import {moduleB} from "../b";
document.write("我是A模块.现在使用webpack config配置打包<br>");
moduleB();
再执行打包
npm run build
看下图打包后的结果:
上面我们一直使用打包之后的默认路径,就是在dist目录下,我们也可以自己定义目录路径!这时候就要用到webpack path模块!比如我想改成test目录下!看下面代码:
const path = require('path');
module.exports = {
entry: "./src/index.js",
output: {
// path:path.join(__dirname,"/out"),
path:path.resolve(__dirname,"./out"),
filename: "bundle.js",
},
mode: "development"
}
执行 npm run build 看下图:
__dirname中文目录名意思,我这个项目的目录是E:/projectTest/webpackTest,__dirname为E:/projectTest/webpackTest,也可以理解为项目的根目录,你可能看到我上面注释了path:path.join(__dirname,"/out"),那么join和resolve有什么区别呢?这个问题先留在这里,后面会详细讲!
tip:根据上面说的资源入口,如果多页应用(上面例子只有一个html,所以是单页面,不过我们可以模拟下多页面应用),我们希望每个页面都只加载各自必要的逻辑,而不是将所有页面打包到同一个bundle中。因此每个页面都需要有一个独立的bundle,这种情形我们使用多入口来实现我们可以改成下面这样!
const path = require('path');
module.exports = {
//context+path就是入口文件的路径
context:path.resolve(__dirname, "./src"),
entry: {
//chunk name one
one: "./index.js",
//chunk name two
two: "./b.js"
},
output: {
path: path.resolve(__dirname, "out"),
},
mode: "development",
}
入口与页面是一一对应的关系,这样每个HTML只要引入各自的JS就可以加载其所需要的模块!在多入口的场景中,我们需要为对应产生的每个bundle指定不同的名字,Webpack支持使用一种类似模板语言的形式动态地生成文件名!
const path = require('path');
module.exports = {
context:path.resolve(__dirname, "./src"),
entry: {
one: "./index.js",
two: "./b.js"
},
output: {
path: path.resolve(__dirname, "out"),
filename:"[name]@[contenthash].js"
},
mode: "development",
}
在资源输出时,上面配置的filename中的[name]会被替换为chunk name,因此最后项目中实际生成的资源是one.js与two.js,看下图!
上面我用到了chunkhash,其实还可用hash、chunkhash、contenthash,三者可以处理缓存,他们都有什么区别呢?
hash :打包后生成的文件名后都会增加相同的一串hash码。
chunkhash:打包后生成的文件会根据是否存在依赖关系(是否为同一chunk块)若存在则打包后的文件会增加相同的hash码。
contenthash打包生成的文件会根据内容是否发生变化,若和上一次比较有变化则生成新的hash值,否则再次打包时hash值不变,当重新编译时,若文件hash值发生变化则浏览器会重新请求新的文件,若文件hash值没变即和上一次请求的文件名相同则浏览器会从缓存中读取文件。
8. path的join和resolve区别
8.1 path.join
语法:path.join([path1][, path2][, ...])
path.join()方法可以连接任意多个路径字符串。要连接的多个路径可做为参数传入!
path:path.join(__dirname,"out","a","b"),
//当前的__dirname是E:/projectTest/webpackTest,则输出结果为
//E:/projectTest/webpackTest/out/a/b
8.2 path.resolve
语法:path.resolve([from ...], to)
path.resolve()方法可以将多个路径解析为一个规范化的绝对路径。其处理方式类似于对这些路径逐一进行cd操作,resolve()方法不会系统判断路径是否存在,而只是进行路径字符串操作!
path:path.resolve(__dirname,"./out","a","b"),
//当前的__dirname是E:/projectTest/webpackTest,则输出结果为
//E:/projectTest/webpackTest/out/a/b
path:path.resolve(__dirname,"/out","a","b"),
//当前的__dirname是E:/projectTest/webpackTest,则输出结果为
//E:out/a/b
path:path.resolve(__dirname,"../out","a","b"),
//当前的__dirname是E:/projectTest/webpackTest,则输出结果为
//E:/projectTest/out/a/b
注释写的很清楚,应该能看的懂,我这里就不上图了!path.resolve和path.join都可以用,但是要正确的使用!
这篇写的有点长了!下篇接着写!后续持续更新!想和我一起学webpack的朋友,或者觉得写的还可以,加个关注呗!