$30 off During Our Annual Pro Sale. View Details »

Experience to migrate JS modules to different bundler system

Sean Hsieh
December 18, 2019

Experience to migrate JS modules to different bundler system

The topic for F2E & FGBA Meetup December 2019

Sean Hsieh

December 18, 2019
Tweet

More Decks by Sean Hsieh

Other Decks in Technology

Transcript

  1. 1 Experience to migrate JS modules to different bundler system

    F2E & FGBA Meetup December 2019 謝任軒, Sean Hsieh <of_alpha@hotmail.com> Software engineer @ Garmin International
  2. 2 Agenda • Self-introduction • Dilemma • What exactly do

    we have to do? • Building artifact • Recap
  3. 3 Self-introduction

  4. 4

  5. 5

  6. 6 Dilemma

  7. Maybe we can do something like… AMD module AMD module

    AMD module AMD module moduleA.js moduleB.js moduleC.js moduleD.js layout.cshtml Index.cshtml Compile AMD module {Common.js moduleA.js moduleB.js} {Map.js moduleC.js moduleD.js} Non-AMD module Non-AMD module Common.js Map.js Boss: How to improve the module system? Require.js define/ require (AMD) BundleConfig.cs Embedded module 7 Requirejs config 1. To reduce the bundle size (We need module dependency management) 2. To reduce the number of requests from the client (We need common dependencies extracting) 3. To improve the development environment (We need plugin supports) It’s a 10-year ASP.NET MVC project … Well.. that’s change the module bundler
  8. The choices of the module bundler 8 .js .js .js

    .js .js .js .js • require('modules') in the browser by bundling up all of your dependencies. • Github stars: 13 k • Github forks: 1.1 k • Companies: • A bundler for javascript and friends. Packs many modules into a few bundled assets • Github stars: 52.1 k • Github forks: 6.6 k • Companies: Browserify Webpack Gulp Gulp Npm scripts
  9. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Boss: what’s the efforts for migrating to Webpack? Webpack.js entry.bundle.js Embedded module 9 Well.. What do I exactly have to do in this case? The efforts depend on the previous JS modules, previous module loader’s configuration, and the previous bundler system Shimming
  10. 10 What exactly do I have to do?

  11. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Phases Webpack.js Shimming entry.bundle.js Embedded module 11 AMD module Phase 1. Make the embedded modules stand-alone Phase 2. Move the module loader’s configurations to webpack.config.js Phase 3. Write the entry bundle according to the contents of the page Phase 4. Generate Webpack bundles with the task runner Phase 5. Replace bundles with Webpack bundles • Transition period • Ready to apply Webpack
  12. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Phases 1 Webpack.js Shimming entry.bundle.js Embedded module 12 AMD module Phase 1. Make the embedded modules stand-alone Phase 2. Move the module loader’s configurations to webpack.config.js Phase 3. Write entry bundle according to the contents of the page Phase 4. Generate Webpack bundles with the task runner Phase 5. Replace original bundles with Webpack bundles
  13. • The mechanism of ScriptBundle and Webpack are quite different

    Index.cshtml Common.bundle.js Why can’t we use embedded modules? Embedded modules Stand-alone Modules 13 • Webpack can’t resolve modules from html/cshtml files import attach Webpack resolve Stand-alone modules resolve
  14. • An embedded module with variables from the controller Implementations:

    an example from ASP.NET MVC 14 ... @using Newtonsoft.Json ... <script type="text/javascript"> define('config', function () { var config = { name: @Model.Name, Types: @Html.Raw(JsonConvert.SerializeObject(Model.Types)), Marks: @Html.Raw(JsonConvert.SerializeObject(Model.Marks)), Data: @Html.Raw(JsonConvert.SerializeObject(Model.Data)) } return config; }); </script> @{ dynamic jsonObj = new Linq.JObject(); jsonObj.name = Model.Name; jsonObj.data = JsonConvert.SerializeObject(Model.Data) == "null" ? null : Linq.JObject.FromObject(Model.Data); jsonObj.marks = JsonConvert.SerializeObject(Model.Marks) == "null" ? null : Linq.JArray.FromObject(Model.Marks); jsonObj.MessageTypes = JsonConvert.SerializeObject(Model.Types) == "null" ? null : Linq.JArray.FromObject(Model.Types); string mapConfig = JsonConvert.SerializeObject(jsonObject).ToString(); } @Html.Hidden("config", @config) define('config', function () { L_PREFER_CANVAS = true; return JSON.parse($("#config").val()); }); Index.cshtml Index.cshtml config.js Put the data into hidden fields Parse the data by using jQuery
  15. Concerns for the transition period 15 Note 1: Add a

    new JS file into the bundleConfig.cs before migrating to Webpack Note 2: It may occur error if you forgot to attach the bundle to other html/cshtml files common.js common.cshtml _layout.cshtml Config.js AController 2. Add 3. Use@Scripts.Render 1.Move embedded modules to stand-alone modules Render. Pass data by model A.cshtml BController B .cshtml bundles.Add(new ScriptWithSourceMapBundle("~/bundles/Home.js").Include( …, "~/Scripts/config.js" )); Render define('config', function () { … }); Use@Scripts.Render require(['config’], function (config) { … }); require
  16. Validation • One choice is to do functional tests with

    Katalon recorder, Selenium, Chrome driver, and NUnit 16
  17. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Phases 2 Webpack.js Shimming entry.bundle.js Embedded module 17 AMD module Phase 1. Make the embedded modules stand-alone Phase 2. Move the module loader’s configurations to webpack.config.js Phase 3. Write entry bundle according to the contents of the page Phase 4. Generate Webpack bundles with the task runner Phase 5. Replace original bundles with Webpack bundles
  18. Map the searching path of modules 18 //requirejs.config requirejs.config({ baseUrl:

    '/Scripts/', paths: { 'knockout': 'knockout-3.0.0.debug', 'koMapper': 'knockout.mapping-latest' } }); • Solution 1: specific the path on the alias field //webpack.config.js const path = require('path'); const config = { resolve: { extensions: ['.js', '.jsx'], alias: { knockout: path.resolve(__dirname, './Scripts/knockout-3.2.0.js'), koMapper: path.resolve(__dirname, './Scripts/knockout.mapping.js’), 'common/svgSheet': path.resolve(__dirname, './Scripts/svgSheet.js’), common: path.resolve(__dirname, './Scripts/common’) } }, }; module.exports = config; • Original module loader (Require.js) Note 2: The order of the module’s name should be prior than folder’s name Module not found: Error: Can’t resolve ‘common/svgSheet’ Note 1: It will lead conflicts if the node module has the same name as the local library’s name 1. Remove that module in the package.json. 2. npm install again. 3. Put AMD modules’ name and path on the alias field const config = { resolve: { extensions: ['.js', '.jsx'], modules: [ path.resolve(__dirname, './Scripts’), 'node_modules' ], alias: { knockout: "knockout-3.2.0.debug ", koMapper: 'knockout.mapping-latest', ... } } }; module.exports = config; • Solution 2: search the module with the alias and modules field
  19. Make modules compatible with Webpack 19 define('someModule', [], function ()

    { ... if (require.defined('otherModule') && require('otherModu le').IsPublic) { successCallback && successCallback([]); return (completeCallback && completeCallback()); } ... define('someModule', ['otherModule'], function (otherModule) { ... if (otherModule.IsPublic) { successCallback && successCallback([]); return (completeCallback && completeCallback()); } ... 1. Webpack provides basic functions for AMD modules 2. We have to replace the require.js APIs with alternative solutions
  20. • Require.js shimming: Import non-AMD JS modules Module shimming 20

    requirejs.config({ shim: { 'jquery': { exports: 'jQuery' } } }); module.exports = { ..., resolve: { alias: { 'jquery’: 'jQuery', ... } }, plugins: [ new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", ... }) ] } • Webpack .config.js shim: Create globals which need to be exported for broken modules
  21. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Phases 3 Webpack.js Shimming entry.bundle.js Embedded module 21 AMD module Phase 1. Make the embedded modules stand-alone Phase 2. Move requiresjs.config to webpack.config Phase 3. Write entry bundle according to the contents of the page Phase 4. Generate Webpack bundles with the task runner Phase 5. Replace original bundles with Webpack bundles
  22. Get JS modules the from the nested layout 22 _layout.cshtml

    _logo.cshtml _nav.cshtml index.cshtml _common.cshtml Bundle.js Bundle.js Bundle.js JS modules JS modules JS modules 1. Turn off the bundling (ScriptBundle) 2. View page source <compilation debug=“true" targetFramework="4.5" /> 3. Attach JS modules into the entry bundle
  23. Ways to import modules into Webpack bundle 1. AMD require

    2. CommonJS synchronous loading 3. CommonJS asynchronous loading 23 require(['map', 'react'], function (map, React) { map.init('inner-map-container'); }); var map = require('map'); var react = require(‘react'); map.init('inner-map-container'); require.ensure([], function (require) { var mapStart = require('mapStart'); var react = require('React'); mapStart.init('inner-map-container'); }); 4. CommonJS Lazy loading 5. ES6 import require.ensure(['mapStart', 'react'], function (require) { var mapStart = require('mapStart'); var react = require(‘react'); mapStart.init('inner-map-container'); }); import mapStart from './mapStart'; import react from './react'; mapStart.init('inner-map-container');
  24. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Phases 4 Webpack.js Shimming entry.bundle.js Embedded module 24 AMD module Phase 1. Make the embedded modules stand-alone Phase 2. Move the module loader’s configurations to webpack.config.js Phase 3. Write entry bundle according to the contents of the page Phase 4. Generate Webpack bundles with the task runner Phase 5. Replace original bundles with Webpack bundles
  25. Append the Webpack task to the task runner 1. Edit

    gulp.js file 2. Run the task 3. Use task runner explorer from Visual studio 25 const webpack = require("webpack-stream"), WEBPACK_CONFIG = require('./webpack.config.js'), src = require('gulp').src, dest = require('gulp').dest, pipeline = require('readable-stream').pipeline; function webpack_bundle(conf) { return pipeline( src('./Scripts/webpack/*.js'), webpack(conf), dest('Scripts/bundle/')); } exports.webpack = () => webpack_bundle(WEBPACK_CONFIG); gulp -b "D:\work\project" --color --gulpfile "D:\work\project\Gulpfile.js" webpack
  26. AMD module AMD module AMD module AMD module layout.cshtml Index.cshtml

    Task runner AMD module moduleA.js moduleB.js moduleC.js Non-AMD module Non-AMD module map.bundle.js Webpack config.js Phases 5 Webpack.js Shimming entry.bundle.js Embedded module 26 AMD module Phase 1. Make the embedded modules stand-alone Phase 2. Move the module loader’s configurations to webpack.config.js Phase 3. Write entry bundle according to the contents of the page Phase 4. Generate Webpack bundles with the task runner Phase 5. Replace original bundles with Webpack bundles
  27. Replace bundles with Webpack bundles for ASP.NET 1. Install Html-Webpack-Plugin

    2. Add Html-Webpack-Plugin in the plugin field in the webpack.config.js 3. Create a template file 4. Render the generated file from the original view 27 npm install Html-Webpack-Plugin --save-dev plugins: [ ..., new HtmlWebpackPlugin({ filename: './Views/_home.bundle.cshtml', template: './Views/BundlesTemp/_JsTemplate.cshtml', chunks: ['home'], inject: false }) ] <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> <script src="<%=htmlWebpackPlugin.files.chunks[chunk].entry %>"></script> <%}%> @Html.Partial("~/Views/Bundle/_home.bundle.cshtml")
  28. 28 Building artifact

  29. Boss: How about the build system? 29 { ... "scripts":

    { "gulp": "gulp", ... }, Package.json <Target Name="BeforeBuild" Condition="'$(BuildingInsideVisualStudio)' == ''"> ... <Exec Command="npm run gulp Release" /> <ItemGroup> <Content Include="Script\bundle\*" /> </ItemGroup> </Target> .csproj <Project ToolsVersion="4.0" xmlns="http://..."> <PropertyGroup> ... <publishUrl>precompiled</publishUrl> ... </PropertyGroup> </Project> <package> <metadata> ... </metadata> <files> <file src="..\..\precompiled\**\*.*" target=""/> ... </files> </package> TeamCity.pubxml .nuspec
  30. 30 Recap

  31. Recap 1. The efforts of migration depends on the previous

    JS modules, previous module system’s configuration, and the previous bundler system 2. Webpack can’t resolve embedded modules. We have to make them stand-alone 3. The validation is important for refactoring layouts 31
  32. 32 Thanks! Any questions? You can find me at: of_alpha@hotmail.com

  33. References • 從無到有建立 webpack 設定檔(一) • Passing serialized C# object

    in JSON to Razor Page • Webpack Code Splitting ‘Loading chunk failed’ error wrong file path • webpack解惑:require的五种用法 • From Require.js to Webpack — Part 1 (the reasons) • xjamundx/blog-webpack-2.md • 使用 Gulp 打包 JavaScript 解決循環相依 (Circular dependencies) 問 • RequireJS进阶:配置文件的学习 • 使用 Html-Webpack-Plugin 引入 Bundle 檔案 33