webpack4技术栈学习笔记

一、webpack

  • 如何初始化环境?
  1. 安装node.js
  2. 创建个项目文件夹
  3. npm init -y(一路yes)
  4. npm install webpack webpack-cli -D(等于–save-dev)
  5. 跳转package.json,删除main防止发布代码,增加private字段
    {
    "name": "webpack-demo",
    "version": "1.0.0", 
    "description": "",
    "private": true,
    "main": "index.js", 
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "webpack": "^4.0.1",
      "webpack-cli": "^2.0.9"
    },
    "dependencies": {}
  }
  • 目录说明?
  1. scr放置项目文件
  2. dist放置打包后的文件和index.html,方便查找
  • webpack.config.js配置文件说明?
  1. 在配置文件中注释
  • 引入方式
  1. ES module 模块引入方式
    export defalut Header;
    import Header from ‘./header.js’;
    new Header();
  2. CommonJS 模块引入方式
    modele.exports = Header
    var Header = require(’./header.js’);
    new Header();
  3. CMD
  4. ADM
  • 为啥没有webpack.config.js配置文件,一样能够进行打包?
    因为webpack开发者致力于智能打包,尽量简化开发者的操作,所以是有个默认的配置文件,如果没有写配置文件的话。

  • webpack安装方式

  1. global:npm install webpack webpack-cli ——> webpack index.js
    劣势:每个项目的依赖可能不同,可能会有干扰影响。例如安装过新的webpack,A项目的依赖版本较低项目运行可能会有问题等
  2. local:npm install webpack webpack-cli -D ——> npx webpack index.js
    优势:项目内安装,每个项目针对安装
  3. npm script:npm run bundle ——>webpack(package.json)
    优势:对命令的的操作一目了然,容易使用和他人观看。且script启动方式会从项目的node包中寻找,所以不用npx。
    "scripts": {
+     "bundle": "webpack"
    }
  • webpack和webpack-cli区别在哪?为什么还需要webpack-cli?
  1. webpack-cli作用是使我们能够在命令行中使用webpack这个命令,如果没有安装webpack-cli包,就不能使用webpack和npx命令。作用是使得我们能够使用命令行运行。

二、loader

  • loader是什么?
    一个打包的方案。webpack不知道的模块(文件)则求助loader。
    是webpack打包时使用的类似做好的配置文件包或者模块。
    例如webpack默认打包的文件为.js文件,不包括.jpg文件。那么可以在webpack.config.js里面配置打包的各种规则,规则指定打包遇到未知后缀时再规则下使用指定.后缀文件所使用的包。
    module:{
        rules:[{
            test:/\.jpg$/,
            use:{
                loader:'file-loader'
            }
        }]
    },

loader:(1)将静态文件移动到配置文件指定的目录和重命名(2)将名字放入打包后的.js文件的静态文件所指的变量名中。

  • 既然知道loader为打包专用配置文件模块或包,那么怎么根据自己的需求去找到某个loader以打包某后缀的文件?
    (1)去webpack官网的loader里面查找
    (2)例如:.vue文件如何找到对应的loader。vue-loader
    (3)查看对应的文档:https://www.webpackjs.com/loaders/file-loader/
    module:{
        rules:[{
            test:/\.vue$/,
            use:{
                loader:'vue-loader'
            }
        }]
    },
  • loader配置,例如静态文件的打包好的名字配置等
    module:{
        rules:[{
            /* 如果打包遇到.jpg文件,则去使用某个(例file-loader)loader打包。*/
            /* file-loader处理.jpg打包的流程,将.jpg复制到list文件夹中,名字可自定义或随机,然后打包后的.js文件直接引用该.jpg文件。所以该文件只是将静态文件进行移动和重命名 */
            test:/\.jpg|png|gif$/,
            use:{
                loader:'file-loader',
                options:{
                    //placeholder 占位符:[]
                    //打包后的文件名及后缀不变
                    name:'[name]_[hash].[ext]',
                    //该后缀资源打包目录所在
                    outputPath:'images/'
                }
            }
        }]
    },
  • url-loader可以完成file-loader的一切功能。但是多了个limit配置项,该项能使某静态资源小于limit参数值时,直接转换为base64字符串打包进js文件,避免过多http请求
    1、同时静态文件直接转换成base64的内容,直接将类似图片打包到.js文件中。省了一次HTTP请求。
    不过这样不合理,带来的问题是如果静态资源很大,则JS文件也很大,也就是说很长的时候里页面什么都显示不出来。所以应用只在图片特别小的情况下才使用这种打包方式。
    解决:使用limit参数,分情况打包,如果图片超过该参数则像file-loader一样打包到指定目录下,否则直接加载入.js文件
    module:{
        rules:[{
            /* 如果打包遇到.jpg文件,则去使用某个(例file-loader)loader打包。*/
            /* file-loader处理.jpg打包的流程,将.jpg复制到list文件夹中,名字可自定义或随机,然后打包后的.js文件直接引用该.jpg文件。所以该文件只是将静态文件进行移动和重命名 */
            test:/\.jpg|png|gif$/,
            use:{
                loader:'url-loader',
                options:{
                    //placeholder 占位符:[]
                    //打包后的文件名及后缀不变
                    //name:'[name]_[hash].[ext]',
                    name(mode){
                      if(mode === 'development'){
                          return '[path][name].[ext]'
                      }
                      return '[hash].[ext]'
                    },
                    //该后缀资源打包目录所在
                    outputPath:'images/',
                    //当静态改后缀名文件大小不超过2048字节,即2kb,则直接转换为base64位字符串打包入js文件
                    limit:2048,
                    //如果静态资源例如图片使用了服务端的资源可以在打包时禁止操作静态资源,而只在打包后的js文件中使用路径及文件名,默认是true
                    emitFile:true,
                }
            }
        }]
    },

三、使用loader打包静态资源中样式CSS文件

  • 如何打包.css文件?
    1、设置规则use使用两个loader,分别是style-loader和css-loader
    style-loader:在得到css-loader生成的内容后,style-loader会把内容挂载到页面的head部分

  • 如何打包.sass(包括scss)等文件?
    需要使用scss相关的loader,以解决scss相关的语法做编译。
    1、根据官网,同样设置use使用两个loader,分别是sass-loader和node-sass。use引入时再css的基础上增加sass-loader
    2、use使用loader是有先后顺序的,从下到上,从右到左。
    3、自动添加厂商前缀功能的loader,例如transform会根据浏览器添加前缀,避免样式文件繁重的书写。该loader为postcss-loader。该loader需要进行配置postcss.config.js。该配置文件还使用了autoprefixer插件自动配置postcss.config.js

    -webkit-transform: rotate(315deg);
    -moz-transform: rotate(315deg);
    -ms-transform: rotate(315deg);
    -o-transform: rotate(315deg);
    transform: rotate(315deg);
    npm install -D autoprefixer
    //postcss.config.js
    module.exports = {
    plugins: [
        require('autoprefixer')({
            "browsers": [
                "defaults",
                "not ie < 11",
                "last 2 versions",
                "> 1%",
                "iOS 7",
                "last 3 iOS versions"
            ]
        })
    ]
    }

        {
            test:/\.scss$/,
            use:[
                'style-loader',
                'css-loader',
                'sass-loader',
                'postcss-loader'
            ]
        }

四、CSS的模块化

引入css文件时容易引起样式冲突问题,引入css模块化的概念。
即import style from ‘index.scss’,使用变量名style.x的形式使用

use: [
    'style-loader',
    {
        loader: 'css-loader',
        options: {
            //无论js还是scss找那个引入scss文件,都会从下到上依次执行多2个loader
            //importLoaders,例如sass文件中再引用sass文件,那么为了让该文件再次从postcss-loader开始往上执行,需要设置该参数表名引入前需要走x个Loader
            importLoaders: 2,
            //如果不是模块化CSS则去掉
            //css模块化参数,使得css文件不是全局作用,不会相互影响,同时css的引用发送也需要做变更。
            //modules:true
        }
    },
    'sass-loader',
    'postcss-loader'
]

五、使用webpack打包字体文件

iconfont字体库使用流程
https://www.iconfont.cn/collections/index?spm=a313x.7781069.1998910419.da2e3581b&type=1
1、创建项目,添加购物车,下载
2、将字体文件拉到项目src目录的font文件下
3、将下载的css文件的代码复制到项目的css文件中
4、引入iconfont + icon-changjingguanli(字体名)
5、webpack安装file-loader,书写对应.后缀的规则。或者使用url-loader直接打包入js文件

,{
            test:/\.(eot|ttf|svg|woff)$/,
            use:{
                loader:'file-loader',
            }
        }

6、打包

六、plugins插件

  • 例如html-webpack-plugin插件:
    https://www.webpackjs.com/plugins/html-webpack-plugin/
    每次打包dist目录下都没有index.html,都需要创建。而使用该插件将在打包结束时在dist目录下生成一个HTML5文件,并把打包生成的js自动引入到Html文件。其中包括使用script标签的body中的所有webpack包。
    1、在webpack配置文件中引入插件
    2、实例化插件,并配置模板html
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html"
    })],
  • 例如第三方插件:clean-webpack-plugin插件
    每次打包时都会生成根据webpack配置的名字的文件,那么之前打包的其他名字文件就会因为不重合而没有被覆盖。则使用该插件再打包前对多余文件进行删除。
    1、在webpack配置文件中引入插件
    2、实例化插件
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    plugins: [
        //打包完成前删除多余文件
        new CleanWebpackPlugin(),
    ],

七、多js文件输入输出打包

    //入口文件,打包多文件,需要对应[name]输出
    entry: {
        main: './src/index.js',
        sub: './src/index.js'
    },
    //输出目录及文件名,__dirname当前目录。[name]占位符,对应这entry的输入文件.publicPath为前面目录或者链接名
    output: {
        //publicPath: "http://cdn.com.cn",
        filename: '[name].[hash].js',
        path: path.resolve(__dirname, 'dist')
    },

八、SourceMap的配置

https://www.webpackjs.com/configuration/devtool/
sourceMap是一个映射关系。例如dist目录下main.js文件96行出错,它知道dist目录下main.js文件96实际对应的是src目录下index.js文件中的第一行
当前其实是index.js中第一行代码出错了
1、默认是sourceMap。devtool:"none"则为打包后的代码

    //devtool指定输出目录,none指定为打包后映射目录,而sourceMap则是打包前的映射目录,inline-source-map的map文件会变成base64放入js文件中,inline精确到行和列。cheap-inline-source-map,cheap只精确到行/cheap-inline-source-map。cheap-module-inline-source-map,module,还显示第三方包的错误代码。eval直接在打包后的js文件后输出错误信息。
    //线上推荐
    //devtool: "cheap-module-source-map",
    //线下选择推荐
    //devtool: "cheap-module-eval-source-map",
    //入口文件,打包多文件,需要对应[name]输出

九、使用webpackDevServer提高开发效率

webpack --watch模式
webpack会监听打包的文件,只要打包的文件发生变化则会重新打包。
webpackDevServer:比–watch好在不但会监听文件的更新打包,还会刷新浏览器。同时普通的webpack没有服务器开启方式,url为文件目录。无法发起ajax请求,webpackDevServer则为服务器连接,可以发起ajax。同时可以配置代理,防止前后台交互跨域问题。
类似react的脚手架,大部分情况下脚手架使用的就是该服务器。
1、安装npm install -D webpack-dev-server
2、配置

    devServer: {
        //webpackDevServer服务器启在哪个文件夹下
        contentBase:'./dist',
        //自动打开浏览器和服务器地址
        open:true,
        //代理
        proxy: {
            //请求到/api/users现在会被代理请求到http://localhost:3000/api/users
            "/api": "http://localhost:3000"
        },
        port:8080,
    },

十、配置个类似webpack-dev-server服务器

  1. 安装express开发框架 和webpack开发中间件
    npm install express webpack-dev-middleware -D
    2、配置script脚本启动
    3、服务器编写,以下功能和webpack --watch一样
"scripts": {
    "server": "node server.js"
  },

server.js
const express = require('express');
//引入webpack库
const webpack = require('webpack');
//webpack中间件
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
//webpack的编译器,complier使用webpack结合配置文件,可以随时进行代码的编译。complier编译器
const complier = webpack(config)

//创建一个服务器
const app = express();
//使用编译器,只要文件发生改变,就会重新运行。文件打包的路径替换webpack.config.js配置。
app.use(webpackDevMiddleware(complier,{
    publicPath:config.output.publicPath
}))

//指定端口和回调函数,启动成功时执行
app.listen(3000, () => {
    console.log('server is running');
});

十一、热模块替换

webpack-dev-server打包后的文件并不会放到dist目录下,而是放到电脑内存中。

应用场景:例如当只变更了css样式时,整个页面都刷新了。那么可以通过热模块替换只刷新css文件即可。

1、配置devserver

devServer: {
        //webpackDevServer服务器启在哪个文件夹下
        contentBase:path.join(__dirname,'dist'),
        //自动打开浏览器和服务器地址
        open:true,
        //代理
        proxy: {
            //请求到/api/users现在会被代理请求到http://localhost:3000/api/users
            "/api": "http://localhost:3000"
        },
        port:8080,
        //开启热模块替换功能
        hot:true,
        //即使热模块替换功能没有生效,也不让页面刷新
        hotOnly:true
    },

2、引入和启用webpack的热模块替换插件

    const webpack = require('webpack')
    new webpack.HotModuleReplacementPlugin()

3、重启devserver

应用场景2:js文件只改变了一部分,但是所有js模块都刷新了,那么可以通过热模块替换只刷新部分js文件。

在应用场景1的情况下,在index.js加多段文件变更判断代码(例如css-loader底层内置了下面这段代码,所以不用手写)。

if (module.hot) {
    //开启了HRM,支持hot加载。如果文件发生了变化,就会执行下面的函数
    //1、依赖文件的名字2、执行后面的函数
    module.hot.accept('./number', () => {
        document.body.removeChild(document.getElementById('number'))
        number()
    })
}

十二、babel

之所以谷歌浏览器能识别ES6的语法是因为谷歌浏览器与时俱进。Babel将ES6的语法转换为ES5,使得浏览器能够识别。

  • 如果是js文件则使用babel-loader与babel进行通信的桥梁,exclude排除了node_modules库中的js文件。babel-loader并不会ES6的语法转换为ES5,需要使用其他入@babel/preset-env -save-dev进行语法转换。
  1. 安装babel-loader通信配置包和核心库
    npm install -D babel-loader @babel/core
  2. 配置babel-loader与babel通信的桥梁
  3. 安装preset-env,该模块包含所有ES6转换成ES5的翻译规则。npm install -D @babel/preset-env
  4. 配置babel翻译工具preset-env
  • 但是有些低版本的浏览器,就算转换到ES5依然不够。还需要把缺少的变量或者函数进行补充。
  1. 安装polyfill
    npm install -D @babel/polyfill
    6、在业务代码的最顶部引入polyfill
    在babel配置中配置了useBuiltIns: ‘usage’,那么在业务代码就不需要引入polyfill。
    import “@babel/polyfill”;
  • 但是直接引入该库会使得最后打包的js文件变的很大,例如原先二十多kb直接变成八百多kb。
{
            //如果是js文件则使用babel-loader与babel进行通信的桥梁,exclude排除了node_modules库中的js文件。babel-loader并不会ES6的语法转换为ES5,需要使用其他入@babel/preset-env -save-dev进行语法转换
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options:{
                //配置babel的ES6转ES5的翻译工具preset-env
                presets:[["@babel/preset-env",{
                    targets: {
                        //对于大于67谷歌浏览器版本则不需要进行ES6的语义转换和语法补充。
                        chrome: "67",
                    },
                    //当做页面做babel的polyfill低版本浏览器不存在的版本特性时(低于ES5),不把所有特性都加入,而是根据业务代码加入特性
                    useBuiltIns:'usage'
                }]]
            }
        }
  • 又但是import polyfill为全局使用,可能会污染到使用的UI组件库
  1. 安装plugin-transform-runtime,该库会以闭包的形式去引入对应的内容,不存在全局污染的状况。
    可创建放入.babelrc文件中
    npm install -D @babel/plugin-transform-runtime
    npm install -D @babel/runtime
     {
        //如果是js文件则使用babel-loader与babel进行通信的桥梁,exclude排除了node_modules库中的js文件。babel-loader并不会ES6的语法转换为ES5,需要使用其他入@babel/preset-env -save-dev进行语法转换
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options:{
            // //配置babel的ES6转ES5的翻译工具preset-env
            // presets:[["@babel/preset-env",{
            //     targets: {
            //         //对于大于67谷歌浏览器版本则不需要进行ES6的语义转换和语法补充。
            //         chrome: "67",
            //     },
            //     //当做页面做babel的polyfill低版本浏览器不存在的版本特性时(低于ES5),不把所有特性都加入,而是根据业务代码加入特性
            //     useBuiltIns:'usage'
            // }]]
            "plugins": [["@babel/plugin-transform-runtime",{
                "absoluteRuntime": false,
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }]]
        }
  • 如何根据编译需求场景寻找适合的babel,或流程回顾
  1. 进入https://www.babeljs.cn/setup,选择使用场景
  2. 按照需求安装,全局直接注入
  3. 简化,按需注入,和根据浏览器版本注入。适合业务代码场景
  4. 闭包按需(corejs2)注入,使用plugin-transform-runtime包。适合写库场景

十三、配置React代码的打包

使用 preset-react解析jsx的语法
npm install --save-dev @babel/preset-react
安装react框架
npm install --save react react-dom

//先将react语法转换为ES6,再替换成ES5。自下向上,自右向左
.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "67"
        },
        "useBuiltIns": "usage"
      }
    ],
    "@babel/preset-react"
  ]
}
import React,{Component} from 'react';
import ReactDom from 'react-dom';

class App extends Component{
    render(){
        return <div>Hello World!</div>
    }
}

ReactDom.render(<App/>, document.getElementById('root'));

十四、Tree Shaking

在babel配置中配置了useBuiltIns: ‘usage’,那么在业务代码就不需要引入polyfill。
babel 绑定一些对象,如window.Promise。没有导出任何内容

当使用{add}引入时其他的方法也会被引入,最理想的应该是只引入要使用的方法。

  • 什么是Tree Sharking?
    引入的每个方法都为一个树节点,顾名思义将不需要的树节点摇晃(不打包)掉。只支持ESModule的引入!
    ESModule底层是静态的引入方式,而CommonJS则是静态引入方式。
    1、查看哪些导出的模块被使用了再做打包
    webpack.config.js
    optimization: {
    usedExports: true
    }
    2、如果配置了Tree Sharking,则打包模块时会使用Tree Shaking方式打包。
    package.json
    //“sideEffects”:["@babel/poly-fill"]//不希望对poll-fill文件进行Tree Shaking
    “sideEffects”:false//但polyfill不需要引入import,所以当前没有不需要Tree Sharking的文件
    “sideEffects:[”*.css"]"//import的css文件没有导出任何内容,Tree Sharking可能会把该文件忽略掉,这样代码也可能有问题
    import ‘./style.css’
    3、当是开发环境模式下只是提醒依然会打包入文件,因为去除未使用的模块会影响source.map的行数
    4、当是生产环境时则Tree Shaking则会生效,且不需要配置1、
    5、devtool: “cheap-module-source-map”,不带eval

十五、develoment和production模式的区分打包

1、development环境的source.map信息非常全,帮助快速定位代码问题
2、production环境的代码一般都是压缩过的

  • 改mode为production
  • devtool:‘cheap-module-source-map’
  • optimization去掉
    这样会产生一个问题,开发和生成环境切换麻烦?
    1、使用两个配置文件
    webpack.dev.js
    webpack.prod.js
    2、pagage.json使用两个script脚本
    3、简化代码,使用webpack.common.js(自定义)取出共同部分
    4、npm install webpack-merge -D,安装合并配置文件合并插件
    5、使用该插件进行合并
    对应配置文件夹下:
    module.exports = merge(commonConfig,prodConfig)
    module.exports = merge(commonConfig,devConfig)
    6、目录变更
  • output目录
  • 插件处理目录

十六、Webpack和Code Splitting

Code Splitting为代码分割,对代码进行拆分,让代码执行的性能更高。一种思想或者说是方法。

手动分割

1、安装loadsh,为功能方法集合
npm install loadsh --save
2、import _ from ‘lodash’,分文件存放,一个是库入口文件,另一个是业务逻辑代码文件

  • 问题:业务代码多次引用使用。例如loadsh 的大小是1mb,业务逻辑是1mb。最后打包的代码是2mb!
    (1)打包文件过大(2)加载时间长(3)如果js文件出现变更又需要加载2mb代码
  • 解决:将引入库文件与业务代码文件分开放。修改业务代码后,浏览器刷新只需要重新加载业务代码。
lodash.js
import _ from 'loadsh';
window._ = _;

3、入口文件增加库文件

entry: {
        main: './src/index.js',
        lodash:'./src/lodash.js'
    },

插件代码分割,splitChunk,该插件已与webpack捆绑。

该打包插件同样将lodash单独提取出来另建JS文件

    optimization:{
        splitChunks:{
            chunks:'all'
        }
    }

同步执行顺序:先引入库,再调用库里面的方法。splitChunks配置参数同步代码做代码分割。
异步执行顺序:例如以下,一开始没有该库代码,通过异步的方法加载,引入的库会放入_变量中,加载完成后then方法会执行。调用时相当于promise直接使用。该方法即使不配置splitChunks也会自动进行代码分割。

  • 异步加载组件有种语法叫魔法注释
    (1)安装官方插件npm install -D @babel/plugin-syntax-dynamic-import
    (2)babel配置 plugins:["@babel/plugin-syntax-dynamic-import"]
    (3)使用魔法注释,给引入的库起一个ChunkName,该name为单独打包生成的文件
function getComponent() {
    return import(/* webpackChunkName:"lodash" */'lodash').then(({default:_})=>{
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell','Lee'],'-')
        return element;
    })
}

getComponent().then(element=>{
    document.body.appendChild(element)
})
  • 如果出现实验性语法错误,我测试后发现不使用该插件反而没错误。
    (1)安装插件
    npm install babel-plugin-dynamic-import
    -webpack
    (2).babelrc配置plugins: [“dynamic-import-webpack”]

splitChunk底层使用的是SplitChunksPlugin插件

无论同步执行的代码分割还是异步执行的代码分割,都需要使用该插件进行配置,即使使用魔法注释,也会被配置所影响。
https://webpack.docschina.org/plugins/split-chunks-plugin/

  • 如果没有进行任何SplitChunksPlugin配置,实际会有一个默认的配置内容,即以下配置。和{}是一样的
optimization:{
    minimize: true,
    splitChunks: {
        chunks: 'all',//all分割同步及异步代码,同时受cacheGroup配置影响。initial同步,async异步
        minSize: 30000,//当文件不小于多少字节时进行代码分割,30kb。为0没反应是因为cacheGroups的走向配置
        maxSize: 0,//可配可不配。对超过该值的js文件进行二次分割,且代码可分割才行
        minChunks: 1,//当一个模块被用了多少次才进行代码分割
        maxAsyncRequests: 5,//同时加载的模块式数最多个数
        maxInitialRequests: 3,//整个网站首页或者入口文件进行加载的时候,入口文件可能会引入其他的JS文件,入口文件引入的文件如果进行做代码分割,最多3个
        automaticNameDelimiter: '~',//cacheGroups组和文件进行文件名连接时中间的连接符
        name: true,//cacheGroups 命名的filename有效true
        cacheGroups: {
            vendors: {//同步加载
                test: /[\\/]node_modules[\\/]/,//如果为该文件目录下则进行分割
                priority: -10,//缓存组优先级别
                filename: "vendors.js"//更改引入分割部分文件名
            },
            default: {//default为所模块都符合该要求,没有test指定。那么就看优先级
                priority: -20,
                reuseExistingChunk: true,//A和B在第一层被引用,但同时第二次B模块也引入了A。则两个代码都打包到该组的文件中。但代码引入存在复用则实际只打包一次。
                filename: 'common.js'
            }
        }
    }
}

十七、Lazy Loading懒加载概念

懒加载:并不是webpack的概念,而是ES里面的里的一种实验性质的语法,webpack则是识别这种语法,一种懒加载的实现。

  • 优点:页面加载速度更快。例如访问首页的时候加载了详情页、列表页等。实际可以做代码分割,访问首页时只加载首页。
    页面加载的时候某些模块不加载,只有进行了操作,例如点击事件时才加载并执行代码。
  1. promise写法
function getComponent() {
    return import(/* webpackChunkName:"lodash" */'lodash').then(({default:_})=>{
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell','Lee'],'-')
        return element;
    })
}

/* 只有点击了才会加载文件,然后挂载内容 */
document.addEventListener('click',()=> {
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})
  1. 异步函数写法
async function getComponent() {
    const {default: _} = await import(/* webpackChunkName:"lodash" */'lodash')
    const element = document.createElement('div');
    element.innerHTML = _.join(['Dell', 'Lee'], '-')
    return element;
}

/* 只有点击了才会加载文件,然后挂载内容 */
document.addEventListener('click', () => {
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})

十八、Chunk

打包进行代码分割,生成的每个JS文件都叫做一个chunk。

十九、打包分析、Preloding/Prefetching

  • 打包分析
    “build”: “webpack --profile --json > stats.json --config build/webpack.prod.js”
    1、配置输出描述性内容stats.json
    http://webpack.github.io/analyse/
    2、上传描述json进行分析,bundle analysis

页面缓存的意义是在第一次加载很慢,后面分割了代码缓存了库文件访问就比较快。
但webpack想达到的效果是第一次访问页面加载就是最快的!
1、打包后打开页面,F12打开开发者模式。ctrl+shift+P,搜索show coverage可查看页面利用率
2、例如点击事件追加元素。页面加载的时候并不执行,只有点击时才会执行利用起来。webpack希望交互的代码把代码放到异步加载的模块中去写。
例如下面当点击时才会加载该代码,代码使用率较高

click.js
function handleClick() {
    const element = document.createElement('div');
    element.innerHTML = 'Dell Lee'
    document.body.appendChild(element)

export default handleClick;

index.js
document.addEventListener('click', () => {
    import('./click.js').then((handleClick)=>{
        handleClick()
    })
})

所以同步的代码打包在一起生成vendor.js意义不大。所以splitChunks的chunks默认选项为async选项。

应用场景例子

1.网页登录的登录框,点击登录按钮时才加载这个框。但点击才加载可能会存在反应过慢的问题。

  • Preloding/Prefetching
    在空闲时间下载代码。
    网页加载时已经把主要内容加载完成了,这时带宽是空闲的,可以在此时加载例如登录框的js的代码。
    实现:借助webpack的打包特性实现的,使用magic common语法
网络空闲时才加载,当点击页面的时候还是会加载,但是要加载的文件以及缓存过,1ms加载完。所以说prefetch利用的是浏览器的缓存功能。

document.addEventListener('click', () => {
    import(/* webpackPrefetch: true */'./click.js').then(({default:_})=>{
        _()
    })
})

两者区别:
Prefetching会加载完页面的核心js文件最后在网络空闲时才加载页面
Preloding则是和主JS文件一起加载的
推荐:懒加载+Prefetching。但最重要的是代码利用率上思考

二十、CSS文件分割

MiniCssExtractPlugin插件
需求:打包时将CSS文件单独打包到CSS文件下而不是打包到JS文件中。借助该插件
1、npm install -D mini-css-extract-plugin
2、webpack使用该插件,new一下
3、使用了该插件同时还需使用该插件的loader解析css文件,将styleloader
4、教程是不支持HRM所以只配置了线上CSS文件分割,但现在已经指出HRM了。那么安装教程来做则:
(1)将css和scss文件处理的loader配置剪切到dev和prod配置文件,然后将prod的style.loader换成该插件的loader:MiniCssExtractPlugin.loader

二十一、webpack与浏览器缓存

项目上线后,更改部分JS代码重新打包后,用户浏览器存在缓存问题而没有加载最新的JS文件。
1、将dev和prod环境的output区分打包,dev有热替换所以不需要配置。prod需要给文件名配置个hash值,当文件发送变更,文件名也跟着变更。而文件不变更则hash值也不变

  • 新版本webpack4
output:{
    publicPath: "./",
    chunkFilename: "[name].[contenthash].js",//非入口文件,为入口JS文件异步加载的JS文件。
    filename: "[name].[contenthash].js",
}
  • 老板本可能出现内容不变但是哈希值也变了的情况,则需要配置个runtime
    manifest的原因,该文件内置了包和包之间的关系,可能会发送变化。而配置runtime则将manifest抽离出一个js文件。
 optimization:{
       runtimeChunk:{
           name:'runtime'
       },
   }

二十二、Shimming——垫片

webpack打包过程中常要做代码或打包过程的兼容,如@babel/polyfill。当低版本不存在promise类似变量的问题,则需要借助polyfill的这样的工具,在低版本浏览器上构建类似Promise的变量。这种兼容性不仅仅在浏览器方面上。还有模块方式打包,则不同模块内都需要引入库。
以下两个行为都是Shimming行为,解决webpack实现不了的功能

  • 不同模块内部引用库问题?
    解决:配置webpack内部插件
    plugins: [
        new webpack.ProvidePlugin({
            $:"jquery",
            _join:['lodash','join']
        }),
    ]
  • 借助loader使得每一个模块的this都指向window,而不是本身模块。
  1. npm install -D imports-loader
  2. 配置js使用loader
    module: {
       rules: [
           {
               test: /\.js$/,
               exclude: /node_modules/,
               use: [
                   {
                       loader: 'babel-loader'
                   },
                   {
                       loader: 'imports-loader?this=>window'
                   }
               ],
           },usedExports
       ]
   },

二十三、react脚手架

1、npx create-react-app my-app
2、cd my-app/git init
3、npm run eject暴露webpack配置文件
4、如暴露后npm run start遇到问题删除yarn.lock和node_module,重新安装依赖

或者不暴露
npm install -D react-app-rewired
进行配置覆盖

config-overrides.js
const {override, fixBabelImports, addLessLoader, addDecoratorsLegacy} = require('customize-cra')
const path = require('path')
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')

const addCustomize = () => config => {
   if (process.env.NODE_ENV === 'production') {
       config.devtool = false;
       if (config.plugins) {
           config.plugins.push(new BundleAnalyzerPlugin({
               openAnalyzer: false,
               generateStatsFile: true,
               statsFilename: path.resolve(__dirname, './stats.json'),
               logLevel: 'error'
           }))
       }
       const optimization = config.optimization;
       if (config.entry && config.entry instanceof Array) {
           config.entry = {
               "uc-web":"./src/index.tsx"
           }
           Object.assign(config.output ,{
               path: path.resolve(__dirname, './build'),
               filename: "./static/js/[name].[contenthash].js",
               chunkFilename: "./static/js/[name].[contenthash].js",
           })
       }
       Object.assign(optimization, {
           splitChunks: {
               chunks: 'all',
               automaticNameDelimiter: '~',
               cacheGroups: {
                   vendors: {
                       test: /[\\/]node_modules[\\/]/,
                       priority: -10,
                       name: 'vendors'
                   },
                   default: {
                       name: 'common',
                       priority: -20,
                       reuseExistingChunk: true,
                   },
                   styles: {
                       name: 'styles',
                       test: '/\.css$/',
                       chunks: 'all',
                       enforce: true
                   }
               }
           },
           usedExports: true
       })
   }
   return config
}

module.exports = override(
   fixBabelImports('import', {
           libraryName: 'antd',
           libraryDirectory: 'es',
           style: true
       }, {
           libraryName: '@shopify/polaris',
           libraryDirectory: 'es',
           style: true
       }
   ),
   addLessLoader({
       javascriptEnabled: true,
       modifyVars: {'@primary-color': '#1DA57A'}
   }),
   addDecoratorsLegacy(),
   addCustomize()
)

二十四、库打包

const path = require('path')

module.exports = {
    mode: 'production',
    entry: './src/index.js',
    externals: ["lodash"],//打包过程中如果遇到lodash则忽略不打包
    // externals:{
    //     lodash:{
    //         root:'_',//既不用commonjs或者Esmodule或AMD等。为script标签,必须在页面注入一个命名为_的全局变量
    //         commonjs:'lodash',//如果库在commonjs环境使用库则名字必须是lodash
    //     }
    // },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library',//可通过script标签引入,打包生成全局变量
        libraryTarget: "umd",//不管在commonJS还是AMD或ESModule任何形式都可以使用。umd是通用的意思。或者window、this挂载到this下
    },
}

index.js
import * as math from './math'
import * as string from './string'

export default {math, string}
  1. 注册npm账号,npm adduser …
  2. npm publish …

二十五、PWA(只有上线的代码才需要做PWA的处理)

Progressive Web Application,如果第一次访问服务器访问成功,而后服务器挂掉后但在用户本地有份缓存,所以服务器挂掉后还是能看到原先加载的页面。

  • 模拟后端服务器,http-server。
  • http-server和webpack-dev-server区别?http-server可为模拟后台api接口的http服务器,易破解所以只能做为测试。直接打开打包后的index.html是无法实现路由组件跳转的。
    而dev-server服务器为web静态资源服务器,它将打包好的文件放入内容,实时热更新代码再进行渲染显示,达到快速显示代码修改的效果,提高开发效率。1为静态文件提供服务2自动刷新和热替换
    (1)npm install http-server -D
    (2)
  "scripts": {
    "start": "http-server dist -i",
    }
  • PWA的实现,React脚手架自带了
    (1)安装npm install workbox-webpack-plugin -D
    (2)生成环境下
    new WorkboxPlugin.GenerateSW({
        clientsClaim:true,
        skipWaiting:true,
    })

(3)打包后的文件会多出precache和service-worker的js文件
(4)在入口js文件写多段业务代码

//判断浏览器是否支持serviceWorker
if('serviceWorker' in navigator){
    //支持则使用该功能,service-worker.js由workbox-webpack-plugin实现
    window.addEventListener('load',()=>{
        navigator.serviceWorker.register('/service-worker.js')
            .then(registration=>{
                console.log('service-worker registed');
            }).catch(error =>{
                console.log('service-worker register error');
        })
    })
}

二十六、TypeScript打包配置

(1)安装npm install -D ts-loader typescript @types/lodash,其中lodash方法库,@types/lodash为lodash在ts的类型声明文件,能进行语法提示
类型声明文件搜索:https://microsoft.github.io/TypeSearch/
(2)配置

webpack.config.js
module:{
        rules:[{
            test:/\.tsx?$/,
            use:'ts-loader',
            exclude: /node_modules/
        }]
    },

tsconfig.js
{
  //编译过程中的配置项
  "compilerOptions": {
    "outDir": "./dist",//打包生成的文件放到dist下,不写也可,webpack已配
    "module": "es6",//使用es的模块引入方式
    "target": "es5",//打包成es5格式
    "allowJs": true //允许在typescript中引入js模块
  }
}

(3)使用变化,引入发送变化,同时会typescript类型提醒
import * as _ from ‘lodash’

二十七、使用WebpackDevServer实现请求转发,方便开发过程使用接口

开发环境请求的接口和线上环境请求的接口可能是不一致的
实现:
由绝对http://www.dell-lee.com/react/api/header.json
变相对/react/api/header.json
进行主域名转发,已经接口转发(模拟接口)
https://www.webpackjs.com/configuration/dev-server/#devserver-proxy
https://github.com/chimurai/http-proxy-middleware#options
(1)安装npm i -S axios
(2)配置webpack.config.js中的dev服务器中的proxy代码选项,也可使用转发工具charles fiddler

webpack.config.js
devServer: {
    proxy: {
        //不支持 '/' 根目录转发,触发设置index:'',
        //index:'',
        '/react/api': {
            target: 'http://www.dell-lee.com',
            secure:false,//对https请求转发需设置为false
            pathRewrite:{
                'header.json':'demo.json'
            },
            bypass: function(req, res, proxyOptions) {//如果请求的地址为html地址则直接返回首页index.html
                if (req.headers.accept.indexOf("html") !== -1) {
                    console.log("Skipping proxy for browser request.");
                    return "/index.html";
                    //或者直接返回请求的html
                    //return false
                }
            },
            changeOrigin: true,//防止外部网站爬虫,对Origin进行限制。设置changeOrigin能突破限制
            headers:{//请求头设置
                host:'www.dell-lee.com',
                cookie:'asdf'//模拟登陆等场景
            }
        },
    },
},

(3)使用

import React,{Component} from 'react';
import ReactDom from 'react-dom';
import axios from 'axios'

class App extends Component{

    componentDidMount() {
        axios.get('/react/api/header.json')
            .then((res)=>{
                console.log(res);
            })
    }

    render(){
        return <div>Hello World!</div>
    }
}

ReactDom.render(<App/>, document.getElementById('root'));

二十八、WebpackDevServer解决单页面应用路由问题,只在dev开发环境有效,上线后续后台进行服务器配置转发

(1)安装路由模块 npm i -S react-router-dom
(2)使用react-router-dom的BrowserRouter和Route

import React,{Component} from 'react';
import {BrowserRouter,Route} from 'react-router-dom';
import ReactDom from 'react-dom';
import Home from './home';
import List from './list'

class App extends Component{
    render(){
        return (
            <BrowserRouter>
                {/*exact精准路由*/}
                <Route path='/' exact component={Home}/>
                <Route path='/list' component={List}/>
            </BrowserRouter>
        )
    }
}

ReactDom.render(<App/>, document.getElementById('root'));

(3)配置webpack.config.js的proxy增加选项,使之任意访问的404页面都会访问index.html。类似后台无html则进行前台查询,而react的router模块又根据url加载指定的component,所以有时为空。
historyApiFallback:true

二十九、EsLint在Webpack中的配置

EsLint是一种代码规范,为约束工具。

  • 方法一
    (1)安装npm i -D eslint
    (2)生成eslint配置文件,npx eslint --lint,根据需求选择
    (3)安装babel-eslint,npm i -D babel-eslint。在.eslintrc.js中增加"parser":“babel-eslint”
    (4)npx eslint src。会具体显示不符合eslint规范的语句error

  • 方法二
    (1)IDE(webstorm、VScode)安装eslint规范的插件——eslint

  • 方法三
    (1)安装npm i -D eslint-loader eslint
    (2)配置loader,在webpack配置的js的转化规则,运行dev服务器后会在控制台显示不符合eslint规则的代码

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: [
        {
            loader: 'babel-loader'
        },
        {
            loader: 'eslint-loader'
        }
    ],
},

(3)在dev-server增加选项overlay:true,之后运行服务器后会在浏览器弹出error项。

三十、提升Webpack的打包速度的方法

  1. 跟上技术的迭代(Node,Npm,Yarn)
  2. 在尽可能少的模块应用Loader。入转化JS文件时,打包exclude node_module文件目录或者include。
  3. Plugin尽可能精简并确保可靠。如开发环境不需要使用插件对代码进行压缩,针对环境进行插件使用。还有一些性能好、官方推荐的等。
  4. resolve参数合理配置。该合理是指不乱配置。child比较常用类似目录别名,避免过长引用,类似路由概念。
resolve: {//当引入其他目录下的模块后会先找js结尾的文件再找jsx目录下的文件,然后引入时可以省略后缀
    extensions: ['.js','.jsx'],
    mainFiles:['index'],//引入文件夹默认引用文件
    alias:{
        //目录引入时当名为key时实为对应值目录
        child:path.resolve(__dirname,'./src/a/b/c/child')
    }
},
  1. 使用DllPlugin提高打包速度。第一次打包时已经把一些不会变化的库文件进行打包,后续理想是直接使用而不是再次打包库文件。即第三方模块(react、jquery)只打包一次,二次第三方模块无变更则再无需打包。
    (1)配置专门打包库文件的配置文件webpack.dll.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
    mode:'production',
    entry: {
        vendors:['react','react-dom','lodash']
    },
    output:{
        filename: "[name].dll.js",
        path:path.resolve(__dirname,'../dll'),
        library: '[name]'//通过全局变量暴露出来
    },
    plugins:[
        //用此插件分析库,把库中第三方模块的映射关系放到[name].manifest.json文件
        new webpack.DllPlugin({
            name:'[name]',
            path:path.resolve(__dirname,'../dll/[name].manifest.json'),
        })
    ]
}

(2)package.json增加库文件打包script
“build:dll”: “webpack --config build/webpack.dll.js”
(3)安装add-asset-html-webpack-plugin插件,作用是index.html中增加dll打包后的文件引用

webpack.common.js的plugins
new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll/vender.dll.js')
})

(4)在打包第三方库文件时使用webpack自带的DllPlugin插件生成映射关系的文件,而使用的webpack.common.js使用该映射文件,如果映射文件中存在业务代码中引用的库时则直接调用该dll打包后的库代码,否则打包node_module中的用到的库。

webpack.dll.js在第(3)步
webpack.common.js
//一旦使用的内容为该分析文件映射关系中的内容则不再引入模块,直接使用了。该分析文件由webpack.dll.js配置插件DllPlugin打包生成。它的底层会到全局变量中去拿,而webpack.dll.js中配置了library选项
new webpack.DllReferencePlugin({
    manifest:path.resolve(__dirname,'../dll/vendors.manifest.json')
})

(5)打包第三方库文件时对库文件进行分割,例如lodash和react&react-dom分为两个文件。但每次第三方分割又生成映射文件又使用映射文件都需要重新new AddAssetHtmlWebpackPlugin和webpack.DllReferencePlugin。所以使用了nodejs的fs文件系统,把文件作为数组进行查看再打印。

webpack.dll.js
entry: {
        vendors:['lodash'],
        react:['react','react-dom']
    },
webpack.common.js
plugins取出外放
const webpack = require('webpack')
const fs = require('fs');

const plugins = [
    new HtmlWebpackPlugin({
        template: "src/index.html"
    }),
    new CleanWebpackPlugin()
];

//node的fs文件系统下的readdirSync,该方法会将dll文件下的内容放入files变量作为数组成员
const files = fs.readdirSync(path.resolve(__dirname,'../dll'))
files.forEach(file =>{
    if(/.*\.dll.js/.test(file)){
        plugins.push(
            new AddAssetHtmlWebpackPlugin({
                filepath:path.resolve(__dirname,'../dll/',file)
            })
        )
    }
    if(/.*\.manifest.json/.test(file)){
        plugins.push(
            new webpack.DllReferencePlugin({
                manifest:path.resolve(__dirname,'../dll/',file)
            })
        )
    }
})
modlue.exports = {
    plugins:plugins
}
  1. 控制包文件大小
    (1)treeshaking防止引入却没有使用的模块进行打包
    (2)splitChunks代码分割?。。

  2. thread-loader,parallel-webpack,happypack多进程打包
    可以用到node的多进程及多个cpu进行打包
    8.合理适用sourceMap
    sourceMap越详细打包的业就越慢

  3. 结合stats分析打包结果

  4. 开发环境内存编译
    dev-server打包会放在内存中

  5. 开发环境无用插件剔除
    如调试阶段不需要压缩代码

三十一、多页面打包配置

多页面:多个.html文件
(1)多个入口js文件
(2)多页面webpack.config.js配置

将配置放到configs中,再根据通过函数对其中的某项(plugins)进行修改,再返回去。再module.exports = configs
const makePlugins = (configs) => {
    const plugins = [
        new CleanWebpackPlugin()
    ]

    Object.keys(configs.entry).forEach(item => {
        plugins.push(
            new HtmlWebpackPlugin({
                filename: `${item}.html`,//生成文件名
                template: `src/index.html`,//模板
                chunks: ['runtime', 'vendors', item]//引入的js文件
            })
        )
    })

    //node的fs文件系统下的readdirSync,该方法会将dll文件下的内容放入files变量作为数组成员
    const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
    files.forEach(file => {
        if (/.*\.dll.js/.test(file)) {
            plugins.push(
                new AddAssetHtmlWebpackPlugin({
                    filepath: path.resolve(__dirname, '../dll/', file)
                })
            )
        }
        if (/.*\.manifest.json/.test(file)) {
            plugins.push(
                new webpack.DllReferencePlugin({
                    manifest: path.resolve(__dirname, '../dll/', file)
                })
            )
        }
    })
    return plugins;
}
const configs = {
    entry: {
        index: './src/index.js',
        list: './src/list.js',
        detail: './src/detail.js'
    },
    。。
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章