美文网首页前端开发那些事儿
前端工程化之webpack学习记录

前端工程化之webpack学习记录

作者: Mstian | 来源:发表于2020-05-16 22:39 被阅读0次

    本文是本人正式开始学习webpack的记录文档,
    时间:2020-05-16,
    webpack版本:"webpack": "^4.43.0", "webpack-cli": "^3.3.11"

    为什么需要构建工具

    转换ES6语法
    转换JSX
    CSS前缀补全/预处理器
    压缩混淆
    图片压缩
    ...
    前端三大框架Vue.js React.js Angular.js的学习与使用都离不开webpack。
    webpack是拓宽前端技术栈必不可少的一步。

    初识webpack

    webpack默认配置文件:webpack.config.js

    可以通过webpack --config指定配置文件,比如区分开发环境与生产环境。

    webpack配置组成

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

    零配置webpack包含内容

    不用编写webpack.config.js时webpack默认配置

    entry:"./src/index.js",
    output:"./dist/main.js"
    

    环境搭建:安装webpack

    默认已安装node,我的node版本:v12.13.0

    1. 创建一个文件夹 npm init -y初始化一个package.json文件
    2. npm install webpack webpack-cli --save-dev,进行局部安装。
    3. 一个基本的例子:
      webpack.config.js
    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"main.js"
      }
      mode:"production"
    }
    

    运行:./node_modules/.bin/webpack

    基本的例子
    1. 通过npm script运行webpack
      package.json文件中
    "scripts":{
      "build":"webpack"
    }
    

    之后直接运行npm run build.

    webpack基础用法

    webpack核心概念之Entry

    Entry用来指定webpack打包的入口

    单入口:entry是一个字符串,适用单页应用。

    module.exports = {
      entry:"./src/index.js"
    }
    

    多入口:entry是一个对象,适用于多页应用。

    module.exports = {
      entry:{
        index:"./src/index.js",
        admin:"./src/admin.js"
      }
    }
    

    webpack核心概念之Output

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

    Output用法:单入口配置

    module.exports = {
      entry:"./src/index.js",
      output:{
        filename:"bundle.js",   //............打包后文件名称
        path:__dirname+"/dist"  //............打包后文件存放目录
      }
    }
    

    Output用法:多入口配置

    module.exports = {
      entry:{
        index:"./src/index.js",
        admin:"./src/admin.js"
      },
      output:{
        filename:"[name].js", //.........通过占位符确保文件名称的唯一
        path:__dirname+"/dist"
      }
    }
    

    不管一个入口还是多个入口,只有一个output,当有多个入口时,使用占位符name表示打包输出后的文件名称。

    webpack核心概念之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的用法

    module.exports = {
      module:{
        rules:[
          {test:/\.txt$/,use:"raw-loader"} // ...    test指定匹配规则 use指定使用的loader名称
        ]
      }
    }
    

    loader需要放到根节点下面的module中,module是一个对象,对象中有一个数组rules,将所有loader都放到rules数组中即可。每一个loader的使用都需要使用test去指定匹配规则,使用use去指定使用的loader名称。loader还有一个参数是exclude,排除,排除掉某项,使其不参与匹配,比如node_modules包中的文件大多都是已经转换过的,无需再参加匹配转换,因此需要将其过滤掉以提高代码构建效率。

    webpack核心概念之Plugins

    插件用于bundle文件的优化,资源管理和环境变量注入。
    作用于整个构建过程。
    任何不能通过loader完成的任务可以使用plugin去完成。

    名称 描述
    CommonsChunkPlugin 将chunks相同的模块代码提取成公共js
    CleanWebpackPlugin 清理构建目录
    ExtractTextWebpackPlugin 将css从bundle文件中提取出来成为一个独立的css文件
    CopyWebpackPlugin 将文件或文件夹拷贝到构建的输出目录
    HtmlWebpackPlugin 创建html文件出承载输出的bundle
    UglifyjsWebpackPlugin 压缩JS(webpack 4.x默认)
    ZipWebpackPlugin 将打包出的资源生成一个zip包

    Plugins的用法

    const HtmlWebpackPlugin = require("html-webpack-plugin")
    module.exports = {
      plugins:[
        new HtmlWebpackPlugin({template:"./src/index.html"}) // ..... 放到plugins数组中
      ]
    }
    

    将插件放到plugins数组中,如有多个插件继续放入plugin数组中即可。

    webpack核心概念之Mode

    Mode用来指定当前的构建环境:production、development、还是none

    设置mode可以自动触发webpack中的某些函数

    Mode的内置函数功能

    选项 描述
    development 设置 process.env.NODE_ENV的值为development.
    开启NameChunksPluginNameModulesPlugin.
    production 设置process.env.NODE_ENV的值为production.
    开启FlagDependencyUsagePlugin, FlagIncludeChunksPlugin
    ModileConcatentationPluginNoEmitOnErrorsPlugin
    OccurrenceOrderPluginSideEffectsFlagPluginTerserPlugin.
    none 不开启任何优化选项

    资源解析

    ES6

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

    安装:
    npm i @babel/core @babel/preset-env babel-loader -D

    配置文件:
    在项目根节点中创建.babelrc

    {
      "presets":[
         "@babel/preset-env" 
      ]
    }
    

    webpack.config.js

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production",
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/}
        ]
      }
    }
    

    解析React JSX

    安装:npm i react react-dom @babel-preset-react -D

    增加 React 的babel preset 配置

    {
      "presets":[
         "@babel/preset-env" ,
         "@babel/preset-react"
      ]
    }
    

    测试:
    src/index.js

    "use strict"
    import React from 'react';
    import ReactDOM from 'react-dom';
    class Test extends React.Component{
        render(){
            return (
                <div className="text">
                    Test 
                </div>
            )
        }
    }
    ReactDOM.render(
        <Test />,
        document.getElementById("root")
    )
    

    执行npm run build 打包,成功后在dist文件夹中新建一个index.html文件,通过script标签手动引入bundle.js,注意需要有id为root的元素

    <!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>
        <div id="root">
            
        </div>
        <script src="./bundle.js"></script>
    </body>
    </html>
    

    然后在网页中打开html文件预览。如果正常显示文本Test那说明成功了。

    解析CSS

    css-loader 用于加载.css文件,并且转换成commonjs对象

    style-loader将样式通过<style>标签插入html文件中

    安装:npm i style-loader css-loader -D

    测试:在src目录下创建index.css文件

    index.css

    .text{
      color:red;
    }
    

    在index.js中引入:

    "use strict"
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css'
    class Test extends React.Component{
        render(){
            return (
                <div className="text">
                    Test 
                </div>
            )
        }
    }
    ReactDOM.render(
        <Test />,
        document.getElementById("root")
    )
    

    在webpack.config.js中进行配置:

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production",
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]}
        ]
      }
    }
    

    在rules数组中继续添加loader选项,这里需要注意两点:

    1. 当需要有多个loader时,需要在use中使用一个数组。
    2. loader是链式调用,有先后顺序,在执行的时候是从右向左执行,因此,在使用style-loadercss-loader时,需要先写style-loader再写css-loader,因为需要先执行css-loader将.css文件识别解析,再通过style-loader将.css文件通过<style>标签插入页面。

    解析Less和Sass

    less-loader 用于将less转换成css
    安装:npm i less less-loader -D
    因为less-loader依赖less,因此需要安装less

    webpack.config.js

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production",
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]},
          {test:/\.less$/,use:["style-loader","css-loader","less-loader"]}
        ]
      }
    }
    

    注意配置的顺序。

    测试:将index.css 后缀修改为.less,然后在index.js中修改对应的后缀,再打包测试即可。

    Sass和less一样
    安装:npm i node-sass sass-loader -D

    注意的是sass-loader依赖node-sass因此需要安装node-sass.

    解析图片

    file-loader用于处理文件

    安装:npm i file-loader -D

    测试:在src目录下创建img文件夹,存放logo.png图片。
    在index.js中引入

    "use strict"
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css'
    import logo from './img/logo.png'
    class Test extends React.Component{
        render(){
            return (
                <div className="text">
                  Text
                    <div>
                        <img src={logo}/>
                    </div>
                </div>
            )
        }
    }
    ReactDOM.render(
        <Test />,
        document.getElementById("root")
    )
    

    webpack.config.js

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production",
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]},
          {test:/\.less$/,use:["style-loader","css-loader","less-loader"]},
          {test:/\.(png | jpg | gif | jpeg)$/,use:"file-loader"}
        ]
      }
    }
    

    再次打包成功后在dist文件夹中会生成一个带有hash命名的图片,webpack自动进行hash命名以防止图片命名冲突。

    解析字体

    字体文件解析也是用file-loader

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production",
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]},
          {test:/\.less$/,use:["style-loader","css-loader","less-loader"]},
          {test:/\.(png | jpg | gif | jpeg)$/,use:"file-loader"},
          {test:/\.(ttf | woff | woff2 | eot | otf )$/,use:"file-loader"}
        ]
      }
    }
    

    测试:
    字体解析与图片解析一样打包之后最终会在dist文件夹中生成一个带有hash命名的字体文件。

    其他资源解析方式:url-loader
    url-loader和file-loader功能差不多,url-loader可以实现小图片,小字体自动base64转换。url-loader内部也是使用了file-loader.

    安装:npm i url-loader -D

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production",
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]},
          {test:/\.less$/,use:["style-loader","css-loader","less-loader"]},
          {
            test:/\.(png | jpg | jpeg | gif)$/,
            use:[
              {
                loader:"url-loader",
                options:{limit:10240}
              }
            ]
          }
        ]
      }
    }
    

    url-loaderoptions选项中配置limit属性,表示小于10240字节(10k)的文件将处理成base64
    测试:可以找一个小于10k的图片和一个大于10k的图片打包之后在浏览器中查找元素,看它们的src对比即可发现。

    base64图片src

    文件监听

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

    webpack开启监听模式,有两种方式:

    • 启动webpack命令时,带上 --watch参数
    • 在配置webpack.config.js中设置watch:true

    package.json

    {
      "scripts":{
        "watch":"webpack --watch"
      }
    }
    

    打包时使用npm run watch之后再修改文件可以监听到文件变化,并且自动进行构建。

    缺点:每次需要手动刷新浏览器(当电脑不给力比较卡时,感觉很明显很明显)

    文件监听原理分析

    热更新 webpack-dev-server

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

    webpack-dev-server主要在开发环境(development)中使用,在生产环境(production)中一般不用,因此我们要在development环境中使用

    在package.json中配置

    {
      "scripts":{
        "dev":"webpack-dev-server"
      }
    }
    

    webpack.config.js

    const path = require("path");
    const webpack = require("webpack")
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"development", //mode改为development
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]},
          {test:/\.less$/,use:["style-loader","css-loader","less-loader"]},
          {
            test:/\.(png | jpg | jpeg | gif)$/,
            use:[
              {
                loader:"url-loader",
                options:{limit:10240}
              }
            ]
          }
        ]
      },
      plugins:[
        new webpack.HotModuleReplacementPlugin()
      ],
      devServer:{
        contentBase:"./dist", // .......服务基础目录
        hot:true,             // .......是否开启热更新
        open:true,            // .......是否自动打开浏览器
        port:3000             // .......端口号
      }
    }
    

    在根目录下新建一个dist文件夹,如果之前有打包好的先删除掉,创建一个新文件夹,里面创建一个index.html如下

    <!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>
        <div id="root"></div>
        <script src="./bundle.js"></script>
    </body>
    </html>
    

    执行npm run dev成功后会默认打开浏览器端口号为3000,此时修改src/index.js中的文件,并保存浏览器会自动更新。而且速度极快。

    其他热更新方式:
    webpack-dev-middleware

    热更新原理分析:

    热更新原理分析

    两个过程:

    1. 启动过程:1-2-A-B
    2. 更新过程:1-2-3-4

    文件指纹

    打包后输出的文件名的后缀

    文件指纹

    文件指纹好处

    版本管理,当一个项目有多个文件时,版本更新时,只有部分文件发生变化,大多数文件并未发生变化,因此可以不需要构建,可以通过文件指纹来达到控制某些未发生变化的文件不再构建。浏览器可以持续使用缓存文件,提高使用体验。

    文件指纹生成

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

    由于文件指纹不能和热更新一起使用,所以从现在开始webpack.config.js分开为:

    webpack.dev.js // mode:development
    webpack.prod.js //mode:production
    

    同时package.json中配置

    "scripts":{
      "build":"webpack --config webpack.prod.js",
      "dev":"webpack-dev-server --config webpack.dev.js"
    }
    

    当执行npm run dev的时候去查找webpack.dev.js(开发环境)然后构建,
    当执行npm run build的时候去查找webpack.prod.js(生产环境)然后构建

    使用文件指纹在生产环境中使用,那么开始看webpack.prod.js文件

    const path = require("path");
    module.exports = {
      entry:"./src/index.js",
      output:{
        path:path.join(__dirname,'dist'),
        filename:"bundle.js"
      },
      mode:"production", //mode改为production
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:["style-loader","css-loader"]},
          {test:/\.less$/,use:["style-loader","css-loader","less-loader"]},
          {
            test:/\.(png | jpg | jpeg | gif)$/,
            use:[
              {
                loader:"file-loader"
              }
            ]
          }
        ]
      }
    }
    

    JS的文件指纹设置

    设置output的filename,使用[chunkhash]

    在webpack.prod.js的基础上进行修改配置

    output:{
      path:path.join(__dirname,"dist"),
      filename:"[name]_[chunkhash:8].js"
    }
    

    JS文件指纹的配置,只需要在output的filename中配置即可。[name]为占位符,表示当有多个入口文件时区分入口文件名称,[chunkhash:8]表示使用chunkhash文件指纹并截取前8位使用“_”与[name]进行拼接也可以使用其他字符进行拼接

    图片/字体的文件指纹设置

    设置file-loader的name使用[hash]

    modules:{
      rules:[
        {
          test:/\.(png | jpg | jpeg | gif )$/,
          options:{
            name:"[name]_[hash:8].[ext]"
          }
       },
       {
          test:/\.(ttf | woff | woff2 | eot | otf )$/,
          options:{
            name:"[name]_[hash:8].[ext]"
          }
       }
      ]
    }
    

    CSS的文件指纹设置

    设置MiniCssExtractPlugin 的filename,使用[contenthash]

    CSS默认是通过style-loader直接插入html文件中的因此,要使用文件指纹就必须将css提取出来成一个单独的.css文件因此需要使用MiniCssExtractPlugin将文件提取出来,然后给该插件设置文件指纹

    安装:npm i mini-css-extract-plugin -D

    webpack.prod.js

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = {
      entry:{
        index:"./src/index.js",
        admin:"./src/admin.js",
      },
      output:{
        path:path.join(__dirname,"dist"),
        filename:"[name]_[chunkhash:8].js"
      }
      mode:"production", //mode改为production
      module:{
        rules:[
          {test:/\.js$/,use:"babel-loader",exclude:/node_modules/},
          {test:/\.css$/,use:[MiniCssExtractPlugin.loader,"css-loader"]}, //替换掉style-loader
          {test:/\.less$/,use:[MiniCssExtractPlugin.loader,"css-loader","less-loader"]},//替换掉style-loader
          {
            test:/\.(png | jpg | jpeg | gif )$/,
            options:{
              name:"[name]_[hash:8].[ext]"
            }
         },
         {
           test:/\.(ttf | woff | woff2 | eot | otf )$/,
           options:{
            name:"[name]_[hash:8].[ext]"
           }
         }
        ]
      },
      plugins:[
        new MiniCssExtractPlugin({ //使用插件提取css
          filename:"[name]_[contenthash:8].css"
        })
      ]
    }
    
    

    测试:


    文件指纹打包后效果

    附录:

    占位符名称 含义
    [ext] 资源后缀名
    [name] 文件名称
    [path] 文件的相对路径
    [folder] 文件所在的文件夹
    [contenthash] 文件内容的hash,默认是md5生成
    [hash] 文件内容的Hash,默认是md5生成
    [emoji] 一个随机的纸袋文件内容的emoji

    代码压缩

    占用字节更小,访问速度更快

    HTML压缩

    使用html-webpack-plugin,设置压缩参数
    支持传入minify参数:可以处理换行,注释,空格等。

    安装:npm i html-webpack-plugin -D

    webpack.prod.js

    const HtmlWebpackPlugin = require("html-webpack-plugin")
    module.exports = {
      plugins:[
        new HtmlWebpackPlugin({
          template:path.join(__dirname,"src/index.html"), //html模板所在位置,里面可以使用ejs语法
          filename:"index.html", //指定打包出来的html文件名称
          chunks:["index"], //指定生成的html要是用哪些chunk
          inject:true, //注入打包后的对应chunk文件
          minify:{
            html5:true,
            collapseWhitespace:true,
            preserveLineBreaks:false,
            minifyCSS:true,
            minifyJS:true,
            removeComments:false
          }
        })
      ]
    }
    

    CSS压缩

    使用optimize-css-assets-webpack-plugin
    同时使用cssnano(预处理器)
    安装:npm i optimize-css-assets-webpack-plugin cssnano -D

    webpack.prod.js

    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    module.exports = {
      plugins:[
        new OptimizeCssAssetsPlugin({
          assetNameRegExp:/\.css$/g,
          cssProcsssor:require("cssnano")
        })
      ]
    }
    

    JS压缩

    webpack4.x默认内置uglifyjs-webpack-plugin

    因此默认打包后就是使用了压缩插件,打包出来的文件也是压缩后的。

    自动清理构建文件dist

    当前每次在调试打包的时候都需要手动删除dist文件夹,这样有一些麻烦,那么有没有更好的办法呢?webpack自己就有一个插件叫clean-webpack-plugin使用它默认会删除output指定的输出目录。
    安装:npm i clean-webpack-plugin -D
    使用:

    //webpack.prod.js
    const CleanWebpackPlugin = require("clean-webpack-plugin")
    
    module.exports = {
      plugins:[
        new CleanWebpackPlugin ()
      ]
    }
    

    当我再次执行npm run build时很遗憾报错了,错误是CleanWebpackPlugin is not a constructor,经过查阅资料发现,使用规则变了,在引用的时候应该是const CleanWebpackPlugin = require("clean-webpack-plugin")

    //webpack.prod.js
    const { CleanWebpackPlugin } = require("clean-webpack-plugin")
    
    module.exports = {
      plugins:[
        new CleanWebpackPlugin ()
      ]
    }
    

    然后每次执行打包的时候就会自动删除掉dist目录,否则dist目录里的文件会越来越多。

    增强CSS

    CSS3的属性为什么要增加前缀呢、?

    • Trident(-ms)
    • Geko(-moz)
    • Webkit(-webkit)
    • Presto(-o)
    -ms-border-radius:10px;
    -moz-border-radius:10px;
    -webkit-border-radius:10px;
    border-radius:10px;
    

    如果需要使用兼容的css3属性很多,那么写起来就很麻烦。
    还好webpack中有autoprefixer插件,自动补齐CSS3前缀

    autoprefixer与less sass不同后者属于预处理器,前者属于后置处理器,在打包时先使用less等预处理器,打包完成后才会使用autoprefixer后置处理器。

    推荐一个工具htttps://www.caniuse.com可以测试css属性的兼容性。

    安装:npm i postcss-loader autoprefixer -D

    使用

    //webpack.prod.js
    module.exports = {
      module:{
        rules:[
          {
            test:/\.less$/,
            use:[
              MiniCssExtractPlugin.loader,
              "css-loader",
              "less-loader",
              "postcss-loader"
            ]
          }
        ]
      }
    }
    

    还需要项目根目录下创建一个新的文件postcss.config.js

    module.exports = {
        "plugins": [
            require('autoprefixer')({
                browsers: [
                    "> 1%",
                    "last 2 versions",
                    "not ie <= 8"
                ]
            })
        ]
    }
    

    这种方法是可行的,但是命令行报了一个提示,

    warning
    大概意思是现在这种配置方法过时了,需要在package.json文件中去配置,或者在.browserslistrc文件中去配置,但是我配置后都报错。不知道为啥。
    其他配置方法可见:https://www.cnblogs.com/ladybug7/p/12360388.html
    以及参考文档:https://github.com/browserslist/browserslist#readme
    配置好之后再次执行npm run build,打开dist文件夹下的css文件可以看到
    .admin{color:pink;font-size:25px;display:-webkit-box;display:-ms-flexbox;display:flex}.text{color:#9acd32;font-size:30px}
    

    被压缩,并且自动补全前缀的css3属性display:flex

    移动端CSS px自动转换成rem

    使用px2rem-loader 配合 lib-flexible插件
    安装:
    npm i px2rem-loader -D npm i lib-flexible -S
    使用:

    {
        test:/\.less$/,
        use:[
            MiniCssExtractPlugin.loader,
            "css-loader",
            "less-loader",
            "postcss-loader",
            {
                loader:'px2rem-loader',
                options:{
                    remUnit:75,
                    remPrecision:8
                }
            }
        ]
    }
    

    其中options参数中remUnit表示基准单位,比如设计稿是750为基准的,那1rem就等于75px,10rem就代表750px。remPrecision表示保留单位精确到多少位。
    具体看打包后的css代码:

    .admin{color:pink;font-size:.33333333rem;display:-webkit-box;display:-ms-flexbox;display:flex}.text{color:#9acd32;font-size:30px}
    

    由于我只是在less文件中配置了px2rem的loader,所以只有.less结尾的文件px会被转化,而以.scss结尾的文件并不会被转化。
    但是此时的打包后页面打开并没有效果,因为lib-flexible并没有在初始化的时候加载到,接着往下看。

    资源内联

    资源内联的好处:
    代码层面

    • 页面框架的初始化脚本
    • 上报相关打点
    • css内联避免页面闪动
      请求层面:减少http网络请求数
    • 小图片或者字体内联(url-loader)

    HTML和JS内联
    通过使用raw-loader进行内联
    raw-loader内联html

    ${require('raw-loader!babel-loader!./meta.html')}
    

    raw-loader内联JS

    <script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>
    

    注意:由于html-webpack-plugin插件默认使用ejs模板引擎,因此可以直接在代码中使用${}这种写法。

    下载:npm i raw-loader@0.5.1 -D
    使用:
    举个例子,比如在一个页面中要是用很多meta标签,这些meta标签可以被公用,那么就可以把它单独拿出来
    meta.html

       <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
        <meta name="apple-mobile-web-app-capable" content="yes"/><!-- 删除苹果默认的工具栏和菜单栏 -->
        <meta name="apple-mobile-web-app-status-bar-style" content="black"/><!-- 设置苹果工具栏颜色 -->
        <meta name="format-detection" content="telephone=no, email=no"/><!--忽略页面中的数字识别为电话,忽略email识别 -->
        <!-- 启用360浏览器的极速模式(webkit) -->
        <meta name="renderer" content="webkit">
        <!-- 避免IE使用兼容模式 -->
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
        <meta name="HandheldFriendly" content="true">
        <!-- 微软的老式浏览器 -->
        <meta name="MobileOptimized" content="320">
        <!-- uc强制竖屏 -->
        <meta name="screen-orientation" content="portrait">
        <!-- QQ强制竖屏 -->
        <meta name="x5-orientation" content="portrait">
        <!-- UC强制全屏 -->
        <meta name="full-screen" content="yes">
        <!-- QQ强制全屏 -->
        <meta name="x5-fullscreen" content="true">
        <!-- UC应用模式 -->
        <meta name="browsermode" content="application">
        <!-- QQ应用模式 -->
        <meta name="x5-page-mode" content="app">
        <!-- windows phone 点击无高光 -->
        <meta name="msapplication-tap-highlight" content="no">
    

    在src的html模板文件中这样写:

    <head>
        ${ require('raw-loader!babel-loader!./meta.html')}
        <title>Document</title>
        <script>${ require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>
    </head>
    

    这样打包后会将html文件和js文件内联到项目文件里。(这个例子在我的使用中完全没生效,也没报错,不知道为啥。。。)

    多页面文件打包

    多页面文件打包需要在配置文件中写多个入口,比如:

    entry:{
      index:"./src/index.js",
      admin:"./src/admin.js"
    }
    

    如果需要使用html模板也需要实用多个HtmlWebpackPlugin插件。比如:

    new HtmlWebpachPlugin({
      template:path.join(__dirname,'src/index.html'),
      filename:"index.html",
      chunks:["index"], //html 使用哪些chunk
      inject:true, // 自动注入
      minify:{
          html5:true,
          collapseWhitespace:true,
          preserveLineBreaks:false,
          minifyCSS:true,
          minifyJS:true,
          removeComments:false
      }
    }),
    new HtmlWebpachPlugin({
      template:path.join(__dirname,'src/admin.html'),
      filename:"admin.html",
      chunks:["admin"], //html 使用哪些chunk
      inject:true, // 自动注入
      minify:{
          html5:true,
          collapseWhitespace:true,
          preserveLineBreaks:false,
          minifyCSS:true,
          minifyJS:true,
          removeComments:false
      }
    })
    

    这样的话,每增加一个页面就需要手动去修改webpack配置文件,不优雅,可以借助glob插件来完成。
    glob模块是node的一个模块,它允许你使用*这样的符号,来获取匹配对应规则的文件。
    我们的目的是通过glob匹配获取到入口文件路径,然后使用正则匹配获取到我们需要标识文件名的字符,之后进行entry和HtmlWebpackPlugins配置。然后到处进行使用。

    首先需要对文件目录进行规划,将每个页面都整合到一个文件夹内然后入口文件名称也需要修改一下。大概如下:


    src目录规划图.png

    安装:npm install glob -D
    引入:const glob= require('glob')
    核心代码:

    const path = require("path");
    const webpack = require("webpack");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const HtmlWebpachPlugin = require('html-webpack-plugin');
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const glob = require('glob');
    const MPA = () => {
        const entry = {};
        const htmlWebpackPlugins = [];
        const entryFiles = glob.sync(path.join(__dirname,'./src/*/index.js'));
        Object.keys(entryFiles).map((index) => {
            const entryFile = entryFiles[index]
            const matchName = entryFile.match(/src\/(.*)\/index\.js/);
            const pageName = matchName && matchName[1] 
            entry[pageName] = entryFile;
            htmlWebpackPlugins.push(
                new HtmlWebpachPlugin({
                    template:path.join(__dirname,`src/${pageName}/${pageName}.html`),
                    filename:`${pageName}.html`,
                    chunks:[pageName], //html 使用哪些chunk
                    inject:true, // 自动注入
                    minify:{
                        html5:true,
                        collapseWhitespace:true,
                        preserveLineBreaks:false,
                        minifyCSS:true,
                        minifyJS:true,
                        removeComments:false
                    }
                })
            )
        })
        return {
            entry,
            htmlWebpackPlugins
        }
    }
    const { entry, htmlWebpackPlugins } = MPA();
    
    module.exports = {
        entry:entry,
        output:{
            path:path.join(__dirname,"dist"),
            filename:"[name]_[chunkhash:8].js"
        },
        mode:"production",
        plugins:[
            new MiniCssExtractPlugin({
                filename:'[name]_[contenthash:8].css'
            }),
            new OptimizeCssAssetsPlugin({
                assetNameRegExp:/\.css$/g,
                cssProcsssor:require("cssnano")
            }),
            new CleanWebpackPlugin(),
            ...htmlWebpackPlugins
        ]
    }
    

    source map

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

    • 线上排查问题的时候可以将sourcemap上传到错误监控系统
      source map关键字
      eval:使用eval包裹模块代码
      source map:产生.map文件
      cheap:不包含列信息
      inline:将.map作为DataURL嵌入,不单独生成.map文件
      module:包含loader的sourcemap

    source map 类型


    source map 类型

    举例:在webpack.prod.js中配置devtool:

    module.exports = {
      entry:...,
      moudle:...,
      plugins:...,
      devtool:"source-map"
    }
    

    source-map会打包出一个.map文件,如果使用inline-source-map那么就会将文件都打包到一个文件中,文件体积会比较大。

    本地开发调试:webpack.dev.js中配置devtool:

    module.exports = {
      entry:...,
      moudle:...,
      plugins:...,
      devtool:"source-map"
    }
    

    如果不使用sourcemap的话,进行debugger的时候那么debugger的文件显示是打包后的,不利于调试,使用了sourcemap之后那么就会显示源代码。

    source-map断点调试代码

    devtool设置成cheap-source-map的时候,那么对于错误定位只能定位到行,而不能定位到列。

    基础库分离

    思路:将react、react-dom基础包通过cdn引入,不打入bundle中。
    方法:使用html-webpack-externals-plugin
    安装:npm i html-webpack-externals-plugin -D

    const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
    plugins:[
      new HtmlWebpackExternalsPlugin({
        externals:[
          {
            module:'react',
            entry:'https://unpkg.com/react@16/umd/react.development.js',
            global:'React'
          },{
            module:'react',
            entry:'https://unpkg.com/react-dom@16/umd/react-dom.development.js',
            global:'ReactDOM'
          }
        ]
      })
    ]
    

    然后执行npm run build可以观察到构建的文件体积变小,在使用react的页面与使用react-dom页面中自动引入了文件。
    利用SplitChunksPlugin进行公共脚本分离

    module.exports = {
        optimization:{
            splitChunks:{
                cacheGroups:{
                    commons:{
                        test:/(react|react-dom)/,
                        name:"vendors",
                        chunks:'all'
                    }
                }
            }
        }
    }
    

    Webpack4内置模块,替代CommonsChunkPlugin插件;
    chunks参数说明:

    • async 异步引入的库进行分离(默认)
    • initial 同步引入的库进行分离
    • all 所有引入的库进行分离(推荐)

    利用SpiltChunksPlugin分离页面公共文件

    module.exports = {
        optimization: {
            splitChunks: {
                cacheGroups:{
                    common:{
                        name:"commons",
                        minSize:0,
                        chunks:"all",
                        priority:5,
                        minChunks:2
                    }
                }
            }
        },
    }
    

    miniChunks:设置最小引用次数为2次
    minSize:需分离的包体积大小
    上述配置表示只要被引用的包体积大于0,至少被引用2次,那么就会被打包到commons文件,执行npm run build构建命令,那么只要符合上述条件,就会被打包到commons文件中。

    小结:webpack分割公用代码方法,可以使用html-webpack-externals-plugin进行基础代码分离,更常用的是使用SpiltChunksPlugin进行代码分割,大概思路有两种,一是对公共脚本进行分离,二是对页面公用文件进行分离(可以进行通过文件大小以及引用次数检测来决定是否需要分离)。

    tree shaking(摇树优化)

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

    使用:webpack默认支持,在mode为production的情况下默认开启。
    要求:必须是ES6的语法,CJS的方式不支持

    一个概念:DCE(dead Code Elimination)死码消除,死码消除(Dead code elimination)是一种编译最优化技术,它的用途是移除对程序运行结果没有任何影响的代码。
    代码不会执行,不可到达
    代码执行结果不会被用到
    代码只会影响死变量(只写不读)
    例如:

    if (false) {
      //这段逻辑永远不会执行
      let a = 12;
    }
    

    Tree-shaking原理:
    利用ES6模块的特点:

    • 只能作为模块顶层的语句出现
    • import的模块名只能是字符串常量
    • import binding是immutable的
      代码擦除:uglify阶段删除无用代码
      测试:在一个文件中写两个方法,A和B。打包时将webpack.prod.js的mode设置成none,观察打包后的结果,A和B都会存在,然后在一个文件中将A导入,并进行使用,之后再讲mode设置成production再去打包,之后去检索会发现只打包了A,没有B。

    scope hoisting

    现象:构建后的代码存在大量闭包代码
    scope hoisiting原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。

    通过scope hoisting可以减少函数声明代码和内存开销
    使用webpack mode为production的时候默认开启。
    必须是ES6语法,cjs不支持。
    在webpack3中还需要使用调用插件:

    plugins:[
      new webpack.optimize.ModuleConcatenationPlugin()
    ]
    

    在webpack4中mode为production时默认自动开启插件。https://www.webpackjs.com/concepts/mode/
    harmony ES6的简称。

    代码分割

    意义:对于大的Web应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块在某些特殊的是时候才会被用到。webpack有一个功能就是将你的代码库分割成chunks,当代码运行到需要它们的时候再进行加载。
    适用的场景:

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

    懒加载JS脚本的方式
    CommonJS:require.ensure
    ES6:动态import (目前需要babel转换)

    安装babel插件:

    npm install @babel/plugin-syntax-dynamic-import --save-dev
    

    使用:

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

    举例:
    比如点击按钮加载一个组件可以使用动态引入

    import ('./Test.js').then((text)=>{
           this.setState({
               Text:text.default
           })
    })
    

    import引入属于异步,因此需要在异步中捕获结果

    webpack中打包库和组件

    webpack除了可以用来打包应用,也可以用来打包js库

    例子:实现一个大整数加法库的打包

    • 需要压缩版本和非压缩版本
    • 支持AMD/CJS/ESM模块引入

    如何将库暴露出去:
    library:指定库的全局变量
    libraryTarget:支持库引入的方式

    module.exports = {
      mode:"production",
      entry:{
        "large-number":"./src/index.js",
        "large-number.min":"./src/index.js"
      },
      output:{
        filename:"[name].js",
        library:"largeNumber",
        libraryExport:"default",
        libraryTarget:"umd"
      }
    }
    

    当mode为production的时候通过打包实现的都是压缩版本。
    当mode为none的时候通过打包实现的都是未压缩版本。
    如何只对.min压缩?
    通过include设置只压缩min.js结尾的文件。

    module.exports = {
      mode:"none",
      entry:{
        "large-number":"./src/index.js",
        "large-number.min":"./src/index.js"
      },
      output:{
        filename:"[name].js",
        library:"largeNumber",
        libraryExport:"default",
        libraryTarget:"umd"
      },
      optimization:{
        minimize:true,
        minimizer:[
          new TerserPlugin({
            include:/\.min\.js$/
          })
        ]
      }
    }
    

    压缩插件TerserPluginwebpack4 production 默认使用。

    设置入口文件
    package.json 的main字段为index.js

    if(process.env.NODE_ENV === "production"){
      module.exports = require("./dist/large-number.min.js")
    }else{
      module.exports = require("./dist/large-number.js")
    }
    

    最后注册npm账号,发布,注意,package.json中的private需要设置成false。以下附上部分代码:
    package.json

    {
      "name": "large-num-add",
      "version": "1.0.0",
      "description": "大整数加法",
      "main": "index.js", // 入口文件,表示引入的包
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack"
      },
      "license": "ISC",
      "author": "Mstian",
      "private": false,
      "devDependencies": {
        "terser-webpack-plugin": "^3.0.6",
        "webpack": "^4.43.0",
        "webpack-cli": "^3.3.12"
      }
    }
    

    index.js package.json中的main 这里做了个环境判断

    if (process.env.NODE_ENV === "production") {
        module.exports = require("./dist/large-number.min.js")
    } else {
        module.exports = require("./dist/large-number.js")
    }
    

    webpack.config.js

    const TerserWebpackPlugin = require("terser-webpack-plugin");
    module.exports = {
      mode:"production",
      entry:{
        "large-number":"./src/index.js",
        "large-number.min":"./src/index.js"
      },
      output:{
        filename:"[name].js",
        library:"largeNumberAdd",
        libraryExport:"default",
        libraryTarget:"umd"
      },
      optimization:{
        minimize:true,
        minimizer:[
          new TerserWebpackPlugin({
            include:/\.min\.js$/
          })
        ]
      }
    }
    

    文件结构图


    文件结构.png

    打包后的js库地址:https://www.npmjs.com/package/large-num-add

    这块内容可结合之前一篇文章vue-cli3自定义插件并发布到npm

    附录

    以上代码完整版
    仅供阅读
    文件目录


    文件目录

    webpack.prod.js

    
    const path = require("path");
    const webpack = require("webpack");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const HtmlWebpachPlugin = require('html-webpack-plugin');
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    
    const glob = require('glob');
    
    const MPA = () => {
        const entry = {};
        const htmlWebpackPlugins = [];
        const entryFiles = glob.sync(path.join(__dirname,'./src/*/index.js'));
        Object.keys(entryFiles).map((index) => {
            const entryFile = entryFiles[index]
            const matchName = entryFile.match(/src\/(.*)\/index\.js/);
            const pageName = matchName && matchName[1] 
            entry[pageName] = entryFile;
            htmlWebpackPlugins.push(
                new HtmlWebpachPlugin({
                    template:path.join(__dirname,`src/${pageName}/${pageName}.html`),
                    filename:`${pageName}.html`,
                    chunks:[pageName], //html 使用哪些chunk
                    inject:true, // 自动注入
                    minify:{
                        html5:true,
                        collapseWhitespace:true,
                        preserveLineBreaks:false,
                        minifyCSS:true,
                        minifyJS:true,
                        removeComments:false
                    }
                })
            )
        })
        return {
            entry,
            htmlWebpackPlugins
        }
    }
    
    const { entry, htmlWebpackPlugins } = MPA();
    
    module.exports = {
        entry:entry,
        output:{
            path:path.join(__dirname,"dist"),
            filename:"[name]_[chunkhash:8].js"
        },
        mode:"production",
        module:{
            rules:[
                {
                    test:/\.js$/,
                    use:"babel-loader"
                },
                {
                    test:/\.css$/,
                    use:[MiniCssExtractPlugin.loader,"css-loader"]
                },
                {
                    test:/\.less$/,
                    use:[
                        MiniCssExtractPlugin.loader,
                        "css-loader",
                        "less-loader",
                        "postcss-loader",
                        {
                            loader:'px2rem-loader',
                            options:{
                                remUnit:75,
                                remPrecision:8
                            }
                        }
                    ]
                },
                {
                    test:/\.scss$/,
                    use:[MiniCssExtractPlugin.loader,'css-loader','sass-loader']
                },
                {
                    test:/\.(otf|woff|eot|ttf|woff2)$/,
                    use:{
                        loader:"file-loader",
                        options:{
                            name:"[name]_[hash:8].[ext]"
                        }
                    }
                },
                {
                    test:/\.(png|jpg|gif)$/,
                    use:{
                        loader:"url-loader",
                        options:{
                            name:"[name]_[hash:8].[ext]",
                            limit:10240
                        }
                    }
                },
            ]
        },
        plugins:[
            new MiniCssExtractPlugin({
                filename:'[name]_[contenthash:8].css'
            }),
            new OptimizeCssAssetsPlugin({
                assetNameRegExp:/\.css$/g,
                cssProcsssor:require("cssnano")
            }),
            new CleanWebpackPlugin(),
            ...htmlWebpackPlugins
        ],
        devtool:'source-map'
    }
    

    webpack.dev.js

    
    const path = require("path");
    const webpack = require("webpack");
    const HtmlWebpachPlugin = require('html-webpack-plugin')
    const {CleanWebpackPlugin} = require("clean-webpack-plugin");
    
    const glob = require('glob');
    
    const MPA = () => {
        const entry = {};
        const htmlWebpackPlugins = [];
        const entryFiles = glob.sync(path.join(__dirname,'./src/*/index.js'));
        Object.keys(entryFiles).map((index) => {
            const entryFile = entryFiles[index]
            const matchName = entryFile.match(/src\/(.*)\/index\.js/);
            const pageName = matchName && matchName[1] 
            entry[pageName] = entryFile;
            htmlWebpackPlugins.push(
                new HtmlWebpachPlugin({
                    template:path.join(__dirname,`src/${pageName}/${pageName}.html`),
                    filename:`${pageName}.html`,
                    chunks:[pageName], //html 使用哪些chunk
                    inject:true, // 自动注入
                    minify:{
                        html5:true,
                        collapseWhitespace:true,
                        preserveLineBreaks:false,
                        minifyCSS:true,
                        minifyJS:true,
                        removeComments:false
                    }
                })
            )
        })
        return {
            entry,
            htmlWebpackPlugins
        }
    }
    
    const { entry, htmlWebpackPlugins } = MPA();
    module.exports = {
        entry:entry,
        output:{
            path:path.join(__dirname,"dist"),
            filename:"[name].js"
        },
        mode:"development",
        module:{
            rules:[
                {
                    test:/\.js$/,
                    use:"babel-loader"
                },
                {
                    test:/\.css$/,
                    use:["style-loader","css-loader"]
                },
                {
                    test:/\.less$/,
                    use:["style-loader","css-loader","less-loader"]
                },
                {
                    test:/\.scss$/,
                    use:['style-loader','css-loader','sass-loader']
                },
                {
                    test:/\.(otf|woff|eot|ttf|woff2)$/,
                    use:"file-loader"
                },
                {
                    test:/\.(png|jpg|gif)$/,
                    use:{
                        loader:"url-loader",
                        options:{
                            limit:10240
                        }
                    }
                },
            ]
        },
        plugins:[
            new webpack.HotModuleReplacementPlugin(),
            new CleanWebpackPlugin(),
            ...htmlWebpackPlugins
        ],
        devServer:{
            contentBase:'./dist',
            hot:true,
            open:true,
            port:3000
        },
        devtool:'source-map'
    }
    

    package.json

    {
      "name": "test",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack --config webpack.prod.js",
        "watch": "webpack --watch",
        "dev": "webpack-dev-server --config webpack.dev.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@babel/core": "^7.9.6",
        "@babel/preset-env": "^7.9.6",
        "@babel/preset-react": "^7.9.4",
        "autoprefixer": "^9.8.0",
        "babel-loader": "^8.1.0",
        "clean-webpack-plugin": "^3.0.0",
        "css-loader": "^3.5.3",
        "cssnano": "^4.1.10",
        "file-loader": "^6.0.0",
        "glob": "^7.1.6",
        "html-webpack-plugin": "^4.3.0",
        "less": "^3.11.1",
        "less-loader": "^6.1.0",
        "mini-css-extract-plugin": "^0.9.0",
        "node-sass": "^4.14.1",
        "optimize-css-assets-webpack-plugin": "^5.0.3",
        "postcss-loader": "^3.0.0",
        "px2rem-loader": "^0.1.9",
        "raw-loader": "^0.5.1",
        "react": "^16.13.1",
        "react-dom": "^16.13.1",
        "sass-loader": "^8.0.2",
        "style-loader": "^1.2.1",
        "url-loader": "^4.1.0",
        "webpack": "^4.43.0",
        "webpack-cli": "^3.3.11",
        "webpack-dev-server": "^3.11.0"
      },
      "dependencies": {
        "lib-flexible": "^0.3.2"
      }
    }
    

    GitHub源码:https://github.com/Mstian/webpack-base

    未完。。。

    写在最后:文中内容大多为自己平时从各种途径学习总结,文中参考文章大多收录在我的个人博客里,欢迎阅览http://www.tianleilei.cn

    相关文章

      网友评论

        本文标题:前端工程化之webpack学习记录

        本文链接:https://www.haomeiwen.com/subject/reedohtx.html