基于 webpack 的 vue 项目编译打包最佳实践工具
postcss
less
sass
stylus
css modules
pages
@vue/cli-service/bin/vue-cli-service.js
接收终端指令@vue/cli-service/lib/Service.js
‘终端指令’到‘用户配置’到‘插件’的处理层
@vue/cli-service
中默认的command
和config
package.json
中
devDependencies
和dependencies
中匹配/^(@vue\/|vue-|@[\w-]+(\.)?[\w-]+\/vue-)cli-plugin-/
的vuePlugins.service
@vue/cli-service/lib/PluginAPI.js
做 server 和 plugin 的中间层,做一定的隔离效果{
// 编译模式,webpack提供了预配置
mode: 'development',
// 工程上下文
context: process.cwd(),
// 入口文件
entry: {
app: './src/main.js',
},
// c出口
output: {
path: 'dist',
filename: '[name].js',
publicPath: '/',
},
// 模块加载路径处理
resolve: {
plugin: [
...require('pnp-webpack-plugin')
],
extensions: ['.mjs', '.js', '.jsx', '.vue', '.json', '.wasm'],
modules: [
'node_modules'
],
alias: [
'@': 'src',
'vue$': 'vue/dist/vue.esm.js'
]
},
resolveLoader: {
plugin: [
...require('pnp-webpack-plugin').topLevelLoader
],
modules: [
'node_modules'
]
},
module: {
noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/,
rule: {
'vue': {
test: /\.vue$/,
use: ['cache-loader', 'vue-loader'],
},
'images': {
test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
use: ['url-loader'],
},
'svg': {
test: /\.(svg)(\?.*)?$/,
use: ['file-loader'],
},
'media': {
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use: ['url-loader'],
},
'fonts': {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use: ['url-loader'],
},
'pug': {
test: /\.pug$/,
oneOf: {
'pug-vue': {
resourceQuery: /vue/,
use: ['pug-plain-loader'],
},
'pug-template': {
use: ['raw-loader', 'pug-plain-loader']
}
}
},
// 由`package.json`中`devDependencies`或`dependencies`配置`@vue/cli-plugin-babel`
'js': {
test: /\.m?jsx?$/,
use: ['cache-loader', 'thread-loader', 'babel-loader']
}
}
},
optimization: {
minimizer: 'terser-webpack-plugin',
splitChunks: {
cacheGroups: {
vendors: {
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: `chunk-common`,
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
},
// 源码映射
devtool: 'cheap-module-eval-source-map',
plugin: [
'vue-loader',
// 模块热替换
'webpack/lib/HotModuleReplacementPlugin',
// 编译进度
'webpack/lib/ProgressPlugin',
require('webpack').DefinePlugin,
// 区分大小写的路径
'case-sensitive-paths-webpack-plugin',
'@soda/friendly-errors-webpack-plugin',
'html-webpack-plugin',
['@vue/preload-webpack-plugin', [{
rel: 'preload',
include: 'initial',
fileBlacklist: [/\.map$/, /hot-update\.js$/]
}]],
['@vue/preload-webpack-plugin', [{
rel: 'prefetch',
include: 'asyncChunks'
}]],
['copy-webpack-plugin']
],
devServer: {
// 是否使用 https 协议
https: false,
// 域名
host: '0.0.0.0',
// 端口
port: 8080,
// 代理
proxy: null,
// 日志等级
logLevel: 'silent',
clientLogLevel: 'silent',
// 使用html5 history接口,用index.html响应404请求
historyApiFallback: {
// 请求中有.符号时,不使用index.html
disableDotRule: true,
// 用于响应的index.html文件,这里支持匹配多页面模式对应的多html情况
rewrites: []
},
// 静态资源基本上下文
contentBase: 'public',
// 观察静态资源文件的变化
watchContentBase: true,
// 启用热更新
hot: true,
// 不启用gzip压缩
compress: false,
// 获取静态资源的请求路径
publicPath: '/',
// 异常时,在整个页面上展示
overlay: { warnings: false, errors: true },
// 不启动自动在浏览器上打开
open: false,
}
}
// 在文件 `babel.config.json` 或 `.babelrc.json` 配置 `presets: ['@vue/cli-plugin-babel/preset']`
// 即 `@vue/babel-preset-app/index.js`
// 通过 `process.env` 环境变量跨工程‘通信’
const envOptions = {
bugfixes: true,
corejs: require('core-js/package.json').version,
loose: false,
debug: false,
modules: false,
// 目标浏览器,默认取`.browserslistrc`文件/`package.json`的`browserslist`配置项
targets: [
"> 1%",
"last 2 versions",
"not dead"
],
useBuiltIns: true,
exclude: [
// promise polyfill alone doesn't work in IE,
// needs this as well. see: #1642
require('core-js-compat').data['es.array.iterator'],
// this is required for webpack code splitting, vuex etc.
require('core-js-compat').data['es.promise'],
// this is needed for object rest spread support in templates
// as vue-template-es2015-compiler 1.8+ compiles it to Object.assign() calls.
require('core-js-compat').data['es.object.assign'],
// #2012 es.promise replaces native Promise in FF and causes missing finally
require('core-js-compat').data['es.promise.finally']
],
}
{
// 源码类型:如果存在import/export语句,则将文件视为“模块” ,否则将其视为“脚本”
sourceType: 'unambiguous',
// 合并当前配置,主要是为了把`@babel/runtime`拎出来处理
overrides: [
{
exclude: [/@babel[\/|\\\\]runtime/, /core-js/],
presets: [
[require('@babel/preset-env'), envOptions]
],
plugins: [
// 小于等于 stage-3 的插件
// 异步import
require('@babel/plugin-syntax-dynamic-import'),
[require('@babel/plugin-proposal-decorators'), {
decoratorsBeforeExport,
legacy: decoratorsLegacy !== false
}],
[require('@babel/plugin-proposal-class-properties'), { loose }],
[require('@babel/plugin-transform-runtime'), {
regenerator: useBuiltIns !== 'usage',
// polyfills are injected by preset-env & polyfillsPlugin, so no need to add them again
corejs: false,
helpers: useBuiltIns === 'usage',
useESModules: !process.env.VUE_CLI_BABEL_TRANSPILE_MODULES,
absoluteRuntime,
version
}],
]
},
{
// there are some untranspiled code in @babel/runtime
// https://github.com/babel/babel/issues/9903
include: [/@babel[\/|\\\\]runtime/],
presets: [
[require('@babel/preset-env'), envOptions]
]
}
]
}
现代模式 按两个线程进行编译(
@vue/cli-service/lib/commands/build/index.js
---Ln55)
差异:
实现:
VUE_CLI_MODERN_MODE``VUE_CLI_MODERN_BUILD
做标识,区分线程类型outputFilename
加后缀-legacy
html-webpack-plugin
的 htmlWebpackPluginAlterAssetTags
保存要插入的内容clean:false
,即保留上面的编译结果;相应的,相同文件将被覆盖babel
配置useESModules:true
,不使用@babel/plugin-transform-modules-commonjs
babel
配置ignoreBrowserslistConfig:true
,不使用默认的browserslisthtml-webpack-plugin
的 htmlWebpackPluginAlterAssetTags
在默认的配置下,优化编译性能。
在package.json
增加插件配置:
{
"vuePlugins": {
"service": [
"./my/vue/plugin.js"
]
}
}
module.exports = (api) => {
api.chainWebpack(webpackConfig => {
// receive the chainable webpack config
})
api.configureWebpack(webpackConfig => {
// receive the raw webpack config
})
}
const cssVersion = {
'css-loader': require('css-loader/package.json').version,
'postcss-loader': require('postcss-loader/package.json').version
}
const vueVersion = {
...cssVersion,
'vue-loader': require('vue-loader/package.json').version,
'@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version,
'vue-template-compiler': require('vue-template-compiler/package.json').version
}
const lessVersion = {
...cssVersion,
'less-loader': require('less-loader/package.json').version
}
const lessVueVersion = {
...vueVersion,
'less-loader': require('less-loader/package.json').version
}
/* eslint-disable indent */
webpackConfig.module.rule('less')
.oneOf('vue-modules')
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.after('extract-css-loader')
.options(api.genCacheConfig('less-vue-modules-loader', lessVueVersion))
.end()
.end()
.oneOf('vue')
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.after('extract-css-loader')
.options(api.genCacheConfig('less-vue-loader', lessVueVersion))
.end()
.end()
.oneOf('normal')
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.after('extract-css-loader')
.options(api.genCacheConfig('less-normal-loader', lessVersion))
webpackConfig.module.rule('css')
.oneOf('vue-modules')
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.after('extract-css-loader')
.options(api.genCacheConfig('css-vue-modules-loader', vueVersion))
.end()
.end()
.oneOf('vue')
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.after('extract-css-loader')
.options(api.genCacheConfig('css-vue-loader', vueVersion))
.end()
.end()
.oneOf('normal')
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.after('extract-css-loader')
.options(api.genCacheConfig('css-normal-loader', cssVersion))
css
less-loader
不会处理文件中的@import('xxx.css')
,由css-loader
拼接loader处理。
这个过程无法将cache-loader
加入。
optimize-css
增加缓存@intervolga/optimize-cssnano-plugin@1.0.5
没有支持缓存的功能,
可以替换插件,这里以css-minimizer-webpack-plugin
为例。
if (webpackConfig.plugins.has('optimize-css')) {
webpackConfig
.plugins
.delete('optimize-css')
webpackConfig
.plugin('css-minimizer-webpack-plugin')
.use(require.resolve('css-minimizer-webpack-plugin'))
}
← url-loader vue-loader →