Omi 框架是騰訊開源的下一代前端框架,提供桌面、移動和小程序總體解決方案(One framework. Mobile & Desktop & Mini Program), Omip 是 Omi 團隊開發的跨端開發工具集,支持小程序和 H5 SPA,最新的 omip 已經適配了 h5,以下方新增的兩條命令:css
npm i omi-cli -g
omi init-p my-app
cd my-app
npm start //開發小程序
npm run dev:h5 //開發 h5
npm run build:h5 //發佈 h5
複製代碼
node 版本要求 >= 8前端
也支持一條命令
npx omi-cli init-p my-app
(npm v5.2.0+)node
固然也支持 TypeScript:webpack
omi init-p-ts my-app
複製代碼
TypeScript 的其餘命令和上面同樣,也支持小程序和 h5 SPA 開發。git
特性包括:github
Omip 不只能夠一鍵生成小程序,還能一鍵生成 h5 SPA。怎麼作到的?下面來一一列舉難點,逐個擊破。web
小程序擴展尺寸單位 rpx(responsive pixel): 能夠根據屏幕寬度進行自適應。規定屏幕寬爲750rpx。如在 iPhone6 上,屏幕寬度爲375px,共有750個物理像素,則750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。npm
這個特性大受好評,製做響應式網站很是有用。由於瀏覽器是不支持 rpx 單位,因此須要運行時轉換,恰好 omi 內置了這個函數:編程
function rpx(str) {
return str.replace(/([1-9]\d*|0)(\.\d*)*rpx/g, (a, b) => {
return (window.innerWidth * Number(b)) / 750 + 'px'
})
}
複製代碼
從 rpx 源碼能夠看到,須要運行時轉換 rpx,而非編譯時!由於只有運行時能拿到 屏幕寬度,omi 早期版本已經支持運行時的 rpx 轉換:小程序
import { WeElement, define, rpx } from 'omi'
define('my-ele', class extends WeElement {
static css = rpx(`div { font-size: 375rpx }`)
render() {
return (
<div>my ele</div>
)
}
})
複製代碼
小程序 Shadow tree 與 omi 有一點點不同,omi 是從根開始 shadow root,而小程序是從自定義組件開始,omio 則沒有 shadow root。
Omi | Omio | 小程序 | |
---|---|---|---|
Shadow DOM | 從根節點開始 | 無 | 從自定義組件開始 |
Scoped CSS | 從根節點開始局部做用域,瀏覽器 scoped | 從根節點開始局部做用域(運行時 scoped) | 自定義組件局部做用域 |
因此,app.css 須要污染到 page 裏的 WXML/JSX,但在 omi 和 omio 中樣式都是隔離的, 須要怎麼作才能突破隔離?先看 app.js 源碼:
import './app.css' //注意這行!!!
import './pages/index/index'
import { render, WeElement, define } from 'omi'
define('my-app', class extends WeElement {
config = {
pages: [
'pages/index/index',
'pages/list/index',
'pages/detail/index',
'pages/logs/index'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
}
複製代碼
上面是使用 omip 開發小程序的入口 js 文件,也是 webpack 編譯的入口文件,在 cli 進行語法樹分析的時候,能夠拿到 import 的各個細節,而後作一些變換處理,好比下面 ImportDeclaration(即 import 語句) 的處理:
traverse(ast, {
ImportDeclaration: {
enter (astPath) {
const node = astPath.node
const source = node.source
const specifiers = node.specifiers
let value = source.value
//當 app.js 裏 import 的文件是以 .css 結尾的時候
if(value.endsWith('.css')){
//讀取對應 js 目錄的 css 文件,移除 css 當中的註釋,保存到 appCSS 變量中
appCSS = fs.readFileSync(filePath.replace('.js','.css'), 'utf-8').replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, '')
//移除這裏條 import 語句
astPath.remove()
return
}
複製代碼
獲得了 appCSS 以後,想辦法注入到全部 page 當中:
traverse(ast, {
ImportDeclaration: {
enter (astPath) {
const node = astPath.node
const source = node.source
let value = source.value
const specifiers = node.specifiers
//當 import 的文件是以 .css 結尾的時候
if(value.endsWith('.css')){
//讀取對應 js 目錄的 css 文件,移除 css 當中的註釋,保存到 css 變量中
let css = fs.readFileSync(filePath.replace('.js','.css'), 'utf-8').replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, '')
//page 注入 appCSS
if(filePath.indexOf('/src/pages/') !== -1||filePath.indexOf('\\src\\pages\\') !== -1){
css = appCSS + css
}
//把 import 語句替換成 const ___css = Omi.rpx(.....) 的形式!
astPath.replaceWith(t.variableDeclaration('const',[t.variableDeclarator(t.identifier(`___css`),t.callExpression(t.identifier('Omi.rpx'),[t.stringLiteral(css)]),)]))
return
}
...
複製代碼
這就夠了嗎?不夠!由於 ___css 並無使用到,須要注入到 WeElement Class 的靜態屬性 css 上,繼續 ast transformation:
const programExitVisitor = {
ClassBody: {
exit (astPath) {
//注入靜態屬性 const css = ___css
astPath.unshiftContainer('body', t.classProperty(
t.identifier('static css'),
t.identifier('___css')
))
}
}
}
複製代碼
編譯出得 page 長這個樣子:
import { WeElement, define } from "../../libs/omip-h5/omi.esm";
const ___css = Omi.rpx("\n.container {\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: 200rpx 0;\n box-sizing: border-box;\n} \n\n.userinfo {\n display: flex;\n flex-direction: column;\n align-items: center;\n}\n\n.userinfo-avatar {\n width: 128rpx;\n height: 128rpx;\n margin: 20rpx;\n border-radius: 50%;\n}\n\n.userinfo-nickname {\n color: #aaa;\n}\n\n.usermotto {\n margin-top: 200px;\n}");
const app = getApp();
define('page-index', class extends WeElement {
static css = ___css;
data = {
motto: 'Hello Omip',
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo')
...
...
複製代碼
大功告成!
因爲小程序裏的一些標籤在瀏覽器中不可以識別,好比瀏覽器不識別 view、text 等標籤,須要轉換成瀏覽器識別的標籤,因此這裏列了一個映射表:
const mapTag = {
'view': 'div',
'picker': 'select',
'image': 'img',
'navigator': 'a',
'text': 'span'
}
const getNodeName = function(name){
if(mapTag[name]) return mapTag[name]
return name
}
複製代碼
在 h
函數建立虛擬 dom 的時候進行 getNodeName
:
function h(nodeName, attributes) {
...
...
var p = new VNode();
p.nodeName = getNodeName(nodeName);
p.children = children;
p.attributes = attributes == null ? undefined : attributes;
p.key = attributes == null ? undefined : attributes.key;
...
...
return p;
}
複製代碼
這裏還有遺留問題,好比內置的一些原生組件如:
這些組件若是你須要開發 h5,就別用上面這些組件。若是必定要使用上面的組件,那麼請使用 omi 先實現上面的組件。
const map = require('./tag-mapping')
const css = require('css')
const cssWhat = require('css-what')
const cssStringify = require('./css-stringify')
function compileWxss(str) {
let obj = css.parse(str)
obj.stylesheet.rules.forEach(rule => {
rule.selectors && rule.selectors.forEach((selector, index) => {
let sltObjs = cssWhat(selector)
sltObjs.forEach(sltObj => {
sltObj.forEach(item => {
if (item.type == 'tag') {
item.name = map(item.name)
}
})
})
rule.selectors[index] = cssStringify(sltObjs)
})
})
return css.stringify(obj)
}
複製代碼
轉換前:
.abc view {
color: red;
}
複製代碼
轉換後
.abc div {
color: red;
}
複製代碼
這裏須要注意的是,不是全部 api 都能適配,只能適配一部分:
wx | web |
---|---|
wx.request | XMLHttpRequest |
界面 api(confirm、loaing、toast等) | 實現對應的omi組件 |
數據存儲 api | localStorage |
wx 特有的 api 還包括一些特有的生命週期函數,如:
這是 wx 裏 Page 裏的生命週期,而 omi 是不包含的。這裏須要在 router 的回調函數中進行主動調用。具體怎麼出發且看路由管理。
先看 cli 編譯出來的 app.js 路由部分:
render() {
return <o-router mode={"hash"} publicPath={"/"} routes={[{
path: '/pages/index/index',
componentLoader: () => import( /* webpackChunkName: "index_index" */'./pages/index/index'),
isIndex: true
}, {
path: '/pages/list/index',
componentLoader: () => import( /* webpackChunkName: "list_index" */'./pages/list/index'),
isIndex: false
}, {
path: '/pages/detail/index',
componentLoader: () => import( /* webpackChunkName: "detail_index" */'./pages/detail/index'),
isIndex: false
}, {
path: '/pages/logs/index',
componentLoader: () => import( /* webpackChunkName: "logs_index" */'./pages/logs/index'),
isIndex: false
}]} customRoutes={{}} basename={"/"} />;
}
});
render(<my-app />, '#app');
複製代碼
4個頁面各自作了分包,這樣能夠加快首屏節省帶寬按需加載。接下來看 <o-router />
的實現:
import { WeElement, define, render } from "../omip-h5/omi.esm";
import 'omi-router';
let currentPage = null;
let stackList = [];
define('o-router', class extends WeElement {
_firstTime = true;
installed() {
...
...
}
});
export function routeUpdate(vnode, selector, byNative, root) {
...
...
}
window.onscroll = function () {
...
...
};
複製代碼
具體實現細節能夠去看 o-router 源碼,主要實現了下面一些功能:
Omi 相關任何問題疑問反饋意見歡迎進羣交流。