关于webpack的理解

webpack是一个JavaScript应用程序的静态模块化打包器,意味着webpack在不进行特殊配置的情况下就只认识JavaScript一种语言,只能处理JavaScript这一种语言

1)原理&作用

0) 理解前端模块化

⇒ 为了避免变量冲突和覆盖,不同js文件间可以使用命名空间,譬如在susan.js中

var susan  = {
     
	name: 'susan',
	tell: function(){
     
		console.log('我的名字:', name)
	}
}

在liming.js中

var liming = {
     
	name: 'liming',
	tell: function(){
     
		console.log('我的名字:', name)
	}
}

⇒ 但是上述办法无法保护变量,变量可以被随意访问和篡改,为了避免变量被随意访问和篡改,可以利用函数自调用的办法,利用闭包,即内层函数引用上层函数的变量,内层函数被return后依然引用着外层函数的变量的特性

var susanModule = (function(){
     
	var name = 'susan'
	return {
     
		tell: function(){
     
		console.log('我的名字:', name)
		}
	}
})()

此时我们为tell方法绑定了一个专属变量name,同时又保护了这个变量不被污染(因为通过susan.tell()可以顺利执行,但是susan.name为undefined)
如上所述,这样一个被保护起来的作用域范围,则成为模块作用域,通过像这样形成模块作用域,来隐藏该隐藏的变量,暴露该暴露的变量,既保证关键变量不被随意更改,同时又让逻辑可重用
⇒ 更加标准和通用的写法如下

(function(window){
     
	var name = 'susan'
	function tell(){
     
		console.log('我的名字:', name)
	}
	
	window.susanModule = {
     tell}
})(window)

即把要暴露的函数挂载到window上,通过susanModule.tell()即可访问执行
所以前端模块化,其实就是在利用作用域链、闭包这些特性,来解决保护变量同时重用逻辑的问题的方案 ,它的优点在于:

  • 对作用域进行了封装
    用模块把代码封装起来,保证模块内部的实现不会暴露在危险的全局作用域中,只需要将模块的功能通过接口的方式(如susanModule.tell())暴露出去给其他模块调用即可,从而避免了污染全局命名空间的问题
  • 保证了模块的复用性,包括vue中的组件化也是同样的道理
  • 解除耦合
    可以帮助快速的定位问题,定位问题时我们只需要去关注对应的模块,从而提升系统的可维护性
    站在软件工程的角度去理解模块化

另:在立即执行函数方法后,模块化方案又经历了以下3个阶段:

  1. AMD(Asynchronous Module Definition 异步模块定义)

  2. COMMONJS

  3. ES6MODULE
    JS这门语言本身不具备模块化的特性,ES6开始,模块化才开始有原生语法的支持

1) webpack的打包机制

  • webpack与立即执行函数的关系
  • webpack打包的核心逻辑

2)webpack的打包过程

即webpack在打包的时候都做了什么,经历了哪些流程
当我们在根目录下运行webpack,webpack会去src目录下寻找index.js文件并将其打包为main.js,放入专门放打包结果的文件夹dist

webpack的打包过程

  • 从默认的入口文件src/index.js开始,分析整个应用的依赖树
    从默认的入口文件src/index.js开始,遍历src/index.js中所引入的模块及src/index.js本身的逻辑,放入默认的出口文件dist/main.js文件中
  • 将每个依赖模块包装起来,放到一个数组中等待调用
  • 实现模块加载的方法,并把它放到模块执行的环境中,确保模块间可以互相调用
  • 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数

4) 为什么要打包:

  1. 一般项目中js文件数目很大且相互之间有依赖关系,如果不打包,直接让这些文件保持分离状态,则我们需要关心加载顺序
  2. 如果不打包,这些js文件处于分离状态,我们需要分开请求加载,则消耗性能
    所以通过打包,将这些文件打包为一个dist/main.js文件,则可以帮助我们解决上述问题

2) webpack的核心特性

1.webpack-dev-server 以及 热更新/热替换

① 实现热更新

热更新(HMR)即在启动本地服务后,不刷新浏览器的情况下,实现文件改动的同步更新,实现办法如下
首先,配置webpack.congif.js参数如下

const path = require('path')
// 实现热更新 -0: 引入webpack
const webpack = require('webpack') 

module.exports = {
     
    entry: path.resolve(__dirname, 'src/index.jsx'),

    // plugins
    plugins: [
        // 实现热更新 -1:配置插件
        new webpack.HotModuleReplacementPlugin()
    ],

    devServer:{
     
        hot: true, // 热更新 -2:配置devServer参数
    },    
}

然后,配置入口文件

import App from './App.jsx'

// 实现热更新 -3
if(module.hot){
      // 如果module.hot为true,则赋予热更新的能力
    module.hot.accept(error=>{
     
        if(error){
     
            console.log('热替换出bug了')
        }
    })
}

最终,在启动服务后即可实现热更新功能

② 关于启动本地服务

  • 通过运行webpack-dev-server命令,原地启动一个本地服务,可以监听工程文件的改动,当修改文件并保存时,可以动态的实现实时打包,自动刷新浏览器
  • 如果是webpack5,命令行输入webpack serve即可
  • webpack server --config webpack.config.js 可以指定执行的webpack配置文件
  • webpack serve --open,启动服务同时打开浏览器
    注1:如果运行webpack-dev-server命令后报错提示command not found,是因为没有全局安装webpack-dev-server,可以通过npm install webpack-dev-server -g安装后再执行命令,或者直接运行node_modules/.bin/webpack-dev-server中的可执行文件,即命令行直接输入./node_modules/.bin/webpack-dev-server

以上命令行可以在package.json的scripts中自定义,如下所示

{
     
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
      // 自定义脚本
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production", // 打包
    "start": "webpack serve --mode development --open" // 启动服务并打开浏览器
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
     
    "css-loader": "^5.0.1",
    "style-loader": "^2.0.0",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "webpack": "^5.17.0",
    "webpack-cli": "^4.4.0",
    "webpack-dev-server": "^3.11.2"
  }
}

注2:如果命令行运行webpack报错,很可能是因为没有全局安装webpack-cli,只需要npm install webpack-cli -g即可

2.loader(翻译官)

loader是对webpack能力的拓展,webpack本身只能处理js文件,其他类型譬如css、html文件则需要通过loader进行处理
loader本质是一个文件加载器,实现文件的转译编译功能
即把浏览器看不懂的代码翻译成浏览器看得懂的代码,譬如把Chrome看不懂的TS代码翻译为浏览器看得懂的JS代码
loader用于处理资源文件,接受资源文件为一个参数,处理完后返回一个新的资源
注:loader独立于webpack,webpack内部不包含loader,所以必须手动安装

  • 使用babel-loader转化ES6代码,转换为浏览器可以直接运行的js代码
npm install --save-dev babel-loader babel-core

常用loader:

  • 使用css-loader处理css文件,再经过style-loader生成style标签并插入HTML页面中
  • 使用postcss-loader以及它的插件autoprefixer为display: flex;添加浏览器前缀
  • 使用less-loader处理less文件
  • 使用sass-loader处理less文件
  • 使用html-loader处理html文件
  • file-loader处理项目中引入的图片文件或者其他文件,可以将项目中以’相对路径’引用的文件,处理为以’绝对路径’引用
  • url-loader,当URL引用的图片或文件小于limit时,可以转换生成一段base64编码被打包进js或者html里面,而不再是一个url,不再需要http请求去load文件,当图片或文件大于limit时,则转交file-loader处理

1) file-loader

对于图片文件 使用file-loader处理,可以将项目中以’相对路径’引用的文件,处理为以’绝对路径’引用

  • 对于 根目录下index.html中直接以相对路径引用的图片,以及 css文件中直接以相对路径引用的图片,使用file-loader处理
    使用项目中以相对路径引用的图片
  • 对于模板文件中直接以相对路径引用的图片,使用如下写法,再使用file-loader处理
<img src="${ require('../../assets/bg.png') }"/>

2)url-loader

当URL引用的图片或文件小于limit时,可以转换生成一段base64编码被打包进js或者html里面,而不再是一个url,不再需要http请求去load文件,当图片或文件大于limit时,则转交file-loader处理

3) 区别

  • file-loader通过http请求load的图片浏览器可以进行缓存,当图片重复性非常高的时候,可以使用file-loader
  • 当图片或者文件多次被引用时,相同的一段base64编码可能存在项目的很多地方,从而导致打包后的代码冗余;但是可以减少http请求次数

3. plugin(插件)

也是对webpack功能的增强,和loader的区别在于,plugin强调时间监听的能力,plugin可以在webpack内部监听一些事件,并且改变一些文件打包后的输出结果
plugin往往都是以构造函数的形式存在,所以使用之前需要引入,引入后才能new

常用plugin

plugins处理loaders无法完成的功能。

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

4.webpack.config.js(webpack配置文件)

webpack通过webpack.config.js进行webpack打包时的自定义配置

一个webpack.config.js样板(webpack4/5)

const path = require('path') // 在处理文件路径时需要用到的模块
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
     

    // - 指定入口文件,即工程资源的入口,默认值=index.js
    // - 可以是一个文件,也可以是多个文件,webpack会以入口文件为切入口,遍历依赖进行打包
    // - 可以理解为一个依赖树的根节点
    entry: './app.js', 

    // - 指定出口文件
    // - 每一个入口都会对应一个最终的打包结果
    output: {
     
        // -- 指定出口文件所在路径,必须是绝对路径,默认值=dist/main.js
        path: path.join(__dirname, 'dist'), // __dirname指当前目录,dist指相对路径,作为参数传入path.jion()中得到绝对路径
        // -- 指定出口文件名称
        filename: 'bundle.js'
    },

    // - 配置devServer,针对npm run dev 或者 webpack-dev-server后启动的本地服务进行配置
    // 执行webpack-dev-server启动本地服务后打包并不会在dist目录生成实际的文件,可以理解为打包后的资源只存在于内存中,当浏览器发生请求时会直接从内存中加载打包后的资源文件
    devServer: {
     
        // -- 指定服务端口,默认值=8080
        port: 3000,
        // -- 指定打包后的资源路径
        publicPath: '/dist'
    },

    // - loader, loader本质是一个文件加载器,实现文件的转译编译功能
    module: {
     
        rules: [
            {
     
                test: /\.css$/, // 对于.css结尾的文件
                exclude: /node_modules/, // 排除node_modules,不对其进行处理
                use: [ // 使用指定的loader 执行顺序 下 → 上,所以要先用css-loader解析,然后用style-loader生成style标签插入页面
                    'style-loader', // 为解析后的样式生成style标签并插入页面中 npm install style-loader --save-dev
                    'css-loader', // 使用css-loader解决css语法解析的问题 需 npm install css-loader --save-dev

                ]
            }
        ]
    },
    // 插件 配置前需要npm install uglifyjs-webpack-plugin --save-dev,并且使用import引入
    plugins: [
    	// 减小整个项目的代码体积
        new UglifyJSPlugin(), 
        
        // 生成打包后的html
        new HtmlWebpackPlugin({
     
            template: path.resolve(__dirname, 'src/index.html'), // 指定需要被处理的文件的路径
            filename: 'index.html', // 指定打包后的文件名
        }),
    ],

	// resolve
    resolve: {
     
        // 向webpack声明以下后缀在进行引入时不再写明
        // 在webpack工作时,如果在文件中读到类似 import App from './App' 的代码,则会遍历下方数组中的后缀,验证和APP拼接后是否能在目录下找到对应文件
        extensions: ['.wasm','.mjs','.js','.jsx','.json']
    }
}

3) 优化webpack性能(包括优化构建性能、优化构建后的结果)

1.打包结果优化(空间维度)

即尽可能减小打包后的文件的体积,加快传输速度,优化用户体验

  • 本身webpack可以实现压缩功能,在指定压缩模式为生产模式,即webpack --mode production时,webpack会自动压缩
  • 通过TerserPlugin自定义优化配置,webpack.config.js配置如下
const TerserPlugin = require('terser-webpack-plugin') // 引入插件

module.exports = {
     
    // 用于存放有关压缩的配置
    optimization: {
     
        mininizer: [
            // 使用插件
            // UglifyJSPlugin 的升级版,UglifyJSPlugin在es5方面比较优秀,UglifyJSPlugin-es对es6做了强化,后续未再更新,TerserPlugin是UglifyJSPlugin-es重新拉取的分支
            new TerserPlugin({
     
                // 使用缓存,加快构建速度
                cache: true, 
                // 开启多线程,加快打包速度
                parallel: true,
                terserOptions: {
     
                    // 压缩体积
                    compress: {
     
                        unused: true, // 自动剔除无用代码
                        drop_debugger: true, // 去掉开启的debugger
                        drop_console: true, // 去掉console的代码
                        dead_code: true, // 移除无用代码
                    }
                }
            })
        ]
    },
}

2.构建过程优化(时间维度)

即尽可能加快构建速度,提升开发者的效率

  • 节省解析时间
// webpack.config.js
mudule.exports = {
     
	module: {
     
	        noParse: /node_module\/(jquery\.js)/, // 节省解析时间
	}
}
  • 节省查找时间
    exclude排除node_modules,不对其进行处理,或者使用include指定要处理的文件
    exclude优先级高于include和test
// webpack.config.js
mudule.exports = {
     
	module: {
     
	        rules: [
            {
     
                test: /\.jsx?$/,
                exclude: /node_modules/, // 排除node_modules,不对其进行处理
                // 或者使用include指定要处理的文件
                use: {
     
                    loader: 'babel-loader',
                    }
                }
            }
        ],
	}
}
  • 利用多线程提升构建速度
    node是单线程,而webpack运行在node环境下,所以webpack本身是单线程的,利用HappyPack可以实现多进程程,将任务分解为多个子进程,并发执行,如下所示
// webpack.config.js
const HappyPack = require('happypack')
// 根据CPU数量创建线程池(利用进程实现多线程)
const happyThreadPool = HappyPack.happyThreadPool({
     size:OscillatorNode.cpus().length})

module.exports = {
     
    plugins: [
        new HappyPack({
     
            id: 'jsx',
            threads: happyThreadPool,
            loaders: ['babel-loader'] // 给loader配置线程
            // 配置时要注意loader是否支持,如url-loader file-loader 则不支持happypack
        })
    ],
}
  • 利用多线程提升构建速度
    或者使用thread-loader针对loader进行优化,将loader放入线程池,设置时需放在所有loader之前,如下所示
// webpack.config.js
mudule.exports = {
     
	module: {
     
        rules: [
            {
     
                test: /\.js$/,
                include: path.resolve('src'),
                use: [
                    'thread-loader'
                    // 后面再增加其他loader
                ]
            },
       	],
	}
}

3.Tree-Shaking(优化思想)——webpack本身的优化特性

本质是消除无用的js代码(DCE),webpack做的事情有2件:
分析es6的引入情况,去除不实用的import引入

4.评价打包性能的可视化工具:webpack bundle analyzer

本身其实是一个插件(plugin)
① 安装

npm install webpack-bundle-analyzer

② 配置webpack.config.js

const BundleAnalyZerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin // 引入插件

module.exports = {
     
    // plugins
    plugins: [
        // BundleAnalyZerPlugin
        new BundleAnalyZerPlugin()
    ],
}

③ 打包,查看效果

npm run build

打包后生成打包后文件同时webpack启动一个服务展示对应图示

打包时排除node_modules

node_modules是已经被打包过的文件,不需要再用babel进行处理打包,所以排除node_modules下的文件,可以提高打包速度,

module.exports = {
     
    entry: './src/app.js',
    output: {
     
        path: path.resolve(__dirname, 'dist'),
        filename: 'js/[name].bundle.js',
        publicPath: 'http://cdn.com'
    },
    module: {
     
        rules: [ 
            {
     
                test: /\.js$/, 
                use: {
     
                    loader: 'babel-loader', 
                    options: {
     
                        presets: ['latest'] 
                    }
                },
                // node_modules是已经被打包过的文件,不需要再用babel进行处理打包
                // 所以排除node_modules下的文件,可以提高打包速度
                exclude: path.resolve(__dirname, './node_modules/'),
                // include可以用来指定包含的范围
                include: path.resolve(__dirname, './src/'),
            },
        ], 
    },

    // 插件
    plugins: [
        // ...
    ],
}

4) 其他:

webpack.config.js(webpack配置文件)

const path = require('path');
const htmlWebpckPlugin = require('html-webpack-plugin')

module.exports = {
     
    // 指定上下文
    context: __dirname,
    // 指定入口文件
    entry: './src/app.js',

    // 指定出口文件
    output: {
     
        // 规定出口文件所在目录,使用node的API,path.resolve(),通过’当前路径‘+’相对路径‘解析得到绝对路径
        path: path.resolve(__dirname, 'dist'), // __dirname:当前路径,'dist': 相对路径
        // 规定出口文件的名称
        filename: 'js/[name].bundle.js',
        // 指定出口文件所在地址
        // publicPath: 'http://cdn.com'
    },

    module: {
     
        // 指定loader
        rules: [ // 数组中的每一项,都是一个规则
            // - 对于.js结尾的文件 使用babel-loader 
            {
     
                test: /\.js$/, 
                use: {
     
                    loader: 'babel-loader',
                    // 需要npm install --save-dev babel-loader babel-core
                    options: {
     
                        presets: ['latest'] // 指定插件转换特定的es特性 
                        // 需要npm install --save-dev babel-preset-latest
                    }
                },
                // node_modules是已经被打包过的文件,不需要再用babel进行处理打包,
                // 所以排除node_modules下的文件,可以提高打包速度
                exclude: path.resolve(__dirname, 'node_modules'),
                // include可以用来指定包含的范围
                include: path.resolve(__dirname, 'src'),
            },

            // - 对于.css结尾的文件 使用css-loader处理,再经过style-loader,生成style标签并插入HTML中
            {
     
                test: /\.css$/,
                use: {
     
                    loader: 'style-loader!css-loader', // loader的处理方式:右 → 左
                    // 需要npm install --save-dev css-loader style-loader
                }
            },

            // - 对于.less结尾的文件 使用less-loader处理,再经过css-loader和style-loader,生成style标签并插入HTML中
            {
     
                test: /\.less$/,
                use: {
     
                    loader: 'style-loader!css-loader!less-loader', // 也可以去除'-loader'
                    // 需要npm install --save-dev less-loader
                }
            },

            // - 对于.sass结尾的文件 使用sass-loader处理,再经过css-loader和style-loader,生成style标签并插入HTML中
            {
     
                test: /\.sass$/,
                use: {
     
                    loader: 'style-loader!css-loader!sass-loader',
                    // 需要npm install --save-dev sass-loader
                }
            },

            // - 对于.html结尾的文件 使用html-loader处理
            {
     
                test: /\.html$/,
                use: {
     
                    loader: 'html-loader'
                    // 需要npm install --save-dev html-loader
                },
            },

            // - 对于图片文件 使用file-loader处理,可以将项目中以'相对路径'引用的文件,处理为以'绝对路径'引用 
            {
     
                test: /\.(png|jpg|gif|svg)$/i,
                use: {
     
                    loader: 'file-loader',
                    // 需要npm install --save-dev file-loader
                    options: {
     
                        // 指定处理后的图片的文件名,[name]为文件名占位符,[ext]为文件名后缀占位符
                        // 如果写为'images/[name].[ext]',还可以指定文件所在的文件夹
                        name: '[name].[ext]',
                        // 处理后的图片文件默认放在dist目录下
                        // 指定处理后的图片的地址
                        outputPath: 'images',
                    }
                },
            },

            // -当URL引用的图片或文件小于limit时,可以转换生成一段base64编码,而不再是一个url,
            // 当图片或文件大于limit时,则转交file-loader处理
            {
     
                test: /\.(png|jpg|gif|svg)$/i,
                use: {
     
                    loader: 'url-loader',
                    // 需要npm install --save-dev url-loader
                    options: {
     
                        // 设置limit为 20kB
                        limit: 20000, 
                        name: '[name].[ext]',
                    }
                }
            }
        ], 
    },

    // 插件
    plugins: [
        // 自动生成html页面
        new htmlWebpckPlugin({
     
            // 即将生成html的文件名
            filename: 'index.html',
            // 以根目录下的index.html为模板生成HTML页面
            template: 'index.html',
            // js文件要注入在head里还是body里,或者可以设置为 false
            inject: 'body',
            // <%= htmlWebpackPlugin.options.title %> 通过上述设置在模板index.html中设置title
            title: 'webpack is good', 
            // 对html文件进行压缩
            minify: {
     
                // 删除模板html文件中的注释
                removeComments: false,
                // 删除模板html文件中的空格
                collapseWhitespace: false,
            },
            // 指定要注入的chunks
            // chunks: ['main', 'a'],
            // 指定要排除的chunks
            // excludeChunks: ['c'],
            // 还可以自定义属性,譬如eee
            eee: new Date()
        }),
        
    ],
}

新建一个webpack项目

新建一个文件夹

mkdir demo

进入文件夹

cd demo

初始化npm

npm init -y

安装webpack和webpack-cli

npm install webpack webpack-cli -d

为了能直接执行命令webpack,全局安装webpack和webpack-cli

npm install webpack webpack-cli -g

新建src文件夹

你可能感兴趣的:(webpack,前端,webpack,前端)