如何使用webpack搭建一个多入口多出口的前端工程

时间:2021-6-12 作者:qvyue

1. 多入口配置

首先我们来看下webpack入口怎么配置:

module.exports = {

    entry: {
        'index' : '../src/index.js'
    }

}

多入口只需要配置多个key:value对即可,key就是你要发布的包名,主要为了后面的outpu准备,value就是你入口文件所在的位置

那么问题来了,是不是我每次加一个入口就要手动去添加呢?这样也太不智能了吧!

别急,不就是个读取目录吗,glob可以办到呀,glob是一个node插件,可以帮助我们按照一定规则读取文件的路径,我们就可以根据一定的规则构建出entry了。

var glob = requir('glob')

var getEntry = function() {
  var entry = {}
  var files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))//你的入口文件相对于当前的路径
  files.forEach(file => {
    var key = file.split('/').splice(-2, 1)[0]
    entry[key] = file
  })
  return entry
}

module.exports = {
  entry: getEntry()
}

这样就解决了我们的多入口问题了

2. 多出口配置

对于多出口webpack还是比较容易实现的,毕竟我们在entry下功夫拿到了key

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

是不是很简单,此时你已经可以使用这套配置再加上常规的module配置开发多入口多出的js项目了,但是距离真正的spa项目还差的远,因为你没有HTML呀,也不能自动把打包好的js编译到HTML文件中呀,而且你要想本地调试,你得有个服务器吧,watch太麻烦得刷新页面,你得有个HMR支持吧…是不是说到这已经懵了,别急别急后面还有更多要配置的-。-哈

3. 添加一个HTML模板

webpack有一个插件叫做html-webpack-plugin,这个插件可以帮助我们构建出你想要的模板。配置如下

{
  plugins: [
    new HtmlWebpackPlugin({
      title: 'your title',
      filename: 'index.html',// 模板要往哪发布
      template: path.resolve(__dirname, '../dist/src/project1/index.html'), // 模板的存放位置
      chunks: [name], // chunks主要用于多入口文件,也就是你引用哪些打包的文件
      hash: true,
      inject: true, // 默认值,决定打包的js在html中的位置,默认在body底部
    })
  ]
}

这样我们就配置完了一个单入口的html-webpack-plugin。

那怎么实现多入口配置呢?答案是有几个入口就new几个HtmlWebpackPlugin,这个是无限制的,但是我们手动去写是不是太麻烦了,程序员嘛,一定要学会使用工具呀!上面我们构建entry的时候就得到了入口文件,那我们是不是也可以仿照上面的方法也构建一个HtmlWebpackPlugin的实例数组呢?为了不浪费性能,我们决定就在一个循环里搞定好了!嗯说干就干,上代码!!

var getEntry = function() {
  var entry = {}
  var htmlPlugins = []
  var files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))//你的入口文件相对于当前的路径
  files.forEach(file => {
    var key = file.split('/').splice(-2, 1)[0]
    entry[key] = file
    htmlPlugins.push(new HtmlWebpackPlugin({
      title: key,
      filename: path.resolve(__dirname, '../dist/' + key + '/index.html'),
      template: path.resolve(__dirname, '../src/' + key + '/index.html'),
      chunk: [key],
      hash: true,
      inject: true
    }))
  })
  return {entry, htmlPlugins}
}
var {htmlPlugins} = getEntry()
module.exports = {
  plugins: [
    ...htmlPlugins
  ]
}

此时你激动地去运行了一下 webpack 确实包如期打好了,但是我们发现里面的js文件貌似路径不太对,恭喜,你又学到了一个新东西,就是publicPath,它是用来为你的资源指定正确的存放位置的。

{
  output: {
    ....
    publicPath: '../', // 因为我们的html和js打包的路径都是基于页面应用project1或者project2路径打包的,所以需要跳出一级,引用路径才能正常访问到。
  }
}

现在HTML,JS都具备了,我们就差一个本地服务就能玩起来了,别急webpack早就帮你安排好了server

4. webpack-dev-server配置

webpack-dev-server可以帮助我们在本地建立一个静态资源服务器,并且为我们提供了HMR热重载技术,使得我们不需要刷新就可以进行组件更新和调试,像极了你在devtools中直接修改代码的样子!是不是很爽

devServer : {
  contentBase: path.resolve(__dirname, '../dist'),// 指定了服务器根目录
  compress: true, // 启用gzip压缩
  hot: true, // 启用HMR
  port: 8080, 
  publicPath: '/', // 如何访问资源总是以'/'开头,需要根据我们的output决定如何配置
}

此时运行 webpack serve,就可以在http://localhost:8080/project1/index.html访问我们打包好的HTML应用了。

5. 基础module配置

此时我们具备了开发多入口多出口的工程化能力了,那我们就可以开发react或者vue了,只要具有相应的loader就好了,在webpack中,每一个文件就是module,而loader其实就是一个函数,可以流式地帮助我们处理文件。

module: {
  rules: [
    {
      test: '/.(js|jsx|ts|tsx)$/',
      exclude: '/node_modules/',
      loader: 'babel-loader'
    },
    {
      test: '/.vue$/',
      loader: 'vue-loader'
    },
    {
      test: '/.(css | scss)$/',
      use: [
        'style-loader',
        'css-loader',
        'sass-loader'
      ]
    },
    {
        test: '/.(jpg|png|jpeg|gif|eot|svg|ttf|woff|woff2)$/',
        loader: 'url-loader'
    }
  ]
},
plugins: [          
  new VueLoaderPlugin(),
]

好了,现在你真的可以去开发项目了!!但是配置还有很大的优化空间

6. 打包优化
  1. 抽离公共依赖

    我们知道,我们一个页面有时候可能会引用多个vue实例化root组件,但是基于现在的打包情况,我们只能把vue或者react打包到应用程序中,整个包体积就会很大,那可不可以多个包公用一个依赖呢,比如我把依赖用CDN的方式引入可以吗,当然,webpack在入口文件递归每一个module的时候,就会组成一个构建树,构建树就叫做chunk,chunk给我们组织好了依赖关系,所以我们也可以通知webpack,树上的哪些东西我们不需要,webpack就会为我们进行tree-shaking把这个依赖从树上摇下来。

    通过配置externals就可以满足需求

    {
      externals: {
        'vue' : 'Vue',
         'vue-router': 'VueRouter',  // key就是你import进来的包的位置,value就是暴露的包名
         
      }
    }
    

    然后我们在html模板中引入对应的js即可,值得注意的是引用时也可以找到性能最佳的引用,例如vue.runtime.min.js,要比我们的开发时构建时esm体积小,性能更好,因为去掉了模板编译的代码,体积小了30%左右,前提是你的vue文件已经被vue-loader编译为了render函数,才可以使用这个运行时。

  2. 抽离CSS

    现在我们的CSS是被style-loader直接打包到了项目js文件中,style-loader主要是帮助我们把css对象转化成字符串并且通过DOM的api追加到head中的,大概原理是这样,细节其实还是有很多考量的。

    那我们为什么要抽离CSS,单纯就是想把CSS拿出来?在JS中放着不也能正常显示麽,费这劲干嘛。这就要说到浏览器的解析机制了,浏览器在得到HTML文档的时候,从上到下HTML解析为DOM,然后生成DOM树,同时解析CSS的时候生成CSSOM,生成CSSOM树,DOM树和CSSOM树结合构建出了render树,render树确定好以后会根据dom和定位构建出布局layout,最后进行paint绘制,当遇到内联的JS时,就会阻塞DOM的解析,而且当上面的CSS没有加载完毕的时候,JavaScript 执行将暂停,直至 CSSOM 就绪(因为JS能查询修改CSSOM和DOM)。重新追加的内嵌的CSS同理会让CSSOM不得不重排或重绘,所以我们找到了问题,我们的JS打包时设置了output.inject = true,所以就会把代码放到body后面,此时DOM已经解析完了,但是我们在JS代码执行的时候,又加了一大段CSS到head中,那浏览器不得不重新走一遍构建流程,不仅如此,用户在没有CSS的时候,看到的就是一块几乎没有意义的首屏,CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。所以我们要把CSS放在head中,并且让他尽快下载并加装到浏览器参与构建CSSOM与渲染,而不是等到最后。

说了这么多题外话,我们看看如何抽离,只需要在loader中新增一个loader处理,并且一定要把style-loader移除,因为你抽离了CSS为单独文件,就没有document对象了,style-loader不仅没有意义而且还会报错。

   {
     test: /.(sc|c)ss$/,
       use: [
         MiniCssExtractPlugin.loader,  
         'css-loader',
         'postcss-loader',
         'sass-loader',
       ]
   },
   .......
   
   plugins: [
     new MiniCssExtractPlugin({filename: '[name]/css/[name].css'}) // path就是默认的output.path
   ]
  1. 清除dist的冗余包
plugins: [
   new CleanWebpackPlugin() // 默认清除output.path
 ]
  1. 我们发现,开发的久了,项目里的入口文件越来越多,打包也变得非常慢,有时候我们只想打包某一个指定目录下的入口文件,并不想全部打包,有办法操作吗?

    当然,我们可以在node中读取环境变量,从而根据命令行下发的指令去构建entry,这样就可以定点打包了。

    具体流程大概是这样:

    命令行输入打包目录 => 通过环境变量取到目录名=>根据目录名在遍历时过滤不需要的entry=>构建出需要打包的entry=>正常打包即可

    代码实现也比较容易

    1.命令行增加后缀: npm run dev --run:project1
    2.process.env.npm_config_argv['original'] 获取到命令值数组
    3.截取得到project1,具体怎么截取很简单这里就不罗列了
    4.写入到环境变量,process.env.BUILD_DIR = 'project1'
    5.let buildFile = process.env.BUILD_DIR
      files = files.filter(file => file.indexOf(buildFile)!==-1)
     过滤一个新的文件集合
    6.进行构建entry
    7.正常打包
    
  2. 定义一些环境变量方便webpack打包时候注入环境变量到我们的js

   plugins: [
  new DefineWebpackPlugins({
        'process.env.ENV_NAME' : """ + process.env.ENV_NAME + """,
         'process.env.BASE_URL' : """ + process.env.BASE_URL + """"
     })
   ]
  1. 把错误信息从控制台或者命令行输出到页面
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');

devServer: {
  ...
   quiet: true,
   overlay: {
     errors: true
   },
  ...
}
  
plugins: [
    new FriendlyErrorsWebpackPlugin(),
]
  1. 写一个webpack插件
  • 一个 JavaScript 函数或 JavaScript 类,用于承接这个插件模块的所有逻辑;
  • 在它原型上定义的 apply 方法,会在安装插件时被调用,并被 webpack compiler 调用一次;
  • 指定一个触及到 webpack 本身的事件钩子,即下文会提及的 hooks,用于特定时机处理额外的逻辑;
  • 对 webpack 实例内部做一些操作处理;
  • 在功能流程完成后可以调用 webpack 提供的回调函数;

插件就是一个函数,函数的原型上要有一个apply方法,webpack会调用该方法,并且把webpack实例传入apply中,webpack执行会有一些事件钩子供我们处理,并且我们可以对实例内部进行操作,还可以执行回调。

根据插件所能触及到的 event hook(事件钩子),对其进行分类。每个 event hook 都被预先定义为 synchronous hook(同步), asynchronous hook(异步), waterfall hook(瀑布), parallel hook(并行),而在 webpack 内部会使用 call/callAsync 方法调用这些 hook。 —— webpack 中文文档

如果你不进一步追究,那么按照如下所示的方式对不同钩子进行 tap 处理即可,其中 tap 方法用于同步处理,异步方式则可以调用 tapAsync 方法或 tapPromise 方法。

class HelloWorldPlugin {
    apply(compiler) {
        console.log(compiler, 'Hello World before!')
        compiler.hooks.done.tap(
            'HelloAsyncPlugin', 
            compilation => {
              console.log("webpack build done.");
            }
        );
    }
}

module.exports = HelloWorldPlugin

最后

webpack为我们提供了十分便利的模块化开发,推动了前端的组件化工程化,依靠流程式和丰富的拓展插件配置的打包模式,在目前来说仍是被各大cli主流使用的打包工具,但是随着ES MODULE的兴起与落地,基于snowpack的vite已经面世,并且效率更高,速度更快,且更为易用,有空我会专门出一期vite的新手配置教程供大家参考,最后放上webpack的全部配置与本地的架构目录。

架构
build
    plugins
    
    env_config
        index.js
    webpack.base.conf.js
src
    project1
        index.js
        index.html
    project2
        index.js
        index.html

npm脚本命令

  "scripts": {
    "lint": "eslint --ext .js,.vue src",
    "start:dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.base.conf.js",
    "watch": "npm run dev --m dev",
    "test": "echo "Error: no test specified" && exit 1",
    "dev": "cross-env NODE_ENV=development webpack --config build/webpack.base.conf.js --progress"
  },
环境变量配置
var env = {
    dev: {
        env_name: 'dev',
        base_url: '//dev.webpack.com/'
    },
    test: {
        env_name: 'test',
        base_url: '//test.webpack.com/'
    },
    build: {
        env_name: 'build',
        base_url: '//build.webpack.com/'
    }
}
let type = JSON.parse(process.env.npm_config_argv)['original'].pop()
let buildDir = ''
if(type.includes('--run:')) {
    buildDir = type.replace('--run:', '')
} 
if('dev_test_build'.includes(type)) {
    type = type.split(':').pop()
} else {
    type = 'dev'
}
console.log(type, 'type')
console.log(process.env.npm_config_argv, 'process.env.npm_config_argv')
process.env.ENV_NAME = env[type || 'dev'].env_name
process.env.BASE_URL = env[type || 'dev'].base_url
process.env.BUILD_DIR = buildDir
module.exports.env = env
webpack.base.conf.js
const path = require('path')
const glob = require('glob')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const {
    VueLoaderPlugin
} = require('vue-loader')

const HelloWorldPlugin = require('./plugins/hell-world-plugin') 

const vueLoaderConfig = require('./vue_config/vue-loader.conf')
require('./env_config/index.js')
function getEntry() {
    let entrys = {}
    let htmlPlugins = []
    let files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))
    let buildFile = process.env.BUILD_DIR
    files = files.filter(file => file.indexOf(buildFile)!==-1)
    files.forEach(item => {
        const name = item.split('/').splice(-2, 1)[0]
        entrys[name] = item
        htmlPlugins.push(new HtmlWebpackPlugin({
            title: name,
            filename: path.resolve(__dirname, '../dist/' + name + '/index.html'),
            chunks: [name],
            hash: true,
            template: path.resolve(__dirname, '../src/' + name + '/index.html'),
            inject: true
        }))
    })
    return {entrys, htmlPlugins}
}
const {entrys, htmlPlugins} = getEntry()
module.exports = (env, option) => {

    return {
        entry: entrys,
        resolve: {
            alias: {
                'vue$': 'vue/dist/vue.esm.js',
                '@' : path.resolve(__dirname, '../src/')
            },
            extensions: ['.js', '.jsx', '.vue']
        },
        output: {
            filename: '[name]/js/vendor.[hash].js',
            path: path.resolve(__dirname, '../dist'),
            publicPath: '../'
        },
        devServer: {
            contentBase: path.resolve(__dirname, '../dist'),
            compress: true,
            port: 8080,
            hot: true,
            quiet: true,
            overlay: {
                errors: true
            },
            publicPath: '/',
            after() {
                console.log(`服务已启动---运行在 http://localhost:8080`)
            }
        },
        module: {
            rules: [
                {
                    test: /.(js|jsx|ts|tsx)$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader',
                    options: {
                        // presets: ['@babel/preset-env', '@babel/preset-react', '@vue/babel-preset-jsx']
                    }
                },
                {
                    test: /.vue$/,
                    loader: 'vue-loader',
                    // options: vueLoaderConfig
                },
                {
                    test: /.(sc|c)ss$/,
                    use: [
                        MiniCssExtractPlugin.loader,  
                        'css-loader',
                        'postcss-loader',
                        'sass-loader',
                        ]
                },
                {
                  test: /.(jpg|png|jpeg|gif|eot|svg|ttf|woff|woff2)$/,
                  loader: "url-loader"
                },
            ]
        },
        plugins: [
            new CleanWebpackPlugin(),
            new FriendlyErrorsWebpackPlugin(),
            new VueLoaderPlugin(),
            new webpack.DefinePlugin({
                'process.env.ENV_NAME' : """ + process.env.ENV_NAME + """,
                'process.env.BASE_URL' : """ + process.env.BASE_URL + """
            }),
            new HelloWorldPlugin({options: true}),
            new MiniCssExtractPlugin({filename: '[name]/css/[name].css'}),
            ...htmlPlugins
            
        ],
        externals: {
            'vue': 'Vue',
            'vue-router': "VueRouter"
        }

    }
}
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。