1. Library打包

1.1 对库文件进行打包

库文件的打包:我们新建一个工程,新建index.js math.js string.js,代码如下:

index.js文件

import * as math from './math.js'
import * as string from './string.js'
export default { math, string }

math.js文件:

export function add (a, b) {
 return a + b;
}
export function minius (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
}

然后我们打包输出,我们这个库,在给被人使用的是,别人会有很多种方式,进行使用;比如:
ES module

import library from ‘library’

commonJS方式:

const library = require('library')

AMD引入方式:

require(['library'], function () {

})

我们如果想让我们的库文件,在外面可以用上面的这些方式引入,我们可以进行配置,在webpack.config.js中配置如下:libraryTarget: 'umd'代码意思就是不管是在AMD的环境,还是CommonJS的环境、或者是import方式引入,都可以正确使用我们的库文件。

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

如果我们想用

<script src="library.js"></script>

这种方式引入,然后使用library.math这种全局变量来使用我们的库,这时候我们需要在webpack.config.js中添加一个配置项:library: 'library',意思是将library挂载到了全局变量上,这样,就可以通过script引入,然后进行使用了。

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library',
        libraryTarget: 'umd'
    }

其实libraryTargetlibrary相互作用的,如果配置成上面的,是没有多大关系的,我们可以修改 libraryTarget: 'this'意思就是它不支持AMD commonJS等这些语法引入了,但是我们的library会挂载到全局的this上面。同样,也可以是window;,如果在nodeJS环境下,也可以配置 global

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library',
        libraryTarget: 'this'
    }

我们有时候在我们的库文件中,我们使用了第三方的库文件,比如我们将我们的string.js代码改成下面的:我们引入了lodash,

import _ from 'lodash'
export function join (a, b) {
    return _.join([a, b],' ')
}

然后别人在使用我们的库文件的时候,有可能也会使用lodash这个库文件,这样可能会导致一个问题,用户打包后的代码会出现两个lodash文件,我们可以通过配置,来去除这种重复引用;externals: ['lodash'],意思就是打包过程中,如果遇到lodash就忽略这个库文件,然后比尔使用我们这个库的使用还是需要在自己的业务代码中引入lodash库。

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

这里的externals也可以是一个对象:下面配置的意思是如果在commonJS环境下,我们使用library库文件:

const library = require('library')

这样,我们引入lodash的时候,命名必须定义成lodash:如下:const lodash = require('lodash')

const path = require('path')
module.exports = {
    mode: 'production',
    entry: './src/index.js',
    externals: {
        lodash: {
            commonjs: 'lodash',
        }
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library',
        libraryTarget: 'umd'
    }
}
1.2 发布一个包

首先我们需要修改pachage.json中入口改成:./dist/library.js表示,我们的库文件最终给别人使用的时候,入口文件是./dist/library.js文件。然后在npm上注册一个账号;
然后在命令行里面输入npm adduser,然后输入用户名与密邮箱等。然后输入命令npm publish进行发布我们的包文件,这里需要注意的是,包的名字,不能重复;需要修改name,值,然后我们使用我们发布的包,可以运行命令npm install 包名就可以了。

2. PWA(Progressive Web Application)的打包配置

这个是一个新的前端技术。首先一个知识点就是在配置js文件打包输出的时候,配置this的指向:在webpack.config.js中的配置打包输出jsloader中,进行配置:loader: "imports-loader?this=>window"这句话就是将this指向了window对象。

{ 
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
            {
              loader: "babel-loader",
            },
            {
              loader: "imports-loader?this=>window"
            }

我们可以通过一个http-server模块来模拟我们的服务器,当然也可以使用live-server两个都是一样的。输入命令npm install http-server -D进行安装。然后我们在package.json里面进行配置一个命令,来运行我们的服务器:"start": "http-server dist"代码的意思就是在我们打包输出的dist的文件夹中启动一个服务器。

  "scripts": {
    "start": "http-server dist"
  },

输入运行命令之后,我们可以看到我们的网页正常显示了。如果这个时候,我们将服务器断开,浏览器会出现:无法访问此网站,也就是我们的网站挂了,而pwa技术是如果你第一次访问网站,访问成功之后,突然之间服务器挂了,你第二次再次访问这个网站的时候,他会利用你本地的缓存,可以利用缓存,把之前访问的页面展示出来。也就是说,即便服务器挂了,我在本地还是可以看到之前访问到的页面。
我们可以在webpack中,通过一个插件来实现:
输入命令:npm install workbox-webpack-plugin --save-dev进行安装,我们只需要在线上环境之后,进行使用pwa技术,让用户体验更好,所以,我们只需要修改webapck.prod.js的配置文件。

.....
const WorkboxPlugin = require('workbox-webpack-plugin')
const prodConfig = {
....
    plugins: [
    ....
      new WorkboxPlugin.GenerateSW({
        // 这些选项帮助快速启用 ServiceWorkers
        // 不允许遗留任何“旧的” ServiceWorkers
        clientsClaim: true,
        skipWaiting: true
      })
    ....
    ],
}
....

然后我们进行运行npm run build进行打包,现在你可以看到,生成了两个额外的文件:service-worker.js和名称冗长的 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.jsservice-worker.jsService Worker文件,precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.jsservice-worker.js引用的文件,所以它也可以运行。可能你本地生成的文件可能会有所不同;但是应该会有一个service-worker.js文件。但是我们重复上面的操作之后,关闭服务器,再次访问的时候,还是不能查看我们上次访问的网页,因为我们还差一步,那就是注册Service Worker,在我们的src目录中的主入口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);
    });
  });
}

再次运行npm build build来构建包含注册代码版本的应用程序。然后用 npm start将构建结果serve到服务下。导航至 http://localhost:8080并查看 console 控制台。应该看到:SW registered,现在来进行测试。停止 server并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。

3. TypeScript 的打包配置

TypeScript可以进行代码的规范,也可以很方便的进行报错提示;可以有效的提升JavaSCript的可维护性。首先我们进行配置打包typescript的规则,我们要使用一个ts-loader这个loader来进行打包tsx的文件,当然我们首先需要安装这个loader,使用这个loader的时候,你必须要安装typescript,所以运行下面的命令进行安装:npm install ts-loader typescript --save-dev
配置webpack.config.json如下:

const path = require('path')
module.exports = {
    entry: './src/index.tsx',
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/
        }]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

但是这样还是不能够进行打包,我们需要添加一个tsconfig.json,进行配置打包TypeScript的规则:

{
    "compilerOptions": {
        "outDir": "./dist",//打包输出的地址
        "module": "es6",// 模块的引用方式是使用esmodule的语法
        "target": "es5",//打包成es5的代码
        "allowJs": true//是否允许在TypeScript中引入js模块
    }
}

这样就可以打包成功了;我们之所以使用TypeScript是进行规范我们代码书写,以及类型引用的正确;比如我们使用lodash这样的库文件,我们在使用这个库的时候,我们有时候不知道这个函数的参数是什么 ,也没有提示我们书写的正确性,只有运行之后,才会知道;当然你也可以去查看文档。我们可以使用对应库文件的TypeScript类型文件,比如我们前面说lodash,我们进行输入命令npm install @types/lodash --save-dev进行安装对应的TypeScript类型文件,然后我们调用里面的函数,如果传入的参数不对,会直接进行提示;
我们在TypeScript中引用lodash,需要使用下面的代码:

import * as _ from 'lodash'

如果需要使用其他库文件,就安装对应的TypeScript类型文件。如果你不知道有还是没有,可以在这个网站;https://microsoft.github.io/TypeSearch/进行搜索对应的库文件。

4. 使用 WebpackDevServer 实现请求转发

我们一般在前端发送ajax请求是使用axios这个库进行发送请求,首先我们需要进行安装;一个简单的请求如下:

import axios from 'axios'
axios.get('http://www.dell-lee.com/react/api/header.json')
.then((res) => {
  console.log(res)
})

我们一般在生产环境的时候,是使用其他服务器的api接口,并不是使用真正的后台接口,是一台测试的服务器,线上环境我们是使用真正的接口数据。所所以这个时候,我们就需要进行对接口的转发。配置devServer如下:'/react/api': 'http://www.dell-lee.com'代码的意思就是当我发送请求/react/api地址的时候,接口转发到http://www.dell-lee.com这个地址。进行获取对应的数据。

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/react/api': 'http://www.dell-lee.com',
      },
      hot: true,
    },

然后在发送请求的地址如下代码:

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

也可以对一个接口请求的地址进行转发,比如我们现在请求的是http://www.dell-lee.com/react/api/header.json实际我们需要请求的是http://www.dell-lee.com/react/api/demo.json,可以在devserver配置如下;

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

当然我们只有在开发环境的时候进行上面的配置是有用的,有时候我们请求的地址是https协议的网址,需要加一个配置项:secure: false,

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          secure: false,
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

我们在这里也可以进行拦截,比如下面的代码配置:bypass里面的配置项就是说当发送请求要接手的是html页面数据的时候,也就是说请求是一个html的地址的时候,直接跳转到index.html页面。

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          secure: false,
          bypass: function(req, res, proxyOptions) {
            if (req.headers.accept.indexOf('html') !== -1) {
              console.log('Skipping proxy for browser request.');
              return '/index.html';
            }
          },
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

如果我们代理多个路基,可以进行下面这样的配置:我们上面只有一个'/react/api'一个路径,下面代码表示遇到'/auth', '/api'这两个地址,都是转发到https://www.dell-lee.com这个服务器。

    devServer: {
      proxy: {
      context: ['/auth', '/api'],
      target: 'https://www.dell-lee.com',
      },
      hot: true,
    },

如果我们想做一个根目录的路径的转发,也就是'/',我们需要将配置项的index设置为false或者为''空字符串,如下配置:

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        index: '',
        '/': {
          target: 'https://www.dell-lee.com',
          secure: false,
          bypass: function(req, res, proxyOptions) {
            if (req.headers.accept.indexOf('html') !== -1) {
              console.log('Skipping proxy for browser request.');
              return '/index.html';
            }
          },
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

我们可以设置changeOrigin: true来覆盖一些请求服务器对origin进行的配置,如下代码:

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        index: '',
        '/': {
          target: 'https://www.dell-lee.com',
          secure: false,
          changeOrigin: true,
          bypass: function(req, res, proxyOptions) {
            if (req.headers.accept.indexOf('html') !== -1) {
              console.log('Skipping proxy for browser request.');
              return '/index.html';
            }
          },
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

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

如果我们需要实现react的路由功能实现单页面应用,需要安装路由组件,输入命令:npm intall react-router-dom --save,然后我们写了两个组件homelist 两个组件,代码如下:

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


class App extends Component {
  return () {
    return (
      <BrowserRouter>
        <div>
         <Router path='/' component={Home} />
         <Router path='/list' component={List}/>
         </div>
      </BrowserRouter>
    )
  }
}
ReactDom.render(<h1>Hello, world!</h1>, document.getElementById('root'))

home.js代码:

import React, { Component } from 'react';

class Home extends Component {
    render () {
        return <div>HomePages</div>
    }
}
export default Home

list.js代码:

import React, { Component } from 'react';

class Home extends Component {
    render () {
        return <div>HomePages</div>
    }
}
export default Home

我们写完代码之后,运行;当我们地址中访问list页面的时候,会发现找不到对应的页面,这是因为,webpack将我们页面 的请求以为是发送到后台的api接口。解决这个问题,我们就需要在devserver中进行配置historyApiFallback的配置项,具体配置如下:historyApiFallback: true,

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      historyApiFallback: true,
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          secure: false,
          changeOrigin: true,
          bypass: function(req, res, proxyOptions) {
            if (req.headers.accept.indexOf('html') !== -1) {
              console.log('Skipping proxy for browser request.');
              return '/index.html';
            }
          },
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

这个选项也可以是一个对象,进行配置规则,进行进一步配置:代码意思是,当访问item.html的时候,实际访问的是list.html页面。

      historyApiFallback: {
        rewrites: [
          { 
            from: '/item.html',
            to: '/list.html'
          }
        ]
      },

其实我们前面配置的historyApiFallback: true,实际等价于下面的配置:也就是不官方文哪一个都是指向index.html页面。

      historyApiFallback: {
        rewrites: [
          { 
            from: '/\.*/',
            to: '/index.html'
          }
        ]
      },

6. EsLint 在 Webpack 中的配置

EsLint就是规范我们代码书写的插件,输入命令npm install eslint --save-dev,然后在我们的项目中,通过命令npx eslint --init快速生成我们代码约束的配置文件,按照提示的信息进行配置我们的代码约束规则。我们可以输入命令npx eslint src这样的命令。来检测src目录下文件的书写规范,或者直接可以检测一个文件npx eslint index.js这样进行检测。
如果我们配置的代码检测的规范是使用airbnb这种规范,我们会发现,我们去检测我们写的react的代码的时候,会提示很多错误,这是因为这个规范只是检测常规的js代码的一些规范,不会进行解析react的代码,进行检测规范,这个时候,我们需要添加一个解析器,.eslintrc.js配置代码如下:需要安装babel-eslint这个解析器,输入命令npm install babel-eslint --save-dev进行安装。

module.exports = {
  "extends": "airbnb",
  "parser": "babel-eslint"
}

也可以直接在vscode这个编译器中进行安装eslint插件,进行检测我们的代码规范,进行提示。我们也可以自己进行配置一些规范是否启用,在.eslintrc.js中进行配置,比如我们不需要一个配置,直接在rules中将对应的规范配置为0:

module.exports = {
  "extends": "airbnb",
  "parser": "babel-eslint",
  "rules": {
    "react/prefer-stateless-function": 0
  }
}

还有一些eslint的配置可以查看另一篇文章:https://blog.csdn.net/cj9551/article/details/90740377#15_ESLint_457 里面有对应的eslint的配置。
如果需要使用全局变量,可以在配置项中进行配置:document: false不会被重新覆盖。

module.exports = {
  "extends": "airbnb",
  "parser": "babel-eslint",
  "rules": {
    "react/prefer-stateless-function": 0
  },
  global: {
    document: false
  }
}

如果有的人的编译软件,是不能安装eslint查看,我们可以直接在webpack中进行配置这样一个检测环境,首先安装eslint-loader这样一个loader,输入命令npm install eslint-loader --save-dev,然后在webpack.config.js中进行配置:

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

然后运行命令,就可以在终端窗口看到一些代码规范的提示,我们也可以在devserver中进行添加一个overlay: true的配置,将代码规范的提示,直接显示在浏览器的页面中:

    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      // historyApiFallback: true,
      historyApiFallback: {
        rewrites: [
          { 
            from: '/item.html',
            to: '/list.html'
          }
        ]
      },
      proxy: {
        '/react/api': {
          target: 'https://www.dell-lee.com',
          secure: false,
          changeOrigin: true,
          overlay: true,
          bypass: function(req, res, proxyOptions) {
            if (req.headers.accept.indexOf('html') !== -1) {
              console.log('Skipping proxy for browser request.');
              return '/index.html';
            }
          },
          pathRewrite: {
            'header.json': 'demo.json'
          }
        },
      },
      hot: true,
    },

7. webpack 性能优化

  • 跟上技术的迭代(node,npm,yarn):升级我们对应的工具;webpack运行在node之上。升级到新的版本,也会间接提升webpack打包速度。
  • 在尽可能少的模块上应用Loader: 比如我们在打包js的文件的时候使用babel-loader进行转换,我们可以通过includes以及excludes等选项,进行配置不对其他库文件进行转换,降低使用loader的频率。
  • Plugin 尽可能精简并确保可靠:比如我们在线上环境的配置中使用了MiniCssExtractPlugin插件进行将css代码进行压缩,但是我们在生产环境中,并不需要使用这个插件进行压缩我们的代码,要尽可能的精简。减少了代码压缩的时间。还有就是尽量使用webpack官网中推荐的插件。
  • 一般我们在打包配置js的文件,与jsx的文件,打包规则是一样的,我们在test匹配的时候,可以使用test: /\.jsx?$/,进行匹配打包。
  • 尽量减少下面的一些操作:我们有时候通过import的方式进行引入一个模块,我们在模块的结尾并没有明确是加载哪个类型的文件,import List from './list'默认是加载js结尾的文件,但是有时候我们的库文件是jsx结尾的文件,我们也想通过这种方式进行加载jsx文件,可以进行下面的配置:extensions: ['.js', '.jsx']代码意思就是引入包之后,他会按照是否有对应的js,如果有就进行加载,如果没有继续找是否有对应的jsx文件进行加载。
module.exports = {
.....
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    resolve: {
       extensions: ['.js', '.jsx']
    }
.....
}

这个选项还可以配置我们通过引入路径,然后对路径中的文件名进行对应的加载,比如:import List from './list/'这个listsrc目录中的一个文件夹名,然后该文件夹中有很多js文件,如果按照这种引入方式,他会自动检查是否有文件名叫index的文件,然后进行引入,如果没有,就不会进行引入,我们可以通过配置resolve选项,配置加载的文件名:mainFiles: ['index', 'list']代码意思是如果通过路径的方式进行引入模块,首先检查进行加载index的文件名,如果没有,进行检查是否有list的文件名进行引入。

module.exports = {
.....
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    resolve: {
       extensions: ['.js', '.jsx'],
       mainFiles: ['index', 'list']
    }
.....
}

同样上面的extensions,以及 mainFiles的配置,会影响打包的性能。在resolve中,我们也可以进行设置路径的别名:比如我们有时候想引入一个模块通过一个字符串,进行代表该库文件的地址,进行引入模块:import list from 'listpath'这样的方法进行引入模块。我们就可以设置别名,让listpath指向该模块的地址。

module.exports = {
.....
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    resolve: {
       extensions: ['.js', '.jsx'],
       mainFiles: ['index', 'list'],
       alias: {
        listpath: path.resolve(__dirname, '../src/list')
      }
    }
.....
}

8. 使用DllPlugin提高打包速度

我们实现的思路:

  1. 第三方模块打包一次
  2. 我们引入第三方模块的时候,要去使用打包输出的第三方模块的文件进行引入。
    我们在打包自己的项目的时候会发现,如果引入了过多的第三方库文件,会使打包的速度降低;我们可以想如果能够实现在第一次打包的时候就去分析第三方模块的代码,然后再次打包的时候,就根据前面分析的结果进行打包,不用再次分析,提高打包效率;
    添加一个webpack.dll.js的配置文件,代码如下:这里是将我们引入的第三方模块打包到dll文件夹中,library: '[name]'这句代码的意思就是,将我们打包后的第三方模块通过变量的形式暴露到全局中;变量的名字叫vender
const path = require('path')
nodule.exports = {
    mode: 'production',
    entry: {
        vendors: ['react', 'react-dom', 'lodash']
    },
    output: {
        filename: '[name].dll.js',
        path: path.resolve(__dirname, '../dll'),
        library: '[name]'
    }
}

然后我们需要安装一个插件,将我们的打包后的文件进行挂载到我们打包输出的页面中,输入命令npm install add-asset-html-webpack-plugin --save这个插件的作用就是往html页面中去增加静态资源。然后在webpack.config.js中进行配置:这样页面就会将我们打包输出的第三方模块的vender.dll.js进行挂载;

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
    plugins: [
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        }) 
    ]
.......
}

接下来我们就要实现引入第三方模块的时候,通过我们上一步打包输出的文件进行引入,首先我们需要进行分析前面打包输出的第三方库文件,将分析结果进行保存,使用DllPlugin进行分析,在webpack.dll.js的配置文件中进行如下配置:name: '[name]',是指要分析的库名,path: path.resolve(__dirname, '../dll/[name].manifest.json')代码意思就是将分析的结构放在../dll/[name].manifest.json

const path = require('path')
const webpack = require('webpack')
nodule.exports = {
    mode: 'production',
    entry: {
        vendors: ['react', 'react-dom', 'lodash']
    },
    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')
        })
    ]
}

然后我们做的就是根据我们前面生成的library: '[name]'全局变量的名字,以及分析的文件,在webpack进行配置打包:
webpack.config.js中使用一个插件:manifest: path.resolve(__dirname, '../dll/vendors.manifest.js')意思就是我们在代码中如果引入了第三方模块。他就会在vendors.manifest.json这个文件中去找对应的第三方模块的映射关系,如果可以找到,他就会知道这个第三方模块不用再次引入了,直接使用vendors.dll.js这个文件就可以了,他会在全局变量中进行使用我们的第三方模块。如果没有映射关系,就会进行将第三方库进行打包。

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
    plugins: [
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        }),
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
        })
    ]
.......
}

然后我们在package.json里面配置一个命令:"build:dll": "webpack --config ./build/webpack.dll.js"来执行我们的打包第三方模块的命令。然后进行打包,会发现比之前的打包速度要快很多。
其实我们前面打包第三方库输出额时候可以进行拆分:

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

对应的在webpack.config.js中也需要进行配置:

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
    plugins: [
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
        }),
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll/react.dll.js')
        }),
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
        }),
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll/react.manifest.json')
        })
    ]
.......
}

如果我们按照上面的,配置很多的话,就特别繁琐,我们可以通过下面的方法进行简单化:

const plugins = [
    new CleanWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpakcPlugin({
        template: './src/index.html'
    })
]
const fs = require('fs')
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)) {
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '../dll', file)
        })
    }
})

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
    plugins: plugins
.......
}

9. Webpack其他打包优化方法

  • 控制包文件大小:在源代码中将用不到的包文件在打包的时候通过Tree Shaking进行去除,或者直接不进行引入;也可以通过splitchunkplugins插件进行包的拆分,提高打包速率。
  • tread-loader, parallel-webpack, happypack 多进程打包。
  • 合理使用sourceMap:不同环境打包,使用最佳的sourceMap方式。
  • 结合stats分析打包结果:根据分析结果进行优化。
  • 开发环境内存编译。
  • 开发环境无用插件剔除:比如开发环境中不需要对代码进行压缩,将mode设置为production

10. 多页面打包配置

我们前面所说的都是单页面进行打包,也就是打包之后就生成一个index.html页面。比如说我们有两个页面一个index.js,一个list.js内容分别如下:
index.js:

class App extends Component {
  return () {
    return (
        <div>
        index
         </div>
    )
  }
}
ReactDom.render(<h1>Hello, world!</h1>, document.getElementById('root'))

list.js:

class App extends Component {
  return () {
    return (
        <div>
        list
         </div>
    )
  }
}
ReactDom.render(<h1>list!</h1>, document.getElementById('root'))

这是两个不同的页面,我们希望打包之后有两个html页面,一个引入iindex.js打包之后的文件,一个引入list.js打包之后的文件。首先我们需要修改打包的入口文件,将list.js文件进行添加:

    // 入口文件
    entry: {
        main: './src/index.js',
        list: './src/list.js'
    },

然后修改我们之前配置的打包输出设置的模板html:设置打包输出的html的文件名,并且设置页面引用的包文件。

plugins: [
    new CleanWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpakcPlugin({
        template: './src/index.html',
        filename: 'index.html',
        chunks: ['runtime', 'vendors', 'main']
    }),
    new HtmlWebpakcPlugin({
        template: './src/index.html',
        filename: 'list.html',
        chunks: ['runtime', 'vendors', 'list']
    })
]

我们如果有很多页面进行打包,如果重复上面的操作就特别麻烦,所以我们可以进行处理上面的代码:

// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack');
const fs = require('fs')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

const makePlugins = (configs) => {
    const plugins = [new CleanWebpackPlugin()]
    const files = fs.readdirSync(path.resolve(__dirname, './dll'))
    Object.keys(configs.entry).forEach(item => [
        plugins.push(new HtmlWebpakcPlugin({
            template: './src/index.html',
            filename: `${item}.html`,
            chunks: ['runtime', 'vendors', item]
        }))
    ]);

    files.forEach(file => {
        if(/.*\.dll.js/.test(file)) {
            plugins.push(
              new AddAssetHtmlWebpackPlugin({
                  filepath: path.resolve(__dirname, '../dll', file)
              }))
        }
        if(/.*\.manifest.json/.test(file)) {
            new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, '../dll', file)
            })
        }
    })
    return plugins;
}

const configs =  {
    // 配置打包模式
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    // 入口文件
    entry: {
        main: './src/index.js',
        list: './src/list.js'
    },
    devServer: {
      // 服务器启动的根路径
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      },
      hot: true,
      hotOnly: true
    },
    stats: { children: false },
    module: {
        rules: [{ 
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader"
        },
            {
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/',
                    limit: 204800
                }
            }
        }, {
            test: /\.vue$/,
            use: {
                loader: 'vue-loader'
            }
        }, {
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }, {
            test: /\.(eot|ttf|svg|woff)$/,
            use: {
                loader: 'file-loader'
            }
        }, {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader',
                'postcss-loader'
            ]
        },{
            test: /\.(html)$/,
            use: {
                loader: 'html-loader',
            }
        }]
    },
    optimization: {
      usedExports: true
    },
    // 打包出的文件配置
    output: {
        path: path.resolve(__dirname, 'dist')
    }
}
module.exports = configs
configs.plugins = makePlugins(configs)