webpack4.0从入门到放弃

一、入坑初探

1. 设置项目为私有

我们只需要在package.json文件中配置,因为是私有项目不需要向外部暴露的,所以我们可以去掉main: index.js

"private": true

2. 运行webpack

一般我们安装webpack时会同时安装webpack-cli,它的作用是使我们可以在命令行使用webpack命令,在命令行中执行

npx webpack --config webpack.config.js

--config指定webpack执行的文件,如果没有,默认是webpack.config.js,因为我们是在命令行中执行,所以需要npx,如果我们写在package.json文件中,则只需要"bundle": "webpack"就可以了。

3. webpack简单配置

webpack只能识别后缀是.js的文件,如果是其它类型的文件,就需要引入loader来帮助我们编译。
下面我们来做一个简单的对图片和css的打包配置:

rules: [
    {
        test: /\.(jpg|png|gif)$/,
        use: {
        loader: 'url-loader',
        options: { // loader额外参数配置
            name: '[name]_[hash].[ext]', // name: 原来的名字 ext:原来的后缀
            outputPath: 'images/', // 输出路径
            limit: 10240 // 限制,大于10240kb时才进行此操作,否则直接打到js文件中
         }
      } 
      },{
          test: /\.scss$/,
          use: [
                'style-loader',  // 将css挂载到header中
                // options: {
                    // insertAt: 'top' // 插到顶部
                // },
            'css-loader',  // 分析当前有几个css文件,将css文件整合,分析@import这种语法
            'sass-loader',
            'postcss-loader'
          ]
     }
]

file-loader和url-loader的区别是url-loader会把图片等(任何文件)文件直接打包到js中,如果图片很小,我们可以使用这种方式,如果图片较大,我们就需要将图片打包到统一的images目录中,在上面代码中我们做了一个限制,当图片大于10kb时,就打包到images目录中,否则直接打包到js中

注意loader执行顺序是从下到上执行的,如css这里,执行顺序为:
postcss-loader->sass-loader->css-loader->style-loader

最后我们再来看打包完命令行中的展示,如下图所示:


image.png

Chunks: 打包的js的id,
Chunk Names: 打包的js名字

二、loader篇

1. css相关loader

{
    test: /\.scss$/,
    use: [
        'style-loader', 
        {
            loader: 'css-loader',
            options: {
                importLoaders: 2, // 如果当前引入的scss文件又引入了其它scss文件,让引入的scss文件也需要通过postcss-loader,sass-loader编译,如果不加,就会直接走css-loader,2代表前两个,几就代表前几个
                modules: true // 开启css模块化,开启后css需要用模块化引入的写法
            }
        },
        'sass-loader',
        'postcss-loader'
    ]
}

关于配置css-next的方法查看postcss-loader的文档:https://webpack.js.org/loaders/postcss-loader

2. 打包字体文件

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

打包字体文件用file-loader把字体文件打包到dist目录中就可以了

三、webpack基础

plugins相当于vue,react中的钩子,可以在webpack运行到某个时刻的时候,帮助我们做一些事情

1. html-webpack-plugin

我们需要自动生成一个html文件,把打包生成的js自动引入到这个html文件中

plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html' // 指定模版文件
    })

2. CleanWebpackPlugin

我们需要在每次打包后删掉上一次的打包文件

new CleanWebpackPlugin(['dist'])]

关于dist目录和webpack配置文件不在同一个根目录下,我们需要如下解决方法

new CleanWebpackPlugin(['dist'], {
    root: path.resolve(__dirname, '../')
})

2. copyWebpackPlugin

有些时候我们需要拷贝一些静态资源文件到dist目录

new CopyWebpackPlugin([
   {from: 'doc', to: './'}
])

2. bannerPlugin

版权声明插件,可以在我们打包生成的文件前生成一些版权信息等

new webpack.BannerPlugin('zxhnext@qq.com')

3. 打包多份js,指定cdn引用路径

首先我们需要配置多入口

entry: {
    main: './src/index.js',
    sub: './src/index.js'
}

出口处我们不能写死一个名字,否则会因打包处两份相同的文件而报错

output: {
    publicPath: 'http://cdn.com.cn', // 设置前缀(cdn地址)
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
}

4. sourceMap

devtool: 'cheap-module-eval-source-map'  // development
devtool: 'cheap-module-source-map'  // production

一般在开发环境中我们使用cheap-module-eval-source-map,在线上环境使用cheap-module-source-map,如果要关闭sourceMap我们需要把devtool置为none
cheap:1. 只指出哪一行出错,不指出哪一页。2. 只报我们的业务代码,不处理loader等中的代码错误。
module:指出loader等中的错误
source-map: 生成一个.map文件
inline: 将映射文件放到main.js中
eval: 将业务代码与 以及source-map通过eval方式执行,速度最快
具体用法参考官方文档:https://webpack.js.org/configuration/devtool/#devtool

5. 热启动

5.1 通过shell脚本

"watch": "webpack --watch",

我们只需要在package.json文件中设置watch即可,但是这种方法存在很多缺陷,如果我们需要开启一个本地服务,那么我们需要使用webpack-dev-server

5.2. webpack-dev-server

devServer: {
    contentBase: './dist',
    open: true, // 是否打开浏览器
    port: 8080
}

我们需要注意的是,使用webpack-dev-server时我们并未发现有dist目录,这时因为webpack-dev-server将打包好的文件隐藏到计算机的内存中了,这样执行更快。
关于webpack-dev-server的更多配置参考官网:https://webpack.js.org/configuration/dev-server

下面我们来实现一个简单的webpack-dev-server

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

// 编译
const complier = webpack(config);
const app = express();
// 在应用里使用webpack
app.use(webpackDevMiddleware(complier, {
  // config.output.publicPath
}));
app.listen(3000, () => {
    console.log('server is running');
});

在命令行中使用webpack语法:https://www.webpackjs.com/api/cli/

在node中使用webpack: https://www.webpackjs.com/api/node/

6. Hot Module Replacement 热模块更新

当我们每次修改代码时,页面都会整个刷新,这样岂不是很麻烦,有没有办法只更新被修改的部分,而不刷新整个页面,这时我们需要用到HotModuleReplacementPlugin

const webpack = require('webpack');
module.exports = {
    ...
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        hotOnly: true
    },
    ...
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
}

这里我们要注意的是,必须在devServer加上hot: true,hotOnly: true,Hot Module Replacement才会生效
hotOnly: HotModuleReplacementPlugin失效时,重新刷新一次页面

修改了某个文件后,我们就需要手动去更新了

if(module.hot) {
    module.hot.accept('./number', () => {
        document.body.removeChild(document.getElementById('number'));
        number();
    })
}

然而我们平常用css,vue和react等的时候并没有这么去做,这是因为是因为css-loader,vue-loader,react-loader中自动帮我们实现了

7. Babel 处理 ES6 语法

这里我们要参考babel官方文档:https://babeljs.io/setup#installation,下面我们先来做一个简单的配置:

{ 
    test: /\.js$/, 
    exclude: /node_modules/, 
    // include: path.resolve(__dirname, '../src'), // 只检测某个目录,exclude除掉某个目录
    loader: 'babel-loader',
    options: {
        presets: [['@babel/preset-env', { // @babel/preset-env将es6转为es5
            useBuiltIns: 'usage'
        }]]
    }
}

这里我们需要注意,我们需要配置exclude: /node_modules/, 否则这里也会去匹配node_modules中的js文件,同时我们可以看到,如果所有配置都写在webpack.config.js中,那将会变得非常复杂,所以这里建议新建一个.babelrc文件,将babel-loader中的配置放在.babelrc中,如下所示:

{
    presets: [
        [
            "@babel/preset-env", {
                targets: {
                    chrome: "67", // 支持哪个版本以上的浏览器
                },
                useBuiltIns: 'usage' // 实现按需加载
            }
        ]
    ]
}

在有些低版本浏览器中是不支持es5的一些语法的,这时我们需要@babel/polyfill帮我们解决,我们直接在入口文件中main.js引入@babel/polyfill即可

import "@babel/polyfill";

但是我们怎么实现按需加载呢,我们再.babelrc中添加useBuiltIns: 'usage'
如果配置了useBuiltIns: 'usage',会默认引入@babel/polyfill,不需要手动调用
参见官网:https://babeljs.io/docs/en/babel-polyfill

8. 类库的配置

当我们写一个类库时,我们可以用@babel/plugin-transform-runtime,相比@babel/polyfill,它是通过闭包实现依赖注入,这样做不会污染全局环境

{
    "plugins": [["@babel/plugin-transform-runtime", {
        "corejs": 2, // 设为2可以实现按需引入而不是全局引入,设为2后需要安装@babel/runtime-corejs2
        "helpers": true,
        "regenerator": true,
        "useESModules": false
    }]]
}

9. watch用法

watch: true,
watchOptions: { // 监控的选项
    poll: 1000, // 每秒监控多少次
    aggregateTimeout: 500, // 防抖,停止输入500ms后再打包
    ignored: /node_modules/ // 不需要监控的文件夹
}

四、Webpack进阶

1. Tree-shaking

Tree-shaking大意就是只打包我们有使用的代码,将无用的部分去掉,举例如下:
我们有一个math.js的方法库,内容如下

export const add = (a, b) => {
    console.log( a + b );
}

export const minus = (a, b) => {
    console.log( a - b );
}

然后我们在index.js中使用math.js的add方法

import { add } from './math.js';

add(1, 7);

这里有一点我们需要注意,Tree-shaking只支持import这种ES Module,不支持require这种形式的。
虽然我们只引入了add方法,但是webpck默认把math.js中所有的文件都帮我们打包了,如何做到只打包我们使用的部分代码呢?这时我们需要在webpack中作如下配置

plugins: [],
...
optimization: {
    usedExports: true
},

然后我们需要在package.json文件中这样配置:

"sideEffects": [ // 不对下面的文件进行tree shaking
    "@babel/polly-fill",
    "*.css"
]

首先来解释下它是什么意思,即忽略掉哪些模块不做Tree-shaking,首先我们要忽略所有的css文件,其次如果像import @babel/polyfill这种形式的,我们没有引入任何东西,webpack会自动帮我们忽略掉,这样打包文件就出错了

在生产环境tree shaking 是自动生效的,不用再webpack中做配置,但是我们依然需要在package.json中需要配置

"sideEffects": false // false代表没有需要忽略的文件

2. Develoment 和 Production

我们仿照create-react-app,创建build目录存放我们的weback配置文件,首先我们将公用文件提到webpack.common.js,然后我们用webpack-merge合并,如下所示:

const commonConfig = require('./webpack.common.js');
const devConfig = {
  ...
}
module.exports = merge(commonConfig, devConfig);

因为我们将webpack配置文件放在了build目录中,此时dist与webpack配置文件不在同一根目录下,这是我们需要解决dist和webpack不在同一个根目录下而产生的clean插件无法删除dist目录问题,解决方法如下:

new CleanWebpackPlugin(['dist'], {
    root: path.resolve(__dirname, '../')
})

3. Code Splitting代码分割

3.1 多入口打包方法

如果我们想把引入的模块单独打包,我们需要单独创建一个文件引入这个包,然后挂载到window上,再在入口处引入这个文件
这里我们以lodash为例:
新建lodash.js文件,内容如下:

import _ from 'lodash';
window._ = _;

然后我们在entry引入这个包,

entry: {
    lodash: './src/lodash.js',
    main: './src/index.js'
}

3.2 配置optimization

在webpack中我们可以配置chunks来自动帮我们做(同步)代码分割

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

3.3 异步模块打包

异步模块不需要我们做任何配置,webpack会自动帮我们将异步代码打包到另一个文件中。
在使用异步加载的写法时(vue中懒加载模块),我们需要安装@babel/plugin-syntax-dynamic-import,然后在.babelrc中配置

{
    presets: [
        [
            "@babel/preset-env", {
                targets: {
                    chrome: "67",
                },
                useBuiltIns: 'usage'
            }
        ]
    ],
    plugins: ["@babel/plugin-syntax-dynamic-import"]
}

异步代码写法

function getComponent() {
    return import('lodash').then(({ default: _ }) => {
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell', 'Lee'], '-');
        return element;
    })
}

getComponent().then(element => {
    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);
    });
})

import(/* webpackChunkName:"lodash" */ 'lodash');

这是魔法注释,加上后,打包出来的js会是你注释的值,否则为一个id(如0)值

4. SplitChunksPlugin 配置参数

splitChunks默认配置,当我们写一个splitChunks: {},默认等于如下

splitChunks: {
    chunks: "async", // async 只对异步代码生效, all同步异步都生效, initial同步生效
    minSize: 30000, // 文件大于多少时才会打包
    //maxSize: 0, // 会尝试对大于多少的文件再次分割为两个小文件
    minChunks: 1, // 当一个模块至少被用了几次后才做代码分割
    maxAsyncRequests: 5, // 最多分割几个包
    maxInitialRequests: 3, // 入口文件引入的库最多能分割成几个包
    automaticNameDelimiter: '~', // 生成文件名字中间的连接符
    name: true, // 使cacheGroups中设置的文件名有效
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10 // 优先级的意思,如果同时满足vendors和default,这个值谁大就打包到哪个组,-10大于-20
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true // 如果引入的某个文件之前已经被打包过,就不会被打包了,会直接去复用之前的
        }
    }
}

chunks: "all"时我们需要注意,这时webpack会继续找到cacheGroups,vendors中的test表示被打包的文件是否在node_modules这个文件夹中,如果是的话,就会打包到vendors这个组中,这时打包出来的文件名字应该是vendors~main.js,main是定义的入口文件名字,如果我们想指定一个名字,可以在vendors中设置filename指定一个名字

default是指如果不符合vendors中的要求的文件,比如我们自己写的一个包,这个包并不在node_modules中,这时会分到default组中
cacheGroups作用是做一个缓存组,如果我们引入了多个包,就会分割成很多模块,而cacheGroups作用就是先将需要打包的文件缓存起来,然后统一打包到一个组中

vendors, default也可以设置为false

5. 打包分析,Preloading, Prefetching

5.1 打包分析

在package.json中设置一个下面的命令,然后运行

"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"

会生成一个stats.json文件,这是一个对打包过程的描述文件,借助一些工具我们可以进行分析。
参考analyse:https://github.com/webpack/analyse
参考官网:https://webpack.js.org/guides/code-splitting/#bundle-analysis

5.2 代码使用率

在浏览器调试工具中按command+shift+p,然后我们选择show coverage选项,可以查看代码的使用率,代码使用率越高说明优化的越好,所以我们开发时尽量多写异步的代码,这样代码使用的时候才会去加载
如下所示:

// click.js
function handleClick() {
    const element = document.createElement('div');
    element.innerHTML = 'Dell Lee';
    document.body.appendChild(element);
}

export default handleClick;

// index.js
document.addEventListener('click', () =>{
    import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
        func();
    })
});

Prefetching是等主代码加载完才会加载,Preloading是与主代码同时加载

6. CSS代码分割

这里我们需要使用mini-css-extract-plugin
参考官网:https://webpack.js.org/plugins/mini-css-extract-plugin

6.1 我们先来看一下output内容:

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

这里说一下filename与chunkFilename的区别:
入口文件的打包用filename,chunk文件打包用chunkFilename

6.2 分割css

如果我们不分割css,webpack会默认把css打包到js文件中,这是我们不希望看到的,下面来看下mini-css-extract-plugin的使用方法。注意,如果打包失败,需要看一下是不是package.json文件中这里配置有误,可能是tree shaking影响了

"sideEffects": "false"
// 改为
"sideEffects": ["*.css"]
{
    test: /\.css$/,
    use: [
        MiniCssExtractPlugin.loader,
        'css-loader',
        'postcss-loader'
    ]
}

plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css'
    })
]

6.3 css压缩

我们还可以对css进行压缩,这时我们需要用到optimize-css-assets-webpack-plugin
然后配置如下:

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})]
}

6.4 多入口的css打包到一个css中

这个配置意思是只要是css文件就打包到这个组中

optimization: {
    splitChunks: {
        cacheGroups: {
            styles: {
                name: 'styles',
                test: /\.css$/,
                chunks: 'all',
                enforce: true
            }
        }
    }
}

enforce为true表示忽略其它的默认参数

6.5 不同入口打包到不同组

参考官网:https://webpack.js.org/plugins/mini-css-extract-plugin

6.6 去掉性能上的警告

performance: false, // 去掉性能上的警告
output: {
    path: path.resolve(__dirname, '../dist')
}

7. runtimeChunk

配置runtimeChunk是因为在一些老版本的webpack中,manifest(包与包之间的关系)文件是加在main与vendors文件中的,这样会导致即使我们没有更改文件,但是包与包之间的关系变了而引起的contenthash发生变化,这时我们就需要这样配置将这部份代码抽离出来,在新版webpack中不会出现这个问题

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

8. Shimming

一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery 中的 $)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方。

8.1 全局引入

new webpack.ProvidePlugin({
    $: 'jquery',
    _join: ['lodash', 'join']
})

当发现一个模块中用了$时,会在模块中默认引入jquery
如果需要使用模块中的某个方法,我们可以用一个数组的方式定义

8.2 修改this指向

每个模块的this指向的都是模块自身,如果想让this指向window,需要imports-loader插件,然后我们再做如下配置:

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

9. 环境变量的使用

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

然后在package.json中设置环境变量

"build": "webpack --env.production --config ./build/webpack.common.js"

五、webpack高级使用技巧

1. 类库代码打包

我们对package.json进行设置

"license": "MIT", // 开源

然后在output中做如下设置:

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'library.js',
    library: 'library', // 可以script标签引入,在全局挂载了一个library变量
    libraryTarget: 'umd' // 使支持amd,cmd,require等语法
}

还可以用如下写法

library: 'library', // 可以script标签引入,在全局挂载了一个library变量
libraryTarget: this' // 这两个配合就不支持amd等写法了,只会挂载一个全局变量
lodash : {
      commonjs: 'lodash', // 通过require(common.js)引入时,名字必须叫lodash
      amd: 'lodash',
      root: '_' // 通过script标签引入时必须在全局挂载一个_变量
}

const lodash = require('lodash')  // commonjs设置的意思是const后的名字必须叫lodash

如果我们编写的库中引入了其它包,我们不希望引入的包被打包,这时我们可以设置

module.exports = {
    ...
    externals: 'lodash',
    output: {
        ...
    }
}

这里写成一个数组,对象,字符串形式都可以,对象形式:

module.exports = {
    ...
    externals: {
        lodash: {
            commonjs: 'lodash'
        }
    },
    output: {
        ...
    }
}

参考官网:https://webpack.js.org/configuration/externals/#externals

最后我们需要把package.json的入口文件改为

"main": "./dist/library.js",

然后在npm注册一个账号,
然后npm adduser添加用户名和密码
再npm publish

2. PWA 的打包配置

安装workbox-webpack-plugin
在plugins中配置:

new WorkboxPlugin.GenerateSW({
    clientsClaim: true,
    skipWaiting: true
})

js文件为

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

3. TypeScript 的打包配置

我们需要安装ts-loader typescript
在rules中配置:

{
    test: /\.tsx?$/, // ?代表可有可无
    use: 'ts-loader',
    exclude: /node_modules/
}

同时创建tsconfig.json文件,做如下配置

{
    "compilerOpitons": {
        "outDir": "./dist",
        "module": "es6", // 使用es6的模块引入方法
        "target": "es5", // 转换为es5形式
        "allowJs": true // 允许ts中引入js文件
    }
}

一般库的typescript版本都是@types/名字,可以参考:https://github.com/DefinitelyTyped/DefinitelyTyped

4. WebpackDevServer 实现请求转发

注意本章只在开发环境生效,对生产环境没有影响

4. 1. 代理接口

devServer: {
  proxy: {
    // index: '', // 如果要代理根路径,需要把index设置为false或者''
    '/react/api': {
      target: 'https://www.dell-lee.com', // 代理请求接口
      secure: false, // 如果是https网址,这里需要设置为false
      pathRewrite: { // 代理接口,访问header.json时会帮你请求demo.json
        'header.json': 'demo.json'
      },
      changeOrigin: true, // 后端可能设置了changeOrigin防止爬虫,这里我们设置true以后就可以避开这个限制了
      headers: { // 设置请求头
        host: 'www.dell-lee.com',
        cookie: ....
      },
      bypass: function(req, res, proxyOptions) { // 拦截,如果请求的是一个html内容,则返回index.html
        if (req.headers.accept.indexOf('html') !== -1) {
          console.log('Skipping proxy for browser request.');
          return '/index.html';
        }
      }
    }
  }
}

webpackdevserver proxy底层用了 http-proxy-middleware这个插件

如何使用mock数据

devServer: {
    before(app) {
        app.get('/user', (req, res) => {
            res.json(....)
        })
    }
}

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

当不使用hash路由时,我们可以设置以下内容

historyApiFallback: true, // 把对服务器的请求都转换为对跟路径的请求
historyApiFallback: {
      rewrites: [ // 访问abc.html时代理到index.html
        { from: /abc.html/, to: '/views/index.html' }
      ]
}

historyApiFallback: true相当于

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

底层用了connect-history-api-fallback这个插件

5. EsLint 在 Webpack 中的配置

安装eslint
npx eslint --init

module.exports = {
    "extends": "airbnb", // 使用那个规则
    "parser": "babel-eslint", // 解析器
    "rules": {
        "react/prefer-stateless-function": 0,
        "react/jsx-filename-extension": 0
    },
    globals: {
        document: false // 不允许覆盖全局变量document
    }
};

在webpack中使用eslint
安装eslint-loader:https://webpack.js.org/loaders/eslint-loader

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

同时配置overlay: true,eslint有错会在浏览器中提示

devServer: {
    overlay: true
}

设置force为pre代表强制先执行,fix会自动修复一些项目中eslint简单的错误

{ 
    test: /\.js$/, 
    exclude: /node_modules/, 
    use: [
        {
            loader: 'eslint-loader',
            options: {
                fix: true
            },
            force: 'pre'
         },
        'babel-loader'
    ]
}

六、webpack 性能优化

1. 经常更新版本

2. 使用loader时指定检测目录,图片没有必要

{ 
    test: /\.js$/, 
    include: path.resolve(__dirname, '../src'), // 只检测某个目录,exclude除掉某个目录
    use: [{
        loader: 'babel-loader'
    }]
}

3. 尽少使用plugin,尽可能精简并确保可靠

4. 合理配置resolve

resolve: {
    extensions: ['.js', '.jsx'],
    mainFIles: ['index', 'child']
},

当一个引入的文件没有后缀时,会识别它是不是.js,.jsx文件
引入一个目录,回去查找目录下是否有index,child文件
给文件或路径设置别名

resolve: {
    extensions: ['.js', '.jsx'],
    alias: {
        child: path.resolve(__dirname, '../src/child')
    }
},

5. 第三方模块只打包一次

新建一个webpack.dll.js, 运行它对第三方模块单独打包,并生成vendors.manifest.json映射文件

const path = require('path');
const webpack = require('webpack');

module.exports = {
    mode: 'production',
     entry: {
        vendors: ['lodash'],
        react: ['react', 'react-dom'],
        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 webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

new webpack.DllReferencePlugin({ // 查找vendors.manifest.json,如果发现这里有,就不会再重复打这个包
    manifest: path.resolve(__dirname, '../dll', '../dll/vendors.manifest.json')
})
new AddAssetHtmlWebpackPlugin({ // 向html中添加引入某个文件
    filepath: path.resolve(__dirname, '../dll',  '../dll/vendors.dll.js')
})

自动化引入

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

6. 控制包文件大小

7. thread-loader,parallel-webpack,happypack多线程打包

let happypack = require('happypack');

rules: [
  {
      test: /\.js$/,
      exclude: /node_modules/,
      include: path.resolve('src'),
      use: 'happypack/loader?id=js'
  }
]

plugins: [
    new happypack({
        id: 'js',
        use: [{
            loader: 'babel-loader',
            options: {
                ...
            }
        }]
    })
]

8. 合理使用sourceMap

9. 开发环境内存编译

webpackdevserver用的就是内存编译

10. 开发环境无用插件剔除

11. noParse

module: {
    noParse: /jquery/, 不去解析jquery中的依赖库
}

12. ignoreplugin

忽略掉我们不需要引入的包文件中的部分内容

// 我们不需要引入moment这个包里的/locale文件夹,就把它忽略掉
new webpack.IgnorePlugin(/\.\/locale/,/moment/)

七、多页面打包配置

多入口

entry: {
    index: './src/index.js',
    list: './src/list.js',
    detail: './src/detail.js',
}

生成多个html

new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'index.html',
    chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}), 
new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'list.html',
    chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}), 
new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'detail.html',
    chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}), 

自动化方式

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]
            })
        )
    });
    return plugins;
}

八、webpack原理篇

1. 编写一个 Loader

1.1 同步操作

新建loader文件夹,在文件夹中新建replaceLoader.js文件

const loaderUtils = require('loader-utils');

module.exports = function(source) { // 注意这里不能使用箭头函数,我们需要变更this指向来调用this中的一些方法
    return source.replace('lee', 'world');
}

然后我们在webpack.config.js中引入

rules: [{
    test: /\.js/,
    use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
}]

同时我们还可以传入一些参数

rules: [{
    test: /\.js/,
    use: [
        {
            loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
            options: {
                name: 'zxh'
            }
        }
    ]
}]

这时我们就可以在replaceLoader.js,通过this.query可以接收到options中的内容

module.exports = function(source) { 
    return source.replace('hello',  this.query.name);
}

或者我们可以通过webpack官方提供的loader-utils模块,使用方法如下

const loaderUtils = require('loader-utils');
module.exports = function(source) {
    const options = loaderUtils.getOptions(this);
    const result = source.replace('dell', options.name);
    return source.replace('hello',  options.name);
}

想要返回多个值时可以用this.callback


image.png
const loaderUtils = require('loader-utils');

module.exports = function(source) {
    const options = loaderUtils.getOptions(this)
    const result = source.replace('dell', options.name);
    this.callback(null, result, source, mata)
}

1.2 使用异步操作 this.async

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

引入模块时,会来node_modules中找,找不到了再来loaders文件夹中找,这时我们就可以像引入node_modules中的loader那样写了

entry: {
    main: './src/index.js'
},
resolveLoader: {
    modules: ['node_modules', './loaders']
},
module: {
    rules: [{
        test: /\.js/,
        use: [
            {
                loader: 'replaceLoader',
            }
        ]
    }]
}

2. 编写一个 Plugin

发布,订阅设计模式
https://webpack.js.org/api/compiler-hooks

class CopyrightWebpackPlugin {

    apply(compiler) {

        compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => { // 同步,不用传callback
            console.log('compiler');
        })

        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { // emit是异步的,我们需要在后面写tapAsync,打包完放到文件夹时,compiler是所有打包文件,compilation是本次打包文件
            debugger;
            compilation.assets['copyright.txt']= {
                source: function() { // 内容
                    return 'copyright by dell lee'
                },
                size: function() { // 文件长度
                    return 21;
                }
            };
            cb(); // 最后必须调一下cb()
        })
    }

}

module.exports = CopyrightWebpackPlugin;

开启node调试工具

"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"

3. Bundler源码编写

安装cli-highlight:命令行高亮显示工具

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser'); // 帮助分析源代码
const traverse = require('@babel/traverse').default; // 帮助遍历module
const babel = require('@babel/core');

const moduleAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8'); // 读取文件内容
    const ast = parser.parse(content, { // 抽象语法树,ast
        sourceType: 'module' // 如果是es6模块方法,这里需要设置
    });
    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) { // 如果有引入语句,就执行
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname, node.source.value); // 改为相对根目录的路径
            dependencies[node.source.value] = newFile;
        }
    });
    const { code } = babel.transformFromAst(ast, null, { // 将ast抽象语法树转换为浏览器可以识别的代码
        presets: ["@babel/preset-env"]
    });
    return {
        filename,
        dependencies,
        code
    }
}

const moduleInfo = moduleAnalyser('./src/index.js'); // 入口文件
console.log(moduleInfo);

vue-cli3多页面配置


image.png

参考:https://cli.vuejs.org/zh/config/

webpack loader与plugins编写:https://www.jianshu.com/p/21cbc228d7f5

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容