标签: webpack

  • Vite 里使用动态加载

    Vite 里使用动态加载

    有时候,我们希望根据用户当前的使用状态决定加载哪些模块。比如一个网页 IDE,用户在写 JS,我们就加载 JS 模块;用户在写 PHP 我们就加载 PHP 模块。这个功能有点类似路由懒加载,但又不完全相同。

    以前在 wepback 里,我们可以通过动态 import 加注释的方式来做:

    const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
    
    // 通过组合不同注释,还可以实现不同的分包和加载策略
    // Single target
    import(
      /* webpackChunkName: "my-chunk-name" */
      /* webpackMode: "lazy" */
      /* webpackExports: ["default", "named"] */
      'module'
    );
    
    // Multiple possible targets
    import(
      /* webpackInclude: /\.json$/ */
      /* webpackExclude: /\.noimport\.json$/ */
      /* webpackChunkName: "my-chunk-name" */
      /* webpackMode: "lazy" */
      /* webpackPrefetch: true */
      /* webpackPreload: true */
      `./locale/${language}`
    );

    上面的内容可以在 Webpack 官方文档 找到。

    Vite 里提供了类似的功能,不过使用方式不太一样。现在的 Vite 2 已经抛弃了以前的插件式实现,即 vite-plugin-dynamic-import 和 @rollup/plugin-dynamic-import-vars 都不会用到。

    首先,我们要使用 import.meta.glob('./*.js`) 声明哪些文件可能要用到;接下来,我们就可以根据实际需求加载具体文件。大概方式如下:

    <script lang="ts" setup>
    // <script setup> 的代码,相当于 `created`,所以可以使用动态加载
    
    // 未来要使用的变量
    let shareTexts:ShareTexts;
    
    // 声明符合 `/src/data/share*.ts` 的文件都可能要用到
    const modules = import.meta.glob('../data/share*.ts');
    
    // 如果 store 里有 `langName`,就加载 i18n 版,否则加载普通版
    modules[`../data/${store.state.langName ? 'share-i18n' : 'share'}.ts`]()
      .then(data => {
        // 因为用到 esm,所以要加 `default`
        shareTexts = data.default;
      });
    </script>

    接下来,还可以配合 vite.config.js,将相关装入特定分包:

    export default defineConfig(({command, mode}) => {
      return {
        plugins: [
          vue(),
        ],
        build: {
          rollupOptions: {
            output: {
              manualChunks(id) {
                let result;
                if (result = /(shares\/)?share\d\.txt\?raw$/.exec(id)) {
                  return result[1] ? 'share-i18n' : 'share';
                } else if (id.includes('node_modules')) {
                  return 'vendor';
                }
              },
            },
          },
        },
      };
    });

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

    近期帮一个朋友做的 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 构建

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

    (更多…)
  • 使用 webpack-rpc-inline-plugin 打包内联函数体

    使用 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 的同学使用,欢迎反馈需求和问题。

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

    使用 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 笔记

    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 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 创建的项目,就不要贸然升级。

    如果你需要升级,比如想用最新的 TailwindCSS@3,那么可以直接升级 @vue/cli,目前最新版本是 v5.0.3,直接集成了 webpack@5。

    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 自带针对 node.js 的 polyfill,所以没什么问题;但是 Webpack 5 把这个 polyfill 移除了,所以就报错。

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

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

  • Webpack 不支持 `import.meta`,利用 ESM 在浏览器里使用 yargs

    Webpack 不支持 `import.meta`,利用 ESM 在浏览器里使用 yargs

    前些天遇到一个需求:解析 curl 请求,并转换成 ajax 请求由浏览器发出去。

    我觉得这个需求听起来不算稀罕,理论上应该有现成的库。于是在 npm 找了一下,发现 curlconverter 似乎可以满足需求。但是使用的时候报错:Module parse failed: Unexpected token

    这个错误很奇怪,看起来像是 loader 没配好。打开报错的文件位置,怎么看语法都没问题。尝试修改 webpack 配置,也未果。因为项目是 vue-cli 创建的,在如何查看最终配置上也浪费很多时间。

    最后继续诉诸 Google,关键词换来换去,终于在搜索 mjs Module parse failed: Unexpected token 时找到这个 issue:https://github.com/arnog/mathlive/issues/525,继而找到 https://github.com/webpack/webpack/issues/6719,终于确定,这是 webpack 的问题。

    因为 webpack 不支持 import.meta,所以会把 import.meta 当成语法错报告。我觉得这个行为很扯,因为 loader 配错也会报这个,所以对于第一次接触到这个问题的开发者(比如我)而言,会浪费大量时间在那些初级错误的搜索结果里。

    接下来解决问题。

    curlconverter 虽然不能直接使用,但仔细阅读它的代码,其中 https://github.com/NickCarneiro/curlconverter/blob/master/util.js 解析 curl 命令的功能实现应该问题不大,我只要想办法把 yargs 加载进来即可。而 yargs 支持浏览器 ESM 加载,所以我在页面里添加了 <script type="module" src="./yargs.js">,使用如下代码:

    // 虽然 yargs 已经到 16.2,但是 16.0.4 之后的版本都有问题
    import Yargs from 'https://unpkg.com/yargs@16.0.3/browser.mjs';
    
    // 加载完成 yargs 之后,把它挂载到 window 上
    window.Yargs = Yargs;

    将 yargs 挂到 window 上,成为外部库。然后在 vue.config.js 里配置 externals:

    module.exports = {
      chainWebpack: config => {
        config.externals.yargs = 'commonjs Yargs';
      }
    }

    接下来,将前面说的 utils.js 复制到本地并修改其中 parseCurlCommand 的实现,最终完成了需求。


    总结一下:

    1. 使用 yargs 解析命令行请求比较方便,远比自己写方便
    2. yargs 无法配合 webpack,据说可以配合 rollup 或者 snowpack,在我的 vue-cli 项目中,需要使用一些特殊的手段加载
    3. curlconverter 也很好用,可惜不能直接用
  • 解决“Error: Rule can only have one resource source (provided resource and test + include + exclude)”

    解决“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。

  • 升级 Vue@2 项目到 Vue@3

    升级 Vue@2 项目到 Vue@3

    这篇主要是笔记。(我估计会是第一篇,因为只迁移了一个项目)

    1. 安装新包

    只记录必须重装的:

    npm i vue@3 vue-loader@16.0.0-beta.8 vue-router@4.0.0-beta.13 @vue/compiler-sfc

    2. 修改 Webpack 配置

    // v2
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    // v3
    const {VueLoaderPlugin} = require('vue-loader');
    
    // for DefinePlugin
    {
      plugins: [
        new DefinePlugin({
          __VUE_OPTIONS_API__: true,
          __VUE_PROD_DEVTOOLS__: false,
        }),
      ],
    }

    3. 修改入口文件

    没有 new Vue({}) 了,取而代之的是 Vue.createApp({}),后者还支持 tree-shaking。

    也不需要注册 Vue-router 了,直接 app.use(router) 就好。所以传统的入口文件就要修改为:

    // v2
    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import App from './app';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import '@/styl/index.styl';
    import router from './router';
    
    Vue.use(VueRouter);
    
    Vue.config.productionTip = false;
    
    new Vue({
      router,
      ...App,
    }).$mount('#app');
    
    // v3
    import {createApp} from 'vue';
    import App from './app';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import '@/styl/index.styl';
    import router from './router';
    
    const app = createApp({
      ...App,
    });
    app.use(router);
    app.mount('#app');

    4. 修改 router

    Vue-router 的变化很大,建议大家好好看看 迁移手册。就我厂这个项目而言,主要是三个变化:

    1. 使用支持 tree-shaking 的函数 createRouter
    2. 修改 history: createWebHistory()
    3. 使用渲染函数 h 替换之前渲染方式
    // 加载方式
    import {h} from 'vue';
    import {
      createRouter,
      createWebHistory,
      createWebHashHistory,
      RouterView,
    } from 'vue-router';
    
    const routes = [
      {
        path: '/',
        name: 'home',
        component: {
          // vue-router v3
          render(createElement) {
            return createElement('router-view');
          }
    
          // vue-router v4
          render() {
            return h(RouterView);
          },
        },
        children: components,
      },
      // ....
    ];
    
    const router = createRouter({
      // vue-router v3
      mode: process.env.NODE_ENV === 'production' ? 'history' : 'hash',
      // vue-router v4
      history: process.env.NODE_ENV === 'production'
        ? createWebHistory()
        : createWebHashHistory(),
      scrollBehavior: (to) => {
        if (to.hash && !/^#/.test(to.hash)) {
          return {selector: to.hash};
        }
        // 这里有个小改动,x => left, y => top,简单提一下
        return {top: 0};
      },
      routes,
    });

    5. 自定义组件 v-model 修改

    • prop: value => modelValue
    • event: input => `update:modelValue`

    6. 一些小修改

    • beforeDestroy => beforeUnmount

    7. createApp 与 Application,与 Component

    v2 时,我们可以通过 new Vue({}) 初始化 Vue 实例。这个阶段,Vue 默认有一个全局对象 + 若干个实例,除了 local 的,就是全局的。

    v3 时,引入了 Application(应用)的概念,在全局和组件之间,增加了一个新的层级。这样一来,我们就可以在同一个 Web 产品中,使用 Application 来划分命令、组件、mixins 的范围。应该会增加代码的强壮程度(虽然我暂时还没用到)。

    不过,迁移代码的时候,也要注意。以前我们可能 new 一个实例,调用它的 methods;现在不行了,要这样做:

    // v2
    const ins = new Vue({});
    ins.doSomething();
    
    // v3
    const app = createApp({});
    const vm = app.mount('$el');
    vm.doSomething();

    8. 新的响应式 API

    v3 最大的变化就是重构了响应式实现,所以新增了不少响应式 API。同时,也会检查开发者的代码,如果发现不需要响应式的地方用到响应式对象,就会提示开发者,因为响应式会增加系统开销。

    这个时候可以用 markRawtoRaw 方法来修改对象,撤销之前附加在上面的响应式属性,提高访问效率。

    其它 API 还很多,后面慢慢更新吧。

    9. Devtool 和 SourceMap

    遗憾的是,目前 Vue Devtool 无法检测到 Vue。老项目的 SourceMap 也完全不生效,无法正常对 SFC 进行 debug。