【原創】從零開始搭建Electron+Vue+Webpack項目框架,一套程式碼,同時構建客戶端、web端(二)

  • 2019 年 11 月 5 日
  • 筆記

摘要上篇文章說到了如何新建工程,並啟動一個最簡單的Electron應用。“跑起來”了Electron,那就接著把Vue“跑起來”吧。有一點需要說明的是,webpack是貫穿這個系列始終的,我也是本著學習的態度,去介紹、總結一些常用到的配置及思路,有不恰當的地方,或者待優化的地方,歡迎留言。項目完整程式碼:https://github.com/luohao8023/electron-vue-template

下面開始~~~

一、安裝依賴
vue、webpack:不多說了
vue-loader:解析、轉換.vue文件
vue-template-compiler:vue-loader的依賴包,但又獨立於vue-loader,簡單的說,作用就是使用這個插件將template語法轉為render函數
webpack-dev-server:快速搭建本地運行環境的工具
webpack-hot-middleware:搭配webpack-dev-server使用,實現熱更新
chalk:命令行輸出帶有顏色的內容
依賴包就介紹這麼多,後面需要什麼可以自行下載,這裡不多贅述了。
 
二、完善工程目錄
  
webpack.render.config.js:渲染進程打包配置
dev.js:本地調試腳本
views:頁面程式碼
index.js:vue工程入口文件
index.ejs:打包生成html文件時的模板
三、配置Vue工程
1、編寫入口文件,render>index.js
import Vue from 'vue';  import index from './views/index.vue';    //取消 Vue 所有的日誌與警告  Vue.config.silent = true;  new Vue({      el: '#app',      render: h => h(index)  });

2、編寫根組件,render>views>index.vue

<template>      <div class="content">          <h1>Welcome to electron-vue-template!</h1>      </div>  </template>    <script>  export default {}  </script>  <style></style>

3、編寫html模板文件,render>index.ejs,webpack解析、打包vue文件時,以此模板生成html文件

<!DOCTYPE html>  <html lang="zh-CN">  <!--template for 2019年10月30日-->  <!--<%= new Date().getFullYear()+'/'+(new Date().getMonth()+1)+'/'+new Date().getDate()+' '+new Date().getHours()+':'+new Date().getMinutes() %>-->  <head>      <meta charset="UTF-8">      <title>模板文件</title>      <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">      <meta HTTP-EQUIV="pragma" CONTENT="no-cache">      <meta HTTP-EQUIV="Cache-Control" CONTENT="no-store, must-revalidate">      <meta HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT">      <meta HTTP-EQUIV="expires" CONTENT="0">  </head>  <body>      <div id="app"></div>  </body>  </html>

4、編寫webpack配置文件,builder>webpack.render.config.js,建議按照本文這種方式,把配置文件單獨抽出來,這樣的話,本地調試和打包可以共用一套配置,只需要傳遞不同參數就可以了,不要把所有的配置和打包邏輯寫在一個文件里,太長、太亂、太難維護

/*  Name:    渲染進程配置  Author: haoluo  Date:   2019-10-30   */  const path = require('path');  const HtmlWebpackPlugin = require('html-webpack-plugin');  const devMode = process.env.NODE_ENV === 'development';    module.exports = {      mode: devMode ? 'development' : 'production',      entry: {          main: './src/render/index.js'      },      output: {          path: path.join(__dirname, '../app/'),          publicPath: devMode ? '/' : '',          filename: './js/[name].[hash:8].js'      },      module: {          rules: [              {                  test: /.vue$/,                  exclude: /node_modules/,                  loader: 'vue-loader'              }          ]      },      plugins: [          new HtmlWebpackPlugin({              template: './src/render/index.ejs',              filename: './index.html',              title: 'electron-vue-template',              inject: false,              hash: true,              mode: devMode          })      ]  }

適當解釋一下:

mode:環境參數,針對不同的環境,webpack內部有一些不同的機制,並對相應環境做相應的優化

entry:入口,webpack執行構建的第一步將從入口文件開始,遞歸查詢並解析所有依賴的模組。配置方式有多種,可參考webpack文檔,這裡我們配置的路徑是’./src/render/index.js’,意思是src目錄下,render文件夾下的index.js,而webpack配置文件是在builder文件夾下,那這個“./”的相對路徑到底是相對於誰呢?這就得說一下webpack中的路徑問題了,context 是 webpack 編譯時的基礎目錄,入口起點(entry)會相對於此目錄查找,那這個context又是個什麼東西?webpack源碼有關默認配置中有這麼一句話

this.set("context", process.cwd());

這就是context的默認值,工程的根目錄,那這個entry的配置就很好理解了。

output:打包的輸入配置,路徑建議設置為絕對路徑。

module和plugins就不多說了。

5、編寫本地調試腳本

/**  * Tip:    調試渲染進程  * Author: haoluo  * Data:   2019-10-30  **/  process.env.NODE_ENV = 'development';  const webpack = require('webpack');  const WebpackDevServer = require('webpack-dev-server');  const webpackHotMiddleware = require('webpack-hot-middleware');  const chalk = require('chalk');
const http = require('http');
function devRender() {      console.log('啟動渲染進程調試......');      const webpackDevConfig = require('./webpack.render.config.js');      const compiler = webpack(webpackDevConfig);      new WebpackDevServer(          compiler, {              contentBase: webpackDevConfig.output.path,              publicPath: webpackDevConfig.output.publicPath,              open: true,//打開默認瀏覽器              inline: true,//刷新模式              hot: true,//熱更新              quiet: true,//除第一次編譯外,其餘不顯示編譯資訊              progress: true,//顯示打包進度              setup(app) {                  app.use(webpackHotMiddleware(compiler));                  app.use('*', (req, res, next) => {                      if (String(req.originalUrl).indexOf('.html') > 0) {                          getHtml(res);                      } else {                          next();                      }                  });              }          }      ).listen(8099, function(err) {          if (err) return console.log(err);          console.log(`Listening at http://localhost:8099`);      });      compiler.hooks.done.tap('doneCallback', (stats) => {          const compilation = stats.compilation;          Object.keys(compilation.assets).forEach(key => console.log(chalk.blue(key)));          compilation.warnings.forEach(key => console.log(chalk.yellow(key)));          compilation.errors.forEach(key => console.log(chalk.red(`${key}:${stats.compilation.errors[key]}`)));          console.log(chalk.green(`${chalk.white('渲染進程調試完畢n')}time:${(stats.endTime-stats.startTime)/1000} s`));      });  }    function getHtml(res) {      http.get(`http://localhost:8099`, (response) => {          response.pipe(res);      }).on('error', (err) => {          console.log(err);      });  }    devRender();

都是一些常規操作,可以閱讀一下程式碼。

6、配置啟動命令,在package.json中新增dev命令,啟動本地調試(先起了再說,報錯什麼的,見招拆招)

  "scripts": {      "start": "electron ./src/main/main.js",      "dev": "node ./builder/dev.js"    },

然後命令行運行npm run dev。。。。。。反正我這兒是報錯了。。。說是找不到html-webpack-plugin模組,那就運行npm i html-webpack-plugin -D安裝一下,如果步驟一沒有做的話,後面可能還會遇到很多模組找不到的情況,解決方法很簡單,缺什麼安裝什麼就好了。安裝完所有的模組之後,啟動,還是報錯了。。。。。。

ModuleNotFoundError: Module not found: Error: Can't resolve 'vue' in ...  ModuleNotFoundError: Module not found: Error: Can't resolve 'vue-loader' in ...

檢查了下package.json文件和node_modules,發現我的vue-loader沒有裝,然後就是裝一下(如果沒有遇到這個步驟,可以忽略)

再次運行

  

這個報錯就很友好了嗎,就是vue-loader告訴你,必須安裝vue-template-compiler插件,不然就不工作,那就裝一下。

接著運行,就知道沒那麼容易成功

  

vue-loader報錯說缺少了插件,讓檢查是否配置了VueLoaderPlugin插件,搜一下這是個什麼鬼,看這裡,15+版本的vue-loader需要配合VueLoaderPlugin使用,然後看了一下我使用的vue-loader版本15.7.1,那就配一下這個東西。

  

接著運行,終於沒有報錯了,但是頁面為啥子是白的,我的h1標籤呢?冷靜下來分析一下問題,頁面沒有東西說明我打包時生成的html文件有問題(devServer會把打包出來的靜態文件保存在記憶體里),而html文件是根據ejs模板生成的,那會不會是模板配置有問題?

  

看一下我們的模板,結構是沒什麼問題啊,但是,沒有引用css和js文件啊,也就是我們辛辛苦苦解析vue文件,打包css和js,最後卻沒有引用。。。好吧,那就再配置一下ejs模板,把相應的文件引入一下

<!DOCTYPE html>  <html lang="zh-CN">  <!--template for 2019年10月30日-->  <!--<%= new Date().getFullYear()+'/'+(new Date().getMonth()+1)+'/'+new Date().getDate()+' '+new Date().getHours()+':'+new Date().getMinutes() %>-->  <%  function getFilePath(filename,libsPath){      let _filenameSearchIndex=filename.indexOf("?");      let _libsPathSearchIndex=libsPath.indexOf("?");      let _filename=filename.substr(0,_filenameSearchIndex<1?filename.length:_filenameSearchIndex);      let _libsPath=libsPath.substr(0,_libsPathSearchIndex<1?libsPath.length:_libsPathSearchIndex);      let htmlfilename=path.relative(_filename,_libsPath);      return libsPath;  }  let path = require('path'),jsArr = [],cssArr = [];  let filename="./index.html";      //修正目錄結構      for(let i=0;i<htmlWebpackPlugin.files.css.length;i++){          let name=getFilePath(filename,String(htmlWebpackPlugin.files.css[i]));          cssArr.push(name);      }      for(let i=0;i<htmlWebpackPlugin.files.js.length;i++){          let name=getFilePath(filename,String(htmlWebpackPlugin.files.js[i]));          jsArr.push(name);      }  %>  <head>      <meta charset="UTF-8">      <title><%= htmlWebpackPlugin.options.title %></title>      <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">      <meta HTTP-EQUIV="pragma" CONTENT="no-cache">      <meta HTTP-EQUIV="Cache-Control" CONTENT="no-store, must-revalidate">      <meta HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT">      <meta HTTP-EQUIV="expires" CONTENT="0">      <% cssArr.forEach(css=>{ %><link rel="stylesheet" href="<%= css %>" />      <% }) %>  </head>  <body>      <div id="app"></div>      <% jsArr.forEach(js=>{ %><script type="text/javascript" src="<%= js %>"></script>      <% }) %>  </body>  </html>

我們可以在ejs中拿到html-webpack-plugin插件的一些資訊,比如插件配置、生成的文件等,然後拿到js和css文件,並引入進來,這裡建議看一下ejs模板語法。

我們接著運行,終於出來了。

  

7、配置打包腳本

在builder文件夾下新建build.js,引入配置,直接運行webpack打包即可,不需要devServer。

/**  * Tip:    打包  * Author: haoluo  * Data:   2019-10-30  **/  process.env.NODE_ENV = 'production';  const chalk = require("chalk");  const del = require("del");  const webpack = require('webpack');  const renderConfig = require('./webpack.render.config.js');    del(["./app/*"]); //刪除歷史打包數據    viewBuilder().then(data => {      console.log("打包輸出===>", data)  }).catch(err => {      console.error("打包出錯,輸出===>", err);      process.exit(1);  });    function viewBuilder() {      return new Promise((resolve, reject) => {          console.log("打包渲染進程......");          const renderCompiler = webpack(renderConfig);          renderCompiler.run((err, stats) => {              if (err) {                  console.log("打包渲染進程遇到Error!");                  reject(chalk.red(err));              } else {                  let log = "";                  stats.compilation.errors.forEach(key => {                      log += chalk.red(`${key}:${stats.compilation.errors[key]}`) + "n";                  })                  stats.compilation.warnings.forEach(key => {                      log += chalk.yellow(key) + "n";                  })                  Object.keys(stats.compilation.assets).forEach(key => {                      log += chalk.blue(key) + "n";                  })                  log += chalk.green(`time:${(stats.endTime-stats.startTime)/1000} sn`) + "n";                  resolve(`${log}`);              }          })      })  }

在package.json中新增打包命令

"scripts": {      "start": "electron ./src/main/main.js",      "dev": "node ./builder/dev.js",      "build": "node ./builder/build.js"    },

npm run build執行打包,這次還真是出奇的順利啊,看一下app文件夾,已經生成了靜態文件,然後直接在瀏覽器打開index.html文件,正常顯示。

 

四、使用vuex,vue-router,axios

說好的全家桶呢,這裡我們不用vue-resource了,使用axios。

1、使用vuex

安裝vuex依賴,在src>render文件夾下新建store文件夾,並在store文件夾下新增:

actions.js

export default {}

index.js

import Vue from 'vue';  import Vuex from 'vuex';  import actions from './actions.js';  import mutations from './mutations.js';  Vue.use(Vuex);  // 這裡為全局的,模組內的請在模組內動態註冊  const store = new Vuex.Store({      strict: true,      state: {          userInfo: {              name: 'haoluo',              address: 'beijing'          }      },      getters: {},      mutations,      actions  });  export default store;

mutations.js

export default {      //設置用戶資訊      setUserInfo(state, config) {          if (!config) {              state.userInfo = {};          }          for (var objName in config) {              state.userInfo[objName] = config[objName];          }      }  }

以上三個文件的實力程式碼,比官網教程還簡單,可以自行研究一下文檔。

文件建好之後,需要把store掛載到vue實例上,找到vue工程的入口文件,src>render>index.js

import Vue from 'vue';  import store from './store/index.js';  import index from './views/index.vue';    //取消 Vue 所有的日誌與警告  Vue.config.silent = true;  new Vue({      el: '#app',      store: store,      render: h => h(index)  });

然後我們就可以使用啦,找到根組件,src>render>views>index.vue

<template>      <div class="content">          <h1>Welcome to electron-vue-template!</h1>          <h2>name:{{userInfo.name}}</h2>          <h2>address:{{userInfo.address}}</h2>      </div>  </template>    <script>  import {mapState} from 'vuex';  export default {      computed: {          ...mapState(['userInfo'])      }  }  </script>  <style></style>

mapState是state的輔助函數,是個語法糖,藉助mapState我們可以更方面的獲取屬性,而不需要寫一堆啰里吧嗦的東西,通過計算屬性computed接收userInfo,然後就可以使用啦,運行本地調試,發現頁面上已經可以正常顯示了

屬性有了之後我們可以使用,但如果想要改變vuex中存儲的屬性呢?為了保證單向數據流以及方便對數據的追蹤等一些其他原因,不建議直接修改vuex的屬性,而是需要通過mutations,這裡也有一個輔助函數mapMutations,用法同mapState類似,只不過需要用methods去接收,作為一個全局方法使用

<!-- render>views>index.vue -->  <template>      <div class="content">          <h1>Welcome to electron-vue-template!</h1>          <h2>name:{{userInfo.name}}</h2>          <h2>address:{{userInfo.address}}</h2>          <button @click="changeAddress">設置address為tianjin</button>      </div>  </template>    <script>  import {mapState,mapMutations} from 'vuex';  export default {      computed: {          ...mapState(['userInfo'])      },      methods: {          ...mapMutations(['setUserInfo']),          changeAddress() {              this.setUserInfo({                  address: 'tianjin'              });          }      }  }  </script>  <style></style>

當點擊按鈕的時候。userInfo中的address被修改了,頁面渲染的值也相應的改變了

 

2、使用vue-router

安裝vue-router依賴,在render文件夾下新增router文件夾,並在其中新增index.js

module.exports = [      {          path: '/index.html',          name: 'index',          meta: {              title: '首頁',              author: '--',              parentRouter: '--'          },          component: (resolve) => {              require.ensure([], () => {                  return resolve(require('../views/index.vue'))              }, "index")          },          children: []      }  ];

在入口文件render>index.js中引入並掛載

// render>index.js  import Vue from 'vue';  import VueRouter from 'vue-router';  import store from './store/index.js';  import routers from './router/index.js';  import index from './views/index.vue';    Vue.use(VueRouter);    let router = new VueRouter({      routes: routers  })    //取消 Vue 所有的日誌與警告  Vue.config.silent = true;  new Vue({      el: '#app',      router: router,      store: store,      render: h => h(index)  });

運行一下,頁面可以正常顯示,在地址欄輸入http://localhost:8099/index.html時,也是沒有問題的,現在新增加一個頁面,訂單頁

<template>      <div class="content">          <h1>order page!</h1>      </div>  </template>    <script>  export default {}  </script>  <style></style>

再配置下路由

module.exports = [      {          path: '/index.html',          name: 'index',          meta: {              title: '首頁',              author: '--',              parentRouter: '--'          },          component: (resolve) => {              require.ensure([], () => {                  return resolve(require('../views/index.vue'))              }, "index")          },          children: []      },      {          path: '/order.html',          name: 'order',          meta: {              title: '訂單頁',              author: '--',              parentRouter: '--'          },          component: (resolve) => {              require.ensure([], () => {                  return resolve(require('../views/order.vue'))              }, "order")          },          children: []      }  ];

並在首頁index.vue中增加跳轉按鈕,運行之後,發現跳不過去,尷尬~~~,路由跳轉,需要有<router-view></router-view>去接收才行啊

改造一下吧,views下新增home.vue,把index.vue中的內容拷貝過去,index.vue改為下面這樣

<!-- render>views>index.vue -->  <template>      <div>          <router-view></router-view>      </div>  </template>    <script>  export default {      methods: {},      mounted() {          this.$router.push({              name: 'home'          });      }  }  </script>  <style></style>

router文件改為下面這樣

module.exports = [      {          path: '/index.html',          name: 'index',          meta: {              title: '首頁',              author: '--',              parentRouter: '--'          },          component: (resolve) => {              require.ensure([], () => {                  return resolve(require('../views/index.vue'))              }, "index")          },          children: [              {                  path: '/home.html',                  name: 'home',                  meta: {                      title: 'home頁',                      author: '--',                      parentRouter: '--'                  },                  component: (resolve) => {                      require.ensure([], () => {                          return resolve(require('../views/home.vue'))                      }, "home")                  },                  children: []              },              {                  path: '/order.html',                  name: 'order',                  meta: {                      title: '訂單頁',                      author: '--',                      parentRouter: '--'                  },                  component: (resolve) => {                      require.ensure([], () => {                          return resolve(require('../views/order.vue'))                      }, "order")                  },                  children: []              }          ]      }  ];

再次運行,頁面已經可以正常跳轉了。

3、axios,這裡暫時不說,後續electron和vue聯調的時候會補上。

幾點說明:

  1、截止目前,webpack配置以及dev和build腳本僅僅是調通大致的流程,還有很多可優化可考究的地方,後續完善項目的過程中會對打包流程逐漸潤色;

  2、vue全家桶在實際項目中高級用法很多,細節也很多,這裡只是最簡單的用法,如若用到實際項目中,還要基於目前情況再做細化;

  3、本系列文章旨在記錄、回顧整個項目框架搭建流程,把握整體結構,很多地方需要根據實際項目再做處理;

  4、如有錯誤或不當的地方,歡迎指出,共同進步!