仓库源文站点原文


layout: "../layouts/BlogPost.astro" title: "Notes on work projects (in Chinese)" slug: work-project-notes-chinese description: "" added: "Oct 19 2021" tags: [web]

updatedDate: "Jult 17 2024"

项目是怎么跑起来的

Vue 项目

Vue npm 包有不同的 Vue.js 构建版本,可以在 node_modules/vue/dist 中看到它们,大致包括完整版、编译器(编译template)、运行时版本、UMD 版本(通过 <script> 标签直接用在浏览器中)、CommonJS 版本(用于很老的打包工具)、ES Module 版本。总的来说,Runtime + Compiler 版本是包含编译代码的,可以把编译过程放在运行时做,如果需要在客户端编译模板 (比如传入一个字符串给 template 选项),就需要加上编译器的完整版。Runtime 版本不包含编译代码,需要借助 webpack 的 vue-loader 事先把 *.vue 文件内的模板编译成 render 函数,在最终打好的包里实际上是不需要编译器的,只用运行时版本即可。

// Using Runtime + Compiler
new Vue({
  el: '#app',
  router,                  
  template: '<App/>',                                     
  components: { App }        
})

// build/webpack.base.conf.js
resolve: {
  alias: {
    'vue$': 'vue/dist/vue.esm.js',
  }
}
// Using Runtime-only
new Vue({
  el: '#app',
  router,
  render: h => h(App)
})

Vue 是如何被编译的详细介绍:https://vue-compiler.iamouyang.cn/template/baseCompile.html

Vue 3 在 2022 年 2 月代替 Vue 2 成为 Vue 的默认版本。

一些 webpack 的配置

filename and chunkFilename

path and publicPath

app, vendor and manifest

In a typical application built with webpack, there are three main types of code:

  1. The source code you have written. 自己编写的代码
  2. Any third-party library or "vendor" code your source is dependent on. 第三方库和框架
  3. A webpack runtime and manifest that conducts the interaction of all modules. 记录了打包后代码模块之间的依赖关系,需要第一个被加载

optimization.splitChunks

It is necessary to differentiate between Code Splitting and splitChunks. Code splitting is a feature native to Webpack, which uses the import('package') statement to move certain modules to a new Chunk. SplitChunks is essentially a further splitting of the Chunks produced by code splitting.

Code splitting also has some drawbacks. There’s a delay between loading the entry point chunk (e.g., the top-level app with the client-side router) and loading the initial page (e.g., home). The way to improve this is by injecting a small script in the head of the HTML, when executed, it preloads the necessary files for the current path by manually adding them to the HTML page as link rel="preload".

After code splitting, many Chunks will be created, and each Chunk will correspond to one ChunkGroup. SplitChunks is essentially splitting Chunk into more Chunks to form a group and to load groups together, for example, under HTTP/2, a Chunk could be split into a group of 20 Chunks for simultaneous loading.

chunks: 'all' | 'initial' | 'async':

resolve

css-loader and style-loader

load images

Webpack goes through all the import and require files in your project, and for all those files which have a .png|.jpg|.gif extension, it uses as an input to the webpack file-loader. For each of these files, the file loader emits the file in the output directory and resolves the correct URL to be referenced. Note that this config only works for webpack 4, and Webpack 5 has deprecated the file-loader. If you are using webpack 5 you should change file-loader to asset/resource.

Webpack 4 also has the concept url-loader. It first base64 encodes the file and then inlines it. It will become part of the bundle. That means it will not output a separate file like file-loader does. If you are using webpack 5, then url-loader is deprecated and instead, you should use asset/inline.

Loaders are transformations that are applied to the source code of a module. When you provide a list of loaders, they are applied from right to left, like use: ['third-loader', 'second-loader', 'first-loader']. This makes more sense once you look at a loader as a function that passes its result to the next loader in the chain third(second(first(source))).

webpack in development

difference between --watch and --hot

something related to bundling/tree shaking

  1. Every component will get its own scope, and when it imports another module, webpack will check if the required file was already included or not in the bundle.
  2. Webpack v5 comes with the latest terser-webpack-plugin out of the box. optimization.minimize is set to true by default, telling webpack to minimize the bundle using the TerserPlugin.
  3. Tree shaking means that unused modules will not be included in the bundle (The term was popularized by Rollup). In order to take advantage of tree shaking, you must use ES2015 module syntax. Ensure no compilers transform your ES2015 module syntax into CommonJS modules (this is the default behavior of the popular Babel preset @babel/preset-env).

    Webpack do tree-shake only happens when you're using a esmodule, while lodash is not. Alternatively, you can try to use lodash-es written in ES6.

    • import cloneDeep from "lodash/cloneDeep"
    • import { camelCase } from "lodash-es"
    • import * as _ from "lodash-es"
     // babel.config.js
     // keep Babel from transpiling ES6 modules to CommonJS modules
     export default {
       presets: [
         [
           "@babel/preset-env", {
             modules: false
           }
         ]
       ]
     }
    
     // if you're using Webpack and `babel-loader`
     {
       test: /\.js$/,
       exclude: /node_modules/,
       use: {
         loader: 'babel-loader',
         options: {
           type: 'module'
         }
       }
     }
    
  4. The sideEffects property of package.json declares whether a module has side effects on import. When side effects are present, unused modules and unused exports may not be tree shaken due to the limitations of static analysis.

webpack-bundle-analyzer(检查打包体积)

It will create an interactive treemap visualization of the contents of all your bundles when you build the application. There are two ways to configure webpack bundle analyzer in a webpack project. Either as a plugin or using the command-line interface.

// Configure the webpack bundle analyzer plugin
// npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

speed-measure-webpack-plugin(检查打包速度)

See how fast (or not) your plugins and loaders are, so you can optimise your builds. This plugin measures your webpack build speed, giving an output in the terminal.

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [new MyPlugin(), new MyOtherPlugin()],
});

Webpack 5 fails if using smp.wrap() the config, with the error: "You forgot to add mini-css-extract-plugin plugin". As a hacky workaround, you can append MiniCssExtractPlugin after wrapping with speed-measure-webpack-plugin.

TypeScript and Webpack

Webpack is extensible with "loaders" that can be added to handle particular file formats.

  1. Install typescript and ts-loader as devDependencies.
  2. The default behavior of ts-loader is to act as a drop-in replacement for the tsc command, so it respects the options in tsconfig.json.
  3. If you want to further optimize the code produced by TSC, use babel-loader with ts-loader. We need to compile a .ts file using ts-loader first and then using babel-loader.
  4. ts-loader does not write any file to disk. It compiles TypeScript files and passes the resulting JavaScript to webpack, which happens in memory.

TypeScript doesn't understand .vue files - they aren't actually Typescript modules. So it will throw an error when you try to import Foo.vue. The solution is shims-vue.d.ts in src directory. The filename does not seem to be important, as long as it ends with .d.ts. TypeScript looks for .d.ts files in the same places it looks for your regular .ts files. It basically means, "every time you import a module with the name *.vue, then treat it as if it had these contents, and the type of Foo will be Vue."

// shims-vue.d.ts
declare module "*.vue" {
  import Vue from 'vue';
  export default Vue;
}

If that doesn't help, make sure the module you are trying to import is tracked by TypeScript. It should be covered in your include array setting and not be present in the exclude array in your tsconfig.json file.

{
  "compilerOptions": {
    // ...
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "src/**/*.spec.ts"]
}

Above was created in the days before Vue shipped with TypeScript out of the box. Now the best path to get started is through the official CLI.

配置 babel-loader 选择性编译引入的 sdk 文件

Transpiling is an expensive process and many projects have thousands of lines of code imported in that babel would need to run over. Your node_modules should already be runnable without transpiling and there are simple ways to exclude your node_modules but transpile any code that needs it.

{
  test: /\.js$/,
  exclude: /node_modules\/(?!(my_main_package\/what_i_need_to_include)\/).*/,
  use: {
    loader: 'babel-loader',
    options: ...
  }
}

本地 build 与上线 build

  1. 公共组件库 C 需要先 build,再 npm link 映射到全局的 node_modules,然后被其他项目 npm link C 引用。(关于 npm link 的使用场景可以看看 https://github.com/atian25/blog/issues/17)
  2. 项目 A 的上线脚本中会先进入组件库 C,执行 npm buildnpm link,之后再进入项目 A 本身,执行 npm link Cnpm build 等项目本身的构建。
  3. 项目 C 会在本地构建(静态资源传七牛),远程仓库中包括 server-static 存放 build 后的静态文件,它的上线脚本里并不含构建过程,只是在拷贝仓库中的 server-static 目录。因为源文件中会有对组件库的引用 import foo from 'C/dist/foo.js,本地 build 时组件库已经被打包进去。
prefixOss=`echo ${CI_COMMIT_REF_NAME} | sed -e "s/\_/-/g" -e "s/\//-/g"`

[ -z ${CI_COMMIT_TAG} ] && sed -i -E "s/^  \"version\": \"[0-9\.]+\"/  \"version\": \"0.0.0-${prefixOss}-${CI_COMMIT_SHORT_SHA}\"/g" package.json

The above checks if the environment variable CI_COMMIT_TAG is empty (meaning it's not a tag build). If that's the case, it uses sed to perform an in-place replacement in the package.json file. Specifically, it looks for lines that start with "version": "[0-9.]+" and replaces them with a new version format 0.0.0-${prefixOss}-${CI_COMMIT_SHORT_SHA}. This script appears to be adjusting versioning and paths based on the branch or tag being built in a CI/CD pipeline.

The s command is for substitute, to replace text -- the format is s/[text to select]/[text to replace]/. For example, sed 's/target/replacement/g' file.txt will globally substitute the word target with replacement.

本地 build 脚本

  1. 使用 ora 做 spinner,提示 building for production...
  2. 使用 rimraf 删除打包路径下的资源 (rimraf command is an alternative to the Linux command rm -rf)
  3. 调用 webpack() 传入配置 webpack.prod.conf 和一个回调函数,webpack stats 对象 作为回调函数的参数,可以通过它获取到 webpack 打包过程中的信息,使用 process.stdout.write(stats.toString(...)) 输出到命令行中 (console.log in Node is just process.stdout.write with formatted output)
  4. 使用 chalk 在命令行中显示一些提示信息。
  5. 补充:目前大多数工程都是通过脚手架来创建的,使用脚手架的时候最明显的就是与命令行的交互,Inquirer.js 是一组常见的交互式命令行用户界面。Commander.js 作为 node.js 命令行解决方案,是开发 node cli 的必备技能。

后端模板

有些 url 请求是后端直出页面返回 html,通过类似 render_to_response(template, data) 的方法,将数据打到模板中,模板里会引用 xx/static/js 路径下的 js 文件,这些 js 使用 require 框架,导入需要的其他 js 文件或 tpl 模板,再结合业务逻辑使用 underscore 的 template 方法(_.template(xx))可以将 tpl 渲染为 html,然后被 jquery .html() 方法插入到 DOM 中。

# urls.py
urlpatterns = [
  url(r'^v/index', foo.index),
  url(r'^web', foo.web),
]

# views.py
# def index(request):
return render_to_response('foo/vue_index.html', context)

# def web(request):
return render_to_response('foo/login.html', context)

# import scripts in above template html
<script>
var isInIframe = window.frames.length !== parent.frames.length;
var ua = window.navigator.userAgent;

if (!isInIframe && !ua.toLowerCase().match(/micromessenger|android|iphone/i)) {
  window.location.href = '/web/?next=' + window.location.pathname;
} 
</script>
<script src="https://cdn.example.com/assets/login.js"></script>

登录逻辑

常规的扫码登录原理(涉及 PC 端、手机端、服务端):

  1. PC 端携带设备信息向服务端发起生成二维码的请求,生成的二维码中封装了 uuid 信息,并且跟 PC 设备信息关联起来,二维码有失效时间。PC 端轮询检查是否已经扫码登录。
  2. 手机(已经登录过)进行扫码,将手机端登录的信息凭证(token)和二维码 uuid 发送给服务端,此时的手机一定是登录的,不存在没登录的情况。服务端生成一个一次性 token 返回给移动端,用作确认时候的凭证。
  3. 移动端携带上一步的临时 token 确认登录,服务端校对完成后,会更新二维码状态,并且给 PC 端一个正式的 token,后续 PC 端就是持有这个 token 访问服务端。

常规的密码存储:

A Rainbow Table is a precomputed table of hashes and their inputs. This allows an attacker to simply look up the hash in the table to find the input. This means that if an attacker gets access to your database, they can simply look up the hashes to find the passwords.

To protect against this, password hashing algorithms use a salt. A salt is a random string that is added to the password before hashing. This means that even if two users have the same password, their hashes will be different. This makes it impossible for an attacker to use a rainbow table to find the passwords. In fact a common practice is to simply append the salt to the hash. This will make it so that the salt is always available when you need to verify the password.

A great library for generating bcrypt hashes is bcryptjs which will generate a random salt for you. This means that you don't need to worry about generating a salt and you can simply store the whole thing as is. Then when the user logs in, you provide the stored hash and the password they provide to bcryptjs's compare function will verify the password is correct.

import { compare, hash } from 'bcryptjs';
import { SignJWT } from 'jose';

// Takes a string as input, and returns a Uint8Array containing UTF-8 encoded text
const key = new TextEncoder().encode(process.env.AUTH_SECRET);
const SALT_ROUNDS = 10;  // Salt length to generate

export async function hashPassword(password) {
  return hash(password, SALT_ROUNDS);
}

export async function comparePasswords(plainTextPassword, hashedPassword) {
  return compare(plainTextPassword, hashedPassword);
}

export async function signToken(payload: SessionData) {
  // https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md
  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('1 day from now')
    .sign(key);
}

export async function setSession(user: NewUser) {
  const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000);
  const session: SessionData = {
    user: { id: user.id! },
    expires: expiresInOneDay.toISOString(),
  };
  const encryptedSession = await signToken(session);
  cookies().set('session', encryptedSession, {
    expires: expiresInOneDay,
    httpOnly: true,
    secure: true, // Only over HTTPS
    sameSite: 'strict', // Prevent CSRF
  });
}

微信网页授权

申请公众号/小程序的时候,都有一个 APPID 作为当前账号的标识,OpenID 就是用户在某一公众平台下的标识(用户微信号和公众平台的 APPID 两个数据加密得到的字符串)。如果开发者拥有多个应用,可以通过获取用户基本信息中的 UnionID 来区分用户的唯一性,因为同一用户,在同一微信开放平台下的不同应用,UnionID 应是相同的,代表同一个人,当然前提是各个公众平台需要先绑定到同一个开放平台。OpenID 同一用户同一应用唯一,UnionID 同一用户不同应用唯一,获取用户的 OpenID 是无需用户同意的,获取用户的基本信息则需要用户同意。

向用户发起授权申请,即打开如下页面: https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

  1. appid 是公众号的唯一标识。
  2. redirect_uri 替换为回调页面地址,用户授权完成后,微信会帮你重定向到该地址,并携带相应的参数如 code,回调页面所在域名必须与后台配置一致。在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中配置授权回调域名。
  3. scope 根据业务需要选择 snsapi_basesnsapi_userinfo。其中 snsapi_base 为静默授权,不弹出授权页面,直接跳转,只能获取用户的 openid,而 snsapi_userinfo 会弹出授权页面,需要用户同意,但无需关注公众号,可在授权后获取用户的基本信息。(对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是 scopesnsapi_userinfo,也是静默授权,用户无感知。)
  4. state 不是必须的,重定向后会带上 state 参数,开发者可以填写 a-zA-Z0-9 的参数值,最多 128 字节。
  5. 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATEcode 作为换取 access_token 的票据,每次用户授权带上的 code 不一样,code 只能使用一次,5分钟未被使用自动过期。
  6. 获取 code 后,请求 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code 获取 access_tokenopenid (未关注公众号时,用户访问公众号的网页,也会产生一个唯一的 openid)。如果 scopesnsapi_userinfo 还会同时获得到 unionid
  7. 如果网页授权作用域为 snsapi_userinfo,则此时可以请求 https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 拉取用户信息,比如用户昵称、头像、unionid 等,不再返回用户性别及地区信息。
  8. 公众号的 secret 和获取到的 access_token 安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新 access_token 以及通过 access_token 获取用户信息等步骤,也必须从服务器发起。
  1. 微信公众平台接口测试帐号申请: https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
  2. 某个公众号的关注页面地址为 https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzI0NDA2OTc2Nw==#wechat_redirect 其中 biz 字符串是微信公众号标识,在浏览器打开该公众号下的任意一篇文章,查看网页源代码,搜索 var biz 这样的关键字即可得到。

微信授权也符合通常的 OAuth 流程:
You first need to register your app with your provider to get the required credentials. You’ll be asked to define a callback URL or a redirect URI.

  1. Redirect the user to the provider.
  2. User is authenticated by the provider.
  3. User is redirected back to your server with a secret code.
  4. Exchange that secret code for the user’s access token.
  5. Use the access token access the user’s data.

唤起微信小程序

微信外网页通过小程序链接 URL Scheme,微信内通过微信开放标签,且微信内不会直接拉起小程序,需要手动点击按钮跳转。这是官方提供的一个例子 https://postpay-2g5hm2oxbbb721a4-1258211818.tcloudbaseapp.com/jump-mp.html 可以用手机浏览器查看效果,直接跳转小程序。

微信小程序相关的仓库,比如 WeUI 组件库、微信小程序示例、computed / watch 扩展等: https://github.com/wechat-miniprogram

国产 APP 各自套壳 Chromium 内核版本,最大的问题就是更新不及时,而且大多被改造过。

Debug iOS Safari from your Mac

  1. On your iPhone, go to Settings > Safari > Advanced and toggle on Web Inspector.
  2. On your Mac, open Safari and go to Safari > Preferences > Advanced then check Show Develop menu in menu bar.
  3. Connect your iPhone to your Mac with the USB cable.
  4. On your iPhone, open the web site that you want to debug.
  5. On your Mac, in Safari, the name of the iOS device will appear as a submenu in the Develop menu. This will open a Web Inspector window on your Mac.

HTTP 请求相关

  1. 使用 vue-resource
  1. 自己对 axios 封装

注意 Axios 遇到 302 的返回:重定向直接被浏览器拦截处理,浏览器 redirect 后,被视为 Axios 发起了跨域请求,所以抛异常。Axios 捕获异常,进入 catch 逻辑。

  const handleResponse = (res) => {
    if(res.headers && res.headers['set-auth']) {
      window.Authorization = res.headers['set-auth'];
    }

    // 之后根据状态码做不同处理...
  }

  export default {
    get(url, params) {
      // 统一加请求头
      axios.defaults.headers['X-Client'] = 'web';
      if (window.Authorization) {
        axios.defaults.headers['Authorization'] = 'Bearer ' + window.Authorization;
      }

      return axios
        .get(url)
        .then(function(response) {
          return response
        })
        .then(handleResponse)    // 统一处理 redirect, 赋值 location.href 
        .catch(errorResponseGet) // 统一处理错误码 4xx, 5xx
    },

    post(url, params) {
      // ...
    }
  }
  // Show progress of Axios during request
  await axios.get('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg', {
    onDownloadProgress: progressEvent => {
      const percentCompleted = Math.floor(progressEvent.loaded / progressEvent.total * 100)
      setProgress(percentCompleted)
    }
  })
  .then(res => {
    console.log("All DONE")
    return res.data
  })
  1. 使用 VueRequest,管理请求状态,支持 SWR、轮询、错误重试、缓存、分页等常用功能。useRequest 接收一个 service 函数,service 是一个异步的请求函数,换句话说,还可以使用 axios 来获取数据,然后返回一个 Promise。useRequest 会返回 data、loading、error 等,它们的值会根据请求状态和结果进行修改。返回的 run 方法,可以手动触发 service 请求。

API 版本和 URI 连字符

API 版本可以放在两个地方: 在 url 中指定 API 的版本,例如 example.com/api/v1,这样不同版本的协议解析可以放在不同的服务器上,不用考虑协议兼容性,开发方便,升级也不受影响。另一种是放在 HTTP header 中,url 显得干净,符合 RESTful 惯例,毕竟版本号不属于资源的属性。缺点是需要解析头部,判断返回。

URI 中尽量使用连字符 - 代替下划线 _ 的使用,连字符用来分割 URI 中出现的单词,提高 URI 的可读性。下划线会和链接的样式冲突重叠。URI 是对大小写敏感的,为了避免歧义,我们尽量用小写字符。但主机名(Host)和协议名(Scheme)对大小写是不敏感的。

阿里云 CDN

阿里云 CDN 对于文件是否支持缓存是以 X-Cache 头部来确定,缓存时间是以 X-Swift-CacheTime 头部来确认。

阿里云 CDN 在全球拥有 3200+ 节点。中国内地拥有 2300+ 节点,覆盖 31 个省级区域。

  1. CDN 节点是指与最终接入用户之间具有较少中间环节的网络节点,对最终接入用户有相对于源站而言更好的响应能力和连接速度。当节点没有缓存用户请求的内容时,节点会返回源站获取资源数据并返回给用户。阿里云 CDN 的源站可以是对象存储OSS、函数计算、自有源站(IP、源站域名)。
  2. 默认情况下将使用 OSS 的 Bucket 地址作为 HOST 地址(如 ***.oss-cn-hangzhou.aliyuncs.com)。如果源站 OSS Bucket 绑定了自定义域名(如 origin.developer.aliyundoc.com),则需要配置回源 HOST 为自定义域名。
  3. 加速域名即网站域名、是终端用户实际访问的域名。CNAME 域名是 CDN 生成的,当您在阿里云 CDN 控制台添加加速域名后,系统会为加速域名分配一个 *.*kunlun*.com 形式的 CNAME 域名。
  4. 添加加速域名后,需要在 DNS 解析服务商处,添加一条 CNAME 记录,将加速域名的 DNS 解析记录指向 CNAME 域名,记录生效后该域名所有的请求都将转向 CDN 节点,达到加速效果。CNAME 域名将会解析到具体哪个节点 IP 地址,将由 CDN 的调度系统综合多个条件来决定。

秒杀系统设计

开发自己的调试工具

日常开发 Tips and Tricks

桌面端 Electron 的本地构建过程

Electron是一个集成项目,允许开发者使用前端技术开发桌面端应用。其中 Chromium 基础能力可以让应用渲染 HTML 页面,执行页面的 JS 脚本,让应用可以在 Cookie 或 LocalStorage 中存取数据。Electron 还继承了 Chromium 的多进程架构,分一个主进程和多个渲染进程,主进程进行核心的调度启动,不同的 GUI 窗口独立渲染,做到进程间的隔离,进程与进程之间实现了 IPC 通信。Node.js 基础能力可以让开发者读写本地磁盘的文件,通过 socket 访问网络,创建和控制子进程等。Electron 内置模块可以支持创建操作系统的托盘图标,访问操作系统的剪切板,获取屏幕信息,发送系统通知,收集崩溃报告等。

  1. 调用 greeting() 方法,根据终端窗口的宽度 process.stdout.columns 显示不同样式的问候语。
  2. 使用 Promise.all() 同时启动主进程和渲染进程的构建,两者分别有自己的 webpack 配置文件 webpack.main.configwebpack.renderer.config
  3. 对于渲染进程,使用类似 web 端的 webpack 配置,设置入口文件、产出位置、需要的 loaders 和 plugins,并根据是否为 production 环境补充引入一些 plugin,在 npm 脚本打包的时候可以通过 cross-env BUILD_ENV=abc 设置一些环境变量。创建一个 WebpackDevServer,传入 webpack 配置,设置代理,监听某一端口,其实这就是启动一个本地服务,使用浏览器也可以访问构建后的页面,这里只是用 electron 的壳子把它加载进来。对于主进程,也使用了 webpack,设置入口文件用来打包产出。
  4. 利用 webpack 编译的 hooks 在构建完成后会打印日志,logStats() 函数接收进程名 (Main or Renderer) 和具体输出的内容。
  5. 在主进程和渲染进程都构建完成后,即主进程有一个打包后的 main.js 且渲染进程本地服务可以访问,这个时候启动 electron,即通常项目的 npm 脚本会执行 electron .,这里是通过 Node API,使用 child_process.spawn() 的方式启动 electron 并传入需要的参数,然后对 electron 进程的 stdout 和 stderr 监听,打印对应的日志。

桌面端状态持久化存储

Electron doesn't have a built-in way to persist user preferences and other data. electron-store handles that for you, so you can focus on building your app. The data is saved in a JSON file in app.getPath('userData').

Advantages over localStorage:

vuex-electron uses electron-store to share your Vuex Store between all processes (including main).

Electron 相关记录

  1. 如果安装 Electron 遇到问题,可以直接在 https://npmmirror.com/mirrors/electron/ 下载需要的版本,然后保存到本地缓存中 ~/Library/Caches/electron
  2. In the case of an electron app, the electron package is bundled as part of the built output. There is no need for your user to get electron from npm to use your built app. Therefore it matches well the definition of a devDependency. (When you publish your package, if the consumer project needs other packages to use yours, then these must be listed as dependencies.) For example, VS Code properly lists electron as a devDependency only: https://github.com/microsoft/vscode/blob/main/package.json
  3. In case you are using an unsupported browser, or if you have other specific needs (for example your application is in Electron), you can use the standalone Vue devtools
  4. Blank screen on builds, but works fine on serve. This issue is likely caused when Vue Router is operating in history mode. In Electron, it only works in hash mode.
    • 本地开发时是 http 服务,当访问某个地址的时候,其实真实目录下是没有这个文件的,本地服务可以帮助重定向到 /index.html 这是一定存在的入口文件,相当于走前端路由。一但打包之后,页面就是静态文件存放在目录中了,Electron 是找不到类似 /index/page/1/2 这样的目录的,所以需要使用 /index.html#page/1/2 这样的 hash 模式。同样,如果是 Web 项目使用了 history 模式打包,如果不在 nginx 中将全部 url 指向 ./index.html 的话,也会出现 404 的错误,也就是需要把路由移交给前端去控制。
    • hash mode 是默认模式,原理是使用 location.hashonhashchange 事件,利用 # 后面的内容不会被发送到服务端实现单页应用。history mode 要手动设置 mode: 'history', 是基于 History API 来实现的,这也是浏览器本身的功能,地址不会被请求到服务端。
  5. 关于 Icon 图标,Windows(.ico 文件)和 Mac(.icns 文件)的都是复合格式,包含了多种尺寸和颜色模式,Linux 就是多张 png。注意不要把 png 直接改成 ico,可以使用在线工具转换。如果 Windows 窗口或任务栏图标未更换成功,可能是 ico 文件中缺少小尺寸图标,如缺少 16x16 或 32x32 的图标。
  6. 可以通过命令行启动程序,查看打包后的主进程日志,Mac 进入到 /Applications/Demo.app/Contents/MacOS/ 路径,执行 ./Demo 启动应用层序。Windows 上打开 Powershell 进入到程序的安装目录,执行 .\Demo.exe,如果文件名中有空格,需要用双引号把文件名引起来。
  7. Electron 参考项目: