阅读 326

浅析webpack基础篇

为什么需要构建工具?

转换ES6语法
转换JSX
CSS前缀补全/预处理器
压缩混淆
图片压缩

为什么选择webpack?

社区生态丰富
配置灵活和插件化拓展
官方更新迭代速度快

初识webpack:配置文件名称

webpack默认配置文件:webpack.config.js
可以通过webpack --config指定配置文件

// 初始webpack:webpack配置组成
module.exports = {
    entry: './src/index.js',  // 打包的入口文件
    output:'./dist/main.js',  // 打包的输出
    mode: 'production',  // 环境
    module: {
        rules: [{  // Loader配置
            test: /\.txt$/, use: 'raw-loader'
        }]
    },
    plugins: [  // 插件配置
        new HtmlwebpackPlugin({
            template: './src/index.html',
        })
    ]
}

// 零配置webpack包含那些内容?
module.exports = {
    entry: './src/index.js',  // 指定默认的entry为: ./src/index.js
    output:'./dist/main.js',  // 指定默认的output为: ./dist/main.js
    mode: 'production',
    module: {
        rules: [{
            test: /\.txt$/, use: 'raw-loader'
        }]
    },
    plugins: [
        new HtmlwebpackPlugin({
            template: './src/index.html',
        })
    ]
}

// 通过npm script 运行webpack
{
  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    //  通过npm run build 运行构建
    "build": "webpack" // 原理:模块局部安装会在node_modules/.bin目录创建软链接
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  }
}
复制代码

webpack核心概念

Entry

Entry 用来制定webpack的打包入口
依赖图的入口是entry
对于非代码比如图片、字体以来也会不断加入到依赖图中

用法: 单入口:entry是一个字符串 单界面应用

module.exports = {
    entry: 'xxxxx',
}
复制代码

多入口:entry是一个对象 多界面应用

module.exports = {
    entry: {
        app: 'xxx',
        adminApp: 'xxxx',
    }
}
复制代码

Output

Output用来告诉webpack如何将编译后的文件输出到磁盘。

Output 的用法:单入口配置

module.exports = {
    entry: 'xxxx',
    output: {
        filename: 'bundle.js',
        path: __dirname + '/dist'
    }
}
复制代码

Output 的用法:多入口配置

module.exports = {
    entry: {
        app: 'xxx',
        adminApp: 'xxx',
    },
    // 没有多出口的说法
    output: {
        // 通过占位符确保文件名称的唯一
        filename: '[name].js',   // name 指定打包的名称
        path: __dirname + '/dist',
    }
}
复制代码

Loaders

webpack开箱即用只支持JS和JSON两种文件类型,通过Loaders去支持其它文件类型并且把它们转化为有效的模块,并且可以添加到依赖图中。
本身是一个函数,接受源文件作为参数,返回转换的结果。

常见的Loaders有那些?

名称描述
babel-loader转换ES6、ES7等JS新特性语法
css-loader支持.css文件的加载和解析
less-loader将less文件转换为css
ts-loader将TS转换为JS
file-loader进行图片、字体等的打包
raw-loader将文件以字符串的形式导入
thread-loader多进程打包JS和CSS(正常是一个进程)

Loaders 的用法

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output:'./dist/main.js',
    mode: 'production',
    module: {
        rules: [{
            // test 指定匹配规则
            // use 指定使用的loader名称
            test: /\.txt$/, use: 'raw-loader'
        }]
    },
}
复制代码

Plugins

插件用于bundle文件的优化,资源管理和环境变量注入,Loader没办法做到事情Plugins都可以做到。
作用于整个构建过程

常见的Plugins有哪些?

名称描述
CommonsChunkPlugin将chunks相同的模块代码提取成公共js
CleanWebpackPlgin清理构建目录
ExtractTextWebpackPlugin将css从bunlde文件里提取成一个独立的css
CopyWebpackPlugin将文件或者文件夹拷贝到构建的输出目录
HtmlWebpackPlugin创建html文件去承载输出的bundle
UglifyjsWebpackPlugin压缩JS
ZipWebpackPlugin将打包出的资源生成一个zip包

Plugins的用法

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.txt$/, use: 'raw-loader'
        }]
    },
    plugins: [
        // 放到plugin数组
        new HtmlwebpackPlugin({
            template: './src/index.html',
        })
    ]
}
复制代码

Mode

Mode用来指定当前构建环境是:production、development还是none,webpack 4以前是没有这个概念的。

设置mode可以使用webpack内置的函数,默认值为production。

Mode的内置函数功能

选项描述
development设置process.env.NODE_ENV 的值为development
开启NameChunkPlugin 和 NameModulesPlugiin
production设置process.env.NODE_ENV的值为production
开启FlagDependencyUsagePlugin,
FlagIncludeChunksPlugin,ModuleConcatenationPlugin,
NoEmitOnErrorsPlugin,OccurrenceOrderPlugin,
SideEffectsFlagPlugin,TerserPlugin
none不开始任何优化选项

资源解析

解析ES6

使用babel-loader
babel的配置文件是:.babelrc

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.js$/, use: 'babel-loader'
        }]
    },
}
复制代码

增加ES6的babel preset配置

{
    "presets": [
        // 增加ES6的babel preset配置
        "@babel/preset-env"
    ],
    "plugins": [
        "@babel/proposal-class-properties"
    ]
}

复制代码

解析React JSX

{
    "presets": [
        // 增加ES6的babel preset配置
        "@babel/preset-env",
        // 增加React的babel preset配置
        "@babel/preset-react"
    ],
    "plugins": [
        "@babel/proposal-class-properties"
    ]
}

复制代码

解析CSS

css-loader用于加载.css文件,并且转换成commonjs对象。
style-loader将样式通过标签插入到head中。

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.css$/, use: [
                'style-loader',
                'css-loader'
            ]
        }]
    },
}
复制代码

解析less 和 sass

less-loader用于将less转换成css

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.css$/, use: [
                'style-loader',
                'css-loader',
                'less-loader'
            ]
        }]
    },
}
复制代码

解析图片和字体

解析图片/字体
file-loader

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/, use: [
                'file-loader',  //用于处理文件
            ]
        },{
            test: /\.(woff|woff2|eot|tff)$/, use: [
                'file-loader',  //用于处理字体
            ]
        }]
    },
}
复制代码

url-loader也可以处理图片和字体,内部也是file-loader
可以设置较小资源自动base64

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/, use: [{
                loader: 'url-loader',  //用于处理文件
                optins: {
                    limit: 10240  // 单位:字节
                }
            }]
        }]
    },
}
复制代码

webpack 中的文件监听

文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。

webpack 如何开启监听模式?

两种方式:

  • 启动webpack命令时,带上--watch参数
  • 在配置webpack.config.js中设置watch:true
// 监听使用
{
  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    //  通过npm run build 运行构建
    "build": "webpack", // 原理:模块局部安装会在node_modules/.bin目录创建软链接
    "watch": 'webpack --watch'  //唯一缺陷:每次需要手动刷新浏览器
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  }
}
复制代码

文件监听原理分析

轮询判断文件的最后编辑时间是否变化

某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等aggregateTimeout

module.export = {
    // 默认false,也就是不开启
    watch: true,
    // 只有开启监听模式时,watchOptions才有意义
    watchOptions: {
        // 默认为空,不监听的文件或者文件夹,支持正则匹配
        ignored: /node_modules/,
        // 监听到变化发生后等300ms再去执行,默认300ms
        aggregateTimeout: 300,
        // 判断文件是否发生变化时通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
        poll: 1000
    }
}

复制代码

webpack 热更新

使用webpack-dev-server

WDS 不刷新浏览器
WDS 不输出文件,没有磁盘IO,而是放在内存中
使用HotModuleReplacementPlugin插件

// 热更新
{
  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    //  通过npm run build 运行构建
    "build": "webpack", // 原理:模块局部安装会在node_modules/.bin目录创建软链接
    "watch": "webpack --watch", //唯一缺陷:每次需要手动刷新浏览器
    "dev": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  }
}
复制代码

使用webpack-dev-middleware

WDM将webpack输出的文件传输给服务器
适用于灵活的定制场景

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack('config');

app.use(webpackDevMiddleware(compiler, {
    publicPath:config.output.publicPath
}));

app.listen(3000, () => {
   console.log('监听'); 
});

复制代码

热更新的原理分析

Webpack Compile:将JS编译成Bundle
HMR Server: 将热更新的文件输出给HMR Runtime
Bundle server: 提供文件在浏览器访问
HMR Runtime:会被注入到浏览器,更新文件的变化
bundle.js: 构建输出的文件

文件指纹

什么是文件指纹?
打包后输出文件名的后缀。

文件指纹如何生成?
Hash: 和整个项目的构建相关,只要项目文件有修改,整个项目构建的hash值就会更改
Chunkhash: 和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值
Contenthash: 根据文件内容来定义hash,文件内容不变,则contenthash不变

JS的文件指纹设置
设置output的filename,使用[chunkhash]

module.exports = {
    entry: './src/index.js', 
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name][chunkhash:8].js',
    },
}
复制代码

CSS的文件指纹设置
设置MiniCssExtractPlugin的filename,使用[contenthash]

module.exports = {
    entry: './src/index.js', 
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name][chunkhash:8].js',
    },
    plugins: [
        new MiniCssExtractPlugin(
            {
                filename: `[name][contenthash:8].css`
            }
        )
    ]
}
复制代码

图片的文件指纹设置
设置file-loader的name,使用[hash]

占位符名称含义
[ext]资源后缀名
[name]文件名称
[path]文件的相对路径
[folder]文件所在的文件夹
[contenthash]文件的内容hash,默认是md5生成
[hash]文件内容的hash,默认是md5生成
[emoji]一个随机的指代文件内容的emoj
module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/, 
            use: [{
                loader: 'file-loader',  //用于处理文件
                options: {
                    name: 'img/[name][hash:8].[ext]',  //8: hash前8位
                }
            }]
        }]
    },
}

复制代码

代码压缩

HTML压缩
CSS压缩
JS压缩

JS 文件的压缩

内置了 uglifyjs-webpack-plugin

CSS压缩

使用optimize-css-assets-webpack-plugin
同时使用cssnano

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/, 
            use: [{
                loader: 'file-loader',  //用于处理文件
                options: {
                    name: 'img/[name][hash:8].[ext]',  //8: hash前8位
                }
            }]
        }]
    },
    plugins: [
        new OptimizeCSSAssetsPlugin({
            assetNameRegExp: /\.css$/g,
            cssProcessor: require('cssnano'),
        })
    ],
}

复制代码

HTML 文件的压缩

修改html-webpack-plugin,设置压缩参数

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/, 
            use: [{
                loader: 'file-loader',  //用于处理文件
                options: {
                    name: 'img/[name][hash:8].[ext]',  //8: hash前8位
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'src/search.html'),
            fileanme: 'search.html',
            chunks: ['search'],
            inject: true,
            minify: {
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false,
            }
        })
    ],
}

复制代码

如何自动清理构建目录产物?

通过npm scripts 清理构建目录

rm -rf ./dist && webpack
rimraf ./dist && webpack

自动清理构建目录

避免构建前每次都需要手动删除dist

使用clean-webpack-plugin,默认会删除output指定的输出目录

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/, 
            use: [{
                loader: 'file-loader',  //用于处理文件
                options: {
                    name: 'img/[name][hash:8].[ext]',  //8: hash前8位
                }
            }]
        }]
    },
    plugins: [
        new CleanWebpackPlugin()
    ],
}

复制代码

PostCSS插件autoprefixer自动补齐css3前缀

CSS3的属性为什么要添加前缀? 浏览器标准不统一

IE Trident(-ms)
...

module.exports = {
    entry: './src/index.js', 
    output:'./dist/main.js',  
    mode: 'production',
    module: {
        
        rules: [
            {
                test: /.js$/,
                use: 'babel-loader'
            }, 
            {
                // 调用顺序从右到左
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }, 
            {
                // 调用顺序从右到左
                test: /.less$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'less-loader', 
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: () => {
                                require('autoprefixer') ({
                                    browsers: ['last 2 version', '>1%', 'iOS 7']
                                })
                            }
                        }
                    }
                ]
            },
            {
                test: /\.(png|svg|jpg|gif)$/, use: [
                    'file-loader',  //用于处理文件
                ]
            },
            // {
            //     test: /\.(png|svg|jpg|gif)$/, use: [{
            //         loader: 'url-loader',  //用于处理文件
            //         optins: {
            //             limit: 10240  // 单位:字节
            //         }
            //     }]
            // },
        ]
    },
    plugins: [
        new CleanWebpackPlugin()
    ],
}

复制代码

移动端px自动转换成rem

css 媒体查询实现响应式布局

W3C对rem定义: font-size of the root element

rem和px对比

.rem 是相对单位
.px 是绝对单位

使用px2rem-loaders

{
    // 调用顺序从右到左
    test: /.less$/,
    use: [
        'style-loader',
        'css-loader',
        'less-loader',
        {
            loader: 'postcss-loader',
            options: {
                plugins: () => {
                    require('autoprefixer') ({
                        browsers: ['last 2 version', '>1%', 'iOS 7']
                    })
                }
            }
        }
    ]
},
复制代码

资源内联

意义

代码层面:

  • 页面框架的初始化脚本
  • 上报相关打点
  • css内联避免页面闪动

请求层面:减少http网络请求数

  • 小图片或者字体内联(url-loader)

HTML和JS内联

raw-loader内联html


raw-loader内联js

CSS内联

方案一: 借助style-loader
方案二:html-inline-css-webpack-plugin

多页面应用(MPA)概念

每一次页面跳转的时候,后台服务器都会返回一个新的HTML文档,这种类型的网站也就是多页网站,也叫做多页应用。

方案

动态获取entry和设置html-webpack-plugin数量

利用glob.sync

使用source map

作用:通过source map定位到源代码
开发环境开启、线上环境关闭

提取公共资源

思路:将React、React-Dom基础包通过cdn引入,不打入bundle中

方法: 使用html-webpack-externals-plugin

webpack 4内置:利用SplitChunksPlugin进行公共脚本分离

Tree Shaking(摇树优化)

概念:1个模块可能有很多方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会在uglify阶段被擦除掉。

使⽤:webpack 默认⽀持,在 .babelrc ⾥设置 modules: false 即可 · production mode的情况下默认开启

要求:必须是 ES6 的语法,CJS 的⽅式不⽀持

DCE (Dead code elimination)
代码执⾏的结果不会被⽤到
代码不会被执⾏,不可到达
代码只会影响死变量(只写不读)

if (false) {
    console.log('这段代码永远不会执行’);
}
复制代码

Tree-shaking 原理

利⽤ ES6 模块的特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable的

代码擦除: uglify 阶段删除⽆⽤代码

代码分割的意义

对于⼤的 Web 应⽤来讲,将所有的代码都放在⼀个⽂件中显然是不够有效的,特别是当你的 某些代码块是在某些特殊的时候才会被使⽤到。webpack 有⼀个功能就是将你的代码库分割成 chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。

适⽤的场景

  • 抽离相同代码到⼀个共享块
  • 脚本懒加载,使得初始下载的代码更⼩

懒加载 JS 脚本的⽅式

  • CommonJS:require.ensure
  • ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)

如何使⽤动态 import?

安装 babel 插件
npm install @babel/plugin-syntax-dynamic-import --save-dev
ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)

{
    "plugins": ["@babel/plugin-syntax-dynamic-import"],
    ...
}
复制代码

ESLint 的必要性

⾏业⾥⾯优秀的 ESLint 规范实践

Airbnb: eslint-config-airbnb、 eslint-config-airbnb-base

腾讯:

制定团队的 ESLint 规范

不重复造轮⼦,基于 eslint:recommend 配置并 改进

  • 能够帮助发现代码错误的规则,全部开启
  • 帮助保持团队的代码⻛格统⼀,⽽不是限制开发体验

小结

上述为我们学习webpack过程中的基础,在webpack的基础上,优化是一个亘古不变的话题,我们接下来会对常用优化手段进行总结学习。

文章分类
前端
文章标签