此文章为阅读《Webpack实战》时结合自己的理解与实践做的笔记,如果错误欢迎指正。
1.1 何为Webpack
Webpack是一个开源的Js模块打包工具,其核心是解决模块之间的依赖问题,将各个模块按照特定的规则和顺序组织在一起,最终合并成一个JS文件。
那么为什么要打包呢?最开始接触到前端的时候其实最简单的三个html、js和css文件就可以完成一个网页,直接放到服务器上给用户来拿去不就可以了?
1.2 为什么需要Webpack
最开始的网站很简单,确实可以只靠几个文件就可以了,但是后续网站的需求越来越复杂,就需要模块的提供更多的工具来帮助建设网站。
1.2.1 何为模块
用最直观的方式来说,模块就是我们在工程中引入一个日期处理的npm包,或者是写一个提供工具方法的JS文件。这种包和文件都可以称为模块。
1.2.2 JavaScript中的模块
在大多数的程序语言中都有模块开发的概念,但是由于历史原因,JavaScript中没有模块开发的概念(因为当初觉得不会用JavaScript去实现复杂场景,当时只当一个简单的脚本语言来使用)。所以当时使用Js的原始方式为将多个script脚本引入到Html中,没有什么import操作和命名空间的概念。比如如下的页面,如果even.js需要使用到cal.js中的函数,直接按顺序执行声明即可。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>这是一个简单的页面</title>
</head>
<body>
<h1>做个加法叭</h1>
<input type="number" value="5" />
<input type="number" value="2" />
<input id="submit" type="submit" value="计算"></input>
<h1>结果入下:</h1>
<p id="result"></p>
</body>
<script type="text/javascript" src="./cal.js"></script>
<script type="text/javascript" src="./event.js"></script>
</html>
cal.js
const myAdd = (a, b) => a + b
even.js
const btn = document.getElementById("submit")
try{
function start(){
const inputs = document.getElementsByTagName("input")
const a = inputs[0].value
const b = inputs[1].value
console.log(myAdd(a,b))
}
start()
}catch(err){
console.log(err)
}
btn.addEventListener("click", () => {
const inputs = document.getElementsByTagName("input")
const a = inputs[0].value
const b = inputs[1].value
console.log(myAdd(a,b))
})
并且会有如下缺点:
- 需要手动维护JavaScript的加载顺序,多个script脚本通常会有依赖关系,并且这个关系是隐式的,需要特定去维护。比如上面的event.js依赖于cal.js,cal.js一定需要在event.js之前执行,就需要特别注意这样的顺序关系。再加上这种关系是隐式的,若这个文件项目特别大,接到别人转手的项目也就会很难下手。
- 每一个script标签都需要请求一次静态文件,在http2之前过多的请求需要建立多次连接,那网络开销也会很大。
- 命名问题,多个script脚本引入就相当于从全局作用域中开始执行这个脚本,那么可能会有重名问题。比如声明另一个文件。
cal2.js
const myAdd = (a, b) => a + b
并按照cal.js ->event.js -> cal2.js的顺序进行引入,由于这里使用的const声明,所以其实是会报重复声明的错:
假如改成了var,那么第一次执行的myAdd和后面点击执行的myAdd就不是一个函数了。下面图片中第一个是start执行的时候,第二个点击的时候的效果。
那么在之前引入三方库的时候谁知道有没有命名冲突呢,或者命名冲突后难道要一个个改吗。所以总结起来之前的网页编写中存在着下列问题1. 各js文件之前没有显式的依赖关系 2.每个js文件对应着一个请求,网络开销大(不知道http2有没有这方面的问题,这里欠个债之后来讲)3. 存在着未知的命名冲突问题。
模块化解决了上述的所有问题:
- 通过在js中导入导出语句可以看出js文件中的依赖关系
- 借助打包工具可以将多个文件打包成一个文件,比如将cal.js打包到evet.js里,就少了一个cal.js的请求。从而减少网络开销。
- 多个模块之前的作用域是相互隔离的,彼此不会有命名冲突(其实还不太懂是作用域上的什么原理)
自2009年就有了对js模块化的社区版本,如用于服务器的commonJS,直到2015年ES6才有了正式的模块化概念。(难怪之前写前端代码的时候糊里糊涂有时候用module.exports+require,有时候用import+export)
可以在浏览器中使用ES6但无法使用CommonJS,在浏览器中引入脚本的时候带上type="module"既可以使用ES6中的模块化。如将上面的页面换成模块化的写法:
再在even.js中引入cal.js,
可以看到html中没有显式引入cal.js和cal2.js,由于event.js中有对它的引用,在event.js脚本执行的时候自动引入了。
综上所述,ES6中的module解决了上面说到的问题1(利用import建立依赖关系)和问题3(每个文件是一个作用域,可以通过as解决命名冲突),那么Webpack则是进一步解决问题2和其他问题:
- ES6无法使用代码分片和删除死代码
- 大多数npm模块还是用的commonJS的形式,没法拿到浏览器中使用。
- 还没解决问题二中代码打包的问题
所以就到了webPack出场了。
1.2.3 模块打包工具
打包工具就是为了解决模块间的依赖,使打包出的产物能够在浏览器上运行。主要的工作方式为两种:
- 根据依赖关系打包出最终的一个JS文件。
- 在页面初始时加载一个入口模块,异步加载其他模块。
1.2.4 为什么选择Webpack
Webpack有如下优势:
- 支持多种模块标准,包括AMD、CommonJS和ES6 module。
- 有完备的代码分片解决方案,能够分割打包后的资源,在首屏只加载必要的部分,不必要的放在后面去加载。
- 可以处理多种类型的资源。
- 有庞大的社区支持。
1.3 安装
依次需要:
- 安装Nodejs(跳过)
- 使用npm安装webpack,这里npm安装有全局和本地两种形式,具体的区别可以看https://blog.csdn.net/weixin_45719444/article/details/127662714。
全局安装会绑定命令行环境变量,这样在cmd中的任何路径下都能够运行这些命令。比如npm create-react-app相当于运行C:\Users\89731\AppData\Roaming\npm\create-react-app,而npx会运行当前路径下的.\node_modules.bin\create-react-app
这里使用本地安装,因为可以切换着不用不同的webpack版本,如果要和别人协作的话就需要共同用某一个特定的版本。执行安装命令:
npm config set registry https://registry.npmmirror.com
npm install webpack webpack-cli -D
可以看到.\node_modules\.bin中真的就有webpack,要运行webpack需要使用npx命令,也需要cmd在这个项目文件路径中。
1.4 打包第一个应用
index.html,需要注意到里面引用的js文件是打包后的产物,虽然后续的js文件都是使用es module的格式去写的,webpack打包后的产物是直接的原生js文件,引用的不需要说明type="module"。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Title</title>
</head>
<body>
<script src="./dist/main.js"></script>
</body>
</html>
index.js
import addContent from "./add-content";
document.write("My first webpack app<br/>")
addContent()
add-content.js
export default function() {
document.write("Hello World!")
}
接下来使用webpack命令打包入口文件index.js
npx webpack --entry=./index.js --output-filename="main.js" --mode=development
可以看到目录中多出了一个打包的js文件。
打开index.html后也可以正常执行了。
命令行中的entry参数指明入口文件,output-filename是输出资源名,mode是指打包模式,提供了有development、production、none这三种模式。
1.4.2 使用npm script
从上面的例子可以看出每次打包都需要输入较长的命令行,为了使命令行更加简洁,可以在package.json中的script字段添加脚本命令。该字段的原理如下:
该知识点摘自一个网站
所以如上所说,当我们添加了字段:
那么在npm run build的时候会先将./node_modules/.bin加到Path路径中,再执行“webpack --entry=./index.js --output-filename=main.js --mode=development”,那么它执行的其实和npx webpack一样,执行的是shell所在目录下的"./node_modules/.bin中的命令。
1.4.3 使用默认目录配置
上面的index.js是放在工程根目录下的,通常是会将项目中的文件分成源码目录/src和资源输出目录/dist中的,webpack的entry的默认值就是/src/index.js,所以我们可以将index.js放到src目录中,然后省略entry参数,webpack的默认资源输出目录就是dist,可以尝试将打包文件名称改成bundle.js。
1.4.4 使用配置文件
webpack提供多项配置,可以通过命令行的方式进行指定,我们可以尝试使用npx webpack -h查看一下可以配置的选项。
但是当项目需要加入很多配置的时候,使用命令行的方式进行指定是一件很麻烦的事情,webpack提供了webpack.config.js的配置文件供我们进行配置。
import {Configuration} from 'webpack'
/**
* @type{Configuration}
*/
const config = {
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
mode: "development"
}
module.exports = config
代码中的Configuration是使用的'webpack'包中types.d.ts中的类型定义,@type{Configuration}是JS doc中的一个语句,帮助声明紧跟着的变量的类型。由于这里示例是js文件而不是ts文件,所以需要使用这种方式去说明一个变量的类型,有了类型在输入的时候就可以有提示。
可以看到配置文件有层级关系,如output-filename是配置文件中output字段中的filename字段。
定义好这个配置文件后直接运行npx webpack或者在package.json的script字段中加入webpack命令即可。
1.4.5 webpack-dev-server
使用webpack进行打包后会发现一个问题,每次修改js文件后需要重新运行一遍打包命令去更新bundle.js才能更新页面,这对开发来说是不方便的,可以使用webpack-dev-server来帮助自动更新打包。
首先安装webpack-dev-server
npm install -D webpack-dev-server
接着在webpack.config.js中的webpack-dev-server进行相应配置。(书中的字段已经过期了,我只能自己摸索了)
// import {Configuration} from 'webpack'
// import {Configuration as devServerConfig} from 'webpack-dev-server'
const path = require('path');
/**
* @type{Configuration}
*/
const config = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: "development",
/**
* @type{devServerConfig}
*/
devServer: {
static: './dist'
}
}
module.exports = config
devServer相当于是一个本地服务器,其网址为http://[dev url]:[port](url默认值为localhost,port默认值为8080),打包出的js文件会存放为http://[dev url]:[port]/bundle.js中并且会保存在内存中,并不会生成出来。
其中devServer中的static字段表示从本地的哪个文件夹中查找文件,由于打包出的js文件存放在http://[dev url]:[port]/bundle.js中,而index.html的访问地址应为http://[dev url]:[port]或者http://[dev url]:[port]/index.html,为了保证在index.html能使用"./bundle.js"访问到js文件,需要将index.html也放到dist文件中(这里书本好像没说,看得我云里雾里)。
之后运行npm run webpack就可以了,可以在提示的url中访问到正确的结果,在add-content.js上的修改也会实时更新。