详谈webpack4

前言

下载啥的就不多说了,就看看我们项目中经常用到的一些配置。码字不易,喜欢的话点个?哦 ~~~

webpack

webpack只是一个打包模块的机制,只是把依赖的模块转化成可以代表这些包的静态文件。webpack就是识别你的 入口文件。识别你的模块依赖,来打包你的代码。至于你的代码使用的是commonjs还是amd或者es6的import。webpack都会对其进行分析。来获取代码的依赖。webpack做的就是分析代码。转换代码,编译代码,输出代码。webpack本身是一个node的模块,所以webpack.config.js是以commonjs形式书写的(node中的模块化是commonjs规范的) webpack中每个模块有一个唯一的id,是从0开始递增的。整个打包后的bundle.js是一个匿名函数自执行。参数则为一个数组。数组的每一项都为个function。function的内容则为每个模块的内容,并按照require的顺序排列。

clean-webpack-plugin

清空打包文件。new CleanWebpackPlugin(), 可以不传参,配置参考这里

source map

sourcemap就是一个信息文件,里面储存着位置信息。目的是为了解决开发代码与实际运行代码不一致时帮助我们debug到原始开发代码的技术。

  • 配置 devtool 属性 ( 最佳实践 )
  • development: cheap-module-eval-source-map
  • production: cheap-module-source-map
  • 五个关键字任意组合: eval,source-map,cheap,module,inline
  1. eval: 包裹模块,在模块尾添加模块来源 //#sourceURL,通过 sourceURL 找到原始代码位置,不单独产生.map 文件
  2. source-map: 产生.map文件,包含原始代码和运行代码的映射关系
  3. 参考文献

webpack-dev-server

npm i webpack-dev-server -D

  • 注意:打包的文件不会放到dist目录中了,而是放在我们的内存中,从而提升了打包速度。
devServer: {
    // open: true, // 打开浏览器
    // port: 8080, // default
    hot: true, // HMR,不刷新页面就能应用你改过的css样式
    hotOnly: true, // 如果HMR没生效,也不刷新页面
    contentBase: './dist', // 告诉服务器从哪里提供内容。只有在您希望提供静态文件时才需要这样做。
    proxy: {
      '/api': 'http://localhost:3000' // 如果我们访问localhost:8000/api ,则转发请求到localhost:3000
    }
  },
复制代码
  • 通过配置hot: true 和 webpack.HotModuleReplacementPlugin() 可以及时更新css样式而不刷新页面!
  • 如果更变js代码,保证其它代码的状态不发生变化,则需要另外加一段代码,如下:
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     // 检测到print.js中的更改时,我们告诉webpack接受更新后的模块。
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }
复制代码
  • 注意: 为什么改变css代码不需要添加 module.hot.accpet 代码呢? 原因是因为 css-loader 已经帮我们处理过这一步了,js 代码就需要我们自己来添加 HMR 了。在使用 vue 的时候,vue-loader 帮我们实现了 js HMR 这一块了,所以也不用我们自己实现了。react 则是借助了 babel-preset 来帮我们实现了 js HMR.

webpack-dev-middleware

通过webpack-dev-middleware 配合 express 可以自己搭建一个简单的webpack-dev-server,通过node来运行webpack,代码如下:

server.js

const webpack = require('webpack')
const middleware = require('webpack-dev-middleware')
const express = require('express')
const config = require('./webpack.config.js')

// webpack 编译器
const compiler = webpack(config)

const app = express()

app.use(
  middleware(compiler, {
    // webpack-dev-middleware options
    publicPath: config.output.publicPath
  })
)

app.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})
复制代码

使用babel处理ES6

进入官网,打开 setup, 进入webpack,查看相关文档

// npm install --save-dev babel-loader @babel/core
module: {
  rules: [
    {
        test: /\.js$/,
        exclude: /node_modules/, // 不包含
        loader: "babel-loader" // webpack 和 babel 做通信的桥梁
    }
  ]
}

// 还需要配置options 或者 .babelrc 文件
npm install @babel/preset-env --save-dev

{
    test: /\.js$/,
    exclude: /node_modules/, // 不包含
    loader: "babel-loader", // webpack 和 babel 做通信的桥梁
    options: {
        "presets": ["@babel/preset-env"]
    }
}
// 但是,这只是将ES6 转 ES5,有一些语法比如promiss,map等,低版本还是不认识,这就要使用 @babel/polyfill 了
复制代码
  • @babel/polyfill
// 提供polyfill是为了方便,但是您应该将它与@babel/preset-env和useBuiltIns选项一起使用
// 这样它就不会包含并非总是需要的整个polyfill。否则,我们建议您手动导入各个填充。
npm install --save @babel/polyfill
// 然后在代码的顶部引入:
import "@babel/polyfill";
"presets": [["@babel/preset-env"], {
    targets: {
      chrome: "67", // chrome 版本大于67的,就不需要将ES6转ES5了,因为chrome对ES6已经支持的很好了
      "ie": "11"
    },
    useBuildIns: "usage" // useBuiltIns选项,如果设置成"usage",那么将会自动检测语法帮你require你代码中使用到的功能。也不需要额外引入@babel/polyfill 了
}]
复制代码

通过 .babelrc 来声明 参考文档

  • @babel/plugin-transform-runtime

参考文献

  1. 避免多次编译出helper函数:
  2. 这里的 @babel/runtime 包就声明了所有需要用到的帮助函数,而 @babel/plugin-transform-runtime 的作用就是将所有需要helper函数的文件,依赖@babel/runtime包
  3. 解决@babel/polyfill提供的类或者实例方法污染全局作用域的情况。
// npm install --save-dev @babel/plugin-transform-runtime
// npm install --save @babel/runtime-corejs2

.babelrc

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 2, // 代表需要使用corejs的版本; npm install --save @babel/runtime-corejs2
        "helpers": true,
        "regenerator": true,
        "useESModules": false // 按需引入babel/polyfill (注入低版本的polyfill)
      }
    ]
  ]
}
复制代码

webpack打包React

安装包 npm i @babel/preset-env @babel/preset-react -D

tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。不支持 require 的引入方式。这是因为 ES 模块的引入方式是静态的,而require 的引入方式是动态的。

  • 配置
  1. 将 mode 配置选项设置为 development 以确保 bundle 是未压缩版本
mode: "development",
optimization: {
    usedExports: true // 查看那些模块被使用了,使用了的就打包
}

// 还要再 package.json 中配置一个属性

"sideEffects": false

// "side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export"sideEffects": ["@babel/polyfill", "*.css"] // 不对 @babel/polyfill和所有引入的css文件 作tree shaking

复制代码

注意: 在开发环境下,使用tree shaking 其实还是会将没有使用的模块打包进 bundle.js 中,只不过会提醒你那些模块没有使用。如果使用 mode: production; 那我们就不需要使用 optimization 配置项了,并且将设置 devtool: cheap-module-source-map。但是 sideEffects 还是要使用。

区分打包(dev and prod)

npm i webpack-merge -D 用来合并 webpack 模块

  1. webpack.common.js,存放 dev 和 prod 公共的配置
  2. webpack.dev.js, 开发环境的配置
  3. webpack.prod.js,生产环境的配置
  4. 我们可以将以上的webpack配置文件放入到 build 文件夹中统一管理

看看 webpack.prod.js 的用法

webpack.prod.js

const merge = require('webpack-merge')
const devConfig = require('webpack.common.js')

cosnt prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map'
}

module.exports = merge(prodConfig, merge)
复制代码

然后修改 package.json 中的配置

// 启动dev
"dev": "webpack-dev-server --config ./build/webpack.dev.js"

// 启动prod
"build": "webpack --config ./build/webpack.prod.js"
复制代码

注意: 如果我们的webpack配置放在build文件夹中,并且我们的配置中使用 clean-webpack-plugin,那么它的配置也需要发生改变

new CleanWebpackPlugin(['dist']) // 指的是删除当前目录下的 dist 目录,但是我们的 dist 目录需要放在 build 文件夹同级目录下。

// 可以这样做 : 在 github 上搜索 clean-webpack-plugin
new CleanWebpack(['dist'], {
    root: path.resolve(__dirname, '../') // 指定根路径
})
复制代码

Code Splitting (代码分割)

代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常用的代码分离方法有三种:

  1. 入口起点:使用 entry 配置手动地分离代码。(优点:最简单最直观。缺点:对我们的示例来说毫无疑问是个严重问题,因为我们在 ./src/index.js 中也引入过 lodash,这样就造成在两个 bundle 中重复引用。)

  2. 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。

optimization: { // 这里可以配置 code splitting, 还可以配置 tree shaking 时需要的 usedExports
    splitChunks: {
        chunks: 'all'
    }
}
复制代码

将index.js 和 another_module.js 中的 lodash 库抽离出来了。

  1. 动态导入:

通过模块中的内联函数调用来分离代码。 我们不再使用 statically import(静态导入) lodash,而是通过 dynamic import(动态导入) 来分离出一个 chunk。 查看官网demo

环境变量

想要消除 开发环境 和 生产环境 之间的 webpack.config.js 差异,你可能需要环境变量(environment variable)。***参考文档***

  • 配置
  1. 对于我们的 webpack 配置,有一个必须要修改之处。通常,module.exports 指向配置对象。要使用 env 变量,你必须将 module.exports 转换成一个函数:
const path = require('path');

module.exports = env => {
  // Use env. here:
  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
};
复制代码
  1. 既然可以在配置文件中接收到 env ,那么我们就可以将上面的 mode 区分部分重新修改一下,怎么修改呢?如下:
// 在上面的 环境区分中,我们在 webpack.common.js 中引入 devConfig、prodConfig、webpack-merge,然后通过 env 来判断当前的打包命令是开发环境还是生产环境。

module.exports = env => {
    if (env && env.production) {
        return merge(commonConfig, prodConfig);
    } else {
        return merge(commonConfig, devConfig);
    }
}
复制代码

Library 打包

除了打包应用程序,webpack 还可以用于打包 JavaScript library. 参考文档

  1. libraryTarget 还可以赋值为 'this' 或者 'window' | 'global',意味着将 library 挂载到 全局上
  2. 如果我们自己写的库中引入的第三方库,比如lodash.js,但是我们不希望它打包到我们的库中,那么应该怎么办呢?我们需要配置如下参数
// webpack.config.js
externals: ['lodash']
// 不打包lodash,但是别人引入我们的库的时候,就必须引入lodash.js,因为我们的库依赖lodash
复制代码
  • 发布自己的库到 npm 上,给别人使用
  1. 首先配置我们的package.json文件
  2. 到 npm 官网上注册我们的账号
  3. npm adduser (添加用户名和密码)
  4. npm publish (将我们自己的库发布到 npm 仓库上去)
  5. 注意,库的名字一定要特别,不能和npm 仓库上的库同名

渐进式网络应用程序 ( PWA )

我们通过搭建一个简易 server 下,测试下这种离线体验。这里使用 http-server package:npm install http-server --save-dev 参考

{
  "scripts": {
+    "build": "webpack",
+    "start": "http-server dist"
  }
}
复制代码
  • 添加 Workbox
npm install workbox-webpack-plugin --save-dev

const WorkboxPlugin = require('workbox-webpack-plugin');

    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
+       title: '渐进式网络应用程序'
+     }),
+     new WorkboxPlugin.GenerateSW({
+       // 这些选项帮助快速启用 ServiceWorkers
+       // 不允许遗留任何“旧的” ServiceWorkers
+       clientsClaim: true,
+       skipWaiting: true
+     })
    ]
复制代码
  • 注册 Service Worker

接下来我们注册 Service Worker,使其出场并开始表演。通过添加以下注册代码来完成此操作:

  import _ from 'lodash';
  import printMe from './print.js';

+ if ('serviceWorker' in navigator) {
+   window.addEventListener('load', () => {
+     navigator.serviceWorker.register('/service-worker.js').then(registration => {
+       console.log('SW registered: ', registration);
+     }).catch(registrationError => {
+       console.log('SW registration failed: ', registrationError);
+     });
+   });
+ }
复制代码

TypeScript

TypeScript 是 JavaScript 的超集,为其增加了类型系统,可以编译为普通 JavaScript 代码 参考

npm install --save-dev typescript ts-loader
复制代码
  1. 在webpack.config.js中配置
  2. 需要创建一个tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/", // 当前目录下的dist
    "noImplicitAny": true, // 
    "module": "es6", // 使用ES Module 引入方式
    "target": "es5", // 转换成 ES5 的代码
    "jsx": "react",
    "allowJs": true // 允许引入 js 文件
  }
}
复制代码
  1. 使用typescript时我们可能使用外部的库,但是外部的库是无法利用typescript自动检测的特性的。这个时候我们就需要安装这些库的类型文件,比如lodash, 我们如何安装它的自动检测类型文件呢?
npm i @types/lodash -D // 通过 @types/ + 库的名字即可
复制代码

devServer.proxy

// 请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users
devServer: {
    proxy: {
        '/api': {
            target: 'http://localhost:3000',
            pathRewrite: {
                'header.json': 'demo.json' //我们请求header.json的时候,转而去请求demo.json
            },
            secure: false, // 默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,修改配置
            bypass: function(req, res, proxyOptions) { // 拦截,如果请求的是 html 类型的数据,就直接返回 html文件,不进行转发
                if (req.headers.accept.indexOf("html") !== -1) {
                    console.log("Skipping proxy for browser request.");
                    return "/index.html";
                }
            }
        }
    }
}
复制代码

devServer.historyApiFallback

当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html.在做单页面路由的时候,需要配置此选项。

devServer: {
    historyApiFallback: true
}
复制代码

ESlint配置

约束代码规范的工具,团队开发尤其重要

// 安装
npm i eslint -D

// 初始化配置文件
npx eslint --init
复制代码

  1. 使用Airbnb公司的规范,我们需要另外覆盖一些配置

  1. 在webpack中配置 eslint-loader

npm i eslint-loader -D (会降低打包的速度哦 ! 可以在options中配置 cache 属性) 参考官网配置

注意: 在webpack.devServer中配置一个属性:

devServer: {
    overlay: true, // 将报错显示在页面上
}
复制代码

webpack性能提升

  • 升级版本
  1. 比如webpack,node,npm,yarn
  • 尽量少使用 Loader
  1. 在使用 babel-loader编译 js 代码的时候,使用 exclude 或者 include来忽略掉 node_modules下的文件,因为这个文件下的文件都是编译过了的,就没必要再次让 babel-loader 来编译了。
  • 尽量少使用 Plugin
  1. 比如在开发环境中,我们就没必要使用压缩css或者js 代码了。
  • 合理配置resolve
  1. resolve.alias: 创建 import 或 require 的别名,来确保模块引入变得更简单
  2. resolve.extensions: 自动解析确定的扩展。默认值为:extensions: [".js", ".json"], 能够使用户在引入模块时不带扩展:import File from '../path/to/file'.也可以添加 'jsx' 之类的文件
  3. resolve.mainFields: 通过配置这个选项,来引入可以像省略 index.js 这样的文件。比如我们引入import index from './src',其实就是引入的src下的index.js,我们也可以配置其它的文件,比如 main.js,hello.js ...

  • DllPlugin

DLLPlugin 和 DLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度。

  1. 实现的逻辑是,将第三方库只打包一次并打包成一个文件 (也可以打包成多个文件,配置 entry 属性就行了),然后将其缓存起来,之后再做打包的时候,不再去node_modules中寻找了
  2. 创建一个 webpack.dll.js 配置文件

通过 webpack.DllPlugin来产生一个第三方库的源代码文件和一个映射文件(manifest.json) 3. npm i add-asset-html-webpack-plugin -D

将我们打包好的第三方库源代码引入到 HtmlWebpackPlugin 生成的 index.html 中

  1. 使用 webpack.DllRefrencePlugin 来分析代码

这个插件是在 webpack 主配置文件(webpack.common.js)中设置的。它会结合manifest.json、第三方库源码文件以及我们引入的第三方库文件做一个分析,如果它发现我们引入的文件在源码文件里面已经有了,它就直接拿过来用了,而不会node_modules里面找了。

new webpack.DllRefrencePlugin({
    manifest: path.resolve(__dirname, './vendors.manifest.json')
})
复制代码
  1. 拆分成多个第三方库文件

动态的导入多个插件,将 plugins 提取出来,动态push 一些插件进去

  • 控制包文件大小
  1. 还记得 tree shaking 吧?去除冗余代码 ...
  2. splitChunks 来将大文件拆分成多个小文件
  • 多进程打包

webpack 默认是使用 node.js 来运行,即采用的单线程机制打包过程

  1. node里面的多进程 thread-loader, parallel-webpack, happypack
  • 合理使用 sourceMap

描述越详细,就越慢哦 ~

  • 结合 stats 分析打包结果

结合分析,查看哪个模块打包耗时比较长,做针对性处理

  • 开发环境内存编译

采用 webpack-dev-server

  • 开发环境剔除无用插件

比如压缩 css 或者 js 的插件

  1. 这里我们需要配置环境变量来区分打包、

多页面打包配置

本质就是创建多个entry 以及 HtmlWebpackPlugin 来实现的

  1. 比如我们要打包两个js文件,并且通过两个 index.html 分别引入

2. 配置 HtmlWebpackPlugin 插件

3. 如果还有多个 js 文件需要打包,那么我们就可以写一些逻辑来动态创建,就不需要一个个来写 HtmlWebpackPlugin了

如何编写 Loader

其实 loader 就是一些函数(不能使用箭头函数哦,因为this),接受的参数就我们的源代码,通过函数来做一些处理并返回而已。loaderAPI

// myself-loader.js

module.exports = function (source) {
    return something ...
}

// 然后在 module 中配置
module: {
    rules: [
        {
            test: /\.js$/,
            loader: path.resolve(__dirname, './loaders/myself-loader.js')
        }
    ]
}

// 如何传参?
// 配置 options 之后,在我们写的 laoder 里面通过 this.query 就能获取到啦 !!!
{
    test: /\.js$/,
    use: [
        loader: path.resolve(__dirname, './loaders/myself-loader.js'),
        options: {
            name: 'alex.cheng'
        }
    ]
}

// 自己定义的 loader 如何像引入 node_modules 里面 loader 一样引入呢 ?
resolveLoader: {
    modules: ["node_modules", "./loader"] // 如果在node_modules里没找到,就到当前文件loader中去找
}
复制代码

如何编写一个Plugin

我们使用别人的 plugin 的时候都是怎么使用的呢? 是不是都要 new Plugin ? 所以啊,我们的 plugin 都是通过构造函数来编写的。来看一个简单的例子

// plugin: alex-cheng-webpack-plugin.js

class AlexChengWebpackPlugin {
  constructor() {
    console.log('alex.cheng plugin is excuted !')
  }
}
module.exports = AlexChengWebpackPlugin

// webpack.config.js

const path = require('path')
const AlexChengWebpackPlugin = require('./src/myPlugins/alex-cheng-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, './dist/')
  },
  plugins: [
    new AlexChengWebpackPlugin() // 调用就行了啦 !!
  ]
}
复制代码

看!我们自己的插件就执行了咯 ! 详情可以参考 官网API

你可能感兴趣的:(详谈webpack4)