Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Experience to migrate JS modules to different b...

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 <[email protected]> Software engineer @ Garmin International
  2. 2 Agenda • Self-introduction • Dilemma • What exactly do

    we have to do? • Building artifact • Recap
  3. 4

  4. 5

  5. 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
  6. 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
  7. 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
  8. 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
  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 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
  10. • 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
  11. • 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
  12. 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. [email protected] 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 () { … }); [email protected] require(['config’], function (config) { … }); require
  13. Validation • One choice is to do functional tests with

    Katalon recorder, Selenium, Chrome driver, and NUnit 16
  14. 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
  15. 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
  16. 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
  17. • 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
  18. 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
  19. 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
  20. 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');
  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 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
  22. 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
  23. 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
  24. 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")
  25. 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
  26. 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
  27. 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