یکی دیگر از ماژولهایی که امکان اتصال Node.js را به SQL Server ممکن میکند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم میکند. این مسئله، توسعه دهندگان Node.js را قادر میسازد تا از فناوریهایی که به صورت سنتی استفادهی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
- SQL Server
- Active Directory
- Nuget packages
- استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)
نصب Node.js
اگر Node.js را بر روی سیستم خود نصب ندارید، میتوانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:
node -v
ایجاد پوشه پروژه
سپس پوشهای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:
md \projects\node-edge-test1 cd \projects\node-edge-test1
نصب Edge.js
Node با استفاده از package manager خود دانلود و نصب ماژولها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:
npm install edge npm install edge-sql
Hello World
ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:var edge = require('edge'); // The text in edge.func() is C# code var helloWorld = edge.func('async (input) => { return input.ToString(); }'); helloWorld('Hello World!', function (error, result) { if (error) throw error; console.log(result); });
node server.js
ایجاد پایگاه داده تست
در مثالهای بعدی، نیاز به یک پایگاه داده داریم تا queryها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، میتوانید نسخهی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.
- در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
- بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
- اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
تنظیمات ConnectionString
قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.
set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"
روش اول: اجرای مستقیم SQL Server Query در Edge.js
فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:
var http = require('http'); var edge = require('edge'); var port = process.env.PORT || 8080; var getTopUsers = edge.func('sql', function () {/* SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC */}); function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Error: " + err); res.end(""); } http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' }); getTopUsers(null, function (error, result) { if (error) { logError(error, res); return; } if (result) { res.write("<ul>"); result.forEach(function(user) { res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>"); }); res.end("</ul>"); } else { } }); }).listen(port); console.log("Node server listening on port " + port);
node server-sql-query.js
روش دوم: اجرای کد دات نت برای SQL Server Query
Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی میکند. در حال حاضر از store procedures و مجموعهای از کد SQL پشتیبانی نمیکند. بنابراین، اگر چیزی بیشتر از عملیات CRUD میخواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.یادتان باشد، همیشه async
مدل اجرایی Node.js به صورت یک حلقهی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js میشود.
ایجاد یک Class Library
اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:
using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Threading.Tasks; namespace EdgeSampleLibrary { public class Sample1 { public async Task<object> Invoke(object input) { // Edge marshalls data to .NET using an IDictionary<string, object> var payload = (IDictionary<string, object>) input; var pageNumber = (int) payload["pageNumber"]; var pageSize = (int) payload["pageSize"]; return await QueryUsers(pageNumber, pageSize); } public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize) { // Use the same connection string env variable var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING"); if (connectionString == null) throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable."); // Paging the result set using a common table expression (CTE). // You may rather do this in a stored procedure or use an // ORM that supports async. var sql = @" DECLARE @RowStart int, @RowEnd int; SET @RowStart = (@PageNumber - 1) * @PageSize + 1; SET @RowEnd = @PageNumber * @PageSize; WITH Paging AS ( SELECT ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum, Id, FirstName, LastName, Email, CreateDate FROM SampleUsers ) SELECT Id, FirstName, LastName, Email, CreateDate FROM Paging WHERE RowNum BETWEEN @RowStart AND @RowEnd ORDER BY RowNum; "; var users = new List<SampleUser>(); using (var cnx = new SqlConnection(connectionString)) { using (var cmd = new SqlCommand(sql, cnx)) { await cnx.OpenAsync(); cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber }); cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize }); using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { var user = new SampleUser { Id = reader.GetInt32(0), FirstName = reader.GetString(1), LastName = reader.GetString(2), Email = reader.GetString(3), CreateDate = reader.GetDateTime(4) }; users.Add(user); } } } } return users; } } public class SampleUser { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateTime CreateDate { get; set; } } }
[project]/bin/Debug/EdgeSampleLibrary.dll
var http = require('http'); var edge = require('edge'); var port = process.env.PORT || 8080; // Set up the assembly to call from Node.js var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' }); function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); } http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' }); // This is the data we will pass to .NET var data = { pageNumber: 1, pageSize: 3 }; // Invoke the .NET function querySample(data, function (error, result) { if (error) { logError(error, res); return; } if (result) { res.write("<ul>"); result.forEach(function(user) { res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>"); }); res.end("</ul>"); } else { res.end("No results"); } }); }).listen(port); console.log("Node server listening on port " + port);
node server-dotnet-query.js
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، میتوانید از ماژول connect استفاده کنید.
افزودن متغیرهای محیطی
در برنامهی نمایش لیست فیلمهایی که تا قسمت 29 آنرا بررسی کردیم، از فایل src\config.json برای ذخیره سازی اطلاعات تنظیمات برنامه استفاده شد. هرچند این روش کار میکند اما بر اساس محیطهای مختلف توسعه، متغیر نیست. اغلب برنامهها باید بتوانند حداقل در سه محیط توسعه، آزمایش و تولید، بر اساس متغیرها و تنظیمات خاص هر کدام، کار کنند. برای مثال بر روی سیستمی که کار توسعه در آن انجام میشود، میخواهیم apiUrl متفاوتی را نسبت به حالتیکه برنامه توزیع میشود، داشته باشیم.
برای رفع این مشکل، برنامههایی که توسط create-react-app تولید میشوند، دارای پشتیبانی توکاری از متغیرهای محیطی هستند. برای این منظور نیاز است در ریشهی پروژه (جائیکه فایل package.json قرار دارد) فایل جدید env. را ایجاد کرد. در ویندوز برای ایجاد یک چنین فایلهایی که فقط از یک پسوند تشکیل میشوند، باید نام فایل را به صورت .env. وارد کرد؛ سپس خود ویندوز نقطهی نهایی را حذف میکند. البته اگر از ادیتور VSCode برای ایجاد این فایل استفاده میکنید، نیازی به درج نقطهی انتهایی نیست. در این فایل environment ایجاد شده میتوان تمام متغیرهای محیطی مورد نیاز را با مقادیر پیشفرض آنها درج کرد. همچنین میتوان این مقادیر پیشفرض را بر اساس محیطهای مختلف کاری، بازنویسی کرد. برای مثال میتوان فایل env.development. را اضافه کرد؛ به همراه فایلهای env.test. و env.production.
متغیرهای محیطی به صورت key=value درج میشوند. این کلیدها نیر باید با REACT_APP_ شروع شوند؛ در غیر اینصورت، کار نخواهند کرد. برای مثال در فایل env.، دو متغیر پیشفرض زیر را تعریف میکنیم:
REACT_APP_NAME=My App REACT_APP_VERSION=1
console.log(process.env);
در این خروجی، متغیر "NODE_ENV: "development به صورت خودکار با تولید بستههای مخصوص ارائهی نهایی، به production تنظیم میشود. سایر متغیرهای محیطی تعریف شده را نیز در اینجا ملاحظه میکنید. با توجه به خواص شیء env، برای مثال جهت دسترسی به نام برنامه میتوان از مقدار process.env.REACT_APP_NAME استفاده کرد.
یک نکته: با هر تغییری در مقادیر متغیرهای محیطی، نیاز است یکبار دیگر برنامه را از ابتدا توسط دستور npm start، راه اندازی مجدد کرد؛ چون این فایلها به صورت خودکار ردیابی نمیشوند.
نحوهی پردازش متغیرهای محیطی درج شدهی در برنامه
اگر همان سطر لاگ کردن خروجی process.env را به صورت زیر تغییر دهیم:
console.log("My App Name", process.env.REACT_APP_NAME);
همانطور که مشاهده میکنید، فراخوانی console.log ما، دیگر به همراه متغیر process.env.REACT_APP_NAME نیست؛ بلکه مقدار اصلی این متغیر در اینجا درج شدهاست. بنابراین اگر در در حین توسعهی برنامه، از متغیرهای محیطی استفاده شود، این متغیرها با مقادیر اصلی آنها در حین پروسهی Build نهایی، جایگزین میشوند.
Build برنامههای React برای محیط تولید
اجرای دستور npm start، سبب ایجاد یک Build مخصوص محیط توسعه میشود که بهینه سازی نشدهاست و به همراه اطلاعات اضافی قابل توجهی جهت دیباگ سادهتر برنامهاست. برای رسیدن به یک خروجی بهینه سازی شدهی مخصوص محیط تولید و ارائهی نهایی باید دستور npm run build را در خط فرمان اجرا کرد. خروجی نهایی این دستور، در پوشهی جدید build واقع در ریشهی پروژه، قرار میگیرد. اکنون میتوان کل محتویات این پوشه را جهت ارائهی نهایی در وب سرور خود، مورد استفاده قرار داد.
پس از پایان اجرای دستور npm run build، پیام «امکان ارائهی آن توسط static server زیر نیز وجود دارد» ظاهر میشود:
> npm install -g serve > serve -s build
البته با توجه به اینکه backend سرور برنامههای ما نیز در همین آدرس قرار دارد و در صورت ورود این آدرس، به صورت خودکار به https://localhost:5001/index.html هدایت خواهید شد، میتوان این پورت پیشفرض را با اجرای دستور serve -s build -l 1234 تغییر داد. اکنون میتوان آدرس جدید http://localhost:1234 را در مرورگر آزمایش کرد که ... با خطای زیر کار نمیکند:
Access to XMLHttpRequest at 'https://localhost:5001/api/genres' from origin 'http://localhost:1234' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
WithOrigins("http://localhost:3000", "http://localhost:1234")
یک نکته: زمانیکه از دستور npm start استفاده میشود، متغیرهای محیطی از فایل env.development. خوانده خواهند شد و زمانیکه از دستور npm run build استفاده میشود، این متغیرها از فایل env.production. تامین میشوند. در این حالتها اگر متغیری در این دو فایل درج نشده بود، از مقدار پیشفرض موجود در فایل env. استفاده میگردد. از فایل env.test. با اجرای دستور npm test، به صورت خودکار استفاده میشود.
آماده سازی برنامهی React، برای توزیع نهایی
تا اینجا برنامهی React تهیه شده، اطلاعات apiUrl خودش را از فایل config.json دریافت میکند. اکنون میخواهیم بر اساس حالات مختلف توسعه و تولید، از apiUrlهای متفاوتی استفاده شود. به همین جهت به فایل env.production. مراجعه کرده و تنظیمات ذیل را به آن اضافه میکنیم:
REACT_APP_API_URL=https://localhost:5001/api REACT_APP_ADMIN_ROLE_NAME=Admin
اکنون به برنامه مراجعه کرده و در هرجائی که ارجاعی به فایل config.json وجود دارد، سطر import آنرا حذف میکنیم. با این تغییر، تمام آدرسهایی مانند:
const apiEndpoint = apiUrl + "/users";
const apiEndpoint = "/users";
axios.defaults.baseURL = process.env.REACT_APP_API_URL;
همچنین adminRoleName مورد نیاز در فایل src\services\authService.js را نیز از همان فایل env جاری تامین میکنیم:
const adminRoleName = process.env.REACT_APP_ADMIN_ROLE_NAME;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-34-frontend.zip و sample-34-backend.zip
public IActionResult Get() { var userData = new User { Name = "Farhad", Family = "Zamani" }; _logger.LogInformation($"ValuesController called. Name:{userData.Name}, Family:{userData.Family}"); return Ok(); }
اگر بخواهیم مرتب سازی و یا فیلتری را بر روی Name و یا Family انجام دهیم، کار آسانی نخواهد بود و وقتگیر خواهد بود؛ زیرا باید با یک فیلد رشته ( message )، که تمامی لاگ درون آن قرار دارد، کار کنیم. اما با استفاده از قابلیت Structured Logging مربوط به Serilog، میتوانیم آبجکت userData را به ILogger پاس دهیم و پراپرتیهای آن را به صورت فیلدهای جدا نمایش دهیم.
برای این کار باید آبجکت موردنظر خود را درون {} قرار دهیم و قبل از نام متغییر آن یک @ قرار دهیم. بدین صورت: {userData@} و سپس دیتای موردنظر را در پارامتر دوم logger قرار دهیم.
_logger.LogInformation("ValuesController called. {@userData}", userData);
اگر کد نوشته شده مربوط به ثبت لاگ را به صورت زیر اصلاح کنیم:
public IActionResult Get() { var userData = new User { Name = "Farhad", Family = "Zamani" }; _logger.LogInformation("ValuesController called. {@userData}", userData); return Ok(); }
در پنل کیبانا به راحتی میتوان عملیات مرتب سازی و یا فیلتر را بر روی پراپرتیهای Name و Family انجام دهیم. اکنون اگر پنل کیبانا را تماشا کنید چنین لاگی را مشاهده میکنیم:
هرکدام از پراپرتیهای userData به صورت یک فیلد جدا ارسال شدهاست که به راحتی میتوانید مرتب سازی، فیلتر و... را بر روی هرکدام از فیلدها انجام دهید.
تنظیمات مربوط به Serilog:
public static void Main(string[] args) { var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .Enrich.WithMachineName() .Filter.ByExcluding(Matching.FromSource("Serilog")) .Filter.ByExcluding(Matching.FromSource("System.Net.Http")) .Filter.ByExcluding(Matching.FromSource("Microsoft.AspNetCore")) .WriteTo.Console() .ReadFrom.Configuration(configuration) .CreateLogger(); CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
فایل appsettings.json:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Serilog": { "WriteTo": [ { "Name": "Elasticsearch", "Args": { "nodeUris": "http://127.0.0.1:9200;", "indexFormat": "structuredlogging-{0:yyyy.MM}", "templateName": "structuredlogging" } } ] } }
فایل docker-compose برای اجرای Elasticsearch و Kibana:
version: '3' services: elasticsearch: container_name: elasticsearch image: elasticsearch:7.14.2 environment: - discovery.type=single-node ports: - 9200:9200 - 9300:9300 - 8200:8200 kibana: container_name: kibana image: kibana:7.14.2 ports: - 5601:5601
منابع استفاده شده:
import svelte from 'rollup-plugin-svelte'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import livereload from 'rollup-plugin-livereload'; import { terser } from 'rollup-plugin-terser'; const production = !process.env.ROLLUP_WATCH; export default { input: 'src/main.js', output: { sourcemap: true, format: 'iife', name: 'app', file: 'public/bundle.js' }, plugins: [ svelte({ // enable run-time checks when not in production dev: !production, // we'll extract any component CSS out into // a separate file — better for performance css: css => { css.write('public/bundle.css'); } }), // If you have external dependencies installed from // npm, you'll most likely need these plugins. In // some cases you'll need additional configuration — // consult the documentation for details: // https://github.com/rollup/rollup-plugin-commonjs resolve(), commonjs(), // Watch the `public` directory and refresh the // browser on changes when not in production !production && livereload('public'), // If we're building for production (npm run build // instead of npm run dev), minify production && terser() ], watch: { clearScreen: false } };
package.json :
{ "name": "svelte-app", "version": "1.0.0", "devDependencies": { "npm-run-all": "^4.1.5", "rollup": "^1.10.1", "rollup-plugin-commonjs": "^9.3.4", "rollup-plugin-livereload": "^1.0.0", "rollup-plugin-node-resolve": "^4.2.3", "rollup-plugin-svelte": "^5.0.3", "rollup-plugin-terser": "^4.0.4", "sirv-cli": "^0.4.0", "svelte": "^3.0.0" }, "scripts": { "build": "rollup -c", "autobuild": "rollup -c -w", "dev": "run-p start:dev autobuild", "start": "sirv public", "start:dev": "sirv public --dev" } }
build | برای ساخت و ایجاد خروجیهای برنامه توسط rollup مورد قرار استفاده میگیرد. |
autobuild | مانند build برای ساخت خروجیهای نهایی برنامه استفاده میشود. ولی تفاوتی که دارد پس از هر تغییر در سورس کد برنامه به صورت خودکار build جدیدی پس از اجرای آن گرفته میشود. |
dev | برنامه را درحالت Developer Mode اجرا میکند که برای مشاهده تغییرات به صورت خودکار در browser، بدون نیاز به رفرش صفحه و همینطور عیب یابی برنامه مناسب است. |
start | از طریق sirv که یک وب سرور سبک برای هاست کردن سایتهای استاتیک است، برنامه را هاست میکند. |
start:dev | مانند start است با این تفاوت که برنامه را در حالت Developer Mode هاست میکند که میتواند برای عیب یابی برنامه از آن استفاده کرد؛ چرا که سورس برنامه از طریق source Map قابل دسترس خواهد بود. |
دو پوشه src و public هم برای ما به صورت پیش فرض ایجاد شدهاند که فولدر public فایلهای نهایی تولید شده برنامه ما را شامل میشود و src، دربرگیرنده تمام سورس کدهای برنامه ما میباشد.
<script> export let name; </script> <style> h1 { color: purple; } </style> <h1>Hello {name}!</h1>
src/main.js :
import App from './App.svelte'; const app = new App({ target: document.body, props: { name: 'world' } }); export default app;
کامپایل sass در دات نت
LibSass is a C/C++ port of the Sass CSS precompiler.
ساخت (Build) برنامههای Angular
Angular CLI کار ساخت و کامپایل برنامه را به صورت خودکار انجام داده و خروجی را در مسیری مشخص درج میکند. در اینجا میتوان گزینههایی را بر اساس نوع کامپایل مدنظر مانند کامپایل برای حالت توسعه و یا کامپایل برای حالت توزیع نهایی، انتخاب کرد. همچنین مباحث bundling و یکی کردن تعداد بالای ماژولهای برنامه در آن لحاظ میشوند تا برنامه در حالت توزیع نهایی، سبب 100ها رفت و برگشت به سرور برای دریافت ماژولهای مختلف آن نشود. به علاوه مباحث uglification (به نوعی obfuscation کدهای جاوا اسکریپتی نهایی) و tree-shaking (حذف کدهایی که در برنامه استفاده نشدهاند؛ یا کدهای مرده) نیز پیاده سازی میشوند. با انجام tree-shaking، نه تنها اندازهی توزیع نهایی به کاربر کاهش پیدا میکند، بلکه مرورگر نیز حجم کمتری از کدهای جاوااسکریپتی را باید تفسیر کند.
برای شروع میتوان از دستور ذیل برای مشاهدهی تمام گزینههای مهیای ساخت برنامه استفاده کرد:
> ng build --help
"apps": [ { "outDir": "dist",
فایل | توضیح |
inline.bundle.js | WebPack runtime از آن برای بارگذاری ماژولهای برنامه و چسباندن قسمتهای مختلف به یکدیگر استفاده میشود. |
main.bundle.js | شامل تمام کدهای ما است. |
polyfills.bundle.js | Polyfills - جهت پشتیبانی از مرورگرهای مختلف. |
styles.bundle.js | شامل بسته بندی تمام شیوه نامههای برنامه است |
vendor.bundle.js | کدهای کتابخانههای ثالث مورد استفاده و همچنین خود Angular، در اینجا بسته بندی میشوند. |
روشی برای بررسی محتوای bundleهای تولید شده
تولید bundleها در جهت کاهش رفت و برگشتهای به سرور و بالا بردن کارآیی برنامه ضروری هستند؛ اما دقیقا این بسته بندیها شامل چه اطلاعاتی میشوند؟ این اطلاعات را میتوان از فایلهای source map تولیدی استخراج کرد و برای این منظور میتوان از برنامهی source-map-explorer استفاده کرد.
روش نصب عمومی آن:
> npm install -g source-map-explorer
> source-map-explorer dist/main.bundle.js
یک مثال: ساخت برنامهی مثال قسمت چهارم - تنظیمات مسیریابی در حالت dev
در ادامه، کار Build همان مثالی را که در قسمت قبل توضیح داده شد، بررسی میکنیم. برای این منظور از طریق خط فرمان به ریشهی پوشهی اصلی پروژه وارد شده و دستور ng build را صادر کنید. یک چنین خروجی را مشاهده خواهید کرد:
D:\Prog\angular-routing>ng build Hash: 123cae8bd8e571f44c31 Time: 33862ms chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered] chunk {1} main.bundle.js, main.bundle.js.map (main) 14.7 kB {3} [initial] [rendered] chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered] chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.34 MB [initial] [rendered] chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
<!doctype html> <html> <head> <meta charset="utf-8"> <title>AngularRouting</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root>Loading...</app-root> <script type="text/javascript" src="inline.bundle.js"> </script><script type="text/javascript" src="polyfills.bundle.js"> </script><script type="text/javascript" src="styles.bundle.js"> </script><script type="text/javascript" src="vendor.bundle.js"> </script><script type="text/javascript" src="main.bundle.js"></script> </body> </html>
یک نکته: زمانیکه دستور ng serve -o صادر میشود، در پشت صحنه دقیقا همین دستور ng build صادر شده و اطلاعات را درون حافظه تشکیل میدهد. اما اگر کار ng build را دستی انجام دهیم، اینبار ng serve -o اطلاعات را از پوشهی dist دریافت میکند. بنابراین در حین کار با ng serve -o نیازی به build دستی پروژه نیست.
سؤال: چرا حجم فایل endor.bundle.js اینقدر بالا است و شامل چه اجزایی میشود؟
نکتهای که در اینجا وجود دارد، حجم بالای فایل vendor.bundle.js آن است که 2.34 MB میباشد:
چون دستور ng build بدون پارامتری ذکر شدهاست، برنامه را برای حالت توسعه Build میکند و به همین جهت هیچگونه بهینه سازی در این مرحله صورت نخواهد گرفت. برای بررسی محتوای این فایل میتوان دستور ذیل را در ریشهی اصلی پروژه صادر کرد:
> source-map-explorer dist/vendor.bundle.js
همانطور که مشاهده میکنید، در حالت بهینه سازی نشده و Build برای توسعه، کامپایلر Angular حدود 41 درصد حجم فایل vendor.bundle.js را تشکیل میدهد. به علاوه ماژولها و قسمتهایی را ملاحظه میکنید که اساسا برنامهی فعلی مثال ما از آنها استفاده نمیکند؛ مانند http، فرمها و غیره.
سفارشی سازی Build برای محیطهای مختلف
اگر به پروژهی تولید شدهی توسط Angular CLI دقت کنید، حاوی پوشهای است به نام src\environments
هدف از فایلهای environment برای نمونه تغییر آدرس توزیع برنامه در حالت توسعه و ارائه نهایی است.
همچنین در اینجا میتوان نحوهی بهینه سازی فایلهای تولیدی را توسط Build Targets مشخص کرد و اینکار توسط ذکر پرچم prod-- (مخفف production) صورت میگیرد.
در ادامه، تفاوتهای دستورهای ng build و ng build --prod را ملاحظه میکنید:
- با اجرای ng build، از فایل environment.ts استفاده میشود؛ برخلاف حالت اجرای ng build --prod که از فایل environment.prod.ts استفاده میکند.
- Cache-busting در حالت ارائهی نهایی، به تمام اجزای پروژه اعمال میشود؛ اما در حالت توسعه فقط برای تصاویر قید شدهی در فایلهای css.
- فایلهای source map فقط برای حالت توسعه تولید میشوند.
- در حالت توسعه، cssها داخل فایلهای js تولیدی قرار میگیرند؛ اما در حالت ارائهی نهایی به صورت فایلهای css بسته بندی میشوند.
- در حالت توسعه برخلاف حالت ارائهی نهایی، کار uglification انجام نمیشود.
- در حالت توسعه برخلاف حالت ارائهی نهایی، کار tree-shaking یا حذف کدهای مرده و بدون ارجاع، انجام نمیشود.
- در حالت توسعه برخلاف حالت ارائهی نهایی، کار AOT انجام نمیشود. در اینجا AOT به معنای Ahead of time compilation است.
- در هر دو حالت توسعه و ارائهی نهایی کار bundling و دسته بندی فایلها انجام خواهد شد.
به همین جهت است که ng build سریع است؛ اما حجم بالاتری را هم تولید میکند. چون بسیاری از بهینه سازیهای حالت ارائهی نهایی را به همراه ندارد.
دستورات build برای حالت توسعه و ارائهی نهایی
برای حالت توسعه، هر 4 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build متداولتر است:
>ng build --target=development --environment=dev >ng build --dev -e=dev >ng build --dev >ng build
برای حالت ارائهی نهایی، هر 3 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build --prod متداولتر است:
>ng build --target=production --environment=prod >ng build --prod -e=prod >ng build --prod
همچنین هر کدام از این دستورات را توسط پرچمهای ذیل نیز میتوان سفارشی سازی کرد:
پرچم | مخفف | توضیح |
sourcemap-- | sm- | تولید سورسمپ |
aot-- | Ahead of Time compilation | |
watch-- | w- | تحت نظر قرار دادن فایلها و ساخت مجدد |
environment-- | e- | محیط ساخت |
target-- | t- | نوع ساخت |
dev-- | مخفف نوع ساخت جهت توسعه | |
prod-- | مخفف نوع ساخت جهت ارائه نهایی |
برای مثال در حالت prod، سورسمپها تولید نخواهند شد. اگر علاقمندید تا این فایلها نیز تولید شوند، پرچم souremap را نیز ذکر کنید.
و یا اگر برای حالت dev میخواهید AOT را فعالسازی کنید، پرچم aot-- را در آنجا قید کنید.
یک مثال: ساخت برنامهی مثال قسمت چهارم - تنظیمات مسیریابی در حالت prod
تا اینجا خروجی حالت dev ساخت برنامهی قسمت چهارم را بررسی کردیم. در ادامه دستور ng build --prod را در ریشهی پروژه صادر میکنیم:
D:\Prog\angular-routing>ng build --prod Hash: f5bd7fd555a85af8a86f Time: 39932ms chunk {0} polyfills.18173234f9641113b9fe.bundle.js (polyfills) 158 kB {4} [initial] [rendered] chunk {1} main.c6958def7c5f51c45261.bundle.js (main) 50.3 kB {3} [initial] [rendered] chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered] chunk {3} vendor.b426ba6883193375121e.bundle.js (vendor) 1.37 MB [initial] [rendered] chunk {4} inline.8cec210370dd3af5f1a0.bundle.js (inline) 0 bytes [entry] [rendered]
همانطور که ملاحظه میکنید، اینبار نه تنها حجم فایلها به میزان قابل ملاحظهای کاهش پیدا کردهاند، بلکه این نامها به همراه یک سری hash هم هستند که کار cache-busting (منقضی کردن کش مرورگر، با ارائهی نگارشی جدید) را انجام میدهند.
در ادامه اگر بخواهیم مجددا برنامهی source-map-explorer را جهت بررسی محتوای فایلهای js اجرا کنیم، به خطای عدم وجود sourcemapها خواهیم رسید (چون در حالت prod، به صورت پیش فرض غیرفعال هستند). به همینجهت برای این مقصود خاص نیاز است از پرچم فعالسازی موقت آن استفاده کرد:
> ng build --prod --sourcemap > source-map-explorer dist/vendor.b426ba6883193375121e.bundle.js
همانطور که در تصویر نیز مشخص است، اینبار کامپایلر Angular به همراه تمام ماژولهایی که در برنامه ارجاعی به آنها وجود نداشتهاست، حذف شدهاند و کل حجم بستهی Angular به 366 KB کاهش یافتهاست.
بررسی دستور ng serve
تا اینجا برای اجرای برنامه در حالت dev از دستور ng serve -o استفاده کردهایم. کار ارائهی برنامه توسط این دستور، از محتوای کامپایل شدهی درون حافظه با مدیریت webpack انجام میشود. به همین جهت بسیار سریع بوده و قابلیت live reload را ارائه میدهد (نمایش آنی تغییرات در مرورگر، با تغییر فایلها).
همانند تمام دستورات دیگر، اطلاعات بیشتری را در مورد این دستور، از طریق راهنمای آن میتوان به دست آورد:
> ng serve --help
که شامل این موارد هستند (علاوه بر تمام مواردی را که در حالت ng build میتوان مشخص کرد؛ مثلا ng serve --prod -o):
پرچم | مخفف | توضیح |
open-- | o- | بازکردن خودکار مرورگر پیش فرض. حالت پیش فرض آن گشودن مرورگر توسط خودتان است و سپس مراجعهی دستی به آدرس برنامه. |
port-- | p- | تغییر پورت پیش فرض مانند ng server -p 8626 |
live-reload-- | lr- |
فعال است مگر اینکه آنرا با false مقدار دهی کنید. |
ssl-- | ارائه به صورت HTTPS | |
proxy-config-- | pc- | Proxy configuration file |
استخراج فایل تنظیمات webpack از Angular CLI
Angular CLI برای مدیریت build، در پشت صحنه از webpack استفاده میکند. فایل تنظیمات آن نیز جزئی از فایلهای توکار این ابزار است و قرار نیست به صورت پیش فرض و مستقیم توسط پروژهی جاری ویرایش شود. به همین جهت آنرا در ساختار پروژهی تولید شده، مشاهده نمیکنید.
اگر علاقمند به سفارشی سازی بیشتر این تنظیمات پیش فرض باشید، ابتدا باید آنرا اصطلاحا eject کنید و سپس میتوان آنرا ویرایش کرد:
> ng eject Ejection was successful. To run your builds, you now need to do the following commands: - "npm run build" to build. - "npm run test" to run unit tests. - "npm start" to serve the app using webpack-dev-server. - "npm run e2e" to run protractor. Running the equivalent CLI commands will result in an error. ============================================ Some packages were added. Please run "npm install".
در این حالت است که فایل webpack.config.js به ریشهی پروژه جهت سفارشی سازی شما اضافه خواهد شد. همچنین فایلهای .angular-cli.json، package.json نیز جهت درج این تغییرات ویرایش میشوند.
و اگر در این لحظه پشیمان شدهاید (!) فقط کافی است تا این مرحلهی جدید commit شدهی به مخزن کد را لغو کنید و باز هم به همان Angular CLI قبلی میرسید.