最近在做项目的时候遇到了一个场景:一个项目有多个入口,不同的入口,路由、组件、资源等有重叠部分,也有各自不同的部分。由于不同入口下的路由页面有一些是重复的,因此我考虑使用 Webpack 多入口配置来解决这个需求。
再一次,在网上找的不少文章都不合我的需求,很多文章都是只简单介绍了生产环境下配置,没有介绍开发环境下的配置,有的也没有将多入口结合vue-router、vuex、ElementUI等进行配置,因此在下通过不断探坑,然后将思路和配置过程记录下来,留给自己作为笔记,同时也分享给大家,希望可以帮助到有同样需求的同学们~
一个项目中保存了多个 HTML 模版,不同的模版有不同的入口,并且有各自的 router、store 等;
不仅可以打包出不同 HTML,而且开发的时候也可以顺利进行调试;
不同入口的文件可以引用同一份组件、图片等资源,也可以引用不同的资源;
代码仓库:multi-entry-vue
示意图如下:
首先我们vue init webpack multi-entry-vue使用vue-cli创建一个 webpack 模版的项。文件结构如下:
. ├── build ├── config ├── src │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ ├── router │ │ └── index.js │ ├── App.vue │ └── main.js ├── static ├── README.md ├── index.html ├── package-lock.json └── package.json
这里顺便介绍在不同系统下生成目录树的方法:
mac 系统命令行生成目录树的方法tree-I node_modules--dirsfirst,这个命令的意思是,不显示node_modules路径的文件,并且以文件夹在前的排序方式生成目录树。如果报没有找到 tree 命令的错,安装 tree 命令行brew install tree即可。
windows 系统在目标目录下使用tree/f1.txt即可把当前目录树生成到一个新文件1.txt中。
首先我们简单介绍一下 Webpack 的相关配置项,这些配置项根据使用的 Webpack 模版不同,一般存放在webpack.config.js或webpack.base.conf.js中:
const path = require('path') module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, output: { path: path.resolve(__dirname, '../dist'), filename: 'output-file.js', publicPath: '/' }, module: {}, // 文件的解析 loader 配置 plugins: [], // 插件,根据需要配置各种插件 devServer: {} // 配置 dev 服务功能 }
这个配置的意思是,进行 Webpack 后,会在命令的执行目录下新建dist目录(如果需要的话),并将打包src目录下的main.js和它的依赖,生成output-file.js放在dist目录中。
下面稍微解释一下相关配置项:
entry: 入口文件配置项,可以为字符串、对象、数组。以上面的对象形式为例,app是入口名称,如果output.filename中有[name]的话,就会被替换成app。
context: 是 webpack 编译时的基础目录,用于解析entry选项的基础目录(绝对路径),entry入口起点会相对于此目录查找,相当于公共目录,下面所有的目录都在这个公共目录下面。
output: 出口文件的配置项。
output/path: 打包文件输出的目录,比如上面的dist,那么就会将输出的文件放在当前目录同级目录的dist文件夹下,没有这个文件夹就新建一个。可以配置为path.resolve(__dirname,'./dist/${Date.now()}/')(md 语法不方便改成模板字符串,请自行修改)方便做持续集成。
output.filename: 输出的文件名称,[name]的意为根据入口文件的名称,打包成相同的名称,有几个入口,就可以打包出几个文件。比如入口的key为app,打包出来就是app.js,入口是my-entry,打包出来就是my-entry.js。
output.publicPath: 静态资源的公共路径,可以记住这个公式:静态资源最终访问路径=output.publicPath+资源loader或插件等配置路径。举个例子,publicPath配置为/dist/,图片的url-loader配置项为name:'img/[name].[ext]',那么最终打包出来文件中图片的引用路径为output.publicPath+'img/[name].[ext]'='/dist/img/[name].[ext]'。
本文由于是入口和出口相关的配置,所以内容主要围绕着entry、output和一个重要的 webpack 插件 html-webpack-plugin,这个插件是跟打包出来的 HTML 文件密切相关,主要有下面几个作用:
根据模版生成 HTML 文件;
给生成的 HTML 文件引入外部资源比如link、script等;
改变每次引入的外部文件的 Hash,防止 HTML 引用缓存中的过时资源;
下面我们从头一步步配置一个多入口项目。
在src目录下将main.js和App.vue两个文件各复制一下,作为不同入口,文件结构变为:
. ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js # 主要配置目标 │ └── webpack.prod.conf.js # 主要配置目标 ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── src │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ ├── router │ │ └── index.js │ ├── App.vue │ ├── App2.vue # 新增的入口 │ ├── main.js │ └── main2.js # 新增的入口 ├── static ├── README.md ├── index.html └── package.json
要想从不同入口,打包出不同 HTML,我们可以改变一下entry和output两个配置,
// build/webpack.prod.conf.js module.exports = { entry: { entry1: './src/main.js', entry2: './src/main2.js' }, output: { filename: '[name].js', publicPath: '/' }, plugins: [ new HtmlWebpackPlugin({ template: "index.html", // 要打包输出哪个文件,可以使用相对路径 filename: "index.html" // 打包输出后该html文件的名称 }) ] }
根据上面一小节我们知道,webpack 配置里的output.filename如果有[name]意为根据入口文件的名称,打包成对应名称的 JS 文件,那么现在我们是可以根据两个入口打包出entry.js和entry2.js。
打包的结果如下:
当前代码:Github - multi-entry-vue1
如上图,此时我们npm run build打包出一个引用了这两个文件的index.html,那么如何打包出不同 HTML 文件,分别应用不同入口 JS 文件呢,此时我们需要借助于HtmlWebpackPlugin这个插件。
HtmlWebpackPlugin这个插件,new一个,就打包一个 HTML 页面,所以我们在plugins配置里new两个,就能打包出两个页面来。
我们把配置文件改成下面这样:
// build/webpack.prod.conf.js module.exports = { entry: { entry: './src/main.js', // 打包输出的chunk名为entry entry2: './src/main2.js' // 打包输出的chunk名为entry2 }, output: { filename: '[name].js', publicPath: '/' }, plugins: [ new HtmlWebpackPlugin({ filename: 'entry.html', // 要打包输出的文件名 template: 'index.html', // 打包输出后该html文件的名称 chunks: ['manifest', 'vendor', 'entry'] // 输出的html文件引入的入口chunk // 还有一些其他配置比如minify、chunksSortMode和本文无关就省略,详见github }), new HtmlWebpackPlugin({ filename: 'entry2.html', template: 'index.html', chunks: ['manifest', 'vendor', 'entry2'] }) ] }
上面一个配置要注意的是chunks,如果没有配置,那么生成的 HTML 会引入所有入口 JS 文件,在上面的例子就是,生成的两个 HTML 文件都会引入entry.js和entry2.js,所以要使用chunks配置来指定生成的 HTML 文件应该引入哪个 JS 文件。配置了chunks之后,才能达到不同的 HTML 只引入对应chunks的 JS 文件的目的。
大家可以看到除了我们打包生成的chunk文件entry.js和entry2.js之外,还有manifest和vendor这两个,这里稍微解释一下这两个chunk:
vendor是指提取涉及node_modules中的公共模块;
manifest是对vendor模块做的缓存;
打包完的结果如下:
文件结构:
现在打包出来的样式正是我们所需要的,此时我们在dist目录下启动live-server(如果你没安装的话可以先安装npm i-g live-server),就可以看到效果出来了:
当前代码:Github - multi-entry-vue2
至此就实现了一个简单的多入口项目的配置。
我们在前文进行了多入口的配置,要想新建一个新的入口,就复制多个文件,再手动改一下对应配置。
但是如果不同的 HTML 文件下不同的vue-router、vuex都放到src目录下,多个入口的内容平铺在一起,项目目录会变得凌乱不清晰,因此在下将多入口相关的文件放到一个单独的文件夹中,以后如果有多入口的内容,就到这个文件夹中处理。
下面我们进行文件结构的改造:
首先我们在根目录创建一个entries文件夹,把不同入口的router、store、main.js都放这里,每个入口相关单独放在一个文件夹;
在src目录下建立一个common文件夹,用来存放多入口共用的组件等;
现在的目录结构:
. ├── build # 没有改动 ├── config # 没有改动 ├── entries # 存放不同入口的文件 │ ├── entry1 │ │ ├── router # entry1 的 router │ │ │ └── index.js │ │ ├── store # entry1 的 store │ │ │ └── index.js │ │ ├── App.vue # entry1 的根组件 │ │ ├── index.html # entry1 的页面模版 │ │ └── main.js # entry1 的入口 │ └── entry2 │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ ├── App.vue │ ├── index.html │ └── main.js ├── src │ ├── assets │ │ └── logo.png │ ├── common # 多入口通用组件 │ │ └── CommonTemplate.vue │ └── components │ ├── HelloWorld.vue │ ├── test1.vue │ └── test2.vue ├── static ├── README.md ├── index.html ├── package-lock.json └── package.json
然后我们在build/utils文件中加两个函数,分别用来生成 webpack 的entry配置和HtmlWebpackPlugin插件配置,由于要使用node.js来读取文件夹结构,因此需要引入fs、glob等模块:
// build/utils const fs = require('fs') const glob = require('glob') const merge = require('webpack-merge') const HtmlWebpackPlugin = require('html-webpack-plugin') const ENTRY_PATH = path.resolve(__dirname, '../entries') // 多入口配置,这个函数从 entries 文件夹中读取入口文件,装配成webpack.entry配置 exports.entries = function() { const entryFiles = glob.sync(ENTRY_PATH + '/*/*.js') const map = {} entryFiles.forEach(filePath => { const filename = filePath.replace(/.*\/(\w+)\/\w+(\.html|\.js)$/, (rs, $1) => $1) map[filename] = filePath }) return map } // 多页面输出模版配置 HtmlWebpackPlugin,根据环境装配html模版配置 exports.htmlPlugin = function() { let entryHtml = glob.sync(ENTRY_PATH + '/*/*.html') let arr = [] entryHtml.forEach(filePath => { let filename = filePath.replace(/.*\/(\w+)\/\w+(\.html|\.js)$/, (rs, $1) => $1) let conf = { template: filePath, filename: filename + '.html', chunks: [filename], inject: true } // production 生产模式下配置 if (process.env.NODE_ENV === 'production') { conf = merge(conf, { chunks: ['manifest', 'vendor'], minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true }, chunksSortMode: 'dependency' }) } arr.push(new HtmlWebpackPlugin(conf)) }) return arr }
稍微解释一下这两个函数:
exports.entries函数从entries文件夹中找到二级目录下的 JS 文件作为入口文件,并且将二级目录的文件夹名作为key,生成这样一个对象:{"entry1":"/multi-entry-vue/entries/entry1/main.js"},多个入口情况下会有更多键值对;
exports.htmlPlugin函数和之前函数的原理类似,不过组装的是HtmlWebpackPlugin插件的配置,生成这样一个数组,可以看到和我们手动设置的配置基本一样,只不过现在是根据文件夹结构来生成的:
// production 下 [ { template: "/multi-entry-vue/entries/entry1/index.html", chunks: ['manifest', 'vendor', 'entry1'], filename: "entry1.html", chunksSortMode: 'dependency' }, { ... } // 下一个入口的配置 ]
有了这两个根据entries文件夹的结构来自动生成 webpack 配置的函数,下面来改一下 webpack 相关的几个配置文件:
// build/webpack.base.conf.js module.exports = { entry: utils.entries(), // 使用函数生成 entry 配置 output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath } }
// build/webpack.dev.conf.js // const HtmlWebpackPlugin = require('html-webpack-plugin') // 不需要了 const devWebpackConfig = merge(baseWebpackConfig, { devServer: { historyApiFallback: { rewrites: [ // 别忘了把 devserver 的默认路由改一下 { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'entry1.html') }, ], } }, plugins: [ // https://github.com/ampedandwired/html-webpack-plugin // new HtmlWebpackPlugin({ // filename: 'index.html', // template: 'index.html', // inject: true // }), // 注释掉原来的 HtmlWebpackPlugin 配置,使用生成的配置 ].concat(utils.htmlPlugin()) })
// build/webpack.prod.conf.js // const HtmlWebpackPlugin = require('html-webpack-plugin') const webpackConfig = merge(baseWebpackConfig, { plugins: [ // new HtmlWebpackPlugin({ // ... 注释掉,不需要了 // }), ].concat(utils.htmlPlugin()) })
现在我们再npm run build,看看生成的目录是什么样的:
此时我们在dist目录下启动live-server看看是什么效果:
当前代码:Github - multi-entry-vue3
网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~
参考:
webpack解惑:多入口文件打包策略
webpack配置文件:入口和出口,多入口、多出口配置
一看就懂之webpack高级配置与优化