返回

Vue CLI 3 & Electron 构建应用动态读取 config.json

vue.js

Vue CLI 3 & Electron 构建后读取动态 config.json 文件

有没有遇到过这种情况:使用 Vue CLI 3 和 Electron 构建应用后,希望从一个 config.json 文件中读取配置,并且在应用运行时可以修改这个文件,让应用立即反映这些修改,而无需重新构建? 这篇文章,咱们就来聊聊怎么解决这个问题。

一、 问题在哪?

默认情况下,Webpack 在构建时会将 config.json 文件中的数据直接打包进 JavaScript 代码。这样一来,用户即便修改了 config.json 文件,应用读取的还是打包时的旧数据,没法动态更新。 你之前试过的把文件放在 assetspublic 文件夹,然后用 importrequire,或者用 axios 读取,都会碰到这个问题。

二、 怎么解决?

核心思路是:让 config.json 文件不被 Webpack 打包,而是在运行时从文件系统直接读取。

2.1. 把 config.json 放在 Electron 的 resources 目录

  1. 目录结构调整:

    不要把 config.json 放在 Vue 项目的 srcpublic 目录。 创建一个和 src 平级的文件夹, 比如叫 resources (或者其他你喜欢的名字), 把 config.json 文件放进去。

    my-project/
    ├── ...
    ├── src/
    ├── resources/
    │   └── config.json
    ├── ...
    

    这避免了被webpack处理.

  2. 修改 vue.config.js (或创建):

    需要告诉 Electron-builder,在打包时把 resources 目录整个复制到应用的资源目录。 在 vue.config.js 文件里加上类似下面的配置:

    // vue.config.js
    module.exports = {
      pluginOptions: {
        electronBuilder: {
          extraResources: [
            {
              from: './resources',
              to: 'resources',
              filter: ['**/*']
            }
          ]
        }
      }
    };
    

    这个配置的意思是:把项目根目录下的 resources 文件夹(from: './resources')的内容,复制到 Electron 打包后的应用的 resources 目录(to: 'resources')。filter 选项确保所有文件都被复制。

2.2. 使用 Node.js 的 fs 模块读取文件

  1. 在 Vue 组件中引入 fs

    因为 Electron 环境支持 Node.js 的 API,可以直接用 fs 模块来读取文件。

    // App.vue  或者其他需要读取配置的组件
    <script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';
    import * as fs from 'fs';
    import * as path from 'path';
    
    @Component
    export default class App extends Vue {
        brand: string = '';
        printerIP: string = '';
    
         getConfigPath(): string {
           //开发环境和生产环境获取路径方式不一样
           const isDevelopment = process.env.NODE_ENV === 'development'
            if (isDevelopment) {
                 return path.join(__dirname, '../../resources/config.json');
            }else{
                 //path.join(__dirname,'../../../')  是错误的写法,不同系统上的结果可能不同.
                return path.join(process.resourcesPath, 'resources/config.json');
            }
         }
    
        loadConfig() {
           try{
                const configPath = this.getConfigPath();
                const rawData:string = fs.readFileSync(configPath, 'utf-8');
                const config = JSON.parse(rawData);
                this.brand = config.brand;
                this.printerIP = config.printerIP;
    
                 fs.watchFile(configPath, (curr, prev) => {
                   if (curr.mtime > prev.mtime) {
                      // 文件有更新,重新加载
                     this.loadConfig();
                    }
                });
            }catch(e){
             //错误处理,可以记录日志
                console.error("Failed to load config.json:",e)
            }
        }
        mounted() {
            this.loadConfig();
        }
    }
    </script>
    
  2. 使用 fs.readFileSync 读取文件:

  • 获取 config.json 的完整路径. 这里注意, 生产环境中, config.json位于应用程序的 resources 文件夹. 这里的关键是根据环境构建不同的文件读取路径.
    - 使用 fs.readFileSync 同步读取文件内容。注意,用同步方法简单, 但是大文件可能造成阻塞.
    - 使用fs.watchFile 监听文件的修改。这样, 修改 config.json 就能立即应用, 不用重启软件.
  1. 解析 JSON:

    使用 JSON.parse() 把读取到的文件内容(字符串)转换为 JavaScript 对象。

2.3 进阶: 使用异步读取 和 封装配置读取

同步读取可能阻塞 UI 线程。 大的配置文件,可以改用异步读取。

    //App.vue (<script lang="ts">)
    //... 引入fs, path 和前面的例子相同

    async loadConfigAsync() {
          try {
              const configPath = this.getConfigPath();

              // 使用异步读取
              const rawData = await fs.promises.readFile(configPath, 'utf-8');
              const config = JSON.parse(rawData);

              this.brand = config.brand;
              this.printerIP = config.printerIP;
            //使用 fs.watch 更好, 但为了演示promises,这里用setTimeout轮询
             setTimeout(() => this.loadConfigAsync(),5000) //5秒检查一次.

          } catch (error) {
            // 处理错误
             console.error("读取config出错", error)
          }
    }

     getConfigPath(): string {
           //开发环境和生产环境获取路径方式不一样
           const isDevelopment = process.env.NODE_ENV === 'development'
            if (isDevelopment) {
                 return path.join(__dirname, '../../resources/config.json');
            }else{
                return path.join(process.resourcesPath, 'resources/config.json');
            }
         }

可以将配置相关的逻辑提取到单独的模块, 实现更好的代码组织:

    // configManager.ts

     import * as fs from 'fs';
     import * as path from 'path';

     let configCache:any = null;

      function getConfigPath(): string {
           //开发环境和生产环境获取路径方式不一样
           const isDevelopment = process.env.NODE_ENV === 'development'
            if (isDevelopment) {
                 return path.join(__dirname, '../../resources/config.json');
            }else{
                return path.join(process.resourcesPath, 'resources/config.json');
            }
         }

    export async function loadConfig():Promise<any> {

        try {
            const configPath:string = getConfigPath()
            const data:string = await fs.promises.readFile(configPath, 'utf-8');
            configCache = JSON.parse(data);

             fs.watchFile(configPath, (curr, prev) => {
                if (curr.mtime > prev.mtime) {
                  fs.promises.readFile(configPath, 'utf-8')
                   .then( newData=> configCache = JSON.parse(newData))
                   .catch(err => console.error("重载配置出错", err));
                }
            });

             return configCache;
        }catch(error){
            console.error("读取配置失败:", error)
            return null
        }
    }

   export function getConfig(){
        return configCache;
   }
    //App.vue
    import {loadConfig, getConfig} from './configManager'

    //...

      async mounted() {
            await loadConfig()
            const config = getConfig();

            if(config){
                this.brand = config.brand;
                 this.printerIP = config.printerIP;
             }

        }

2.4 安全建议

对配置文件内容进行校验。 用户可能错误编辑导致应用异常。 加入类型判断,缺失值赋予默认值等措施,提高程序的健壮性。

三、 总结一下

通过把 config.json 文件放到 Electron 的 resources 目录,并使用 Node.js 的 fs 模块读取,就成功实现了在 Vue CLI 3 和 Electron 构建的应用中读取动态配置的需求。 而且文件变化会触发更新,很方便!