“Yeah It’s on. ”
正文
框架设计
整体思路是用gulp复制src文件夹中的文件到dist文件夹,在个过程中完成编译,处理相关文件。如处理js文件,用babel将es6语法转成es5;处理less文件,将less文件转wxss;处理图片文件进行压缩等。
核心的三条命令
- npm run dev (进行开发环境打包,并监听文件)
- npm run watch (监听文件)
- npm run build (生产环境打包)
在development环境中js会加上sourcemaps 在production环境中会压缩wxss,压缩js,压缩图片,去除js的sourcemaps
diff
https://github.com/mattphillips/deep-object-diff/blob/master/src/diff/index.js
小程序框架中由于小程序原生setData所做的diff并不够高效,所以打算自己来搞diff
deep-object-diff
找到了一个diff库
export const properObject = o => isObject(o) && !o.hasOwnProperty ? { ...o } : o;
import { isDate, isEmpty, isObject, properObject } from '../utils';
const diff = (lhs, rhs) => {
if (lhs === rhs) return {}; // equal return no diff
if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs
const l = properObject(lhs);
const r = properObject(rhs);
// 找到删除了的属性 (循环l)
const deletedValues = Object.keys(l).reduce((acc, key) => {
// 找到l有r没有的属性
return r.hasOwnProperty(key) ? acc : {...acc, [key]: undefined};
}, {});
// 存在日期
if (isDate(l) || isDate(r)) {
if (l.valueOf() == r.valueOf()) return {};
return r;
}
// (循环r)
return Object.keys(r).reduce((acc, key) => {
// return added r key (找到r有l没有的属性)
if (!l.hasOwnProperty(key)) return {...acc, [key]: r[key]};
// l有r也有的属性 递归diff
const difference = diff(l[key], r[key]);
// return no diff
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc;
return {...acc, [key]: difference}; // return updated key
}, deletedValues);
};
wxa框架中的diff就是采用这个diff
https://github.com/wxajs/wxa/blob/dev/packages/wxa-core/src/diff/diff.js
flat
https://github.com/hughsk/flat
Take a nested Javascript object and flatten it, or unflatten an object with delimited keys.
取一个嵌套的Javascript对象并将其展平,或使用分隔键取消展平一个对象。
is-buffer
https://www.npmjs.com/package/is-buffer
Determine if an object is a Buffer (including the browserify Buffer)
Why not use Buffer.isBuffer?
This module lets you check if an object is a Buffer without using Buffer.isBuffer (which includes the whole buffer module in browserify). It’s future-proof and works in node too
爬坑
对node_modules的处理
不应该对src中node_modules进行处理
复制src中package.json到dist,利用gulp-install安装依赖,生成dist下的node_modules。
不监听src中node_modules文件的修改
async/await
新版本官方已支持
要在小程序中使用async/await
如何实现?两种方式
第一直接写 不勾选微信开发者工具中的ES6转ES5 和 不用babel编译js
总之不对js做处理
但是es6在低端机的兼容性不是很好
第二种需要自己引入regeneratorRuntime这个模块
如果 勾选微信开发者工具中的ES6转ES5或者自己用babel等工具把小程序的js文件转成了es5那么会报错
thirdScriptError
sdk uncaught third Error
regeneratorRuntime is not defined
ReferenceError: regeneratorRuntime is not defined
解决方案:
// 在app.js头部增加
global.regeneratorRuntime = require('./lib/regenerator/runtime-module')
使用async/await的js文件头部增加
const {regeneratorRuntime} = global
使用ES6特性Class后出现编译异常
https://segmentfault.com/a/1190000009739674
Uncaught SyntaxError: Unexpected token export
这里也是因为es6语法的问题,将js转成es5就行了
require的坑
微信小程序中require是不支持对象解构
// 下面这种情况是错误的
const {setTagId} = require('./utils/util')
//下面这种写法也是错误的 在其他文件中无法require进来
module.exports = {
getSign,
getFormIdSign,
basePostRequest,
singletonForLogin,
getVersion,
};
但是在web开发中是可以。。。
只能改成
exports.getSign = getSign
exports.getFormIdSign = getFormIdSign
exports.basePostRequest = basePostRequest
exports.singletonForLogin = singletonForLogin
exports.getVersion = getVersion
框架的打包的坑
由于我们使用es6转es5和增强编译,所以要把框架的npm包打成es5的形式
{
"presets": [
[
"es2015",
{
"loose": true
}
],
"stage-1"
],
"plugins": [
[
"transform-runtime",
{
"helpers": false,
"polyfill": false,
"regenerator": true
}
]
]
}
这样纸这个npm包会带上@babel/runtime的东西
而小程序增强编译后自带@babel/runtime里面有helpers和generator.js
由于小程序npm的特殊性,和小程序自带的处理,这样的做法其实并不好
更好的做法应该是将npm与工作流结合起来,更好的使用npm,更好的开发体验。
解决方法一:
像beautywejs一样
利用 webpack-stream 和 npm/index
// 在src的npm/index.js中
import storage from '@beautywe/plugin-storage';
import event from '@beautywe/plugin-event';
import listpage from '@beautywe/plugin-listpage';
import logger from '@beautywe/plugin-logger';
export const plugin = {
storage,
event,
listpage,
logger,
};
export { default as beautywe } from '@beautywe/core';
把这些东西通过gulp和webpack-stream,经过babel-loader处理,转移到dist下的npm
这样就可以了
必要的情况下可以通过一些ast手段,改变引入npm的路径。
总结起来:
Npm直接在项目引入node_modules的内容,无需借助小程序的那套npm流程,无需手工复制依赖库。
解决方法二:
重新设计一些module机制
像wepy,我们去查看它的dist
"use strict";
var _regeneratorRuntime2 = _interopRequireDefault(require('./vendor.js')(2));
var _core = _interopRequireDefault(require('./vendor.js')(0));
var _eventHub = _interopRequireDefault(require('./common/eventHub.js'));
var _x = _interopRequireDefault(require('./vendor.js')(4));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
它将npm的包移动到了vendor中
最好的解决办法
其实,我们完全可以直接使用小程序的npm流程
我之所以不使用是因为我遇到一个坑,这个坑拖累了我一个星期。
真的真的非常的坑
下面来详细说一说
首先,我们使用webpack打包框架,不指定filename,默认会是main.js
接着,我们吧打包好的文件上传npm
接入小程序的npm使用流程
我们发现
无法引入我们已经上传好了的npm包,很奇怪
const xhw = require("@xhw/core")
但是之前sdk是可以的,经过长时间的对比和探究,发现:
这里居然是main.js….
难怪require不进来,使用require(“@xhw/core/main.js”)应该就行了
更好的办法
之后,使用webpack打包框架,指定filename为index.js
const xhw = require("@xhw/core")
一切正常,可以找到xhw
这个教训告诉我,如果要打包一个第三方库最好命名index.js 这个教训告诉我,如果要打包一个第三方库最好命名index.js 这个教训告诉我,如果要打包一个第三方库最好命名index.js
over
慢着,还有一个问题,增强编译自带@babel/runtime,那我们打包的库就没必要带上runtime
所以我们打包框架所用的babel配置为
{
"presets": [
"@babel/preset-env"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"decoratorsBeforeExport": true
}
],
[
"@babel/plugin-proposal-class-properties"
]
]
}
进一步减少包体积
去掉 use strict
默认babel转义后的js文件头会带上’use strict’
第一次尝试:在babel7中使用@babel/plugin-transform-strict-mode,失败
第二次尝试:在babel中使用plugin-transform-strict-mode,失败
第三次尝试:使用gulp-remove-use-strict,失败
第四次尝试:在babel中设置sourceType为”script”,失败
https://www.babeljs.cn/docs/options#misc-options
sourceType
Type: "script" | "module" | "unambiguous"
Default: "module"
- “script” - Parse the file using the ECMAScript Script grammar. No import/export statements allowed, and files are not in strict mode.
- “module” - Parse the file using the ECMAScript Module grammar. Files are automatically strict, and import/export statements are allowed.
- “unambiguous” - Consider the file a “module” if import/export statements are present, or else consider it a “script”.
最终解决办法:
使用babel6,增加了配置”modules”: false
https://segmentfault.com/q/1010000013533162
{
presets: [
["env", { "modules": false }]
]
}
gulp相关
gulp-clean-css报错
https://segmentfault.com/q/1010000009464383/a-1020000009474922
Error [ERR_UNHANDLED_ERROR]: Unhandled error. (Ignoring local @import of "../../common/common" as resource is missing.)
at Domain.emit (events.js:178:17)
at Domain.EventEmitter.emit (domain.js:441:20)
at DestroyableTransform.EventEmitter.emit (domain.js:454:12)
at DestroyableTransform.onerror (F:\前端项目\xhw-native\node_modules\readable-stream\lib\_stream_readable.js:640:52)
at DestroyableTransform.emit (events.js:189:13)
at DestroyableTransform.EventEmitter.emit (domain.js:441:20)
at onwriteError
可以非常快的定位到是css中@import导致路径的问题
尝试解决:
http://www.imooc.com/wenda/detail/467650
gulp-clean-css 只是一个 gulp 的插件,内部使用了clean-css,因此可以去 clean-css 项目找解决方案:
https://github.com/jakubpawlowicz/clean-css#how-to-process-remote-imports-correctly
How to process remote @imports correctly?
In order to inline remote @import statements you need to provide a
callback to minify method as fetching remote assets is an asynchronous
operation, e.g.:
var source = '@import url(http://example.com/path/to/remote/styles);';new CleanCSS({ inline: ['remote'] }).minify(source, function
(error, output) { // output.styles});
f you don't provide a callback, then remote @imports will be left as is.
加一个参数:
{ inline: ['remote'] }
但是,解决失败。。。。。
经过n次解决后,决定,放弃使用gulp-clean-css压缩css
gulp-htmlmin报错
gulp-htmlmin压缩小程序wxml会有奇奇怪怪的问题
如:样式错乱 页面白屏 单标签闭合 等
.pipe(gulpif(!config.isDev, htmlmin({
collapseWhitespace: true,
removeComments: true,
keepClosingSlash: true
})))
经过多次的尝试也没有找到解决办法,最终放弃了
压缩wxml和wxss
经过前面的gulp-htmlmin和gulp-clean-css都未能正确压缩
那该如何压缩wxml和wxss呢?
经过查看wepy源码发现它用到了一个库 pretty-data
https://github.com/Tencent/wepy/blob/2.0.x/packages/wepy-plugin-filemin/src/index.js
https://www.npmjs.com/package/pretty-data
nodejs plugin to pretty-print or minify text in XML, JSON, CSS and SQL formats.
nodejs插件,美化打印或缩小文本在XML, JSON, CSS和SQL格式。
所以,我找到了它的对应gulp包gulp-pretty-data
https://www.npmjs.com/package/gulp-pretty-data
.pipe(gulpif(!config.isDev, prettyData(
{
type: 'minify',
extensions: {
'wxss': 'css',
'less': 'css'
}
}
)))
.pipe(gulpif(!config.isDev, prettyData(
{
type: 'minify',
extensions: {
'wxml': 'xml'
}
}
)))
最终完美解决了问题,所以有空看看一些优秀框架源码还是有用的。
对比
迁移原生框架之前,采用wepy框架,sdk采用git submodule管理内联进了项目(这种方式其实不好,sdk很容易被人修改)
sdk体积320 kb,内含protobuf相关88kb
迁移原生框架之后,sdk采用webpack打包,发布至npm私服管理
sdk体积64kb,内除去了protobuf相关
这里整个小程序体积前后对比其实并没有什么意义,原生小程序框架会含有node_modules和miniprogram_npm等原因,体积会有很多冗余。
前面打包进行了压缩wxml和wxss,分析下原生框架打包前后体积对比
- npm run dev情况下,体积1.34m (包含node_modules和miniprogram_npm)
- npm run build情況下,体积1.21m (包含node_modules和miniprogram_npm)
感觉优化并不是很大。。。还是有点用。。。。
再加上微信开发者工具对js的压缩和混淆体积会进一步减少
补充
对store的优化
但我们同一时间多次调用store.get的时候,有时候会多次触发getter函数,getter里面存在耗时操作
按道理,同时多次调用get我们只需要走一次get流程,后面的采用第一次的结果即可
下面进行优化
const STORE = 'store';
global.singleton = global.singleton || {};
const singleton = global.singleton;
/**
* (流程锁,减少不必要的耗时操作)
* get之前的操作 存储多余的任务
* @param name
* @param resolve
* @param reject
* @returns {boolean} 是否继续执行流程
* @private
*/
function _beforeGet({name, resolve, reject}) {
name = `store_${name}`
let lockName = `${name}_lock`
singleton[lockName] = singleton[lockName] || false;
if (singleton[lockName]) {
// 锁上了 把信息存储在任务队列
singleton[name].push({resolve, reject});
return false
} else {
// 锁起来
singleton[lockName] = true
singleton[name] = []
return true
}
}
/**
* 执行多余的任务 同时把锁关闭
* @private
*/
function _afterGet({name, result}) {
name = `store_${name}`
let lockName = `${name}_lock`
let task
// eslint-disable-next-line
while (task = singleton[name].shift()) {
debugger
task.resolve && task.resolve(result)
}
singleton[lockName] = false
}
/**
* 获取当前store中的state中的具体数据
* @param{String} name 在state中的名字 (必填)
* @param{Object} data 额外的参数 (非必填) (会穿透到对应getters函数)
* @returns {Promise<any>}
* 问题:同时多次调用get我们只需要走一次get流程,后面的采用第一次的结果即可
*/
get(name, data) {
if (typeof name !== 'string') {
throw new Error(logger(`get方法必须传进一个string, "${name}" 非法`));
}
return new Promise(async (resolve, reject) => {
console.log(`res 我执行 ${name}`)
if (_beforeGet({name, resolve, reject})) {
let value = this.state[name];
let result
if (value !== null && value !== undefined && value !== "") {
// value有值 值有可能为false 或 0
// value !== "" 是因为 wx.getStorage一个不存在的东西 返回 ""
result = isObject(value) ? deepCopy(value) : value;
} else {
// state没有值 触发getters
let getter = this.getters[name];
if (getter) {
result = await getter(data, this.commit.bind(this))
}
}
resolve(result)
_afterGet({result, name})
}
});
}
通过beforeGet和afterGet控制流程实现
无法对Component实例封装
先看一个例子
class P {
constructor(options) {
for (let name in options) {
if (options.hasOwnProperty(name)) {
this[name] = options[name]
}
}
}
}
P.prototype.aaa = "aaa"
P.prototype.ccc = "ccc"
let p = new P({
methods: {
async onLoad(options) {
console.log("onLoad", options, this.aaa, this.ccc)
},
},
})
Page(p)
p里面的内容会根据不同的构造函数更改
当我们使用Page(p) 和 App(p) 时我们可以访问到 this.aaa 和 this.ccc
但是但我们使用Component(p)时 this.aaa 和 this.ccc 都是 undefined
这是一个非常严重的问题,意味着框架无法统一处理Component,Page和App
但是,框架封装Page和App进行扩展其实已经够用,我们可以不去封装Component
gulp无法与cli脚手架结合起来
[14:07:29] Local gulp not found in /path/to/project
[14:07:29] Try running: npm install gulp
gulpfile.js必须存在在项目的根目录,不能放到cli脚手架中,这就意味着build过程要放在小程序项目中无法抽取出来,若要抽取出来就不能使用gulp,只能二选一。
从这个issue中可以看出
https://github.com/gulpjs/gulp/issues/2126