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

概述

前端工程使用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低代码应用平台体验

Like (0)
张博昊's avatar张博昊数式管理员
Previous 2024年4月19日 pm8:46
Next 2024年4月22日 pm2:34

相关推荐

  • 表格字段配置Switch开关

    在业务开发中,我们经常会遇到表格中有个Switch开关组件: 那么如何通过界面设计器配置这个组件呢,下面让我们一起来学习下吧。 设计布尔类型的组件 1: 首先在界面设计器的组件区域添加一个组件。 2: 我们给这个组件添加一个元件,元件的配置必须跟下面的一致 3: 给元件添加属性配置 拖拽一个单行文本字段, 字段编码必须是truthyAction,代表的是该字段为true的时候要执行的动作 再拖拽一个单行文本字段, 字段编码必须是falsyAction,代表的是该字段为false的时候要执行的动作 4: 发布元件,然后再去对应的界面设计器页面,将对应的字段切换成刚刚设计的组件 5: 发布界面设计器,染红我们可以在运行时的页面看到效果 当switch切换的时候,会执行对应的action

    2023年11月21日
    2.6K00
  • 文件上传组件前端校验超128长度大小,不清楚怎么配置

    原因是拼上后端返回的文件全路径后超出了字段的存储长度,后端通过注解@Field.String(size=1024)修改字段的存储长度后重新运行一遍服务。

    2023年11月1日
    1.9K00
  • 「前端」关闭源码依赖

    问题背景 在 5.x 版本的 oinone 前端架构中,我们开放了部分源码,但是导致以下性能问题: 1.构建耗时增加​​:Webpack 需要编译源码文件 2.​​加载性能下降​​:页面需加载全部编译产物 3.​​冷启动缓慢​​:开发服务器启动时间延长 以下方案通过关闭源码依赖。 操作步骤 1. 添加路径修正脚本 1: 在当前项目工程根目录中的scripts目录添加patch-package-entry.js脚本,内容如下: const fs = require('fs'); const path = require('path'); const targetPackages = [ '@kunlun+vue-admin-base', '@kunlun+vue-admin-layout', '@kunlun+vue-router', '@kunlun+vue-ui', '@kunlun+vue-ui-antd', '@kunlun+vue-ui-common', '@kunlun+vue-ui-el', '@kunlun+vue-widget' ]; // 递归查找目标包的 package.json function findPackageJson(rootDir, pkgName) { const entries = fs.readdirSync(rootDir); for (const entry of entries) { const entryPath = path.join(rootDir, entry); const stat = fs.statSync(entryPath); if (stat.isDirectory()) { if (entry.startsWith(pkgName)) { const [pkGroupName, name] = pkgName.split('+'); const pkgDir = path.join(entryPath, 'node_modules', pkGroupName, name); const pkgJsonPath = path.join(pkgDir, 'package.json'); if (fs.existsSync(pkgJsonPath)) { return pkgJsonPath; } } // 递归查找子目录 const found = findPackageJson(entryPath, pkgName); if (found) return found; } } return null; } // 从 node_modules/.pnpm 开始查找 const pnpmDir = path.join(__dirname, '../', 'node_modules', '.pnpm'); for (const pkgName of targetPackages) { const packageJsonPath = findPackageJson(pnpmDir, pkgName); if (packageJsonPath) { try { const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (packageJSON.main === 'index.ts') { const libName = packageJSON.name.replace('@', '').replace('/', '-'); packageJSON.main = `dist/${libName}.esm.js`; packageJSON.module = `dist/${libName}.esm.js`; const typings = 'dist/types/index.d.ts'; packageJSON.typings = typings; const [pwd] = packageJsonPath.split('package.json'); const typingsUrl = path.resolve(pwd, typings); const dir = fs.existsSync(typingsUrl); if (!dir)…

    2025年4月17日
    74100
  • oio-select 选择器

    API Select props 参数 说明 类型 默认值 版本 allowClear 支持清除 boolean false autofocus 默认获取焦点 boolean false clearIcon 自定义的多选框清空图标 VNode | slot – disabled 是否禁用 boolean false dropdownClassName 下拉菜单的 className 属性 string – dropdownRender 自定义下拉框内容 ({menuNode: VNode, props}) => VNode | v-slot – filterOption 是否根据输入项进行筛选。当其为一个函数时,会接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false。 boolean | function(inputValue, option) true getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 function(triggerNode) () => document.body menuItemSelectedIcon 自定义当前选中的条目图标 VNode | slot – options options 数据,如果设置则不需要手动构造 selectOption 节点 array<{value, label, [disabled, key, title]}> [] placeholder 选择框默认文字 string|slot – removeIcon 自定义的多选框清除图标 VNode | slot – suffixIcon 自定义的选择框后缀图标 VNode | slot – value(v-model:value) 指定当前选中的条目 string|string[]|number|number[] – 注意,如果发现下拉菜单跟随页面滚动,或者需要在其他弹层中触发 Select,请尝试使用 getPopupContainer={triggerNode => triggerNode.parentNode} 将下拉弹层渲染节点固定在触发器的父元素中。 事件 事件名称 说明 回调参数 blur 失去焦点的时回调 function change 选中 option,或 input 的 value 变化(combobox 模式下)时,调用此函数 function(value, option:Option/Array<Option>) deselect 取消选中时调用,参数为选中项的 value (或 key) 值,仅在 multiple 或 tags 模式下生效 function(value,option:Option) dropdownVisibleChange 展开下拉菜单的回调 function(open) focus 获得焦点时回调 function inputKeyDown 键盘按下时回调 function mouseenter 鼠标移入时回调 function mouseleave 鼠标移出时回调 function popupScroll 下拉列表滚动时的回调 function search 文本框值变化时回调 function(value: string) select 被选中时调用,参数为选中项的 value (或 key) 值 function(value, option:Option)

    2023年12月18日
    1.3K00
  • 表格字段API

    BaseTableFieldWidget 表格字段的基类. 示例 class MyTableFieldClass extends BaseTableFieldWidget{ } 内置常见的属性 dataSource 当前表格数据 rootData 根视图数据 activeRecords 当前选中行 userPrefer 用户偏好 width 单元格宽度 minWidth 单元格最小宽度 align 内容对齐方式 headerAlign 头部内容对齐方式 metadataRuntimeContext 当前视图运行时的上下文,可以获取当前模型、字段、动作、视图等所有的数据 urlParameters 获取当前的url field 当前字段 详细信息 用来获取当前字段的元数据 model 当前模型 详细信息 用来获取当前模型的元数据 view 当前视图 详细信息 界面设计器配置的视图dsl disabled 是否禁用 详细信息 来源于界面设计器的配置 invisible 当前字段是否不可见 详细信息 来源于界面设计器的配置,true -> 不可见, false -> 可见 required 是否必填 详细信息 来源于界面设计器的配置,如果当前字段是在详情页,那么是false readonly 是否只读 详细信息 来源于界面设计器的配置,如果当前字段是在详情页、搜索,那么是false label 当前字段的标题 详细信息 用来获取当前字段的标题 内置常见的方法 renderDefaultSlot 渲染单元格内容 示例 @Widget.Method() public renderDefaultSlot(context): VNode[] | string { // 当前单元格的数据 const currentValue = this.compute(context) as string[]; return [createVNode('div', { class: 'table-string-tag' }, currentValue)]; } renderHeaderSlot 自定义渲染头部 示例 @Widget.Method() public renderHeaderSlot(context: RowContext): VNode[] | string { const children = [createVNode('span', { class: 'oio-column-header-title' }, this.label)]; return children; } getTableInstance 获取当前表格实例(vxe-table) getDsl 获取界面设计器的配置

    2023年11月16日
    1.3K00

Leave a Reply

Please Login to Comment