内容简介:开发discourse插件, 依赖的知识: ES6/SCSS/Ember.js/Rails/handlebars.Ember是前端MVVM框架, 支持数据双向绑定/虚拟DOM, 模板引擎使用handlebars, 依赖jQuery(处理DOM兼容性操作), 遵循依赖: ruby, postgres, redis 需要提前安装好.
开发discourse插件, 依赖的知识: ES6/SCSS/Ember.js/Rails/handlebars.
Ember是前端MVVM框架, 支持数据双向绑定/虚拟DOM, 模板引擎使用handlebars, 依赖jQuery(处理DOM兼容性操作), 遵循 约定优于配置
原则(类似Rails).
discourse 本地环境搭建
依赖: ruby, postgres, redis 需要提前安装好.
-
macOS 安装 Ruby 2.6
brew install rbenv; rbenv init; rbenv install 2.6.2; # discourse依赖 Ruby 2.5+
-
(推荐) 用 docker 搭建本地 postgres, redis
# code db.yml version: '3.1' services: redis: container_name: redis image: redis:alpine ports: - "6379:6379" postgres: container_name: postgres image: postgres:9-alpine ports: - "5432:5432" environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres # docker-compose up -d -f db.yml # 启动 postgresql/redis # docker restart postgres redis # 重启 postgresql/redis
git clone https://github.com/discourse/discourse.git; # git checkout tags/v2.2.4; # 指定某个稳定的版本 code config/database.yml # 设定 postgres 数据库信息 development: # 设定正确用户名/密码 username: name password: pass host: localhost bundle install; # 安装依赖 bundle exec rake db:create db:migrate; # 创建数据库 # rails r "SiteSetting.min_password_length=8;SiteSetting.min_admin_password_length=8;" # 设定密码最少8位 rake admin:create # 创建用户, 输入Email/password/是否管理员 # rails r "u=User.find_by_email('test@test.com'); u.password='11112222'; u.save!;" # 修改用户密码 rails s -p 8000 # 启动论坛, 访问 localhost:8000
创建plugin
例子: 创建名为 DiscourseTest
的插件
rails g plugin DiscourseTest # 创建插件, 自动创建以下文件: # plugins/discourse-test/README.md # plugins/discourse-test/LICENSE # 默认MIT # plugins/discourse-test/plugin.rb # 插件后端入口 # plugins/discourse-test/assets/stylesheets/common/discourse-test.scss # plugins/discourse-test/assets/javascripts/initializers/discourse-test.es6 # 插件前端入口 # plugins/discourse-test/config/settings.yml # 插件配置项 # plugins/discourse-test/config/locales/client.en.yml # 扩展i18n语言包 # plugins/discourse-test/config/locales/server.en.yml # 插件生成器有bug, 生成的js文件名 要手动修改: # discourse-test.es6 => discourse-test.js.es6 # 方便 vscode 开发: code .gitignore # 增加3行: /jsapp/ /adminjs/ !/plugins/discourse-test # 插件路径 code .gitmodules # 增加(方便vscode 管理多个插件): [submodule "plugins/discourse-test"] path = plugins/discourse-test url = git@github.com/username/discourse-test.git
重启discourse后, 管理员登录论坛, 可以在 /admin/plugins
看到已安装的插件: DiscourseTest
.
开发模式: 修改scss页面会自动更新, 修改js文件需刷新页面.
plugin API (前端)
前端 app/assets/javascripts/discourse/lib/plugin-api.js.es6
的一些主要方法.
在插件 assets/javascripts/initializers/discourse-test.js.es6
的 initialize
内使用.
- api.getCurrentUser(); 获取当前用户信息
- api.replaceIcon(source, destination); 替换Discourse icon
- api.modifyClass(resolverName, changes, opts); 覆盖或扩展类的方法(controller/component/model)
- api.modifyClassStatic(resolverName, changes, opts); 覆盖或扩展类中的静态方法
- api.reopenWidget(name, args); 扩展或覆盖方法
-
api.decorateWidget('widgetName:LOCATION', helper => {}); 在widget前/后 添加内容, LOCATION:
before
或after
还可以指定Widget里面的applyDecorators(this, "footerLinks", ...)
部分, 比如, 在后面追加菜单:
api.decorateWidget('hamburger-menu:footerLinks', () => ({ href: '#', rawLabel: 'Test' }));
- api.createWidget(name, args); 创建 Widget
- api.changeWidgetSetting(widgetName, settingName, newValue); 如果Widget有settings, 改变settings值
- api.addNavigationBarItem({}); 导航栏navigation-bar添加菜单 (帖子列表Categories后面)
- api.addUserMenuGlyph(); 在用户菜单中添加图标(右上角用户头像的展开菜单)
- api.onPageChange((url, title) => {}); 每次页面变更时触发 (比如用于统计)
- api.decorateCooked($elem => $elem.css({ backgroundColor: '#000' })); 帖子渲染后, 使用jQuery修改帖子内容
- api.onToolbarCreate(toolbar => { toolbar.addButton(); }); 给编辑器的 工具 栏添加新按钮
- api.onAppEvent(name, () => {}); 侦听 appEvents.trigger() 事件
- api.attachWidgetAction('header', actionName, fn); 给widget里增加action, 通过this.sendWidgetAction(actionName)触发
plugin API (后端)
后端 lib/plugin/instance.rb
的一些主要方法, 在插件内的 plugin.rb
内使用:
-
register_asset "stylesheets/common/discourse-test.scss"
注册资源文件(支持scss/css/js/es6文件), 主要针对样式/lib库
assets/javascripts/initializers
内的js文件会自动引入, 无需注册. -
enabled_site_setting :discourse_test_enabled
把插件内config/settings.yml
的值, 默认存到site_settings表中(使用多站点模式时必须)
后台修改插件的setting时, 修改也会保存到site_settings表, 不启用多站点, 可以省略. -
register_svg_icon "fab-youtube" if respond_to?(:register_svg_icon)
# 注册FontAwesome图标(svg-icons/fontawesome/内并未包含所有图标) -
register_html_builder('server:xxx')
, 会在 .erb 里<%= build_plugin_html 'server:xxx' %>
位置插入HTML, 比如:register_html_builder('server:before-head-close') do # <%= build_plugin_html 'server:before-head-close' %> '<script src="//cdn.jsdelivr.net/gh/kenwheeler/slick@1.8.0/slick/slick.min.js"></script>' end
-
extend_content_security_policy, 扩展引入外部资源策略CSP (论坛默认防XSS, 禁止了外部资源), 比如:
extend_content_security_policy( # 允许加载cdnjs/jsdelivr的资源 script_src: ['cdnjs.cloudflare.com', 'cdn.jsdelivr.net'], )
-
after_initialize do
实例化后执行的逻辑 (比如: 添加路由等操作)
Routes
-
定义路由
// assets/javascripts/discourse/*-route-map.js.es6 this.route('forum', { path: '/forum' }); // 读取 routes/forum.js.es6, 文件不存在, 页面会空白 this.route('discovery', function() { this.route('forum', { path: '/forum' }); // 读取 routes/discovery-forum.js.es6 });
// assets/javascripts/discourse/routes/routeName.js.es6 // 不设定 templateName 或 renderTemplate (默认用当前路由对应的template: templates/routeName.hbs) export default Discourse.Route.extend({ // templateName: 'test', // [可选] 指定模板: templates/test.hbs renderTemplate(controller, model) { // [可选] 设定当前路由的渲染逻辑 // render: 渲染一个模板到另一个模板的某个位置(位置 在模板里用 {{outlet}} 表示) this.render('test', { // [可选] 待渲染的模板名称 (templates/test.hbs) into: 'application', // [可选] 在哪个模板插入当前模板 (在 templates/application.hbs 的{{outlet}}位置插入) outlet: 'modal', // [可选] 指定outlet名称, 改变插入模板的位置 (在 application.hbs 的{{outlet "modal"}}位置插入) controller: 'controllerName', // [可选] 指定模板的controller名称 (controllers/controllerName.js.es6) // model: model // [可选] 给 controller 设定model (controller里: this.get('model')) }); }, model() { // 初始化数据: 获得model 给controller 使用 return ajax('/about.json').then(json => json); }, setupController(controller, model) { // 给controller设置属性 (如省略: 默认给controller设置: `model`等于model()方法的返回值) controller.setProperties({ model }); }, resetController(controller, isExiting, transition) { // model改变或离开路由时, 修改属性 if (isExiting) controller.setProperties({ page: 1 }); } actions: { // 定义方法 willTransition(transition) { // [自带事件] 离开当前路由 // transition.abort(); // 终止路由的转换 }, didTransition() { // [自带事件] 路由完成转换(在beforeModel,model,afterModel,setupController后执行) // return true; // 冒泡: 触发父路由的willTransition方法 }, alert(msg) { // 定义事件给模板使用: <a {{action "alert" "test"}}>test</a> alert(msg); } } });
-
Methods/Properties:
-
queryParams: query的变量可直接在 hbs 模板使用
- refreshModel: true // queryParams值改变, 触发: model() 方法
- titleToken() // 设定页面的 title
- beforeModel() 在model方法前执行
- afterModel() 在model方法后执行
- redirect() 页面跳转逻辑
- activate/deactivate 进入路由/退出路由时执行
-
-
路由跳转:
this.transitionTo('posts', { queryParams: { page: 1 } })
orthis.replaceWith('discovery.latest')
-
console.log(Discourse.__container__.lookup('router:main').currentRouteName)
打印当前路由 -
console.log(Discourse.__container__.lookup('router:main')._routerMicrolib.recognizer.names)
打印所有路由 - 延伸: Creating Routes in Discourse and Showing Data
Controllers
-
定义控制器 (controller已经被弱化, 功能大都可以在route实现)
// assets/javascripts/discourse/controllers/*.js.es6 export default Ember.Controller.extend({ queryParams: ['page'], // 定义查询参数 page: 1, // 设置 queryParams 默认值 application: Ember.inject.controller('application'), // 定义属性给自己/模板使用 path: Ember.computed.alias('application.currentPath'), init() { // 控制器初始化 this.set('list', []); }, actions: { // 定义事件给模板使用 loadMore () { const p = this.get('page') + 1; this.set('page', p); // 更新queryParams, 触发路由的model()更新数据 } } });
-
引用其他控制器:
this.controllerFor('user')
,controllers/*.js.es6
文件必须存在 -
路由跳转:
this.transitionToRoute('posts', { queryParams: { page: 1 } })
Models
- 定义模型 (插件开发一般用不到)
// app/assets/javascripts/discourse/models/test.js.es6 import { ajax } from "discourse/lib/ajax"; const Test = Discourse.Model.extend({}); Test.reopenClass({ // 覆盖或扩展类 findAll() { return ajax('/about.json').then(json => json); }, }); export default Test;
import Test from "discourse/plugins/DiscourseTest/discourse/models/test"; // Test.findAll().then(json => console.warn({ json })); // 取数据
-
store 数据仓库 (已经加载的数据自动缓存), router/controller/component/widget 里都可以使用 this.store
// find: 从store中获取id为1的数据 (第1个参数是model类名, 第2个参数对象的id值) this.store.find('test', 1); // request: /tests/1 this.store.findAll('test'); // request: /tests
需要 plugin.rb 创建 /tests 的顶级路由, 和对应的 controller.
Templates
-
assets/javascripts/discourse/templates/**.hbs
-
{{#if xx }} Y {{else if isTest}} test {{else}} N {/if}}
if...else -
{{#unless readOnly}} ... {{/unless}}
unless -
{{#each items as |item index|}} {{item.name}} {{/each}}
循环 -
调用 route/controller 里定义的action
-
<a onclick={{action "save"}}>
点击调用controller的action, 可简写成<a {{action "save"}}>
-
<a {{route-action "save" model}}></a>
点击调用route的action (传参 model) -
<a {{action "save" (hash test=true)}}>
用hash helper传递 键值对 到 action -
<a {{action 'save' bubbles=false preventDefault=false}}>
阻止冒泡: bubbles=false, 取消 阻止默认行为: preventDefault=false
-
-
attr属性绑定
<div class="{{if isActive 'active' 'none'}}> {{d-button class=(if isActive 'active' 'none')}} {{d-button classNameBindings="isActive:active:none"}}
-
表单元素
-
<input onchange={{action "changeValue"}} />
HTML元素 -
{{input type="checkbox" change=(action "changeValue")}}
自带组件 -
{{textarea type="text" focus-out=(action "changeValue")}}
自带组件 onfocusout
-
-
超链接:
<a href={{link}} target="_blank"> TEST </a> {{link-to 'search' (query-params page=1)}} TEST {{/link-to}}
-
{{partial 'test'}}
输出 controller 指定的templates/test.hbs
-
console.log(Ember.TEMPLATES)
打印所有的templates -
{{log model}}
可以打印模板的变量
Helpers
-
assets/javascripts/helpers/*.js.es6
会被自动加载 -
方式1
import { registerUnbound } from "discourse-common/lib/helpers"; registerUnbound('eq', (param1, param2) => {});
-
方式2
import { registerHelper } from "discourse-common/lib/helpers"; registerHelper('eq', (params, hash) => {});
-
方式3 Writing Helpers
// assets/javascripts/helpers/custom-field-validation.js.es6 function validation([type, title, key], hash) { // params, hash console.warn({ type, title, key }, hash); } export default Ember.Helper.helper(validation); // {{custom-field-validation value.type value.title value.key test=true}}
-
方式2/3, 参数支持 es6解构
([type, title, key], { test })
, (推荐用1/2方式, 遵循discourse的约定) -
.hbs文件里使用
{{helper-name arg1 arg2}}
-
*注:
hbs`...`
方法里不能调用helper, 会提示: not defined
Components
- components包含2个部分: js定义行为, hbs 定义UI.
-
定义组件: 例子
login-button
, 点击显示登录界面// assets/javascripts/discourse/components/login-button.js.es6 // .hbs里使用: {{#login-button class="btn"}} Login {{/login-button}} export default Ember.Component.extend({ tagName: 'a', classNames: ['loginBtn'], click() { const application = Ember.getOwner(this).lookup('route:application'); application.send('showLogin'); }, // actions: {} });
{{!-- assets/javascripts/discourse/templates/components/login-button.hbs --}} {{yield}}
- Adding Ember Components to Discourse , 插件修改Components
- Ember Component
-
Methods/Properties: init, didReceiveAttrs(收到attrs), willRender, didInsertElement, didRender
- buildArgs 传递 props到 widgets 的 attrs
-
表单组件
<!-- input props: placeholder, maxlength, readonly, checked, disabled --> {{input type="text" value=value}} {{textarea value=name cols="80" rows="6"}} <!-- dropdown/checkbox(初始values必须为数组) --> {{multi-select allowAny=false maximum=10 content=types values=values valueAttribute="id" nameProperty="name" minimum=1}} <!-- select/radio --> {{combo-box value=value content=options none="plugins.placeholder_select" valueAttribute="id" nameProperty="name" onSelect=(action "statusChanged") isDisabled=readOnly}} <!-- radio-button --> {{radio-button name="upload" id="local" value="local" selection=selection}} {{radio-button name="upload" id="remote" value="remote" selection=selection}} <!-- admin: string array(自由创建数组内容) --> {{value-list values=images inputType="array" addKey="plugins.add_options"}} <!-- markdown editor --> {{d-editor value='markdown'}} <!-- ace-editor --> {{ace-editor mode="html_ruby" content=buffered.content}} <!-- 类似 input type="text" --> {{text-field value="slug" placeholderKey="category.slug_placeholder" maxlength="255"}} <!-- button --> {{d-button action=(action "addValue") actionParam=value icon="plus" translatedLabel='Add' class="add-value-btn btn-small"}} <!-- loading --> {{conditional-loading-spinner condition=loading}} <!-- 同时添加多个 key value 多个\n切分 --> {{secret-value-list values="firstKey|FirstValue"}}
Widgets
-
Widget是一个带有
html()
函数的类,它生成渲染自身的虚拟dom (类似简化版的 Component). -
// assets/javascripts/discourse/widgets/menus.js.es6 import { createWidget } from "discourse/widgets/widget"; import { h } from "virtual-dom"; createWidget('menus', { tagName: 'div.menus', html(attrs, state) { // 输出要渲染的html内容 return [ h('div.list', { className: 'box' }, [ // div.list.box h('a', 'menu1'), h('a', 'menu2'), ]) ]; }, click() { // widget被点击 }, clickOutside() { // 点击widget外部 }, // 除了click, 还有 drag/keyUp/keyDown });
-
hbs里引入Widget:
{{mount-widget widget="widget-name" args=(hash param1=123)}}
引入 -
Widget里引入Widget:
this.attach(name, attrs, props)
; -
插件里引入Widget
helper.attach(name, attrs, { model: post })
; -
this.sendWidgetAction(actionName)
调当前Widget或上级Widget的 action - this.queueRerender() 重新渲染自己(Components/Widgets)
-
输出富文本HTML内容:
RawHtml({ html: `<span>${html}</span>` })
或h('i.iconfont', { innerHTML: $html })
Connectors
-
connector 由 hbs和js 组成, 会渲染到
{{plugin-outlet name="FolderName"}}
语法指定的位置 -
创建connector (在模板文件里的
{{plugin-outlet name="above-site-header"}}
位置渲染)<!-- assets/javascripts/discourse/templates/connectors/above-site-header/topbar.hbs --> <h4>{{title}}</h4>
// assets/javascripts/discourse/templates/connectors/above-site-header/topbar.js.es6 export default { setupComponent(_, ctx) { ctx.set('title', 'ABOVE-SITE-HEADER'); }, shouldRender(_, ctx) { return true; // show: true, hide: false }, // actions: {}, };
- Plugin Outlets for Ember 2.10
-
{{~raw-plugin-outlet name="topic-list-after-title"}}
会渲染 connectors/topic-list-after-title/*.raw.hbs 的内容
Settings
-
config/settings.yml 设置后台的插件配置项, 可以参考 site_settings.yml 里面的说明
- How to add settings to your Discourse theme
- default: 默认值
- client: true 变量给JavaScript使用
- type: 字段类型, 比如 upload, list(多选), enum(单选)
- list_type: compact (list 一行展示)
- allow_any: false (list 限制 不能添加自定义内容)
- refresh: 后台修改配置, 前端取变量时 会自动更新 (不需要reload页面)
-
增加FontAwesome图标: Admin/Settings - 搜索
svg_icon_subset
- 强制静态文件https: Admin/Settings - 勾选 `force https
Emberjs/Handlebars
- Ember-Teach教程目录
- Ember.run.next(() => {}); // setTimeout (run.later 1ms)
- Ember.run.scheduleOnce('afterRender', () => {}); // 所有渲染任务完成后(DOM树更新后, 但最终插入之前)
- afterRender比run.next的优势: 元素呈现到屏幕之前执行, 防止渲染后闪烁灯问题. 而run.next依赖setTimeout, 具有不确定性.
-
Em.A([])
,Em.Object.create({})
创建的Array/Object 可以使用Ember的方法/属性 - Array 变量 要 pushObject 才会触发 propertyChange
- Ember.typeOf() // 变量类型
Examples
-
路由跳转: redirect
/categories
to/latest
api.modifyClass('route:discovery.categories', { redirect() { this.replaceWith('discovery.latest') } });
-
修改默认的首页
-
assets/javascripts/discourse/homepage-route-map.js.es6
export default function () { this.route('homepage', { path: '/home' }); }
-
assets/javascripts/discourse/routes/homepage.js.es6
export default Discourse.Route.extend({});
-
assets/javascripts/discourse/templates/homepage.hbs
<h1>Hi</h1>
-
assets/javascripts/initializers/discourse-test.js.es6
import { setDefaultHomepage } from "discourse/lib/utilities"; setDefaultHomepage('home');
-
(可选) app/controllers/discourse-test/homepage_controller.rb
class HomepageController < ApplicationController end
-
(可选) config/routes.rb
Discourse::Application.routes.append do get '/home' => 'homepage#index' # homepage_controller.rb end
-
Faqs
-
rm -rf ./tmp
# 如发现修改code不生效, 可尝试 删除缓存并重启 -
rm -rf ~/Library/Logs/DiagnosticReports/*
删除 crash log -
rm -rf ./log/development.log
删除 dev log -
scss 里的颜色 尽量使用
dark-light-diff
方法, 因为论坛有 light/dark 模式切换 -
隐藏开发环境的 mini-profiler(左上角浮动栏), 快捷键
alt+p
-
htmlSafe:
-
Components里: Ember.String.htmlSafe(),
${}
.htmlSafe(); (deprecated: new Handlebars.SafeString()) -
Widgets里插入HTML:
RawHtml({ html: `<span>${html}</span>` })
或h('i.iconfont', { innerHTML: '' })
(不支持 SafeString) -
不用htmlSafe, 在.hbs里, 可以使用
{{{variable}}}
三个括号显示HTML字符串 -
XSS: 用户输入的内容不使用htmlSafe; 需要拼接可以用
escapeExpression()
转义 (import { escapeExpression } from "discourse/lib/utilities";
)
-
Components里: Ember.String.htmlSafe(),
-
引用插件内的静态图片:
public/images/logo.png
=><img src="/plugins/DiscourseTest/images/logo.png">
-
按钮点击无效: 如果console没报错, 检查是否是Chrome插件影响的. 比如:
Enable Copy
-
Modal
-
bootbox.confirm
,bootbox.alert
,bootbox.prompt
,bootbox.dialog
,dialog.modal('hide')
-
import showModal from "discourse/lib/show-modal";
render template
-
-
icons
- Font Awesome 5 and SVG icons
-
自定义icon: (plugin support for custom icons)( https://github.com/discourse/discourse/commit/47cbfb1)
如果未生效, 尝试rm -rf ./tmp
再启动论坛 - api.replaceIcon('far-eye', 'ak-icon-eye'); 替换默认的icon
-
Badges
勋章设置
Show badge on the public badges page: 在 /badges 显示本勋章 -
设定全局变量给所有模板使用:
-
Discourse.Site.currentProp('appView', true);
hbs:{{#if site.appView}}
或:this.site.setProperties({ appView: true });
-
Discourse.SiteSettings.appView = true;
hbs:{{#if siteSettings.appView}}
-
-
console.log(Discourse.Site.current())
打印 this.site -
console.log(Discourse.SiteSettings)
打印 siteSettings - 暂不能用...object(扩展运算符)传递多个attrs, Splat redux with es6-ish syntax
-
坑的地方:
- discourse插件文档是论坛帖子, 查找/筛选很麻烦, 需结合Google/GitHub/论坛 综合筛选.
- discourse/ember都有很多过时的内容和示例.
- Ember的现成组件比较少, 而且有很多不太维护了, 可能需要自己写.
参考
- Beginner’s Guide to Creating Discourse Plugins
- Install Plugins
- Discourse后端接口文档 方便理解discourse的字段结构
- Developer’s guide to Discourse Themes
- How to create a Discourse plugin
-
plugins 参考:
- 自带的插件
- ruby 钩子
- js api
- discourse-plugin-template 路由和样式
- discourse-solved , 官方插件, 有Settings配置 (需要: Discourse 2.3.0.beta5 or above)
- discourse-events , 帖子设定时间范围
- Babble 在线聊天插件
- discourse-translator 翻译帖子
- discourse-adplugin 投放广告
- discourse-layouts 侧边栏中显示小部件或广告
以上所述就是小编给大家介绍的《discourse 插件开发》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Gradle插件开发系列之开发第一个gradle插件
- (是时候开发属于自己的插件了)数据校验插件开发指南
- IDEA 插件:多线程文件下载插件开发
- 从头开发一个Flutter插件(二)高德地图定位插件
- Gradle插件开发系列之gradle插件调试方法
- WordPress插件开发 -- 在插件使用数据库存储数据
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Hacking Growth
Sean Ellis、Morgan Brown / Crown Business / 2017-4-25 / USD 29.00
The definitive playbook by the pioneers of Growth Hacking, one of the hottest business methodologies in Silicon Valley and beyond. It seems hard to believe today, but there was a time when Airbnb w......一起来看看 《Hacking Growth》 这本书的介绍吧!