webpack4配置一个多页面

738 阅读4分钟

为什么

因为项目是用glpu搭建的工作流,在转Jenkins自动发布的时候,考虑到如果需要多页面动态打包,需要改写glup配置(不会glup也不想学),就将项目重构为webpack4.

遇到的难点

  1. 扫描项目多页面入口
  2. 区分生产和开发环境webpck配置
  3. 指定项目打包
  4. 使用shell指令进行打包操作
  5. 如何处理不同的模块模式
  6. 修改html如何触发热更新
  7. 热更新的原理
  8. 区分生产和开发环境的懒加载

解决方式

1. 扫描项目多页面入口

引入glob第三方插件(大佬说fs模块自带扫描,表示懵逼没找到),

//获得一个动态的目录
const glob = require('glob')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
//import glob from "glob"
var obj = {
    detail: {},
    entry: {
    }
}
const htmlWebpackPluginDevConfig = { plugins: [] }
//${process.env.ENV_file}用来区别项目
glob.sync(`./src/${process.env.ENV_file}/**/*.html`).forEach(e => { //同步
    let arr = e.split('/')
    function getDir(arr) {
        let arr1 = JSON.parse(JSON.stringify(arr))
        arr1.splice(-1)
        return arr1
    }
    const chunk = arr[arr.length - 1].split('.')[0]
    const filename = chunk + '.html'
    //这个是入口的详细情况,以后会用到
    obj['detail'][chunk] = {
        name: chunk,
        path: e,
        dir: path.resolve(__dirname, `${getDir(arr).join('/')}`)
    }
    //entry是暴露出去的入口文件
    obj['entry'][chunk] = `${getDir(arr).join('/')}` + '/main.js'
    const htmlConf = {
        filename: filename,
        template: e,
        //favicon: './src/assets/images/favicon.ico',
        inject: 'body',
        chunks: [chunk]
    }
    //新建htmlWebpackPluginDevConfig对象,输出html文件
    htmlWebpackPluginDevConfig.plugins.push(new HtmlWebpackPlugin(htmlConf))
})
module.exports = { obj, htmlWebpackPluginDevConfig }

2. 区分生产和开发环境

将公共配置抽离出来成为webpack.common.js

//webpack.common.js
const util = require('./util')//用来获得entry和htmlWebpackPluginDevConfig
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
    entry: util.obj.entry,
    plugins: [
        new ExtractTextPlugin('style/[name]_main[hash].css'),
        new CleanWebpackPlugin([`../dist/${process.env.ENV_file}`],
            {
                allowExternal: true //允许删除根目录以外的文件夹
            }),
        ...util.htmlWebpackPluginDevConfig.plugins,
        new CopyWebpackPlugin([{
            from: path.resolve(__dirname, `../src/${process.env.ENV_file}/static`)
        }]),
    ],
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: ['file-loader']
            },
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    fallback: "style-loader",
                    // use: ["css-loader", 'postcss-loader']
                    use: [{
                        loader: 'css-loader',
                    }, {
                        loader: 'postcss-loader'
                    }
                    ]
                }),
            },
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components|dependencies)/,
                use: [{
                    loader: 'babel-loader',
                },
                {
                    loader: path.resolve("./inject-loader.js") // 开发模式使用注入代码实现html热更新
                }
                ]
            }

        ]
    },
    output: {
        path: path.resolve(__dirname, `../dist/${process.env.ENV_file}`),
        filename: 'js/[name].bundle[hash].js',
        chunkFilename: 'help.bundle.js',
    }
};

引入webpack-merge进行配置合并(类似于object.assign) 以dev为例,于pro相比,取消了代码压缩,增加了开发服务器和热更新

//webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin')
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
    mode: 'development',
    // devtool: 'inline-source-map',
    devServer: {
        contentBase: `./dist/${process.env.ENV_file}`,
        hot: true
    },
    plugins: [
        new webpack.NamedModulesPlugin(),//以便更容易查看要修补(patch)的依赖
        new webpack.HotModuleReplacementPlugin(),
        new CopyWebpackPlugin([{
            from: path.resolve(__dirname, `../src/${process.env.ENV_file}/static`)
        }])
    ],
    optimization: {
    },
    module: {
        rules: [
            {
                test: /\.(html)$/,
                use: {
                    loader: 'html-loader',
                    options: {
                        attrs: [':data-src']
                    }
                }
            },
        ]
    },
});

3. 指定项目打包

引入cross-env(这个包是能够跨平台引入全局对象的包,可在webpack配置和开发代码中引用,我很喜欢这个插件) 例如在package.json中的进行ENV_file进行写死赋值(后期是通过shell命令动态赋值)

然后就可以在任何一个地方使用process.env.ENV_file引用该变量

//获得一个动态的目录
const glob = require('glob')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
//import glob from "glob"
var obj = {
    detail: {},
    entry: {
    }
}
const htmlWebpackPluginDevConfig = { plugins: [] }
glob.sync(`./src/${process.env.ENV_file}/**/*.html`).forEach(e => { //同步
    let arr = e.split('/')
    function getDir(arr) {
        let arr1 = JSON.parse(JSON.stringify(arr))
        arr1.splice(-1)
        return arr1
    }
    const chunk = arr[arr.length - 1].split('.')[0]
    const filename = chunk + '.html'
    obj['detail'][chunk] = {
        name: chunk,
        path: e,
        dir: path.resolve(__dirname, `${getDir(arr).join('/')}`)
    }
    obj['entry'][chunk] = `${getDir(arr).join('/')}` + '/main.js'
    const htmlConf = {
        filename: filename,
        template: e,
        //favicon: './src/assets/images/favicon.ico',
        inject: 'body',
        chunks: [chunk]
    }
    htmlWebpackPluginDevConfig.plugins.push(new HtmlWebpackPlugin(htmlConf))
})
module.exports = { obj, htmlWebpackPluginDevConfig }

4. 使用shell指令的时候,调用开发本地库而不是全局库

项目需要上Jenkins自动发布,因此打包脚本要改为动态打包

//build.sh
dir=$(ls -l ./src |awk '/^d/ {print $NF}')
cur_date="`date +%Y%m%d`" 
for i in $dir
do
npx cross-env ENV_file=${i} webpack --config ./build/webpack.pro.js 
echo "打包的文件夹是: $i"
done
cd dist
tar -zcvf ../sparrow_${cur_date}.tar.gz ./*
echo "打包完毕""

使用shell获得文件夹目录,使用for循环动态执行webpack命令,其中npx是npm(5.2以上)自带的一个包,可用来执行本地指令(与package中scripts类似的作用),这里有对 cross-env进行动态赋值的操作

5. 如何处理不同的模块模式

原先项目使用的是src引入的第三方依赖,会将变量暴露在window下 而模块化本地打包之后,会通过require暴露一个对象需要手动在全局注册一下。

6. 修改html如何触发热更新

手动写一个inject-loader,在dev环境下使用该loader,在pro就不用了

    //webpack.dev
            {
                test: /\.(html)$/,
                use: {
                    loader: 'html-loader',
                    options: {
                        attrs: [':data-src']
                    }
                }
            },
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components|dependencies)/,
                use: [{
                    loader: 'babel-loader',
                },
                {
                    loader: path.resolve("./inject-loader.js") // 开发模式使用注入代码实现html热更新
                }
                ]
            }

其中source是webpack内置的一个参数,其中会返回上一级loader返回的字符串,然后你要做得就是在其前面拼接一个html模板的字符串,然后将其返回给下一级

//inject-loader
const path = require("path");
module.exports = function (source) {
    var file = path.basename(this.resourcePath)
    var dir = this.resourcePath.split('/')[this.resourcePath.split('/').length - 2]
    console.log('------------------------------------', file, dir)
    if (file === "main.js" && dir !== 'scripts') {
        return `if (process.env.NODE_ENV === "development") {
        require("./${dir}.html");
    };` + source;
    }
    return source
}

7. 热更新的原理

这里我也迷了很久,后来才知道大致和dom事件差不多,都是采用的冒泡形式,自变更的那个文件,冒泡到引用该文件的最上层,vue-loader,css-loader都有对其变化进行拦截,关键点在module.hot.accept这个函数,一般不会出现在业务代码中,会出现在loader源码中。 推荐一个 大佬的github进行尾随学习

8. babel配置

开发环境,不需要有懒加载,懒加载只在生产上面用

//.babelrc
{
  "env": {
    "development": {
      "plugins": ["dynamic-import-node"]
    }
  }
}
  1. dynamic-import-node这个插件是用来将import()转化为require(),在生产环境用代码分割,在开发环境使用同步引入,可以加快热更新的速度
  2. useBuiltIns这个option可以在入口处引入babel-polyfill之后进行按需补丁
  3. targets可以选择编译后的版本类型

更多env配置可查看

其他

啊~虽然学的很累,但是不会说学不动了,大佬是灯塔,就算我看不见,也有人会需要

demo仓库