基于Vue Cli4搭建Vue3 TSX移动端项目(三)

855 阅读6分钟

基于Vue Cli4搭建Vue3 TSX移动端项目(三)

上一篇基本上我们把webpack相关的一些配置都已经讲完了,本篇主要讲述打包发布时的一些配置。因为我们目前的生产环境使用的k8s,会用到docker去发布前端的代码,所以这里我用到了express作为项目的启动服务。本篇主要讲述3个点:资源上传CDN、express项目启动服务、Dockerfile

静态资源打包上传

我们知道在前端的性能优化中,使用CDN是一个比较常见的优化点。一般CDN会设置多个节点服务器,分布在不同区域中,便于用户进行数据传递和访问。当某一个节点出现问题时,通过其他节点仍然可以完成数据传输工作,可以提高用户访问网站的响应速度。且现在的云提供商基本上都采用“分布式存储”,将中心平台的内容分发到各地的边缘服务器,使用户能够就近获取所需内容,降低网络用时,提高用户访问响应速度和命中率。利用了索引、缓存等技术。

那么这里我们在项目打包时,就可以将生成的js文件,css文件等上传到云提供商的对象存储中再开启CDN,以此来利用CDN的这些特性来加快我们的前端页面加载。

安装ali-oss依赖

这里我使用了阿里云的OSS,只做一个示例,如果是非阿里云的可以根据自己的云提供商去看下node相关的SDK。具体思路就是在docker build的时候先执行一下上传脚本,然后通过配置vue.config.jspublicPath路径即可。

npm i -D ali-oss

npm i -D ali-oss                                                 
npm WARN @vue/eslint-config-typescript@7.0.0 requires a peer of @typescript-eslint/eslint-plugin@^4.4.0 but none is installed. You must install peer dependencies yourself.
npm WARN @vue/eslint-config-typescript@7.0.0 requires a peer of @typescript-eslint/parser@^4.4.0 but none is installed. You must install peer dependencies yourself.
npm WARN postcss-modules@4.0.0 requires a peer of postcss@^8.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN icss-utils@5.1.0 requires a peer of postcss@^8.1.0 but none is installed. You must install peer dependencies yourself.
npm WARN postcss-modules-extract-imports@3.0.0 requires a peer of postcss@^8.1.0 but none is installed. You must install peer dependencies yourself.
npm WARN postcss-modules-local-by-default@4.0.0 requires a peer of postcss@^8.1.0 but none is installed. You must install peer dependencies yourself.
npm WARN postcss-modules-scope@3.0.0 requires a peer of postcss@^8.1.0 but none is installed. You must install peer dependencies yourself.
npm WARN postcss-modules-values@4.0.0 requires a peer of postcss@^8.1.0 but none is installed. You must install peer dependencies yourself.
npm WARN tslint-eslint-rules@5.4.0 requires a peer of typescript@^2.2.0 || ^3.0.0 but none is installed. You must install peer dependencies yourself.

+ ali-oss@6.15.0
added 72 packages from 123 contributors in 9.619s

编写上传文件

这里我在scripts/upload.js中新建静态资源上传的js文件,具体代码如下

const OSS = require('ali-oss')
const path = require('path')
const fs = require('fs')
const config = require('../config')
const cfg = config().buildtime.cdn

const DistPath = path.join(__dirname, '../dist');

// 这里因为这个代码只会在docker中执行,不对外暴露
// 所以我这里图省事,直接使用accessKeyId,accessKeySecret直连OSS
// 如果对于安全性要求比较高的话,可以通过接口请求STS临时票据
const OSSClient = new OSS({
  region: cfg.region,
  accessKeyId: cfg.accessKeyId,
  accessKeySecret: cfg.accessKeySecret,
  bucket: cfg.bucket
})

/**
 * 递归读取所有文件
 */
function readFilePath(path, prefix, files = []) {
  const fileList = fs.readdirSync(path);
  for (let fileName of fileList) {
    const filePath = `${path}/${fileName}`;
    const stat = fs.lstatSync(filePath);
    if (stat.isFile()) {
      files.push({
        filePath,
        fileName: `${prefix ? prefix + '/' : ''}${fileName}`
      })
    } else if (stat.isDirectory()) {
      readFilePath(filePath, fileName, files)
    }
  }
  return files;
}

/**
 * 上传静态资源到OSS
 */
function uploadFile2Oss() {
  const files = readFilePath(DistPath);
  for (let item of files) {
    const file = fs.readFileSync(item.filePath);
    OSSClient.put(cfg.path + item.fileName, file).then(res => {
      if (res.res.status === 200) {
        console.log('阿里云静态资源上传成功:', item.filePath)
      } else {
        console.log('阿里云静态资源上传失败:', item.filePath)
      }
    }, () => {
      console.log('阿里云静态资源上传失败:', item.filePath)
    });
  }
}

uploadFile2Oss();

上传验证

我们package.json中添加"cdn_upload": "node ./scripts/upload.js"这个scripts,在终端中执行npm run cdn_upload后即可看到

✗ node ./scripts/upload.js
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/js/about.bc0d4022.js.map
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/favicon.ico
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/img/logo.82b9c7a5.png
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/js/about.bc0d4022.js
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/css/app.48b12f04.css
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/index.html
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/js/app.bf5cd594.js.map
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/js/app.bf5cd594.js
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/css/chunk-vendors.bc00f759.css
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/js/chunk-vendors.e9b1228d.js
阿里云静态资源上传成功: /Users/chenweibin/Desktop/vue-plus-example/dist/js/chunk-vendors.e9b1228d.js.map

再去阿里云OSS中查看可以看到静态资源已经完成了上传

image-20210330234346830

配置Vue.config.js

vue.config.js中添加publicPathindexPath配置,这样我们就只需要在docker打包时,将文件上传到CDN即可。

// 获取环境配置
const cfg = require("./config/index")();
const Pxtovw = require("postcss-px-to-viewport");
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin");
const StylelintWebpackPlugin = require("stylelint-webpack-plugin");
const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  publicPath: isDev ? '/' : cfg.buildtime.cdn.cdn_path,
  indexPath: 'index.html',
  devServer: {
    host: cfg.buildtime.origin_server.ip,
    port: cfg.buildtime.origin_server.port,
  },
  chainWebpack: (config) => {
    // HTML模板注入配置
    config.plugin("html").tap((args) => {
      // 嵌入环境配置script
      const configScript = `<!--configArea--><script>window.CUSTOMCONFIG = ${JSON.stringify(
        config.runtime
      )}</script><!--endOfConfigArea-->`;
      args[0].config = configScript;
      // 非本地开发环境及生产环境时,注入eruda
      if (!["local", "production"].includes(cfg.runtime.env)) {
        const erudaCDN = "//cdn.bootcdn.net/ajax/libs/eruda/2.4.1/eruda.min.js";
        const erudaDomCDN = "//cdn.jsdelivr.net/npm/eruda-dom@2.0.0";
        const erudaScript = `<!--erudaArea-->
          <script src="${erudaCDN}"></script>
          <script src="${erudaDomCDN}"></script>
          <script>
            eruda.init({
              tool: ['console', 'network', 'elements', 'resources', 'snippets', 'sources'],
            });
            eruda.add(erudaDom);
          </script>
        <!--endOfRrudaArea-->`;
        args[0].eruda = erudaScript;
      }
      return args;
    });
  },
  configureWebpack: {
    plugins: [
      // 配置HardSource加快二次构建
      new HardSourceWebpackPlugin(),
      // 配置styleint
      new StylelintWebpackPlugin({
        files: ['**/*.{vue,htm,html,css,sss,less,scss}'],
        fix: true, // 开启自动修复
        cache: false, // 开启缓存
        emitErrors: true,
        failOnError: false,
      }),
    ],
  },
  css: {
    loaderOptions: {
      // 全局引入common.scss
      sass: {
        prependData: '@import "~@/assets/styles/common.scss";',
      },
      // 配置px2vw
      postcss: {
        plugins: [
          new Pxtovw({
            unitToConvert: "px", // 需要转换的单位,默认为"px";
            viewportWidth: 750, // 设计稿的视口宽度
            unitPrecision: 5, // 单位转换后保留的小数位数
            propList: ["*"], // 要进行转换的属性列表,*表示匹配所有,!表示不转换
            viewportUnit: "vw", // 转换后的视口单位
            fontViewportUnit: "vw", // 转换后字体使用的视口单位
            selectorBlackList: [], // 不进行转换的css选择器,继续使用原有单位
            minPixelValue: 1, // 设置最小的转换数值
            mediaQuery: false, // 设置媒体查询里的单位是否需要转换单位
            replace: true, // 是否直接更换属性值,而不添加备用属性
            exclude: [/node_modules/], // 忽略某些文件夹下的文件
          }),
        ],
      },
    },
  },
};

这里有个点注意一下,Vue Router在初始化时会以BASE_URL作为前缀,这里记得在src/router/index.ts中改写为createWebHistory()

Express启动

上面提到我们是通过Docker来部署前端项目,所以我们需要express来作为启动服务。这里我们需要利用express来实现:

  1. 前端路由代理
  2. 动态配置校验文件

前端路由代理

这里我们开启了一个express服务,然后将所有的路由全部指向dist文件夹,将路由交给前端router去处理

const express = require("express");
const path = require("path");
const ms = require("ms");
const app = express();

// 获取环境配置
const config = require("../config/index.js");
const cfg = config();

const IP = cfg.buildtime.origin_server.ip;
const PORT = cfg.buildtime.origin_server.port;
// 开启静态文件代理
app.use(
  express.static(path.join(__dirname, "../dist/"), {
    etag: true,
    lastModified: true,
    maxAge: ms("10 days"),
    setHeaders: (res, path) => {
      if (path.endsWith(".html")) {
        res.set("Cache-Control", "no-cache");
      }
    }
  })
);

// 页面刷新时,将路由交给静态文件
app.use((req, res) => {
  res.set("Cache-Control", "no-cache");
  res.sendFile(path.join(__dirname, "../dist/"));
});

// 开启监听
app.listen(PORT, function () {
  console.log(`The app server is working at: http://${IP}:${PORT}`);
});

动态配置校验文件

因为这个是移动端项目,所以避免不了跟微信、支付宝之类的第三方平台打交道。例如我们在设置微信公众号安全域名时,一般会提供给我们一个txt文件让我们放在项目根目录下。但是我们使用k8s的话,一般都需要我们去添加文件然后重新构建部署项目。所幸我们这里用到express,那么我们可以通过express来实现将txt文件上传到CDN然后自动读取的功能。

首先我们在之前提到的环境配置buildtime.cdn中添加"verifyCdnPath": "https://frontend.oss-cn-shenzhen.aliyuncs.com/vue-plus/verifyCdnPath"配置项,作为校验文件的存放地址。

image-20210331014231676

接着我们创建一个get请求,匹配/*.txt(这里我默认认为所有的配置文件都是TXT格式)。然后将所有匹配到该路由的请求去访问CDN,成功请求到文件则直接返回,请求失败则跳转到前端路由。

const express = require("express");
const https = require('https');
const path = require("path");
const ms = require("ms");
const app = express();

// 获取环境配置
const config = require("../config/index.js");
const cfg = config();

const IP = cfg.buildtime.origin_server.ip;
const PORT = cfg.buildtime.origin_server.port;

// 校验文件配置
app.get("/*.txt", (req, res) => {
  https.get(`${cfg.buildtime.cdn.verifyCdnPath}${req.url}`, (response) => {
    let data = '';
    response.on('data', (chunk) => {
      data += chunk;
    });
    response.on('end', () => {
      res.send(data);
    });
  }).on("error", (err) => {
    req.next()
  });
})

// 开启静态文件代理
app.use(
  express.static(path.join(__dirname, "../dist/"), {
    etag: true,
    lastModified: true,
    maxAge: ms("10 days"),
    setHeaders: (res, path) => {
      if (path.endsWith(".html")) {
        res.set("Cache-Control", "no-cache");
      }
    }
  })
);

// 页面刷新时,将路由交给静态文件
app.use((req, res) => {
  res.set("Cache-Control", "no-cache");
  res.sendFile(path.join(__dirname, "../dist/"));
});

// 开启监听
app.listen(PORT, function () {
  console.log(`The app server is working at: http://${IP}:${PORT}`);
});

配置package.json

我们在package.jsonscripts新增"start": "node ./bin/server.js"。然后我们手动模拟触发下打包、上传、启动观察下服务是否正常启动。

image-20210331011936006

我们再测试下校验文件,这里我上传了个text.txt文件作为测试,可以现在校验文件内容已成功返回。

image-20210331014522230

Docker构建

dockerfile

在项目根目录下创建dockerfile文件。然后我们进行下列Dockerfile构建:

  1. 选择基础镜像为node 13
  2. 指定工作目录
  3. 复制所需的配置文件安装依赖
  4. 复制代码
  5. 声明front_end环境变量
  6. build打包
  7. 静态资源上传CDN
  8. 暴露端口号
  9. 启动服务
# 基础镜像
FROM node:13

# Workdir is unprivileged user home
WORKDIR /usr/src/app

# 安装依赖
COPY package.json /usr/src/app
COPY package-lock.json /usr/src/app
COPY .npmrc /usr/src/app
RUN npm install

# 复制代码
COPY . /usr/src/app/

# 声明环境变量
ARG front_env=''

# build正式环境
RUN npm run build

# 静态资源上传到cdn
RUN npm run cdn_upload

# 暴露内部端口号
EXPOSE 5000

# 起服务
ENTRYPOINT ["npm", "run"]
CMD ["start"]

docker build

dockerfile文件写好后,一般是通过jenkins进行docker构建,不过我们现在仍然可以在本地进行测试,在项目根目录下执行:docker build . -t vue-plus --build-arg front_env=test

指令解释:

  • docker build .:指定build的目录为当前目录,这里不能少了这个点
  • -t vue-plus:镜像名为vue-plus
  • --build-arg front_env=test:自定义环境变量参数,这里我指定了front_envtest环境

执行结果:

✗ docker build . -t vue-plus --build-arg front_env=test
[+] Building 2.6s (14/14) FINISHED                                                                          
 => [internal] load build definition from Dockerfile                                                   0.1s
 => => transferring dockerfile: 37B                                                                    0.0s
 => [internal] load .dockerignore                                                                      0.0s
 => => transferring context: 2B                                                                        0.0s
 => [internal] load metadata for docker.io/library/node:13                                             0.0s
 => [internal] load build context                                                                      2.4s
 => => transferring context: 3.15MB                                                                    2.3s
 => [1/9] FROM docker.io/library/node:13                                                               0.0s
 => CACHED [2/9] WORKDIR /usr/src/app                                                                  0.0s
 => CACHED [3/9] COPY package.json /usr/src/app                                                        0.0s
 => CACHED [4/9] COPY package-lock.json /usr/src/app                                                   0.0s
 => CACHED [5/9] COPY .npmrc /usr/src/app                                                              0.0s
 => CACHED [6/9] RUN npm install                                                                       0.0s
 => CACHED [7/9] COPY . /usr/src/app/                                                                  0.0s
 => CACHED [8/9] RUN npm run build                                                                     0.0s
 => CACHED [9/9] RUN npm run cdn_upload                                                                0.0s
 => exporting to image                                                                                 0.1s
 => => exporting layers                                                                                0.0s
 => => writing image sha256:5ca9f2f4143692bff98ddd73f00da7a5eecea2d15122cc672d4a313e6d3d6b0d           0.0s
 => => naming to docker.io/library/vue-plus

构建完成后,执行docker images查看构建出来的docker镜像

image-20210331111712787

docker run

上面已经看到了我们构建后的docker镜像,接下来我们将docker在本地部署起来。执行指令

docker run --name vue-plus-example -d --restart=always -p 5000:5000 5ca9f2f41436

指令解释:

  • --name vue-plus-example:设置容器名为vue-plus-example
  • -p 5000:5000:将本地5000端口映射到docker容器中的5000端口
  • 5ca9f2f41436:是刚刚我们生成的镜像的IMAGE ID

执行结果:

~ docker run --name vue-plus-example -d --restart=always -p 5000:5000 5ca9f2f41436
e32cbf972f092369cf83f4c0b1ff087b653053bb260d70bc3b842f1688072db7

部署成功后,我们通过 docker ps查看下当前部署是否成功。这里主要关注STATUS,如果一直在重启的话,那么就是镜像里面的服务启动有问题。

执行结果:

image-20210331112853867

本地访问

我们成功部署后,本地的5000端口就会映射到镜像的5000端口。这时候我们可以通过浏览器访问http://127.0.0.1:5000来访问这个服务。

image-20210331113340955

OK,至此基本上完成了这个项目所有配置及部署。下一章主要讲述下在Vue3中封装通用的Hooks、使用TSX写法进行开发及vue中常见语法的TSX写法