kooh-q-hook

いわゆる一つの技術メモ

SharePoint Framework と SharePoint 2013 クライアント開発でこれから意識したいこと

2017年の後半に、SharePoint Framework (SPFx) という新しい開発モデルが出ていました。
SharePoint Framework の概要 (SPFx) | Microsoft Docs

すごくざっくり言うと、SharePointでもモダンなWeb開発が始まる、というものです。
任意のJavaScriptフレームワーク利用やオープンソースの開発ツールのチェイン等を謳っています。

ただ、対象は SharePoint Online と オンプレミスの SharePoint 2016 Feature Pack 2 なので、
SharePoint 2013の環境等では適用されません。

適用対象外ではあるのですが、将来的なバージョンアップを考慮するならば、
オンプレミスの SharePoint 2013 クライアント開発でも意識したいことがあります。

それは、SharePoint JSOM の積極的な利用をやめることです。

クライアントからSharePointとの通信を行う場合、
現在は SharePoint REST API か sp-pnp-js ライブラリ(REST API ラッパー)の利用が推奨されています。

JavaScript オブジェクト モデル (JSOM) を使用して SharePoint に接続する | Microsoft Docs

GitHub - SharePoint/PnP-JS-Core: Repository for the PnP JavaScript Core component development together with community members

もちろん、JSOM のコードは自体は依然有効ではあるのですが、
非推奨・下位互換性のために残されているような API を使い続ける必要性は薄いでしょう。


――と自分で書いていながら、衝撃の事実に震えています。もっと早くその方向性を示して欲しかった。

JSOM は SSOM(サーバーサイドAPI)に精通していれば手軽に使える(似ている)ので、
ある程度 SharePoint開発に携わっていると、RESTよりもJSOMを選択してしまうことが多いのではと思います。

悲観していても仕方がないので、SharePoint REST API を積極的に利用し始めました。
sp-pnp-js ライブラリ を活用しても良いのですが、いったんは様子見です。
IE 11だと Promise や fetch の Polyfill が必須だとか、その他もろもろの理由で。

でも、REST をそのまま使うのも微妙なところです。

呼び出し毎に共通的な設定を都度行うくらいなら(ヘッダーとか)、
軽めにラップしたライブラリを使いたい。そんな理由で作りました。


sp-request.js
Provides method for executing SharePoint REST API (based on SP.RequestExecutor.js) · GitHub

jQuery.ajax に依存するのも XMLHttpRequest を直接使うのも避けたかったので、
クロスドメイン用ライブラリの SP.RequestExecutor.js を利用しています。

よろしければ自己責任でご利用ください。

SharePoint JSOM でコレクションに対して列挙操作を行う

SharePoint JSOM(JavaScript クライアント オブジェクト モデル)の基本操作の一つであり、
おそらくは最も頻繁に使用される処理ではないかと思います。

MSDN にもサンプルコードの例が載っています。
SharePoint 2013 の JavaScript ライブラリ コードを使用して基本的な操作を完了する

確かにこのサンプルコードは問題なく動作するのですが、
コレクション系のオブジェクトを列挙する部分はこのようになっています(イメージです)。

var collection; // 結果のコレクション

// 非同期で collection に結果をロード
someLoadProcess().then(function() {
  var enumerator = collection.get_Enumerator();
  var current;
  while (enumerator.moveNext()) {
    current = enumerator.get_current();
  }
});

別に Enumerator を内部的に実装しているのは良いのです。
ただ、頻繁に使う処理がこんなに面倒なのはちょっといただけません。
正直、Enumerator を直接操作するメリットを感じません。

と、疑問に思いながらも数年間、代替方法を見出せずにいたのですが、
最近になってやっと判明したのでメモしておきます。

var collection; // 結果のコレクション

// 非同期で collection に結果をロード
someLoadProcess().then(function() {
  collection.get_data().forEach(function(current) {
  });
});

結果のコレクションは、型が SP.XXXCollection という名前です。

先述のサンプルコードでは、SP.ListItemCollection が該当します。
SP.ListItemCollection object

SP.XXXCollection は、prototype に get_data メソッドが定義されている
SP.ClientObjectCollection を継承しています。data とは。

そして、この get_data メソッドは、Array<SP.XXX> を返します。
つまり結果のコレクションの要素が格納された配列が取得できるということです。

ちなみにこの処理は Enumerator から新たに配列を生成しているわけではなく、
結果のロードが完了した時点で各要素が設定されている(隠し)配列を返しているだけです。

SharePoint JSOM のコレクションは get_data すれば配列が取れる、ということだけ覚えておけばよさそうです。
MSDN等のAPIリファレンスには載っていない情報なので、自己責任でご利用ください。

CSSセレクタの優先順位

同一要素に対して複数のセレクタでstyle指定がされている時、
最終的にどのstyleが適用されているのか、といった話。

開発者ツールで、取り消し線が引かれているものと
そうでないものを日頃見てますけど、詳細はどうなっていたっけと。

私の場合、感覚的に長らく以下のようなイメージを持ってました。
ちょうどHTMLElementを検索する時と似たような感じで。

  1. ID (document.getElementById ?)
  2. クラス (document.getElementsByClassName ?)
  3. タグ (document.getElementsByTagName ?)
  4. その他属性 (document.querySelector ?, document.querySelctorAll ?)

でも、これ違うのですね。リファレンス見ましょう。
Selectors Level 4

優先度および評価順

  1. (A) ID
  2. (B) クラス、属性、疑似クラス
  3. (C) タグ、疑似要素

補足

A >> B >> C という越えられない壁があるので、より上位の方法で指定された場合はそちらが優先される。
(#id と .class ならば、#id の方が優先される)

優先度で差がつかない時は、より多くの指定がある方が優先される。
(.class と tag.class は共に A = 0, B = 1。ただし前者は C=0, 後者は C = 1 なので、後者が優先される)

所感

二番目の「より詳しく修飾した方が勝つ」というのが知れて良かったです。

一から作るならバッティングしないように設計すれば良いのですが、
そうもいかずスタイルを上書きするというシナリオは多くありますから。

それと、やはり !important 指定は安易に使ってはいけないと、改めて思い直しました。

セレクタによる指定 << インライン指定 << !important 指定 なので、
一度 !important が出てきてしまうと、対抗するにも !important が必要となってしまいます。

いたるところに「重要」「重要」とマーキングされてたら、
本当に重要なものが何かわからなくなってしまうイメージですね。


SharePoint カスタマイズにおけるCSSスタイル上書きを考える

せっかくなのでSharePoint カスタマイズに話を関連付けてみます。
というよりも、こちらが本題です。

SharePointで生成されるページ内容を見てみると、
ビルトインのCSSクラスやそれを指定したスタイルが溢れている事がわかります。

例えば、リストビューの一覧上のセル(td要素)には "ms-cellStyle" といったクラスが指定されていて、
そのクラスにはスタイルの設定(余白など)がされているわけです。

この設定を上書きする上で、どのような方法が取れるかを考えてみます。

CSSのみで対応する場合

/* 上書きする為にタグを含めて指定 */
td.ms-cellStyle { padding: 0; }

/*
または冗長的ながらクラスを属性として指定
.ms-cellStyle[class] { padding: 0; }
*/

/*
!important 指定は最終手段なので使用しない
.ms-cellStyle { padding: 0 !important; }
*/

JavaScriptで対応する場合

インラインで指定する

var cells = document.getElementsByClassName('ms-cellStyle');
var cellsCount = cells.length;
for (var i = 0; i < cellsCount; i++) { cells[i].style.padding = '0'; }

CSS+JavaScriptで対応する場合

(1) 独自クラスを追加し、既存クラスと併せて指定する

/* ms-cellStyleの内容は変更せず、独自クラスでスタイルを定義する */
/* クラスの多重指定は単一指定より優先される */
.ms-cellStyle.my-cellStyle { padding: 0; }
var cells = document.getElementsByClassName('ms-cellStyle');
var cellsCount = cells.length;
for (var i = 0; i < cellsCount; i++) { cells[i].classList.add('my-cellStyle'); }

(2) カスタムデータ属性を追加し、既存クラスと併せて指定する

/* ms-cellStyleの内容は変更せず、独自属性を加えたセレクタでスタイルを定義する */
/* クラス+属性での指定はクラスの単一指定より優先される */
.ms-cellStyle[data-my] { padding: 0; }
var cells = document.getElementsByClassName('ms-cellStyle');
var cellsCount = cells.length;
for (var i = 0; i < cellsCount; i++) { cells[i].dataset.my = 'true';  }

(3) 独自クラスとカスタム属性を追加して、それを指定する

/* ms-cellStyleの内容は変更せず、独自クラス+カスタム属性でスタイルを定義する */
/* クラス+属性での指定はクラスの単一指定より優先される */
.my-cellStyle[data-my] { padding: 0; }
var cells = document.getElementsByClassName('ms-cellStyle');
var cellsCount = cells.length;
for (var i = 0; i < cellsCount; i++) {
  var cell = cells[i];
  cell.classList.add('my-cellStyle');
  cell.dataset.my = 'true';
}

汎用的に使えるのは、CSS+Javascript でしょうか。
(3) のレベルまで適用すれば、ビルトインのクラス名を指定することもなく、 独自のスタイルで上書きできるようになるのでお勧めです。

CSS で translate 対象とするth要素に border を設定する時の注意

table 要素のヘッダーを固定化する方法は色々ありますが、
th 要素に transform: translateY を動的に指定する方法がシンプルで使いやすいと思います。

Freezing of the SharePoint List/Library header · GitHub

この方法で固定化を実現した th 要素に border を設定する場合、
border-collapse に collapsed を指定すると、スクロール時に th の border は表示されません。

border は隣接セル同士で共有される(各セルでは保持しない)ため、
translateY で 各 th 要素の表示位置をずらしても追随しないというのが理由です。

html - combination of border-collapse:collapse and transform:translate - Stack Overflow

この条件に当てはまる場合には、border-collapse は separate とする必要があります。
つまり border も左端・右端・それ以外で、それぞれスタイルを指定します。

border-collapse の思わぬ落とし穴でした。

SharePoint REST APIとPowerShell

SharePointREST API は Accept ヘッダーに
"application/json;odata=verbose" を指定することで、
JSON 形式で結果を取得することができます(既定では Atom形式)。

取得はできるのですが、返却される プロパティに "ID" が含まれる場合、
奇妙なことに、そのJSON表現 には "id" と "Id" が出現します。

これは SharePoint 以外のWindows サーバーから、
SharePoint REST API を利用する時には注意が必要な点です。

PowerShell 3.0から導入された Invoke-RestMethod コマンドレットでは、
JSON内に同一のキーが含まれる場合、キー重複でエラーとなってしまうからです。
(ConvertFrom-Json コマンドレットも、このようなJSONをパースできません)

Atom形式で取得するか、
Invoke-RestMethod / ConvertFrom-Json を使用せずに何とかするか。

Invoke-WebRequest コマンドレット と
JavaScriptSerializer.DeserializeObject を併用するのが簡単でお勧めです。

jQuery UIのAutocompleteでIME変換中はサジェストしないようにする

日本語環境ならではの「あるある」のようです。

keydownとkeyupのkeyCodeを見て制御する方法を試すもうまく動かず。
compositionstart/compositionendイベントを見るのが良さげです。

jqueryUIのautocompleteを日本語確定後に実行させる方法 at softelメモ

ただ、compositionstartの時点でautocompleteをdisableするだけでは、
サジェストの候補は取得しにいってしまうという点。

「取得までは行うが、結果は表示しない」という挙動のようです。

  1. 日本語入力を開始 (確定しない)
  2. compositionstartイベントが発生、autocompleteをdisable
  3. autocompletesearch イベントが発生
  4. autocompleteのsource取得
  5. autocompleteresponse イベントが発生
  6. autocompleteopen イベントは発生しない (候補を表示しない)

候補リストのsourceがローカルデータであればこのままでも良いのですが、
リモートからajaxで取得する場合、余計な通信が発生してしまいます。
source取得の前にキャンセルしたいところです。

APIリファレンスによれば検索前というのは search イベントでフックできるので、
composition の状態を見て続行可否を判定すれば良さそうです。
Autocomplete Widget | jQuery UI API Documentation

$(function() {
  // IME入力中かどうか
  var composition = false;

  // Autocomplete の対象(仮)
  var $target = $('#target');

  $target
    .autocomplete({
      // IME入力中でなければ続行
      search: function(event, ui) {
        return !composition;
      },
      source: function(request, response) {
        // データ取得までに5秒かかる処理(ダミー)
        setTimeout(function() {
          response(['Ant', 'Beetle', 'Cicada'];
        }, 5000);
      }
    })
    // IME入力の開始を記憶
    .on('compositionstart', function(event) {
      composition = true;
      $target.autocomplete('disable');
    })
    // IME入力の終了を記憶
    .on('compositionend', function(event) {
      composition = false;
      $target.autocomplete('enable').autocomplete('search');
    });
});

求める挙動になりました。
UI上は分からないので開発者ツールでの確認が必要ですね。

Tx.Windows を使用して IIS W3C ログをCSVに変換してみる

IIS W3C ログをごにょごにょしたい時があります。ログ解析とか。

でも、Log Parserを使ってSQLのようなクエリは書きたくない。思いませんか。
C#LINQを使って何とかしたい。そんな時には、Tx の Tx.Windows を使いましょう。

GitHub - Microsoft/Tx: Tx (LINQ to Events)

適当ですがコードサンプルを。
args[0] には IIS ログのファイルパスが入っている前提です。
コンソール アプリケーション(.exe)にログファイルをドラッグ&ドロップするイメージ。

using CsvHelper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Tx.Windows;

namespace TxWindowsSample
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length > 0 && File.Exists(args[0]))
            {
                var records = W3CEnumerable.FromFile(args[0]).Select(x => new
                {
                    LocalDateTimeLocal = x.dateTime.ToLocalTime(),
                    ClientIPAddress = x.c_ip,
                    Method = x.cs_method,
                    Referrer = x.cs_Referer ?? "NULL",
                    URIQuery = x.cs_uri_query ?? "NULL",
                    URIStem = x.cs_uri_stem,
                    UserAgent = x.cs_User_Agent ?? "NULL",
                    UserName = x.cs_username ?? "NULL",
                    ServerIPAddress = x.s_ip,
                    ServerPort = x.s_port,
                    ProtocolStatus = x.sc_status,
                    ProtocolSubStatus = x.sc_substatus,
                    Win32Status = x.sc_win32_status,
                    TimeTaken = x.time_taken
                });

                using (var writer = new CsvWriter(new StreamWriter(Path.ChangeExtension(args[0], ".csv"))))
                {
                    writer.WriteRecords(records);
                }
            }
        }
    }
}

実際にログ解析をするなら、.log ファイルから毎度読み出すのは現実的ではないので、
データベースに移した方が良さそうです。ファイルサイズも塵も積もればとんでもないことですし。