webpack介绍
什么是webpack?
webpack是一个本地的编译环境,他可以把浏览器不识别的文件类型或语法,通过loader和plugin等插件转换成浏览器能识别的html,css和javascript并实时编译和输出
webpack的简介
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
我们来通过图片理解一下webpack的核心功能
本质上,webpack就是一个加工工厂,将左侧复杂的文件类型和语法转换成右侧的web页面可用的语法和文件
webpack的基本使用方式
npm i webpack -D #安装webpack核心文件
npm i webpack-cli -D #安装webpack脚手架工具
-D的介绍,平时我们安装依赖都是使用-s来安装今天使用了-D他们的主要区别就是-s代表项目构建的必要依赖,比如我们使用jquery开发项目,那么jquery就需要-s安装,因为打包构建之后jquery也必须出现在项目里,-D的依赖叫做开发依赖他的作用就是为了解决项目在开发过程中的一些调试编译的功能,他是用来操作项目的,也就是说构建之后webpack这种打包工具不需要存在于项目里,所以我们使用-D安装.
创建一个简单的webpack项目
首先在文件夹中创建一个名为 webpack01 的文件夹并在该目录下执行
npm init -y#将其初始化为npm项目,并声明 package.json
然后安装webpack和webpack-cli
安装完毕之后我们创建src文件夹并且在src文件夹之下创建index.js文件
目录结构如下
├── package-lock.json #package的相关锁定依赖
├── package.json #项目依赖核心描述文件
├── src #源代码文件夹
└── index.js #项目内容
在index.js中创建如下内容
const name = 'hello webpack'
在项目根目录下创建webpack.confifig.js文件
//webpack.config.js
const path = require('path')//引入path模块
module.exports = {
entry:{
//entry表示webpack编译的入口文件,代表当前我们项目中使用的源代码部分的文件路径和依赖名称
index:'./src/index.js'
//index代表构建时生成的js依赖名称,他的值代表编译之前要扫描的文件路径
},
mode:"production",
//设置当前打包的方式为生产环境构建
output:{
//output代表构建完毕后生成的依赖的配置对象
//path代表生成的js文件要放置的文件路径
path:path.resolve(__dirname,'dist'),
//filename代表生成的js文件要生成的名称[name]相当于从entry中获取的
//名称,就是entry中配置的key
filename:'[name].bundle.js'
}
}
在package.json中配置webpack编译要执行的指令
"scripts": {
//该注释在package.json中使用时要删掉,package.json不允许使用注释
//build为webpack运行命令,执行npm run build时会执行该命令
//该命令的意义为通过webpack运行webpack.config.js文件的配置参数,并展示颜色和过程
//运行后webpack就会编译entry中配置的js文件并输出到output中配置的路径内
"build": "webpack --config webpack.config.js --color --progress"
},
以上操作完成之后在项目根目录下运行
npm run build
然后我们发现在index.bundle.js中什么都没有,这时我们将index.js改造成如下效果
const name = 'hello webpack'
console.log(name)
我们再次运行 npm run build 查看结果这时index.bundle.js中的输出的结果为
console.log("hello webpack");
总结:
通过以上案例我们可以将webpack理解成一个加工厂,他的工作就是将我们写的js以及其他代码 进行一次修改之后再输出到指定的位置。
webpack的用法
配合loader使用
babel-loader
我们在src/index.js中添加如下代码
//src/index.js
const name = 'hello webpack'
console.log(name)
let d = new Promise((resolve,reject) => {
setTimeout(()=>{
resolve('hello')
},1000)
})
d.then(res => {
console.log(res)
})
const arr = [1,2]
arr.map(item => {
console.log(item)
})
然后再次执行
npm run build
查看生成的代码后我们发现Promise和es6的map循环都是原样输出的只是做了代码混淆并没有其他的变化
那么这个构建对我们来说其实是一次无意义的构建。因为Promise和es6以后的语法并不是所有浏览器都支持的
webpack存在的意义不光是构建代码,还要在构建代码的过程中解决兼容性的问题
这里我们就需要学习一个新的功能loader。
首先我们看一下官方对loader的介绍:
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 可以将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
我们在这里可以对他做一个最初步的理解,就是loader可以帮我们解决javascript兼容性的问题,
还可以让浏览器本来不能执行的代码变成浏览器能执行的js代码
这里就需要使用一个叫做babel-loader的插件
npm i babel-loader -D @babel/core -D
建议同时安装babel-loader和@babel/core 这样npm会帮助我们分析他们彼此依赖的版本
运行 npm run build 查看 index.bundle.js发现内容并没有发生任何改变
这是因为光有babel-loader是不够的所以接下来我们要把babel的核心包配置起来
npm i @babel/preset-env -D
npm i core-js -s
执行完以上操作之后我们在项目根目录创建一个名为.babelrc的文件
该文件的作用是用来让babel可以根据我们代码中使用的语法来决定是否将代码做兼容性处理
//.babelrc 配置babel/preset-env用来驱动core-js来解析Promise等包
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
//设置为自动检测es6以后的语法并按需引入相关库
"corejs": 3
//使用core-js3版本的源码库
}
]
]
}
再创建.browserslistrc文件
> 0.25%
last 2 versions
browserslistrc文件用来描述我们当前的项目要兼容的浏览器的范围,babel会根据需要兼容的浏览器范围自动的进行代码的转译,这样我们就不需要考虑当前写的js是否在我们想要兼容的浏览 器中可以使用,因为我们设置浏览器范围之后babel就会输出成这个范围内的浏览器都能正常执行 的代码
browserlist配置说明
在webpack.config.js中添加如下配置
module: {
rules: [
{
//test代表当前的loader扫描的文件类型
test: /\.js$/,
//use代表对当前文件类型应用的loader是什么
use: [
{
loader: path.resolve(__dirname, "./node_modules/babel-loader"),
},
],
},
],
},
然后我们再次执行npm run build
这里我们看他从
core-js中拿出了map和promise的包,我们可以在构建出来的代码中搜索promise和map发现有很多地方对promise和map做了声明。 这就是babel根据我们在browserslistrc中定义的兼容范围来决定的构建结果,为了兼容> 0.25%使用率的浏览器并且兼容到最后两个版本,babel为了防止部分浏览器对Promise和map循环不兼容,他引用了core-js中单独对map循环和Promise库的代码,这样就算浏览器不兼容map循环和promise对象,也能直接调用core-js中实现的promise和map循环,这个就是babel的作用。
我们可以在当前的目录下在命令行执行npx browserslist ,用来查看当前项目的.browserslistrc文件中的配置描述所兼容的浏览器列表
配合plugin使用
当我们已经解决了
js的兼容性问题之后再考虑一个问题,我们使用webpack一定会配合项目去使用,这样的话如果只能编译js文件是完全没有用的,因为只有js文件我们没法在本地直接运行,需要依赖html容器,这样我们才能实现边开发边看结果。所以想要让我们写的js不光能编译还能执行就需要使用webpack另一个功能:plugins
html-webpack-plugin
它可以将我们定义的
js文件打包构建生成html与js混合的项目。首先我们要安装它
npm i html-webpack-plugin -D
安装完成之后我们需要做的就是在webpack.confifig.js文件中引用他并且配置到plugins中
//webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 然后在module,entry,output的同级别位置添加
plugins:[
new HtmlWebpackPlugin({
template:'./public/index.html',//html原始模版位置
filename:'index.html',//生成的html文件名
//生成的html文件中引用的js依赖模块,这里填写的就是在entry中定义的key
//也就是说如下配置相当于在index.html中引用了src/index.js
chunks:['index']
})
]
然后我们在项目根目录下创建public文件夹
并在public文件夹内部创建index.html文件 然后我们再执行
npm run build
查看结果,这次我们的dist文件夹中多生成了一个html文件,我们查看他的源代码,发现这个
html文件自动引入了index.bundle.js并且运行他可以自动执行index.js文件。
完成此步骤之后我们回想
vue,react等脚手架的结构。我们这里的index.html就是当时我们项目中的index.html,这里的index.js就是项目中的main.js,学习这里我们就能理解为什么当时脚手架中不需要在index.html中引入任何依赖就可以关联到js文件了。因为他们都是通过html-webpack-plugin实现的。
resolve的使用
resolve是webpack提供的一个用来解析文件以及引用路径的模块。
比如我们在过去的脚手架中使用import引用文件时,不需要写文件的后缀就可以实现引用对应的模块
并且如果引用的文件是某个文件夹中的index.js如test/index.js,我们可以直接引用到该文件夹即可,如import xxx from './test',还有我们可以通过import xxx from '@/components/xxx'
这种@的形式来当作绝对路径进行模块的引用。
这些能力都是通过resolve对象进行解析的。
他的配置方式如下,在webpack.confifig.js中添加如下代码
resolve: {
//extensions用来定义后缀列表,在这里定义的后缀的文件类型在import引用时就不需要填写后缀
extensions: [".js", ".jsx", ".vue", ".less", ".sass"],
//alias代表引用路径的别名,如下配置代表import中如果存在@符号的路径
//@符号就会被解释为从电脑根目录到当前项目的src,所以如果引用models/user-model.js
//只需要import userModel from '@/models/user-model.js'就相当于从根目录到user-
// model.js的全路径
alias: {
"@": path.resolve(__dirname, "src"),
},
},
我们现在配置完resolve之后在src中创建一个model文件夹并且在其中创建一个user-model.js文件
//model/user-model.js
export default {
namespaced:true,
state:{
list:[]
}
}
然后我们在src目录index.js中添加
import userModel from "@/user-model";
console.log(userModel)
然后运行npm run build
然后我们直接运行生成的index.html文件,查看控制台是否打印了userModel对象的内容,
如果打印出了结果就说明我们定义的别名和全路径的代号都生效了
样式处理
完成了上述功能之后我们的webpack环境已经可以解决开发和代码的初步编译了,下面我们学习一下如何在webpack环境中使用css样式。
首先我们在src中创建一个index.css文件
/*index.css*/
.test {
display: flex;
flex-direction: column;
justify-content: center;
width: 100px;
height: 100px;background-color: green;
}
如果想在js中引入css样式需要通过style-loader和css-loader
npm i style-loader -D #安装style-loader,style-loader是处理style样式的,他用来将样式文件通过js动态的添加到html的head标签中来实现样式加载
npm i css-loader -D #安装css-loader,css-loader是处理css样式文件的
安装完毕之后我们需要在webpack的module中的rules内部添加css文件的loader
//webpack.config.js
//将这段代码添加到module中的rules中
{
test: /\.css$/,
//检测css结尾的文件
//这里的loader是有顺序的,固定写法,先放style-loader,
// 后放css-loader,loader的加载顺 序是倒着加载的,所以会
// 先执行css-loader将css样式代码转成js代码,然后执行
// style-loader将js中 的样式动态添加到网页中
use: [
{ loader: "style-loader" },
//使用style-loader用来将style样式追加到js代码中
{ loader: "css-loader" },
//使用css-loader用来解析css文件
],
},
然后我们尝试在index.js中通过import引用css文件
import '@/index.css' 然后执行 npm run build
我们运行dist文件夹下的index.html,发现样式生效
这里我们在看打包构建的index.bundle.js,发现内部有处理样式的js文件 合的话会造成index.bundle.js单文件过大。
这里我们还需要使用一个
plugin来处理css的分离:mini-css-extract-plugin
npm i mini-css-extract-plugin -D
然后我们在webpack.confifig.js中引用
改造css-loader部分的代码改造为如下
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// ...
{
test:/\.css$/,
use:[
//{loader:'style-loader'},
MiniCssExtractPlugin.loader,//这里必须将MiniCssExtractPlugin放在这个位置并且提
取css之后不需要使用style-loader
{loader:'css-loader'},
]
}
最后一步是将MiniCssExtractPlugin注册到plugins中
//将它放入plugins中
new MiniCssExtractPlugin({
filename:'[name].css'//这里的[name]获取的还是entry中的key
})
完成之后我们执行npm run build
然后发现打包构建出来的文件多了一个index.css并且查看html文件发现已经将css自动引入了。
但是这里还缺少一个功能就是css在各浏览器的兼容性也是存在差异的,有些浏览器处理的样式需要单独的增加浏览厂商的前缀才能展示正确的样式,这里我们可以通过一个叫做postcss-loader的loader来处理css兼容性问题所以我们需要安装postcss-loader和postcss 以及postcss-preset-env三个组件
npm i postcss-loader -D postcss -D postcss-preset-env -D
然后在webpack.confifig.js中配置postcss-loader,改造module如下
{
test:/\.css$/,
use:[
//{loader:'style-loader'},
MiniCssExtractPlugin.loader,
{loader:'css-loader'},
{loader:'postcss-loader'}//postcss-loader安装在最后
]
}
然后在根目录创建postcss.config.js
//postcss.config.js这里默认插件使用postcss-preset-env
module.exports = {
plugins: {
"postcss-preset-env": {},
},
};
然后将browserslistrc改成
> 0.25%
last 2 versions
让他变成兼容使用率大于0.25%的浏览器并且兼容到每款浏览器的最后两个版本
然后我们在通过npm run build来查看得到的index.css发现
.test {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
width: 100px;
height: 100px;background-color: green;
}
生成的css样式变成了带前缀的样式表,这样我们就可以实现兼容更多的浏览器了。
但是还有一点不足的地方,我们来看一下当前的css打包的结果并不是压缩效果,而是纵向展示的,这样我们的css文件会比压缩文件大,所以我们应该使用css压缩技术来让他和js效果一样,这里需要安装postcss的插件cssnano
npm i cssnano -D
安装完成后改造postcss.confifig.js
module.exports = {
plugins: {
"postcss-preset-env": {},
'cssnano':{}
},
};
然后我们接着打包构建npm run build,查看dist/index.css,发现index.css只在一行显示,说明压缩成功。
样式处理之sass处理
首先我们在src中创建一个index.scss
.p-test {
display: flex;
justify-content: center;
.p-item {
flex-grow: 1;
}
}
然后我们在index.js中引入index.scss
import '@/index.scss'
然后安装sass loader
npm i sass-loader -D sass -D
配置webpack.config.js文件
{
test: /\.scss$/,
use: [
//{loader:'style-loader'},
MiniCssExtractPlugin.loader,
{ loader: "css-loader" },
{ loader: "postcss-loader" },
{ loader: "sass-loader" }, //使用sass-loader来编译sass语法
],
},
然后我们接着打包构建
npm run build
关于抽象语法树
AST介绍
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示。这里特指编程语言的源代码。
树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。
抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。
为什么需要抽象语法树
编程语言太多,需要一个统一的结构让计算机识别。
作用:比如 typescript 的类型检查,IDE的语法高亮,代码检查,转译等等,都是需要先将代码转化成AST在进行后续的操作。
抽象语法树的生成过程(编译)
以js为例:
词法分析(lexical analysis):进行词法分析的程序或者函数叫作词法分析器(Lexical analyzer,简
称Lexer),也叫扫描器(Scanner,例如typescript源码中的scanner.ts),字符流转换成对应的 Token流。
tokenize:tokenize就是按照一定的规则,例如token令牌(通常代表关键字,变量名,语法符号 等),将代码分割为一个个的“串”,也就是语法单元)。涉及到词法解析的时候,常会用到tokennize。
语法分析(parse analysis):是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成语法树,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确。源程序的结构由上下文无关文法描述。
词法分析的示例:
const a = 1;
const b = a + 1; 词法分析器在解释这段语法的时候会把他们整理成一个一维的线性表如下:
[
{ type: "Keyword", value: "const" },
{ type: "Identifier", value: "a" },
{ type: "Punctuator", value: "=" },
{ type: "Numeric", value: "1" },
{ type: "Punctuator", value: ";" },
{ type: "Keyword", value: "const" },
{ type: "Identifier", value: "b" },
{ type: "Punctuator", value: "=" },
{ type: "Identifier", value: "a" },
{ type: "Punctuator", value: "+" },
{ type: "Numeric", value: "1" },
{ type: "Punctuator", value: ";" },
];
词法分析主要是通过scanner扫描的过程,通过正则,有穷自动机等方式识别关键词,分隔符,运算符等内容,最终将识别的结构装入到结果集中,这样便实现了编译的第一个步骤。
语法分析的示例
刚才的案例得到的线性表可以通过语法分析最终转换成AST结构,这个结构通常以树的形式变现,可以通过recast框架进行抽象语法树的查看。
随意创建一个node项目,在项目中安装recast工具。
npm i recast -s 在项目中的index.js中输入
const recast = require("recast");
const util = require("util");
const code = `
const a = 1;
const b = a + 1;
`;
let ast = recast.parse(code);
console.log(util.inspect(ast.program.body, { showHidden: false, depth: null }));
在命令行工具中运行,会发现经过语法分析转换之后的ast语法树的结构