美文网首页WebPackwebpack
webpack4.0从入门到放弃

webpack4.0从入门到放弃

作者: zxhnext | 来源:发表于2019-04-03 09:05 被阅读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

    相关文章

      网友评论

        本文标题:webpack4.0从入门到放弃

        本文链接:https://www.haomeiwen.com/subject/jxknvqtx.html