webpack4从0到1的配置详解

it2023-02-07  53

代码地址

简介

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。

最新回复(0)