概论
webpack是一个静态模块打包工具,可以将项目中所有文件打包成浏览器可以直接识别的文件,使得项目可以在浏览器中运行
前端模块化历史沿革
在es6的模块化标准出现之前,前端实现模块化的方案主要为以下几种
- 最简单的模块化包(设置命名空间)
/*moduleA.js*/
var susan = {
name: "susan",
sex: "girl",
intro: function() {
console.log("我叫", this.name)
console.log("我是", this.sex)
}
}
//通过命名空间解决了沙箱隔离问题,防止了变量冲突,但是会导出所有变量,存在被篡改或者恶意获取风险
- 进化版模块化包(通过闭包将部份变量私有化)
/*moduleA.js*/
var susanModule = (function(){
var name = "susan"
var sex = "girl"
return {
intro: function() {
console.log("我叫", name)
console.log("我是", sex)
}
}
})()
//通过闭包制造私有域,实现只暴露需要暴露的变量
- 标准模块化包(终极版)
/*moduleA.js*/
(function(window){
var name = "susan"
var sex = "girl"
function intro() {
console.log("我叫", name)
console.log("我是", sex)
}
window.susanModule = {intro}
})(window)
//最标准写法,为早期模块化标准写法
早期的一些前端插件大多是通过方法三来实现模块化,后面es6提出了模块化方案之后,文件即模块的思想以及简洁的语法给我们开发带来了不少便利,不过也带来了新问题,就是不是所有浏览器都支持es6的语法,我们基于es6模块标准编写的代码也不被浏览器所识别,怎么让我们基于es6模块方案编写的项目在浏览器运行起来,这就是webpack做的工作。
webpack基础
从一个简单小例子开始
新建一个项目,文件目录如下所示,其中package.json为npm init -y生成的默认空白配置
- webpack-test
- index.html
- index.js
- module1.js
- package.json
index.html文件如下图所示,是一个简单的html5模板文件,并且引入index.js
<!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>Document</title>
</head>
<body>
</body>
<script src="./index.js"></script>
</html>
index.js文件如下图所示,引入module1模块中的默认方法并执行,之后在document中写一段文本
import module1 from "./module1.js"
module1()
document.write("write by index.js")
module1.js文件内容如下图所示
export default function module1(){
document.write("write by module1")
}
这时候直接去浏览器打开这个html会报错,因为浏览器识别不了我们的import语法,所以这个时候我们就需要webpack把我们的模块打包成浏览器可以识别的语法,通过npm或者yarn在我们刚才的项目中添加webpack和webpack-cli两个包然后在我们项目根目录下创建一个wepack.config.js文件,内容如下:
const path = require("path")
module.exports = {
entry: path.resolve(__dirname, "index.js")
}
这个文件中我们只制定了一个entry字段,因为webpack默认的入口文件路径为src/index,所以我们手动修改为我们实际的入口文件地址,然后我们在命令行中在项目路径下运行webpack指令,然后我们会发现,项目被打包成功了,目录下多了dist文件夹,和dist目录下的main.js,这时候我们去修改下index.html里外链js的引入,改为
<script src="./dist/main.js"></script>
再去浏览器中打开index.html这时候我们发现,页面中输出了write by module1write by index.js,出现了我们预期的结果,如果你去看一下打包好之后的main.js,你就会发现输出的文件内容与我们之前提到的早期模块化方案方法三很像,以上就是一个最简单的webpack项目打包的过程,通过这个小demo我们知道了webpack到底对我们的项目做了什么
webpack进阶(loader与plugin)
通过上面的小demo,我们简单的实践了webpack的打包,但是实际项目中,项目的复杂度肯定是远远比我们的demo复杂的,并且在使用特定框架时,我们可能会使用一些浏览器无法识别的文件类型,比如jsx,亦或是为了提升开发效率我们使用less或sass等css预编译语言,或者我们在开发时为了方便使用px作为单位,但是我们希望打包好之后的文件以rem为单位,即节省我们计算的时间,又保证了多终端适配,当这些复杂的需求出现时,使用对应的loader/plugin就可以满足我们的需要
loader
不同的loader可以给webpack提供处理不同类型文件的功能,比如css文件有css-loader,html文件有html-loader,当我们要通过webpack处理其原生不支持的文件类型时,就需要安装相应的loader
我们对之前的小demo进行一番改造,首先安装react和react-dom这两个包到我们的项目里,然后把入口文件和module1模块都改为jsx文件,并且在index.html中加一个dom作为jsx文件生成dom的挂载点,并且在里面使用一些es6语法,改完之后的文件对应如下:
/* 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>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
/* module1.jsx */
import React from "react"
import ReactDom from "react-dom"
const App = () => {
return (
<div>
<p>jsx打包测试</p>
</div>
)
}
ReactDom.render(<App/>, document.getElementById("app"))
export default App
/* index.jsx */
import App from "./module1.jsx"
这样改完之后,我们的需求就很明确了,因为webpack原生只支持打包js与json格式的文件,现在我们面临的第一个问题就是要让我们的webpack能够打包jsx文件,然后因为我们用了es6的箭头函数,为了保证浏览器兼容性,我们还需要将代码编译为es5的代码。
通过搜索可以知道babel-loader即可以用来处理jsx文件,还能转义es6代码,因此需要在项目中引入babel-loader,要使用babel-loader还需要先引入@babel/core和@babel/cli两个包,然后针对转义es6与处理jsx的需求,还需要引入@babel/preset-react和@babel/preset-env两个依赖项(关于loader配置不再赘述,详细查阅文档)修改webpack.config.js如下
const path = require("path")
module.exports = {
entry: path.resolve(__dirname, "index.jsx"),
module: {
rules:[
{
test: /.jsx?/,
//正则表达式来匹配loader适配的文件类型
exclude: /node_modules/,
//不包含的文件目录
use: {
loader: "babel-loader",
options: {
babelrc: false,
//设置为false代表无bablerc配置文件,babel配置以webpack文件为准
presets: [
require.resolve("@babel/preset-react"),
require.resolve("@babel/preset-env")
]
//引入我们需要的两个配置项
}
}
}
]
}
}
再运行webpack指令,打包成功之后,在index.html里面引入打包好的main.js,在浏览器中打开,如我们所预期的正常渲染了,这就是一个最简单的loader能力的demo,通过loader我们让webpack能处理的文件类型变的丰富了起来,但这时候我们发现,手动指定html的引入好像有些不太合理,加入我们修改了webpack的输出文件名,我们还要手动修改引入的路径,并且要部署的话,我们还要分别把dist里的内容和index.html作部署,可不可以自动把打包好的文件引入到index.html中,并且把引入后的文件也输出到dist文件夹中呢?答案肯定是可以的,这就要借助webpack的另一大主要特性plugins
plugins
与loader不同,plugins不提供新的文件打包能力,但是它能扩展webpack的能力,实现对文件的压缩,修改,替换等功能,并且能改变webpack输出结果,根据上面我们的需求,我们需要改变index.html的内容,并且输出他,通过搜索我们知道这里需要用html-webpack-plugin这个plugin,对应该修改配置如下:
const path = require("path")
const HtmlWebPackPlugin = require("html-webpack-plugin")
module.exports = {
entry: path.resolve(__dirname, "index.jsx"),
module: {
rules:[
{
test: /.jsx?/,
//正则表达式来匹配loader适配的文件类型
exclude: /node_modules/,
//不包含的文件目录
use: {
loader: "babel-loader",
options: {
babelrc: false,
//设置为false代表无bablerc配置文件,babel配置以webpack文件为准
presets: [
require.resolve("@babel/preset-react"),
require.resolve("@babel/preset-env")
]
//引入我们需要的两个配置项
}
}
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: path.resolve(__dirname, "index.html"),
//指定文件路径
filename: "index.html"
//指定输出文件名
})
],
}
再执行webpack打包命令,发现输出的dist文件夹中多了index.html文件
webpack-dev-server
实际开发中我们不可能改一点代码就手动打一次包然后刷新浏览器看变化,如果你使用过诸如create-react-app这类的脚手架,你肯定知道我们开发的时候都会通过npm run start指令来起一个本地的服务,而这个npm run start就是脚手架为我们写好的webpack-dev-server指令。
例如我们也可以在刚才的项目中添加这样的指令,在package.json中的scripts字段中添加我们需要的指令,如"myStart": "webpack-dev-server --mode development --open",当我们在命令行运行npm run myStart指令时,就会替我们默认执行我们写好的对应指令。
webpack-dev-server做的工作就是在本地起一个web服务,然后对我们的项目进行打包,并且不会输出打包结果,而是把打包后的结果存在内存中,当我们访问其监听的指定端口时,就会看到我们打包后的运行结果,而至于端口这些属性也都是可以配置的,热重载之类的给我们开发带来便利的特性,也可以通过对应plugin来实现,在此就不再赘述,感兴趣的可以自己查阅文档,动手配置一下。
备注
本文为学习腾讯NEXT学院发布的webpack教程视频之后整理,加入了一些本人的理解,附原视频链接: Webpack从原理到实战完整版