改造一个JQ老项目,实现HMR,开发体验提升1000倍

316 阅读9分钟

今天周五了,HAPPY!!

记录一下对旧项目开发环境改造过程,提升开发体验和开发效率。 仅记录大致思路,不贴太多代码。

公司有一个5年前的项目,是一个js + jq + es5 + webpack4的旧项目。别看它使用了webpack4,它利用webpack所做的功能仅仅是将多个js打包成一个,仅此而已。它改完代码不能自动打包、不能自动刷新浏览器、不支持HMR、打包时间久、没有CICD、需要打包发布到服务器上,开发体验很差,开发效率极低。

之前对这个项目的维护并不多,他是属于另一个部门负责的,今年移交到我们部门,我负责整个部门前端会时不时抽空改造一些历史问题,提升团队效率。

前前后后我花了3次去改造,最终实现了毫秒级热更新、并且支持热模块替换、自动发布,前端同学直呼YYDS

项目介绍

一个复杂的H5展示类型的框架,会有7套皮肤,以及支持不同设备:手机、pc、安装平板、iPad。数据只会有一份,但每一套皮肤都会打包对应的js文件,引入不同的js达到使用不同皮肤的目的。月活:pv 130万,uv:15万。

老项目开发的流程

  1. 项目打包的结果输出在bin目录中
  2. 在bin目录放入index.html等相关内容
  3. 改完代码,需要手动执行打包 ---> 等待8秒打包完成 --> 手动刷新浏览器,整个过程花费了10几秒钟,电脑配置差的,那是更久,这简直是道德的沦丧、人性的扭曲!
  4. 发布的过程也很曲折,由于框架有两种不同的引用方式,需要手动打包两次,然后将它上传到OSS上,这样的话就完成了。(其它所有的H5都会引用到这个地方来)

第一次优化:自动发布

我们发布框架的方式不适合使用CICD,需要将框架上传到OSS,可以将框架应用到所有H5项目,也可单独应用于某个项目,并且需要有日志记录。所以,我将这个手动打包上传和应用的流程自动化。

流程如下:执行一个命令 --> 自动打包 --> 打包完成 --> 压缩成zip --> 在终端输入账号密码、描述 --> 上传到oss --> 打开浏览器到指定页面

将流程自动化的目的除了方便高效,还有另外一个原因:不容易将我们的时间碎片化。因为每个环节都需要有一定的等待时间,当我们在等待打包的时候为了不浪费时间去等待,就会去做其它事情,过一段时间又回过头来执行下一个阶段,甚至还有可能忘记自己在打包了。

我把打包的操作封装到了js中,再使用node去执行脚本。

在package.json 新增2条命令,打包发布到测试环境,打包发布到正式环境:

{
    "scripts": {
        "build:test": "cross-env buildType=test node './build/index.js' ",
        "build:prod": "cross-env buildType=prod node './build/index.js' "
    },
}

组装命令,然后使用child_process来执行命令:

// 1、封装 processUtil.js,执行终端命令,并且将终端的输出打印出来

const childProcess = require('child_process')

module.exports.execCmd = function (cmd, successCallback, errorCallback, dataCallback) {
    let processWorker = childProcess.exec(cmd, {maxBuffer: 10240 * 10240}, function (error, stdout, stderr) {
        console.log('error:' + error)
        successCallback && successCallback(stdout)
    })
    processWorker.stderr.on('data', function (data) {
        console.log(data)
        errorCallback && errorCallback()
    })
    processWorker.stdout.on('data', function (data) {
        console.log(data)
        dataCallback && dataCallback()
    })
}

// 2、通过 process.env.xxx 拿到在终端输入的参数组装打包命令
const cmd = 'cross-env frame=xxx outPutPath=xxx npx webpack -p'
// 3、执行命令打包
processUtil.execCmd(cmd, function () {
    // 打包完成
    // 使用 compressing 库对文件夹进行压缩
    // 使用 axios 进行上传
    // 使用 open 库打开浏览器指定页面
})

通过以上思想,就实现了一条命令自动实现打包发布的流程。

第二次优化:手写热更新

由于我需要在这个项目开发很多功能,想要高效一点,我花了1天时间去实现自动编译、自动刷新浏览器。

想达到的目的是:改完代码,自动编译,自动刷新浏览器。

我对这个项目只掌握了7成,不敢贸然改动太多,一旦出问题会影响十几万用户,所以我很保守地使用原来的方式去打包,但是把流程自动化了。

流程如下:

  1. 以前打包会打包所有皮肤的内容,而现在需要按需打包,调试某个皮肤就只打包某个皮肤,这样速度更快;
  2. 由于HTML引入多个js,即webpack entry 有多个入口,需要检测改动的是哪个js,仅打包对应入口文件,减少冗余打包;
  3. 本地开一个服务,服务路径指向bin,第一次打包就自动开启服务并且打开浏览器,之后每当bin目录下面文件有变化就自动刷新浏览器

01. 新建命令如: npm run dev

02. 执行命令后,检测目录是否存在index.html

// 检测文件是否存在
function checkFileExist(fileUrl) {
    return new Promise((resolve, reject) => {
        fs.access(fileUrl, fs.constants.F_OK, (err) => {
            if (err) {
                resolve(false)
            } else {
                resolve(true)
            }
        })
    })
}

03. 从index.html中通过正则匹配用来某个皮肤,并将皮肤类型当做命令的参数:skin=xxx

04. 打包

新建一个文件 webpack.entyrs.js 用于存放所有entry,这个文件居然多达800行!!!

webpack.config.js 的entry通过上一步获取到的皮肤类型,获取需要打包的文件。

组装打包命令,然后使用child_process执行命令

05. 启动服务器

本地服务我使用了一个叫做live-server的库

function runServer() {
   const params = {
       port: 8091,
       root: path.join(__dirname, '../bin/'),
       open: true,
       file: 'index.html',
   }
   liveServer.start(params)
}

06. 监听文件的改动

监听文件我使用chokidar,这个库监听速度非常快,vite也是使用这个库。

当我代码保存,监听到文件变化,获取相应入口,自动执行打包命令,打包完成后,bin目录文件就改变了,这时候服务监听到了文件变化,浏览器就会刷新

自此,自动编译和刷新的功能就实现了。这个功能虽然非常简陋,但是却很有用,每次保存代码3秒内就会自动刷新浏览器,已经给我节省了很多时间了。

第三次优化:毫秒级热更新、HMR(热模块替换)

几个月过后。。。。。。。

我日常上班开发,都是使用我自己的m1 pro芯片的MacBook Pro,速度真的非常快,其它同学都是使用Intel的 iMac,即使我知道手动写了热更新功能,他们电脑速度还是很慢。我决定继续优化。

这一次我舍弃原来手动写的热更新功能,使用webpack的能力:

  1. webpack-dev-server
  2. HardSourceWebpackPlugin
  3. HtmlWebpackPlugin
  4. webpack-merge

新建一个js文件,runServer.js,作为命令的入口。这个操作为了检查bin目录下面有无index.html文件,如果没有就报错。

async function start() {
    const isFileExit = await file.checkFileExist(`${binPath}/index.html`)
    if (!isFileExit) {
        console.log(chalk.bgRedBright('启动失败,需要将【资源】放在bin目录下'))
        return
    }
    runServe()
}

function runServe() {
    // 组装命令,配置文件使用webpack.dev.config.js
    const cmd = `cross-env frame=relative skinType=none outPutPath='./bin' npx webpack-dev-server --config webpack.dev.config.js`
    processUtil.execCmd(cmd)
}

新建文件webpack.dev.config.js,用于存放开发环境webpack的配置。

这个项目有个不一样的地方,比如会打包:A B C D E文件,html只会引入A和B,剩下的C D E js文件,会在A或B中,点击某个按钮的时候才去加载对应js,然后再执行对应的事件。所以,在HtmlWebpackPlugin中,需要在chunks中排除掉这些文件(CDE),由于template中已经引入了对于js文件了,故HtmlWebpackPlugin完全不需要将任何js写入index.html中。

其实,他这么做是为了按需加载,减少启动内容,提高页面启动速度。现在我们已经不会这么去做了,这个方法太古老了。

const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpackbaseConfig = require('./webpack.config')
const webpackEntry = require('./webpackEntry')

functin getEntry() {
    .....
}

// 这份配置是在原来的基础上增加了dev的配置,故将原来配置合并过来
module.exports = merge({
    entry: getEntry(), // 从html去读取需要打包的文件
    plugins: [
        new HtmlWebpackPlugin({
            template: __dirname + '/bin/index.html',
            chunks: [], // 因为index.html里面已经引入了对应的chunks,所以不需要再往里面填写内容了,并且有很多chunks不能往里面写,因为他仅仅是用于被主js文件按需加载
        }),
    ],
    devServer: {
        contentBase: path.join(__dirname, 'bin'),
        port: 9200,
        hot: true,
        open: true, // 开启热模块替换,在改css的时候就不需要刷新浏览器
        watchContentBase: true, // 检测bin目录的变化,当我们改动html的时候,会重新编译和刷新浏览器
    },
}, webpackbaseConfig)

新配一个命令:

"serve": "node './build/runServer.js' ",

执行命令,进行测试,确实是快了不少,我电脑更新的速度在1s左右,但其实这还不够,我很多项目一般都在300ms左右。经过研究,发现webpack4并不会开启缓存,需要手动安装hard-source-webpack-plugin插件来实现,安装完之后,再次测试。第一次启动确速度还是一样,但是第二次开始速度就非常快快了,基本控制在300ms左右。

自此,这个项目的开发体验得到了很大提升。早期维护的同学说之前需要几十秒,而我优化后,在他的破电脑也只需要1秒,速度提升几十倍,但是连贯的开发体验,带来愉悦的心情,整体开发体验提升1000倍,不过分。

总结

大家可以发现,其实我们实现的过程并不非常困难,只要了解node.js、webpack和自身项目,完全就可以配出来。但这个项目经手这么多人,这么差的开发体验,这么低的开发效率,居然没人一个人去改进它。

我问了大家,大家都说不会做,不知道怎么去优化。

  1. 其实很显然,大家对于webpack还没掌握(我也会抽空给组员进行培训)
  2. 大家没解决困难的决心,执行力低