最近需要新开一个项目,用于公众号的基础服务搭建,可能对接多个平台。
项目各页面基本无关联,需要多个 HTML 文件,多个入口 JS,看了一下网上没有类似的比较全面的 Webpack 配置教程,所以写一篇较为详细的配置过程,自己去写一个 Demo。
项目 Demo 在这里,release@4/babel7 分支下 => 多入口 Webpack 配置
环境(MAC)
node 10.13.0
npm 6.4.1
webpack 4+
babel 7
项目 Init
首先需要全局安装 node、npm、webpack、webpack-cli 不细说
本文安装使用 cnpm,可以自行 Google 设置
然后初始化项目,按照自己的需求填写配置。
$ npm init
我这里初始化后 package.json 中有很多包,感觉没有什么用处都删除掉了,如下图。
修改后的 package.json 如下
{
"name": "module_web",
"version": "1.0.0",
"description": "A web project",
"author": "WenJW <***@163.com>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"test": "webpack",
"build": "node build/build.js"
},
"dependencies": {
},
"devDependencies": {
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"repository": {
"type": "git",
"url": "git+https://github.com/lwpersonal/module_Vue2.git"
},
"license": "ISC",
"bugs": {
"url": "https://github.com/lwpersonal/module_Vue2/issues"
},
"homepage": "https://github.com/lwpersonal/module_Vue2#readme"
}
下面先创建一些会用到的项目配置文件和目录
某些文件不会详细说明,可以自行查阅相关资料,一些重要的文件后面会详细介绍
root
+-- build // 项目打包配置目录
+-- config // 项目配置文件
+-- dist // 打包后的文件输出目录
+-- src // 前端代码
+-- service // 后端代码
+-- .babelrc // babel 配置文件
+-- .eslintignore // 忽略 eslint 检测
+-- .eslintrc.js // eslint 配置
+-- .gitignore // 忽略 git 检测
+-- .npmrc // npm 配置
+-- .nvmrc // node 版本 控制
+-- package.json
+-- ...
然后项目目录下安装 webpack、webpack-cli
$ cnpm i -D webpack webpack-cli
// 这里简单说一下安装命令的简写和规范
// i => install
// -D => --save-dev
// -S => --save
// -S 会安装到 dependencies 属性下
// -D 安装到 devDependencies 属性下
// 规范是,-S 安装项目中会用到的代码,例如 vue、react、lodash 等
// -D 安装构建代码,不会在项目代码中使用,例如 webpack、css-loader 等
我们可以先简单测试一下 webpack 的命令
在 src 中创建一个 index.js 的入口文件
这里说明一下,webpack4 的默认入口文件,为项目 src 目录下的 index.js,如果没有此文件,会报错;默认输出为 dist/main.js
// /src/index.js
const a = 4
然后执行命令
$ webpack --mode production
在 dist 目录下可以看到输出的 main.js 文件,里面是构建后的代码,项目的基本结构就是这样了。
下面就进行详细的配置了,我们可以想一下项目中会打包什么文件?
- html
- es6
- css预编译,scss、less
- 图片字体资源
- ……
我们一步步完善 webpack 的配置去处理这些模块
build
build 目录的拆分与 vue-cli 一致
// build
build.js // 生产环境打包入口
check-versions.js // npm 包版本检测
utils.js // 公共模块
vue-loader.conf.js
webpack.base.conf.js // development、production 环境公共配置
webpack.dev.conf.js // dev 配置
webpack.prod.conf.js // prod 配置
下面我们先配置 dev 的打包环境
创建多个入口文件
目标是输出结果互相不耦合,且 html 可以正确引入对应的的 js 文件
首先在 webpack.dev.conf.js 中简单的进行配置
'use strict'
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const resolve = (dir) => {
return path.join(__dirname, '..', dir)
}
const webpackConfig = {
mode: 'development', // webpack4 需要指定编译环境
context: path.resolve(__dirname, '../'), // 下面的配置会以这里为解析入口点
entry: {
app1: './src/pages/app1/index.js', // 前面的属性 app1、app2 是下面的 [name],chunks
app2: './src/pages/app2/index.js'
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name]/[name].bundle.js',
publicPath: '/' // 此选项指定在浏览器中引用时输出目录的公共URL
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
}
]
},
plugins: [
// 多个文件需要多次实例化
new HtmlWebpackPlugin({
filename: './app1/index.html', // 输出目录
template: './src/pages/app1/index.html', // 模版文件
inject: true, // 引入 js 的位置,body 底部
chunks: ['app1'] // 只引入 app1 的 js 代码,这里必须是一个数组,单个的字符串会全部引入
}),
new HtmlWebpackPlugin({
filename: './app2/index.html',
template: './src/pages/app2/index.html',
inject: true,
chunks: ['app2']
}),
],
node: {
setImmediate: false,
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
module.exports = webpackConfig
这里使用 babel7 去编译 ES6 的代码,需要配置 .babelrc 文件
首先安装对应的插件
$ cnpm i -D html-webpack-plugin
$ cnpm i -D babel-loader
$ cnpm i -D @babel/runtime
$ cnpm i -D @babel/preset-env
$ cnpm i -D @babel/plugin-transform-runtime
$ cnpm i -D @babel/core
然后配置
// .babelrc
// targets, useBuiltIns 等选项用于编译出兼容目标环境的代码
// 其中 useBuiltIns 如果设为 "usage"
// Babel 会根据实际代码中使用的 ES6/ES7 代码,以及与你指定的 targets,按需引入对应的 polyfill
// 而无需在代码中直接引入 import '@babel/polyfill',避免输出的包过大,同时又可以放心使用各种新语法特性。
{
"presets": [
["@babel/preset-env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
},
"useBuiltIns": "usage"
}]
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
我们改造一下我们的示例文件,在 js 中写一些 ES6 的代码
然后编译运行一下
// 加 --config 可以查看详细的报错信息,便于排查错误
$ webpack --config build/webpack.dev.conf.js
可以看到成功编译的信息
我们在 dist 目录下开一个服务器去测试
$ cd dist/
$ http-server -p10099 -o
// 这里安利一个很好用的插件,http-server,可以很方便的开一个测试服务器
执行命令后会自动打开浏览器,localhost:10099,我这里由于修改了 host 所以会不同
点击对应的路由可以访问对应的页面,这里我们进入到 app2,可以看到代码已经正确编译为 ES5
不同的 html 文件也正确的引入了对应 chunks 的 js
这里构建的基本流程已经完成了,当然现在的代码比较混乱,后面会陆续对代码进行抽离,完善项目的构建流程。
上面的配置遇到的较为麻烦的错误是这个,找了很多资料都没有解决。我这里是由于配置项的数据类型错误导致的问题。
后面用 --config 重新跑了一下就定位到问题了。所以遇到错误还是多看日志去慢慢排查。
完善构建流程
我们把 dev 构建中的通用配置抽离,然后写入 webpack.base.conf.js 文件中
仔细分析可以发现,一些配置例如:entry、output、module等大部分是可以通用的
plugins 插件这一块大部分都需要分别去配置
还有一些公共的函数和项目端口配置等,都是可以抽离的
下面的大部分都是由 vue-cli 改造来的
具体体现在 build 和 config 目录下,例如 config 下的 dev.env.js、prod.env.js 等都是没有改变的,具体可以去我的 Demo 中查看代码,下面只会提到改动和新增的部分
首先我们把 config 配置抽离,创建不同环境下的 config 文件
// config/config.dev.js
"use strict"
module.exports = {
"port": 10011,
"server": {
"port": 10012
},
cdn: '',
"timeOut": 10000,
“api”: {
...
}
}
// config.prod.js 文件类似,根据情况设置线上的 api 和 cdn 等
第一次启动项目时,复制一份 dev 的配置到 config/config.js,后续在配置中引入 config.js
这里需要注意,因为 config.js 文件,线上和本地是区分的,所以需要在 .gitignore 文件中去掉 git 检测
$ cp ./config/config.dev.js ./config/config.js
由于是多个入口,且后续可能会继续添加,我们写一个入口文件的配置,方便后面去维护
./config/app_list.js
'use strict'
// pages 目录下
module.exports = {
app1: {
html: 'app1/index.html',
js: 'app1/index.js'
},
app2: {
html: 'app2/index.html',
js: 'app2/index.js'
}
}
项目根目录下添加了一个 postcss.config.js,否则新版的 webpack 会报错
/postcss.config.js
module.exports = {
plugins: {
'autoprefixer': {
browsers: 'last 5 version'
}
}
}
将一些打包配置抽离到 config/index.js
可以根据需求自行更改
// config/index.js
'use strict'
const path = require('path')
const config = require('./config')
module.exports = {
base: {
pathPrefix: './src/pages/' // 打包入口目录
},
dev: {
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: config.port, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
改动 utils.js 文件,加入我们自己的配置
// build/utils.js
'use strict'
const path = require('path')
const config = require('../config')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const packageConfig = require('../package.json')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const appList = require('../config/app_list')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return [MiniCssExtractPlugin.loader].concat(loaders)
} else {
return ['style-loader'].concat(loaders)
}
}
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
// 系统提示弹窗
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
// ********
// 这里是我们自定义的配置
const createHtmlWebpackConfig = (template, chunkname, pluginConfig) => {
return new HtmlWebpackPlugin({
filename: `./${chunkname}/index.html`,
template,
inject: true,
favicon: '',
chunks: [chunkname], // 必须放在数组中,单个字符串不生效
...pluginConfig
})
}
// HtmlWebpackConfig 配置
exports.getAllHtmlWebpackConfig = (pluginConfig = {}) => {
let res = []
Object.keys(appList).map(key => {
const item = appList[key]
return res.push(createHtmlWebpackConfig(config.base.pathPrefix + item.html, key, pluginConfig))
})
return res
}
// webpack 入口配置
exports.getEntryWebpackConfig = () => {
let res = {}
Object.keys(appList).map(key => {
const item = appList[key]
return Object.assign(res, { [key]: item.js })
})
return res
}
然后完善 webpack.base.conf.js 文件
// build/webpack.base.conf.js
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const appList = require('../config/appList')
const { VueLoaderPlugin } = require('vue-loader')
const resolve = (dir) => {
return path.join(__dirname, '..', dir)
}
// 根据 appList 获取 entry 的配置
const getEntryCOnfig = () => {
const res = {}
const rootPrefix = './src/pages'
Object.keys(appList).map(key => {
Object.assign(res, { [key]: `${rootPrefix}/${appList[key].js}` })
})
return res
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
const webpackConfig = {
context: path.resolve(__dirname, '../'),
entry: getEntryCOnfig(),
output: {
path: config.build.assetsRoot,
filename: '[name]/[name].bundle.js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json', 'less'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
}
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
plugins: [
new VueLoaderPlugin()
],
node: {
setImmediate: false,
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
module.exports = webpackConfig
更改 webpack.dev.conf.js 文件
// build/webpack.dev.conf.js
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
mode: 'development',
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
disableHostCheck: true,
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
// 这里使用我们封装的方法去生成配置
...(utils.getAllHtmlWebpackConfig())
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
此时我们需要下面这些插件
"dependencies": {
"axios": "^0.18.0",
"lodash": "^4.17.11",
"vue": "^2.5.17",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-transform-runtime": "^7.1.0",
"@babel/preset-env": "^7.1.6",
"@babel/runtime": "^7.1.5",
"autoprefixer": "^9.3.1",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.4",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^1.0.1",
"eslint": "^5.9.0",
"eslint-config-standard": "^12.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^4.0.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.4",
"node-notifier": "^5.3.0",
"node-sass": "^4.10.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"ora": "^3.0.0",
"portfinder": "^1.0.13",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-url": "^8.0.0",
"rimraf": "^2.6.0",
"sass-loader": "^7.0.3",
"sass-resources-loader": "^2.0.0",
"semver": "^5.3.0",
"shelljs": "^0.8.3",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"vue-loader": "^15.4.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.25.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10",
"webpack-merge": "^4.1.4"
},
修改 package.json 加入环境变量
"scripts": {
"dev": "NODE_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "NODE_ENV=production node build/build.js"
},
终端运行
$ npm run dev
然后就可以看到熟悉的界面了
我们打开浏览器,在配置的端口查看页面,然后输入对应的路由
可以看到我们配置的多个入口渲染正确,且引入的 js 正确
简单修改 js 测试文件,可以看到 webpack-dev-server 运行也没有问题,可以对页面热更新
下面我们把测试的 Demo 完善,然后测试一下其他资源解析,加入 scss 和 图片资源,然后打包
运行没有问题,这里基本配置就完成了
线上环境的配置,主要问题集中在代码分割上,后续会详细讲解一下