かきスタンプ

福岡でフリーランスの物流系のエンジニアやってます。

【2019年版】Mac で IE11 を動かそうとしましたが、諦めました。【人柱】

ミッション

新機能の動作確認をしてみたところ、Windows10 + IE11 環境のみ、エラーが発生して登録できなかった。
このままではリリースができない! 何とかしてくれ !
 

動作環境の整備

<環境>
macOS High Sierra
 
原因がさっぱり分からないので、トライアンドエラーを繰り返すことになりそう。
修正してステージング環境にアップ作業を繰り返すのは手間なんで、ローカルで IE11 が動かせないを試す。
 

IE エミュレート

Chrome Developer Tools の IEエミュレート機能を使用。
 

手順

Chrome Developer Tools → More tools → Network conditions → User agent にて、「Select automatically」のチェックを外し、IE11 を選択。 01_Chrome_conf    
yourbrowser.is にて、IE11で動いている事を確認。  
が、検証機で起こった現象は再現せず。
調べてみると、この方法はレンダリングが絡む部分は対応できてないらしく、確認できる事は限られているみたい。
 
別の方法で。  

Wine(エミュレータツール)を使う

こちらのサイトを参考にさせて頂きました。
Use IE on OSX with Wine

参考サイトでは IE8 を起動していますが、使いたいのは IE11だったので以下のように変更。

brew install wine
brew install winetricks
winetricks ie11

wine 'C:\Program Files\Internet Explorer\iexplore'
起動画面

02_wine
起動はできたものの、重すぎて使い物にならず。
あと、何かするごとにすぐにエラーが出る。実用には耐えられないす。

Winebottlerを使う

こちらのサイトを参考にさせて頂きました。
http://adragoona.hatenablog.com/entry/2015/07/11/170822
 
Wineをラップしたツールみたいです。  
ツールのダウンロードページは、
http://winebottler.kronenberg.org/
なのですが、もんの凄く分かりづらい。
「ウィルスをダウンロードさせるサイトか?」と思えるレベルでトラップが存在する。
 
そして IE のバージョンは 8 しか選択できず。
結果は・・・うん、まぁこんな感じ。
 
03_Winebottler  
固まって動かなくなっているので、強制終了してやって下さい。
 

Easy Wine を使う

こちらのサイトを参考にさせて頂きました。
https://aprico-media.com/posts/1397
 
exe を macOS で動作させるツールみたいです。
 
Microsoftのサイトより、IE11をダウンロードして実行。
結果は、ご覧の有様だ。
04_EasyWine  
IEはどうやら起動時にOSのチェックをしているみたいで、それに引っかかってるっぽい。
 

仮装マシンを使う

Microsoftのサイトより、マシンイメージが配布されています。
https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/  
OS は Windows 7, 8, 10 から選べるのですが、「IE11 + Windows 10」の組み合わせは無し。
何故だ!?
 
仕方なしに 「IE11 + Windows 8」の仮想マシンをダウンロード。(7.62GB)
 
長い長い DL が終わり、ようやく動かすものの、検証機で発生していた現象が再現されず。
 
何故だーーっ!?
仮装マシンを使ってるせいなのか、Windows 8 と 10 の違いなのか、その辺は不明。
 

結論

素直に Windows マシンを用意しましょう。

頑張りましたが、幸せになれませんでした。
 

Netlify:JAMstack templates を使った、お手軽サイト作成

静的サイトのホスティング先として、かなりメジャーな感じになってきた Netlify

GitHub や Bitbuket のリポジトリを指定するだけでデプロイできてしまう手軽さが魅力ですが、リポジトリが無くても、テンプレートを指定するだけで静的サイトを作れたりします。

テンプレートには、Gatsby を使った爆速テンプレートもあったりします。

作り方

以下、Github アカウントは作成済みで、Netlify アカウントと連携している事を前提としています。

1.

JAMstack project templates にアクセスします。
f:id:kakisoft:20190512221221p:plain

2.

テンプレートを選択します。
f:id:kakisoft:20190512221435p:plain

今回は、「Gatsby + Netlify CMS Starter」を選択しています。
『Deploy to netlify』ボタンを押します。

3.

『Connect to Github』ボタンを押します。
f:id:kakisoft:20190512220850p:plain

4.

「Repository name」をデフォルトから変えたければ変更し、
『Save & Deploy』をボタンを押します。
 
f:id:kakisoft:20190512220912p:plain

5.

Site deploy in progress ...
の状態からしばらく待つと、デプロイが完了します。
URLをクリックすると、デプロイされたページに遷移します。
 
f:id:kakisoft:20190512220941p:plain
今回選択したテーマのデモページ(本家)は、こんな感じです。
 
f:id:kakisoft:20190512221008p:plain

6.

GitHub に、新しいリポジトリが自動で作成されています。
masterブランチを push すると、自動でデプロイされます。  
使い方はテンプレートごとに異なるので、各種 Readme をご参照下さい。  
 
自分の GitHubアカウントに自動生成されたリポジトリが、以下のような状態となっていた場合、画面下の方にある「Import Code」ボタンを押すと、ソースがコピーされます。 f:id:kakisoft:20190602111047p:plain (中略) f:id:kakisoft:20190602110906p:plain  
 

実際に作ってみたもの

Gatsby developer blog というテンプレートを使って、サイトを作成してみました。
 
福岡市周辺の、公園などの子供が遊ぶ場所の情報をかいてます。
kaki-playground.com

お手軽に使う PHP

『お手軽に使う PHP』というスライドを作成してみました。
 
https://gitpitch.com/kakisoft/UsePHPLightly    
f:id:kakisoft:20190428192416p:plain  
 
PHPは他の軽量言語と比較し、「軽く触ってみる」というケースが少ないんじゃないかと思い、スライドにしてみました。
 
Fukuoka.php Vol.29』 のLT資料として使用しました。

Node.js :csvファイルから jsonファイルへの変換は、convert-csv-to-json がいい感じ。

Node.js は標準で csvを扱うライブラリを持ってないんで、npm で引っ張ってこないといけないみたい。
という訳で、以下を試してみました。

ダウンロード数はそれほど多くないけど、ファイル変換に使うなら、convert-csv-to-json がいい感じ。

<インストール>

npm i convert-csv-to-json

csvファイル → jsonファイルへの変換が、わずか5行。

<変換プログラム例>

let csvToJson = require('convert-csv-to-json');
 
let fileInputName = 'translate_01.csv'; 
let fileOutputName = 'translate_11.json';
 
csvToJson.fieldDelimiter(',');
csvToJson.generateJsonFileFromCsv(fileInputName,fileOutputName);

デリミタ(区切り文字)のデフォルトが「;」となっています。
カンマ区切りの csv を対象とする場合、上記のように fieldDelimiter にて「,」を指定します。
 
詳細はnpmドキュメントをご参照ください。

Visual Studio 2019 Launch Event in Fukuoka振り返り:ハンズオンの Visual Studio 2019 使用バージョン

こちらのイベントに参加させて頂きました。
fukuten.connpass.com

Visual Studio をはじめとした Microsoft の最新情報を届けてくれると同時に、ハンズオンまであるという濃いイベント。
 
ハンズオンにて紹介している資料では、Azure Cloud Shell を使用していますが、せっかくなので、Visual Studio 2019 でやってみました。
 
なお、ハンズオン資料は以下です。
Visual Studio 2019 Launch Event in Fukuoka

ASP.NET Core を使用して Web API を構築する - Learn | Microsoft Docs

1.新しいプロジェクトの作成

f:id:kakisoft:20190416015035p:plain

2.ASP.NET Core Webアプリケーション

f:id:kakisoft:20190416015046p:plain

3.プロジェクト名を入力

プロジェクト名は、チュートリアル同様「RetailApi」としました。
f:id:kakisoft:20190416015058p:plain

4.「API」を選択

f:id:kakisoft:20190416015112p:plain

5.実行

▶ボタンを押すと、Webサーバが起動します。
f:id:kakisoft:20190416015124p:plain

f:id:kakisoft:20190416015137p:plain

6.フォルダを追加

一旦、Webサーバを停止させます。
フォルダを追加します。RetailApiの階層にて右クリックし、フォルダを追加。

f:id:kakisoft:20190416015155p:plain

7.

以下の3つのフォルダを作成します。

  • Controllers
  • Data
  • Models

f:id:kakisoft:20190416015215p:plain

8.ファイルを追加

ファイルを追加します。
追加する階層にて右クリックし、「新しい項目」を選択。

f:id:kakisoft:20190416015227p:plain

9.

コードファイルを選択し、作成。

f:id:kakisoft:20190416015237p:plain

10.

最終的にこんな感じになります。

f:id:kakisoft:20190416015247p:plain

追加・編集するコードは以下のようになります。

Controllers/ProductsController.cs

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RetailApi.Data;
using RetailApi.Models;

namespace RetailApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ProductsContext _context;

        public ProductsController(ProductsContext context)
        {
            _context = context;
        }

        [HttpGet]
        public ActionResult<List<Product>> GetAll() =>
            _context.Products.ToList();

        // GET by ID action
        [HttpGet("{id}")]
        public async Task<ActionResult<Product>> GetById(long id)
        {
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            return product;
        }

        // POST action
        [HttpPost]
        public async Task<ActionResult<Product>> Create(Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
        }

        // PUT action
        [HttpPut("{id}")]
        public async Task<IActionResult> Update(long id, Product product)
        {
            if (id != product.Id)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;
            await _context.SaveChangesAsync();

            return NoContent();
        }

        // DELETE action
        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(long id)
        {
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

    }
}

Controllers/ValuesController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace RetailApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Data/ProductsContext.cs

using Microsoft.EntityFrameworkCore;
using RetailApi.Models;

namespace RetailApi.Data
{
    public class ProductsContext : DbContext
    {
        public ProductsContext(DbContextOptions<ProductsContext> options)
            : base(options)
        {
        }

        public DbSet<Product> Products { get; set; }
    }
}

Data/SeedData.cs

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RetailApi.Models;

namespace RetailApi.Data
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new ProductsContext(serviceProvider
                .GetRequiredService<DbContextOptions<ProductsContext>>()))
            {
                if (!context.Products.Any())
                {
                    context.Products.AddRange(
                        new Product { Name = "Squeaky Bone", Price = 20.99m },
                        new Product { Name = "Knotted Rope", Price = 12.99m }
                    );

                    context.SaveChanges();
                }
            }
        }
    }
}   

Models/Product.cs

using System.ComponentModel.DataAnnotations;

namespace RetailApi.Models
{
    public class Product
    {
        public long Id { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        [Range(minimum: 0.01, maximum: (double)decimal.MaxValue)]
        public decimal Price { get; set; }
    }
}

Program.cs

using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RetailApi.Data;

namespace RetailApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();
            SeedDatabase(host);
            host.Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();

        private static void SeedDatabase(IWebHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ProductsContext>();
                    context.Database.EnsureCreated();
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "A database seeding error occurred.");
                }
            }
        }
    }
}

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore;
using RetailApi.Data;

namespace RetailApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ProductsContext>(options =>
                options.UseInMemoryDatabase("Products"));

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

ソースの詳細については、公式サイトをご参照ください。

docs.microsoft.com

Apache:mod_rewrite モジュールの、『RewriteCond %{HTTPS} off』がよく分からなかったから調べた。

.htaccess の記述設定で

RewriteCond %{HTTPS} off
RewriteCond %{HTTPS} !on

といった記述があった。

が、RewriteCond の構文は、

RewriteCond TestString CondPattern(正規表現) [flags]

となっている。 https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritecond

え? 「off」って正規表現じゃなくね? って思ってマニュアルをよく調べたら、こんな記述があった。

If the TestString has the special value expr, the CondPattern will be treated as an ap_expr. HTTP headers referenced in the expression will be added to the Vary header if the novary flag is not given.
 
(TestStringが特別な値だったら、CondPattern は、正規表現でなく ap_exprとして扱われます。 )

って事だった。

という訳で、参照するべきページはここ。 https://httpd.apache.org/docs/current/expr.html

HTTPS  -  "on" if the request uses https, "off" otherwise
(リクエストがhttpsを使用している場合は 'on'、それ以外の場合は 'off')

って事らしい。

なので、常時SSL対応として .htaccess(または httpd.conf)を編集する場合、

RewriteCond %{HTTPS} off

を決まり文句として考えて良さそう。

GitHub に csv・tsv のファイルをアップすると、いい感じに表現してくれる。

タイトル通りです。
特別な事は何もせず、csv や tsv をアップし、ブラウザ上でアップしたファイルを見ると、こんな感じに表現してくれます。

f:id:kakisoft:20190304211256p:plain

フィルタリング機能も付いてたりと、色々と気が利いています。
 
無料ダウンロードできるデータを適当に拾い、リポジトリに上げてみたので、見てみたい方はどうぞ。
csv
tsv
 
 
 

ただ、あんまり大きなデータは表現してくれないみたいです。

この機能についての、GitHubのリファレンスはこちらです。