页面渲染的方式主要有三种
1.web渲染
2.Native原生渲染
3.web与Native两者掺杂,即Hybrid渲染。
小程序的呈现形式为第三种。
微信小程序是如何实现跨域请求的?
后端映射、你请求的接口实际到微信的后端做了一道映射
微信后端拿到你的wx.request调用的url、用后端请求后端
拿到数据后将body返给你
这就是为什么、请求后端之后、拿回来的只有body没有header、取不到response header
以前fetch也是可以在开发者工具用的、后面被屏蔽了
双线程通信方式
为什么要双线程 ? -> 为了管控安全,避免操作DOM。
小程序的渲染层和逻辑层分别由 2 个线程管理:渲染层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS 脚本。
微信小程序的框架包含两部分 view视图层、APP service逻辑层。
view层用来渲染页面结构,
AppService用来逻辑处理、数据请求、接口调用。
在两个进程(两个webview)里运行。
视图层和逻辑层通过系统层的JSBridage进行通信。
逻辑层: 创建一个单独的线程去执行JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码
渲染层: 界面渲染相关的任务全都在webView线程中执行,通过逻辑层的代码去控制渲染哪些界面。
一个小程序存在多个界面,所以渲染层存在多个webview线程。
逻辑层和渲染层的通信会由Native(微信客户端)做中转,
逻辑层发送网络请求也会经由Native转发。
evaluate Javascript
视图层和逻辑层的数据传输,实际上通过两边提供的evaluateJavascript实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份JS脚本,在通过JS脚本的形式传递到两边独立环境。
因为evaluateJavascript的执行会受很多方面的影响,数据到达视图层并不是实时的。随意我们的setData函数将数据从逻辑层发送到视图层,是异步的。
模板数据绑定方案
1.解析语法生成AST
2.根据AST结果生成DOM
3.将数据绑定更新至模板
抽象语法树(abstract syntax tree或者缩写为AST)
最容易引发性能问题的主要是第三点,而关于数据更新的解决方案,React首先提出了虚拟DOM的设计,而现在也基本被大部分框架吸收,小程序也不例外。
虚拟 DOM 机制 virtual Dom
用JS对象模拟DOM树 -> 比较两个DOM树 -> 比较两个DOM树的差异 -> 把差异应用到真正的DOM树上
1.在渲染层把WXML转化成对应的JS对象
2.在逻辑层发生数据变更的时候,通过宿主环境提供的setData方法把数据从逻辑层传递到Native,再转发到渲染层
3.经过对比前后差异,把差异应用在原来的DOM树上,更新界面
小程序的基础库
小程序的基础库是JavaScript编写的,它可以被注入到渲染层和逻辑层运行。主要用于:
在渲染层,提供各类组件来组件页面的元素
在逻辑层,提供各种API来处理各种元素。
处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑
小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。
小程序的基础库不会打包在小程序的代码中,它会被提前内置在微信客户端。这样可以:
降低业务小程序的代码包大小
可以单独修复基础库中的Bug,无需修改到业务小程序的代码包
Exparser
Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础支持。小程序内所有组件,包括内置组件和自定义组件,都有Exparser组织管理。
双线程的渲染机制
双线程的渲染,其实是结合了前面的一系列机制。
1.通过模板数据绑定和虚拟DOM机制,小程序提供了带有数据绑定语法的DSL,渲染层来描述页面结构。
2.小程序在逻辑层提供了设置页面数据的api
this.setData({
key : value
});
3.逻辑层需要更改页面时,只要把修改后的data通过setData传到渲染层。
传输的数据,会转换为字符串形式传输,故应避免传递大量数据。
4.渲染层会根据渲染机制重新生成虚拟DOM树,并更新到对应的DOM树上,引起界面变化。
引入原生组件
绕过 setData、数据通信和重渲染流程,使渲染性能更好。
扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
体验更好,同时也减轻 WebView 的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
原生组件的渲染过程:
组件被创建,包括组件属性会依次赋值。
组件被插入到 DOM 树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y坐标)、宽高。
组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面。
当位置或宽高发生变化时,组件会通知客户端做相应的调整。
查看小程序服务端的源码,不难发现,在服务器上配置数据库及预创建需连接的数据库是在
\server\tools\cAuth.sql中实现的:
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
– —————————-
– Table structure for cSessionInfo
– —————————-
DROP TABLE IF EXISTS cSessionInfo
;
CREATE TABLE cSessionInfo
(
open_id
varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
uuid
varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
skey
varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
create_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_visit_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
session_key
varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
user_info
varchar(2048) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (open_id
),
KEY openid
(open_id
) USING BTREE,
KEY skey
(skey
) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=’会话管理用户信息’;
SET FOREIGN_KEY_CHECKS = 1;
然后,在\server\tools\initdb.js中通过knex实例化我们的mysql数据库对象,通过对象DB的raw方法执行我们上面的SQL语句,执行成功后,将在cAuth数据库中创建成功cSessionInfo表:
/**
console.log(‘\n======================================’)
console.log(‘开始初始化数据库…’)
// 初始化 SQL 文件路径
const INIT_DB_FILE = path.join(__dirname, ‘./cAuth.sql’)
const DB = require(‘knex’)({ //通过knex框架链接数据库并实例化为DB对象
client: ‘mysql’,
connection: {
host: config.host,
port: config.port,
user: config.user,
password: config.pass,
database: config.db,
charset: config.char,
multipleStatements: true
}
})
console.log(准备读取 SQL 文件:${INIT_DB_FILE}
)
// 读取 .sql 文件内容
const content = fs.readFileSync(INIT_DB_FILE, ‘utf8’)
console.log(‘开始执行 SQL 文件…’)
// 执行 .sql 文件内容
DB.raw(content).then(res => {
console.log(‘数据库初始化成功!’)
process.exit(0)
}, err => {
throw new Error(err)
})
以上便是官方测试接口程序在服务端的数据库创建脚本程序。
编写小程序客户端与服务端交互的代码(重点)
首先,我们还是要理顺一下小程序客户端与服务端交互的流程与原理
我自己是通过sequelize框架对数据库进行增删改查的(sequelize和knex都是nodejs框架,用于数据库操作的模块),至于官方demo时通过脚本的方式对数据库初始化,我们完全可以直接在mysql命令行对数据库及表结构进行创建:
//在mysql中用原生sql语句创建数据库
mysql>create database meetdata;
// 在数据库中以原生sql语句生成数据库的表结构,以及定义其字段和数据类型
mysql>create table joiners(
id varchar(50) COLLATE utf8mb4_unicode_ci not null,
name varchar(50) COLLATE utf8mb4_unicode_ci not null,
gender bool not null,
phone bigint not null,
company varchar(100) COLLATE utf8mb4_unicode_ci not null,
imgPath varchar(100) COLLATE utf8mb4_unicode_ci not null,
createdAt bigint not null,
updatedAt bigint not null,
primary key(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=’备注信息’;
上面只能算是对服务器中数据库的创建和表结构的创建,下面是小程序客户端与服务端交互(数据发送和请求):
小程序端上传表单数据和文件的源码(结合图2.1):
(1)在客户端上传form表单数据和文件或图片(通过wx.uploadFile()实现的示例代码):
// 提交数据,包括form表单中的数据,和上传的图片到指定的URL中(POST请求方式)
formSubmit: function (e) {
var that = this;
uploadFile(
“https://www.joyitsai.cn/weapp/uploadFile”, // 上传文件到指定url
tongzhiImgPath, //wx.chooseImage()的缓存图片地址
“uploadImg”, //上传文件时的备注名
{ // 上传文件时可以将附加的表单数据提交到url,formData为{}格式
name: e.detail.value.meetname,
zhuban: e.detail.value.meetZhuban,
chengban: e.detail.value.meetChengban,
jieshao: e.detail.value.meetjieshao,
file1: e.detail.value.file1,
file2: e.detail.value.file2,
latitude: Latitude,
longitude: Longitude,
Location: location
});
// 暂时没有更好的上传多张图片的方式,暂且用多个uploadFile上传多张图片
uploadFile(
“https://www.joyitsai.cn/weapp/uploadImage”,
logoImgPath,
“logo”,
{
});
}
(2)在服务器配置路由分发器:
接着在服务端的路由配置文件中,配置与客户端对应的URL路由:
/*
ajax 服务路由集合
*/
// 配置在客户端的请求URL
const router = require(‘koa-router’)({
prefix: ‘/weapp’
})
const controllers = require(‘../controllers’)
// 从 sdk 中取出中间件
// 这里展示如何使用 Koa 中间件完成登录态的颁发与验证
const { auth: { authorizationMiddleware, validationMiddleware } } = require(‘../qcloud’)
// 图片上传接口,对应小程序中url的”https://www.joyitsai.cn/weapp/uploadImage”
router.post(‘/uploadImage’, controllers.uploadImage)
// 文件上传接口,对应小程序中url的”https://www.joyitsai.cn/weapp/uploadFile”
router.post(‘/uploadFile’, controllers.uploadFile)
(3)编写接受数据存储数据的服务器js脚本:
const Sequelize = require(‘sequelize’);
const multer = require(‘koa-multer’);
const config = require(‘../config’);
let path = require(‘path’)
var meetImgPath = ‘’;
var meet_name = ‘’;
var zhuban_company = ‘’;
var chengban_company = ‘’;
var now = null;
var filetitle = ‘’;
// 创建一个Sequelize对象实例,并通过config.js文件中的连接数据库参数,将sequelize连接到指定数据库
// 同时说明以mysql的驱动进行操作(因为我们用的是mysql数据库)
var sequelize = new Sequelize(config.mysql.userdb, config.mysql.user, config.mysql.pass, {
host: config.mysql.host,
dialect: ‘mysql’,
pool: {
max: 5,
min: 0,
idle: 30000
}
});
const storage = multer.diskStorage({
destination: (req, file, cb) => { //声明文件存储位置
meetImgPath = path.resolve(__dirname, ‘../uploadFiles/’);
console.log(req.body);
// 从request请求中获取上传的文件名
name = req.body.name
// 上传的文件存储的路径
cb(null, path.resolve(__dirname, '../uploadFiles/'));
},
// 定义文件名,file即在小程序端上传的原文件
filename: (req, file, cb) => {
//.pop()弹出文件扩展名,合并成存储在服务器上时的文件名
cb(null, `${name}${filetitle}.${file.originalname.split('.').pop()}`);
} });
// 为multer(opts)定义的opts参数
const uploadCfg = {
storage: storage,
limits: {
//上传文件的大小限制,单位bytes
fileSize: 1024 * 1024 * 20
}
};
module.exports = async (req, res) => {
console.log(‘you are uploading the user informations into the mysql…’);
let upload = multer(uploadCfg).any();
upload(req, res, async (err) => {
if(err){
console.log(err);
return;
}else{
console.log(‘the file has uploaded into’ + path.resolve(__dirname, ‘../uploadFiles/${uploadFile.filename}’));
}
})
}
小程序端向服务器请求数据(结合图2.1):
(1)通过wx.request()向指定url请求数据
onLoad: function () {
var that = this;
// wx.request时GET请求
wx.request({
url: ‘https://www.joyitsai.cn/weapp/downloads’,
data: {
},//给服务器传递数据,本次请求不需要数据,可以不填
header: {
// 默认值,返回的数据设置为json数组格式
'content-type': 'application/json'
},
success: function (res) {
var data = res.data;
if (data.data.downloads) {
var downloads = data.data.downloads;
for(let i=0; i<downloads.length; i++){
// 下面就是通过请求获取的json合适的数据
// 依据具体的需求,获取需要的数据,传递到前端
var download = downloads[i];
}
that.setData({
proList: filelist,
})
}
}, // success: function(){}
fail: function (res) {
console.log('下载失败');
}
}); (2)在服务器为小程序端的请求url分配路由:
/*
ajax 服务路由集合
*/
// 配置在客户端的请求URL
const router = require(‘koa-router’)({
prefix: ‘/weapp’
})
const controllers = require(‘../controllers’)
// 从 sdk 中取出中间件
// 这里展示如何使用 Koa 中间件完成登录态的颁发与验证
const { auth: { authorizationMiddleware, validationMiddleware } } = require(‘../qcloud’)
// 图片上传接口,对应小程序中url的”https://www.joyitsai.cn/weapp/uploadImage”
router.post(‘/uploadImage’, controllers.uploadImage)
// 文件上传接口,对应小程序中url的”https://www.joyitsai.cn/weapp/uploadFile”
router.post(‘/uploadFile’, controllers.uploadFile)
// 小程序端数据请求接口,对应wx.request中url的’https://www.joyitsai.cn/weapp/downloads’
router.get(‘/downloads’, controllers.downloads)
(3)编写服务器请求数据库数据并返回给小程序端的js脚本:
const Sequelize = require(‘sequelize’);
const multer = require(‘koa-multer’);
const config = require(‘../config’);
let path = require(‘path’)
// 创建一个Sequelize对象实例,并通过config.js文件中的连接数据库参数,将sequelize连接到指定数据库
// 同时说明以mysql的驱动进行操作(因为我们用的是mysql数据库)
var sequelize = new Sequelize(config.mysql.userdb, config.mysql.user, config.mysql.pass, {
host: config.mysql.host,
dialect: ‘mysql’,
pool: {
max: 5,
min: 0,
idle: 30000
}
});
// 定义一个数据模型Download,告诉sequelize如何按照字段及其数据类型去映射到downloads数据库
var Download = sequelize.define(‘meetlist’, {
id: {
type: Sequelize.STRING(50),
primaryKey: true
},
name: Sequelize.STRING(100),
file1: Sequelize.STRING(100),
file2: Sequelize.STRING(100),
createdAt: Sequelize.BIGINT,
updatedAt: Sequelize.BIGINT,
latitude: Sequelize.DOUBLE,
longitude:Sequelize.DOUBLE,
Location: Sequelize.STRING(100),
logo: Sequelize.STRING(100)
},{ timestamps: false }
);
module.exports = async(ctx) => {
var download_list = new Array();
// 查找数据库中所有joiner数据
var downloads = await Download.findAll({
});
for (let download of downloads) {
// 对downloads中每个元素按一定需求进行处理后
// 返回所需的格式
download_list.push(download);
}
console.log(`find ${downloads.length} downloads:`);
let url=ctx.url; //获取url
// 从上下文中直接获取数据
let ctx_query = ctx.query; //query返回格式化的对象
let ctx_querystring = ctx.querystring; //querystring返回原字符
// 从上下文的request对象中获取
let request=ctx.request;
let req_query=request.query; //query返回格式化好的对象
let req_querystring=request.querystring; //querystring返回原字符串。
ctx.body={
data: {downloads: download_list},
url,
ctx_query,
ctx_querystring,
req_query,
req_querystring
} } (4)关于ctx,简单的console.log一下,就知道它是什么了:
{ request:
{ method: ‘GET’,
url: ‘/weapp/downloads’,
header:
{ connection: ‘upgrade’,
host: ‘www.joyitsai.cn’,
pragma: ‘no-cache’,
‘cache-control’: ‘no-cache’,
‘user-agent’: ‘Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 wechatdevtools/1.02.1811290 MicroMessenger/6.7.3 Language/zh_CN webview/ token/c6a74c101254905a6526830ac4c466aa’,
‘content-type’: ‘application/json’,
accept: ‘/’,
referer: ‘https://servicewechat.com/wx80d2948ba1252c7d/devtools/page-frame.html’,
‘accept-encoding’: ‘gzip, deflate, br’ } },
response: { status: 404, message: ‘Not Found’, header: {} },
app: { subdomainOffset: 2, proxy: false, env: ‘development’ },
originalUrl: ‘/weapp/downloads’,
req: ‘
res: '
socket: '
Native预先额外加载一个WebView
当打开指定页面时,用默认数据直接渲染,请求数据回来时局部更新
返回显示历史View
退出小程序,View状态不销毁
小程序入口
扫码进入小程序
搜索小程序
小程序发送到桌面(Android)
发送给朋友
二、小程序架构
微信小程序的框架包含两部分View视图层、App Service逻辑层,View层用来渲染页面结构,AppService层用来逻辑处理、数据请求、接口调用,它们在两个线程里运行。
视图层使用WebView渲染,逻辑层使用JSCore运行。
视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
小程序启动时会从CDN下载小程序的完整包
三、View (页面视图)
视图层由 WXML 与 WXSS 编写,由组件来进行展示。
将逻辑层的数据反应成视图,同时将视图层的事件发送给逻辑层。
1、View – WXML
WXML(WeiXin Markup Language)
支持数据绑定
支持逻辑算术、运算
支持模板、引用
支持添加事件(bindtap)
微信小程序原理解析
wxml编译器:wcc 把wxml文件 转为 js 执行方式:wcc index.wxml
2、View – WXSS
WXSS(WeiXin Style Sheets)
支持大部分CSS特性
添加尺寸单位rpx,可根据屏幕宽度自适应
使用@import语句可以导入外联样式表
不支持多层选择器-避免被组件内结构破坏
四、App Service(逻辑层)
逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈
1、App( ) 小程序的入口;Page( ) 页面的入口
3、提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。
4、每个页面有独立的作用域,并提供模块化能力。
5、数据绑定、事件分发、生命周期管理、路由管理
运行环境
IOS – JSCore
Android – X5 JS解析器
DevTool – nwjs Chrome 内核
1、App Service – Binding
数据绑定使用 Mustache 语法(双大括号)将变量包起来,动态数据均来自对应 Page 的 data,可以通过setData方法修改数据。
事件绑定的写法同组件的属性,以 key、value 的形式,key 以bind或catch开头,然后跟上事件的类型,如bindtap, catchtouchstart,value 是一个字符串,需要在对应的 Page 中定义同名的函数。
4、App Service – Router
navigateTo(OBJECT)
保留当前页面,跳转到应用内的某个页面,使用navigateBack可以返回到原页面。页面路径只能是五层
redirectTo(OBJECT)
关闭当前页面,跳转到应用内的某个页面。
navigateBack(OBJECT)
关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages()) 获取当前的页面栈,决定需要返回几层。
五、小程序开发经验
1、小程序存在的问题
小程序仍然使用WebView渲染,并非原生渲染
需要独立开发,不能在非微信环境运行。
开发者不可以扩展新组件。
服务端接口返回的头无法执行,比如:Set-Cookie。
依赖浏览器环境的js库不能使用,因为是JSCore执行的,没有window、document对象。
WXSS中无法使用本地(图片、字体等)。
WXSS转化成js 而不是css,为了兼容rpx。
WXSS不支持级联选择器。
小程序无法打开页面,无法拉起APP。
小程序不能和公众号重名,于是小程序的名字就成了:自选股+、滴滴出行DiDi 。
2、小程序可以借鉴的优点
提前新建WebView,准备新页面渲染。
View层和逻辑层分离,通过数据驱动,不直接操作DOM。
使用Virtual DOM,进行局部更新。
全部使用https,确保传输中安全。
使用离线能力。
前端组件化开发。
加入rpx单位,隔离设备尺寸,方便开发。
3、脱离微信的“小程序”:PWA 渐进式应用
PWA 全称是 Progressive Web Apps ,译成中文就是渐进式应用,是 Google 在 2015 年 6 月 15 日提出的概念。
Progressive Web Apps 是结合了 web 和 原生应用中最好功能的一种体验。对于首次访问的用户它是非常有利的, 用户可以直接在浏览器中进行访问,不需要安装应用。随着时间的推移当用户渐渐地和应用建立了联系,它将变得越来越强大。它能够快速地加载,即使在弱网络环境下,能够推送相关消息, 也可以像原生应用那样添加至主屏,能够有全屏浏览的体验。
PWA具有如下特点:
渐进增强 – 支持的新特性的浏览器获得更好的体验,不支持的保持原来的体验。
离线访问 – 通过 service workers 可以在离线或者网速差的环境下工作。
类原生应用 – 使用app shell model做到原生应用般的体验。
可安装 – 允许用户保留对他们有用的应用在主屏幕上,不需要通过应用商店。
容易分享 – 通过 URL 可以轻松分享应用。
持续更新 – 受益于 service worker 的更新进程,应用能够始终保持更新。
安全 – 通过 HTTPS 来提供服务来防止网络窥探,保证内容不被篡改。
可搜索 – 得益于 W3C manifests 元数据和 service worker 的登记,让搜索引擎能够找到 web 应用。
再次访问 – 通过消息推送等特性让用户再次访问变得容易。
Web App Manifest使Web更像Native
Web App Manifest以JSON的格式定义Web应用的相关配置(应用名称、图标或图像连接、启动URL、自定义特性、启动默认配置、全屏设置等)。
App Shell 提升显示效率
App Shell(应用外壳)是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript,首次加载后立刻被缓存下来,不需要每次使用时都被下载,而是只异步加载需要的数据,以达到UI保持本地化。
微信小程序使用了前端技术栈 JavaScript/WXML/WXSS。但和常规的前端开发又有一些区别:
JavaScript: 微信小程序的 JavaScript 运行环境即不是 Browser 也不是 Node.js。它运行在微信 App 的上下文中,不能操作 Browser context 下的 DOM,也不能通过 Node.js 相关接口访问操作系统 API。所以,严格意义来讲,微信小程序并不是 Html5,虽然开发过程和用到的技术栈和 Html5 是相通的。 WXML: 作为微信小程序的展示层,并不是使用 Html,而是自己发明的基于 XML 语法的描述。 WXSS: 用来修饰展示层的样式。官方的描述是 “ WXSS (WeiXin Style Sheets) 是一套样式语言,用于描述 WXML 的组件样式。WXSS 用来决定 WXML 的组件应该怎么显示。” “我们的 WXSS 具有 CSS 大部分特性…我们对 CSS 进行了扩充以及修改。”基于 CSS2 还是 CSS3?大部分是哪些部分?是否支持 CSS3 里的动画?不得而知。
在微信小程序官方文档上,有下面这段话:
微信小程序运行在三端:iOS、Android 和 用于调试的开发者工具
在 iOS 上,小程序的 javascript 代码是运行在 JavaScriptCore 中 在 Android 上,小程序的 javascript 代码是通过 X5 内核来解析 在 开发工具上, 小程序的 javascript 代码是运行在 nwjs(chrome内核) 中
我们先从开发工具谈起。
开发工具
小程序的 javascript 代码运行在 nwjs 中。nwjs 是什么鬼呢?官方介绍是这样写的:
NW.js (previously known as node-webkit) lets you call all Node.js modules directly from DOM and enables a new way of writing applications with all Web technologies.
nwjs 合并 Browser 和 Node.js 的运行时,可以使用前端开发技术来开发跨平台的应用程序。借助 Node.js 访问操作系统原生 API 的能力,可以开发中跨平台的应用程序。微信小程序开发工具就是使用 nwjs 开发的。如果你是 Mac 用户,进入目录 /Applications/wechatwebdevtools.app/Contents/Resources/app.nw/app 可以看到开发工具的实现代码,当然代码是经过混淆的。网上流行的破解版本开发工具原理上就是修改这里面的代码。
与此类似的,一个更火的项目是 Electron,由 GitHub 推出的,它也是把 Browser 和 Node.js 结合,用来开发跨平台的应用程序。程序员们应该听说过 Atom 这个编辑器界的后起之秀。包括微软拥抱开源社区的编辑器 vscode 也是使用 Electron 开发的。
Electron vs nwjs
这两个平台有什么区别?为什么微信选择 nwjs 呢?我们不妨猜一猜。
从技术角度来讲:
应用程序入口不同:Electron 入口是一个 javascript 脚本,脚本里要自己负责创建浏览器窗口,加载 html 页面。而 nwjs 的入口就是一个 html 页面,框架自己会创建浏览器窗口来显示这个 html 页面。 Node.js 集成方式不同:Electron 直接使用 Node.js 的共享库,不需要修改 Chromium 代码。而 nwjs 为了集成 Node.js ,需要修改 Chromium 代码,以便在浏览器里能通过 Node.js 访问系统原生 API。 Multi-Context: nwjs 有多个上下文,一个是浏览器的上下文,用来访问 Browser 相关 API,比如操作 DOM ,另外一个是 Node 上下文,用来访问操作系统 API。Electron 没有使用多个上下文,对开发者更友好。
从应用角度来讲:
打包后的文件大小:Electron 打包后文件会比 nwjs 小不少。一个 18M 的程序,使用 Electron 打包后是 117M,而使用 nwjs 打包后的程序是 220M。微信小程序开发工具打包后是 219M (v0.10.102800)。没有亲测,评价来源参考文档。 代码保护:Electron 只支持代码混淆来保护,而 nwjs 把核心代码放在 V8 引擎里,不但可以保护代码,还可以提高执行效率。 开源社区活跃度:Electron 应该是完胜的。看看使用 Electron 构建的应用程序就知道了。而据说 nwjs 的开发文档有些都没有及时更新。 应用程序启动时间:Electron 会稍微快一点。没有亲测,评价来源参考文档。
从这个分析猜测,微信选择 nwjs 的原因可能是出于代码保护。毕竟开发工具可以上传小程序,有些接口和数据需要比较严密的保护。哪位大牛可以挖挖看哪些代码被保护起来了。
真机运行环境
下面内容完全是猜测的,如有言中,实属运气。
微信小程序的运行环境应该更类似 ReactNative 之类,而不是纯 Html5。两者最大的不同在于,ReactNative 的界面是由原生控件渲染出来的,而 Html5 的界面是由浏览器内核渲染出来的。两者在性能上有较大的差异,感兴趣的可以参阅我的另外一篇文章《跨平台 App 开发技术方案汇总》。
原理上,小程序是如何在微信 App 里运行的呢?
微信 App 里包含 javascript 运行引擎。 微信 App 里包含了 WXML/WXSS 处理引擎,最终会把界面翻译成系统原生的控件,并展示出来。这样做的目的是为了提供和原生 App 性能相当的用户体验。
我们来意淫一下小程序加载运行的过程:
用户点击打开一个小程序 微信 App 从微信服务器下载这个小程序 分析 app.json 得到应用程序的配置信息(导航栏,窗口样式,包含的页面列表等) 加载并运行 app.js 加载并显示在 app.json 里配置的第一个页面
这个只是从开发者眼中看到的一个简化版的过程,实际过程应该比这要复杂得多,涉及到浏览器线程(就是运行我们的逻辑层代码 app.js 等的线程)和 AppService 线程之间的交互。从官方网站上的一个图片可以看出端倪:
生命周期
至于微信 App 是如何与小程序的逻辑层 javascript 交互的呢?可以简单地归纳如下:
JavaScript 是脚本语言,可以在运行时解释并执行。微信 App 里包含了一个 JavaScript 引擎,由它来负责执行逻辑层的 JavaScript 代码。那么 JavaScript 调用的小程序相关 API 怎么实现的呢?答案是最终会被翻译成实现在微信 App 里的原生接口。比如开发者调用 wx.getLocation(OBJECT) 获取当前地理位置,微信 App 里的 JavaScript 引擎在执行这个代码时,会去调用微信 App 里实现的原生接口来获取地理位置坐标。