记录一次前端远古项目的构建升级

124 阅读2分钟

项目概述

这是一个7 8年前的项目,页面是使用freemaker做的服务端渲染,构建工具是gulp@3、webpack@3

升级原因

项目中的node-sass需要固定版本的python、c++环境,由于云迁移导致CI环境变化,造成了构建异常。为了降低后续CI环境的运维压力,需要对这个项目的构建过程进行升级,同时这个项目中的angular、cms、vue等较为复杂的子项目已经停止维护了,在生产环境的内容只有一部分,所以升级的事项和影响范围都是比较小的,可以放心升级。

升级核心点是使用sass替换node-sass,它由原来的c++实现换成了node实现,因此需要伴随一系列升级,包括:node版本、webpack、gulp、gulp-sass、sass语法等,以下是升级时的修改细节,以供参考。

改动细节

Node版本

node版本从10.16.0升级为21.1.0

webpack

loader配置

webpack5不再支持loader字符串写法,,需要改写成options,比如

/* ---------------- */
// 这些是废弃的语法
{
    loader: "expose-loader?jQuery!expose-loader?$",
}
{
    loader: "style-loader!css-loader?importLoaders=1!postcss-loader!sass-loader",
}
{
    loader: "imports?this=>window",
}
/* ---------------- */
// 分别需要被替换成
{
    loader: "expose-loader",
    options: { exposes: ["jQuery", "$"] },
}
{
    use: [
      "style-loader",
      { loader: "css-loader", options: { importLoaders: 1 } },
      "postcss-loader",
      {
        loader: "sass-loader",
        options: { implementation: require("sass") },
      },
    ],
}
{
    loader: "imports-loader",
    options: {
      imports: "this=>window",
    },
},

公共包提取

不再支持webpack.optimize.CommonsChunkPlugin,而是使用optimization.splitChunks配置,并且公共包不允许从webpack入口处(entry)引入(之前是这样做的)。以下配置是将公共包提取到lib.js文件中。

optimization: {
   splitChunks: {
     cacheGroups: {
       // 创建一个名为 lib 的代码分离组
       lib: {
         // 提取来自 /node_modules/、handlebars/、jquery/ 的代码
         test: /(node_modules)|(handlebars[\/])|(jquery[\/])/,
         name: "lib", // 输出的文件名
         chunks: "all", // 提取所有的 chunks(异步和同步都提取)
         enforce: true, // 强制提取,即使没有引用
       },
     },
   },
 }

由于部分代码被分离到lib.js了,它无法受到 exposes: ["jQuery", "$"] 的作用(它是为了让模块中没有显式引入jquery时,也能通过指定全局变量访问到jquery),因此会出现Uncaught ReferenceError: $ is not defined的错误。我们可以用插件webpack.ProvidePlugin来注入$jQuery全局变量,直接移除expose-loader

依赖冲突

许多依赖不能在webpack5中使用,因此有以下变化:

删除extract-text-webpack-plugin,官方已废弃,且项目中并未实际使用,推荐使用mini-css-extract-plugin抽取css文件

删除style-ext-html-webpack-plugin,并未实际使用

删除on-build-webpack,并未实际使用

babel系列升级为7,子包需要变成@babel/xxx,如babel-preset-env -> @babel/preset-env 同时presets: ["env", "stage-1"]需要变成presets: ["@babel/preset-env"],去掉stage-1,已经不推荐使用stage特性。

还需要加上useBuiltInscorejs配置。usage表示只会在使用到目标环境不支持的代码特性时才会引入polyfills,按需减少打包体积。Babel 默认不会自动引入 polyfills(比如 core-jsregenerator-runtime中的)来填补 JavaScript 环境中缺少的功能。

{
  useBuiltIns: "usage",
  corejs: 3,
},

由于新旧代码有cjsesm混用的情况,所以还需要在babelrc中配置"sourceType": "unambiguous"(默认是module,只允许以esm导出),否则会遇到Uncaught Error: ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead报错。

html-webpack-plugin@2.30.1 -> @5.6.3

file-loader@1.1.11 -> @6.2.0

gulp

gulp升级到5版本

gulp-sass升级

为了支持sassgulp-sass需要升级到5以上,它必须指定底层使用哪个sass编译器,而低版本只会强制使用node-sass。

// 指定使用 Dart Sass
const sass = require('gulp-sass')(require('sass'));

const sassOptions = {
  // ...options
};

sass(sassOptions)

gulp任务调度重写

之前使用的是run-sequence调度gulp任务,从gulp@4开始原生支持了串行(gulp.series)、并发(gulp.parallel)任务调度,并且gulp@4以上的一些破坏性修改使run-sequence无法使用了。因此删除了run-sequence,使用gulp的内置函数重写任务调度流程。

gulp任务声明重写

之前的gulp3使用gulp.task("task_name", taskFunction)定义任务,然后调度任务的地方使用"task_name"字符串执行任务,现在4以上的版本已经不推荐甚至不支持了,因为这样写难以追踪和维护。因此所有任务都被改写为函数+模块导出的形式,用到的地方引入模块。

显式模块化引入gulp任务

原先是使用require-dir库引入tasks文件夹中的所有任务,这样难以维护,不能明显看出到底有哪些任务。所以停止使用这个库,显式在gulpfile中引入具体的任务模块。

autoprefixer浏览器配置

不再支持autoprefixer内部配置受支持的浏览器范围,而是安装browserslist并使用它的配置。关于这些配置含义,比如> 5%代表兼容性会考虑到所有份额大于5%的浏览器。

// 之前
autoprefixer({
  browsers: [
    'last 5 versions',
    "Firefox >= 20",
    "iOS 7",
    "> 5%"
  ]
})

// 现在npm i -D browserslist,并在package.json中写上配置:
{
  "browserslist": [
    "last 5 versions",
    "Firefox >= 20",
    "iOS 7",
    "> 5%"
  ]
}

gulp-rev-all

这是给资源文件名称增加hash的库,不支持gulp5,升级到1.0

gulp-uglify-es

发现项目中混用gulp-uglifygulp-uglify-es压缩代码,所以统一成gulp-uglify-es,并升级到3.0.0以解决低版本报错。

sass语法

重写了项目中scss所有不兼容的语法

@use

已经不推荐使用@import,它引入的变量是全局的,重名会相互覆盖,没有作用域概念,变量来源也不清晰。使用@use可以让变量有命名空间,且有作用域,只能在该模块使用。空间名称默认是文件名(会去掉开头_),也可以用as自定义。

@use './xxx/bar.scss';
@use './xxx/foo.scss' as foo2;

bar.$a;
foo2.$b;

@forward

如果需要从本模块转发其他模块的变量,可以使用这个语法,但是变量的原命名空间会转变成本模块。

// in bar.scss
$a = "#fff";

// in main.scss
@forward './bar.scss';

// in foo.scss
@use './main.scss';
main.$a; // 而不是bar.$a

工具函数空间

不再推荐使用全局的函数,而是从sass提供的命名空间引入,如原来的nth()应该变成list.nth()

@use 'sass:list';

@mixin sprite-width( $sprite) {
  width: list.nth($sprite, 3);
}

项目公共库

项目中使用git subtree引入的公共包,被私有npm库替代。原先git subtree的方式,如果不严格按照规范,会在每个使用它的项目里都产生一个独立演化的副本,可能造成维护上的问题,用统一的npm库并通过版本号管理可以缓解维护压力,所以对项目当中的依赖做了全量替换