【前端】生产环境部署及性能调优

概述

前端工程使用vue-cli-service进行构建,生成dist静态资源目录,其中包括htmlcssjavascript以及其他项目中使用到的所有资源。

在生产环境中,我们通常使用Nginx开启访问服务,并定位其访问目录至dist目录下的index.html,以此来实现前端工程的访问。

不仅如此,为了使得前端发起请求时,可以正确访问到后端服务,也需要在nginx中配置相应的代理,使得访问过程在同域中进行,以达到Cookie共享的目的。

当然,服务部署的形式可以有多种,上述的部署方式也是较为常用的部署方式。

部署

  • 使用production模式进行打包
  • 使用dotenv-webpack插件启用process.env
  • 配置chainWebpack调整资源加载顺序
  • 使用thread-loader进行打包加速

性能调优

  • 使用compression-webpack-plugin插件进行压缩打包
  • 启用Nginxgzipgzip_static功能
  • 使用OSS加速静态资源访问(可选)

使用production模式进行打包

在package.json中添加执行脚本
{
  "scripts": {
    "build": "vue-cli-service build --mode production"
  }
}
执行打包命令
npm run build

使用dotenv-webpack插件启用process.env

参考资料
在package.json中添加依赖或使用npm安装
{
  "devDependencies": {
    "dotenv-webpack": "1.7.0"
  }
}
npm install dotenv-webpack@1.7.0 --save-dev
在vue.config.js中添加配置
const Dotenv = require('dotenv-webpack');

module.exports = {
  publicPath: '/',
  productionSourceMap: false,
  lintOnSave: false,
  configureWebpack: {
    plugins: [
      new Dotenv()
    ]
  }
};
.env加载顺序

使用不同模式,加载的文件不同。文件按照从上到下依次加载。

  • development

    • .env
    • .env.development
  • production

    • .env
    • .env.production

配置chainWebpack调整资源加载顺序

chainWebpack对资源加载顺序取决于name属性,而不是priority属性。如示例中的加载顺序为:chunk-a --> chunk-b --> chunk-c。

code>test属性决定其打包范围,但集合之间会自动去重。如chunk-a打包node_modules下所有内容,chunk-b打包node_modules/@kunlun下所有内容。因此在chunk-a中将不再包含node_modules/@kunlun中的内容。没有test属性将意味着打包其他内容。

通常情况下,我们希望第三方资源优先加载,@kunlun会覆盖第三方资源(如css样式在同级别选择器中优先级取决于加载顺序),项目中的资源优先级最高。

module.exports = {
  publicPath: '/',
  productionSourceMap: false,
  lintOnSave: false,
  chainWebpack: (config) => {
    config.plugin('html').tap((args) => {
      args[0].chunksSortMode = (a, b) => {
        if (a.entry !== b.entry) {
          // make sure entry is loaded last so user CSS can override
          // vendor CSS
          return b.entry ? -1 : 1;
        } else {
          return 0;
        }
      };
      return args;
    });
    config.optimization.splitChunks({
      ...config.optimization.get('splitChunks'),
      cacheGroups: {
        libs: {
          name: 'chunk-a',
          test: /[\\/]node_modules[\\/]/,
          priority: 1,
          chunks: 'initial',
          maxSize: 6000000,
          minSize: 3000000,
          maxInitialRequests: 5
        },
        kunlun: {
          name: 'chunk-b',
          test: /[\\/]node_modules[\\/]@kunlun[\\/]/,
          priority: 2,
          chunks: 'initial',
          maxSize: 6000000,
          minSize: 3000000,
          maxInitialRequests: 5
        },
        common: {
          name: 'chunk-c',
          minChunks: 2,
          priority: 3,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    });
  }
};

使用thread-loader进行打包加速

参考资料
在package.json中添加依赖或使用npm安装
{
  "devDependencies": {
    "thread-loader": "3.0.4"
  }
}
npm install thread-loader@3.0.4 --save-dev
在vue.config.js中添加配置
const path = require('path');

module.exports = {
  publicPath: '/',
  productionSourceMap: false,
  lintOnSave: false,
  configureWebpack: {
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          include: path.resolve('src'),
          use: [
            {
              loader: 'thread-loader',
              options: {
                workers: 3
              }
            }
          ]
        }
      ]
    }
  }
};

使用compression-webpack-plugin插件进行压缩打包

参考资料
在package.json中添加依赖或使用npm安装
{
  "devDependencies": {
    "compression-webpack-plugin": "4.0.1"
  }
}
npm install compression-webpack-plugin@4.0.1 --save-dev
在vue.config.js中添加配置
const CompressionPlugin = require("compression-webpack-plugin");

module.exports = {
    publicPath: '/',
    productionSourceMap: false,
    lintOnSave: false,
    configureWebpack: {
        plugins: [
            new CompressionPlugin({
                algorithm: 'gzip',
                test: /\.js$|\.html$|\.css$/,
                filename: '[path].gz[query]',
                minRatio: 0.8,
                threshold: 10240,
                deleteOriginalAssets: false
            })
        ]
    }
}

启用Nginxgzipgzip_static功能

参考资料
在nginx.conf中添加配置

该配置支持位置:httpserverlocation

http {
    gzip_static         on;
    gzip_proxied        expired no-cache no-store private auth;
    gzip                on;
    gzip_min_length     1024;
    gzip_buffers        32 4k;
    gzip_comp_level     5;
    gzip_vary           on;
    gzip_types          text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
    gzip_disable        "MSIE [1-6]\.";
}
常用Nginx配置参考
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    # access_log  /var/log/nginx/host.access.log  main;

    root /opt/pamirs;

    location / {
        try_files $uri $uri/ /index.html;
        index  index.html index.htm;
        expires 1d;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache nginx_cache;
        proxy_cache_valid 200 302 1d;
        proxy_cache_valid 404 10m;
        proxy_cache_valid any 1h;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
        if ($request_filename ~* ^.*?.(html|htm|js)$) {
            add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
        }
    }

    location /pamirs {
        proxy_pass          http://127.0.0.1:8091;
        proxy_set_header    Host    $host;
        proxy_set_header    X-Real-IP   $remote_addr;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /pamirs/openapi {
        proxy_pass          http://127.0.0.1:8092;
        proxy_set_header    Host    $host;
        proxy_set_header    X-Real-IP   $remote_addr;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

PS:127.0.0.1:8091表示服务端访问地址;127.0.0.1:8092表示服务端EIP访问地址;

使用本地OSSNginx配置参考

静态资源文件包下载

server {
    location /file/upload {
        proxy_pass              http://127.0.0.1:8081;
        proxy_set_header    Host    $host;
        proxy_set_header    X-Real-IP   $remote_addr;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static {
        alias /opt/pamirs/static;

        add_header Content-Disposition attachment;
        add_header Content-Type application/octet-stream;
    }

    location /css/static {
        alias /opt/pamirs/static;

        add_header Content-Disposition attachment;
        add_header Content-Type application/octet-stream;
    }
}

PS:127.0.0.1:8091表示服务端访问地址;

使用OSS加速静态资源访问(可选)

本文以阿里云为例进行讲解,其他OSS可自行翻阅资料进行配置。

参考资料
在package.json中添加依赖或使用npm安装
{
  "devDependencies": {
    "webpack-aliyun-oss": "0.5.2"
  }
}
npm install webpack-aliyun-oss@0.5.2 --save-dev
在vue.config.js中添加配置,并填入OSS配置
const WebpackAliyunOss = require('webpack-aliyun-oss');

const OSS_URL = 'https//';
const OSS_DIST = "/pamirs/oinone/kunlun"
const OSS_REGION = '';
const OSS_ACCESS_KEY_ID = '';
const OSS_ACCESS_KEY_SECRET = '';
const OSS_BUCKET = '';

const UNIQUE_KEY = Math.random();

module.exports = {
  publicPath: `${OSS_URL}/${OSS_DIST}/${UNIQUE_KEY}/`,
  productionSourceMap: false,
  lintOnSave: false,
  configureWebpack: {
    plugins: [
      new WebpackAliyunOss({
        from: ['./dist/**/**', '!./dist/**/*.html'],
        dist: `${OSS_DIST}/${UNIQUE_KEY}`,
        region: OSS_REGION,
        accessKeyId: OSS_ACCESS_KEY_ID,
        accessKeySecret: OSS_ACCESS_KEY_SECRET,
        bucket: OSS_BUCKET,
        timeout: 1200000,
        deleteOrigin: true,
        deleteEmptyDir: true,
        overwrite: true
      })
    ]
  }
};

Q/A

为什么需要使用压缩打包方式生成.gz文件?

通过Nginx启用gzip压缩虽然也能达到压缩传输的目的,但gzip模块实际上在每次传输时(非disk cache)都会耗费cpu性能对文件内容进行压缩后再进行传输。

文中对Nginx的优化不仅启用了gzip模块,还启用了gzip_static模块。而gzip_static模块在传输时默认会优先使用.gz已经压缩好的文件直接进行传输。这种方式在支持gzip_static浏览器(如Chrome)上可以大大提高访问性能,并且可以减少服务器压力,综合提高服务器响应能力。

演示文件传输

image.png

使用gzip的响应头

image.png

image.png

使用gzip_static的响应头

image.png

image.png

使用OSS而不是通过Nginx进行静态文件的分发有什么好处?

一般来说,云厂商提供的OSS服务不仅是一个高性能的对象存储服务,搭配云厂商提供的CDN加速服务,可以不限地域的使用云资源,以此来提高页面响应速度。

阿里云的OSS是否也支持gzipgzip_static

支持

附录

下面是在本文中提到的完整配置

package.json
{
  "scripts": {
    "build": "vue-cli-service build --mode production"
  },
  "devDependencies": {
    "compression-webpack-plugin": "4.0.1",
    "dotenv-webpack": "1.7.0",
    "thread-loader": "3.0.4",
    "webpack-aliyun-oss": "0.5.2"
  }
}
vue.config.js

在该配置中,当process.env包含DEPLOY=online时自动使用OSS

const Dotenv = require('dotenv-webpack');
const path = require('path');
const WebpackAliyunOss = require('webpack-aliyun-oss');
const CompressionPlugin = require("compression-webpack-plugin");

let BASE_URL = '/';
const { DEPLOY, OSS_REGION, OSS_DIST, OSS_URL, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_BUCKET } = process.env;
const UNIQUE_KEY = Math.random();
switch (DEPLOY) {
  case 'online':
    BASE_URL = `${OSS_URL}${UNIQUE_KEY}/`;
    break;
  default:
    BASE_URL = '/';
}

module.exports = {
  publicPath: BASE_URL,
  productionSourceMap: false,
  lintOnSave: false,
  configureWebpack: {
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          include: path.resolve('src'),
          use: [
            {
              loader: 'thread-loader',
              options: {
                workers: 3
              }
            }
          ]
        }
      ]
    },
    plugins: [
      new Dotenv(),
      new CompressionPlugin({
        algorithm: 'gzip',
        test: /\.js$|\.html$|\.css$/,
        filename: '[path].gz[query]',
        minRatio: 0.8,
        threshold: 10240,
        deleteOriginalAssets: false
      }),
      DEPLOY === 'online'
        ? new WebpackAliyunOss({
          from: ['./dist/**/**', '!./dist/**/*.html'],
          dist: `${OSS_DIST}/${UNIQUE_KEY}`,
          region: OSS_REGION,
          accessKeyId: OSS_ACCESS_KEY_ID,
          accessKeySecret: OSS_ACCESS_KEY_SECRET,
          bucket: OSS_BUCKET,
          timeout: 1200000,
          deleteOrigin: true,
          deleteEmptyDir: true,
          overwrite: true
        })
        : () => { }
    ]
  },
  chainWebpack: (config) => {
    config.plugin('html').tap((args) => {
      args[0].chunksSortMode = (a, b) => {
        if (a.entry !== b.entry) {
          // make sure entry is loaded last so user CSS can override
          // vendor CSS
          return b.entry ? -1 : 1;
        } else {
          return 0;
        }
      };
      return args;
    });
    config.optimization.splitChunks({
      ...config.optimization.get('splitChunks'),
      cacheGroups: {
        libs: {
          name: 'chunk-a',
          test: /[\\/]node_modules[\\/]/,
          priority: 1,
          chunks: 'initial',
          maxSize: 6000000,
          minSize: 3000000,
          maxInitialRequests: 5
        },
        kunlun: {
          name: 'chunk-b',
          test: /[\\/]node_modules[\\/]@kunlun[\\/]/,
          priority: 2,
          chunks: 'initial',
          maxSize: 6000000,
          minSize: 3000000,
          maxInitialRequests: 5
        },
        common: {
          name: 'chunk-c',
          minChunks: 2,
          priority: 3,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    });
  },
  devServer: {
    port: 8080,
    disableHostCheck: true,
    progress: false,
    proxy: {
      '/pamirs': {
        changeOrigin: true,
        target: 'http://127.0.0.1:8080'
      }
    }
  }
};

Oinone社区 作者:张博昊原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/6796.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
张博昊的头像张博昊数式管理员
上一篇 2024年4月19日 pm8:46
下一篇 2024年4月22日 pm2:34

相关推荐

  • 「前端」动作API

    概述 在 oinone 前端平台中,提供了四种动作 跳转动作(页面跳转、打开弹窗、抽屉) 服务端动作(调用接口) 客户端动作(返回上一页、关闭弹窗等) 链接动作(打开执行的链接) 快速开始 // 基础使用示例 import { executeViewAction, executeServerAction, executeUrlAction } from '@kunlun/dependencies'; // 示例 1: 基础页面跳转(去创建页面) executeViewAction(action); // 示例 2: 带参数的页面跳转(查询ID为123的数据),去编辑、详情页 executeViewAction(action, undefined, undefined, { id: '123' }); // 示例 3: 页面跳转的参数,用最新的,防止当前页面的参数被带到下一个页面 executeViewAction(action, undefined, undefined, { id: '123' , preserveParameter: true}); // 示例 4: 调用服务端接口 const params = { id: 'xxx', name: 'xxx' }; await executeServerAction(action, params); await executeServerAction(action, params, { maxDepth: 2 }); // 接口数据返回的数据层级是3层 -> 从0开始计算, 默认是2层 // 执行链接动作 executeUrlAction(action); API 详解 executeViewAction 参数名 描述 类型 必填 默认值 — action 视图动作 RuntimeViewAction true router 路由实例 Router false undefined matched 路由匹配参数 Matched false undefined extra 扩展参数 object false {} target 规定在何处打开被链接文档(可参考 a 标签的 target) string false undefined executeServerAction 参数名 描述 类型 必填 默认值 ​action 服务端动作 RuntimeServerAction true param 传递给后端的参数 object true context 配置接口返回的数据层级(默认是两层) {maxDepth: number} false executeUrlAction 参数名 描述 类型 必填 默认值 ​action 链接动作 IURLAction true

    2025年3月21日
    64300
  • 如何通过浏览器开发者工具提高调试效率

    1.通过vue devtool查看vue组件和oinone组件的信息 平台字段、动作、视图组件都是以Widget结尾的,可以通过关键字快速找到组件 安装vue devtool插件 chrome安装最新版的vue devtool插件 谷歌应用商店插件地址,隐藏窗口需要在扩展程序的详情页额外设置才能使用该插件 安装好插件后,可以通过插件选中html页面中的元素查看相关信息 相关特性了解 组件自动创建的vue组件以组件的class类名命名,据此特性可以得出自定义组件的时候,可继承该命名的父类 一般oinone的组件以Widget后缀命名,也推荐大家也以此为后缀,方便在vue调试面板快速看出哪些是oinone组件 选中oinone组件后可以在右侧面板看到相关的组件信息,部分属性介绍 template属性为dsl的配置 activeRecords、formData、rootData、parentData、dataSource等属性为常用数据属性 action为动作的元数据 field为字段的元数据 2.直接在浏览器开发者工具的源码处调试源码 打开浏览器开发者工具,进入标签页源代码/来源(英文版为Source),win系统用快捷键ctrl+O,mac系统用快捷键cmd+O, 可以打开文件搜索面板,通过关键字搜索文件名找到代码文件,可直接在里面debug调试或者查看执行逻辑

    2024年9月9日
    1.1K00
  • oio-spin 加载中

    用于页面和区块的加载中状态。 何时使用 页面局部处于等待异步数据或正在渲染过程时,合适的加载动效会有效缓解用户的焦虑。 API 参数 说明 类型 默认值 版本 delay 延迟显示加载效果的时间(防止闪烁) number (毫秒) – loading 是否为加载中状态 boolean true wrapperClassName 包装器的类属性 string –

    2023年12月18日
    82800
  • 表单页如何在服务端动作点击后让整个表单都处于loading状态

    介绍 在业务场景中,有时候由于提交的数据很多,导致服务端动作耗时较长,为了保证这个过程中表单内的字段不再能被编辑,我们可以通过自定义能力将整个表单区域处于loading状态 自定义动作组件代码 import { ActionType, ActionWidget, BaseElementViewWidget, BaseView, ClickResult, ServerActionWidget, SPI, Widget } from '@kunlun/dependencies'; @SPI.ClassFactory(ActionWidget.Token({ actionType: ActionType.Server })) class LoadingServerActionWidget extends ServerActionWidget { protected async clickAction(): Promise<ClickResult> { const baseView = Widget.select(this.rootHandle) as unknown as BaseView; if (!baseView) { return super.clickAction(); } const baseViewWidget = baseView.getChildrenInstance().find((a) => a instanceof BaseElementViewWidget) as unknown as BaseElementViewWidget; if (!baseViewWidget) { return super.clickAction(); } return new Promise((resolve, reject) => { try { baseViewWidget.load(async () => { const res = await super.clickAction(); resolve(res); }); } catch (e) { reject(false); } }); } } 本案例知识点 BaseElementWidget提供了load方法将继承了该class的元素渲染的区域做整体loading交互,等入参的函数处理完成后恢复正常状态,其实所有继承了ActionWidget的组件也提供了这个能力让按钮在执行函数中的时候处于loading状态, 每个组件都有一个全局唯一的handle值,所在根视图的rootHandle,组件可以用this.rootHandle通过Widget.Select方法查找到所在的根视图组件,从视图的实例化子元素里可以查找到具体的业务类型视图组件,如详情页的DetailWidget、表单页的FormWidget、表格页的TableWidget,拿到这些实例后就可以操作里面的属性和方法了

    2024年5月29日
    1.0K00
  • TS 结合 Vue 实现动态注册和响应式管理

    基础知识 1: 面向对象 面向对象编程是 JavaScript 中一种重要的编程范式,它帮助开发者通过类和对象组织代码,提高代码的复用性。 2: 装饰器 装饰器在 JavaScript 中是用于包装 class 或方法的高阶函数 为了统一术语,下面的内容会把装饰器讲成注解 在 oinone 平台中,无论是字段还是动作,都是通过 ts + vue 来实现的,ts 中是面向对象的写法,所有的属性、方法建议都在对应的 class 中,如果有通用的属性跟方法,可以放在一个公共的 class 中,然后通过继承来实现,这样便于维护。 <!– FormString.vue –> <template> <div> <p>用户名: {{userName}}</p> <button @click="updateUser({name: '王五'})">修改用户名</button> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: { userName: { type: String, default: '' }, userInfo: { type: Object, default: () => ({}) }, updateUser: { type: Function, default: () => () => ({}) } } }); </script> import FormString from './FormString.vue' @SPI.ClassFactory( FormFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String }) ) export class FormCustomStringFieldWidget extends FormFieldWidget { public initialize(props) { super.initialize(props); // 调用父类方法,确保继承的属性和方法正常初始化 this.setComponent(FormString); // 注册 Vue 组件,这样该 Widget 就会渲染 FormString 组件 return this; } public otherInfo = { name:'张三' } @Widget.Reactive() public userInfo = { name:'李四' } @Widget.Reactive() public get userName() { return this.userInfo.name } @Widget.Method() public updateUser userName(user) { this.userInfo = user } public updateOtherUser userName(user) { this.otherUser = user } } 这段代码定义了一个 FormCustomStringFieldWidget 类,用于处理表单中 String 类型字段的展示和交互。该类继承自 FormFieldWidget,并使用了多种注解和特性来实现不同功能。下面是对代码的详细讲解。 SPI 讲解 @SPI.ClassFactory() 无论是自定义字段还是动作,或者是自定义 mask、layout,都会用到@SPI.ClassFactory来注册,@SPI.ClassFactory 是一个注解,它标记了该类是通过工厂模式注册的。 在前端中,所有的注解(装饰器)本质上还是高阶函数,下面是一段伪代码。 const SPI = { ClassFactory(token) { return (targetClass) => {…

    2024年9月21日
    1.8K00

Leave a Reply

登录后才能评论