vue2 项目升级到vue3之后npm run build
执行两遍打包
实际是在
@vue/cli-service
升级到5.0版本之后出现的问题
先说解决方法
两种办法
- 执行
build
的时候加一个--no-module
1 | vue-cli-service build --no-module |
- 修改
browserslist
,一般在package.json
中或者单独的.browserslistrc
文件中,添加一个not ie 11
package.json
1 | "browserslist": [ |
.browserslistrc
1 | > 1% |
分析原因
通过执行 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 | if (args.target === 'app') { |
发现当 args.needsDifferentialLoading
为 true
的时候就会出现打包两次所出现的日志,所以基本可以肯定问题出在这个上,继续找一下它的复制,往上查找,在67行发现了赋值语句
1 | args.needsDifferentialLoading = needsDifferentialLoading |
继续查找 needsDifferentialLoading
变量声明和赋值的地方,往上看就可以看到
1 | const { allProjectTargetsSupportModule } = require('../../util/targets') |
needsDifferentialLoading
初始值如果 args.module
是 false
的话就是 false
在正常的项目开发中
arr.target
的值一定是app
,如果开发的是插件的话,那么一般在打包的时候会指定--target lib
还有就是如果 allProjectTargetsSupportModule
这个值是true的话, needsDifferentialLoading
会被手动赋值成 false
,于是我们发现了两个可以让 needsDifferentialLoading
是 false
的方法
–no-module的原理
先查找 args.module
的复制,会发现没有直接的赋值,args
是整个回调函数的参数,而且在下面还给 args
中没有的部分值,附上了默认参数,第23行
1 | api.registerCommand('build', { |
可以看到 defaults
中给了 module
一个默认值true
, 那怎么让 module
变成 false
呢,其实可以看到 options
中有一项 --no-module
的描述是: 构建应用程序,无需为现代浏览器生成< script type=”module “ >,到这里基本就能猜到了加个 --no-module
就可以把 module
赋值成 false
了,但猜到归猜到了,我们还是看一下具体的实现吧。
从
package.json
中确定程序执行的入口bin/vue-cli-service.js
1
2
3"bin": {
"vue-cli-service": "bin/vue-cli-service.js"
},在
bin/vue-cli-service.js
中通过minimist
解析了参数,并创建了Service
的实例,并调用了run
方法,并传入了解析后的参数minimist
会把参数中以--no-
开头的参数,解析为false
minimist/index.js
1
2
3
4if (/^--no-.+/.test(arg)) {
var key = arg.match(/^--no-(.+)/)[1];
setArg(key, false, arg);
}bin/vue-cli-service.js
1
2
3
4
5
6const 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)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
27module.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
}
}执行了
service.run
方法,run
方法中调用了init
方法,在init
方法中初始化好了插件之后,用传入的参数调用对应的回调函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18async 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
13class 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 || {} }
}
}至此
build
的回调函数就收到了解析后的参数module: false
not ie 11
来看第二种解决方案的原理,只要从 ../../util/targets
中导入的allProjectTargetsSupportModule
值为 true
,就可以了
1 | const { allProjectTargetsSupportModule } = require('../../util/targets') |
lib/util/targets.js
1 | const projectTargets = getTargets() |
getTargets
是 babel
提供的方法,如果参数为空,返回 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
方法中对 browserList
和 allModuleTargets
进行了比较,如果 browserList
中有 allModuleTargets
不存在的属性,就返回 false
或者 browserList
中的版本号,比 allModuleTargets
小,也会返回 false
输出对比一下这两个对象
1 | // browserList |
发现 browserList
比 allModuleTargets
中多了一个 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