Deferred Requirements support

This commit is contained in:
Tony Air 2018-02-22 20:39:58 +07:00
parent 50476d1002
commit 3571b920d0
11 changed files with 245 additions and 93 deletions

View File

@ -10,14 +10,22 @@ Checkout files at /site/ folder for details
+ WebpackTemplateProvider::WebpackJS('file-name') and WebpackTemplateProvider::WebpackCSS('file-name') can be used at php area + WebpackTemplateProvider::WebpackJS('file-name') and WebpackTemplateProvider::WebpackCSS('file-name') can be used at php area
+ All images will be optimised at /site/src/img and will be written to /site/dist/img (by default) + All images will be optimised at /site/src/img and will be written to /site/dist/img (by default)
+ Favicons will be generated at /site/dist/icons using /site/src/favicon.png + Favicons will be generated at /site/dist/icons using /site/src/favicon.png
+ Folder /site/src/js/types is used to create page specific JS (just create a JS file there and it will be compiled) + Folder /site/src/js/types is used to create page specific JS (just create JS file there and it will be compiled)
+ Folder /site/src/scss/types is used to create page specific CSS (just create SCSS file there and it will be compiled)
+ Automatic linting (JS+SCSS)
+ Bootstrap 4 included by default + Bootstrap 4 included by default
+ Font-Awesome included by default
+ Deferred requirements loading
+ Requirements auto-loading
### Folder structure: ### Folder structure:
+ /site/_config/webpack.yml (Webpack configurtion) + /site/_config/webpack.yml (Webpack configurtion)
+ /site/code/WebpackTemplateProvider.php (WebpackJS and WebpackCSS functionality) + /site/code/WebpackTemplateProvider.php (WebpackJS and WebpackCSS functionality)
+ /site/code/DeferedRequirements.php (Deferred Requirements + Requirements auto-loader)
+ /site/templates/Page.ss (An example Page.ss) + /site/templates/Page.ss (An example Page.ss)
+ /site/src (Your sources) + /site/src (Your sources)
+ /site/dist (Your production assets)
@ -28,18 +36,16 @@ Checkout files at /site/ folder for details
+ /site/src/js/main.js (Your custom site-wide functionality) + /site/src/js/main.js (Your custom site-wide functionality)
+ /site/src/js/_events.js (Your custom site-wide events) + /site/src/js/_events.js (Your custom site-wide events)
+ /site/src/js/_pageType_and_component_template.js (A template which can be used to create new modules) + /site/src/js/_pageType_and_component_template.js (A template which can be used to create new modules)
+ /site/src/types/*.js (Extra page-specific modules to be autocompiled. My suggestion is to use *ClassName*.js and then require it at SilverStripe custom controller area) + /site/src/types/*.js (Extra page-specific modules to be auto-compiled. My suggestion is to use *ClassName*.js and then require it at SilverStripe custom controller area)
+ /site/src/scss (Your styling to be compiled) + /site/src/scss (Your styling to be compiled)
+ /site/src/scss/_components (Your custom SCSS components) + /site/src/scss/_components (Your custom SCSS components)
+ /site/src/scss/app.scss (main application file to include sie-wide components) + /site/src/scss/app.scss (main application file to include site-wide components)
+ /site/src/scss/_variables.sccs (your custom variables, ex. bootstrap) + /site/src/scss/_variables.sccs (your custom variables, ex. bootstrap)
+ /site/src/scss/_layout.sccs (Your site-wide styling) + /site/src/scss/_layout.sccs (Your site-wide styling)
##### P.S to compile page specific styling add following line to /site/src/types/*PageClassName*.js
###### import "../scss/types/*PageClassName*.scss";
### Requirements: ### Requirements:
@ -51,14 +57,16 @@ Checkout files at /site/ folder for details
+ git clone https://github.com/a2nt/silverstripe-webpack.git + git clone https://github.com/a2nt/silverstripe-webpack.git
+ cd silverstripe-webpack + cd silverstripe-webpack
+ composer install + composer install
+ yarn install + npm install
+ edit robots.txt, humans.txt, cache.appcache, manifest.json and package.json to setup your own project + edit robots.txt, humans.txt, cache.appcache, manifest.json and package.json to setup your own project
### Commands: ### Commands:
+ yarn - to update packages + yarn - to update packages
+ yarn start - to start webpack webserver + yarn start - to start webpack development webserver
+ yarn build - to build assets# silverstripe-webpack + yarn build - to build production assets
+ yarn lint:check - to check SCSS and JS linting
+ yarn lint:fix - to fix SCSS and JS linting automatically
### TODO: ### TODO:

View File

@ -3,9 +3,9 @@
# Cuz WebPack compiling script use it to set configuration # Cuz WebPack compiling script use it to set configuration
WebpackTemplateProvider: WebpackTemplateProvider:
dist: site/dist SRC: site/src
hostname: localhost DIST: site/dist
port: "3000" HOSTNAME: localhost
pages: site/src/js/types PORT: "3000"
pagesscss: site/src/scss/types TYPESJS: site/src/js/types
src: site/src TYPESSCSS: site/src/scss/types

122
site/code/DeferedRequirements.php Executable file
View File

@ -0,0 +1,122 @@
<?php
class DeferedRequirements extends Object implements TemplateGlobalProvider {
private static $css = [];
private static $js = [];
private static $defered = false;
private static $static_domain;
private static $version;
/**
* @return array
*/
public static function get_template_global_variables()
{
return [
'AutoRequirements' => 'Auto',
'DeferedCSS' => 'loadCSS',
'DeferedJS' => 'loadJS',
];
}
public static function Auto($class = false)
{
// Initialization
Requirements::block(THIRDPARTY_DIR.'/jquery/jquery.js');
if (defined('FONT_AWESOME_DIR')) {
Requirements::block(FONT_AWESOME_DIR.'/css/lib/font-awesome.min.css');
}
Requirements::set_force_js_to_bottom(true);
// Main libs
DeferedRequirements::loadJS('//ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js');
// App libs
DeferedRequirements::loadJS(project().'/dist/js/app.js');
DeferedRequirements::loadCSS(project().'/dist/css/app.css');
// Class libs
if($class) {
DeferedRequirements::loadJS(project() . '/dist/js/' . $class . '.js');
DeferedRequirements::loadCSS(project() . '/dist/css/' . $class . '.css');
}
return self::forTemplate();
}
public static function loadCSS($css)
{
if (self::$defered && !self::_webpackActive()) {
self::$css[] = $css;
} else {
WebpackTemplateProvider::loadCSS($css);
}
}
public static function loadJS($js)
{
if (self::$defered && !self::_webpackActive()) {
self::$js[] = $js;
} else {
WebpackTemplateProvider::loadJS($js);
}
}
protected static function _webpackActive()
{
return class_exists('WebpackTemplateProvider') && WebpackTemplateProvider::isActive();
}
public static function setDefered($bool)
{
self::$defered = $bool;
}
public static function forTemplate()
{
if (!self::$defered || self::_webpackActive()) {
return false;
}
$result = '';
foreach (self::$css as $css) {
$result .= '<i class="defer-cs" data-load="' . self::get_url($css) . '"></i>';
}
foreach (self::$js as $js) {
$result .= '<i class="defer-sc" data-load="' . self::get_url($js) . '"></i>';
}
$result .=
'<script type="text/javascript">function lsc(a,b){var c=document.createElement("script");c.type="text/javascript",c.readyState'
.'?c.onreadystatechange=function(){"loaded"!=c.readyState&&"complete"!=c.readyState||(c.onreadystatechange=null,b())}'
.':c.onload=function(){b()},c.src=a,document.getElementsByTagName("body")[0].appendChild(c)}'
.'function lscd(a){a<s.length-1&&(a++,lsc(s.item(a).getAttribute("data-load"),function(){lscd(a)}))}'
.'for(var s=document.getElementsByClassName("defer-cs"),i=0;i<s.length;i++){var b=document.createElement("link");b.rel="stylesheet",'
.'b.type="text/css",b.href=s.item(i).getAttribute("data-load"),b.media="all";var c=document.getElementsByTagName("body")[0];'
.'c.appendChild(b)}var s=document.getElementsByClassName("defer-sc"),i=0;lsc(s.item(i).getAttribute("data-load"),function(){lscd(i)});'
.'</script>';
return $result;
}
private static function get_url($url)
{
// external URL
if (strpos($url, '//') !== false) {
return $url;
}
$version = Config::inst()->get('DeferedRequirements', 'version');
$version = $version
? strpos($url, '?') // inner URL
? '&'.$version // add param
: '?'.$version // new param
: ''; // no version defined
$static_domain = Config::inst()->get('DeferedRequirements', 'static_domain');
$static_domain = $static_domain ? $static_domain : '';
return $url.$version;
}
}

View File

@ -6,18 +6,6 @@
class WebpackTemplateProvider extends Object implements TemplateGlobalProvider class WebpackTemplateProvider extends Object implements TemplateGlobalProvider
{ {
/**
* @return array
*/
public static function get_template_global_variables()
{
return [
'WebpackDevServer' => 'isActive',
'WebpackCSS' => 'loadCSS',
'WebpackJS' => 'loadJS',
];
}
/** /**
* @var int port number * @var int port number
*/ */
@ -31,7 +19,19 @@ class WebpackTemplateProvider extends Object implements TemplateGlobalProvider
/** /**
* @var string assets static files directory * @var string assets static files directory
*/ */
private static $distDir = 'site/dist'; private static $dist = 'site/dist';
/**
* @return array
*/
public static function get_template_global_variables()
{
return [
'WebpackDevServer' => 'isActive',
'WebpackCSS' => 'loadCSS',
'WebpackJS' => 'loadJS',
];
}
/** /**
* Load CSS file * Load CSS file
@ -39,9 +39,7 @@ class WebpackTemplateProvider extends Object implements TemplateGlobalProvider
*/ */
public static function loadCSS($path) public static function loadCSS($path)
{ {
if (!self::isActive()) { Requirements::css(self::_getPath($path));
Requirements::css(self::_toPublicPath($path));
}
} }
/** /**
@ -50,11 +48,7 @@ class WebpackTemplateProvider extends Object implements TemplateGlobalProvider
*/ */
public static function loadJS($path) public static function loadJS($path)
{ {
$path = self::isActive() ? Requirements::javascript(self::_getPath($path));
self::_toDevServerPath($path) :
self::_toPublicPath($path);
Requirements::javascript($path);
} }
@ -66,9 +60,16 @@ class WebpackTemplateProvider extends Object implements TemplateGlobalProvider
{ {
$class = __CLASS__; $class = __CLASS__;
return Director::isDev() && !!@fsockopen( return Director::isDev() && !!@fsockopen(
$class::config()->get('hostname'), $class::config()->get('HOSTNAME'),
$class::config()->get('port') $class::config()->get('PORT')
); );
}
protected static function _getPath($path)
{
return self::isActive() && strpos($path,'//') === false ?
self::_toDevServerPath($path) :
self::_toPublicPath($path);
} }
protected static function _toDevServerPath($path) protected static function _toDevServerPath($path)
@ -77,15 +78,21 @@ class WebpackTemplateProvider extends Object implements TemplateGlobalProvider
return sprintf( return sprintf(
'%s%s:%s/%s', '%s%s:%s/%s',
Director::protocol(), Director::protocol(),
$class::config()->get('hostname'), $class::config()->get('HOSTNAME'),
$class::config()->get('port'), $class::config()->get('PORT'),
$path basename($path)
); );
} }
protected static function _toPublicPath($path) protected static function _toPublicPath($path)
{ {
$class = __CLASS__; $class = __CLASS__;
return $class::config()->get('distDir') . '/' . $path; return strpos($path,'//') === false ?
Controller::join_links(
$class::config()->get('DIST'),
(strpos($path,'.css') ? 'css' : 'js' ),
$path
)
: $path;
} }
} }

View File

@ -18,9 +18,6 @@ import 'bootstrap/js/dist/tab';
// import your custom UI components // import your custom UI components
import './main'; import './main';
// bootstrap fix
window.Popper = Popper;
// import images // import images
function importAll(r) { function importAll(r) {
return r.keys().map(r); return r.keys().map(r);

View File

@ -0,0 +1 @@
<style>html,body{font-size:14px;margin:0;padding:0;background:#fff;color:#333}.bubblingG{width:78px;height:49px;margin:auto}#PageLoading{position:fixed;left:0;top:0;margin:0;width:100%!important;height:100%!important;background:rgba(255,255,255,.9);display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;z-index:9999}.bubblingG span{display:inline-block;vertical-align:middle;width:10px;height:10px;margin:24px auto;background:#000;border-radius:49px;animation:bubblingG 1.5s infinite alternate}#bubblingG_1{animation-delay:0s}#bubblingG_2{animation-delay:.45s}#bubblingG_3{animation-delay:.9s}@keyframes bubblingG{0%{width:10px;height:10px;background-color:#000;transform:translateY(0)}100%{width:23px;height:23px;background-color:#fff;transform:translateY(-20px)}}.main-bn{position:fixed;top:0;left:0;width:100%;z-index:9999;padding:.5rem 1rem;text-align:center;color:#fff;background:#FF0000}</style>

View File

@ -40,8 +40,19 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1" /> <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1" />
<% include Prestyling %>
</head> </head>
<body oncontextmenu="return false;"> <body oncontextmenu="return false;">
<%-- Upgrade your Browser notice --%>
<!--[if lt IE 10]><div class="main-bn"><a href="https://www.google.com/chrome/browser/desktop/" title="<%t Page.UPGRADEBROWSER 'Upgrade your browser' %>"><%t Page.OUTDATEDBROWSER 'You are using an outdated browser. For a faster, safer browsing experience, upgrade for free today.' %></a></div><![endif]-->
<%-- No JS enabled notice --%>
<noscript><div class="main-bn"><%t Page.JAVASCRIPTREQUIRED 'Please, enable javascript.' %></div></noscript>
<%-- Loading Spinner --%>
<div id="PageLoading"><div class="loading-spinner"><div class="bubblingG"><span id="bubblingG_1"></span><span id="bubblingG_2"></span><span id="bubblingG_3"></span></div><br/><%t Page.LOADINGTEXT 'LOADING ..' %></div></div>
<header> <header>
</header> </header>
@ -60,8 +71,7 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" crossorigin="anonymous"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" crossorigin="anonymous"></script>
$WebpackJS('app.js') <%-- Require CSS+JS from /site/dist/[js,css]/[ClassName].[js,css] --%>
$WebpackCSS('app.css') $AutoRequirements($ClassName).RAW
</body> </body>
</html> </html>

View File

@ -1,15 +1,19 @@
const path = require("path"); /*
* Common Environment
*/
const webpack = require("webpack"); const webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const conf = require("./webpack.configuration"); const conf = require("./webpack.configuration");
const isProduction = process.env.NODE_ENV === "production";
const path = require("path");
const includes = { const includes = {
app: path.join(conf.SRC, "js/app.js"), app: path.join(__dirname, conf.SRC, "js/app.js"),
}; };
const _getAllFilesFromFolder = function(dir) { const _getAllFilesFromFolder = function(dir) {
dir = path.join(__dirname, dir);
let filesystem = require("fs"); let filesystem = require("fs");
let results = []; let results = [];
@ -28,13 +32,13 @@ const _getAllFilesFromFolder = function(dir) {
}; };
// add page specific scripts // add page specific scripts
const pageScripts = _getAllFilesFromFolder(conf.PAGES); const pageScripts = _getAllFilesFromFolder(conf.TYPESJS);
pageScripts.forEach((file) => { pageScripts.forEach((file) => {
includes[path.basename(file, ".js")] = file; includes[path.basename(file, ".js")] = file;
}); });
// add page specific scss // add page specific scss
const scssIncludes = _getAllFilesFromFolder(conf.PAGESSCSS); const scssIncludes = _getAllFilesFromFolder(conf.TYPESSCSS);
scssIncludes.forEach((file) => { scssIncludes.forEach((file) => {
includes[path.basename(file, ".scss")] = file; includes[path.basename(file, ".scss")] = file;
}); });
@ -75,7 +79,9 @@ module.exports = {
test: /\.(png|jpg|gif|svg)$/, test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader", loader: "file-loader",
options: { options: {
name: "img/[name].[ext]", name: "[name].[ext]",
outputPath: 'img/',
publicPath: '/site/dist/img/'
} }
}, { }, {
test: /\.worker\.js$/, test: /\.worker\.js$/,

View File

@ -1,3 +1,8 @@
/*
* Development assets generation
*/
const path = require("path");
const autoprefixer = require('autoprefixer'); const autoprefixer = require('autoprefixer');
const webpack = require('webpack'); const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
@ -16,14 +21,14 @@ const config = merge.strategy({
'react-hot-loader/patch', 'react-hot-loader/patch',
'webpack-dev-server/client?https://' + conf.HOSTNAME + ':' + conf.PORT + '/', 'webpack-dev-server/client?https://' + conf.HOSTNAME + ':' + conf.PORT + '/',
'webpack/hot/only-dev-server', 'webpack/hot/only-dev-server',
], ]
}, },
output: { output: {
path: conf.BUILD, path: path.join(__dirname, conf.DIST),
filename: '[name].js', filename: '[name].js',
// necessary for HMR to know where to load the hot update chunks // necessary for HMR to know where to load the hot update chunks
publicPath: 'http://' + conf.HOSTNAME + ':' + conf.PORT + '/' publicPath: 'https://' + conf.HOSTNAME + ':' + conf.PORT + '/'
}, },
module: { module: {
@ -56,9 +61,7 @@ const config = merge.strategy({
'Chrome >= 44', // Retail 'Chrome >= 44', // Retail
'Samsung >= 4' 'Samsung >= 4'
] ]
}), })
// http://lostgrid.org/docs.html
require('lost')
] ]
} }
}, { }, {
@ -72,7 +75,7 @@ const config = merge.strategy({
use: [{ use: [{
loader: 'url-loader' loader: 'url-loader'
}] }]
}, ] }]
}, },
plugins: [ plugins: [
new webpack.NamedModulesPlugin(), new webpack.NamedModulesPlugin(),
@ -84,7 +87,9 @@ const config = merge.strategy({
host: IP, host: IP,
port: PORT, port: PORT,
historyApiFallback: true, historyApiFallback: true,
hot: true, hot: false,
clientLogLevel: "info",
//watchContentBase: true,
overlay: { overlay: {
warnings: true, warnings: true,
errors: true errors: true

View File

@ -1,22 +1,25 @@
const path = require('path'); /*
const autoprefixer = require('autoprefixer'); * Production assets generation
*/
const webpack = require('webpack'); const webpack = require('webpack');
const conf = require('./webpack.configuration');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const common = require('./webpack.config.common.js'); const common = require('./webpack.config.common.js');
const OptimizeCSSAssets = require('optimize-css-assets-webpack-plugin');
const conf = require('./webpack.configuration'); const path = require('path');
const autoprefixer = require('autoprefixer');
const ExtractTextPlugin = require("extract-text-webpack-plugin"); const ExtractTextPlugin = require("extract-text-webpack-plugin");
const OptimizeCSSAssets = require('optimize-css-assets-webpack-plugin');
const FaviconsWebpackPlugin = require("favicons-webpack-plugin"); const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const fs = require("fs");
const yaml = require("js-yaml");
const confYML = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "site/_config/webpack.yml"), "utf8"));
module.exports = merge(common, { module.exports = merge(common, {
output: { output: {
path: conf.BUILD, path: path.join(__dirname, conf.DIST),
filename: '[name].js', filename: 'js/[name].js',
publicPath: confYML.WebpackTemplateProvider.dist + '/', publicPath: conf.DIST + '/',
}, },
module: { module: {
@ -46,9 +49,7 @@ module.exports = merge(common, {
'Chrome >= 44', // Retail 'Chrome >= 44', // Retail
'Samsung >= 4' 'Samsung >= 4'
] ]
}), })
// http://lostgrid.org/docs.html
require('lost')
] ]
} }
}, { }, {
@ -66,35 +67,33 @@ module.exports = merge(common, {
loader: 'file-loader', loader: 'file-loader',
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'fonts/', // where the fonts will go outputPath: 'fonts/',
publicPath: './' // override the default path publicPath: '../fonts/'
} }
}] }]
} ] } ]
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
'NODE_ENV': JSON.stringify('production') 'NODE_ENV': JSON.stringify('production')
} }
}), }),
new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
sourceMap: true, sourceMap: true,
comments: false comments: false
}), }),
new ExtractTextPlugin({ new ExtractTextPlugin({
filename: '[name].css', filename: 'css/[name].css',
allChunks: true allChunks: true
}), }),
new OptimizeCSSAssets(), new OptimizeCSSAssets(),
new FaviconsWebpackPlugin({ new FaviconsWebpackPlugin({
logo: conf.SRC + '/favicon.png', logo: path.join(__dirname, conf.SRC) + '/favicon.png',
prefix: '/icons/', prefix: '/icons/',
statsFilename: confYML.WebpackTemplateProvider.dist + '/icons/iconstats.json', statsFilename: conf.DIST + '/icons/iconstats.json',
icons: { icons: {
android: true, android: true,
appleIcon: true, appleIcon: true,

View File

@ -1,13 +1,10 @@
/*
* Load webpack configuration from site/_config/webpack.yml
*/
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const yaml = require("js-yaml"); const yaml = require("js-yaml");
const conf = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "site/_config/webpack.yml"), "utf8")); const conf = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "site/_config/webpack.yml"), "utf8"));
module.exports = { module.exports = conf.WebpackTemplateProvider;
SRC: path.join(__dirname, conf.WebpackTemplateProvider.src),
BUILD: path.join(__dirname, conf.WebpackTemplateProvider.dist),
PAGES: path.join(__dirname, conf.WebpackTemplateProvider.pages),
PAGESSCSS: path.join(__dirname, conf.WebpackTemplateProvider.pagesscss),
HOSTNAME: conf.WebpackTemplateProvider.hostname,
PORT: conf.WebpackTemplateProvider.port
};