webpack 4 实战学习笔记记录

安装webpack

npm init 帮助我们以node规范的方式创建一个项目或者创建一个node的包文件,最终的结果就是在项目目录下生成一个package.json文件。

npm init / npm init -y (自动生成默项)
npm install webpack webpack-cli --save-dev

运行webpack

npx webpack index.js // 用webpack翻译index.js文件

注意:

  1. npx 和 npm 的区别,其实很简单,npx会在目录下的node_modules下面找webpack,没有就安装一个。npm 则先去全局,然后再去当前目录下的node_modules找webpack,没有就不找了;
  2. devDependencies 和 dependencies的区别,devDependencies 指的是你本地开发环境下的依赖(比如webpack,babel以及一系列的loader等);dependencies 指的是你生产环境下打包所需要的依赖(比如你项目中用到了lodash,那么你就应该配置到这个下边)

webpack环境搭建

webpack是基于nodejs开发的模块打包工具。提升webpack打包速度,一定要安装最新稳定版本。 (1) node版本要使用最新稳定版;(2)webpack要使用最新版本;

webpack的安装方式

全局安装: npm install webpack webpack-cli -g; (安装时,如果npm安装很慢,建议使用手机分享热点,就不会有该问题了) webpack-cli@3.1.2 webpack@4.26.0 全局安装特定版本:npm install webpack@4.26.0 webpack-cli -g;

不推荐使用webpack全局安装;卸载全局安装的webpack npm uninstall webpack webpack-cli -g 项目中安装: npm install webpack webpack-cli --save-dev 或者npm install webpack webpack-cli -D 其中–save-dev等价于-D;

安装特定版本: npm install webpack@4.26.0 webpack-cli@3.1.2 --save-dev 此时无法运行webpack命令,如果要运行webpakc命令,需要执行npx webpack -v, 输出webpack版本 npm info webpack or npm view webpack versions 当不知道某个版本号是否存在时,查看webpack的所有版本号

webpack的配置文件

创建默认配置文件 --- webpack.config.js,默认文件名必须是wepack.config.js;若要修改默认文件名,可用以下命令:

npx webpack --config  webpackconfig.js // 修改默认配置文件名为 webpackconfig.js


// webpack.config.js
const path = require('path');
module.exports = {
    mode:'production', // 开发环境: development,bundle.js中代码不会被压缩
    entry : './src/index.js',
     /* entry :  {
        main: './src/index.js'
    },*/
    output : {
       filename:'bundle.js',
       path:path.resolve(__dirname,'dist') // 绝对路径:打包后的文件放在什么目录下
    }
}

配置打包命令时,可用npm script 替代 npx webpack,需在package.json文件中进行配置,如下,运行npm run bundle即可

scripts:{
    "bundle" : "webpack"
}

webpack-cli的作用就是使得我们可以使用 npx webpack 或 webpack 的命令。webpack默认的打包的模式是production。

什么是Loader

webpack 是什么? 模块是什么?(js,html, css,图片,其他文件) webpack的配置文件的作用是什么? loader 本质是一个打包的方案,它知道对于某一个特定的文件,webpack应该怎样打包,因为webpack无法识别非.js结尾的模块,loader在module中配置。安装file-loader : npm install file-loader --save-dev。file-loader能够处理任何静态资源的文件,不止图片,例如excel,word , ppt,txt等。 file-loader底层的原理是:第一步,将文件移动到dist目录下;第二步:把该文件的地址返回给变量。

module:{
        // rules 是一个对象数组
        rules:[{ 
            test:/\.(jpg|png|gif)$/,
            use:{
                loader:'file-loader',
                options:{
                    // placeholder 占位符语法
                    name:'[name]_[hash].[ext]', // 图片文件以原来的名字输出
                    outputPath:'https://jerrychane.oss-cn-beijing.aliyuncs.com/images/'
                }
            }
        }]
    },

url-loader 除了能做 file-loader 做的事情,还能做额外的事情,url-loader会将图片转换成一个base64的字符串,安装 url-loader :npm install url-loader --save-dev; 当图片非常小时,

module:{
        rules:[{
            test:/\.(jpg|png|gif)$/,
            use:{
                loader:'url-loader',
                options:{
                    // placeholder 占位符语法,需要加引号,为字符串
                    name:'[name]_[hash].[ext]', // 图片文件以原来的名字输出
                    outputPath:'https://jerrychane.oss-cn-beijing.aliyuncs.com/images/',
                    limit:2048 // 大于limit,生成图片文件; 小于limit,生成base64字符串
                }
            }
        }]
    },

Vue Loader 是什么

Vue Loader 是一个 webpack 的 loader,它允许你以一种单文件组件的格式撰写 Vue 组件:

<template>
    <div class="example">
        { { msg }}
    </div>
</template>
<script>
    export default {
        data() {
            return {
                msg:'Hello Vue!'
            }
        }
    }
</script>
<style>
    .example {
        color:red;
    }
</style>

使用 loader 打包静态资源(样式篇一)

为了使webpack能够识别css文件,需要引入 style-loader , css-loader; 其中css-loader可以帮助我们识别几个.css文件之间的关系,最终帮我们合并成一段css;style-loader 是在得到 css-loader生成的内容之后,style-loader会将这段内容挂载在head部分,生成<style>...</style> ; 若要识别 .scss文件,还要引入 sass-loader。 安装loader:

npm install style-loader css-loader --save-dev

在webpack.config.js中配置:

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

安装sass-loader:

npm install sass-loader node-sass --save-dev

问题描述:执行上述命令后,执行 npm run bundle , 项目报错:TypeError: this.getResolve is not a function 错误原因:安装 sass-loader 的版本为最新版本,由于版本过高,而导致编译出错; 解决方法:进入npm官方网站,搜索sass-loader,查看发布的版本号,选择一年前或两年前的低版本。我选择的是7.3.0版本,继续执行安装命令:

npm install sass-loader@7.3.0 node-sass --save-dev

在webpack中loader是有先后顺序的,按照从下到上,从右到左的执行顺序。例如以上文件中,首先通过sass-loader对sass文件进行翻译,翻译成css代码以后,给到css-loader,然后都处理好,通过style-loader挂载到页面上。 在写css3新特性时,一般要加浏览器厂商前缀,可以通过 postcss-loader ,自动添加各个浏览器厂商的前缀。首先安装 postcss-loader ,然后创建 postcss.config.js 文件,在该文件中写入配置规则。

npm i -D postcss-loader // 安装 post-loader


npm install autoprefixer -D // 安装 autoprefixer plugin

在postcss.config.js中配置:

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

遇到的坑:

//scss中写的代码
body {
    .avatar {
        width: 150px;
        height: 150px;
        transform: translate(100px,100px);
    }
}

结果在编译至css代码时,并未加上-webkit-前缀,需要在package.json中加入browserslist,其他地方无需任何修改,代码如下:

"browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],

使用 loader 打包静态资源(样式篇二)

css-loader中常用的配置项:

use: [
    'style-loader', 
    {
        loader:'css-loader',
        options:{ 
            importLoaders:2, // 以import引入的.scss文件,也要走postcss-loader和scss-loader的2个loader
             modules:true, // css文件的模块化打包,区分不同css的作用域
        }
    }, 
    'sass-loader',
    'postcss-loader'
]

在使用时,引入时 import style from ‘./index.scss’;

import avatar from "./avatar.jpg";
import style from './index.scss';
import createAvatar from './createAvatar';
// 调用createAvatar()函数
createAvtar();

var img = new Image();
img.src = avatar;
img.classList.add(style.avatar);// 只控制当前页面中img的样式,不影响createAvatar()中的样式

var root = document.getElementById('root');
root.append(img);

使用 file-loader打包字体文件:

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

使用plugins让打包更便捷

安装 html-webpack-plugin:

npm install html-webpack-plugin --save-dev // html-webpack-plugin@3.2.0

在 webpack.config.js 中的配置 plugins 数组:

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

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },

  plugins: [new HtmlWebpackPlugin({ // 实例化HtmlWebpackPlugin,在打包之后运行
      template:'src/index.html' // dist目录中index.html的模板文件
  })]
};

HtmlwebpackPlugin的作用,会在打包结束后,自动生成一个html文件,并把打包生成的js自动注入到这个html文件中。 webpack 中的 plugin 可以在webpack运行到某个时刻的时候(例如,htmlwebpackPlugin在打包结束的时刻),帮你做一些事情,类似于 vue 和 react 中的生命周期函数。 安装 clean-webpack-plugin,第三方的plugin:

npm install clean-webpack-plugin --save-dev // clean-webpack-plugin@1.0.0

在webpack.config.js中的配置:

var CleanWepackPlugin = require('clean-webpack-plugin');
var path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new CleanWepackPlugin(['dist'])]// 实例化CleanWebpackPlugin,在打包前运行
};

clean-webpack-plugin的作用是,在打包前删除上一次打包生成的文件。对比html-webpack-plugin和clean-webpack-plugin: 前者是在打包后执行(可通过控制台查看顺序);后者是在打包前执行。

Entry 与 Output 的基本配置

entry的入口文件可以写成一个字符串,表示打包文件的路径;output默认生成的文件为main.js。如果要将同一个文件,打包生成两个或多个文件,需要使用占位符( [name] / [hash] )。与此同时,会将打包生成的文件,放在template:’src/index.html’文件中。

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js',
        sub:'./src/index.js'
    },
    output: {
        filename: '[name].js', // name的值为entry中对象的key值,即main.js 和 sub.js
        path: path.resolve(__dirname, 'dist')
    }
}

在打包生成的文件前加入统一的地址,如引入CDN地址,需要在output中加上publicPath

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js',
        sub:'./src/index.js'
    },
    output: {
        publicPath:'http://cdn.jerrychane.com',
        filename: '[name].js', // name的值为entry中对象的key值,即main.js 和 sub.js
        path: path.resolve(__dirname, 'dist')
    }
}

SouceMap的配置

在开发者模式(mode:’development’),默认sourceMap已经配置,如果要关掉soucemap,可设置 devtool:’none’; souceMap本质上是一个映射关系,可以映射出打包后文件的源代码所在源文件所在的位置。 开启这种映射关系,可设置devtool:’source-map’,在打包时,会在dist目录下生成*.js.map的文件;若要去掉dist目录下的*.js.map文件可设置 devtool:’inline-source-map’, 将*.js.map里内容直接放在打包文件的底部,变成base64位的一段代码;

devtools:'inline-source-map'; // 可以精确到哪一行的哪一列的记录,这样的映射很耗费性能
devtools:'cheap-inline-source-map'; // 可以精确到哪一行的记录,不用告诉哪一列,这样可以提高性能,只映射业务代码,不会管第三方的loader
devtools:'cheap-module-inline-source-map';// 映射业务代码和第三方的loader
devtool:'eval';// 通过eval执行js代码,执行效率最快,性能最好,但复杂代码慎用

最佳实践:在开发环境(mode: ‘devlopment’)中使用sourceMap,最好使用devtool:'cheap-module-eval-source-map';在生产环境(线上环境mode: ‘production’)建议使用devtool:'cheap-module-source-map'。 cheap 可以提升打包速度,eval是打包速度最快,性能最好的方式, 原因是 eval 是通过eval的执行形式来生成sourceMap的映射关系。

使用WebpackDevServer提升开发效率

实现代码即时更新有三种方法: 第一种,在package.json中添加watch监听配置:

"scripts": {
    "watch": "webpack --watch", // 运行npm run watch,监听源文件代码的修改,实现自动打包,但无法实现自动打开浏览并刷新的特性
  },

第二种,在webpack.config.js中添加devServer配置(推荐使用): 首先要在当前项目中安装webpack-dev-server,安装命令如下:

npm install webpack-dev-server -D // 安装webpack-dev-server@3.1.10


# webpack.config.js
devServer:{
        contentBase:'./dist', // web服务器的地址即根路径下的dist目录,默认是localhost:8080
        open:true, // 自动打开浏览器,自动访问服务器地址
        port:8081,
        proxy: {
                 '/api': 'http://localhost:3000' // 配置代理地址
        }
    },
# package.json
 "scripts": {
    "start": "webpack-dev-server"
  },     

webpack-dev-server不仅可以监听到源文件代码的修改,实现自动打包,而且可以自动打开浏览器,并自动刷新浏览器,从而提升我们代码开发的效率。 通过file的形式打开html页面,就没办法去发ajax请求了,因为发ajax请求必须dist/index.html必须在一个服务器上,通过http协议的方式打开。 在devServer中可以添加proxy项,用来配置跨域代理地址,进行接口模拟,从而解决跨域问题;也可以配置端口号port。 扩充知识:可以使用express实现devServer创建本地http服务器的功能,需要安装expess和webpack-dev-middleware。

npm install express -D // express@4.16.4
npm install webpeck-dev-middleware -D // webpack-dev-middleware@3.4.0


# server.js
const expess = require('express');// 创建http服务器
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');// webpack中间件,监听打包
const config = require('./webpack.config.js');
const complier = webpack(config); // webpack编译器

const app = expess();
app.use(webpackDevMiddleware(complier,{
    publicPath:config.output.publicPath
}))

app.listen(3000,()=>{
    console.log('server is running');
})

运行webpack方式:第一种为在命令行中运行webpack index.js; 第二种是在node中运行,如node server.js。

Hot Module Replacement 热模块替换

webpack-dev-server在运行npm run start时,并不会生成dist目录并存放打包生成后的文件,而是将打包生成后的文件放在内存中,可以有效地提升打包的速度,让我们的开发更快。 HRM只替换页面中css的改变,而不改变页面中原有的js渲染的内容,方便我们调试css样式。修改webpack配置文件后,最好重启一下命令。

devServer:{
        contentBase:'./dist',
        open:true,
        port:8080,
        hot:true, // 开启HotModuleReplacement功能
        hotOnly:true // 即使html未生效,浏览器也不自动刷新
    },


const webpack = require('webpack');// webpack.config.js头部引入webpack
plugins:[
        new webpack.HotModuleReplacementPlugin()
],

当在一段代码中引入其他的模块的时候,如果你希望这个模块发生了变化,只去更新这个模块的代码,就要用的HRM技术了。需在webpack.config.js的devServer中配置hot和hotOnly项,在plugins中引入 HotModuleReplacementPlugin插件。

// 要实现局部刷新需要使用到accept方法,接收两个参数,第一个是要变更的模块,第二个是变更的回调函数
if (module.hot) {
  module.hot.accept("./number", () => {
    document.body.removeChild( document.getElementById('number'))  
    number();
  })
}

使用Babel处理ES6语法

当使用npx webpack命令做打包,可以查看打包生成的main.js文件;而使用npm run start,用webpack-dev-server做打包,打包生成的文件都在内存中,无法查看main.js。 如果main.js中能够将ES6打包生成的语法转换成为ES5的语法,就能够兼容所有的浏览器,此时就需要借助Babel来实现,可访问其官方网站https://babeljs.io , 在setup - webpack 目录下找到其在webpack的详细配置。 babel-loader只是babel与webpack之间的一座桥梁,用来连接webpack,并不对ES6语法进行翻译。@babel/core是babel的一个核心库,能够让babel去识别js代码里的内容,把js代码转换成ast抽象语法树,最后将语法树编译成新的js语法。

# 安装babel-loader和@babel/core
npm install --save-dev babel-loader @babel/core // babel-loader@8.0.4 @babel/core@7.2.0


# webpack.config.js
module: {
  rules: [
    { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader",
      options:{
        presets:["@babel/preset-env"]
    } 
    }
  ]
}

preset-env是真正将ES6转换成ES5语法的模块,里面包含了所有ES6翻译到ES5的语法翻译规则。

npm install --save-dev @babel/preset-env // @babel/preset-env@7.2.0

babel-polyfill配置

只有babel-loader和preset-env没办法解决变量或函数(例如:promise、map方法)在低版本浏览器中的识别,需要借助babel-ployfill,补充实现变量或函数在低版本的浏览器中被识别。

npm install --save @babel/polyfill@7.0.0 // @babel/polyfill@7.0.0


#/src/index.js
import "@babel/polyfill";  // 使用@babel/polyfill


# webpack.config.js 配置
module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', 
        options:{
        presets:[
            ['@babel/preset-env', {
              targets: {
                edge: "17",
                firefox: "60",
                chrome: "67",// 在chrome版本低于67时进行babel的翻译
                safari: "11.1",
              },
              useBuiltIns:'usage' // 根据业务代码,按需加入ES6代码翻译
                                }
            ]
        ]
    }},
}        

chrome: “67”表示在chrome版本低于67时进行babel的翻译,如果浏览器版本支持从ES6语法,就会忽略转译成ES5。polyfill的相关参数,可以去官网查看相应的配置。 在编写业务代码时,如果需要用到babel,可以参考上面的配置方案。但是,这个方案并非所有场景都能使用。

babel-plugin-transform-runtime

在开发一个类库、第三模块、或者组件库,如果用babel-pollyfill方案,会出现问题。因为pollyfill在注入类似Promise,map方法时,会通过全局变量的方式来注入,会污染全局环境。 安装 plugin-transform-runtime(参考: https://babeljs.io/docs/en/babel-plugin-transform-runtime ) :

npm install --save-dev @babel/plugin-transform-runtime //@7.2.0

此外还要安装 runtime 模块:

npm install --save @babel/runtime //@7.2.0


# webpack.config.js配置
module: {
        rules: [
        { 
            test: /\.js$/, 
            exclude: /node_modules/, 
            loader: "babel-loader",
            options:{
                "plugins": [["@babel/plugin-transform-runtime",{
                    "corejs": 2,
                    "helpers": true,
                    "regenerator": true,
                    "useESModules": false,
                }]]
            }
        },
       ......      

如果webpack.config.js中配置了”corejs”:2,则还需要安装一个依赖包 , 否则会报promise模块找不到的错误,然后运行 npx webpack 打包,就不会有任何问题了:

npm install --save @babel/runtime-corejs2 //@7.2.0

babel的配置项会非常多,webpack.config.js中options的内容会非常长,可以在项目根目录下创建.babelrc文件,将options里的内容添加到.babelrc中。

#.babelrc
"plugins": [
    ["@babel/plugin-transform-runtime",{
                    "corejs": 2,
                    "helpers": true,
                    "regenerator": true,
                    "useESModules": false,
                }]
            ]

Webpack 实现对React框架代码的打包

配置 React 代码的打包

参考url: https://babeljs.io/docs/en/7.1.0/babel-preset-react 可以通过 @babel/preset-react 来解析 react 中的 jsx 语法,首先需要安装 react&react dom;

npm install react react-dom --save //  react@16.6.3 react-dom@16.6.3 

想要使用webpack结合babel打包react的代码,还需要安装@babel/preset-react:

npm install --save-dev @babel/preset-react  // @babel/preset-react@7.0.0

同时.babelrc里也需要做相应的配置,.babelrc里文件的执行顺序是从下到上,从右到左。先把react里的代码进行转换,然后把转换后代码中的ES6代码部分进行转换为ES5。

{
    presets:[
                    ["@babel/preset-env",{
                       targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                                 },
                       useBuiltIns:"usage"
                                        }
                    ],
                    "@babel/preset-react"
            ]
}

Tree Shaking

如果在webpack.config.js配置了babel-loader相关的内容,同时.babelrc里加入了”useBuiltIns”,那么在业务代码中就不需要引入polyfill。

// import "@babel/polyfill"; 无需引入

Tree Shaking 的意思是把一个模块中没用的东西都摇晃掉,一个模块可以理解为一颗树。只引入我们需要的部分,不需要的部分通过 Tree Shaking帮我们摇晃掉,剔除掉。在webpack中,要实现Tree Shaking 我们应该怎么做呢? 首先Tree Shaking只支持ES Module的引入,即 import方式的引入,使用cont add = require(…)等CommonJS的方式引入,Tree Shaking的方式是不支持的。这是因为import这种ES Module的引入,底层是一个静态引入方式,而CommonJS底层是动态引入的方式,所以Tree Shaking 只支持静态引入方式。

Tree Shakingの配置

mode:”development”下默认是没有Tree Shakin功能的,可以在webpack.config.js的plugins下面添加一个optimization的对象;同时在package.json中添加 “sideEffects”:fale,意思是tree shaking 对所有的模块进行tree shaking,没有特殊处理的部分。

# webpack.config.js
optimization:{
        usedExports:true
            },


# package.json
{
  "name": "lesson2",
  "sideEffects":false,
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server"
  },
    ......

在mode:’development’环境下,tree shaking 并不会通过减少代码的方式,来处理多余模块,只是会做一个提示。 如果要排除某些文件不做tree shaking,可在package.json中添加sideEffects:[‘*.css’],没有需要排除的直接写 “sideEffects”:false。 在mode:’production’环境下,webpack已经拥有一些自动的tree shaking配置,可以注释掉webpack.config.js中的optimization配置。但是,package.json中的sideEffects还是需要写的。

Development 和 Production 模式的区分打包

在开发环境下使用development,一旦在开发环境开发完代码,需要部署上线时,就要用的production模式。development 和 production 的差异主要在几个方面:

  1. 在开发环境中,sourcemap是非常完整的;在生产环境中,sourcemap是非常简洁的;
  2. 在开发环境中,代码一般不需要压缩;在生产环境中,代码一般是被压缩过的代码;

为了区分development和production的配置文件,可以分别创建webpack.dev.js 和 webpack.prod.js文件,在package.json的scripts做如下配置:

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

build一般指打包上线后的代码,dev是开发时的代码。当运行 npm run dev时,如果不想手动刷新浏览器页面,自动刷新,可以将webpack.dev.js中的hotOnly:true去掉,改过webpack的配置文件后,一定要重启服务器。 如果要上线,则运行npm run build,会产生一个dist目录,将dist目录下的文件上传至服务器,就可以完成产品的上线。 当webpack.dev.js和webpack.prod.js中,有大量相同的配置代码,可以在根目录下创建webpack.common.js,然后将webpack.dev.js和webpack.prod.js中共用的代码,提取到webpack.common.js中。 最后要将webpack.common.js和webpack.dev.js的配置做合并,再输出,此时,需要引入第三方的模块,叫做webpack-merge。

npm install webpack-merge -D // webpack-merge@4.1.5

在webpack.dev.js中引入webpack-merge

const merge = require('webpack-merge');

此时这三个文件的配置如下:

# webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
    entry: {
        main: './src/index.js',
    },
    module: {
        rules: [{
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {
                    // presets:[
                    //  ["@babel/preset-env",{
                    //     targets: {
                    //          edge: "17",
                    //          firefox: "60",
                    //          chrome: "67",
                    //          safari: "11.1",
                    //      },
                    //     useBuiltIns:'usage' // 根据业务代码,按需加入ES6代码翻译
                    //  }]
                    // ]
                }
            },
            {
                test: /\.(jpg|png|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        name: '[name]_[hash].[ext]',
                        outputPath: 'https://jerrychane.oss-cn-beijing.aliyuncs.com/images/',
                        limit: 10240
                    }
                }
            }, {
                test: /\.(eot|ttf|svg)$/,
                use: {
                    loader: 'file-loader'
                }
            }, {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            }, {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(['dist']),
        // new webpack.HotModuleReplacementPlugin()
    ],
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
}


# webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
    mode: 'production',
    // devtool: 'cheap-module-eval-source-map',
    devtool: 'cheap-module-source-map',
    // devServer: {
    //     contentBase: './dist',
    //     open:true,
    //     port:8080,
    //     hot:true,
    //     hotOnly:true
    // },

    // optimization:{
    //  usedExports:true
    // },
}

module.exports = merge(commonConfig, prodConfig);


# webpack.dev.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    // devtool: 'cheap-module-source-map',
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        // hotOnly:true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization: {
        usedExports: true
    },

}
module.exports = merge(commonConfig, devConfig);

可以将webpack.dev.js,webpack.prod.js,webpack.common.js文件统一放在一个文件夹中,例如build文件夹,才是需要修改package.json。

# package.json
"scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
  },

Webpakc和Code Spliting

Code Spliting 的意思是代码分割,代码与webpack之间的关系是什么呢? 为了说明两者的关系,需要在开发环境下,通过打包文件来查看,此时在package.json中可以创建一个新的脚本命令, dev-build:

# package.json
"scripts": {
    "dev-build":"webpack --config ./build/webpack.dev.js",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  },

此时会在build目录下生成一个dist目录,需要在webpack.common.js中修改output配置中dist目录的位置,同时要修改clean-webpack-plugin中的目录。

plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new CleanWebpackPlugin(['dist'],{
            root: path.resolve(__dirname, '../') // 防止在dist目录下生成未参与打包的文件
        }),
        // new webpack.HotModuleReplacementPlugin()
    ],
output: {
        filename: '[name].js',
        path: path.resolve(__dirname, '../dist')
    }

webpack的配置和插件巨多,要想完全掌握是不可能的,需要分析在打包的流程中,一步一步地分析具体哪一个流程出了问题,找到问题,截取出来到搜索引擎或论坛上寻找解决问题。 接下来引入本地的正题,Code Spliting与Webpack之间具体是什么关系呢? 首先安装一个第三方的包lodash,它是一个功能集合,提供了很多功能和方法,可以让我们高性能地去使用一些,比如说字符串拼接的函数等等。

npm install lodash --save  // lodash@4.17.11

在src/index.js中写入一些代码,然后 npm run dev-build打包,在浏览器中运行dist目录下的index.html:

import _ from 'lodash'; // 同步打包
console.log(_.join(['a','b','c'],'***'));
// 此处省略10万行业务逻辑;
console.log(_.join(['a','b','c'],'***'));

当业务逻辑代码较多时,带来的问题是:打包文件会很大,加载时间会很长;当修改业务代码时,需要重新打包,用户需要重新加载main.js,才能获取业务代码。 第一种方式,首次访问页面时,加载main.js(2mb),当业务逻辑发生变化时,又要加载2mb的内容;第二种方式,main.js被拆成了 lodash.js(1mb),main.js(1mb),当业务逻辑发生变化时,只需要加载main.js(1mb)即可。 在一些项目之中,通过对代码公用部分进行拆分,来提升项目的运行速度,提升性能和用户体验。Code Spliting 本质上与 Webpack没有任何关系,只是webpack中有很多插件能够很方便的实现Code Spliting也就是代码分割的功能。在Webpack4中,split-chunks-plugin与webpack做了捆绑,不用安装,直接可以用。 在webpack.common.js中可以配置如下:

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

还可以通过另外一种异步引入方式,进行webpack中的代码分割,不用配置webpack.common.js。

npm install babel-plugin-dynamic-import-webpack -D //@1.1.0


# src/index.js
function getComponent() {
    return import('lodash').then(({default:_}) => { // 异步打包
        var element = document.createElement('div');
        element.innerHTML = _.join(['Jerry','Chane'],'_');
        return element;
    })
}

getComponent().then(element => {
    document.body.appendChild(element);
})


# .babelrc
{
    presets:[
                    ["@babel/preset-env",{
                       targets: {
                            edge: "17",
                            firefox: "60",
                            chrome: "67",
                            safari: "11.1",
                                 },
                       useBuiltIns:"usage"
                                        }
                    ],
                    "@babel/preset-react"
            ],
            plugins:["babel-plugin-dynamic-import-webpack"]
}

运行npm run dev-build,会发现lodash.js代码会被打包到dist/0.js目录下。 总结:代码分割,和webpack无关;webpack中实现代码分割,有两种方式,同步方式只需要在webpack.common.js中做optimization的配置,异步代码,无需做任何配置,会自动进行代码分割,放置到新的文件中。

SplitChunksPlugin 配置参数详解

在CodeSpliting中,打包产出的文件0.js需要修改为一个可以识别的名字,改怎么办呢?在异步加载的代码中,有一种注释代码叫做Magic Comment,首先要移除package.json中的插件”babel-plugin-dynamic-import-webpack”: “^1.1.0”, .babelrc中也去掉 plugins:[“babel-plugin-dynamic-import-webpack”] ,改为 “plugins”: [“@babel/plugin-syntax-dynamic-import”] ,而index.js文件中代码如下:

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


# src/index.js
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);
})

其中注释webpackChunkName:”lodash”,就是npm run dev-build后生成的vendor-lodash.js文件。如果要使生成的文件名字为lodash.js,前面不带vendor,则需要对webpack.common.js 进行一下配置。

# webpack.common.js
optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendors:false,
                default:false
            }
        }
    },

无论是同步代码的分割还是异步代码的分割,都需要splitChunks,最开始可以将splitChunks设置为一个空对象,照样可以进行代码的分割。因为splitChunks默认的配置项里,已经将所有的配置都补全完整了。

# webpack.common.js
splitChunks: {
      chunks: 'async', // 只对异步代码生效
      minSize: 30000, // 引入模块大于30000个字节即30kb,就做代码分割
      maxSize: 0,
      minChunks: 1, // 模块被引入1次就分割打包
      maxAsyncRequests: 6, 
      maxInitialRequests: 4,
      automaticNameDelimiter: '~',
      name:true,
      cacheGroups: { // 缓存组
            vendors: false,
            default: false
            }
        }

这里会有一个坑,官方文档中有minRemainingSize: 0,这一项,在打包的时候会无法识别,需要将这一项注释掉。 当设置chunks:’all’ 后还无法实现代码分割,此时需要对cacheGroups进行如下配置:

cacheGroups: {
    vendors: {
        test: /[\\/]node_modules[\\/]/,// node_modules引入模块进行分割
            priority: -10,
            filename:'vendors.js'
    },
        default: false
            }

当对非node_modules中的模块进行打包时,就要用到cacheGroups中的default配置:

 cacheGroups: {
     vendors: {
         test: /[\\/]node_modules[\\/]/,
             priority: -10,
             filename:'vendors.js'
     },
         default: {
             priority: -20,
             reuseExistingChunk: true,
             filename:'common.js'
         }
            }

在打包同步代码时,除了走cacheGroups即缓存分组上面的配置,也会走cacheGroups里的配置,并根据priority的大小,有限走priority大的分组配置。

Lazy Loading 懒加载,Chunk是什么

懒加载是通过import异步加载一个模块,这个概念并不是webpack中的,而是ES中的Promise类型。在代码中如果要使用这种import,必须要引入babel-polyfill或promise-polyfill等等,因为在低版本浏览器下,很有可能不支持Promise的语法。

function getComponent() {
    return import(/*webpackChunkName:"lodash"*/ 'loadsh').then(({default:_})=>{
        // 逻辑||业务代码
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell', 'Lee'], '_');
        return element;
    })
}
document.addEventListener('clikc',()=>{
    getComponent(element).then(()=>{
        document.body.appendChild(element);
    })
})

在ES7中引入了异步函数的概念,上面的代码可以改造如下:

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是什么?在webpack打包后,dist目录下生成的一个js文件,就叫做一个chunk。在webpack.common.js中splitChunks,一般配置一个chunks:’all’即可,其他均为默认配置。

optimization: {// loadsh.js
        splitChunks: {
            chunks: 'all', //对同步和异步代码都进行代码分割,默认async
            // minSize: 30000,
            // minRemainingSize: 0,
            // maxSize: 50000,// 50kb,lodash 1mb
            // minChunks: 2,
            // maxAsyncRequests: 6,
            // maxInitialRequests: 4,
            // automaticNameDelimiter: '~',
            // name:true,
            // cacheGroups: {
            //     vendors: false,
            //     default: false
            }
        }
    },

打包分析:Preloading&Prefetching

打包分析指的是当我们使用webpack进行代码的打包之后,可以借助打包分析的一些工具,对打包生成的文件一定的分析,然后来看打包是否合理。 首先需要访问webpack打包分析的一个github仓库,执行命令npm run dev-build生成stats.json文件。

# https://github.com/webpack/analyse
"scripts": {
"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js",
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},

stats.json中是对整个打包文件的一个描述性的文件,将该文件上传至http://webpack.github.com/analyse,可以看可视化的分析。开发人员使用较多的是插件webpack-bundle-analyzer。 当页面需要在第一次加载时,提升js代码的使用率。查看代码使用率的方法,在控制台输入ctrl + shilf + p,输入coverage, 点击Show Coverage。webpack官方推荐,多写一些异步的代码,才能使代码的性能真正的得到提升,这也是webpack为什么默认的配置项里splitChunks的chunks:’async’。 Preloading&Prefetching 在网络带宽空闲的时候,预先将click.js文件加载好。webpackPrefetch会等核心的代码加载完成后,页面带框空闲的时候再去加载click.js等需要懒加载的文件;webpackPreload是和核心的代码一起加载。

document.addEventListener('click', () => {
 import(/* webpackPrefetch: true */'./click.js').then(({ default: func }) => {
     fun(); // func指的是handleClick()函数
 });
});


document.addEventListener('click', () => {
 import(/* webpackPreload: true */'./click.js').then(({ default: func }) => {
     fun(); // func指的是handleClick()函数
 });
});

在做前端代码性能优化时,缓存并不是最重要的点,最重要的点应该是code coverage即代码有效使用率上。

CSS 文件的代码分割

chunkFileName

通常会见到在webpack中output项配置了chunkFileName:’[name].chunk.js’,例如在src/index.js中:

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);
    })
})

运行npm run dev-build,可以在dist目录下生成loadash.chunk.js文件。入口文件即entry:{main: ‘./src/index.js’,} 会走filename: ‘[name].js’的配置参数,而间接引入的模块文件,会走chunkFilename:’[name].chunk.js’的配置参数。

MiniCssExtractPlugin

在没有安装MiniCssExtractPlugin插件时,webpack默认会将.css文件打包到js文件中。如果index.js中引入的文件为.css文件,将.css文件单独打包,在dist目录下生成style.css文件,则需要用到MiniCssExtractPlugin插件。

npm install --save-dev mini-css-extract-plugin@0.5.0

由于0.5.0版本只支持线上环境的打包(npm run build),所以在webpack.prod.js中需要引入该插件;

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

同时在webpack.pro.js中进行配置:

plugins: [
    new MiniCssExtractPlugin({
         filename: '[name].css',
         chunkFilename: '[name].chunk.css',
    })],
    module: {
        rules: [{
                test: /\.scss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2
                        }
                    },
                    'sass-loader',
                    'postcss-loader'
                ]
            }, {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader'
                ]
            }],
    },

在package.json中配置:

"sideEffects": [
    "*.css"
  ],
optimize-css-assets-webpack-plugin

该插件可以自动将抽离出来的css代码文件进行合并和压缩。

npm install --save-dev optimize-css-assets-webpack-plugin@5.0.1


# webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
};

Webpack 与浏览器缓存( Caching )

去掉webpack中warning警告,在配置项中添加performance:false。vendor.js中一般会放置第三方的公共模块的代码,即node_modules下的模块,如果需要对vendor.js进行修改,其配置项如下。

// webpack.common.js
splitChunks: {
            chunks: 'all', 
            cacheGroups: {
                vendors: {
                    test:/[\\/]node_modules[\\/]/,
                    priority:-10,
                    name:'vendors',
                },
            }
        }

为了能够使浏览器在开发环境不使用缓存,而在生产环境中使用缓存,需要用到contenthash占位符,在webpack.dev.js和webpack.prod.js中对output项进行如下的配置:

// webpack.dev.js
output:{
        filename: '[name].js',
        chunkFilename:'[name].js',
    }
// webpack.prod.js
output:{
        filename: '[name].[contenthash].js', // 只要不修改文件,hash值永远不变
        chunkFilename:'[name].[contenthash].js',
    }

如果源代码没有做变更,用户可以访问本地缓存的文件;如果源代码发生变更,文件对应的hash值将发生变化,用户必须到服务器上加载新的文件。通过contenthash,用户只需要更新有变化的代码,没有变化的代码,用户可以使用本地的缓存。 main.js中放置的业务逻辑的代码,vendor.Js放置的是第三方的库,vendor.js和main.js中间关联的代码放在manifest.js中,默认manifest存在于main.js中,也存在于vendor.js中。在老版本中,即使没有修改代码,contenthash也会发生改变,这时需要在webpack.common.js中配置runtime, 会生成runtime.js,将main.js和vendor.js之间的关系,存放在runtime.js中。

// webpack.common.js
optimization: {// loadsh.js
        runtimeChunk:{
            name:'runtime'
        }
        usedExports: true,
        splitChunks: {
            chunks: 'all', 
            cacheGroups: {
                vendors: {
                    test:/[\\/]node_modules[\\/]/,
                    priority:-10,
                    name:'vendors',
                },
            }
        }

Shimming(垫片)的作用

能够解决webpack打包过程中的一些兼容性问题,不局限于浏览器的高低版本。

// webpack.common.js
plugins: [
        new webpack.ProvidePlugin({
            $:'jquery',
            _:'lodash',
            _join:['lodash','join']
        }),
    ]

在index.js中打印this,发现this永远指向这个模块自身,而非window全局变量。如果想要每一个js中的this都指向window全局变量,需要安装一个imports-loader,实现this指向window全局变量。

npm install imports-loader@0.8.0 --save-dev


// webpack.common.js
rules: [{
                test: /\.js$/,
                exclude: /node_modules/,
                use:[{
                    loader:'babel-loader'
                },{
                    loader:'imports-loader?this=>window'
                }],
            },

环境变量的使用方法

在package.json中传递env环境变量,给它一个属性为production,默认值为true。

"scripts": {
    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.common.js",
    "dev": "webpack-dev-server --config ./build/webpack.common.js",
    "build": "webpack --env.production --config ./build/webpack.common.js"
  },

在webpack.common.js中引入devConfig和prodConfig模块以及merge,当在开发环境时走devConfig的配置文件,在生产环境则走prodConfig的配置。

// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const commonConfig = {
    ....
}
module.exports = (env) => {
    if (env && env.production) {
        return merge(commonConfig, prodConfig);
    } else {
        return merge(commonConfig, devConfig);
    }
}

Webpack实战配置案例

Library的打包

npm install webpack@4.27.1 webpack-cli@3.1.2 --save

创建文件夹library,创建文件math.js,string.js.index.js,webpack.config.js文件,代码如下:

// math.js
export function add(a, b) {
    return a + b;
}

export function minus(a, b) {
    return a - b;
}

export function multiply(a, b) {
    return a * b;
}

export function division(a, b) {
    return a / b;
}

// string.js
export function join(a,b) {
    return a + '' + b;
}

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

export default { math, string };

//webpack.config.js
const path = require('path');
module.exports = {
    mode:'production',
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'library.js'
    }
}

如果要使我们的库文件,以多种形式都能被引用。则需要在output多加一个参数libraryTarget:’umd’,表示以任何形式引入,该库都能引用被引用到,支持的语法格式如下。

import library from 'library';
const library = require('library');
require(['library'],function(){});

如果需要支持<script sr='library.js'></script>,并通过library.math方法,则需要在output配置参数library:’library’。

module.exports = {
    mode:'production',
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'library.js',
        library:'library',
        libraryTarget:'umd'  // value => this,window,global
    }
}

如果将上述libraryTarget中的‘umd’修改成‘this’,则表示将library挂载到全局的this中即this指向library,library将不再支持import和require的方式引入了。

npm install lodash@1.0.0 --save

在webpakc.config.js中加入参数externals:[“lodash”],表示遇到lodash时,忽略该库,不要把它打包进代码中;

module.exports = {
    mode:'production',
    entry:'./src/index.js',
    externals:["lodash"],// {lodash:{commonjs:'lodash'}}
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'library.js',
        library:'library',
        libraryTarget:'this'
    }
}

如何将库文件发布到npm官网上呢?首先修改package.json中main的配置参数为”main”: “./dist/library.js”,在命令行中运行npm adduser,填写完账号密码后,运行npm publish,就能将我们打包好的库文件发送到npm的官网。别人如果要使用这个库文件,直接npm install就可以了。库的名字(在npm中是独一无二的)就是package.json中name的值。

PWA的打包

PWA的全称是Progressive Web Application。模拟后端服务器,需要安装一个http-server服务器。

npm install http-server@0.11.1 --save-dev

在package.json中修改script,目的是能够让dist目录下的文件,通过npm start在http-server下启动起来。

"scripts": {
    "start":"http-server dist",
    "dev": "webpack-dev-server --config ./build/webpack.common.js",
    "build": "webpack --env.production --config ./build/webpack.common.js"
  },

在浏览器中访问 http://localhost:8080/index.html 即可访问dist目录下的文件。pwa可以实现一个什么样的效果呢?第一次访问某个网站,访问成功了,突然之间服务器挂掉了;第二次再重新这个网站时,可以利用本地的缓存,直接将之前访问的网站再展示出来。这样即使服务器挂掉了,在本地还是能够看到之前访问的页面。 要想实现pwa的功能,可以利用第三方(Google封装)的模块workbox-webpack-plugin。

npm install workbox-webpack-plugin@3.6.3 --save-dev

只有线上的代码才需要考虑pwa的技术,本地开发的代码无需pwa,需要在webpack.prod.js进行配置。

const WorkboxPlugin = require('workbox-webpack-plugin');
...
plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[name].chunk.css',
        }),
        new WorkboxPlugin.GenerateSW({ // 注意GenerateSW的大小写
            clientsClaim: true,
            skipWaiting: true
        }),
    ],

npm run build后会在dist目录下生成precache-manifest.js和service -worker.js文件,通过这两个文件就能使service worker生效,使我们的项目支持pwa的功能。service worker可以理解为一种另类的缓存。

// src/index.js
console.log('hello,this is jerrychane');
if('serviceWorker' in navigator) {
    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 打包配置

TypeScript可以规范我们的代码风格,也可以方便的提示错误。首先初始化项目,然后安装webpack。

npm install webpack@4.28.3 webpack-cli@3.2.0 --save-dev

TypeScript官方推荐的loader是ts-loader,需要进行依赖环境包安装,同时在根目录下创建ts的配置参数。

npm install ts-loader@5.3.2 typescript@3.2.2 --save-dev
touch tsconfig.json

tsconfig.json的配置项,可以参考TypeScript官网提供的配置,这里只写几个常用的配置项。

{
    "compilerOptions":
    {
        "outDir": "./dist",
        "module": "es6",
        "target": "es5",
        "allowJs": true // 允许tsx中引入js的模块
    }
}

TypeScript中引入lodash等第三方的库文件以及对应的types文件,对错误参数进行校验,以避免我们调用的错误。

npm install lodash@4.17.11 --save-dev
npm install @types/lodash@4.14.119 --save-dev
npm install @types/jquery --save-dev
npm install @types/vue --save-dev

使用WebpackDevServer实现请求转发

使用2-11课程目录的文件,运行npm run start,如果页面没有显示hello world,则需要到浏览器console控制台,找到application,取消掉在浏览器中注册的service worker。

npm install axios@0.18.0  --save-dev

在进行axions请求路径配置时,一般会使用相对路径,而不会使用绝路径。

// webpack.config.js
devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        hotOnly: true,
        proxy:{
            '/react/api':'http://www.dell-lee.com',// 当请求'/react/api'开头的接口时,都会转发到'http://www.dell-lee.com'的服务器
        }
    },

当请求’/react/api’开头的接口,如果暂时无法拿到header.json,可以通过配置pathRewrite间接地拿到demo.json的数据。其配置如下:

proxy: {
            '/react/api': {
                target: 'http://www.dell-lee.com',
                pathRewrite: {
                    'header.json': 'demo.json'
                }
            },
        }

proxy只是方便我们在开发环境中做线上接口的转发,在线上环境即生产环境中不会进行转发,因为没有devServer,线上接口的地址,应该在初次写代码时就要写好。 此外,如果请求的地址是https的接口,还要在target下面配置一个secure:false,这样才能对https的接口进行请求转发。

WebpackDevServer解决单页面应用路由问题

npm install react-router-dom@4.3.1 --save-dev  // 安装路由的依赖包

需要在webpack.config.js的devServer中配置:historyApiFallback: true, 保证路由不进行跳转,src/index.js中路由如下:

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

class App extends Component {
    render() {
        return (
            <BrowserRouter>
                <div>
                <Route path='/' exact component={Home}/>
                <Route path='/list' component={List}/>
                </div>
            </BrowserRouter>
        )
    }
}

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

Eslint在Webpack中的配置(一)

Eslint是用来约束代码编写规范的工具,具体怎么约束,可以进行自定义配置。(该方法较复杂)

npm install eslint@5.12.0 --save-dev
npx eslint --init // 初始化生成.eslintrc.js文件
npm install babel-eslint@10.0.1 --save-dev
npx eslint src

Eslint在Webpack中的配置(二)

借助eslint-loader来完成

npm install eslint-loader@2.1.1 --save-dev

在webpack.config.js中的devServer配置overlay:true和eslint-loader:

devServer: {
        overlay:true, // 将错误信息显示在浏览器中并弹出
        ...
}    
 module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            use:['babel-loader','eslint-loader']
        }, 
          ...
}        

提升webpack打包速度的方法

1.跟上技术的迭代(Node,Npm,Yarn); 如果想提升webpack打包的速度,可以升级webpack、node、npm、yarn等相关工具的版本。 2.在尽可能少的模块上应用Loader; 3.Plugin尽可能精简并确保可靠(webpack官方提供的插件); 4.resolve参数合理配置;

module.exports = {
    entry: {
        main: './src/index.js',
    },
    resolve:{
        extensions:['.js','.jsx'],//查询目录下的以.js和.jsx结尾的文件,节约查找的时间
        mainFiles:['index','child'],//查询目录下的index.jsx或child.jsx,一般不需要配置
        alias:{
            dellee:path.resolve(__dirname,'../src/child/')
        }
    },
    ...
}

5.使用DLLPlugin提高打包速度:对于第三方模块,其代码一般是不会变的,可以将其单独生成一个文件,只在第一次打包的时候,进行打包;第二次进行打包的时候,不再对第三方的模块进行分析打包。

// webpack.dll.js
const path = require('path');

module.exports = {
    mode:'production',
    entry:{
        vendors:['react','react-dom','lodash']
    },
    output:{
        filename:'[name].dll.js',
        path: path.resolve(__dirname, '../dll')
    }
}


npm install add-asset-html-webpack-plugin@3.1.2 --save-dev

安装add-asset-html-webpack-plugin 插件,并对 webpack.common.js 进行配置,AddAssetHtmlWebpackPlugin将第三方库挂载在全局变量上。

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

        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll/vendors.manifest.json'),
        })
    ],

我们引入第三方模块的时候,要去使用dll中的vendors.dll.js文件,需要对vendor.dll.js文件进行映射。

// webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
    ...
    plugins:[
        new webpack.DllPlugin({
            name:'[name]',
            path:path.resolve(__dirname, '../dll/[name].manifest.json'),
        })
    ]
}


// webpack.common.js
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const plugins = [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new CleanWebpackPlugin(['dist'], {
        root: path.resolve(__dirname, '../')
    })
];

const files = fs.readdirSync(path.resolve(__dirname,'../dll'));
console.log(files);
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)
            }))
    }
})
module.exports = {
    entry: {
        main: './src/index.js',
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
    plugins,
    ...
}

6.控制包文件大小 7.thread-loader,parallel-webpack,happypack多进程打包 8.合理使用 sourceMap 9.结合 stats 分析打包结果 10.开发环境内存编译 11.开发环境无用插件剔除

多页面打包配置

// webpack.common.js
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');

const makePlugins = (configs) => {
    const plugins = [
        new CleanWebpackPlugin(['dist'], {
            root: path.resolve(__dirname, '../')
        }),
    ];

    Object.keys(configs.entry).forEach((item) => {
        plugins.push(new HtmlWebpackPlugin({
            template: 'src/index.html',
            filename: `${item}.html`,
            chunks: ['runtime', 'vendors', item]
        }))
    });

    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'
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            use: [{
                loader: 'babel-loader'
            }, {
                loader: 'imports-loader'
            }]
        }, {
            test: /\.(jpg|png|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: 'https://jerrychane.oss-cn-beijing.aliyuncs.com/images/',
                    limit: 10240
                }
            }
        }, {
            test: /\.(eot|ttf|svg)$/,
            use: {
                loader: 'file-loader'
            }
        }]
    },
    optimization: {
        runtimeChunk: {
            name: 'runtime'
        },
        usedExports: true,
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    name: 'vendors',
                }
            }
        }
    },
    performance: false,
    output: {
        path: path.resolve(__dirname, '../dist')
    }
}

configs.plugins = makePlugins(configs);
module.exports = configs;

如何编写一个loader

可以参考 webpack 官网中 Documentation 中的 API

npm install webpack@4.29.0 webpack-cli@3.2.1 --save-dev

loader本质上就是一个函数(非箭头函数,会改变this的指向)

// makeloader/loaders/replaceLoader.js
module.exports = function(source) {
    return source.replace('dell', 'dell-lee');
}

webpakc.config.js怎么用上面的loader呢?

const path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    module: {
        rules: [{
            test: /\.js/,
            use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
        }]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    }
}

此外也可以对loader进行参数配置:

const path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    module: {
        rules: [
        {
            loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
            options:{
                name:'lee'
            }
        }
        ]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    }
}


// makeloader/loaders/replaceLoader.js
module.exports = function(source) {
    console.log(this.query);
    return source.replace('dell', this.query.name);
}


npm intall loader-utils@1.2.3 --save-dev

当使用异步的loader时,可以使用this.callback

// makeloader/loaders/replaceLoader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
    const options = loaderUtils.getOptions(this);
    const callback = this.async();
    setTimeout(() => {
        const result = source.replace('dell', options.name);
        callback(null,result);
    }, 1000);
}


const path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    resolveLoader:{
        modules:['node_modules','./loaders']
    },
    module: {
        rules: [{
            test: /\.js/,
            use: [
            {
                loader: 'replaceLoader'
            },
            {
                loader: 'replaceLoaderAsync',
                options: {
                    name: 'lee'
                }
            }

            ]

        }]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    }
}

如何编写一个Plugin

插件是一个类:

//plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
    constructor(options) {
        console.log(options);
    }

    apply(compiler) {

        compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
            console.log('compiler');
        })

        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
            debugger;
            compilation.assets['copyright.txt']= {
                source: function() {
                    return 'copyright by dell lee'
                },
                size: function() {
                    return 21;
                }
            };
            cb();
        })
    }

}

module.exports = CopyrightWebpackPlugin;

在webpack.config.js中使用插件,new插件的类:

const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [
        new CopyRightWebpackPlugin()
    ],
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    }
}

Bundler 源码编写(模块分析)

读取项目的入口文件,然后去分析项目的代码。

npm install cli-highlight@2.0.0 -g   //高亮显示代码

在运行命令时,后面加上|highlight,便可以看到高亮显示的代码。

node bundler.js | highlight


npm install @babel/parser@7.2.3 --save-dev
npm install @babel/traverse@7.2.3 --save-dev

通过 CreateReactApp 深入学习 Webpack 配置

全局安装create-react-app脚手架工具:

npm install -g create-react-app@3.3.1

初始化react项目my-app

create-react-app my-app

不要使用npx create-react-app my-app 的命令初始化项目,会报以下错误:A template was not provided. This is likely because you’re using an outdated version of create-react-app.Please note that global installs of create-react-app are no longer supported.

npm run eject // 弹射出隐藏的webpakc配置项,会生成config和scripts目录
npm run start // 启动命令
npm run build // 生成线上环境即生产环境的代码

Vue CLI 3 的配置方法及课程总结

安装vue cli 脚手架工具,其中封装了常用的配置项目,具体如何配置可参考 Vue 官网进行配置。

npm install -g @vue/cli

初始化vue项目my-project

vue create my-project

启动vue项目

npm run serve

产出vue项目

npm run build

基础知识学会,然后学会如何查询文档。