【原創】從零開始搭建Electron+Vue+Webpack專案框架,一套程式碼,同時構建客戶端、web端(二)
導航:
(一)Electron跑起來
(二)從零搭建Vue全家桶+webpack專案框架
(三)Electron+Vue+Webpack,聯合除錯整個專案(未完待續)
(四)Electron配置潤色(未完待續)
(五)預載入及自動更新(未完待續)
(六)構建、釋出整個專案(包括client和web)(未完待續)
摘要:上篇文章說到了如何新建工程,並啟動一個最簡單的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.jsimport 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} s\n`) + "\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、如有錯誤或不當的地方,歡迎指出,共同進步!