用 vue2 和 webpack 疾速建构 NW.js 项目(2)

打包NW.js运用和制造windows装置文件

更新:
此文章部份手艺点已落伍,能够检察 最新文章

这多是中文史上最细致的 NW.js 打包教程

本文顺应有肯定 js 基本,第一次玩 windows 下 setup 打包的同砚,默许的环境 windows。然后,文章太过于详确,看完会消耗大批时刻,暂时不想实操的,我会直接供应一个 vue-nw-seed 种子项目,包括了当前文章的一些优化点。

本文涉及到的点:

  • Node.js 打包 zip 、文件处置惩罚、crypto 提取 MD5 、iconv 处置惩罚字符串等

  • Resource Hacker 设置运用的权限、图标、版权等

  • InnoSetup 制造装置包、iss 文件设置

  • NW.js 运用的更新(增量、全量更新)

未涉及到的点:

  • 代码加密,本着前端的心态做的桌面端运用,代码 Uglify 后就已不可看了。假如有秘要代码或许加密算法等须要别的斟酌,不在本文的议论局限,供应一个官方文档 Protect JavaScript Source Code

一、折腾能力强,直接上文档

  1. How-to-package-and-distribute-your-apps

  2. setup-on-windows

这部份没啥好说的,都很简朴。

对新手友爱。。。另有个 NW.js 的打包在 gayhub 上还特地有个 npm 包 nw-builder ,这个用起来就更简朴了,我连示例都不想写的那种简朴。然后这儿须要下载 NW.js 的 SDK 或许 NORMAL 的包,要领同我上一篇文章 用 vue2 和 webpack 疾速建构 NW.js 项目收集不太好 部份

二、自助打包

NW.js 被打包出来后是一个文件夹,内里有全部 runtime 和一个 exe 文件,这时刻全部打包就胜利了,差不多有 100MB 摆布。
然则,我们的运用不再是给内部运用,给用户下载总不能直接给用户拷贝一个文件夹或许下载 zip 紧缩包,那样忒不靠谱的模样,还以为是啥病毒呢。

我们能不能就像吃自助餐那样,想吃啥就拿啥,想打包成啥样就弄成啥样。

完成思绪
本身搞一个 runtime,然后用 Node.js 对打包好的代码举行 zip 紧缩为 package.nw,然后放到 runtime 中,再用官方引荐的 InnoSetup 来打包成一个 setup.exe。

1. XP 兼容性题目

运用 NW.js 的主要上风是兼容 XP,教诲行业这个真的很主要呀。。。
NW.js 不是全版本都支撑 XP,由于 Chromium50 最先就不支撑XP了,所以假如你的客户端要支撑 XP,现在最好的版本挑选是 0.14.7 。拜见 NW.js 的博客 NW.js v0.14.7 (LTS) Released

2. 制造一个本身的 runtime

从官网 http://dl.nwjs.io/v0.14.7/ 下载一个 normal 的包,然后在此基本上举行 DIY。

也许目次就是这模样
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

然后就最先优化和自定义事情:

1) 先整顿下 locales 下的语言包,削减部份冗余。

2) 替代下 ffmpeg.dll 处理部份花样 video 的播放题目等,下载的时刻注意下版本,和 NW.js 相对应就好。

3) 将 nw.exe 改名字为我们的运用的名字,比方myProgramApp.exe,更正规一点。然后用 Resource Hacker 修改下版本和版权公司等相干信息。

4) 再用运用 Resource Hacker 举行图标替代,发起尺寸是256。

5) 同时为其增加管理员权限。由于我们要做增量更新,须要用 Node.js 写文件到运用地点目次,当装置目次是 C:\Program Files\ 的时刻,一般权限用户没有写权限。
详细操作照样用 Resource Hacker 翻开myProgramApp.exe,找到 Manifest

<requestedExecutionLevel level="asInvoker" uiAccess="false"/></requestedPrivileges>

修改成

<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/></requestedPrivileges>

弄完了也许是这个模样
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

3. 用 Node.js 打包 package.nw

须要一个 zip 处置惩罚的依靠 archiver,第一次用这个依靠,发起直接去看他们的英文文档,郑重运用 bulk 这个要领,在 0.21.0 的时刻就被烧毁了。
打包 zip 的要领也许就长如许:

const fs = require('fs')
const archive = require('archive')

function buildZipFile({ outZipPath, files, mainPackage } = {}) {
  let filesArr = Array.isArray(files) ? files : [files]

  // 建立一个可写流的 zip 文件
  var output = fs.createWriteStream(outZipPath)
  var archive = archiver('zip', { store: true })

  archive.on('error', console.error)

  // 打包 dist 目次为 zip 紧缩包花样的 nw 文件
  archive.pipe(output)

  if (filesArr.length > 0) {
    filesArr.forEach(p => {
      if (!p) return

      // 剔除 package.json
      let hasPackJson = path.resolve(p, 'package.json')
      if (fs.existsSync(hasPackJson)) fs.unlinkSync(hasPackJson)

      // 紧缩目次
      archive.directory(p, '')
    })

    // 增加 package.json
    archive.file(mainPackage, { name: 'package.json' })
  }

  archive.finalize()
}

4. InnoSetup 打包装置包

Node.js 的雄厚的生态已有人供应了一个 node-innosetup-compiler 了,所以这个也很轻易。不过关于我这类第一次玩这个的玩家照样有点懵逼,特别是谁人 iss 文件的编写。。。

鉴于本文不想写成 InnoSetup 的运用教程,所以只讲讲一般运用,假如你须要更庞杂的功用,给你个文档 Inno Setup Help

我供应一个我用的 setup.iss 文件,其顶用下划线开首(如: _appName )这类将会被 js 正则婚配掉

; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
; 该实行目次为 setup.iss 地点的目次,请注意拼接相对目次

#define MyAppName "_appName"
#define MyAppNameZh "_appZhName"
#define MyAppVersion "_appVersion"
#define MyAppPublisher "_appPublisher"
#define MyAppURL "_appURL"
#define MyAppExeName "_appName.exe"
#define OutputPath "_appOutputPath"
#define SourceMain "_appRuntimePath\_appName.exe"
#define SourceFolder "_appRuntimePath\*"
#define LicenseFilePath "_appResourcesPath\license.txt"
#define SetupIconFilePath "_appResourcesPath\_appName.ico"
#define MyAppId "_appId"

[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\{#MyAppName}
LicenseFile={#LicenseFilePath}
OutputDir={#OutputPath}
OutputBaseFilename={#MyAppName}-v{#MyAppVersion}-setup
SetupIconFile={#SetupIconFilePath}
Compression=lzma
SolidCompression=yes
PrivilegesRequired=admin
Uninstallable=yes
UninstallDisplayName={#MyAppNameZh}
DefaultGroupName={#MyAppNameZh}

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce

[Files]
Source: {#SourceMain}; DestDir: "{app}"; Flags: ignoreversion
Source: {#SourceFolder}; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Icons]
Name: "{commondesktop}\{#MyAppNameZh}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{group}\{#MyAppNameZh}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\卸载{#MyAppNameZh}"; Filename: "{uninstallexe}"

[Languages]
Name: "chinese"; MessagesFile: "innosetup\Languages\ChineseSimp.isl"

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

建立一个 resources 文件夹,内里放上 icon 和 license,就像如许
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

再然后此 iss 合营 makeExeSetup 运用,分外酸爽,请疏忽那一串 replace,233333333

// 新依靠,用于处置惩罚 utf 和 ansi 的字符串
const iconv = require('iconv-lite')

function makeExeSetup(opt) {
  const { issPath, outputPath, mainPackage, runtimePath, resourcesPath, appPublisher, appURL, appId } = opt
  const { name, appName, version } = require(mainPackage)
  const tmpIssPath = path.resolve(path.parse(issPath).dir, '_tmp.iss')
  const innosetupCompiler = require('innosetup-compiler')

  // rewrite name, version to iss
  fs.readFile(issPath, null, (err, text) => {
    if (err) throw err

    let str = iconv.decode(text, 'gbk')
      .replace(/_appName/g, name)
      .replace(/_appZhName/g, appName)
      .replace(/_appVersion/g, version)
      .replace(/_appOutputPath/g, outputPath)
      .replace(/_appRuntimePath/g, runtimePath)
      .replace(/_appResourcesPath/g, resourcesPath)
      .replace(/_appPublisher/g, appPublisher)
      .replace(/_appURL/g, appURL)
      .replace(/_appId/g, appId)


    fs.writeFile(tmpIssPath, iconv.encode(str, 'gbk'), null, err => {
      if (err) throw err

      // inno setup start
      innosetupCompiler(tmpIssPath, { gui: false, verbose: true }, function(err) {
        fs.unlinkSync(tmpIssPath)
        if (err) throw err
      })
    })
  })
}

这个时刻就可以制造出一个装置包了,就像如许
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

然后是装置的流程
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

装置完成的目次
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

三、炫酷的装置界面

虽然 InnoSetup 简朴好使,然则制造出来的装置包的装置界面默许是 windows2000 的界面,谁人丑谁人老旧哟。。。

假如你的运用只需能用就好了,那这一步已完整够了。
但手艺人怎样能不折腾,下面,我们来搞炫酷的装置包的制造要领。

先摆一个被我模拟的例子 INNOSETUP 仿有道云装置包界面,同时另有个参考材料:互联网软件的装置包界面设想-Inno setup 至心吐个槽,这方面的材料真少。。。

我实在都依据已有的素材包写好了一个了,但我们的 ui 还没设想出更美丽的装置界面出来,所以,我就暂时不放相干资本和结果了。

四、运用的更新

这一块,应当是最轻松的,蛤。

我们的更新战略分为两种,一种是只更新我们的营业代码,每次只须要下载1MB多的营业代码就搞定,走增量更新渠道;另一种是更新了我们的 runtime ,或许其他啥玩意的主要更新,须要全量更新,走全量更新的渠道。

完成思绪
在打包的时刻把版本和更新信息写入到 update.json 中,在每次客户端翻开的时刻都去请求这个 json ,搜检 json 中版本和客户端版本是不是婚配,不婚配则依据 json 中的商定划定规矩举行增量更新或全量更新。

1、预备好更新文件

一个开辟准绳是能懒就懒,能用东西做的就肯定要用东西做。蛤蛤,在这个准绳的对峙下,我们来继承优化上文提到的打包建构。

用 Node.js 把之前暂时放在 runtime 中的 package.nw (zip) 包拷贝到 output 目次,再依据 changelog.txt 文件写更新信息到 update.json 中。

预备一个 changelog.txt 文件在 config 设置目次下,也许就长这模样,每次更新以--- 举行支解,第一行是版本,背面是更新信息:

0.1.0
- 程序员 peter 最先开辟了!
- 趁便,请老板给 peter 涨工资。
---
1.0.0
- 客户端正式版胜利宣布啦!
- 同时,peter 由于请求涨工资已被打残住院中,所以暂时不会有其他更新。
---

有同砚问我,为啥要这么设想个 log.txt 出来,不直接用 json 等其他情势举行形貌?
由于这个文件在将来能够要被打包到运用中,连同 license 文件举行打包;另有就是星散这部份形貌,更容易扩大。

然后写一读取这个 log 的要领

function getLatestLogBycheckVersion({ changelogPath, mainPackage }) {
  // get package.json by package
  const packageJson = require(mainPackage)

  // check version
  // 大于即是3是由于正当的版本信息起码 "---" 有3个长度
  const changeLogArr = fs.readFileSync(changelogPath, 'utf-8').split('---').filter(v => v.trim().length >= 3)
  const latestInfo = changeLogArr.pop().split('\n').map(v => v.trim()).filter(v => v.length)
  const version = latestInfo[0]

  if (packageJson.version !== version) {
    // 更新 package.json 的版本
    packageJson.version = version
    fs.writeFileSync(mainPackage, JSON.stringify(packageJson, null, '  '), 'utf-8')
  }
  return latestInfo
}

// 这就是全局的 options
opt.latestLog = getLatestLogBycheckVersion(opt)

// 更新商定,用来推断当前版本是不是须要增量更新
opt.noIncremental = process.argv.indexOf('--noIncremental') >= 0

增量更新的商定
经由过程 process.argv 来检测当前是不是须要增量更新,并写入到 options 中,这一点看起来有点轻微烦琐,假如有其他更好的点子,迎接积极来提 issue 或许直接私信我,感谢!

接下来继承处置惩罚打包完成的系列流程,需求是要挪动 nw 到 output 目次,还要写一个 update.json

const crypto = require('crypto')

function finishedPackage(opt) {
  const { mainPackage, outputPath, latestLog, outZipPath, updateServerPath, noIncremental } = opt
  const { name, appName, version } = require(mainPackage)

  let versionCode = parseInt(version.replace(/\./g, ''))
  let updateDesc = latestLog.slice(1).join('#%#')

  let outNWName = `${name}-v${version}.nw`
  let outNWPath = path.resolve(outputPath, outNWName)
  let updateJsonPath = path.resolve(outputPath, 'update.json')

  // write update.json
  let updateJson = {
    appName,
    version,
    versionCode,
    requiredVersion: version,
    requiredVersionCode: versionCode,
    updateDesc,
    filePath: updateServerPath + outNWName,
    incremental: !noIncremental
  }

  // fileSize and MD5
  getMd5ByFile(outZipPath, (err, hexStr) => {
    if (err) throw err
    updateJson.MD5 = hexStr
    updateJson.fileSize = fs.statSync(outZipPath).size
    fs.writeFileSync(updateJsonPath, JSON.stringify(updateJson, null, '  '), 'utf-8')

    copyFile(outZipPath, outNWPath)
    fs.unlink(outZipPath, err => err && console.error(err))
  })
}

function getMd5ByFile(filePath, callback) {
  let rs = fs.createReadStream(filePath)
  let hash = crypto.createHash('md5')
  rs.on('error', err => {
    if (typeof callback === 'function') callback(err)
  })
  rs.on('data', hash.update.bind(hash))
  rs.on('end', () => {
    if (typeof callback === 'function') callback(null, hash.digest('hex'))
  })
}

function copyFile(src, dst) {
  fs.createReadStream(src).pipe(fs.createWriteStream(dst))
}

全部打包完了差不多就这模样了
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

谁人 update.json 内里的现实内容就是这些

{
  "appName": "doudou",
  "version": "1.0.1-beta19",
  "versionCode": 101,
  "requiredVersion": "1.0.1-beta19",
  "requiredVersionCode": 101,
  "updateDesc": "- 程序员 peter 无话可说",
  "filePath": "http://upgrade.iclassedu.com/doudou/upgrade/teacher/doudou-v1.0.1-beta19.nw",
  "incremental": true,
  "MD5": "9be46fc8fb04d38449eeb4358c3b5a31",
  "fileSize": 5469
}

2、猎取 update.json 并搜检更新

上代码,代码切换到 src 目次中,在我们的运用代码中写上 utils/update.js 的相干要领。详细的几个小要领,看解释吧。

import { updateApi } from 'config/app'
import { App } from 'nw.gui'

const options = { method: 'GET', mode: 'cors', credentials: 'include' }
let tmpUpdateJson = null

// 请求 update.json,返回的是 promise 范例的 json
export function getUpdateJson(noCache) {
  if (!noCache && tmpUpdateJson) return new Promise((resolve, reject) => resolve(tmpUpdateJson))
  return window.fetch(updateApi + '?' + (new Date().getTime()), options)
    .then(resp => resp.json())
    .then(json => {
      tmpUpdateJson = json
      return tmpUpdateJson
    })
}

// 搜检版本,假如有更新则跳转到更新页面
export function checkUpdate() {
  getUpdateJson().then(json => {
    if (json.version === App.manifest.version) return
    setTimeout(() => { window.location.hash = '/update' }, 500)
  })
}

然后在 main.js 中举行更新搜检

// 优先更新
import { checkUpdate } from '@/utils/update'
if (process.env.NODE_ENV !== 'development') checkUpdate()

3、更新

在上面的基本上做增量更新,基本思绪就是用 Node.js 去下载 nw 包到运用地点的目次,并直接替代掉原有的 package.nw ,再重启一下本身就搞定了;全量更新的话,就直接翻开运用的下载页面,让用户自行下载掩盖装置就搞定了。

// 下载 nw 包
export function updatePackage() {
  return new Promise((resolve, reject) => {
    getUpdateJson().then(json => {
      // 全量更新
      if (!json.incremental) {
        Shell.openExternal(getSetupApi)
        return reject({ message: '请下载最新版本,再掩盖装置' })
      }

      // 增量更新
      let packageZip = fs.createWriteStream(tmpNWPath)
      http
        .get(json.filePath, res => {
          if (res.statusCode < 200 || res.statusCode >= 300) return reject({ message: '下载失足,请稍后重试' })
          res.on('end', () => {
            if (fs.statSync(tmpNWPath).size < 10) return reject({ message: '更新包失足,请稍后重试' })
            fs.renameSync(tmpNWPath, appPath)
            resolve(json)
          })
          res.pipe(packageZip)
        })
        .on('error', reject)
    })
  })
}

// 重启本身
export function restartSelf(waitTime) {
  setTimeout(() => {
    require('child_process').spawn('restart.bat', [], { detached: true, cwd: rootPath })
  }, ~~waitTime || 2000)
}

这儿有个小小的 hack ,细致看看代码的同砚应当已发现了 restart.bat 。我尝试了许多方法,想让 NW.exe 重启本身,终究多番尝试后失利了。。。就写了个 bat 来重启本身。

taskkill /im doudou.exe /f
start .\doudou.exe
exit

假如有其他更好的方法,迎接积极来提 issue 或许直接私信我,感谢!

能够会有同砚会问,为啥不直接下载 exe 包下来,再翻开指导装置?
我试过了,当运用被装置在 C:\Program Files 目次内里,管理员权限都不能写 .exe 后缀的文件进去。。。所以,我痛快用浏览器翻开我们的运用的下载页,让用户本身去下载后,本身装置算了。这儿应当能够优化,下载到 用户数据目次,或许其他暂时目次。

4、update 页面

这个页面就没啥手艺点,就是体力劳动了。依据前面 getUpdateJson 要领取得的 json 来衬着出要更新的版本和更新信息,然后供应一个更新按钮,按钮点击后,实行 updatePackage 这个要领,假如顺遂实行就在 then 内里挪用 restartSelf 重启本身就好了。

团体结果就是如许的
《用 vue2 和 webpack 疾速建构 NW.js 项目(2)》

假如对您有效,帮我点个 star ,感谢!您的支撑是我继承更新下去的动力。

    原文作者:不爱吃西红柿的鱼
    原文地址: https://segmentfault.com/a/1190000008829356
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞