CSS – SUSY3 – 安裝教學(1),透過 webpack 從頭開始建立架構

前言

Susy3 是一個大改版,隨著 CSS 前端的潮流趨勢,精簡了許多 Susy 2 的功能,也廢除了所有 mixin。在第三版的標語是

Your design, our math

Susy 3 推動的計畫是完全客製你自己的網格系統(柵欄系統),比起 Bootstrap 能做到更天馬行空的版面設計。整體而言只保留非常簡單的概念 span()、gutter()

// class names are for demonstration only…
.float {
  width: span(3);
  margin-right: gutter();
}

.flexbox {
  flex: 1 1 span(3);
  padding: 0 gutter() / 2;
}

.push-3 {
  margin-left: span(3 wide);
}

可以簡單製作尺寸的樣式

$large-screens: (
  'columns': susy-repeat(12, 4em),
  'gutters': 1em,
);

nav {
  @media (min-width: 40em) {
    width: span(3 wide, $large-screens);
  }
}

為了輕量化與快速編譯,其他如好用的斷點 mixin 設定,官方則建議若有需要請自行黏貼,好在這些擴充非常的簡單。

讓 Susy 負責做複雜的算數,其他呈現的設計例如排版使用 float 或是 flexbox, Susy 則不做預設限制,也不用再去記複雜的 mixin。官方文章提到,如果你已經開始使用 CSS Grid module 那麼就忘記使用 Susy 吧,但如果還不是,你則可以借助 Susy 來替你做複雜的柵欄溝槽運算。

 

安裝

以下使用 webpack 4.8.3 來安裝 SUSY,當然這部分的教學包含了實用的 webpack 架構的方法,例如避免重複生產程式碼、全域使用程式碼…….直接貼上程式碼是可以運作的。以下安裝 webpack, sass, susy3, compass。注意,你的專案名稱若在測試的時候不可以取名為 susy 喔,否則安裝 susy 的時候會被擋下來。

npm init -y
npm install --save-dev webpack webpack-cli sass-loader node-sass style-loader css-loader mixin-loader compass-mixins webpack-glob-folder-entries susy

若要透過 npm 安裝套件可以這樣寫,我在這篇範例中加入了這兩個 jQuery Plugin供後續示範

// 下載已經在 npm 註冊
npm install jquery vmodel.js

// 若要下載沒有註冊在 npm 的 github 檔案,"git+https://" 後方填入 github 網址
npm install git+https://github.com/.......js.git

這裡奇怪的是連續使用 npm install git+https 例如

npm install git+https://github.com/......js.git
npm install git+https://github.com/......js.git

npm 會把前一個 git 套件給刪除,所以請使用空白連接的寫法

npm install git+https://github.com/......js.git git+https://github.com/......js.git

或是寫在 package.json 中,然後使用 npm install 或 npm update 下載。

 

路徑架構

我的範例專案名稱設定為 susy3-demo,專案底下這麼設計,檔名路徑都先建立好

  • assets/ (( 整合資源的目錄
    • dist/ (( 原文是 distribution ,JS 會由 webpack 自動產出
      • xxxxx.js (( 自動產生的 JS
    • src/ ((原文是 source 代表我們手工編撰的程式碼
      • javascript/
        • md/ (( 範例:我用來放置 JS 模型的地方,每個檔案代表每個模型
          • global/ (( 範例:全域使用的共用 JS
            • menu.js (( 範例:共用的 JS
          • form.js (( 範例:單頁所需使用的 JS
        • index.js (( 範例:首頁的 JS 進入點,也包含 SCSS
        • content.js (( 範例:內頁的 JS 進入點,也包含 SCSS
      • scss/
        • global/ (( 會共用的 SCSS 都放在這裡
          • global.scss (( 編輯的 scss,也就是我們 susy 編寫的地方。完整部屬可參考這裡
        • config/ (( 設定檔都放這
          • _global.scss (( 全域引入,包含使用設定、載入需要的 mixin
          • _susy.scss (( 放置 susy map 的部分,包含斷點的設置都在這裡
        • mixin/ (( 日後擴增的 mixin 位置
          • susy/ (( 放置官方提供的片段
            • _gallery.scss (( 參考官方建議 如 susy2 的 gallery 一行解決相簿模式排列
            • _susy-breakpoint.scss (( 如 susy2 的斷點,讓元素在不同尺寸切換樣式
            • _with-layout.scss (( 給斷點使用 layout
            • _span.scss (( 使用 float 快速排版,當然你也可以把 float 換成 display: flexbox, inlin-block 或其他方法
        • index.scss
        • content.scss
  • node_modules/ (( 開發時會用到的 node.js 相依開發模組,正式發佈不需要上傳
  • package-lock.json
  • package.json (( node.js 設定與相依套件設定
  • webpack.config.js (( webpack 設定檔
  • index.php (( 範例:首頁,當然也以用 HTML 檔
  • content.php (( 範例:內容頁,當然也以用 HTML 檔

 

設定

在 npm 自動產生的 package.json 添加 build
{
  "name": "susy3-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "jquery": "^3.3.1",
    "vmodel.js": "^1.7.5"
  },
  "devDependencies": {
    "compass-mixins": "^0.12.10",
    "css-loader": "^0.28.11",
    "mixin-loader": "^2.0.3",
    "node-sass": "^4.9.0",
    "sass-loader": "^7.0.3",
    "style-loader": "^0.21.0",
    "susy": "^3.0.5",
    "webpack": "^4.14.0",
    "webpack-cli": "^3.0.8",
    "webpack-glob-folder-entries": "^1.0.0"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack" <------------- 添加
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

 

設定 webpack 輸入輸出

webpack.config.js,我們透過 splitChunks 把共用的部分分離至 vendors.js。分離出來我認為很重要,因為

  1. 開發上 compile 的時候不需要每次更新就要把第三方套件一併 compile ,或是寫 JS 卻還要編譯 沒更動到的 CSS ,可以省下許多等候時間。
  2. 讓 web 切換頁面的時候,僅下載該頁面獨用的 JS 檔,然後直接使用上一頁 cache 過的 JS 共用檔(例如第三方套件 jQuery, Lodash 或是我們自訂的全域 CSS ),節省流量開銷。

 

const webpack = require('webpack'); 
const path = require('path');
 
module.exports = {
    // 監聽
    watch: true,
 
    // development (開發模式未壓縮) | production (產品模式可壓縮)
    mode: 'production',
    
    // 進入點,每個頁面使用一個 JS 檔
    entry: {
        index: './assets/src/javascript/index.js',
        content: './assets/src/javascript/content.js'
    },
    
    // 自動輸出位置,我們對應到 ./assets/dist/[檔名].js
    output: {
        path: path.resolve(__dirname, 'assets/dist'), // JS 輸出點路徑
        filename: '[name].js'
    },
 
    // 優化設置
    optimization: {
 
        // 分離區塊
        splitChunks: {
            chunks: 'initial',
            cacheGroups: {
 
                // 將 import 路徑出現 "\node_modules\" 或 "\md\global\" 或 "\scss\global\" 底下的共用程式碼
                // 分離到 vendors.js
                vendors: {
                    test: /[\\/]node_modules[\\/]|[\\/]md[\\/]global[\\/]|[\\/]scss[\\/]global[\\/]/,
                    name: "vendors",
                }
            }
        }
    },
    
    // 讓 jQuery 可以在不同的文件使用
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery'
        }),
    ],
    
    // SUSY SASS 相關設置
    module: {
        rules: [{
            test: /\.scss$/,
            use: [{
                loader: "style-loader" // creates style nodes from JS strings
            }, {
                loader: "css-loader" // translates CSS into CommonJS
            }, {
                loader: "sass-loader", // compiles Sass to CSS
                options: {
                    includePaths: ["./assets/src/scss"] // 指定讀取的位置
                }
            }]
        }]
    }
};

 

SCSS 設定檔

assets/src/scss/config/_global.scss

// 使用 susy, 參考 http://oddbird.net/susy/docs/
@import "~susy/sass/susy";

// 使用 susy plugin, 參考 http://oddbird.net/susy/docs/plugin_svg-grid.html
@import "~susy/sass/plugins/svg-grid";

// 使用 compass 
@import "~compass-mixins/lib/compass";

// 載入 設定檔
@import "./config/susy";

// 載入 mixin
@import "../mixin/susy/with-layout";
@import "../mixin/susy/susy-breakpoint";
@import "../mixin/susy/gallery";
@import "../mixin/susy/span";

 

SUSY MAP 設定檔

assets/src/scss/config/_susy.scss,不同的 map 定義都放在這裡,日後調整斷點或整體外框也能方便許多。

$susy: (
  'columns': susy-repeat(4),
  'gutters': 0.25,
  'spread': 'narrow',
  'container-spread': 'narrow',
);

$mobile: (
  'min-width': 0px,
  'columns': susy-repeat(4),
  'gutters': 0.25,
  'spread': 'narrow',
  'container-spread': 'narrow',
);
 
$pad: (
  'min-width': 480px,
  'columns': susy-repeat(8),
  'gutters': 0.25,
  'spread': 'narrow',
  'container-spread': 'narrow',
);
 
$desktop: (
  'min-width': 800px,
  'columns': susy-repeat(12),
  'gutters': 0.25,
  'spread': 'narrow',
  'container-spread': 'narrow',
);

 

依照你的喜好添加 Mixin

相關說明已在上面說過囉

assets/src/scss/mixin/susy/_gallery.scss

// https://github.com/oddbird/susy/issues/648
@mixin gallery(
    $span,
    $config: ()
) {
    $grid: susy-compile($span, $config);
    $span: map-get($grid, 'span');
    $column-count: length(map-get($grid, 'columns'));
    $count: floor($column-count / $span);
    $spread: map-get($grid, 'spread') + 2;
    $container-spread: map-get($grid, 'container-spread') + 2;
    $extra: ($container-spread - $spread) * 0.5;
    $extra-push: su-call('su-gutter', $grid) * $extra;
    
    float: left;
    margin-right: -100%;

    @for $n from 1 through ($count) {
        $nth: unquote('#{$count}n + #{$n}');
        $location: $span * ($n - 1) + 1;    
        
        &:nth-child(#{$nth}) {
            $width: susy-compile($span at $location, $grid);
            width: su-call('su-span', $width);

            @if ($location > 1) {
                $wide: susy-compile('first' $location - 1 'wide', $grid);
                clear: none;
                margin-left: su-call('su-span', $wide) + $extra-push;
            } @else {
                clear: both;
                margin-left: if($extra-push > 0, $extra-push, 0);
            }
        }
    }
}

 

assets/src/scss/mixin/susy/_susy-breakpoint.scss

@mixin susy-breakpoint(
    $config
) {
    //  parse and normalize any shorthand arguments
    $config: susy-compile($config);

    // build min-and-max queries
    $min: map-get($config, 'min-width');
    $min: if($min, '(min-width: #{$min})', null);
    $max: map-get($config, 'max-width');
    $max: if($max, '(max-width: #{$max})', null);

    // combine them if we need both
    $and: if($min and $max, '#{$min} and #{$max}', null);
    // or fall back to the value we need…
    $query: $and or $min or $max;

    // apply the results…
    @media #{$query} {
        @include with-layout($config) {
            @content;
        }
    }
}

 

assets/src/scss/mixin/susy/_with-layout.scss

@mixin with-layout(
    $config
) {
    //  parse and normalize any shorthand arguments
    $config: susy-compile($config);

    // record the global settings -
    // and update the global variable with our new settings
    $global: $susy;
    $susy: map-merge($susy, $config) !global;

    // any content inside this mixin
    // will use the local settings
    @content;

    // return the global variable to its initial value
    $susy: $global !global;
}

 

assets/src/scss/mixin/susy/_span.scss

@mixin span(
  $span,
  $config: $susy
) {
  width: span($span, $config);
 
  @if index($span, 'last') {
    float: right;
  } @else {
    float: left;
    margin-right: gutter();
  }
}

 

SUSY 樣式檔

assets/src/scss/global/global.scss

@import "../config/global";

// 使用 compass 提供的重設所有瀏覽器預設屬性
@import "~compass-mixins/lib/compass/reset";

* {
    box-sizing: border-box;
}
html, body {
    height: 100%;
}

.container {
    // 寫法可參考範例 http://oddbird.net/2017/06/13/susy-spread/
    @include susy-breakpoint($mobile) {
        background: svg-grid() no-repeat scroll;
        height: 100%;
        margin: 0 gutter();
    }
    @include susy-breakpoint($pad) {
        background: svg-grid() no-repeat scroll;
        margin: 0 gutter();
    }
    @include susy-breakpoint($desktop) {
        background: svg-grid() no-repeat scroll;
        margin: 0 gutter();
    }
}

 

index.scss

@import "config/global";

.message {
    background: #d23f3f;
    color: white;
    height: 100px;
    font-size: 18px;
    line-height: 100px;
    text-align: center;
}

 

content.scss

@import "config/global";

.message {
    background: #5186D9;
    color: white;
    height: 100px;
    font-size: 18px;
    line-height: 100px;
    text-align: center;
}

 

JavaScript 載入樣式檔

提供給首頁使用的引入 SASS(SCSS) 與添加 jQuery 、jQuery 外掛,我們檢視能否正確執行。

assets/src/javascript/index.js

// 載入 SCSS
import '../scss/global/global.scss';
import '../scss/index.scss';

// 載入 jQuery Plugin
import 'vmodel.js';

// 載入會使用到的 JS 程式碼
import './md/global/menu.js';
import './md/form.js';

// 若要全域使用加入這塊
window.$ = $
window.jQuery = $

$(function (){
    console.log('來自 index.js')
    
    $.vmodel.get("menu", true);
    $.vmodel.get("form", true);
})

其中看到 window.$ 這是讓 jQuery 在任何地方都可以被使用,若不加的話那麼只能在這份 JS 檔使用 $。

 

assets/src/javascript/content.js

// 載入 SCSS
import '../scss/global/global.scss';
import '../scss/content.scss';

// 載入 jQuery Plugin
import 'vmodel.js';

// 載入會使用到的 JS 程式碼
import './md/global/menu.js';

// 若要全域使用加入這塊
window.$ = $
window.jQuery = $

$(function (){
    console.log('來自 content.js')
    
    $.vmodel.get("menu", true);
})

 

接著添加 vmodel.js 模組範例

assets/src/javascript/md/global/menu.js

$(function (){
    $.vmodel.create({
        selector: 'body',
        model: '--menu',
        isautoload: false,
        method: function (){
            var vs = this;
            this.autoload = ['init'];
            this.init = function (){
                console.log('初始化 menu,所有頁面都會用到')
            }
        }
    });
})

 

assets/src/javascript/md/form.js

$(function (){
    $.vmodel.create({
        selector: 'body',
        model: '--form',
        isautoload: false,
        method: function (){
            var vs = this;
            this.autoload = ['init'];
            this.init = function (){
                console.log('初始化 form,只有單頁用到')
            }
            this.say = function (){
                console.log('來自 form::say()')
            }
        }
    });
})

 

執行

npm run build

 

在首頁呈現

載入兩份 JS,包含共用程式碼與該頁獨用的程式碼。我在 <script> 中加入了 $ 呼叫 jQuery 是要測試是否全域可用,因為我們上面寫過 window.$ = $ 的關係。

index.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="assets/dist/vendors.js?t=<?=uniqid()?>"></script>
    <script src="assets/dist/index.js?t=<?=uniqid()?>"></script>
    <script>
        $(function (){
            console.log('全域使用 jQuery')
        })
    </script>
</head>
<body>
    <div class="container">
        <div class="message">
            首頁使用 index.js,<a href="content.php">前往內頁</a>
        </div>        
    </div>
</body>
</html>

 

content.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="assets/dist/vendors.js?t=<?=uniqid()?>"></script>
    <script src="assets/dist/content.js?t=<?=uniqid()?>"></script>
</head>
<body>
    <div class="container">
        <div class="message">
            Hi, 這是內頁,內頁使用 content.js
        </div>        
    </div>
</body>
</html>

 

打開瀏覽器以後,除了能看到 SUSY 樣是之外,還能檢視 console.log ,了解 JS 與 SCSS 被分割在哪些位置。

 

下一篇我們了解如何使用 Susy3

 

 

Comments

  1. Good, thanks for sharing!

發表迴響