从今天开始,学习Webpack,减少对脚手架的依赖(下)

栏目: JavaScript · 发布时间: 5年前

内容简介:问:这篇文章适合哪些人?答:适合没接触过Webpack或者了解不全面的人。问:这篇文章的目录怎么安排的?

问:这篇文章适合哪些人?

答:适合没接触过Webpack或者了解不全面的人。

问:这篇文章的目录怎么安排的?

答:先介绍背景,由背景引入Webpack的概念,进一步介绍Webpack基础、核心和一些常用配置案例、优化手段,Webpack的plugin和loader确实非常多,短短2w多字还只是覆盖其中一小部分。

问:这篇文章的出处?

答:此篇文章知识来自付费视频(链接在文章末尾),文章由自己独立撰写,已获得讲师授权并首发于掘金

如果你觉得写的不错,请给我点一个star,原博客地址:原文地址

PWA配置

PWA全称 Progressive Web Application (渐进式应用框架),它能让我们主动缓存文件,这样用户离线后依然能够使用我们缓存的文件打开网页,而不至于让页面挂掉,实现这种技术需要安装 workbox-webpack-plugin 插件。

如果你的谷歌浏览器还没有开启支持PWA,请开启它再进行下面的测试。

安装插件

$ npm install workbox-webpack-plugin -D
复制代码

webpack.config.js文件配置

// PWA只有在线上环境才有效,所以需要在webpack.prod.js文件中进行配置
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const prodConfig = {
  // 其它配置
  plugins: [
    new MiniCssExtractPlugin({}),
    new WorkboxWebpackPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}
module.exports = merge(commonConfig, prodConfig);
复制代码

以上配置完毕后,让我们使用 npm run build 打包看一看生成了哪些文件, dist 目录的打包结果如下:

|-- dist
|   |-- index.html
|   |-- main.f28cbac9bec3756acdbe.js
|   |-- main.f28cbac9bec3756acdbe.js.map
|   |-- precache-manifest.ea54096f38009609a46058419fc7009b.js
|   |-- service-worker.js
复制代码

我们可以代码块高亮的部分,多出来了 precache-manifest.xxxxx.js 文件和 service-worker.js ,就是这两个文件能让我们实现PWA。

改写index.js

需要判断浏览器是否支持PWA,支持的时候我们才进行注册,注册的 .js 文件为我们打包后的 service-worker.js 文件。

console.log('hello,world');
if('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js').then((register) => {
    console.log('注册成功');
  }).catch(error => {
    console.log('注册失败');
  })
}
复制代码

PWA实际效果

npm run dev 后,我们利用 webpack-dev-server 启动了一个小型的服务器,然后我们停掉这个服务器,刷新页面,PWA的实际结果如下图所示

从今天开始,学习Webpack,减少对脚手架的依赖(下)

WebpackDevServer请求转发

在这一小节中,我们要学到的技能有:

  • 如何进行接口代理配置
  • 如何使用接口路径重写
  • 其他常见配置的介绍

假设我们现在有这样一个需求:我有一个URL地址( http://www.dell-lee.com/react/api/header.json ),我希望我请求的时候,请求的地址是 /react/api/header.json ,能有一个什么东西能自动帮我把请求转发到 http://www.dell-lee.com 域名下,那么这个问题该如何解决呢?可以使用 Webpack 的 webpack-dev-server 这个插件来解决,其中需要配置 proxy 属性。

如何进行接口代理配置

既然我们要做请求,那么安装 axios 来发请求再合适不过了,使用如下命令安装 axios :

$ npm install axios --save-dev
复制代码

因为我们的请求代理只能在开发环境下使用,线上的生产环境,需要走其他的代理配置,所以我们需要在 webpack.dev.js 中进行代理配置

const devConfig = {
  // 其它配置
  devServer: {
    contentBase: './dist',
    open: false,
    port: 3000,
    hot: true,
    hotOnly: true,
    proxy: {
      '/react/api': {
        target: 'http://www.dell-lee.com'
      }
    }
  }
}
复制代码

以上配置完毕后,我们在 index.js 文件中引入 axios 模块,再做请求转发。

import axios from 'axios';

axios.get('/react/api/header.json').then((res) => {
  let {data,status} = res;
  console.log(data);
})
复制代码

使用 npm run dev 后, 我们可以在浏览器中看到,我们已经成功请求到了我们的数据。

从今天开始,学习Webpack,减少对脚手架的依赖(下)

如何使用接口路径重写

现在依然假设有这样一个场景: http://www.dell-lee.com/react/api/header.json 这个后端接口还没有开发完毕,但后端告诉我们可以先使用 http://www.dell-lee.com/react/api/demo.json 这个测试接口,等接口开发完毕后,我们再改回来。解决这个问题最佳办法是,代码中的地址不能变动,我们只在 proxy 代理中处理即可,使用 pathRewrite 属性进行配置。

const devConfig = {
  // 其它配置
  devServer: {
    contentBase: './dist',
    open: false,
    port: 3000,
    hot: true,
    hotOnly: true,
    proxy: {
      '/react/api': {
        target: 'http://www.dell-lee.com',
        pathRewrite: {
          'header.json': 'demo.json'
        }
      }
    }
  }
}
复制代码

同样,我们打包后在浏览器中可以看到,我们的测试接口的数据已经成功拿到了。

从今天开始,学习Webpack,减少对脚手架的依赖(下)

其他常见配置的含义

转发到https:一般情况下,不接受运行在 https 上,如果要转发到 https 上,可以使用如下配置

module.exports = {
  //其它配置
  devServer: {
    proxy: {
      '/react/api': {
        target: 'https://www.dell-lee.com',
        secure: false
      }
    }
  }
}
复制代码

跨域:有时候,在请求的过程中,由于同源策略的影响,存在跨域问题,我们需要处理这种情况,可以如下进行配置。

module.exports = {
  //其它配置
  devServer: {
    proxy: {
      '/react/api': {
        target: 'https://www.dell-lee.com',
        changeOrigin: true,
      }
    }
  }
}
复制代码

代理多个路径到同一个target:代理多个路径到同一个 target ,可以如下进行配置

module.exports = {
  //其它配置
  devServer: {
    proxy: [{
      context: ['/vue/api', '/react/api'],
      target: 'http://www.dell-lee.com'
    }]
  }
}
复制代码

多页打包

现在流行的前端框架都推行单页引用(SPA),但有时候我们不得不兼容一些老的项目,他们是多页的,那么如何进行多页打包配置呢? 现在我们来思考一个问题:多页运用,即 多个入口文件+多个对应的html文件 ,那么我们就可以配置 多个入口+配置多个 html-webpack-plugin 来进行。

场景:假设现在我们有这样三个页面: index.html , list.html , detail.html ,我们需要配置三个入口文件,新建三个 .js 文件。

webpack.common.js 中配置多个 entry 并使用 html-webpack-plugin 来生成对应的多个 .html 页面。 HtmlWebpackPlugin参数说明

  • template :代表以哪个HTML页面为模板
  • filename :代表生成页面的文件名
  • chunks :代表需要引用打包后的哪些 .js 文件
module.exports = {
  // 其它配置
  entry: {
    index: './src/index.js',
    list: './src/list.js',
    detail: './src/detail.js',
  },
  plugins: [
    new htmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new htmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'list.html',
      chunks: ['list']
    }),
    new htmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'detail.html',
      chunks: ['detail']
    }),
    new cleanWebpackPlugin()
  ]
}
复制代码

src 目录下新建三个 .js 文件,名字分别是: index.jslist.jsdetail.js ,它们的代码如下:

// index.js代码
document.getElementById('root').innerHTML = 'this is index page!'

// list.js代码
document.getElementById('root').innerHTML = 'this is list page!'

// detail.js代码
document.getElementById('root').innerHTML = 'this is detail page!'
复制代码

运行 npm run build 进行打包:

$ npm run build
复制代码

打包后的 dist 目录:

|-- dist
|   |-- detail.dae2986ea47c6eceecd6.js
|   |-- detail.dae2986ea47c6eceecd6.js.map
|   |-- detail.html
|   |-- index.ca8e3d1b5e23e645f832.js
|   |-- index.ca8e3d1b5e23e645f832.js.map
|   |-- index.html
|   |-- list.5f40def0946028db30ed.js
|   |-- list.5f40def0946028db30ed.js.map
|   |-- list.html
复制代码

随机选择 list.html 在浏览器中运行,结果如下:

从今天开始,学习Webpack,减少对脚手架的依赖(下)

思考:现在只有三个页面,即我们要配置三个入口+三个对应的 html ,如果我们有十个入口,那么我们也要这样做重复的劳动吗?有没有什么东西能帮助我们自动实现呢?答案当然是有的!

我们首先定义一个 makeHtmlPlugins 方法,它接受一个 Webpack 配置项的参数 configs ,返回一个 plugins 数组

const makeHtmlPlugins = function (configs) {
  const htmlPlugins = []
  Object.keys(configs.entry).forEach(key => {
    htmlPlugins.push(
      new htmlWebpackPlugin({
        template: 'src/index.html',
        filename: `${key}.html`,
        chunks: [key]
      })
    )
  })
  return htmlPlugins
}
复制代码

通过调用 makeHtmlPlugins 方法,它返回一个 htmlplugins 数组,把它和原有的 plugin 进行合并后再复制给 configs

configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs));
module.exports = configs;
复制代码

以上配置完毕后,打包结果依然还是一样的,请自行测试,以下是 webpack.commom.js 完整的代码:

const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const configs = {
  entry: {
    index: './src/index.js',
    list: './src/list.js',
    detail: './src/detail.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { 
            loader: miniCssExtractPlugin.loader,
            options: {
              hmr: true,
              reloadAll: true
            }
          },
          'css-loader'
        ]
      },
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: [
          {
            loader: "babel-loader"
          },
          {
            loader: "imports-loader?this=>window"
          }
        ] 
      }
    ]
  },
  plugins: [
    new cleanWebpackPlugin(),
    new miniCssExtractPlugin({
      filename: '[name].css'
    }),
    new webpack.ProvidePlugin({
      '$': 'jquery',
      '_': 'lodash'
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
    minimizer: [
      new optimizaCssAssetsWebpackPlugin()
    ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname,'../dist')
  }
}
const makeHtmlPlugins = function (configs) {
  const htmlPlugins = []
  Object.keys(configs.entry).forEach(key => {
    htmlPlugins.push(
      new htmlWebpackPlugin({
        template: 'src/index.html',
        filename: `${key}.html`,
        chunks: [key]
      })
    )
  })
  return htmlPlugins
}
configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs))
module.exports = configs
复制代码

如何打包一个库文件(Library)

在上面所有的 Webpack 配置中,几乎都是针对业务代码的,如果我们要打包发布一个库,让别人使用的话,该怎么配置?在下面的几个小节中,我们将来讲一讲该怎么样打包一个库文件,并让这个库文件在多种场景能够使用。

创建一个全新的项目

步骤:

  • 创建library项目
  • 使用 npm init -y 进行配置 package.json
  • 新建 src 目录,创建 math.js 文件、 string.js 文件、 index.js 文件
  • 根目录下创建 webpack.config.js 文件
  • 安装 webpackwebpack-cli :::

按上面的步骤走完后,你的目录大概看起来是这样子的:

|-- src
|   |-- index.js
|   |-- math.js
|   |-- string.js
|-- webpack.config.js
|-- package.json
复制代码

初始化package.json

// 初始化后,改写package.json
{
  "name": "library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "MIT"
}

复制代码

创建src目录,并添加文件

src 目录下新建 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;
}
复制代码

src 目录下新建 string.js ,它有一个 join 方法,如下:

export function join(a, b) {
  return a + '' + b;
}
复制代码

src 目录下新建 index.js 文件,它引用 math.jsstring.js 并导出,如下:

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

export default { math, string };
复制代码

添加webpack.config.js

因为我们是要打包一个库文件,所以 mode 只配置为生产环境( production )即可。

在以上文件添加完毕后,我们来配置一下 webpack.config.js 文件,它的代码非常简单,如下:

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

安装Webpack

因为涉及到 Webpack 打包,所以我们需要使用 npm instll 进行安装:

$ npm install webpack webpack-cli -D
复制代码

进行第一次打包

使用 npm run build 进行第一次打包,在 dist 目录下会生成一个叫 library.js 的文件,我们要测试这个文件的话,需要在 dist 目录下新建 index.html

$ npm run build
$ cd dist
$ touch index.html
复制代码

index.html 中引入 library.js 文件:

<script src="./library.js"></script>
复制代码

至此,我们已经基本把项目目录搭建完毕,现在我们来考虑一下,可以在哪些情况下使用我们打包的文件:

  • 使用 ES Module 语法引入,例如 import library from 'library'
  • 使用 CommonJS 语法引入,例如 const library = require('library')
  • 使用 AMDCMD 语法引入,例如 require(['library'], function() {// todo})
  • 使用 script 标签引入,例如 <script src="library.js"></script>

针对以上几种使用场景,我们可以在output中配置library和libraryTarget属性(注意:这里的library和libraryTarget和我们的库名字library.js没有任何关系,前者是Webpack固有的配置项,后者只是我们随意取的一个名字)

const path = require('path');
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    library: 'library',
    libraryTarget: 'umd'
  }
}
复制代码

配置属性说明:

  • library :这个属性指,我们库的全局变量是什么,类似于 jquery 中的 $ 符号
  • libraryTarget : 这个属性指,我们库应该支持的模块引入方案, umd 代表支持 ES ModuleCommomJSAMD 以及 CMD

在配置完毕后,我们再使用 npm run build 进行打包,并在浏览器中运行 index.html ,在 console 控制台输出 library 这个全局变量,结果如下图所示:

从今天开始,学习Webpack,减少对脚手架的依赖(下)

以上我们所写的库非常简单,在实际的库开发过程中,往往需要使用到一些 第三方库 ,如果我们不做其他配置的话,第三方库会直接打包进我们的库文件中。

如果用户在使用我们的库文件时,也引入了这个第三方库,就造成了重复引用的问题,那么如何解决这个问题呢?可以在 webpack.config.js 文件中配置 externals 属性。

string.js 文件的 join 方法中,我们使用第三方库 lodash 中的 _join() 方法来进行字符串的拼接。

import _ from 'lodash';
export function join(a, b) {
  return _.join([a, b], ' ');
}
复制代码

在修改完毕 string.js 文件后,使用 npm run build 进行打包,发现 lodash 直接打包进了我们的库文件,造成库文件积极臃肿,有70.8kb。

$ npm run build
Built at: 2019-04-05 00:47:25
     Asset      Size  Chunks             Chunk Names
library.js  70.8 KiB       0  [emitted]  main
复制代码

针对以上问题,我们可以在 webpack.config.js 中配置 externals 属性,更多 externals 的用法请点击externals

const path = require('path');
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  externals: ['lodash'],
  output: {
    filename: 'library.js',
    path: path.resolve(__dirname, 'dist'),
    library: 'library',
    libraryTarget: 'umd'
  }
}
复制代码

配置完 externals 后,我们再进行打包,它的打包结果如下,我们可以看到我们的库文件又变回原来的大小了,证明我们的配置起作用了。

$ npm run build
Built at: 2019-04-05 00:51:22
     Asset      Size  Chunks             Chunk Names
library.js  1.63 KiB       0  [emitted]  main
复制代码

如何发布并使用我们的库文件

在打包完毕后,我们如何发布我们的库文件呢,以下是 发布的步骤

  • 注册 npm 账号
  • 修改 package.json 文件的入口,修改为: "main": "./dist/library.js"
  • 运行 npm adduser 添加账户名称
  • 运行 npm publish 命令进行发布
  • 运行 npm install xxx 来进行安装

为了维护npm仓库的干净,我们并未实际运行npm publish命令,因为我们的库是无意义的,发布上去属于垃圾代码,所以请自行尝试发布。另外自己包的名字不能和npm仓库中已有的包名字重复,所以需要在package.json中给name属性起一个特殊一点的名字才行,例如"name": "why-library-2019"

TypeScript配置

随着 TypeScript 的不断发展,相信未来使用 TypeScript 来编写 JS 代码将变成主流形式,那么如何在 Webpack 中配置支持 TypeScript 呢?可以安装 ts-loadertypescript 来解决这个问题。

新建一个项目webpack-typescript

新创建一个项目,命名为 webpack-typescript ,并按如下步骤处理:

  • 使用 npm init -y 初始化 package.json 文件,并在其中添加 build Webpack打包命令
  • 新建 webpack.config.js 文件,并做一些简单配置,例如 entryoutput
  • 新建 src 目录,并在 src 目录下新建 index.ts 文件
  • 新建 tsconfig.json 文件,并做一些配置
  • 安装 webpackwebpack-cli
  • 安装 ts-loadertypescript

按以上步骤完成后,项目目录大概如下所示:

|-- src
|   |-- index.ts
|-- tsconfig.json
|-- webpack.config.js
|-- package.json
复制代码

package.json 中添加好打包命令命令:

"scripts": {
  "build": "webpack"
},
复制代码

接下来我们需要对 webpack.config.js 做一下配置:

const path = require('path');
module.exports = {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.(ts|tsx)?$/,
        use: {
          loader: 'ts-loader'
        }
      }
    ]
  },
  entry: {
    main: './src/index.ts'
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
复制代码

tsconfig.json 里面进行 typescript 的相关配置,配置项的说明如下

  • module : 表示我们使用 ES6 模块
  • target : 表示我们转换成 ES5 代码
  • allowJs : 允许我们在 .ts 文件中通过 import 语法引入其他 .js 文件
{
  "compilerOptions": {
    "module": "ES6",
    "target": "ES5",
    "allowJs": true
  }
}
复制代码

src/index.ts 文件中书写 TypeScript 代码,像下面这样

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return 'hello, ' + this.greeting;
  }
}

let greeter = new Greeter('why');
console.log(greeter.greet());
复制代码

打包测试

  • 运行 npm run build 进行打包
  • 在生成 dist 目录下,新建 index.html ,并引入打包后的 main.js 文件
  • 在浏览器中运行 index.html
从今天开始,学习Webpack,减少对脚手架的依赖(下)

使用其他模块的类型定义文件

如果我们要使用lodash库,必须安装其对应的类型定义文件,格式为@types/xxx

安装 lodash 对应的 typescript 类型文件:

$ npm install lodash @types/lodash -D
复制代码

安装完毕后,我们在 index.ts 中引用 lodash ,并使用里面的方法:

import * as _ from 'lodash'

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return _.join(['hello', this.greeting], '**');
  }
}

let greeter = new Greeter('why');
console.log(greeter.greet());
复制代码

打包测试

使用 npm run build ,在浏览器中运行 index.html ,结果如下:

从今天开始,学习Webpack,减少对脚手架的依赖(下)

Webpack性能优化

打包分析

在进行 Webpack 性能优化之前,如果我们知道我们每一个打包的文件有多大,打包时间是多少,它对于我们进行性能优化是很有帮助的,这里我们使用 webpack-bundle-analyzer 来帮助我们解决这个问题。

首先需要使用如下命令去安装这个插件:

$ npm install webpack-bundle-analyzer --save-dev
复制代码

安装完毕后,我们需要在 webpack.prod.js 文件中做一点小小的改动:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const prodConfig = {
  // 其它配置项
  mode: 'production',
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
复制代码

配置完毕后,我们运行 npm run build 命令来查看打包分析结果,以下打包结果仅供参考:

从今天开始,学习Webpack,减少对脚手架的依赖(下)

缩小文件的搜索范围

首先我们要弄明白 Webpack 的一个配置参数( Resolve )的作用:它告诉了 Webpack 怎么去搜索文件,它同样有几个属性需要我们去理解:

  • extensions :它告诉了 Webpack 当我们在导入模块,但没有写模块的后缀时应该如何去查找模块。
  • mainFields :它告诉了 Webpack 当我们在导入模块,但并没有写模块的具体名字时,应该如何去查找这个模块。
  • alias :当我们有一些不得不引用的第三方库或者模块的时候,可以通过配置别名,直接引入它的 .min.js 文件,这样可以库内的直接解析
  • 其它 includeexcludetest 来配合loader进行限制文件的搜索范围

extensions参数

就像上面所说的那样, extensions 它告诉了 Webpack 当我们在导入模块,但没有写模块的后缀时,应该如何去查找模块。这种情况在我们开发中是很常见的,一个情形可能如下所示:

// 书写了模块后缀
import main from 'main.js'

// 没有书写模块后缀
import main from 'main'
复制代码

像上面那样,我们不写 main.js.js 后缀,是因为 Webpack 会默认帮我们去查找一些文件,我们也可以去配置自己的文件后缀配置:

extensions参数应尽可能只配置主要的文件类型,不可为了图方便写很多不必要的,因为每多一个,底层都会走一遍文件查找的工作,会损耗一定的性能。

module.exports = {
  // 其它配置
  resolve: {
    extensions: ['.js', '.json', '.vue']
  }
}
复制代码

如果我们像上面配置后,我们可以在代码中这样写:

// 省略 .vue文件扩展
import BaseHeader from '@/components/base-header';

// 省略 .json文件扩展
import CityJson from '@/static/city';
复制代码

mainFields参数

mainFields 参数主要应用场景是,我们可以不写具体的模块名称,由 Webpack 去查找,一个可能的情形如下:

// 省略具体模块名称
import BaseHeader from '@components/base-header/';

// 以上相当于这一段代码
import BaseHeader from '@components/base-header/index.vue';
// 或者这一段
import BaseHeader from '@components/base-header/main.vue';
复制代码

我们也可以去配置自己的 mainFields 参数:

同extensions参数类似,我们也不建议过多的配置mainFields的值,原因如上。

module.exports = {
  // 其它配置
  resolve: {
    extensions: ['.js', '.json', '.vue'],
    mainFields: ['main', 'index']
  }
}
复制代码

alias参数

alias 参数更像一个别名,如果你有一个目录很深、文件名很长的模块,为了方便,配置一个别名这是很有用的;对于一个庞大的第三方库,直接引入 .min.js 而不是从 node_modules 中引入也是一个极好的方案,一个可能得情形如下:

通过别名配置的模块,会影响Tree Shaking,建议只对整体性比较强的库使用,像lodash库不建议通过别名引入,因为lodash使用Tree Shaking更合适。

// 没有配置别名之前
import main from 'src/a/b/c/main.js';
import React from 'react';

// 配置别名之后
import main from 'main.js';
import React from 'react';
复制代码
// 别名配置
const path = require('path');
module.exports = {
  // 其它配置
  resolve: {
    extensions: ['.js', '.json', '.vue'],
    mainFields: ['main', 'index'],
    alias: {
      main: path.resolve(__dirname, 'src/a/b/c'),
      react: path.resolve(__dirname, './node_modules/react/dist/react.min.js')
    }
  }
}
复制代码

Tree Shaking去掉冗余的代码

Tree Shaking 配置我们已经在上面讲过,配置 Tree Shaking 也很简单。

module.exports = {
  // 其它配置
  optimization: {
    usedExports: true
  }
}
复制代码

如果你对 Tree Shaking 还不是特别理解,请点击Tree Shaking阅读更多。

DllPlugin减少第三方库的编译次数

对于有些固定的第三方库,因为它是固定的,我们每次打包,Webpack 都会对它们的代码进行分析,然后打包。那么有没有什么办法,让我们只打包一次,后面的打包直接使用第一次的分析结果就行。答案当然是有的,我们可以使用 Webpack 内置的 DllPlugin 来解决这个问题,解决这个问题可以分如下的步骤进行:

  • 把第三方库单独打包在一个 xxx.dll.js 文件中
  • index.html 中使用 xxx.dll.js 文件
  • 生成第三方库的打包分析结果保存在 xxx.manifest.json 文件中
  • npm run build 时,引入已经打包好的第三方库的分析结果
  • 优化

单独打包第三方库

为了单独打包第三方库,我们需要进行如下步骤:

  • 根目录下生成 dll 文件夹
  • build 目录下生成一个 webpack.dll.js 的配置文件,并进行配置。
  • package.json 文件中,配置 build:dll 命令
  • 使用 npm run build:dll 进行打包

生成 dll 文件夹:

$ mkdir dll
复制代码

build 文件夹下生成 webpack.dll.js :

$ cd build
$ touch webpack.dll.js
复制代码

创建完毕后,需要在 webpack.dll.js 文件中添加如下代码:

const path = require('path');
module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  }
}
复制代码

最后需要在 package.json 文件中添加新的打包命令:

{
  // 其它配置
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "build:dll": "webpack --config ./build/webpack.dll.js"
  }
}
复制代码

使用 npm run build:dll 打包结果,你的打包结果看起来是下面这样的:

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.dll.js
|   |-- webpack.prod.js
|-- dll
|   |-- vendors.dll.js
|-- src
|   |-- index.html
|   |-- index.js
|-- package.json
复制代码

引用 xxx.dll.js 文件

在上一小节中我们成功拿到了 xxx.dll.js 文件,那么如何在 index.html 中引入这个文件呢?答案是需要安装 add-asset-html-webpack-plugin 插件:

$ npm install add-asset-html-webpack-plugin -D
复制代码

webpack.common.js 中使用 add-asset-html-webpack-plugin 插件:

const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const configs = {
  // 其它配置
  plugins: [
    new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    })
  ]
}
module.exports = configs;
复制代码

我们将第三方库全局暴露了一个 vendors 变量,现引入 xxx.dll.js 文件结果如下所示:

从今天开始,学习Webpack,减少对脚手架的依赖(下)

生成打包分析文件

webpack.dll.js 中使用 Webpack 内置的 DllPlugin 插件,进行打包分析:

const path = require('path');
const webpack = require('webpack');
module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json')
    })
  ]
}
复制代码

引用打包分析文件

webpack.common.js 中使用 Webpack 内置的 DllReferencePlugin 插件来引用打包分析文件:

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 path = require('path');
module.exports = {
  // 其它配置
  plugins: [
    new cleanWebpackPlugin(),
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
    })
  ]
}
复制代码

优化

现在我们思考一个问题,我们目前是把 lodashjquery 全部打包到了 vendors 文件中,那么如果我们要拆分怎么办,拆分后又该如何去配置引入?一个可能的拆分结果如下:

const path = require('path');
const webpack = require('webpack');
module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash'],
    jquery: ['jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json')
    })
  ]
}
复制代码

根据上面的拆分结果,我们需要在 webpack.common.js 中进行如下的引用配置:

const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const path = require('path');
const configs = {
  // ... 其他配置
  plugins: [
    new cleanWebpackPlugin(),
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    }),
     new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/jquery.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')
    })
  ]
}
module.exports = configs;
复制代码

我们可以发现:随着我们引入的第三方模块越来越多,我们不断的要进行 Webpack 配置文件的修改。对于这个问题,我们可以使用 Node 的核心模块 fs 来分析 dll 文件夹下的文件,进行动态的引入,根据这个思路我们新建一个 makePlugins 方法,它返回一个 Webpack 的一个 plugins 数组:

const makePlugins = function() {
  const plugins = [
    new cleanWebpackPlugin(),
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
  ];

  // 动态分析文件
  const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
  files.forEach(file => {
    // 如果是xxx.dll.js文件
    if(/.*\.dll.js/.test(file)) {
      plugins.push(
        new addAssetHtmlWebpackPlugin({
          filepath: path.resolve(__dirname, '../dll', file)
        })
      )
    }
    // 如果是xxx.manifest.json文件
    if(/.*\.manifest.json/.test(file)) {
      plugins.push(
        new webpack.DllReferencePlugin({
          manifest: path.resolve(__dirname, '../dll', file)
        })
      )
    }
  })
  return plugins;
}
configs.plugins = makePlugins(configs);
module.exports = configs;
复制代码

使用 npm run build:dll 进行打包第三方库,再使用 npm run build 打包,打包结果如下:

本次试验,第一次打包时间为1100ms+,后面的打包稳定在800ms+,说明我们的 Webpack性能优化已经生效。

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.dll.js
|   |-- webpack.prod.js
|-- dist
|   |-- index.html
|   |-- jquery.dll.js
|   |-- main.1158fa9f961c50aaea21.js
|   |-- main.1158fa9f961c50aaea21.js.map
|-- dll
|   |-- jquery.dll.js
|   |-- jquery.manifest.json
|   |-- vendors.dll.js
|   |-- vendors.manifest.json
|-- src
|   |-- index.html
|   |-- index.js
|-- package.json
|-- postcss.config.js
复制代码

小结:Webpack 性能优化是一个长久的话题,本章也仅仅只是浅尝辄止,后续会有关于 Webpack 更加深入的解读博客,敬请期待(立个flag)。

编写自己的Loader

在我们使用 Webpack 的过程中,我们使用了很多的 loader ,那么那些 loader 是哪里来的?我们能不能写自己的 loader 然后使用? 答案当然是可以的,Webpack 为我们提供了一些 loader 的API,通过这些API我们能够编写出自己的 loader 并使用。

如何编写及使用自己的Loader

场景: 我们需要把 .js 文件中,所有出现 Webpack is good! ,改成 Webpack is very good! 。实际上我们需要编写自己的 loader ,所以我们有如下的步骤需要处理:

  • 新建 webpack-loader 项目
  • 使用 npm init -y 命令生成 package.json 文件
  • 创建 webpack.config.js 文件
  • 创建 src 目录,并在 src 目录下新建 index.js
  • 创建 loaders 目录,并在 loader 目录下新建 replaceLoader.js
  • 安装 webpackwebpack-cli

按上面的步骤新建后的项目目录如下:

|-- loaders
|   | -- replaceLoader.js
|-- src
|   | -- index.js
|-- webpack.config.js
|-- package.json
复制代码

首先需要在 webpack.config.js 中添加下面的代码:

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
      }
    ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
复制代码

随后在 package.json 文件添加 build 打包命令:

// 其它配置
"scripts": {
  "build": "webpack"
}
复制代码

接下来在 src/index.js 文件中添加一行代码:这个文件使用最简单的例子,只是打印一句话。

console.log('Webpack is good!');
复制代码

最后就是在 loader/replaceLoader.js 编写我们自己 loader 文件中的代码:

  • 编写 loader 时, module.exports 是固定写法,并且它只能是一个普通函数,不能写箭头函数(因为需要 this 指向自身)
  • source 是打包文件的源文件内容
const loaderUtils = require('loader-utils');
module.exports = function(source) {
  return source.replace('good', 'very good');
}
复制代码

使用我们的 loader : 要使用我们的 loader ,则需要在 modules 中写 loaderresolveLoader 它告诉了 Webpack 使用 loader 时,应该去哪些目录下去找,默认是 node_modules ,做了此项配置后,我们就不用去显示的填写其路径了,因为它会自动去 loaders 文件夹下面去找。

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  resolveLoader: {
    modules: ['node_modules', './loaders']
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [{
          loader: 'replaceLoader',
          options: {
            name: 'wanghuayu'
          }
        }]
      }
    ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
复制代码

最后我们运行 npm run build ,在生成的 dist 目录下打开 main.js 文件,可以看到文件内容已经成功替换了,说明我们的 loader 已经使用成功了。

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("console.log('Webpack is very good!');\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });
复制代码

如何向Loader传参及返回多个值

问题:

  • 我们如何返回多个值?
  • 我们如何向自己的Loader传递参数?

如何返回多个值

Webpack 的 API允许我们使用 callback(error, result, sourceMap?, meta?) 返回多个值,它有四个参数:

  • Error || Null :错误类型, 没有错误传递 null
  • result :转换后的结果
  • sourceMap :可选参数,处理分析后的 sourceMap
  • meta : 可选参数,元信息

返回多个值,可能有如下情况:

// 第三,第四个参数是可选的。
this.callback(null, result);
复制代码

如何传递参数

我们知道在使用 loader 的时候,可以写成如下的形式:

// options里面可以传递一些参数
{
  test: /\.js$/,
  use: [{
    loader: 'replaceLoader',
    options: {
      word: 'very good'
    }
  }]
}
复制代码

再使用 options 传递参数后,我们可以使用官方提供的 loader-utils 来获取 options 参数,可以像下面这样写:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  var options = loaderUtils.getOptions(this);
  return source.replace('good', options.word)
}
复制代码

如何在Loader中写异步代码

在上面的例子中,我们都是使用了同步的代码,那么如果我们有必须异步的场景,该如何实现呢?我们不妨做这样的假设,先写一个 setTimeout

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  var options = loaderUtils.getOptions(this);
  setTimeout(() => {
    var result = source.replace('World', options.name);
    return this.callback(null, result);
  }, 0);
}
复制代码

如果你运行了 npm run build 进行打包,那么一定会报错,解决办法是:使用 this.async() 主动标识有异步代码:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  var options = loaderUtils.getOptions(this);
  var callback = this.async();
  setTimeout(() => {
    var result = source.replace('World', options.name);
    callback(null, result);
  }, 0);
}
复制代码

至此,我们已经掌握了如何编写、如何引用、如何传递参数以及如何写异步代码,在下一小节当中我们将学习如何编写自己的 plugin

编写自己的Plugin

loader 一样,我们在使用 Webpack 的过程中,也经常使用 plugin ,那么我们学习如何编写自己的 plugin 是十分有必要的。 场景:编写我们自己的 plugin 的场景是在打包后的 dist 目录下生成一个 copyright.txt 文件

plugin基础

plugin 基础讲述了怎么编写自己的 plugin 以及如何使用,与创建自己的 loader 相似,我们需要创建如下的项目目录结构:

|-- plugins
|   -- copyWebpackPlugin.js
|-- src
|   -- index.js
|-- webpack.config.js
|-- package.json
复制代码

copyWebpackPlugins.js 中的代码:使用 npm run build 进行打包时,我们会看到控制台会输出 hello, my plugin 这段话。

plugin与loader不同,plugin需要我们提供的是一个类,这也就解释了我们必须在使用插件时,为什么要进行new操作了。

class copyWebpackPlugin {
  constructor() {
    console.log('hello, my plugin');
  }
  apply(compiler) {

  }
}
module.exports = copyWebpackPlugin;
复制代码

webpack.config.js 中的代码:

const path = require('path');
// 引用自己的插件
const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    // new自己的插件
    new copyWebpackPlugin()
  ]
}
复制代码

如何传递参数

在使用其他 plugin 插件时,我们经常需要传递一些参数进去,那么我们如何在自己的插件中传递参数呢?在哪里接受呢?

其实,插件传参跟其他插件传参是一样的,都是在构造函数中传递一个对象,插件传参如下所示:

const path = require('path');
const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    // 向我们的插件传递参数
    new copyWebpackPlugin({
      name: 'why'
    })
  ]
}
复制代码

plugin 的构造函数中调用:使用 npm run build 进行打包,在控制台可以打印出我们传递的参数值 why

class copyWebpackPlugin {
  constructor(options) {
    console.log(options.name);
  }
  apply(compiler) {

  }
}
module.exports = copyWebpackPlugin;
复制代码

如何编写及使用自己的Plugin

apply
apply
compilation.assets

我们现在有这样一个需求:使用自己的插件,在打包目录下生成一个 copyright.txt 版权文件,那么该如何编写这样的插件呢? 首先我们需要知道 plugin 的钩子函数,符合我们规则钩子函数叫: emit ,它的用法如下:

class CopyWebpackPlugin {
  constructor() {
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyWebpackPlugin', (compilation, cb) => {
      var copyrightText = 'copyright by why';
      compilation.assets['copyright.txt'] = {
        source: function() {
          return copyrightText
        },
        size: function() {
          return copyrightText.length;
        }
      }
      cb();
    })
  }
}
module.exports = CopyWebpackPlugin;
复制代码

使用 npm run build 命名打包后,我们可以看到 dist 目录下,确实生成了我们的 copyright.txt 文件。

|-- dist
|   |-- copyright.txt
|   |-- main.js
|-- plugins
|   |-- copyWebpackPlugin.js
|-- src
|   |-- index.js
|-- webpack.config.js
|-- package.json
复制代码

我们打开 copyright.txt 文件,它的内容如下:

copyright by why
复制代码

本篇博客由慕课网视频 从基础到实战手把手带你掌握新版Webpack4.0 阅读整理而来,观看视频请支持正版。


以上所述就是小编给大家介绍的《从今天开始,学习Webpack,减少对脚手架的依赖(下)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Building Web Reputation Systems

Building Web Reputation Systems

Randy Farmer、Bryce Glass / Yahoo Press / 2010 / GBP 31.99

What do Amazon's product reviews, eBay's feedback score system, Slashdot's Karma System, and Xbox Live's Achievements have in common? They're all examples of successful reputation systems that enable ......一起来看看 《Building Web Reputation Systems》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具