项目升级到vue3之后build执行两遍打包

vue2 项目升级到vue3之后npm run build执行两遍打包

实际是在 @vue/cli-service升级到5.0版本之后出现的问题

先说解决方法

两种办法

  1. 执行 build 的时候加一个 --no-module
1
vue-cli-service build --no-module
  1. 修改 browserslist,一般在 package.json 中或者单独的 .browserslistrc 文件中,添加一个 not ie 11

package.json

1
2
3
4
5
6
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]

.browserslistrc

1
2
3
4
> 1%
last 2 versions
not dead
not ie 11

分析原因

通过执行 npm run build 的时候打印的日志可以发现两次打包之前都输出了不一样的日志

1
Building legacy bundle for production...
1
Building module bundle for production...

正常只执行一次的打包只会输出一种日志

1
Building for production...

然后我们根据日志输出的关键字在 @vue/cli-service 项目中查找一下,我们执行的是 build 命令,所以先看这个命令的文件 @vue/cli-service/lib/commands/build/index.js,搜索一下关键字 legacy bundle 会查找到第 116 行

1
2
3
4
5
6
7
8
if (args.target === 'app') {
const bundleTag = args.needsDifferentialLoading
? args.moduleBuild
? `module bundle `
: `legacy bundle `
: ``
logWithSpinner(`Building ${bundleTag}for ${mode}...`)
}

发现当 args.needsDifferentialLoadingtrue 的时候就会出现打包两次所出现的日志,所以基本可以肯定问题出在这个上,继续找一下它的复制,往上查找,在67行发现了赋值语句

1
args.needsDifferentialLoading = needsDifferentialLoading

继续查找 needsDifferentialLoading 变量声明和赋值的地方,往上看就可以看到

1
2
3
4
5
6
7
8
9
const { allProjectTargetsSupportModule } = require('../../util/targets')
let needsDifferentialLoading = args.target === 'app' && args.module
if (allProjectTargetsSupportModule) {
log(
`All browser targets in the browserslist configuration have supported ES module.\n` +
`Therefore we don't build two separate bundles for differential loading.\n`
)
needsDifferentialLoading = false
}

needsDifferentialLoading 初始值如果 args.modulefalse 的话就是 false

在正常的项目开发中 arr.target 的值一定是 app,如果开发的是插件的话,那么一般在打包的时候会指定 --target lib

还有就是如果 allProjectTargetsSupportModule 这个值是true的话, needsDifferentialLoading 会被手动赋值成 false ,于是我们发现了两个可以让 needsDifferentialLoadingfalse 的方法

–no-module的原理

先查找 args.module 的复制,会发现没有直接的赋值,args是整个回调函数的参数,而且在下面还给 args中没有的部分值,附上了默认参数,第23行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
api.registerCommand('build', {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: {
// ...
'--no-module': `build app without generating <script type="module"> chunks for modern browsers`,
// ...
}
}, async (args, rawArgs) => {
for (const key in defaults) {
if (args[key] == null) {
args[key] = defaults[key]
}
}
// ...
})
// defaults 第一行
const defaults = {
clean: true,
target: 'app',
module: true,
formats: 'commonjs,umd,umd-min'
}

可以看到 defaults 中给了 module 一个默认值true, 那怎么让 module 变成 false 呢,其实可以看到 options 中有一项 --no-module 的描述是: 构建应用程序,无需为现代浏览器生成< script type=”module “ >,到这里基本就能猜到了加个 --no-module 就可以把 module 赋值成 false 了,但猜到归猜到了,我们还是看一下具体的实现吧。

  1. package.json 中确定程序执行的入口 bin/vue-cli-service.js

    1
    2
    3
    "bin": {
    "vue-cli-service": "bin/vue-cli-service.js"
    },
  2. bin/vue-cli-service.js 中通过 minimist 解析了参数,并创建了 Service 的实例,并调用了 run 方法,并传入了解析后的参数

    minimist 会把参数中以 --no- 开头的参数,解析为 false

    minimist/index.js

    1
    2
    3
    4
    if (/^--no-.+/.test(arg)) {
    var key = arg.match(/^--no-(.+)/)[1];
    setArg(key, false, arg);
    }

    bin/vue-cli-service.js

    1
    2
    3
    4
    5
    6
    const Service = require('../lib/Service')
    const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
    const rawArgv = process.argv.slice(2)
    const args = require('minimist')(rawArgv, {/*...*/})
    const command = args._[0]
    service.run(command, args, rawArgv)
  3. Service 在实例化的时候,添加了内置的 plugin 其中就包括了 ./command/build 命令

    lib/Service.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    module.exports = class Service {
    constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    // ...
    this.commands = {}
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    }
    resolvePlugins(inlinePlugins, useBuiltIn) {
    const idToPlugin = (id, absolutePath) => ({
    id: id.replace(/^.\//, 'built-in:'),
    apply: require(absolutePath || id)
    })
    let plugins
    const builtInPlugins = [
    './commands/build',
    // ...
    ].map((id) => idToPlugin(id))

    if (inlinePlugins) {
    // ...
    } else {
    const projectPlugins = // ...
    plugins = builtInPlugins.concat(projectPlugins)
    }
    const orderedPlugins = sortPlugins(plugins)
    return orderedPlugins
    }
    }
  4. 执行了 service.run 方法,run 方法中调用了 init 方法,在 init 方法中初始化好了插件之后,用传入的参数调用对应的回调函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    async run (name, args = {}, rawArgv = []) {
    // load env variables, load user config, apply plugins
    await this.init(mode)
    args._ = args._ || []
    let command = this.commands[name]
    if (!command || args.help || args.h) {
    command = this.commands.help
    }
    const { fn } = command
    return fn(args, rawArgv)
    }
    init() {
    // apply plugins.
    this.plugins.forEach(({ id, apply }) => {
    if (this.pluginsToSkip.has(id)) return
    apply(new PluginAPI(id, this), this.projectOptions)
    })
    }

    为每一个插件创建了一个 PluginAPI 的实例,PluginAPI 提供了 registerCommand 方法,并把回调函数保存在了service.commands

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class PluginAPI {
    constructor (id, service) {
    this.id = id
    this.service = service
    }
    registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
    fn = opts
    opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {} }
    }
    }
  5. 至此 build 的回调函数就收到了解析后的参数 module: false

not ie 11

来看第二种解决方案的原理,只要从 ../../util/targets 中导入的allProjectTargetsSupportModule 值为 true,就可以了

1
2
3
4
const { allProjectTargetsSupportModule } = require('../../util/targets')
if (allProjectTargetsSupportModule) {
needsDifferentialLoading = false
}

lib/util/targets.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const projectTargets = getTargets()
const allModuleTargets = getTargets(
{ esmodules: true },
{ ignoreBrowserslistConfig: true }
)
const allProjectTargetsSupportModule = doAllTargetsSupportModule(projectTargets)
function doAllTargetsSupportModule (targets) {
const browserList = Object.keys(targets)

return browserList.every(browserName => {
if (!allModuleTargets[browserName]) {
return false
}

return semver.gte(
semver.coerce(targets[browserName]),
semver.coerce(allModuleTargets[browserName])
)
})
}

getTargetsbabel 提供的方法,如果参数为空,返回 browserlists 查询的默认值,参考:https://babeljs.io/docs/en/babel-helper-compilation-targets#gettargets

传入 esmodules: true ,返回 https://github.com/babel/babel/blob/v7.13.15/packages/babel-compat-data/data/native-modules.json 这个json文件中查询的结果.

doAllTargetsSupportModule 方法中对 browserListallModuleTargets进行了比较,如果 browserList 中有 allModuleTargets 不存在的属性,就返回 false 或者 browserList 中的版本号,比 allModuleTargets 小,也会返回 false

输出对比一下这两个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // browserList
{
android: '98.0.0',
chrome: '97.0.0',
edge: '98.0.0',
firefox: '96.0.0',
ie: '11.0.0',
ios: '14.5.0',
opera: '82.0.0',
safari: '15.2.0',
samsung: '15.0.0'
}
// allModuleTargets
{
android: '61.0.0',
chrome: '61.0.0',
edge: '16.0.0',
firefox: '60.0.0',
ios: '10.3.0',
node: '13.2.0',
opera: '48.0.0',
safari: '10.1.0',
samsung: '8.2.0'
}

发现 browserListallModuleTargets 中多了一个 ie: 11.0.0 ,那我们只要配置 browserlists 让他没有 ie 这一项就可以,ie最后的版本就是 11了,所以加一个 not IE 11 就可以了。

参考资料

[1] https://babeljs.io/docs/en/babel-helper-compilation-targets#gettargets

[2] https://github.com/browserslist/browserslist#query-composition