0


Flutter web - 5 项目打包优化

介绍

目前

flutter

web

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

main.dart.js

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

hash

值,如果有使用

CDN

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

优化打包产物体积

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

main.dart.js

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

flutter web

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

在这里插入图片描述

打包产物目录结构:

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

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

flutter

版本(3.19.0)在

build web

时,默认开启了

tree-shake-icons

,可以自行运行

flutter build web -h

查看。所以优化的重心为

main.dart.js

文件。

打包脚本目录结构:

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

使用 deferred 延迟加载

dart

官方提供了

deferred

关键字来实现

widget

页面

的延迟加载。
文档

使用

deferred

关键字标识的

widget

页面

就会从

main.dart.js

文件中抽离出来,生成如

main.dart.js_1.part.js

main.dart.js_2.part.js

main.dart.js_x.part.js

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

main.dart.js

文件体积。

参考文章

开启 gzip 压缩

让服务端开启

gzip

压缩

文件分片

可以对

main.dart.js

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

build.js

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

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`);
    fs.writeFileSync(chunkFilePath, chunk);}// 删除 main.dart.js 文件
  fs.unlinkSync(targetFile);}

分片后还需修改

flutter.js

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

文件名添加 hash 值

build.js

中新增:

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 =
      oldFilePath.substring(0,
        oldFilePath.length - path.extname(oldFilePath).length
      )+"."+getFileMD5({filePath: oldFilePath })+
      path.extname(oldFilePath);
    fs.renameSync(oldFilePath, newFilePath);const oldFileName = path.basename(oldFilePath);const newFileName = path.basename(newFilePath);
    hashFileMap.set(oldFileName,{
      oldFilePath,
      newFilePath,
      newFileName,});if(oldFileName.includes("main.dart_chunk"))
      mainDartJsFileMap[oldFileName]= newFileName;}}/**
 * 获取文件的 md5 值
 * @param {{fileContent?: string, filePath?: string}} options
 * @returns {string}
 */functiongetFileMD5(options){const{ fileContent, filePath }= options;const _fileContent = fileContent || fs.readFileSync(filePath);const hash = crypto.createHash("md5");
  hash.update(_fileContent);return hash.digest("hex").substring(0,8);}

修改

flutter.js

内容

查看

flutter.js

文件代码可以发现,

main.dart.js

是由

flutter.js

loadEntrypoint

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

_createScriptTag

函数,在

DOM

中插入了有

main.dart.js

地址的

script

标签。

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)
      console.debug("Injecting <script> tag. Using callback."),(this._onEntrypointLoaded = r),
        document.body.append(o);elsereturnnewPromise((s, c)=>{
        console.debug("Injecting <script> tag. Using Promises. Use the callback approach instead!"),(this._didCreateEngineInitializerResolve = s),
          o.addEventListener("error", c),
          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),
    t
  );}

因为我们把

main.dart.js

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

main.dart.js

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

_createScriptTag

函数。

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

main.dart.js

分片文件的

loadChunk.js

脚本文件,把

_createScriptTag

函数中加载

main.dart.js

的代码替换成加载

loadChunk.js

即可。

loadChunk.js

代码:

functionloadChunkScript(url){returnnewPromise((resolve, reject)=>{const xhr =newXMLHttpRequest();
    xhr.open("get", url,true);
    xhr.onreadystatechange=()=>{if(xhr.readyState ==4){if((xhr.status >=200&& xhr.status <300)|| xhr.status ==304){resolve(xhr.responseText);}}};
    xhr.onerror = reject;
    xhr.ontimeout = reject;
    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);
Promise.all(promises).then((values)=>{const contents = values.join("");const script = document.createElement("script");
    script.text = contents;
    script.type ="text/javascript";

    document.body.appendChild(script);}).catch(()=>{if(++retryCount >3){
      console.error("load chunk fail");}else{_createScriptTag(url);}});

只要替换掉其中的

const mainDartJsFileMapJSON = "{}";

${baseHref}

即可,所以在

build.js

文件新增函数:

import fs from"fs";import path from"path";import{ minify_sync }from"terser";import{ transform }from"@babel/core";// 插入加载分片脚本functioninsertLoadChunkScript(){// 读取 loadChunk.js 内容,并替换let loadChunkContent = fs
    .readFileSync(path.resolve("./scripts/buildScript/loadChunk.js")).toString();
  loadChunkContent = loadChunkContent
    .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")))
    fs.mkdirSync(path.resolve(buildOutPath,"script"));// 文件名加 hash 值const loadChunkJsHash =getFileMD5({fileContent: terserRes.code });

  fs.writeFileSync(
    path.resolve(buildOutPath,`./script/loadChunk.${loadChunkJsHash}.js`),
    Buffer.from(terserRes.code));// 替换 flutter.js 里的 _createScriptTagconst pattern =/_createScriptTag\([\w,]+\){(.*?)}/;const flutterJsPath = path.resolve(buildOutPath,"./flutter.js");let flutterJsContent = fs.readFileSync(flutterJsPath).toString();
  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
  fs.writeFileSync(flutterJsPath, Buffer.from(flutterJsContent));const flutterJsHashName =`flutter.${getFileMD5({fileContent: flutterJsContent,})}.js`;
  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>");
  htmlText =
    htmlText.substring(0, headEndIndex)+
    bridgeScript +
    htmlText.substring(headEndIndex);

  fs.writeFileSync(htmlPath, Buffer.from(htmlText));}

完整代码

需安装依赖:

pnpm i chalk crypto terser glob @babel/core commander @babel/preset-env -D
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";

program
  .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);});
program.parse(process.argv);/**
 * @param {{ project: string, env: string, webRenderer: string }} args
 */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 ={};// 删除原打包文件
  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(
    commandStr,{cwd: buildTargetPath,},async(error, stdout, stderr)=>{if(error){
        console.error(`exec error: ${error}`);return;}
      console.log(`stdout: ${stdout}`);splitFile();awaithashFile();insertLoadChunkScript();if(stderr){
        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`);
      fs.writeFileSync(chunkFilePath, chunk);}
    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 =
        oldFilePath.substring(0,
          oldFilePath.length - path.extname(oldFilePath).length
        )+"."+getFileMD5({filePath: oldFilePath })+
        path.extname(oldFilePath);
      fs.renameSync(oldFilePath, newFilePath);const oldFileName = path.basename(oldFilePath);const newFileName = path.basename(newFilePath);
      hashFileMap.set(oldFileName,{
        oldFilePath,
        newFilePath,
        newFileName,});if(oldFileName.includes("main.dart_chunk"))
        mainDartJsFileMap[oldFileName]= newFileName;}}/**
   * 获取文件的 md5 值
   * @param {{fileContent?: string, filePath?: string}} options
   * @returns {string}
   */functiongetFileMD5(options){const{ fileContent, filePath }= options;const _fileContent = fileContent || fs.readFileSync(filePath);const hash = crypto.createHash("md5");
    hash.update(_fileContent);return hash.digest("hex").substring(0,8);}// 插入加载分片脚本functioninsertLoadChunkScript(){let loadChunkContent = fs
      .readFileSync(path.resolve("./scripts/buildScript/loadChunk.js")).toString();
    loadChunkContent = loadChunkContent
      .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")))
      fs.mkdirSync(path.resolve(buildOutPath,"script"));const loadChunkJsHash =getFileMD5({fileContent: terserRes.code });

    fs.writeFileSync(
      path.resolve(buildOutPath,`./script/loadChunk.${loadChunkJsHash}.js`),
      Buffer.from(terserRes.code));// 替换 flutter.js 里的 _createScriptTagconst pattern =/_createScriptTag\([\w,]+\){(.*?)}/;const flutterJsPath = path.resolve(buildOutPath,"./flutter.js");let flutterJsContent = fs.readFileSync(flutterJsPath).toString();
    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
    fs.writeFileSync(flutterJsPath, Buffer.from(flutterJsContent));const flutterJsHashName =`flutter.${getFileMD5({fileContent: flutterJsContent,})}.js`;
    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>");
    htmlText =
      htmlText.substring(0, headEndIndex)+
      bridgeScript +
      htmlText.substring(headEndIndex);

    fs.writeFileSync(htmlPath, Buffer.from(htmlText));}}

存在问题

目前只处理的

main.dart_chunk_i.js

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

参考文章

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

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


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

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

还没有评论