0


Flutter web - 5 项目打包优化

介绍

目前

  1. flutter

  1. web

的打包产物优化较少,存在

  1. main.dart.js

单个文件体积过大问题,打包文件名没有

  1. hash

值,如果有使用

  1. CDN

会存在资源不能及时更新问题。本文章会对这些问题进行优化。

优化打包产物体积

从打包产物中可以看到其中

  1. main.dart.js

文件体积较大,且该文件是

  1. flutter web

运行的主要文件之一,该文件体积会随着业务代码的增多而变大,如果不对其体积进行优化,会造成页面白屏时间过长,影响用户体验。

在这里插入图片描述

打包产物目录结构:

  1. ├── assets // 静态资源文件,主要包括图片、字体、清单文件等
  2. ├── AssetManifest.json // 资源(图片、视频等)清单文件
  3. ├── FontManifest.json // 字体清单文件
  4. ├── NOTICES
  5. ├── fonts
  6. └─ MaterialIcons-Regular.otf // 字体文件,Material风格的图标
  7. ├── packages
  8. └─ cupertino_icons // 字体文件
  9. └─ cupertino_icons
  10. └─ assets
  11. └─CupertinoIcons.ttf
  12. ├── images // 图片文件夹
  13. ├── canvaskit // canvaskit渲染模式构建产生的文件
  14. ├── favicon.png
  15. ├── flutter.js // 主要下载main.dart.js文件、读取service worker缓存等,被index.html调用
  16. ├── flutter_service_worker.js // service worker的使用,主要实现文件缓存
  17. ├── icons // pwa应用图标
  18. ├── index.html // 入口文件
  19. ├── main.dart.js // JS主体文件,由flutter框架、第三方库、业务代码编译产生的
  20. ├── manifest.json // pwa应用清单文件
  21. └── version.json // 版本文件

对于字体文件,我所使用的

  1. flutter

版本(3.19.0)在

  1. build web

时,默认开启了

  1. tree-shake-icons

,可以自行运行

  1. flutter build web -h

查看。所以优化的重心为

  1. main.dart.js

文件。

打包脚本目录结构:

  1. ├── scripts
  2. ├── buildScript
  3. ├─ build.js // 打包脚本
  4. └─ loadChunk.js // 加载并合并分片脚本

使用 deferred 延迟加载

  1. dart

官方提供了

  1. deferred

关键字来实现

  1. widget

  1. 页面

的延迟加载。
文档

使用

  1. deferred

关键字标识的

  1. widget

  1. 页面

就会从

  1. main.dart.js

文件中抽离出来,生成如

  1. main.dart.js_1.part.js

  1. main.dart.js_2.part.js

  1. main.dart.js_x.part.js

等文件,可以一定程度上优化

  1. main.dart.js

文件体积。

参考文章

开启 gzip 压缩

让服务端开启

  1. gzip

压缩

文件分片

可以对

  1. main.dart.js

文件进行分片处理,充分利用浏览器并行加载的机制来节省加载时间。

  1. build.js

中加入分片代码 (文章中代码是在 Flutter web - 2 多项目架构设计 文章基础上修改)

  1. import fs from"fs";import path from"path";// 对 main.dart.js 进行分片functionsplitFile(){const chunkCount =5;// 分片数量// buildOutPath 为打包输出路径,如未改动则为项目根目录下的 build/web 文件夹const targetFile = path.resolve(buildOutPath,`./main.dart.js`);const fileData = fs.readFileSync(targetFile,"utf8");const fileDataLen = fileData.length;// 计算每个分片的大小const eachChunkLen = Math.floor(fileDataLen / chunkCount);for(let i =0; i < chunkCount; i++){const start = i * eachChunkLen;const end = i === chunkCount -1? fileDataLen :(i +1)* eachChunkLen;const chunk = fileData.slice(start, end);const chunkFilePath = path.resolve(`./build/${args.env}/${args.project}/main.dart_chunk_${i}.js`);
  2. fs.writeFileSync(chunkFilePath, chunk);}// 删除 main.dart.js 文件
  3. fs.unlinkSync(targetFile);}

分片后还需修改

  1. flutter.js

内容,使其加载分片后的文件,在后续步骤中会讲解。

文件名添加 hash 值

  1. build.js

中新增:

  1. import fs from"fs";import path from"path";import{ glob }from"glob";// 使用了 glob 依赖包const hashFileMap =newMap();// 记录新旧文件的文件名和文件路径信息const mainDartJsFileMap ={};// 记录分片后的// 文件名添加 hash 值asyncfunctionhashFile(){const files =awaitglob(["**/main.dart_chunk_@(*).js"],// ["**/images/**.*", "**/*.{otf,ttf}", "**/main.dart@(*).js"],{cwd: buildOutPath,nodir:true,});// console.log(files);for(let i =0; i < files.length; i++){const oldFilePath = path.resolve(buildOutPath, files[i]);const newFilePath =
  2. oldFilePath.substring(0,
  3. oldFilePath.length - path.extname(oldFilePath).length
  4. )+"."+getFileMD5({filePath: oldFilePath })+
  5. path.extname(oldFilePath);
  6. fs.renameSync(oldFilePath, newFilePath);const oldFileName = path.basename(oldFilePath);const newFileName = path.basename(newFilePath);
  7. hashFileMap.set(oldFileName,{
  8. oldFilePath,
  9. newFilePath,
  10. newFileName,});if(oldFileName.includes("main.dart_chunk"))
  11. mainDartJsFileMap[oldFileName]= newFileName;}}/**
  12. * 获取文件的 md5 值
  13. * @param {{fileContent?: string, filePath?: string}} options
  14. * @returns {string}
  15. */functiongetFileMD5(options){const{ fileContent, filePath }= options;const _fileContent = fileContent || fs.readFileSync(filePath);const hash = crypto.createHash("md5");
  16. hash.update(_fileContent);return hash.digest("hex").substring(0,8);}

修改

  1. flutter.js

内容

查看

  1. flutter.js

文件代码可以发现,

  1. main.dart.js

是由

  1. flutter.js

  1. loadEntrypoint

函数加载的,实际是通过调用

  1. _createScriptTag

函数,在

  1. DOM

中插入了有

  1. main.dart.js

地址的

  1. script

标签。

  1. asyncloadEntrypoint(e){let{entrypointUrl: r =`${l}main.dart.js`,onEntrypointLoaded: t,nonce: i,}= e ||{};returnthis._loadEntrypoint(r, t, i);}_loadEntrypoint(e, r, t){let i =typeof r =="function";if(!this._scriptLoaded){this._scriptLoaded =!0;let o =this._createScriptTag(e, t);if(i)
  2. console.debug("Injecting <script> tag. Using callback."),(this._onEntrypointLoaded = r),
  3. document.body.append(o);elsereturnnewPromise((s, c)=>{
  4. console.debug("Injecting <script> tag. Using Promises. Use the callback approach instead!"),(this._didCreateEngineInitializerResolve = s),
  5. o.addEventListener("error", c),
  6. document.body.append(o);});}}_createScriptTag(e, r){let t = document.createElement("script");(t.type ="application/javascript"), r &&(t.nonce = r);let i = e;return(this._ttPolicy !=null&&(i =this._ttPolicy.createScriptURL(e)),(t.src = i),
  7. t
  8. );}

因为我们把

  1. main.dart.js

分片处理了,就不需要加载原来的

  1. main.dart.js

文件,只需要加载分片的文件,再合并起来就可以了。所以我们修改的主要地方是

  1. _createScriptTag

函数。

思路:创建一个加载并合并

  1. main.dart.js

分片文件的

  1. loadChunk.js

脚本文件,把

  1. _createScriptTag

函数中加载

  1. main.dart.js

的代码替换成加载

  1. loadChunk.js

即可。

  1. loadChunk.js

代码:

  1. functionloadChunkScript(url){returnnewPromise((resolve, reject)=>{const xhr =newXMLHttpRequest();
  2. xhr.open("get", url,true);
  3. xhr.onreadystatechange=()=>{if(xhr.readyState ==4){if((xhr.status >=200&& xhr.status <300)|| xhr.status ==304){resolve(xhr.responseText);}}};
  4. xhr.onerror = reject;
  5. xhr.ontimeout = reject;
  6. xhr.send();});}let retryCount =0;const mainDartJsFileMapJSON ="{}";const mainDartJsFileMap =JSON.parse(mainDartJsFileMapJSON);const promises = Object.keys(mainDartJsFileMap).sort().map((key)=>`${baseHref}${mainDartJsFileMap[key]}`).map(loadChunkScript);
  7. Promise.all(promises).then((values)=>{const contents = values.join("");const script = document.createElement("script");
  8. script.text = contents;
  9. script.type ="text/javascript";
  10. document.body.appendChild(script);}).catch(()=>{if(++retryCount >3){
  11. console.error("load chunk fail");}else{_createScriptTag(url);}});

只要替换掉其中的

  1. const mainDartJsFileMapJSON = "{}";

  1. ${baseHref}

即可,所以在

  1. build.js

文件新增函数:

  1. import fs from"fs";import path from"path";import{ minify_sync }from"terser";import{ transform }from"@babel/core";// 插入加载分片脚本functioninsertLoadChunkScript(){// 读取 loadChunk.js 内容,并替换let loadChunkContent = fs
  2. .readFileSync(path.resolve("./scripts/buildScript/loadChunk.js")).toString();
  3. loadChunkContent = loadChunkContent
  4. .replace('const mainDartJsFileMapJSON = "{}";',`const mainDartJsFileMapJSON = '${JSON.stringify(mainDartJsFileMap)}';`).replace("${baseHref}",`${baseHref}`);// 使用 babel 进行代码降级const parseRes =transform(loadChunkContent,{presets:["@babel/preset-env"],});// 代码混淆和压缩const terserRes =minify_sync(parseRes.code,{compress:true,mangle:true,output:{beautify:false,comments:false,},});// 在打包产物中创建 script 文件夹if(!fs.existsSync(path.resolve(buildOutPath,"script")))
  5. fs.mkdirSync(path.resolve(buildOutPath,"script"));// 文件名加 hash 值const loadChunkJsHash =getFileMD5({fileContent: terserRes.code });
  6. fs.writeFileSync(
  7. path.resolve(buildOutPath,`./script/loadChunk.${loadChunkJsHash}.js`),
  8. Buffer.from(terserRes.code));// 替换 flutter.js 里的 _createScriptTagconst pattern =/_createScriptTag\([\w,]+\){(.*?)}/;const flutterJsPath = path.resolve(buildOutPath,"./flutter.js");let flutterJsContent = fs.readFileSync(flutterJsPath).toString();
  9. flutterJsContent = flutterJsContent.replace(pattern,(match, p1)=>{return`_createScriptTag(){let t=document.createElement("script");t.type="application/javascript";t.src='${baseHref}script/loadChunk.${loadChunkJsHash}.js';return t}`;});// flutter js 加 hash
  10. fs.writeFileSync(flutterJsPath, Buffer.from(flutterJsContent));const flutterJsHashName =`flutter.${getFileMD5({fileContent: flutterJsContent,})}.js`;
  11. fs.renameSync(flutterJsPath, path.resolve(buildOutPath, flutterJsHashName));// 替换 index.html 内容const bridgeScript =`<script src="${flutterJsHashName}" defer></script>`;const htmlPath = path.resolve(buildOutPath,"./index.html");let htmlText = fs.readFileSync(htmlPath).toString();const headEndIndex = htmlText.indexOf("</head>");
  12. htmlText =
  13. htmlText.substring(0, headEndIndex)+
  14. bridgeScript +
  15. htmlText.substring(headEndIndex);
  16. fs.writeFileSync(htmlPath, Buffer.from(htmlText));}

完整代码

需安装依赖:

  1. pnpm i chalk crypto terser glob @babel/core commander @babel/preset-env -D
  1. import fs from"fs";import path from"path";import{ glob }from"glob";import crypto from"crypto";import{ minify_sync }from"terser";import{ exec }from"child_process";import{ transform }from"@babel/core";import{ program, Option }from"commander";
  2. program
  3. .command("build").requiredOption("-p, --project <string>","project name")// 要打包的项目名.addOption(newOption("-e, --env <string>","dev or prod environment")// 运行的环境.choices(["dev","prod"]).default("dev")).addOption(newOption("--web-renderer <string>","web renderer mode")// 渲染方式.choices(["auto","html","canvaskit"]).default("auto")).action((cmd)=>{build(cmd);});
  4. program.parse(process.argv);/**
  5. * @param {{ project: string, env: string, webRenderer: string }} args
  6. */functionbuild(args){// 要打包的项目路劲const buildTargetPath = path.resolve(`./lib/${args.project}`);// 打包文件输出位置,如:build/dev/project_1const buildOutPath = path.resolve(`./build/${args.env}/${args.project}`);// 见下方解释,具体根据部署路劲设置const baseHref =`/${args.project}/`;const hashFileMap =newMap();const mainDartJsFileMap ={};// 删除原打包文件
  7. fs.rmSync(buildOutPath,{recursive:true,force:true});// 打包命令 -o 指定输出位置// --release 构建发布版本,有对代码进行混淆压缩等优化// --pwa-strategy none 不使用 pwaconst commandStr =`fvm flutter build web --base-href ${baseHref} --web-renderer ${args.webRenderer} --release --pwa-strategy none -o ${buildOutPath} --dart-define=INIT_ENV=${args.env}`;exec(
  8. commandStr,{cwd: buildTargetPath,},async(error, stdout, stderr)=>{if(error){
  9. console.error(`exec error: ${error}`);return;}
  10. console.log(`stdout: ${stdout}`);splitFile();awaithashFile();insertLoadChunkScript();if(stderr){
  11. console.error(`stderr: ${stderr}`);return;}});// 对 main.dart.js 进行分片functionsplitFile(){const chunkCount =5;// 分片数量const targetFile = path.resolve(buildOutPath,`./main.dart.js`);const fileData = fs.readFileSync(targetFile,"utf8");const fileDataLen = fileData.length;const eachChunkLen = Math.floor(fileDataLen / chunkCount);for(let i =0; i < chunkCount; i++){const start = i * eachChunkLen;const end = i === chunkCount -1? fileDataLen :(i +1)* eachChunkLen;const chunk = fileData.slice(start, end);const chunkFilePath = path.resolve(`./build/${args.env}/${args.project}/main.dart_chunk_${i}.js`);
  12. fs.writeFileSync(chunkFilePath, chunk);}
  13. fs.unlinkSync(targetFile);}// 文件名添加 hash 值asyncfunctionhashFile(){const files =awaitglob(["**/main.dart@(*).js"],// ["**/images/**.*", "**/*.{otf,ttf}", "**/main.dart@(*).js"],{cwd: buildOutPath,nodir:true,});// console.log(files);for(let i =0; i < files.length; i++){const oldFilePath = path.resolve(buildOutPath, files[i]);const newFilePath =
  14. oldFilePath.substring(0,
  15. oldFilePath.length - path.extname(oldFilePath).length
  16. )+"."+getFileMD5({filePath: oldFilePath })+
  17. path.extname(oldFilePath);
  18. fs.renameSync(oldFilePath, newFilePath);const oldFileName = path.basename(oldFilePath);const newFileName = path.basename(newFilePath);
  19. hashFileMap.set(oldFileName,{
  20. oldFilePath,
  21. newFilePath,
  22. newFileName,});if(oldFileName.includes("main.dart_chunk"))
  23. mainDartJsFileMap[oldFileName]= newFileName;}}/**
  24. * 获取文件的 md5 值
  25. * @param {{fileContent?: string, filePath?: string}} options
  26. * @returns {string}
  27. */functiongetFileMD5(options){const{ fileContent, filePath }= options;const _fileContent = fileContent || fs.readFileSync(filePath);const hash = crypto.createHash("md5");
  28. hash.update(_fileContent);return hash.digest("hex").substring(0,8);}// 插入加载分片脚本functioninsertLoadChunkScript(){let loadChunkContent = fs
  29. .readFileSync(path.resolve("./scripts/buildScript/loadChunk.js")).toString();
  30. loadChunkContent = loadChunkContent
  31. .replace('const mainDartJsFileMapJSON = "{}";',`const mainDartJsFileMapJSON = '${JSON.stringify(mainDartJsFileMap)}';`).replace("${baseHref}",`${baseHref}`);const parseRes =transform(loadChunkContent,{presets:["@babel/preset-env"],});const terserRes =minify_sync(parseRes.code,{compress:true,mangle:true,output:{beautify:false,comments:false,},});if(!fs.existsSync(path.resolve(buildOutPath,"script")))
  32. fs.mkdirSync(path.resolve(buildOutPath,"script"));const loadChunkJsHash =getFileMD5({fileContent: terserRes.code });
  33. fs.writeFileSync(
  34. path.resolve(buildOutPath,`./script/loadChunk.${loadChunkJsHash}.js`),
  35. Buffer.from(terserRes.code));// 替换 flutter.js 里的 _createScriptTagconst pattern =/_createScriptTag\([\w,]+\){(.*?)}/;const flutterJsPath = path.resolve(buildOutPath,"./flutter.js");let flutterJsContent = fs.readFileSync(flutterJsPath).toString();
  36. flutterJsContent = flutterJsContent.replace(pattern,(match, p1)=>{return`_createScriptTag(){let t=document.createElement("script");t.type="application/javascript";t.src='${baseHref}script/loadChunk.${loadChunkJsHash}.js';return t}`;});// flutter js 加 hash
  37. fs.writeFileSync(flutterJsPath, Buffer.from(flutterJsContent));const flutterJsHashName =`flutter.${getFileMD5({fileContent: flutterJsContent,})}.js`;
  38. fs.renameSync(flutterJsPath, path.resolve(buildOutPath, flutterJsHashName));// 替换 index.html 内容const bridgeScript =`<script src="${flutterJsHashName}" defer></script>`;const htmlPath = path.resolve(buildOutPath,"./index.html");let htmlText = fs.readFileSync(htmlPath).toString();const headEndIndex = htmlText.indexOf("</head>");
  39. htmlText =
  40. htmlText.substring(0, headEndIndex)+
  41. bridgeScript +
  42. htmlText.substring(headEndIndex);
  43. fs.writeFileSync(htmlPath, Buffer.from(htmlText));}}

存在问题

目前只处理的

  1. main.dart_chunk_i.js

等分片文件,未对延迟加载文件、图片、字体等文件进行处理。

参考文章

Flutter Web 在《一起漫部》的性能优化探索与实践

Flutter for Web 首次首屏优化——JS 分片优化


本文转载自: https://blog.csdn.net/qq_35886767/article/details/142146893
版权归原作者 kerry丶 所有, 如有侵权,请联系我们删除。

“Flutter web - 5 项目打包优化”的评论:

还没有评论