分类
前端工具链

近期帮一个朋友做的 Vue 网站优化方案

前几天有个朋友找到我,说他们公司的网站产品打开速度不太理想,加载的数据量很大,想优化一下。并且询问我,是不是用微前端会好一些。

分析

我看了一下,大概有几个点:

  1. 他们是做自动化运维的,提供多个工具,所有工具都是独立的 Vue 项目
  2. 用户一般先登录 dashboard 页面,然后导航到具体的工具页
  3. 因为每个工具都独立开发、独立打包,所以每个页面都会用到不同的 app.[hash].jschunk.vendors.[hash].js
  4. 于是,用户每次切换工具,都要完整加载一遍公共资源,如 vue 全家桶和 antdv
  5. 已经对一些模块做了 lazy-loading

于是我得出一些结论:

  1. 首先,因为所有项目的技术栈高度趋同,所以并不需要微前端(微前端的解释在后面)
  2. 现在重复加载最多的应该是 vue 全家桶和 antdv,重复加载的原因是没有拆包。适当拆分打包后资源,应该可以大大提高代码复用率,减轻不同项目间的切换成本
  3. 同时还应开启 http/2,提高连接复用率

方案

于是我给朋友提出了以下改进方案:

  1. 整站启用 http/2
  2. 所有项目手动分 chunk
    1. vue 全家桶
    2. antdv
    3. 其它好统计的、全站都在使用的仓库
    4. 其它仓库
  3. 统一所有项目的依赖,提交 lock 文件入库
  4. 所有项目对公共仓库的引用顺序需保持一致,保证 webpack 打包之后的序号能维持一致
  5. 替换页面中的资源位置,指向同一个资源
  6. 延长资源缓存时间,提高利用率

下一步

他们需要一些时间来消化和实施这些方案,所以这一次咨询先到这里。

接下来,我可能会建议他们调整支持的浏览器、增加 ESM build。以及使用 npm workspace 创建一个核心项目,把所有工具项目放到一起构建,减少前面的(2)(4)(5)环节。

总结和广告

希望上述方案对大家也有启发和帮助。

顺便帮他们打个广告吧:

OpsAny,云原生场景下的智能化运维平台。我们倡导“以资源为中心”和“以应用为中心”相融合的运维理念,提高运维效率、保障业务连续性。

OpsAny, make ops perfect
分类
前端工具链

复盘近期升级工具链的过程

公司希望我提升产品在移动端的体验,于是我就打开了 Lighthouse,然后看了眼代码,发现有几个问题:

  1. 移动端和 pc 端一起编译,共用一套编译配置
  2. 目标平台包括 IE11
  3. 还在使用 babel@6
  4. 全量引用 babel-polyfill
  5. CDN 没有完全 http2

最后一项联系运维同学解决就好了,我开始尝试解决其它几项。

0. 目标

  1. 移动端和 pc 端采用不同的编译配置
  2. 尽量用最新的工具链
  3. 兼容 IE11

1. 尝试升级到 babel@7

babel@6 停留在 4 年前,存在着各种各样的问题,包括本身的实现和兼容代码都有问题。我希望先升级到 babel@7,以便使用 useBuiltIns: 'usage',减少打包后的代码量。

实际结果很不理想。升级后代码膨胀了很多,经过研究,发现原因是 core-js 策略比较保守,所以多引用了很多兼容性代码,远远多于 babel@6。

比较详细的分析可见:babel@6 升级到 babel@7,兼容性代码膨胀的原因

2. 尝试从 webpack 迁移到 esbuild

接下来尝试离开 webpack + babel 体系,使用 esbuild。先打包一套 es6 代码,供移动端和现代化浏览器使用,然后再生成一套兼容性代码,给 IE11 使用。

相比于 webpack + babel,esbuild 的优势是统一+快。按照官网统计,它的速度可能是 webpack 体系的 100 倍。它的问题是只支持 ES6,也不支持扩展,不像 babel 那样,加个插件就能什么标准都支持。

这次尝试我也失败了。原因倒不是因为 IE11,而是原来的架构跟 webpack 深度耦合,比如:lazy-loading、分割模块、i18n,等。所以迁移成本非常高,发现情况不对之后只好止损。

3. 使用 esbuild-loader 替换 babel-loader

webpack 暂时不能放弃,那就用 esbuild-loader 吧,毕竟要快很多。于是就遭遇到上面说的规范问题。

  1. esbuild 只支持标准规范,原本可以通过 babel plugins 支持的特性现在都不支持了,需要改回去
  2. 一些写的不规范的代码,比如变量先使用后定义,在 var 能跑,但是在 letconst 阶段就跑不了,也得改回去

经过一段时间的折腾,这个尝试还是比较成功。构建时间缩短了一半以上。

4. 尝试用 babel-standalone 编译 IE11 代码

上面一步只算完成一半,因为还不支持 IE11。接下来我们计划使用 babel-standalone 实时转译代码。

babel-standalone 提供在 JS 运行时里实时编译代码的功能,比如做一个在线编辑器,或者应用里内嵌了 V8 等运行时,希望提升兼容性,就可以用这个工具。

这个尝试失败了,因为 IE11 的 JS 运行时效率太差,我们生产级别的 JS 直接就卡死了,且优化不能。

5. 尝试用 SWC 在编译时生成 IE11 代码

SWC 跟 esbuild 比较类似,是另一套新生态尝试,基于 Rust 开发,也能提供极高的编译效率,且支持 ES3。

我希望能把需要在 IE11 运行的几个工具组件单独编译一套,然后根据浏览器入口加载不同的 JS。在开发阶段,则通过 swc-loader 提供兼容性代码。

重构工具链的过程也挺费时,现有构建工具链实在是反人类。不过最终还是完成了,真正使得这次尝试又失败的原因是 SWC 本身的 bug。几个无法 work around 的 bug 导致我只能放弃这个方案。

详见:初试 SWC(Speedy Web Compiler)

不过,SWC 最新版本已经修好了我提交的 bug,大家有机会试试吧。

6. 用 babel 在编译时生成 IE11 代码

最后回到 babel+babel-loader。为了体积考虑,我决定继续使用 babel@6。

然后我发现,目前的代码架构实在是,哎……相信看过 Git 操作特定分支的小技巧 一文的同学都明白我在痛苦什么。

所以又跟项目结构搏斗许久,终于基本完成了这次重构。

7. 结果

这次重构达成了几个目的:

  1. 移除了全量 babel-polyfill,只有 IE11 继续加载
  2. 大部分模块用 ES5 构建,但是不加载 polyfill
  3. 少数模块,不需要兼容 IE11 的使用 ES6 构建

具体数字没机会统计,就不列了。

分类
前端工具链

初试 SWC(Speedy Web Compiler)

SWC 是一个用 Rust 写的编译工具,功能跟 babel 很类似。它的优势在于速度,按照官网所说,在单核上它的速度是 babel 的 20倍,四核上是 babel 的 70 倍。

我最近尝试升级工具链,第一步是使用 esbuild-loader 替代 babel-loader,比较顺利。但是 esbuild 只支持 ES2015,为了支持 IE11,我们还需要再转译一次。这次我想试试 SWC。

安装与配置

安装 swc 很简单。我打算 build 的时候用 @swc/cli 转译;debug IE11 的时候把 swc-loader 套在 esbuild-loader 前面输出。所以需要安装三个包:

nom i @swc/core @swc/cli swc-loader -D

接下来添加配置文件 .swcrc

{
  "sourceMaps": false,
  "module": {
    "type": "umd"
  },
  "minify": true
}

默认目标平台 target: 'es5',不需要再写。其它语言选项也走默认即可。

SWC sourceMap 支持三种配置:truefalseinline,意思一看就明白。但目前 bug 比较多,即使设置 sourceMap:false,也只是不生成 sourcemap 文件,对应的链接标记仍然会生成。

module.type 有另一个 bug:即使目标平台是 ES5,它也会使用 ESM 引入依赖,导致语法解析出错。比如代码中使用了 async function,SWC 转译时就需要 import regeneratorRuntime from 'regenerator-runtime'。所以上面我配置 module.type"umd",一方面避免错误的 ESM import,另一方面可以通过全局方式引用这些库。

minify: true 表示我们希望 SWC 转译代码时顺便把它们压缩一下。官方文档说这个功能还不是很完善,不过我暂时没发现明显的问题。

swc 游乐场

SWC 官方提供预览网站:SWC Playground – SWC。我们可以把源码贴上去,查看编译结果。也可以调整选项,看看不同选项对结果的影响。

问题

把 SWC 放进工具链的过程还算顺利,不过接下来使用的时候却遭遇了不少问题。

首先就是上文说的 ESM 导入的问题。默认情况下 SWC 会 import 'regenerator-runtime',这个问题必须解决。考虑到这个代码特征比较明显,起初,我打算直接字符串过滤掉。后来翻了文档,又在 playground 里尝试了一下,发现 UMD 应该可以解决问题。

然后是语法问题。下面这段代码 SWC 编译错误:

// 输入
let a = 10;
for (let b = 0; b < a; b++) {
    let c = 0, b = 10, d = 100;
    console.log(b);
}

// 输出
var a = 10;
for (var b = 0; b < a; b++) {
    var c = 0, b = 10, d = 100;
    console.log(b);
}

这段代码其实有点意思,我看了很久才找到问题:let 声明变量时,for () 相对于下面的代码块,是上级 context;而 var 时,它们处于同级 context。所以下面这段循环只会运行一次,而不是像上面一样,执行 10 次。

(这段代码很能考察 letvar 的不同,我准备把它加入我的面试题库。)

还有其它一些语法问题,因为上面这个问题无法绕过,意味着使用 SWC 这条路不可行,所以我也没有深入研究,就不一一列举了。

总结

SWC 用户量太小,未知 Bug 很多;开发团队不大,又是用 Rust 写的,也给想贡献代码的前端社区带来很大阻碍。所以目前还难以应对大型项目、工业级别的需求。

不过它的速度的确很快,对改进项目构建速度有很大帮助。希望那些 bug 能尽快修复,我们可以早点把它应用到产品当中。

分类
前端工具链

偶然发现 babel@6 + babel-preset-es2015 的 bug

这两天尝试做一套解决方案,能够只编译一套 ES2015+ 代码,在现代浏览器就正常使用,在 IE11 自动切换到 babel-standalone 实时编译。

然后偶然发现 babel@6 的一个 bug,如下:

class T {
  foo() {
    const {hasOn: o} = (() => {
      for (let e = 0; e < 1; e++) {
        if (o = 1, 2 >= o) return null;
      }
      var o;
    })() || {};
  }
}

t = new T();
t.foo();

上面这段代码其实是符合语法的。看起来,L3 使用 const 声明了变量 o,然后在 L5 又再次试图给它赋值,似乎有修改常量之嫌。但其实,因为 L5 在另外一个块域里,而且 L7 var o; 会产生变量提升,所以这个赋值操作的是 var o 声明的局部变量。

如果你把它放在 V8 里,比如 node.js 或者 Chrome 浏览器,它就能正常执行;如果你用 babel@6 + babel-preset-es2015 转译,就会报错,说 o is read only

产生这个错误的原因是 babel-plugin-transform-es2015-classes 解析上面这段代码时存在错误,导致 babel-plugin-check-es2015-constants 认为常量被修改。不过我也只查到这一步,暂时不知道怎么修复这个 bug,也不知道怎么在 babel@7 里检查这个问题。有兴趣的同学可以试一下。

分类
前端工具链

使用 webpack-rpc-inline-plugin 打包内联函数体

使用 Puppeteer 的时候,我们常常要使用 page.evaluate() 或者 page.evaluateHandle() 函数到目标页面的环境里执行一段代码。

如果是简单的代码,比如返回页面标题、改变某个节点的属性,那么直接写在 .evaluate() 里面就行了;但实际生产中,尤其是前厂的 Showman 产品里,要执行的函数往往非常复杂,经常需要组合多个函数:

page.evaluate(() => {
  function func1() {}
  function func2() {}
  // ...
  function funcN() {}

  func1();
  func2();
  // ...
  funcN();
});

这种场景下,我们必须用上面这种写法,而不是下面这种我们更熟悉的写法:

import func1 from './func1';
import func2 from './func2';
// ...
import funcN from './funcN';

page.evaluate(() => {
  func1();
  func2();
  // ...
  funcN();
});

因为被执行的函数会被转换成字符串,传输到目标环境,然后重新实例化成函数,再执行。所以上面这种写法,引擎会在全局环境下查找需要的函数,而那些函数都没传递过去,就会找不到。

如果开发时按照方案一组织代码,会遇到几个问题:

  1. 子函数放在主函数体内部,不方便独立开发、调试、测试
  2. 每个主函数内部都需要写死子函数,不方便共享复用

所以我就想从工具链入手,写一个专用工具,可以继续用方案二的形式组织代码,但是编译打包之后,就恢复到方案一的状态。

我选择了 webpack 插件,原因有二:

  1. 我比较熟悉 webpack
  2. 这种情况不能用 loader

最后我选择在 compilation.afterProcessAssets 钩子处理 JS。此时 JS 已经打包了所有资源,并且经过 terser 压缩。所以我会先将 bundle 解开(直接用 string.substring),然后 return webpack 对象,从中找到目标函数替换。

具体的代码在 GitHub 仓库里,我就不详细解释了(困了),感兴趣的同学可以看看。

欢迎需要在 rpc 环境下执行 JS 的同学使用,欢迎反馈需求和问题。

分类
前端工具链

babel@6 升级到 babel@7,兼容性代码膨胀的原因

最近尝试把厂里项目的依赖从 babel@6 升级到 babel@7,发现打包之后体积大了很多。于是打开 webpack-bundle-analyzer,果然大部分代码都是 corejs 引入的,项目本身的逻辑只占少部分。

从报告来看,虽然目标浏览器的版本均高于 Promise 的启动版本(比如 Chrome 32),但 es.promise 仍然会被打包进来。于是以 es.promise 为突破口开始分析,找到答案:因为 JavaScript 引擎 V8 直至 v6.6 版本时,在 Promise 实现方面都存在严重 bug,所以 babel@7 保守地选择 Chrome 66/67 作为临界点。

想来其它体积多半也是这么加上来的,就不再一个一个排查了。 ​​​

这就很难处理。不升级吧,新特性兼容不了;升级吧,包体积变大,公司上层又未必同意。

下一步试试 esbuild 吧,或者回头手动打包一套 polyfill。目前设想的方案是:

  1. 用 babel 之类的工具提取出所有特性
  2. 根据 caniuse 生成必须的特性列表
  3. 像上面说的 Promise,因为 bug 所以必须兼容,我们就不考虑了,可以反过来加一条 eslint 规则规避
  4. 最终生成新的 polyfill 打包进来
分类
前端工具链

使用 caniuse-lite 检查目标浏览器的特性支持情况

起因

之前得知 loading="lazy" 新特性,正巧在学习如何使用 html-webpack-plugin,于是就写了个 lazyload-webpack-plugin,可以给页面里所有 <img><iframe> 加上 loading="lazy" 属性,以启动原生 lazyload。

不过当初写得很简单,只会不分青红皂白加属性,甚至可能会覆盖已有的 loading="eager" 属性,引发 bug。所以这几天就想找时间升级一下:

  1. 不再覆盖 loading 属性
  2. 根据 browserslist 得到的目标浏览器器列表采取不同策略
    1. 支持 loading="lazy" 就延续之前的做法
    2. 不支持的话,用 data-src 替换 src,然后在页面里根据浏览器特性处理

caniuse-lite

没想到这个需求还挺难满足,找了一圈竟然没有成型的教程,只好自己摸索一下,还好并不复杂。

以下代码实现了根据环境配置检查目标浏览器是否支持 loading="lazy" 的功能。我用在新版本的 lazyload-webpack-plugin 中,现在可以实现前面说的功能了。

// caniuse-lite 是官方提供的 caniuse 仓库封装,方便我们查询特性支持
const lite = require('caniuse-lite');
const browserslist = require('browserslist');
// `features` 是特性支持列表,`feature()` 可以将其转换成好用的 json 格式
const {features, feature: unpackFeature} = lite;

// 这一步,用特性名 'loading-lazy-attr' 获取支持列表
const feature = unpackFeature(features['loading-lazy-attr']);
// 直接声明 browserslist 实例,它会自动查找本地 `.browserslistrc` 或环境变量 `BROWSERSLIST` 来生成浏览器列表
const browsers = browserslist();
const {stats} = feature;
// 遍历浏览器列表,根据名称、版本验证对 `loading="lazy"` 的支持情况
const isSupport = browsers.every(browser => {
  const [name, version] = browser.split(' ');
  const browserData = stats[name];
  const isSupport = browserData && browserData[version] === 'y';
  if (!isSupport) {
    console.log(`[lazyload-webpack-plugin] target browser '${browser}' DOES NOT supported \`loading="lazy"\`.`);
  }
  return isSupport;
});

module.exports = isSupport;

问题

现在的情况是,如果知道特性名称(如“loading-lazy-attr”),可以判断目标浏览器是否支持;但是如果不知道准确名称,就没法判断。如果我想在项目当中使用,比如检查当前代码仓库用到哪些特性不被目标浏览器兼容,并生成 polyfill 套件,就很难操作。

有待进一步学习。

自荐

欢迎有制作静态页面需求的同学使用 lazyload-webpack-plugin<img><iframe> 添加 loading="lazy"。关于使用 webpack 制作多页面站点的经验分享,可以阅读我的这篇文章《使用 Webpack 开发多页面站点》。

有任何问题、意见、建议,欢迎通过各种方式提给我。

分类
前端工具链

Tailwind.css + Postcss 笔记

0. 缘由

去年,一篇《Tailwind CSS: From Side-Project Byproduct to Multi-Million Dollar Business》在我的时间线上刷屏,作为 side project 和自由职业的翘楚,他的产品和商业项目十分令人羡慕。

所以,我一直想找个机会试用一下 Tailwind.css。这次春节,想着放松休闲一下,就开了个小项目,尝试一下新技术栈:

  1. Vue3 全家桶
  2. Tailwind.css + PostCSS
  3. Webpack 工具链

这篇笔记用来记录心得和体会。


1. 基础

官方网站:https://tailwindcss.com/

2. 安装&配置

npm install tailwindcss@latest postcss@latest autoprefixer@latest
// postcss.config.js
module.exports = {
   plugins: {
     tailwindcss: {},
     autoprefixer: {},
   }
}

2.1 创建 tailwindcss 配置

npx tailwindcss init

生成的配置文件如下:

// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

2.2 创建 CSS

@tailwind base;
@tailwind components;
@tailwind utilities;

这个 CSS 无法直接被浏览器使用,需要经过 PostCSS 调用 Tailwind 插件编译后才行。

2.3 配置 Webpack

只需要配置 CSS 和 Stylus 规则:

module.exports = {
  module: {
    rules: [
      {
         test: /.css$/,
         use: [
           isDevServer ? 'style-loader' : MiniCssExtractPlugin.loader,
           'css-loader',
           'postcss-loader'
         ]
       },
       {
         test: /.styl(us)?$/,
         use: [
           isDevServer ? 'style-loader' : MiniCssExtractPlugin.loader,
           'css-loader',
           'postcss-loader',
           'stylus-loader',
         ],
       },
    ]
  }
}

2.4 配置 .browserslistrc

PostCSS 同样需要用 browserslist 处理兼容性问题,所以一定要配置好,比如我近期喜欢用 bootstrap-icons 为图标,需要用到 svg-mask 系列属性,在 Chrome 里就需要补充前缀。那么,如果 browserslist 里没有 Chrome 就不会加前缀(我昨天就踩在这个坑里)。可以使用 npx browserslist 来检查。

2.5 修改 npm scripts

PostCSS 和 Tailwind.css 需要用 NODE_ENV 变量决定动作内容,所以必须加到 npm scripts 里。

{
  "scripts": {
    "serve": "NODE_ENV=development webpack serve --config build/webpack.config.js",
    "build": "NODE_ENV=production webpack --config build/webpack.config.prod.js --mode=production",
    "lint": "eslint --fix --ext=.vue,.js ./"
  }
}

2.6 完成

至此,基础 Tailwind.css + PostCSS + Webpack 配置完成,接下来就可以使用 CSS 实现界面了。

分类
前端工具链

升级 Webpack 4 至 Webpack 5 笔记

Webpack 5 已经发布一段时间了,我也找机会把几个项目从 Webpack 4 升级到 Webpack 5,从中积累了一些经验和教训,记录于本文。

0. 请先阅读官方升级指引

链接在此:To v5 from v4。(其实我也没认真读完……)

0.1 @vue/cli 不要贸然升级

@vue/cli 目前的版本号是 4.x,使用它创建的项目需要 webpack 4,升级到 Webpack 5 的话,因为两个版本配置文件存在差异,就无法正常使用了。所以如果是 @vue/cli 创建的项目,就不要贸然升级。

1. 升级依赖

随着 Webpack 一起升级的,还有 webpack-cli、webpack-bundle-analyzer、webpack-dev-server;以及一众插件,比如 html-webpack-plugin、terser-webpack-plugin 等。

因为存在依赖关系,建议大家一起升级:

# 检查新版本
npm outdated

# 安装新版本的 webpack 套件
npm i webpack@5 webpack-cli@4 webpack-dev-server@3

2. 升级配置文件

大部分配置可以直接继续使用。

3. 升级 npm scripts

Webpack 5 对内部模块的使用有所调整,所以我们需要调整一下 npm scripts。

3.1 webpack-dev-server

# 仍然需要安装 webpack-dev-server
npm i webpack-dev-server -D

# webpack 4
webpack-dev-server --config build/webpack.config.dev.js

# webpack 5
webpack serve --config build/webpack.config.dev.js

3.2 webpack-bundle-analyzer

# 仍然需要安装 webpack-bundle-analyzer
npm i webpack-bundle-analyzer -D

# webpack 5
webpack --analyze --config build/webpack.config.js

# webpack 4,配置 build/webpack.config.js 实现

4. 问题&解决

4.1 解决:`Can’t resolve ‘http’ in ‘axios’

在一个项目中,因为需要针对不同浏览器进行不同的适配,所以我们给 .browserslist 加入了环境配置:

[modern]
last 5 chrome versions
last 3 firefox versions
last 2 safari versions
 
[withie]
last 5 chrome versions
last 3 firefox versions
last 2 safari versions
edge >= 18

然后编译时就会报这个错误:

Can't resolve 'http' in 'axios'

经过一段时间的 Google,发现给 webpack.config.js 添加 target: 'web' 可解。所以我猜测,是因为我们的 .browserslist 有环境配置,所以 webpack 没认出来,所以当作 node 来打包。

在 Webpack 4 时期,Webpack 自带 polyfill,所以没什么问题;但是 Webpack 5 把这个 polyfill 移除了,所以就报错。

4.2 解决 HMR(自动更新,热模块更新,hot module reload)失效的问题

使用 Webpack 5 后,有些项目的自动更新会失效,这是因为 Webpack 没有正确的识别项目的执行环境,错把它当成 node.js。这个时候,在 package.json 里添加 target: web' 即可解决。

分类
前端工具链

解决“Error: Rule can only have one resource source (provided resource and test + include + exclude)”

又有一台服务器到期,不想续了,所以把东西往另一台服务器上搬。其中有一个小服务,用来存储 CI 测试失败的截图。里面有用到 font-awesome,但只用一个图标,太浪费。所以这次就顺手换成了 bootstrap-icons,然后顺便更新依赖,结果,再编译的时候,就报错:

ERROR  Error: Rule can only have one resource source (provided resource and test + include + exclude) in {
   "exclude": [
     null
   ],
   "use": [
     {
       "loader": "/Users/meathill/Projects/mini-store-admin/node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js",
       "options": {
         "cacheDirectory": "/Users/meathill/Projects/mini-store-admin/node_modules/.cache/babel-loader",
         "cacheIdentifier": "219fb45a"
       },
       "ident": "clonedRuleSet-38[0].rules[0].use[0]"
     },
     {
       "loader": "/Users/meathill/Projects/mini-store-admin/node_modules/babel-loader/lib/index.js",
       "options": "undefined",
       "ident": "undefined"
     }
   ]
 }

非常诡异,按照错误栈点进去,从代码可以推断是生成的 webpack config 有问题,在 babel-loader 配置里既包含 resource 又包含 exclude。但是,这个项目是通过 @vue/cli 创建的,所以它的配置也是 @vue/cli-service 自动生成的,我并没有修改过。而且只有这个项目有问题,其它项目,同样使用 @vue/cli 创建,但是没有更新依赖,就没问题。

Google 之,有一些关于这个错误的讨论,但几乎都是用户自己写 webpack.config.js 没写好出问题。有人建议 rm -rf node_modules & rm package-lock.json & npm i,即重装所有依赖,我试了两次,也不行。

开始尝试、推翻、再尝试、再推翻。后来怀疑到 webpack ,在 package-lock.json 里查找之,发现安装的版本竟然是 5.1.0,而没有更新过依赖,可以正常编译的项目里都是 4.x。那基本可以确认了。

  1. 先删掉 node_modulespackage-lock.json
  2. 手动在 package.jsondevDependencies 里添加 "webpack": "^4.44.2"
  3. 重新安装全部依赖: npm i
  4. 尝试编译,npm run build,发现问题解决

总结

我猜问题是这样的:某些新版本的库要求 webpack@5,更新依赖时,根据依赖选择的规则,就以 webpack@5 作为主依赖安装。然而 @vue/cli 依赖 webpack@4,它自带的 webpack 配置无法兼容 webpack@5 ,于是就报错,不能继续编译。如果你也在使用 @vue/cli,那么请不要贸然升级 webpack@5。