简介
Webpack
是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader 的转换,任何形式的资源都可以视作模块
基本原理
webpack
就是识别你的 入口文件。识别你的模块依赖,来打包你的代码。
webpack
做的就是分析代码,转换代码,编译代码,输出代码。
webpack
中每个模块有一个唯一的id
,是从0开始递增的。整个打包后的bundle.js
是一个匿名函数自执行。参数则为一个数组。数组的每一项都为个function
。function
的内容则为每个模块的内容,并按照require
的顺序排列。
- 如何实现一个简单的webpack
读取文件分析模块依赖
对模块进行解析执行(深度遍历)
针对不同的模块使用相应的loader
编译模块,生成抽象语法树AST
。
循环遍历AST
树,拼接输出js
。
配置
一、创建初始化文件
mkdir demo
cd demo
npm init -y
二、安装
npm i webpack webpack-cli --save-dev
三、创建入口文件
- 创建
src
文件,src
文件下创建index.js
作为入口
console.log('this is index.js')
四、创建webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
outputPath: {
filename: 'bundle.js',
path: path.join(__dirname,'dist'),
}
}
- 命令行执行
webpack --mode development
将会生成distbundle.js
五、创建index.html并引入bundle.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
<script src="./dist/bundle.js"></script>
</body>
</html>
此时用浏览器打开index.html
可以看到index.js
输出this is index.js
六、创建其它js和css文件
a.js
const a = {description: 'i am from a.js'}
export default a
b.js
const b = {description: 'i am from b.js'}
export default b
index.css
h1 {
color: red;
}
index.js
import a from "./a";
import b from "./b";
import acss from "./index.css";
const c = { name: "i am c" };
console.log(a);
console.log(b);
console.log(c);
七、css处理
style-loader
和css-loader
安装
npm install style-loader css-loader --save-dev
- 配置
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
}
]
}
};
- 此时在命令行执行
webpack --mode development
再打开index.html
可以看到js
和css
都被正常使用
八、图片处理
url-loader
和fiel-loader
安装
npm install url-loader file-loader --save-dev
- 加入图片
h1 {
color: red;
}
body {
background-image: url("./im.png");
background-color: yellow;
}
- 配置
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /.(png|gif|jpg)$/,
use: [
{
loader: "url-loader",
options: {
outputPath: "img/",
limit: 500,
},
},
],
},
],
},
};
九、自动注入html
使用插件html-webpack-plugin
,可以将生成的js自动引入html
页面,不用手动添加
- 安装
npm install html-webpack-plugin --save-dev
- 配置
plugins: [// 对应的插件
new HtmlWebpackPlugin({ //配置
filename: 'index.html',//输出文件名
template: './index.html',//以当前目录下的index.html文件为模板生成dist/index.html文件
}),
]
十、删除指定文件
为了在每次打包后删除之前的dist文件夹,可以用插件clean-webpack-plugin
- 安装
npm install clean-webpack-plugin --save-dev
- 配置
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin(), //传入数组,指定要删除的目录
]
十一、热更新和本地服务器
- 安装
npm install webpack-dev-serve --save-dev
- 配置
const webpack = require("webpack");
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {//配置此静态文件服务器,可以用来预览打包后项目
inline:true,//打包后加入一个websocket客户端
hot:true,//热加载
contentBase: path.resolve(__dirname, 'dist'),//开发服务运行时的文件根目录
host: 'localhost',//主机地址
port: 9090,//端口号
compress: true//开发服务器是否启动gzip等压缩
},
十二、命令行配置(package.json)
"scripts": {
"build": "webpack --mode development",
"dev": "webpack-dev-server --open"
},
十三、公共部分代码抽离
optimization: {
splitChunks: {
cacheGroups: {
commons: {
// 抽离自己写的公共代码
chunks: "initial",
name: "common", // 打包后的文件名,任意命名
minChunks: 2, // 最小引用2次
minSize: 0, // 只要超出0字节就生成一个新包
},
vendor: {
// 抽离第三方插件
test: /node_modules/, // 指定是node_modules下的第三方包
chunks: "initial",
name: "vendor", // 打包后的文件名,任意命名
priority: 10,
// 设置优先级,防止和自定义的公共代码提取时被覆盖,不进行打包
},
},
},
},
十四、CSS的其他配置
1、 抽离CSS
文件
- 安装
npm install mini-css-extract-plugin --save
- 配置
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
test: /.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
new MiniCssExtractPlugin({
filename: "[name].css",
}),
注意: 此时已经不用再使用style.loader
2、 自动添加前缀
- 安装
npm i postcss-loader autoprefixer -D
- 配置
创建.browserslistrc
文件
last 2 versions
> 1%
iOS 7
last 3 iOS versions
创建postcss.config.css
module.exports = {
plugins: [require("autoprefixer")],
};
webpack.config.js
配置
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
},
3、Sass
和Less
以下以Sass
为例,Less
类似操作
- 安装
npm install --save-dev node-sass sass-loader
- 配置
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
"postcss-loader"],
},
babel配置
- 安装
npm install babel-loader babel-core babel-preset-env
PS: babel-core
不能安装太高版本,可以安装7.1.5
- 配置
创建.babelrc
{
"presets": ["env"]
}
webpack.config.js
配置
{
test: /.js$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
补充
path
- 上面代码中的
path
是node
中的一个模块,主要用来文件路径
path.join和path.resolve的区别
- 在使用
path
模块时,经常会用到这两个方法,他们都是用来拼接路径,但是存在些许不同。
path.join
- 长度为零的
path
片段会被忽略。 如果连接后的路径字符串是一个长度为零的字符串,则返回'.'
,表示当前工作目录
path.join('/img', 'book', 'net/abc', 'inter', '..'); // returns /img/book/net/abc
console.log(path.join('/img/books', '../net')) // returns /img/net
console.log(path.join('img/books', '../net')) // returns img/net
console.log(path.join('/img/books', './net')) // returns /img/books/net
console.log(path.join('img/books', './net')) // returns img/books/net
console.log(path.join('/img/books', 'net')) // returns /img/books/net
console.log(path.join('img/books', 'net')) // returns /img/books/net
console.log(path.join('/img/books', '/net')) // returns /img/books/net
console.log(path.join('img/books', '/net')) // returns img/books/net
path.resolve
- 如果没有传入
path
片段,或者path
片段长度为零(空字符),则path.resolve()
会返回当前工作目录的绝对路径(相当于使用path.resolve(__dirname)
)
例子:我当前的工作路径为/workspace/demo
console.log(path.resolve())
// returns /workspace/demo
console.log(path.resolve(''))
// returns /workspace/demo
console.log(path.resolve(__dirname))
// returns /workspace/demo
console.log(path.resolve('/img/books', '/net'))
// returns '/net'
console.log(path.resolve('img/books', '/net'))
// returns '/net'
console.log(path.resolve('img/books', './net'))
// returns '/workspace/demo/img/books/net'
console.log(path.resolve('/img/books', './net'))
// returns '/img/books/net'
console.log(path.resolve('/img/books', 'net'))
// returns '/img/books/net'
console.log(path.resolve('/img/books', '../net'))
// returns '/img/net'
console.log(path.resolve('src','/img/books', '../net'))
// returns '/img/net'
console.log(path.resolve('src','./img/books', '../net'))
// returns '/workspace/demo/src/img/net'
console.log(path.resolve('src','img/books', '../net'))
// returns '/workspace/demo/src/img/net'
__dirname和./的区别
Node.js
中,__dirname
总是指向被执行 js
文件的绝对路径,所以当你在 /d1/d2/myscript.js
文件中写了 __dirname
, 它的值就是 /d1/d2
。
相反,./
会返回你执行 node
命令的路径,例如你的工作路径。
假设有如下目录结构
/dir1
/dir2
pathtest.js
然后在 pathtest.js
中,有如下代码
var path = require("path");
console.log(". = %s", path.resolve("."));
console.log("__dirname = %s", path.resolve(__dirname));
然后执行了下面命令
cd /dir1/dir2
node pathtest.js
.
是你的当前工作目录,在这个例子中就是 /dir1/dir2
,__dirname
是 pathtest.js
的文件路径,在这个例子中就是 /dir1/dir2
。
然而,如果我们的工作目录是 /dir1
cd /dir1
node dir2/pathtest.js
将会得到
. = /dir1
__dirname = /dir1/dir2
在 require 中使用 .
如果在 dir2/pathtest.js
中调用了 require
方法,去引入位于 dir1
目录的 js
文件,你需要写成
require('../thefile')
因为 require
中的路径总是相对于包含它的文件,跟你的工作目录没有关系。
url-loader和file-path
- 如果我们希望在页面引入图片(包括
img
的src
和background
的url
)。webpack
最终会将各个模块打包成一个文件,因此我们样式中的url
路径是相对入口html
页面的,而不是相对于原始css
文件所在的路径的。这就会导致图片引入失败。这个问题是用file-loader
解决的,file-loader
可以解析项目中的url引入(不仅限于css
),根据我们的配置,将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。 - 另外,如果图片较多,会发很多http请求,会降低页面性能。这个问题可以通过
url-loader
解决。url-loader
会将引入的图片编码,生成dataURl
。相当于把图片数据翻译成一串字符。再把这串字符打包到文件中,最终只需要引入这个文件就能访问图片了。当然,如果图片较大,编码会消耗性能。因此url-loader
提供了一个limit
参数,小于limit
字节的文件会被转为DataURl
,大于limit的还会使用file-loader
进行copy
。 - **PS:**虽然在配置中不用写
fiel-loader
,但是url-loader
可能会调用file-loader
,所以还是需要安装
各种Babel
基本原理
1、解析
解析步骤接收代码并输出 AST
。 这个步骤分为两个阶段:词法分析(Lexical Analysis)
和 语法分析(Syntactic Analysis)
。
词法分析
词法分析阶段把字符串形式的代码转换为 令牌(tokens
) 流。
你可以把令牌看作是一个扁平的语法片段数组:
n * n;
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
每一个 type
有一组属性来描述该令牌:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
和 AST
节点一样它们也有 start,end,loc
属性。
语法分析
语法分析阶段会把一个令牌流转换成 AST
的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST
的表述结构,这样更易于后续的操作。
2、转换
转换步骤接收 AST
并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel
或是其他编译器中最复杂的过程。
Babel
提供了@babel/traverse
(遍历)方法维护这AST
树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST
和自定义的转换规则,返回结果为转换后的AST
。
3.生成
代码生成步骤把最终(经过一系列转换之后)的 AST
转换成字符串形式的代码,同时还会创建源码映射(source maps
)。
代码生成其实很简单:深度优先遍历整个 AST
,然后构建可以表示转换后代码的字符串。
Babel
使用 @babel/generator
将修改后的 AST
转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap
。
babel-polyfill
babel-polyfill
是为了模拟一个完整的ES2015+
环境,旨在用于应用程序而不是库/工具。并且使用babel-node
时,这个polyfill
会自动加载。这里要注意的是babel-polyfill
是一次性引入你的项目中的,并且同项目代码一起编译到生产环境。而且会污染全局变量。像Map
,Array.prototype.find
这些就存在于全局空间中。
npm install babel-polyfill --save
webpack.config.js
配置
entry: {
main: ['babel-polyfill','./index.js']
}
babel-runtime
babel-runtime
不会污染全局空间和内置对象原型。事实上babel-runtime
是一个模块,你可以把它作为依赖来达成ES2015
的支持。
比如环境不支持Promise
,你可以在项目中加入
require(‘babel-runtime/core-js/promise’)
这样我们就弥补了babel-polyfill
的缺点,达到了按需加载的效果。但是在实际项目开发过程中,我们往往会写很多新的es6 api
,每次都要手动引入相应的包比较麻烦,维护起来也不方便,每个文件重复引入也造成代码的臃肿。
要解决这个问题,就要用到 babel-plugin-transform-runtime
,它会分析我们的 ast
中,是否有引用 babel-rumtime
中的垫片(通过映射关系),如果有,就会在当前模块顶部插入我们需要的垫片。
npm install --save babel-runtime
npm install --save-dev babel-plugin-transform-runtime
下面在.babelrc
中加入以下配置
{
"plugins": ["transform-runtime"]
}
babel-runtime
有个缺点,它不模拟实例方法,即内置对象原型上的方法,所以类似Array.prototype.find
,你通过babel-runtime
是无法使用的,这只能通过 babel-polyfill
来转码
babel-preset-env
babel-preset-env
能根据当前的运行环境,自动确定你需要的 plugins
和 polyfills
。
修改一下.babelrc
的配置
{
"presets": [
["env", {
"targets": {
"chrome": 52,
"browsers": ["last 2 versions", "safari 7"]
},
"modules": false,
"useBuiltIns": "usage",
"debug": false
}]
]
}
preset-env.targets
"targets" : "last 2 versions, not dead"
"targets" : {
"chrome": "58",
"ie": "11"
}
"targets" : {
"browsers": "last 2 versions",
"esmodules": true, // 指定该选项,将会忽略browserslist,
//仅支持那些那些原生支持es6 module的浏览器
"safari": true , // 启用safari前沿技术
"node": "true" || "current" //兼容当前node版本代码
}
browserslist
last 2 vsersions
not dead
preset-env.modules
"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto"
| false, defaults to "auto"
指定将es6 modules
转换为何种模块规范。一般在webpack
项目中,我们会将此参数设置为false
,既将module
交由webpack
处理,而不是babel
。
preset-env.userBuiltIns
"usage" | "entry" | false, defaults to false
"entry":
在入口文件中加入所有的内置类型
如果在.babelrc
中指定useBuiltIns: 'entry'
, 则应该在项目代码的顶部引入babel-polyfill
import "@babel/polyfill"
"usage":
只在当前文件中加入该文件用到的内置类型的polyfill
。
设置为usage
不需要在顶部引入polyfill
"false":
不自动加入内置类型的polyfill
。