Webpack - 解析

856 阅读4分钟

常用 loader

常用 plugin

webpack.DefinePlugin

作用:在打包阶段定义全局变量

使用的好处:定义的全局变量可以在业务代码中获取

webpack.HashedModuleIdsPlugin

作用:保持打包文件的资源稳定。

在浏览器下载资源文件的时候获取的源会有几种:

1.从服务器拉取 2. from memeory cache 3. from disk cache

那么浏览器如何判断哪些资源从哪里获取呢?这就是打包文件结尾 hash 值的作用。一旦资源文件发生改变,hash 的值就会变化,浏览器就会重新从服务器拉取。

但是这也会有一个问题,就是每次打包的时候,一些没有改变的文件的 hash 也会发生变化,比较典型的是 vender.js 打包的一般的第三方库如:vue,element-ui,loadash等,这些的内容是不会变得,我们希望这样的文件的 hash 值最好能保持不变,这样就不用每次都从服务重新拉取了。

所以我们会看到,通过 vue-cli 脚手架搭建的项目的 build.js 文件使用的是 chuckhash,chuckhash 可以保持hash值的稳定。但是当增加一些第三方库或者减少一些第三方库,chuckhash 生成的 hash 值还是会变化的。这时候就可以使用这个 HashedModuleIdsPlugin 插件了。

使用方式:在需要打包的环境中增加配置:

plugins: [
  new webpack.HashedModuleIdsPlugin()
]

webpack.NoEmitOnErrorsPlugin

这个插件多是在开发环境中配置使用,当代码中有一些错误,会导致开发环境运行失败,这时候增加这个插件,会先将工程运行成功,然后将错误信息打印到浏览器的控制台上,方便查看和调试。

plugins: [
  new webpack.NoEmitOnErrorsPlugin()
]

webpack.ProvidePlugin

提供全局的引用库。

当很多组件都在使用某一个第三方js,在组件中都需要 import 进来,这有点繁琐,比如 jquery, axios。这个组件就可以定义这些公共的js,在组件中使用就不需要 import 了。这些文件在打包的时候会直接被打包到 vender 中,只是挂在到 vue 下。

plugins: [
  new webpack.ProvidePlugin({
      $ : 'jquery',
      axios: 'axios'
  })
]

copy-webpack-plugin

用来复制静态文件。

webpack 在打包的时候,只会将使用过的文件打包进去,那么如果在 static 文件夹下有 100 张图片,在代码中值使用到了 10 张,那么另外 90 张是不会被打包的。而这些照片可能是需要用的,那么就可以使用这个插件,帮助我们将这些静态文件复制到指定目录下。

plugins: [
  new CopyWebpackPlugin([
    {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
    }
  ])
]

打包优化

dll 优化

我们使用的第三方库一般是不会修改的,但是每次打包的时候还是会处理这些第三方库,这个过程是比较耗时的。那么能不能将这些第三方库做单独的打包呢?这样每次打包就,除了第一次需要处理这些第三方库,后边再次进行的编译就不要处理这些第三方库了。

const webpack = require('webpack')
module.exports = {
    enryt:{
        vender: ['jquery', 'loadsh'] // 这个 vender 是打包出来想叫的名字
    },
    output: {
        path: __dirname + '/dll',
        filename: '[name].dll.js',
        library: '[name]_library' // 这里的那么取的是上面 entry 的命名
    },
    plugins: [
        new webpack.DllPlugin({
            path:  __dirname + '/dll/[name]_manifest.json',
            name: '[name]_library' // 要和上面 library 中的信息一样
        })
    ]
}

然后需要再在 webpack.config.js 中定义插件,通知它 dll 的打包的信息。

const webpack = require('webpack')
const HappyPack = require('happypack')
const os = require('os') // os 是 node 的内置模块
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length})//使用 cpu 的核数来做线程的数量
module.exports = {
    mode: ''development,
    enryt:{
        app: './app.js'
    },
    output: {
        filename: '[name].js'
    },
    module : {
        rules: [
            {
                test: /\.js$/,
                // loader: 'babel-loader'
                use: [
                    {
                        loader: 'happypack/loader?id=happybabel'
                    },
                    {
                        loader: './myloader.js' //执行自定义的 loader 
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: './dll/vender_manifest.json'
        }),
        new HappyPack({
            id: 'happybabel',
            loaders: 'babel-loader?cacheDirectory=true',
            threadPool: happyThreadPool
        }),
        new HappyPack({
            id: 'happycss',
            loaders: 'css-loader?cacheDirectory=true',
            threadPool: happyThreadPool
        })
    ]
}

happyPack

node 默认是单线程的,但是其实 node 是可以开启工作进程的,也就是可以多进程工作。使用 happyPack 处理文件很多的工程效果是很明显的,如果是文件很少的文件的化,时长反而可能会增长。

dll 优化的build.js 文件

'use strict'
require('./check-versions')();

process.env.NODE_ENV = 'production';
process.env.BUILD_MODE = '';

if (!!process.argv[2] && process.argv[2] === 'online') {
	process.env.BUILD_MODE = 'online'
} 

const ora = require('ora');
const rm = require('rimraf');
const path = require('path');
const chalk = require('chalk');
const webpack = require('webpack');
const config = require('../config');
const webpackDllConfig = require('./webpack.dll.config');

const spinner = ora('building for production...');

spinner.start();

function buildDll () {
	return new Promise ((resolve, reject) => {
		webpack(webpackDllConfig, (err, stats) => {
			spinner.stop();
			if (err) throw err;
			process.stdout.write(stats.toString({
				colors: true,
				modules: false,
				children: false,
				chunks: false,
				chunkModules: false
			}) + '\n\n');

			if(stats.hasErrors()) {
				console.log(chalk.red('  Build failed with errors.\n'));
				reject();
				process.exit(1);
			}

			console.log(chalk.cyan('  Build complete.\n'));
			console.log(chalk.yellow(
				' Tip: built files are meant to be served over an HTTP server.\n' + 
				' Opening index.html over file:// won\'t work.\n'
			));
			resolve();
		})
	})
}

function buildProject (config) {
	return new Promise((resolve, reject) => {
		webpack(config, (err, stats) => {
			spinner.stop();
			if (err) throw err;
			process.stdout.write(stats.toString({
				colors: true,
				modules: false,
				children: false, //if you are using ts-loader, setting this to true will make
				chunks: false,
				chunkModules: false
			}) + '\n\n');

			if(stats.hasErrors()) {
				console.log(chalk.red('  Build failed with errors.\n'));
				reject();
				process.exit(1);
			}

			console.log(chalk.cyan('  Build complete.\n'));
			console.log(chalk.yellow(
				' Tip: built files are meant to be served over an HTTP server.\n' + 
				' Opening index.html over file:// won\'t work.\n'
			));
			resolve();

		})
	})
}

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
	if (err) throw err;
	if (process.env.BUILD_MODE === 'online') {
		buildDll ()
			.then (() => {
				return require('./webpack.prod.conf');
			})
			.then ((config) => {
				return buildProject(config);
			})
	} else {
		buildDll ()
			.then (() => {
				return require('./webpack.test.config')
			})
			.then ((config) => {
				return buildProject(config);
			})
	}
})


解决方案

如果是对模块内容进行处理: loader 是第一解决方案 这里的模块比如:js 文件, css 文件的某类文件

loader 的本质其实就是执行了一个方法,这个方法接受的参数,就是 test 匹配出来的文件的内容,那么这个loader 就可以针对文件的内容进行操作了。 举例:myloader.js

module.exports = function (context) {
  console.log(context),
  context.replace('bind', 'on')
  return context  // 切记,一定要返回
}

如果要增加一些特殊的功能:可以自定义插件

而当有一些需求处理的内容是散落在各种类型的文件中的时候,就需要使用自定义插件了。 例如有一个需求:在开发阶段引用的是 static 下的文件,但是生产上是使用的是文件服务器的文件,那么就需要将 static 的路径替换为 www.xxx.com,这个时候该怎么处理呢?这就是典型的使用 plugin 的场景,因为这个引用可以是散落在任意类型的文件中的。

定义文件 /myplugin/index.js:

const fs = require('fs')
const path = require('path')
module.exports = a
function a () {}
a.prototype.apply = function (compiler) {
    // 插件就是取监听webpack 的生命周期,可以在某个生命周期做一些事情
    compiler.hooks.done.tap('changeStatic', function (compilation) {
        let context = compiler.options.context
        let publicPath = path.resolve(context, 'dist')
        compilation.toJson().assets.forEach( ast => {
            const filePath = path.resolve(publicPath, ast.name)
            fs.readFile(filePath, function(err, file) {
                let newContext = file.toSring().replace('/static', 'www.xxx.com')
                fs.writeFile(filePath, newContext, function() {})
            })
        })
    })
}

项目上的打包简化,可变性配置等:编写相应的操作函数