Webpack介绍

官方定义

一个现代的JavaScript应用程序的静态模块打包器。

怎么理解打包动作?

假如一个项目有 a.js b.js c.js ...(实际项目肯定很多)

如果让各个js保持分离状态,要考虑的问题点有:

  • 各个js 之间的依赖关系

  • 每个资源分开请求加载,增加了性能开销

出于种种原因,我们希望将多个JS文件揉合成一个,只加载一次,也不用考虑依赖关系。

接着打包这个动作登场

为什么要打包?

  • 逻辑多

  • 文件多

  • 项目复杂度增加了

eg:

  • 原生JS没有类型校验的能力 于是有了 TypeScript

  • css 不够好用 有了 sass, less

Webpack 不止打包,还有翻译能力(loader), 也可进行一些骚操作(plugin),loader 和 plugin 都是可插拔的,webpack没有规定你必须用什么,或者不能用什么。

webpack 不仅强大 而且灵活

Webpack的原理与背景

Webpack产生的背景就是因为,随着前端项目的复杂度不断提高,文件数目,文件复杂度提高,传统的方式容易带来很多问题,比如命名冲突,各个模块之间的依赖关系需要人工去处理,以及后续代码的可维护性,代码耦合度等等一系列问题。

模块化优点

作用域封装

重用性

解除模块之间耦合, 提高系统的可维护性

模块化发展

详情见链接:

[JavaScript早期实现模块化](JavaScript早期实现模块化 - lvzl - 博客园 (cnblogs.com))

[AMD模块化](JavaScript AMD模块化规范 - lvzl - 博客园 (cnblogs.com))

[CMD模块化](JavaScript CMD规范 - lvzl - 博客园 (cnblogs.com))

[ES6模块化](JS模块化 - lvzl - 博客园 (cnblogs.com))

打包工具

gulp

GRUNT

webpack

区别:前两者定位都是实现一个自动化构建工具,帮助程序员完成那些需要重复的操作。而webpack则是专注于打包。

打包机制

输出文件结构

(function(module) {
    // 记录已经安装过的模块,防止二次加载,浪费时间
    var installedModules = {};
    // 加载模块的核心方法
    function __webpack_require__(moduleId){
        // SOME CODE
    }
    // 。。。
    return __webpack_require__(0); // 入口文件
})([ /* 依赖模块的集合 */])

核心方法

function __webpack_require__(moduleId) {
    // check if module is in cache
    if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // create a new module (and put into cache)
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };
    // exe the module func
    modules[moduleId].call{
        module.exports,
            module,
            module.exports,
            __webpack_require__
    };
    // flag the module as loaded
    module.l = true;
    // return the exxports of the module
    return module.exports;
}

打包过程简述

  • 从入口文件开始,分析整个应用的依赖树

  • 将每个依赖模块包装起来,放到一个数组中等待调用

  • 实现模块加载的方法,并把它放到模块执行的环境中,确保模块间可以互相调用

  • 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数

Webpack实战

npm & 包管理器

包管理器

指可以便捷的让开发者获取、分发代码的工具,比如Vue,React就是在包管理器中管理的。

npm

npm init 初始化一个项目 npm init -y 所有选项都是y

package.json

{
  "name": "webpack-demo", // 包名称
  "version": "1.0.0", //版本号
  "description": "",
  "main": "index.js", // 包执行的入口文件
  "dependencies": {
    "loadsh": "0.0.4",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  },
  "devDependencies": {},
  "scripts": { // 自定义脚本
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

npm '仓库' & '依赖'

仓库就是npm包的站点

// 设置仓库为淘宝镜像地址
npm config set registry https://registry.npm.taobao.org

// --save 会将安装包信息添加到package.json中的dependencies中,npm5之后默认会添加,之前的版本需要 --save参数才会
npm install loadsh --save 

// --save-dev 简写 -D 会将安装包信息添加到package.json中的devDependencies中 开发环境的依赖
npm install loadsh --save-dev

// 安装dependencies & devDependencies中的依赖信息
npm install 

// 只安装dependencies中的依赖信息
npm install --only=prod 

// 只安装devDependencies中的依赖信息
npm install --only=dev 

// 当我们把自己的npm包发布出去以后,别人通过npm install 安装时,只会安装dependencies中的依赖
// dependencies: 与功能相关的依赖 
// devDependencies: 开发时需要的一些辅助工具,比如eslint
// 删除模块
npm uninstall modulename  
// 清除缓存
npm cache clean --force 

npm '语义化版本'

^3.4.1 -----> 3.X.X

~3.4.1 ------> 3.4.X

3.4.1 ------> 3.4.1 特定版本

npm '自定义工程脚本'

package.json 的 script

npm install的过程

  • 寻找包版本信息文件package.json, 依照进行安装
  • 查package.json中的依赖,并检查项目中其他的版本信息文件
  • 如果发现了新包,就更新版本信息文件

demo

初始化react-dom

mkdir react-demo
cd react-demo
// 使用npm初始化,生成package.json
npm init -y
// 安装webpack相关模块
npm install webpack@4.44.1 webpack-cli@3.3.12 webpack-dev-server
npm install react react-dom
// 安装处理JSX 和 ES6语法的模块
npm install @babel/core @babel/preset-env @babel/preset-react 
// 安装处理HTML文件的模块
npm install html-webpack-plugin
// 通过vscode 打开
code .

工程目录

package.json

{
  "name": "react-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --open",
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.14.3",
    "@babel/preset-env": "^7.14.2",
    "@babel/preset-react": "^7.13.13",
    "babel-loader": "^8.2.2",
    "happypack": "^5.0.1",
    "html-webpack-plugin": "^4.5.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "terser-webpack-plugin": "4.2.3",
    "webpack": "^4.44.1",
    "webpack-bundle-analyzer": "^4.4.2",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}

新建webpack.config.js

const HtmlWebPackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
    resolve: { // import模块时,在此配置过的文件名后缀,可以不写
        extensions: ['.jsx', '.js', '.json', '.mjs', '.wasm']
    },
    entry: path.resolve(__dirname, 'src/entry.jsx'), // 入口文件
    module: { // loader
        rules: [
            {
                test: /\.jsx?/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        babelrc: false,
                        presets: [
                            require.resolve('@babel/preset-react'),
                            [require.resolve('@babel/preset-env', {module: false})]
                        ]
                    }
                }
            }
        ]
    },
    plugins: [ // plugin
        new HtmlWebPackPlugin({
            template: path.resolve(__dirname, 'src/index.html'),
            filename: 'index.html'
        })
    ],
    devServer: {
        hot: true
    }
}s

新建app.jsx模块

import React from 'react'
import ReactDom from 'react-dom'

const App = () => {
    return (
        <h1>React Test</h1>
    )
}
export default App
ReactDom.render(<App/>, document.getElementById('app'))

新建入口文件entry.jsx

import App from './app'
if (module.hot) {
    module.hot.accept(error => {
        if (error) {
            console.error('HMR出BUG了');  
        }
    })
}

新建index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

打包

node_modules\.bin\webpack //此处使用的局部 webpack 若已全局安装,直接使用webpack即可

启动预览

node_modules\.bin\webpack-dev-server
node_modules\.bin\webpack-dev-server --open // 会直接在浏览器打开

npm run dev

Webpack与前端性能

打包结果优化

打包结果的体积越小越好

借助一些插件完成打包结果的优化,比如去掉console,debugger,alert,以及一些无用代码。配置示例:

const HtmlWebPackPlugin = require('html-webpack-plugin')
const path = require('path')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin') // 处理压缩代码的模块
// 打包结果分析器
const WebpackAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 

// uglifyjs-webpack-plugin 对es5的代码 压缩效果较好,而TerserPlugin是uglifyjs-es的一个分支,对ES6压缩效果较好
module.exports = {
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                // 缓存,可以加快构建速度
                parallel: 4, // 多进程并发 加快构建
                terserOptions: {
                    compress: {
                        unused: true, // 没有用到的,剔除
                        drop_debugger: true, // debugger 剔除
                        drop_console: true, // console 剔除
                        dead_code: true // 无用代码 剔除
                    }
                }
            })
        ]
    },
    resolve: {
        extensions: ['.jsx', '.js', '.json', '.mjs', '.wasm']
    },
    entry: path.resolve(__dirname, 'src/entry.jsx'),
    module: {
        rules: [
            {
                test: /\.jsx?/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        babelrc: false,
                        presets: [
                            require.resolve('@babel/preset-react'),
                            [require.resolve('@babel/preset-env', {module: false})]
                        ]
                    }
                }
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: path.resolve(__dirname, 'src/index.html'),
            filename: 'index.html'
        }),
        new webpack.HotModuleReplacementPlugin(),
        new WebpackAnalyzer()
    ],
    devServer: {
        hot: true
    }
}

借助webpack-bundle-analyzer plugin对打包后的模块大小进行分析,从而进一步考虑还需要优化的文件

构建过程优化

  • 首先我们先试着减少需要打包处理的文件,量少了,自然也就快了,比如解析不动的文件就不解析。jquery echarts

npParse: /node_modules/(jquery.js)/

exclude > include test

  • 但是当"量"已经没办法在减少了,那就只能想别的方法加快构建速度并发构建。借助HappyPack, thread-loader

HappyPack

const HappyPack = require('happypack')
// 根据CPU的数量创建线程池
const happyPackPool = HappyPack.ThreadPool({size: OscillatorNode.cpus().length})
modules.exports = {
     plugins: [
        new HappyPack({
            ids: 'jsx',
            threads: happyPackPool,
            loaders: ['bable-loader'] // 需要注意happypack支持的loader
        })
    ]   
}

thread-loader

module: {
        rules: [
            {
                test: '/\.js$/',
                include: path.resolve('src'),
                use: [
                    'thread-loader' // 放到其他loader之前
                ]
            }
        ]
}
  • sourceMap 生成优化,这应该十个很耗时间的操作

  • 找些本身就很快的loader替换普通的loader,比如fast-sass-loader

tree-Shaking

树—摇晃

去除无用的代码(DCE)

一种DCE的实现

webpack 本身会分析ES6的module引入情况,去掉没有用到的,再借助terserPlugin进一步删除无用的模块。

Webpack: 不止'pack'

前端蓬勃发展的产物

模块化打包方案

工程化方案